From b0d985a72100f1112c3d100a1afdf666d2e9c9e7 Mon Sep 17 00:00:00 2001 From: Nahiyan Khan Date: Sun, 10 May 2026 11:08:19 -0400 Subject: [PATCH 1/2] Make root Ghost bundle canonical --- .changeset/add-survey-schema-and-merge.md | 4 +- .changeset/add-survey-summary.md | 2 +- .changeset/add-verify-profile-gate.md | 2 +- .changeset/authored-fingerprint-contract.md | 2 +- .changeset/canonical-decision-vocabulary.md | 2 +- .changeset/fix-profile-recipe-paths.md | 2 +- .changeset/implemented-surface-evidence.md | 2 +- .changeset/initial-ghost-fingerprint.md | 2 +- .changeset/phase-4c-recipe-branching.md | 2 +- .changeset/phase-5-bugs-and-schema.md | 2 +- .changeset/refactor-fingerprint-package.md | 2 +- .changeset/remove-profile-public-concept.md | 6 + .changeset/root-fingerprint-bundle.md | 6 + .changeset/tighten-fingerprint-evidence.md | 5 + CLAUDE.md | 26 +- README.md | 95 ++-- apps/docs/src/app/page.tsx | 16 +- apps/docs/src/app/tools/drift/page.tsx | 4 +- apps/docs/src/app/tools/fingerprint/page.tsx | 2 +- apps/docs/src/content/docs/cli-reference.mdx | 136 +++--- .../docs/src/content/docs/getting-started.mdx | 264 ++--------- apps/docs/src/generated/cli-manifest.json | 52 +- docs/fingerprint-format.md | 221 ++++----- docs/generation-loop.md | 212 ++++----- install/install.sh | 2 +- install/manifest.json | 2 +- map.md | 2 +- packages/ghost-core/src/checks/schema.ts | 1 + packages/ghost-core/src/checks/types.ts | 1 + .../ghost-core/src/decision-vocabulary.ts | 6 +- .../ghost-core/src/embedding/embed-api.ts | 2 +- .../ghost-core/src/fingerprint-package.ts | 13 +- packages/ghost-core/src/index.ts | 45 +- packages/ghost-core/src/patterns/index.ts | 22 + packages/ghost-core/src/patterns/lint.ts | 140 ++++++ packages/ghost-core/src/patterns/schema.ts | 74 +++ packages/ghost-core/src/patterns/types.ts | 67 +++ packages/ghost-core/src/resources/index.ts | 18 + packages/ghost-core/src/resources/lint.ts | 90 ++++ packages/ghost-core/src/resources/schema.ts | 48 ++ packages/ghost-core/src/resources/types.ts | 50 ++ packages/ghost-core/src/survey/index.ts | 2 + packages/ghost-core/src/survey/lint.ts | 2 +- packages/ghost-core/src/survey/schema.ts | 13 + packages/ghost-core/src/survey/types.ts | 18 +- packages/ghost-core/src/types.ts | 10 +- .../test/resources-patterns.test.ts | 80 ++++ packages/ghost-core/test/survey-lint.test.ts | 24 + packages/ghost-drift/README.md | 17 +- packages/ghost-drift/package.json | 2 +- packages/ghost-drift/src/cli.ts | 51 +- .../ghost-drift/src/comparable-fingerprint.ts | 124 +++++ packages/ghost-drift/src/core/check.ts | 21 +- .../ghost-drift/src/core/evolution/emit.ts | 4 +- .../src/core/evolution/tracking.ts | 10 +- .../ghost-drift/src/core/scope-resolver.ts | 6 +- .../ghost-drift/src/evolution-commands.ts | 2 +- .../ghost-drift/src/skill-bundle/SKILL.md | 40 +- .../src/skill-bundle/references/compare.md | 12 +- .../src/skill-bundle/references/remediate.md | 24 +- .../src/skill-bundle/references/review.md | 16 +- .../src/skill-bundle/references/verify.md | 13 +- packages/ghost-drift/test/cli.test.ts | 143 ++++-- .../ghost-drift/test/scope-resolver.test.ts | 6 +- packages/ghost-fingerprint/README.md | 67 ++- packages/ghost-fingerprint/package.json | 2 +- packages/ghost-fingerprint/src/cli.ts | 447 +++++++++++++----- .../ghost-fingerprint/src/core/constants.ts | 19 +- .../src/core/context/review-command.ts | 8 +- .../src/core/context/tokens-css.ts | 4 +- .../src/core/context/writer.ts | 24 +- .../src/core/fingerprint-package.ts | 147 ++++-- packages/ghost-fingerprint/src/core/index.ts | 24 +- .../ghost-fingerprint/src/core/lint-map.ts | 2 +- packages/ghost-fingerprint/src/core/lint.ts | 55 ++- .../ghost-fingerprint/src/core/scan-status.ts | 81 ++-- ...erify-profile.ts => verify-fingerprint.ts} | 148 ++++-- .../src/core/verify-package.ts | 298 ++++++++++++ .../ghost-fingerprint/src/emit-command.ts | 20 +- .../src/skill-bundle/SKILL.md | 69 ++- .../src/skill-bundle/references/map.md | 6 +- .../src/skill-bundle/references/patterns.md | 46 ++ .../src/skill-bundle/references/profile.md | 92 ---- .../src/skill-bundle/references/scan.md | 83 ++-- .../src/skill-bundle/references/schema.md | 96 ++-- .../src/skill-bundle/references/survey.md | 24 +- packages/ghost-fingerprint/test/cli.test.ts | 132 ++++-- .../__snapshots__/review-command.test.ts.snap | 4 +- .../test/context/writer.test.ts | 10 +- .../test/fingerprint-package.test.ts | 66 ++- .../test/fingerprint/lint.test.ts | 40 +- ...ile.test.ts => verify-fingerprint.test.ts} | 71 ++- .../goose2/fingerprint.md | 2 +- .../goose2/survey.json | 0 .../fingerprint.md | 4 +- .../survey.json | 10 +- .../test/scan-status.test.ts | 40 +- packages/ghost-fleet/src/core/members.ts | 8 +- .../ghost-fleet/src/skill-bundle/SKILL.md | 6 +- .../{profile.md => fingerprint.md} | 0 .../cash-web/{profile.md => fingerprint.md} | 0 .../members/cash-web/fingerprints/accounts.md | 4 +- .../members/cash-web/fingerprints/payments.md | 2 +- .../ghost-ui/{profile.md => fingerprint.md} | 0 scripts/check-file-sizes.mjs | 2 +- 105 files changed, 3018 insertions(+), 1437 deletions(-) create mode 100644 .changeset/remove-profile-public-concept.md create mode 100644 .changeset/root-fingerprint-bundle.md create mode 100644 .changeset/tighten-fingerprint-evidence.md create mode 100644 packages/ghost-core/src/patterns/index.ts create mode 100644 packages/ghost-core/src/patterns/lint.ts create mode 100644 packages/ghost-core/src/patterns/schema.ts create mode 100644 packages/ghost-core/src/patterns/types.ts create mode 100644 packages/ghost-core/src/resources/index.ts create mode 100644 packages/ghost-core/src/resources/lint.ts create mode 100644 packages/ghost-core/src/resources/schema.ts create mode 100644 packages/ghost-core/src/resources/types.ts create mode 100644 packages/ghost-core/test/resources-patterns.test.ts create mode 100644 packages/ghost-drift/src/comparable-fingerprint.ts rename packages/ghost-fingerprint/src/core/{verify-profile.ts => verify-fingerprint.ts} (84%) create mode 100644 packages/ghost-fingerprint/src/core/verify-package.ts create mode 100644 packages/ghost-fingerprint/src/skill-bundle/references/patterns.md delete mode 100644 packages/ghost-fingerprint/src/skill-bundle/references/profile.md rename packages/ghost-fingerprint/test/fingerprint/{verify-profile.test.ts => verify-fingerprint.test.ts} (81%) rename packages/ghost-fingerprint/test/fixtures/{profile-verifier => fingerprint-verifier}/goose2/fingerprint.md (89%) rename packages/ghost-fingerprint/test/fixtures/{profile-verifier => fingerprint-verifier}/goose2/survey.json (100%) rename packages/ghost-fingerprint/test/fixtures/{surface-profile => surface-fingerprint}/fingerprint.md (94%) rename packages/ghost-fingerprint/test/fixtures/{surface-profile => surface-fingerprint}/survey.json (90%) rename packages/ghost-fleet/test/fixtures/small-fleet/members/cash-android/{profile.md => fingerprint.md} (100%) rename packages/ghost-fleet/test/fixtures/small-fleet/members/cash-web/{profile.md => fingerprint.md} (100%) rename packages/ghost-fleet/test/fixtures/small-fleet/members/ghost-ui/{profile.md => fingerprint.md} (100%) diff --git a/.changeset/add-survey-schema-and-merge.md b/.changeset/add-survey-schema-and-merge.md index 960c71b1..8a793dc0 100644 --- a/.changeset/add-survey-schema-and-merge.md +++ b/.changeset/add-survey-schema-and-merge.md @@ -17,10 +17,10 @@ Land the three-stage scan pipeline: map (`map.md`) → survey (`survey.json`) **New skill recipes:** - `map.md` — author `map.md` from a target (the topology stage). Migrated from the standalone `ghost-map` package. - `survey.md` — author `survey.json` from a target (the observed evidence stage). Walks the agent through LLM-driven extraction with dialect-specific grep strategies, exhaustiveness discipline, and saturation predicate. -- `scan.md` — meta-recipe that orchestrates map → survey → profile end-to-end via `scan-status` checkpoints. Use when the user wants a full scan rather than a specific stage. +- `scan.md` — meta-recipe that orchestrates map → survey → fingerprint end-to-end via `scan-status` checkpoints. Use when the user wants a full scan rather than a specific stage. **Refactored skill recipe:** -- `profile.md` — now strictly the fingerprint stage. Reads `survey.json` as ground truth; cannot fabricate values not in the survey; cites survey rows as evidence. Pre-requires `map.md` + `survey.json`. Hard split from the previous one-pass extract+interpret recipe. +- `fingerprint.md` — now strictly the fingerprint stage. Reads `survey.json` as ground truth; cannot fabricate values not in the survey; cites survey rows as evidence. Pre-requires `map.md` + `survey.json`. Hard split from the previous one-pass extract+interpret recipe. **Removed:** the `ghost-map` package is deleted. `ghost.map/v1` schema and types now live in `@ghost/core`; `inventory` and `lint` (for `map.md`) move to `ghost-fingerprint`. Consumers that imported from `ghost-map` should switch to `@ghost/core` (schemas/types) or `ghost-fingerprint` (CLI verbs / library functions). diff --git a/.changeset/add-survey-summary.md b/.changeset/add-survey-summary.md index d43617bf..29cafc06 100644 --- a/.changeset/add-survey-summary.md +++ b/.changeset/add-survey-summary.md @@ -2,4 +2,4 @@ "ghost-fingerprint": minor --- -Add a bounded `ghost-fingerprint survey summarize` digest for profiling large surveys without loading full raw evidence into agent context. +Add a bounded `ghost-fingerprint survey summarize` digest for fingerprint authoring large surveys without loading full raw evidence into agent context. diff --git a/.changeset/add-verify-profile-gate.md b/.changeset/add-verify-profile-gate.md index 5cfa2a20..d8bbc039 100644 --- a/.changeset/add-verify-profile-gate.md +++ b/.changeset/add-verify-profile-gate.md @@ -2,4 +2,4 @@ "ghost-fingerprint": minor --- -Add a deterministic profile verification command that checks fingerprint palette provenance and promoted check calibration against survey evidence. +Add a deterministic fingerprint verification command that checks fingerprint palette provenance and promoted check calibration against survey evidence. diff --git a/.changeset/authored-fingerprint-contract.md b/.changeset/authored-fingerprint-contract.md index 1deec307..0f63a83c 100644 --- a/.changeset/authored-fingerprint-contract.md +++ b/.changeset/authored-fingerprint-contract.md @@ -2,4 +2,4 @@ "ghost-fingerprint": major --- -Make fingerprint.md an authored-only contract, remove fragment-era on-disk embedding and check fields, add survey catalog, and expand profile verification. +Make fingerprint.md an authored-only contract, remove fragment-era on-disk embedding and check fields, add survey catalog, and expand fingerprint verification. diff --git a/.changeset/canonical-decision-vocabulary.md b/.changeset/canonical-decision-vocabulary.md index 3400833c..665c21c7 100644 --- a/.changeset/canonical-decision-vocabulary.md +++ b/.changeset/canonical-decision-vocabulary.md @@ -2,4 +2,4 @@ "ghost-fingerprint": minor --- -Add a controlled vocabulary of 12 canonical decision dimensions (`color-strategy`, `surface-hierarchy`, `shape-language`, `typography-voice`, `spatial-system`, `density`, `motion`, `elevation`, `theming-architecture`, `interactive-patterns`, `token-architecture`, `font-sourcing`) so fleet-aggregation primitives can group decisions across members. Profile recipe nudges authors toward canonical slugs; novel project-flavored slugs may pair with an optional `dimension_kind` that maps to a canonical survey. New soft `non-canonical-dimension` lint warning suggests the closest canonical match. The schema accepts the optional `dimension_kind` field on `decisions[]`; existing fingerprints remain valid. +Add a controlled vocabulary of 12 canonical decision dimensions (`color-strategy`, `surface-hierarchy`, `shape-language`, `typography-voice`, `spatial-system`, `density`, `motion`, `elevation`, `theming-architecture`, `interactive-patterns`, `token-architecture`, `font-sourcing`) so fleet-aggregation primitives can group decisions across members. Fingerprint recipe nudges authors toward canonical slugs; novel project-flavored slugs may pair with an optional `dimension_kind` that maps to a canonical survey. New soft `non-canonical-dimension` lint warning suggests the closest canonical match. The schema accepts the optional `dimension_kind` field on `decisions[]`; existing fingerprints remain valid. diff --git a/.changeset/fix-profile-recipe-paths.md b/.changeset/fix-profile-recipe-paths.md index 09639545..bc37032c 100644 --- a/.changeset/fix-profile-recipe-paths.md +++ b/.changeset/fix-profile-recipe-paths.md @@ -2,4 +2,4 @@ "ghost-fingerprint": patch --- -Fix the profile recipe — it now reads `design_system.paths` (the actual map.md frontmatter field) instead of the nonexistent `design_system.location`. The skill bundle ships under ghost-fingerprint, so the broken recipe shipped to host agents. +Fix the fingerprint recipe — it now reads `design_system.paths` (the actual map.md frontmatter field) instead of the nonexistent `design_system.location`. The skill bundle ships under ghost-fingerprint, so the broken recipe shipped to host agents. diff --git a/.changeset/implemented-surface-evidence.md b/.changeset/implemented-surface-evidence.md index 0abf88df..3c21f5a3 100644 --- a/.changeset/implemented-surface-evidence.md +++ b/.changeset/implemented-surface-evidence.md @@ -2,4 +2,4 @@ "ghost-fingerprint": major --- -Upgrade scans to `ghost.map/v2` and `ghost.survey/v2`, requiring implemented UI surface evidence and using those surfaces to guide fingerprint profiling. +Upgrade scans to `ghost.map/v2` and `ghost.survey/v2`, requiring implemented UI surface evidence and using those surfaces to guide fingerprint authoring. diff --git a/.changeset/initial-ghost-fingerprint.md b/.changeset/initial-ghost-fingerprint.md index 83536a1a..cdbc2d8c 100644 --- a/.changeset/initial-ghost-fingerprint.md +++ b/.changeset/initial-ghost-fingerprint.md @@ -2,4 +2,4 @@ "ghost-fingerprint": minor --- -Bootstrap `ghost-fingerprint` — Ghost's fingerprint.md authoring package. CLI verbs: `lint`, `describe`, `diff` (new — structural prose-level diff), and `emit ` (kinds: review-command, context-bundle, skill). The skill bundle ships the map-aware `profile.md` recipe alongside the condensed schema reference. All four verbs are deterministic; profile is a recipe the host agent executes. Mirrors the BYOA contract that the rest of Ghost follows. +Bootstrap `ghost-fingerprint` — Ghost's fingerprint.md authoring package. CLI verbs: `lint`, `describe`, `diff` (new — structural prose-level diff), and `emit ` (kinds: review-command, context-bundle, skill). The skill bundle ships the map-aware `fingerprint.md` recipe alongside the condensed schema reference. All four verbs are deterministic; fingerprint is a recipe the host agent executes. Mirrors the BYOA contract that the rest of Ghost follows. diff --git a/.changeset/phase-4c-recipe-branching.md b/.changeset/phase-4c-recipe-branching.md index 37e26773..5699caee 100644 --- a/.changeset/phase-4c-recipe-branching.md +++ b/.changeset/phase-4c-recipe-branching.md @@ -2,4 +2,4 @@ "ghost-fingerprint": patch --- -Branch the profile recipe by detected repo kind. The recipe now reads `design_system.token_source`, `composition.frameworks`, `registry`, and `platform` from `map.md` and chooses one of three sampling strategies — ui-library (default), token-pipeline (sample at layer level through YAML graph), or consumer-of-external-DS (record upstream slugs and override patterns instead of resolving to hex). Library-mode `feature_areas` guidance now distinguishes component categories from token-architecture layers. No schema changes. +Branch the fingerprint recipe by detected repo kind. The recipe now reads `design_system.token_source`, `composition.frameworks`, `registry`, and `platform` from `map.md` and chooses one of three sampling strategies — ui-library (default), token-pipeline (sample at layer level through YAML graph), or consumer-of-external-DS (record upstream slugs and override patterns instead of resolving to hex). Library-mode `feature_areas` guidance now distinguishes component categories from token-architecture layers. No schema changes. diff --git a/.changeset/phase-5-bugs-and-schema.md b/.changeset/phase-5-bugs-and-schema.md index 5a4e6e21..6d61c3f9 100644 --- a/.changeset/phase-5-bugs-and-schema.md +++ b/.changeset/phase-5-bugs-and-schema.md @@ -6,7 +6,7 @@ Phase 5 fixes and schema widening for real-world repo variety. Bug fixes (5a): -- Skill-bundle `schema.md` and `profile.md`: `decisions[].evidence` belongs in the body under `**Evidence:**` bullets, not as a frontmatter array. The condensed reference the LLM loads still showed the old shape; agents following the profile recipe were hitting 10 schema errors on first lint. +- Skill-bundle `schema.md` and `fingerprint.md`: `decisions[].evidence` belongs in the body under `**Evidence:**` bullets, not as a frontmatter array. The condensed reference the LLM loads still showed the old shape; agents following the fingerprint recipe were hitting 10 schema errors on first lint. - `unused-palette` now propagates slug-bindings: `roles[].tokens.palette.` referencing `{palette.dominant.X}` marks the underlying hex as cited. Phase 4b claimed this; the code only matched literal hexes. Schema widenings (5b): diff --git a/.changeset/refactor-fingerprint-package.md b/.changeset/refactor-fingerprint-package.md index 10a585d9..1c3a4bbe 100644 --- a/.changeset/refactor-fingerprint-package.md +++ b/.changeset/refactor-fingerprint-package.md @@ -3,4 +3,4 @@ "ghost-drift": major --- -Redefine the canonical fingerprint as `.ghost/fingerprint/` with profile guidance, survey evidence, map routing, checks gates, and deterministic drift checking. +Redefine the canonical fingerprint as `.ghost/fingerprint/` with fingerprint guidance, survey evidence, map routing, checks gates, and deterministic drift checking. diff --git a/.changeset/remove-profile-public-concept.md b/.changeset/remove-profile-public-concept.md new file mode 100644 index 00000000..ef0b7398 --- /dev/null +++ b/.changeset/remove-profile-public-concept.md @@ -0,0 +1,6 @@ +--- +"ghost-fingerprint": major +"ghost-drift": major +--- + +Replace the public fingerprint artifact and CLI surface with canonical fingerprint naming. diff --git a/.changeset/root-fingerprint-bundle.md b/.changeset/root-fingerprint-bundle.md new file mode 100644 index 00000000..1821aa23 --- /dev/null +++ b/.changeset/root-fingerprint-bundle.md @@ -0,0 +1,6 @@ +--- +"ghost-fingerprint": major +"ghost-drift": major +--- + +Make the root `.ghost/` directory the canonical Ghost fingerprint bundle, replacing `.ghost/fingerprint/fingerprint.md` package flows with resources, survey, patterns, checks, and optional intent artifacts. diff --git a/.changeset/tighten-fingerprint-evidence.md b/.changeset/tighten-fingerprint-evidence.md new file mode 100644 index 00000000..a827ef65 --- /dev/null +++ b/.changeset/tighten-fingerprint-evidence.md @@ -0,0 +1,5 @@ +--- +"ghost-fingerprint": patch +--- + +Tighten fingerprint verification and linting so survey-backed evidence is recognized and missing decision evidence is surfaced. diff --git a/CLAUDE.md b/CLAUDE.md index c6c25cd7..b95fe425 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -55,7 +55,7 @@ Run `just` to list all recipes. Key ones: `setup`, `build`, `check`, `fmt`, `tes ## Architecture -Ghost is **BYOA (bring-your-own-agent)**. The host agent — Claude Code, Codex, Cursor, Goose, whatever ships next — does the reading, deciding, and writing. The judgement work (profile, review, verify, remediate) lives in [agentskills.io](https://agentskills.io)-compatible skill bundles the agent executes. Ghost's CLIs are the calculator the agent reaches for when it needs a reproducible answer (vector math, schema validation, structural diffs). +Ghost is **BYOA (bring-your-own-agent)**. The host agent — Claude Code, Codex, Cursor, Goose, whatever ships next — does the reading, deciding, and writing. The judgement work (fingerprint, review, verify, remediate) lives in [agentskills.io](https://agentskills.io)-compatible skill bundles the agent executes. Ghost's CLIs are the calculator the agent reaches for when it needs a reproducible answer (vector math, schema validation, structural diffs). The repo decomposes into **four tools plus a reference design system**, each with a single responsibility: @@ -85,7 +85,7 @@ Each tool lives under `packages//` with the same shape: |---------|-----------|-------------| | `packages/ghost-core` | ❌ private (`@ghost/core`) | Workspace-only library. Embedding math, shared types, target resolution, skill-bundle loader, `ghost.map/v2` schema, `ghost.survey/v2` schema + lint/merge/fix-ids primitives. No CLI. Consumed by every other tool. | | `packages/ghost-drift` | ✅ `ghost-drift` on npm (v0.2+) | Drift detection. CLI verbs: `compare`, `ack`, `track`, `diverge`, `emit skill`. Skill recipes: `compare.md`, `review.md`, `verify.md`, `remediate.md`. Old `lint`/`describe`/`emit review-command`/`emit context-bundle` stay registered as stub commands that point users to `ghost-fingerprint`. | -| `packages/ghost-fingerprint` | ✅ intended-public (`publishConfig.access: public`, currently v0.0.0) | Owns the three-stage scan pipeline (`map.md` → `survey.json` → `fingerprint.md`). CLI verbs: `lint` (auto-detects file kind), `verify-profile` (fingerprint-to-survey fidelity), `inventory`, `describe`, `diff`, `survey ` (merge / fix-ids), `emit` (kinds: `review-command`, `context-bundle`, `skill`). Skill recipes: `map.md`, `survey.md`, `profile.md`, `schema.md`. | +| `packages/ghost-fingerprint` | ✅ intended-public (`publishConfig.access: public`, currently v0.0.0) | Owns the root `.ghost/` fingerprint bundle (`resources.yml` → `map.md` → `survey.json` → `patterns.yml`, plus optional `checks.yml` / `intent.md`). CLI verbs: `init-package`, `lint`, `verify`, `inventory`, `describe`, `diff`, `survey `, `emit`. Skill recipes: `scan.md`, `map.md`, `survey.md`, `patterns.md`, `schema.md`. | | `packages/ghost-fleet` | ❌ private | Read-only elevation across many `(map.md, fingerprint.md)` members. CLI verbs: `members`, `view`, `emit skill`. Skill recipes: `target.md`. | | `packages/ghost-ui` | ❌ private | Reference component library — 49 UI primitives + 48 AI elements + theme + hooks, distributed via the shadcn `registry.json`, not npm. Also ships the `ghost-mcp` bin (`src/mcp/`, built via `tsconfig.mcp.json` → `dist-mcp/`) — an MCP server re-exposing the registry to AI assistants (5 tools, 2 resources). | | `apps/docs` | ❌ private | The deployed docs site (`ghost-docs`) — home, drift tooling docs, design language foundations, live component catalogue. Consumes `ghost-ui`. | @@ -97,9 +97,9 @@ Verbs are scoped to the tool that owns the artifact. The full surface across all | Tool | Command | Description | |------|---------|-------------| | `ghost-fingerprint` | `inventory [path]` | Emit raw repo signals (manifests, language histogram, registry, top-level tree, git remote) as JSON. Feeds the topology recipe. | -| `ghost-fingerprint` | `scan-status [dir]` | Report which scan stages have produced artifacts (`map.md` / `survey.json` / `fingerprint.md`) and which stage to run next. | -| `ghost-fingerprint` | `lint [file]` | Validate `fingerprint.md`, `map.md`, or `survey.json` — auto-detects the kind from path/content. | -| `ghost-fingerprint` | `verify-profile ` | Verify fingerprint-to-survey fidelity after profiling; palette values must be survey-backed and promoted checks must be calibrated. | +| `ghost-fingerprint` | `scan-status [dir]` | Report which scan stages have produced artifacts (`resources.yml` / `map.md` / `survey.json` / `patterns.yml`) and which stage to run next. | +| `ghost-fingerprint` | `lint [file]` | Validate a root `.ghost/` bundle or a single artifact — auto-detects the kind from path/content. | +| `ghost-fingerprint` | `verify [dir] --root ` | Verify cross-artifact fidelity: pattern evidence exists in survey, resources are reachable, and checks reference known scopes/patterns. | | `ghost-fingerprint` | `describe [fingerprint]` | Print section ranges + token estimates (so agents can selectively load). | | `ghost-fingerprint` | `diff ` | Structural prose-level diff between fingerprints (decisions + palette roles). **Not** vector distance. | | `ghost-fingerprint` | `survey [...surveys]` | Operate on `ghost.survey/v2` files. Ops: `merge` (concat with id-based dedup), `fix-ids` (recompute IDs from content). | @@ -115,10 +115,10 @@ Verbs are scoped to the tool that owns the artifact. The full surface across all **Workflows (agent recipes).** Each tool ships its own skill-bundle references under `packages//src/skill-bundle/references/`. These are the agent's job, not CLI verbs: -- **Scan** (orchestrate map → survey → profile end-to-end) — `ghost-fingerprint/.../scan.md` +- **Scan** (orchestrate map → survey → fingerprint end-to-end) — `ghost-fingerprint/.../scan.md` - **Map** (write `map.md` from a repo, the topology stage) — `ghost-fingerprint/.../map.md` - **Survey** (write `survey.json` from a target, the observed evidence stage) — `ghost-fingerprint/.../survey.md` -- **Profile** (interpret a `survey.json` into `fingerprint.md`, the fingerprint stage) — `ghost-fingerprint/.../profile.md` +- **Fingerprint** (interpret a `survey.json` into `fingerprint.md`, the fingerprint stage) — `ghost-fingerprint/.../fingerprint.md` - **Review** (flag drift in PR changes) — `ghost-drift/.../review.md` - **Verify** (generate → review loop) — `ghost-drift/.../verify.md` - **Compare interpretation** — `ghost-drift/.../compare.md` @@ -136,7 +136,7 @@ The `resolveTarget()` function in `@ghost/core` (`packages/ghost-core/src/target - `https://...` — URL - `.` — current directory -Used by `resolveTrackedFingerprint` (in `ghost-drift`) and legacy library consumers. Profile and map flows don't consume targets directly — the host agent explores whatever directory is relevant. +Used by `resolveTrackedFingerprint` (in `ghost-drift`) and legacy library consumers. Fingerprint and map flows don't consume targets directly — the host agent explores whatever directory is relevant. ## Canonical artifacts @@ -144,7 +144,7 @@ Three artifacts produced in sequence by a scan, all owned by `ghost-fingerprint` - **`map.md`** — the topology card (stage 1). Human-readable answer to "where is the design system, which folders matter, and where are implemented surfaces observable?" Schema is `ghost.map/v2` (lives in `@ghost/core`), validated by `ghost-fingerprint lint map.md`. Authored from `ghost-fingerprint inventory` + the `map.md` skill recipe. The repo's own `map.md` lives at the root. - **`survey.json`** — the observed evidence scan (stage 2). Catalogues every concrete design value (colors, spacings, typography, radii, shadows, breakpoints, motion, layout primitives) plus tokens, components, and implemented UI surfaces observed in the target. Each row carries occurrence counts and a deterministic content-hashed `id`. Schema is `ghost.survey/v2` (lives in `@ghost/core`); four sections — `values`, `tokens`, `components`, `ui_surfaces`. External libraries (icons, primitives, charting) deliberately *do not* have a survey section — whether a system uses Radix or hand-rolls primitives doesn't change what its design language *is*; load-bearing library choices surface as prose evidence in the interpreter stage. Validated by `ghost-fingerprint lint survey.json`. Authored via the `survey.md` skill recipe. -- **`fingerprint.md`** — the design language (stage 3, terminal). Human-readable, LLM-editable, with YAML frontmatter (machine layer: references + 49-dim embedding + palette/spacing/typography/surfaces/checks) and a three-section prose body (Character → Signature → Decisions). Authored by interpreting `survey.json` per the `profile.md` skill recipe. See `docs/fingerprint-format.md` for the full spec; the condensed reference ships at `packages/ghost-fingerprint/src/skill-bundle/references/schema.md`. +- **`fingerprint.md`** — the design language (stage 3, terminal). Human-readable, LLM-editable, with YAML frontmatter (machine layer: references + 49-dim embedding + palette/spacing/typography/surfaces/checks) and a three-section prose body (Character → Signature → Decisions). Authored by interpreting `survey.json` per the `fingerprint.md` skill recipe. See `docs/fingerprint-format.md` for the full spec; the condensed reference ships at `packages/ghost-fingerprint/src/skill-bundle/references/schema.md`. ## Releasing & Changesets @@ -183,10 +183,10 @@ The slug should be short and descriptive: `add-temporal-flag.md`, `fix-palette-l ## Key Conventions -- Each `fingerprint.md` carries a 49-dimensional embedding vector (palette [0–20], spacing [21–30], typography [31–40], surfaces [41–48]; see `packages/ghost-core/src/embedding/embedding.ts`). The canonical on-disk form is the Markdown file itself — there is no parallel JSON/DTCG representation. -- `ghost-drift compare` takes **file paths** to `fingerprint.md`, not target strings. Mode auto-detects from N and flags: `--semantic` / `--temporal` require N=2; N≥3 returns a composite fingerprint. -- `ghost-drift ack` / `track` / `diverge` read the local `fingerprint.md`. The host agent is responsible for regenerating `fingerprint.md` (via the `profile` recipe) before acknowledging drift. +- The canonical on-disk form is the root `.ghost/` bundle. Direct `fingerprint.md` remains only for legacy/direct compare and context-bundle flows. +- `ghost-drift compare` accepts `.ghost` bundle directories and direct fingerprint markdown files. Mode auto-detects from N and flags: `--semantic` / `--temporal` require N=2; N≥3 returns a composite fingerprint. +- `ghost-drift ack` / `track` / `diverge` read the local `fingerprint.md`. The host agent is responsible for regenerating `fingerprint.md` (via the `fingerprint` recipe) before acknowledging drift. - `ghost-fingerprint lint` takes a single `fingerprint.md` and reports schema/partition violations. Use as the shape gate when authoring a fingerprint. -- `ghost-fingerprint verify-profile fingerprint.md survey.json --root .` is the required scan-stage fidelity gate after profiling: it checks palette provenance and promoted-check calibration against the survey/root. +- `ghost-fingerprint verify .ghost --root .` is the required scan-stage fidelity gate after bundle authoring. - `ghost-fingerprint lint ` validates against `ghost.map/v2` (auto-detected by frontmatter or filename). Use as the success gate when authoring a map. - The CLI manifest at `apps/docs/src/generated/cli-manifest.json` is auto-generated by `pnpm dump:cli-help`. CI guards drift via `pnpm check:cli-manifest`. Re-run `pnpm dump:cli-help` after adding/removing flags or verbs to any tool. diff --git a/README.md b/README.md index a68c5b29..0e7650e2 100644 --- a/README.md +++ b/README.md @@ -12,12 +12,14 @@ Ghost introduces that second layer as a repository-local, versioned fingerprint. The scan earns that package with evidence: -- **`.ghost/fingerprint/map.md`** routes changes to repo scopes and surfaces. -- **`.ghost/fingerprint/survey.json`** records the values, tokens, components, and surfaces it found. -- **`.ghost/fingerprint/profile.md`** shapes agent judgment as non-enforcing design-language guidance. -- **`.ghost/fingerprint/checks.yml`** stores human-promoted deterministic gates. +- **`.ghost/resources.yml`** declares the references that define the product. +- **`.ghost/map.md`** routes changes to repo scopes and surfaces. +- **`.ghost/survey.json`** records values, tokens, components, surfaces, and factual composition observations. +- **`.ghost/patterns.yml`** codifies repeated composition grammar with evidence. +- **`.ghost/checks.yml`** optionally stores human-promoted deterministic gates. +- **`.ghost/intent.md`** optionally records human-authored or human-approved product intent. -Specs describe what exists. The fingerprint package describes how the product repeatedly chooses to use what exists. Checks fail builds. Profile shapes judgment. Survey grounds both. The package is the fingerprint. +Specs describe what exists. The fingerprint bundle describes how the product repeatedly chooses to use what exists. Survey grounds it; patterns operationalize composition; optional checks fail builds; optional intent preserves the human voice. ## Works with your agent @@ -29,25 +31,25 @@ No API key is required to run any CLI verb. Each tool's `emit skill` verb instal Ghost gives agents a few practical abilities: -- **Generate from repo-local memory**: `.ghost/fingerprint/profile.md` and survey examples tell the agent what the design language is before it writes UI. +- **Generate from repo-local memory**: `.ghost/patterns.yml`, `.ghost/survey.json`, and optional `.ghost/intent.md` tell the agent how the product composes UI before it writes. - **Fail deterministic drift**: `ghost-drift check` applies active `checks.yml` gates to a diff. -- **Review changes with evidence**: `ghost-drift review` emits an advisory packet grounded in profile, survey, examples, checks, and diff. +- **Review changes with evidence**: `ghost-drift review` emits an advisory packet grounded in patterns, survey, optional intent, checks, and diff. - **Compare systems**: `ghost-drift compare` and `ghost-fleet view` show how fingerprints differ across projects. - **Record intent**: `ack`, `track`, and `diverge` record whether drift is accepted, tracked against a new reference, or intentionally different. -- **Stay readable**: `profile.md` and `map.md` are Markdown, `survey.json` is factual evidence, and `checks.yml` is the human-curated gate layer. +- **Stay readable**: `map.md` and optional `intent.md` are Markdown, `survey.json` is factual evidence, `patterns.yml` is operational grammar, and `checks.yml` is the human-curated gate layer. ## Tools around the loop Ghost is split into focused tools. The common path is simple: ```text -.ghost/fingerprint/map.md -> survey.json -> profile.md + checks.yml -> check/review +.ghost/resources.yml -> map.md -> survey.json -> patterns.yml -> check/review ``` | Tool | Job | Verbs | | --- | --- | --- | -| **`ghost-fingerprint`** | Create and check the `.ghost/fingerprint/` package. | `init-package`, `inventory`, `lint`, `verify-profile`, `describe`, `diff`, `survey `, `emit` | -| **`ghost-drift`** | Run deterministic checks, emit advisory review packets, compare profiles, and record what changed intentionally. | `check`, `review`, `compare`, `ack`, `track`, `diverge`, `emit skill` | +| **`ghost-fingerprint`** | Create and check the root `.ghost/` bundle. | `init-package`, `inventory`, `lint`, `verify`, `describe`, `diff`, `survey `, `emit` | +| **`ghost-drift`** | Run deterministic checks, emit advisory review packets, compare fingerprints, and record what changed intentionally. | `check`, `review`, `compare`, `ack`, `track`, `diverge`, `emit skill` | | **`ghost-fleet`** | See how many project fingerprints relate. | `members`, `view`, `emit skill` | | **`ghost-ui`** | Reference design system Ghost dogfoods — 97 shadcn components + an MCP server. | (no verbs) | @@ -61,8 +63,8 @@ Ghost is a pnpm monorepo. Four tools, one reference design system, one docs site | Path | Role | Published? | | ---- | ---- | --- | -| [`packages/ghost-core`](./packages/ghost-core) | Workspace-only shared library — embedding math, target resolver, skill loader, `ghost.map/v2`, `ghost.survey/v2`, and `ghost.checks/v1` schemas. | ❌ private (`@ghost/core`) | -| [`packages/ghost-fingerprint`](./packages/ghost-fingerprint) | The scan package pipeline: `.ghost/fingerprint/{map.md,survey.json,profile.md,checks.yml}`. Authoring, lint, profile verification, describe, diff, survey ops, emit. | ✅ intended-public on npm | +| [`packages/ghost-core`](./packages/ghost-core) | Workspace-only shared library — embedding math, target resolver, skill loader, `ghost.resources/v1`, `ghost.map/v2`, `ghost.survey/v2`, `ghost.patterns/v1`, and `ghost.checks/v1` schemas. | ❌ private (`@ghost/core`) | +| [`packages/ghost-fingerprint`](./packages/ghost-fingerprint) | The root bundle pipeline: `.ghost/{resources.yml,map.md,survey.json,patterns.yml}` plus optional checks and intent. Authoring, lint, verify, describe, diff, survey ops, emit. | ✅ intended-public on npm | | [`packages/ghost-drift`](./packages/ghost-drift) | Deterministic check, advisory review, comparison, and stance verbs. | ✅ `ghost-drift` on npm | | [`packages/ghost-fleet`](./packages/ghost-fleet) | Fleet view across many members. | ❌ private | | [`packages/ghost-ui`](./packages/ghost-ui) | Reference design system: 97 shadcn components + the `ghost-mcp` MCP server. | ❌ private (distributed via shadcn registry, not npm) | @@ -86,9 +88,9 @@ After install, in any repo: > Scan this project with ghost ``` -The agent walks `.ghost/fingerprint/map.md` → `survey.json` → `profile.md` + `checks.yml`, then checks or reviews UI changes against that package. The recipes work without any Ghost CLI on PATH — every CLI-using step has a prose fallback. +The agent walks `.ghost/resources.yml` → `map.md` → `survey.json` → `patterns.yml` + optional `checks.yml` / `intent.md`, then checks or reviews UI changes against that bundle. The recipes work without any Ghost CLI on PATH — every CLI-using step has a prose fallback. -If you want the CLI helpers for linting, profile verification, diffing, comparing, and fleet views, install from source instead. See *Getting Started* below. +If you want the CLI helpers for linting, fingerprint verification, diffing, comparing, and fleet views, install from source instead. See *Getting Started* below. ## Getting Started @@ -114,7 +116,7 @@ ghost-fingerprint emit skill # → ./.claude/skills/ghost-fingerprint ghost-fleet emit skill # → ./.claude/skills/ghost-fleet ``` -Once a skill is installed, ask your agent in plain English ("profile this design language", "review this PR for drift", "compute the fleet view") and it'll follow the recipe, calling the relevant CLI whenever it needs a reproducible answer. +Once a skill is installed, ask your agent in plain English ("scan this project with Ghost", "review this PR for drift", "compute the fleet view") and it'll follow the recipe, calling the relevant CLI whenever it needs a reproducible answer. ### Quick start @@ -124,25 +126,25 @@ Once a skill is installed, ask your agent in plain English ("profile this design ghost-fingerprint init-package ``` -**1. Map the repo** (the first checkpoint before survey and profile). Ask your host agent to write `.ghost/fingerprint/map.md`, then validate: +**1. Map the repo**. Ask your host agent to write `.ghost/map.md`, then validate: ```bash ghost-fingerprint inventory -ghost-fingerprint lint .ghost/fingerprint +ghost-fingerprint lint .ghost ``` -**2. Survey the design values** (the observed evidence stage). Ask your host agent to write `.ghost/fingerprint/survey.json`, then validate: +**2. Survey the design values and composition observations**. Ask your host agent to write `.ghost/survey.json`, then validate: ```bash -ghost-fingerprint survey fix-ids .ghost/fingerprint/survey.json -o .ghost/fingerprint/survey.json -ghost-fingerprint survey patterns .ghost/fingerprint/survey.json -ghost-fingerprint lint .ghost/fingerprint +ghost-fingerprint survey fix-ids .ghost/survey.json -o .ghost/survey.json +ghost-fingerprint lint .ghost ``` -**3. Profile and promote checks** — ask your host agent to write `.ghost/fingerprint/profile.md` and propose lintable checks. Humans promote durable gates into `.ghost/fingerprint/checks.yml`: +**3. Codify patterns and promote checks** — ask your host agent to write `.ghost/patterns.yml` and propose lintable checks. Humans promote durable gates into `.ghost/checks.yml`: ```bash -ghost-fingerprint verify-profile .ghost/fingerprint/profile.md .ghost/fingerprint/survey.json --root . +ghost-fingerprint survey patterns .ghost/survey.json -o .ghost/patterns.yml +ghost-fingerprint verify .ghost --root . ghost-fingerprint lint ``` @@ -154,11 +156,11 @@ ghost-drift check --diff change.patch --format json ghost-drift review --base main ``` -**5. Compare profiles:** +**5. Compare fingerprints:** ```bash # Pairwise: per-dimension distance -ghost-drift compare market.profile.md dashboard.profile.md +ghost-drift compare market/.ghost dashboard/.ghost # Add qualitative interpretation of decisions + palette ghost-drift compare a.md b.md --semantic @@ -167,14 +169,14 @@ ghost-drift compare a.md b.md --semantic ghost-drift compare before.md after.md --temporal # Composite (N≥3): pairwise matrix, centroid, clusters — the org fingerprint -ghost-drift compare *.profile.md +ghost-drift compare */.ghost ``` -**6. Track intent toward another profile:** +**6. Track intent toward another fingerprint:** ```bash ghost-drift ack --stance aligned --reason "Initial baseline" -ghost-drift track new-tracked.profile.md +ghost-drift track new-tracked.fingerprint.md ghost-drift diverge typography --reason "Editorial product uses a different type scale" ``` @@ -182,11 +184,11 @@ ghost-drift diverge typography --reason "Editorial product uses a different type ```bash ghost-fingerprint emit review-command # .claude/commands/design-review.md (per-project slash command) -ghost-fingerprint emit context-bundle # ghost-context/ (SKILL.md + profile/fingerprint context + prompt.md + tokens.css) +ghost-fingerprint emit context-bundle # ghost-context/ (SKILL.md + fingerprint context + prompt.md + tokens.css) ghost-fingerprint emit skill # .claude/skills/ghost-fingerprint (the agentskills.io bundle) ``` -**8. View a fleet** (when you have ≥2 members each with their own package/profile): +**8. View a fleet** (when you have ≥2 members each with their own package/fingerprint): ```bash ghost-fleet members ./fleet # list registered members + freshness @@ -207,13 +209,13 @@ Commands are grouped by the tool that owns the file. Pure inputs → pure output | Tool | Command | Description | | --- | --- | --- | | `ghost-fingerprint` | `inventory` | Emit raw repo signals (manifests, language histogram, registry presence, top-level tree, git remote) as JSON. Feeds the map recipe. | -| `ghost-fingerprint` | `init-package` | Create `.ghost/fingerprint/{map.md,survey.json,profile.md,checks.yml}`. | -| `ghost-fingerprint` | `lint` | Validate the fingerprint package or an individual `profile.md`, `map.md`, `survey.json`, or `checks.yml`. | -| `ghost-fingerprint` | `verify-profile` | Validate profile-to-survey fidelity after profiling; palette, spacing, typography, radii, and shadow posture must be survey-backed. | -| `ghost-fingerprint` | `describe` | Print `profile.md` section ranges + token estimates so agents can selectively load. | +| `ghost-fingerprint` | `init-package` | Create `.ghost/{resources.yml,map.md,survey.json,patterns.yml,checks.yml}`. | +| `ghost-fingerprint` | `lint` | Validate the fingerprint bundle or an individual artifact. | +| `ghost-fingerprint` | `verify` | Validate cross-artifact fidelity: resources, pattern evidence, and check references. | +| `ghost-fingerprint` | `describe` | Print optional `intent.md` or direct markdown section ranges + token estimates. | | `ghost-fingerprint` | `diff` | Structural prose-level diff between two fingerprints (NOT vector distance — for that, use `ghost-drift compare`). | | `ghost-fingerprint` | `survey ` | Operate on `ghost.survey/v2` files. Ops: `merge`, `fix-ids`, `summarize`, `catalog`, `patterns`. | -| `ghost-fingerprint` | `emit` | Derive an output from the profile/package: `review-command`, `context-bundle`, or `skill`. | +| `ghost-fingerprint` | `emit` | Derive an output from direct fingerprint markdown or install the skill bundle. | | `ghost-drift` | `check` | Run active `ghost.checks/v1` deterministic gates against a diff; exits nonzero on failures. | | `ghost-drift` | `review` | Emit an evidence-routed advisory review packet; findings are non-blocking unless tied to active checks. | | `ghost-drift` | `compare` | Pairwise (N=2) or composite (N≥3) over runtime fingerprint embeddings. `--semantic` / `--temporal` add qualitative enrichment. | @@ -233,7 +235,7 @@ The interpretive work is done by recipes the agent runs. Install the relevant bu | --- | --- | --- | --- | | `map` | `ghost-fingerprint` | Write the repo map (stage 1) | "map this repo", "write map.md" | | `survey` | `ghost-fingerprint` | Author the survey of values (stage 2) | "survey design values", "extract design tokens" | -| `profile` | `ghost-fingerprint` | Author the non-enforcing design-language prior (stage 3) | "profile this design language", "write profile.md" | +| `patterns` | `ghost-fingerprint` | Author operational composition grammar (stage 4) | "write patterns.yml", "codify composition patterns" | | `review` | `ghost-drift` | Review PR changes for drift | "review this PR for drift" | | `verify` | `ghost-drift` | Check generated UI against the fingerprint | "verify generated UI against the fingerprint" | | `compare` | `ghost-drift` | Compare fingerprints | "why did these two fingerprints drift?" | @@ -257,30 +259,31 @@ Each CLI auto-loads `.env` and `.env.local` from the working directory. ### The fingerprint -What the agent reads when it writes or reviews UI is the **fingerprint package**: +What the agent reads when it writes or reviews UI is the **fingerprint bundle**: - **`map.md`**: where surfaces live and how changed files route to scopes. - **`survey.json`**: factual observed evidence. -- **`profile.md`**: non-enforcing design-language prior. It shapes judgment but never fails CI by itself. -- **`checks.yml`**: human-promoted enforceable gates. These are the only blocking mechanism in v1. +- **`patterns.yml`**: operational composition grammar. It shapes advisory review but never fails CI by itself. +- **`intent.md`**: optional human-authored or human-approved product intent. +- **`checks.yml`**: optional human-promoted enforceable gates. These are the only blocking mechanism in v1. -Generate one with the `profile` recipe (in the `ghost-fingerprint` skill bundle). See [`docs/fingerprint-format.md`](./docs/fingerprint-format.md) for the full spec. +Generate one with the `scan` and `patterns` recipes (in the `ghost-fingerprint` skill bundle). See [`docs/fingerprint-format.md`](./docs/fingerprint-format.md) for the full spec. ### The map -What Ghost uses during scan and drift workflows to understand the repo. **`.ghost/fingerprint/map.md`** is stage 1 of a scan. It records languages, build system, package manifests, registry files, design-system paths, observable surfaces, feature areas, and scopes. Deterministic drift starts by routing changed files through this map. +What Ghost uses during scan and drift workflows to understand the repo. **`.ghost/map.md`** is the topology stage of a scan. It records languages, build system, package manifests, registry files, design-system paths, observable surfaces, feature areas, and scopes. Deterministic drift starts by routing changed files through this map. Generate one with the `map` recipe (in the `ghost-fingerprint` skill bundle). The agent reads `ghost-fingerprint inventory` (raw repo signals as JSON) and writes the short body. ### Author + Review Loop -The loop is simple: the agent writes UI, `ghost-drift check` fails active gates, `ghost-drift review` provides advisory critique grounded in evidence, and a human or agent decides what to do next. Fix the drift, accept it, track a new profile, or promote a durable rule into `checks.yml`. See [`docs/generation-loop.md`](./docs/generation-loop.md) for details. +The loop is simple: the agent writes UI, `ghost-drift check` fails active gates, `ghost-drift review` provides advisory critique grounded in evidence, and a human or agent decides what to do next. Fix the drift, accept it, track a new fingerprint, or promote a durable rule into `checks.yml`. See [`docs/generation-loop.md`](./docs/generation-loop.md) for details. ### Remediation Three files record what happened: -- **`.ghost/fingerprint/`**: The repo-local design memory package. +- **`.ghost/`**: The repo-local design memory bundle. - **`.ghost-sync.json`**: Per-dimension stances toward the tracked fingerprint (aligned, accepted, or diverging), each with recorded reasoning. Written by `ghost-drift ack` / `track` / `diverge`. - **`.ghost/history.jsonl`**: Append-only fingerprint history for temporal analysis. Read by `ghost-drift compare --temporal`. @@ -288,8 +291,8 @@ Three files record what happened: To look across many projects: -- **Many profiles, no map**: run `ghost-drift compare` with three or more `profile.md` files. It returns pairwise distances, a centroid, and similarity clusters. -- **A registered fleet** (members each with a fingerprint package): run `ghost-fleet view`. It adds groupings such as platform, build system, and design-system status. +- **Many bundles, no fleet**: run `ghost-drift compare` with three or more `.ghost` bundle directories. It returns pairwise distances, a centroid, and similarity clusters. +- **A registered fleet** (members each with a fingerprint bundle): run `ghost-fleet view`. It adds groupings such as platform, build system, and design-system status. ## Project Resources diff --git a/apps/docs/src/app/page.tsx b/apps/docs/src/app/page.tsx index 3015ab64..953ac773 100644 --- a/apps/docs/src/app/page.tsx +++ b/apps/docs/src/app/page.tsx @@ -64,20 +64,22 @@ export default function Home() {

  • - .ghost/fingerprint/map.md defines where to observe - and route UI + .ghost/resources.yml names what the bundle is + grounded in
  • - .ghost/fingerprint/survey.json records extracted - tokens, components, and patterns + .ghost/map.md defines where to observe and route UI
  • - .ghost/fingerprint/profile.md shapes judgment + .ghost/survey.json records factual extracted + evidence +
  • +
  • + .ghost/patterns.yml shapes advisory judgment without enforcing CI
  • - .ghost/fingerprint/checks.yml stores human-promoted - gates + .ghost/checks.yml stores human-promoted gates

diff --git a/apps/docs/src/app/tools/drift/page.tsx b/apps/docs/src/app/tools/drift/page.tsx index 3367125c..f2cab9ba 100644 --- a/apps/docs/src/app/tools/drift/page.tsx +++ b/apps/docs/src/app/tools/drift/page.tsx @@ -30,7 +30,7 @@ const cards: { name: "CLI reference", href: "/docs/cli#ghost-drift--review-and-compare", description: - "Run checks, emit advisory review, compare profiles, and record intent.", + "Run checks, emit advisory review, compare fingerprints, and record intent.", icon: , }, ]; @@ -47,7 +47,7 @@ export default function GhostDriftLanding() {

, }, ]; diff --git a/apps/docs/src/content/docs/cli-reference.mdx b/apps/docs/src/content/docs/cli-reference.mdx index e0b8af26..940765a3 100644 --- a/apps/docs/src/content/docs/cli-reference.mdx +++ b/apps/docs/src/content/docs/cli-reference.mdx @@ -9,25 +9,25 @@ slug: cli -The CLIs do the repeatable parts: validate files, compare fingerprints, and +The CLIs do the repeatable parts: validate files, compare bundles, and record decisions. Your agent does the reading, writing, and reviewing. A scan follows one path, all owned by `ghost-fingerprint`: ```text -map.md -> survey.json -> fingerprint.md +resources.yml -> map.md -> survey.json -> patterns.yml ``` -Most commands accept a path; kind-aware commands default to -`./fingerprint.md`. No API key required. +Most commands accept a path; bundle-aware commands default to `.ghost`. +No API key required. Commands are grouped by job: -- **Create and check the fingerprint** — `ghost-fingerprint`: `inventory`, `scan-status`, `lint`, `verify-profile`, `describe`, `diff`, `survey `, `emit` +- **Create and check the fingerprint bundle** — `ghost-fingerprint`: `inventory`, `scan-status`, `lint`, `verify`, `describe`, `diff`, `survey `, `emit` - **Review and compare drift** — `ghost-drift`: `compare`, `ack`, `track`, `diverge`, `emit skill` - **View many projects** — `ghost-fleet`: `members`, `view`, `emit skill` -Workflows like _scan_, _map_, _survey_, _profile_, _review_, _verify_, and +Workflows like _scan_, _map_, _survey_, _patterns_, _review_, _verify_, and _remediate_ are skill recipes your host agent runs — not CLI verbs. Install them with each tool's `emit skill` verb. @@ -39,10 +39,10 @@ this reference. -`ghost-fingerprint` owns the scan: `map.md` → `survey.json` → -`fingerprint.md`. It inventories the repo, validates each file, verifies the -fingerprint against survey evidence, describes and diffs fingerprints, runs -survey ops, and emits agent-ready outputs. +`ghost-fingerprint` owns the root bundle scan: `resources.yml` → `map.md` → +`survey.json` → `patterns.yml`. It inventories the repo, validates each file, +verifies cross-artifact fidelity, describes markdown, diffs direct fingerprint +files, runs survey ops, and emits agent-ready outputs. ### Repo map — `inventory` @@ -62,9 +62,9 @@ ghost-fingerprint inventory ../other-repo ### Pipeline progress — `scan-status` -Report which scan stages already exist in a directory: map (`map.md`), survey -(`survey.json`), fingerprint (`fingerprint.md`). This tells the agent which -stage to run next. +Report which scan stages already exist in a directory: resources +(`resources.yml`), map (`map.md`), survey (`survey.json`), and patterns +(`patterns.yml`). This tells the agent which stage to run next. @@ -88,9 +88,10 @@ Operate on `ghost.survey/v2` files: - **`merge`** — combine surveys with id-based dedup. - **`fix-ids`** — recompute every row's `id` from its content. -- **`summarize`** — print a short Markdown or JSON digest for profiling. +- **`summarize`** — print a short Markdown or JSON digest for pattern authoring. - **`catalog`** — print exact value enums/specs derived from survey rows. Markdown is the default; JSON uses `ghost.survey.catalog/v1`. +- **`patterns`** — derive a `ghost.patterns/v1` draft from surface evidence. @@ -104,7 +105,7 @@ ghost-fingerprint survey merge fleet/members/*/survey.json -o cohort.json # Populate IDs after authoring rows with empty id fields ghost-fingerprint survey fix-ids draft.json -o draft.json -# Broad profiling context +# Broad bundle context ghost-fingerprint survey summarize survey.json # Exact value enums/specs @@ -117,16 +118,14 @@ ghost-fingerprint survey merge a.json b.json | jq '.values | length' ### Validation — `lint` -Validate `fingerprint.md`, `map.md`, or `survey.json` against its schema — -auto-detects which by `.json` extension, `schema: ghost.map/v2` frontmatter, -or filename. Use this before declaring a file structurally valid; every -authoring recipe calls it. For profiled fingerprints, follow `lint` with -`verify-profile` to check fidelity against `survey.json`. +Validate a root `.ghost` bundle or an individual artifact against its schema. +Use this before declaring a file structurally valid; every authoring recipe +calls it. Follow `lint` with `verify` to check cross-artifact fidelity. ```bash -# Default — reads ./fingerprint.md +# Default — reads .ghost ghost-fingerprint lint # Validate a map.md (auto-detected by frontmatter) @@ -136,46 +135,35 @@ ghost-fingerprint lint map.md ghost-fingerprint lint survey.json # JSON output -ghost-fingerprint lint path/to/fingerprint.md --format json +ghost-fingerprint lint .ghost/patterns.yml --format json ``` -### Profile fidelity — `verify-profile` +### Bundle fidelity — `verify` -Validate that a `fingerprint.md` matches its `survey.json`. This is stricter -than `lint`: palette, spacing, typography, radii, and shadow posture must be -survey-backed. High-salience survey values omitted from the fingerprint become -warnings. With `--root`, promoted check patterns are counted under their -`paths` scopes; `contexts` are guidance only. +Validate that a root `.ghost` bundle is internally faithful. This is stricter +than `lint`: resources should resolve from `--root`, composition patterns must +cite survey-backed evidence, and checks must reference known pattern IDs when +they use pattern metadata. - + ```bash -# Profile success gate after lint -ghost-fingerprint verify-profile fingerprint.md survey.json --root . +# Bundle success gate after lint +ghost-fingerprint verify .ghost --root . # Machine-readable for CI -ghost-fingerprint verify-profile fingerprint.md survey.json --root . --format json +ghost-fingerprint verify .ghost --root . --format json ``` ### Inspection — `describe` -Print a section map of `fingerprint.md` — frontmatter range, body sections -(`# Character`, `# Signature`, `# Decisions`), and each `### dimension` -block under Decisions, with line ranges and token estimates. -The host agent uses this to load only the sections it needs instead of the -whole file. - -A typical `fingerprint.md` runs 3–5k tokens. The `# Decisions` block alone -is usually 60–80% of that, and an agent reviewing a single component change -rarely needs every dimension. `describe` answers "what's in this file and -where" with the same answer every time — the recall safety rule (when in -doubt, load the whole `# Decisions` block) lives in the review/remediate -skill recipes. +Print a section map of optional `.ghost/intent.md` or a direct fingerprint +markdown file. The host agent uses this to load only the sections it needs. ```bash -# Default — reads ./fingerprint.md +# Default — reads .ghost/intent.md ghost-fingerprint describe # Specific file @@ -185,24 +173,8 @@ ghost-fingerprint describe path/to/fingerprint.md ghost-fingerprint describe --format json ``` -Sample output (against `packages/ghost-fingerprint/test/fixtures/ghost-ui-fingerprint/fingerprint.md`): - -```text -fingerprint.md — 305 lines, ~3,934 tokens - -FRONTMATTER 1–164 ~973 tok [references, palette, spacing, typography, surfaces, checks, observation, decisions] -# Character 166–169 ~159 tok -# Signature 170–174 ~120 tok -# Decisions 175–305 ~2,800 tok - ### color-strategy 177–187 ~250 tok - ### shape-language 195–206 ~181 tok - ### typography-voice 207–219 ~258 tok - … -``` - -Line ranges are 1-indexed and inclusive — they plug directly into a Read -tool's `offset` / `limit = end - start + 1`. Token counts are a `chars / 4` -approximation, sufficient for context budgeting. +Line ranges are 1-indexed and inclusive; token counts are a `chars / 4` +approximation. ### Structural diff — `diff` @@ -217,13 +189,12 @@ ghost-fingerprint diff a/fingerprint.md b/fingerprint.md ghost-fingerprint diff a.md b.md --format json ``` -### Emit — derive outputs from `fingerprint.md` +### Emit — derived outputs and skill bundles -Derive an output from `fingerprint.md`. Kinds: `review-command` (a -per-project slash command at `.claude/commands/design-review.md`), -`context-bundle` (SKILL.md + fingerprint.md + prompt.md + tokens.css for any -generator), or `skill` (the `ghost-fingerprint` agentskills.io bundle — -install this into your host agent for the `profile` recipe). +Kinds: `review-command`, `context-bundle`, or `skill`. The skill bundle installs +the scan/map/survey/patterns recipes into your host agent. Review/context +outputs still operate on direct fingerprint markdown until their bundle-native +replacement lands. @@ -250,15 +221,16 @@ ghost-fingerprint emit context-bundle --out dist/context ### Comparison — `compare` -Pairwise distance (N=2) or composite analysis (N≥3) over fingerprint -embeddings. Pure math. Exits non-zero when drift exceeds 0.5. `--semantic` -and `--temporal` add qualitative enrichment for N=2. +Pairwise distance (N=2) or composite analysis (N≥3) over root bundles or direct +fingerprint markdown. Bundle inputs are synthesized from survey value +distributions and pattern frequencies. Direct markdown inputs still support +`--semantic` and `--temporal`. ```bash # Pairwise (N=2) -ghost-drift compare market.fingerprint.md dashboard.fingerprint.md +ghost-drift compare market/.ghost dashboard/.ghost # Qualitative diff of decisions + palette ghost-drift compare a.fingerprint.md b.fingerprint.md --semantic @@ -267,7 +239,7 @@ ghost-drift compare a.fingerprint.md b.fingerprint.md --semantic ghost-drift compare before.fingerprint.md after.fingerprint.md --temporal # Composite (N≥3) — pairwise matrix + centroid + clusters -ghost-drift compare *.fingerprint.md +ghost-drift compare */.ghost ``` ### Intent — `ack` / `track` / `diverge` @@ -278,7 +250,7 @@ and `diverge` need a tracked fingerprint declared in `ghost.config.ts`; Acknowledge current drift — record your intentional stance (aligned, accepted, or diverging). Reads the tracked fingerprint from `ghost.config.ts` -and the local `fingerprint.md`. +and the local direct fingerprint markdown. @@ -330,8 +302,8 @@ ghost-drift emit skill --out ~/.my-agent/skills/ghost-drift -A fleet is a directory containing many projects, each with `map.md` and -`fingerprint.md`. +A fleet is a directory containing many projects, each with a Ghost fingerprint +bundle. `ghost-fleet` reads the fleet, emits per-fleet outputs (`fleet.md` + `fleet.json`), and ships the agentskills.io bundle for the narrative recipe. @@ -372,12 +344,12 @@ once, then ask your agent in plain English: | Recipe | Bundle | Trigger | | ---------- | ------------------ | ------------------------------------------------------ | -| `scan` | `ghost-fingerprint` | "scan this project" / "go end-to-end" — meta-recipe orchestrating map → survey → profile | +| `scan` | `ghost-fingerprint` | "scan this project" / "go end-to-end" — meta-recipe orchestrating resources → map → survey → patterns | | `map` | `ghost-fingerprint` | "map this repo" / "write map.md" | | `survey` | `ghost-fingerprint` | "survey design values" / "extract tokens" | -| `profile` | `ghost-fingerprint` | "profile this design language" / "write fingerprint.md" | +| `patterns` | `ghost-fingerprint` | "write patterns.yml" / "codify composition patterns" | | `review` | `ghost-drift` | "review this PR for drift" | -| `verify` | `ghost-drift` | "verify generated UI against the fingerprint" | +| `verify` | `ghost-drift` | "verify generated UI against the bundle" | | `compare` | `ghost-drift` | "why did these two fingerprints drift?" | | `remediate`| `ghost-drift` | "fix this drift" | | `target` | `ghost-fleet` | "describe this fleet" | @@ -388,8 +360,8 @@ steps with its normal tools and calls the relevant CLI when it needs a reproducible answer. Each recipe also declares `handoffs` in its frontmatter — structured -next-step suggestions that point at another skill (e.g. `profile` → -`compare`) or a CLI invocation (e.g. `profile` → +next-step suggestions that point at another skill (e.g. `fingerprint` → +`compare`) or a CLI invocation (e.g. `fingerprint` → `ghost-fingerprint emit review-command`). Hosts that read the field can surface the next action inline; hosts that ignore unknown frontmatter see no change. diff --git a/apps/docs/src/content/docs/getting-started.mdx b/apps/docs/src/content/docs/getting-started.mdx index 9548d6b0..a8613215 100644 --- a/apps/docs/src/content/docs/getting-started.mdx +++ b/apps/docs/src/content/docs/getting-started.mdx @@ -1,6 +1,6 @@ --- title: Getting Started -description: Install Ghost, scan a repo, and review generated UI against fingerprint.md. +description: Install Ghost, scan a repo, and review generated UI against a root fingerprint bundle. kicker: Docs section: guide order: 10 @@ -9,97 +9,35 @@ slug: getting-started -Ghost keeps a project's design language in the repo, where your agent can use -it. The main file is `fingerprint.md`: a readable guide to how the UI should -look and feel. +Ghost keeps a project's design memory in the repo, where your agent can use it. +The fingerprint is the root `.ghost/` bundle: -The scan earns that file with evidence: - -``` -map.md -> survey.json -> fingerprint.md -where to look what exists what it means +```text +resources.yml -> map.md -> survey.json -> patterns.yml +what to read where UI lives what exists composition grammar ``` -Your host agent (Claude Code, Cursor, Goose, Codex, …) does the judgment work: -reading the repo, naming patterns, reviewing drift, and suggesting fixes. The -CLIs handle repeatable checks such as linting, profile verification, diffs, and -distance. +`checks.yml` is optional deterministic enforcement. `intent.md` is optional +human-authored or human-approved product intent. | Tool | Job | Verbs | | --- | --- | --- | -| `ghost-fingerprint` | Create and check `map.md`, `survey.json`, and `fingerprint.md`. | `inventory`, `scan-status`, `lint`, `verify-profile`, `describe`, `diff`, `survey `, `emit` | -| `ghost-drift` | Compare fingerprints, review UI drift, and record drift decisions. | `compare`, `ack`, `track`, `diverge`, `emit skill` | +| `ghost-fingerprint` | Create and check the root `.ghost/` bundle. | `inventory`, `scan-status`, `lint`, `verify`, `describe`, `diff`, `survey `, `emit` | +| `ghost-drift` | Check diffs, emit advisory review packets, compare bundles, and record drift decisions. | `check`, `review`, `compare`, `ack`, `track`, `diverge`, `emit skill` | | `ghost-fleet` | See how many project fingerprints relate. | `members`, `view`, `emit skill` | | `ghost-ui` | Reference design system Ghost uses to test itself. | — | -Install the CLI(s) you need, then emit the matching skill bundle into your -agent. - - - - - -Ghost has one main loop: - -```text -scan the repo -> write fingerprint.md -> review drift -> update intent -``` - -`ghost-fingerprint` owns the scan. It helps your agent find where UI lives, -record the design evidence, and turn that evidence into `fingerprint.md`. -`ghost-drift` uses that fingerprint when generated or changed UI needs review. -`ghost-fleet` is for the moment you have many project fingerprints and want to -see how they relate. - -That is the whole stance: agents do judgment, Ghost gives them a local file and -repeatable checks. `map.md` and `survey.json` support the scan, but -`fingerprint.md` is the thing the agent uses before and after it writes UI. - - - - - -`ghost-drift` and `ghost-fingerprint` are the published tools today. Add -either or both: - -```bash -pnpm add -D ghost-drift ghost-fingerprint -``` - -Or install globally: - -```bash -pnpm add -g ghost-drift ghost-fingerprint -``` - -`ghost-fleet` is workspace-only for now — clone the repo and `pnpm build` -to use it. - -No API key is required for any CLI verb. If your host agent uses Anthropic -or OpenAI models, it'll handle auth itself. Each CLI auto-loads `.env` and -`.env.local` from the working directory for a couple of optional variables -(`OPENAI_API_KEY` / `VOYAGE_API_KEY` for paraphrase-robust semantic -embeddings, `GITHUB_TOKEN` for fetching tracked fingerprints). - -Each tool ships its own recipe bundle: - ```bash -ghost-drift emit skill # → .claude/skills/ghost-drift -ghost-fingerprint emit skill # → .claude/skills/ghost-fingerprint -# ghost-fleet emit skill # → .claude/skills/ghost-fleet (workspace tool today) +ghost-drift emit skill +ghost-fingerprint emit skill +# ghost-fleet emit skill ``` -Each bundle gives your agent a few recipes: - -- **`ghost-fingerprint`** — scan a repo, write `map.md`, write `survey.json`, and turn that evidence into `fingerprint.md`. -- **`ghost-drift`** — `compare.md` (interpretation), `review.md` (PR review), `verify.md` (generation→review loop), `remediate.md` (suggest minimal fixes). -- **`ghost-fleet`** — `target.md` (synthesize fleet narrative from `view` output). - -Once installed, ask your agent in plain English: "profile this design language" +Once installed, ask your agent in plain English: "scan this project with Ghost" or "review this PR for drift". The recipe tells the agent what to read, what to write, and which CLI checks to run. @@ -107,188 +45,70 @@ write, and which CLI checks to run. -Scanning is a recipe your agent runs. Open your agent in the project you want -to profile and ask: - ```text Scan this project with Ghost ``` -The `scan` recipe checkpoints between stages with `scan-status` and -dispatches into the per-stage recipes: - -1. **Map (`map.md`)** — find where UI, tokens, components, examples, and - design-system files live. The recipe reads `ghost-fingerprint inventory` - and writes a short map. Validated by - `ghost-fingerprint lint map.md`. -2. **Survey (`survey.json`)** — record the concrete colors, spacing, type, - tokens, components, and surfaces found in source. Validated by - `ghost-fingerprint lint survey.json`. -3. **Fingerprint (`fingerprint.md`)** — turn the evidence into the - design-language file an agent can use. The recipe names important choices, - cites source evidence, and cannot invent values not found in the survey. - Validated by - `ghost-fingerprint lint fingerprint.md` and - `ghost-fingerprint verify-profile fingerprint.md survey.json --root .`. +The `scan` recipe checkpoints between stages: -If you only want one stage, ask for it directly: "map this repo", "survey -the design values", "profile this fingerprint from the survey". The recipes -chain via `handoffs`, so the agent surfaces the next stage when ready. - -A **fingerprint** is one Markdown file. YAML frontmatter stores compact values -such as palette, spacing, typography, surfaces, references, and checks. The -body stores prose: Character, Signature, and Decisions. Runtime comparison -derives its data from the file; there is no sibling embedding file to edit. -See the fingerprint fixture at -[`packages/ghost-fingerprint/test/fixtures/ghost-ui-fingerprint/fingerprint.md`](https://github.com/block/ghost/blob/main/packages/ghost-fingerprint/test/fixtures/ghost-ui-fingerprint/fingerprint.md) -for a full example. +1. **Resources (`resources.yml`)** — declare the primary target, design-system + references, canonical surfaces, screenshots, docs, resolvers, and path + boundaries. +2. **Map (`map.md`)** — find where UI, tokens, components, examples, and + design-system files live. +3. **Survey (`survey.json`)** — record values, tokens, components, implemented + surfaces, and factual composition observations. +4. **Patterns (`patterns.yml`)** — codify surface types and repeated + composition grammar with survey-backed evidence. ```bash -# Check what stage to run next +ghost-fingerprint init-package ghost-fingerprint scan-status - -# Validate any scan file (auto-detects the kind) -ghost-fingerprint lint # ./fingerprint.md -ghost-fingerprint lint map.md # ghost.map/v2 -ghost-fingerprint lint survey.json # ghost.survey/v2 -ghost-fingerprint verify-profile fingerprint.md survey.json --root . - # fingerprint-to-survey fidelity +ghost-fingerprint lint +ghost-fingerprint survey patterns .ghost/survey.json -o .ghost/patterns.yml +ghost-fingerprint verify .ghost --root . ``` -Once you have two fingerprints, `ghost-drift compare` returns distance and -per-dimension deltas: - ```bash -# Pairwise (N=2) — distance + per-dimension delta -ghost-drift compare market.fingerprint.md dashboard.fingerprint.md - -# Qualitative diff of decisions + palette -ghost-drift compare a.fingerprint.md b.fingerprint.md --semantic - -# Velocity + trajectory (reads .ghost/history.jsonl) -ghost-drift compare before.fingerprint.md after.fingerprint.md --temporal - -# Composite (N≥3) — pairwise matrix + centroid + clusters -ghost-drift compare *.fingerprint.md +ghost-drift compare market/.ghost dashboard/.ghost +ghost-drift compare */.ghost ``` -`compare` uses palette, spacing, typography, surfaces, and runtime decision -embeddings. Decisions can match semantically even when the wording differs. +Bundle comparison synthesizes comparable design signals from survey value +distributions and pattern frequencies. Direct fingerprint markdown files still +work for legacy semantic comparison: -For a structural prose-level diff (what decisions and palette roles -changed, ignoring vector distance), use `ghost-fingerprint diff` instead. +```bash +ghost-drift compare a.fingerprint.md b.fingerprint.md --semantic +``` -Reviewing PRs is a skill recipe shipped in `ghost-drift`'s bundle. Once -installed, open a PR branch in your host agent and ask: - ```text Review this PR for design drift ``` -The `review` recipe diffs changed files against the local `fingerprint.md` -and flags hardcoded colors off the palette, spacing off the scale, and type -choices that violate decisions. - -If you want a repeatable, per-project slash command for your agent, emit -one from the fingerprint itself: - -```bash -# Writes .claude/commands/design-review.md -ghost-fingerprint emit review-command -``` - -(`emit review-command` lives in `ghost-fingerprint` because it derives from -`fingerprint.md`. `ghost-drift` ships only `emit skill`.) +`ghost-drift check` applies active deterministic gates from `.ghost/checks.yml`. +`ghost-drift review` emits advisory context grounded in `.ghost/patterns.yml`, +`.ghost/survey.json`, optional `.ghost/intent.md`, checks, and the diff. - + -Drift without intent is noise; drift with intent is signal. Ghost tracks -your stance toward a tracked fingerprint in `.ghost-sync.json`: +Drift without intent is noise; drift with intent is signal. `ack`, `track`, and +`diverge` still operate on tracked direct fingerprint markdown files: ```bash -# Record an overall stance (reads ghost.config.ts for the tracked fingerprint) ghost-drift ack --stance aligned --reason "Initial baseline" - -# Shift the tracked fingerprint ghost-drift track new-tracked.fingerprint.md - -# Mark a dimension as intentionally different ghost-drift diverge typography --reason "Editorial product uses a different type scale" ``` -`ack` and `diverge` need a tracked fingerprint declared in `ghost.config.ts`. -`track` takes the tracked fingerprint as its argument. - - - - - -Ghost can sit inside an AI-generated UI pipeline. The fingerprint gives the -generator design-language context; the `review` recipe checks the output. - -1. `ghost-fingerprint emit context-bundle` — emit a context bundle from a - fingerprint (SKILL.md + fingerprint.md + prompt.md + tokens.css) that any - generator can consume. -2. Run any generator — your host agent, Cursor, v0, or an in-house tool — - with the bundle in context. -3. Use the `review` recipe to gate the output. Use the `verify` recipe to - run the loop across a prompt suite and aggregate per-dimension drift, - classifying each dimension as _tight_, _leaky_, or _uncaptured_. - -```bash -# Emit a context bundle — default output: ./ghost-context/ -ghost-fingerprint emit context-bundle - -# Single prompt.md for plain-text LLM context -ghost-fingerprint emit context-bundle --prompt-only - -# Custom output directory -ghost-fingerprint emit context-bundle --out dist/context -``` - - - - - -`ack` and `diverge` need a tracked fingerprint declared. Most other verbs -are zero-config. - -```ts -import { defineConfig } from "ghost-drift"; - -export default defineConfig({ - // Path to a local fingerprint.md, or a URL / npm package name. - // Commit that file and point `tracks` at it. - tracks: { type: "path", value: "./tracked.fingerprint.md" }, - - targets: [{ type: "path", value: "./packages/my-ui" }], - - rules: { - "hardcoded-color": "error", - "token-override": "warn", - "missing-token": "warn", - "structural-divergence": "error", - "missing-component": "warn", - }, - - // Optional: paraphrase-robust semantic embeddings - // embedding: { provider: "openai" }, -}); -``` - ---- - -Next: jump to the [CLI Reference](/docs/cli) for every verb across all three -tools. - diff --git a/apps/docs/src/generated/cli-manifest.json b/apps/docs/src/generated/cli-manifest.json index 714518f0..9a45228e 100644 --- a/apps/docs/src/generated/cli-manifest.json +++ b/apps/docs/src/generated/cli-manifest.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-05-07T00:33:39.453Z", + "generatedAt": "2026-05-10T06:12:15.163Z", "tools": [ { "tool": "ghost-drift", @@ -8,7 +8,7 @@ "tool": "ghost-drift", "name": "compare", "rawName": "compare [...fingerprints]", - "description": "Compare two or more fingerprints. N=2 returns a pairwise delta; N≥3 returns a composite fingerprint (pairwise matrix, centroid, spread, clusters).", + "description": "Compare two or more fingerprints or root .ghost bundles. N=2 returns a pairwise delta; N≥3 returns a composite fingerprint.", "options": [ { "rawName": "--semantic", @@ -152,7 +152,7 @@ "tool": "ghost-drift", "name": "check", "rawName": "check", - "description": "Run active ghost.checks/v1 gates from .ghost/fingerprint/checks.yml against a git diff.", + "description": "Run active ghost.checks/v1 gates from .ghost/checks.yml against a git diff.", "options": [ { "rawName": "--base ", @@ -173,7 +173,7 @@ { "rawName": "--package ", "name": "package", - "description": "Fingerprint package directory (default: .ghost/fingerprint)", + "description": "Fingerprint package directory (default: .ghost)", "default": null, "takesValue": true, "negated": false @@ -213,7 +213,7 @@ { "rawName": "--package ", "name": "package", - "description": "Fingerprint package directory (default: .ghost/fingerprint)", + "description": "Fingerprint package directory (default: .ghost)", "default": null, "takesValue": true, "negated": false @@ -267,7 +267,7 @@ "tool": "ghost-fingerprint", "name": "lint", "rawName": "lint [file]", - "description": "Validate a fingerprint package, profile.md, map.md, survey.json, or checks.yml — defaults to .ghost/fingerprint", + "description": "Validate a root fingerprint bundle, resources.yml, map.md, survey.json, patterns.yml, checks.yml, or markdown — defaults to .ghost", "options": [ { "rawName": "--format ", @@ -283,8 +283,16 @@ "tool": "ghost-fingerprint", "name": "init-package", "rawName": "init-package [dir]", - "description": "Create a .ghost/fingerprint package skeleton (map.md, survey.json, profile.md, checks.yml)", + "description": "Create a root .ghost fingerprint bundle skeleton (resources.yml, map.md, survey.json, patterns.yml, checks.yml)", "options": [ + { + "rawName": "--with-intent", + "name": "withIntent", + "description": "Also create optional intent.md for human-authored or human-approved intent", + "default": null, + "takesValue": false, + "negated": false + }, { "rawName": "--format ", "name": "format", @@ -297,14 +305,14 @@ }, { "tool": "ghost-fingerprint", - "name": "verify-profile", - "rawName": "verify-profile ", - "description": "Verify profile.md is faithful to its survey.json: palette values must be survey-backed", + "name": "verify", + "rawName": "verify [dir]", + "description": "Verify a root fingerprint bundle: resources are reachable, patterns are survey-backed, and checks reference known patterns.", "options": [ { "rawName": "--root ", "name": "root", - "description": "Optional target root used by profile fidelity checks that need repo context", + "description": "Optional target root used to resolve resources.yml local paths (default: cwd)", "default": null, "takesValue": true, "negated": false @@ -323,7 +331,7 @@ "tool": "ghost-fingerprint", "name": "scan-status", "rawName": "scan-status [dir]", - "description": "Report which fingerprint package stages have produced artifacts: map.md, survey.json, profile.md, checks.yml.", + "description": "Report which root fingerprint bundle stages have produced artifacts: resources.yml, map.md, survey.json, patterns.yml, and optional checks.yml/intent.md.", "options": [ { "rawName": "--include-scopes", @@ -353,8 +361,8 @@ { "tool": "ghost-fingerprint", "name": "describe", - "rawName": "describe [profile]", - "description": "Print a section map of profile.md (line ranges + token estimates) so agents can selectively load only the sections they need.", + "rawName": "describe [fingerprint]", + "description": "Print a section map of intent.md or a direct fingerprint markdown file (line ranges + token estimates).", "options": [ { "rawName": "--format ", @@ -370,7 +378,7 @@ "tool": "ghost-fingerprint", "name": "diff", "rawName": "diff ", - "description": "Structural diff between two profile.md files — what decisions, palette roles, and tokens changed (text-level, NOT embedding distance; for that, use `ghost-drift compare`).", + "description": "Structural diff between two fingerprint.md files — what decisions, palette roles, and tokens changed (text-level, NOT embedding distance; for that, use `ghost-drift compare`).", "options": [ { "rawName": "--format ", @@ -399,8 +407,8 @@ { "rawName": "--format ", "name": "format", - "description": "survey summarize/catalog output format: markdown or json", - "default": "markdown", + "description": "Output format: summarize/catalog use markdown or json; patterns use yaml, json, or markdown", + "default": null, "takesValue": true, "negated": false }, @@ -426,12 +434,12 @@ "tool": "ghost-fingerprint", "name": "emit", "rawName": "emit ", - "description": "Emit a derived artifact from the fingerprint package profile (kinds: review-command, context-bundle, skill)", + "description": "Emit a derived artifact from the fingerprint package (kinds: review-command, context-bundle, skill)", "options": [ { - "rawName": "-p, --profile ", - "name": "profile", - "description": "Source profile file (default: .ghost/fingerprint/profile.md)", + "rawName": "-f, --fingerprint ", + "name": "fingerprint", + "description": "Source direct fingerprint markdown file (default: .ghost/fingerprint.md)", "default": null, "takesValue": true, "negated": false @@ -471,7 +479,7 @@ { "rawName": "--prompt-only", "name": "promptOnly", - "description": "Emit only prompt.md — skips SKILL.md / fingerprint.md / tokens.css (context-bundle)", + "description": "Emit only prompt.md (skips SKILL.md / fingerprint.md / tokens.css) (context-bundle)", "default": null, "takesValue": false, "negated": false diff --git a/docs/fingerprint-format.md b/docs/fingerprint-format.md index 2fcaab0c..d68a6ede 100644 --- a/docs/fingerprint-format.md +++ b/docs/fingerprint-format.md @@ -1,165 +1,134 @@ -# The Fingerprint Package Format +# The Root Fingerprint Bundle Format -A Ghost fingerprint is a repo-local design memory package, not one Markdown -file. The canonical on-disk shape is: +A Ghost fingerprint is a repo-local identity bundle rooted at `.ghost/`. It is +not a single prose file. The canonical on-disk shape is: ```text -.ghost/fingerprint/ +.ghost/ + resources.yml map.md survey.json - profile.md + patterns.yml checks.yml + intent.md # optional ``` -Checks fail builds. Profile shapes judgment. Survey grounds both. The package is -the fingerprint. +`survey.json` is the evidence ledger. `patterns.yml` is the operational +composition grammar. `checks.yml` is the deterministic gate layer. `intent.md` +is optional human-authored or human-approved intent. ## Package Artifacts -### `map.md` - -`map.md` is the routing layer. It tells Ghost where UI surfaces live, which -scopes own which paths, and how changed files should find relevant evidence and -checks. It uses `ghost.map/v2` frontmatter plus a short human-readable topology -body. - -Validate it directly or as part of the package: - -```bash -ghost-fingerprint lint .ghost/fingerprint/map.md -ghost-fingerprint lint .ghost/fingerprint -``` - -### `survey.json` - -`survey.json` is factual observed evidence. It records concrete values, tokens, -components, and implemented UI surfaces with counts and examples. Agents should -use it to ground claims rather than inventing design rules from prose alone. - -Useful derived views: - -```bash -ghost-fingerprint survey summarize .ghost/fingerprint/survey.json -ghost-fingerprint survey catalog .ghost/fingerprint/survey.json -ghost-fingerprint survey patterns .ghost/fingerprint/survey.json -``` - -### `profile.md` - -`profile.md` is the non-enforcing design-language prior. It is Markdown with -YAML frontmatter for compact value digests plus prose for judgment-heavy design -guidance. +### `resources.yml` -Required frontmatter remains focused on design-language evidence: +`resources.yml` tells Ghost what references define the product: the primary +target, design-system references, canonical surfaces, screenshots, docs, +resolver/upstream sources, and include/exclude boundaries. ```yaml ---- +schema: ghost.resources/v1 id: cash-ios -source: llm -timestamp: 2026-05-06T00:00:00Z -references: - specs: [Code/DesignSystem] - examples: [Code/Features/Lending] -observation: - personality: [restrained, utilitarian] - resembles: [cash-app] -decisions: - - dimension: color-strategy - - dimension: spatial-system -palette: - dominant: [] - neutrals: { steps: [], count: 0 } - semantic: [] - saturationProfile: muted - contrast: moderate -spacing: { scale: [4, 8, 12, 16], baseUnit: 4, regularity: 1 } -typography: - families: [] - sizeRamp: [] - weightDistribution: {} - lineHeightPattern: normal +primary: + target: . + paths: [Code] +design_system: + - id: arcade + target: ../arcade-ios-package + paths: [Sources] surfaces: - borderRadii: [] - shadowComplexity: deliberate-none - borderUsage: minimal ---- + - id: lending + locator: Code/Features/Lending + paths: [Code/Features/Lending] +include: ["Code/**"] +exclude: ["**/Tests/**"] ``` -The body stores `# Character`, `# Signature`, and `# Decisions` with -`### ` blocks and evidence bullets. Write the body so it can shape -generation and advisory review. Do not put enforceable gates in `profile.md`. -`checks[]` is not part of profile frontmatter. +### `map.md` -Validate fidelity: +`map.md` is the topology layer. It tells Ghost where UI surfaces live, which +scopes own which paths, and how changed files should route to evidence and +checks. It still uses `ghost.map/v2` frontmatter plus a short topology body. -```bash -ghost-fingerprint verify-profile .ghost/fingerprint/profile.md .ghost/fingerprint/survey.json --root . -``` +### `survey.json` -`verify-profile` checks that profile palette, spacing, typography, radii, and -shadow posture are backed by survey evidence. It does not calibrate gates. +`survey.json` is factual observed evidence. It records values, tokens, +components, implemented UI surfaces, and factual per-surface composition +observations. It should not assign design meaning or declare intent. + +Surface rows may include: + +```json +{ + "composition": { + "anatomy": ["shell", "compact-header", "filter-row", "table"], + "primary_region": "table", + "action_placement": ["row", "selection-toolbar"], + "navigation_context": "persistent-shell", + "responsive_behavior": ["mobile filters collapse above table"], + "confidence": 0.82 + } +} +``` -### `checks.yml` +### `patterns.yml` -`checks.yml` is the only blocking layer in v1. It uses `ghost.checks/v1` and -contains human-promoted deterministic gates. +`patterns.yml` is the operational composition grammar derived from survey +evidence and curated by the agent/human loop. It names surface types and +composition patterns so generation and review have stable handles. ```yaml -schema: ghost.checks/v1 +schema: ghost.patterns/v1 id: cash-ios -checks: - - id: no-hardcoded-ui-color - title: Use design tokens for UI color - status: active - severity: serious - applies_to: - scopes: [lending] - paths: [Code/Features/Lending] - detector: - type: forbidden-regex - pattern: '#[0-9a-fA-F]{3,8}|UIColor\\(' - contexts: [swift] +surface_types: + - id: resource-index + preferred_patterns: [dense-resource-index] + evidence: + - surface_id: customers-index +composition_patterns: + - id: dense-resource-index + surface_types: [resource-index] + frequency: 7 + confidence: 0.88 + anatomy: + ordered: [shell, compact-header, filter-row, table] + required: [filter-row, table] + forbidden: [oversized-hero] + traits: + density: [compressed] + dominant_components: [Table, SearchInput] evidence: - support: 0.94 - observed_count: 47 - examples: - - Code/Features/Lending/LendingUI - repair: Replace literals with Arcade/Cash semantic tokens. + - surface_id: customers-index + locator: /customers + advisory: + - Resource-index surfaces should preserve the work surface before explanation. ``` -Supported detector types: - -- `forbidden-regex` -- `required-regex` -- `banned-import` -- `banned-component` -- `required-token` +### `checks.yml` -Statuses: +`checks.yml` remains deterministic-only in this version. It uses +`ghost.checks/v1`; checks may reference `surface_types` and `pattern_ids` as +metadata, but composition policy is advisory until a later detector layer +exists. -- `active`: enforced by `ghost-drift check` -- `proposed`: linted as a candidate, not enforced -- `disabled`: retained as historical context, not enforced +### `intent.md` -## Drift Gates +`intent.md` is optional. When present, it should contain human-authored or +human-approved product intent: tradeoffs to preserve, misleading observations, +and what the product refuses to become. AI may draft it, but it is not +authoritative until accepted by a human. -Run deterministic gates against a diff: +## Commands ```bash +ghost-fingerprint init-package --with-intent +ghost-fingerprint lint +ghost-fingerprint survey patterns .ghost/survey.json -o .ghost/patterns.yml +ghost-fingerprint verify --root . ghost-drift check --base main -ghost-drift check --diff change.patch --format json -``` - -`ghost-drift check` reads package `map.md`, `survey.json`, and `checks.yml`, -routes changed files through map scopes, applies matching active checks, emits -stable JSON or Markdown, and exits nonzero on active check failures. - -Advisory review remains separate: - -```bash ghost-drift review --base main +ghost-drift compare .ghost ../other/.ghost ``` -Advisory findings are non-blocking unless tied to an active deterministic check. -Every finding should cite a diff location, profile section, survey evidence, -precedent/example, and repair. +`ghost-fingerprint verify` validates cross-artifact fidelity: resources should +resolve, composition patterns must cite survey-backed evidence, and checks must +reference known pattern IDs when they use pattern metadata. diff --git a/docs/generation-loop.md b/docs/generation-loop.md index 6e89d274..f937ddf0 100644 --- a/docs/generation-loop.md +++ b/docs/generation-loop.md @@ -1,144 +1,124 @@ # Generation Loop -Ghost gives UI generators a local design-language input and a review loop. -`fingerprint.md` is the file the generator reads. The *review* recipe checks -the result for drift. The *verify* recipe repeats that loop across a prompt -suite to show where the fingerprint needs more detail. - -Only the bundle step is a deterministic CLI verb (`ghost-fingerprint -emit context-bundle`). *Review*, *verify*, and *remediate* are skill recipes -the host agent follows — installed with `ghost-drift emit skill`. - -Use any generator — the host agent itself, Cursor, v0, or an in-house tool — -with the emitted context bundle in its prompt. Ghost prepares the input and -checks the output; it does not run the generator. - -## Pipeline shape - +Ghost gives UI generators a local design-language input and a review loop. The +canonical input is the root `.ghost/` fingerprint bundle: + +```text +.ghost/ + resources.yml + map.md + survey.json + patterns.yml + checks.yml + intent.md ``` -fingerprint.md ──► [ghost-fingerprint emit context-bundle] ──► SKILL.md / fingerprint.md / prompt.md / tokens.css - │ - ▼ - any generator - (host agent, Cursor, v0, - in-house tool) - │ - ▼ HTML / JSX - [review recipe — ghost-drift] ──► drift disposition - (block / annotate - / ack / track) -``` - -## Pieces -### `ghost-fingerprint emit context-bundle [flags]` — the one CLI verb - -Emit a context bundle any generator can consume. Default output writes -`SKILL.md` + `fingerprint.md` + `prompt.md` + `tokens.css` into -`./ghost-context/`. The generated `prompt.md` turns the fingerprint into a -short generation prompt: Character sets feel, Signature describes the final -picture, Local References point to optional source material, Decisions give -style direction, Checks name review gates, and Tokens provide portable values. -It does not ask the generator to explain or cite decisions unless the user asks. - -Flags: -- `--out ` — output directory (default: `./ghost-context`) -- `--prompt-only` — single `prompt.md` only; skips `SKILL.md` / `fingerprint.md` / `tokens.css` -- `--no-tokens` — skip `tokens.css` -- `--readme` — include `README.md` -- `--name ` — override the skill name (default: fingerprint id) - -Point a Claude Code or MCP client at the output directory and the agent -reads `SKILL.md`. - -### Driving the generator +`patterns.yml` is the operational composition grammar, `survey.json` is the +evidence ledger, `resources.yml` says what the scan is grounded in, `checks.yml` +contains deterministic gates, and `intent.md` is optional human authority. The +generator can work without `intent.md`; when it is present, treat it as +human-authored or human-approved context. + +## Pipeline Shape + +```text +.ghost/resources.yml +.ghost/map.md +.ghost/survey.json +.ghost/patterns.yml +.ghost/checks.yml +.ghost/intent.md + | + v +any generator +(host agent, Cursor, v0, in-house tool) + | + v +HTML / JSX / app code + | + v +ghost-drift review + ghost-drift check + | + v +advisory composition findings + deterministic check results +``` -The host agent drives this step. It loads the fingerprint (often just the -sections it needs via `ghost-fingerprint describe`), builds a system -prompt from Character + Signature + Local References when accessible + -Decisions + Checks + Tokens, asks the underlying model, -extracts the artifact (HTML/JSX/etc.), and hands it to the `review` -recipe for self-check. Retries with drift feedback until it passes or the -agent gives up. +Ghost prepares the input and checks the output; it does not own the generator. +Use any generator that can read local context. -This isn't a recipe Ghost ships — `generate.md` was dropped. The agent's -own driver code (or whatever generator it shells out to) owns this step. -Ghost's job is the bundle that goes in and the review that checks the output. +## Pieces -### The `review` recipe +### `.ghost/patterns.yml` -The agent diffs generated output against the fingerprint. Flags hardcoded -colors outside the palette, spacing off the scale, and type choices that -violate decisions. For pre-baked, per-project review commands use -`ghost-fingerprint emit review-command` (which writes a slash command at -`.claude/commands/design-review.md`). +The generator should read this before composing UI. It contains surface types, +composition pattern IDs, repeated anatomy, variants, traits, confidence, and +evidence links back into `survey.json`. -Source: `packages/ghost-drift/src/skill-bundle/references/review.md`. +Patterns are advisory in this version. They affect review packets and repair +guidance, not deterministic blocking gates. -### The `verify` recipe +### `.ghost/survey.json` -Runs the generate→review loop over a versioned prompt suite. Aggregates -drift per dimension and classifies: +The survey is a lean evidence ledger. It records factual observations: +implemented values, tokens, components, UI surfaces, and optional composition +facts such as ordered anatomy, primary region, action placement, navigation +context, responsive behavior, and confidence. -- **tight** (mean < 1): fingerprint reproduces faithfully -- **leaky** (1–3): generator drifts here often — tighten Decisions -- **uncaptured** (≥ 3): fingerprint likely under-specifies this dimension +Interpretation belongs in `patterns.yml` or human-approved `intent.md`, not in +survey prose. -A useful test: run `verify` on a mature fingerprint, remove one section -(for example, motion), then run it again. Drift should rise in the dimension -that lost guidance. +### `.ghost/checks.yml` -Source: `packages/ghost-drift/src/skill-bundle/references/verify.md`. +Checks are deterministic gates: color allowlists, radius floors, banned +classes, required attributes, and similar rules that can be evaluated without +AI judgment. Checks may carry `surface_types` or `pattern_ids` as metadata so a +reviewer knows which composition scope they belong to, but composition +detectors are a later feature. -### The `remediate` recipe +### `.ghost/intent.md` -Once `review` flags drift, `remediate` walks the agent through the smallest -correction that lands the output back inside the fingerprint. The output is -either a fix proposal (the agent applies it) or — when the drift turns out -to be intentional — a recommendation to record stance with `ghost-drift ack` -or `ghost-drift diverge` instead of correcting the code. +Intent is optional. It is where humans can name product purpose, audience, +voice, or strategic constraints that cannot be proven from code. Agents may +summarize it, but tooling should not require it and should not pretend generated +intent is the user's voice unless a human approves it. -Source: `packages/ghost-drift/src/skill-bundle/references/remediate.md`. +## Review Loop -## The standard prompt suite +`ghost-drift review` reads `.ghost/patterns.yml`, `.ghost/survey.json`, +optional `.ghost/intent.md`, and optional `.ghost/checks.yml`. Advisory +findings should cite both pattern evidence and survey evidence. -A versioned set of UI-construction tasks, each tagged with the fingerprint -dimensions it stresses. Tagging prompts with dimensions lets the agent -distinguish *targeted* drift (a pricing-page prompt leaking spacing) from -*incidental* drift (the same prompt leaking color, which it wasn't -supposed to stress). +`ghost-drift check` reads `.ghost/checks.yml` and remains deterministic. It is +the blocking side of the loop. -## Why Each Section Exists +When review flags drift, the host agent applies the smallest correction that +brings the output back toward the observed composition grammar. If the drift is +intentional, record a stance with `ghost-drift ack`, `ghost-drift track`, or +`ghost-drift diverge` as appropriate. -Each layer has a concrete job somewhere in the loop: +## Verification -| Layer | Role in the loop | -|---|---| -| **Character** | Prompt context — shapes feel | -| **Signature** | Final-picture guidance — dominant moves and output shape | -| **References** | Local provenance / optional source material; use when accessible | -| **Checks** | Human-promoted drift gates; presence-floor checks codify important absences | -| **Decisions** | Pattern guidance the generator consults for specific choices | +`ghost-fingerprint verify [dir] --root ` checks cross-artifact fidelity: -Fingerprint filter: include a fact in `fingerprint.md` only when it can change -generated UI or a drift verdict. `survey.json` can stay broad as evidence — -including implemented `ui_surfaces[]` specimens and their observed composition -signals — while `fingerprint.md` stays compact. +- pattern evidence exists in `survey.json` +- resource paths are reachable from the supplied root when local +- checks reference known surface types and pattern IDs -If a section does not affect generation or review, the format is probably too -large. The `verify` recipe is how you notice that. +The skill-level verify recipe can still run a generate -> review loop over a +prompt suite, but the deterministic package verifier is the first gate for the +bundle itself. -## Integration patterns +## Integration Patterns -**CI**: a per-project `design-review` slash command emitted from -`ghost-fingerprint emit review-command`, invoked by the host agent as a -required check on PRs that touch UI files. +**In a generation pipeline:** load the root `.ghost/` bundle into the host +agent, generate the requested UI, then run `ghost-drift review` and +`ghost-drift check`. -**In a generation pipeline**: `ghost-fingerprint emit context-bundle` writes -the skill bundle into the generator's context; the generator produces; the -`review` recipe checks the output. Drift disposition belongs to the pipeline -owner (block, annotate, require `ghost-drift ack`). +**In CI:** run deterministic checks for UI-touching changes and attach advisory +review packets when generated or changed UI appears to drift from +`patterns.yml`. -**Fingerprint maintenance**: run `verify` periodically. When a dimension -shows up consistently leaky, the fingerprint needs more Decisions for -that dimension. +**Fingerprint maintenance:** scan in order: +`resources -> map -> survey -> patterns`. Keep `survey.json` factual, promote +repeated composition observations into `patterns.yml`, and add `intent.md` only +when a human has supplied or approved the intent. diff --git a/install/install.sh b/install/install.sh index 0e58e30f..7438b244 100755 --- a/install/install.sh +++ b/install/install.sh @@ -14,7 +14,7 @@ # What gets installed: # /ghost/ # SKILL.md -# references/scan.md, map.md, survey.md, profile.md, schema.md +# references/scan.md, map.md, survey.md, fingerprint.md, schema.md # assets/fingerprint.template.md # # Exit codes: diff --git a/install/manifest.json b/install/manifest.json index 7c5a8087..32a35756 100644 --- a/install/manifest.json +++ b/install/manifest.json @@ -11,7 +11,7 @@ "references/scan.md", "references/map.md", "references/survey.md", - "references/profile.md", + "references/fingerprint.md", "references/schema.md", "assets/fingerprint.template.md" ] diff --git a/map.md b/map.md index 32838607..5f4370cd 100644 --- a/map.md +++ b/map.md @@ -98,7 +98,7 @@ Ghost is a TypeScript pnpm monorepo that helps agents detect and manage visual-language drift in the design systems they generate against. The canonical artifact is `fingerprint.md` — a human-readable, LLM-editable Markdown file with a YAML machine layer plus a three-section prose body. -Ghost is BYOA: judgement work (profile, review, verify, generate, discover) +Ghost is BYOA: judgement work (fingerprint, review, verify, generate, discover) lives in skill recipes the host agent executes; the CLIs are the calculator the agent reaches for when it needs a reproducible answer. diff --git a/packages/ghost-core/src/checks/schema.ts b/packages/ghost-core/src/checks/schema.ts index 1386f381..14387819 100644 --- a/packages/ghost-core/src/checks/schema.ts +++ b/packages/ghost-core/src/checks/schema.ts @@ -9,6 +9,7 @@ const GhostCheckAppliesToSchema = z scopes: z.array(z.string().min(1)).optional(), paths: z.array(z.string().min(1)).optional(), surface_types: z.array(z.string().min(1)).optional(), + pattern_ids: z.array(z.string().min(1)).optional(), }) .strict(); diff --git a/packages/ghost-core/src/checks/types.ts b/packages/ghost-core/src/checks/types.ts index e399257c..417340de 100644 --- a/packages/ghost-core/src/checks/types.ts +++ b/packages/ghost-core/src/checks/types.ts @@ -17,6 +17,7 @@ export interface GhostCheckAppliesTo { scopes?: string[]; paths?: string[]; surface_types?: string[]; + pattern_ids?: string[]; } export interface GhostCheckDetector { diff --git a/packages/ghost-core/src/decision-vocabulary.ts b/packages/ghost-core/src/decision-vocabulary.ts index 0175a964..d1e9629a 100644 --- a/packages/ghost-core/src/decision-vocabulary.ts +++ b/packages/ghost-core/src/decision-vocabulary.ts @@ -7,7 +7,7 @@ * and N-way overlap on incidentally-shared labels is not a basis for * cross-system distance. * - * The fix is a small controlled vocabulary. Profilers pick from this list + * The fix is a small controlled vocabulary. Fingerprint authors pick from this list * first; non-canonical slugs are still permitted (the schema allows any * string), but the recommended pattern is to pair them with a * `dimension_kind` that maps to a canonical slug. Lint warns when a @@ -15,7 +15,7 @@ * group by `dimension_kind` (or by `dimension` when it's already * canonical) so the decision-overlap distance axis becomes meaningful. * - * The list below started from the actual decisions produced by profiling + * The list below started from the actual decisions produced by fingerprinting * ghost-ui, then absorbed dogfood learnings where generated UI needed a * first-class place for task-shaped composition rather than treating every * answer as a generic card stack. @@ -214,7 +214,7 @@ export function isCanonicalDimension( * 3. Token-affinity scoring across `TOKEN_HINTS` — wins when a single * dimension scores strictly higher than all others. * 4. `null` when there's no clear winner. Callers should treat null as - * "this slug is genuinely novel; lint warns and the profile keeps it + * "this slug is genuinely novel; lint warns and the fingerprint keeps it * long-tail." * * Pure / deterministic. No I/O. diff --git a/packages/ghost-core/src/embedding/embed-api.ts b/packages/ghost-core/src/embedding/embed-api.ts index 79ade3f2..22476895 100644 --- a/packages/ghost-core/src/embedding/embed-api.ts +++ b/packages/ghost-core/src/embedding/embed-api.ts @@ -26,7 +26,7 @@ export async function computeSemanticEmbedding( * Embed a batch of texts in one API call. * * Returns one vector per input in the same order. Used to embed design - * decisions at profile time so compare can match them by cosine similarity + * decisions at fingerprint authoring time so compare can match them by cosine similarity * without making API calls during comparison. */ export async function embedTexts( diff --git a/packages/ghost-core/src/fingerprint-package.ts b/packages/ghost-core/src/fingerprint-package.ts index 5a36ca94..7fadb59e 100644 --- a/packages/ghost-core/src/fingerprint-package.ts +++ b/packages/ghost-core/src/fingerprint-package.ts @@ -1,10 +1,17 @@ -export const FINGERPRINT_PACKAGE_DIR = ".ghost/fingerprint" as const; -export const PROFILE_FILENAME = "profile.md" as const; +export const FINGERPRINT_PACKAGE_DIR = ".ghost" as const; +export const RESOURCES_FILENAME = "resources.yml" as const; +export const PATTERNS_FILENAME = "patterns.yml" as const; +export const INTENT_FILENAME = "intent.md" as const; +export const FINGERPRINT_FILENAME = "fingerprint.md" as const; export interface FingerprintPackagePaths { dir: string; + resources: string; map: string; survey: string; - profile: string; + patterns: string; + /** Legacy direct markdown path; not part of the canonical root bundle. */ + fingerprint: string; checks: string; + intent: string; } diff --git a/packages/ghost-core/src/index.ts b/packages/ghost-core/src/index.ts index af985c54..4546f57e 100644 --- a/packages/ghost-core/src/index.ts +++ b/packages/ghost-core/src/index.ts @@ -55,9 +55,12 @@ export { } from "./embedding/index.js"; // --- Map (ghost.map/v2) --- export { + FINGERPRINT_FILENAME, FINGERPRINT_PACKAGE_DIR, type FingerprintPackagePaths, - PROFILE_FILENAME, + INTENT_FILENAME, + PATTERNS_FILENAME, + RESOURCES_FILENAME, } from "./fingerprint-package.js"; // --- Map (ghost.map/v2) --- export { @@ -76,6 +79,27 @@ export { slugifyScopeId, type TopLevelEntry, } from "./map/index.js"; +// --- Patterns (ghost.patterns/v1) --- +export type { + GhostCompositionAnatomy, + GhostCompositionPattern, + GhostPatternEvidence, + GhostPatternsDocument, + GhostPatternsLintIssue, + GhostPatternsLintReport, + GhostPatternsLintSeverity, + GhostSurfaceTypePattern, +} from "./patterns/index.js"; +export { + GHOST_PATTERNS_FILENAME, + GHOST_PATTERNS_SCHEMA, + GhostCompositionAnatomySchema, + GhostCompositionPatternSchema, + GhostPatternEvidenceSchema, + GhostPatternsSchema, + GhostSurfaceTypePatternSchema, + lintGhostPatterns, +} from "./patterns/index.js"; // --- Perceptual prior (drift severity calibration) --- export { computeCheckSeverity, @@ -90,6 +114,23 @@ export { TIER_SEVERITY, tierForCanonical, } from "./perceptual-prior.js"; +// --- Resources (ghost.resources/v1) --- +export type { + GhostResourceRef, + GhostResourcesDocument, + GhostResourcesLintIssue, + GhostResourcesLintReport, + GhostResourcesLintSeverity, + GhostSurfaceResource, +} from "./resources/index.js"; +export { + GHOST_RESOURCES_FILENAME, + GHOST_RESOURCES_SCHEMA, + GhostResourceRefSchema, + GhostResourcesSchema, + GhostSurfaceResourceSchema, + lintGhostResources, +} from "./resources/index.js"; // --- Skill bundle loader --- export type { SkillBundleFile } from "./skill-bundle-loader.js"; export { loadSkillBundle } from "./skill-bundle-loader.js"; @@ -151,6 +192,8 @@ export { tokenRowId, type UiSurfaceClassification, UiSurfaceClassificationSchema, + type UiSurfaceComposition, + UiSurfaceCompositionSchema, type UiSurfaceDensity, type UiSurfaceEvidenceSummary, type UiSurfaceGroupSummary, diff --git a/packages/ghost-core/src/patterns/index.ts b/packages/ghost-core/src/patterns/index.ts new file mode 100644 index 00000000..a12a2d37 --- /dev/null +++ b/packages/ghost-core/src/patterns/index.ts @@ -0,0 +1,22 @@ +export { lintGhostPatterns } from "./lint.js"; +export { + GhostCompositionAnatomySchema, + GhostCompositionPatternSchema, + GhostPatternEvidenceSchema, + GhostPatternsSchema, + GhostSurfaceTypePatternSchema, +} from "./schema.js"; +export type { + GhostCompositionAnatomy, + GhostCompositionPattern, + GhostPatternEvidence, + GhostPatternsDocument, + GhostPatternsLintIssue, + GhostPatternsLintReport, + GhostPatternsLintSeverity, + GhostSurfaceTypePattern, +} from "./types.js"; +export { + GHOST_PATTERNS_FILENAME, + GHOST_PATTERNS_SCHEMA, +} from "./types.js"; diff --git a/packages/ghost-core/src/patterns/lint.ts b/packages/ghost-core/src/patterns/lint.ts new file mode 100644 index 00000000..b2090e56 --- /dev/null +++ b/packages/ghost-core/src/patterns/lint.ts @@ -0,0 +1,140 @@ +import type { ZodIssue } from "zod"; +import { GhostPatternsSchema } from "./schema.js"; +import type { + GhostPatternsDocument, + GhostPatternsLintIssue, + GhostPatternsLintReport, +} from "./types.js"; + +export function lintGhostPatterns(input: unknown): GhostPatternsLintReport { + const issues: GhostPatternsLintIssue[] = []; + const result = GhostPatternsSchema.safeParse(input); + if (!result.success) { + issues.push(...zodIssues(result.error.issues)); + return finalize(issues); + } + + const doc = result.data as GhostPatternsDocument; + checkDuplicateIds(doc, issues); + checkReferences(doc, issues); + doc.composition_patterns.forEach((pattern, index) => { + if (!pattern.evidence?.length) { + issues.push({ + severity: "warning", + rule: "pattern-evidence-missing", + message: + "composition patterns should cite survey-backed evidence before they guide review.", + path: `composition_patterns[${index}].evidence`, + }); + } + }); + + return finalize(issues); +} + +function checkDuplicateIds( + doc: GhostPatternsDocument, + issues: GhostPatternsLintIssue[], +): void { + const seenSurfaceTypes = new Map(); + doc.surface_types.forEach((surfaceType, index) => { + const previous = seenSurfaceTypes.get(surfaceType.id); + if (previous !== undefined) { + issues.push({ + severity: "error", + rule: "surface-type-id-duplicate", + message: `surface type id '${surfaceType.id}' is duplicated (also at surface_types[${previous}])`, + path: `surface_types[${index}].id`, + }); + } else { + seenSurfaceTypes.set(surfaceType.id, index); + } + }); + + const seenPatterns = new Map(); + doc.composition_patterns.forEach((pattern, index) => { + const previous = seenPatterns.get(pattern.id); + if (previous !== undefined) { + issues.push({ + severity: "error", + rule: "composition-pattern-id-duplicate", + message: `composition pattern id '${pattern.id}' is duplicated (also at composition_patterns[${previous}])`, + path: `composition_patterns[${index}].id`, + }); + } else { + seenPatterns.set(pattern.id, index); + } + }); +} + +function checkReferences( + doc: GhostPatternsDocument, + issues: GhostPatternsLintIssue[], +): void { + const surfaceTypeIds = new Set( + doc.surface_types.map((surfaceType) => surfaceType.id), + ); + const patternIds = new Set( + doc.composition_patterns.map((pattern) => pattern.id), + ); + + doc.surface_types.forEach((surfaceType, index) => { + surfaceType.preferred_patterns?.forEach((patternId, patternIndex) => { + if (patternIds.has(patternId)) return; + issues.push({ + severity: "error", + rule: "surface-type-pattern-unknown", + message: `surface type '${surfaceType.id}' references unknown preferred pattern '${patternId}'.`, + path: `surface_types[${index}].preferred_patterns[${patternIndex}]`, + }); + }); + surfaceType.discouraged_patterns?.forEach((patternId, patternIndex) => { + if (patternIds.has(patternId)) return; + issues.push({ + severity: "error", + rule: "surface-type-pattern-unknown", + message: `surface type '${surfaceType.id}' references unknown discouraged pattern '${patternId}'.`, + path: `surface_types[${index}].discouraged_patterns[${patternIndex}]`, + }); + }); + }); + + doc.composition_patterns.forEach((pattern, index) => { + pattern.surface_types?.forEach((surfaceTypeId, surfaceTypeIndex) => { + if (surfaceTypeIds.has(surfaceTypeId)) return; + issues.push({ + severity: "error", + rule: "composition-pattern-surface-type-unknown", + message: `composition pattern '${pattern.id}' references unknown surface type '${surfaceTypeId}'.`, + path: `composition_patterns[${index}].surface_types[${surfaceTypeIndex}]`, + }); + }); + }); +} + +function zodIssues(issues: ZodIssue[]): GhostPatternsLintIssue[] { + return issues.map((issue) => ({ + severity: "error" as const, + rule: `schema/${issue.code}`, + message: issue.message, + path: formatZodPath(issue.path), + })); +} + +function formatZodPath(path: ZodIssue["path"]): string | undefined { + if (path.length === 0) return undefined; + return path.reduce((formatted, segment) => { + if (typeof segment === "number") return `${formatted}[${segment}]`; + const key = String(segment); + return formatted ? `${formatted}.${key}` : key; + }, ""); +} + +function finalize(issues: GhostPatternsLintIssue[]): GhostPatternsLintReport { + return { + issues, + errors: issues.filter((issue) => issue.severity === "error").length, + warnings: issues.filter((issue) => issue.severity === "warning").length, + info: issues.filter((issue) => issue.severity === "info").length, + }; +} diff --git a/packages/ghost-core/src/patterns/schema.ts b/packages/ghost-core/src/patterns/schema.ts new file mode 100644 index 00000000..14926b7a --- /dev/null +++ b/packages/ghost-core/src/patterns/schema.ts @@ -0,0 +1,74 @@ +import { z } from "zod"; +import { GHOST_PATTERNS_SCHEMA } from "./types.js"; + +const SlugIdSchema = z + .string() + .min(1) + .regex(/^[a-z0-9][a-z0-9._-]*$/, { + message: + "id must be a slug (lowercase alphanumeric plus . _ -, leading alphanumeric)", + }); + +export const GhostPatternEvidenceSchema = z + .object({ + surface_id: z.string().min(1).optional(), + path: z.string().min(1).optional(), + locator: z.string().min(1).optional(), + note: z.string().min(1).optional(), + }) + .strict(); + +export const GhostSurfaceTypePatternSchema = z + .object({ + id: SlugIdSchema, + title: z.string().min(1).optional(), + description: z.string().min(1).optional(), + signals: z.array(z.string().min(1)).optional(), + preferred_patterns: z.array(SlugIdSchema).optional(), + discouraged_patterns: z.array(SlugIdSchema).optional(), + evidence: z.array(GhostPatternEvidenceSchema).optional(), + }) + .strict(); + +export const GhostCompositionAnatomySchema = z + .object({ + ordered: z.array(z.string().min(1)).optional(), + required: z.array(z.string().min(1)).optional(), + optional: z.array(z.string().min(1)).optional(), + forbidden: z.array(z.string().min(1)).optional(), + }) + .strict(); + +export const GhostCompositionPatternSchema = z + .object({ + id: SlugIdSchema, + title: z.string().min(1).optional(), + intent: z.string().min(1).optional(), + surface_types: z.array(SlugIdSchema).optional(), + frequency: z.number().int().nonnegative().optional(), + confidence: z.number().min(0).max(1).optional(), + anatomy: GhostCompositionAnatomySchema.optional(), + traits: z + .record(z.string(), z.union([z.string(), z.array(z.string())])) + .optional(), + variants: z.array(z.string().min(1)).optional(), + anti_patterns: z.array(z.string().min(1)).optional(), + evidence: z.array(GhostPatternEvidenceSchema).optional(), + advisory: z.array(z.string().min(1)).optional(), + }) + .strict(); + +export const GhostPatternsSchema = z + .object({ + schema: z.literal(GHOST_PATTERNS_SCHEMA), + id: SlugIdSchema, + surface_types: z.array(GhostSurfaceTypePatternSchema), + composition_patterns: z.array(GhostCompositionPatternSchema), + advisory: z + .object({ + review_expectations: z.array(z.string().min(1)).optional(), + }) + .strict() + .optional(), + }) + .strict(); diff --git a/packages/ghost-core/src/patterns/types.ts b/packages/ghost-core/src/patterns/types.ts new file mode 100644 index 00000000..7685af5e --- /dev/null +++ b/packages/ghost-core/src/patterns/types.ts @@ -0,0 +1,67 @@ +export const GHOST_PATTERNS_SCHEMA = "ghost.patterns/v1" as const; +export const GHOST_PATTERNS_FILENAME = "patterns.yml" as const; + +export interface GhostPatternEvidence { + surface_id?: string; + path?: string; + locator?: string; + note?: string; +} + +export interface GhostSurfaceTypePattern { + id: string; + title?: string; + description?: string; + signals?: string[]; + preferred_patterns?: string[]; + discouraged_patterns?: string[]; + evidence?: GhostPatternEvidence[]; +} + +export interface GhostCompositionAnatomy { + ordered?: string[]; + required?: string[]; + optional?: string[]; + forbidden?: string[]; +} + +export interface GhostCompositionPattern { + id: string; + title?: string; + intent?: string; + surface_types?: string[]; + frequency?: number; + confidence?: number; + anatomy?: GhostCompositionAnatomy; + traits?: Record; + variants?: string[]; + anti_patterns?: string[]; + evidence?: GhostPatternEvidence[]; + advisory?: string[]; +} + +export interface GhostPatternsDocument { + schema: typeof GHOST_PATTERNS_SCHEMA; + id: string; + surface_types: GhostSurfaceTypePattern[]; + composition_patterns: GhostCompositionPattern[]; + advisory?: { + review_expectations?: string[]; + }; +} + +export type GhostPatternsLintSeverity = "error" | "warning" | "info"; + +export interface GhostPatternsLintIssue { + severity: GhostPatternsLintSeverity; + rule: string; + message: string; + path?: string; +} + +export interface GhostPatternsLintReport { + issues: GhostPatternsLintIssue[]; + errors: number; + warnings: number; + info: number; +} diff --git a/packages/ghost-core/src/resources/index.ts b/packages/ghost-core/src/resources/index.ts new file mode 100644 index 00000000..54a611ab --- /dev/null +++ b/packages/ghost-core/src/resources/index.ts @@ -0,0 +1,18 @@ +export { lintGhostResources } from "./lint.js"; +export { + GhostResourceRefSchema, + GhostResourcesSchema, + GhostSurfaceResourceSchema, +} from "./schema.js"; +export type { + GhostResourceRef, + GhostResourcesDocument, + GhostResourcesLintIssue, + GhostResourcesLintReport, + GhostResourcesLintSeverity, + GhostSurfaceResource, +} from "./types.js"; +export { + GHOST_RESOURCES_FILENAME, + GHOST_RESOURCES_SCHEMA, +} from "./types.js"; diff --git a/packages/ghost-core/src/resources/lint.ts b/packages/ghost-core/src/resources/lint.ts new file mode 100644 index 00000000..66555c3c --- /dev/null +++ b/packages/ghost-core/src/resources/lint.ts @@ -0,0 +1,90 @@ +import type { ZodIssue } from "zod"; +import { GhostResourcesSchema } from "./schema.js"; +import type { + GhostResourcesDocument, + GhostResourcesLintIssue, + GhostResourcesLintReport, +} from "./types.js"; + +export function lintGhostResources(input: unknown): GhostResourcesLintReport { + const issues: GhostResourcesLintIssue[] = []; + const result = GhostResourcesSchema.safeParse(input); + if (!result.success) { + issues.push(...zodIssues(result.error.issues)); + return finalize(issues); + } + + const doc = result.data as GhostResourcesDocument; + checkDuplicateIds(doc, issues); + if (!doc.include?.length) { + issues.push({ + severity: "info", + rule: "resources-include-empty", + message: + "resources.yml has no include globs; scanners will fall back to map.md surface sources.", + path: "include", + }); + } + + return finalize(issues); +} + +function checkDuplicateIds( + doc: GhostResourcesDocument, + issues: GhostResourcesLintIssue[], +): void { + const seen = new Map(); + const groups = [ + ["design_system", doc.design_system], + ["surfaces", doc.surfaces], + ["screenshots", doc.screenshots], + ["docs", doc.docs], + ["resolvers", doc.resolvers], + ["upstreams", doc.upstreams], + ] as const; + + if (doc.primary.id) seen.set(doc.primary.id, "primary.id"); + for (const [group, refs] of groups) { + refs?.forEach((ref, index) => { + if (!ref.id) return; + const previous = seen.get(ref.id); + if (previous) { + issues.push({ + severity: "error", + rule: "resource-id-duplicate", + message: `resource id '${ref.id}' is duplicated (also at ${previous})`, + path: `${group}[${index}].id`, + }); + } else { + seen.set(ref.id, `${group}[${index}].id`); + } + }); + } +} + +function zodIssues(issues: ZodIssue[]): GhostResourcesLintIssue[] { + return issues.map((issue) => ({ + severity: "error" as const, + rule: `schema/${issue.code}`, + message: issue.message, + path: formatZodPath(issue.path), + })); +} + +function formatZodPath(path: ZodIssue["path"]): string | undefined { + if (path.length === 0) return undefined; + return path.reduce((formatted, segment) => { + if (typeof segment === "number") return `${formatted}[${segment}]`; + const key = String(segment); + return formatted ? `${formatted}.${key}` : key; + }, ""); +} + +function finalize(issues: GhostResourcesLintIssue[]): GhostResourcesLintReport { + return { + issues, + errors: issues.filter((issue) => issue.severity === "error").length, + warnings: issues.filter((issue) => issue.severity === "warning").length, + info: issues.filter((issue) => issue.severity === "info").length, + }; +} diff --git a/packages/ghost-core/src/resources/schema.ts b/packages/ghost-core/src/resources/schema.ts new file mode 100644 index 00000000..c912064d --- /dev/null +++ b/packages/ghost-core/src/resources/schema.ts @@ -0,0 +1,48 @@ +import { z } from "zod"; +import { GHOST_RESOURCES_SCHEMA } from "./types.js"; + +const SlugIdSchema = z + .string() + .min(1) + .regex(/^[a-z0-9][a-z0-9._-]*$/, { + message: + "id must be a slug (lowercase alphanumeric plus . _ -, leading alphanumeric)", + }); + +export const GhostResourceRefSchema = z + .object({ + id: SlugIdSchema.optional(), + target: z.string().min(1), + kind: z.string().min(1).optional(), + paths: z.array(z.string().min(1)).optional(), + note: z.string().min(1).optional(), + }) + .strict(); + +export const GhostSurfaceResourceSchema = z + .object({ + id: SlugIdSchema.optional(), + name: z.string().min(1).optional(), + kind: z.string().min(1).optional(), + target: z.string().min(1).optional(), + locator: z.string().min(1).optional(), + paths: z.array(z.string().min(1)).optional(), + note: z.string().min(1).optional(), + }) + .strict(); + +export const GhostResourcesSchema = z + .object({ + schema: z.literal(GHOST_RESOURCES_SCHEMA), + id: SlugIdSchema, + primary: GhostResourceRefSchema, + design_system: z.array(GhostResourceRefSchema).optional(), + surfaces: z.array(GhostSurfaceResourceSchema).optional(), + screenshots: z.array(GhostResourceRefSchema).optional(), + docs: z.array(GhostResourceRefSchema).optional(), + resolvers: z.array(GhostResourceRefSchema).optional(), + upstreams: z.array(GhostResourceRefSchema).optional(), + include: z.array(z.string().min(1)).optional(), + exclude: z.array(z.string().min(1)).optional(), + }) + .strict(); diff --git a/packages/ghost-core/src/resources/types.ts b/packages/ghost-core/src/resources/types.ts new file mode 100644 index 00000000..b36c0e0b --- /dev/null +++ b/packages/ghost-core/src/resources/types.ts @@ -0,0 +1,50 @@ +export const GHOST_RESOURCES_SCHEMA = "ghost.resources/v1" as const; +export const GHOST_RESOURCES_FILENAME = "resources.yml" as const; + +export interface GhostResourceRef { + id?: string; + target: string; + kind?: string; + paths?: string[]; + note?: string; +} + +export interface GhostSurfaceResource { + id?: string; + name?: string; + kind?: string; + target?: string; + locator?: string; + paths?: string[]; + note?: string; +} + +export interface GhostResourcesDocument { + schema: typeof GHOST_RESOURCES_SCHEMA; + id: string; + primary: GhostResourceRef; + design_system?: GhostResourceRef[]; + surfaces?: GhostSurfaceResource[]; + screenshots?: GhostResourceRef[]; + docs?: GhostResourceRef[]; + resolvers?: GhostResourceRef[]; + upstreams?: GhostResourceRef[]; + include?: string[]; + exclude?: string[]; +} + +export type GhostResourcesLintSeverity = "error" | "warning" | "info"; + +export interface GhostResourcesLintIssue { + severity: GhostResourcesLintSeverity; + rule: string; + message: string; + path?: string; +} + +export interface GhostResourcesLintReport { + issues: GhostResourcesLintIssue[]; + errors: number; + warnings: number; + info: number; +} diff --git a/packages/ghost-core/src/survey/index.ts b/packages/ghost-core/src/survey/index.ts index 61bbaaa2..7f71586a 100644 --- a/packages/ghost-core/src/survey/index.ts +++ b/packages/ghost-core/src/survey/index.ts @@ -37,6 +37,7 @@ export { SurveySourceSchema, TokenRowSchema, UiSurfaceClassificationSchema, + UiSurfaceCompositionSchema, UiSurfaceKindSchema, UiSurfaceRenderabilitySchema, UiSurfaceRowSchema, @@ -83,6 +84,7 @@ export type { TokenRow, TypographySpec, UiSurfaceClassification, + UiSurfaceComposition, UiSurfaceDensity, UiSurfaceKind, UiSurfaceLayoutShape, diff --git a/packages/ghost-core/src/survey/lint.ts b/packages/ghost-core/src/survey/lint.ts index 9e7af6ea..e971a883 100644 --- a/packages/ghost-core/src/survey/lint.ts +++ b/packages/ghost-core/src/survey/lint.ts @@ -213,7 +213,7 @@ function checkResolution( severity: "info", rule: "resolution-unresolved-context-missing", message: - "unresolved rows should include `symbol` or `message` so the profile can surface coverage gaps.", + "unresolved rows should include `symbol` or `message` so the fingerprint can surface coverage gaps.", path, }); } diff --git a/packages/ghost-core/src/survey/schema.ts b/packages/ghost-core/src/survey/schema.ts index cfe91cef..9c0426f8 100644 --- a/packages/ghost-core/src/survey/schema.ts +++ b/packages/ghost-core/src/survey/schema.ts @@ -187,6 +187,17 @@ const UiSurfaceSignalsSchema = z }) .strict(); +const UiSurfaceCompositionSchema = z + .object({ + anatomy: z.array(z.string().min(1)).optional(), + primary_region: z.string().min(1).optional(), + action_placement: z.array(z.string().min(1)).optional(), + navigation_context: z.string().min(1).optional(), + responsive_behavior: z.array(z.string().min(1)).optional(), + confidence: z.number().min(0).max(1).optional(), + }) + .strict(); + const UiSurfaceRowSchema = RowBaseSchema.extend({ name: z.string().min(1), kind: UiSurfaceKindSchema, @@ -194,6 +205,7 @@ const UiSurfaceRowSchema = RowBaseSchema.extend({ renderability: UiSurfaceRenderabilitySchema, files: z.array(z.string().min(1)), classification: UiSurfaceClassificationSchema.optional(), + composition: UiSurfaceCompositionSchema.optional(), signals: UiSurfaceSignalsSchema, }); @@ -213,6 +225,7 @@ export { SurveySourceSchema, TokenRowSchema, UiSurfaceClassificationSchema, + UiSurfaceCompositionSchema, UiSurfaceKindSchema, UiSurfaceRenderabilitySchema, UiSurfaceRowSchema, diff --git a/packages/ghost-core/src/survey/types.ts b/packages/ghost-core/src/survey/types.ts index b6488dfa..18cdc639 100644 --- a/packages/ghost-core/src/survey/types.ts +++ b/packages/ghost-core/src/survey/types.ts @@ -242,10 +242,25 @@ export interface UiSurfaceSignals { breakpoint_behavior?: string[]; /** IDs of value rows that are visibly load-bearing for this surface. */ value_refs?: string[]; - /** Short factual notes; rationale belongs in fingerprint.md, not here. */ + /** Short factual notes; rationale belongs in patterns.yml or intent.md, not here. */ notes?: string[]; } +export interface UiSurfaceComposition { + /** Ordered factual anatomy (`shell`, `compact-header`, `filter-row`, `table`). */ + anatomy?: string[]; + /** Dominant region carrying the surface's work (`table`, `form`, `canvas`). */ + primary_region?: string; + /** Where actions live relative to objects or regions. */ + action_placement?: string[]; + /** Navigation relationship (`persistent-shell`, `local-tabs`, `none`). */ + navigation_context?: string; + /** Factual responsive behavior observed for this surface. */ + responsive_behavior?: string[]; + /** Confidence in the observed composition facts, not in interpretation. */ + confidence?: number; +} + export interface UiSurfaceRow extends RowBase { name: string; kind: UiSurfaceKind; @@ -254,6 +269,7 @@ export interface UiSurfaceRow extends RowBase { renderability: UiSurfaceRenderability; files: string[]; classification?: UiSurfaceClassification; + composition?: UiSurfaceComposition; signals: UiSurfaceSignals; } diff --git a/packages/ghost-core/src/types.ts b/packages/ghost-core/src/types.ts index 1c43ae2c..918a6299 100644 --- a/packages/ghost-core/src/types.ts +++ b/packages/ghost-core/src/types.ts @@ -234,7 +234,7 @@ export interface Check { /** Regex (or fixed string) the reviewer greps for. */ pattern: string; /** - * Repo-relative filesystem scopes used by `verify-profile` when checking + * Repo-relative filesystem scopes used by `verify-fingerprint` when checking * calibrated `observed_count` values. */ paths?: string[]; @@ -319,7 +319,7 @@ export interface DesignDecision { evidence: string[]; /** * Semantic embedding of `${dimension}: ${decision}`. - * Computed at profile time when an embedding provider is configured, + * Computed at fingerprint authoring time when an embedding provider is configured, * and used by compareDecisions for paraphrase-robust matching. * * Runtime-only. `fingerprint.md` no longer stores decision embeddings. @@ -331,7 +331,7 @@ export interface Fingerprint { id: string; source: "registry" | "extraction" | "llm" | "unknown"; timestamp: string; - /** When profiled from multiple sources, lists what was combined */ + /** When fingerprinted from multiple sources, lists what was combined */ sources?: string[]; // --- Three-layer model: observation → decisions → values --- @@ -390,7 +390,7 @@ export interface SampledFile { path: string; content: string; reason: string; - /** Which source this file came from (multi-source profiling) */ + /** Which source this file came from (multi-source fingerprinting) */ sourceLabel?: string; } @@ -407,7 +407,7 @@ export interface SampledMaterial { totalFiles: number; sampledFiles: number; targetType: TargetType; - /** When profiled from multiple sources, per-source breakdown */ + /** When fingerprinted from multiple sources, per-source breakdown */ sources?: SourceInfo[]; packageJson?: { name?: string; diff --git a/packages/ghost-core/test/resources-patterns.test.ts b/packages/ghost-core/test/resources-patterns.test.ts new file mode 100644 index 00000000..ad25aaac --- /dev/null +++ b/packages/ghost-core/test/resources-patterns.test.ts @@ -0,0 +1,80 @@ +import { describe, expect, it } from "vitest"; +import { lintGhostPatterns } from "../src/patterns/lint.js"; +import { lintGhostResources } from "../src/resources/lint.js"; + +describe("ghost.resources/v1", () => { + it("accepts a root resource ledger", () => { + const report = lintGhostResources({ + schema: "ghost.resources/v1", + id: "local", + primary: { target: ".", paths: ["."] }, + design_system: [{ id: "ui", target: "../ghost-ui", paths: ["src"] }], + surfaces: [{ id: "settings", locator: "/settings", paths: ["src"] }], + include: ["src/**"], + exclude: ["**/node_modules/**"], + }); + + expect(report.errors).toBe(0); + }); + + it("rejects duplicate resource ids", () => { + const report = lintGhostResources({ + schema: "ghost.resources/v1", + id: "local", + primary: { id: "ui", target: "." }, + design_system: [{ id: "ui", target: "../ghost-ui" }], + }); + + expect(report.errors).toBeGreaterThan(0); + expect( + report.issues.some((issue) => issue.rule === "resource-id-duplicate"), + ).toBe(true); + }); +}); + +describe("ghost.patterns/v1", () => { + it("accepts surface types and composition patterns", () => { + const report = lintGhostPatterns({ + schema: "ghost.patterns/v1", + id: "local", + surface_types: [ + { + id: "settings", + preferred_patterns: ["sectioned-form"], + evidence: [{ surface_id: "surface_1" }], + }, + ], + composition_patterns: [ + { + id: "sectioned-form", + surface_types: ["settings"], + frequency: 3, + confidence: 0.8, + anatomy: { + ordered: ["shell", "header", "sections", "actions"], + required: ["sections"], + }, + evidence: [{ surface_id: "surface_1", locator: "/settings" }], + }, + ], + }); + + expect(report.errors).toBe(0); + }); + + it("rejects unknown pattern references", () => { + const report = lintGhostPatterns({ + schema: "ghost.patterns/v1", + id: "local", + surface_types: [{ id: "settings", preferred_patterns: ["missing"] }], + composition_patterns: [], + }); + + expect(report.errors).toBeGreaterThan(0); + expect( + report.issues.some( + (issue) => issue.rule === "surface-type-pattern-unknown", + ), + ).toBe(true); + }); +}); diff --git a/packages/ghost-core/test/survey-lint.test.ts b/packages/ghost-core/test/survey-lint.test.ts index e7402ad4..ab5ddafc 100644 --- a/packages/ghost-core/test/survey-lint.test.ts +++ b/packages/ghost-core/test/survey-lint.test.ts @@ -255,6 +255,30 @@ describe("lintSurvey", () => { ); }); + it("accepts factual composition observations on UI surfaces", () => { + const report = lintSurvey( + makeSurvey( + [], + [], + [SOURCE], + [ + makeUiSurfaceRow({ + composition: { + anatomy: ["shell", "compact-header", "sectioned-form"], + primary_region: "form", + action_placement: ["footer", "section-local"], + navigation_context: "persistent-shell", + responsive_behavior: ["mobile stacks sections vertically"], + confidence: 0.74, + }, + }), + ], + ), + ); + + expect(report.errors).toBe(0); + }); + it("rejects sources array with no entries", () => { const survey: unknown = { ...makeSurvey(), diff --git a/packages/ghost-drift/README.md b/packages/ghost-drift/README.md index 39bb6bd7..c9517e97 100644 --- a/packages/ghost-drift/README.md +++ b/packages/ghost-drift/README.md @@ -2,7 +2,7 @@ **Deterministic design drift detection. Five verbs. No LLM calls.** -`ghost-drift` compares design-language fingerprints, records intent across drift, and ships the agentskills.io recipes a host agent uses to review, verify, and remediate. It pairs with **[`ghost-fingerprint`](../ghost-fingerprint)** — the package that owns authoring `fingerprint.md` (the compact authored contract this tool consumes). +`ghost-drift` checks root Ghost fingerprint bundles, compares bundle-derived design signals, records intent across drift, and ships the agentskills.io recipes a host agent uses to review, verify, and remediate. It pairs with **[`ghost-fingerprint`](../ghost-fingerprint)** — the package that owns authoring `.ghost/`. ## Requirements @@ -35,9 +35,9 @@ Once npm publishing is unblocked this will move to the registry — swap the URL ## Use ```bash -ghost-drift compare a/fingerprint.md b/fingerprint.md # pairwise distance (N=2) -ghost-drift compare ./*/fingerprint.md # composite, N≥3 -ghost-drift compare a.md b.md --semantic # add qualitative diff +ghost-drift compare a/.ghost b/.ghost # pairwise bundle distance (N=2) +ghost-drift compare ./*/.ghost # composite bundle comparison, N≥3 +ghost-drift compare a.md b.md --semantic # direct markdown qualitative diff ghost-drift compare a.md b.md --temporal # add velocity / trajectory ghost-drift ack # acknowledge drift against the tracked fingerprint ghost-drift track path/to/new-tracked.fingerprint.md # track another fingerprint @@ -49,14 +49,13 @@ Zero config for every verb. No API key needed. `OPENAI_API_KEY` / `VOYAGE_API_KE ### Authoring a scan? -Scans live in **[`ghost-fingerprint`](../ghost-fingerprint)**, which owns the three-stage pipeline (`map.md` → `survey.json` → `fingerprint.md`). Install it for `inventory`, `lint`, `verify-profile`, `describe`, `diff`, `survey merge` / `fix-ids` / `summarize` / `catalog`, `scan-status`, and `emit review-command` / `emit context-bundle`: +Scans live in **[`ghost-fingerprint`](../ghost-fingerprint)**, which owns the root bundle pipeline (`resources.yml` → `map.md` → `survey.json` → `patterns.yml`). Install it for `inventory`, `lint`, `verify`, `describe`, `diff`, `survey merge` / `fix-ids` / `summarize` / `catalog` / `patterns`, `scan-status`, and `emit review-command` / `emit context-bundle`: ```bash ghost-fingerprint inventory # raw repo signals → JSON (feeds map.md) ghost-fingerprint scan-status # per-stage state + next stage -ghost-fingerprint lint # auto-detects fingerprint.md / map.md / survey.json -ghost-fingerprint verify-profile fingerprint.md survey.json --root . - # fingerprint-to-survey fidelity gate +ghost-fingerprint lint # auto-detects .ghost bundle artifacts +ghost-fingerprint verify .ghost --root . # cross-artifact fidelity gate ghost-fingerprint survey merge a.json b.json # union with id-based dedup ghost-fingerprint survey catalog survey.json # derived value enum/spec view ghost-fingerprint diff a.md b.md # structural prose-level diff between fingerprints @@ -93,7 +92,7 @@ ghost-drift emit skill The agent runs the recipes; the CLI runs the arithmetic. The CLI never calls an LLM. -(Authoring recipes — `scan` / `map` / `survey` / `profile` — all ship in `ghost-fingerprint`'s skill bundle, since one tool now owns the whole three-stage scan pipeline. Fleet narrative recipes ship in `ghost-fleet`.) +(Authoring recipes — `scan` / `map` / `survey` / `patterns` — all ship in `ghost-fingerprint`'s skill bundle, since one tool now owns the root bundle pipeline. Fleet narrative recipes ship in `ghost-fleet`.) ## Full story diff --git a/packages/ghost-drift/package.json b/packages/ghost-drift/package.json index df1fb13f..270a89d4 100644 --- a/packages/ghost-drift/package.json +++ b/packages/ghost-drift/package.json @@ -1,7 +1,7 @@ { "name": "ghost-drift", "version": "0.2.0", - "description": "Deterministic Ghost design checks, advisory review packets, profile comparison, and drift stance tracking", + "description": "Deterministic Ghost design checks, advisory review packets, root bundle comparison, and drift stance tracking", "license": "Apache-2.0", "author": "Block, Inc.", "repository": { diff --git a/packages/ghost-drift/src/cli.ts b/packages/ghost-drift/src/cli.ts index d90aae84..f67da188 100644 --- a/packages/ghost-drift/src/cli.ts +++ b/packages/ghost-drift/src/cli.ts @@ -8,9 +8,10 @@ import { loadSkillBundle } from "@ghost/core"; import { cac } from "cac"; import { formatSemanticDiff, - loadFingerprint, resolveFingerprintPackage, } from "ghost-fingerprint"; +import { parse as parseYaml, stringify as stringifyYaml } from "yaml"; +import { loadComparableFingerprint } from "./comparable-fingerprint.js"; import { compare, formatComparison, @@ -50,7 +51,7 @@ export function buildCli(): ReturnType { cli .command( "compare [...fingerprints]", - "Compare two or more fingerprints. N=2 returns a pairwise delta; N≥3 returns a composite fingerprint (pairwise matrix, centroid, spread, clusters).", + "Compare two or more fingerprints or root .ghost bundles. N=2 returns a pairwise delta; N≥3 returns a composite fingerprint.", ) .option("--semantic", "Qualitative diff of decisions + palette (N=2 only)") .option( @@ -64,10 +65,9 @@ export function buildCli(): ReturnType { .option("--format ", "Output format: cli or json", { default: "cli" }) .action(async (fingerprints: string[], opts) => { try { - const parsed = await Promise.all( - fingerprints.map((path) => loadFingerprint(path)), + const exprs = await Promise.all( + fingerprints.map((path) => loadComparableFingerprint(path)), ); - const exprs = parsed.map((p) => p.fingerprint); let history: Awaited> | undefined; let manifest: Awaited> | null = @@ -136,7 +136,7 @@ export function buildCli(): ReturnType { cli .command( "check", - "Run active ghost.checks/v1 gates from .ghost/fingerprint/checks.yml against a git diff.", + "Run active ghost.checks/v1 gates from .ghost/checks.yml against a git diff.", ) .option("--base ", "Git ref to diff against (default: HEAD)") .option( @@ -145,7 +145,7 @@ export function buildCli(): ReturnType { ) .option( "--package ", - "Fingerprint package directory (default: .ghost/fingerprint)", + "Fingerprint package directory (default: .ghost)", ) .option("--format ", "Output format: markdown or json", { default: "markdown", @@ -195,7 +195,7 @@ export function buildCli(): ReturnType { ) .option( "--package ", - "Fingerprint package directory (default: .ghost/fingerprint)", + "Fingerprint package directory (default: .ghost)", ) .option("--format ", "Output format: markdown or json", { default: "markdown", @@ -217,14 +217,16 @@ export function buildCli(): ReturnType { const packet = { schema: "ghost.advisory-review/v1", package_dir: paths.dir, - profile: await readFile(paths.profile, "utf-8"), + patterns: parseYaml(await readFile(paths.patterns, "utf-8")), survey: JSON.parse(await readFile(paths.survey, "utf-8")), - checks: await readFile(paths.checks, "utf-8"), + intent: (await readOptional(paths.intent)) ?? null, + checks: (await readOptional(paths.checks)) ?? null, diff: diffText, required_finding_citations: [ "diff location", - "profile section", + "patterns.yml composition pattern", "survey evidence", + "intent.md when relevant", "precedent/example", "repair", ], @@ -329,9 +331,10 @@ async function readGitDiff(cwd: string, base: unknown): Promise { function formatReviewPacketMarkdown(packet: { package_dir: string; - profile: string; + patterns: unknown; survey: unknown; - checks: string; + intent: string | null; + checks: string | null; diff: string; required_finding_citations: string[]; }): string { @@ -341,10 +344,10 @@ Package: ${packet.package_dir} Review this diff as a non-blocking design-language critic. Advisory findings must be evidence-routed and must cite: ${packet.required_finding_citations.join(", ")}. Do not fail the build unless the issue is tied to an active deterministic check in checks.yml. -## Profile +## Patterns -\`\`\`markdown -${packet.profile} +\`\`\`yaml +${stringifyYaml(packet.patterns)} \`\`\` ## Survey Evidence @@ -353,10 +356,16 @@ ${packet.profile} ${JSON.stringify(packet.survey, null, 2)} \`\`\` +## Human Intent + +\`\`\`markdown +${packet.intent ?? "_No intent.md present. Treat patterns.yml and survey.json as observed evidence, not declared human intent._"} +\`\`\` + ## Active Checks \`\`\`yaml -${packet.checks} +${packet.checks ?? "schema: ghost.checks/v1\nid: none\nchecks: []\n"} \`\`\` ## Diff @@ -366,3 +375,11 @@ ${packet.diff} \`\`\` `; } + +async function readOptional(path: string): Promise { + try { + return await readFile(path, "utf-8"); + } catch { + return undefined; + } +} diff --git a/packages/ghost-drift/src/comparable-fingerprint.ts b/packages/ghost-drift/src/comparable-fingerprint.ts new file mode 100644 index 00000000..0722ddf8 --- /dev/null +++ b/packages/ghost-drift/src/comparable-fingerprint.ts @@ -0,0 +1,124 @@ +import { readFile } from "node:fs/promises"; +import { resolve } from "node:path"; +import { + computeEmbedding, + type Fingerprint, + type GhostPatternsDocument, + type Survey, +} from "@ghost/core"; +import { loadFingerprint, resolveFingerprintPackage } from "ghost-fingerprint"; +import { parse as parseYaml } from "yaml"; + +export async function loadComparableFingerprint( + path: string, +): Promise { + const target = resolve(process.cwd(), path); + if (target.endsWith(".md")) { + return (await loadFingerprint(target)).fingerprint; + } + + const paths = resolveFingerprintPackage(path, process.cwd()); + try { + const [surveyRaw, patternsRaw] = await Promise.all([ + readFile(paths.survey, "utf-8"), + readFile(paths.patterns, "utf-8"), + ]); + return synthesizeFingerprintFromBundle( + paths.dir, + JSON.parse(surveyRaw) as Survey, + parseYaml(patternsRaw) as GhostPatternsDocument, + ); + } catch { + return (await loadFingerprint(target)).fingerprint; + } +} + +function synthesizeFingerprintFromBundle( + path: string, + survey: Survey, + patterns: GhostPatternsDocument, +): Fingerprint { + const colors = survey.values + .filter((row) => row.kind === "color") + .slice() + .sort((a, b) => b.occurrences - a.occurrences) + .slice(0, 8) + .map((row, index) => ({ + role: row.role_hypothesis ?? `color-${index + 1}`, + value: row.value, + })); + const spacingScale = survey.values + .filter((row) => row.kind === "spacing") + .map((row) => scalarValue(row.value)) + .filter((value): value is number => value !== null) + .sort((a, b) => a - b); + const typographySizes = survey.values + .filter((row) => row.kind === "typography") + .map((row) => scalarValue(row.value)) + .filter((value): value is number => value !== null) + .sort((a, b) => a - b); + const radii = survey.values + .filter((row) => row.kind === "radius") + .map((row) => scalarValue(row.value)) + .filter((value): value is number => value !== null) + .sort((a, b) => a - b); + + const fingerprint: Fingerprint = { + id: patterns.id, + source: "extraction", + timestamp: survey.sources[0]?.scanned_at ?? new Date(0).toISOString(), + sources: [path], + observation: { + summary: `Root Ghost bundle synthesized from ${survey.ui_surfaces.length} surveyed surfaces and ${patterns.composition_patterns.length} composition patterns.`, + personality: [], + resembles: [], + }, + decisions: patterns.composition_patterns.map((pattern) => ({ + dimension: pattern.id, + dimension_kind: "composition-patterns", + decision: pattern.intent ?? pattern.title ?? pattern.id, + evidence: + pattern.evidence?.map( + (entry) => + entry.locator ?? entry.path ?? entry.surface_id ?? pattern.id, + ) ?? [], + })), + palette: { + dominant: colors, + neutrals: { steps: [], count: 0 }, + semantic: [], + saturationProfile: "mixed", + contrast: "moderate", + }, + spacing: { + scale: uniqueNumbers(spacingScale), + regularity: spacingScale.length > 0 ? 1 : 0, + baseUnit: spacingScale[0] ?? null, + }, + typography: { + families: [], + sizeRamp: uniqueNumbers(typographySizes), + weightDistribution: {}, + lineHeightPattern: "normal", + }, + surfaces: { + borderRadii: uniqueNumbers(radii), + shadowComplexity: survey.values.some((row) => row.kind === "shadow") + ? "subtle" + : "deliberate-none", + borderUsage: "minimal", + }, + embedding: [], + }; + fingerprint.embedding = computeEmbedding(fingerprint); + return fingerprint; +} + +function scalarValue(value: string): number | null { + const match = value.match(/-?\d+(?:\.\d+)?/); + return match ? Number(match[0]) : null; +} + +function uniqueNumbers(values: number[]): number[] { + return [...new Set(values)]; +} diff --git a/packages/ghost-drift/src/core/check.ts b/packages/ghost-drift/src/core/check.ts index 2a72d00a..31f95777 100644 --- a/packages/ghost-drift/src/core/check.ts +++ b/packages/ghost-drift/src/core/check.ts @@ -200,9 +200,20 @@ async function loadCheckPackage( const paths = resolveFingerprintPackage(packageDir, cwd); const [mapRaw, checksRaw] = await Promise.all([ readFile(paths.map, "utf-8"), - readFile(paths.checks, "utf-8"), + readOptional(paths.checks), ]); const map = parseMap(mapRaw); + if (checksRaw === undefined) { + return { + dir: paths.dir, + map, + checks: { + schema: "ghost.checks/v1", + id: "none", + checks: [], + }, + }; + } const checksInput = parseYaml(checksRaw); const checksResult = GhostChecksSchema.safeParse(checksInput); if (!checksResult.success) { @@ -224,6 +235,14 @@ async function loadCheckPackage( return { dir: paths.dir, map, checks: checksResult.data }; } +async function readOptional(path: string): Promise { + try { + return await readFile(path, "utf-8"); + } catch { + return undefined; + } +} + function parseMap(raw: string): MapFrontmatter { const block = raw.match(/^---\n([\s\S]*?)\n---/)?.[1]; if (!block) throw new Error("map.md is missing YAML frontmatter"); diff --git a/packages/ghost-drift/src/core/evolution/emit.ts b/packages/ghost-drift/src/core/evolution/emit.ts index 07299faa..21086930 100644 --- a/packages/ghost-drift/src/core/evolution/emit.ts +++ b/packages/ghost-drift/src/core/evolution/emit.ts @@ -6,7 +6,7 @@ import { } from "ghost-fingerprint"; /** - * Write a profile as the publishable design-language prior inside the + * Write a fingerprint as the publishable design-language prior inside the * fingerprint package. Other projects can track this file as a reference. */ export async function emitFingerprint( @@ -15,7 +15,7 @@ export async function emitFingerprint( ): Promise { const paths = resolveFingerprintPackage(undefined, cwd); await mkdir(paths.dir, { recursive: true }); - const target = paths.profile; + const target = paths.fingerprint; await writeFile(target, serializeFingerprint(fingerprint), "utf-8"); return target; diff --git a/packages/ghost-drift/src/core/evolution/tracking.ts b/packages/ghost-drift/src/core/evolution/tracking.ts index e9d8e322..a87e2643 100644 --- a/packages/ghost-drift/src/core/evolution/tracking.ts +++ b/packages/ghost-drift/src/core/evolution/tracking.ts @@ -2,8 +2,8 @@ import { resolve } from "node:path"; import type { Fingerprint, Target } from "@ghost/core"; import { resolveTarget } from "@ghost/core"; import { + FINGERPRINT_FILENAME, loadFingerprint, - PROFILE_FILENAME, parseFingerprint, resolveFingerprintPackage, } from "ghost-fingerprint"; @@ -14,7 +14,7 @@ import { * - "path": reads a local fingerprint.md, or a directory containing one. * - "url": fetches a remote fingerprint.md * - "npm": resolves node_modules//fingerprint.md - * - "github": not yet supported for direct resolution (use profile flow instead) + * - "github": not yet supported for direct resolution (use fingerprint flow instead) */ export async function resolveTrackedFingerprint( target: Target, @@ -46,7 +46,7 @@ export async function resolveTrackedFingerprint( default: throw new Error( - `Cannot resolve tracked fingerprint from target type "${target.type}". Generate one first by running the profile recipe in your host agent (install with "ghost-drift emit skill").`, + `Cannot resolve tracked fingerprint from target type "${target.type}". Generate one first by running the fingerprint recipe in your host agent (install with "ghost-drift emit skill").`, ); } } @@ -64,10 +64,10 @@ async function readFingerprintFile(path: string): Promise { async function readFingerprintFromDir(dir: string): Promise { try { return await readFingerprintFile( - resolveFingerprintPackage(undefined, dir).profile, + resolveFingerprintPackage(undefined, dir).fingerprint, ); } catch { - return readFingerprintFile(resolve(dir, PROFILE_FILENAME)); + return readFingerprintFile(resolve(dir, FINGERPRINT_FILENAME)); } } diff --git a/packages/ghost-drift/src/core/scope-resolver.ts b/packages/ghost-drift/src/core/scope-resolver.ts index 3d88fd84..5ef941d3 100644 --- a/packages/ghost-drift/src/core/scope-resolver.ts +++ b/packages/ghost-drift/src/core/scope-resolver.ts @@ -8,7 +8,7 @@ import { MapFrontmatterSchema, type MapScope, } from "@ghost/core"; -import { PROFILE_FILENAME } from "ghost-fingerprint"; +import { FINGERPRINT_FILENAME } from "ghost-fingerprint"; import { parse as parseYaml } from "yaml"; const FINGERPRINTS_DIRNAME = "fingerprints"; @@ -28,7 +28,7 @@ export interface ResolveFingerprintsForPathsOptions { /** * Resolve the governing fingerprint for each changed path in a scan * directory. Paths matching a product-surface scope use - * `fingerprints/.md`; everything else falls back to `profile.md`. + * `fingerprints/.md`; everything else falls back to `fingerprint.md`. */ export async function resolveFingerprintsForPaths( scanDir: string, @@ -90,7 +90,7 @@ function parentResolution( ): PathFingerprintResolution { return { changed_path: changedPath, - fingerprint_path: join(root, PROFILE_FILENAME), + fingerprint_path: join(root, FINGERPRINT_FILENAME), fallback: true, reason, }; diff --git a/packages/ghost-drift/src/evolution-commands.ts b/packages/ghost-drift/src/evolution-commands.ts index 90ad89e1..6f065c6c 100644 --- a/packages/ghost-drift/src/evolution-commands.ts +++ b/packages/ghost-drift/src/evolution-commands.ts @@ -8,7 +8,7 @@ import { } from "./core/index.js"; async function loadLocalFingerprint() { - const path = resolveFingerprintPackage(undefined, process.cwd()).profile; + const path = resolveFingerprintPackage(undefined, process.cwd()).fingerprint; const { fingerprint } = await loadFingerprint(path); return fingerprint; } diff --git a/packages/ghost-drift/src/skill-bundle/SKILL.md b/packages/ghost-drift/src/skill-bundle/SKILL.md index c235c331..f6cbab49 100644 --- a/packages/ghost-drift/src/skill-bundle/SKILL.md +++ b/packages/ghost-drift/src/skill-bundle/SKILL.md @@ -1,6 +1,6 @@ --- name: ghost-drift -description: Run deterministic Ghost checks, emit advisory review packets, compare profiles, and record drift stance. Use for "check for drift", "review this PR", "verify generated UI", "compare profiles", or "accept this divergence". +description: Run deterministic Ghost checks, emit advisory review packets, compare root fingerprint bundles or direct fingerprint files, and record drift stance. Use for "check for drift", "review this PR", "verify generated UI", "compare fingerprints", or "accept this divergence". license: Apache-2.0 metadata: homepage: https://github.com/block/ghost @@ -9,32 +9,35 @@ metadata: # Ghost Drift — Check And Review -Ghost Drift consumes the repo-local fingerprint package: +Ghost Drift consumes the repo-local root fingerprint bundle: ```text -.ghost/fingerprint/ +.ghost/ + resources.yml map.md survey.json - profile.md - checks.yml + patterns.yml + checks.yml (optional) + intent.md (optional) ``` The rule is simple: - `ghost-drift check` is deterministic and blocking. -- `ghost-drift review` is advisory and evidence-routed. -- `ghost-drift compare` compares profile embeddings. -- `ack`, `track`, and `diverge` record intentional drift. +- `ghost-drift review` is advisory and evidence-routed through patterns and survey evidence. +- `ghost-drift compare` compares root bundles or direct fingerprint markdown files. +- `ack`, `track`, and `diverge` record intentional drift for direct tracked fingerprints. ## CLI Verbs | Verb | Purpose | |---|---| -| `ghost-drift check --base ` | Route changed files through `map.md`, apply active `checks.yml`, and exit nonzero on failures. | +| `ghost-drift check --base ` | Route changed files through `.ghost/map.md`, apply active `.ghost/checks.yml`, and exit nonzero on failures. | | `ghost-drift check --diff --format json` | Check a saved unified diff and emit stable JSON. | -| `ghost-drift review --base ` | Emit an advisory review prompt packet grounded in profile, survey, checks, and diff. | -| `ghost-drift compare [...more]` | Pairwise or composite profile distance. | -| `ghost-drift ack` / `track ` / `diverge ` | Record stance in `.ghost-sync.json`. | +| `ghost-drift review --base ` | Emit an advisory review prompt packet grounded in patterns, survey, optional intent, checks, and diff. | +| `ghost-drift compare [...more]` | Pairwise or composite distance over root bundles. | +| `ghost-drift compare ` | Compare direct fingerprint markdown files. | +| `ghost-drift ack` / `track ` / `diverge ` | Record stance in `.ghost-sync.json` for direct tracked fingerprints. | | `ghost-drift emit skill` | Install this skill bundle. | ## Review Rule @@ -43,8 +46,9 @@ Advisory findings are non-blocking unless tied to an active deterministic check. Every advisory finding should cite: - diff location -- profile section +- `patterns.yml` composition pattern - survey evidence +- `intent.md` when relevant - precedent/example - repair @@ -52,13 +56,13 @@ Every advisory finding should cite: - "Run the gate" → `ghost-drift check --base `. - "Review this PR for design drift" → run `ghost-drift check`, then use `ghost-drift review` as the evidence packet for advisory critique. -- "Compare these profiles" → run `ghost-drift compare `, add `--semantic` when the user asks why. -- "Accept this drift" → use `ack`, `track`, or `diverge`. +- "Compare these bundles" → run `ghost-drift compare `. +- "Accept this drift" → use `ack`, `track`, or `diverge` for direct tracked fingerprints. -Authoring `.ghost/fingerprint/` lives in the sibling `ghost-fingerprint` skill. +Authoring `.ghost/` lives in the sibling `ghost-fingerprint` skill. ## Never -- Never treat profile prose as a CI gate. -- Never fail a build on advisory-only judgment. +- Never treat advisory composition judgment as a CI gate. +- Never fail a build on advisory-only findings. - Never auto-promote an advisory finding into `checks.yml`; a human must curate deterministic gates. diff --git a/packages/ghost-drift/src/skill-bundle/references/compare.md b/packages/ghost-drift/src/skill-bundle/references/compare.md index c7031327..f18b2cd2 100644 --- a/packages/ghost-drift/src/skill-bundle/references/compare.md +++ b/packages/ghost-drift/src/skill-bundle/references/compare.md @@ -5,9 +5,9 @@ handoffs: - label: Accept the drift as aligned reality command: ghost-drift ack prompt: Accept current drift across the board - - label: Track the other profile + - label: Track the other fingerprint command: ghost-drift track - prompt: Track the other profile.md as the new reference + prompt: Track the other fingerprint.md as the new reference - label: Declare a dimension intentionally divergent command: ghost-drift diverge prompt: Record an intentional divergence on a specific dimension @@ -21,17 +21,17 @@ handoffs: ### Pairwise (N=2) - ghost-drift compare a.md b.md + ghost-drift compare a/.ghost b/.ghost -Output: distance (0 = identical, 1 = unrelated) and per-dimension deltas (palette, spacing, typography, surfaces). +Output: distance (0 = identical, 1 = unrelated) and per-dimension deltas. Bundle inputs are synthesized from survey values plus pattern frequencies; direct fingerprint markdown files still use their embedded frontmatter. Flags: -- `--semantic` — add qualitative diff (which decisions changed, which colors appeared/disappeared) +- `--semantic` — add qualitative diff for direct fingerprint markdown comparisons - `--temporal` — add drift velocity, trajectory, and ack bounds (reads `.ghost/history.jsonl`) ### Composite (N≥3) - ghost-drift compare a.md b.md c.md d.md + ghost-drift compare a/.ghost b/.ghost c/.ghost d/.ghost Output: pairwise distance matrix, centroid, spread, and cluster assignments. The centroid is the composite (org-scale) fingerprint: what the members average out to. diff --git a/packages/ghost-drift/src/skill-bundle/references/remediate.md b/packages/ghost-drift/src/skill-bundle/references/remediate.md index e2b9fd5b..5dc06cbb 100644 --- a/packages/ghost-drift/src/skill-bundle/references/remediate.md +++ b/packages/ghost-drift/src/skill-bundle/references/remediate.md @@ -7,7 +7,7 @@ handoffs: prompt: Re-run the review against the patched files to confirm the drift is closed - label: Acknowledge the drift as accepted command: ghost-drift ack - prompt: Acknowledge that the current profile no longer matches and accept the drift + prompt: Acknowledge that the current fingerprint no longer matches and accept the drift - label: Declare a dimension intentionally divergent command: ghost-drift diverge prompt: Record an intentional divergence on a specific dimension so it stops flagging @@ -15,9 +15,9 @@ handoffs: # Recipe: Remediate drift -**Goal:** turn drift findings into a small, surgical patch — the minimal code change that closes the gap between the working tree and the fingerprint. Remediate is the loop after `review`: review *finds* drift; remediate *proposes the fix*. +**Goal:** turn drift findings into a small, surgical patch — the minimal code change that closes the gap between the working tree and the root fingerprint bundle. Remediate is the loop after `review`: review *finds* drift; remediate *proposes the fix*. -Ghost has no `ghost-drift remediate` CLI command. You — the host agent — read the findings, weigh them against the fingerprint, and write the patch. +Ghost has no `ghost-drift remediate` CLI command. You — the host agent — read the findings, weigh them against `patterns.yml`, `survey.json`, optional `intent.md`, and active checks, then write the patch. ## Steps @@ -27,24 +27,24 @@ You need: - The **drift output** — either the JSON from `ghost-drift compare --semantic --format json` or the structured findings from a [review](review.md) pass. - The **offending diff** — `git diff -- ` for each flagged file. -- The **fingerprint package** — `ghost-fingerprint describe` to plan profile reads, plus `.ghost/fingerprint/checks.yml` for active gates and `.ghost/fingerprint/survey.json` for evidence. +- The **fingerprint bundle** — `.ghost/patterns.yml`, `.ghost/survey.json`, optional `.ghost/intent.md`, and `.ghost/checks.yml` for active gates. - The **sync manifest** if present (`.ghost-sync.json`) — anything stance:`diverging` is intentional and must NOT be remediated. ### 2. Match each finding to a token For every drift finding, identify the token the code *should* have used: -- Hardcoded `#3b82f6` → `palette.dominant` or `palette.semantic` entry whose value is closest. If nothing fits, the fingerprint is silent on this color and the right move is to add a decision, not a remediation. -- Off-grid `padding: 14px` → nearest step in `spacing.scale`. Prefer rounding *down* unless the surrounding rhythm suggests otherwise. -- Hard-coded `border-radius: 12px` not in `surfaces.borderRadii` → snap to nearest declared radius. -- Font family not in `typography.families` → flag for human; family swaps are rarely a one-line fix. -- Behavioral drift (e.g. an animation when the decision says "no animation") → propose removing the offending property; cite the decision dimension. +- Hardcoded `#3b82f6` → token or value evidenced in `survey.json`. If nothing fits, the bundle is silent and the right move is to add evidence or human intent, not a remediation. +- Off-pattern composition → restore the `patterns.yml` anatomy or use an allowed variant. +- Off-grid `padding: 14px` → nearest survey-backed spacing/token value. Prefer rounding *down* unless the surrounding rhythm suggests otherwise. +- Hard-coded radius not evidenced in `survey.json` → snap to a survey-backed radius or flag for human review. +- Behavioral drift → propose removing the offending property; cite the relevant pattern or intent. ### 3. Score by impact Rank findings by how much distance they close: -- **Load-bearing** — fixes that resolve a `### color-strategy` or `### shape-language` decision violation. Patch first. +- **Load-bearing** — fixes that restore required pattern anatomy, token usage, or active checks. Patch first. - **Snap-to-grid** — off-scale spacing/radii values. Patch in the same pass. - **Cosmetic** — values that drift slightly (`16px` vs `15px`) but don't break a decision. Group these into one cleanup commit. @@ -71,7 +71,7 @@ Group patches by file. Keep each patch surgical — do not refactor surrounding Some findings have no clean fix: -- The fingerprint is silent on the dimension → tell the user the fingerprint is missing a decision; offer to run the profile recipe to add it. +- The bundle is silent on the dimension → tell the user the bundle is missing evidence, pattern policy, or accepted intent; offer to update the bundle. - The drift is intentional (e.g. a brand-tier override on a single page) → suggest `ghost-drift diverge --reason "..."` instead of a code patch. - The fix would cascade across many files → flag and stop. A 30-file refactor disguised as "remediation" is a separate change with its own review. @@ -81,7 +81,7 @@ After the user applies (or rejects) the patches: - Re-run `ghost-drift compare` — distance should drop. If it doesn't, the patches missed. - If the user accepts the drift instead of fixing it, run `ghost-drift ack` (overall) or `ghost-drift diverge ` (one axis). -- Never silently re-profile the fingerprint to "absorb" the drift. That hides the act. +- Never silently regenerate the bundle to "absorb" the drift. That hides the act. ## Why this is a recipe, not a verb diff --git a/packages/ghost-drift/src/skill-bundle/references/review.md b/packages/ghost-drift/src/skill-bundle/references/review.md index df70586e..4aeb27e0 100644 --- a/packages/ghost-drift/src/skill-bundle/references/review.md +++ b/packages/ghost-drift/src/skill-bundle/references/review.md @@ -1,13 +1,13 @@ --- name: review -description: Review PR or working-tree changes against the local Ghost fingerprint package. +description: Review PR or working-tree changes against the local Ghost fingerprint bundle. handoffs: - label: Suggest minimal fixes skill: remediate prompt: Given the drift findings, suggest the minimal code changes that bring the diff back inside the fingerprint package - label: Accept the drift command: ghost-drift ack - prompt: Acknowledge that the current profile no longer matches and record the drift + prompt: Acknowledge that the current fingerprint no longer matches and record the drift --- # Recipe: Review Code Changes For Design Drift @@ -33,9 +33,10 @@ ghost-drift review --base Use the emitted packet as context. It includes: -- `.ghost/fingerprint/profile.md` -- `.ghost/fingerprint/survey.json` -- `.ghost/fingerprint/checks.yml` +- `.ghost/patterns.yml` +- `.ghost/survey.json` +- optional `.ghost/intent.md` +- optional `.ghost/checks.yml` - the diff ### 3. Write Advisory Findings @@ -44,8 +45,9 @@ Advisory findings are non-blocking unless tied to an active deterministic check. Each finding must cite: - diff location -- profile section +- `patterns.yml` composition pattern - survey evidence +- `intent.md` when relevant - precedent/example - repair @@ -60,7 +62,7 @@ Good advisory topics: Bad advisory topics: - vague taste objections with no example -- restating profile prose without a diff location +- restating pattern prose without a diff location - enforcing a rule that is not in `checks.yml` ### 4. Promote Durable Rules Later diff --git a/packages/ghost-drift/src/skill-bundle/references/verify.md b/packages/ghost-drift/src/skill-bundle/references/verify.md index c85e3b1c..3362364a 100644 --- a/packages/ghost-drift/src/skill-bundle/references/verify.md +++ b/packages/ghost-drift/src/skill-bundle/references/verify.md @@ -1,6 +1,6 @@ --- name: verify -description: Confirm generated UI stays within the local Ghost fingerprint package; iterate if not. +description: Confirm generated UI stays within the local Ghost fingerprint bundle; iterate if not. handoffs: - label: Remediate deterministic or advisory findings skill: remediate @@ -9,13 +9,12 @@ handoffs: # Recipe: Verify Generated UI -**Goal:** run the generate → check → review → repair loop against -`.ghost/fingerprint/`. +**Goal:** run the generate → check → review → repair loop against `.ghost/`. ## Steps -1. Generate the UI using `.ghost/fingerprint/profile.md`, survey pattern - summaries, nearest examples, and active checks as context. +1. Generate the UI using `.ghost/patterns.yml`, `.ghost/survey.json`, + optional `.ghost/intent.md`, nearest examples, and active checks as context. 2. Run the deterministic gate: ```bash @@ -30,6 +29,6 @@ handoffs: ``` 5. Repair high-confidence advisory issues when they cite a diff location, - profile section, survey evidence, precedent/example, and repair. + `patterns.yml` composition pattern, survey evidence, precedent/example, and repair. -Profile prose shapes judgment. Only active `checks.yml` failures block. +Patterns and optional intent shape judgment. Only active `checks.yml` failures block. diff --git a/packages/ghost-drift/test/cli.test.ts b/packages/ghost-drift/test/cli.test.ts index 8812a03b..e6a73791 100644 --- a/packages/ghost-drift/test/cli.test.ts +++ b/packages/ghost-drift/test/cli.test.ts @@ -125,10 +125,20 @@ describe("ghost-drift CLI", () => { expect(result.stdout).toContain("Distance"); }); + it("compares root fingerprint bundle directories", async () => { + await writeComparableBundle(join(dir, "a", ".ghost"), "sectioned-form"); + await writeComparableBundle(join(dir, "b", ".ghost"), "data-table"); + + const result = await runCli(["compare", "a/.ghost", "b/.ghost"], dir); + + expect(result.code).toBe(0); + expect(result.stdout).toContain("Distance"); + }); + it("track writes the neutral sync manifest shape", async () => { - await mkdir(join(dir, ".ghost", "fingerprint"), { recursive: true }); + await mkdir(join(dir, ".ghost"), { recursive: true }); await writeFile( - join(dir, ".ghost", "fingerprint", "profile.md"), + join(dir, ".ghost", "fingerprint.md"), fingerprintWithId("local"), ); await writeFile( @@ -202,6 +212,19 @@ describe("ghost-drift CLI", () => { expect(result.stdout).toContain("Design Check: PASS"); }); + it("check passes when optional checks.yml is absent", async () => { + await writeCheckPackage(dir, { checks: false }); + await writeFile( + join(dir, "change.patch"), + lendingPatch("UIColor(#ffffff)"), + ); + + const result = await runCli(["check", "--diff", "change.patch"], dir); + + expect(result.code).toBe(0); + expect(result.stdout).toContain("No active deterministic check failures."); + }); + it("review emits an advisory packet with required citation fields", async () => { await writeCheckPackage(dir); await writeFile( @@ -214,18 +237,29 @@ describe("ghost-drift CLI", () => { expect(result.code).toBe(0); expect(result.stdout).toContain("# Ghost Advisory Review"); expect(result.stdout).toContain("diff location"); - expect(result.stdout).toContain("profile section"); + expect(result.stdout).toContain("patterns.yml composition pattern"); expect(result.stdout).toContain("survey evidence"); + expect(result.stdout).toContain("intent.md when relevant"); expect(result.stdout).toContain("precedent/example"); expect(result.stdout).toContain("repair"); }); }); -async function writeCheckPackage(dir: string): Promise { - const pkg = join(dir, ".ghost", "fingerprint"); +async function writeCheckPackage( + dir: string, + options: { checks?: boolean } = {}, +): Promise { + const pkg = join(dir, ".ghost"); await mkdir(pkg, { recursive: true }); + await writeFile( + join(pkg, "resources.yml"), + `schema: ghost.resources/v1 +id: cash-ios +primary: + target: . +`, + ); await writeFile(join(pkg, "map.md"), mapWithScopes()); - await writeFile(join(pkg, "profile.md"), profile()); await writeFile( join(pkg, "survey.json"), JSON.stringify({ @@ -237,6 +271,16 @@ async function writeCheckPackage(dir: string): Promise { ui_surfaces: [], }), ); + await writeFile( + join(pkg, "patterns.yml"), + `schema: ghost.patterns/v1 +id: cash-ios +surface_types: [] +composition_patterns: [] +`, + ); + if (options.checks === false) return; + await writeFile( join(pkg, "checks.yml"), `schema: ghost.checks/v1 @@ -263,39 +307,60 @@ checks: ); } -function profile(): string { - return `--- -id: cash-ios -source: llm -timestamp: 2026-05-06T00:00:00.000Z -palette: - dominant: [] - neutrals: { steps: [], count: 0 } - semantic: [] - saturationProfile: muted - contrast: moderate -spacing: { scale: [], baseUnit: null, regularity: 0 } -typography: - families: [] - sizeRamp: [] - weightDistribution: {} - lineHeightPattern: normal -surfaces: - borderRadii: [] - shadowComplexity: deliberate-none - borderUsage: minimal ---- - -# Character - -Restrained native Cash surfaces. - -# Signature - -Feature screens prefer token-backed controls. - -# Decisions -`; +async function writeComparableBundle( + pkg: string, + patternId: string, +): Promise { + await mkdir(pkg, { recursive: true }); + await writeFile( + join(pkg, "survey.json"), + JSON.stringify({ + schema: "ghost.survey/v2", + sources: [ + { id: patternId, target: ".", scanned_at: "2026-05-10T00:00:00Z" }, + ], + values: [ + { + id: `value_${patternId}`, + source: { target: ".", scanned_at: "2026-05-10T00:00:00Z" }, + kind: "spacing", + value: "8px", + raw: "p-2", + occurrences: 4, + files_count: 2, + }, + ], + tokens: [], + components: [], + ui_surfaces: [ + { + id: `surface_${patternId}`, + source: { target: ".", scanned_at: "2026-05-10T00:00:00Z" }, + name: patternId, + kind: "route", + locator: `/${patternId}`, + renderability: "source-only", + files: [`src/${patternId}.tsx`], + classification: { surface_type: "settings" }, + signals: { layout_patterns: [patternId] }, + }, + ], + }), + ); + await writeFile( + join(pkg, "patterns.yml"), + `schema: ghost.patterns/v1 +id: ${patternId} +surface_types: + - id: settings + preferred_patterns: [${patternId}] +composition_patterns: + - id: ${patternId} + surface_types: [settings] + evidence: + - locator: /${patternId} +`, + ); } function lendingPatch(line: string): string { diff --git a/packages/ghost-drift/test/scope-resolver.test.ts b/packages/ghost-drift/test/scope-resolver.test.ts index 3e73c0e4..d8d18499 100644 --- a/packages/ghost-drift/test/scope-resolver.test.ts +++ b/packages/ghost-drift/test/scope-resolver.test.ts @@ -14,7 +14,7 @@ describe("resolveFingerprintsForPaths", () => { ); await mkdir(join(dir, "fingerprints"), { recursive: true }); await writeFile(join(dir, "map.md"), mapWithScopes(), "utf-8"); - await writeFile(join(dir, "profile.md"), "parent", "utf-8"); + await writeFile(join(dir, "fingerprint.md"), "parent", "utf-8"); await writeFile(join(dir, "fingerprints", "checkout.md"), "child", "utf-8"); }); @@ -42,7 +42,7 @@ describe("resolveFingerprintsForPaths", () => { expect(resolution).toEqual({ changed_path: "packages/core/src/Button.tsx", - fingerprint_path: join(dir, "profile.md"), + fingerprint_path: join(dir, "fingerprint.md"), fallback: true, reason: "no-scope-match", }); @@ -55,7 +55,7 @@ describe("resolveFingerprintsForPaths", () => { expect(resolution).toEqual({ changed_path: "apps/portal/src/page/Home.tsx", - fingerprint_path: join(dir, "profile.md"), + fingerprint_path: join(dir, "fingerprint.md"), fallback: true, reason: "scope-fingerprint-missing", scope_id: "portal", diff --git a/packages/ghost-fingerprint/README.md b/packages/ghost-fingerprint/README.md index a39cd3c3..ce1d5272 100644 --- a/packages/ghost-fingerprint/README.md +++ b/packages/ghost-fingerprint/README.md @@ -1,56 +1,51 @@ # ghost-fingerprint -**Author and validate Ghost's repo-local design memory package. No LLM calls in any verb.** +**Author and validate Ghost's root repo-local fingerprint bundle. No LLM calls in any verb.** Canonical package: ```text -.ghost/fingerprint/ +.ghost/ + resources.yml map.md survey.json - profile.md + patterns.yml checks.yml + intent.md # optional ``` -Checks fail builds. Profile shapes judgment. Survey grounds both. The package is -the fingerprint. +Survey grounds the bundle. Patterns make composition operational. Optional +checks fail builds. Optional intent records human-authored or human-approved +product direction. ## Stages | Stage | Artifact | Schema | Role | |---|---|---|---| +| Resources | `resources.yml` | `ghost.resources/v1` | Declare references that define the product. | | Map | `map.md` | `ghost.map/v2` | Route changes to scopes and observable UI surfaces. | -| Survey | `survey.json` | `ghost.survey/v2` | Record factual values, tokens, components, and UI surface evidence. | -| Profile | `profile.md` | profile frontmatter/body | Provide non-enforcing design-language guidance. | -| Checks | `checks.yml` | `ghost.checks/v1` | Store human-promoted deterministic gates. | - -The CLI validates files, verifies profile-to-survey fidelity, inventories repo -signals, runs survey ops (`merge`, `fix-ids`, `summarize`, `catalog`, -`patterns`), diffs profiles, reports scan progress, and emits derived artifacts. - -The actual writing is done by your host agent using the recipes in this package. -The CLI is the checker. - -For deterministic drift gates and advisory review packets, see -**[`ghost-drift`](../ghost-drift)**. +| Survey | `survey.json` | `ghost.survey/v2` | Record factual values, tokens, components, surfaces, and composition observations. | +| Patterns | `patterns.yml` | `ghost.patterns/v1` | Codify surface types and composition grammar with evidence. | +| Checks | `checks.yml` | `ghost.checks/v1` | Store optional human-promoted deterministic gates. | +| Intent | `intent.md` | Markdown | Optional human authority. | ## Use ```bash -ghost-fingerprint init-package +ghost-fingerprint init-package --with-intent ghost-fingerprint inventory -ghost-fingerprint lint # defaults to .ghost/fingerprint +ghost-fingerprint lint # defaults to .ghost ghost-fingerprint scan-status -ghost-fingerprint survey fix-ids .ghost/fingerprint/survey.json -o .ghost/fingerprint/survey.json -ghost-fingerprint survey summarize .ghost/fingerprint/survey.json -ghost-fingerprint survey catalog .ghost/fingerprint/survey.json --kind color -ghost-fingerprint survey patterns .ghost/fingerprint/survey.json +ghost-fingerprint survey fix-ids .ghost/survey.json -o .ghost/survey.json +ghost-fingerprint survey summarize .ghost/survey.json +ghost-fingerprint survey catalog .ghost/survey.json --kind color +ghost-fingerprint survey patterns .ghost/survey.json -o .ghost/patterns.yml -ghost-fingerprint verify-profile .ghost/fingerprint/profile.md .ghost/fingerprint/survey.json --root . -ghost-fingerprint describe # defaults to .ghost/fingerprint/profile.md -ghost-fingerprint diff a.profile.md b.profile.md +ghost-fingerprint verify .ghost --root . +ghost-fingerprint describe # defaults to .ghost/intent.md +ghost-fingerprint diff a.fingerprint.md b.fingerprint.md ghost-fingerprint emit context-bundle ghost-fingerprint emit skill @@ -64,14 +59,16 @@ Zero config for every verb. No API key needed. import { initFingerprintPackage, lintFingerprintPackage, - parseFingerprint, - verifyProfile, + verifyFingerprintPackage, } from "ghost-fingerprint"; -const paths = await initFingerprintPackage(undefined, process.cwd()); +const paths = await initFingerprintPackage(undefined, process.cwd(), { + withIntent: true, +}); const lint = await lintFingerprintPackage(undefined, process.cwd()); -const { fingerprint } = parseFingerprint(await readFile(paths.profile, "utf8")); -const verify = verifyProfile(profileSource, survey, { root: "." }); +const verify = await verifyFingerprintPackage(undefined, process.cwd(), { + root: ".", +}); ``` ## Skill Bundle @@ -80,9 +77,9 @@ const verify = verifyProfile(profileSource, survey, { root: "." }); ghost-fingerprint emit skill ``` -The bundle ships recipes for scan, map, survey, profile, and schema reference. -Ask your agent to "scan this design language end-to-end" or "profile this design -language"; it will author package artifacts and use the CLI for validation. +The bundle ships recipes for scan, map, survey, patterns, and schema reference. +Ask your agent to "scan this design language end-to-end"; it will author package +artifacts and use the CLI for validation. ## Format Docs diff --git a/packages/ghost-fingerprint/package.json b/packages/ghost-fingerprint/package.json index 81e3441f..330f7750 100644 --- a/packages/ghost-fingerprint/package.json +++ b/packages/ghost-fingerprint/package.json @@ -1,7 +1,7 @@ { "name": "ghost-fingerprint", "version": "0.0.0", - "description": "Author and validate the .ghost/fingerprint design-memory package (map, survey, profile, checks)", + "description": "Author and validate the root .ghost design-memory bundle (resources, map, survey, patterns, checks, intent)", "license": "Apache-2.0", "author": "Block, Inc.", "repository": { diff --git a/packages/ghost-fingerprint/src/cli.ts b/packages/ghost-fingerprint/src/cli.ts index 1e87ef58..6cb5e549 100644 --- a/packages/ghost-fingerprint/src/cli.ts +++ b/packages/ghost-fingerprint/src/cli.ts @@ -4,10 +4,12 @@ import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { catalogSurveyValues, - type Fingerprint, formatSurveyCatalogMarkdown, formatSurveySummaryMarkdown, + type GhostPatternsDocument, lintGhostChecks, + lintGhostPatterns, + lintGhostResources, lintSurvey, mergeSurveys, recomputeSurveyIds, @@ -17,12 +19,12 @@ import { summarizeSurvey, } from "@ghost/core"; import { cac } from "cac"; -import { parse as parseYaml } from "yaml"; +import { parse as parseYaml, stringify as stringifyYaml } from "yaml"; import { diffFingerprints, formatLayout, formatSemanticDiff, - formatVerifyProfileReport, + formatVerifyFingerprintReport, initFingerprintPackage, inventory, layoutFingerprint, @@ -32,21 +34,21 @@ import { loadFingerprint, resolveFingerprintPackage, scanStatus, - verifyProfile, + verifyFingerprintPackage, } from "./core/index.js"; import { registerEmitCommand } from "./emit-command.js"; /** * Build the cac CLI for `ghost-fingerprint`. * - * Verbs author and validate the `.ghost/fingerprint/` package: - * `lint` (schema check, auto-detects file kind), `verify-profile` - * (profile-to-survey fidelity check), `describe` (section ranges + token - * estimates for profiles), `diff` (structural prose-level diff between two - * profiles), `emit` (derive review-command, context-bundle, or skill + * Verbs author and validate the root `.ghost/` fingerprint bundle: + * `lint` (schema check, auto-detects file kind), `verify` (cross-artifact + * fidelity), `describe` (section ranges + token estimates for intent or direct + * fingerprint markdown), `diff` (structural prose-level diff between direct + * fingerprint files), `emit` (derive review-command, context-bundle, or skill * artifacts), and `survey` operations for deterministic `ghost.survey/v2` * merge, ID repair, bounded summary output, derived value catalogs, and - * observed pattern summaries. + * operational pattern synthesis. * * Embedding-based comparison lives in `ghost-drift`. `diff` here is * text/structural — what decisions and palette roles changed — not @@ -59,7 +61,7 @@ export function buildCli(): ReturnType { cli .command( "lint [file]", - "Validate a fingerprint package, profile.md, map.md, survey.json, or checks.yml — defaults to .ghost/fingerprint", + "Validate a root fingerprint bundle, resources.yml, map.md, survey.json, patterns.yml, checks.yml, or markdown — defaults to .ghost", ) .option("--format ", "Output format: cli or json", { default: "cli" }) .action(async (path: string | undefined, opts) => { @@ -82,11 +84,15 @@ export function buildCli(): ReturnType { ? lintSurveyFile(raw) : kind === "map" ? lintMap(raw) - : kind === "checks" - ? lintChecksFile(raw) - : lintFingerprint(raw); - - if (kind === "profile" && hasExtends(raw) && report.errors === 0) { + : kind === "resources" + ? lintResourcesFile(raw) + : kind === "patterns" + ? lintPatternsFile(raw) + : kind === "checks" + ? lintChecksFile(raw) + : lintFingerprint(raw); + + if (kind === "fingerprint" && hasExtends(raw) && report.errors === 0) { try { await loadFingerprint(fileTarget, { noEmbeddingBackfill: true }); } catch (err) { @@ -114,22 +120,32 @@ export function buildCli(): ReturnType { cli .command( "init-package [dir]", - "Create a .ghost/fingerprint package skeleton (map.md, survey.json, profile.md, checks.yml)", + "Create a root .ghost fingerprint bundle skeleton (resources.yml, map.md, survey.json, patterns.yml, checks.yml)", + ) + .option( + "--with-intent", + "Also create optional intent.md for human-authored or human-approved intent", ) .option("--format ", "Output format: cli or json", { default: "cli" }) .action(async (dirArg: string | undefined, opts) => { try { - const paths = await initFingerprintPackage(dirArg, process.cwd()); + const paths = await initFingerprintPackage(dirArg, process.cwd(), { + withIntent: Boolean(opts.withIntent), + }); if (opts.format === "json") { process.stdout.write(`${JSON.stringify(paths, null, 2)}\n`); } else { process.stdout.write( `Initialized fingerprint package: ${paths.dir}\n`, ); + process.stdout.write(` resources.yml: ${paths.resources}\n`); process.stdout.write(` map.md: ${paths.map}\n`); process.stdout.write(` survey.json: ${paths.survey}\n`); - process.stdout.write(` profile.md: ${paths.profile}\n`); + process.stdout.write(` patterns.yml: ${paths.patterns}\n`); process.stdout.write(` checks.yml: ${paths.checks}\n`); + if (opts.withIntent) { + process.stdout.write(` intent.md: ${paths.intent}\n`); + } } process.exit(0); } catch (err) { @@ -140,18 +156,18 @@ export function buildCli(): ReturnType { } }); - // --- verify-profile --- + // --- verify --- cli .command( - "verify-profile ", - "Verify profile.md is faithful to its survey.json: palette values must be survey-backed", + "verify [dir]", + "Verify a root fingerprint bundle: resources are reachable, patterns are survey-backed, and checks reference known patterns.", ) .option( "--root ", - "Optional target root used by profile fidelity checks that need repo context", + "Optional target root used to resolve resources.yml local paths (default: cwd)", ) .option("--format ", "Output format: cli or json", { default: "cli" }) - .action(async (profilePath: string, surveyPath: string, opts) => { + .action(async (dirArg: string | undefined, opts) => { try { if (opts.format !== "cli" && opts.format !== "json") { console.error("Error: --format must be 'cli' or 'json'"); @@ -159,42 +175,14 @@ export function buildCli(): ReturnType { return; } - const profileTarget = resolve(process.cwd(), profilePath); - const surveyTarget = resolve(process.cwd(), surveyPath); - const [fingerprintRaw, surveyRaw] = await Promise.all([ - readFile(profileTarget, "utf-8"), - readFile(surveyTarget, "utf-8"), - ]); - - let survey: unknown; - try { - survey = JSON.parse(surveyRaw); - } catch (err) { - console.error( - `Error: ${surveyTarget} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`, - ); - process.exit(2); - return; - } - - let resolvedFingerprint: Fingerprint | undefined; - if (hasExtends(fingerprintRaw)) { - resolvedFingerprint = ( - await loadFingerprint(profileTarget, { - noEmbeddingBackfill: true, - }) - ).fingerprint; - } - - const report = verifyProfile(fingerprintRaw, survey, { + const report = await verifyFingerprintPackage(dirArg, process.cwd(), { root: opts.root ? resolve(process.cwd(), opts.root) : undefined, - resolvedFingerprint, }); if (opts.format === "json") { process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); } else { - process.stdout.write(formatVerifyProfileReport(report)); + process.stdout.write(formatVerifyFingerprintReport(report)); } process.exit(report.errors > 0 ? 1 : 0); @@ -210,7 +198,7 @@ export function buildCli(): ReturnType { cli .command( "scan-status [dir]", - "Report which fingerprint package stages have produced artifacts: map.md, survey.json, profile.md, checks.yml.", + "Report which root fingerprint bundle stages have produced artifacts: resources.yml, map.md, survey.json, patterns.yml, and optional checks.yml/intent.md.", ) .option( "--include-scopes", @@ -229,6 +217,9 @@ export function buildCli(): ReturnType { const fmt = (state: string) => state === "present" ? "present" : "missing"; process.stdout.write(`scan dir: ${status.dir}\n\n`); + process.stdout.write( + ` resources (resources.yml): ${fmt(status.resources.state)}\n`, + ); process.stdout.write( ` map (map.md): ${fmt(status.map.state)}\n`, ); @@ -236,11 +227,14 @@ export function buildCli(): ReturnType { ` survey (survey.json): ${fmt(status.survey.state)}\n`, ); process.stdout.write( - ` profile (profile.md): ${fmt(status.profile.state)}\n`, + ` patterns (patterns.yml): ${fmt(status.patterns.state)}\n`, ); process.stdout.write( ` checks (checks.yml): ${fmt(status.checks.state)}\n\n`, ); + process.stdout.write( + ` intent (intent.md): ${fmt(status.intent.state)}\n\n`, + ); if (status.recommended_next) { process.stdout.write( `next: run the ${status.recommended_next} stage\n`, @@ -295,15 +289,15 @@ export function buildCli(): ReturnType { // --- describe --- cli .command( - "describe [profile]", - "Print a section map of profile.md (line ranges + token estimates) so agents can selectively load only the sections they need.", + "describe [fingerprint]", + "Print a section map of intent.md or a direct fingerprint markdown file (line ranges + token estimates).", ) .option("--format ", "Output format: cli or json", { default: "cli" }) .action(async (path: string | undefined, opts) => { try { const target = path ? resolve(process.cwd(), path) - : resolveFingerprintPackage(undefined, process.cwd()).profile; + : resolveFingerprintPackage(undefined, process.cwd()).intent; const raw = await readFile(target, "utf-8"); const layout = layoutFingerprint(raw); if (opts.format === "json") { @@ -326,7 +320,7 @@ export function buildCli(): ReturnType { cli .command( "diff ", - "Structural diff between two profile.md files — what decisions, palette roles, and tokens changed (text-level, NOT embedding distance; for that, use `ghost-drift compare`).", + "Structural diff between two fingerprint.md files — what decisions, palette roles, and tokens changed (text-level, NOT embedding distance; for that, use `ghost-drift compare`).", ) .option("--format ", "Output format: cli or json", { default: "cli" }) .action(async (a: string, b: string, opts) => { @@ -367,8 +361,7 @@ export function buildCli(): ReturnType { ) .option( "--format ", - "survey summarize/catalog output format: markdown or json", - { default: "markdown" }, + "Output format: summarize/catalog use markdown or json; patterns use yaml, json, or markdown", ) .option( "--kind ", @@ -416,8 +409,9 @@ export function buildCli(): ReturnType { process.exit(2); return; } - if (op === "summarize" || op === "catalog" || op === "patterns") { - if (opts.format !== "markdown" && opts.format !== "json") { + const format = defaultSurveyFormat(op, opts.format); + if (op === "summarize" || op === "catalog") { + if (format !== "markdown" && format !== "json") { console.error( `Error: survey ${op} --format must be 'markdown' or 'json'`, ); @@ -425,6 +419,15 @@ export function buildCli(): ReturnType { return; } } + if (op === "patterns") { + if (format !== "yaml" && format !== "json" && format !== "markdown") { + console.error( + "Error: survey patterns --format must be 'yaml', 'json', or 'markdown'", + ); + process.exit(2); + return; + } + } if (op === "summarize") { if (!isSurveySummaryBudget(opts.budget)) { console.error( @@ -485,7 +488,7 @@ export function buildCli(): ReturnType { budget: opts.budget as SurveySummaryBudget, }); out = - opts.format === "json" + format === "json" ? `${JSON.stringify(summary, null, 2)}\n` : formatSurveySummaryMarkdown(summary); } else if (op === "catalog") { @@ -493,15 +496,12 @@ export function buildCli(): ReturnType { kind: typeof opts.kind === "string" ? opts.kind : undefined, }); out = - opts.format === "json" + format === "json" ? `${JSON.stringify(catalog, null, 2)}\n` : formatSurveyCatalogMarkdown(catalog); } else if (op === "patterns") { const patterns = summarizeSurveyPatterns(parsed[0]); - out = - opts.format === "json" - ? `${JSON.stringify(patterns, null, 2)}\n` - : formatSurveyPatternsMarkdown(patterns); + out = formatPatternsOutput(patterns, format); } else { const result = op === "merge" @@ -535,20 +535,25 @@ export function buildCli(): ReturnType { } /** - * Decide whether a file is a `profile.md`, `map.md`, `survey.json`, or - * `checks.yml`. JSON paths/contents route to the survey linter; markdown with - * `schema: ghost.map/v2` in its YAML frontmatter routes to the map linter; - * checks YAML routes to the checks linter; everything else stays on the profile - * path. + * Decide whether a file is a bundle artifact. JSON paths/contents route to + * the survey linter; markdown with `schema: ghost.map/v2` in frontmatter + * routes to the map linter; YAML schemas route to resources/patterns/checks; + * everything else stays on the direct fingerprint markdown path. */ function detectFileKind( path: string, raw: string, -): "survey" | "map" | "profile" | "checks" { +): "survey" | "map" | "fingerprint" | "checks" | "resources" | "patterns" { if (path.toLowerCase().endsWith(".json")) return "survey"; + if (path.toLowerCase().endsWith("resources.yml")) return "resources"; + if (path.toLowerCase().endsWith("resources.yaml")) return "resources"; + if (path.toLowerCase().endsWith("patterns.yml")) return "patterns"; + if (path.toLowerCase().endsWith("patterns.yaml")) return "patterns"; if (path.toLowerCase().endsWith(".yml")) return "checks"; if (path.toLowerCase().endsWith(".yaml")) return "checks"; if (raw.trimStart().startsWith("{")) return "survey"; + if (/^\s*schema:\s*ghost\.resources\/v1\b/m.test(raw)) return "resources"; + if (/^\s*schema:\s*ghost\.patterns\/v1\b/m.test(raw)) return "patterns"; if (/^\s*schema:\s*ghost\.checks\/v1\b/m.test(raw)) return "checks"; // Cheap markdown frontmatter sniff for `schema: ghost.map/v2`. We don't // parse YAML here; the linter does the heavy lift. @@ -558,7 +563,7 @@ function detectFileKind( if (/\bschema:\s*ghost\.map\/v2\b/.test(fm)) return "map"; } if (path.toLowerCase().endsWith("map.md")) return "map"; - return "profile"; + return "fingerprint"; } function lintSurveyFile(raw: string): SurveyLintReport { @@ -603,6 +608,48 @@ function lintChecksFile(raw: string): ReturnType { } } +function lintResourcesFile(raw: string): ReturnType { + try { + return lintGhostResources(parseYaml(raw)); + } catch (err) { + return { + issues: [ + { + severity: "error", + rule: "resources-not-yaml", + message: `resources file is not valid YAML: ${ + err instanceof Error ? err.message : String(err) + }`, + }, + ], + errors: 1, + warnings: 0, + info: 0, + }; + } +} + +function lintPatternsFile(raw: string): ReturnType { + try { + return lintGhostPatterns(parseYaml(raw)); + } catch (err) { + return { + issues: [ + { + severity: "error", + rule: "patterns-not-yaml", + message: `patterns file is not valid YAML: ${ + err instanceof Error ? err.message : String(err) + }`, + }, + ], + errors: 1, + warnings: 0, + info: 0, + }; + } +} + function writeLintReport( report: ReturnType, format: unknown, @@ -682,24 +729,23 @@ function surveyVerbName(op: string): string { return op; } -interface SurveyPatternSummary { - schema: "ghost.survey.patterns/v1"; - surfaces: number; - surface_types: Array<{ value: string; count: number; examples: string[] }>; - densities: Array<{ value: string; count: number; examples: string[] }>; - layout_shapes: Array<{ value: string; count: number; examples: string[] }>; - layout_patterns: Array<{ value: string; count: number; examples: string[] }>; - components: Array<{ value: string; count: number; examples: string[] }>; - examples: Array<{ name: string; locator: string; files: string[] }>; +function defaultSurveyFormat(op: string, format: unknown): string { + if (typeof format === "string") return format; + return op === "patterns" ? "yaml" : "markdown"; +} + +function formatPatternsOutput( + patterns: GhostPatternsDocument, + format: string, +): string { + if (format === "json") return `${JSON.stringify(patterns, null, 2)}\n`; + if (format === "markdown") return formatSurveyPatternsMarkdown(patterns); + return stringifyYaml(patterns); } -function summarizeSurveyPatterns(survey: Survey): SurveyPatternSummary { +function summarizeSurveyPatterns(survey: Survey): GhostPatternsDocument { const surfaceTypes = new Map(); - const densities = new Map(); - const layoutShapes = new Map(); const layoutPatterns = new Map(); - const components = new Map(); - const examples: SurveyPatternSummary["examples"] = []; for (const surface of survey.ui_surfaces) { const label = surface.locator || surface.name; @@ -707,80 +753,128 @@ function summarizeSurveyPatterns(survey: Survey): SurveyPatternSummary { if (classification?.surface_type) { addPattern(surfaceTypes, classification.surface_type, label); } - if (classification?.density) { - addPattern(densities, classification.density, label); - } - if (classification?.layout_shape) { - addPattern(layoutShapes, classification.layout_shape, label); - } for (const pattern of surface.signals?.layout_patterns ?? []) { - addPattern(layoutPatterns, pattern, label); + addPattern(layoutPatterns, pattern, label, surface); } - for (const component of surface.signals?.dominant_components ?? []) { - addPattern(components, component, label); - } - examples.push({ - name: surface.name, - locator: surface.locator, - files: surface.files.slice(0, 3), - }); } + const surfaceTypeRows = topPatterns(surfaceTypes).map((entry) => ({ + id: slug(entry.value), + title: entry.value, + signals: entry.examples, + preferred_patterns: preferredPatternsForSurfaceType(entry.value, survey), + evidence: evidenceForSurfaceType(entry.value, survey), + })); + const surfaceTypeIds = new Set(surfaceTypeRows.map((row) => row.id)); + return { - schema: "ghost.survey.patterns/v1", - surfaces: survey.ui_surfaces.length, - surface_types: topPatterns(surfaceTypes), - densities: topPatterns(densities), - layout_shapes: topPatterns(layoutShapes), - layout_patterns: topPatterns(layoutPatterns), - components: topPatterns(components), - examples: examples.slice(0, 12), + schema: "ghost.patterns/v1", + id: slug(survey.sources[0]?.id ?? "survey-patterns"), + surface_types: surfaceTypeRows, + composition_patterns: topPatterns(layoutPatterns).map((entry) => ({ + id: slug(entry.value), + title: entry.value, + surface_types: surfaceTypesForPattern(entry.value, survey).filter((id) => + surfaceTypeIds.has(id), + ), + frequency: entry.count, + confidence: + survey.ui_surfaces.length > 0 + ? Number( + Math.min(1, entry.count / survey.ui_surfaces.length).toFixed(2), + ) + : 0, + anatomy: { + ordered: anatomyForPattern(entry.value, survey), + }, + traits: traitsForPattern(entry.value, survey), + evidence: entry.evidence, + advisory: [ + "Use as advisory composition evidence; deterministic enforcement belongs in checks.yml.", + ], + })), + advisory: { + review_expectations: [ + "Identify the surface type before judging composition.", + "Cite matching composition_patterns[].evidence and survey.ui_surfaces evidence for advisory findings.", + "Treat intent.md as human authority when present.", + ], + }, }; } interface PatternAccumulator { count: number; examples: string[]; + evidence: Array<{ surface_id?: string; locator?: string; path?: string }>; } function addPattern( map: Map, value: string, example: string, + surface?: Survey["ui_surfaces"][number], ): void { - const current = map.get(value) ?? { count: 0, examples: [] }; + const current = map.get(value) ?? { count: 0, examples: [], evidence: [] }; current.count += 1; if (!current.examples.includes(example) && current.examples.length < 5) { current.examples.push(example); } + if (surface && current.evidence.length < 5) { + current.evidence.push({ + surface_id: surface.id, + locator: surface.locator, + ...(surface.files[0] ? { path: surface.files[0] } : {}), + }); + } map.set(value, current); } -function topPatterns( - map: Map, -): Array<{ value: string; count: number; examples: string[] }> { +function topPatterns(map: Map): Array<{ + value: string; + count: number; + examples: string[]; + evidence: Array<{ surface_id?: string; locator?: string; path?: string }>; +}> { return [...map.entries()] .map(([value, accumulator]) => ({ value, count: accumulator.count, examples: accumulator.examples, + evidence: accumulator.evidence, })) .sort((a, b) => b.count - a.count || a.value.localeCompare(b.value)); } -function formatSurveyPatternsMarkdown(summary: SurveyPatternSummary): string { - const lines = ["# Survey Patterns", "", `Surfaces: ${summary.surfaces}`, ""]; - appendPatternSection(lines, "Surface Types", summary.surface_types); - appendPatternSection(lines, "Densities", summary.densities); - appendPatternSection(lines, "Layout Shapes", summary.layout_shapes); - appendPatternSection(lines, "Layout Patterns", summary.layout_patterns); - appendPatternSection(lines, "Dominant Components", summary.components); - lines.push("## Examples", ""); - for (const example of summary.examples) { - lines.push( - `- ${example.name} (${example.locator}) — ${example.files.join(", ")}`, - ); - } +function formatSurveyPatternsMarkdown(summary: GhostPatternsDocument): string { + const lines = [ + "# Survey Patterns", + "", + `Schema: ${summary.schema}`, + `Surface types: ${summary.surface_types.length}`, + `Composition patterns: ${summary.composition_patterns.length}`, + "", + ]; + appendPatternSection( + lines, + "Surface Types", + summary.surface_types.map((surfaceType) => ({ + value: surfaceType.id, + count: surfaceType.evidence?.length ?? 0, + examples: surfaceType.signals ?? [], + })), + ); + appendPatternSection( + lines, + "Composition Patterns", + summary.composition_patterns.map((pattern) => ({ + value: pattern.id, + count: pattern.frequency ?? 0, + examples: + pattern.evidence?.map((entry) => entry.locator ?? entry.path ?? "") ?? + [], + })), + ); return `${lines.join("\n")}\n`; } @@ -800,6 +894,97 @@ function appendPatternSection( lines.push(""); } +function preferredPatternsForSurfaceType( + surfaceType: string, + survey: Survey, +): string[] { + const counts = new Map(); + for (const surface of survey.ui_surfaces) { + if (surface.classification?.surface_type !== surfaceType) continue; + for (const pattern of surface.signals?.layout_patterns ?? []) { + counts.set(slug(pattern), (counts.get(slug(pattern)) ?? 0) + 1); + } + } + return [...counts.entries()] + .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])) + .slice(0, 5) + .map(([id]) => id); +} + +function evidenceForSurfaceType( + surfaceType: string, + survey: Survey, +): Array<{ surface_id: string; locator: string; path?: string }> { + return survey.ui_surfaces + .filter((surface) => surface.classification?.surface_type === surfaceType) + .slice(0, 5) + .map((surface) => ({ + surface_id: surface.id, + locator: surface.locator, + ...(surface.files[0] ? { path: surface.files[0] } : {}), + })); +} + +function surfaceTypesForPattern(pattern: string, survey: Survey): string[] { + const types = new Set(); + for (const surface of survey.ui_surfaces) { + if (!surface.signals?.layout_patterns?.includes(pattern)) continue; + const surfaceType = surface.classification?.surface_type; + if (surfaceType) types.add(slug(surfaceType)); + } + return [...types].sort(); +} + +function anatomyForPattern(pattern: string, survey: Survey): string[] { + const counts = new Map(); + for (const surface of survey.ui_surfaces) { + if (!surface.signals?.layout_patterns?.includes(pattern)) continue; + for (const item of surface.composition?.anatomy ?? []) { + counts.set(item, (counts.get(item) ?? 0) + 1); + } + } + return [...counts.entries()] + .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])) + .map(([item]) => item); +} + +function traitsForPattern( + pattern: string, + survey: Survey, +): Record { + const densities = new Set(); + const layoutShapes = new Set(); + const components = new Set(); + for (const surface of survey.ui_surfaces) { + if (!surface.signals?.layout_patterns?.includes(pattern)) continue; + if (surface.classification?.density) { + densities.add(surface.classification.density); + } + if (surface.classification?.layout_shape) { + layoutShapes.add(surface.classification.layout_shape); + } + for (const component of surface.signals?.dominant_components ?? []) { + components.add(component); + } + } + return { + density: [...densities].sort(), + layout_shape: [...layoutShapes].sort(), + dominant_components: [...components].sort().slice(0, 8), + source_signal: [pattern], + }; +} + +function slug(value: string): string { + return ( + value + .toLowerCase() + .replace(/[^a-z0-9._-]+/g, "-") + .replace(/^-+/, "") + .replace(/-+$/, "") || "pattern" + ); +} + function readPackageVersion(): string { const here = dirname(fileURLToPath(import.meta.url)); const pkg = JSON.parse( diff --git a/packages/ghost-fingerprint/src/core/constants.ts b/packages/ghost-fingerprint/src/core/constants.ts index 9bdbf96e..6a5e7de7 100644 --- a/packages/ghost-fingerprint/src/core/constants.ts +++ b/packages/ghost-fingerprint/src/core/constants.ts @@ -1,14 +1,17 @@ /** Canonical directory for the Ghost fingerprint package. */ -export const FINGERPRINT_PACKAGE_DIR = ".ghost/fingerprint"; +export const FINGERPRINT_PACKAGE_DIR = ".ghost"; -/** Canonical filename for the non-enforcing design-language prior. */ -export const PROFILE_FILENAME = "profile.md"; +/** Canonical filename for scan resource references. */ +export const RESOURCES_FILENAME = "resources.yml"; -/** - * @deprecated Internal alias kept while older compare/evolution helpers are - * renamed. New user-facing flows must say `profile.md`. - */ -export const FINGERPRINT_FILENAME = PROFILE_FILENAME; +/** Canonical filename for operational composition grammar. */ +export const PATTERNS_FILENAME = "patterns.yml"; + +/** Optional filename for human-authored or human-approved intent. */ +export const INTENT_FILENAME = "intent.md"; + +/** Legacy direct fingerprint filename. Not part of the root package shape. */ +export const FINGERPRINT_FILENAME = "fingerprint.md"; /** Directory containing scoped fingerprint overlays. */ export const FINGERPRINTS_DIRNAME = "fingerprints"; diff --git a/packages/ghost-fingerprint/src/core/context/review-command.ts b/packages/ghost-fingerprint/src/core/context/review-command.ts index 4afac9c5..a1e565bc 100644 --- a/packages/ghost-fingerprint/src/core/context/review-command.ts +++ b/packages/ghost-fingerprint/src/core/context/review-command.ts @@ -22,7 +22,7 @@ export interface EmitReviewInput { * - **Checks-driven** (legacy programmatic input): when `fingerprint.checks[]` * is non-empty, group checks by computed perceptual severity and render a * Critical / Serious / Nit layout. - * - **Structured-fallback**: for normal profile.md inputs, + * - **Structured-fallback**: for normal fingerprint.md inputs, * emit the original palette/radius/spacing/typography sections * derived from frontmatter alone. Preserved verbatim so existing * fingerprints keep working through the v0 transition. @@ -173,7 +173,7 @@ function calibrationFooter(fp: Fingerprint, resolved: ResolvedCheck[]): string { "", "Color and font-family checks are loud (critical) by default. Shape, elevation, surface, and interactive-pattern checks are structural (serious). Spacing, density, motion-detail, and theming checks are rhythmic (nit).", "", - `Generated from a profile object (${(fp.checks ?? []).length} legacy checks). Re-run \`ghost-fingerprint emit review-command\` after package updates.`, + `Generated from a fingerprint object (${(fp.checks ?? []).length} legacy checks). Re-run \`ghost-fingerprint emit review-command\` after package updates.`, ); return lines.join("\n"); } @@ -206,7 +206,7 @@ function emitStructuredFallback(fp: Fingerprint): string { function structuredFallbackNotice(): string { return `## Calibration note -This profile has no embedded checks, so this command uses a coarse token fallback from palette, spacing, typography, and surfaces. Treat findings as lower-confidence than \`ghost-drift check\`; promote enforceable rules in \`.ghost/fingerprint/checks.yml\` when a pattern should become a gate.`; +This fingerprint has no embedded checks, so this command uses a coarse token fallback from palette, spacing, typography, and surfaces. Treat findings as lower-confidence than \`ghost-drift check\`; promote enforceable rules in \`.ghost/checks.yml\` when a pattern should become a gate.`; } function frontmatter(id: string): string { @@ -473,7 +473,7 @@ function footer(fp: Fingerprint): string { const count = fp.decisions?.length ?? 0; return `--- -Generated from \`profile.md\` (${count} decisions). Re-run \`ghost-fingerprint emit review-command\` after package updates.`; +Generated from \`fingerprint.md\` (${count} decisions). Re-run \`ghost-fingerprint emit review-command\` after package updates.`; } // --- helpers ------------------------------------------------------------ diff --git a/packages/ghost-fingerprint/src/core/context/tokens-css.ts b/packages/ghost-fingerprint/src/core/context/tokens-css.ts index 3d6d0c97..2a4eb8f9 100644 --- a/packages/ghost-fingerprint/src/core/context/tokens-css.ts +++ b/packages/ghost-fingerprint/src/core/context/tokens-css.ts @@ -1,7 +1,7 @@ import type { Fingerprint } from "@ghost/core"; export interface TokensCssOptions { - /** Source path (e.g. ".ghost/fingerprint/profile.md") — surfaced in the provenance header. */ + /** Source path (e.g. ".ghost/fingerprint.md") surfaced in the provenance header. */ sourcePath?: string; /** Generator version string — surfaced in the provenance header. */ generator?: string; @@ -89,7 +89,7 @@ export function buildTokensCss( } const generator = options.generator ?? "ghost"; - const source = options.sourcePath ?? ".ghost/fingerprint/profile.md"; + const source = options.sourcePath ?? ".ghost/fingerprint.md"; const timestamp = fingerprint.timestamp ?? new Date().toISOString(); const header = [ "/*", diff --git a/packages/ghost-fingerprint/src/core/context/writer.ts b/packages/ghost-fingerprint/src/core/context/writer.ts index 76d31c13..cfb1bc60 100644 --- a/packages/ghost-fingerprint/src/core/context/writer.ts +++ b/packages/ghost-fingerprint/src/core/context/writer.ts @@ -22,7 +22,7 @@ export interface WriteContextOptions { tokens?: boolean; /** Emit README.md. Default: false. */ readme?: boolean; - /** Emit only prompt.md (skips SKILL.md / profile.md / tokens.css). Default: false. */ + /** Emit only prompt.md (skips SKILL.md / fingerprint.md / tokens.css). Default: false. */ promptOnly?: boolean; /** Override the skill name. Default: derived from fingerprint.id. */ name?: string; @@ -34,7 +34,7 @@ export interface WriteContextOptions { * "prompt" → promptOnly:true */ format?: ContextFormat; - /** Source path (e.g. ".ghost/fingerprint/profile.md") — surfaced in generated file headers. */ + /** Source path (e.g. ".ghost/fingerprint.md") surfaced in generated file headers. */ sourcePath?: string; /** Generator version string — surfaced in generated file headers. */ generator?: string; @@ -67,7 +67,7 @@ export async function writeContextBundle( ); files.push(skillPath); - const exprPath = join(options.outDir, "profile.md"); + const exprPath = join(options.outDir, "fingerprint.md"); await writeFile(exprPath, serializeFingerprint(fingerprint)); files.push(exprPath); @@ -137,8 +137,8 @@ export function buildSkillMd( ): string { const description = buildSkillDescription(fingerprint, name); const fileList = [ - "- `profile.md` — non-enforcing design-language prior (YAML digest + Character/Signature/References/Decisions)", - "- `prompt.md` — generation prompt distilled from profile.md", + "- `fingerprint.md` — non-enforcing design-language fingerprint (YAML digest + Character/Signature/References/Decisions)", + "- `prompt.md` — generation prompt distilled from fingerprint.md", ...(includesCss ? [ "- `tokens.css` — CSS custom properties derived from fingerprint tokens", @@ -148,13 +148,13 @@ export function buildSkillMd( const body = `This skill grounds UI generation in the **${name}** design language. -Read \`profile.md\` first — it is the design-language prior. It has these layered sections: +Read \`fingerprint.md\` first — it is the design-language fingerprint. It has these layered sections: 1. **Character** — what this fingerprint is (one-paragraph summary in the body) 2. **Signature** — dominant moves and the recognizable output posture 3. **References** — local provenance and optional source material (frontmatter \`references\`) 4. **Decisions** — abstract design choices with evidence from the source (body \`### dimension\` blocks) -5. **Checks** — active gates from \`.ghost/fingerprint/checks.yml\` when available +5. **Checks** — active gates from \`.ghost/checks.yml\` when available When generating UI in this language: @@ -236,8 +236,8 @@ Generated by \`ghost-fingerprint emit context-bundle\`. Grounding material for A ## Files - \`SKILL.md\` — Agent Skill manifest (user-invocable) -- \`profile.md\` — non-enforcing design-language prior (YAML frontmatter + Character/Signature/Decisions) -- \`prompt.md\` — portable prompt distilled from the profile +- \`fingerprint.md\` — non-enforcing design-language fingerprint (YAML frontmatter + Character/Signature/Decisions) +- \`prompt.md\` — portable prompt distilled from the fingerprint - \`tokens.css\` — CSS custom properties derived from fingerprint tokens - \`README.md\` — this file @@ -245,9 +245,9 @@ Generated by \`ghost-fingerprint emit context-bundle\`. Grounding material for A **As a Claude Code / MCP skill:** point the client at this directory. The agent will read \`SKILL.md\` and follow its instructions. -**As context for any LLM:** load \`profile.md\` into the system prompt. For more explicit grounding, concatenate with \`tokens.css\`. +**As context for any LLM:** load \`fingerprint.md\` into the system prompt. For more explicit grounding, concatenate with \`tokens.css\`. -**Feedback loop:** ask your host agent to review the generated output against this profile and run \`ghost-drift check\` in the source repo when a full package is available. +**Feedback loop:** ask your host agent to review the generated output against this fingerprint and run \`ghost-drift check\` in the source repo when a full package is available. `; } @@ -256,7 +256,7 @@ function buildSkillDescription(fingerprint: Fingerprint, name: string): string { const traitPhrase = personality.length ? ` (${personality.slice(0, 3).join(", ")})` : ""; - return `Use this skill to generate UI in the ${name} design language${traitPhrase}. Contains the profile prior and token reference.`; + return `Use this skill to generate UI in the ${name} design language${traitPhrase}. Contains the fingerprint and token reference.`; } function defaultSkillName(fingerprint?: Fingerprint): string { diff --git a/packages/ghost-fingerprint/src/core/fingerprint-package.ts b/packages/ghost-fingerprint/src/core/fingerprint-package.ts index cffe30ac..74a648a9 100644 --- a/packages/ghost-fingerprint/src/core/fingerprint-package.ts +++ b/packages/ghost-fingerprint/src/core/fingerprint-package.ts @@ -3,6 +3,8 @@ import { join, resolve } from "node:path"; import { GHOST_CHECKS_FILENAME, lintGhostChecks, + lintGhostPatterns, + lintGhostResources, lintSurvey, MAP_FILENAME, type MapFrontmatter, @@ -10,17 +12,30 @@ import { SURVEY_FILENAME, } from "@ghost/core"; import { parse as parseYaml } from "yaml"; -import { FINGERPRINT_PACKAGE_DIR, PROFILE_FILENAME } from "./constants.js"; +import { + FINGERPRINT_FILENAME, + FINGERPRINT_PACKAGE_DIR, + INTENT_FILENAME, + PATTERNS_FILENAME, + RESOURCES_FILENAME, +} from "./constants.js"; import type { LintIssue, LintReport } from "./lint.js"; -import { lintFingerprint } from "./lint.js"; import { lintMap } from "./lint-map.js"; export interface FingerprintPackagePaths { dir: string; + resources: string; map: string; survey: string; - profile: string; + patterns: string; + /** Legacy direct markdown path; not part of the canonical root bundle. */ + fingerprint: string; checks: string; + intent: string; +} + +export interface InitFingerprintPackageOptions { + withIntent?: boolean; } export function resolveFingerprintPackage( @@ -30,25 +45,33 @@ export function resolveFingerprintPackage( const dir = resolve(cwd, dirArg ?? FINGERPRINT_PACKAGE_DIR); return { dir, + resources: join(dir, RESOURCES_FILENAME), map: join(dir, MAP_FILENAME), survey: join(dir, SURVEY_FILENAME), - profile: join(dir, PROFILE_FILENAME), + patterns: join(dir, PATTERNS_FILENAME), + fingerprint: join(dir, FINGERPRINT_FILENAME), checks: join(dir, GHOST_CHECKS_FILENAME), + intent: join(dir, INTENT_FILENAME), }; } export async function initFingerprintPackage( dirArg: string | undefined, cwd = process.cwd(), + options: InitFingerprintPackageOptions = {}, ): Promise { const paths = resolveFingerprintPackage(dirArg, cwd); await mkdir(paths.dir, { recursive: true }); const now = new Date().toISOString(); await Promise.all([ + writeFile(paths.resources, templateResources(), "utf-8"), writeFile(paths.map, templateMap(now), "utf-8"), writeFile(paths.survey, templateSurvey(now), "utf-8"), - writeFile(paths.profile, templateProfile(now), "utf-8"), + writeFile(paths.patterns, templatePatterns(), "utf-8"), writeFile(paths.checks, templateChecks(), "utf-8"), + ...(options.withIntent + ? [writeFile(paths.intent, templateIntent(), "utf-8")] + : []), ]); return paths; } @@ -60,10 +83,28 @@ export async function lintFingerprintPackage( const paths = resolveFingerprintPackage(dirArg, cwd); const issues: LintIssue[] = []; + const resourcesRaw = await readRequired( + paths.resources, + "resources.yml", + issues, + ); const mapRaw = await readRequired(paths.map, "map.md", issues); const surveyRaw = await readRequired(paths.survey, "survey.json", issues); - const profileRaw = await readRequired(paths.profile, "profile.md", issues); - const checksRaw = await readRequired(paths.checks, "checks.yml", issues); + const patternsRaw = await readRequired( + paths.patterns, + "patterns.yml", + issues, + ); + const checksRaw = await readOptional(paths.checks); + const intentRaw = await readOptional(paths.intent); + + if (resourcesRaw !== undefined) { + const resources = parseYamlSafe(resourcesRaw, "resources.yml", issues); + if (resources !== undefined) { + const resourcesReport = lintGhostResources(resources); + issues.push(...prefixIssues("resources.yml", resourcesReport.issues)); + } + } let mapFrontmatter: MapFrontmatter | undefined; if (mapRaw !== undefined) { @@ -80,9 +121,12 @@ export async function lintFingerprintPackage( } } - if (profileRaw !== undefined) { - const profileReport = lintFingerprint(profileRaw); - issues.push(...prefixIssues("profile.md", profileReport.issues)); + if (patternsRaw !== undefined) { + const patterns = parseYamlSafe(patternsRaw, "patterns.yml", issues); + if (patterns !== undefined) { + const patternsReport = lintGhostPatterns(patterns); + issues.push(...prefixIssues("patterns.yml", patternsReport.issues)); + } } if (checksRaw !== undefined) { @@ -93,6 +137,16 @@ export async function lintFingerprintPackage( } } + if (intentRaw !== undefined && intentRaw.trim().length === 0) { + issues.push({ + severity: "warning", + rule: "intent-empty", + message: + "intent.md is optional, but when present it should contain human-authored or human-approved intent.", + path: "intent.md", + }); + } + return finalize(issues); } @@ -114,6 +168,14 @@ async function readRequired( } } +async function readOptional(path: string): Promise { + try { + return await readFile(path, "utf-8"); + } catch { + return undefined; + } +} + function parseJson( raw: string, label: string, @@ -191,6 +253,26 @@ function finalize(issues: LintIssue[]): LintReport { }; } +function templateResources(): string { + return `schema: ghost.resources/v1 +id: local +primary: + target: . + paths: + - . +design_system: [] +surfaces: [] +screenshots: [] +docs: [] +resolvers: [] +upstreams: [] +include: + - "**/*" +exclude: + - "**/node_modules/**" +`; +} + function templateMap(now: string): string { return `--- schema: ghost.map/v2 @@ -255,38 +337,14 @@ function templateSurvey(now: string): string { )}\n`; } -function templateProfile(now: string): string { - return `--- +function templatePatterns(): string { + return `schema: ghost.patterns/v1 id: local -source: unknown -timestamp: ${now} -palette: - dominant: [] - neutrals: { steps: [], count: 0 } - semantic: [] - saturationProfile: muted - contrast: moderate -spacing: { scale: [], baseUnit: null, regularity: 0 } -typography: - families: [] - sizeRamp: [] - weightDistribution: {} - lineHeightPattern: normal -surfaces: - borderRadii: [] - shadowComplexity: deliberate-none - borderUsage: minimal ---- - -# Character - -No design-language prior has been authored yet. - -# Signature - -No recognizable signature has been authored yet. - -# Decisions +surface_types: [] +composition_patterns: [] +advisory: + review_expectations: + - Cite survey evidence and pattern evidence for composition findings. `; } @@ -296,3 +354,10 @@ id: local checks: [] `; } + +function templateIntent(): string { + return `# Intent + +This optional file is reserved for human-authored or human-approved product intent. +`; +} diff --git a/packages/ghost-fingerprint/src/core/index.ts b/packages/ghost-fingerprint/src/core/index.ts index a7c86444..988dfbc6 100644 --- a/packages/ghost-fingerprint/src/core/index.ts +++ b/packages/ghost-fingerprint/src/core/index.ts @@ -10,7 +10,7 @@ import { validateFrontmatter } from "./schema.js"; function assertMarkdownPath(path: string): void { if (!path.endsWith(".md")) { throw new Error( - `Fingerprint files must be Markdown (.md). Got: ${path}. The legacy JSON format has been removed — regenerate by running the profile recipe in your host agent (install with \`ghost-fingerprint emit skill\`).`, + `Fingerprint files must be Markdown (.md). Got: ${path}. The legacy JSON format has been removed — regenerate by running the fingerprint recipe in your host agent (install with \`ghost-fingerprint emit skill\`).`, ); } } @@ -24,7 +24,9 @@ export { FINGERPRINT_FILENAME, FINGERPRINT_PACKAGE_DIR, FINGERPRINTS_DIRNAME, - PROFILE_FILENAME, + INTENT_FILENAME, + PATTERNS_FILENAME, + RESOURCES_FILENAME, SCOPE_SURVEYS_DIRNAME, } from "./constants.js"; // --- Context (review-command + context-bundle) --- @@ -98,15 +100,17 @@ export { validateFrontmatter, } from "./schema.js"; export type { - VerifyProfileIssue, - VerifyProfileOptions, - VerifyProfileReport, - VerifyProfileSeverity, -} from "./verify-profile.js"; + VerifyFingerprintIssue, + VerifyFingerprintOptions, + VerifyFingerprintReport, + VerifyFingerprintSeverity, +} from "./verify-fingerprint.js"; export { - formatVerifyProfileReport, - verifyProfile, -} from "./verify-profile.js"; + formatVerifyFingerprintReport, + verifyFingerprint, +} from "./verify-fingerprint.js"; +export type { VerifyFingerprintPackageOptions } from "./verify-package.js"; +export { verifyFingerprintPackage } from "./verify-package.js"; export type { SerializeOptions } from "./writer.js"; export { serializeFingerprint } from "./writer.js"; diff --git a/packages/ghost-fingerprint/src/core/lint-map.ts b/packages/ghost-fingerprint/src/core/lint-map.ts index 13d98e5c..94d73487 100644 --- a/packages/ghost-fingerprint/src/core/lint-map.ts +++ b/packages/ghost-fingerprint/src/core/lint-map.ts @@ -84,7 +84,7 @@ export function lintMap(raw: string): MapLintReport { /** * Cross-field checks for `design_system`: * - At least one of `entry_files` or `derived_files` should be present - * (warning, not error — early in profiling there may be neither yet). + * (warning, not error — early in fingerprint authoring there may be neither yet). * - `upstream` is meaningful only when `token_source` is `external` or * `mixed`; flag a stray `upstream` paired with `inline` (or unset). * - When `token_source` is `external`, `upstream` should be set. diff --git a/packages/ghost-fingerprint/src/core/lint.ts b/packages/ghost-fingerprint/src/core/lint.ts index 88c7ce48..d4f0d3d0 100644 --- a/packages/ghost-fingerprint/src/core/lint.ts +++ b/packages/ghost-fingerprint/src/core/lint.ts @@ -39,8 +39,8 @@ export interface LintOptions { * * Under schema 3 the body/frontmatter partition is enforced by zod-strict. * Lint adds softer rules: orphan prose (body block with no frontmatter - * entry), missing rationale (frontmatter entry with no body block), - * legacy `**Evidence:**` bullets in the body, and broken palette citations. + * entry), missing rationale (frontmatter entry with no body block), malformed + * Decisions sections, missing body evidence, and broken palette citations. */ export function lintFingerprint( raw: string, @@ -68,6 +68,7 @@ export function lintFingerprint( checkSchemaValidity(rawYaml, rawIssues); checkDecisionPartition(fingerprint, body, rawIssues); + checkDecisionBodyShape(bodyText, body, rawIssues); checkStrayEvidenceInBody(bodyText, rawIssues); checkEvidenceHexes(fingerprint, rawIssues); checkUnusedPalette(fingerprint, rawIssues); @@ -175,6 +176,56 @@ function checkStrayEvidenceInBody( // no-op; body evidence is canonical as of schema 5. } +function checkDecisionBodyShape( + bodyText: string, + body: BodyData, + issues: LintIssue[], +): void { + const decisionsSection = h1SectionBody(bodyText, "Decisions"); + if ( + decisionsSection?.trim() && + !(body.decisions?.length ?? 0) && + !/^###\s+/m.test(decisionsSection) + ) { + issues.push({ + severity: "warning", + rule: "missing-decision-headings", + message: + "`# Decisions` has prose but no `### ` blocks, so no decisions are parseable.", + path: "decisions", + }); + } + + (body.decisions ?? []).forEach((decision, index) => { + if (decision.evidence?.length) return; + issues.push({ + severity: "warning", + rule: "missing-evidence", + message: `Decision \`${decision.dimension}\` has no \`**Evidence:**\` bullet list.`, + path: `decisions[${index}].evidence`, + }); + }); +} + +function h1SectionBody(bodyText: string, heading: string): string | null { + const lines = bodyText.split(/\r?\n/); + const target = heading.toLowerCase(); + const buf: string[] = []; + let collecting = false; + + for (const line of lines) { + const match = /^#\s+(.*?)\s*$/.exec(line); + if (match) { + if (collecting) break; + collecting = match[1]?.toLowerCase() === target; + continue; + } + if (collecting) buf.push(line); + } + + return collecting || buf.length ? buf.join("\n") : null; +} + const HEX_RE = /#[0-9a-f]{3,8}\b/gi; function checkEvidenceHexes(fp: Fingerprint, issues: LintIssue[]): void { diff --git a/packages/ghost-fingerprint/src/core/scan-status.ts b/packages/ghost-fingerprint/src/core/scan-status.ts index 19cc0751..d0917012 100644 --- a/packages/ghost-fingerprint/src/core/scan-status.ts +++ b/packages/ghost-fingerprint/src/core/scan-status.ts @@ -2,6 +2,8 @@ import { readFile, stat } from "node:fs/promises"; import { join, resolve } from "node:path"; import { GHOST_CHECKS_FILENAME, + GHOST_PATTERNS_FILENAME, + GHOST_RESOURCES_FILENAME, getEffectiveMapScopes, MAP_FILENAME, type MapFrontmatter, @@ -11,7 +13,7 @@ import { import { parse as parseYaml } from "yaml"; import { FINGERPRINTS_DIRNAME, - PROFILE_FILENAME, + INTENT_FILENAME, SCOPE_SURVEYS_DIRNAME, } from "./constants.js"; @@ -31,7 +33,7 @@ export interface ScanStageReport { path: string; } -export type ScanStage = "map" | "survey" | "profile" | "checks"; +export type ScanStage = "resources" | "map" | "survey" | "patterns"; export interface ScanScopeReport { id: string; @@ -49,16 +51,19 @@ export interface ScanStatusOptions { export interface ScanStatus { /** Absolute path to the scan directory. */ dir: string; + resources: ScanStageReport; map: ScanStageReport; survey: ScanStageReport; - profile: ScanStageReport; + patterns: ScanStageReport; checks: ScanStageReport; + intent: ScanStageReport; scopes?: ScanScopeReport[]; scope_error?: string; /** - * The next stage an orchestrator should run, or `null` if every stage - * is `present`. Stages run in order: map → survey → profile → checks. - * The recommendation surfaces the first stage in `missing` state. + * The next stage an orchestrator should run, or `null` if every required + * stage is `present`. Stages run in order: + * resources → map → survey → patterns. `checks.yml` and `intent.md` are + * reported but optional, so they never block completion. */ recommended_next: ScanStage | null; } @@ -68,10 +73,12 @@ export interface ScanStatus { * * Existence-only check today. The artifacts checked are: * - * - map → `map.md` - * - survey → `survey.json` - * - profile → `profile.md` - * - checks → `checks.yml` + * - resources → `resources.yml` + * - map → `map.md` + * - survey → `survey.json` + * - patterns → `patterns.yml` + * - checks → optional `checks.yml` + * - intent → optional `intent.md` * * Hash-keyed freshness (`.scan-meta.json` with input/output hashes per * stage) is the planned enhancement. For now, orchestrators that want @@ -83,19 +90,33 @@ export async function scanStatus( options: ScanStatusOptions = {}, ): Promise { const dir = resolve(dirPath); + const resourcesPath = resolve(dir, GHOST_RESOURCES_FILENAME); const mapPath = resolve(dir, MAP_FILENAME); const surveyPath = resolve(dir, SURVEY_FILENAME); - const profilePath = resolve(dir, PROFILE_FILENAME); + const patternsPath = resolve(dir, GHOST_PATTERNS_FILENAME); const checksPath = resolve(dir, GHOST_CHECKS_FILENAME); - - const [mapPresent, surveyPresent, profilePresent, checksPresent] = - await Promise.all([ - pathExists(mapPath), - pathExists(surveyPath), - pathExists(profilePath), - pathExists(checksPath), - ]); - + const intentPath = resolve(dir, INTENT_FILENAME); + + const [ + resourcesPresent, + mapPresent, + surveyPresent, + patternsPresent, + checksPresent, + intentPresent, + ] = await Promise.all([ + pathExists(resourcesPath), + pathExists(mapPath), + pathExists(surveyPath), + pathExists(patternsPath), + pathExists(checksPath), + pathExists(intentPath), + ]); + + const resources: ScanStageReport = { + state: resourcesPresent ? "present" : "missing", + path: resourcesPath, + }; const map: ScanStageReport = { state: mapPresent ? "present" : "missing", path: mapPath, @@ -104,27 +125,33 @@ export async function scanStatus( state: surveyPresent ? "present" : "missing", path: surveyPath, }; - const profile: ScanStageReport = { - state: profilePresent ? "present" : "missing", - path: profilePath, + const patterns: ScanStageReport = { + state: patternsPresent ? "present" : "missing", + path: patternsPath, }; const checks: ScanStageReport = { state: checksPresent ? "present" : "missing", path: checksPath, }; + const intent: ScanStageReport = { + state: intentPresent ? "present" : "missing", + path: intentPath, + }; let recommended_next: ScanStage | null = null; - if (map.state === "missing") recommended_next = "map"; + if (resources.state === "missing") recommended_next = "resources"; + else if (map.state === "missing") recommended_next = "map"; else if (survey.state === "missing") recommended_next = "survey"; - else if (profile.state === "missing") recommended_next = "profile"; - else if (checks.state === "missing") recommended_next = "checks"; + else if (patterns.state === "missing") recommended_next = "patterns"; const status: ScanStatus = { dir, + resources, map, survey, - profile, + patterns, checks, + intent, recommended_next, }; diff --git a/packages/ghost-fingerprint/src/core/verify-profile.ts b/packages/ghost-fingerprint/src/core/verify-fingerprint.ts similarity index 84% rename from packages/ghost-fingerprint/src/core/verify-profile.ts rename to packages/ghost-fingerprint/src/core/verify-fingerprint.ts index fcd1a9b9..47c6d2e7 100644 --- a/packages/ghost-fingerprint/src/core/verify-profile.ts +++ b/packages/ghost-fingerprint/src/core/verify-fingerprint.ts @@ -3,10 +3,10 @@ import { lintSurvey } from "@ghost/core"; import { lintFingerprint } from "./lint.js"; import { parseFingerprint } from "./parser.js"; -export type VerifyProfileSeverity = "error" | "warning" | "info"; +export type VerifyFingerprintSeverity = "error" | "warning" | "info"; -export interface VerifyProfileIssue { - severity: VerifyProfileSeverity; +export interface VerifyFingerprintIssue { + severity: VerifyFingerprintSeverity; rule: string; message: string; path?: string; @@ -14,14 +14,14 @@ export interface VerifyProfileIssue { actual?: unknown; } -export interface VerifyProfileReport { - issues: VerifyProfileIssue[]; +export interface VerifyFingerprintReport { + issues: VerifyFingerprintIssue[]; errors: number; warnings: number; info: number; } -export interface VerifyProfileOptions { +export interface VerifyFingerprintOptions { root?: string; /** * Resolved fingerprint after applying `extends:`. CLI callers should pass @@ -42,17 +42,17 @@ const HIGH_SALIENCE_ROLE_TOKENS = [ const HIGH_SALIENCE_VALUE_THRESHOLD = 5; /** - * Deterministically verify that a profiled fingerprint is faithful to the + * Deterministically verify that a fingerprinted design language is faithful to the * survey that produced it. `lint` remains the shape/schema gate; this verifier * checks scan-stage provenance for the non-enforcing design-language prior. * Enforceable checks live in `checks.yml` and are validated separately. */ -export function verifyProfile( +export function verifyFingerprint( fingerprintRaw: string, surveyInput: unknown, - options: VerifyProfileOptions = {}, -): VerifyProfileReport { - const issues: VerifyProfileIssue[] = []; + options: VerifyFingerprintOptions = {}, +): VerifyFingerprintReport { + const issues: VerifyFingerprintIssue[] = []; const fingerprintLint = lintFingerprint(fingerprintRaw); issues.push( @@ -100,7 +100,9 @@ export function verifyProfile( return finalize(issues); } -export function formatVerifyProfileReport(report: VerifyProfileReport): string { +export function formatVerifyFingerprintReport( + report: VerifyFingerprintReport, +): string { const lines: string[] = []; for (const issue of report.issues) { const prefix = @@ -127,13 +129,13 @@ export function formatVerifyProfileReport(report: VerifyProfileReport): string { function fromLintIssue( issue: { - severity: VerifyProfileSeverity; + severity: VerifyFingerprintSeverity; rule: string; message: string; path?: string; }, source: "fingerprint" | "survey", -): VerifyProfileIssue { +): VerifyFingerprintIssue { return { severity: issue.severity, rule: `${source}/${issue.rule}`, @@ -189,34 +191,35 @@ function collectSurveyEvidence(survey: Survey): SurveyValueEvidence { survey.values.forEach((row, index) => { const path = `survey.values[${index}]`; + const kind = canonicalSurveyValueKind(row); const entry: SurveyValueEvidenceRow = { - kind: row.kind, + kind, value: row.value, occurrences: row.occurrences, files_count: row.files_count, path: `${path}.value`, }; - if (row.kind === "color") { + if (kind === "color") { add(row.value, `${path}.value`); const spec = row.spec; if (isRecord(spec) && typeof spec.hex === "string") { add(spec.hex, `${path}.spec.hex`); } entry.color = firstHexColor(row.value) ?? specHex(row.spec); - } else if (row.kind === "spacing") { + } else if (kind === "spacing") { const scalar = rowScalarPx(row); if (scalar !== null) { addNumberEvidence(evidence.spacing, scalar, `${path}.value`); entry.scalarPx = scalar; } - } else if (row.kind === "radius") { + } else if (kind === "radius") { const scalar = rowScalarPx(row); if (scalar !== null) { addNumberEvidence(evidence.radii, scalar, `${path}.value`); entry.scalarPx = scalar; } - } else if (row.kind === "typography") { + } else if (kind === "typography") { const family = rowTypographyFamily(row); if (family) { addTextEvidence(evidence.typographyFamilies, family, `${path}.value`); @@ -232,7 +235,7 @@ function collectSurveyEvidence(survey: Survey): SurveyValueEvidence { addNumberEvidence(evidence.typographyWeights, weight, `${path}.value`); entry.typographyWeight = weight; } - } else if (row.kind === "shadow") { + } else if (kind === "shadow") { addTextEvidence(evidence.shadowValues, row.value, `${path}.value`); } evidence.rows.push(entry); @@ -248,7 +251,7 @@ function collectSurveyEvidence(survey: Survey): SurveyValueEvidence { function checkPaletteProvenance( fingerprint: Fingerprint, colorEvidence: Map, - issues: VerifyProfileIssue[], + issues: VerifyFingerprintIssue[], ): void { fingerprint.palette.dominant.forEach((color, index) => { checkPaletteColor( @@ -280,7 +283,7 @@ function checkPaletteColor( value: string, path: string, colorEvidence: Map, - issues: VerifyProfileIssue[], + issues: VerifyFingerprintIssue[], ): void { const normalized = normalizeHexColor(value); if (!normalized) { @@ -306,7 +309,7 @@ function checkPaletteColor( function checkRoleTokenAgreement( fingerprint: Fingerprint, survey: Survey, - issues: VerifyProfileIssue[], + issues: VerifyFingerprintIssue[], ): void { const paletteByRole = new Map(); collectSemanticPalette(fingerprint.palette.dominant, "dominant").forEach( @@ -371,7 +374,7 @@ function addPaletteRole( function checkStructuredValueProvenance( fingerprint: Fingerprint, evidence: SurveyValueEvidence, - issues: VerifyProfileIssue[], + issues: VerifyFingerprintIssue[], ): void { fingerprint.spacing.scale.forEach((value, index) => { checkNumberEvidence( @@ -442,7 +445,7 @@ function checkNumberEvidence( rule: string, message: string, evidence: Map, - issues: VerifyProfileIssue[], + issues: VerifyFingerprintIssue[], decimals = 3, ): void { const key = numberKey(value, decimals); @@ -460,7 +463,7 @@ function checkNumberEvidence( function checkShadowPosture( shadowComplexity: Fingerprint["surfaces"]["shadowComplexity"], evidence: SurveyValueEvidence, - issues: VerifyProfileIssue[], + issues: VerifyFingerprintIssue[], ): void { const distinct = evidence.shadowValues.size; const matches = @@ -488,7 +491,7 @@ function checkShadowPosture( function checkHighSalienceOmissions( fingerprint: Fingerprint, evidence: SurveyValueEvidence, - issues: VerifyProfileIssue[], + issues: VerifyFingerprintIssue[], ): void { const fingerprintValues = { colors: new Set([ @@ -627,10 +630,18 @@ function addTextEvidence( function rowScalarPx(row: ValueRow): number | null { const spec = row.spec; - if (isRecord(spec) && typeof spec.scalar === "number") { - const unit = typeof spec.unit === "string" ? spec.unit : "px"; - const px = scalarUnitToPx(spec.scalar, unit); - if (px !== null) return px; + if (isRecord(spec)) { + const scalar = + typeof spec.scalar === "number" + ? spec.scalar + : typeof spec.number === "number" + ? spec.number + : null; + if (scalar !== null) { + const unit = typeof spec.unit === "string" ? spec.unit : "px"; + const px = scalarUnitToPx(scalar, unit); + if (px !== null) return px; + } } return parseLengthPx(row.value); } @@ -638,6 +649,9 @@ function rowScalarPx(row: ValueRow): number | null { function rowTypographyFamily(row: ValueRow): string | null { const spec = row.spec; if (isRecord(spec) && typeof spec.family === "string") return spec.family; + const declaredFamily = declarationValue(row.value, "font-family"); + if (declaredFamily) return declaredFamily; + if (looksLikeDeclaration(row.value)) return null; if (!parseLengthPx(row.value) && rowTypographyWeight(row) === null) { return row.value; } @@ -653,8 +667,9 @@ function rowTypographySizePx(row: ValueRow): number | null { return scalarUnitToPx(scalar, unit); } } - if (/^[1-9]00$/.test(row.value.trim())) return null; - return parseLengthPx(row.value); + const value = declarationValue(row.value, "font-size") ?? row.value; + if (/^[1-9]00$/.test(value.trim())) return null; + return parseLengthPx(value); } function rowTypographyWeight(row: ValueRow): number | null { @@ -663,26 +678,87 @@ function rowTypographyWeight(row: ValueRow): number | null { const parsed = Number(spec.weight); return Number.isFinite(parsed) ? parsed : null; } - if (/^[1-9]00$/.test(row.value.trim())) return Number(row.value.trim()); + const value = declarationValue(row.value, "font-weight") ?? row.value; + if (/^[1-9]00$/.test(value.trim())) return Number(value.trim()); return null; } function scalarUnitToPx(scalar: number, unit: string): number | null { const normalized = unit.trim().toLowerCase(); if (normalized === "px") return scalar; + if (normalized === "dp" || normalized === "sp") return scalar; if (normalized === "rem" || normalized === "em") return scalar * 16; if (normalized === "") return scalar; return null; } function parseLengthPx(value: string): number | null { - const match = value.trim().match(/^(-?\d+(?:\.\d+)?)(px|rem|em)?$/i); + const match = value.trim().match(/^(-?\d+(?:\.\d+)?)(px|rem|em|dp|sp)?$/i); if (!match) return null; const scalar = Number(match[1]); const unit = match[2] ?? "px"; return scalarUnitToPx(scalar, unit); } +function canonicalSurveyValueKind(row: ValueRow): string { + const raw = row as ValueRow & { category?: unknown }; + const kind = + typeof raw.kind === "string" ? raw.kind.trim().toLowerCase() : ""; + const category = + typeof raw.category === "string" ? raw.category.trim().toLowerCase() : ""; + + if (isCanonicalValueKind(kind)) return kind; + if (isCanonicalValueKind(category)) return category; + if ( + category === "color" && + ["hex-color", "rgba-color", "keyword"].includes(kind) + ) { + return "color"; + } + if ( + category === "spacing" && + ["length", "keyword", "number"].includes(kind) + ) { + return "spacing"; + } + if (category === "radius" && ["length", "number"].includes(kind)) { + return "radius"; + } + if ( + category === "typography" && + ["font-stack", "length", "number", "keyword"].includes(kind) + ) { + return "typography"; + } + if (category === "shadow") return "shadow"; + return kind || category; +} + +function isCanonicalValueKind(kind: string): boolean { + return [ + "color", + "spacing", + "typography", + "radius", + "shadow", + "breakpoint", + "motion", + "layout-primitive", + ].includes(kind); +} + +function declarationValue(value: string, property: string): string | null { + const escaped = property.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const match = value + .trim() + .match(new RegExp(`^${escaped}\\s*:\\s*(.+)$`, "i")); + return match ? (match[1]?.trim() ?? null) : null; +} + +function looksLikeDeclaration(value: string): boolean { + return /^[a-z-]+\s*:/i.test(value.trim()); +} + function specHex(spec: unknown): string | undefined { if (isRecord(spec) && typeof spec.hex === "string") { return normalizeHexColor(spec.hex) ?? undefined; @@ -756,7 +832,7 @@ function compareStrings(a: string, b: string): number { return a.localeCompare(b); } -function finalize(issues: VerifyProfileIssue[]): VerifyProfileReport { +function finalize(issues: VerifyFingerprintIssue[]): VerifyFingerprintReport { let errors = 0; let warnings = 0; let info = 0; diff --git a/packages/ghost-fingerprint/src/core/verify-package.ts b/packages/ghost-fingerprint/src/core/verify-package.ts new file mode 100644 index 00000000..71a8b269 --- /dev/null +++ b/packages/ghost-fingerprint/src/core/verify-package.ts @@ -0,0 +1,298 @@ +import { access, readFile } from "node:fs/promises"; +import { isAbsolute, resolve } from "node:path"; +import type { + GhostCheck, + GhostPatternsDocument, + GhostResourcesDocument, + Survey, +} from "@ghost/core"; +import { parse as parseYaml } from "yaml"; +import { + lintFingerprintPackage, + resolveFingerprintPackage, +} from "./fingerprint-package.js"; +import type { + VerifyFingerprintIssue, + VerifyFingerprintReport, +} from "./verify-fingerprint.js"; + +export interface VerifyFingerprintPackageOptions { + root?: string; +} + +export async function verifyFingerprintPackage( + dirArg: string | undefined, + cwd = process.cwd(), + options: VerifyFingerprintPackageOptions = {}, +): Promise { + const paths = resolveFingerprintPackage(dirArg, cwd); + const root = resolve(cwd, options.root ?? "."); + const issues: VerifyFingerprintIssue[] = []; + + const packageLint = await lintFingerprintPackage(dirArg, cwd); + issues.push( + ...packageLint.issues.map((issue) => ({ + severity: issue.severity, + rule: `package/${issue.rule}`, + message: issue.message, + path: issue.path, + })), + ); + if (packageLint.errors > 0) return finalize(issues); + + const [resources, survey, patterns, checks] = await Promise.all([ + readYaml(paths.resources, "resources.yml", issues), + readJson(paths.survey, "survey.json", issues), + readYaml(paths.patterns, "patterns.yml", issues), + readOptionalYaml<{ checks?: GhostCheck[] }>( + paths.checks, + "checks.yml", + issues, + ), + ]); + + if (resources) { + await verifyResourcesReachable(resources, root, issues); + } + + if (survey && patterns) { + verifyPatternEvidence(patterns, survey, issues); + verifyPatternSurfaceTypes(patterns, survey, issues); + } + + if (patterns && checks?.checks) { + verifyCheckPatternReferences(patterns, checks.checks, issues); + } + + return finalize(issues); +} + +async function readYaml( + path: string, + label: string, + issues: VerifyFingerprintIssue[], +): Promise { + try { + return parseYaml(await readFile(path, "utf-8")) as T; + } catch (err) { + issues.push({ + severity: "error", + rule: "verify-yaml-read-failed", + message: `${label} could not be read as YAML: ${ + err instanceof Error ? err.message : String(err) + }`, + path: label, + }); + return undefined; + } +} + +async function readOptionalYaml( + path: string, + label: string, + issues: VerifyFingerprintIssue[], +): Promise { + try { + return parseYaml(await readFile(path, "utf-8")) as T; + } catch (err) { + if (isMissingFileError(err)) return undefined; + issues.push({ + severity: "error", + rule: "verify-yaml-read-failed", + message: `${label} could not be read as YAML: ${ + err instanceof Error ? err.message : String(err) + }`, + path: label, + }); + return undefined; + } +} + +async function readJson( + path: string, + label: string, + issues: VerifyFingerprintIssue[], +): Promise { + try { + return JSON.parse(await readFile(path, "utf-8")) as T; + } catch (err) { + issues.push({ + severity: "error", + rule: "verify-json-read-failed", + message: `${label} could not be read as JSON: ${ + err instanceof Error ? err.message : String(err) + }`, + path: label, + }); + return undefined; + } +} + +async function verifyResourcesReachable( + resources: GhostResourcesDocument, + root: string, + issues: VerifyFingerprintIssue[], +): Promise { + const refs: Array< + readonly [ + string, + { + target?: string; + paths?: string[]; + }, + ] + > = [ + ["primary", resources.primary], + ...(resources.design_system ?? []).map( + (ref, index) => [`design_system[${index}]`, ref] as const, + ), + ...(resources.surfaces ?? []).map( + (ref, index) => [`surfaces[${index}]`, ref] as const, + ), + ...(resources.screenshots ?? []).map( + (ref, index) => [`screenshots[${index}]`, ref] as const, + ), + ...(resources.docs ?? []).map( + (ref, index) => [`docs[${index}]`, ref] as const, + ), + ...(resources.resolvers ?? []).map( + (ref, index) => [`resolvers[${index}]`, ref] as const, + ), + ...(resources.upstreams ?? []).map( + (ref, index) => [`upstreams[${index}]`, ref] as const, + ), + ]; + + for (const [label, ref] of refs) { + const target = + "target" in ref && typeof ref.target === "string" ? ref.target : ""; + const candidates = [ + ...(target && isLocalTarget(target) ? [target] : []), + ...(ref.paths ?? []), + ]; + for (const candidate of candidates) { + const path = isAbsolute(candidate) ? candidate : resolve(root, candidate); + if (await pathExists(path)) continue; + issues.push({ + severity: "warning", + rule: "resource-unreachable", + message: `resource path '${candidate}' could not be resolved from ${root}.`, + path: `resources.yml.${label}`, + }); + } + } +} + +function verifyPatternEvidence( + patterns: GhostPatternsDocument, + survey: Survey, + issues: VerifyFingerprintIssue[], +): void { + patterns.composition_patterns.forEach((pattern, index) => { + const evidence = pattern.evidence ?? []; + if (evidence.length === 0) { + issues.push({ + severity: "error", + rule: "pattern-evidence-missing", + message: `composition pattern '${pattern.id}' has no survey evidence.`, + path: `patterns.yml.composition_patterns[${index}].evidence`, + }); + return; + } + + const supported = evidence.some((entry) => + survey.ui_surfaces.some((surface) => { + if (entry.surface_id && surface.id === entry.surface_id) return true; + if (entry.locator && surface.locator === entry.locator) return true; + if (entry.path && surface.files.includes(entry.path)) return true; + return false; + }), + ); + if (!supported) { + issues.push({ + severity: "error", + rule: "pattern-evidence-unbacked", + message: `composition pattern '${pattern.id}' evidence does not match any survey surface id, locator, or file.`, + path: `patterns.yml.composition_patterns[${index}].evidence`, + }); + } + }); +} + +function verifyPatternSurfaceTypes( + patterns: GhostPatternsDocument, + survey: Survey, + issues: VerifyFingerprintIssue[], +): void { + const surveyedTypes = new Set( + survey.ui_surfaces + .map((surface) => surface.classification?.surface_type) + .filter((value): value is string => Boolean(value)), + ); + patterns.surface_types.forEach((surfaceType, index) => { + if (surveyedTypes.size === 0 || surveyedTypes.has(surfaceType.id)) return; + issues.push({ + severity: "warning", + rule: "surface-type-unobserved", + message: `surface type '${surfaceType.id}' is not observed in survey.ui_surfaces[].classification.surface_type.`, + path: `patterns.yml.surface_types[${index}].id`, + }); + }); +} + +function verifyCheckPatternReferences( + patterns: GhostPatternsDocument, + checks: GhostCheck[], + issues: VerifyFingerprintIssue[], +): void { + const patternIds = new Set( + patterns.composition_patterns.map((pattern) => pattern.id), + ); + checks.forEach((check, checkIndex) => { + check.applies_to?.pattern_ids?.forEach((patternId, patternIndex) => { + if (patternIds.has(patternId)) return; + issues.push({ + severity: "error", + rule: "check-pattern-unknown", + message: `check '${check.id}' references unknown composition pattern '${patternId}'.`, + path: `checks.yml.checks[${checkIndex}].applies_to.pattern_ids[${patternIndex}]`, + }); + }); + }); +} + +function isLocalTarget(target: string): boolean { + return ( + target === "." || + target.startsWith("./") || + target.startsWith("../") || + target.startsWith("/") + ); +} + +async function pathExists(path: string): Promise { + try { + await access(path); + return true; + } catch { + return false; + } +} + +function isMissingFileError(err: unknown): boolean { + return ( + typeof err === "object" && + err !== null && + "code" in err && + (err as { code?: string }).code === "ENOENT" + ); +} + +function finalize(issues: VerifyFingerprintIssue[]): VerifyFingerprintReport { + return { + issues, + errors: issues.filter((issue) => issue.severity === "error").length, + warnings: issues.filter((issue) => issue.severity === "warning").length, + info: issues.filter((issue) => issue.severity === "info").length, + }; +} diff --git a/packages/ghost-fingerprint/src/emit-command.ts b/packages/ghost-fingerprint/src/emit-command.ts index 62950056..e8853bb9 100644 --- a/packages/ghost-fingerprint/src/emit-command.ts +++ b/packages/ghost-fingerprint/src/emit-command.ts @@ -53,11 +53,11 @@ export function registerEmitCommand(cli: CAC): void { cli .command( "emit ", - `Emit a derived artifact from the fingerprint package profile (kinds: ${SUPPORTED_KINDS.join(", ")})`, + `Emit a derived artifact from the fingerprint package (kinds: ${SUPPORTED_KINDS.join(", ")})`, ) .option( - "-p, --profile ", - "Source profile file (default: .ghost/fingerprint/profile.md)", + "-f, --fingerprint ", + "Source direct fingerprint markdown file (default: .ghost/fingerprint.md)", ) .option( "-o, --out ", @@ -72,7 +72,7 @@ export function registerEmitCommand(cli: CAC): void { .option("--readme", "Include README.md (context-bundle)") .option( "--prompt-only", - "Emit only prompt.md — skips SKILL.md / fingerprint.md / tokens.css (context-bundle)", + "Emit only prompt.md (skips SKILL.md / fingerprint.md / tokens.css) (context-bundle)", ) .option( "--name ", @@ -106,14 +106,14 @@ export function registerEmitCommand(cli: CAC): void { process.exit(0); } - const profilePath = resolve( + const fingerprintPath = resolve( process.cwd(), - opts.profile ?? - resolveFingerprintPackage(undefined, process.cwd()).profile, + opts.fingerprint ?? + resolveFingerprintPackage(undefined, process.cwd()).fingerprint, ); if (parsed.kind === "review-command") { - const loaded = await loadFingerprint(profilePath, { + const loaded = await loadFingerprint(fingerprintPath, { noEmbeddingBackfill: true, }); const content = emitReviewCommand({ @@ -141,14 +141,14 @@ export function registerEmitCommand(cli: CAC): void { (opts.out as string | undefined) ?? DEFAULT_CONTEXT_OUT, ); - const { fingerprint } = await loadFingerprint(profilePath); + const { fingerprint } = await loadFingerprint(fingerprintPath); const result = await writeContextBundle(fingerprint, { outDir, tokens: opts.tokens !== false, readme: Boolean(opts.readme), promptOnly: Boolean(opts.promptOnly), name: opts.name as string | undefined, - sourcePath: profilePath, + sourcePath: fingerprintPath, }); process.stdout.write( diff --git a/packages/ghost-fingerprint/src/skill-bundle/SKILL.md b/packages/ghost-fingerprint/src/skill-bundle/SKILL.md index fb37e4f2..dbb2969c 100644 --- a/packages/ghost-fingerprint/src/skill-bundle/SKILL.md +++ b/packages/ghost-fingerprint/src/skill-bundle/SKILL.md @@ -1,6 +1,6 @@ --- name: ghost-fingerprint -description: Author and validate the repo-local Ghost fingerprint package. Use when the user wants to create or update .ghost/fingerprint, write map.md/survey.json/profile.md/checks.yml, lint the package, describe a profile, diff profiles, or emit derived artifacts. +description: Author and validate the repo-local Ghost fingerprint bundle. Use when the user wants to create or update .ghost, write resources.yml/map.md/survey.json/patterns.yml/checks.yml/intent.md, lint or verify the bundle, derive patterns from survey evidence, or emit derived artifacts. license: Apache-2.0 metadata: homepage: https://github.com/block/ghost @@ -9,73 +9,68 @@ metadata: # Ghost Fingerprint — Repo-Local Design Memory -Ghost Fingerprint is the package, not a single Markdown file: +A Ghost fingerprint is the root `.ghost/` bundle, not one prose file: ```text -.ghost/fingerprint/ +.ghost/ + resources.yml map.md survey.json - profile.md + patterns.yml checks.yml + intent.md # optional ``` -Checks fail builds. Profile shapes judgment. Survey grounds both. The package is -the fingerprint. - -You do the synthesis. The `ghost-fingerprint` CLI gives deterministic answers -for package initialization, schema validation, layout, profile fidelity, survey -summaries, and structural profile diffs. +Survey grounds the bundle. Patterns make composition operational. Checks are +deterministic gates. Intent is optional human-authored or human-approved product +direction. ## CLI Verbs | Verb | Purpose | |---|---| -| `ghost-fingerprint init-package [dir]` | Create `map.md`, `survey.json`, `profile.md`, and `checks.yml` under `.ghost/fingerprint` or the provided package directory. | -| `ghost-fingerprint lint [file-or-dir]` | Validate a full package by default, or an individual `profile.md`, `map.md`, `survey.json`, or `checks.yml`. | -| `ghost-fingerprint verify-profile [--root ]` | Validate that profile values are backed by survey evidence. This does not enforce checks. | +| `ghost-fingerprint init-package [dir] [--with-intent]` | Create a root `.ghost` bundle skeleton. | +| `ghost-fingerprint lint [file-or-dir]` | Validate a full bundle by default, or an individual artifact. | +| `ghost-fingerprint verify [dir] [--root ]` | Validate cross-artifact fidelity: resources, pattern evidence, and check references. | | `ghost-fingerprint inventory [path]` | Emit raw repo signals for the map recipe. | -| `ghost-fingerprint scan-status [dir] [--include-scopes]` | Report package stage presence: `map`, `survey`, `profile`, `checks`. | -| `ghost-fingerprint describe [profile.md]` | Print profile section ranges and token estimates. Defaults to `.ghost/fingerprint/profile.md`. | -| `ghost-fingerprint diff ` | Structural prose-level diff between two profiles. | +| `ghost-fingerprint scan-status [dir] [--include-scopes]` | Report required stages: `resources`, `map`, `survey`, `patterns`. | +| `ghost-fingerprint describe [intent.md]` | Print markdown section ranges. Defaults to `.ghost/intent.md`. | +| `ghost-fingerprint diff ` | Structural diff between direct fingerprint markdown files. | | `ghost-fingerprint survey ` | Survey ops: `merge`, `fix-ids`, `summarize`, `catalog`, `patterns`. | | `ghost-fingerprint emit ` | Derive static artifacts. Kinds: `review-command`, `context-bundle`, `skill`. | -When the CLI is unavailable, follow the same recipes manually with file reads, -search, and careful validation. Do not block on installation. - ## Workflows - Full scan: follow [references/scan.md](references/scan.md). -- Map the repo: follow [references/map.md](references/map.md). Output `.ghost/fingerprint/map.md`. -- Survey design evidence: follow [references/survey.md](references/survey.md). Output `.ghost/fingerprint/survey.json`. -- Profile design language: follow [references/profile.md](references/profile.md). Output `.ghost/fingerprint/profile.md`. -- Promote deterministic checks: write human-selected gates into `.ghost/fingerprint/checks.yml` using `ghost.checks/v1`. -- Lint the package: run `ghost-fingerprint lint`. -- Verify profile fidelity: run `ghost-fingerprint verify-profile .ghost/fingerprint/profile.md .ghost/fingerprint/survey.json --root `. +- Map the repo: follow [references/map.md](references/map.md). Output `.ghost/map.md`. +- Survey design evidence: follow [references/survey.md](references/survey.md). Output `.ghost/survey.json`. +- Derive/codify composition grammar: follow [references/patterns.md](references/patterns.md). Output `.ghost/patterns.yml`. +- Promote deterministic checks: write human-selected gates into `.ghost/checks.yml` using `ghost.checks/v1`. +- Capture human intent only when supplied or approved: write `.ghost/intent.md`. Drift detection and PR checking live in the sibling `ghost-drift` skill: `ghost-drift check` is blocking; `ghost-drift review` is advisory. ## Package Rules +- `resources.yml` declares what references define the product. - `map.md` routes changed files to scopes and examples. -- `survey.json` records observed evidence and counts. -- `profile.md` is non-enforcing guidance for generation and advisory review. -- `checks.yml` is the only deterministic gate layer in v1. -- Do not put `checks[]` in profile frontmatter. -- Do not invent tokens or values. If a value is absent from the survey, omit it or resurvey. -- Keep `checks.yml` human-promoted. Candidate checks belong in notes until a human selects them. +- `survey.json` records observed evidence, counts, and factual composition observations. +- `patterns.yml` names surface types and composition grammar with survey-backed evidence. +- `checks.yml` is the optional deterministic gate layer. +- `intent.md` is optional human authority; never treat AI-generated prose as authoritative until accepted. - Prefer lintable checks: regex, imports, components, required tokens, and path-scoped patterns. ## Always -- Initialize or target `.ghost/fingerprint/` before authoring. -- Use `ghost-fingerprint survey summarize`, `catalog`, and `patterns` to ground profile prose and proposed checks. -- Validate the whole package with `ghost-fingerprint lint`. -- Treat profile prose as judgment-shaping, not CI-enforcing. +- Initialize or target `.ghost/` before authoring. +- Use `ghost-fingerprint survey summarize`, `catalog`, and `patterns` to ground patterns and proposed checks. +- Validate the whole bundle with `ghost-fingerprint lint`. +- Run `ghost-fingerprint verify --root ` before declaring the bundle complete. ## Never - Never describe root-level `fingerprint.md` as canonical. -- Never promote a subjective taste call directly into `checks.yml` unless it has a deterministic detector and evidence. -- Never write prose into structured frontmatter or structural gates into profile prose. +- Never invent values or composition patterns absent from `survey.json`. +- Never promote subjective composition judgment directly into `checks.yml`; keep it advisory until a deterministic detector exists. +- Never treat `intent.md` as machine-generated truth. diff --git a/packages/ghost-fingerprint/src/skill-bundle/references/map.md b/packages/ghost-fingerprint/src/skill-bundle/references/map.md index d0775472..4ea430f4 100644 --- a/packages/ghost-fingerprint/src/skill-bundle/references/map.md +++ b/packages/ghost-fingerprint/src/skill-bundle/references/map.md @@ -12,7 +12,7 @@ handoffs: # Recipe: Author a target's map.md -**Goal:** produce a valid `.ghost/fingerprint/map.md` (`ghost.map/v2`) that captures the *topology* of the target — what platform it ships on, what it builds with, where the design system lives, and where implemented UI can actually be observed. `map.md` is the first stage of a package scan: every later stage (`survey.json`, `profile.md`, and `checks.yml`) reads it to skip rediscovery and route changes. +**Goal:** produce a valid `.ghost/map.md` (`ghost.map/v2`) that captures the *topology* of the target — what platform it ships on, what it builds with, where the design system lives, and where implemented UI can actually be observed. `map.md` is the topology stage of a package scan: later stages (`survey.json`, `patterns.yml`, and optional `checks.yml`) read it to skip rediscovery and route changes. This recipe is *your* job. Ghost's CLI provides `ghost-fingerprint inventory` (deterministic raw signals) and `ghost-fingerprint lint ` (validation), but you do the synthesis. @@ -44,7 +44,7 @@ The `ghost.map/v2` frontmatter requires: - **`schema: ghost.map/v2`** (literal) - **`id`** — slug (lowercase alphanumeric plus `.` `_` `-`, leading alphanumeric). For fleet scans, this is the fleet target id. - **`repo`** — GitHub `org/repo`, or any source identifier that uniquely names this target. -- **`subject`** — optional `{id, target}` that names the single thing this fingerprint will describe. Use it when the scan needs multiple sources; `subject` stays the primary claim. +- **`subject`** — optional `{id, target}` that names the single thing this bundle will describe. Use it when the scan needs multiple sources; `subject` stays the primary claim. - **`sources`** — optional scan source graph. Each source is `{id?, role, target, resolves?, paths?}` where `role` is `primary` or `resolver`. `primary` supplies usage/salience; `resolver` supplies concrete meaning for imported symbols. Declare exactly one primary when `sources[]` is present. - **`mapped_at`** — current ISO date (`YYYY-MM-DD`) or full datetime. - **`platform`** — one of `web`, `ios`, `android`, `desktop`, `flutter`, `mixed`, `other`, or an array spanning multiple. The inventory's `platform_hints` is your starting point — accept it when consistent, override when you have evidence. @@ -83,7 +83,7 @@ Choose the strongest observation path the target supports: ### 3a. Source graph for split repos -Use a source graph when the target's design language is only observable through dependencies (apps consuming token packages, native apps importing design-system modules, wrappers over upstream registries). The fingerprint still has one subject; the scan may have many sources. +Use a source graph when the target's design language is only observable through dependencies (apps consuming token packages, native apps importing design-system modules, wrappers over upstream registries). The bundle still has one subject; the scan may have many sources. Rule: diff --git a/packages/ghost-fingerprint/src/skill-bundle/references/patterns.md b/packages/ghost-fingerprint/src/skill-bundle/references/patterns.md new file mode 100644 index 00000000..68456b17 --- /dev/null +++ b/packages/ghost-fingerprint/src/skill-bundle/references/patterns.md @@ -0,0 +1,46 @@ +--- +name: patterns +description: Interpret .ghost/survey.json surface evidence into .ghost/patterns.yml, the operational composition grammar. +handoffs: + - label: Verify bundle + command: ghost-fingerprint verify .ghost --root . + prompt: Verify the root fingerprint bundle +--- + +# Recipe: Write `patterns.yml` + +**Goal:** produce `.ghost/patterns.yml` (`ghost.patterns/v1`) from +`.ghost/survey.json`. + +`patterns.yml` is operational composition grammar. It names surface types, +composition patterns, anatomy, variants, anti-patterns, confidence, and evidence +so generation and advisory review have stable handles. It is not human intent; +use optional `intent.md` for accepted product strategy. + +## Start From Survey Evidence + +```bash +ghost-fingerprint survey patterns .ghost/survey.json -o .ghost/patterns.yml +``` + +Then curate the draft. Keep every pattern evidence-backed. + +## Authoring Rules + +- Use stable slugs: `resource-index`, `dense-resource-index`, `settings-stack`. +- Record surface type selection policy in `surface_types[].preferred_patterns`. +- Record pattern anatomy in `composition_patterns[].anatomy`. +- Put sparse but important differences in `variants` or `anti_patterns`. +- Use `confidence` for observed support, not taste. +- Cite `survey.ui_surfaces` via `surface_id`, `locator`, or `path`. +- Keep subjective or strategic rationale out of `patterns.yml`; put approved + intent in `intent.md`. + +## Validate + +```bash +ghost-fingerprint lint .ghost +ghost-fingerprint verify .ghost --root . +``` + +Verification fails when composition patterns lack survey-backed evidence. diff --git a/packages/ghost-fingerprint/src/skill-bundle/references/profile.md b/packages/ghost-fingerprint/src/skill-bundle/references/profile.md deleted file mode 100644 index 42f4b15b..00000000 --- a/packages/ghost-fingerprint/src/skill-bundle/references/profile.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -name: profile -description: Interpret .ghost/fingerprint/survey.json into .ghost/fingerprint/profile.md. -handoffs: - - label: Run deterministic drift checks - command: ghost-drift check - prompt: Run ghost-drift check against the current diff - - label: Emit advisory review packet - command: ghost-drift review - prompt: Emit an advisory review packet for the current diff ---- - -# Recipe: Profile Into `profile.md` - -**Goal:** write `.ghost/fingerprint/profile.md` from an existing -`.ghost/fingerprint/map.md` and `.ghost/fingerprint/survey.json`. - -`profile.md` is non-enforcing guidance. It shapes generation and advisory -review, but it never fails CI by itself. Deterministic gates live only in -`.ghost/fingerprint/checks.yml`. - -## Prerequisites - -- `.ghost/fingerprint/map.md` exists and lints. -- `.ghost/fingerprint/survey.json` exists and lints. -- Use these bounded views first: - -```bash -ghost-fingerprint survey summarize .ghost/fingerprint/survey.json -ghost-fingerprint survey catalog .ghost/fingerprint/survey.json -ghost-fingerprint survey patterns .ghost/fingerprint/survey.json -``` - -Read raw `survey.json` only for targeted row lookup. - -## Write The Profile - -Keep `profile.md` selective: - -> Put a fact in `profile.md` only if it can change generated UI or advisory review. - -Frontmatter stores compact observed values: - -- `id`, `source`, `timestamp` -- `references` -- `observation.personality` / `observation.resembles` -- `decisions[].dimension` -- `palette`, `spacing`, `typography`, `surfaces` - -The body stores: - -- `# Character`: 2–4 sentences about the design language's stance. -- `# Signature`: 2–4 sentences about what native output tends to look like. -- `# Decisions`: `### ` blocks with evidence bullets. - -Do not include `checks[]` in frontmatter. - -## Propose Checks Separately - -While profiling, keep a scratch list of candidate deterministic gates. Good -candidates are lintable: - -- forbidden raw colors -- required token usage -- banned imports or components -- path-scoped layout/component patterns -- required repo-specific wrappers - -For each candidate, provide: - -- id -- title -- detector type and pattern/value -- scope/path -- observed support -- examples -- repair - -Do not write a candidate into `checks.yml` unless the human curator promotes it. -Subjective hierarchy, rhythm, and composition observations belong in profile -prose or advisory review unless they can be made deterministic. - -## Validate - -```bash -ghost-fingerprint lint .ghost/fingerprint -ghost-fingerprint verify-profile .ghost/fingerprint/profile.md .ghost/fingerprint/survey.json --root . -``` - -Fix profile fidelity errors by going back to survey evidence. If the survey is -missing a value the repo actually uses, rerun or repair the survey; do not invent -values in the profile. diff --git a/packages/ghost-fingerprint/src/skill-bundle/references/scan.md b/packages/ghost-fingerprint/src/skill-bundle/references/scan.md index fcc484e8..4616b2f8 100644 --- a/packages/ghost-fingerprint/src/skill-bundle/references/scan.md +++ b/packages/ghost-fingerprint/src/skill-bundle/references/scan.md @@ -1,40 +1,41 @@ --- name: scan -description: Drive a full Ghost scan to produce .ghost/fingerprint/{map.md,survey.json,profile.md,checks.yml}. +description: Drive a full Ghost scan to produce .ghost/{resources.yml,map.md,survey.json,patterns.yml} plus optional checks.yml and intent.md. handoffs: - label: Inspect stage status command: ghost-fingerprint scan-status - prompt: What fingerprint package stage should I run next? + prompt: What fingerprint bundle stage should I run next? - label: Run deterministic drift checks command: ghost-drift check - prompt: Run ghost-drift check against this package + prompt: Run ghost-drift check against this bundle --- # Recipe: Scan A Target End-To-End -**Goal:** produce a complete fingerprint package: +**Goal:** produce a complete root fingerprint bundle: ```text -.ghost/fingerprint/ +.ghost/ + resources.yml map.md survey.json - profile.md + patterns.yml checks.yml + intent.md # optional ``` -You orchestrate stages. The CLI validates; it does not perform the interpretive -scan for you. - ## Overview ```text -map -> survey -> profile -> checks +resources -> map -> survey -> patterns ``` +- `resources.yml`: references that define the product. - `map.md`: topology and routing. -- `survey.json`: observed facts. -- `profile.md`: non-enforcing design-language prior. -- `checks.yml`: human-promoted deterministic gates. +- `survey.json`: observed factual evidence. +- `patterns.yml`: operational composition grammar backed by survey evidence. +- `checks.yml`: optional human-promoted deterministic gates. +- `intent.md`: optional human-authored or human-approved product intent. ## Steps @@ -45,52 +46,56 @@ ghost-fingerprint init-package ghost-fingerprint scan-status ``` -If the CLI is not available, create the directory and four files manually. +Use `--with-intent` only when you have human-authored or human-approved intent +to record. + +### 1. Resources + +Run when `scan-status` recommends `resources`. -### 1. Map +Author `.ghost/resources.yml` from the target, any design-system repositories, +canonical screenshots, docs, resolver sources, and include/exclude boundaries. + +### 2. Map Run when `scan-status` recommends `map`. -Follow [map.md](map.md). Write `.ghost/fingerprint/map.md`, then validate: +Follow [map.md](map.md). Write `.ghost/map.md`, then validate: ```bash -ghost-fingerprint lint .ghost/fingerprint +ghost-fingerprint lint .ghost ``` -### 2. Survey +### 3. Survey Run when `scan-status` recommends `survey`. -Follow [survey.md](survey.md). Write `.ghost/fingerprint/survey.json`, then -finalize and validate: +Follow [survey.md](survey.md). Write `.ghost/survey.json`, then finalize and +validate: ```bash -ghost-fingerprint survey fix-ids .ghost/fingerprint/survey.json -o .ghost/fingerprint/survey.json -ghost-fingerprint lint .ghost/fingerprint +ghost-fingerprint survey fix-ids .ghost/survey.json -o .ghost/survey.json +ghost-fingerprint lint .ghost ``` -### 3. Profile +### 4. Patterns -Run when `scan-status` recommends `profile`. +Run when `scan-status` recommends `patterns`. -Follow [profile.md](profile.md). Use these bounded evidence views: +Follow [patterns.md](patterns.md). Start from the derived pattern draft: ```bash -ghost-fingerprint survey summarize .ghost/fingerprint/survey.json -ghost-fingerprint survey catalog .ghost/fingerprint/survey.json -ghost-fingerprint survey patterns .ghost/fingerprint/survey.json +ghost-fingerprint survey patterns .ghost/survey.json -o .ghost/patterns.yml ``` -Write `.ghost/fingerprint/profile.md`. Validate: +Curate names, anatomy, variants, anti-patterns, confidence, and evidence. Then: ```bash -ghost-fingerprint verify-profile .ghost/fingerprint/profile.md .ghost/fingerprint/survey.json --root -ghost-fingerprint lint .ghost/fingerprint +ghost-fingerprint verify .ghost --root +ghost-fingerprint lint .ghost ``` -### 4. Checks - -Run when `scan-status` recommends `checks`, or whenever a human promotes gates. +### Optional. Checks First scans may leave `checks.yml` with `checks: []`. Candidate checks belong in your response or scan notes until a human curator promotes them. @@ -98,7 +103,7 @@ your response or scan notes until a human curator promotes them. When checks are promoted, validate and smoke-test: ```bash -ghost-fingerprint lint .ghost/fingerprint +ghost-fingerprint lint .ghost ghost-drift check --base HEAD ``` @@ -106,11 +111,11 @@ ghost-drift check --base HEAD Run `ghost-fingerprint scan-status` between stages. To force a stage rerun, delete or replace that artifact and re-run status. Do not move forward from a -failed lint. +failed lint or verify result. ## Never - Never describe root-level `fingerprint.md` as canonical. -- Never invent values absent from `survey.json`. -- Never promote subjective prose directly into `checks.yml`; make it lintable or - keep it advisory. +- Never invent values or composition observations absent from `survey.json`. +- Never promote subjective composition prose directly into `checks.yml`; make it + deterministic or keep it advisory. diff --git a/packages/ghost-fingerprint/src/skill-bundle/references/schema.md b/packages/ghost-fingerprint/src/skill-bundle/references/schema.md index a7faf40e..00dbc0bb 100644 --- a/packages/ghost-fingerprint/src/skill-bundle/references/schema.md +++ b/packages/ghost-fingerprint/src/skill-bundle/references/schema.md @@ -1,41 +1,61 @@ -# Fingerprint Package Schema Reference +# Root Fingerprint Bundle Schema Reference Canonical package: ```text -.ghost/fingerprint/ - map.md ghost.map/v2 - survey.json ghost.survey/v2 - profile.md non-enforcing design-language prior - checks.yml ghost.checks/v1 +.ghost/ + resources.yml ghost.resources/v1 + map.md ghost.map/v2 + survey.json ghost.survey/v2 + patterns.yml ghost.patterns/v1 + checks.yml optional ghost.checks/v1 gates + intent.md optional human intent ``` -## `profile.md` +## `resources.yml` -Profile frontmatter may include: - -- required: `id`, `source`, `timestamp`, `palette`, `spacing`, `typography`, `surfaces` -- optional: `sources`, `references`, `observation.personality`, `observation.resembles`, `decisions[]`, `metadata`, `name`, `slug`, `generator`, `confidence`, `generated`, `extends` - -Forbidden: root `schema`, root `embedding`, `checks[]`, -`decisions[].embedding`, `decisions[].decision`, -`decisions[].evidence`, `observation.summary`, and unknown root keys. - -The body uses: - -```markdown -# Character - -# Signature +```yaml +schema: ghost.resources/v1 +id: my-project +primary: + target: . + paths: [src] +design_system: + - id: ui + target: ../ui + paths: [packages/ui] +surfaces: + - id: settings + locator: /settings + paths: [src/routes/settings.tsx] +include: ["src/**"] +exclude: ["**/node_modules/**"] +``` -# Decisions +## `patterns.yml` -### color-strategy +```yaml +schema: ghost.patterns/v1 +id: my-project +surface_types: + - id: settings + preferred_patterns: [settings-stack] + evidence: + - surface_id: settings-account +composition_patterns: + - id: settings-stack + surface_types: [settings] + frequency: 4 + confidence: 0.8 + anatomy: + ordered: [shell, compact-header, sections, actions] + required: [sections] + forbidden: [oversized-hero] + evidence: + - surface_id: settings-account + locator: /settings/account ``` -Evidence bullets live in the body. Profile prose shapes judgment; it does not -enforce CI. - ## `checks.yml` ```yaml @@ -49,19 +69,19 @@ checks: applies_to: scopes: [checkout] paths: [src/checkout] + surface_types: [resource-index] + pattern_ids: [dense-resource-index] detector: type: forbidden-regex pattern: '#[0-9a-fA-F]{3,8}' - contexts: [typescript] evidence: support: 0.94 observed_count: 31 examples: - src/checkout/Button.tsx - repair: Replace literals with semantic tokens. ``` -Detector types: +Detector types remain deterministic only: - `forbidden-regex` - `required-regex` @@ -69,20 +89,14 @@ Detector types: - `banned-component` - `required-token` -Statuses: - -- `active`: enforced by `ghost-drift check` -- `proposed`: kept as a candidate -- `disabled`: retained but ignored - ## Validation ```bash -ghost-fingerprint lint .ghost/fingerprint -ghost-fingerprint verify-profile .ghost/fingerprint/profile.md .ghost/fingerprint/survey.json --root . +ghost-fingerprint lint .ghost +ghost-fingerprint verify .ghost --root . ghost-drift check --base main ``` -`lint` validates all four artifacts together, including check scope references -against `map.md`. `verify-profile` validates profile-to-survey fidelity. -`ghost-drift check` is the deterministic pass/fail gate. +`lint` validates artifact shape. `verify` validates cross-artifact fidelity: +resources resolve, patterns cite survey evidence, and checks reference known +pattern IDs. `ghost-drift check` is the deterministic pass/fail gate. diff --git a/packages/ghost-fingerprint/src/skill-bundle/references/survey.md b/packages/ghost-fingerprint/src/skill-bundle/references/survey.md index fdcf7ddb..35032a55 100644 --- a/packages/ghost-fingerprint/src/skill-bundle/references/survey.md +++ b/packages/ghost-fingerprint/src/skill-bundle/references/survey.md @@ -2,9 +2,9 @@ name: survey description: Scan a target and produce a survey.json — the observed catalogue of design values, with no interpretation. handoffs: - - label: Interpret the survey into profile.md + - label: Interpret the survey into patterns.yml command: (next stage — interpreter recipe) - prompt: Interpret the survey I just wrote into .ghost/fingerprint/profile.md + prompt: Interpret the survey I just wrote into .ghost/patterns.yml - label: Validate the survey command: ghost-fingerprint lint survey.json prompt: Lint the survey I just wrote @@ -14,9 +14,9 @@ handoffs: **Goal:** produce a valid `survey.json` (`ghost.survey/v2`) that catalogues every concrete design value and implemented UI surface the target ships, with structured specs, occurrence counts, and surface evidence. **You are the surveyor, not the interpreter.** Record what is there. Do not assign meaning. Do not write prose. Do not invent. -`survey.json` is the evidence artifact in the package scan: map (`map.md`) → survey (`survey.json`) → profile (`profile.md`) → checks (`checks.yml`). The interpreter reads your survey as evidence and writes the compact profile. If you skip values or fabricate them here, the package downstream is wrong. +`survey.json` is the evidence artifact in the package scan: resources (`resources.yml`) → map (`map.md`) → survey (`survey.json`) → patterns (`patterns.yml`). The interpreter reads your survey as evidence and writes operational composition grammar. If you skip values or fabricate them here, the package downstream is wrong. -The survey is exhaustive evidence, not prompt context. It should be large enough to support interpretation; the profile stage decides what becomes generation-facing guidance and check candidates. +The survey is an evidence ledger, not prompt context. It should be large enough to justify patterns, checks, and advisory review; the patterns stage decides what becomes generation-facing composition guidance and check candidates. ## Pre-requisite @@ -123,7 +123,7 @@ Procedure: 1. **Scan primary usage first.** Record every local token/symbol/class usage in the primary target with occurrences and files_count. These counts are the only salience signal. 2. **Open resolver sources.** Read the upstream package/source/build artifact named by `sources[].role: resolver` or `design_system.upstream`. Find the exported token tables, generated Swift/Kotlin/TS accessors, CSS variables, registry metadata, or other symbol definitions. 3. **Join symbols to definitions.** Follow `CashTheme.color.background → ArcadeColor.background → #ffffff` (or equivalent) as far as source permits. Preserve the chain in `resolution.chain`. -4. **Emit resolved rows only for observed usage.** If a resolver defines 400 colors and the primary app uses 12, the app survey gets the 12 observed values. Unused resolver inventory belongs in the resolver's own fingerprint, not the app's. +4. **Emit resolved rows only for observed usage.** If a resolver defines 400 colors and the primary app uses 12, the app survey gets the 12 observed values. Unused resolver inventory belongs in the resolver's own bundle, not the app's. 5. **Mark gaps honestly.** For unresolved external symbols, emit token rows with `resolution.status: "unresolved-external"` plus `symbol` / `message`; add a scratchpad coverage note with unresolved counts by kind. Coverage gate: before declaring done, report resolved vs unresolved counts for each resolver-backed kind (color, spacing, typography, radius, shadow). Weak resolver coverage lowers confidence downstream; it is not a reason to fabricate literals. @@ -167,7 +167,7 @@ For components: Use `surface_sources` and `feature_areas[]` from `map.md` to enumerate representative implemented surfaces. This section is required in `ghost.survey/v2`. If no implemented surface can be observed, write `ui_surfaces: []`, ensure `map.md` uses `surface_sources.render_strategy: unknown`, and carry the coverage gap in your scratchpad for the interpreter. -Surface rows are evidence, not exemplars and not prose. Record facts that a later profiler can cluster: +Surface rows are evidence, not exemplars and not prose. Record facts that the later patterns authoring pass can cluster: ```json { @@ -185,6 +185,14 @@ Surface rows are evidence, not exemplars and not prose. Record facts that a late "layout_shape": "control-surface", "confidence": 0.75 }, + "composition": { + "anatomy": ["shell", "compact-header", "sectioned-form", "persistent-actions"], + "primary_region": "form", + "action_placement": ["footer"], + "navigation_context": "persistent-shell", + "responsive_behavior": ["mobile stacks sections vertically"], + "confidence": 0.75 + }, "signals": { "dominant_components": ["Tabs", "Input", "Button"], "layout_patterns": ["sectioned-form", "persistent-actions"], @@ -201,7 +209,7 @@ Discovery guidance: - For `native-screenshot`, record the screenshot or fixture locator and the source files when known. - For `static-source`, use route files, screens, stories, examples, or feature entrypoints as the observable specimens. - Prefer 1–3 high-signal surfaces per feature area. This is not exhaustive in the same way components are; it is coverage of implemented composition families. -- Keep `signals.notes` factual. "Sectioned settings form with persistent action row" is survey evidence. "Feels professional and calm" belongs in profile interpretation. +- Keep `signals.notes` and `composition` factual. "Sectioned settings form with persistent action row" is survey evidence. "Feels professional and calm" belongs in `intent.md` only when human-approved. ### 5. Sample feature areas for usage counts @@ -281,4 +289,4 @@ If you hit a hard stop with exhaustiveness *not* met, write a `# Coverage` note - **Never undercount silently.** If your coverage is weak (mobile dialects, custom DSLs, no canonical signal in this repo), surface it in a `# Coverage` scratchpad note and tell the interpreter. - **Never compute IDs by hand.** Use `survey fix-ids`. - **Never use placeholder/glob names.** A component row with `name: "*Button"` or `name: ""` is sampling-disguised-as-a-row. Enumerate concretely. -- **Never edit a survey after the interpreter has used it.** If you find a missed value later, re-run survey end-to-end. Treat the survey used for a profile as frozen evidence. +- **Never edit a survey after the interpreter has used it.** If you find a missed value later, re-run survey end-to-end. Treat the survey used for `patterns.yml` as frozen evidence. diff --git a/packages/ghost-fingerprint/test/cli.test.ts b/packages/ghost-fingerprint/test/cli.test.ts index 33d2dace..1b626038 100644 --- a/packages/ghost-fingerprint/test/cli.test.ts +++ b/packages/ghost-fingerprint/test/cli.test.ts @@ -48,16 +48,6 @@ function fingerprintWithId(id: string): string { return BASE_FINGERPRINT.replace("id: local", `id: ${id}`); } -function profileFingerprintWithPalette(hex: string): string { - return BASE_FINGERPRINT.replace( - '- { role: primary, value: "#111111" }', - `- { role: primary, value: "${hex}" }`, - ).replace( - 'neutrals: { steps: ["#ffffff", "#111111"], count: 2 }', - `neutrals: { steps: ["${hex}"], count: 1 }`, - ); -} - async function runCli(argv: string[], cwd: string) { const cli = buildCli(); const previousCwd = process.cwd(); @@ -129,17 +119,32 @@ describe("ghost-fingerprint CLI defaults", () => { await rm(dir, { recursive: true, force: true }); }); - it("init-package creates the fingerprint package skeleton", async () => { + it("init-package creates the root fingerprint package skeleton", async () => { const result = await runCli(["init-package"], dir); expect(result.code).toBe(0); - expect(result.stdout).toContain(".ghost/fingerprint"); + expect(result.stdout).toContain(".ghost"); + expect( + await readFile(join(dir, ".ghost", "resources.yml"), "utf-8"), + ).toContain("schema: ghost.resources/v1"); expect( - await readFile(join(dir, ".ghost", "fingerprint", "profile.md"), "utf-8"), - ).toContain("# Character"); + await readFile(join(dir, ".ghost", "patterns.yml"), "utf-8"), + ).toContain("schema: ghost.patterns/v1"); + await expect( + readFile(join(dir, ".ghost", "intent.md"), "utf-8"), + ).rejects.toThrow(); + }); + + it("init-package creates optional intent.md when requested", async () => { + const result = await runCli(["init-package", "--with-intent"], dir); + + expect(result.code).toBe(0); + expect(await readFile(join(dir, ".ghost", "intent.md"), "utf-8")).toContain( + "# Intent", + ); }); - it("lint defaults to .ghost/fingerprint", async () => { + it("lint defaults to .ghost", async () => { await runCli(["init-package"], dir); const result = await runCli(["lint"], dir); @@ -149,18 +154,18 @@ describe("ghost-fingerprint CLI defaults", () => { expect(result.stderr).toBe(""); }); - it("describe defaults to .ghost/fingerprint/profile.md", async () => { - await mkdir(join(dir, ".ghost", "fingerprint"), { recursive: true }); + it("describe defaults to .ghost/intent.md", async () => { + await mkdir(join(dir, ".ghost"), { recursive: true }); await writeFile( - join(dir, ".ghost", "fingerprint", "profile.md"), - fingerprintWithId("local"), + join(dir, ".ghost", "intent.md"), + "# Intent\n\nHuman-authored direction.\n", ); const result = await runCli(["describe"], dir); expect(result.code).toBe(0); - expect(result.stdout).toContain("profile.md"); - expect(result.stdout).toContain("# Character"); + expect(result.stdout).toContain("intent.md"); + expect(result.stdout).toContain("# Intent"); }); it("diff returns 0 (unchanged) when comparing identical fingerprints", async () => { @@ -397,13 +402,13 @@ describe("ghost-fingerprint lint dispatches by file kind", () => { }); }); -describe("ghost-fingerprint verify-profile", () => { +describe("ghost-fingerprint verify", () => { let dir: string; beforeEach(async () => { dir = join( tmpdir(), - `ghost-fingerprint-verify-profile-${Date.now()}-${Math.random().toString(36).slice(2)}`, + `ghost-fingerprint-verify-${Date.now()}-${Math.random().toString(36).slice(2)}`, ); await mkdir(dir, { recursive: true }); }); @@ -412,18 +417,29 @@ describe("ghost-fingerprint verify-profile", () => { await rm(dir, { recursive: true, force: true }); }); - it("writes JSON output for a survey-backed fingerprint", async () => { + it("writes JSON output for a survey-backed pattern bundle", async () => { + await runCli(["init-package"], dir); await writeFile( - join(dir, "fingerprint.md"), - profileFingerprintWithPalette("#f97316"), + join(dir, ".ghost", "survey.json"), + JSON.stringify(makeSurvey(SOURCE_A), null, 2), ); await writeFile( - join(dir, "survey.json"), - JSON.stringify(makeSurvey(SOURCE_A), null, 2), + join(dir, ".ghost", "patterns.yml"), + `schema: ghost.patterns/v1 +id: local +surface_types: + - id: settings + preferred_patterns: [sectioned-form] +composition_patterns: + - id: sectioned-form + surface_types: [settings] + evidence: + - locator: /settings +`, ); const result = await runCli( - ["verify-profile", "fingerprint.md", "survey.json", "--format", "json"], + ["verify", "--root", dir, "--format", "json"], dir, ); @@ -432,23 +448,39 @@ describe("ghost-fingerprint verify-profile", () => { expect(report.errors).toBe(0); }); - it("exits non-zero when palette values are absent from the survey", async () => { + it("exits non-zero when pattern evidence is absent from the survey", async () => { + await runCli(["init-package"], dir); await writeFile( - join(dir, "fingerprint.md"), - profileFingerprintWithPalette("#1c1c1c"), + join(dir, ".ghost", "survey.json"), + JSON.stringify(makeSurvey(SOURCE_A), null, 2), ); await writeFile( - join(dir, "survey.json"), - JSON.stringify(makeSurvey(SOURCE_A), null, 2), + join(dir, ".ghost", "patterns.yml"), + `schema: ghost.patterns/v1 +id: local +surface_types: + - id: settings + preferred_patterns: [sectioned-form] +composition_patterns: + - id: sectioned-form + surface_types: [settings] + evidence: + - locator: /missing +`, ); - const result = await runCli( - ["verify-profile", "fingerprint.md", "survey.json"], - dir, - ); + const result = await runCli(["verify", "--root", dir], dir); expect(result.code).toBe(1); - expect(result.stdout).toContain("palette-color-not-in-survey"); + expect(result.stdout).toContain("pattern-evidence-unbacked"); + }); + + it("registers verify and not the old verify-fingerprint command", async () => { + const commands = buildCli().commands.map((command) => command.name); + + expect(commands).toContain("verify"); + expect(commands).not.toContain("verify-fingerprint"); + expect(commands).not.toContain("verify-profile"); }); }); @@ -871,7 +903,7 @@ describe("ghost-fingerprint survey patterns", () => { const result = await runCli(["survey", "patterns", "survey.json"], dir); expect(result.code).toBe(0); - expect(result.stdout).toContain("# Survey Patterns"); + expect(result.stdout).toContain("schema: ghost.patterns/v1"); expect(result.stdout).toContain("settings"); expect(result.stdout).toContain("sectioned-form"); }); @@ -889,7 +921,23 @@ describe("ghost-fingerprint survey patterns", () => { expect(result.code).toBe(0); const patterns = JSON.parse(result.stdout); - expect(patterns.schema).toBe("ghost.survey.patterns/v1"); - expect(patterns.surface_types[0].value).toBe("settings"); + expect(patterns.schema).toBe("ghost.patterns/v1"); + expect(patterns.surface_types[0].id).toBe("settings"); + expect(patterns.composition_patterns[0].id).toBe("sectioned-form"); + }); + + it("keeps markdown output available", async () => { + await writeFile( + join(dir, "survey.json"), + JSON.stringify(makeSurvey(SOURCE_A)), + ); + + const result = await runCli( + ["survey", "patterns", "survey.json", "--format", "markdown"], + dir, + ); + + expect(result.code).toBe(0); + expect(result.stdout).toContain("# Survey Patterns"); }); }); diff --git a/packages/ghost-fingerprint/test/context/__snapshots__/review-command.test.ts.snap b/packages/ghost-fingerprint/test/context/__snapshots__/review-command.test.ts.snap index b6a0550d..640e7606 100644 --- a/packages/ghost-fingerprint/test/context/__snapshots__/review-command.test.ts.snap +++ b/packages/ghost-fingerprint/test/context/__snapshots__/review-command.test.ts.snap @@ -20,7 +20,7 @@ If \`$ARGUMENTS\` is empty, ask the user which file(s) to review, or offer to sc ## Calibration note -This profile has no embedded checks, so this command uses a coarse token fallback from palette, spacing, typography, and surfaces. Treat findings as lower-confidence than \`ghost-drift check\`; promote enforceable rules in \`.ghost/fingerprint/checks.yml\` when a pattern should become a gate. +This fingerprint has no embedded checks, so this command uses a coarse token fallback from palette, spacing, typography, and surfaces. Treat findings as lower-confidence than \`ghost-drift check\`; promote enforceable rules in \`.ghost/checks.yml\` when a pattern should become a gate. ## 1. Palette drift @@ -150,6 +150,6 @@ Drift score: XX/100 --- -Generated from \`profile.md\` (11 decisions). Re-run \`ghost-fingerprint emit review-command\` after package updates. +Generated from \`fingerprint.md\` (11 decisions). Re-run \`ghost-fingerprint emit review-command\` after package updates. " `; diff --git a/packages/ghost-fingerprint/test/context/writer.test.ts b/packages/ghost-fingerprint/test/context/writer.test.ts index c3a1589b..bea08876 100644 --- a/packages/ghost-fingerprint/test/context/writer.test.ts +++ b/packages/ghost-fingerprint/test/context/writer.test.ts @@ -69,12 +69,12 @@ afterEach(async () => { }); describe("writeContextBundle", () => { - it("default: emits SKILL.md + profile.md + prompt.md + tokens.css", async () => { + it("default: emits SKILL.md + fingerprint.md + prompt.md + tokens.css", async () => { const res = await writeContextBundle(FINGERPRINT, { outDir: dir }); const names = res.files.map((f) => f.split("/").pop()); expect(names).toEqual([ "SKILL.md", - "profile.md", + "fingerprint.md", "prompt.md", "tokens.css", ]); @@ -85,13 +85,13 @@ describe("writeContextBundle", () => { expect(skill).toContain("tokens.css"); }); - it("--no-tokens: emits SKILL.md + profile.md + prompt.md only", async () => { + it("--no-tokens: emits SKILL.md + fingerprint.md + prompt.md only", async () => { const res = await writeContextBundle(FINGERPRINT, { outDir: dir, tokens: false, }); const names = res.files.map((f) => f.split("/").pop()); - expect(names).toEqual(["SKILL.md", "profile.md", "prompt.md"]); + expect(names).toEqual(["SKILL.md", "fingerprint.md", "prompt.md"]); const skill = await readFile(res.files[0], "utf-8"); expect(skill).not.toContain("tokens.css"); @@ -105,7 +105,7 @@ describe("writeContextBundle", () => { const names = res.files.map((f) => f.split("/").pop()); expect(names).toEqual([ "SKILL.md", - "profile.md", + "fingerprint.md", "prompt.md", "tokens.css", "README.md", diff --git a/packages/ghost-fingerprint/test/fingerprint-package.test.ts b/packages/ghost-fingerprint/test/fingerprint-package.test.ts index fac17602..0d94bcf6 100644 --- a/packages/ghost-fingerprint/test/fingerprint-package.test.ts +++ b/packages/ghost-fingerprint/test/fingerprint-package.test.ts @@ -6,6 +6,7 @@ import { initFingerprintPackage, lintFingerprintPackage, resolveFingerprintPackage, + verifyFingerprintPackage, } from "../src/core/index.js"; describe("fingerprint package", () => { @@ -23,15 +24,16 @@ describe("fingerprint package", () => { await rm(dir, { recursive: true, force: true }); }); - it("discovers .ghost/fingerprint by default", () => { + it("discovers .ghost by default", () => { const paths = resolveFingerprintPackage(undefined, dir); - expect(paths.dir).toBe(join(dir, ".ghost", "fingerprint")); - expect(paths.profile).toBe(join(paths.dir, "profile.md")); + expect(paths.dir).toBe(join(dir, ".ghost")); + expect(paths.resources).toBe(join(paths.dir, "resources.yml")); + expect(paths.patterns).toBe(join(paths.dir, "patterns.yml")); expect(paths.checks).toBe(join(paths.dir, "checks.yml")); }); - it("lints all four package artifacts together", async () => { + it("lints package artifacts together when checks are present", async () => { await initFingerprintPackage(undefined, dir); const report = await lintFingerprintPackage(undefined, dir); @@ -39,6 +41,15 @@ describe("fingerprint package", () => { expect(report.errors).toBe(0); }); + it("passes package lint when optional checks.yml is absent", async () => { + const paths = await initFingerprintPackage(undefined, dir); + await rm(paths.checks, { force: true }); + + const report = await lintFingerprintPackage(undefined, dir); + + expect(report.errors).toBe(0); + }); + it("fails package lint when checks reference unknown map scopes", async () => { const paths = await initFingerprintPackage(undefined, dir); await writeFile( @@ -70,4 +81,51 @@ checks: "check-scope-unknown", ); }); + + it("verifies patterns against survey evidence", async () => { + const paths = await initFingerprintPackage(undefined, dir); + await writeFile( + paths.survey, + JSON.stringify({ + schema: "ghost.survey/v2", + sources: [{ target: ".", scanned_at: "2026-05-10T00:00:00Z" }], + values: [], + tokens: [], + components: [], + ui_surfaces: [ + { + id: "surface_settings", + source: { target: ".", scanned_at: "2026-05-10T00:00:00Z" }, + name: "Settings", + kind: "route", + locator: "/settings", + renderability: "source-only", + files: ["src/settings.tsx"], + classification: { surface_type: "settings" }, + signals: { layout_patterns: ["sectioned-form"] }, + }, + ], + }), + ); + await writeFile( + paths.patterns, + `schema: ghost.patterns/v1 +id: local +surface_types: + - id: settings + preferred_patterns: [sectioned-form] +composition_patterns: + - id: sectioned-form + surface_types: [settings] + evidence: + - surface_id: surface_settings +`, + ); + + const report = await verifyFingerprintPackage(undefined, dir, { + root: dir, + }); + + expect(report.errors).toBe(0); + }); }); diff --git a/packages/ghost-fingerprint/test/fingerprint/lint.test.ts b/packages/ghost-fingerprint/test/fingerprint/lint.test.ts index 949f9a70..ca6296a6 100644 --- a/packages/ghost-fingerprint/test/fingerprint/lint.test.ts +++ b/packages/ghost-fingerprint/test/fingerprint/lint.test.ts @@ -87,7 +87,7 @@ Checkout uses tighter rows than the parent. }); it("keeps a surface-derived composition-patterns fixture lint-clean", () => { - const fixtureDir = resolve(FIXTURES, "surface-profile"); + const fixtureDir = resolve(FIXTURES, "surface-fingerprint"); const survey = JSON.parse( readFileSync(resolve(fixtureDir, "survey.json"), "utf-8"), ); @@ -139,6 +139,42 @@ Rationale without frontmatter decisions[] entry — body is authoritative. ); }); + it("warns when the Decisions section has prose but no parseable H3 blocks", () => { + const md = build( + ``, + `# Decisions + +**Color strategy.** This looks like a decision, but it is not addressable. +`, + ); + const report = lintFingerprint(md); + expect(report.issues).toContainEqual( + expect.objectContaining({ + severity: "warning", + rule: "missing-decision-headings", + }), + ); + }); + + it("warns when a decision block has no Evidence list", () => { + const md = build( + ``, + `# Decisions + +### spatial-system +Spacing is compact and regular. +`, + ); + const report = lintFingerprint(md); + expect(report.issues).toContainEqual( + expect.objectContaining({ + severity: "warning", + rule: "missing-evidence", + path: "decisions[0].evidence", + }), + ); + }); + it("rejects prose (summary, decision, values) in the frontmatter via schema-invalid", () => { const md = build( `\nobservation: @@ -328,7 +364,7 @@ No cool grays. expect(issue?.path).toBe("decisions[0].dimension_kind"); }); - it("rejects checks in profile frontmatter because checks.yml owns gates", () => { + it("rejects checks in fingerprint frontmatter because checks.yml owns gates", () => { const md = build( `\nchecks: - id: no-hardcoded-colors diff --git a/packages/ghost-fingerprint/test/fingerprint/verify-profile.test.ts b/packages/ghost-fingerprint/test/fingerprint/verify-fingerprint.test.ts similarity index 81% rename from packages/ghost-fingerprint/test/fingerprint/verify-profile.test.ts rename to packages/ghost-fingerprint/test/fingerprint/verify-fingerprint.test.ts index 1f1bcc2d..ddfb3af8 100644 --- a/packages/ghost-fingerprint/test/fingerprint/verify-profile.test.ts +++ b/packages/ghost-fingerprint/test/fingerprint/verify-fingerprint.test.ts @@ -5,7 +5,7 @@ import { fileURLToPath } from "node:url"; import type { Survey, SurveySource } from "@ghost/core"; import { tokenRowId, uiSurfaceRowId, valueRowId } from "@ghost/core"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { loadFingerprint, verifyProfile } from "../../src/core/index.js"; +import { loadFingerprint, verifyFingerprint } from "../../src/core/index.js"; const SOURCE: SurveySource = { id: "local", @@ -160,13 +160,13 @@ function makeSurvey({ }; } -describe("verifyProfile", () => { +describe("verifyFingerprint", () => { let dir: string; beforeEach(async () => { dir = join( tmpdir(), - `ghost-fingerprint-verify-profile-${Date.now()}-${Math.random().toString(36).slice(2)}`, + `ghost-fingerprint-verify-fingerprint-${Date.now()}-${Math.random().toString(36).slice(2)}`, ); await mkdir(dir, { recursive: true }); }); @@ -176,7 +176,7 @@ describe("verifyProfile", () => { }); it("passes when all palette colors exist in survey values or tokens", () => { - const report = verifyProfile( + const report = verifyFingerprint( fingerprint(), makeSurvey({ colors: ["#ffffff"], tokens: { "--brand": "#1a1a1a" } }), ); @@ -187,6 +187,51 @@ describe("verifyProfile", () => { ); }); + it("uses survey value category as provenance for extension kinds", () => { + const survey = makeSurvey(); + survey.values = survey.values.map((row) => { + if (row.kind === "color") { + return { ...row, kind: "hex-color", category: "color" }; + } + if (row.kind === "spacing") { + const scalar = + typeof row.spec === "object" && + row.spec !== null && + "scalar" in row.spec + ? Number(row.spec.scalar) + : Number.parseFloat(row.value); + return { + ...row, + kind: "length", + category: "spacing", + spec: { number: scalar, unit: "px" }, + }; + } + if (row.kind === "radius") { + return { ...row, kind: "length", category: "radius" }; + } + if (row.kind === "typography") { + if (row.value === "Inter") { + return { ...row, kind: "font-stack", category: "typography" }; + } + if (row.value === "400") { + return { ...row, kind: "number", category: "typography" }; + } + return { + ...row, + kind: "length", + category: "typography", + value: `font-size: ${row.value}`, + }; + } + return row; + }) as Survey["values"]; + + const report = verifyFingerprint(fingerprint(), survey); + + expect(report.errors).toBe(0); + }); + it("verifies scoped overlays against the resolved parent fingerprint", async () => { await writeFile(join(dir, "fingerprint.md"), fingerprint(), "utf-8"); await mkdir(join(dir, "fingerprints"), { recursive: true }); @@ -206,7 +251,7 @@ Checkout tightens the inherited control-surface rhythm. await writeFile(childPath, childRaw, "utf-8"); const resolved = (await loadFingerprint(childPath)).fingerprint; - const report = verifyProfile(childRaw, makeSurvey(), { + const report = verifyFingerprint(childRaw, makeSurvey(), { resolvedFingerprint: resolved, }); @@ -214,7 +259,7 @@ Checkout tightens the inherited control-surface rhythm. }); it("errors when the fingerprint invents a palette color", () => { - const report = verifyProfile( + const report = verifyFingerprint( fingerprint({ brand: "#1c1c1c", neutralSteps: ["#ffffff", "#1a1a1a"] }), makeSurvey(), ); @@ -231,7 +276,7 @@ Checkout tightens the inherited control-surface rhythm. }); it("warns when a same-role fingerprint palette entry disagrees with a high-salience token", () => { - const report = verifyProfile( + const report = verifyFingerprint( fingerprint({ brand: "#ffffff" }), makeSurvey({ colors: ["#ffffff", "#1a1a1a"] }), ); @@ -249,7 +294,7 @@ Checkout tightens the inherited control-surface rhythm. }); it("errors when spacing, typography, or radii are not survey-backed", () => { - const report = verifyProfile( + const report = verifyFingerprint( fingerprint() .replace("scale: [4, 8, 16]", "scale: [4, 10, 16]") .replace('families: ["Inter"]', 'families: ["Aspirational Sans"]') @@ -283,7 +328,7 @@ Checkout tightens the inherited control-surface rhythm. files_count: 3, }); - const report = verifyProfile(fingerprint(), survey); + const report = verifyFingerprint(fingerprint(), survey); expect(report.issues).toContainEqual( expect.objectContaining({ @@ -307,7 +352,7 @@ Checkout tightens the inherited control-surface rhythm. files_count: 4, }); - const report = verifyProfile(fingerprint(), survey); + const report = verifyFingerprint(fingerprint(), survey); expect(report.errors).toBe(0); expect(report.issues).toContainEqual( @@ -326,7 +371,7 @@ Checkout tightens the inherited control-surface rhythm. fixtureDir, "..", "fixtures", - "profile-verifier", + "fingerprint-verifier", "goose2", "fingerprint.md", ), @@ -338,7 +383,7 @@ Checkout tightens the inherited control-surface rhythm. fixtureDir, "..", "fixtures", - "profile-verifier", + "fingerprint-verifier", "goose2", "survey.json", ), @@ -346,7 +391,7 @@ Checkout tightens the inherited control-surface rhythm. ), ); - const report = verifyProfile(fingerprintRaw, survey); + const report = verifyFingerprint(fingerprintRaw, survey); expect(report.issues).toContainEqual( expect.objectContaining({ diff --git a/packages/ghost-fingerprint/test/fixtures/profile-verifier/goose2/fingerprint.md b/packages/ghost-fingerprint/test/fixtures/fingerprint-verifier/goose2/fingerprint.md similarity index 89% rename from packages/ghost-fingerprint/test/fixtures/profile-verifier/goose2/fingerprint.md rename to packages/ghost-fingerprint/test/fixtures/fingerprint-verifier/goose2/fingerprint.md index 28bf3c6a..27a08e7b 100644 --- a/packages/ghost-fingerprint/test/fixtures/profile-verifier/goose2/fingerprint.md +++ b/packages/ghost-fingerprint/test/fixtures/fingerprint-verifier/goose2/fingerprint.md @@ -35,5 +35,5 @@ Dense operational surfaces with restrained neutrals. # Decisions ### color-strategy -The profile claims a near-black neutral, but the observed survey evidence used +The fingerprint claims a near-black neutral, but the observed survey evidence used `#1a1a1a`, not `#1c1c1c`. diff --git a/packages/ghost-fingerprint/test/fixtures/profile-verifier/goose2/survey.json b/packages/ghost-fingerprint/test/fixtures/fingerprint-verifier/goose2/survey.json similarity index 100% rename from packages/ghost-fingerprint/test/fixtures/profile-verifier/goose2/survey.json rename to packages/ghost-fingerprint/test/fixtures/fingerprint-verifier/goose2/survey.json diff --git a/packages/ghost-fingerprint/test/fixtures/surface-profile/fingerprint.md b/packages/ghost-fingerprint/test/fixtures/surface-fingerprint/fingerprint.md similarity index 94% rename from packages/ghost-fingerprint/test/fixtures/surface-profile/fingerprint.md rename to packages/ghost-fingerprint/test/fixtures/surface-fingerprint/fingerprint.md index f7080f7c..26d87207 100644 --- a/packages/ghost-fingerprint/test/fixtures/surface-profile/fingerprint.md +++ b/packages/ghost-fingerprint/test/fixtures/surface-fingerprint/fingerprint.md @@ -1,9 +1,9 @@ --- -id: surface-profile-fixture +id: surface-fingerprint-fixture source: llm timestamp: 2026-05-04T00:00:00Z references: - specs: [packages/ghost-fingerprint/test/fixtures/surface-profile/survey.json] + specs: [packages/ghost-fingerprint/test/fixtures/surface-fingerprint/survey.json] components: [src/components/MetricCard.tsx, src/components/StatusTimeline.tsx] examples: [src/routes/dashboard.tsx] observation: diff --git a/packages/ghost-fingerprint/test/fixtures/surface-profile/survey.json b/packages/ghost-fingerprint/test/fixtures/surface-fingerprint/survey.json similarity index 90% rename from packages/ghost-fingerprint/test/fixtures/surface-profile/survey.json rename to packages/ghost-fingerprint/test/fixtures/surface-fingerprint/survey.json index 6df11b4e..6cddadc0 100644 --- a/packages/ghost-fingerprint/test/fixtures/surface-profile/survey.json +++ b/packages/ghost-fingerprint/test/fixtures/surface-fingerprint/survey.json @@ -2,7 +2,7 @@ "schema": "ghost.survey/v2", "sources": [ { - "target": "fixture:surface-profile", + "target": "fixture:surface-fingerprint", "commit": "surface-v2", "scanned_at": "2026-05-04T00:00:00Z", "scanner_version": "fixture" @@ -12,7 +12,7 @@ { "id": "97719a4b33026494", "source": { - "target": "fixture:surface-profile", + "target": "fixture:surface-fingerprint", "commit": "surface-v2", "scanned_at": "2026-05-04T00:00:00Z", "scanner_version": "fixture" @@ -28,7 +28,7 @@ { "id": "75c94be49fc97eca", "source": { - "target": "fixture:surface-profile", + "target": "fixture:surface-fingerprint", "commit": "surface-v2", "scanned_at": "2026-05-04T00:00:00Z", "scanner_version": "fixture" @@ -43,7 +43,7 @@ { "id": "9edd2ada171052cb", "source": { - "target": "fixture:surface-profile", + "target": "fixture:surface-fingerprint", "commit": "surface-v2", "scanned_at": "2026-05-04T00:00:00Z", "scanner_version": "fixture" @@ -56,7 +56,7 @@ { "id": "3df16501df968163", "source": { - "target": "fixture:surface-profile", + "target": "fixture:surface-fingerprint", "commit": "surface-v2", "scanned_at": "2026-05-04T00:00:00Z", "scanner_version": "fixture" diff --git a/packages/ghost-fingerprint/test/scan-status.test.ts b/packages/ghost-fingerprint/test/scan-status.test.ts index d04c2572..350caa99 100644 --- a/packages/ghost-fingerprint/test/scan-status.test.ts +++ b/packages/ghost-fingerprint/test/scan-status.test.ts @@ -21,14 +21,25 @@ describe("scanStatus", () => { it("reports all-missing for an empty directory", async () => { const status = await scanStatus(dir); + expect(status.resources.state).toBe("missing"); expect(status.map.state).toBe("missing"); expect(status.survey.state).toBe("missing"); - expect(status.profile.state).toBe("missing"); + expect(status.patterns.state).toBe("missing"); expect(status.checks.state).toBe("missing"); + expect(status.intent.state).toBe("missing"); + expect(status.recommended_next).toBe("resources"); + }); + + it("recommends map when only resources.yml exists", async () => { + await writeFile(join(dir, "resources.yml"), "schema: ghost.resources/v1\n"); + const status = await scanStatus(dir); + expect(status.resources.state).toBe("present"); + expect(status.map.state).toBe("missing"); expect(status.recommended_next).toBe("map"); }); - it("recommends survey when only map.md exists", async () => { + it("recommends survey when resources.yml and map.md exist", async () => { + await writeFile(join(dir, "resources.yml"), "schema: ghost.resources/v1\n"); await writeFile(join(dir, "map.md"), "---\nschema: ghost.map/v2\n---\n"); const status = await scanStatus(dir); expect(status.map.state).toBe("present"); @@ -36,7 +47,8 @@ describe("scanStatus", () => { expect(status.recommended_next).toBe("survey"); }); - it("recommends profile when map + survey exist but profile is missing", async () => { + it("recommends patterns when resources + map + survey exist but patterns are missing", async () => { + await writeFile(join(dir, "resources.yml"), "schema: ghost.resources/v1\n"); await writeFile(join(dir, "map.md"), "---\nschema: ghost.map/v2\n---\n"); await writeFile( join(dir, "survey.json"), @@ -45,28 +57,32 @@ describe("scanStatus", () => { const status = await scanStatus(dir); expect(status.map.state).toBe("present"); expect(status.survey.state).toBe("present"); - expect(status.profile.state).toBe("missing"); - expect(status.recommended_next).toBe("profile"); + expect(status.patterns.state).toBe("missing"); + expect(status.recommended_next).toBe("patterns"); }); - it("recommends checks when map + survey + profile exist but checks is missing", async () => { + it("reports complete when required artifacts exist but checks and intent are missing", async () => { + await writeFile(join(dir, "resources.yml"), "x"); await writeFile(join(dir, "map.md"), "---\nschema: ghost.map/v2\n---\n"); await writeFile( join(dir, "survey.json"), JSON.stringify({ schema: "ghost.survey/v2" }), ); - await writeFile(join(dir, "profile.md"), "y"); + await writeFile(join(dir, "patterns.yml"), "y"); const status = await scanStatus(dir); - expect(status.profile.state).toBe("present"); + expect(status.patterns.state).toBe("present"); expect(status.checks.state).toBe("missing"); - expect(status.recommended_next).toBe("checks"); + expect(status.intent.state).toBe("missing"); + expect(status.recommended_next).toBeNull(); }); it("returns recommended_next: null when every stage is present", async () => { + await writeFile(join(dir, "resources.yml"), "w"); await writeFile(join(dir, "map.md"), "x"); await writeFile(join(dir, "survey.json"), "{}"); - await writeFile(join(dir, "profile.md"), "y"); + await writeFile(join(dir, "patterns.yml"), "y"); await writeFile(join(dir, "checks.yml"), "z"); + await writeFile(join(dir, "intent.md"), "intent"); const status = await scanStatus(dir); expect(status.recommended_next).toBeNull(); }); @@ -74,8 +90,8 @@ describe("scanStatus", () => { it("treats empty (zero-byte) artifacts as missing", async () => { await writeFile(join(dir, "map.md"), ""); const status = await scanStatus(dir); - expect(status.map.state).toBe("missing"); - expect(status.recommended_next).toBe("map"); + expect(status.resources.state).toBe("missing"); + expect(status.recommended_next).toBe("resources"); }); it("paths returned in the report are absolute", async () => { diff --git a/packages/ghost-fleet/src/core/members.ts b/packages/ghost-fleet/src/core/members.ts index 55a62049..e7fa744c 100644 --- a/packages/ghost-fleet/src/core/members.ts +++ b/packages/ghost-fleet/src/core/members.ts @@ -8,7 +8,7 @@ import { MapFrontmatterSchema, type MapScope, } from "@ghost/core"; -import { loadFingerprint, PROFILE_FILENAME } from "ghost-fingerprint"; +import { FINGERPRINT_FILENAME, loadFingerprint } from "ghost-fingerprint"; import { parse as parseYaml } from "yaml"; import { FLEET_MEMBERS_DIRNAME } from "./schema.js"; import type { FleetMember, MemberSummary } from "./types.js"; @@ -48,7 +48,7 @@ export async function loadMembers(dir: string): Promise { /** * Resolve the members directory. * - * Convention is `/members//{map.md,profile.md}`. We also + * Convention is `/members//{map.md,fingerprint.md}`. We also * accept being pointed directly at a `members/` directory. */ function pickMembersRoot(root: string): string { @@ -66,7 +66,7 @@ function pickMembersRoot(root: string): string { /** * Load a single member directory. * - * Reads map.md, profile.md, and optional .ghost-sync.json. Each is + * Reads map.md, fingerprint.md, and optional .ghost-sync.json. Each is * surfaced through a status field so missing/broken inputs are visible * without crashing the rest of the load. */ @@ -74,7 +74,7 @@ async function loadMember(memberPath: string): Promise { const dirName = memberPath.split("/").pop() ?? ""; const mapPath = join(memberPath, MAP_FILENAME); - const fingerprintPath = join(memberPath, PROFILE_FILENAME); + const fingerprintPath = join(memberPath, FINGERPRINT_FILENAME); // Default identity is the directory basename; map.md `id` overrides. let id = dirName; diff --git a/packages/ghost-fleet/src/skill-bundle/SKILL.md b/packages/ghost-fleet/src/skill-bundle/SKILL.md index 06b41905..240e94fc 100644 --- a/packages/ghost-fleet/src/skill-bundle/SKILL.md +++ b/packages/ghost-fleet/src/skill-bundle/SKILL.md @@ -40,7 +40,7 @@ fleet/ └── fleet.json ``` -Each member is read-only. Fleet does **not** re-profile, refresh, or fetch; fingerprints evolve by deliberate act. If a member is stale, regenerate its `fingerprint.md` in that member's repo and re-run `ghost-fleet view`. +Each member is read-only. Fleet does **not** regenerate, refresh, or fetch; fingerprints evolve by deliberate act. If a member is stale, regenerate its `fingerprint.md` in that member's repo and re-run `ghost-fleet view`. ## Workflow — synthesizing fleet.md @@ -59,7 +59,7 @@ For the heuristics and reasoning patterns, see [references/target.md](references ## What this milestone does not do -- **Scoped governance.** Fleet reads scoped overlays and compares them as nested nodes, but it does not author or promote scoped divergences. The scan/profile pipeline owns those files. +- **Scoped governance.** Fleet reads scoped overlays and compares them as nested nodes, but it does not author or promote scoped divergences. The scan/fingerprint pipeline owns those files. - **Tracks-graph projection beyond the recorded edges.** Fleet emits exactly what each member declared in `.ghost-sync.json`. It does not infer transitive references. - **Temporal aggregation.** Per-member history aggregation (`fleet.history.json`) is deferred. - **Axis stacking.** `--groupby platform,registry` and similar filters are deferred to a follow-up. @@ -73,7 +73,7 @@ For the heuristics and reasoning patterns, see [references/target.md](references ## Never -- Never re-profile a member from inside the fleet recipe. Members are read-only inputs. +- Never regenerate a member from inside the fleet recipe. Members are read-only inputs. - Never invent clusters from thin air — anchor every cohort in either the pairwise distances or a group-by axis. - Never write distances back into the body. Numbers go in frontmatter; the body explains them. - Never rename a member in the CLI's output. If the id is wrong, fix the member's `map.md` and re-run. diff --git a/packages/ghost-fleet/test/fixtures/small-fleet/members/cash-android/profile.md b/packages/ghost-fleet/test/fixtures/small-fleet/members/cash-android/fingerprint.md similarity index 100% rename from packages/ghost-fleet/test/fixtures/small-fleet/members/cash-android/profile.md rename to packages/ghost-fleet/test/fixtures/small-fleet/members/cash-android/fingerprint.md diff --git a/packages/ghost-fleet/test/fixtures/small-fleet/members/cash-web/profile.md b/packages/ghost-fleet/test/fixtures/small-fleet/members/cash-web/fingerprint.md similarity index 100% rename from packages/ghost-fleet/test/fixtures/small-fleet/members/cash-web/profile.md rename to packages/ghost-fleet/test/fixtures/small-fleet/members/cash-web/fingerprint.md diff --git a/packages/ghost-fleet/test/fixtures/small-fleet/members/cash-web/fingerprints/accounts.md b/packages/ghost-fleet/test/fixtures/small-fleet/members/cash-web/fingerprints/accounts.md index 88308116..37caab0c 100644 --- a/packages/ghost-fleet/test/fixtures/small-fleet/members/cash-web/fingerprints/accounts.md +++ b/packages/ghost-fleet/test/fixtures/small-fleet/members/cash-web/fingerprints/accounts.md @@ -1,5 +1,5 @@ --- -extends: ../profile.md +extends: ../fingerprint.md id: cash-web-accounts palette: dominant: @@ -19,7 +19,7 @@ decisions: ### color-strategy Account management softens the inherited green into a calmer teal accent for -settings and profile surfaces. +settings and account surfaces. **Evidence:** - `#00b894` appears as the local account accent in `src/accounts`. diff --git a/packages/ghost-fleet/test/fixtures/small-fleet/members/cash-web/fingerprints/payments.md b/packages/ghost-fleet/test/fixtures/small-fleet/members/cash-web/fingerprints/payments.md index 409a8c97..bd43b8cf 100644 --- a/packages/ghost-fleet/test/fixtures/small-fleet/members/cash-web/fingerprints/payments.md +++ b/packages/ghost-fleet/test/fixtures/small-fleet/members/cash-web/fingerprints/payments.md @@ -1,5 +1,5 @@ --- -extends: ../profile.md +extends: ../fingerprint.md id: cash-web-payments decisions: - dimension: density diff --git a/packages/ghost-fleet/test/fixtures/small-fleet/members/ghost-ui/profile.md b/packages/ghost-fleet/test/fixtures/small-fleet/members/ghost-ui/fingerprint.md similarity index 100% rename from packages/ghost-fleet/test/fixtures/small-fleet/members/ghost-ui/profile.md rename to packages/ghost-fleet/test/fixtures/small-fleet/members/ghost-ui/fingerprint.md diff --git a/scripts/check-file-sizes.mjs b/scripts/check-file-sizes.mjs index 4bcdd4b2..e22f99a8 100644 --- a/scripts/check-file-sizes.mjs +++ b/scripts/check-file-sizes.mjs @@ -13,7 +13,7 @@ const EXCEPTIONS = { "packages/ghost-drift/src/bin.ts": { limit: 580, justification: - "CLI command registry — each command is small but there are 12 of them, plus multi-target profile parsing", + "CLI command registry — each command is small but there are 12 of them, plus multi-target fingerprint parsing", }, "packages/ghost-core/src/embedding/compare.ts": { limit: 600, From cff4e18a396a4e0fd5cfb86bd50624e8b9c94bf8 Mon Sep 17 00:00:00 2001 From: Nahiyan Khan Date: Sun, 10 May 2026 18:20:12 -0400 Subject: [PATCH 2/2] Dogfood root fingerprint package --- .changeset/emit-package-context-bundle.md | 5 + .ghost/checks.yml | 44 + .ghost/intent.md | 18 + .ghost/map.md | 234 + .ghost/patterns.yml | 514 + .ghost/resources.yml | 92 + .ghost/survey.json | 8306 +++++++++++++++++ apps/docs/src/generated/cli-manifest.json | 10 +- .../src/core/context/index.ts | 2 + .../src/core/context/package-writer.ts | 276 + packages/ghost-fingerprint/src/core/index.ts | 2 + .../ghost-fingerprint/src/emit-command.ts | 47 +- .../test/context/writer.test.ts | 48 + 13 files changed, 9582 insertions(+), 16 deletions(-) create mode 100644 .changeset/emit-package-context-bundle.md create mode 100644 .ghost/checks.yml create mode 100644 .ghost/intent.md create mode 100644 .ghost/map.md create mode 100644 .ghost/patterns.yml create mode 100644 .ghost/resources.yml create mode 100644 .ghost/survey.json create mode 100644 packages/ghost-fingerprint/src/core/context/package-writer.ts diff --git a/.changeset/emit-package-context-bundle.md b/.changeset/emit-package-context-bundle.md new file mode 100644 index 00000000..d54c4d4f --- /dev/null +++ b/.changeset/emit-package-context-bundle.md @@ -0,0 +1,5 @@ +--- +"ghost-fingerprint": patch +--- + +Emit context bundles from the root fingerprint package by default, while preserving legacy direct fingerprint emission behind `--fingerprint`. diff --git a/.ghost/checks.yml b/.ghost/checks.yml new file mode 100644 index 00000000..c404ef4e --- /dev/null +++ b/.ghost/checks.yml @@ -0,0 +1,44 @@ +schema: ghost.checks/v1 +id: ghost +checks: + - id: no-hardcoded-surface-hex + title: Prefer semantic color tokens over inline hex in UI surfaces + status: proposed + severity: serious + applies_to: + scopes: + - ghost-ui-components + - docs-site + surface_types: + - component-catalogue + - docs-foundation + - docs-home + detector: + type: forbidden-regex + pattern: '#[0-9a-fA-F]{6,8}' + evidence: + support: 0.9 + observed_count: 1 + examples: + - path: packages/ghost-ui/src/styles/main.css + note: Canonical color values are declared as CSS variables before use. + repair: Move repeatable UI colors into the semantic token layer in packages/ghost-ui/src/styles/main.css. + - id: component-pages-use-display-scale + title: Component catalogue pages keep the editorial display scale + status: proposed + severity: nit + applies_to: + scopes: + - docs-site + pattern_ids: + - component-catalogue-shell + detector: + type: required-regex + pattern: 'font-display' + evidence: + support: 0.88 + observed_count: 4 + examples: + - apps/docs/src/components/docs/page-header.tsx + - apps/docs/src/components/docs/component-page-shell.tsx + repair: Use the existing display heading helpers or `font-display` heading scale instead of local one-off title styling. diff --git a/.ghost/intent.md b/.ghost/intent.md new file mode 100644 index 00000000..e5e5b9f5 --- /dev/null +++ b/.ghost/intent.md @@ -0,0 +1,18 @@ +# Intent + +Ghost is dogfooding the root fingerprint package so the artifact is shaped by +real maintenance pressure before it becomes ceremony for other repos. + +The package should make four boundaries easy to feel: + +- `resources.yml` declares what counts as evidence. +- `map.md` routes work to scopes and surface families. +- `survey.json` records observed facts without taste or rationale. +- `patterns.yml` and `checks.yml` decide what those facts mean for generation, + advisory review, and deterministic enforcement. + +For Ghost's own UI, preserve the current editorial/workbench tension: plain +monochrome foundations, strong display type on docs surfaces, pill-like +controls, compact borders, and token-first color usage. Divergence is welcome +when it teaches the package something, but it should be named in this bundle +rather than hidden inside generated UI. diff --git a/.ghost/map.md b/.ghost/map.md new file mode 100644 index 00000000..353ceb38 --- /dev/null +++ b/.ghost/map.md @@ -0,0 +1,234 @@ +--- +schema: ghost.map/v2 +id: ghost +repo: block/ghost +subject: + id: ghost + target: github:block/ghost +sources: + - id: ghost + role: primary + target: github:block/ghost + paths: + - . + - id: ghost-ui + role: resolver + target: github:block/ghost + resolves: + - color + - spacing + - typography + - radius + - shadow + - component + paths: + - packages/ghost-ui/src + - packages/ghost-ui/registry.json +mapped_at: 2026-05-10 +platform: web +languages: + - { name: typescript, files: 453, share: 0.60 } + - { name: json, files: 160, share: 0.21 } + - { name: markdown, files: 92, share: 0.12 } + - { name: javascript, files: 18, share: 0.02 } + - { name: css, files: 10, share: 0.01 } + - { name: yaml, files: 5, share: 0.01 } +build_system: + - pnpm + - vite + - nx +package_manifests: + - package.json + - apps/docs/package.json + - packages/ghost-core/package.json + - packages/ghost-drift/package.json + - packages/ghost-fingerprint/package.json + - packages/ghost-fleet/package.json + - packages/ghost-ui/package.json +composition: + frameworks: + - { name: react, version: "19" } + - { name: vite } + - { name: vitest } + - { name: cac } + rendering: react-spa + styling: + - tailwind-v4 + - css-vars + navigation: react-router-file-routes +registry: + path: packages/ghost-ui/registry.json + components: 97 +design_system: + paths: + - packages/ghost-ui/src/components + - packages/ghost-ui/src/styles + - packages/ghost-ui/src/lib + entry_files: + - packages/ghost-ui/src/styles/main.css + - packages/ghost-ui/src/lib/theme-defaults.ts + - packages/ghost-ui/registry.json + derived_files: + - packages/ghost-ui/public/r/registry.json + - packages/ghost-ui/dist-lib/r/registry.json + token_source: inline + status: active +surface_sources: + render_strategy: docs + include: + - packages/ghost-ui/src/components/** + - apps/docs/src/app/** + - apps/docs/src/components/docs/** + - apps/docs/src/components/theme-panel/** + exclude: + - "**/dist/**" + - "**/dist-lib/**" + - "**/node_modules/**" + - "**/test/**" + - "**/*.test.ts" + coverage_gaps: + - Docs routes are source-observed in this dogfood pass; rendered browser screenshots are a later verification pass. +feature_areas: + - name: root-fingerprint-package + paths: + - .ghost + - packages/ghost-fingerprint/src/core/fingerprint-package.ts + - packages/ghost-fingerprint/src/core/verify-package.ts + sub_areas: + - resources + - map + - survey + - patterns + - checks + - name: ghost-fingerprint-cli + paths: + - packages/ghost-fingerprint/src/cli.ts + - packages/ghost-fingerprint/src/skill-bundle + sub_areas: + - inventory + - lint + - scan-status + - survey-ops + - emit + - name: ghost-drift + paths: + - packages/ghost-drift/src + sub_areas: + - compare + - check + - review + - evolution + - name: ghost-fleet + paths: + - packages/ghost-fleet/src + sub_areas: + - members + - view + - fleet-narrative + - name: ghost-ui + paths: + - packages/ghost-ui/src/components + - packages/ghost-ui/src/styles + - packages/ghost-ui/src/lib + sub_areas: + - primitives + - ai-elements + - theme + - registry + - name: docs-site + paths: + - apps/docs/src + sub_areas: + - home + - tools + - foundations + - component-catalogue + - theme-panel +scopes: + - id: root-fingerprint-package + name: Root fingerprint package + kind: design-memory + paths: + - .ghost + - packages/ghost-fingerprint/src/core/fingerprint-package.ts + - packages/ghost-fingerprint/src/core/verify-package.ts + - id: ghost-fingerprint-cli + name: Ghost Fingerprint CLI + kind: cli-tool + paths: + - packages/ghost-fingerprint/src/cli.ts + - packages/ghost-fingerprint/src/skill-bundle + - id: ghost-drift + name: Ghost Drift + kind: cli-tool + paths: + - packages/ghost-drift/src + - id: ghost-fleet + name: Ghost Fleet + kind: cli-tool + paths: + - packages/ghost-fleet/src + - id: ghost-core + name: Ghost Core + kind: library + paths: + - packages/ghost-core/src + - id: ghost-ui-components + name: Ghost UI Components + kind: design-system + paths: + - packages/ghost-ui/src/components + - id: ghost-ui-theme + name: Ghost UI Theme + kind: token-system + paths: + - packages/ghost-ui/src/styles + - packages/ghost-ui/src/lib/theme-defaults.ts + - packages/ghost-ui/src/lib/theme-presets.ts + - packages/ghost-ui/src/lib/theme-utils.ts + - id: docs-site + name: Docs Site + kind: product-surface + paths: + - apps/docs/src +orientation_files: + - README.md + - CLAUDE.md + - docs/fingerprint-format.md + - packages/ghost-fingerprint/README.md + - packages/ghost-fingerprint/src/skill-bundle/references/schema.md +--- + +## Identity + +Ghost is a TypeScript pnpm monorepo for repository-local design memory: +agents use the fingerprint package to preserve a product's design identity +while generating, reviewing, and verifying UI. The current shape is a root +`.ghost/` bundle, not a single fingerprint markdown file; the package separates +resource references, topology, observed evidence, operational patterns, +deterministic checks, and optional human intent. + +## Topology + +The design system lives in `packages/ghost-ui`, where `src/styles/main.css` +and `src/lib/theme-defaults.ts` define the token layer and +`registry.json` enumerates 97 shipped UI components. The docs app under +`apps/docs/src` is the main rendered surface: it hosts product pages, +foundation pages, component catalogue pages, AI element demos, and the theme +panel used to exercise the design system. + +The tool packages split by responsibility: `@ghost/core` owns shared schemas +and primitives, `ghost-fingerprint` owns the root package scan, `ghost-drift` +owns drift checking and evolution, and `ghost-fleet` owns multi-member fleet +narrative. The root `.ghost/` bundle is intentionally inside the repo so the +tools can dogfood their own artifact boundaries. + +## Conventions + +Code follows the package pattern `src/bin.ts`, `src/cli.ts`, `src/core/`, and +`test/`, with public library exports flowing through each package's core index. +The UI layer is Tailwind v4 plus CSS variables, with a monochrome-first palette, +pill-oriented controls, large editorial display type in docs, and a shadcn +registry for distribution. Checks should stay deterministic and scoped through +map scopes; judgment and product direction belong in `patterns.yml` and +`intent.md`, not in survey rows. diff --git a/.ghost/patterns.yml b/.ghost/patterns.yml new file mode 100644 index 00000000..19fa24f3 --- /dev/null +++ b/.ghost/patterns.yml @@ -0,0 +1,514 @@ +schema: ghost.patterns/v1 +id: ghost +surface_types: + - id: docs-foundation + title: docs-foundation + signals: + - /ui/foundations/colors + - /ui/foundations/typography + preferred_patterns: + - foundation-reference-grid + - token-swatch-table + - type-specimen-stack + evidence: + - surface_id: 39aebcb9d8b40803 + locator: /ui/foundations/colors + path: apps/docs/src/app/ui/foundations/colors/page.tsx + - surface_id: fb6207f34d22412c + locator: /ui/foundations/typography + path: apps/docs/src/app/ui/foundations/typography/page.tsx + - id: ai-element-demo + title: ai-element-demo + signals: + - /ui/components/agent + preferred_patterns: + - ai-element-demo-gallery + - preview-code-split + evidence: + - surface_id: 0bb1b078b4d715c4 + locator: /ui/components/agent + path: apps/docs/src/components/docs/ai-elements/index.tsx + - id: component-catalogue + title: component-catalogue + signals: + - /ui/components + preferred_patterns: + - component-catalogue-shell + - preview-code-split + evidence: + - surface_id: 3ef2b46784c9f5c7 + locator: /ui/components + path: apps/docs/src/app/ui/components/page.tsx + - id: docs-home + title: docs-home + signals: + - / + preferred_patterns: + - bento-preview-grid + - editorial-home-hero + evidence: + - surface_id: 70758ab39f697637 + locator: / + path: apps/docs/src/app/page.tsx + - id: primitive-demo + title: primitive-demo + signals: + - /ui/components/button + preferred_patterns: + - preview-code-split + - primitive-demo-grid + evidence: + - surface_id: bc0df98e421cb69c + locator: /ui/components/button + path: apps/docs/src/components/docs/primitives/index.tsx + - id: theme-control + title: theme-control + signals: + - apps/docs/src/components/theme-panel + preferred_patterns: + - theme-control-panel + - token-control-stack + evidence: + - surface_id: 0d73531a1c4e659f + locator: apps/docs/src/components/theme-panel + path: apps/docs/src/components/theme-panel/ThemePanel.tsx + - id: tool-doc + title: tool-doc + signals: + - /tools/fingerprint + preferred_patterns: + - cli-reference-block + - tool-doc-article + evidence: + - surface_id: 988c6ea8181325b8 + locator: /tools/fingerprint + path: apps/docs/src/app/tools/fingerprint/page.tsx +composition_patterns: + - id: preview-code-split + title: preview-code-split + surface_types: + - ai-element-demo + - component-catalogue + - primitive-demo + frequency: 3 + confidence: 0.38 + anatomy: + ordered: + - component-page-shell + - source-snippet + - code-tabs + - demo-grid + - display-header + - interactive-demo + - metadata-rail + - preview-frame + - sidebar + - variant-examples + - variant-list + traits: + density: + - standard + layout_shape: + - control-surface + - tracker + dominant_components: + - Accordion + - Agent + - Button + - Card + - CodeBlock + - Input + - Sidebar + - Tabs + source_signal: + - preview-code-split + evidence: + - surface_id: 3ef2b46784c9f5c7 + locator: /ui/components + path: apps/docs/src/app/ui/components/page.tsx + - surface_id: 0bb1b078b4d715c4 + locator: /ui/components/agent + path: apps/docs/src/components/docs/ai-elements/index.tsx + - surface_id: bc0df98e421cb69c + locator: /ui/components/button + path: apps/docs/src/components/docs/primitives/index.tsx + advisory: + - Use as advisory composition evidence; deterministic enforcement belongs + in checks.yml. + - id: foundation-reference-grid + title: foundation-reference-grid + surface_types: + - docs-foundation + frequency: 2 + confidence: 0.25 + anatomy: + ordered: + - display-header + - body-specimens + - swatch-grid + - token-sections + - token-table + - type-scale-specimens + - usage-notes + traits: + density: + - standard + layout_shape: + - comparison + dominant_components: + - Badge + - Card + - Table + source_signal: + - foundation-reference-grid + evidence: + - surface_id: 39aebcb9d8b40803 + locator: /ui/foundations/colors + path: apps/docs/src/app/ui/foundations/colors/page.tsx + - surface_id: fb6207f34d22412c + locator: /ui/foundations/typography + path: apps/docs/src/app/ui/foundations/typography/page.tsx + advisory: + - Use as advisory composition evidence; deterministic enforcement belongs + in checks.yml. + - id: ai-element-demo-gallery + title: ai-element-demo-gallery + surface_types: + - ai-element-demo + frequency: 1 + confidence: 0.13 + anatomy: + ordered: + - component-page-shell + - interactive-demo + - source-snippet + - variant-list + traits: + density: + - standard + layout_shape: + - control-surface + dominant_components: + - Accordion + - Agent + - CodeBlock + - Tabs + source_signal: + - ai-element-demo-gallery + evidence: + - surface_id: 0bb1b078b4d715c4 + locator: /ui/components/agent + path: apps/docs/src/components/docs/ai-elements/index.tsx + advisory: + - Use as advisory composition evidence; deterministic enforcement belongs + in checks.yml. + - id: bento-preview-grid + title: bento-preview-grid + surface_types: + - docs-home + frequency: 1 + confidence: 0.13 + anatomy: + ordered: + - bento-preview + - dock + - editorial-hero + - tool-links + traits: + density: + - breathing + layout_shape: + - article + dominant_components: + - Button + - Card + - Dock + source_signal: + - bento-preview-grid + evidence: + - surface_id: 70758ab39f697637 + locator: / + path: apps/docs/src/app/page.tsx + advisory: + - Use as advisory composition evidence; deterministic enforcement belongs + in checks.yml. + - id: cli-reference-block + title: cli-reference-block + surface_types: + - tool-doc + frequency: 1 + confidence: 0.13 + anatomy: + ordered: + - callouts + - cli-reference + - display-header + - prose-sections + traits: + density: + - standard + layout_shape: + - article + dominant_components: + - Callout + - CliHelp + - CodeBlock + source_signal: + - cli-reference-block + evidence: + - surface_id: 988c6ea8181325b8 + locator: /tools/fingerprint + path: apps/docs/src/app/tools/fingerprint/page.tsx + advisory: + - Use as advisory composition evidence; deterministic enforcement belongs + in checks.yml. + - id: component-catalogue-shell + title: component-catalogue-shell + surface_types: + - component-catalogue + frequency: 1 + confidence: 0.13 + anatomy: + ordered: + - code-tabs + - display-header + - metadata-rail + - preview-frame + - sidebar + traits: + density: + - standard + layout_shape: + - tracker + dominant_components: + - Button + - Input + - Sidebar + - Tabs + source_signal: + - component-catalogue-shell + evidence: + - surface_id: 3ef2b46784c9f5c7 + locator: /ui/components + path: apps/docs/src/app/ui/components/page.tsx + advisory: + - Use as advisory composition evidence; deterministic enforcement belongs + in checks.yml. + - id: editorial-home-hero + title: editorial-home-hero + surface_types: + - docs-home + frequency: 1 + confidence: 0.13 + anatomy: + ordered: + - bento-preview + - dock + - editorial-hero + - tool-links + traits: + density: + - breathing + layout_shape: + - article + dominant_components: + - Button + - Card + - Dock + source_signal: + - editorial-home-hero + evidence: + - surface_id: 70758ab39f697637 + locator: / + path: apps/docs/src/app/page.tsx + advisory: + - Use as advisory composition evidence; deterministic enforcement belongs + in checks.yml. + - id: primitive-demo-grid + title: primitive-demo-grid + surface_types: + - primitive-demo + frequency: 1 + confidence: 0.13 + anatomy: + ordered: + - component-page-shell + - demo-grid + - source-snippet + - variant-examples + traits: + density: + - standard + layout_shape: + - control-surface + dominant_components: + - Button + - Card + - Tabs + source_signal: + - primitive-demo-grid + evidence: + - surface_id: bc0df98e421cb69c + locator: /ui/components/button + path: apps/docs/src/components/docs/primitives/index.tsx + advisory: + - Use as advisory composition evidence; deterministic enforcement belongs + in checks.yml. + - id: theme-control-panel + title: theme-control-panel + surface_types: + - theme-control + frequency: 1 + confidence: 0.13 + anatomy: + ordered: + - reset-export-actions + - section-tabs + - sheet + - token-controls + traits: + density: + - compressed + layout_shape: + - control-surface + dominant_components: + - Button + - Input + - Sheet + - Slider + - Tabs + source_signal: + - theme-control-panel + evidence: + - surface_id: 0d73531a1c4e659f + locator: apps/docs/src/components/theme-panel + path: apps/docs/src/components/theme-panel/ThemePanel.tsx + advisory: + - Use as advisory composition evidence; deterministic enforcement belongs + in checks.yml. + - id: token-control-stack + title: token-control-stack + surface_types: + - theme-control + frequency: 1 + confidence: 0.13 + anatomy: + ordered: + - reset-export-actions + - section-tabs + - sheet + - token-controls + traits: + density: + - compressed + layout_shape: + - control-surface + dominant_components: + - Button + - Input + - Sheet + - Slider + - Tabs + source_signal: + - token-control-stack + evidence: + - surface_id: 0d73531a1c4e659f + locator: apps/docs/src/components/theme-panel + path: apps/docs/src/components/theme-panel/ThemePanel.tsx + advisory: + - Use as advisory composition evidence; deterministic enforcement belongs + in checks.yml. + - id: token-swatch-table + title: token-swatch-table + surface_types: + - docs-foundation + frequency: 1 + confidence: 0.13 + anatomy: + ordered: + - display-header + - swatch-grid + - token-sections + - usage-notes + traits: + density: + - standard + layout_shape: + - comparison + dominant_components: + - Badge + - Card + source_signal: + - token-swatch-table + evidence: + - surface_id: 39aebcb9d8b40803 + locator: /ui/foundations/colors + path: apps/docs/src/app/ui/foundations/colors/page.tsx + advisory: + - Use as advisory composition evidence; deterministic enforcement belongs + in checks.yml. + - id: tool-doc-article + title: tool-doc-article + surface_types: + - tool-doc + frequency: 1 + confidence: 0.13 + anatomy: + ordered: + - callouts + - cli-reference + - display-header + - prose-sections + traits: + density: + - standard + layout_shape: + - article + dominant_components: + - Callout + - CliHelp + - CodeBlock + source_signal: + - tool-doc-article + evidence: + - surface_id: 988c6ea8181325b8 + locator: /tools/fingerprint + path: apps/docs/src/app/tools/fingerprint/page.tsx + advisory: + - Use as advisory composition evidence; deterministic enforcement belongs + in checks.yml. + - id: type-specimen-stack + title: type-specimen-stack + surface_types: + - docs-foundation + frequency: 1 + confidence: 0.13 + anatomy: + ordered: + - body-specimens + - display-header + - token-table + - type-scale-specimens + traits: + density: + - standard + layout_shape: + - comparison + dominant_components: + - Card + - Table + source_signal: + - type-specimen-stack + evidence: + - surface_id: fb6207f34d22412c + locator: /ui/foundations/typography + path: apps/docs/src/app/ui/foundations/typography/page.tsx + advisory: + - Use as advisory composition evidence; deterministic enforcement belongs + in checks.yml. +advisory: + review_expectations: + - Identify the surface type before judging composition. + - Cite matching composition_patterns[].evidence and survey.ui_surfaces + evidence for advisory findings. + - Treat intent.md as human authority when present. diff --git a/.ghost/resources.yml b/.ghost/resources.yml new file mode 100644 index 00000000..86ae4b82 --- /dev/null +++ b/.ghost/resources.yml @@ -0,0 +1,92 @@ +schema: ghost.resources/v1 +id: ghost +primary: + id: ghost + target: github:block/ghost + paths: + - . + note: Ghost dogfoods its own root fingerprint package. +design_system: + - id: ghost-ui + target: . + kind: shadcn-registry + paths: + - packages/ghost-ui/src/components + - packages/ghost-ui/src/styles + - packages/ghost-ui/src/lib/theme-defaults.ts + - packages/ghost-ui/registry.json +surfaces: + - id: docs-home + name: Docs home + kind: route + locator: / + paths: + - apps/docs/src/app/page.tsx + - apps/docs/src/components/docs/hero.tsx + - id: docs-component-catalogue + name: Component catalogue + kind: route + locator: /ui/components + paths: + - apps/docs/src/app/ui/components/page.tsx + - apps/docs/src/components/docs/component-page-shell.tsx + - id: docs-foundations + name: Foundations + kind: route + locator: /ui/foundations + paths: + - apps/docs/src/app/ui/foundations/page.tsx + - apps/docs/src/components/docs/foundations/colors.tsx + - apps/docs/src/components/docs/foundations/typography.tsx + - id: docs-ai-elements + name: AI element demos + kind: doc-example + locator: /ui/components/agent + paths: + - apps/docs/src/components/docs/ai-elements/index.tsx + - id: docs-theme-panel + name: Theme panel + kind: source + locator: apps/docs/src/components/theme-panel + paths: + - apps/docs/src/components/theme-panel +docs: + - id: readme + target: . + paths: + - README.md + - docs/fingerprint-format.md + - packages/ghost-fingerprint/README.md + - id: agent-orientation + target: . + paths: + - CLAUDE.md + - GOVERNANCE.md +resolvers: + - id: registry + target: . + kind: component-registry + paths: + - packages/ghost-ui/registry.json + - id: theme + target: . + kind: token-source + paths: + - packages/ghost-ui/src/styles/main.css + - packages/ghost-ui/src/lib/theme-defaults.ts +include: + - ".ghost/**" + - "packages/ghost-core/src/**" + - "packages/ghost-drift/src/**" + - "packages/ghost-fingerprint/src/**" + - "packages/ghost-fleet/src/**" + - "packages/ghost-ui/src/**" + - "apps/docs/src/**" + - "docs/**" +exclude: + - "**/node_modules/**" + - "**/dist/**" + - "**/dist-lib/**" + - "**/coverage/**" + - "**/*.test.ts" + - "packages/ghost-fingerprint/test/fixtures/**" diff --git a/.ghost/survey.json b/.ghost/survey.json new file mode 100644 index 00000000..d3de0dc9 --- /dev/null +++ b/.ghost/survey.json @@ -0,0 +1,8306 @@ +{ + "schema": "ghost.survey/v2", + "sources": [ + { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + } + ], + "values": [ + { + "id": "a9f31b4bf3b06bf1", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "color", + "value": "#000000", + "raw": "--color-black, --background-inverse, --border-inverse, --color-background-inverse, --color-border-inverse", + "spec": { + "space": "srgb", + "hex": "#000000" + }, + "occurrences": 17, + "files_count": 2, + "usage": { + "css_var": 5 + }, + "role_hypothesis": "color-black" + }, + { + "id": "18a6a19beec6665b", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "color", + "value": "#0a0a0a", + "raw": "--surface-dark, --color-surface-dark", + "spec": { + "space": "srgb", + "hex": "#0a0a0a" + }, + "occurrences": 12, + "files_count": 2, + "usage": { + "css_var": 2 + }, + "role_hypothesis": "surface-dark" + }, + { + "id": "ba8513eb3edd6984", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "color", + "value": "#1a1a1a", + "raw": "--color-gray-900, --background-accent, --border-accent, --text-accent, --border-strong, --text-default, --ring, --foreground", + "spec": { + "space": "srgb", + "hex": "#1a1a1a" + }, + "occurrences": 104, + "files_count": 2, + "usage": { + "css_var": 31 + }, + "role_hypothesis": "color-gray-900" + }, + { + "id": "7b20111d17f2073c", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "color", + "value": "#232323", + "raw": "--color-gray-800", + "spec": { + "space": "srgb", + "hex": "#232323" + }, + "occurrences": 3, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "color-gray-800" + }, + { + "id": "aad3b191cd2542d1", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "color", + "value": "#333333", + "raw": "--color-gray-700", + "spec": { + "space": "srgb", + "hex": "#333333" + }, + "occurrences": 5, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "color-gray-700" + }, + { + "id": "3e3c87f0bddf3ef7", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "color", + "value": "#5c98f9", + "raw": "--color-blue-200, --background-info, --border-info, --text-info, --color-background-info, --color-border-info, --color-text-info", + "spec": { + "space": "srgb", + "hex": "#5c98f9" + }, + "occurrences": 23, + "files_count": 2, + "usage": { + "css_var": 7 + }, + "role_hypothesis": "color-blue-200" + }, + { + "id": "7ea7eb465c152edc", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "color", + "value": "#666666", + "raw": "--color-gray-600, --text-alt, --color-text-alt", + "spec": { + "space": "srgb", + "hex": "#666666" + }, + "occurrences": 9, + "files_count": 2, + "usage": { + "css_var": 3 + }, + "role_hypothesis": "color-gray-600" + }, + { + "id": "f166f4e0ace7fe2d", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "color", + "value": "#7585ff", + "raw": "--chart-2, --color-chart-2", + "spec": { + "space": "srgb", + "hex": "#7585ff" + }, + "occurrences": 6, + "files_count": 2, + "usage": { + "css_var": 2 + }, + "role_hypothesis": "chart-2" + }, + { + "id": "23867a939d5afe87", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "color", + "value": "#7cacff", + "raw": "--color-blue-100", + "spec": { + "space": "srgb", + "hex": "#7cacff" + }, + "occurrences": 3, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "color-blue-100" + }, + { + "id": "b5db7630fcbb35fa", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "color", + "value": "#91cb80", + "raw": "--color-green-200, --background-success, --border-success, --text-success, --chart-5, --color-background-success, --color-border-success, --color-text-success", + "spec": { + "space": "srgb", + "hex": "#91cb80" + }, + "occurrences": 29, + "files_count": 2, + "usage": { + "css_var": 9 + }, + "role_hypothesis": "color-green-200" + }, + { + "id": "42ad7d46c710cdd6", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "color", + "value": "#999999", + "raw": "--color-gray-500, --text-muted, --muted-foreground, --color-muted-foreground, --color-text-muted", + "spec": { + "space": "srgb", + "hex": "#999999" + }, + "occurrences": 16, + "files_count": 2, + "usage": { + "css_var": 5 + }, + "role_hypothesis": "color-gray-500" + }, + { + "id": "9c49a962962e0466", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "color", + "value": "#a3d795", + "raw": "--color-green-100", + "spec": { + "space": "srgb", + "hex": "#a3d795" + }, + "occurrences": 3, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "color-green-100" + }, + { + "id": "a3b8797d2f93fbfa", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "color", + "value": "#cccccc", + "raw": "--color-gray-400, --background-medium, --border-input-hover, --color-background-medium, --color-border-input-hover", + "spec": { + "space": "srgb", + "hex": "#cccccc" + }, + "occurrences": 15, + "files_count": 2, + "usage": { + "css_var": 5 + }, + "role_hypothesis": "color-gray-400" + }, + { + "id": "960c3648379754ed", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "color", + "value": "#d185e0", + "raw": "--chart-4, --color-chart-4", + "spec": { + "space": "srgb", + "hex": "#d185e0" + }, + "occurrences": 6, + "files_count": 2, + "usage": { + "css_var": 2 + }, + "role_hypothesis": "chart-4" + }, + { + "id": "4f0edcf1a4491e62", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "color", + "value": "#d76a6a", + "raw": "--chart-3, --color-chart-3", + "spec": { + "space": "srgb", + "hex": "#d76a6a" + }, + "occurrences": 6, + "files_count": 2, + "usage": { + "css_var": 2 + }, + "role_hypothesis": "chart-3" + }, + { + "id": "4e47c9f98274f2e8", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "color", + "value": "#e5e5e5", + "raw": "--color-gray-300, --border-input, --input, --color-input, --color-border-input", + "spec": { + "space": "srgb", + "hex": "#e5e5e5" + }, + "occurrences": 20, + "files_count": 2, + "usage": { + "css_var": 5 + }, + "role_hypothesis": "color-gray-300" + }, + { + "id": "d3e04ccbb7491453", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "color", + "value": "#e8e8e8", + "raw": "--color-gray-200, --border-default, --border-card, --border, --sidebar-border, --sidebar-ring, --color-border, --color-border-default", + "spec": { + "space": "srgb", + "hex": "#e8e8e8" + }, + "occurrences": 110, + "files_count": 2, + "usage": { + "css_var": 11 + }, + "role_hypothesis": "color-gray-200" + }, + { + "id": "8ff1823e26a321bd", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "color", + "value": "#f0f0f0", + "raw": "--color-gray-100, --background-muted, --secondary, --muted, --accent, --sidebar-accent, --color-secondary, --color-muted", + "spec": { + "space": "srgb", + "hex": "#f0f0f0" + }, + "occurrences": 48, + "files_count": 2, + "usage": { + "css_var": 11 + }, + "role_hypothesis": "color-gray-100" + }, + { + "id": "131a760e30a6cf86", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "color", + "value": "#f5f5f5", + "raw": "--color-gray-50, --background-alt, --surface-dark-text, --color-background-alt, --color-surface-dark-text", + "spec": { + "space": "srgb", + "hex": "#f5f5f5" + }, + "occurrences": 15, + "files_count": 2, + "usage": { + "css_var": 5 + }, + "role_hypothesis": "color-gray-50" + }, + { + "id": "6ecfdd9de39b9d71", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "color", + "value": "#f6b44a", + "raw": "--chart-1, --color-chart-1", + "spec": { + "space": "srgb", + "hex": "#f6b44a" + }, + "occurrences": 6, + "files_count": 2, + "usage": { + "css_var": 2 + }, + "role_hypothesis": "chart-1" + }, + { + "id": "be0766c0f7eab4de", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "color", + "value": "#f94b4b", + "raw": "--color-red-200, --background-danger, --border-danger, --text-danger, --destructive, --color-destructive, --color-background-danger, --color-border-danger", + "spec": { + "space": "srgb", + "hex": "#f94b4b" + }, + "occurrences": 33, + "files_count": 2, + "usage": { + "css_var": 9 + }, + "role_hypothesis": "color-red-200" + }, + { + "id": "7d02f6dd63d88b95", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "color", + "value": "#fbcd44", + "raw": "--color-yellow-200, --background-warning, --border-warning, --text-warning, --color-background-warning, --color-border-warning, --color-text-warning", + "spec": { + "space": "srgb", + "hex": "#fbcd44" + }, + "occurrences": 23, + "files_count": 2, + "usage": { + "css_var": 7 + }, + "role_hypothesis": "color-yellow-200" + }, + { + "id": "7ac40607dfb94364", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "color", + "value": "#ff6b6b", + "raw": "--color-red-100", + "spec": { + "space": "srgb", + "hex": "#ff6b6b" + }, + "occurrences": 3, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "color-red-100" + }, + { + "id": "dc01a8e50ed10552", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "color", + "value": "#ffd966", + "raw": "--color-yellow-100", + "spec": { + "space": "srgb", + "hex": "#ffd966" + }, + "occurrences": 3, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "color-yellow-100" + }, + { + "id": "bc5b4a11a51acf0a", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "color", + "value": "#ffffff", + "raw": "--color-white, --background-default, --text-inverse, --background, --card, --popover, --primary-foreground, --destructive-foreground", + "spec": { + "space": "srgb", + "hex": "#ffffff" + }, + "occurrences": 180, + "files_count": 2, + "usage": { + "css_var": 19 + }, + "role_hypothesis": "color-white" + }, + { + "id": "cf67469014699a80", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "color", + "value": "10px", + "raw": "--text-xxs", + "spec": { + "space": "unknown" + }, + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "text-xxs" + }, + { + "id": "1f36f8208f746e6f", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "color", + "value": "rgba(255, 255, 255, 0.08)", + "raw": "--surface-dark-border, --color-surface-dark-border", + "spec": { + "space": "srgb" + }, + "occurrences": 3, + "files_count": 2, + "usage": { + "css_var": 2 + }, + "role_hypothesis": "surface-dark-border" + }, + { + "id": "63b6f75bea048dea", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "color", + "value": "rgba(255, 255, 255, 0.5)", + "raw": "--surface-dark-muted, --color-surface-dark-muted", + "spec": { + "space": "srgb" + }, + "occurrences": 3, + "files_count": 2, + "usage": { + "css_var": 2 + }, + "role_hypothesis": "surface-dark-muted" + }, + { + "id": "e9adf99b5142ca5f", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "color", + "value": "rgba(26, 26, 26, 0.04)", + "raw": "--dark-04, --color-dark-04", + "spec": { + "space": "srgb" + }, + "occurrences": 4, + "files_count": 2, + "usage": { + "css_var": 2 + }, + "role_hypothesis": "dark-04" + }, + { + "id": "d1cdce4e51d4031e", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "color", + "value": "rgba(26, 26, 26, 0.1)", + "raw": "--dark-10, --color-dark-10", + "spec": { + "space": "srgb" + }, + "occurrences": 4, + "files_count": 2, + "usage": { + "css_var": 2 + }, + "role_hypothesis": "dark-10" + }, + { + "id": "4f8038bcb5f9a0c8", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "color", + "value": "rgba(26, 26, 26, 0.4)", + "raw": "--dark-40, --color-dark-40", + "spec": { + "space": "srgb" + }, + "occurrences": 4, + "files_count": 2, + "usage": { + "css_var": 2 + }, + "role_hypothesis": "dark-40" + }, + { + "id": "c2e7836d21ca4381", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "layout-primitive", + "value": "1440px", + "raw": "--page-container-max-width, --breakpoint-desktop", + "spec": { + "kind": "layout", + "scalar": 1440, + "unit": "px" + }, + "occurrences": 2, + "files_count": 2, + "usage": { + "css_var": 2 + }, + "role_hypothesis": "page-container-max-width" + }, + { + "id": "f3c68f24a0fb2388", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "motion", + "value": "0.15s", + "raw": "--duration-fast", + "spec": { + "duration_ms": 150 + }, + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "duration-fast" + }, + { + "id": "cfb32ebebed6cd6b", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "motion", + "value": "0.2s", + "raw": "--duration-normal", + "spec": { + "duration_ms": 200 + }, + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "duration-normal" + }, + { + "id": "771b722d1c949d22", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "motion", + "value": "0.4s", + "raw": "--duration-slow", + "spec": { + "duration_ms": 400 + }, + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "duration-slow" + }, + { + "id": "d5c83c3c956b8e21", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "motion", + "value": "accordion-down 0.2s ease-out", + "raw": "--animate-accordion-down", + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "animate-accordion-down" + }, + { + "id": "4234ff3696a48f01", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "motion", + "value": "accordion-up 0.2s ease-out", + "raw": "--animate-accordion-up", + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "animate-accordion-up" + }, + { + "id": "c76b1d1c38163037", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "motion", + "value": "caret-blink 1s ease-out infinite", + "raw": "--animate-caret-blink", + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "animate-caret-blink" + }, + { + "id": "7e03fc1230a1289b", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "motion", + "value": "cubic-bezier(0.33, 1, 0.68, 1)", + "raw": "--ease-spring", + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "ease-spring" + }, + { + "id": "eee639a2e706e2e2", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "motion", + "value": "enter-from-left 0.2s ease", + "raw": "--animate-enter-from-left", + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "animate-enter-from-left" + }, + { + "id": "f5d1c4abd5d79c30", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "motion", + "value": "enter-from-right 0.2s ease", + "raw": "--animate-enter-from-right", + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "animate-enter-from-right" + }, + { + "id": "7a6f96d08a408c7a", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "motion", + "value": "exit-to-left 0.2s ease", + "raw": "--animate-exit-to-left", + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "animate-exit-to-left" + }, + { + "id": "d53f1d345fc85826", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "motion", + "value": "exit-to-right 0.2s ease", + "raw": "--animate-exit-to-right", + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "animate-exit-to-right" + }, + { + "id": "e6bff463eaea4dd5", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "motion", + "value": "fade-in 0.2s ease", + "raw": "--animate-fade-in", + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "animate-fade-in" + }, + { + "id": "355c3b24f6d7a47f", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "motion", + "value": "fade-out 0.15s ease", + "raw": "--animate-fade-out", + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "animate-fade-out" + }, + { + "id": "ed713b344a604a68", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "motion", + "value": "scale-in 0.2s ease", + "raw": "--animate-scale-in", + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "animate-scale-in" + }, + { + "id": "239b9a9117f7c368", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "motion", + "value": "scale-out 0.15s ease", + "raw": "--animate-scale-out", + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "animate-scale-out" + }, + { + "id": "3edc9ec58bf00bba", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "motion", + "value": "word-reveal 0.4s ease-out", + "raw": "--animate-word-reveal", + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "animate-word-reveal" + }, + { + "id": "18ea813030328efb", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "radius", + "value": "10px", + "raw": "--radius-dropdown", + "spec": { + "scalar": 10, + "unit": "px" + }, + "occurrences": 3, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "radius-dropdown" + }, + { + "id": "c291dd5d4153c2d5", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "radius", + "value": "14px", + "raw": "--radius-card-sm", + "spec": { + "scalar": 14, + "unit": "px" + }, + "occurrences": 3, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "radius-card-sm" + }, + { + "id": "605073302e69cc8d", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "radius", + "value": "16px", + "raw": "--radius-modal", + "spec": { + "scalar": 16, + "unit": "px" + }, + "occurrences": 3, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "radius-modal" + }, + { + "id": "3ad5f3d6629fbc5b", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "radius", + "value": "20px", + "raw": "--radius, --radius-card, --radius-lg", + "spec": { + "scalar": 20, + "unit": "px" + }, + "occurrences": 45, + "files_count": 2, + "usage": { + "css_var": 3 + }, + "role_hypothesis": "radius" + }, + { + "id": "4584548f1ce64aeb", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "radius", + "value": "24px", + "raw": "--radius-card-lg", + "spec": { + "scalar": 24, + "unit": "px" + }, + "occurrences": 3, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "radius-card-lg" + }, + { + "id": "9db6261ab3311b2c", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "radius", + "value": "999px", + "raw": "--radius-pill, --radius-button, --radius-input", + "spec": { + "scalar": 999, + "unit": "px" + }, + "occurrences": 9, + "files_count": 2, + "usage": { + "css_var": 3 + }, + "role_hypothesis": "radius-pill" + }, + { + "id": "062105655a5d0f2a", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "radius", + "value": "calc(var(--radius) - 2px)", + "raw": "--radius-md", + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "radius-md" + }, + { + "id": "68287068c84eb4b9", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "radius", + "value": "calc(var(--radius) - 4px)", + "raw": "--radius-sm", + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "radius-sm" + }, + { + "id": "1fbb3aa0d3e1855e", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "radius", + "value": "calc(var(--radius) + 4px)", + "raw": "--radius-xl", + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "radius-xl" + }, + { + "id": "df05c7091824fc9c", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "shadow", + "value": "0 0 0 3px rgba(26, 26, 26, 0.15)", + "raw": "--shadow-date-field-focus", + "occurrences": 8, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "shadow-date-field-focus" + }, + { + "id": "f00de3f5c01d128b", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "shadow", + "value": "0 1px 4px rgba(76, 76, 76, 0.1) inset", + "raw": "--shadow-mini-inset", + "occurrences": 8, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "shadow-mini-inset" + }, + { + "id": "a0d219bd4865659b", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "shadow", + "value": "0 20px 60px rgba(0, 0, 0, 0.2)", + "raw": "--shadow-modal", + "occurrences": 8, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "shadow-modal" + }, + { + "id": "f7bce6dc77c2a982", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "shadow", + "value": "0 2px 8px rgba(76, 76, 76, 0.15)", + "raw": "--shadow-mini, --shadow-btn, --shadow-card, --shadow-kbd", + "occurrences": 40, + "files_count": 2, + "usage": { + "css_var": 4 + }, + "role_hypothesis": "shadow-mini" + }, + { + "id": "973c0f17cdec4787", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "shadow", + "value": "0 3px 12px rgba(76, 76, 76, 0.22)", + "raw": "--shadow-elevated", + "occurrences": 8, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "shadow-elevated" + }, + { + "id": "a10fe02018508861", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "shadow", + "value": "0 8px 30px rgba(0, 0, 0, 0.12)", + "raw": "--shadow-popover", + "occurrences": 8, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "shadow-popover" + }, + { + "id": "83562aee5c9507d9", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "spacing", + "value": "100px", + "raw": "--section-padding-vertical", + "spec": { + "scalar": 100, + "unit": "px" + }, + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "section-padding-vertical" + }, + { + "id": "19cab281365e5c2a", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "spacing", + "value": "2.75rem", + "raw": "--spacing-input-sm, --spacing-button", + "spec": { + "scalar": 2.75, + "unit": "rem" + }, + "occurrences": 3, + "files_count": 2, + "usage": { + "css_var": 2 + }, + "role_hypothesis": "spacing-input-sm" + }, + { + "id": "2b4c8f1d78bd2173", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "spacing", + "value": "20px", + "raw": "--page-container-side-gutter", + "spec": { + "scalar": 20, + "unit": "px" + }, + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "page-container-side-gutter" + }, + { + "id": "0fd4e40083e1c4cd", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "spacing", + "value": "2rem", + "raw": "--spacing-button-sm", + "spec": { + "scalar": 2, + "unit": "rem" + }, + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "spacing-button-sm" + }, + { + "id": "ae3d7c157798b919", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "spacing", + "value": "3.25rem", + "raw": "--spacing-input", + "spec": { + "scalar": 3.25, + "unit": "rem" + }, + "occurrences": 2, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "spacing-input" + }, + { + "id": "bfe99f529fc93178", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "typography", + "value": "-0.01em", + "raw": "--heading-card-letter-spacing, --body-reading-letter-spacing", + "occurrences": 2, + "files_count": 2, + "usage": { + "css_var": 2 + }, + "role_hypothesis": "heading-card-letter-spacing" + }, + { + "id": "951007b3c75825dc", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "typography", + "value": "-0.02em", + "raw": "--heading-sub-letter-spacing, --pullquote-letter-spacing", + "occurrences": 2, + "files_count": 2, + "usage": { + "css_var": 2 + }, + "role_hypothesis": "heading-sub-letter-spacing" + }, + { + "id": "211d147623c58cb0", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "typography", + "value": "-0.035em", + "raw": "--heading-section-letter-spacing", + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "heading-section-letter-spacing" + }, + { + "id": "569faba23dec2505", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "typography", + "value": "-0.05em", + "raw": "--heading-display-letter-spacing, --display-letter-spacing", + "occurrences": 2, + "files_count": 2, + "usage": { + "css_var": 2 + }, + "role_hypothesis": "heading-display-letter-spacing" + }, + { + "id": "a1c25d2eb54e86a6", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "typography", + "value": "\"Geist Mono\", monospace", + "raw": "--font-mono", + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "font-mono" + }, + { + "id": "bd3341bcb5f9c1da", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "typography", + "value": "0.12em", + "raw": "--label-letter-spacing", + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "label-letter-spacing" + }, + { + "id": "16777e9636f3668c", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "typography", + "value": "0.85", + "raw": "--display-line-height", + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "display-line-height" + }, + { + "id": "91f7103dfcef948f", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "typography", + "value": "0.88", + "raw": "--heading-display-line-height", + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "heading-display-line-height" + }, + { + "id": "44656ab8161b08f4", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "typography", + "value": "0.95", + "raw": "--heading-section-line-height", + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "heading-section-line-height" + }, + { + "id": "9744c6d26645e0ec", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "typography", + "value": "1", + "raw": "--heading-sub-line-height", + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "heading-sub-line-height" + }, + { + "id": "fe52d4dd9e779286", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "typography", + "value": "1.1", + "raw": "--heading-card-line-height", + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "heading-card-line-height" + }, + { + "id": "b92f1890b6712a12", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "typography", + "value": "1.2", + "raw": "--label-line-height", + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "label-line-height" + }, + { + "id": "bbb2f3695f1bc946", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "typography", + "value": "1.3", + "raw": "--pullquote-line-height", + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "pullquote-line-height" + }, + { + "id": "d46687996635218b", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "typography", + "value": "1.65", + "raw": "--body-reading-line-height", + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "body-reading-line-height" + }, + { + "id": "42bf8c3d3dbfb4de", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "typography", + "value": "11px", + "raw": "--label-font-size", + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "label-font-size" + }, + { + "id": "8e7e56bcc217c2ea", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "typography", + "value": "300", + "raw": "--pullquote-weight", + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "pullquote-weight" + }, + { + "id": "e774ba2bf08a1e0d", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "typography", + "value": "600", + "raw": "--heading-card-font-weight, --label-font-weight", + "occurrences": 2, + "files_count": 2, + "usage": { + "css_var": 2 + }, + "role_hypothesis": "heading-card-font-weight" + }, + { + "id": "01bb37f1c4f1995c", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "typography", + "value": "700", + "raw": "--heading-section-font-weight, --heading-sub-font-weight", + "occurrences": 2, + "files_count": 2, + "usage": { + "css_var": 2 + }, + "role_hypothesis": "heading-section-font-weight" + }, + { + "id": "9430236818f8410c", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "typography", + "value": "75px", + "raw": "--section-heading-margin-bottom", + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "section-heading-margin-bottom" + }, + { + "id": "afe9b28df06dcfd6", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "typography", + "value": "900", + "raw": "--heading-display-font-weight", + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "heading-display-font-weight" + }, + { + "id": "b7147d2408156970", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "typography", + "value": "clamp(1.5rem, 3vw, 2.5rem)", + "raw": "--pullquote-size", + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "pullquote-size" + }, + { + "id": "c4fb400e12e098fb", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "typography", + "value": "clamp(1rem, 1.3vw, 1.25rem)", + "raw": "--body-reading-size", + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "body-reading-size" + }, + { + "id": "7012dc2d0b80a7be", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "typography", + "value": "clamp(20px, 2vw, 28px)", + "raw": "--heading-card-font-size", + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "heading-card-font-size" + }, + { + "id": "413144b579b94f92", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "typography", + "value": "clamp(28px, 3vw, 40px)", + "raw": "--heading-sub-font-size", + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "heading-sub-font-size" + }, + { + "id": "ebb0972744526cd3", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "typography", + "value": "clamp(3rem, 12vw, 12rem)", + "raw": "--display-size", + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "display-size" + }, + { + "id": "edb9a16a3492c2df", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "typography", + "value": "clamp(44px, 5vw, 64px)", + "raw": "--heading-section-font-size", + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "heading-section-font-size" + }, + { + "id": "a4564bd5bd562131", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "typography", + "value": "clamp(64px, 8vw, 96px)", + "raw": "--heading-display-font-size", + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "heading-display-font-size" + }, + { + "id": "57e04e226e262587", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "typography", + "value": "serif", + "raw": "--font-serif", + "occurrences": 1, + "files_count": 2, + "usage": { + "css_var": 1 + }, + "role_hypothesis": "font-serif" + }, + { + "id": "68ca87c4122c1b55", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "kind": "typography", + "value": "system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif", + "raw": "--font-sans, --font-display", + "occurrences": 2, + "files_count": 2, + "usage": { + "css_var": 2 + }, + "role_hypothesis": "font-sans" + } + ], + "tokens": [ + { + "id": "af5376dba40bd163", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-white", + "alias_chain": [], + "resolved_value": "#ffffff", + "occurrences": 10 + }, + { + "id": "1b53a49ad3987bca", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-black", + "alias_chain": [], + "resolved_value": "#000000", + "occurrences": 5 + }, + { + "id": "845522c0986ef894", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-gray-50", + "alias_chain": [], + "resolved_value": "#f5f5f5", + "occurrences": 6 + }, + { + "id": "4f0809060ce66c2f", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-gray-100", + "alias_chain": [], + "resolved_value": "#f0f0f0", + "occurrences": 2 + }, + { + "id": "3bf740fe6dca78f9", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-gray-200", + "alias_chain": [], + "resolved_value": "#e8e8e8", + "occurrences": 3 + }, + { + "id": "736c067f592652a9", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-gray-300", + "alias_chain": [], + "resolved_value": "#e5e5e5", + "occurrences": 2 + }, + { + "id": "f9469ec2ab204b56", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-gray-400", + "alias_chain": [], + "resolved_value": "#cccccc", + "occurrences": 3 + }, + { + "id": "45644d8a3a969034", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-gray-500", + "alias_chain": [], + "resolved_value": "#999999", + "occurrences": 4 + }, + { + "id": "1a1ac6250d6372db", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-gray-600", + "alias_chain": [], + "resolved_value": "#666666", + "occurrences": 3 + }, + { + "id": "3f8b6ab728b82fec", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-gray-700", + "alias_chain": [], + "resolved_value": "#333333", + "occurrences": 5 + }, + { + "id": "ceacf3e0c937e617", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-gray-800", + "alias_chain": [], + "resolved_value": "#232323", + "occurrences": 3 + }, + { + "id": "1ed26988465ca21d", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-gray-900", + "alias_chain": [], + "resolved_value": "#1a1a1a", + "occurrences": 6 + }, + { + "id": "37dc02e5f16524e7", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-red-100", + "alias_chain": [], + "resolved_value": "#ff6b6b", + "occurrences": 3 + }, + { + "id": "1e69bec3b7b52d76", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-red-200", + "alias_chain": [], + "resolved_value": "#f94b4b", + "occurrences": 5 + }, + { + "id": "d3baf72fe56b5fe3", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-blue-100", + "alias_chain": [], + "resolved_value": "#7cacff", + "occurrences": 3 + }, + { + "id": "549b693914a168ef", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-blue-200", + "alias_chain": [], + "resolved_value": "#5c98f9", + "occurrences": 5 + }, + { + "id": "4d1fbd5b9cc21c50", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-green-100", + "alias_chain": [], + "resolved_value": "#a3d795", + "occurrences": 3 + }, + { + "id": "b7e45b4c0fafa933", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-green-200", + "alias_chain": [], + "resolved_value": "#91cb80", + "occurrences": 5 + }, + { + "id": "874df568d63deaf8", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-yellow-100", + "alias_chain": [], + "resolved_value": "#ffd966", + "occurrences": 3 + }, + { + "id": "b384db8e30d9cdfc", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-yellow-200", + "alias_chain": [], + "resolved_value": "#fbcd44", + "occurrences": 5 + }, + { + "id": "906349e51068ff1d", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--radius", + "alias_chain": [], + "resolved_value": "20px", + "occurrences": 35 + }, + { + "id": "9f51ced1db38f369", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--background-accent", + "alias_chain": ["--color-gray-900"], + "resolved_value": "#1a1a1a", + "occurrences": 9, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--background-accent", + "chain": ["--background-accent", "--color-gray-900"] + } + }, + { + "id": "a56c53d0aac38baf", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--border-accent", + "alias_chain": ["--color-gray-900"], + "resolved_value": "#1a1a1a", + "occurrences": 5, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--border-accent", + "chain": ["--border-accent", "--color-gray-900"] + } + }, + { + "id": "2413f90e2e19e3a5", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--text-accent", + "alias_chain": ["--color-gray-900"], + "resolved_value": "#1a1a1a", + "occurrences": 5, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--text-accent", + "chain": ["--text-accent", "--color-gray-900"] + } + }, + { + "id": "74fcf8d457c7bb85", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--background-default", + "alias_chain": ["--color-white"], + "resolved_value": "#ffffff", + "occurrences": 13, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--background-default", + "chain": ["--background-default", "--color-white"] + } + }, + { + "id": "97106902d16cfc16", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--background-alt", + "alias_chain": ["--color-gray-50"], + "resolved_value": "#f5f5f5", + "occurrences": 5, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--background-alt", + "chain": ["--background-alt", "--color-gray-50"] + } + }, + { + "id": "fc77e7ddf04a5016", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--background-medium", + "alias_chain": ["--color-gray-400"], + "resolved_value": "#cccccc", + "occurrences": 5, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--background-medium", + "chain": ["--background-medium", "--color-gray-400"] + } + }, + { + "id": "36ae5e90d20f1802", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--background-muted", + "alias_chain": ["--color-gray-100"], + "resolved_value": "#f0f0f0", + "occurrences": 13, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--background-muted", + "chain": ["--background-muted", "--color-gray-100"] + } + }, + { + "id": "28d2cdd26b7bede8", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--background-inverse", + "alias_chain": ["--color-black"], + "resolved_value": "#000000", + "occurrences": 5, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--background-inverse", + "chain": ["--background-inverse", "--color-black"] + } + }, + { + "id": "3b5705823c010799", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--background-danger", + "alias_chain": ["--color-red-200"], + "resolved_value": "#f94b4b", + "occurrences": 7, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--background-danger", + "chain": ["--background-danger", "--color-red-200"] + } + }, + { + "id": "1681c807705d45b7", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--background-success", + "alias_chain": ["--color-green-200"], + "resolved_value": "#91cb80", + "occurrences": 5, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--background-success", + "chain": ["--background-success", "--color-green-200"] + } + }, + { + "id": "52eec3b75127557e", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--background-info", + "alias_chain": ["--color-blue-200"], + "resolved_value": "#5c98f9", + "occurrences": 5, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--background-info", + "chain": ["--background-info", "--color-blue-200"] + } + }, + { + "id": "4aff120e7e5e91ad", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--background-warning", + "alias_chain": ["--color-yellow-200"], + "resolved_value": "#fbcd44", + "occurrences": 5, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--background-warning", + "chain": ["--background-warning", "--color-yellow-200"] + } + }, + { + "id": "d63df01772e2f3eb", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--border-default", + "alias_chain": ["--color-gray-200"], + "resolved_value": "#e8e8e8", + "occurrences": 11, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--border-default", + "chain": ["--border-default", "--color-gray-200"] + } + }, + { + "id": "bca5db483e6615b0", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--border-input", + "alias_chain": ["--color-gray-300"], + "resolved_value": "#e5e5e5", + "occurrences": 12, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--border-input", + "chain": ["--border-input", "--color-gray-300"] + } + }, + { + "id": "8ab85c7071fbdca1", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--border-input-hover", + "alias_chain": ["--color-gray-400"], + "resolved_value": "#cccccc", + "occurrences": 5, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--border-input-hover", + "chain": ["--border-input-hover", "--color-gray-400"] + } + }, + { + "id": "cc2f2efc3698129b", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--border-strong", + "alias_chain": ["--color-gray-900"], + "resolved_value": "#1a1a1a", + "occurrences": 7, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--border-strong", + "chain": ["--border-strong", "--color-gray-900"] + } + }, + { + "id": "bad35cd0c705bfed", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--border-card", + "alias_chain": ["--color-gray-200"], + "resolved_value": "#e8e8e8", + "occurrences": 5, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--border-card", + "chain": ["--border-card", "--color-gray-200"] + } + }, + { + "id": "bbbc47781f13dfe2", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--border-inverse", + "alias_chain": ["--color-black"], + "resolved_value": "#000000", + "occurrences": 5, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--border-inverse", + "chain": ["--border-inverse", "--color-black"] + } + }, + { + "id": "a3b8067f01a3637f", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--border-danger", + "alias_chain": ["--color-red-200"], + "resolved_value": "#f94b4b", + "occurrences": 5, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--border-danger", + "chain": ["--border-danger", "--color-red-200"] + } + }, + { + "id": "a6cc64c89d4ae06e", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--border-success", + "alias_chain": ["--color-green-200"], + "resolved_value": "#91cb80", + "occurrences": 5, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--border-success", + "chain": ["--border-success", "--color-green-200"] + } + }, + { + "id": "168a992269069883", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--border-warning", + "alias_chain": ["--color-yellow-200"], + "resolved_value": "#fbcd44", + "occurrences": 5, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--border-warning", + "chain": ["--border-warning", "--color-yellow-200"] + } + }, + { + "id": "090b0a052619f6b4", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--border-info", + "alias_chain": ["--color-blue-200"], + "resolved_value": "#5c98f9", + "occurrences": 5, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--border-info", + "chain": ["--border-info", "--color-blue-200"] + } + }, + { + "id": "f319b69e0cb0e5d3", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--text-default", + "alias_chain": ["--color-gray-900"], + "resolved_value": "#1a1a1a", + "occurrences": 19, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--text-default", + "chain": ["--text-default", "--color-gray-900"] + } + }, + { + "id": "da3ea0c72c44316c", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--text-muted", + "alias_chain": ["--color-gray-500"], + "resolved_value": "#999999", + "occurrences": 7, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--text-muted", + "chain": ["--text-muted", "--color-gray-500"] + } + }, + { + "id": "1c430ed2ee31c29b", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--text-alt", + "alias_chain": ["--color-gray-600"], + "resolved_value": "#666666", + "occurrences": 5, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--text-alt", + "chain": ["--text-alt", "--color-gray-600"] + } + }, + { + "id": "f8cf1defe7f9010b", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--text-inverse", + "alias_chain": ["--color-white"], + "resolved_value": "#ffffff", + "occurrences": 9, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--text-inverse", + "chain": ["--text-inverse", "--color-white"] + } + }, + { + "id": "26dc7db856f6426d", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--text-danger", + "alias_chain": ["--color-red-200"], + "resolved_value": "#f94b4b", + "occurrences": 5, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--text-danger", + "chain": ["--text-danger", "--color-red-200"] + } + }, + { + "id": "e45d2fb4aeb6347f", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--text-success", + "alias_chain": ["--color-green-200"], + "resolved_value": "#91cb80", + "occurrences": 5, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--text-success", + "chain": ["--text-success", "--color-green-200"] + } + }, + { + "id": "ebbcf1398a288317", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--text-warning", + "alias_chain": ["--color-yellow-200"], + "resolved_value": "#fbcd44", + "occurrences": 5, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--text-warning", + "chain": ["--text-warning", "--color-yellow-200"] + } + }, + { + "id": "c04f04c9e422377c", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--text-info", + "alias_chain": ["--color-blue-200"], + "resolved_value": "#5c98f9", + "occurrences": 5, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--text-info", + "chain": ["--text-info", "--color-blue-200"] + } + }, + { + "id": "471db6cbe4169490", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--ring", + "alias_chain": ["--border-strong"], + "resolved_value": "#1a1a1a", + "occurrences": 3, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--ring", + "chain": ["--ring", "--border-strong"] + } + }, + { + "id": "0ad56e1af6604abc", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--dark-10", + "alias_chain": [], + "resolved_value": "rgba(26, 26, 26, 0.1)", + "occurrences": 3 + }, + { + "id": "98e6b2a6639d33e8", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--dark-40", + "alias_chain": [], + "resolved_value": "rgba(26, 26, 26, 0.4)", + "occurrences": 3 + }, + { + "id": "d1437ab1b912152b", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--dark-04", + "alias_chain": [], + "resolved_value": "rgba(26, 26, 26, 0.04)", + "occurrences": 3 + }, + { + "id": "9024fff88b875e08", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--shadow-mini", + "alias_chain": [], + "resolved_value": "0 2px 8px rgba(76, 76, 76, 0.15)", + "occurrences": 16 + }, + { + "id": "8f417a0fc7f6de68", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--shadow-mini-inset", + "alias_chain": [], + "resolved_value": "0 1px 4px rgba(76, 76, 76, 0.1) inset", + "occurrences": 8 + }, + { + "id": "b80feb62715baae4", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--shadow-btn", + "alias_chain": [], + "resolved_value": "0 2px 8px rgba(76, 76, 76, 0.15)", + "occurrences": 8 + }, + { + "id": "381133012de067b6", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--shadow-card", + "alias_chain": [], + "resolved_value": "0 2px 8px rgba(76, 76, 76, 0.15)", + "occurrences": 8 + }, + { + "id": "b5d0e21f7c626b9a", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--shadow-elevated", + "alias_chain": [], + "resolved_value": "0 3px 12px rgba(76, 76, 76, 0.22)", + "occurrences": 8 + }, + { + "id": "e4023da4084e33ae", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--shadow-popover", + "alias_chain": [], + "resolved_value": "0 8px 30px rgba(0, 0, 0, 0.12)", + "occurrences": 8 + }, + { + "id": "7f24fe6ec1071390", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--shadow-modal", + "alias_chain": [], + "resolved_value": "0 20px 60px rgba(0, 0, 0, 0.2)", + "occurrences": 8 + }, + { + "id": "10087d66f765c570", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--shadow-kbd", + "alias_chain": [], + "resolved_value": "0 2px 8px rgba(76, 76, 76, 0.15)", + "occurrences": 8 + }, + { + "id": "13fb7c6bffa9a87e", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--shadow-date-field-focus", + "alias_chain": [], + "resolved_value": "0 0 0 3px rgba(26, 26, 26, 0.15)", + "occurrences": 8 + }, + { + "id": "4536fe6bd99eb83b", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--surface-dark", + "alias_chain": [], + "resolved_value": "#0a0a0a", + "occurrences": 8 + }, + { + "id": "625c65cc95cd0609", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--surface-dark-text", + "alias_chain": [], + "resolved_value": "#f5f5f5", + "occurrences": 2 + }, + { + "id": "6950aa023d7213ed", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--surface-dark-muted", + "alias_chain": [], + "resolved_value": "rgba(255, 255, 255, 0.5)", + "occurrences": 2 + }, + { + "id": "defd533639cddeb2", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--surface-dark-border", + "alias_chain": [], + "resolved_value": "rgba(255, 255, 255, 0.08)", + "occurrences": 2 + }, + { + "id": "a4f6eb218fc5c7cc", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--chart-1", + "alias_chain": [], + "resolved_value": "#f6b44a", + "occurrences": 5 + }, + { + "id": "fda9ebc4d3fc35cd", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--chart-2", + "alias_chain": [], + "resolved_value": "#7585ff", + "occurrences": 5 + }, + { + "id": "d0fdbbc1d4512092", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--chart-3", + "alias_chain": [], + "resolved_value": "#d76a6a", + "occurrences": 5 + }, + { + "id": "56bf3265ce048aeb", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--chart-4", + "alias_chain": [], + "resolved_value": "#d185e0", + "occurrences": 5 + }, + { + "id": "f426e5d0753d64dc", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--chart-5", + "alias_chain": [], + "resolved_value": "#91cb80", + "occurrences": 5 + }, + { + "id": "0a985b13ffd36500", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--background", + "alias_chain": ["--background-default"], + "resolved_value": "#ffffff", + "occurrences": 75, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--background", + "chain": ["--background", "--background-default"] + } + }, + { + "id": "53148a8eb1c75ac5", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--foreground", + "alias_chain": ["--text-default"], + "resolved_value": "#1a1a1a", + "occurrences": 3, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--foreground", + "chain": ["--foreground", "--text-default"] + } + }, + { + "id": "e8cb8dd81fd8d8d7", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--card", + "alias_chain": ["--background-default"], + "resolved_value": "#ffffff", + "occurrences": 6, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--card", + "chain": ["--card", "--background-default"] + } + }, + { + "id": "dbe38e947e776b79", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--card-foreground", + "alias_chain": ["--text-default"], + "resolved_value": "#1a1a1a", + "occurrences": 3, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--card-foreground", + "chain": ["--card-foreground", "--text-default"] + } + }, + { + "id": "1809440b51fbf75e", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--popover", + "alias_chain": ["--background-default"], + "resolved_value": "#ffffff", + "occurrences": 6, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--popover", + "chain": ["--popover", "--background-default"] + } + }, + { + "id": "9e36b58628419e93", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--popover-foreground", + "alias_chain": ["--text-default"], + "resolved_value": "#1a1a1a", + "occurrences": 3, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--popover-foreground", + "chain": ["--popover-foreground", "--text-default"] + } + }, + { + "id": "8475b48d1d48cfaa", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--primary", + "alias_chain": ["--background-accent"], + "resolved_value": "#1a1a1a", + "occurrences": 6, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--primary", + "chain": ["--primary", "--background-accent"] + } + }, + { + "id": "527d3246bb36c0ea", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--primary-foreground", + "alias_chain": ["--text-inverse"], + "resolved_value": "#ffffff", + "occurrences": 3, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--primary-foreground", + "chain": ["--primary-foreground", "--text-inverse"] + } + }, + { + "id": "856aeafc0c76dde9", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--secondary", + "alias_chain": ["--background-muted"], + "resolved_value": "#f0f0f0", + "occurrences": 6, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--secondary", + "chain": ["--secondary", "--background-muted"] + } + }, + { + "id": "748b4f47859407a6", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--secondary-foreground", + "alias_chain": ["--text-default"], + "resolved_value": "#1a1a1a", + "occurrences": 3, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--secondary-foreground", + "chain": ["--secondary-foreground", "--text-default"] + } + }, + { + "id": "feccdbd0fc2ce5bc", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--muted", + "alias_chain": ["--background-muted"], + "resolved_value": "#f0f0f0", + "occurrences": 6, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--muted", + "chain": ["--muted", "--background-muted"] + } + }, + { + "id": "1a6fc8fdab70d52f", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--muted-foreground", + "alias_chain": ["--text-muted"], + "resolved_value": "#999999", + "occurrences": 3, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--muted-foreground", + "chain": ["--muted-foreground", "--text-muted"] + } + }, + { + "id": "a5f2bb78be949af2", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--accent", + "alias_chain": ["--background-muted"], + "resolved_value": "#f0f0f0", + "occurrences": 6, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--accent", + "chain": ["--accent", "--background-muted"] + } + }, + { + "id": "b653a5d1f3435d3d", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--accent-foreground", + "alias_chain": ["--text-default"], + "resolved_value": "#1a1a1a", + "occurrences": 3, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--accent-foreground", + "chain": ["--accent-foreground", "--text-default"] + } + }, + { + "id": "dc8af16bb70cd3dc", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--destructive", + "alias_chain": ["--background-danger"], + "resolved_value": "#f94b4b", + "occurrences": 6, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--destructive", + "chain": ["--destructive", "--background-danger"] + } + }, + { + "id": "fe7b477439f0b507", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--destructive-foreground", + "alias_chain": [], + "resolved_value": "#ffffff", + "occurrences": 3 + }, + { + "id": "92d316a5bac7e85e", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--border", + "alias_chain": ["--border-default"], + "resolved_value": "#e8e8e8", + "occurrences": 68, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--border", + "chain": ["--border", "--border-default"] + } + }, + { + "id": "a231fa4e31b38083", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--input", + "alias_chain": ["--border-input"], + "resolved_value": "#e5e5e5", + "occurrences": 3, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--input", + "chain": ["--input", "--border-input"] + } + }, + { + "id": "3ef4068ddda579e4", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--sidebar", + "alias_chain": ["--background-default"], + "resolved_value": "#ffffff", + "occurrences": 24, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--sidebar", + "chain": ["--sidebar", "--background-default"] + } + }, + { + "id": "8e2222d70dadc317", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--sidebar-foreground", + "alias_chain": ["--text-default"], + "resolved_value": "#1a1a1a", + "occurrences": 3, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--sidebar-foreground", + "chain": ["--sidebar-foreground", "--text-default"] + } + }, + { + "id": "3e11587a434a9437", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--sidebar-primary", + "alias_chain": ["--background-accent"], + "resolved_value": "#1a1a1a", + "occurrences": 6, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--sidebar-primary", + "chain": ["--sidebar-primary", "--background-accent"] + } + }, + { + "id": "efa291c3fcd4e6c8", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--sidebar-primary-foreground", + "alias_chain": ["--text-inverse"], + "resolved_value": "#ffffff", + "occurrences": 3, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--sidebar-primary-foreground", + "chain": ["--sidebar-primary-foreground", "--text-inverse"] + } + }, + { + "id": "d10461023825b5c1", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--sidebar-accent", + "alias_chain": ["--background-muted"], + "resolved_value": "#f0f0f0", + "occurrences": 6, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--sidebar-accent", + "chain": ["--sidebar-accent", "--background-muted"] + } + }, + { + "id": "d8ad5ee92c43038b", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--sidebar-accent-foreground", + "alias_chain": ["--text-default"], + "resolved_value": "#1a1a1a", + "occurrences": 3, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--sidebar-accent-foreground", + "chain": ["--sidebar-accent-foreground", "--text-default"] + } + }, + { + "id": "d19d6b917be64789", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--sidebar-border", + "alias_chain": ["--border-default"], + "resolved_value": "#e8e8e8", + "occurrences": 3, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--sidebar-border", + "chain": ["--sidebar-border", "--border-default"] + } + }, + { + "id": "0e1e42898ac124d3", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--sidebar-ring", + "alias_chain": ["--border-default"], + "resolved_value": "#e8e8e8", + "occurrences": 3, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--sidebar-ring", + "chain": ["--sidebar-ring", "--border-default"] + } + }, + { + "id": "896713a7d8c37f01", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--heading-display-font-size", + "alias_chain": [], + "resolved_value": "clamp(64px, 8vw, 96px)", + "occurrences": 1 + }, + { + "id": "43b136e6b1d7566f", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--heading-display-line-height", + "alias_chain": [], + "resolved_value": "0.88", + "occurrences": 1 + }, + { + "id": "8f4701932c8686d7", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--heading-display-letter-spacing", + "alias_chain": [], + "resolved_value": "-0.05em", + "occurrences": 1 + }, + { + "id": "b6912a0e0f61ab17", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--heading-display-font-weight", + "alias_chain": [], + "resolved_value": "900", + "occurrences": 1 + }, + { + "id": "1a105e10345a7003", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--heading-section-font-size", + "alias_chain": [], + "resolved_value": "clamp(44px, 5vw, 64px)", + "occurrences": 1 + }, + { + "id": "775e9908aced14bd", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--heading-section-line-height", + "alias_chain": [], + "resolved_value": "0.95", + "occurrences": 1 + }, + { + "id": "bff128ced47d083e", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--heading-section-letter-spacing", + "alias_chain": [], + "resolved_value": "-0.035em", + "occurrences": 1 + }, + { + "id": "00ecea1141fb2834", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--heading-section-font-weight", + "alias_chain": [], + "resolved_value": "700", + "occurrences": 1 + }, + { + "id": "c374f08405de4318", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--heading-sub-font-size", + "alias_chain": [], + "resolved_value": "clamp(28px, 3vw, 40px)", + "occurrences": 1 + }, + { + "id": "dabd7c88ec5ea7a6", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--heading-sub-line-height", + "alias_chain": [], + "resolved_value": "1", + "occurrences": 1 + }, + { + "id": "2716db8c3403c93c", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--heading-sub-letter-spacing", + "alias_chain": [], + "resolved_value": "-0.02em", + "occurrences": 1 + }, + { + "id": "edd2c5578033c782", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--heading-sub-font-weight", + "alias_chain": [], + "resolved_value": "700", + "occurrences": 1 + }, + { + "id": "88841a7ccb052d3e", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--heading-card-font-size", + "alias_chain": [], + "resolved_value": "clamp(20px, 2vw, 28px)", + "occurrences": 1 + }, + { + "id": "40eea04958a37506", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--heading-card-line-height", + "alias_chain": [], + "resolved_value": "1.1", + "occurrences": 1 + }, + { + "id": "061a8ef71162d8ae", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--heading-card-letter-spacing", + "alias_chain": [], + "resolved_value": "-0.01em", + "occurrences": 1 + }, + { + "id": "5adf49eb38b9c8b9", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--heading-card-font-weight", + "alias_chain": [], + "resolved_value": "600", + "occurrences": 1 + }, + { + "id": "cea8572063047e40", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--display-size", + "alias_chain": [], + "resolved_value": "clamp(3rem, 12vw, 12rem)", + "occurrences": 1 + }, + { + "id": "3ee713ee72464630", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--display-line-height", + "alias_chain": [], + "resolved_value": "0.85", + "occurrences": 1 + }, + { + "id": "4205949d423d745a", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--display-letter-spacing", + "alias_chain": [], + "resolved_value": "-0.05em", + "occurrences": 1 + }, + { + "id": "b5b8fb38091a64c9", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--body-reading-size", + "alias_chain": [], + "resolved_value": "clamp(1rem, 1.3vw, 1.25rem)", + "occurrences": 1 + }, + { + "id": "70affdbdd41e1804", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--body-reading-line-height", + "alias_chain": [], + "resolved_value": "1.65", + "occurrences": 1 + }, + { + "id": "c0806c09c55148ed", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--body-reading-letter-spacing", + "alias_chain": [], + "resolved_value": "-0.01em", + "occurrences": 1 + }, + { + "id": "91b5b8138e98cce6", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--label-font-size", + "alias_chain": [], + "resolved_value": "11px", + "occurrences": 1 + }, + { + "id": "34ae51276ff287a4", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--label-letter-spacing", + "alias_chain": [], + "resolved_value": "0.12em", + "occurrences": 1 + }, + { + "id": "1988807b42f22192", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--label-font-weight", + "alias_chain": [], + "resolved_value": "600", + "occurrences": 1 + }, + { + "id": "a20a68251d83c7e8", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--label-line-height", + "alias_chain": [], + "resolved_value": "1.2", + "occurrences": 1 + }, + { + "id": "0a5832c734826ecd", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--pullquote-size", + "alias_chain": [], + "resolved_value": "clamp(1.5rem, 3vw, 2.5rem)", + "occurrences": 1 + }, + { + "id": "0e4c50633c8a4022", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--pullquote-line-height", + "alias_chain": [], + "resolved_value": "1.3", + "occurrences": 1 + }, + { + "id": "59fc48a20b209a2f", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--pullquote-weight", + "alias_chain": [], + "resolved_value": "300", + "occurrences": 1 + }, + { + "id": "09798f55339c9d1f", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--pullquote-letter-spacing", + "alias_chain": [], + "resolved_value": "-0.02em", + "occurrences": 1 + }, + { + "id": "b0351519a550f14a", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--page-container-max-width", + "alias_chain": [], + "resolved_value": "1440px", + "occurrences": 1 + }, + { + "id": "b3f0f4ee176d22ac", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--page-container-side-gutter", + "alias_chain": [], + "resolved_value": "20px", + "occurrences": 1 + }, + { + "id": "8bab5f720bdf19a3", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--section-padding-vertical", + "alias_chain": [], + "resolved_value": "100px", + "occurrences": 1 + }, + { + "id": "52abae73e353ea26", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--section-heading-margin-bottom", + "alias_chain": [], + "resolved_value": "75px", + "occurrences": 1 + }, + { + "id": "bc7108a54f98f698", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--ease-spring", + "alias_chain": [], + "resolved_value": "cubic-bezier(0.33, 1, 0.68, 1)", + "occurrences": 1 + }, + { + "id": "886a1ef5b5909914", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--duration-fast", + "alias_chain": [], + "resolved_value": "0.15s", + "occurrences": 1 + }, + { + "id": "22994d1ff0e2556d", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--duration-normal", + "alias_chain": [], + "resolved_value": "0.2s", + "occurrences": 1 + }, + { + "id": "2b9f4fd8f399a92e", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--duration-slow", + "alias_chain": [], + "resolved_value": "0.4s", + "occurrences": 1 + }, + { + "id": "f72bbf735bc6e972", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-background", + "alias_chain": ["--background"], + "resolved_value": "#ffffff", + "occurrences": 11, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-background", + "chain": ["--color-background", "--background"] + } + }, + { + "id": "f8ca66e251a6933f", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-foreground", + "alias_chain": ["--foreground"], + "resolved_value": "#1a1a1a", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-foreground", + "chain": ["--color-foreground", "--foreground"] + } + }, + { + "id": "8b1184027956cb34", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-card", + "alias_chain": ["--card"], + "resolved_value": "#ffffff", + "occurrences": 2, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-card", + "chain": ["--color-card", "--card"] + } + }, + { + "id": "df8040971797960a", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-card-foreground", + "alias_chain": ["--card-foreground"], + "resolved_value": "#1a1a1a", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-card-foreground", + "chain": ["--color-card-foreground", "--card-foreground"] + } + }, + { + "id": "7cafe9e00919fb33", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-popover", + "alias_chain": ["--popover"], + "resolved_value": "#ffffff", + "occurrences": 2, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-popover", + "chain": ["--color-popover", "--popover"] + } + }, + { + "id": "a5fcb064aac808e3", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-popover-foreground", + "alias_chain": ["--popover-foreground"], + "resolved_value": "#1a1a1a", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-popover-foreground", + "chain": ["--color-popover-foreground", "--popover-foreground"] + } + }, + { + "id": "3f9811d489c979a4", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-primary", + "alias_chain": ["--primary"], + "resolved_value": "#1a1a1a", + "occurrences": 2, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-primary", + "chain": ["--color-primary", "--primary"] + } + }, + { + "id": "c2b9254b0168029b", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-primary-foreground", + "alias_chain": ["--primary-foreground"], + "resolved_value": "#ffffff", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-primary-foreground", + "chain": ["--color-primary-foreground", "--primary-foreground"] + } + }, + { + "id": "96bb91e7250442ac", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-secondary", + "alias_chain": ["--secondary"], + "resolved_value": "#f0f0f0", + "occurrences": 2, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-secondary", + "chain": ["--color-secondary", "--secondary"] + } + }, + { + "id": "367366f2acf4b0e7", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-secondary-foreground", + "alias_chain": ["--secondary-foreground"], + "resolved_value": "#1a1a1a", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-secondary-foreground", + "chain": ["--color-secondary-foreground", "--secondary-foreground"] + } + }, + { + "id": "cf870345df82b4a1", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-muted", + "alias_chain": ["--muted"], + "resolved_value": "#f0f0f0", + "occurrences": 2, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-muted", + "chain": ["--color-muted", "--muted"] + } + }, + { + "id": "d8ad9386beb1ea44", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-muted-foreground", + "alias_chain": ["--muted-foreground"], + "resolved_value": "#999999", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-muted-foreground", + "chain": ["--color-muted-foreground", "--muted-foreground"] + } + }, + { + "id": "eb3ca21dadca918e", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-accent", + "alias_chain": ["--accent"], + "resolved_value": "#f0f0f0", + "occurrences": 2, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-accent", + "chain": ["--color-accent", "--accent"] + } + }, + { + "id": "3dead680e65486f8", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-accent-foreground", + "alias_chain": ["--accent-foreground"], + "resolved_value": "#1a1a1a", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-accent-foreground", + "chain": ["--color-accent-foreground", "--accent-foreground"] + } + }, + { + "id": "f07463a819a32f77", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-destructive", + "alias_chain": ["--destructive"], + "resolved_value": "#f94b4b", + "occurrences": 2, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-destructive", + "chain": ["--color-destructive", "--destructive"] + } + }, + { + "id": "13fe4de1e03e9f3f", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-destructive-foreground", + "alias_chain": ["--destructive-foreground"], + "resolved_value": "#ffffff", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-destructive-foreground", + "chain": ["--color-destructive-foreground", "--destructive-foreground"] + } + }, + { + "id": "bce4c88f0c1b891b", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-border", + "alias_chain": ["--border"], + "resolved_value": "#e8e8e8", + "occurrences": 13, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-border", + "chain": ["--color-border", "--border"] + } + }, + { + "id": "c2c3e2eb9b20fc14", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-input", + "alias_chain": ["--input"], + "resolved_value": "#e5e5e5", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-input", + "chain": ["--color-input", "--input"] + } + }, + { + "id": "4a0b5878231bfdcb", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-background-default", + "alias_chain": ["--background-default"], + "resolved_value": "#ffffff", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-background-default", + "chain": ["--color-background-default", "--background-default"] + } + }, + { + "id": "66d7dcfcab470bea", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-background-alt", + "alias_chain": ["--background-alt"], + "resolved_value": "#f5f5f5", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-background-alt", + "chain": ["--color-background-alt", "--background-alt"] + } + }, + { + "id": "f054479b6b90ebe4", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-background-medium", + "alias_chain": ["--background-medium"], + "resolved_value": "#cccccc", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-background-medium", + "chain": ["--color-background-medium", "--background-medium"] + } + }, + { + "id": "afb96ed9a7a17042", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-background-inverse", + "alias_chain": ["--background-inverse"], + "resolved_value": "#000000", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-background-inverse", + "chain": ["--color-background-inverse", "--background-inverse"] + } + }, + { + "id": "3789bf3bc9abefba", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-background-muted", + "alias_chain": ["--background-muted"], + "resolved_value": "#f0f0f0", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-background-muted", + "chain": ["--color-background-muted", "--background-muted"] + } + }, + { + "id": "374fd06df2eba681", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-background-danger", + "alias_chain": ["--background-danger"], + "resolved_value": "#f94b4b", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-background-danger", + "chain": ["--color-background-danger", "--background-danger"] + } + }, + { + "id": "a6d402c7703a51d3", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-background-success", + "alias_chain": ["--background-success"], + "resolved_value": "#91cb80", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-background-success", + "chain": ["--color-background-success", "--background-success"] + } + }, + { + "id": "bf3810a479a5cc74", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-background-info", + "alias_chain": ["--background-info"], + "resolved_value": "#5c98f9", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-background-info", + "chain": ["--color-background-info", "--background-info"] + } + }, + { + "id": "13713948a79f2679", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-background-warning", + "alias_chain": ["--background-warning"], + "resolved_value": "#fbcd44", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-background-warning", + "chain": ["--color-background-warning", "--background-warning"] + } + }, + { + "id": "21def7f0d4e5cb45", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-background-accent", + "alias_chain": ["--background-accent"], + "resolved_value": "#1a1a1a", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-background-accent", + "chain": ["--color-background-accent", "--background-accent"] + } + }, + { + "id": "aac6e9227330b55a", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-border-accent", + "alias_chain": ["--border-accent"], + "resolved_value": "#1a1a1a", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-border-accent", + "chain": ["--color-border-accent", "--border-accent"] + } + }, + { + "id": "27d7b82ca6aefc14", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-text-accent", + "alias_chain": ["--text-accent"], + "resolved_value": "#1a1a1a", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-text-accent", + "chain": ["--color-text-accent", "--text-accent"] + } + }, + { + "id": "c8ae4cd2d4b53d80", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-border-default", + "alias_chain": ["--border-default"], + "resolved_value": "#e8e8e8", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-border-default", + "chain": ["--color-border-default", "--border-default"] + } + }, + { + "id": "2545a0b379c5dd74", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-border-input", + "alias_chain": ["--border-input"], + "resolved_value": "#e5e5e5", + "occurrences": 2, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-border-input", + "chain": ["--color-border-input", "--border-input"] + } + }, + { + "id": "00f9bab2dd595709", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-border-input-hover", + "alias_chain": ["--border-input-hover"], + "resolved_value": "#cccccc", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-border-input-hover", + "chain": ["--color-border-input-hover", "--border-input-hover"] + } + }, + { + "id": "7ce7e455a0fb9f8b", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-border-strong", + "alias_chain": ["--border-strong"], + "resolved_value": "#1a1a1a", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-border-strong", + "chain": ["--color-border-strong", "--border-strong"] + } + }, + { + "id": "61b66beffac8bf24", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-border-card", + "alias_chain": ["--border-card"], + "resolved_value": "#e8e8e8", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-border-card", + "chain": ["--color-border-card", "--border-card"] + } + }, + { + "id": "6a9c040ecf9c31a0", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-border-inverse", + "alias_chain": ["--border-inverse"], + "resolved_value": "#000000", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-border-inverse", + "chain": ["--color-border-inverse", "--border-inverse"] + } + }, + { + "id": "69eba625282a1af4", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-border-danger", + "alias_chain": ["--border-danger"], + "resolved_value": "#f94b4b", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-border-danger", + "chain": ["--color-border-danger", "--border-danger"] + } + }, + { + "id": "1a6a79f20ffd733b", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-border-success", + "alias_chain": ["--border-success"], + "resolved_value": "#91cb80", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-border-success", + "chain": ["--color-border-success", "--border-success"] + } + }, + { + "id": "80074da885dc0913", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-border-warning", + "alias_chain": ["--border-warning"], + "resolved_value": "#fbcd44", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-border-warning", + "chain": ["--color-border-warning", "--border-warning"] + } + }, + { + "id": "373539346e5f9fa5", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-border-info", + "alias_chain": ["--border-info"], + "resolved_value": "#5c98f9", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-border-info", + "chain": ["--color-border-info", "--border-info"] + } + }, + { + "id": "870e5c1a971604f6", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-text-default", + "alias_chain": ["--text-default"], + "resolved_value": "#1a1a1a", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-text-default", + "chain": ["--color-text-default", "--text-default"] + } + }, + { + "id": "4a4fd28e2a6b1fa7", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-text-muted", + "alias_chain": ["--text-muted"], + "resolved_value": "#999999", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-text-muted", + "chain": ["--color-text-muted", "--text-muted"] + } + }, + { + "id": "c8bc01b43c6a0a47", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-text-alt", + "alias_chain": ["--text-alt"], + "resolved_value": "#666666", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-text-alt", + "chain": ["--color-text-alt", "--text-alt"] + } + }, + { + "id": "c114c23a14781c9c", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-text-inverse", + "alias_chain": ["--text-inverse"], + "resolved_value": "#ffffff", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-text-inverse", + "chain": ["--color-text-inverse", "--text-inverse"] + } + }, + { + "id": "7e1e5055d2f662d9", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-text-danger", + "alias_chain": ["--text-danger"], + "resolved_value": "#f94b4b", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-text-danger", + "chain": ["--color-text-danger", "--text-danger"] + } + }, + { + "id": "5dd42f85c53d8a64", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-text-success", + "alias_chain": ["--text-success"], + "resolved_value": "#91cb80", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-text-success", + "chain": ["--color-text-success", "--text-success"] + } + }, + { + "id": "e4b951e0386246be", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-text-warning", + "alias_chain": ["--text-warning"], + "resolved_value": "#fbcd44", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-text-warning", + "chain": ["--color-text-warning", "--text-warning"] + } + }, + { + "id": "a045974ec37b2079", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-text-info", + "alias_chain": ["--text-info"], + "resolved_value": "#5c98f9", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-text-info", + "chain": ["--color-text-info", "--text-info"] + } + }, + { + "id": "72158c0c05d566b0", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-dark-10", + "alias_chain": ["--dark-10"], + "resolved_value": "rgba(26, 26, 26, 0.1)", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-dark-10", + "chain": ["--color-dark-10", "--dark-10"] + } + }, + { + "id": "7a205d2a717e9d7d", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-dark-40", + "alias_chain": ["--dark-40"], + "resolved_value": "rgba(26, 26, 26, 0.4)", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-dark-40", + "chain": ["--color-dark-40", "--dark-40"] + } + }, + { + "id": "104f16489094253a", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-dark-04", + "alias_chain": ["--dark-04"], + "resolved_value": "rgba(26, 26, 26, 0.04)", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-dark-04", + "chain": ["--color-dark-04", "--dark-04"] + } + }, + { + "id": "9e079954a701e3ef", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-surface-dark", + "alias_chain": ["--surface-dark"], + "resolved_value": "#0a0a0a", + "occurrences": 4, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-surface-dark", + "chain": ["--color-surface-dark", "--surface-dark"] + } + }, + { + "id": "d82f673a36b1e02a", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-surface-dark-text", + "alias_chain": ["--surface-dark-text"], + "resolved_value": "#f5f5f5", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-surface-dark-text", + "chain": ["--color-surface-dark-text", "--surface-dark-text"] + } + }, + { + "id": "8980bfaa40a3d7fd", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-surface-dark-muted", + "alias_chain": ["--surface-dark-muted"], + "resolved_value": "rgba(255, 255, 255, 0.5)", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-surface-dark-muted", + "chain": ["--color-surface-dark-muted", "--surface-dark-muted"] + } + }, + { + "id": "0b124afe4b6a158e", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-surface-dark-border", + "alias_chain": ["--surface-dark-border"], + "resolved_value": "rgba(255, 255, 255, 0.08)", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-surface-dark-border", + "chain": ["--color-surface-dark-border", "--surface-dark-border"] + } + }, + { + "id": "2ca372c658a805db", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--font-sans", + "alias_chain": [], + "resolved_value": "system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif", + "occurrences": 1 + }, + { + "id": "f942d84b83e5d13a", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--font-mono", + "alias_chain": [], + "resolved_value": "\"Geist Mono\", monospace", + "occurrences": 1 + }, + { + "id": "43a2dee56e586fcd", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--font-serif", + "alias_chain": [], + "resolved_value": "serif", + "occurrences": 1 + }, + { + "id": "461226f8e0c4b997", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--font-display", + "alias_chain": [], + "resolved_value": "system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif", + "occurrences": 1 + }, + { + "id": "b4887cf71b4269d7", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--radius-pill", + "alias_chain": [], + "resolved_value": "999px", + "occurrences": 3 + }, + { + "id": "d36a686a7a98c06d", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--radius-button", + "alias_chain": [], + "resolved_value": "999px", + "occurrences": 3 + }, + { + "id": "337f14ab2dedd9a9", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--radius-input", + "alias_chain": [], + "resolved_value": "999px", + "occurrences": 3 + }, + { + "id": "42cb2e0e8566d246", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--radius-card", + "alias_chain": [], + "resolved_value": "20px", + "occurrences": 9 + }, + { + "id": "43038a1db7d19802", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--radius-card-lg", + "alias_chain": [], + "resolved_value": "24px", + "occurrences": 3 + }, + { + "id": "a14e793206786600", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--radius-card-sm", + "alias_chain": [], + "resolved_value": "14px", + "occurrences": 3 + }, + { + "id": "f7ebedc18a9ef7d9", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--radius-dropdown", + "alias_chain": [], + "resolved_value": "10px", + "occurrences": 3 + }, + { + "id": "50b6bd21a6abf67e", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--radius-modal", + "alias_chain": [], + "resolved_value": "16px", + "occurrences": 3 + }, + { + "id": "f630daf74d01a80d", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--radius-sm", + "alias_chain": ["--radius"], + "resolved_value": "calc(var(--radius) - 4px)", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--radius-sm", + "chain": ["--radius-sm", "--radius"] + } + }, + { + "id": "8bfd780b7c2af0cc", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--radius-md", + "alias_chain": ["--radius"], + "resolved_value": "calc(var(--radius) - 2px)", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--radius-md", + "chain": ["--radius-md", "--radius"] + } + }, + { + "id": "55ef653dc4c27842", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--radius-lg", + "alias_chain": ["--radius"], + "resolved_value": "20px", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--radius-lg", + "chain": ["--radius-lg", "--radius"] + } + }, + { + "id": "21d6ea0f52376302", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--radius-xl", + "alias_chain": ["--radius"], + "resolved_value": "calc(var(--radius) + 4px)", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--radius-xl", + "chain": ["--radius-xl", "--radius"] + } + }, + { + "id": "4352868437d1e274", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-ring", + "alias_chain": ["--ring"], + "resolved_value": "#1a1a1a", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-ring", + "chain": ["--color-ring", "--ring"] + } + }, + { + "id": "69ef4ec85b16f330", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--spacing-input", + "alias_chain": [], + "resolved_value": "3.25rem", + "occurrences": 2 + }, + { + "id": "aae86efdef21f3eb", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--spacing-input-sm", + "alias_chain": [], + "resolved_value": "2.75rem", + "occurrences": 1 + }, + { + "id": "88298c9d13e5ec6f", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--spacing-button", + "alias_chain": [], + "resolved_value": "2.75rem", + "occurrences": 2 + }, + { + "id": "359468435621dd21", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--spacing-button-sm", + "alias_chain": [], + "resolved_value": "2rem", + "occurrences": 1 + }, + { + "id": "68fc8e65d1aa066a", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--text-xxs", + "alias_chain": [], + "resolved_value": "10px", + "occurrences": 1 + }, + { + "id": "4fe2fe73c9b5e047", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-chart-1", + "alias_chain": ["--chart-1"], + "resolved_value": "#f6b44a", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-chart-1", + "chain": ["--color-chart-1", "--chart-1"] + } + }, + { + "id": "ce176e5bbc8f5c7d", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-chart-2", + "alias_chain": ["--chart-2"], + "resolved_value": "#7585ff", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-chart-2", + "chain": ["--color-chart-2", "--chart-2"] + } + }, + { + "id": "48cf5e193ce0942c", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-chart-3", + "alias_chain": ["--chart-3"], + "resolved_value": "#d76a6a", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-chart-3", + "chain": ["--color-chart-3", "--chart-3"] + } + }, + { + "id": "58a65aa3edd80d01", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-chart-4", + "alias_chain": ["--chart-4"], + "resolved_value": "#d185e0", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-chart-4", + "chain": ["--color-chart-4", "--chart-4"] + } + }, + { + "id": "ae35e45ef7642e48", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-chart-5", + "alias_chain": ["--chart-5"], + "resolved_value": "#91cb80", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-chart-5", + "chain": ["--color-chart-5", "--chart-5"] + } + }, + { + "id": "f6d33cdef6460d8b", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-sidebar", + "alias_chain": ["--sidebar"], + "resolved_value": "#ffffff", + "occurrences": 8, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-sidebar", + "chain": ["--color-sidebar", "--sidebar"] + } + }, + { + "id": "60f603fa02b381ad", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-sidebar-foreground", + "alias_chain": ["--sidebar-foreground"], + "resolved_value": "#1a1a1a", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-sidebar-foreground", + "chain": ["--color-sidebar-foreground", "--sidebar-foreground"] + } + }, + { + "id": "633c45b1bad4a784", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-sidebar-primary", + "alias_chain": ["--sidebar-primary"], + "resolved_value": "#1a1a1a", + "occurrences": 2, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-sidebar-primary", + "chain": ["--color-sidebar-primary", "--sidebar-primary"] + } + }, + { + "id": "06310fca3f303c38", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-sidebar-primary-foreground", + "alias_chain": ["--sidebar-primary-foreground"], + "resolved_value": "#ffffff", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-sidebar-primary-foreground", + "chain": [ + "--color-sidebar-primary-foreground", + "--sidebar-primary-foreground" + ] + } + }, + { + "id": "15b77f1dd1b2e340", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-sidebar-accent", + "alias_chain": ["--sidebar-accent"], + "resolved_value": "#f0f0f0", + "occurrences": 2, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-sidebar-accent", + "chain": ["--color-sidebar-accent", "--sidebar-accent"] + } + }, + { + "id": "3558fbe98bf7adca", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-sidebar-accent-foreground", + "alias_chain": ["--sidebar-accent-foreground"], + "resolved_value": "#1a1a1a", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-sidebar-accent-foreground", + "chain": [ + "--color-sidebar-accent-foreground", + "--sidebar-accent-foreground" + ] + } + }, + { + "id": "3889b034962021f1", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-sidebar-border", + "alias_chain": ["--sidebar-border"], + "resolved_value": "#e8e8e8", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-sidebar-border", + "chain": ["--color-sidebar-border", "--sidebar-border"] + } + }, + { + "id": "6290855e5d5bab12", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--color-sidebar-ring", + "alias_chain": ["--sidebar-ring"], + "resolved_value": "#e8e8e8", + "occurrences": 1, + "resolution": { + "status": "resolved", + "source_id": "ghost", + "target": "packages/ghost-ui/src/styles/main.css", + "symbol": "--color-sidebar-ring", + "chain": ["--color-sidebar-ring", "--sidebar-ring"] + } + }, + { + "id": "52a4c7a748db4fc1", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--breakpoint-desktop", + "alias_chain": [], + "resolved_value": "1440px", + "occurrences": 1 + }, + { + "id": "21f028c273a6497e", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--animate-accordion-down", + "alias_chain": [], + "resolved_value": "accordion-down 0.2s ease-out", + "occurrences": 1 + }, + { + "id": "ee8c914a6e65006e", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--animate-accordion-up", + "alias_chain": [], + "resolved_value": "accordion-up 0.2s ease-out", + "occurrences": 1 + }, + { + "id": "ddd6b1e4eb3379ce", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--animate-caret-blink", + "alias_chain": [], + "resolved_value": "caret-blink 1s ease-out infinite", + "occurrences": 1 + }, + { + "id": "21235468757fa9c3", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--animate-scale-in", + "alias_chain": [], + "resolved_value": "scale-in 0.2s ease", + "occurrences": 1 + }, + { + "id": "0769cc137943e4f2", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--animate-scale-out", + "alias_chain": [], + "resolved_value": "scale-out 0.15s ease", + "occurrences": 1 + }, + { + "id": "5be60b6a4c8b777e", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--animate-fade-in", + "alias_chain": [], + "resolved_value": "fade-in 0.2s ease", + "occurrences": 1 + }, + { + "id": "1eaf0ceca8a6309c", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--animate-fade-out", + "alias_chain": [], + "resolved_value": "fade-out 0.15s ease", + "occurrences": 1 + }, + { + "id": "25183612050db8e1", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--animate-enter-from-left", + "alias_chain": [], + "resolved_value": "enter-from-left 0.2s ease", + "occurrences": 1 + }, + { + "id": "4c3e690d0e6ddf3d", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--animate-enter-from-right", + "alias_chain": [], + "resolved_value": "enter-from-right 0.2s ease", + "occurrences": 1 + }, + { + "id": "4c9a47d4b748cd79", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--animate-exit-to-left", + "alias_chain": [], + "resolved_value": "exit-to-left 0.2s ease", + "occurrences": 1 + }, + { + "id": "71ba4c76ec142f27", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--animate-exit-to-right", + "alias_chain": [], + "resolved_value": "exit-to-right 0.2s ease", + "occurrences": 1 + }, + { + "id": "078cf45522eed6da", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "--animate-word-reveal", + "alias_chain": [], + "resolved_value": "word-reveal 0.4s ease-out", + "occurrences": 1 + } + ], + "components": [ + { + "id": "12dee52001272b8a", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "accordion", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "e2660fc7b589d53e", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "alert-dialog", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "c7a8217228d9e976", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "alert", + "discovered_via": "packages/ghost-ui/registry.json", + "variants": ["palette", "spacing", "typography"] + }, + { + "id": "7e2c510f50a8570a", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "aspect-ratio", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "6761b54d432fa666", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "avatar", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "7aad92bc7e04b5fd", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "badge", + "discovered_via": "packages/ghost-ui/registry.json", + "variants": ["surfaces", "typography", "palette"] + }, + { + "id": "0dd9f36f23424c64", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "breadcrumb", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "cb390c2fd7d599c0", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "button-group", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "d16b29d8ea4ba907", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "button", + "discovered_via": "packages/ghost-ui/registry.json", + "variants": ["palette", "surfaces", "typography", "spacing"] + }, + { + "id": "ec8438075d5dd2f2", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "calendar", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "bbb638f190cf105d", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "card", + "discovered_via": "packages/ghost-ui/registry.json", + "variants": ["surfaces", "spacing", "typography"] + }, + { + "id": "651a8119f842986a", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "carousel", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "c09715b657226fa0", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "chart", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "49708ff995cd73fe", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "checkbox", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "0b06a7ef729534c9", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "collapsible", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "17c201825d483bdf", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "command", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "4c0e316019320d70", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "context-menu", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "4a73ce6a225fd987", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "dialog", + "discovered_via": "packages/ghost-ui/registry.json", + "variants": ["surfaces", "spacing", "palette"] + }, + { + "id": "8774aef166af65c7", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "drawer", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "3fff9558a1b10ec3", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "dropdown-menu", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "583a3bf9eb56b6f1", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "form", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "9e7ff525c68e1706", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "hover-card", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "93feb2cb2ba7236b", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "input-group", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "61fc771ec9e906c4", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "input-otp", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "ae83998ac74e36ae", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "input", + "discovered_via": "packages/ghost-ui/registry.json", + "variants": ["surfaces", "palette", "spacing"] + }, + { + "id": "2ee5ef7681802505", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "label", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "da8cf27745422e52", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "menubar", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "9056a27c59017996", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "navigation-menu", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "eb7cc2ccc5929700", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "pagination", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "875215866fc03ef9", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "popover", + "discovered_via": "packages/ghost-ui/registry.json", + "variants": ["surfaces", "palette", "spacing"] + }, + { + "id": "dbf94924a0df07af", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "progress", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "ad7eb6a376b9c48b", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "radio-group", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "7778fbe01435ea41", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "resizable", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "455dc479d8107a3f", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "scroll-area", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "e532c383ed14e686", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "select", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "ea40d850da0e61e0", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "separator", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "dc248ffedf15f003", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "sheet", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "246c2b8e20dd0cd7", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "sidebar", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "ac61a239c264b591", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "skeleton", + "discovered_via": "packages/ghost-ui/registry.json", + "variants": ["surfaces", "palette"] + }, + { + "id": "a42c49aa90427fa4", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "slider", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "ab5dc0298c729da9", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "sonner", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "07a9968cbea13031", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "spinner", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "edd21fd149640320", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "switch", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "7f9b06c5040e29d7", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "table", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "e2c3736cb15f60a5", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "tabs", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "6098f105c3e1edcd", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "textarea", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "4c8d6700b8892acd", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "toggle-group", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "41be7d939374f8bd", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "toggle", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "3d305e1e020f2969", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "tooltip", + "discovered_via": "packages/ghost-ui/registry.json", + "variants": ["palette", "surfaces", "typography"] + }, + { + "id": "c1dd0eeccd32ef38", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "agent", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "68b33e44d3c52a5d", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "artifact", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "b51c30d10d4d32e8", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "attachments", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "5db54926a5cfd239", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "audio-player", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "53f77eaadf3bc640", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "canvas", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "8585a7023828144f", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "chain-of-thought", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "1911ace57ca649e0", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "checkpoint", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "f2c58b9961e81025", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "code-block", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "4238fc57b0d03fe9", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "commit", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "9793f81865dc43d1", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "confirmation", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "6c7dc794c38e8bcc", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "connection", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "6db1b2aeb20e4893", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "context", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "bfe65e5c43d3e653", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "controls", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "493d8a3e54180860", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "conversation", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "eeb7f06e78f08fbd", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "edge", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "785b5999bbd1e4af", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "environment-variables", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "6215ab49d5ba3cbe", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "file-tree", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "3cc52ce506792527", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "image", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "eea3240eff6ae032", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "inline-citation", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "23dacaefe7e522d4", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "jsx-preview", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "01b2e1093242ddfc", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "message", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "fe79b5b0bb0b7b38", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "mic-selector", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "bcd47625208a412d", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "model-selector", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "f4143669c1ebe212", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "node", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "f21eed7fbcc37431", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "open-in-chat", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "c054eeb39ebd8cde", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "package-info", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "bd2af282a1f1f410", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "panel", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "45e40e4db7ebf0af", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "persona", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "d31270b70d7f050f", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "plan", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "a3013e2c67f9ce1f", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "prompt-input", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "113d5f3cdfc5d7d6", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "queue", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "ad6420db6f438451", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "reasoning", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "42e06f84c3874f55", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "sandbox", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "03209973ddceffdc", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "schema-display", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "0ccc859b514e65ce", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "shimmer", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "82e6b09db8922c97", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "snippet", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "8f9222184a16c0a9", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "sources", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "92f3e84577a17f03", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "speech-input", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "b03869884b50b80a", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "stack-trace", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "35adea9aa288021e", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "suggestion", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "8ef4915fd9983015", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "task", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "6f54a5bbb0aabdd7", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "terminal", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "c213ad390db3f611", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "test-results", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "4fbd0f25bbb2b4b3", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "tool", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "5b2076412ffc78b4", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "toolbar", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "378a44daa2d243c1", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "transcription", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "f990078c3f8751bf", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "voice-selector", + "discovered_via": "packages/ghost-ui/registry.json" + }, + { + "id": "613c98ed8657ff10", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "web-preview", + "discovered_via": "packages/ghost-ui/registry.json" + } + ], + "ui_surfaces": [ + { + "id": "70758ab39f697637", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "Docs home", + "kind": "route", + "locator": "/", + "renderability": "source-only", + "files": [ + "apps/docs/src/app/page.tsx", + "apps/docs/src/components/docs/hero.tsx" + ], + "classification": { + "intent": "introduce", + "surface_type": "docs-home", + "density": "breathing", + "layout_shape": "article", + "confidence": 0.82 + }, + "composition": { + "anatomy": ["editorial-hero", "tool-links", "bento-preview", "dock"], + "primary_region": "hero", + "action_placement": ["below-heading", "dock"], + "navigation_context": "docs-shell", + "responsive_behavior": [ + "hero type remains oversized", + "cards collapse into a single column" + ], + "confidence": 0.78 + }, + "signals": { + "dominant_components": ["Button", "Card", "Dock"], + "layout_patterns": ["editorial-home-hero", "bento-preview-grid"], + "breakpoint_behavior": [ + "mobile stacks supporting panels under the hero" + ], + "notes": [ + "First viewport uses large black display type with sparse controls." + ] + } + }, + { + "id": "3ef2b46784c9f5c7", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "Component catalogue index", + "kind": "route", + "locator": "/ui/components", + "renderability": "source-only", + "files": [ + "apps/docs/src/app/ui/components/page.tsx", + "apps/docs/src/components/docs/component-page-shell.tsx" + ], + "classification": { + "intent": "browse", + "surface_type": "component-catalogue", + "density": "standard", + "layout_shape": "tracker", + "confidence": 0.86 + }, + "composition": { + "anatomy": [ + "sidebar", + "display-header", + "preview-frame", + "code-tabs", + "metadata-rail" + ], + "primary_region": "catalogue-content", + "action_placement": ["header", "preview-toolbar"], + "navigation_context": "persistent-registry-sidebar", + "responsive_behavior": [ + "mobile opens sidebar from icon button", + "content remains one-column" + ], + "confidence": 0.84 + }, + "signals": { + "dominant_components": ["Sidebar", "Tabs", "Button", "Input"], + "layout_patterns": ["component-catalogue-shell", "preview-code-split"], + "breakpoint_behavior": ["sidebar becomes mobile drawer"], + "notes": [ + "Catalogue pages pair editorial headers with utilitarian demo controls." + ] + } + }, + { + "id": "39aebcb9d8b40803", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "Foundations colors", + "kind": "route", + "locator": "/ui/foundations/colors", + "renderability": "source-only", + "files": [ + "apps/docs/src/app/ui/foundations/colors/page.tsx", + "apps/docs/src/components/docs/foundations/colors.tsx" + ], + "classification": { + "intent": "reference", + "surface_type": "docs-foundation", + "density": "standard", + "layout_shape": "comparison", + "confidence": 0.86 + }, + "composition": { + "anatomy": [ + "display-header", + "token-sections", + "swatch-grid", + "usage-notes" + ], + "primary_region": "reference-grid", + "action_placement": ["none"], + "navigation_context": "registry-sidebar", + "responsive_behavior": [ + "swatch grids reduce columns on smaller screens" + ], + "confidence": 0.8 + }, + "signals": { + "dominant_components": ["Card", "Badge"], + "layout_patterns": ["foundation-reference-grid", "token-swatch-table"], + "breakpoint_behavior": ["grid density adapts by viewport"], + "notes": [ + "Color documentation foregrounds semantic tokens over raw hex values." + ] + } + }, + { + "id": "fb6207f34d22412c", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "Foundations typography", + "kind": "route", + "locator": "/ui/foundations/typography", + "renderability": "source-only", + "files": [ + "apps/docs/src/app/ui/foundations/typography/page.tsx", + "apps/docs/src/components/docs/foundations/typography.tsx" + ], + "classification": { + "intent": "reference", + "surface_type": "docs-foundation", + "density": "standard", + "layout_shape": "comparison", + "confidence": 0.84 + }, + "composition": { + "anatomy": [ + "display-header", + "type-scale-specimens", + "body-specimens", + "token-table" + ], + "primary_region": "reference-grid", + "action_placement": ["none"], + "navigation_context": "registry-sidebar", + "responsive_behavior": [ + "specimens keep hierarchy while columns collapse" + ], + "confidence": 0.8 + }, + "signals": { + "dominant_components": ["Card", "Table"], + "layout_patterns": ["foundation-reference-grid", "type-specimen-stack"], + "breakpoint_behavior": ["type specimens stack vertically on mobile"], + "notes": [ + "Typography examples expose the display scale and label letter spacing directly." + ] + } + }, + { + "id": "0d73531a1c4e659f", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "Theme panel", + "kind": "source", + "locator": "apps/docs/src/components/theme-panel", + "renderability": "source-only", + "files": [ + "apps/docs/src/components/theme-panel/ThemePanel.tsx", + "apps/docs/src/components/theme-panel/ColorControls.tsx", + "apps/docs/src/components/theme-panel/TypographyControls.tsx", + "apps/docs/src/components/theme-panel/RadiusControls.tsx", + "apps/docs/src/components/theme-panel/ShadowControls.tsx" + ], + "classification": { + "intent": "customize", + "surface_type": "theme-control", + "density": "compressed", + "layout_shape": "control-surface", + "confidence": 0.87 + }, + "composition": { + "anatomy": [ + "sheet", + "section-tabs", + "token-controls", + "reset-export-actions" + ], + "primary_region": "controls", + "action_placement": ["panel-footer", "section-header"], + "navigation_context": "overlay-panel", + "responsive_behavior": ["panel occupies constrained side sheet"], + "confidence": 0.84 + }, + "signals": { + "dominant_components": ["Sheet", "Slider", "Input", "Button", "Tabs"], + "layout_patterns": ["theme-control-panel", "token-control-stack"], + "breakpoint_behavior": ["controls remain stacked in the sheet"], + "notes": [ + "Controls are compact, token-named, and preview values in-place." + ] + } + }, + { + "id": "0bb1b078b4d715c4", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "AI element demos", + "kind": "doc-example", + "locator": "/ui/components/agent", + "renderability": "source-only", + "files": [ + "apps/docs/src/components/docs/ai-elements/index.tsx", + "apps/docs/src/components/docs/ai-elements/agent-demo.tsx" + ], + "classification": { + "intent": "demonstrate", + "surface_type": "ai-element-demo", + "density": "standard", + "layout_shape": "control-surface", + "confidence": 0.82 + }, + "composition": { + "anatomy": [ + "component-page-shell", + "interactive-demo", + "source-snippet", + "variant-list" + ], + "primary_region": "demo", + "action_placement": ["demo-toolbar"], + "navigation_context": "registry-sidebar", + "responsive_behavior": [ + "demo and source stack when width is constrained" + ], + "confidence": 0.78 + }, + "signals": { + "dominant_components": ["Agent", "Accordion", "CodeBlock", "Tabs"], + "layout_patterns": ["ai-element-demo-gallery", "preview-code-split"], + "breakpoint_behavior": ["demo/code split collapses to stack"], + "notes": [ + "AI demos use the same component-page shell while showcasing more operational surfaces." + ] + } + }, + { + "id": "bc0df98e421cb69c", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "Primitive demos", + "kind": "doc-example", + "locator": "/ui/components/button", + "renderability": "source-only", + "files": [ + "apps/docs/src/components/docs/primitives/index.tsx", + "apps/docs/src/components/docs/primitives/button-demo.tsx" + ], + "classification": { + "intent": "demonstrate", + "surface_type": "primitive-demo", + "density": "standard", + "layout_shape": "control-surface", + "confidence": 0.8 + }, + "composition": { + "anatomy": [ + "component-page-shell", + "demo-grid", + "variant-examples", + "source-snippet" + ], + "primary_region": "demo", + "action_placement": ["demo-toolbar"], + "navigation_context": "registry-sidebar", + "responsive_behavior": ["demo grid collapses before controls shrink"], + "confidence": 0.76 + }, + "signals": { + "dominant_components": ["Button", "Card", "Tabs"], + "layout_patterns": ["primitive-demo-grid", "preview-code-split"], + "breakpoint_behavior": ["examples wrap into fewer columns"], + "notes": [ + "Primitive demos keep operational controls close to the rendered specimen." + ] + } + }, + { + "id": "988c6ea8181325b8", + "source": { + "id": "ghost", + "role": "primary", + "target": "github:block/ghost", + "commit": "b0d985a", + "scanned_at": "2026-05-10T00:00:00-04:00", + "scanner_version": "dogfood-manual-2026-05-10" + }, + "name": "Tool docs pages", + "kind": "route", + "locator": "/tools/fingerprint", + "renderability": "source-only", + "files": [ + "apps/docs/src/app/tools/fingerprint/page.tsx", + "apps/docs/src/app/tools/drift/page.tsx", + "apps/docs/src/app/tools/fleet/page.tsx" + ], + "classification": { + "intent": "explain", + "surface_type": "tool-doc", + "density": "standard", + "layout_shape": "article", + "confidence": 0.8 + }, + "composition": { + "anatomy": [ + "display-header", + "prose-sections", + "cli-reference", + "callouts" + ], + "primary_region": "article", + "action_placement": ["inline-links"], + "navigation_context": "docs-shell", + "responsive_behavior": ["article width stays constrained"], + "confidence": 0.76 + }, + "signals": { + "dominant_components": ["Callout", "CliHelp", "CodeBlock"], + "layout_patterns": ["tool-doc-article", "cli-reference-block"], + "breakpoint_behavior": ["article stays single-column"], + "notes": [ + "Tool docs privilege concise prose and command references over decorative layout." + ] + } + } + ] +} diff --git a/apps/docs/src/generated/cli-manifest.json b/apps/docs/src/generated/cli-manifest.json index 9a45228e..bf5b30f5 100644 --- a/apps/docs/src/generated/cli-manifest.json +++ b/apps/docs/src/generated/cli-manifest.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-05-10T06:12:15.163Z", + "generatedAt": "2026-05-10T22:20:03.558Z", "tools": [ { "tool": "ghost-drift", @@ -439,7 +439,7 @@ { "rawName": "-f, --fingerprint ", "name": "fingerprint", - "description": "Source direct fingerprint markdown file (default: .ghost/fingerprint.md)", + "description": "Source legacy direct fingerprint markdown file (required for review-command; optional legacy mode for context-bundle)", "default": null, "takesValue": true, "negated": false @@ -463,7 +463,7 @@ { "rawName": "--no-tokens", "name": "tokens", - "description": "Skip tokens.css output (context-bundle)", + "description": "Skip tokens.css output (legacy direct fingerprint context-bundle)", "default": true, "takesValue": false, "negated": true @@ -479,7 +479,7 @@ { "rawName": "--prompt-only", "name": "promptOnly", - "description": "Emit only prompt.md (skips SKILL.md / fingerprint.md / tokens.css) (context-bundle)", + "description": "Emit only prompt.md (context-bundle)", "default": null, "takesValue": false, "negated": false @@ -487,7 +487,7 @@ { "rawName": "--name ", "name": "name", - "description": "Override the skill name (default: fingerprint id) (context-bundle)", + "description": "Override the skill name (default: package or fingerprint id) (context-bundle)", "default": null, "takesValue": true, "negated": false diff --git a/packages/ghost-fingerprint/src/core/context/index.ts b/packages/ghost-fingerprint/src/core/context/index.ts index 20012557..f7d965c3 100644 --- a/packages/ghost-fingerprint/src/core/context/index.ts +++ b/packages/ghost-fingerprint/src/core/context/index.ts @@ -1,3 +1,5 @@ +export type { WritePackageContextOptions } from "./package-writer.js"; +export { writePackageContextBundle } from "./package-writer.js"; export type { EmitReviewInput } from "./review-command.js"; export { emitReviewCommand } from "./review-command.js"; export { buildTokensCss } from "./tokens-css.js"; diff --git a/packages/ghost-fingerprint/src/core/context/package-writer.ts b/packages/ghost-fingerprint/src/core/context/package-writer.ts new file mode 100644 index 00000000..770fedc9 --- /dev/null +++ b/packages/ghost-fingerprint/src/core/context/package-writer.ts @@ -0,0 +1,276 @@ +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { + formatSurveySummaryMarkdown, + lintSurvey, + type Survey, + summarizeSurvey, +} from "@ghost/core"; +import { parse as parseYaml } from "yaml"; +import type { FingerprintPackagePaths } from "../fingerprint-package.js"; +import type { WriteContextResult } from "./writer.js"; + +export interface WritePackageContextOptions { + outDir: string; + /** Override the skill/package name. Default: resources.yml id. */ + name?: string; + /** Emit only prompt.md. Default: false. */ + promptOnly?: boolean; + /** Include README.md. Default: false. */ + readme?: boolean; +} + +interface PackageContext { + name: string; + resources: string; + map: string; + surveySummary: string; + patterns: string; + checks?: string; + intent?: string; +} + +export async function writePackageContextBundle( + paths: FingerprintPackagePaths, + options: WritePackageContextOptions, +): Promise { + const context = await loadPackageContext(paths, options.name); + await mkdir(options.outDir, { recursive: true }); + const files: string[] = []; + + const promptPath = join(options.outDir, "prompt.md"); + await writeFile(promptPath, buildPackagePromptMd(context), "utf-8"); + files.push(promptPath); + + if (options.promptOnly) { + return { outDir: options.outDir, files }; + } + + const skillPath = join(options.outDir, "SKILL.md"); + await writeFile(skillPath, buildPackageSkillMd(context), "utf-8"); + files.push(skillPath); + + await writeContextFile( + options.outDir, + files, + "resources.yml", + context.resources, + ); + await writeContextFile(options.outDir, files, "map.md", context.map); + await writeContextFile( + options.outDir, + files, + "survey-summary.md", + context.surveySummary, + ); + await writeContextFile( + options.outDir, + files, + "patterns.yml", + context.patterns, + ); + if (context.checks) { + await writeContextFile(options.outDir, files, "checks.yml", context.checks); + } + if (context.intent) { + await writeContextFile(options.outDir, files, "intent.md", context.intent); + } + if (options.readme) { + await writeContextFile( + options.outDir, + files, + "README.md", + buildPackageReadmeMd(context), + ); + } + + return { outDir: options.outDir, files }; +} + +async function loadPackageContext( + paths: FingerprintPackagePaths, + nameOverride?: string, +): Promise { + const [resources, map, surveyRaw, patterns, checks, intent] = + await Promise.all([ + readFile(paths.resources, "utf-8"), + readFile(paths.map, "utf-8"), + readFile(paths.survey, "utf-8"), + readFile(paths.patterns, "utf-8"), + readOptional(paths.checks), + readOptional(paths.intent), + ]); + + const survey = parseSurvey(surveyRaw); + const report = lintSurvey(survey); + if (report.errors > 0) { + throw new Error( + `survey.json failed lint with ${report.errors} error(s); fix before emitting a context bundle.`, + ); + } + + return { + name: sanitizeName(nameOverride ?? inferPackageName(resources, map)), + resources, + map, + surveySummary: formatSurveySummaryMarkdown( + summarizeSurvey(survey, { budget: "compact" }), + ), + patterns, + checks, + intent, + }; +} + +function parseSurvey(raw: string): Survey { + try { + return JSON.parse(raw) as Survey; + } catch (err) { + throw new Error( + `survey.json is not valid JSON: ${ + err instanceof Error ? err.message : String(err) + }`, + ); + } +} + +async function readOptional(path: string): Promise { + try { + return await readFile(path, "utf-8"); + } catch { + return undefined; + } +} + +async function writeContextFile( + outDir: string, + files: string[], + name: string, + content: string, +): Promise { + const outPath = join(outDir, name); + await writeFile(outPath, ensureTrailingNewline(content), "utf-8"); + files.push(outPath); +} + +function inferPackageName(resources: string, map: string): string { + const fromResources = parseYamlSafe(resources); + if (isRecord(fromResources) && typeof fromResources.id === "string") { + return fromResources.id; + } + + const frontmatter = map.match(/^---\n([\s\S]*?)\n---/)?.[1]; + if (frontmatter) { + const fromMap = parseYamlSafe(frontmatter); + if (isRecord(fromMap) && typeof fromMap.id === "string") { + return fromMap.id; + } + } + + return "ghost-fingerprint-package"; +} + +function parseYamlSafe(raw: string): unknown { + try { + return parseYaml(raw); + } catch { + return undefined; + } +} + +function isRecord(value: unknown): value is Record { + return Boolean(value && typeof value === "object" && !Array.isArray(value)); +} + +function sanitizeName(value: string): string { + const name = value + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-|-$/g, ""); + return name || "ghost-fingerprint-package"; +} + +function buildPackageSkillMd(context: PackageContext): string { + return `--- +name: ${context.name} +description: Use this root Ghost fingerprint package to preserve design identity during UI generation and review. +user-invocable: true +--- + +This skill grounds work in the **${context.name}** root Ghost fingerprint package. + +Read the files in this order: + +1. \`intent.md\` when present - human-approved direction. +2. \`map.md\` - topology, scopes, surface families, and routing. +3. \`patterns.yml\` - operational composition grammar backed by survey evidence. +4. \`checks.yml\` when present - deterministic gates or proposed gates. +5. \`survey-summary.md\` - compact evidence digest from \`survey.json\`. +6. \`resources.yml\` - what counted as source evidence. + +When generating or reviewing UI, identify the map scope and surface type first, +then apply matching composition patterns. Treat survey rows as evidence, not +taste. Treat checks as gates only when their status is \`active\`; proposed +checks are advisory until promoted by a human. +`; +} + +function buildPackagePromptMd(context: PackageContext): string { + const parts = [ + `You are working inside the **${context.name}** design language as captured by a root Ghost fingerprint package.`, + ]; + + if (context.intent?.trim()) { + parts.push(`# Intent\n\n${context.intent.trim()}`); + } + + parts.push(`# Use The Package + +- Start with \`map.md\` to route the task to scopes, surface families, and examples. +- Use \`patterns.yml\` for composition decisions; cite evidence when reviewing. +- Use \`checks.yml\` for deterministic gates. Only \`active\` checks block. +- Use \`survey-summary.md\` for observed tokens, values, components, and surfaces. +- Use \`resources.yml\` to understand what evidence was included or excluded. +- Do not invent tokens, components, or surface patterns when the package provides an observed option.`); + + parts.push(`# Package Files + +- \`resources.yml\` +- \`map.md\` +- \`survey-summary.md\` +- \`patterns.yml\` +${context.checks ? "- `checks.yml`\n" : ""}${context.intent ? "- `intent.md`\n" : ""}`); + + parts.push(`# Review Posture + +Before calling drift, classify the changed file by \`map.md\` scope. For UI +generation, preserve matching \`patterns.yml\` anatomy and prefer values from +the survey's token/value digest. If a divergence is intentional, name it in the +package rather than hiding it in generated UI.`); + + return `${parts.join("\n\n")}\n`; +} + +function buildPackageReadmeMd(context: PackageContext): string { + return `# ${context.name} context bundle + +Generated by \`ghost-fingerprint emit context-bundle\` from a root Ghost +fingerprint package. + +## Files + +- \`SKILL.md\` - agent skill manifest. +- \`prompt.md\` - portable prompt distilled from the package. +- \`resources.yml\` - evidence sources. +- \`map.md\` - topology and routing. +- \`survey-summary.md\` - compact survey evidence. +- \`patterns.yml\` - composition grammar. +${context.checks ? "- `checks.yml` - deterministic gates and proposed gates.\n" : ""}${context.intent ? "- `intent.md` - human-approved direction.\n" : ""} +The full \`survey.json\` stays in the source package by default because it can +be large; regenerate this bundle when the survey changes. +`; +} + +function ensureTrailingNewline(value: string): string { + return value.endsWith("\n") ? value : `${value}\n`; +} diff --git a/packages/ghost-fingerprint/src/core/index.ts b/packages/ghost-fingerprint/src/core/index.ts index 988dfbc6..49e22b68 100644 --- a/packages/ghost-fingerprint/src/core/index.ts +++ b/packages/ghost-fingerprint/src/core/index.ts @@ -35,12 +35,14 @@ export type { EmitReviewInput, WriteContextOptions, WriteContextResult, + WritePackageContextOptions, } from "./context/index.js"; export { buildSkillMd, buildTokensCss, emitReviewCommand, writeContextBundle, + writePackageContextBundle, } from "./context/index.js"; export type { ColorChange, diff --git a/packages/ghost-fingerprint/src/emit-command.ts b/packages/ghost-fingerprint/src/emit-command.ts index e8853bb9..d6b5f7f0 100644 --- a/packages/ghost-fingerprint/src/emit-command.ts +++ b/packages/ghost-fingerprint/src/emit-command.ts @@ -8,6 +8,7 @@ import { loadFingerprint, resolveFingerprintPackage, writeContextBundle, + writePackageContextBundle, } from "./core/index.js"; /** @@ -57,7 +58,7 @@ export function registerEmitCommand(cli: CAC): void { ) .option( "-f, --fingerprint ", - "Source direct fingerprint markdown file (default: .ghost/fingerprint.md)", + "Source legacy direct fingerprint markdown file (required for review-command; optional legacy mode for context-bundle)", ) .option( "-o, --out ", @@ -68,15 +69,15 @@ export function registerEmitCommand(cli: CAC): void { "Write to stdout instead of a file (review-command only)", ) // context-bundle flags: - .option("--no-tokens", "Skip tokens.css output (context-bundle)") - .option("--readme", "Include README.md (context-bundle)") .option( - "--prompt-only", - "Emit only prompt.md (skips SKILL.md / fingerprint.md / tokens.css) (context-bundle)", + "--no-tokens", + "Skip tokens.css output (legacy direct fingerprint context-bundle)", ) + .option("--readme", "Include README.md (context-bundle)") + .option("--prompt-only", "Emit only prompt.md (context-bundle)") .option( "--name ", - "Override the skill name (default: fingerprint id) (context-bundle)", + "Override the skill name (default: package or fingerprint id) (context-bundle)", ) .action(async (kind: string, opts) => { try { @@ -106,13 +107,14 @@ export function registerEmitCommand(cli: CAC): void { process.exit(0); } - const fingerprintPath = resolve( - process.cwd(), - opts.fingerprint ?? - resolveFingerprintPackage(undefined, process.cwd()).fingerprint, - ); + const explicitFingerprint = typeof opts.fingerprint === "string"; if (parsed.kind === "review-command") { + const fingerprintPath = resolve( + process.cwd(), + opts.fingerprint ?? + resolveFingerprintPackage(undefined, process.cwd()).fingerprint, + ); const loaded = await loadFingerprint(fingerprintPath, { noEmbeddingBackfill: true, }); @@ -141,6 +143,29 @@ export function registerEmitCommand(cli: CAC): void { (opts.out as string | undefined) ?? DEFAULT_CONTEXT_OUT, ); + if (!explicitFingerprint) { + const result = await writePackageContextBundle( + resolveFingerprintPackage(undefined, process.cwd()), + { + outDir, + readme: Boolean(opts.readme), + promptOnly: Boolean(opts.promptOnly), + name: opts.name as string | undefined, + }, + ); + + process.stdout.write( + `Wrote ${result.files.length} file${ + result.files.length === 1 ? "" : "s" + } to ${result.outDir}:\n`, + ); + for (const f of result.files) { + process.stdout.write(` ${f}\n`); + } + process.exit(0); + } + + const fingerprintPath = resolve(process.cwd(), opts.fingerprint); const { fingerprint } = await loadFingerprint(fingerprintPath); const result = await writeContextBundle(fingerprint, { outDir, diff --git a/packages/ghost-fingerprint/test/context/writer.test.ts b/packages/ghost-fingerprint/test/context/writer.test.ts index bea08876..8b7a6dc2 100644 --- a/packages/ghost-fingerprint/test/context/writer.test.ts +++ b/packages/ghost-fingerprint/test/context/writer.test.ts @@ -6,8 +6,10 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { buildSkillMd, writeContextBundle, + writePackageContextBundle, } from "../../src/core/context/index.js"; import { buildTokensCss } from "../../src/core/context/tokens-css.js"; +import { initFingerprintPackage } from "../../src/core/fingerprint-package.js"; const FINGERPRINT: Fingerprint = { id: "sample-ds", @@ -231,6 +233,52 @@ describe("writeContextBundle", () => { }); }); +describe("writePackageContextBundle", () => { + it("emits portable context from root package artifacts", async () => { + const paths = await initFingerprintPackage(join(dir, ".ghost"), undefined, { + withIntent: true, + }); + const res = await writePackageContextBundle(paths, { + outDir: join(dir, "bundle"), + readme: true, + }); + + const names = res.files.map((f) => f.split("/").pop()); + expect(names).toEqual([ + "prompt.md", + "SKILL.md", + "resources.yml", + "map.md", + "survey-summary.md", + "patterns.yml", + "checks.yml", + "intent.md", + "README.md", + ]); + + const prompt = await readFile(res.files[0], "utf-8"); + expect(prompt).toContain("# Use The Package"); + expect(prompt).toContain("patterns.yml"); + + const summary = await readFile( + res.files.find((f) => f.endsWith("survey-summary.md")) ?? "", + "utf-8", + ); + expect(summary).toContain("# Survey Summary"); + }); + + it("promptOnly emits just prompt.md for package bundles", async () => { + const paths = await initFingerprintPackage(join(dir, ".ghost"), undefined); + const res = await writePackageContextBundle(paths, { + outDir: join(dir, "bundle"), + promptOnly: true, + }); + + expect(res.files).toHaveLength(1); + expect(res.files[0]).toMatch(/prompt\.md$/); + }); +}); + describe("buildTokensCss", () => { it("emits only dimensions present on the fingerprint", () => { const minimal: Fingerprint = {