From 6cbfcff6fae985fca27a1d221eb8e3835a5d94e5 Mon Sep 17 00:00:00 2001
From: Nahiyan Khan
Date: Sun, 28 Jun 2026 15:54:32 -0400
Subject: [PATCH 01/12] docs: refresh README + docs site onto the node-graph
model
The README, docs site, published package README, and CLAUDE.md still
described the removed facet model (intent/inventory/composition/validate.yml
plus relay/compare/stack/ack/track/diverge/lint/verify). Rewrite them onto
the node-graph model and the current 9-command surface, and tighten the
homepage thesis and hero into a strong, simple intro.
---
.changeset/docs-node-model-refresh.md | 5 +
CLAUDE.md | 99 ++++----
README.md | 222 +++++++-----------
apps/docs/src/app/docs/page.tsx | 11 +-
apps/docs/src/app/page.tsx | 132 ++++-------
apps/docs/src/app/tools/page.tsx | 2 +-
apps/docs/src/app/tools/scan/page.tsx | 12 +-
apps/docs/src/components/docs/hero.tsx | 4 +
apps/docs/src/content/docs/cli-reference.mdx | 207 ++++++++--------
.../content/docs/fingerprint-authoring.mdx | 178 +++++++-------
.../docs/src/content/docs/getting-started.mdx | 191 ++++++++-------
packages/ghost/README.md | 83 ++++---
12 files changed, 549 insertions(+), 597 deletions(-)
create mode 100644 .changeset/docs-node-model-refresh.md
diff --git a/.changeset/docs-node-model-refresh.md b/.changeset/docs-node-model-refresh.md
new file mode 100644
index 00000000..96d5ba3c
--- /dev/null
+++ b/.changeset/docs-node-model-refresh.md
@@ -0,0 +1,5 @@
+---
+"@anarchitecture/ghost": patch
+---
+
+Refresh the README and docs onto the node-graph model and the current command set.
diff --git a/CLAUDE.md b/CLAUDE.md
index 89dd8174..2e09ca34 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -4,10 +4,12 @@ Agents can assemble UI. What they cannot reliably preserve is the product
surface composition behind that UI: hierarchy, density, restraint, repetition,
trust, flow, and the choices that make a surface feel intentional.
-Ghost keeps that surface composition in a repo-local `.ghost/` fingerprint package. The public npm shape
-is one package, `@anarchitecture/ghost`, with one user-facing bin, `ghost`.
-The CLI validates, computes, compares, and emits deterministic packets. The
-host agent does the interpretive BYOA work through the installed `ghost` skill.
+Ghost keeps that surface composition in a repo-local `.ghost/` fingerprint
+package — a graph of prose nodes. The public npm shape is one package,
+`@anarchitecture/ghost`, with one user-facing bin, `ghost`. The CLI validates
+the node graph, composes context, routes checks, and emits deterministic
+packets. The host agent does the interpretive BYOA work through the installed
+`ghost` skill.
## Build & Run
@@ -30,38 +32,42 @@ pnpm --filter @anarchitecture/ghost exec ghost
Ghost is **BYOA (bring your own agent)**. Claude Code, Codex, Cursor, Goose, or
another host agent reads, decides, and writes. Ghost is the deterministic
-calculator the agent reaches for: schema validation, repo-signal helpers,
-structural diffs, drift checks, comparison math, and handoff packets.
+calculator the agent reaches for: schema and graph validation, repo-signal
+helpers, context composition, check routing, and advisory review packets.
-The canonical root `.ghost/` package follows:
+The canonical root `.ghost/` package is a flat folder:
```text
-manifest.yml
-intent.yml
-inventory.yml
-composition.yml
-validate.yml
+manifest.yml # schema + id
+surfaces.yml # the spine: surfaces and their parent (core is implicit)
+nodes/*.md # prose nodes — the design expression
+checks/*.md # optional ghost.check/v1 checks
```
-The three root facet files are the core model:
+The fingerprint is a **graph of nodes**. A node is one markdown file:
+frontmatter handles (`id`, `description`, `under`, `relates`, `incarnation`)
+plus a prose body. The body is written through three authoring lenses — they
+guide what to capture, they are not fields or node types:
-- `intent.yml` for surface intent.
-- `inventory.yml` for curated material, exemplars, and source links.
-- `composition.yml` for experience patterns.
+- **intent** — the why and the stance.
+- **inventory** — the materials, and pointers to code the agent can inspect.
+- **composition** — the patterns that make the surface feel intentional.
-`validate.yml` validates output through deterministic checks; it is not
-generation input.
-Ordinary Git review is the approval boundary for fingerprint edits.
+`under` cascades a node downward (`core` is the implicit root and reaches every
+surface). `relates` links nodes laterally. `description` is the retrieval
+payload. `checks/*.md` validate output, routed by surface; they are not
+generation input. Surfaces are declared in `surfaces.yml`, never inferred from
+filenames. Ordinary Git review is the approval boundary for fingerprint edits.
-Legacy `resources.yml`, `map.md`, `survey.json`, and `patterns.yml` may still
-appear in older repos or as migration source material. They are not canonical
-fingerprint input for new Ghost work.
+A package may `extend` another by identity (the shared-brand pattern): the
+manifest's `extends` maps a package id to where it lives, and nodes reference
+inherited context by identity (`under: brand:core`), never by path.
## Packages
| Package | Published? | Description |
| --- | --- | --- |
-| `packages/ghost` | yes: `@anarchitecture/ghost` | Unified public package. Ships the `ghost` CLI, fingerprint package authoring, checks, advisory review packets, comparison, drift stance verbs, and the unified skill bundle. |
+| `packages/ghost` | yes: `@anarchitecture/ghost` | Unified public package. Ships the `ghost` CLI, node authoring, graph validation, check routing, advisory review packets, and the unified skill bundle. |
| `packages/ghost-core` | no | Private historical shared package. Runtime code needed by npm is folded into `packages/ghost/src/ghost-core`. |
| `packages/ghost-fleet` | no | Private fleet view across many Ghost bundles. Consumes workspace exports from `@anarchitecture/ghost`. |
| `packages/ghost-ui` | no | Reference design system: shadcn registry plus `ghost-mcp` MCP server. |
@@ -69,34 +75,33 @@ fingerprint input for new Ghost work.
## CLI Commands
+Core workflow:
+
+| Command | Description |
+| --- | --- |
+| `ghost init` | Scaffold `.ghost/` — manifest, surfaces spine, and a seed node. |
+| `ghost scan` | Report node and surface contribution. |
+| `ghost validate` | Validate the package: artifact shape and the node graph (links resolve, one root, acyclic). |
+| `ghost gather` | List nodes by id + description, or compose a surface's context slice (own + inherited + edges). |
+| `ghost checks` | Select and ground the markdown checks governing the named surfaces. |
+| `ghost review` | Emit an advisory review packet: touched surfaces, routed checks, fingerprint grounding, and the diff. |
+| `ghost skill install` | Install the unified `ghost` skill bundle. |
+
+Advanced/maintenance:
+
| Command | Description |
| --- | --- |
-| `ghost init` | Create `.ghost/` with manifest, facets, and deterministic checks. |
-| `ghost scan` | Report fingerprint contribution facets and BYOA next-step guidance. |
| `ghost signals` | Emit raw repo signals as JSON for fingerprint authoring. |
-| `ghost lint` | Validate a fingerprint package or single artifact. |
-| `ghost verify` | Validate fingerprint evidence and exemplar paths, and typed check refs. |
-| `ghost describe` | Print direct markdown section ranges. |
-| `ghost diff` | Structural direct-fingerprint prose diff between direct fingerprints. |
-| `ghost survey ` | Legacy survey helpers for optional `ghost.survey/v1` workflows. |
-| `ghost check` | Run active `ghost.validate/v1` deterministic gates against a diff. |
-| `ghost review` | Emit an evidence-routed advisory review packet grounded in fingerprint facets, inventory exemplars, checks, and the diff. |
-| `ghost compare` | Pairwise or composite comparison over packages or direct fingerprints. |
-| `ghost ack` | Record stance toward the tracked fingerprint in `.ghost-sync.json`. |
-| `ghost track` | Shift the tracked fingerprint. |
-| `ghost diverge` | Declare intentional divergence on a dimension. |
-| `ghost emit ` | Emit `review-command`. |
-| `ghost skill install` | Install the unified `ghost` agentskills.io bundle. |
-
-`ghost scan --format json` is deterministic contribution and source-signal state.
-It does not run an LLM.
+| `ghost migrate` | Migrate a legacy `.ghost/` package onto the node-graph surface model. |
+
+`ghost scan --format json` is deterministic contribution state. It does not run
+an LLM.
## Public Exports
- `@anarchitecture/ghost` for the combined surface.
-- `@anarchitecture/ghost/scan` for scan contribution, source signals, and stack discovery.
-- `@anarchitecture/ghost/fingerprint` for fingerprint package authoring, linting, verification, parsing, and serialization.
-- `@anarchitecture/ghost/drift` for check/review/compare/stance helpers.
+- `@anarchitecture/ghost/scan` for scan contribution and source signals.
+- `@anarchitecture/ghost/fingerprint` for node package authoring, validation, parsing, and serialization.
- `@anarchitecture/ghost/core` for shared schemas, types, and loaders.
- `@anarchitecture/ghost/cli` for `buildCli()`.
@@ -132,8 +137,10 @@ Use `patch` for fixes and docs, `minor` for new commands/flags/exports, and
- Keep publishable runtime code self-contained in `packages/ghost`; no
`workspace:*` runtime dependencies in the packed public artifact.
-- The canonical on-disk form is a flat `.ghost/` package.
-- Direct `fingerprint.md` remains only for legacy/direct compare workflows.
+- The canonical on-disk form is a flat `.ghost/` package: `manifest.yml`,
+ `surfaces.yml`, `nodes/*.md`, and optional `checks/*.md`.
+- The graph is the only model. Surfaces are the only locality; they are
+ declared in `surfaces.yml`, never inferred from paths or filenames.
- Skill recipes live in `packages/ghost/src/skill-bundle/references/`; install
them with `ghost skill install`.
- The CLI manifest at `apps/docs/src/generated/cli-manifest.json` is generated
diff --git a/README.md b/README.md
index 3aa464c0..7aef4327 100644
--- a/README.md
+++ b/README.md
@@ -1,189 +1,141 @@
# Ghost
-**Ghost captures the composition of a product surface: the intent behind it,
-the materials it draws from, and the patterns that make it feel intentional.**
+**Agents can assemble UI. They can't reliably preserve the _composition_ behind
+it — the hierarchy, density, restraint, copy, trust, and flow that make a
+surface feel intentional.**
-Ghost gives AI agents a checked-in product fingerprint they can read before
-they generate UI and validate after they change it. The public package is
-`@anarchitecture/ghost`, and it installs one CLI: `ghost`.
+Ghost is a checked-in product-surface fingerprint your agent reads before it
+builds and checks after it changes. One package, `@anarchitecture/ghost`. One
+CLI, `ghost`.
-Agents can assemble components. What they need help preserving is the product
-surface behind those components: hierarchy, density, restraint, behavior, copy,
-accessibility, trust, and flow. Ghost keeps that surface composition in a
-portable `.ghost/` package that ordinary Git review can approve.
+[Documentation](https://block.github.io/ghost/) · [npm](https://www.npmjs.com/package/@anarchitecture/ghost) · [Skill](#skill)
## The Shape
-The canonical package is intentionally small:
+A fingerprint is a small folder of prose. The CLI computes; your agent reads,
+writes, and decides.
```text
.ghost/
- manifest.yml # ghost.fingerprint-package/v1 anchor
- intent.yml # surface intent
- inventory.yml # curated material and exemplars
- composition.yml # patterns, flows, states, and arrangements
- validate.yml # optional deterministic gates
+ manifest.yml # schema + id
+ surfaces.yml # the spine: surfaces and their parent (core is implicit)
+ nodes/*.md # prose nodes — the design expression
+ checks/*.md # optional rules an agent evaluates
```
-A package can be sparse: it contributes whichever facets are locally true. Generation usually uses intent + inventory + composition:
+The fingerprint is a **graph of nodes**. A node is one markdown file:
+frontmatter handles (`id`, `description`, `under`, `relates`, `incarnation`)
+plus a prose body. You write that body through three lenses — they guide what to
+capture, they are not fields:
-- `intent.yml` says what the surface is trying to do and for whom.
-- `inventory.yml` points agents at materials they can inspect or reuse.
-- `composition.yml` captures the patterns that make those materials feel like
- one intentional product.
+- **intent** — the why and the stance.
+- **inventory** — the materials, and pointers to the code an agent can inspect.
+- **composition** — the patterns that make the surface feel like one product.
-`validate.yml`, nested packages, custom host-wrapper package locations, and raw repo
-signals are supporting features. They do not replace curated fingerprint
-facets. `ghost signals` answers what exists; curated fingerprint facets answer
-what the surface is trying to preserve.
-
-Older `resources.yml`, `map.md`, `survey.json`, `patterns.yml`, and direct
-`fingerprint.yml` artifacts can still inform migration workflows, but new Ghost
-work should target `.ghost/`.
-
-## Project Status: Beta
-
-> [!WARNING]
-> Ghost is pre-1.0 and under active development. The CLI, fingerprint schema,
-> on-disk `.ghost/` package shape, and public JavaScript exports may
-> change in breaking ways before a stable 1.0 release.
->
-> Breaking changes may ship in minor versions while Ghost is pre-1.0. Patch
-> versions are reserved for fixes that should not require migration. If you adopt
-> Ghost today, expect some churn, pin the version you depend on, and review
-> release notes before upgrading.
+`under` cascades a node downward (`core` reaches every surface). `relates` links
+nodes laterally. `description` is the retrieval payload — how an agent finds the
+right node for a task. Checks validate output; they are never generation input.
## Install
```bash
npm install -D @anarchitecture/ghost
npx ghost --help
-npx ghost --help --all
```
-`ghost --help` shows the core workflow. `ghost --help --all` shows the complete
-command index, and each command supports `ghost --help`.
-
-Install the BYOA skill bundle so Codex, Claude, Cursor, Goose, or another host
-agent knows how to author and use the fingerprint:
+## Quick Start
```bash
-npx ghost skill install
-# or choose an explicit destination
-npx ghost skill install --dest ~/.codex/skills/ghost
-```
-
-Then ask your agent in plain English:
-
-```text
-Set up the Ghost fingerprint for this repo.
-Brief this work from the Ghost fingerprint.
-Review this PR against the Ghost fingerprint.
-Compare these two Ghost bundles.
+ghost init # scaffold .ghost/ — manifest, surfaces spine, one seed node
+ghost validate # links resolve, one root, acyclic
+ghost gather # list nodes; ghost gather composes a context slice
```
-## Author A Fingerprint
+A node looks like this:
-Ghost authoring is a human-plus-agent workflow. The CLI creates, inspects, and
-validates the package; the host agent interviews, reads the repo, drafts facet
-edits, and asks you to curate the claims.
+```markdown
+---
+id: checkout-trust
+description: Trust at the payment moment.
+under: checkout
+relates:
+ - to: core-trust
+ as: reinforces
+---
-```bash
-ghost init
-ghost init --package product-surface
-ghost scan --format json
-ghost signals .
-ghost lint .ghost
-ghost lint product-surface
-ghost verify .ghost --root .
+Near the moment of payment, reduce felt risk. Proximity of reassurance to the
+action beats completeness. Never introduce a new visual system here.
```
-Use `--reference` when a reference library should seed inventory, `--scope`
-for nested product areas, or `--package ` when initializing an exact
-package directory such as `product-surface/`.
-For monorepos, `ghost init --monorepo` creates or preserves the root package,
-detects workspace child roots, and prints proposed `ghost init --scope ...`
-commands by default. Run `ghost init --monorepo --apply` to create the detected
-child packages. Host wrappers that need Ghost files somewhere other than
-`.ghost` may set `GHOST_PACKAGE_DIR=` on the child `ghost`
-process. Exact `--package ` values win over the environment default.
-
-Drafted fingerprint edits are just ordinary file changes until Git review
-accepts them. Checked-in Ghost facet files are the Ghost source of truth.
+## Skill
-## Generate From Ghost
-
-Before generating or revising UI, gather the Relay brief for the target path:
+Ghost is **bring-your-own-agent**. Install the skill bundle so Claude Code,
+Codex, Cursor, Goose, or another host agent knows how to author and use the
+fingerprint:
```bash
-ghost relay gather apps/checkout/review/page.tsx
+npx ghost skill install
```
-Relay compiles selected context from the resolved stack as context hits:
-fingerprint refs, why they matched, suggested reads, omissions, and gaps.
-The important shift is timing: Ghost gives agents surface-composition context
-before they build, not only after a review finds drift.
+Then ask in plain English:
-After implementation, run the deterministic and advisory workflows against the
-same fingerprint:
-
-```bash
-ghost check --base main
-ghost review --base main
+```text
+Set up the Ghost fingerprint for this repo.
+Brief this work from the Ghost fingerprint.
+Review this change against the Ghost fingerprint.
```
-`ghost check` runs active `ghost.validate/v1` gates and can fail. `ghost review`
-emits an evidence-routed advisory packet for a human or host adapter to use.
+The skill tells the agent what to read, what to write, and which CLI checks to
+run. The CLI does the deterministic work; the agent does the interpretation.
-## Compare And Govern
-
-Advanced workflows remain available when a repo needs package stacks,
-comparison, or explicit drift stance:
+## The Loop
```bash
-ghost stack apps/checkout/review/page.tsx
-ghost compare market/.ghost dashboard/.ghost
-ghost ack --stance aligned --reason "Initial baseline"
-ghost track new-tracked.fingerprint.md
-ghost diverge typography --reason "Editorial product uses a different type scale"
-ghost emit review-command --path apps/checkout/review/page.tsx
+ghost gather # before: compose the context slice for the work
+ghost checks --surface # route the markdown checks the change touches
+ghost review --surface # after: an advisory packet grounded in the diff
```
-`ghost scan --format json` emits deterministic contribution state for `intent`,
-`inventory`, `composition`, and `validate`. A sparse package can be useful with
-only one contributing facet; absent facets may be inherited from broader stack
-context. It does not call an LLM.
+The shift is timing: Ghost gives agents surface-composition context **before**
+they build, not only after a review finds drift. Checked-in nodes are the source
+of truth; ordinary Git review is the approval boundary for fingerprint edits.
## CLI Commands
| Command | Description |
| --- | --- |
-| `ghost init` | Create `.ghost/` package facet files. |
-| `ghost scan` | Report sparse fingerprint contribution facets. |
-| `ghost lint` | Validate a fingerprint package or individual artifact. |
-| `ghost verify` | Validate evidence paths, exemplar paths, and typed check refs. |
-| `ghost check` | Run active deterministic gates against a diff. |
-| `ghost review` | Emit an evidence-routed advisory packet from fingerprint facets and a diff. |
-| `ghost relay gather` | Gather fingerprint-grounded context for an agent target. |
-| `ghost emit ` | Emit `review-command` artifacts. |
-| `ghost skill install` | Install the unified Ghost skill bundle. |
-| `ghost stack` | Inspect resolved root-to-leaf fingerprint stacks. |
-| `ghost signals` | Emit raw repo signals as JSON for fingerprint authoring. |
-| `ghost describe` | Print markdown section ranges. |
-| `ghost compare` | Compare fingerprint packages. |
-| `ghost ack` / `track` / `diverge` | Record stance toward tracked drift. |
-| `ghost diff` / `survey` | Maintain direct markdown fingerprints or survey/cache files for compatibility workflows. |
+| `ghost init` | Scaffold `.ghost/` — manifest, surfaces spine, and a seed node. |
+| `ghost scan` | Report node and surface contribution. |
+| `ghost validate` | Validate the package: artifact shape and the node graph. |
+| `ghost gather` | List nodes, or compose a surface's context slice. |
+| `ghost checks` | Select and ground the checks a change touches, by surface. |
+| `ghost review` | Emit an advisory review packet grounded in fingerprint + diff. |
+| `ghost skill install` | Install the BYOA skill bundle. |
+| `ghost signals` | Emit raw repo signals as authoring evidence _(advanced)_. |
+| `ghost migrate` | Migrate a legacy `.ghost/` package onto the node model _(maintenance)_. |
+
+Run `ghost --help` for the core workflow, `ghost --help --all` for everything,
+and `ghost --help` for flags.
+
+## Status: Beta
+
+> [!WARNING]
+> Ghost is pre-1.0 and under active development. The CLI, node schema, on-disk
+> `.ghost/` shape, and public exports may change in breaking ways before 1.0.
+> Breaking changes may ship in minor versions while pre-1.0; patch versions are
+> reserved for fixes that should not require migration. Pin the version you
+> depend on and review release notes before upgrading.
## Repo Layout
Ghost is a pnpm monorepo. The public package is self-contained for npm; private
-workspace packages remain development context.
+workspace packages are development context.
| Path | Role | Published? |
| ---- | ---- | --- |
-| [`packages/ghost`](./packages/ghost) | Public package. Ships the `ghost` CLI, folded core runtime, fingerprint package helpers, deterministic checks, advisory review packets, comparison/stance helpers, and the unified skill bundle. | yes: `@anarchitecture/ghost` |
+| [`packages/ghost`](./packages/ghost) | Public package: the `ghost` CLI, folded core runtime, node authoring, checks, advisory review, and the skill bundle. | yes: `@anarchitecture/ghost` |
| [`packages/ghost-fleet`](./packages/ghost-fleet) | Private fleet view across many Ghost bundles. | no |
-| [`packages/ghost-ui`](./packages/ghost-ui) | Reference design system: shadcn registry plus `ghost-mcp` MCP server. | no |
+| [`packages/ghost-ui`](./packages/ghost-ui) | Reference design system: shadcn registry plus `ghost-mcp` server. | no |
| [`apps/docs`](./apps/docs) | Docs site. | no |
## Development
@@ -194,17 +146,11 @@ pnpm build
pnpm test
pnpm check
pnpm dump:cli-help
-pnpm --filter @anarchitecture/ghost pack
```
No API key is required to run Ghost. `OPENAI_API_KEY` / `VOYAGE_API_KEY` are
optional and only used by semantic embedding helpers when a host opts in.
-## Resources
+## License
-| Resource | Description |
-| --- | --- |
-| [docs/purposes.md](./docs/purposes.md) | What fingerprints are for: one model, many projections. |
-| [docs/ideas/](./docs/ideas) | Live design notes, anchored by `fingerprint-first-architecture.md`. |
-| [GOVERNANCE.md](./GOVERNANCE.md) | Project governance. |
-| [LICENSE](./LICENSE) | Apache License, Version 2.0. |
+[Apache License 2.0](./LICENSE) · [Governance](./GOVERNANCE.md)
diff --git a/apps/docs/src/app/docs/page.tsx b/apps/docs/src/app/docs/page.tsx
index 2bf96ca0..541383bb 100644
--- a/apps/docs/src/app/docs/page.tsx
+++ b/apps/docs/src/app/docs/page.tsx
@@ -1,7 +1,7 @@
"use client";
import { useStaggerReveal } from "ghost-ui";
-import { BookOpen, Rocket } from "lucide-react";
+import { BookOpen, FileText, Rocket } from "lucide-react";
import type { ReactNode } from "react";
import { Link } from "react-router";
import { AnimatedPageHeader } from "@/components/docs/animated-page-header";
@@ -20,11 +20,18 @@ const sections: {
"Install Ghost, set up the repo fingerprint, and learn the loop around .ghost.",
icon: ,
},
+ {
+ name: "Fingerprint Authoring",
+ href: "/docs/fingerprint-authoring",
+ description:
+ "Co-author nodes through the intent, inventory, and composition lenses, and place them for inheritance.",
+ icon: ,
+ },
{
name: "CLI Reference",
href: "/docs/cli",
description:
- "Commands for checks and comparison, plus the skill recipes your agent runs.",
+ "Every command around the node-graph fingerprint: init, validate, gather, checks, and review.",
icon: ,
},
];
diff --git a/apps/docs/src/app/page.tsx b/apps/docs/src/app/page.tsx
index 12e77918..0326b569 100644
--- a/apps/docs/src/app/page.tsx
+++ b/apps/docs/src/app/page.tsx
@@ -29,120 +29,80 @@ export default function Home() {
- Agents can assemble UI. What they cannot reliably preserve is the
- surface composition that UI belongs to.
+ Agents can assemble UI. What they can't reliably preserve is the
+ composition behind it — the hierarchy, density, restraint, copy,
+ trust, and flow that make a surface feel intentional.
- For years, design systems solved a human assembly problem. They
- gave teams shared tokens, components, examples, and usage rules so
- new surfaces could be composed from known parts.
+ Design systems solved a human assembly problem: shared tokens,
+ components, and usage rules so teams could build from known parts.
+ That layer still matters. But agents already recombine those
+ parts. The scarce layer now is the composition that tells them
+ when and how the parts belong.
- That layer still matters, but agents change the scarce layer.
- Models can copy local patterns and recombine components. They do
- not consistently preserve the composition that makes a product
- surface feel intentional: hierarchy, density, restraint, behavior,
- copy, accessibility, trust, and flow.
+ Ghost captures that composition and checks it into the repo, where
+ generation happens. It is a{" "}
+ graph of prose nodes —
+ one markdown file each — that your agent reads before it builds
+ and checks after it changes.
-
- Ghost captures the composition of a product surface: the intent
- behind it, the materials it draws from, and the patterns that make
- it feel intentional.
-
-
- It stores that composition as checked-in fingerprint facets: which
- intent shapes the surface, which materials agents can draw from,
- which situations change the obligation, which patterns hold the
- surface together, and which examples show it at its best.
-
-
- Components, tokens, and libraries become implementation material.
- Ghost does not replace them. It gives agents the surface context
- that tells them when and how those materials belong.
-
-
Ghost keeps that model compact:
- .ghost/ is the default portable fingerprint package
+ .ghost/ is the portable fingerprint package
- intent.yml, inventory.yml, and{" "}
- composition.yml store the three facets
+ surfaces.yml is the spine; nodes/*.md{" "}
+ are the design expression
- validate.yml stores optional deterministic gates
- grounded in fingerprint refs
+ each node is written through intent,{" "}
+ inventory, and composition — the why,
+ the materials, the patterns
- ordinary Git review separates draft fingerprint edits from
- checked-in truth
+ checks/*.md validate output; they are never
+ generation input
+
ordinary Git review is the approval boundary for edits
- The split is deliberate. intent.yml captures the
- intent behind the surface. inventory.yml captures the
- materials it draws from. composition.yml captures the
- patterns that make it feel intentional. Checks validate output;
- they are not generation input.
+ A node inherits everything it sits under. The brand
+ soul lives at core and reaches every surface;
+ surface-specific nodes refine it; relates links them
+ laterally. Asking for context becomes a graph traversal:{" "}
+ ghost gather <surface> composes the slice that
+ applies.
-
A typical loop becomes:
+
The loop is small:
-
Brief from the fingerprint facets and exemplars
-
Generate or edit with the host agent
-
Run active deterministic checks and advisory review
- Fix code, explain intentional divergence, or update the Ghost
- package through Git
+ Gather the composed context for the surface you're touching
+
+
Generate or edit with your agent
+
Route checks and emit an advisory review against the diff
+
+ Fix code, explain intentional divergence, or update the
+ fingerprint through Git
Ghost stays bring-your-own-agent. The agent reads, decides, and
- writes. Ghost does the repeatable work: initialization, schema
- validation, inventory, evidence verification, checks, advisory
- review packets, comparison, and upstream handoff packets.
-
-
- This is critical because surface composition that cannot be
- recalled or evaluated cannot be delegated. A product surface that
- only its original author can assess is not transferable: to
- agents, to new engineers, or to forks of the product.
-
-
- Drift becomes measurable within this system. When generated or
- modified UI diverges from checked-in fingerprint facets, the
- failure is not just error; it is signal. Drift can originate from:
-
- Ghost does not eliminate drift; it surfaces and localizes it. The
- system's boundary becomes visible where composition fails.
-
-
- The fingerprint package must live where generation happens: in the
- repository, versioned alongside the code it governs. As the
- product changes, fingerprint edits move through the same ordinary
- Git review that introduces new UI.
-
-
- This leads to a practical governance model. Each repository owns
- its product-surface fingerprint. Advanced workflows can add nested
- packages for product areas, custom fingerprint directories for
- host wrappers, comparison across systems, and declared drift
- stances.
+ writes. Ghost does the repeatable work: scaffolding, schema and
+ graph validation, context composition, check routing, and advisory
+ review packets.
- Across an organization, the collection of Ghost packages forms a
- higher-order map: a distributed model of product-surface
- composition as it is actually practiced, not as it is only
- described.
+ Composition that can't be recalled or evaluated can't be
+ delegated. A surface only its author can assess isn't transferable
+ — not to agents, not to new engineers, not to forks. Ghost makes
+ it transferable, and makes drift measurable: where generated UI
+ diverges from the fingerprint, the gap is signal, and it is
+ localized.
Design systems were libraries for humans. Ghost is composition
- context for agents: every surface can carry the fingerprint it
+ context for agents — every surface carries the fingerprint it
extends, and every deviation can carry evidence.
diff --git a/apps/docs/src/app/tools/page.tsx b/apps/docs/src/app/tools/page.tsx
index ffaefd8f..4ea48d66 100644
--- a/apps/docs/src/app/tools/page.tsx
+++ b/apps/docs/src/app/tools/page.tsx
@@ -78,7 +78,7 @@ export default function ToolsIndex() {
diff --git a/apps/docs/src/app/tools/scan/page.tsx b/apps/docs/src/app/tools/scan/page.tsx
index c4a97f49..ee36b79d 100644
--- a/apps/docs/src/app/tools/scan/page.tsx
+++ b/apps/docs/src/app/tools/scan/page.tsx
@@ -22,16 +22,16 @@ const cards: {
},
{
name: "CLI reference",
- href: "/docs/cli#ghost--fingerprint-layers-and-package-checks",
+ href: "/docs/cli",
description:
- "Check fingerprint contribution facets, validate packages, and emit context.",
+ "Report node and surface contribution, validate the graph, and compose context.",
icon: ,
},
{
- name: "Format spec",
- href: "https://github.com/block/ghost/blob/main/docs/fingerprint-format.md",
+ name: "Authoring",
+ href: "/docs/fingerprint-authoring",
description:
- "The full package format for fingerprint intent, inventory, composition, and validation.",
+ "How to write nodes through the intent, inventory, and composition lenses.",
icon: ,
},
];
@@ -48,7 +48,7 @@ export default function GhostScanLanding() {
Ghost
+
+ The product-surface fingerprint your agent reads before it builds
+ and checks after it changes.
+
>
diff --git a/apps/docs/src/content/docs/cli-reference.mdx b/apps/docs/src/content/docs/cli-reference.mdx
index 2fed608b..cb236d95 100644
--- a/apps/docs/src/content/docs/cli-reference.mdx
+++ b/apps/docs/src/content/docs/cli-reference.mdx
@@ -1,6 +1,6 @@
---
title: CLI Reference
-description: Commands around the portable fingerprint lifecycle. Your agent handles the composition work.
+description: The deterministic commands around the node-graph fingerprint lifecycle. Your agent does the reading, writing, and reviewing.
kicker: Docs
section: guide
order: 30
@@ -9,25 +9,22 @@ slug: cli
-The CLI does the repeatable parts around the fingerprint lifecycle: create
-packages, report contribution state, validate files, gather optional source material,
-emit handoff packets, govern diffs, compare packages, and record intent. Your
-agent does the reading, writing, and reviewing.
+The CLI does the repeatable parts around the fingerprint lifecycle: scaffold a
+package, validate the node graph, compose context for a surface, route checks,
+and emit advisory review packets. Your agent does the interpretation.
-`ghost --help` intentionally shows the short core workflow for new adopters.
-Run `ghost --help --all` for the complete command index; command-specific help
-remains available with `ghost --help`.
+`ghost --help` shows the short core workflow. `ghost --help --all` shows the
+complete command index, and `ghost --help` shows flags for one
+command.
-Canonical Ghost fingerprints start here, with optional child packages for scoped
-product areas:
+The canonical fingerprint is a flat `.ghost/` package:
```text
-.ghost/manifest.yml
-.ghost/intent.yml
-.ghost/inventory.yml
-.ghost/composition.yml
-.ghost/validate.yml
-apps/checkout/.ghost/manifest.yml
+.ghost/
+ manifest.yml # schema + id
+ surfaces.yml # the spine: surfaces and their parent (core is implicit)
+ nodes/*.md # prose nodes — the design expression
+ checks/*.md # optional rules an agent evaluates
```
The command tables below are generated from the CLI source. Run
@@ -37,59 +34,40 @@ The command tables below are generated from the CLI source. Run
-### Initialize - `init`
+### Initialize — `init`
-Create a `.ghost/` package with a manifest, raw facet files,
-and deterministic checks. Use `--scope ` for nested package roots. Use
-`--monorepo` to create or preserve the root package, detect workspace child
-roots, and print scoped init commands; add `--apply` to create the detected
-child packages. Use
-`GHOST_PACKAGE_DIR=` only when a host wrapper stores Ghost package
-roots under a different safe relative directory; raw `ghost` defaults to
-`.ghost`. Exact `--package ` values win over the environment default.
+Scaffold a `.ghost/` package: a manifest, an empty surfaces spine (the `core`
+root needs no declaration), and one seed node placed at `core`. Use
+`--template ` to pick a starter, `--package ` for an exact directory,
+or set `GHOST_PACKAGE_DIR` when a host wrapper stores Ghost files outside the
+default `.ghost`.
```bash
ghost init
-ghost init --monorepo
-ghost init --monorepo --apply
-ghost init --scope apps/checkout
+ghost init --template default
ghost init --package product-surface
-ghost init --package .design/custom-ghost
GHOST_PACKAGE_DIR=.agents/ghost ghost init
-GHOST_PACKAGE_DIR=.design/memory ghost init --scope apps/checkout
```
-### Contribution facets - `scan`
+### Contribution — `scan`
-Report whether `manifest.yml` is present and which sparse facets
-this package contributes: `intent`, `inventory`, `composition`, and `validate`.
-Raw repo signals do not count toward inventory contribution.
+Report what the package contributes: presence of the manifest and surfaces
+spine, and the nodes and surfaces it carries.
```bash
ghost scan
ghost scan --format json
-GHOST_PACKAGE_DIR=.agents/ghost ghost scan --format json
-GHOST_PACKAGE_DIR=.design/memory ghost scan --include-nested --format json
```
-### Stack inspection - `stack`
-
-Inspect the root-to-leaf fingerprint stack for one or more paths.
-
-
-
-```bash
-ghost stack apps/checkout/review/page.tsx --format json
-```
-
-### Inspect repo signals - `signals`
+### Repo signals — `signals`
Emit raw signals about a frontend repo as JSON. Use this as scratch evidence
-while authoring curated fingerprint facets.
+while authoring curated nodes — it does not contribute to the fingerprint by
+itself.
@@ -99,108 +77,109 @@ ghost signals .
-
+
-### Validation - `lint`
+### Validation — `validate`
-Validate a root `.ghost` fingerprint package or an individual split artifact.
-`--all` validates every nested package and merged stack.
+Validate the package: artifact shape plus the node graph — every `under` and
+`relates` link resolves, there is exactly one root, and the graph is acyclic.
+Defaults to `.ghost`; pass a file to validate a single artifact.
-
+
```bash
-ghost lint
-ghost lint .ghost/intent.yml
-ghost lint .ghost/validate.yml --format json
-ghost lint --all
+ghost validate
+ghost validate .ghost/nodes/checkout-trust.md
+ghost validate --format json
```
-### Package fidelity - `verify`
+
+
+
+
+### Compose a surface slice — `gather`
-Validate fingerprint evidence and exemplar paths, typed check refs, and
-optional rationale files.
+With no argument, list every node by id and description so an agent can match a
+task to one. With a surface, compose its context slice: the surface's own nodes,
+the ancestors it inherits via `under`, and one-hop `relates` edges. Use `--as`
+to filter to a single incarnation; untagged essence nodes always pass.
-
+
```bash
-ghost verify .ghost --root .
-GHOST_PACKAGE_DIR=.design/memory ghost verify --all
+ghost gather
+ghost gather checkout
+ghost gather checkout --as email
+ghost gather checkout --format json
```
-### Reusable Review Command - `emit`
+This is the pre-generation step: Ghost gives agents surface-composition context
+before they build, not only after a review finds drift.
-Emit `review-command` from split fingerprint facets when a host wants a
-reusable review prompt.
+
-
+
-### Agent Context - `relay gather`
+### Route checks — `checks`
-Gather Relay context for a target path or structured Relay request. Relay loads
-config first; omitted `base` means `base.kind: fingerprint`, while
-`base.kind: none` lets agent-framework repos gather declared request context
-without a `.ghost` package. For agents and host adapters, use JSON: the full
-`ghost.relay.gather/v2` result is the stable contract, and its nested `context`
-is `ghost.relay-context/v1`. Plain markdown output remains a compact human
-preview.
+Select and ground the markdown checks governing the named surfaces. The agent
+names the surfaces the change touches, then evaluates the returned checks. Use
+`--no-grounding` to omit the grounded nodes and return only the relevant checks.
-
+
```bash
-ghost relay gather apps/checkout/review/page.tsx --format json
-ghost relay gather apps/checkout/review/page.tsx --package product-surface --format json
-ghost relay gather apps/checkout/review/page.tsx --config .ghost/relay.yml --format json
-GHOST_RELAY_CONFIG=.agents/ghost/relay.yml ghost relay gather --request-stdin --format json
-ghost relay gather stacks/portal.renewal-reminder.email.yml --config .agents/ghost/relay.yml --format json
-ghost relay gather --request request.yml --format json
-ghost relay gather --request-stdin --format json
-ghost relay gather apps/checkout/review/page.tsx # human preview
+ghost checks --surface checkout
+ghost checks --surface checkout,billing
+ghost checks --surface checkout --format json
```
-### Inspection - `describe`
-
-Print a markdown section map.
+### Advisory review packet — `review`
-
+Emit an advisory packet for a diff: touched surfaces, routed checks, and
+fingerprint grounding, with the diff embedded verbatim. Diff against a git ref
+with `--base`, or pass a patch with `--diff` (use `-` for stdin).
-### Survey/cache ops - `survey `
+
-Operate on `ghost.survey/v1` files as compatibility cache source material.
+```bash
+ghost review --surface checkout --base main
+ghost review --surface checkout --diff change.patch
+git diff | ghost review --surface checkout --diff -
+ghost review --surface checkout --format json
+```
-
+Wrappers should consume `--format json` and map Ghost severities (`critical`,
+`serious`, `nit`) into their own review format. Advisory review is never a CI
+gate on its own.
-
-
-### Deterministic gates - `check`
+
-Run active `ghost.validate/v1` gates against a git diff. Without `--package`,
-Ghost groups changed files by resolved fingerprint stack and runs merged checks
-per group.
+### Install the skill — `skill`
-
+Install the unified Ghost skill bundle so a host agent knows how to author and
+use the fingerprint.
-### Advisory governance packet - `review`
-
-Emit an evidence-routed advisory review packet grounded in selected context,
-validation checks, and the diff.
-
-
+
-### Comparison - `compare`
-
-Pairwise distance or composite analysis over fingerprint packages.
+```bash
+ghost skill install
+ghost skill install --agent claude
+ghost skill install --dest ~/.codex/skills/ghost
+```
-
+### Migrate a legacy package — `migrate`
-### Drift stance - `ack` / `track` / `diverge`
+Migrate a legacy `.ghost/` package onto the node-graph surface model. Use
+`--dry-run` to print the plan without writing.
-Record how the repo should treat tracked fingerprint drift. These compatibility
-governance verbs still operate on tracked direct fingerprint markdown files.
+
-
-
-
+```bash
+ghost migrate
+ghost migrate .ghost --dry-run
+```
diff --git a/apps/docs/src/content/docs/fingerprint-authoring.mdx b/apps/docs/src/content/docs/fingerprint-authoring.mdx
index 6f285361..d12a1c21 100644
--- a/apps/docs/src/content/docs/fingerprint-authoring.mdx
+++ b/apps/docs/src/content/docs/fingerprint-authoring.mdx
@@ -1,6 +1,6 @@
---
title: Fingerprint Authoring
-description: Co-author Ghost fingerprints with human intent, repo evidence, agent synthesis, and Git review.
+description: Co-author a Ghost fingerprint as a graph of prose nodes — human intent, repo evidence, agent synthesis, and Git review.
kicker: Docs
section: guide
order: 20
@@ -10,32 +10,65 @@ slug: fingerprint-authoring
A Ghost fingerprint is not a scan dump. It is durable product-surface
-composition that a human and agent shape together.
+composition that a human and agent shape together, stored as a graph of prose
+**nodes**.
-The human names the intent: what the product surface should feel like, who it
-serves, which situations matter, and what should not drift. Repo scans provide
-evidence: components, routes, docs, stories, copy, screenshots, tokens,
-examples, and UI library references. The agent synthesizes drafts, but ordinary
-Git review is where fingerprint edits become canonical.
+The human names the intent: what the surface should feel like, who it serves,
+which situations matter, and what should not drift. Repo scans provide evidence:
+components, routes, docs, stories, copy, tokens, and library references. The
+agent synthesizes drafts. Ordinary Git review is where node edits become
+canonical.
+
+
+
+
+
+Each node is one markdown file in `nodes/`. Frontmatter carries the machine
+handles; the body carries the design expression, written through the intent /
+inventory / composition lenses.
+
+```markdown
+---
+id: checkout-trust
+description: Trust at the payment moment.
+under: checkout
+relates:
+ - to: core-trust
+ as: reinforces
+---
+
+Near the moment of payment, reduce felt risk. Proximity of reassurance to the
+action beats completeness…
+```
+
+| Handle | Role |
+| --- | --- |
+| `id` | Unique, stable identifier. How the node is referenced. |
+| `description` | The retrieval payload — a one-line "what this is / when to gather it." Write one on any node worth anchoring a task at. |
+| `under` | Places the node so it is inherited downward. `core` is the implicit root and reaches every surface. |
+| `relates` | Lateral links carrying rationale (`reinforces`, `contrasts`, `variant`). |
+| `incarnation` | Tags a medium-bound expression (`email`, `voice`, …). Essence is untagged. |
+
+Free-form keys (`audience`, `stage`, …) are allowed and pass through untouched.
+Surfaces themselves are declared in `surfaces.yml`, never inferred from paths.
-Start by classifying the authoring scenario. The scenario determines how much
-weight to give human intent, existing code, and library evidence.
+Classify the authoring scenario first. It determines how much weight to give
+human intent, existing code, and library evidence.
| Scenario | Authoring posture |
| --- | --- |
| Net new repo | Human-led. Capture intent, audience, posture, and early anti-goals before inventory grows. |
| Net new repo + UI library | Human-led with library evidence. Explain how this product uses the library. |
| Existing repo | Human + scan. Find repeated patterns and exemplars, then ask which ones are canonical. |
-| Existing repo with mixed quality | Curated scan. Separate durable surface composition from legacy debt and accidental repetition. |
-| Design system or UI library | Grammar-led. Describe primitives, tokens, component behavior, accessibility, and composition constraints. |
-| Rebrand, redesign, or migration | Human-led transition. Capture current, target, and migration cautions. |
+| Existing repo, mixed quality | Curated scan. Separate durable composition from legacy debt and accidental repetition. |
+| Design system or UI library | Grammar-led. Describe primitives, tokens, behavior, accessibility, and composition constraints. |
+| Rebrand, redesign, migration | Human-led transition. Capture current, target, and migration cautions. |
| Prototype becoming product | Ratification-led. Preserve only the emergent patterns humans want to keep. |
-| Fork, white label, or tenant variant | Shared base + local divergence. Keep common surface composition broad and local differences scoped. |
-| Monorepo or nested surfaces | Stack-aware. Use root guidance for broad composition and nested packages for surfaces assessed differently. |
+| Fork, white label, tenant variant | Shared base + local divergence. Keep common composition at `core`, scope differences to surface nodes. |
@@ -43,109 +76,86 @@ weight to give human intent, existing code, and library evidence.
Ghost supports two agent authoring modes:
-- **Default** - interview first, scan as needed, draft facet edits, then
- curate.
-- **Auto-draft** - scan first, draft a small starter fingerprint, then curate
+- **Default** — interview first, scan as needed, draft nodes, then curate.
+- **Auto-draft** — scan first, draft a small starter fingerprint, then curate
the claims with a human.
-Auto-draft is a skill workflow, not a Ghost CLI command. Ask for it in plain
-English:
+Auto-draft is a skill workflow, not a CLI command. Ask for it in plain English:
```text
Set up the Ghost fingerprint for this repo with auto-draft.
```
-1. **Interview** - ask what the product should feel like, who it serves, which
+1. **Interview** — ask what the product should feel like, who it serves, which
surfaces show it at its best, and which existing patterns are accidental or
- legacy. In auto-draft mode, use this step after the starter draft to curate
- claims.
-2. **Scan** - inspect routes, components, stories, tests, docs, screenshots,
- copy, tokens, assets, and UI library references.
-3. **Draft** - write the smallest useful `intent.yml`, `inventory.yml`, and
- `composition.yml` entries.
-4. **Curate** - have the human keep, soften, reject, scope, or record important
- claims before treating them as durable surface context.
-5. **Validate** - run Ghost validation and use Git review as the approval
+ legacy.
+2. **Scan** — inspect routes, components, stories, tests, docs, copy, tokens,
+ and library references. Use `ghost signals` for raw observations.
+3. **Draft** — write the smallest useful nodes. Place durable, cross-surface
+ guidance at `core`; place surface-specific obligations `under` that surface.
+4. **Curate** — have the human keep, soften, reject, or scope each claim before
+ it is treated as durable context.
+5. **Validate** — run `ghost validate` and use Git review as the approval
boundary.
```bash
-ghost scan --format json
ghost signals .
-ghost lint .ghost
-ghost verify .ghost --root .
+ghost scan
+ghost validate
```
-Raw repo signals are source evidence only. They can support curated inventory,
-but they do not establish surface-composition guidance by themselves. Signal
-frequency may seed a draft, but it does not decide what the surface should do.
+Raw repo signals are source evidence only. Signal frequency may seed a draft,
+but it does not decide what the surface should do.
-
+
-Keep each claim in the file that will make it useful later:
+The graph is the model. Decide where each claim lives by how far it should
+reach.
-| Facet | What belongs there |
-| --- | --- |
-| `intent.yml` | Audience, goals, anti-goals, situations, principles, and experience contracts. |
-| `inventory.yml` | Scopes, surface types, files, routes, libraries, assets, building blocks, exemplars, and source links. |
-| `composition.yml` | Repeatable rules, layouts, structures, flows, states, content patterns, behavior, and visual arrangements. |
-| `validate.yml` | Deterministic gates that can be checked from a diff. |
+- Put the brand soul — voice, trust posture, broad product intent — at `core`.
+ It cascades to every surface.
+- Put surface-specific obligations `under` the surface that owns them
+ (`under: checkout`).
+- Link nodes laterally with `relates` only when the relationship carries
+ rationale a future agent needs.
-
+```bash
+ghost gather # list nodes by id + description
+ghost gather checkout # compose checkout's slice (own + inherited + edges)
+```
+
+`ghost gather ` is the test: if the composed slice reads like coherent
+guidance for that surface, the placement is right.
-
+
-A useful fingerprint should help future agents choose, restrain, route, anchor,
-and review. It should not only describe what exists or collect every available
-style detail.
+
-Write facet content so generation decisions become explicit:
+A useful node helps a future agent choose, restrain, and review — not just
+describe what exists. Write the body so generation decisions become explicit:
-- `goals` name what generated work should preserve.
-- `anti_goals` block plausible defaults that would make the surface feel
- generic or wrong.
-- `tradeoffs` say which value wins when choices conflict.
-- `situations` route guidance by task, surface type, state, or audience need.
-- `principles` capture broad product intent.
-- `experience_contracts` turn taste, trust, recovery, or disclosure into
- obligations.
-- `composition.patterns` give repeatable layout, flow, state, content,
- behavior, or visual rules.
-- `inventory.exemplars` anchor the guidance in concrete material an agent can
- inspect.
+- Name what generated work should preserve.
+- Block the plausible defaults that would make the surface feel generic.
+- Say which value wins when choices conflict.
+- Anchor the guidance in concrete material an agent can inspect.
Write less like a brand book and more like a decision engine.
-
+
-Nested fingerprints are opt-in. Create a local `.ghost/` only when a surface
-should be assessed differently from the root product fingerprint.
+Uncommitted or unmerged node edits are drafts. Checked-in nodes are canonical.
-Use a nested package when a surface has distinct users, information density,
-trust or recovery posture, interaction rhythm, component grammar, UI library
-usage, or review criteria for the same UI decision.
-
-Keep broad product-family guidance at the root. Put local obligations in the
-nearest package that owns the surface.
+Add `ghost.check/v1` markdown checks in `checks/*.md` sparingly, and only when a
+rule can be enforced from a diff. Checks are routed by surface and validate
+output — they are never generation input.
```bash
-ghost init --scope apps/checkout
-ghost stack apps/checkout
-ghost lint --all
-ghost verify --all
+ghost checks --surface checkout
+ghost review --surface checkout --base main
```
-
-
-
-Uncommitted or unmerged fingerprint edits are drafts. Checked-in
-Ghost package facet files are canonical.
-
-Add deterministic checks sparingly, and only when a rule can be enforced
-deterministically.
-
-
diff --git a/apps/docs/src/content/docs/getting-started.mdx b/apps/docs/src/content/docs/getting-started.mdx
index ebc88699..8be29b1f 100644
--- a/apps/docs/src/content/docs/getting-started.mdx
+++ b/apps/docs/src/content/docs/getting-started.mdx
@@ -1,35 +1,40 @@
---
title: Getting Started
-description: Install Ghost, author a product-surface composition fingerprint, and use it to generate, validate, compare, and govern product surfaces.
+description: Install Ghost, scaffold a product-surface fingerprint as a graph of prose nodes, and use it to brief, validate, and review your agent's work.
kicker: Docs
section: guide
order: 10
slug: getting-started
---
-
+
-Ghost captures the composition of a product surface: the intent behind it, the
-materials it draws from, and the patterns that make it feel intentional. The
-public package is `@anarchitecture/ghost`, and it installs one CLI: `ghost`.
+Ghost captures the composition of a product surface — the intent behind it, the
+materials it draws from, and the patterns that make it feel intentional — and
+checks it into the repo. The public package is `@anarchitecture/ghost`, and it
+installs one CLI: `ghost`.
-The canonical portable fingerprint is a folder:
+A fingerprint is a small folder of prose:
```text
.ghost/
- manifest.yml
- intent.yml
- inventory.yml
- composition.yml
- validate.yml
+ manifest.yml # schema + id
+ surfaces.yml # the spine: surfaces and their parent (core is implicit)
+ nodes/*.md # prose nodes — the design expression
+ checks/*.md # optional rules an agent evaluates
```
-Generation starts from `intent.yml`, `inventory.yml`, and `composition.yml`.
-`validate.yml` checks validate the result afterward; they are not generation input.
+The fingerprint is a **graph of nodes**. A node is one markdown file:
+frontmatter handles plus a prose body. You write that body through three lenses
+— they guide what to capture, they are not fields:
-Nested product areas can add child package roots such as
-`apps/checkout/.ghost/`. Ghost resolves fingerprint stacks
-root-to-leaf for the file or diff being reviewed.
+- **intent** — the why and the stance.
+- **inventory** — the materials, and pointers to the code an agent can inspect.
+- **composition** — the patterns that make the surface feel like one product.
+
+`under` cascades a node downward (`core` is the implicit root and reaches every
+surface). `relates` links nodes laterally. Checks validate output afterward;
+they are never generation input.
@@ -37,20 +42,20 @@ root-to-leaf for the file or diff being reviewed.
-Ghost is pre-1.0 and under active development. The CLI, fingerprint schema,
-on-disk `.ghost/` package shape, and public JavaScript exports may
-change in breaking ways before a stable 1.0 release.
+Ghost is pre-1.0 and under active development. The CLI, node schema, on-disk
+`.ghost/` shape, and public JavaScript exports may change in breaking ways
+before a stable 1.0 release.
Breaking changes may ship in minor versions while Ghost is pre-1.0. Patch
versions are reserved for fixes that should not require migration. If you adopt
-Ghost today, expect some churn, pin the version you depend on, and review
-release notes before upgrading.
+Ghost today, pin the version you depend on and review release notes before
+upgrading.
-
+
```bash
npm install -D @anarchitecture/ghost
@@ -59,15 +64,16 @@ npx ghost --help --all
npx ghost skill install
```
-`ghost --help` shows the core new-adopter workflow. Use `ghost --help --all`
-when you want the complete command index.
+`ghost --help` shows the core workflow. `ghost --help --all` shows the complete
+command index, and `ghost --help` shows flags for one command.
-Once the skill is installed, ask your agent in plain English:
+Ghost is **bring-your-own-agent**. Once the skill is installed, ask your agent
+in plain English:
```text
Set up the Ghost fingerprint for this repo.
Brief this work from the Ghost fingerprint.
-Review this PR against the Ghost fingerprint.
+Review this change against the Ghost fingerprint.
```
The skill tells the agent what to read, what to write, and which CLI checks to
@@ -75,95 +81,108 @@ run.
-
+
-The CLI handles the deterministic package work. Your agent handles the
-composition work: interviewing, reading repo evidence, drafting facet edits, and
-asking you to curate the claims.
+`ghost init` writes a minimal package: a manifest, an empty surfaces spine (the
+`core` root needs no declaration), and one seed node placed at `core`.
```bash
ghost init
-ghost scan --format json
-ghost signals .
-ghost lint .ghost
-ghost verify .ghost --root .
+ghost validate
+ghost scan
```
-The fingerprint records durable surface-composition guidance:
-
-1. **Intent** - what must remain true: what product this is, who it
- serves, which situations matter, and which principles or contracts apply.
-2. **Inventory** - the materials it draws from: topology, building blocks,
- files, routes, assets, libraries, exemplars, and source links agents may
- inspect or use.
-3. **Composition** - the patterns that make it intentional: rules, layouts,
- structures, flows, states, content, behavior, and visual arrangements.
-
-Raw repo signals are optional authoring evidence. Curate durable intent,
-inventory, and composition into the facet files, then use normal Git
-review for approval. For a fuller human-agent workflow, read
-[Fingerprint Authoring](/docs/fingerprint-authoring).
+`ghost validate` confirms the package is well-formed: artifact shape plus the
+node graph (links resolve, exactly one root, acyclic). `ghost scan` reports what
+the package contributes.
-
+
-Before generating or revising UI, gather Relay JSON for the target path:
+A node is one markdown file in `nodes/`. The frontmatter is machine handles; the
+body is the design expression.
-```bash
-ghost relay gather apps/checkout/review/page.tsx --format json
-```
+```markdown
+---
+id: checkout-trust
+description: Trust at the payment moment.
+under: checkout
+relates:
+ - to: core-trust
+ as: reinforces
+incarnation: web
+---
-`ghost.relay.gather/v2` is the agent contract. Agents should read `context`,
-`selected_context`, `targetPaths`, `source`, `stackDirs`, gaps, and trace fields
-from JSON. Plain `ghost relay gather ` remains a compact human preview.
-For prompt-shaped work where there is no clear path, host agents can create a
-`ghost.relay-request/v1` and run
-`ghost relay gather --request-stdin --format json`.
-Relay config controls the runtime. Omitted `base` uses the resolved fingerprint
-stack; `base.kind: none` lets frameworks provide declared request context
-without a `.ghost` package:
+Near the moment of payment, reduce felt risk. Proximity of reassurance to the
+action beats completeness. Never introduce a new visual system here.
+```
-```bash
-GHOST_RELAY_CONFIG=.agents/ghost/relay.yml ghost relay gather --request-stdin --format json
-ghost relay gather stacks/portal.renewal-reminder.email.yml --config .agents/ghost/relay.yml --format json
+- **`id`** — unique and stable; how the node is referenced.
+- **`description`** — the retrieval payload: a one-line "what this is and when to
+ gather it," exactly like a tool's name and description. `ghost gather` with no
+ argument lists nodes by id and description so an agent can match a task to one.
+- **`under`** — places the node so it is inherited downward. `core`-placed nodes
+ reach every surface.
+- **`relates`** — links nodes laterally (`reinforces`, `contrasts`, `variant`).
+- **`incarnation`** — tags a medium-bound expression (`email`, `voice`, …).
+ Leave essence untagged. Free-form keys (`audience`, `stage`, …) pass through.
+
+Surfaces are declared in `surfaces.yml`, never inferred from filenames:
+
+```yaml
+schema: ghost.surfaces/v1
+surfaces:
+ - id: checkout
+ parent: core
```
-The package remains the approved product-surface context; review and check
-commands apply it after implementation.
+The CLI handles the deterministic work — scaffolding, validation, context
+composition. Your agent handles the composition work: interviewing, reading
+repo evidence, drafting nodes, and asking you to curate the claims. Use
+`ghost signals` for raw repo observations while authoring. For the full
+human-agent workflow, read [Fingerprint Authoring](/docs/fingerprint-authoring).
+
+Drafted fingerprint edits are ordinary file changes until Git review accepts
+them. Checked-in nodes are the Ghost source of truth.
-
+
-After implementation, run Ghost against the same fingerprint:
+Before generating or revising UI, compose the context slice for the surface
+you're touching:
```bash
-ghost check --base main
-ghost review --base main
+ghost gather # list nodes by id + description
+ghost gather checkout # compose checkout's slice
+ghost gather checkout --as email # filter to one incarnation
```
-`ghost check` applies active deterministic gates from the resolved fingerprint
-stack for each changed file. `ghost review` emits advisory context grounded in
-the same selected context as Relay, selected validation checks, and the diff.
-
-Wrappers should consume `ghost check --format json` and map Ghost severities
-outside Ghost. Ghost severities remain `critical`, `serious`, and `nit`.
+`ghost gather ` traverses the graph: the surface's own nodes, the
+ancestors it inherits via `under`, and one-hop `relates` edges. The important
+shift is timing — Ghost gives agents surface-composition context **before** they
+build, not only after a review finds drift.
-
+
+
+After implementation, route the relevant checks and emit an advisory packet
+against the diff. The agent names the surfaces the change touches.
```bash
-ghost compare market/.ghost dashboard/.ghost
-ghost stack apps/checkout/review/page.tsx
-ghost ack --stance aligned --reason "Initial baseline"
-ghost track new-tracked.fingerprint.md
-ghost diverge typography --reason "Editorial product uses a different type scale"
+ghost checks --surface checkout
+ghost review --surface checkout --base main
```
-Package comparison uses canonical `.ghost/` packages. `ack`,
-`track`, and `diverge` record stance for compatibility drift workflows that
-track direct fingerprint markdown references.
+`ghost checks` selects and grounds the markdown checks governing the named
+surfaces — the agent evaluates them. `ghost review` emits an advisory packet:
+touched surfaces, routed checks, and fingerprint grounding, with the diff
+embedded verbatim.
+
+Wrappers should consume `--format json` and map Ghost severities into their own
+review format. Ghost severities are `critical`, `serious`, and `nit`. Advisory
+review is never a CI gate on its own.
diff --git a/packages/ghost/README.md b/packages/ghost/README.md
index bdef9c74..3983b78a 100644
--- a/packages/ghost/README.md
+++ b/packages/ghost/README.md
@@ -2,24 +2,24 @@
**A unified Ghost CLI for product-surface composition fingerprints.**
-Ghost captures the composition of a product surface: the intent behind it, the
-materials it draws from, and the patterns that make it feel intentional. It
-stores that composition in a repo-local `.ghost/` package that host
-agents can read before generation and validate after changes.
+Agents can assemble UI. They can't reliably preserve the _composition_ behind it
+— the hierarchy, density, restraint, copy, trust, and flow that make a surface
+feel intentional. Ghost captures that composition in a repo-local `.ghost/`
+package that a host agent reads before it builds and checks after it changes.
This package ships one CLI: `ghost`.
## Project Status: Beta
> [!WARNING]
-> Ghost is pre-1.0 and under active development. The CLI, fingerprint schema,
-> on-disk `.ghost/` package shape, and public JavaScript exports may
-> change in breaking ways before a stable 1.0 release.
+> Ghost is pre-1.0 and under active development. The CLI, node schema, on-disk
+> `.ghost/` package shape, and public JavaScript exports may change in breaking
+> ways before a stable 1.0 release.
>
> Breaking changes may ship in minor versions while Ghost is pre-1.0. Patch
> versions are reserved for fixes that should not require migration. If you adopt
-> Ghost today, expect some churn, pin the version you depend on, and review
-> release notes before upgrading.
+> Ghost today, pin the version you depend on and review release notes before
+> upgrading.
## Install
@@ -30,7 +30,24 @@ npx ghost --help --all
```
`ghost --help` shows the core workflow. `ghost --help --all` shows the complete
-command index.
+command index, and `ghost --help` shows flags for one command.
+
+## The Shape
+
+A fingerprint is a small folder of prose — a **graph of nodes**:
+
+```text
+.ghost/
+ manifest.yml # schema + id
+ surfaces.yml # the spine: surfaces and their parent (core is implicit)
+ nodes/*.md # prose nodes — the design expression
+ checks/*.md # optional rules an agent evaluates
+```
+
+A node is one markdown file: frontmatter handles (`id`, `description`, `under`,
+`relates`, `incarnation`) plus a prose body written through three lenses —
+**intent** (the why), **inventory** (the materials), and **composition** (the
+patterns). `under` cascades a node downward; `core` reaches every surface.
## Use
@@ -38,65 +55,63 @@ Create and validate the fingerprint package:
```bash
ghost init
-ghost scan --format json
-ghost lint .ghost
-ghost verify .ghost --root .
+ghost validate
+ghost scan
```
Gather context before generation:
```bash
-ghost relay gather apps/checkout/review/page.tsx
+ghost gather # list nodes by id + description
+ghost gather checkout # compose a surface's context slice
```
Govern changes afterward:
```bash
-ghost check --base main
-ghost review --base main
+ghost checks --surface checkout
+ghost review --surface checkout --base main
```
-Install the BYOA skill bundle so your host agent can author, brief, review,
-verify, remediate, and update fingerprints:
+Install the BYOA skill bundle so your host agent can author, brief, review, and
+verify fingerprints:
```bash
ghost skill install
```
-Advanced commands such as `signals`, `stack`, `compare`, `ack`, `track`, and
-`diverge` remain available in the full command index.
+Advanced and maintenance commands — `signals` and `migrate` — remain available
+in the full command index.
-Zero config for every verb. No API key is required. `OPENAI_API_KEY` /
-`VOYAGE_API_KEY` are optional and only used by semantic embedding helpers when a
-host opts in.
+No API key is required. `OPENAI_API_KEY` / `VOYAGE_API_KEY` are optional and
+only used by semantic embedding helpers when a host opts in.
## Library
```ts
-import { compare } from "@anarchitecture/ghost/compare";
-import { runGhostCheck } from "@anarchitecture/ghost/govern";
-import { gatherRelayContext } from "@anarchitecture/ghost/relay";
import {
initFingerprintPackage,
lintFingerprintPackage,
- verifyFingerprintPackage,
} from "@anarchitecture/ghost/fingerprint";
+import { buildCli } from "@anarchitecture/ghost/cli";
```
+Available subpath exports: `@anarchitecture/ghost`,
+`@anarchitecture/ghost/scan`, `@anarchitecture/ghost/fingerprint`,
+`@anarchitecture/ghost/core`, and `@anarchitecture/ghost/cli`.
+
## BYOA
Ghost is bring-your-own-agent. The CLI performs deterministic work: repo
-signals, readiness reporting, linting, verification, comparison, checks, and
-advisory review packet generation. The installed `ghost` skill teaches a host
-agent how to capture canonical `.ghost/` surface-composition
-context, brief and generate work from it, review changes against it, verify
-generated UI, remediate issues, and suggest fingerprint edits when the user
-asks.
+signals, contribution reporting, graph validation, context composition, check
+routing, and advisory review packets. The installed `ghost` skill teaches a host
+agent how to capture canonical `.ghost/` surface-composition context, brief and
+generate work from it, review changes against it, and verify generated UI.
```text
Set up the Ghost fingerprint for this repo.
Brief this work from the Ghost fingerprint.
-Review this PR against the Ghost fingerprint.
+Review this change against the Ghost fingerprint.
```
## Maintainers
From 017870cb99defe35ccb1321b4bc39752b7d9516f Mon Sep 17 00:00:00 2001
From: Nahiyan Khan
Date: Sun, 28 Jun 2026 17:13:18 -0400
Subject: [PATCH 02/12] =?UTF-8?q?feat!:=20directory-as-architecture=20?=
=?UTF-8?q?=E2=80=94=20the=20tree=20is=20the=20graph?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Collapse the on-disk node model into the directory tree. A node's id is its
file path (`marketing/email.md` → `marketing/email`) and its parent is its
containing directory; a surface is just a directory, and a directory's own
prose lives in its `index.md` (the package-root `index.md` is the implicit
`core` node).
Removed:
- `surfaces.yml` spine file and the `ghost.surfaces/v1` artifact/module
- the `nodes/` directory convention (any `*.md` outside `checks/` is a node)
- node frontmatter `id` and `under` — identity and containment come from the
file's location, never from frontmatter or a declared spine
Node frontmatter is now descriptive properties only (`description`, `relates`,
`incarnation`, plus passthrough keys). `relates`/`extends` refs are path ids
(`core/trust`, `brand:core/trust`). Moving a node is a rename; `ghost validate`
reports `relates` that no longer resolve.
The graph query layer (gather/checks/review/slice) is unchanged — it was
already id-based; this only changes discovery, schema, and scaffolding.
`ghost init` scaffolds `manifest.yml` + a core `index.md`; `ghost migrate`
writes a directory tree. Skill bundle, docs, README, and CLAUDE.md updated;
unreleased changesets re-pointed so the 0.19.0 changelog reads as one coherent
facet→directory-tree story.
---
.changeset/cross-package-extends.md | 2 +-
.changeset/described-nodes.md | 2 +-
.changeset/directory-tree-nodes.md | 18 ++
.changeset/docs-node-model-refresh.md | 3 +-
.changeset/facet-removal.md | 2 +-
.changeset/migrate-command.md | 7 +-
.changeset/node-authoring.md | 2 +-
.changeset/remove-compare-drift-fleet.md | 2 +-
.changeset/surface-coordinate-space.md | 4 +-
CLAUDE.md | 46 ++--
README.md | 60 +++--
apps/docs/src/app/page.tsx | 14 +-
apps/docs/src/content/docs/cli-reference.mdx | 34 +--
.../content/docs/fingerprint-authoring.mdx | 195 ++++++++--------
.../docs/src/content/docs/getting-started.mdx | 198 ++++++++---------
apps/docs/src/generated/cli-manifest.json | 4 +-
packages/ghost/README.md | 21 +-
.../ghost/src/commands/migrate-command.ts | 33 ++-
.../ghost/src/ghost-core/graph/assemble.ts | 73 +++---
packages/ghost/src/ghost-core/graph/index.ts | 5 +-
packages/ghost/src/ghost-core/graph/lint.ts | 61 ++---
packages/ghost/src/ghost-core/graph/menu.ts | 6 +-
packages/ghost/src/ghost-core/graph/slice.ts | 6 +-
packages/ghost/src/ghost-core/graph/types.ts | 33 +--
packages/ghost/src/ghost-core/index.ts | 14 +-
packages/ghost/src/ghost-core/node/schema.ts | 47 ++--
.../ghost/src/ghost-core/node/serialize.ts | 17 +-
packages/ghost/src/ghost-core/node/types.ts | 25 +--
.../ghost/src/ghost-core/surfaces/index.ts | 19 --
.../ghost/src/ghost-core/surfaces/lint.ts | 208 ------------------
.../ghost/src/ghost-core/surfaces/schema.ts | 35 ---
.../ghost/src/ghost-core/surfaces/types.ts | 48 ----
packages/ghost/src/scan/file-kind.ts | 40 ++--
.../src/scan/fingerprint-contribution.ts | 21 +-
.../src/scan/fingerprint-package-layers.ts | 41 +---
.../ghost/src/scan/fingerprint-package.ts | 11 +-
packages/ghost/src/scan/migrate-legacy.ts | 71 +++---
packages/ghost/src/scan/node-tree.ts | 126 +++++++++++
packages/ghost/src/scan/nodes-dir.ts | 50 -----
packages/ghost/src/scan/scan-status.ts | 1 -
packages/ghost/src/scan/templates.ts | 37 ++--
packages/ghost/src/skill-bundle/SKILL.md | 45 ++--
.../references/authoring-scenarios.md | 6 +-
.../src/skill-bundle/references/capture.md | 58 ++---
.../src/skill-bundle/references/schema.md | 55 +++--
packages/ghost/test/cli.test.ts | 176 ++++++---------
.../ghost/test/fingerprint-package.test.ts | 19 +-
.../ghost/test/ghost-core/check-route.test.ts | 34 +--
.../ghost/test/ghost-core/graph-fold.test.ts | 78 ++++---
.../ghost/test/ghost-core/graph-slice.test.ts | 125 +++++------
.../ghost/test/ghost-core/node-schema.test.ts | 122 +++++-----
.../test/ghost-core/surfaces-lint.test.ts | 102 ---------
.../test/ghost-core/surfaces-schema.test.ts | 86 --------
packages/ghost/test/migrate-legacy.test.ts | 30 ++-
packages/ghost/test/scan-status.test.ts | 46 ++--
55 files changed, 1096 insertions(+), 1528 deletions(-)
create mode 100644 .changeset/directory-tree-nodes.md
delete mode 100644 packages/ghost/src/ghost-core/surfaces/index.ts
delete mode 100644 packages/ghost/src/ghost-core/surfaces/lint.ts
delete mode 100644 packages/ghost/src/ghost-core/surfaces/schema.ts
delete mode 100644 packages/ghost/src/ghost-core/surfaces/types.ts
create mode 100644 packages/ghost/src/scan/node-tree.ts
delete mode 100644 packages/ghost/src/scan/nodes-dir.ts
delete mode 100644 packages/ghost/test/ghost-core/surfaces-lint.test.ts
delete mode 100644 packages/ghost/test/ghost-core/surfaces-schema.test.ts
diff --git a/.changeset/cross-package-extends.md b/.changeset/cross-package-extends.md
index 0c891863..4f06d699 100644
--- a/.changeset/cross-package-extends.md
+++ b/.changeset/cross-package-extends.md
@@ -2,4 +2,4 @@
"@anarchitecture/ghost": minor
---
-Add cross-package inheritance via `extends`. A package's `manifest.yml` can declare `extends: { : }`, mapping another contract's identity to where it lives. Node refs then reference inherited context by identity, never path — `under: brand:core` or `relates: [{ to: brand:core-trust }]` (the `:` form replaces the earlier npm-style `#` ref grammar). Inherited nodes load read-only and flow into gather and validate like local ones. `ghost validate` resolves cross-package refs and reports unresolved refs, packages not declared in `extends`, identity mismatches, and cross-package cycles. This delivers the shared-brand story: one brand contract extended by many products, without copy-paste or merge. One level of `extends` in v1 (no transitive); location is an explicit relative dir (identity-based discovery is a future upgrade that keeps refs unchanged).
+Add cross-package inheritance via `extends`. A package's `manifest.yml` can declare `extends: { : }`, mapping another contract's identity to where it lives. Node refs then reference inherited context by identity, never path — `relates: [{ to: brand:core/trust }]` (the `:` form replaces the earlier npm-style `#` ref grammar). Inherited nodes load read-only and flow into gather and validate like local ones. `ghost validate` resolves cross-package refs and reports unresolved refs, packages not declared in `extends`, identity mismatches, and cross-package cycles. This delivers the shared-brand story: one brand contract extended by many products, without copy-paste or merge. One level of `extends` in v1 (no transitive); location is an explicit relative dir (identity-based discovery is a future upgrade that keeps refs unchanged).
diff --git a/.changeset/described-nodes.md b/.changeset/described-nodes.md
index 60fbe52a..7b695163 100644
--- a/.changeset/described-nodes.md
+++ b/.changeset/described-nodes.md
@@ -2,4 +2,4 @@
"@anarchitecture/ghost": minor
---
-Make `description` a first-class node field — the retrieval payload an agent matches a task against, the way a tool is selected by name + description. `ghost gather` with no argument now lists nodes by id + description (the catalog), built from the graph rather than a separate surface menu. Node frontmatter is now passthrough: free-form descriptive keys (`audience`, `stage`, …) are allowed and ride along untouched. The surface composition-edge vocabulary (`composes`/`governed-by`) is removed — lateral composition lives on node `relates`; `surfaces.yml` is now an optional terse spine file (id + parent + optional description) that folds into the node id space, not a distinct content concept.
+Make `description` a first-class node field — the retrieval payload an agent matches a task against, the way a tool is selected by name + description. `ghost gather` with no argument now lists nodes by id + description (the catalog), built from the graph rather than a separate surface menu. Node frontmatter is now passthrough: free-form descriptive keys (`audience`, `stage`, …) are allowed and ride along untouched. The surface composition-edge vocabulary (`composes`/`governed-by`) is removed — lateral composition lives on node `relates`.
diff --git a/.changeset/directory-tree-nodes.md b/.changeset/directory-tree-nodes.md
new file mode 100644
index 00000000..2b05ddc8
--- /dev/null
+++ b/.changeset/directory-tree-nodes.md
@@ -0,0 +1,18 @@
+---
+"@anarchitecture/ghost": minor
+---
+
+Collapse the on-disk node model into the directory tree: the layout *is* the
+graph. A node's id is its file path (`marketing/email.md` → `marketing/email`)
+and its parent is its containing directory; a surface is just a directory, and a
+directory's own prose lives in its `index.md` (the package-root `index.md` is
+the implicit `core` node). The `surfaces.yml` spine file and the `nodes/`
+directory are removed, along with the node frontmatter `id` and `under` fields —
+identity and containment now come from where a file sits, never from frontmatter
+or a declared spine. Node frontmatter carries descriptive properties only
+(`description`, `relates`, `incarnation`, plus passthrough keys); `relates` and
+cross-package `extends` refs are path ids (`core/trust`, `brand:core/trust`).
+`ghost init` scaffolds `manifest.yml` + a core `index.md`; `ghost migrate`
+writes a directory tree; any `*.md` outside the reserved `checks/` subtree lints
+as a node. Moving a node is a rename — `ghost validate` reports `relates` that no
+longer resolve.
diff --git a/.changeset/docs-node-model-refresh.md b/.changeset/docs-node-model-refresh.md
index 96d5ba3c..b884f37d 100644
--- a/.changeset/docs-node-model-refresh.md
+++ b/.changeset/docs-node-model-refresh.md
@@ -2,4 +2,5 @@
"@anarchitecture/ghost": patch
---
-Refresh the README and docs onto the node-graph model and the current command set.
+Refresh the README and docs site onto the current command set (drop the removed
+`lint`/`verify`/`relay`/`describe`/`survey`/`emit` commands).
diff --git a/.changeset/facet-removal.md b/.changeset/facet-removal.md
index 30ccabff..56b0a693 100644
--- a/.changeset/facet-removal.md
+++ b/.changeset/facet-removal.md
@@ -2,4 +2,4 @@
"@anarchitecture/ghost": minor
---
-Remove the facet model — the graph is now the only fingerprint model. The `intent.yml`/`inventory.yml`/`composition.yml` schemas, the `GhostFingerprintDocument`, the facet→node load-time projection, and the dormant facet slice/grounding are deleted; the loader folds `nodes/*.md` + `surfaces.yml` directly into the graph. `ghost lint` and `ghost verify` are replaced by one `ghost validate` verb (artifact shape pass + node-graph pass: links resolve, one root, acyclic); `ghost emit` is removed. `ghost scan` now reports node/surface contribution instead of facet contribution. Legacy facet packages no longer load directly — `ghost validate`/load fail with guidance to run `ghost migrate`. Structured exemplar-path and evidence verification is dropped (evidence lives in node prose, per the prose-node model).
+Remove the facet model — the graph is now the only fingerprint model. The `intent.yml`/`inventory.yml`/`composition.yml` schemas, the `GhostFingerprintDocument`, the facet→node load-time projection, and the dormant facet slice/grounding are deleted; the loader folds the package's directory tree of prose nodes directly into the graph. `ghost lint` and `ghost verify` are replaced by one `ghost validate` verb (artifact shape pass + node-graph pass: links resolve, one root, acyclic); `ghost emit` is removed. `ghost scan` now reports node/surface contribution instead of facet contribution. Legacy facet packages no longer load directly — `ghost validate`/load fail with guidance to run `ghost migrate`. Structured exemplar-path and evidence verification is dropped (evidence lives in node prose, per the prose-node model).
diff --git a/.changeset/migrate-command.md b/.changeset/migrate-command.md
index a0c7180d..79f18982 100644
--- a/.changeset/migrate-command.md
+++ b/.changeset/migrate-command.md
@@ -2,6 +2,7 @@
"@anarchitecture/ghost": minor
---
-Add `ghost migrate`: transform a legacy `.ghost/` package onto the surface model
-— derive `surfaces.yml` from old `topology.scopes`, place single-scope nodes via
-`surface:`, and report (never guess) any node it cannot place unambiguously.
+Add `ghost migrate`: transform a legacy `.ghost/` package onto the directory-tree
+node model — derive surface directories from old `topology.scopes`, place
+single-scope nodes inside them, and report (never guess) any node it cannot place
+unambiguously.
diff --git a/.changeset/node-authoring.md b/.changeset/node-authoring.md
index d7fa23e9..31f0d4d0 100644
--- a/.changeset/node-authoring.md
+++ b/.changeset/node-authoring.md
@@ -2,4 +2,4 @@
"@anarchitecture/ghost": minor
---
-`ghost init` now scaffolds a node package (`manifest.yml` + `surfaces.yml` spine + a seed `nodes/*.md`) via a template registry (`--template `, `default` for now) instead of emitting `intent.yml`/`inventory.yml`/`composition.yml`; the `--reference` flag is removed. `ghost migrate` now performs a one-way conversion of legacy/facet packages into `surfaces.yml` + `nodes/*.md` (the facet→node projection becomes the writer) and removes the old facet files. The authoring skill (`capture.md`, `SKILL.md`) teaches node authoring with intent/inventory/composition as authoring lenses rather than facet files.
+`ghost init` now scaffolds a node package (`manifest.yml` + a core `index.md` node) via a template registry (`--template `, `default` for now) instead of emitting `intent.yml`/`inventory.yml`/`composition.yml`; the `--reference` flag is removed. `ghost migrate` now performs a one-way conversion of legacy/facet packages into a directory tree of nodes (the facet→node projection becomes the writer) and removes the old facet files. The authoring skill (`capture.md`, `SKILL.md`) teaches node authoring with intent/inventory/composition as authoring lenses rather than facet files.
diff --git a/.changeset/remove-compare-drift-fleet.md b/.changeset/remove-compare-drift-fleet.md
index d1b61dad..64c47989 100644
--- a/.changeset/remove-compare-drift-fleet.md
+++ b/.changeset/remove-compare-drift-fleet.md
@@ -2,4 +2,4 @@
"@anarchitecture/ghost": minor
---
-Remove `compare`, `drift`, `ack`, `track`, and `diverge` commands and the direct `fingerprint.md` machinery (parser, writer, semantic diff, decisions/dimensions, embeddings, perceptual prior). These rested on a quantified visual-design-system model (fixed dimensions + decision embeddings) that the context-graph reframe abandoned; the concepts are parked for a graph-native rethink (see docs/ideas/compare-drift-fleet-rethink.md). The `./compare` and `./drift` package subpaths and the root `compare`/`drift` exports are removed. `ghost lint` now validates `.ghost/` packages and node/surface/check artifacts only (direct `fingerprint.md` is no longer linted); a `nodes/*.md` file lints as a `ghost.node/v1` node.
+Remove `compare`, `drift`, `ack`, `track`, and `diverge` commands and the direct `fingerprint.md` machinery (parser, writer, semantic diff, decisions/dimensions, embeddings, perceptual prior). These rested on a quantified visual-design-system model (fixed dimensions + decision embeddings) that the context-graph reframe abandoned; the concepts are parked for a graph-native rethink (see docs/ideas/compare-drift-fleet-rethink.md). The `./compare` and `./drift` package subpaths and the root `compare`/`drift` exports are removed. `ghost lint` now validates `.ghost/` packages and node/surface/check artifacts only (direct `fingerprint.md` is no longer linted); a `*.md` node file lints as a `ghost.node/v1` node.
diff --git a/.changeset/surface-coordinate-space.md b/.changeset/surface-coordinate-space.md
index efbd27a1..582b5fc8 100644
--- a/.changeset/surface-coordinate-space.md
+++ b/.changeset/surface-coordinate-space.md
@@ -2,7 +2,7 @@
"@anarchitecture/ghost": minor
---
-Replace topology/applies_to/surface_type/scope coordinates with a surfaces.yml
-coordinate space and a single `surface:` placement per node. Remove the
+Replace topology/applies_to/surface_type/scope coordinates with a surface
+coordinate space and a single surface placement per node. Remove the
`ghost.map/v1` (`map.md`) coordinate and routing system; checks now route by
`applies_to.paths`.
diff --git a/CLAUDE.md b/CLAUDE.md
index 2e09ca34..57ff0084 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -35,33 +35,39 @@ another host agent reads, decides, and writes. Ghost is the deterministic
calculator the agent reaches for: schema and graph validation, repo-signal
helpers, context composition, check routing, and advisory review packets.
-The canonical root `.ghost/` package is a flat folder:
+The canonical root `.ghost/` package is a directory tree of prose nodes:
```text
-manifest.yml # schema + id
-surfaces.yml # the spine: surfaces and their parent (core is implicit)
-nodes/*.md # prose nodes — the design expression
-checks/*.md # optional ghost.check/v1 checks
+manifest.yml # schema + id
+index.md # the core node — true everywhere (optional)
+/index.md # a surface's own prose (the directory is the surface)
+/.md # a prose node placed in that surface
+checks/*.md # optional ghost.check/v1 checks
```
-The fingerprint is a **graph of nodes**. A node is one markdown file:
-frontmatter handles (`id`, `description`, `under`, `relates`, `incarnation`)
-plus a prose body. The body is written through three authoring lenses — they
-guide what to capture, they are not fields or node types:
+The **directory tree is the graph**. A node is a markdown file: descriptive
+frontmatter (`description`, `relates`, `incarnation`) plus a prose body. A
+node's identity is its path (`marketing/email.md` → `marketing/email`) and its
+parent is its containing directory — a surface is just a directory, and a
+directory's own prose lives in its `index.md`. The package-root `index.md` is
+the implicit `core` node. The body is written through three authoring lenses
+(they guide what to capture, they are not fields):
- **intent** — the why and the stance.
- **inventory** — the materials, and pointers to code the agent can inspect.
- **composition** — the patterns that make the surface feel intentional.
-`under` cascades a node downward (`core` is the implicit root and reaches every
-surface). `relates` links nodes laterally. `description` is the retrieval
-payload. `checks/*.md` validate output, routed by surface; they are not
-generation input. Surfaces are declared in `surfaces.yml`, never inferred from
-filenames. Ordinary Git review is the approval boundary for fingerprint edits.
+`description` is the retrieval payload; `relates` links nodes laterally;
+`incarnation` tags a medium-bound expression. Reserved at the package root:
+`manifest.yml` and the `checks/` subtree; every other `*.md` is a node. Moving a
+node is a rename. `checks/*.md` validate output, routed by surface; they are not
+generation input. Ordinary Git review is the approval boundary for fingerprint
+edits.
A package may `extend` another by identity (the shared-brand pattern): the
manifest's `extends` maps a package id to where it lives, and nodes reference
-inherited context by identity (`under: brand:core`), never by path.
+inherited context by identity (`relates: [{ to: brand:core/trust }]`), never by
+path.
## Packages
@@ -79,7 +85,7 @@ Core workflow:
| Command | Description |
| --- | --- |
-| `ghost init` | Scaffold `.ghost/` — manifest, surfaces spine, and a seed node. |
+| `ghost init` | Scaffold `.ghost/` with a manifest and a core `index.md` node. |
| `ghost scan` | Report node and surface contribution. |
| `ghost validate` | Validate the package: artifact shape and the node graph (links resolve, one root, acyclic). |
| `ghost gather` | List nodes by id + description, or compose a surface's context slice (own + inherited + edges). |
@@ -137,10 +143,10 @@ Use `patch` for fixes and docs, `minor` for new commands/flags/exports, and
- Keep publishable runtime code self-contained in `packages/ghost`; no
`workspace:*` runtime dependencies in the packed public artifact.
-- The canonical on-disk form is a flat `.ghost/` package: `manifest.yml`,
- `surfaces.yml`, `nodes/*.md`, and optional `checks/*.md`.
-- The graph is the only model. Surfaces are the only locality; they are
- declared in `surfaces.yml`, never inferred from paths or filenames.
+- The canonical on-disk form is a `.ghost/` directory tree: `manifest.yml` plus
+ prose nodes (`index.md` and `/.md`) and optional `checks/*.md`.
+ The directory layout is the graph — ids and parents come from paths, never a
+ spine file.
- Skill recipes live in `packages/ghost/src/skill-bundle/references/`; install
them with `ghost skill install`.
- The CLI manifest at `apps/docs/src/generated/cli-manifest.json` is generated
diff --git a/README.md b/README.md
index 7aef4327..a0d2ce5d 100644
--- a/README.md
+++ b/README.md
@@ -17,24 +17,45 @@ writes, and decides.
```text
.ghost/
- manifest.yml # schema + id
- surfaces.yml # the spine: surfaces and their parent (core is implicit)
- nodes/*.md # prose nodes — the design expression
- checks/*.md # optional rules an agent evaluates
+ manifest.yml # ghost.fingerprint-package/v1 anchor: schema + id
+ index.md # the core node — true everywhere (optional)
+ /index.md # a surface's own prose (the directory is the surface)
+ /.md # a prose node placed in that surface
+ checks/*.md # optional ghost.check/v1 checks
```
-The fingerprint is a **graph of nodes**. A node is one markdown file:
-frontmatter handles (`id`, `description`, `under`, `relates`, `incarnation`)
-plus a prose body. You write that body through three lenses — they guide what to
-capture, they are not fields:
+The fingerprint is a graph of **nodes**, and the **directory tree is the graph**.
+A node is a markdown file: descriptive frontmatter (`description`, `relates`,
+`incarnation`) plus a prose body. A node's identity is its path
+(`marketing/email.md` → `marketing/email`) and its parent is its containing
+directory — a surface is just a directory, and a directory's own prose lives in
+its `index.md`. The package-root `index.md` is the implicit `core` node, true
+everywhere.
-- **intent** — the why and the stance.
-- **inventory** — the materials, and pointers to the code an agent can inspect.
-- **composition** — the patterns that make the surface feel like one product.
+The body is written through three authoring lenses — they guide what to capture,
+they are not fields:
-`under` cascades a node downward (`core` reaches every surface). `relates` links
-nodes laterally. `description` is the retrieval payload — how an agent finds the
-right node for a task. Checks validate output; they are never generation input.
+- **intent** — what the surface is trying to do and for whom.
+- **inventory** — the materials, and pointers to code the agent can inspect.
+- **composition** — the patterns that make the surface feel intentional.
+
+`description` is the retrieval payload; `relates` links nodes laterally;
+`incarnation` tags a medium-bound expression (essence is untagged). Reserved at
+the package root: `manifest.yml` and the `checks/` subtree; every other `*.md`
+is a node. `ghost signals` answers what exists; the curated node graph answers
+what the surface is trying to preserve.
+
+## Project Status: Beta
+
+> [!WARNING]
+> Ghost is pre-1.0 and under active development. The CLI, fingerprint schema,
+> on-disk `.ghost/` package shape, and public JavaScript exports may
+> change in breaking ways before a stable 1.0 release.
+>
+> Breaking changes may ship in minor versions while Ghost is pre-1.0. Patch
+> versions are reserved for fixes that should not require migration. If you adopt
+> Ghost today, expect some churn, pin the version you depend on, and review
+> release notes before upgrading.
## Install
@@ -46,20 +67,19 @@ npx ghost --help
## Quick Start
```bash
-ghost init # scaffold .ghost/ — manifest, surfaces spine, one seed node
+ghost init # scaffold .ghost/ — manifest + a core index.md node
ghost validate # links resolve, one root, acyclic
ghost gather # list nodes; ghost gather composes a context slice
```
-A node looks like this:
+A node is a markdown file; its id is its path (`checkout/trust.md` →
+`checkout/trust`) and its parent is its directory:
```markdown
---
-id: checkout-trust
description: Trust at the payment moment.
-under: checkout
relates:
- - to: core-trust
+ - to: core/trust
as: reinforces
---
@@ -104,7 +124,7 @@ of truth; ordinary Git review is the approval boundary for fingerprint edits.
| Command | Description |
| --- | --- |
-| `ghost init` | Scaffold `.ghost/` — manifest, surfaces spine, and a seed node. |
+| `ghost init` | Scaffold `.ghost/` — a manifest and a core `index.md` node. |
| `ghost scan` | Report node and surface contribution. |
| `ghost validate` | Validate the package: artifact shape and the node graph. |
| `ghost gather` | List nodes, or compose a surface's context slice. |
diff --git a/apps/docs/src/app/page.tsx b/apps/docs/src/app/page.tsx
index 0326b569..2261501a 100644
--- a/apps/docs/src/app/page.tsx
+++ b/apps/docs/src/app/page.tsx
@@ -52,8 +52,13 @@ export default function Home() {
.ghost/ is the portable fingerprint package
- surfaces.yml is the spine; nodes/*.md{" "}
- are the design expression
+ the directory tree is the graph: a node's id is
+ its file path, and its parent is its containing directory
+
+
+ a surface is just a directory; its own prose lives in that
+ directory's index.md, and the root{" "}
+ index.md is the implicit core node
each node is written through intent,{" "}
@@ -67,8 +72,9 @@ export default function Home() {
ordinary Git review is the approval boundary for edits
- A node inherits everything it sits under. The brand
- soul lives at core and reaches every surface;
+ A node inherits everything in the directories above it. The brand
+ soul lives in the root index.md (the{" "}
+ core node) and reaches every surface;
surface-specific nodes refine it; relates links them
laterally. Asking for context becomes a graph traversal:{" "}
ghost gather <surface> composes the slice that
diff --git a/apps/docs/src/content/docs/cli-reference.mdx b/apps/docs/src/content/docs/cli-reference.mdx
index cb236d95..e19c6b6d 100644
--- a/apps/docs/src/content/docs/cli-reference.mdx
+++ b/apps/docs/src/content/docs/cli-reference.mdx
@@ -17,14 +17,15 @@ and emit advisory review packets. Your agent does the interpretation.
complete command index, and `ghost --help` shows flags for one
command.
-The canonical fingerprint is a flat `.ghost/` package:
+The canonical fingerprint is a `.ghost/` directory tree of prose nodes:
```text
.ghost/
- manifest.yml # schema + id
- surfaces.yml # the spine: surfaces and their parent (core is implicit)
- nodes/*.md # prose nodes — the design expression
- checks/*.md # optional rules an agent evaluates
+ manifest.yml # schema + id
+ index.md # the core node — true everywhere (optional)
+ /index.md # a surface's own prose (the directory is the surface)
+ /.md # a prose node placed in that surface
+ checks/*.md # optional ghost.check/v1 checks
```
The command tables below are generated from the CLI source. Run
@@ -36,11 +37,11 @@ The command tables below are generated from the CLI source. Run
### Initialize — `init`
-Scaffold a `.ghost/` package: a manifest, an empty surfaces spine (the `core`
-root needs no declaration), and one seed node placed at `core`. Use
-`--template ` to pick a starter, `--package ` for an exact directory,
-or set `GHOST_PACKAGE_DIR` when a host wrapper stores Ghost files outside the
-default `.ghost`.
+Scaffold a `.ghost/` package: a manifest and a core `index.md` node. Add
+surfaces by adding directories (`checkout/index.md` is the `checkout` surface).
+Use `--template ` to pick a starter, `--package ` for an exact
+directory, or set `GHOST_PACKAGE_DIR` when a host wrapper stores Ghost files
+outside the default `.ghost`.
@@ -81,15 +82,15 @@ ghost signals .
### Validation — `validate`
-Validate the package: artifact shape plus the node graph — every `under` and
-`relates` link resolves, there is exactly one root, and the graph is acyclic.
-Defaults to `.ghost`; pass a file to validate a single artifact.
+Validate the package: artifact shape plus the node graph — every `relates` link
+resolves, there is exactly one root, and the graph is acyclic. Defaults to
+`.ghost`; pass a file to validate a single node.
```bash
ghost validate
-ghost validate .ghost/nodes/checkout-trust.md
+ghost validate .ghost/checkout/trust.md
ghost validate --format json
```
@@ -101,8 +102,9 @@ ghost validate --format json
With no argument, list every node by id and description so an agent can match a
task to one. With a surface, compose its context slice: the surface's own nodes,
-the ancestors it inherits via `under`, and one-hop `relates` edges. Use `--as`
-to filter to a single incarnation; untagged essence nodes always pass.
+the ancestors it inherits from its parent directories, and one-hop `relates`
+edges. Use `--as` to filter to a single incarnation; untagged essence nodes
+always pass.
diff --git a/apps/docs/src/content/docs/fingerprint-authoring.mdx b/apps/docs/src/content/docs/fingerprint-authoring.mdx
index d12a1c21..72fa8d94 100644
--- a/apps/docs/src/content/docs/fingerprint-authoring.mdx
+++ b/apps/docs/src/content/docs/fingerprint-authoring.mdx
@@ -1,6 +1,6 @@
---
title: Fingerprint Authoring
-description: Co-author a Ghost fingerprint as a graph of prose nodes — human intent, repo evidence, agent synthesis, and Git review.
+description: Co-author Ghost fingerprints with human intent, repo evidence, agent synthesis, and Git review.
kicker: Docs
section: guide
order: 20
@@ -10,65 +10,32 @@ slug: fingerprint-authoring
A Ghost fingerprint is not a scan dump. It is durable product-surface
-composition that a human and agent shape together, stored as a graph of prose
-**nodes**.
+composition that a human and agent shape together.
-The human names the intent: what the surface should feel like, who it serves,
-which situations matter, and what should not drift. Repo scans provide evidence:
-components, routes, docs, stories, copy, tokens, and library references. The
-agent synthesizes drafts. Ordinary Git review is where node edits become
-canonical.
-
-
-
-
-
-Each node is one markdown file in `nodes/`. Frontmatter carries the machine
-handles; the body carries the design expression, written through the intent /
-inventory / composition lenses.
-
-```markdown
----
-id: checkout-trust
-description: Trust at the payment moment.
-under: checkout
-relates:
- - to: core-trust
- as: reinforces
----
-
-Near the moment of payment, reduce felt risk. Proximity of reassurance to the
-action beats completeness…
-```
-
-| Handle | Role |
-| --- | --- |
-| `id` | Unique, stable identifier. How the node is referenced. |
-| `description` | The retrieval payload — a one-line "what this is / when to gather it." Write one on any node worth anchoring a task at. |
-| `under` | Places the node so it is inherited downward. `core` is the implicit root and reaches every surface. |
-| `relates` | Lateral links carrying rationale (`reinforces`, `contrasts`, `variant`). |
-| `incarnation` | Tags a medium-bound expression (`email`, `voice`, …). Essence is untagged. |
-
-Free-form keys (`audience`, `stage`, …) are allowed and pass through untouched.
-Surfaces themselves are declared in `surfaces.yml`, never inferred from paths.
+The human names the intent: what the product surface should feel like, who it
+serves, which situations matter, and what should not drift. Repo scans provide
+evidence: components, routes, docs, stories, copy, screenshots, tokens,
+examples, and UI library references. The agent synthesizes drafts, but ordinary
+Git review is where fingerprint edits become canonical.
-Classify the authoring scenario first. It determines how much weight to give
-human intent, existing code, and library evidence.
+Start by classifying the authoring scenario. The scenario determines how much
+weight to give human intent, existing code, and library evidence.
| Scenario | Authoring posture |
| --- | --- |
| Net new repo | Human-led. Capture intent, audience, posture, and early anti-goals before inventory grows. |
| Net new repo + UI library | Human-led with library evidence. Explain how this product uses the library. |
| Existing repo | Human + scan. Find repeated patterns and exemplars, then ask which ones are canonical. |
-| Existing repo, mixed quality | Curated scan. Separate durable composition from legacy debt and accidental repetition. |
-| Design system or UI library | Grammar-led. Describe primitives, tokens, behavior, accessibility, and composition constraints. |
-| Rebrand, redesign, migration | Human-led transition. Capture current, target, and migration cautions. |
+| Existing repo with mixed quality | Curated scan. Separate durable surface composition from legacy debt and accidental repetition. |
+| Design system or UI library | Grammar-led. Describe primitives, tokens, component behavior, accessibility, and composition constraints. |
+| Rebrand, redesign, or migration | Human-led transition. Capture current, target, and migration cautions. |
| Prototype becoming product | Ratification-led. Preserve only the emergent patterns humans want to keep. |
-| Fork, white label, tenant variant | Shared base + local divergence. Keep common composition at `core`, scope differences to surface nodes. |
+| Fork, white label, or tenant variant | Shared base + local divergence. Keep common surface composition broad and local differences scoped. |
+| Monorepo or nested surfaces | Stack-aware. Use root guidance for broad composition and nested packages for surfaces assessed differently. |
@@ -76,86 +43,134 @@ human intent, existing code, and library evidence.
Ghost supports two agent authoring modes:
-- **Default** — interview first, scan as needed, draft nodes, then curate.
-- **Auto-draft** — scan first, draft a small starter fingerprint, then curate
+- **Default** - interview first, scan as needed, draft node prose, then
+ curate.
+- **Auto-draft** - scan first, draft a small starter fingerprint, then curate
the claims with a human.
-Auto-draft is a skill workflow, not a CLI command. Ask for it in plain English:
+Auto-draft is a skill workflow, not a Ghost CLI command. Ask for it in plain
+English:
```text
Set up the Ghost fingerprint for this repo with auto-draft.
```
-1. **Interview** — ask what the product should feel like, who it serves, which
+1. **Interview** - ask what the product should feel like, who it serves, which
surfaces show it at its best, and which existing patterns are accidental or
- legacy.
-2. **Scan** — inspect routes, components, stories, tests, docs, copy, tokens,
- and library references. Use `ghost signals` for raw observations.
-3. **Draft** — write the smallest useful nodes. Place durable, cross-surface
- guidance at `core`; place surface-specific obligations `under` that surface.
-4. **Curate** — have the human keep, soften, reject, or scope each claim before
- it is treated as durable context.
-5. **Validate** — run `ghost validate` and use Git review as the approval
+ legacy. In auto-draft mode, use this step after the starter draft to curate
+ claims.
+2. **Scan** - inspect routes, components, stories, tests, docs, screenshots,
+ copy, tokens, assets, and UI library references.
+3. **Draft** - write the smallest useful node prose, reading each node through
+ the intent, inventory, and composition lenses.
+4. **Curate** - have the human keep, soften, reject, scope, or record important
+ claims before treating them as durable surface context.
+5. **Validate** - run Ghost validation and use Git review as the approval
boundary.
```bash
+ghost scan --format json
ghost signals .
-ghost scan
-ghost validate
+ghost lint .ghost
+ghost verify .ghost --root .
```
-Raw repo signals are source evidence only. Signal frequency may seed a draft,
-but it does not decide what the surface should do.
+Raw repo signals are source evidence only. They can support curated inventory,
+but they do not establish surface-composition guidance by themselves. Signal
+frequency may seed a draft, but it does not decide what the surface should do.
-
+
-The graph is the model. Decide where each claim lives by how far it should
-reach.
+The fingerprint is a directory tree of prose nodes. The tree _is_ the graph: a
+node's identity is its file path with `.md` dropped (`marketing/email.md` is the
+node `marketing/email`), and its parent is the directory that contains it. A
+surface is just a directory — its own prose lives in that directory's
+`index.md` (`checkout/index.md` is the `checkout` surface), and the
+package-root `index.md` is the implicit `core` node that is true everywhere.
-- Put the brand soul — voice, trust posture, broad product intent — at `core`.
- It cascades to every surface.
-- Put surface-specific obligations `under` the surface that owns them
- (`under: checkout`).
-- Link nodes laterally with `relates` only when the relationship carries
- rationale a future agent needs.
+There is no spine file. A surface exists when its directory exists. Reserved at
+the package root are `manifest.yml` and the `checks/` subtree; every other
+`*.md` is a node. Moving a node to another directory is a rename — its id and
+parent change — and `ghost validate` reports any `relates` that no longer
+resolve.
-```bash
-ghost gather # list nodes by id + description
-ghost gather checkout # compose checkout's slice (own + inherited + edges)
-```
+Node frontmatter carries only descriptive properties:
-`ghost gather ` is the test: if the composed slice reads like coherent
-guidance for that surface, the placement is right.
+| Property | What it does |
+| --- | --- |
+| `description` | A short summary of the node. |
+| `relates` | Lateral links to other nodes by path id (`to: core/trust`); cross-package refs use `:`, e.g. `brand:core/trust`. |
+| `incarnation` | Tags a medium-bound expression (`email`, `billboard`, `voice`, `web`); untagged nodes are essence. |
+| _passthrough_ | Free-form keys are preserved for host tooling. |
-
+
-A useful node helps a future agent choose, restrain, and review — not just
-describe what exists. Write the body so generation decisions become explicit:
+A node's prose body is written — and read — through three lenses. They shape
+how you write, never frontmatter fields:
+
+| Lens | What belongs there |
+| --- | --- |
+| Intent | Audience, goals, anti-goals, situations, principles, and experience contracts. |
+| Inventory | Scopes, surface types, files, routes, libraries, assets, building blocks, exemplars, and source links. |
+| Composition | Repeatable rules, layouts, structures, flows, states, content patterns, behavior, and visual arrangements. |
+
+Deterministic `checks/` gates that can be evaluated from a diff live alongside
+the tree in the `checks/` subtree.
+
+
+
+
+
+A useful fingerprint should help future agents choose, restrain, route, anchor,
+and review. It should not only describe what exists or collect every available
+style detail.
+
+Write node prose so generation decisions become explicit:
- Name what generated work should preserve.
-- Block the plausible defaults that would make the surface feel generic.
+- Block plausible defaults that would make the surface feel generic or wrong.
- Say which value wins when choices conflict.
-- Anchor the guidance in concrete material an agent can inspect.
+- Route guidance by task, surface type, state, or audience need.
+- Capture broad product intent.
+- Turn taste, trust, recovery, or disclosure into obligations.
+- Give repeatable layout, flow, state, content, behavior, or visual rules.
+- Anchor the guidance in concrete exemplars an agent can inspect.
Write less like a brand book and more like a decision engine.
-
+
+
+Nested fingerprints are opt-in. Create a local `.ghost/` only when a surface
+should be assessed differently from the root product fingerprint.
-Uncommitted or unmerged node edits are drafts. Checked-in nodes are canonical.
+Use a nested package when a surface has distinct users, information density,
+trust or recovery posture, interaction rhythm, component grammar, UI library
+usage, or review criteria for the same UI decision.
-Add `ghost.check/v1` markdown checks in `checks/*.md` sparingly, and only when a
-rule can be enforced from a diff. Checks are routed by surface and validate
-output — they are never generation input.
+Keep broad product-family guidance at the root. Put local obligations in the
+nearest package that owns the surface.
```bash
-ghost checks --surface checkout
-ghost review --surface checkout --base main
+ghost init --scope apps/checkout
+ghost stack apps/checkout
+ghost lint --all
+ghost verify --all
```
+
+
+
+Uncommitted or unmerged fingerprint edits are drafts. Checked-in
+Ghost package node prose is canonical.
+
+Add deterministic checks sparingly, and only when a rule can be enforced
+deterministically.
+
+
diff --git a/apps/docs/src/content/docs/getting-started.mdx b/apps/docs/src/content/docs/getting-started.mdx
index 8be29b1f..3d5e92ad 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, scaffold a product-surface fingerprint as a graph of prose nodes, and use it to brief, validate, and review your agent's work.
+description: Install Ghost, author a product-surface composition fingerprint, and use it to generate, validate, compare, and govern product surfaces.
kicker: Docs
section: guide
order: 10
@@ -9,32 +9,34 @@ slug: getting-started
-Ghost captures the composition of a product surface — the intent behind it, the
-materials it draws from, and the patterns that make it feel intentional — and
-checks it into the repo. The public package is `@anarchitecture/ghost`, and it
-installs one CLI: `ghost`.
+Ghost captures the composition of a product surface: the intent behind it, the
+materials it draws from, and the patterns that make it feel intentional. The
+public package is `@anarchitecture/ghost`, and it installs one CLI: `ghost`.
-A fingerprint is a small folder of prose:
+The canonical portable fingerprint is a directory tree of prose nodes:
```text
.ghost/
- manifest.yml # schema + id
- surfaces.yml # the spine: surfaces and their parent (core is implicit)
- nodes/*.md # prose nodes — the design expression
- checks/*.md # optional rules an agent evaluates
+ manifest.yml # schema + package id
+ index.md # the core node — true everywhere (optional)
+ checkout/index.md # the `checkout` surface's own prose
+ checkout/review.md # a node placed in the checkout surface
+ checks/*.md # optional ghost.check/v1 deterministic checks
```
-The fingerprint is a **graph of nodes**. A node is one markdown file:
-frontmatter handles plus a prose body. You write that body through three lenses
-— they guide what to capture, they are not fields:
+The directory tree _is_ the graph. A node's identity is its file path with
+`.md` dropped (`checkout/review.md` is the node `checkout/review`), and its
+parent is the directory that contains it. A surface is simply a directory: its
+own prose lives in that directory's `index.md`, and the package-root `index.md`
+is the implicit `core` node that is true everywhere. There is no spine file to
+maintain — a surface exists when its directory exists.
-- **intent** — the why and the stance.
-- **inventory** — the materials, and pointers to the code an agent can inspect.
-- **composition** — the patterns that make the surface feel like one product.
+Every prose node is read through three lenses — intent, inventory, and
+composition — and deterministic `checks/` validate the result afterward; they
+are not generation input.
-`under` cascades a node downward (`core` is the implicit root and reaches every
-surface). `relates` links nodes laterally. Checks validate output afterward;
-they are never generation input.
+One contract per package: a repo's `.ghost/` is the whole fingerprint, and
+surfaces (directories) are the only locality.
@@ -42,20 +44,20 @@ they are never generation input.
-Ghost is pre-1.0 and under active development. The CLI, node schema, on-disk
-`.ghost/` shape, and public JavaScript exports may change in breaking ways
-before a stable 1.0 release.
+Ghost is pre-1.0 and under active development. The CLI, fingerprint schema,
+on-disk `.ghost/` package shape, and public JavaScript exports may
+change in breaking ways before a stable 1.0 release.
Breaking changes may ship in minor versions while Ghost is pre-1.0. Patch
versions are reserved for fixes that should not require migration. If you adopt
-Ghost today, pin the version you depend on and review release notes before
-upgrading.
+Ghost today, expect some churn, pin the version you depend on, and review
+release notes before upgrading.
-
+
```bash
npm install -D @anarchitecture/ghost
@@ -64,16 +66,15 @@ npx ghost --help --all
npx ghost skill install
```
-`ghost --help` shows the core workflow. `ghost --help --all` shows the complete
-command index, and `ghost --help` shows flags for one command.
+`ghost --help` shows the core new-adopter workflow. Use `ghost --help --all`
+when you want the complete command index.
-Ghost is **bring-your-own-agent**. Once the skill is installed, ask your agent
-in plain English:
+Once the skill is installed, ask your agent in plain English:
```text
Set up the Ghost fingerprint for this repo.
Brief this work from the Ghost fingerprint.
-Review this change against the Ghost fingerprint.
+Review this PR against the Ghost fingerprint.
```
The skill tells the agent what to read, what to write, and which CLI checks to
@@ -81,108 +82,99 @@ run.
-
+
-`ghost init` writes a minimal package: a manifest, an empty surfaces spine (the
-`core` root needs no declaration), and one seed node placed at `core`.
+The CLI handles the deterministic package work. Your agent handles the
+composition work: interviewing, reading repo evidence, drafting node prose, and
+asking you to curate the claims. `ghost init` scaffolds `manifest.yml` and a
+core `index.md` node.
```bash
ghost init
-ghost validate
-ghost scan
+ghost scan --format json
+ghost signals .
+ghost lint .ghost
+ghost verify .ghost --root .
```
-`ghost validate` confirms the package is well-formed: artifact shape plus the
-node graph (links resolve, exactly one root, acyclic). `ghost scan` reports what
-the package contributes.
+Each node's prose records durable surface-composition guidance through three
+lenses:
-
+1. **Intent** - what must remain true: what product this is, who it
+ serves, which situations matter, and which principles or contracts apply.
+2. **Inventory** - the materials it draws from: topology, building blocks,
+ files, routes, assets, libraries, exemplars, and source links agents may
+ inspect or use.
+3. **Composition** - the patterns that make it intentional: rules, layouts,
+ structures, flows, states, content, behavior, and visual arrangements.
-
+These lenses are how the prose body is written, never frontmatter fields. Node
+frontmatter carries only descriptive properties — `description`, `relates`,
+`incarnation`, plus free-form passthrough keys. Raw repo signals are optional
+authoring evidence. Curate durable intent, inventory, and composition into the
+node prose, then use normal Git review for approval. For a fuller human-agent
+workflow, read [Fingerprint Authoring](/docs/fingerprint-authoring).
-A node is one markdown file in `nodes/`. The frontmatter is machine handles; the
-body is the design expression.
+
-```markdown
----
-id: checkout-trust
-description: Trust at the payment moment.
-under: checkout
-relates:
- - to: core-trust
- as: reinforces
-incarnation: web
----
+
-Near the moment of payment, reduce felt risk. Proximity of reassurance to the
-action beats completeness. Never introduce a new visual system here.
-```
+Before generating or revising UI, gather Relay JSON for the target path:
-- **`id`** — unique and stable; how the node is referenced.
-- **`description`** — the retrieval payload: a one-line "what this is and when to
- gather it," exactly like a tool's name and description. `ghost gather` with no
- argument lists nodes by id and description so an agent can match a task to one.
-- **`under`** — places the node so it is inherited downward. `core`-placed nodes
- reach every surface.
-- **`relates`** — links nodes laterally (`reinforces`, `contrasts`, `variant`).
-- **`incarnation`** — tags a medium-bound expression (`email`, `voice`, …).
- Leave essence untagged. Free-form keys (`audience`, `stage`, …) pass through.
-
-Surfaces are declared in `surfaces.yml`, never inferred from filenames:
-
-```yaml
-schema: ghost.surfaces/v1
-surfaces:
- - id: checkout
- parent: core
+```bash
+ghost relay gather apps/checkout/review/page.tsx --format json
```
-The CLI handles the deterministic work — scaffolding, validation, context
-composition. Your agent handles the composition work: interviewing, reading
-repo evidence, drafting nodes, and asking you to curate the claims. Use
-`ghost signals` for raw repo observations while authoring. For the full
-human-agent workflow, read [Fingerprint Authoring](/docs/fingerprint-authoring).
+`ghost.relay.gather/v2` is the agent contract. Agents should read `context`,
+`selected_context`, `targetPaths`, `source`, `stackDirs`, gaps, and trace fields
+from JSON. Plain `ghost relay gather ` remains a compact human preview.
+For prompt-shaped work where there is no clear path, host agents can create a
+`ghost.relay-request/v1` and run
+`ghost relay gather --request-stdin --format json`.
+Relay config controls the runtime. Omitted `base` uses the resolved fingerprint
+stack; `base.kind: none` lets frameworks provide declared request context
+without a `.ghost` package:
-Drafted fingerprint edits are ordinary file changes until Git review accepts
-them. Checked-in nodes are the Ghost source of truth.
+```bash
+GHOST_RELAY_CONFIG=.agents/ghost/relay.yml ghost relay gather --request-stdin --format json
+ghost relay gather stacks/portal.renewal-reminder.email.yml --config .agents/ghost/relay.yml --format json
+```
+
+The package remains the approved product-surface context; review and check
+commands apply it after implementation.
-
+
-Before generating or revising UI, compose the context slice for the surface
-you're touching:
+After implementation, run Ghost against the same fingerprint:
```bash
-ghost gather # list nodes by id + description
-ghost gather checkout # compose checkout's slice
-ghost gather checkout --as email # filter to one incarnation
+ghost check --base main
+ghost review --base main
```
-`ghost gather ` traverses the graph: the surface's own nodes, the
-ancestors it inherits via `under`, and one-hop `relates` edges. The important
-shift is timing — Ghost gives agents surface-composition context **before** they
-build, not only after a review finds drift.
+`ghost check` applies active deterministic gates from the resolved fingerprint
+stack for each changed file. `ghost review` emits advisory context grounded in
+the same selected context as Relay, selected validation checks, and the diff.
-
+Wrappers should consume `ghost check --format json` and map Ghost severities
+outside Ghost. Ghost severities remain `critical`, `serious`, and `nit`.
-
+
-After implementation, route the relevant checks and emit an advisory packet
-against the diff. The agent names the surfaces the change touches.
+
```bash
-ghost checks --surface checkout
-ghost review --surface checkout --base main
+ghost compare market/.ghost dashboard/.ghost
+ghost stack apps/checkout/review/page.tsx
+ghost ack --stance aligned --reason "Initial baseline"
+ghost track new-tracked.fingerprint.md
+ghost diverge typography --reason "Editorial product uses a different type scale"
```
-`ghost checks` selects and grounds the markdown checks governing the named
-surfaces — the agent evaluates them. `ghost review` emits an advisory packet:
-touched surfaces, routed checks, and fingerprint grounding, with the diff
-embedded verbatim.
-
-Wrappers should consume `--format json` and map Ghost severities into their own
-review format. Ghost severities are `critical`, `serious`, and `nit`. Advisory
-review is never a CI gate on its own.
+Package comparison uses canonical `.ghost/` packages. `ack`,
+`track`, and `diverge` record stance for compatibility drift workflows that
+track direct fingerprint markdown references.
diff --git a/apps/docs/src/generated/cli-manifest.json b/apps/docs/src/generated/cli-manifest.json
index 3bc070d8..5c74ff8e 100644
--- a/apps/docs/src/generated/cli-manifest.json
+++ b/apps/docs/src/generated/cli-manifest.json
@@ -1,5 +1,5 @@
{
- "generatedAt": "2026-06-28T13:42:06.396Z",
+ "generatedAt": "2026-06-28T21:25:38.799Z",
"tools": [
{
"tool": "ghost",
@@ -191,7 +191,7 @@
"tool": "ghost",
"name": "migrate",
"rawName": "migrate [dir]",
- "description": "Migrate a legacy .ghost/ package onto the surface model (surfaces.yml + surface: placement).",
+ "description": "Migrate a legacy .ghost/ package onto the directory-tree node model.",
"group": "maintenance",
"defaultHelp": false,
"compactName": "migrate",
diff --git a/packages/ghost/README.md b/packages/ghost/README.md
index 3983b78a..315c964e 100644
--- a/packages/ghost/README.md
+++ b/packages/ghost/README.md
@@ -34,20 +34,23 @@ command index, and `ghost --help` shows flags for one command.
## The Shape
-A fingerprint is a small folder of prose — a **graph of nodes**:
+A fingerprint is a directory tree of prose — a **graph of nodes**:
```text
.ghost/
- manifest.yml # schema + id
- surfaces.yml # the spine: surfaces and their parent (core is implicit)
- nodes/*.md # prose nodes — the design expression
- checks/*.md # optional rules an agent evaluates
+ manifest.yml # schema + id
+ index.md # the core node — true everywhere (optional)
+ /index.md # a surface's own prose (the directory is the surface)
+ /.md # a prose node placed in that surface
+ checks/*.md # optional ghost.check/v1 checks
```
-A node is one markdown file: frontmatter handles (`id`, `description`, `under`,
-`relates`, `incarnation`) plus a prose body written through three lenses —
-**intent** (the why), **inventory** (the materials), and **composition** (the
-patterns). `under` cascades a node downward; `core` reaches every surface.
+The **directory tree is the graph**. A node is one markdown file: descriptive
+frontmatter (`description`, `relates`, `incarnation`) plus a prose body written
+through three lenses — **intent** (the why), **inventory** (the materials), and
+**composition** (the patterns). A node's id is its path and its parent is its
+directory; a surface is just a directory, and the package-root `index.md` is the
+implicit `core` node that reaches every surface.
## Use
diff --git a/packages/ghost/src/commands/migrate-command.ts b/packages/ghost/src/commands/migrate-command.ts
index d761cd0e..23f0673e 100644
--- a/packages/ghost/src/commands/migrate-command.ts
+++ b/packages/ghost/src/commands/migrate-command.ts
@@ -1,7 +1,7 @@
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import type { CAC } from "cac";
-import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
+import { parse as parseYaml } from "yaml";
import { resolveFingerprintPackage } from "../fingerprint.js";
import {
looksLegacy,
@@ -15,7 +15,7 @@ export function registerMigrateCommand(cli: CAC): void {
cli
.command(
"migrate [dir]",
- "Migrate a legacy .ghost/ package onto the surface model (surfaces.yml + surface: placement).",
+ "Migrate a legacy .ghost/ package onto the directory-tree node model.",
)
.option("--dry-run", "Print the migration plan and report; write nothing")
.option("--force", "Overwrite existing facet files with the migrated form")
@@ -50,7 +50,7 @@ export function registerMigrateCommand(cli: CAC): void {
`${JSON.stringify(reportJson(result), null, 2)}\n`,
);
} else {
- process.stdout.write(formatReport(result, paths.surfaces));
+ process.stdout.write(formatReport(result, paths.packageDir));
}
if (opts.dryRun) {
@@ -61,7 +61,6 @@ export function registerMigrateCommand(cli: CAC): void {
await writeMigrated(
{
packageDir: paths.packageDir,
- surfaces: paths.surfaces,
facetFiles: [paths.intent, paths.inventory, paths.composition],
},
result,
@@ -96,24 +95,22 @@ async function readYaml(
async function writeMigrated(
paths: {
packageDir: string;
- surfaces: string;
facetFiles: string[];
},
result: MigrationResult,
force: boolean,
): Promise {
- // One-way conversion to the node form: surfaces.yml (spine) + nodes/*.md.
- // Facet files are removed; Git history preserves the old form.
+ // One-way conversion to the directory-tree node form. Facet files are
+ // removed; Git history preserves the old form.
const nodeFiles = migratedNodeFiles(result);
- const writes: Array<[string, string]> = [
- [paths.surfaces, stringifyYaml(result.surfaces)],
- ...nodeFiles.map((file): [string, string] => [
+ const writes: Array<[string, string]> = nodeFiles.map(
+ (file): [string, string] => [
join(paths.packageDir, file.relativePath),
file.content,
- ]),
- ];
+ ],
+ );
- // Ensure nested dirs (nodes/) exist.
+ // Ensure nested surface directories exist.
const dirs = new Set(writes.map(([path]) => dirname(path)));
await Promise.all([...dirs].map((dir) => mkdir(dir, { recursive: true })));
@@ -147,18 +144,18 @@ function isExisting(err: unknown): boolean {
function reportJson(result: MigrationResult): Record {
return {
- surfaces: (result.surfaces.surfaces as unknown[]) ?? [],
+ surfaces: result.surfaceIds,
notes: result.notes,
};
}
-function formatReport(result: MigrationResult, surfacesPath: string): string {
- const surfaces = (result.surfaces.surfaces as Array<{ id: string }>) ?? [];
+function formatReport(result: MigrationResult, packageDir: string): string {
+ const surfaceIds = result.surfaceIds;
const lines: string[] = ["# Ghost Migration"];
lines.push(
"",
- `Derived ${surfaces.length} surface(s) → ${surfacesPath}`,
- ...surfaces.map((surface) => ` - \`${surface.id}\``),
+ `Derived ${surfaceIds.length} surface director(ies) under ${packageDir}/`,
+ ...surfaceIds.map((id) => ` - \`${id}/\``),
);
lines.push("", `## Review (${result.notes.length})`);
if (result.notes.length === 0) {
diff --git a/packages/ghost/src/ghost-core/graph/assemble.ts b/packages/ghost/src/ghost-core/graph/assemble.ts
index 20e2c2b0..dc42f916 100644
--- a/packages/ghost/src/ghost-core/graph/assemble.ts
+++ b/packages/ghost/src/ghost-core/graph/assemble.ts
@@ -1,16 +1,24 @@
import type { GhostNodeDocument } from "../node/types.js";
-import type { GhostSurfacesDocument } from "../surfaces/types.js";
import {
GHOST_GRAPH_ROOT_ID,
type GhostGraph,
type GhostGraphNode,
} from "./types.js";
+/**
+ * One local node located in the package directory tree: its computed path id,
+ * the id of its containing directory (absent ⇒ the node *is* the `core` root,
+ * i.e. a package-root `index.md`), and the parsed document.
+ */
+export interface PlacedNode {
+ id: string;
+ parent?: string;
+ doc: GhostNodeDocument;
+}
+
export interface AssembleGraphInput {
- /** Authored on-disk node files (parsed `ghost.node/v1` documents). */
- nodeFiles?: GhostNodeDocument[];
- /** The explicit surface tree, which seeds tree nodes even when empty. */
- surfaces?: GhostSurfacesDocument;
+ /** Local nodes located in the package's directory tree. */
+ placedNodes?: PlacedNode[];
/**
* Read-only nodes inherited from extended packages. Their ids are already
* qualified (`:`). Local nodes never override these and
@@ -22,10 +30,11 @@ export interface AssembleGraphInput {
/**
* Fold the package's sources into one in-memory prose-node graph.
*
- * Authored node files are unioned with the surface tree (`surfaces.yml`), which
- * seeds containment so a surface with no node still exists as a tree position,
- * plus any read-only nodes inherited from extended packages. The implicit
- * `core` root is never required to be declared.
+ * Local nodes are the package's directory tree: each node's id is its path and
+ * its parent is its containing directory. Intermediate directories that hold no
+ * `index.md` are still materialized as bare tree positions so children resolve.
+ * Inherited nodes from extended packages join as read-only context. The
+ * implicit `core` root is never required to be declared.
*/
export function assembleGraph(input: AssembleGraphInput): GhostGraph {
const nodes = new Map();
@@ -35,21 +44,24 @@ export function assembleGraph(input: AssembleGraphInput): GhostGraph {
nodes.set(node.id, node);
}
- for (const doc of input.nodeFiles ?? []) {
- const fm = doc.frontmatter;
- nodes.set(fm.id, {
- id: fm.id,
+ for (const placed of input.placedNodes ?? []) {
+ const fm = placed.doc.frontmatter;
+ // A node whose parent is absent is the package-root index — the core node.
+ const id = placed.parent === undefined ? GHOST_GRAPH_ROOT_ID : placed.id;
+ nodes.set(id, {
+ id,
...(fm.description !== undefined ? { description: fm.description } : {}),
- ...(fm.under !== undefined ? { under: fm.under } : {}),
+ ...(placed.parent !== undefined ? { parent: placed.parent } : {}),
relates: fm.relates ?? [],
...(fm.incarnation !== undefined ? { incarnation: fm.incarnation } : {}),
- body: doc.body,
+ body: placed.doc.body,
origin: "node-file",
});
}
- // Build the containment tree. Surfaces seed positions; node `under` edges and
- // surface `parent` edges both contribute. The root (`core`) has no parent.
+ // Build the containment tree from each node's parent (its directory). The
+ // root (`core`) has no parent. Intermediate directories with no index node
+ // are seeded as bare positions so the chain resolves to the root.
const parents = new Map();
const children = new Map();
@@ -64,25 +76,30 @@ export function assembleGraph(input: AssembleGraphInput): GhostGraph {
}
};
- // Surface tree edges (the authoritative spine in Phase 2).
- for (const surface of input.surfaces?.surfaces ?? []) {
- if (surface.id === GHOST_GRAPH_ROOT_ID) continue;
- link(surface.id, surface.parent ?? GHOST_GRAPH_ROOT_ID);
- }
-
- // Node containment: a node `under` X is a child of X. A placed node whose
- // `under` is itself a node id nests under that node; otherwise it attaches to
- // the named surface (or core).
for (const node of nodes.values()) {
if (node.id === GHOST_GRAPH_ROOT_ID) continue;
- if (node.under !== undefined) {
- link(node.id, node.under);
+ if (node.origin === "inherited") continue;
+ link(node.id, node.parent ?? GHOST_GRAPH_ROOT_ID);
+ // Seed any ancestor directories that have no index node of their own, so a
+ // deep node (a/b/c) still has a/b → a → core links even when a, a/b are
+ // empty directories.
+ let current = node.parent;
+ while (current !== undefined && current !== GHOST_GRAPH_ROOT_ID) {
+ const grandparent = parentIdOf(current);
+ link(current, grandparent ?? GHOST_GRAPH_ROOT_ID);
+ current = grandparent;
}
}
return { nodes, parents, children };
}
+/** The id of the directory containing `id`, or undefined when `id` is top-level. */
+function parentIdOf(id: string): string | undefined {
+ const slash = id.lastIndexOf("/");
+ return slash === -1 ? undefined : id.slice(0, slash);
+}
+
/** The ancestor chain for a node id, nearest parent first, ending at the root. */
export function ancestorChain(graph: GhostGraph, id: string): string[] {
const chain: string[] = [];
diff --git a/packages/ghost/src/ghost-core/graph/index.ts b/packages/ghost/src/ghost-core/graph/index.ts
index 426a83ce..053b3498 100644
--- a/packages/ghost/src/ghost-core/graph/index.ts
+++ b/packages/ghost/src/ghost-core/graph/index.ts
@@ -1,13 +1,14 @@
/**
* Public surface for the in-memory fingerprint graph — the only fingerprint
- * model. The graph is folded from authored node files + the surface tree, and
- * is what every consumer traverses (gather, checks, validate).
+ * model. The graph is folded from the package's directory tree of prose nodes,
+ * and is what every consumer traverses (gather, checks, validate).
*/
export {
type AssembleGraphInput,
ancestorChain,
assembleGraph,
+ type PlacedNode,
} from "./assemble.js";
export {
type GraphLintIssue,
diff --git a/packages/ghost/src/ghost-core/graph/lint.ts b/packages/ghost/src/ghost-core/graph/lint.ts
index 1b5df230..9664a02b 100644
--- a/packages/ghost/src/ghost-core/graph/lint.ts
+++ b/packages/ghost/src/ghost-core/graph/lint.ts
@@ -1,4 +1,4 @@
-import { GHOST_GRAPH_ROOT_ID, type GhostGraph } from "./types.js";
+import type { GhostGraph } from "./types.js";
export type GraphLintSeverity = "error" | "warning" | "info";
@@ -20,39 +20,23 @@ export interface GraphLintReport {
/**
* The graph pass of `validate`: the ghost-specific network is correct.
*
- * - every `under` parent resolves to a node or a declared surface tree position;
- * - every local `relates` target resolves (cross-package `pkg#id` refs are
- * skipped here — they are resolved in the cross-package phase);
- * - exactly one root (no `under`) — the implicit `core`;
- * - the containment graph is acyclic.
+ * Containment comes from the directory tree (a node's parent is its directory),
+ * so parent edges resolve by construction — there is no "unresolved parent" to
+ * check. What remains is the network correctness the layout cannot guarantee:
+ *
+ * - every local `relates` target resolves (cross-package `pkg:id` refs resolve
+ * against inherited nodes; an unknown one is reported);
+ * - the containment graph reaches the single implicit `core` root (it always
+ * does by construction; verified defensively);
+ * - the containment graph is acyclic (a directory tree is, defensively checked).
*
* Pure: operates on the assembled in-memory graph, no I/O.
*/
export function lintGraph(graph: GhostGraph): GraphLintReport {
const issues: GraphLintIssue[] = [];
const ids = new Set(graph.nodes.keys());
- // Valid containment targets: nodes, declared surface tree positions, and the
- // implicit root. Surfaces are tree positions (in parents/children), not nodes.
- const treePositions = new Set([
- GHOST_GRAPH_ROOT_ID,
- ...graph.parents.keys(),
- ...graph.children.keys(),
- ]);
for (const node of graph.nodes.values()) {
- // under must resolve to a known node or surface tree position
- if (
- node.under !== undefined &&
- !ids.has(node.under) &&
- !treePositions.has(node.under)
- ) {
- issues.push({
- severity: "error",
- rule: "unresolved-parent",
- message: `node '${node.id}' is under '${node.under}', which is not a known node or surface.`,
- node: node.id,
- });
- }
// relates targets must resolve. A `:` ref resolves to an
// inherited node (id-keyed the same way) — same lookup, no special case.
for (const relation of node.relates) {
@@ -67,25 +51,8 @@ export function lintGraph(graph: GhostGraph): GraphLintReport {
}
}
- // Exactly one root: the implicit core. Nodes with no `under` are roots.
- // Inherited (extended-package) nodes are read-only context, not part of this
- // package's tree — they are exempt from the single-root rule.
- const roots = [...graph.nodes.values()].filter(
- (node) =>
- node.under === undefined &&
- node.id !== GHOST_GRAPH_ROOT_ID &&
- node.origin !== "inherited",
- );
- for (const root of roots) {
- issues.push({
- severity: "error",
- rule: "multiple-roots",
- message: `node '${root.id}' has no 'under'; every node must descend from the implicit '${GHOST_GRAPH_ROOT_ID}' root (give it an 'under').`,
- node: root.id,
- });
- }
-
- // Cycle detection over containment.
+ // Cycle detection over containment (defensive — a directory tree cannot cycle,
+ // but inherited/seeded positions are checked for safety).
for (const node of graph.nodes.values()) {
const seen = new Set();
let cursor: string | undefined = node.id;
@@ -94,13 +61,13 @@ export function lintGraph(graph: GhostGraph): GraphLintReport {
issues.push({
severity: "error",
rule: "containment-cycle",
- message: `node '${node.id}' is part of an 'under' cycle.`,
+ message: `node '${node.id}' is part of a containment cycle.`,
node: node.id,
});
break;
}
seen.add(cursor);
- cursor = graph.nodes.get(cursor)?.under;
+ cursor = graph.parents.get(cursor);
}
}
diff --git a/packages/ghost/src/ghost-core/graph/menu.ts b/packages/ghost/src/ghost-core/graph/menu.ts
index bed23a64..efbe1f49 100644
--- a/packages/ghost/src/ghost-core/graph/menu.ts
+++ b/packages/ghost/src/ghost-core/graph/menu.ts
@@ -38,12 +38,12 @@ export function buildGraphMenu(graph: GhostGraph): GraphMenuEntry[] {
entries.push({
id: node.id,
...(node.description ? { description: node.description } : {}),
- parent: node.under ?? GHOST_GRAPH_ROOT_ID,
+ parent: node.parent ?? GHOST_GRAPH_ROOT_ID,
});
}
- // Tree positions declared only in the spine file (surfaces.yml) — no node of
- // their own yet — are still anchorable. Include them as bare entries.
+ // Intermediate directories with no index node of their own are still
+ // anchorable tree positions. Include them as bare entries.
for (const [id, parent] of graph.parents) {
if (seen.has(id)) continue;
seen.add(id);
diff --git a/packages/ghost/src/ghost-core/graph/slice.ts b/packages/ghost/src/ghost-core/graph/slice.ts
index d28464ef..fd94de51 100644
--- a/packages/ghost/src/ghost-core/graph/slice.ts
+++ b/packages/ghost/src/ghost-core/graph/slice.ts
@@ -101,13 +101,13 @@ export function resolveGraphSlice(
// *is* a surface in the cascade are themselves placed there. We resolve
// placement as: a node belongs to surface S if its containment parent chain
// reaches S directly (its `under` is S), or the node id equals S.
- const placementOf = (nodeUnder?: string): string =>
- nodeUnder ?? GHOST_GRAPH_ROOT_ID;
+ const placementOf = (nodeParent?: string): string =>
+ nodeParent ?? GHOST_GRAPH_ROOT_ID;
// Own + ancestor: walk every node, place it, decide provenance by cascade.
for (const node of graph.nodes.values()) {
const placement =
- node.id === surfaceId ? surfaceId : placementOf(node.under);
+ node.id === surfaceId ? surfaceId : placementOf(node.parent);
if (placement === surfaceId || node.id === surfaceId) {
add(node.id, { kind: "own" });
} else if (cascadeIds.has(placement)) {
diff --git a/packages/ghost/src/ghost-core/graph/types.ts b/packages/ghost/src/ghost-core/graph/types.ts
index 76d9e23b..8d64f62e 100644
--- a/packages/ghost/src/ghost-core/graph/types.ts
+++ b/packages/ghost/src/ghost-core/graph/types.ts
@@ -1,28 +1,33 @@
import type { GhostNodeRelation } from "../node/types.js";
-import { GHOST_SURFACE_ROOT_ID } from "../surfaces/types.js";
-/** The implicit root every node ultimately descends from (shared with surfaces). */
-export const GHOST_GRAPH_ROOT_ID = GHOST_SURFACE_ROOT_ID;
+/**
+ * The implicit root every node ultimately descends from. A package-root
+ * `index.md` *is* this node's prose; otherwise it exists implicitly and never
+ * needs to be declared.
+ */
+export const GHOST_GRAPH_ROOT_ID = "core";
/**
- * Where a node in the resolved graph came from. The fold unions authored
- * on-disk node files with a transition projection of the legacy facet model;
- * `origin` records which, so later phases and lint can treat them differently
- * (and so the projection can be deleted cleanly in the facet-removal phase).
+ * Where a node in the resolved graph came from. A local node is read from the
+ * package's own directory tree (its path is its id, its directory its parent);
+ * an inherited node is read-only context pulled in by `extends`. `origin`
+ * records which, so later phases and lint can treat them differently.
*/
export type GhostGraphNodeOrigin = "node-file" | "inherited";
/**
* A resolved graph node — pure prose (Option A). The body is the design
- * expression; there are no structured node fields. `under` is the single
- * containment parent (absent ⇒ child of the implicit `core` root); `relates`
- * are the typed lateral links; `incarnation` is the optional projection tag.
+ * expression; there are no structured node fields. `id` is the node's path in
+ * the package; `parent` is its containing directory — the single containment
+ * parent (absent ⇒ the implicit `core` root itself); `relates` are the typed
+ * lateral links; `incarnation` is the optional projection tag.
*/
export interface GhostGraphNode {
id: string;
/** One-line "what this is / when to gather it" — the retrieval payload. */
description?: string;
- under?: string;
+ /** The containing directory's id; absent ⇒ this node is the `core` root. */
+ parent?: string;
relates: GhostNodeRelation[];
incarnation?: string;
body: string;
@@ -31,9 +36,9 @@ export interface GhostGraphNode {
/**
* The in-memory fingerprint graph: prose nodes indexed by id, plus the
- * containment tree (`under` parent edges, root = `core`) that is the traversal
- * spine. This is the shape later phases (gather, checks, compare) traverse;
- * disk layout is just one serialization of it.
+ * containment tree (parent edges from the directory layout, root = `core`) that
+ * is the traversal spine. This is the shape later phases (gather, checks,
+ * review) traverse; the directory layout is just one serialization of it.
*/
export interface GhostGraph {
/** Every node, indexed by id. */
diff --git a/packages/ghost/src/ghost-core/index.ts b/packages/ghost/src/ghost-core/index.ts
index 3daa70b3..237e6133 100644
--- a/packages/ghost/src/ghost-core/index.ts
+++ b/packages/ghost/src/ghost-core/index.ts
@@ -37,6 +37,7 @@ export {
type GraphSliceNode,
type GraphSliceProvenance,
lintGraph,
+ type PlacedNode,
type ResolveGraphSliceOptions,
resolveGraphSlice,
} from "./graph/index.js";
@@ -75,16 +76,3 @@ export type {
// --- Skill bundle loader ---
export type { SkillBundleFile } from "./skill-bundle-loader.js";
export { loadSkillBundle } from "./skill-bundle-loader.js";
-// --- Surfaces (ghost.surfaces/v1) — the optional terse spine file ---
-export {
- GHOST_SURFACE_ROOT_ID,
- GHOST_SURFACES_SCHEMA,
- GHOST_SURFACES_YML_FILENAME,
- type GhostSurface,
- type GhostSurfacesDocument,
- type GhostSurfacesLintIssue,
- type GhostSurfacesLintReport,
- type GhostSurfacesLintSeverity,
- GhostSurfacesSchema,
- lintGhostSurfaces,
-} from "./surfaces/index.js";
diff --git a/packages/ghost/src/ghost-core/node/schema.ts b/packages/ghost/src/ghost-core/node/schema.ts
index 60e3cbdc..e23a7357 100644
--- a/packages/ghost/src/ghost-core/node/schema.ts
+++ b/packages/ghost/src/ghost-core/node/schema.ts
@@ -2,33 +2,38 @@ import { z } from "zod";
import { GHOST_NODE_RELATION_KINDS } from "./types.js";
/**
- * A node id is a permissive lowercase slug, unique within the package. The
- * charset is liberal on purpose (lowercase alphanumeric plus `.` `_` `-`): the
- * schema enforces machine-tractability, not a separator style. Dashes are the
- * emitted convention (skill / init / agent authoring), nudged in guidance — not
- * a lint rule. The tree lives only in `under`; an id never encodes hierarchy.
+ * A node id is its path within the package, `.md` dropped (`marketing/email`).
+ * The directory tree is the containment spine: the containing directory is the
+ * parent, so the id *does* encode hierarchy by design. A segment is a permissive
+ * lowercase slug (alphanumeric plus `.` `_` `-`); segments join with `/`. No
+ * leading, trailing, or doubled slash. Ids are computed by the loader from the
+ * file path, never authored in frontmatter.
*/
-const NodeIdSchema = z
- .string()
- .min(1)
- .regex(/^[a-z0-9][a-z0-9._-]*$/, {
- message:
- "node id must be a lowercase slug (alphanumeric plus . _ -, leading alphanumeric)",
- });
+const NODE_ID_PATTERN = /^[a-z0-9][a-z0-9._-]*(?:\/[a-z0-9][a-z0-9._-]*)*$/;
+
+const NodeIdSchema = z.string().min(1).regex(NODE_ID_PATTERN, {
+ message:
+ "node id must be a path of lowercase slug segments joined by '/' (alphanumeric plus . _ -, no leading/trailing/doubled slash)",
+});
/**
- * A node ref points at another node: a local id (``), or a cross-package
- * ref `:` where `` is a key declared in the
- * package manifest's `extends` map. Reference is by identity, never by path —
- * `:` is Ghost's qualifier lineage (e.g. the old `intent.principle:foo` refs).
+ * A node ref points at another node by its path id (`marketing/email`), or a
+ * cross-package ref `:` where `` is a key declared
+ * in the package manifest's `extends` map. The local part is a path id; `:` is
+ * Ghost's cross-package qualifier lineage (e.g. the old `intent.principle:foo`).
*/
const NodeRefSchema = z
.string()
.min(1)
- .regex(/^(?:[a-z0-9][a-z0-9._-]*:)?[a-z0-9][a-z0-9._-]*$/, {
- message:
- "node ref must be a local id '' or a cross-package ref ':'",
- });
+ .regex(
+ new RegExp(
+ `^(?:[a-z0-9][a-z0-9._-]*:)?${NODE_ID_PATTERN.source.slice(1, -1)}$`,
+ ),
+ {
+ message:
+ "node ref must be a path id 'marketing/email' or a cross-package ref ':'",
+ },
+ );
const NodeRelationSchema = z
.object({
@@ -47,9 +52,7 @@ const NodeRelationSchema = z
*/
export const GhostNodeFrontmatterSchema = z
.object({
- id: NodeIdSchema,
description: z.string().min(1).optional(),
- under: NodeRefSchema.optional(),
relates: z.array(NodeRelationSchema).optional(),
incarnation: z.string().min(1).optional(),
})
diff --git a/packages/ghost/src/ghost-core/node/serialize.ts b/packages/ghost/src/ghost-core/node/serialize.ts
index fdeb2643..0bae0ea6 100644
--- a/packages/ghost/src/ghost-core/node/serialize.ts
+++ b/packages/ghost/src/ghost-core/node/serialize.ts
@@ -3,14 +3,15 @@ import type { GhostNodeDocument, GhostNodeFrontmatter } from "./types.js";
/**
* Serialize a node back to its `---\n\n---\n` markdown form. Keys
- * are emitted in a stable order (id, under, relates, incarnation) so round-trips and
- * diffs are deterministic. Undefined fields are omitted.
+ * are emitted in a stable order (description, relates, incarnation) so
+ * round-trips and diffs are deterministic. Identity and containment are not
+ * serialized — they are the node's path in the directory tree. Undefined fields
+ * are omitted; a node with no frontmatter fields emits an empty block.
*/
export function serializeNode(node: GhostNodeDocument): string {
const fm = node.frontmatter;
- const ordered: Record = { id: fm.id };
+ const ordered: Record = {};
if (fm.description !== undefined) ordered.description = fm.description;
- if (fm.under !== undefined) ordered.under = fm.under;
if (fm.relates !== undefined) {
ordered.relates = fm.relates.map((relation) => {
const entry: Record = { to: relation.to };
@@ -20,9 +21,13 @@ export function serializeNode(node: GhostNodeDocument): string {
}
if (fm.incarnation !== undefined) ordered.incarnation = fm.incarnation;
- const yaml = stringifyYaml(ordered).trimEnd();
+ // An empty frontmatter object stringifies to "{}"; emit a bare block instead.
+ const yaml =
+ Object.keys(ordered).length === 0
+ ? ""
+ : `${stringifyYaml(ordered).trimEnd()}\n`;
const body = node.body.replace(/^\n+/, "");
- return `---\n${yaml}\n---\n${body.length ? `\n${body}\n` : "\n"}`;
+ return `---\n${yaml}---\n${body.length ? `\n${body}\n` : "\n"}`;
}
export type { GhostNodeFrontmatter };
diff --git a/packages/ghost/src/ghost-core/node/types.ts b/packages/ghost/src/ghost-core/node/types.ts
index 89bf6a22..326d7e0d 100644
--- a/packages/ghost/src/ghost-core/node/types.ts
+++ b/packages/ghost/src/ghost-core/node/types.ts
@@ -25,28 +25,21 @@ export interface GhostNodeRelation {
}
/**
- * A node's frontmatter: the machinery's handle (identity, tree, links,
- * incarnation).
- * The prose body carries the design expression; intent / inventory /
- * composition are authorship lenses, never fields.
+ * A node's frontmatter: descriptive properties only. Identity and containment
+ * are not here — they are the node's location in the directory tree (the file
+ * path is the id; the containing directory is the parent). The prose body
+ * carries the design expression; intent / inventory / composition are
+ * authorship lenses, never fields.
*/
export interface GhostNodeFrontmatter {
- /** Unique, addressable id within the package. */
- id: string;
/**
* One-line statement of what this node is and when to gather it — the
- * retrieval payload. Together with `id` it is how an agent selects a node,
- * exactly like a tool's name + description. The body is the node's
- * "implementation"; the description is what makes it discoverable. Optional,
- * but strongly encouraged on any node worth anchoring a task at.
+ * retrieval payload. Together with the node's id (its path) it is how an
+ * agent selects a node, exactly like a tool's name + description. The body is
+ * the node's "implementation"; the description is what makes it discoverable.
+ * Optional, but strongly encouraged on any node worth anchoring a task at.
*/
description?: string;
- /**
- * The single containment parent (the tree + the cascade). Absent means a
- * top-level node under the implicit `core` root. The tree lives only here;
- * the id never encodes hierarchy.
- */
- under?: string;
/** Typed lateral links to other nodes (composition graph). */
relates?: GhostNodeRelation[];
/**
diff --git a/packages/ghost/src/ghost-core/surfaces/index.ts b/packages/ghost/src/ghost-core/surfaces/index.ts
deleted file mode 100644
index cf79d30c..00000000
--- a/packages/ghost/src/ghost-core/surfaces/index.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-/**
- * Public surface for `ghost.surfaces/v1` schema and types.
- *
- * Phase 1 ships schema + types only. Lint (graph validation) is Phase 2; the
- * disk loader and CLI wiring come later. See docs/ideas/phase-1-plan.md.
- */
-
-export { lintGhostSurfaces } from "./lint.js";
-export { GhostSurfacesSchema } from "./schema.js";
-export {
- GHOST_SURFACE_ROOT_ID,
- GHOST_SURFACES_SCHEMA,
- GHOST_SURFACES_YML_FILENAME,
- type GhostSurface,
- type GhostSurfacesDocument,
- type GhostSurfacesLintIssue,
- type GhostSurfacesLintReport,
- type GhostSurfacesLintSeverity,
-} from "./types.js";
diff --git a/packages/ghost/src/ghost-core/surfaces/lint.ts b/packages/ghost/src/ghost-core/surfaces/lint.ts
deleted file mode 100644
index 940e5329..00000000
--- a/packages/ghost/src/ghost-core/surfaces/lint.ts
+++ /dev/null
@@ -1,208 +0,0 @@
-import type { ZodIssue } from "zod";
-import { GhostSurfacesSchema } from "./schema.js";
-import {
- GHOST_SURFACE_ROOT_ID,
- type GhostSurfacesDocument,
- type GhostSurfacesLintIssue,
- type GhostSurfacesLintReport,
-} from "./types.js";
-
-/**
- * Lint a `ghost.surfaces/v1` document for document-level correctness that the
- * schema cannot express in isolation: the containment tree (parent refs, no
- * cycles), the composition graph (edge refs), the reserved root, duplicate ids,
- * and teaching warnings for near-miss references.
- *
- * Containment (`parent`) is tree-constrained: cycles and self-parents are
- * errors. Composition (`edges`) may form a graph, including cycles among edges;
- * only dangling edge targets are errors.
- */
-export function lintGhostSurfaces(input: unknown): GhostSurfacesLintReport {
- const result = GhostSurfacesSchema.safeParse(input);
- if (!result.success) return finalize(zodIssues(result.error.issues));
-
- const doc = result.data as GhostSurfacesDocument;
- const issues: GhostSurfacesLintIssue[] = [];
-
- const ids = new Set();
- for (const surface of doc.surfaces) ids.add(surface.id);
- // `core` is always a resolvable target (implicit root) even if not declared.
- const knownIds = new Set(ids);
- knownIds.add(GHOST_SURFACE_ROOT_ID);
-
- checkDuplicateIds(doc, issues);
- checkReservedCore(doc, issues);
- checkParentRefs(doc, knownIds, issues);
- checkParentCycles(doc, issues);
- checkNearMissIds(doc, ids, issues);
-
- return finalize(issues);
-}
-
-function checkDuplicateIds(
- doc: GhostSurfacesDocument,
- issues: GhostSurfacesLintIssue[],
-): void {
- const seen = new Map();
- doc.surfaces.forEach((surface, index) => {
- const previous = seen.get(surface.id);
- if (previous !== undefined) {
- issues.push({
- severity: "error",
- rule: "duplicate-id",
- message: `surface id '${surface.id}' is duplicated (also at surfaces[${previous}])`,
- path: `surfaces[${index}].id`,
- });
- } else {
- seen.set(surface.id, index);
- }
- });
-}
-
-function checkReservedCore(
- doc: GhostSurfacesDocument,
- issues: GhostSurfacesLintIssue[],
-): void {
- // `core` is the implicit root: it may be declared (to describe it) but may
- // never have a parent.
- doc.surfaces.forEach((surface, index) => {
- if (surface.id === GHOST_SURFACE_ROOT_ID && surface.parent !== undefined) {
- issues.push({
- severity: "error",
- rule: "surface-core-reserved",
- message: `'${GHOST_SURFACE_ROOT_ID}' is the reserved implicit root and cannot declare a parent`,
- path: `surfaces[${index}].parent`,
- });
- }
- });
-}
-
-function checkParentRefs(
- doc: GhostSurfacesDocument,
- knownIds: Set,
- issues: GhostSurfacesLintIssue[],
-): void {
- doc.surfaces.forEach((surface, index) => {
- if (surface.parent === undefined) return;
- if (!knownIds.has(surface.parent)) {
- issues.push({
- severity: "error",
- rule: "surface-parent-unknown",
- message: `parent '${surface.parent}' does not match any surface id`,
- path: `surfaces[${index}].parent`,
- });
- }
- });
-}
-
-function checkParentCycles(
- doc: GhostSurfacesDocument,
- issues: GhostSurfacesLintIssue[],
-): void {
- const parentOf = new Map();
- for (const surface of doc.surfaces) parentOf.set(surface.id, surface.parent);
-
- doc.surfaces.forEach((surface, index) => {
- const visited = new Set([surface.id]);
- let current = surface.parent;
- while (current !== undefined && current !== GHOST_SURFACE_ROOT_ID) {
- if (visited.has(current)) {
- issues.push({
- severity: "error",
- rule: "surface-parent-cycle",
- message: `surface '${surface.id}' is part of a parent cycle (revisits '${current}')`,
- path: `surfaces[${index}].parent`,
- });
- return;
- }
- visited.add(current);
- // Only walk ids that exist; an unknown parent is reported separately.
- if (!parentOf.has(current)) return;
- current = parentOf.get(current);
- }
- });
-}
-
-function checkNearMissIds(
- doc: GhostSurfacesDocument,
- ids: Set,
- issues: GhostSurfacesLintIssue[],
-): void {
- const candidates = [...ids];
-
- doc.surfaces.forEach((surface, index) => {
- if (surface.parent !== undefined && !ids.has(surface.parent)) {
- const near = nearest(surface.parent, candidates);
- if (near) {
- issues.push({
- severity: "warning",
- rule: "surface-id-near-miss",
- message: `parent '${surface.parent}' is unknown; did you mean '${near}'?`,
- path: `surfaces[${index}].parent`,
- });
- }
- }
- });
-}
-
-/** Nearest candidate within edit distance 2, or null. */
-function nearest(value: string, candidates: string[]): string | null {
- let best: string | null = null;
- let bestDistance = 3;
- for (const candidate of candidates) {
- const distance = levenshtein(value, candidate);
- if (distance < bestDistance) {
- bestDistance = distance;
- best = candidate;
- }
- }
- return bestDistance <= 2 ? best : null;
-}
-
-function levenshtein(a: string, b: string): number {
- const rows = a.length + 1;
- const cols = b.length + 1;
- const dist: number[][] = Array.from({ length: rows }, () =>
- new Array(cols).fill(0),
- );
- for (let i = 0; i < rows; i++) dist[i][0] = i;
- for (let j = 0; j < cols; j++) dist[0][j] = j;
- for (let i = 1; i < rows; i++) {
- for (let j = 1; j < cols; j++) {
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
- dist[i][j] = Math.min(
- dist[i - 1][j] + 1,
- dist[i][j - 1] + 1,
- dist[i - 1][j - 1] + cost,
- );
- }
- }
- return dist[a.length][b.length];
-}
-
-function zodIssues(issues: ZodIssue[]): GhostSurfacesLintIssue[] {
- 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: GhostSurfacesLintIssue[]): GhostSurfacesLintReport {
- 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/src/ghost-core/surfaces/schema.ts b/packages/ghost/src/ghost-core/surfaces/schema.ts
deleted file mode 100644
index a03d370b..00000000
--- a/packages/ghost/src/ghost-core/surfaces/schema.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { z } from "zod";
-import { GHOST_SURFACES_SCHEMA } from "./types.js";
-
-/**
- * Flat slug for surface ids. The dot is excluded: a dotted id (`email.marketing`)
- * would pretend to be a `parent` link, creating a second source of truth for the
- * tree. Containment lives only in `parent`.
- */
-const SurfaceIdSchema = z
- .string()
- .min(1)
- .regex(/^[a-z0-9][a-z0-9_-]*$/, {
- message:
- "surface id must be a flat slug (lowercase alphanumeric plus _ -, no dots; the tree lives in parent)",
- });
-
-const SurfaceSchema = z
- .object({
- id: SurfaceIdSchema,
- description: z.string().min(1).optional(),
- parent: SurfaceIdSchema.optional(),
- })
- .strict();
-
-/**
- * Zod schema for `surfaces.yml` (`ghost.surfaces/v1`) — the optional terse spine
- * file. Validates each position in isolation; graph-level rules (parent exists,
- * no cycles) are covered by the node-graph lint after the fold.
- */
-export const GhostSurfacesSchema = z
- .object({
- schema: z.literal(GHOST_SURFACES_SCHEMA),
- surfaces: z.array(SurfaceSchema).optional().default([]),
- })
- .strict();
diff --git a/packages/ghost/src/ghost-core/surfaces/types.ts b/packages/ghost/src/ghost-core/surfaces/types.ts
deleted file mode 100644
index 2a31aebc..00000000
--- a/packages/ghost/src/ghost-core/surfaces/types.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-export const GHOST_SURFACES_SCHEMA = "ghost.surfaces/v1" as const;
-export const GHOST_SURFACES_YML_FILENAME = "surfaces.yml" as const;
-
-/** The implicit root every node ultimately descends from. */
-export const GHOST_SURFACE_ROOT_ID = "core" as const;
-
-/**
- * `surfaces.yml` is an optional terse spine file: a place to declare bare tree
- * positions (id + parent) in one file rather than as bodyless node files. It
- * folds into the same node id space at load time — a position that needs
- * guidance is simply a node with that id. Lateral composition lives on node
- * `relates`, never here (the old surface edge vocabulary is gone).
- */
-export interface GhostSurface {
- id: string;
- description?: string;
- /**
- * The single containment parent. Absent means a top-level position under the
- * implicit `core` root. Containment lives only here; the id never encodes
- * hierarchy (see GhostSurfacesSchema id rules).
- */
- parent?: string;
-}
-
-export interface GhostSurfacesDocument {
- schema: typeof GHOST_SURFACES_SCHEMA;
- surfaces: GhostSurface[];
-}
-
-/**
- * Lint report types reuse the fingerprint facet shape verbatim so Phase 2 and
- * the CLI can treat all facet lint reports uniformly.
- */
-export type GhostSurfacesLintSeverity = "error" | "warning" | "info";
-
-export interface GhostSurfacesLintIssue {
- severity: GhostSurfacesLintSeverity;
- rule: string;
- message: string;
- path?: string;
-}
-
-export interface GhostSurfacesLintReport {
- issues: GhostSurfacesLintIssue[];
- errors: number;
- warnings: number;
- info: number;
-}
diff --git a/packages/ghost/src/scan/file-kind.ts b/packages/ghost/src/scan/file-kind.ts
index 539f58bc..21199173 100644
--- a/packages/ghost/src/scan/file-kind.ts
+++ b/packages/ghost/src/scan/file-kind.ts
@@ -3,22 +3,20 @@ import {
GhostFingerprintPackageManifestSchema,
lintGhostCheck,
lintGhostNode,
- lintGhostSurfaces,
} from "#ghost-core";
import type { LintReport } from "./lint.js";
export type DetectedFileKind =
| "fingerprint-manifest"
- | "surfaces"
| "check"
| "node"
| "unsupported";
/**
- * Decide whether a file is a bundle artifact. Canonical filenames and YAML
- * `schema:` markers route to their artifact linters; markdown under `nodes/`
- * or `checks/` routes to the node / check linter. Unknown files remain
- * unsupported instead of being guessed at.
+ * Decide whether a file is a bundle artifact. The manifest routes to its
+ * artifact linter; markdown under `checks/` is a check; any other markdown is a
+ * node (its path is its id — containment is the directory tree). Unknown files
+ * remain unsupported instead of being guessed at.
*/
export function detectFileKind(path: string, raw: string): DetectedFileKind {
const lowerPath = path.toLowerCase();
@@ -29,21 +27,19 @@ export function detectFileKind(path: string, raw: string): DetectedFileKind {
if (filename === "manifest.yaml") {
return "fingerprint-manifest";
}
- if (filename === "surfaces.yml") return "surfaces";
- if (filename === "surfaces.yaml") return "surfaces";
// A markdown check lives under a `checks/` directory. Detected by location so
// the established agent-check format (no `schema:` field) is recognized.
if (filename.endsWith(".md") && /(^|[\\/])checks[\\/]/.test(lowerPath)) {
return "check";
}
- // A markdown node lives under a `nodes/` directory (ghost.node/v1).
- if (filename.endsWith(".md") && /(^|[\\/])nodes[\\/]/.test(lowerPath)) {
+ // Any other markdown file is a node (ghost.node/v1). Its id is its path; the
+ // containing directory is its parent.
+ if (filename.endsWith(".md")) {
return "node";
}
if (/^\s*schema:\s*ghost\.fingerprint-package\/v1\b/m.test(raw)) {
return "fingerprint-manifest";
}
- if (/^\s*schema:\s*ghost\.surfaces\/v1\b/m.test(raw)) return "surfaces";
return "unsupported";
}
@@ -53,13 +49,11 @@ export function lintDetectedFileKind(
): LintReport {
return kind === "fingerprint-manifest"
? lintFingerprintManifestFile(raw)
- : kind === "surfaces"
- ? lintSurfacesFile(raw)
- : kind === "check"
- ? lintGhostCheck(raw)
- : kind === "node"
- ? lintGhostNode(raw)
- : lintUnsupportedFile();
+ : kind === "check"
+ ? lintGhostCheck(raw)
+ : kind === "node"
+ ? lintGhostNode(raw)
+ : lintUnsupportedFile();
}
function lintFingerprintManifestFile(raw: string): LintReport {
@@ -98,14 +92,6 @@ function zodLintReport(result: {
};
}
-function lintSurfacesFile(raw: string): LintReport {
- try {
- return lintGhostSurfaces(parseYaml(raw));
- } catch (err) {
- return yamlErrorReport("surfaces-not-yaml", "surfaces file", err);
- }
-}
-
function lintUnsupportedFile(): LintReport {
return {
issues: [
@@ -113,7 +99,7 @@ function lintUnsupportedFile(): LintReport {
severity: "error",
rule: "unsupported-artifact",
message:
- "File is not a recognized Ghost artifact. Use manifest.yml, surfaces.yml, a checks/*.md check, or a nodes/*.md node.",
+ "File is not a recognized Ghost artifact. Use manifest.yml, a checks/*.md check, or a *.md node.",
},
],
errors: 1,
diff --git a/packages/ghost/src/scan/fingerprint-contribution.ts b/packages/ghost/src/scan/fingerprint-contribution.ts
index 6ac1037c..b9abd1d5 100644
--- a/packages/ghost/src/scan/fingerprint-contribution.ts
+++ b/packages/ghost/src/scan/fingerprint-contribution.ts
@@ -35,8 +35,6 @@ export interface ScanContributionReport {
*/
export function summarizeFingerprintContribution(input: {
graph?: GhostGraph;
- /** Declared surface ids from surfaces.yml (excluding the implicit root). */
- surfaceIds?: string[];
missing?: boolean;
invalidReason?: string;
}): ScanContributionReport {
@@ -52,20 +50,25 @@ export function summarizeFingerprintContribution(input: {
}
const graph = input.graph;
+ // Authored local nodes contribute. The root `index.md` is a real authored
+ // node (origin node-file) and counts; the implicit root (when undeclared) and
+ // inherited nodes do not.
const nodes = [...graph.nodes.values()].filter(
- (node) => node.id !== GHOST_GRAPH_ROOT_ID,
+ (node) => node.origin === "node-file",
);
const essence = nodes.filter((node) => node.incarnation === undefined);
const tagged = nodes.filter((node) => node.incarnation !== undefined);
- // Surface coverage: count nodes whose `under` is each declared surface.
+ // Surface coverage: count nodes whose parent is each declared surface.
const placement = new Map();
for (const node of nodes) {
- const under = node.under ?? GHOST_GRAPH_ROOT_ID;
- placement.set(under, (placement.get(under) ?? 0) + 1);
+ const parent = node.parent ?? GHOST_GRAPH_ROOT_ID;
+ placement.set(parent, (placement.get(parent) ?? 0) + 1);
}
- const surfaceIds = (input.surfaceIds ?? []).filter(
- (id) => id !== GHOST_GRAPH_ROOT_ID,
+ // Surfaces are the tree's interior positions: any id that is a parent of at
+ // least one node (a directory), excluding the implicit root.
+ const surfaceIds = [...graph.parents.values()].filter(
+ (id, index, all) => id !== GHOST_GRAPH_ROOT_ID && all.indexOf(id) === index,
);
const surfaces: ScanSurfaceCoverage[] = surfaceIds
.map((id) => ({ id, node_count: placement.get(id) ?? 0 }))
@@ -88,7 +91,7 @@ export function summarizeFingerprintContribution(input: {
? [`Add nodes for sparse surfaces: ${sparse.join(", ")}.`]
: ["Package contributes nodes across its declared surfaces."]
: [
- "Package is valid but has no nodes yet. Add nodes/*.md to contribute.",
+ "Package is valid but has no nodes yet. Add an index.md or /.md to contribute.",
],
};
}
diff --git a/packages/ghost/src/scan/fingerprint-package-layers.ts b/packages/ghost/src/scan/fingerprint-package-layers.ts
index 860051a9..24ee6c7e 100644
--- a/packages/ghost/src/scan/fingerprint-package-layers.ts
+++ b/packages/ghost/src/scan/fingerprint-package-layers.ts
@@ -6,37 +6,31 @@ import {
type GhostFingerprintPackageManifest,
GhostFingerprintPackageManifestSchema,
type GhostGraphNode,
- type GhostSurfacesDocument,
- GhostSurfacesSchema,
lintGraph,
} from "#ghost-core";
-import { isMissingPathError, readOptionalUtf8 } from "../internal/fs.js";
+import { isMissingPathError } from "../internal/fs.js";
import {
type FingerprintPackagePaths,
type LoadedFingerprintPackage,
resolveFingerprintPackage,
} from "./fingerprint-package.js";
import type { LintIssue } from "./lint.js";
-import { loadNodesDir } from "./nodes-dir.js";
+import { loadNodeTree } from "./node-tree.js";
const LEGACY_FACET_FILES = ["intent.yml", "inventory.yml", "composition.yml"];
export async function loadFingerprintPackage(
paths: FingerprintPackagePaths,
): Promise {
- const [manifestRaw, surfacesRaw] = await Promise.all([
- readFile(paths.manifest, "utf-8"),
- readOptional(paths.surfaces),
- ]);
+ const manifestRaw = await readFile(paths.manifest, "utf-8");
const manifest = parseManifest(manifestRaw, "manifest.yml");
- const surfaces = parseSurfaces(surfacesRaw);
// Legacy facet packages no longer load directly — guide to `ghost migrate`.
await assertNotLegacyFacetPackage(paths);
- const { nodes: nodeFiles } = await loadNodesDir(paths.dir);
+ const { nodes: placedNodes } = await loadNodeTree(paths.packageDir);
const inheritedNodes = await loadInheritedNodes(manifest, paths);
- const graph = assembleGraph({ nodeFiles, surfaces, inheritedNodes });
+ const graph = assembleGraph({ placedNodes, inheritedNodes });
const report = lintGraph(graph);
if (report.errors > 0) {
@@ -51,7 +45,6 @@ export async function loadFingerprintPackage(
manifest,
manifestRaw,
graph,
- ...(surfaces ? { surfaces } : {}),
};
}
@@ -107,18 +100,16 @@ async function loadInheritedNodes(
}
/**
- * If a package still ships the legacy facet files and has no `nodes/`, fail
- * with migrate guidance rather than a confusing graph error.
+ * If a package still ships the legacy facet files, fail with migrate guidance
+ * rather than a confusing graph error.
*/
async function assertNotLegacyFacetPackage(
paths: FingerprintPackagePaths,
): Promise {
- const hasNodes = await pathExists(paths.nodes);
- if (hasNodes) return;
for (const facet of LEGACY_FACET_FILES) {
if (await pathExists(`${paths.packageDir}/${facet}`)) {
throw new Error(
- `This is a legacy facet package (found ${facet}, no nodes/). Run \`ghost migrate\` to convert it to the node model.`,
+ `This is a legacy facet package (found ${facet}). Run \`ghost migrate\` to convert it to the directory-tree node model.`,
);
}
}
@@ -134,20 +125,6 @@ async function pathExists(path: string): Promise {
}
}
-function parseSurfaces(
- raw: string | undefined,
-): GhostSurfacesDocument | undefined {
- if (raw === undefined) return undefined;
- const result = GhostSurfacesSchema.safeParse(parseYaml(raw));
- if (!result.success) {
- const first = result.error.issues[0];
- throw new Error(
- `surfaces.yml failed schema validation: ${first?.message ?? "invalid surfaces"}`,
- );
- }
- return result.data as GhostSurfacesDocument;
-}
-
export function lintFingerprintPackageManifest(
raw: string,
issues: LintIssue[],
@@ -211,5 +188,3 @@ function parseYamlSafe(
return undefined;
}
}
-
-const readOptional = readOptionalUtf8;
diff --git a/packages/ghost/src/scan/fingerprint-package.ts b/packages/ghost/src/scan/fingerprint-package.ts
index 2d4a58cf..006eb6b2 100644
--- a/packages/ghost/src/scan/fingerprint-package.ts
+++ b/packages/ghost/src/scan/fingerprint-package.ts
@@ -1,10 +1,8 @@
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, join, resolve } from "node:path";
import {
- GHOST_SURFACES_YML_FILENAME,
type GhostFingerprintPackageManifest,
type GhostGraph,
- type GhostSurfacesDocument,
lintGraph,
} from "#ghost-core";
import { isExistingPathError, isMissingPathError } from "../internal/fs.js";
@@ -32,9 +30,6 @@ export interface FingerprintPackagePaths {
dir: string;
packageDir: string;
manifest: string;
- surfaces: string;
- /** The `nodes/` directory holding `ghost.node/v1` markdown nodes. */
- nodes: string;
/** Legacy facet paths — used only to detect legacy packages for migration. */
intent: string;
inventory: string;
@@ -44,8 +39,6 @@ export interface FingerprintPackagePaths {
export interface LoadedFingerprintPackage {
manifest: GhostFingerprintPackageManifest;
manifestRaw: string;
- /** Parsed `surfaces.yml`, or `undefined` when the package has no surfaces file. */
- surfaces?: GhostSurfacesDocument;
/** The in-memory node graph — the only fingerprint model. */
graph: GhostGraph;
}
@@ -72,8 +65,6 @@ export function resolveFingerprintPackage(
dir,
packageDir,
manifest: join(packageDir, FINGERPRINT_MANIFEST_FILENAME),
- surfaces: join(packageDir, GHOST_SURFACES_YML_FILENAME),
- nodes: join(packageDir, "nodes"),
intent: join(packageDir, FINGERPRINT_INTENT_FILENAME),
inventory: join(packageDir, FINGERPRINT_INVENTORY_FILENAME),
composition: join(packageDir, FINGERPRINT_COMPOSITION_FILENAME),
@@ -187,7 +178,7 @@ export async function lintFingerprintPackage(
severity: issue.severity,
rule: issue.rule,
message: issue.message,
- ...(issue.node ? { path: `nodes/${issue.node}` } : {}),
+ ...(issue.node ? { path: `${issue.node}.md` } : {}),
})),
);
} catch (err) {
diff --git a/packages/ghost/src/scan/migrate-legacy.ts b/packages/ghost/src/scan/migrate-legacy.ts
index f9604e95..177fef87 100644
--- a/packages/ghost/src/scan/migrate-legacy.ts
+++ b/packages/ghost/src/scan/migrate-legacy.ts
@@ -1,19 +1,15 @@
-import {
- GHOST_SURFACE_ROOT_ID,
- GHOST_SURFACES_SCHEMA,
- type GhostNodeDocument,
- serializeNode,
-} from "#ghost-core";
+import { type GhostNodeDocument, serializeNode } from "#ghost-core";
/**
* One-shot migration of a legacy `.ghost/` package (pre-surface coordinates)
- * onto the surface model. Operates on raw parsed YAML, because the current
- * schema rejects the legacy fields (`topology`, `applies_to`, `surface_type`,
- * `scope`) and a legacy package no longer parses through the loader.
+ * onto the directory-tree node model. Operates on raw parsed YAML, because the
+ * current schema rejects the legacy fields (`topology`, `applies_to`,
+ * `surface_type`, `scope`) and a legacy package no longer parses through the
+ * loader.
*
* Core discipline: report, don't guess. A node whose home cannot be derived
- * unambiguously is left unplaced and recorded for human review, never
- * auto-placed.
+ * unambiguously is left unplaced (at the package root, cascading from core) and
+ * recorded for human review, never auto-placed into a surface.
*/
type Yaml = Record;
@@ -31,7 +27,8 @@ export interface MigrationNote {
}
export interface MigrationResult {
- surfaces: Yaml;
+ /** Derived surface ids (directories), each a child of the implicit `core`. */
+ surfaceIds: string[];
intent: Yaml | undefined;
inventory: Yaml | undefined;
composition: Yaml | undefined;
@@ -61,17 +58,10 @@ export function migrateLegacyPackage(
? structuredClone(input.composition)
: undefined;
- // --- surfaces.yml from inventory.topology.scopes ---
- const scopeIds = collectScopeIds(inventory);
- const surfaces: Yaml = {
- schema: GHOST_SURFACES_SCHEMA,
- surfaces: scopeIds.map((id) => ({
- id,
- parent: GHOST_SURFACE_ROOT_ID,
- })),
- };
+ // --- surface ids (directories) from inventory.topology.scopes ---
+ const surfaceIds = collectScopeIds(inventory);
- // Drop topology from inventory (its data is now surfaces.yml).
+ // Drop topology from inventory (its data is now the directory layout).
if (inventory && "topology" in inventory) delete inventory.topology;
// --- place + clean nodes ---
@@ -86,7 +76,7 @@ export function migrateLegacyPackage(
placeArray(composition, "patterns", "composition.patterns", notes);
placeArray(inventory, "exemplars", "inventory.exemplars", notes);
- return { surfaces, intent, inventory, composition, notes };
+ return { surfaceIds, intent, inventory, composition, notes };
}
function collectScopeIds(inventory: Yaml | undefined): string[] {
@@ -224,28 +214,45 @@ export interface MigratedNodeFile {
}
/**
- * Convert the migrated facet docs into `nodes/*.md` files — the persistent form
- * of the Phase 2 facet→node projection. Each facet entry becomes one prose node
- * whose body is the entry's primary text and whose `under` is its placement
- * (`surface`, omitted when unplaced ⇒ cascades from core). Lossy by design:
- * structured affordances (evidence, check_refs, exemplar paths) are dropped, in
- * line with Option A. Returns one file per node (`nodes/.md`).
+ * Convert the migrated facet docs into a directory tree of `*.md` nodes — the
+ * persistent form of the facet→node projection. Each facet entry becomes one
+ * prose node placed by the directory layout: a placed node lands at
+ * `/.md` (its directory is its parent), an unplaced node at
+ * `.md` (the package root, cascading from core). Each derived surface also
+ * gets a bare `/index.md` so the directory survives even when no node
+ * lands in it. Lossy by design: structured affordances (evidence, check_refs,
+ * exemplar paths) are dropped, in line with Option A.
*/
export function migratedNodeFiles(result: MigrationResult): MigratedNodeFile[] {
const files: MigratedNodeFile[] = [];
const seen = new Set();
+ // Seed each derived surface as a directory with an index node, so an empty
+ // surface still exists as a tree position.
+ for (const surfaceId of result.surfaceIds) {
+ files.push({
+ relativePath: `${surfaceId}/index.md`,
+ content: serializeNode({
+ frontmatter: {},
+ body: `The \`${surfaceId}\` surface.`,
+ }),
+ });
+ }
+
const emit = (entry: Yaml, body: string) => {
const id = typeof entry.id === "string" ? entry.id : undefined;
if (!id || seen.has(id)) return;
seen.add(id);
- const under = typeof entry.surface === "string" ? entry.surface : undefined;
+ const surface =
+ typeof entry.surface === "string" ? entry.surface : undefined;
const doc: GhostNodeDocument = {
- frontmatter: { id, ...(under !== undefined ? { under } : {}) },
+ frontmatter: {},
body: body.trim(),
};
+ const relativePath =
+ surface !== undefined ? `${surface}/${id}.md` : `${id}.md`;
files.push({
- relativePath: `nodes/${id}.md`,
+ relativePath,
content: serializeNode(doc),
});
};
diff --git a/packages/ghost/src/scan/node-tree.ts b/packages/ghost/src/scan/node-tree.ts
new file mode 100644
index 00000000..67ce4641
--- /dev/null
+++ b/packages/ghost/src/scan/node-tree.ts
@@ -0,0 +1,126 @@
+import { readdir, readFile } from "node:fs/promises";
+import { join } from "node:path";
+import { type PlacedNode, parseNode } from "#ghost-core";
+import { GHOST_CHECKS_DIRNAME } from "./checks-dir.js";
+import { FINGERPRINT_MANIFEST_FILENAME } from "./constants.js";
+
+/** A directory `index.md` denotes the prose for the directory itself. */
+const INDEX_FILENAME = "index.md";
+
+/**
+ * Reserved package-root entries that are never nodes. `checks/` is a reserved
+ * top-level subtree (the markdown checks that govern surfaces). The manifest is
+ * the package anchor.
+ *
+ * NOTE: `checks/` is reserved at the package root only. Internal/nested reuse
+ * (e.g. teams that compose nested `.agents`-style trees) will want this set to
+ * be configurable per package — a planned follow-up, deliberately not built yet.
+ */
+const RESERVED_ROOT_ENTRIES = new Set([
+ FINGERPRINT_MANIFEST_FILENAME,
+ "manifest.yaml",
+ GHOST_CHECKS_DIRNAME,
+]);
+
+export interface LoadedNodeTree {
+ nodes: PlacedNode[];
+ /** Files that failed lint, with their first error message (path-relative id). */
+ invalid: Array<{ file: string; message: string }>;
+}
+
+/**
+ * Load authored prose nodes from the package's directory tree.
+ *
+ * Every `*.md` file under the package directory is a node. Its id is its path
+ * with `.md` dropped (`marketing/email.md` → `marketing/email`); its parent is
+ * its containing directory (`marketing`), or the implicit `core` root at the
+ * top level. A directory's own prose lives in its `index.md`: the root
+ * `index.md` is the `core` node (parent absent); `marketing/index.md` is the
+ * `marketing` node (id `marketing`, parent `core`). The `checks/` subtree and
+ * `manifest.yml` are reserved and skipped.
+ *
+ * A file that fails per-node lint is collected in `invalid` (with its first
+ * error) and skipped rather than throwing, so one bad node does not block
+ * folding the rest. Absent or empty tree → no nodes.
+ */
+export async function loadNodeTree(
+ packageDir: string,
+): Promise {
+ const nodes: PlacedNode[] = [];
+ const invalid: LoadedNodeTree["invalid"] = [];
+
+ await walk(packageDir, "", true, nodes, invalid);
+
+ // Deterministic order by id, mirroring the old sorted readdir.
+ nodes.sort((a, b) => a.id.localeCompare(b.id));
+ invalid.sort((a, b) => a.file.localeCompare(b.file));
+ return { nodes, invalid };
+}
+
+async function walk(
+ packageDir: string,
+ relDir: string,
+ isRoot: boolean,
+ nodes: PlacedNode[],
+ invalid: LoadedNodeTree["invalid"],
+): Promise {
+ const absDir = relDir === "" ? packageDir : join(packageDir, relDir);
+ let entries: Array<{ name: string; isDir: boolean }>;
+ try {
+ const dirents = await readdir(absDir, { withFileTypes: true });
+ entries = dirents.map((d) => ({ name: d.name, isDir: d.isDirectory() }));
+ } catch {
+ return;
+ }
+
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
+ if (isRoot && RESERVED_ROOT_ENTRIES.has(entry.name)) continue;
+ if (entry.name.startsWith(".")) continue;
+
+ const relPath = relDir === "" ? entry.name : `${relDir}/${entry.name}`;
+
+ if (entry.isDir) {
+ await walk(packageDir, relPath, false, nodes, invalid);
+ continue;
+ }
+ if (!entry.name.endsWith(".md")) continue;
+
+ const raw = await readFile(join(packageDir, relPath), "utf-8");
+ const { node, report } = parseNode(raw);
+ if (node === null || report.errors > 0) {
+ const first = report.issues.find((issue) => issue.severity === "error");
+ invalid.push({
+ file: relPath,
+ message: first?.message ?? "invalid node",
+ });
+ continue;
+ }
+
+ const { id, parent } = locate(relPath);
+ nodes.push({ id, ...(parent !== undefined ? { parent } : {}), doc: node });
+ }
+}
+
+/**
+ * Compute a node's id and parent from its package-relative file path.
+ * - `index.md` → id `core`, parent absent (the root node).
+ * - `a/index.md` → id `a`, parent `core`.
+ * - `a/b/index.md` → id `a/b`, parent `a`.
+ * - `a.md` → id `a`, parent `core`.
+ * - `a/b.md` → id `a/b`, parent `a`.
+ */
+function locate(relPath: string): { id: string; parent?: string } {
+ const withoutExt = relPath.replace(/\.md$/, "");
+ const segments = withoutExt.split("/");
+ const isIndex = segments[segments.length - 1] === "index";
+ const idSegments = isIndex ? segments.slice(0, -1) : segments;
+
+ if (idSegments.length === 0) {
+ // Root index.md → the core node.
+ return { id: "core" };
+ }
+ const id = idSegments.join("/");
+ const parent =
+ idSegments.length === 1 ? "core" : idSegments.slice(0, -1).join("/");
+ return { id, parent: parent === "core" ? "core" : parent };
+}
diff --git a/packages/ghost/src/scan/nodes-dir.ts b/packages/ghost/src/scan/nodes-dir.ts
deleted file mode 100644
index 652fe848..00000000
--- a/packages/ghost/src/scan/nodes-dir.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { readdir, readFile } from "node:fs/promises";
-import { join } from "node:path";
-import { type GhostNodeDocument, parseNode } from "#ghost-core";
-
-export const GHOST_NODES_DIRNAME = "nodes";
-
-export interface LoadedNodesDir {
- nodes: GhostNodeDocument[];
- /** Files that failed lint, with their first error message. */
- invalid: Array<{ file: string; message: string }>;
-}
-
-/**
- * Load authored prose nodes from `/nodes/*.md`. Each file is parsed
- * and validated per-node; a file with errors is collected in `invalid` (with
- * its first error) and skipped rather than throwing, so one bad node does not
- * block folding the rest. Absent directory → no nodes.
- *
- * Phase 2 keeps discovery deliberately minimal (one default `nodes/` directory,
- * mirroring `checks/`). Loose-anywhere and custom layouts are a later
- * refinement.
- */
-export async function loadNodesDir(
- packageDir: string,
-): Promise {
- const dir = join(packageDir, GHOST_NODES_DIRNAME);
- let entries: string[];
- try {
- entries = await readdir(dir);
- } catch {
- return { nodes: [], invalid: [] };
- }
-
- const nodes: GhostNodeDocument[] = [];
- const invalid: LoadedNodesDir["invalid"] = [];
-
- for (const name of entries.sort()) {
- if (!name.endsWith(".md")) continue;
- const raw = await readFile(join(dir, name), "utf-8");
- const { node, report } = parseNode(raw);
- if (node === null || report.errors > 0) {
- const first = report.issues.find((issue) => issue.severity === "error");
- invalid.push({ file: name, message: first?.message ?? "invalid node" });
- continue;
- }
- nodes.push(node);
- }
-
- return { nodes, invalid };
-}
diff --git a/packages/ghost/src/scan/scan-status.ts b/packages/ghost/src/scan/scan-status.ts
index 27d2cf93..6019fcd1 100644
--- a/packages/ghost/src/scan/scan-status.ts
+++ b/packages/ghost/src/scan/scan-status.ts
@@ -69,7 +69,6 @@ async function scanContribution(
const loaded = await loadFingerprintPackage(paths);
return summarizeFingerprintContribution({
graph: loaded.graph,
- surfaceIds: (loaded.surfaces?.surfaces ?? []).map((s) => s.id),
});
} catch (err) {
return summarizeFingerprintContribution({
diff --git a/packages/ghost/src/scan/templates.ts b/packages/ghost/src/scan/templates.ts
index 5cf56b66..972210e2 100644
--- a/packages/ghost/src/scan/templates.ts
+++ b/packages/ghost/src/scan/templates.ts
@@ -29,45 +29,34 @@ function manifestFile(): TemplateFile {
}
/**
- * The default starter: the surfaces spine (the implicit `core` root needs no
- * declaration, so the file starts empty) plus one `core`-placed intent node
- * that demonstrates the shape — frontmatter handles + prose body written
- * through the intent/inventory/composition lenses.
+ * The default starter: a manifest plus the package-root `index.md` — the `core`
+ * node whose prose cascades to every surface. The directory tree is the spine:
+ * add a surface by adding a directory, give it prose with its own `index.md`,
+ * and place nodes as `/.md`.
*/
const DEFAULT_TEMPLATE: GhostInitTemplate = {
name: "default",
- description: "Minimal node package: surfaces spine + one core intent node.",
+ description: "Minimal node package: manifest + a core index node.",
files() {
return [
manifestFile(),
{
- relativePath: "surfaces.yml",
- content: `schema: ghost.surfaces/v1
-# The implicit \`core\` root needs no declaration. Add surfaces as you author,
-# e.g.:
-# surfaces:
-# - id: checkout
-# parent: core
-surfaces: []
-`,
- },
- {
- relativePath: "nodes/core-voice.md",
+ relativePath: "index.md",
content: `---
-id: core-voice
-under: core
+description: The product-wide root; true everywhere.
---
-Replace this with your product's voice. A node is prose written through the
-intent / inventory / composition lenses — they guide what to capture, they are
-not fields:
+Replace this with your product's voice — the \`core\` node. A node is prose
+written through the intent / inventory / composition lenses; they guide what to
+capture, they are not fields:
- intent — the why and the stance (e.g. "calm, direct, never breathless").
- inventory — the material you have (tokens, components, pointers to code).
- composition — how it is assembled (the patterns that make it feel intentional).
-This node sits at \`core\`, so it cascades to every surface. Place
-surface-specific nodes with \`under: \`, link related nodes with
+This file is the package-root \`index.md\`, so it cascades to every surface. Add
+a surface by adding a directory: \`checkout/index.md\` is the \`checkout\` surface,
+and \`checkout/payment.md\` is a node under it. Link related nodes with
\`relates\`, and tag medium-bound expressions with \`incarnation\` (e.g. email,
billboard, voice). Leave essence untagged.
`,
diff --git a/packages/ghost/src/skill-bundle/SKILL.md b/packages/ghost/src/skill-bundle/SKILL.md
index 3f97a616..f1a5705f 100644
--- a/packages/ghost/src/skill-bundle/SKILL.md
+++ b/packages/ghost/src/skill-bundle/SKILL.md
@@ -14,10 +14,11 @@ materials it draws from, and the patterns that make it feel intentional.
```text
.ghost/
- manifest.yml # schema + id
- surfaces.yml # the spine: surfaces + their parent (core is implicit)
- nodes/*.md # prose nodes — the design expression
- checks/*.md # optional ghost.check/v1 checks
+ manifest.yml # schema + id
+ index.md # the core node — true everywhere (optional)
+ /index.md # a surface's own prose
+ /.md # a node placed in that surface
+ checks/*.md # optional ghost.check/v1 checks
```
The checked-in `.ghost/` package is the source of truth. Ordinary Git
@@ -26,10 +27,15 @@ are drafts, and committed fingerprint changes are canonical for Ghost. Checks ar
markdown rules an agent evaluates. Ghost is not a lifecycle manager, proposal system,
design-system registry, or screenshot archive.
-The fingerprint is a graph of **nodes**. A node is a markdown file:
-frontmatter (`id`, `description`, `under`, `relates`, `incarnation`) + a prose
-body. **Intent + inventory + composition** are the authoring lenses the body is
-written through — they guide what to capture, they are not fields or node types:
+The fingerprint is a graph of **nodes**, and the **directory tree is the graph**.
+A node is a markdown file: descriptive frontmatter (`description`, `relates`,
+`incarnation`) + a prose body. A node's **identity is its path** (`marketing/email.md`
+→ `marketing/email`) and its **parent is its containing directory** — a surface
+is just a directory, and a directory's own prose lives in its `index.md`
+(`marketing/index.md` is the `marketing` surface; the package-root `index.md` is
+the implicit `core` node, true everywhere). **Intent + inventory + composition**
+are the authoring lenses the body is written through — they guide what to
+capture, they are not fields or node types:
- intent — the why and the stance.
- inventory — the materials and pointers to implementation the agent can inspect.
@@ -37,16 +43,19 @@ written through — they guide what to capture, they are not fields or node type
`description` is the retrieval payload — a one-line "what this is / when to
gather it" (like a tool's name + description); `ghost gather` with no argument
-lists nodes by id + description for the agent to match against. `under` places a
-node so it is inherited downward (`core` is the implicit root that reaches every
-surface); `relates` links nodes laterally; `incarnation` tags a medium-bound
-expression (essence is untagged). Free-form keys (`audience`, …) pass through.
-See [references/capture.md](references/capture.md) for the full node shape.
+lists nodes by id + description for the agent to match against. The directory
+places a node so it is inherited downward (`core` is the implicit root that
+reaches every surface); `relates` links nodes laterally; `incarnation` tags a
+medium-bound expression (essence is untagged). Free-form keys (`audience`, …)
+pass through. See [references/capture.md](references/capture.md) for the full
+node shape.
Checks and review validate output; they are not generation input.
`manifest.yml` anchors the package with `schema: ghost.fingerprint-package/v1`.
-The tree is declared in `surfaces.yml`, never inferred from filenames or paths.
+The tree is the layout itself: ids and parents come from where files sit, so
+moving a node is a rename. Reserved at the package root: `manifest.yml` and the
+`checks/` subtree; every other `*.md` is a node.
Optional `ghost.check/v1` markdown checks live in `checks/*.md`, routed by surface.
Use `ghost signals` as a stdout-only reconnaissance helper when an agent needs
@@ -62,14 +71,14 @@ and map severities into their own review or check format.
A package can **extend** another by identity — the shared-brand pattern. The
manifest's `extends` maps a package id to where it lives:
`extends: { brand: ../brand/.ghost }`. Then nodes reference inherited context by
-identity, never path: `under: brand:core` or `relates: [{ to: brand:core-trust }]`.
-Inherited nodes are read-only and flow into gather/validate like local ones.
+identity, never path: `relates: [{ to: brand:core/trust }]` (a `:`
+ref). Inherited nodes are read-only and flow into gather/validate like local ones.
## Core CLI Verbs
| Verb | Purpose |
|---|---|
-| `ghost init [--template ]` | Scaffold `.ghost/` with manifest, surfaces spine, and a seed node. |
+| `ghost init [--template ]` | Scaffold `.ghost/` with a manifest and a core `index.md` node. |
| `ghost scan [dir] [--format json]` | Report node/surface contribution. |
| `ghost validate [file-or-dir]` | Validate the package — artifact shape and the node graph (links resolve, one root, acyclic). |
| `ghost checks --surface ` | Select and ground the markdown checks governing the named surfaces. |
@@ -83,7 +92,7 @@ Inherited nodes are read-only and flow into gather/validate like local ones.
|---|---|
| `GHOST_PACKAGE_DIR= ghost init` / `ghost init --package ` | Create or resolve a custom fingerprint package directory for host wrappers or a monorepo package. |
| `ghost signals [path]` | Emit raw repo signals for fingerprint authoring. |
-| `ghost migrate [dir]` | Migrate a legacy `.ghost/` package onto the surface model. |
+| `ghost migrate [dir]` | Migrate a legacy `.ghost/` package onto the directory-tree node model. |
## Workflows
diff --git a/packages/ghost/src/skill-bundle/references/authoring-scenarios.md b/packages/ghost/src/skill-bundle/references/authoring-scenarios.md
index 0fc69c19..fd362645 100644
--- a/packages/ghost/src/skill-bundle/references/authoring-scenarios.md
+++ b/packages/ghost/src/skill-bundle/references/authoring-scenarios.md
@@ -83,9 +83,9 @@ content; scan frequency and raw signals do not establish guidance.
## 4. Draft The Nodes
-Write the smallest useful set of `nodes/*.md`, each a purpose-coherent prose
-body with a one-line `description`, placed with `under` and linked with
-`relates` where a relationship carries meaning. Write each body through the
+Write the smallest useful set of nodes — each a purpose-coherent prose body with
+a one-line `description`, placed by putting its file in the right surface
+directory and linked with `relates` where a relationship carries meaning. Write each body through the
intent / inventory / composition lenses — the why, the material (with pointers
to implementation), and how it is assembled. These are lenses, not fields.
diff --git a/packages/ghost/src/skill-bundle/references/capture.md b/packages/ghost/src/skill-bundle/references/capture.md
index 0827298e..c56234f6 100644
--- a/packages/ghost/src/skill-bundle/references/capture.md
+++ b/packages/ghost/src/skill-bundle/references/capture.md
@@ -18,27 +18,29 @@ is checked in, Ghost treats the fingerprint package as canonical.
```text
.ghost/
- manifest.yml # schema + id
- surfaces.yml # the spine: surfaces + their `parent` (core is implicit)
- nodes/ # one prose node per file
- core-voice.md
- checkout-trust.md
- checks/ # optional ghost.check/v1 markdown checks
+ manifest.yml # schema + id
+ index.md # the core node — true everywhere
+ checkout/ # a surface is a directory
+ index.md # the checkout surface's own prose
+ trust.md # a node placed under checkout
+ checks/ # optional ghost.check/v1 markdown checks
```
-A **node** is a markdown file: YAML frontmatter (the machine handles) + a prose
-body (the design expression). The fingerprint is the graph of nodes the loader
-folds together; `ghost gather ` traverses it.
+A **node** is a markdown file: YAML frontmatter (descriptive properties) + a
+prose body (the design expression). The **directory tree is the graph**: a
+node's id is its path, its parent is its containing directory, and a surface is
+just a directory whose own prose lives in its `index.md`. `ghost gather
+` traverses it.
## The node shape
+A node at `checkout/trust.md` (id `checkout/trust`, parent `checkout`):
+
```markdown
---
-id: checkout-trust # required: unique, stable
description: Trust at the payment moment. # the retrieval payload (see below)
-under: checkout # optional: parent — inherited downward
relates: # optional: lateral links
- - to: core-trust
+ - to: core/trust
as: reinforces # reinforces | contrasts | variant
incarnation: web # optional: email | billboard | voice | … (omit = essence)
# free-form keys (audience, stage, …) are allowed and pass through untouched
@@ -54,9 +56,9 @@ action beats completeness…
against those and names one. The body is the node's "implementation"; the
description is what makes it discoverable. Write one on any node worth
anchoring a task at.
-- **`under`** places the node — a node inherits everything it sits under. The
- brand soul lives at `core` (implicit root), so `core`-placed nodes reach every
- surface.
+- **The directory places the node** — a node inherits everything in the
+ directories above it. The brand soul lives in the package-root `index.md` (the
+ `core` node), so it reaches every surface.
- **`relates`** links laterally when a relationship carries rationale. When the
rationale is rich (e.g. "checkout and item-detail disagree on density on
purpose"), write a **relationship node** whose body explains the tension.
@@ -76,7 +78,8 @@ prose — a node may lean entirely on one:
A finding cites a node by id, so keep a node **purpose-coherent**: one purpose,
any length. Split into a second node only when a handle diverges — a different
-`under`, a different `incarnation`, or a genuinely different `relates` role.
+directory (parent), a different `incarnation`, or a genuinely different
+`relates` role.
## Steps
@@ -94,18 +97,20 @@ a single contract organizes locality.
### 2. Initialize
```bash
-ghost init # scaffolds manifest + surfaces.yml + a seed node
+ghost init # scaffolds manifest + a core index.md node
ghost scan
```
`ghost init` is template-driven (`--template ` selects a starter). The
-default template seeds the spine plus one `core` node demonstrating the shape.
+default template seeds the package-root `index.md` — the `core` node —
+demonstrating the shape.
-### 3. Shape the spine
+### 3. Shape the tree
-Edit `surfaces.yml` to declare the surfaces this product has and their `parent`
-(containment). `core` is implicit. The tree is always declared here — never
-inferred from node filenames or repo paths.
+Add a surface by adding a directory: `checkout/` is the `checkout` surface, and
+`checkout/index.md` holds its prose. Nest surfaces by nesting directories. The
+tree is the layout itself — a node's id and parent come from where its file
+sits, never from a declared spine.
### 4. Orient
@@ -116,9 +121,10 @@ scratch observations — curate, never copy verbatim into a node.
### 5. Write sparse nodes
-Add the smallest useful set of `nodes/*.md`, each a purpose-coherent prose body
-written through the lenses, placed with `under` and linked with `relates` where
-a relationship carries meaning. Prefer a few high-confidence nodes over a noisy
+Add the smallest useful set of nodes — each a purpose-coherent prose body
+written through the lenses, placed by putting its file in the right directory
+and linked with `relates` where a relationship carries meaning. Prefer a few
+high-confidence nodes over a noisy
catalog. Ask the human to keep, soften, reject, or re-place important claims
before treating draft nodes as durable.
@@ -139,8 +145,6 @@ ghost check --base HEAD
- Never describe any file outside `.ghost/` as canonical package input.
- Never treat raw `ghost signals` output as a node without curation.
-- Never infer the surface tree from filenames or repo paths — declare it in
- `surfaces.yml`.
- Never invent surface-composition obligations absent from evidence or human
direction.
- Never promote subjective taste directly into checks; make it deterministic or
diff --git a/packages/ghost/src/skill-bundle/references/schema.md b/packages/ghost/src/skill-bundle/references/schema.md
index a5c1cca1..f793ec2c 100644
--- a/packages/ghost/src/skill-bundle/references/schema.md
+++ b/packages/ghost/src/skill-bundle/references/schema.md
@@ -9,27 +9,33 @@ Canonical package:
```text
.ghost/
- manifest.yml ghost.fingerprint-package/v1 — id + optional extends
- nodes/*.md ghost.node/v1 — the design expression (the unit)
- surfaces.yml optional ghost.surfaces/v1 — a terse spine (id + parent)
- checks/*.md optional ghost.check/v1 — agent-evaluated output checks
+ manifest.yml ghost.fingerprint-package/v1 — id + optional extends
+ index.md the core node — true everywhere (optional)
+ /index.md a surface's own prose (the directory is the surface)
+ /.md ghost.node/v1 — a node placed in that surface
+ checks/*.md optional ghost.check/v1 — agent-evaluated output checks
```
+The **directory tree is the graph**: a node's id is its path and its parent is
+its containing directory. A surface is a directory; its own prose is its
+`index.md`. The package-root `index.md` is the implicit `core` node. Reserved at
+the root: `manifest.yml` and `checks/`; every other `*.md` is a node.
+
Git is the approval boundary: checked-in files are canonical; uncommitted or
unmerged edits are draft work. One contract per package; the contract carries no
paths and infers nothing from repo location.
## Nodes
-A node is the unit — a markdown file with frontmatter + a prose body:
+A node is the unit — a markdown file with descriptive frontmatter + a prose
+body. Identity and containment are not in the frontmatter; they are where the
+file sits. A node at `checkout/trust.md`:
```yaml
---
-id: checkout-trust # required, unique
description: Trust at the payment moment. # the retrieval payload
-under: checkout # optional parent (inherited downward)
relates: # optional lateral links
- - to: core-trust
+ - to: core/trust
as: reinforces # reinforces | contrasts | variant
incarnation: web # optional: email | billboard | voice | … (omit = essence)
# free-form keys (audience, stage, …) pass through untouched
@@ -39,25 +45,18 @@ lenses, not fields.
```
`description` is how an agent selects a node (like a tool's name + description).
-`under` places the node so it is inherited downward (`core` is the implicit root that
-reaches everywhere). `relates` links nodes laterally. `incarnation` tags a
-medium-bound expression. The tree lives only in `under`/`surfaces.yml`, never in
-the id and never inferred from a path.
-
-## The spine (optional)
+The file's location places it: `checkout/trust.md` has id `checkout/trust` and
+is inherited downward from `checkout` (`core` is the implicit root that reaches
+everywhere). `relates` links nodes laterally. `incarnation` tags a medium-bound
+expression. The tree is the layout; ids encode hierarchy because they *are* paths.
-`surfaces.yml` is a terse place to declare bare tree positions (id + parent +
-optional description) in one file instead of as bodyless node files. It folds
-into the same node id space — a position that needs guidance is just a node with
-that id.
+## Surfaces are directories
-```yaml
-schema: ghost.surfaces/v1
-surfaces:
- - id: checkout
- parent: core
- description: The purchase flow.
-```
+There is no spine file. A surface exists when its directory exists; give it prose
+with an `index.md`, place nodes inside it, and nest surfaces by nesting
+directories. A surface that needs no prose of its own is simply a directory that
+holds nodes. Moving a node to another directory changes its id (a rename) and
+its parent — `ghost validate` reports any `relates` that no longer resolve.
## Manifest + extends
@@ -68,9 +67,9 @@ extends:
brand: ../brand/.ghost # inherit another contract's nodes, by identity
```
-A `brand:core-trust` ref in `under`/`relates` resolves into the extended
-package's nodes (read-only). Reference is by identity (the `extends` key), never
-by path.
+A `brand:core/trust` ref in `relates` resolves into the extended package's nodes
+(read-only) — a `:` ref. Reference is by identity (the `extends`
+key), never by repo path.
## Gather
diff --git a/packages/ghost/test/cli.test.ts b/packages/ghost/test/cli.test.ts
index bc1a4463..2a49ebb6 100644
--- a/packages/ghost/test/cli.test.ts
+++ b/packages/ghost/test/cli.test.ts
@@ -221,12 +221,9 @@ describe("ghost CLI", () => {
expect(init.code).toBe(0);
const initOutput = JSON.parse(init.stdout);
expect(Object.keys(initOutput).sort()).toEqual(["dir", "written"]);
- // Node package: manifest + surfaces spine + a seed node, no facet files.
+ // Node package: manifest + the package-root core index node, no facet files.
expect(initOutput.written).toContain("manifest.yml");
- expect(initOutput.written).toContain("surfaces.yml");
- expect(initOutput.written.some((p: string) => p.startsWith("nodes/"))).toBe(
- true,
- );
+ expect(initOutput.written).toContain("index.md");
await expect(
readFile(join(dir, ".ghost", "manifest.yml"), "utf-8"),
).resolves.toContain("schema: ghost.fingerprint-package/v1");
@@ -324,8 +321,8 @@ describe("ghost CLI", () => {
it("refuses to overwrite existing fingerprint files unless forced", async () => {
await runCli(["init"], dir);
await writeFile(
- join(dir, ".ghost", "nodes", "core-voice.md"),
- "---\nid: core-voice\nunder: core\n---\n\nCurated Surface voice.\n",
+ join(dir, ".ghost", "index.md"),
+ "---\n---\n\nCurated Surface voice.\n",
);
const refused = await runCli(["init"], dir);
@@ -335,14 +332,14 @@ describe("ghost CLI", () => {
"Refusing to overwrite existing Ghost fingerprint file(s)",
);
await expect(
- readFile(join(dir, ".ghost", "nodes", "core-voice.md"), "utf-8"),
+ readFile(join(dir, ".ghost", "index.md"), "utf-8"),
).resolves.toContain("Curated Surface");
const forced = await runCli(["init", "--force"], dir);
expect(forced.code).toBe(0);
await expect(
- readFile(join(dir, ".ghost", "nodes", "core-voice.md"), "utf-8"),
+ readFile(join(dir, ".ghost", "index.md"), "utf-8"),
).resolves.toContain("intent / inventory / composition");
});
@@ -383,8 +380,7 @@ describe("ghost CLI", () => {
expect(init.code).toBe(0);
expect(init.stdout).toContain("manifest.yml");
- expect(init.stdout).toContain("surfaces.yml");
- expect(init.stdout).toContain("nodes/");
+ expect(init.stdout).toContain("index.md");
expect(init.stdout).not.toContain("cache/:");
expect(init.stdout).not.toContain("memory/intent.md:");
expect(
@@ -425,13 +421,11 @@ describe("ghost CLI", () => {
const lint = await runCli(["validate"], dir);
expect(lint.code).toBe(0);
- // The seed node lives at core, so it cascades to a gather of any surface.
+ // The seed node is the package-root index — the core node itself.
const gather = await runCli(["gather", "core", "--format", "json"], dir);
expect(gather.code).toBe(0);
const slice = JSON.parse(gather.stdout);
- expect(slice.nodes.some((n: { id: string }) => n.id === "core-voice")).toBe(
- true,
- );
+ expect(slice.nodes.some((n: { id: string }) => n.id === "core")).toBe(true);
});
it("runs signals and validate from the unified cli", async () => {
@@ -804,13 +798,13 @@ composition:
await writeGatherPackage(dir);
const result = await runCli(
- ["gather", "email-marketing", "--package", ".ghost", "--format", "json"],
+ ["gather", "email/marketing", "--package", ".ghost", "--format", "json"],
dir,
);
expect(result.code).toBe(0);
const slice = JSON.parse(result.stdout);
- expect(slice.surface).toBe("email-marketing");
+ expect(slice.surface).toBe("email/marketing");
const byId = Object.fromEntries(
slice.nodes.map((node: { id: string; provenance: unknown }) => [
node.id,
@@ -818,12 +812,12 @@ composition:
]),
);
// Graph slice (Option A, prose nodes): own + cascaded ancestors.
- expect(byId["brand-voice"]).toEqual({ kind: "ancestor", from: "core" });
- expect(byId["marketing-urgency"]).toEqual({ kind: "own" });
- // Phase 3 decision: edge contributions come from node `relates`, not from
- // legacy `composes` surface edges. checkout-clarity sits on a sibling
- // surface with no `relates` link in, so it is no longer pulled in.
- expect(byId["checkout-clarity"]).toBeUndefined();
+ // The root index (`core`) cascades; the marketing index node is own.
+ expect(byId["core"]).toEqual({ kind: "ancestor", from: "core" });
+ expect(byId["email/marketing"]).toEqual({ kind: "own" });
+ // checkout/clarity sits on a sibling surface with no `relates` link in, so
+ // it is not pulled in.
+ expect(byId["checkout/clarity"]).toBeUndefined();
});
it("filters the gather slice by incarnation via --as", async () => {
@@ -848,34 +842,31 @@ composition:
const ids = slice.nodes.map((n: { id: string }) => n.id).sort();
// essence (untagged) + matching web; the email node is filtered out.
expect(ids).toContain("launch");
- expect(ids).toContain("launch-web");
- expect(ids).not.toContain("launch-email");
+ expect(ids).toContain("launch/web");
+ expect(ids).not.toContain("launch/email");
});
it("inherits nodes from an extended package via extends", async () => {
- // Brand contract.
- await mkdir(join(dir, "brand", "nodes"), { recursive: true });
+ // Brand contract: a node at brand/core-trust.md → id `core-trust`.
+ await mkdir(join(dir, "brand"), { recursive: true });
await writeFile(
join(dir, "brand", "manifest.yml"),
"schema: ghost.fingerprint-package/v1\nid: brand\n",
);
await writeFile(
- join(dir, "brand", "nodes", "core-trust.md"),
- "---\nid: core-trust\nunder: core\n---\n\nReduce felt risk.\n",
+ join(dir, "brand", "core-trust.md"),
+ "---\n---\n\nReduce felt risk.\n",
);
- // Product contract extends the brand.
- await mkdir(join(dir, "product", "nodes"), { recursive: true });
+ // Product contract extends the brand. The checkout surface is the
+ // directory `product/checkout/`, with a node at checkout/trust.md.
+ await mkdir(join(dir, "product", "checkout"), { recursive: true });
await writeFile(
join(dir, "product", "manifest.yml"),
"schema: ghost.fingerprint-package/v1\nid: acme-checkout\nextends:\n brand: ../brand\n",
);
await writeFile(
- join(dir, "product", "surfaces.yml"),
- "schema: ghost.surfaces/v1\nsurfaces:\n - id: checkout\n parent: core\n",
- );
- await writeFile(
- join(dir, "product", "nodes", "checkout-trust.md"),
- "---\nid: checkout-trust\nunder: checkout\nrelates:\n - to: brand:core-trust\n as: reinforces\n---\n\nReassure at payment.\n",
+ join(dir, "product", "checkout", "trust.md"),
+ "---\nrelates:\n - to: brand:core-trust\n as: reinforces\n---\n\nReassure at payment.\n",
);
const validate = await runCli(
@@ -899,14 +890,13 @@ composition:
});
it("fails validate when a cross-package ref is not in extends", async () => {
- await mkdir(join(dir, "nodes"), { recursive: true });
await writeFile(
join(dir, "manifest.yml"),
"schema: ghost.fingerprint-package/v1\nid: solo\n",
);
await writeFile(
- join(dir, "nodes", "n.md"),
- "---\nid: n\nunder: core\nrelates:\n - to: brand:core-trust\n---\n\nBody.\n",
+ join(dir, "n.md"),
+ "---\nrelates:\n - to: brand:core-trust\n---\n\nBody.\n",
);
const validate = await runCli(["validate", "."], dir);
@@ -926,7 +916,7 @@ composition:
const payload = JSON.parse(result.stdout);
expect(payload.kind).toBe("menu");
expect(payload.surfaces.map((entry: { id: string }) => entry.id)).toContain(
- "email-marketing",
+ "email/marketing",
);
});
@@ -980,12 +970,10 @@ experience_contracts: []
expect(result.code).toBe(0);
const report = JSON.parse(result.stdout);
- expect(report.surfaces.map((s: { id: string }) => s.id)).toEqual([
- "lending",
- ]);
+ expect(report.surfaces).toEqual(["lending"]);
// The migrated package must lint clean and gather correctly.
- const lint = await runCli(["validate", ".ghost/surfaces.yml"], dir, {
+ const lint = await runCli(["validate", ".ghost"], dir, {
allowNoExit: true,
});
expect(lint.stdout).toContain("0 error(s)");
@@ -995,8 +983,9 @@ experience_contracts: []
dir,
);
const slice = JSON.parse(gather.stdout);
+ // The single-scope node landed at lending/scoped.md → id `lending/scoped`.
expect(
- slice.nodes.find((node: { id: string }) => node.id === "scoped")
+ slice.nodes.find((node: { id: string }) => node.id === "lending/scoped")
?.provenance,
).toEqual({ kind: "own" });
});
@@ -1019,16 +1008,14 @@ experience_contracts: []
join(ghost, "manifest.yml"),
"schema: ghost.fingerprint-package/v1\nid: c3\n",
);
+ // Surfaces are directories: checkout/ and email/ each with an index node.
+ await mkdir(join(ghost, "checkout"), { recursive: true });
+ await mkdir(join(ghost, "email"), { recursive: true });
await writeFile(
- join(ghost, "surfaces.yml"),
- `schema: ghost.surfaces/v1
-surfaces:
- - id: checkout
- parent: core
- - id: email
- parent: core
-`,
+ join(ghost, "checkout", "index.md"),
+ "---\n---\n\nCheckout.\n",
);
+ await writeFile(join(ghost, "email", "index.md"), "---\n---\n\nEmail.\n");
await writeFile(
join(ghost, "checks", "brand.md"),
"---\nname: brand\ndescription: Brand voice.\nseverity: medium\nsurface: core\n---\n## Instructions\nVoice.\n",
@@ -1071,17 +1058,12 @@ surfaces:
join(ghost, "manifest.yml"),
"schema: ghost.fingerprint-package/v1\nid: c4\n",
);
+ // core prose at the package root; checkout surface as a directory node.
+ await mkdir(join(ghost, "checkout"), { recursive: true });
+ await writeFile(join(ghost, "index.md"), "---\n---\n\nWarm everywhere.\n");
await writeFile(
- join(ghost, "surfaces.yml"),
- "schema: ghost.surfaces/v1\nsurfaces:\n - id: checkout\n parent: core\n",
- );
- await writeFile(
- join(ghost, "nodes", "brand-voice.md"),
- "---\nid: brand-voice\nunder: core\n---\n\nWarm everywhere.\n",
- );
- await writeFile(
- join(ghost, "nodes", "checkout-clarity.md"),
- "---\nid: checkout-clarity\nunder: checkout\n---\n\nCheckout copy is plain.\n",
+ join(ghost, "checkout", "clarity.md"),
+ "---\n---\n\nCheckout copy is plain.\n",
);
await writeFile(
join(ghost, "checks", "checkout.md"),
@@ -1108,10 +1090,10 @@ surfaces:
);
// Grounding is the gather slice: prose nodes by provenance (Phase 4).
const ids = checkout.nodes.map((n: { id: string }) => n.id);
- expect(ids).toContain("checkout-clarity"); // own
- expect(ids).toContain("brand-voice"); // inherited from core
+ expect(ids).toContain("checkout/clarity"); // own
+ expect(ids).toContain("core"); // cascades from the root index
const own = checkout.nodes.find(
- (n: { id: string }) => n.id === "checkout-clarity",
+ (n: { id: string }) => n.id === "checkout/clarity",
);
expect(own.provenance).toEqual({ kind: "own" });
});
@@ -1123,9 +1105,10 @@ surfaces:
join(ghost, "manifest.yml"),
"schema: ghost.fingerprint-package/v1\nid: c4b\n",
);
+ await mkdir(join(ghost, "checkout"), { recursive: true });
await writeFile(
- join(ghost, "surfaces.yml"),
- "schema: ghost.surfaces/v1\nsurfaces:\n - id: checkout\n parent: core\n",
+ join(ghost, "checkout", "index.md"),
+ "---\n---\n\nCheckout.\n",
);
const result = await runCli(
@@ -1155,62 +1138,47 @@ async function writeIncarnationPackage(dir: string): Promise {
join(ghost, "manifest.yml"),
"schema: ghost.fingerprint-package/v1\nid: incarnation-demo\n",
);
+ // launch surface as a directory; web/email incarnations as nodes under it.
+ await mkdir(join(ghost, "launch"), { recursive: true });
await writeFile(
- join(ghost, "surfaces.yml"),
- `schema: ghost.surfaces/v1
-surfaces:
- - id: launch
- description: Launch announcement.
- parent: core
-`,
+ join(ghost, "launch", "index.md"),
+ "---\n---\n\nOne idea, stated with confidence.\n",
);
await writeFile(
- join(ghost, "nodes", "launch.md"),
- "---\nid: launch\nunder: core\n---\n\nOne idea, stated with confidence.\n",
+ join(ghost, "launch", "web.md"),
+ "---\nincarnation: web\n---\n\nHero with one CTA.\n",
);
await writeFile(
- join(ghost, "nodes", "launch-web.md"),
- "---\nid: launch-web\nunder: launch\nincarnation: web\n---\n\nHero with one CTA.\n",
- );
- await writeFile(
- join(ghost, "nodes", "launch-email.md"),
- "---\nid: launch-email\nunder: launch\nincarnation: email\n---\n\nSubject is the headline.\n",
+ join(ghost, "launch", "email.md"),
+ "---\nincarnation: email\n---\n\nSubject is the headline.\n",
);
}
async function writeGatherPackage(dir: string): Promise {
const ghost = join(dir, ".ghost");
- await mkdir(join(ghost, "nodes"), { recursive: true });
+ await mkdir(join(ghost, "email", "marketing"), { recursive: true });
+ await mkdir(join(ghost, "checkout"), { recursive: true });
await writeFile(
join(ghost, "manifest.yml"),
"schema: ghost.fingerprint-package/v1\nid: gather-demo\n",
);
+ // Surfaces are directories: email/ (with marketing/ nested) and checkout/.
+ // The root index is the brand voice that cascades everywhere.
await writeFile(
- join(ghost, "surfaces.yml"),
- `schema: ghost.surfaces/v1
-surfaces:
- - id: email
- description: Email surface.
- parent: core
- - id: email-marketing
- description: Marketing email.
- parent: email
- - id: checkout
- description: Checkout.
- parent: core
-`,
+ join(ghost, "index.md"),
+ "---\ndescription: Brand voice.\n---\n\nWarm and concise.\n",
);
await writeFile(
- join(ghost, "nodes", "brand-voice.md"),
- "---\nid: brand-voice\nunder: core\n---\n\nWarm and concise.\n",
+ join(ghost, "email", "index.md"),
+ "---\ndescription: Email surface.\n---\n\nEmail.\n",
);
await writeFile(
- join(ghost, "nodes", "marketing-urgency.md"),
- "---\nid: marketing-urgency\nunder: email-marketing\n---\n\nMarketing may use urgency.\n",
+ join(ghost, "email", "marketing", "index.md"),
+ "---\ndescription: Marketing email.\n---\n\nMarketing may use urgency.\n",
);
await writeFile(
- join(ghost, "nodes", "checkout-clarity.md"),
- "---\nid: checkout-clarity\nunder: checkout\n---\n\nCheckout copy is plain.\n",
+ join(ghost, "checkout", "clarity.md"),
+ "---\n---\n\nCheckout copy is plain.\n",
);
}
diff --git a/packages/ghost/test/fingerprint-package.test.ts b/packages/ghost/test/fingerprint-package.test.ts
index 74f170e6..91836449 100644
--- a/packages/ghost/test/fingerprint-package.test.ts
+++ b/packages/ghost/test/fingerprint-package.test.ts
@@ -36,20 +36,24 @@ describe("split fingerprint package", () => {
expect([...loaded.graph.nodes.keys()]).toEqual([]);
});
- it("folds authored nodes/*.md into the graph", async () => {
+ it("folds the directory tree of *.md nodes into the graph", async () => {
await writeManifest(dir);
- await mkdir(join(dir, "nodes"), { recursive: true });
+ await mkdir(join(dir, "checkout"), { recursive: true });
await writeFile(
- join(dir, "nodes", "checkout-trust.md"),
- "---\nid: checkout-trust\nunder: core\nincarnation: web\n---\n\nReduce felt risk near payment.\n",
+ join(dir, "checkout", "trust.md"),
+ "---\nincarnation: web\n---\n\nReduce felt risk near payment.\n",
);
const loaded = await loadFingerprintPackage(resolveFingerprintPackage(dir));
- const authored = loaded.graph.nodes.get("checkout-trust");
+ // id is the path; parent is the containing directory.
+ const authored = loaded.graph.nodes.get("checkout/trust");
expect(authored?.origin).toBe("node-file");
+ expect(authored?.parent).toBe("checkout");
expect(authored?.body).toBe("Reduce felt risk near payment.");
expect(authored?.incarnation).toBe("web");
+ // The containing directory resolves up to the implicit core root.
+ expect(loaded.graph.parents.get("checkout")).toBe("core");
});
it("guides legacy facet packages to migrate", async () => {
@@ -62,10 +66,7 @@ describe("split fingerprint package", () => {
});
it("reports a missing manifest", async () => {
- await writeFile(
- join(dir, "surfaces.yml"),
- "schema: ghost.surfaces/v1\nsurfaces: []\n",
- );
+ await writeFile(join(dir, "index.md"), "---\n---\n\nRoot prose.\n");
const report = await lintFingerprintPackage(dir);
diff --git a/packages/ghost/test/ghost-core/check-route.test.ts b/packages/ghost/test/ghost-core/check-route.test.ts
index a28e3f5d..5d325477 100644
--- a/packages/ghost/test/ghost-core/check-route.test.ts
+++ b/packages/ghost/test/ghost-core/check-route.test.ts
@@ -1,9 +1,8 @@
import { describe, expect, it } from "vitest";
import {
assembleGraph,
- GHOST_SURFACES_SCHEMA,
type GhostCheckDocument,
- type GhostSurfacesDocument,
+ type PlacedNode,
selectChecksForSurfaces,
} from "../../src/ghost-core/index.js";
@@ -19,22 +18,25 @@ function check(name: string, surface?: string): GhostCheckDocument {
};
}
-const SURFACES: GhostSurfacesDocument = {
- schema: GHOST_SURFACES_SCHEMA,
- surfaces: [
- { id: "checkout", parent: "core" },
- { id: "email", parent: "core" },
- { id: "email-marketing", parent: "email" },
- ],
-};
+function placed(id: string, parent: string): PlacedNode {
+ return { id, parent, doc: { frontmatter: {}, body: "Prose." } };
+}
-const GRAPH = assembleGraph({ surfaces: SURFACES });
+// The directory tree that establishes the surfaces:
+// checkout/ email/ email/marketing/
+const GRAPH = assembleGraph({
+ placedNodes: [
+ placed("checkout", "core"),
+ placed("email", "core"),
+ placed("email/marketing", "email"),
+ ],
+});
const CHECKS = [
check("brand", "core"),
check("checkout-color", "checkout"),
check("email-links", "email"),
- check("marketing-unsub", "email-marketing"),
+ check("marketing-unsub", "email/marketing"),
check("unplaced"), // governs core
];
@@ -55,7 +57,7 @@ describe("selectChecksForSurfaces", () => {
});
it("cascades multiple ancestor levels", () => {
- const routed = selectChecksForSurfaces(CHECKS, GRAPH, ["email-marketing"]);
+ const routed = selectChecksForSurfaces(CHECKS, GRAPH, ["email/marketing"]);
// own marketing + ancestor email + ancestor core (brand, unplaced)
expect(names(routed)).toEqual([
"brand",
@@ -66,18 +68,18 @@ describe("selectChecksForSurfaces", () => {
});
it("tags provenance own vs. ancestor", () => {
- const routed = selectChecksForSurfaces(CHECKS, GRAPH, ["email-marketing"]);
+ const routed = selectChecksForSurfaces(CHECKS, GRAPH, ["email/marketing"]);
const byName = Object.fromEntries(
routed.map((r) => [r.check.frontmatter.name, r.relevance]),
);
expect(byName["marketing-unsub"]).toEqual({
kind: "own",
- surface: "email-marketing",
+ surface: "email/marketing",
});
expect(byName["email-links"]).toMatchObject({
kind: "ancestor",
surface: "email",
- via: "email-marketing",
+ via: "email/marketing",
});
});
diff --git a/packages/ghost/test/ghost-core/graph-fold.test.ts b/packages/ghost/test/ghost-core/graph-fold.test.ts
index ac6f87a3..c96e9814 100644
--- a/packages/ghost/test/ghost-core/graph-fold.test.ts
+++ b/packages/ghost/test/ghost-core/graph-fold.test.ts
@@ -3,72 +3,82 @@ import {
ancestorChain,
assembleGraph,
GHOST_GRAPH_ROOT_ID,
- type GhostNodeDocument,
+ type PlacedNode,
} from "../../src/ghost-core/index.js";
-function nodeDoc(
- frontmatter: GhostNodeDocument["frontmatter"],
+function placed(
+ id: string,
+ parent: string | undefined,
+ frontmatter: PlacedNode["doc"]["frontmatter"] = {},
body = "Prose.",
-): GhostNodeDocument {
- return { frontmatter, body };
+): PlacedNode {
+ return {
+ id,
+ ...(parent !== undefined ? { parent } : {}),
+ doc: { frontmatter, body },
+ };
}
-describe("assembleGraph (node + surfaces fold)", () => {
- it("folds authored node files into the graph", () => {
+describe("assembleGraph (directory-tree fold)", () => {
+ it("folds placed nodes into the graph", () => {
const graph = assembleGraph({
- nodeFiles: [
- nodeDoc(
+ placedNodes: [
+ placed(
+ "checkout/trust",
+ "checkout",
{
- id: "checkout-trust",
- under: "checkout",
- relates: [{ to: "core-trust", as: "reinforces" }],
+ relates: [{ to: "core/trust", as: "reinforces" }],
incarnation: "web",
},
"Reduce felt risk near payment.",
),
],
});
- const node = graph.nodes.get("checkout-trust");
+ const node = graph.nodes.get("checkout/trust");
expect(node?.origin).toBe("node-file");
expect(node?.body).toBe("Reduce felt risk near payment.");
expect(node?.incarnation).toBe("web");
- expect(node?.relates).toEqual([{ to: "core-trust", as: "reinforces" }]);
+ expect(node?.relates).toEqual([{ to: "core/trust", as: "reinforces" }]);
+ expect(node?.parent).toBe("checkout");
});
- it("seeds the containment tree from surfaces and resolves ancestors", () => {
+ it("seeds the containment tree from directory parents and resolves ancestors", () => {
const graph = assembleGraph({
- surfaces: {
- schema: "ghost.surfaces/v1",
- surfaces: [
- { id: "checkout", parent: "core" },
- { id: "payment", parent: "checkout" },
- ],
- },
+ placedNodes: [
+ placed("checkout", "core"),
+ placed("checkout/payment", "checkout"),
+ ],
});
- expect(graph.parents.get("payment")).toBe("checkout");
+ expect(graph.parents.get("checkout/payment")).toBe("checkout");
expect(graph.parents.get("checkout")).toBe(GHOST_GRAPH_ROOT_ID);
- expect(ancestorChain(graph, "payment")).toEqual([
+ expect(ancestorChain(graph, "checkout/payment")).toEqual([
"checkout",
GHOST_GRAPH_ROOT_ID,
]);
});
- it("attaches an under-less node to the implicit core root", () => {
+ it("seeds intermediate directories that have no index node", () => {
+ // Only the deep leaf is placed; a/b and a are empty directories.
+ const graph = assembleGraph({
+ placedNodes: [placed("a/b/c", "a/b")],
+ });
+ expect(ancestorChain(graph, "a/b/c")).toEqual([
+ "a/b",
+ "a",
+ GHOST_GRAPH_ROOT_ID,
+ ]);
+ });
+
+ it("treats a parentless node as the implicit core root", () => {
const graph = assembleGraph({
- nodeFiles: [nodeDoc({ id: "top-level" })],
+ placedNodes: [placed("core", undefined, {}, "Root prose.")],
});
- expect(ancestorChain(graph, "top-level")).toEqual([GHOST_GRAPH_ROOT_ID]);
+ expect(graph.nodes.get(GHOST_GRAPH_ROOT_ID)?.body).toBe("Root prose.");
});
it("records children for downward traversal", () => {
const graph = assembleGraph({
- surfaces: {
- schema: "ghost.surfaces/v1",
- surfaces: [
- { id: "checkout", parent: "core" },
- { id: "email", parent: "core" },
- ],
- },
+ placedNodes: [placed("checkout", "core"), placed("email", "core")],
});
expect(graph.children.get(GHOST_GRAPH_ROOT_ID)?.sort()).toEqual([
"checkout",
diff --git a/packages/ghost/test/ghost-core/graph-slice.test.ts b/packages/ghost/test/ghost-core/graph-slice.test.ts
index 6caff93c..511c5c70 100644
--- a/packages/ghost/test/ghost-core/graph-slice.test.ts
+++ b/packages/ghost/test/ghost-core/graph-slice.test.ts
@@ -1,25 +1,23 @@
import { describe, expect, it } from "vitest";
import {
assembleGraph,
- type GhostNodeDocument,
+ type PlacedNode,
resolveGraphSlice,
} from "../../src/ghost-core/index.js";
-function nodeDoc(
- frontmatter: GhostNodeDocument["frontmatter"],
+function placed(
+ id: string,
+ parent: string | undefined,
+ frontmatter: PlacedNode["doc"]["frontmatter"] = {},
body = "Prose.",
-): GhostNodeDocument {
- return { frontmatter, body };
+): PlacedNode {
+ return {
+ id,
+ ...(parent !== undefined ? { parent } : {}),
+ doc: { frontmatter, body },
+ };
}
-const surfaces = {
- schema: "ghost.surfaces/v1" as const,
- surfaces: [
- { id: "checkout", parent: "core" },
- { id: "payment", parent: "checkout" },
- ],
-};
-
function provenanceOf(slice: ReturnType, id: string) {
return slice.nodes.find((n) => n.id === id)?.provenance;
}
@@ -27,46 +25,46 @@ function provenanceOf(slice: ReturnType, id: string) {
describe("resolveGraphSlice", () => {
it("tags own, ancestor, and edge provenance", () => {
const graph = assembleGraph({
- surfaces,
- nodeFiles: [
- nodeDoc({ id: "brand-voice", under: "core" }, "Calm everywhere."),
- nodeDoc(
- {
- id: "checkout-trust",
- under: "checkout",
- relates: [{ to: "density", as: "contrasts" }],
- },
+ placedNodes: [
+ placed("brand-voice", "core", {}, "Calm everywhere."),
+ placed(
+ "checkout/trust",
+ "checkout",
+ { relates: [{ to: "dashboard/density", as: "contrasts" }] },
"Reduce felt risk.",
),
- nodeDoc({ id: "density", under: "dashboard" }, "Pack it in."),
+ placed("dashboard/density", "dashboard", {}, "Pack it in."),
],
});
const slice = resolveGraphSlice(graph, "checkout");
- expect(provenanceOf(slice, "checkout-trust")).toEqual({ kind: "own" });
+ expect(provenanceOf(slice, "checkout/trust")).toEqual({ kind: "own" });
expect(provenanceOf(slice, "brand-voice")).toEqual({
kind: "ancestor",
from: "core",
});
- expect(provenanceOf(slice, "density")).toEqual({
+ expect(provenanceOf(slice, "dashboard/density")).toEqual({
kind: "edge",
via: "contrasts",
- from: "checkout-trust",
+ from: "checkout/trust",
});
});
it("cascades through multiple ancestor levels", () => {
const graph = assembleGraph({
- surfaces,
- nodeFiles: [
- nodeDoc({ id: "brand-voice", under: "core" }, "Calm."),
- nodeDoc({ id: "checkout-clarity", under: "checkout" }, "Plain."),
- nodeDoc({ id: "pay-now", under: "payment" }, "One tap."),
+ placedNodes: [
+ placed("brand-voice", "core", {}, "Calm."),
+ placed("checkout", "core", {}, "Checkout surface."),
+ placed("checkout/clarity", "checkout", {}, "Plain."),
+ placed("checkout/payment", "checkout", {}, "Payment surface."),
+ placed("checkout/payment/pay-now", "checkout/payment", {}, "One tap."),
],
});
- const slice = resolveGraphSlice(graph, "payment");
- expect(provenanceOf(slice, "pay-now")).toEqual({ kind: "own" });
- expect(provenanceOf(slice, "checkout-clarity")).toEqual({
+ const slice = resolveGraphSlice(graph, "checkout/payment");
+ expect(provenanceOf(slice, "checkout/payment/pay-now")).toEqual({
+ kind: "own",
+ });
+ expect(provenanceOf(slice, "checkout/clarity")).toEqual({
kind: "ancestor",
from: "checkout",
});
@@ -79,15 +77,13 @@ describe("resolveGraphSlice", () => {
it("filters by incarnation: essence always in, matching in, mismatched out", () => {
const graph = assembleGraph({
- surfaces,
- nodeFiles: [
- nodeDoc({ id: "brand-voice", under: "core" }, "Calm."), // essence
- nodeDoc(
- { id: "checkout-web", under: "checkout", incarnation: "web" },
- "Inline.",
- ),
- nodeDoc(
- { id: "checkout-mail", under: "checkout", incarnation: "email" },
+ placedNodes: [
+ placed("brand-voice", "core", {}, "Calm."), // essence
+ placed("checkout/web", "checkout", { incarnation: "web" }, "Inline."),
+ placed(
+ "checkout/mail",
+ "checkout",
+ { incarnation: "email" },
"Subject.",
),
],
@@ -95,47 +91,46 @@ describe("resolveGraphSlice", () => {
const slice = resolveGraphSlice(graph, "checkout", { incarnation: "web" });
const ids = slice.nodes.map((n) => n.id).sort();
expect(ids).toContain("brand-voice"); // essence
- expect(ids).toContain("checkout-web"); // matches
- expect(ids).not.toContain("checkout-mail"); // mismatched
+ expect(ids).toContain("checkout/web"); // matches
+ expect(ids).not.toContain("checkout/mail"); // mismatched
expect(slice.incarnation).toBe("web");
});
it("includes every node when no incarnation filter is given", () => {
const graph = assembleGraph({
- surfaces,
- nodeFiles: [
- nodeDoc(
- { id: "checkout-web", under: "checkout", incarnation: "web" },
- "x",
- ),
- nodeDoc(
- { id: "checkout-mail", under: "checkout", incarnation: "email" },
- "y",
- ),
+ placedNodes: [
+ placed("checkout/web", "checkout", { incarnation: "web" }, "x"),
+ placed("checkout/mail", "checkout", { incarnation: "email" }, "y"),
],
});
const slice = resolveGraphSlice(graph, "checkout");
const ids = slice.nodes.map((n) => n.id).sort();
- expect(ids).toEqual(["checkout-mail", "checkout-web"]);
+ expect(ids).toEqual(["checkout/mail", "checkout/web"]);
expect(slice.incarnation).toBeUndefined();
});
it("follows relates edges one hop only (no recursion)", () => {
const graph = assembleGraph({
- surfaces,
- nodeFiles: [
- nodeDoc(
- { id: "a", under: "checkout", relates: [{ to: "b" }] },
+ placedNodes: [
+ placed(
+ "checkout/a",
+ "checkout",
+ { relates: [{ to: "dashboard/b" }] },
"node a",
),
- nodeDoc({ id: "b", under: "dashboard", relates: [{ to: "c" }] }, "b"),
- nodeDoc({ id: "c", under: "dashboard" }, "c"),
+ placed(
+ "dashboard/b",
+ "dashboard",
+ { relates: [{ to: "dashboard/c" }] },
+ "b",
+ ),
+ placed("dashboard/c", "dashboard", {}, "c"),
],
});
const slice = resolveGraphSlice(graph, "checkout");
const ids = slice.nodes.map((n) => n.id);
- expect(ids).toContain("a"); // own
- expect(ids).toContain("b"); // one hop from a
- expect(ids).not.toContain("c"); // two hops — excluded
+ expect(ids).toContain("checkout/a"); // own
+ expect(ids).toContain("dashboard/b"); // one hop from a
+ expect(ids).not.toContain("dashboard/c"); // two hops — excluded
});
});
diff --git a/packages/ghost/test/ghost-core/node-schema.test.ts b/packages/ghost/test/ghost-core/node-schema.test.ts
index bbf5f7b3..72b295cb 100644
--- a/packages/ghost/test/ghost-core/node-schema.test.ts
+++ b/packages/ghost/test/ghost-core/node-schema.test.ts
@@ -3,6 +3,8 @@ import {
GHOST_NODE_RELATION_KINDS,
type GhostNodeDocument,
lintGhostNode,
+ NodeIdSchema,
+ NodeRefSchema,
parseNode,
serializeNode,
} from "../../src/ghost-core/node/index.js";
@@ -12,25 +14,12 @@ function node(frontmatter: string, body = "Prose body."): string {
}
describe("ghost.node/v1 schema", () => {
- it("parses and validates a minimal node (id only)", () => {
- const { node: doc, report } = parseNode(node("id: checkout"));
+ it("parses and validates a minimal node (empty frontmatter)", () => {
+ const { node: doc, report } = parseNode(node(""));
expect(report.errors).toBe(0);
- expect(doc?.frontmatter.id).toBe("checkout");
expect(doc?.body).toBe("Prose body.");
});
- it("accepts dashed and dotted ids (permissive charset)", () => {
- for (const id of ["core", "checkout-trust-signals", "email.marketing"]) {
- expect(lintGhostNode(node(`id: ${id}`)).errors).toBe(0);
- }
- });
-
- it("rejects only genuinely malformed ids", () => {
- for (const id of ["Checkout", "-leading", "_leading"]) {
- expect(lintGhostNode(node(`id: ${id}`)).errors).toBeGreaterThan(0);
- }
- });
-
it("errors when frontmatter is missing", () => {
const report = lintGhostNode("# just a heading\n\nno frontmatter");
expect(report.errors).toBe(1);
@@ -40,62 +29,40 @@ describe("ghost.node/v1 schema", () => {
it("accepts the closed relates qualifier set and rejects unknowns", () => {
for (const as of GHOST_NODE_RELATION_KINDS) {
const report = lintGhostNode(
- node(`id: a\nrelates:\n - to: core\n as: ${as}`),
+ node(`relates:\n - to: core\n as: ${as}`),
);
expect(report.errors).toBe(0);
}
- const bad = lintGhostNode(
- node("id: a\nrelates:\n - to: core\n as: governs"),
- );
+ const bad = lintGhostNode(node("relates:\n - to: core\n as: governs"));
expect(bad.errors).toBeGreaterThan(0);
});
it("allows untyped relations (qualifier omitted)", () => {
- const report = lintGhostNode(node("id: a\nrelates:\n - to: core"));
- expect(report.errors).toBe(0);
- });
-
- it("accepts local and cross-package refs in under/relates", () => {
- const report = lintGhostNode(
- node(
- "id: checkout-trust\nunder: checkout\nrelates:\n - to: 'brand:core-trust'\n as: reinforces",
- ),
- );
+ const report = lintGhostNode(node("relates:\n - to: core"));
expect(report.errors).toBe(0);
});
- it("rejects malformed refs", () => {
- expect(
- lintGhostNode(node("id: a\nunder: 'Bad Ref'")).errors,
- ).toBeGreaterThan(0);
- });
-
it("accepts an arbitrary incarnation string", () => {
- expect(lintGhostNode(node("id: a\nincarnation: billboard")).errors).toBe(0);
- expect(lintGhostNode(node("id: a\nincarnation: voice-kiosk")).errors).toBe(
- 0,
- );
+ expect(lintGhostNode(node("incarnation: billboard")).errors).toBe(0);
+ expect(lintGhostNode(node("incarnation: voice-kiosk")).errors).toBe(0);
});
it("passes through free-form descriptive keys (e.g. audience)", () => {
// Authors may add descriptive keys; Ghost does not gate on them.
- expect(lintGhostNode(node("id: a\naudience: enterprise")).errors).toBe(0);
+ expect(lintGhostNode(node("audience: enterprise")).errors).toBe(0);
});
it("accepts a description (the retrieval payload)", () => {
- expect(
- lintGhostNode(node("id: email\ndescription: Lifecycle email.")).errors,
- ).toBe(0);
+ expect(lintGhostNode(node("description: Lifecycle email.")).errors).toBe(0);
});
- it("round-trips through serialize/parse", () => {
+ it("round-trips through serialize/parse (frontmatter is properties only)", () => {
const original: GhostNodeDocument = {
frontmatter: {
- id: "checkout-trust-signals",
- under: "checkout",
+ description: "Near payment, reduce felt risk.",
relates: [
- { to: "core-trust", as: "reinforces" },
- { to: "checkout-density" },
+ { to: "core/trust", as: "reinforces" },
+ { to: "checkout/density" },
],
incarnation: "web",
},
@@ -107,9 +74,66 @@ describe("ghost.node/v1 schema", () => {
expect(reparsed.node?.body).toBe(original.body);
});
+ it("round-trips an empty-frontmatter node", () => {
+ const original: GhostNodeDocument = {
+ frontmatter: {},
+ body: "Just prose.",
+ };
+ const reparsed = parseNode(serializeNode(original));
+ expect(reparsed.report.errors).toBe(0);
+ expect(reparsed.node?.frontmatter).toEqual({});
+ expect(reparsed.node?.body).toBe("Just prose.");
+ });
+
it("preserves the body verbatim, stripping only frontmatter", () => {
const body = "# Heading\n\n- a list item\n\nA paragraph with `code`.";
- const { node: doc } = parseNode(node("id: a", body));
+ const { node: doc } = parseNode(node("", body));
expect(doc?.body).toBe(body);
});
});
+
+describe("node id / ref grammar (path-based identity)", () => {
+ it("accepts flat and nested path ids", () => {
+ for (const id of [
+ "core",
+ "checkout",
+ "checkout-trust-signals",
+ "marketing/email",
+ "a/b/c",
+ "email.marketing",
+ ]) {
+ expect(NodeIdSchema.safeParse(id).success).toBe(true);
+ }
+ });
+
+ it("rejects malformed ids", () => {
+ for (const id of [
+ "Checkout",
+ "-leading",
+ "_leading",
+ "/leading-slash",
+ "trailing-slash/",
+ "double//slash",
+ "Bad Ref",
+ ]) {
+ expect(NodeIdSchema.safeParse(id).success).toBe(false);
+ }
+ });
+
+ it("accepts local path refs and cross-package refs", () => {
+ for (const ref of [
+ "core",
+ "marketing/email",
+ "brand:core/trust",
+ "brand:core",
+ ]) {
+ expect(NodeRefSchema.safeParse(ref).success).toBe(true);
+ }
+ });
+
+ it("rejects malformed refs", () => {
+ for (const ref of ["Bad Ref", "/x", "x/", "a//b"]) {
+ expect(NodeRefSchema.safeParse(ref).success).toBe(false);
+ }
+ });
+});
diff --git a/packages/ghost/test/ghost-core/surfaces-lint.test.ts b/packages/ghost/test/ghost-core/surfaces-lint.test.ts
deleted file mode 100644
index 7a2725b9..00000000
--- a/packages/ghost/test/ghost-core/surfaces-lint.test.ts
+++ /dev/null
@@ -1,102 +0,0 @@
-import { describe, expect, it } from "vitest";
-import {
- GHOST_SURFACES_SCHEMA,
- lintGhostSurfaces,
-} from "../../src/ghost-core/surfaces/index.js";
-
-function doc(surfaces: unknown[]) {
- return { schema: GHOST_SURFACES_SCHEMA, surfaces };
-}
-
-function rules(report: { issues: { rule: string }[] }): string[] {
- return report.issues.map((issue) => issue.rule);
-}
-
-describe("lintGhostSurfaces", () => {
- it("passes a valid tree (id + parent + description)", () => {
- const report = lintGhostSurfaces(
- doc([
- { id: "core", description: "True everywhere." },
- { id: "email", parent: "core" },
- { id: "email-marketing", parent: "email" },
- { id: "checkout", parent: "core" },
- ]),
- );
-
- expect(report.issues).toEqual([]);
- expect(report.errors).toBe(0);
- });
-
- it("allows parent: core without an explicit core surface (implicit root)", () => {
- const report = lintGhostSurfaces(doc([{ id: "email", parent: "core" }]));
-
- expect(report.errors).toBe(0);
- });
-
- it("errors on a parent that matches no surface", () => {
- const report = lintGhostSurfaces(
- doc([{ id: "email-marketing", parent: "emial" }]),
- );
-
- expect(rules(report)).toContain("surface-parent-unknown");
- expect(report.errors).toBeGreaterThan(0);
- });
-
- it("warns with a near-miss suggestion for an unknown parent close to a real id", () => {
- const report = lintGhostSurfaces(
- doc([
- { id: "email", parent: "core" },
- { id: "marketing", parent: "emial" },
- ]),
- );
-
- const nearMiss = report.issues.find(
- (issue) => issue.rule === "surface-id-near-miss",
- );
- expect(nearMiss?.severity).toBe("warning");
- expect(nearMiss?.message).toContain("email");
- });
-
- it("errors when core declares a parent", () => {
- const report = lintGhostSurfaces(doc([{ id: "core", parent: "root" }]));
-
- expect(rules(report)).toContain("surface-core-reserved");
- });
-
- it("errors on a parent cycle", () => {
- const report = lintGhostSurfaces(
- doc([
- { id: "a", parent: "b" },
- { id: "b", parent: "a" },
- ]),
- );
-
- expect(rules(report)).toContain("surface-parent-cycle");
- });
-
- it("errors on a self-parent", () => {
- const report = lintGhostSurfaces(doc([{ id: "a", parent: "a" }]));
-
- expect(rules(report)).toContain("surface-parent-cycle");
- });
-
- it("errors on duplicate ids", () => {
- const report = lintGhostSurfaces(
- doc([
- { id: "email", parent: "core" },
- { id: "email", parent: "core" },
- ]),
- );
-
- expect(rules(report)).toContain("duplicate-id");
- });
-
- it("reports schema failures as issues rather than throwing", () => {
- const report = lintGhostSurfaces(
- doc([{ id: "email.marketing", parent: "email" }]),
- );
-
- expect(report.errors).toBeGreaterThan(0);
- expect(report.issues[0]?.rule).toContain("schema/");
- });
-});
diff --git a/packages/ghost/test/ghost-core/surfaces-schema.test.ts b/packages/ghost/test/ghost-core/surfaces-schema.test.ts
deleted file mode 100644
index 13d39dd6..00000000
--- a/packages/ghost/test/ghost-core/surfaces-schema.test.ts
+++ /dev/null
@@ -1,86 +0,0 @@
-import { describe, expect, it } from "vitest";
-import {
- GHOST_SURFACES_SCHEMA,
- GhostSurfacesSchema,
-} from "../../src/ghost-core/surfaces/index.js";
-
-describe("ghost.surfaces/v1", () => {
- it("accepts a minimal document and defaults surfaces to []", () => {
- const result = GhostSurfacesSchema.safeParse({
- schema: GHOST_SURFACES_SCHEMA,
- });
-
- expect(result.success).toBe(true);
- if (!result.success) throw new Error("minimal surfaces.yml should parse");
- expect(result.data).toEqual({
- schema: GHOST_SURFACES_SCHEMA,
- surfaces: [],
- });
- });
-
- it("accepts a realistic tree (id + parent + optional description)", () => {
- const result = GhostSurfacesSchema.safeParse({
- schema: GHOST_SURFACES_SCHEMA,
- surfaces: [
- { id: "core", description: "True everywhere." },
- { id: "email", description: "Lifecycle email.", parent: "core" },
- { id: "email-marketing", parent: "email" },
- { id: "checkout", parent: "core" },
- ],
- });
-
- expect(result.success).toBe(true);
- });
-
- it("rejects a dotted id (the tree lives only in parent)", () => {
- const result = GhostSurfacesSchema.safeParse({
- schema: GHOST_SURFACES_SCHEMA,
- surfaces: [{ id: "email.marketing", parent: "email" }],
- });
-
- expect(result.success).toBe(false);
- if (result.success) throw new Error("dotted id must be rejected");
- expect(result.error.issues[0]?.message).toContain("flat slug");
- });
-
- it("rejects a parent given as an array (single parent only)", () => {
- const result = GhostSurfacesSchema.safeParse({
- schema: GHOST_SURFACES_SCHEMA,
- surfaces: [{ id: "email-marketing", parent: ["email", "marketing"] }],
- });
-
- expect(result.success).toBe(false);
- });
-
- it("rejects an unknown surface key (strict; edges are gone)", () => {
- const result = GhostSurfacesSchema.safeParse({
- schema: GHOST_SURFACES_SCHEMA,
- surfaces: [
- { id: "checkout", edges: [{ kind: "composes", to: "payments" }] },
- ],
- });
-
- expect(result.success).toBe(false);
- });
-
- it("rejects an unknown top-level key (strict)", () => {
- const result = GhostSurfacesSchema.safeParse({
- schema: GHOST_SURFACES_SCHEMA,
- surfaces: [],
- routes: [],
- });
-
- expect(result.success).toBe(false);
- });
-
- it("accepts a parent that does not exist as a surface", () => {
- // INTENTIONAL: dangling-reference detection is a lint concern, not a schema
- // concern. Zod validates a position in isolation and cannot see the tree.
- const result = GhostSurfacesSchema.safeParse({
- schema: GHOST_SURFACES_SCHEMA,
- surfaces: [{ id: "checkout", parent: "nonexistent" }],
- });
-
- expect(result.success).toBe(true);
- });
-});
diff --git a/packages/ghost/test/migrate-legacy.test.ts b/packages/ghost/test/migrate-legacy.test.ts
index 7fb597c2..1f9282c3 100644
--- a/packages/ghost/test/migrate-legacy.test.ts
+++ b/packages/ghost/test/migrate-legacy.test.ts
@@ -1,9 +1,5 @@
import { describe, expect, it } from "vitest";
-import {
- GhostSurfacesSchema,
- lintGhostNode,
- lintGhostSurfaces,
-} from "../src/ghost-core/index.js";
+import { lintGhostNode } from "../src/ghost-core/index.js";
import {
type LegacyPackageInput,
looksLegacy,
@@ -58,12 +54,9 @@ function legacy(): LegacyPackageInput {
}
describe("migrateLegacyPackage", () => {
- it("derives surfaces.yml from topology.scopes", () => {
- const { surfaces } = migrateLegacyPackage(legacy());
- const parsed = GhostSurfacesSchema.safeParse(surfaces);
- expect(parsed.success).toBe(true);
- const ids = (surfaces.surfaces as Array<{ id: string }>).map((s) => s.id);
- expect(ids).toEqual(["lending", "checkout"]);
+ it("derives surface directories from topology.scopes", () => {
+ const { surfaceIds } = migrateLegacyPackage(legacy());
+ expect(surfaceIds).toEqual(["lending", "checkout"]);
});
it("places single-scope nodes via surface: and strips legacy fields", () => {
@@ -126,18 +119,23 @@ describe("migrateLegacyPackage", () => {
expect(principles[0]).toHaveProperty("applies_to");
});
- it("produces a node package: valid surfaces + parseable nodes", () => {
+ it("produces a directory tree of parseable nodes", () => {
const result = migrateLegacyPackage(legacy());
- expect(lintGhostSurfaces(result.surfaces).errors).toBe(0);
-
- // The migration emits one prose node per facet entry.
+ // The migration emits one prose node per facet entry, plus an index.md per
+ // derived surface directory. Placed nodes land under their surface dir.
const files = migratedNodeFiles(result);
expect(files.length).toBeGreaterThan(0);
for (const file of files) {
- expect(file.relativePath).toMatch(/^nodes\/.+\.md$/);
+ expect(file.relativePath).toMatch(/\.md$/);
expect(lintGhostNode(file.content).errors).toBe(0);
}
+ // Each derived surface gets its index.md directory marker.
+ expect(files.some((f) => f.relativePath === "lending/index.md")).toBe(true);
+ // A single-scope node lands inside its surface directory.
+ expect(
+ files.some((f) => f.relativePath === "lending/single-scope.md"),
+ ).toBe(true);
});
});
diff --git a/packages/ghost/test/scan-status.test.ts b/packages/ghost/test/scan-status.test.ts
index c7459b69..d1a9a917 100644
--- a/packages/ghost/test/scan-status.test.ts
+++ b/packages/ghost/test/scan-status.test.ts
@@ -1,6 +1,6 @@
import { mkdir, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
-import { join } from "node:path";
+import { dirname, join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { scanStatus } from "../src/scan/scan-status.js";
@@ -39,41 +39,33 @@ describe("scanStatus contribution", () => {
expect(status.contribution.node_count).toBe(0);
});
- it("reports node contribution and surface coverage", async () => {
- await writePackage(
- dir,
- `schema: ghost.surfaces/v1
-surfaces:
- - id: checkout
- parent: core
- - id: email
- parent: core
-`,
- {
- "core-voice.md": "---\nid: core-voice\nunder: core\n---\n\nCalm.\n",
- "checkout-trust.md":
- "---\nid: checkout-trust\nunder: checkout\nincarnation: web\n---\n\nReassure.\n",
- },
- );
+ it("reports node contribution and surface coverage over the directory tree", async () => {
+ await writePackage(dir, {
+ // The core root prose (essence).
+ "index.md": "---\n---\n\nCalm.\n",
+ // The checkout surface directory, with one placed node (web incarnation).
+ "checkout/index.md": "---\n---\n\nCheckout surface.\n",
+ "checkout/trust.md": "---\nincarnation: web\n---\n\nReassure.\n",
+ });
const status = await scanStatus(join(dir, ".ghost"));
expect(status.contribution.state).toBe("contributing");
- expect(status.contribution.node_count).toBe(2);
- expect(status.contribution.essence_count).toBe(1);
+ // 3 authored nodes: root index + checkout/index + checkout/trust.
+ expect(status.contribution.node_count).toBe(3);
+ // Two essence (the two index nodes) + one web-tagged (checkout/trust).
+ expect(status.contribution.essence_count).toBe(2);
expect(status.contribution.incarnation_count).toBe(1);
+ // `checkout` is an interior directory holding one node.
const checkout = status.contribution.surfaces.find(
(s) => s.id === "checkout",
);
expect(checkout?.node_count).toBe(1);
- // email surface declared but has no nodes → sparse.
- expect(status.contribution.sparse_surfaces).toContain("email");
});
});
async function writePackage(
dir: string,
- surfacesYml?: string,
nodes?: Record,
): Promise {
await mkdir(join(dir, ".ghost"), { recursive: true });
@@ -81,13 +73,11 @@ async function writePackage(
join(dir, ".ghost", "manifest.yml"),
"schema: ghost.fingerprint-package/v1\nid: local\n",
);
- if (surfacesYml) {
- await writeFile(join(dir, ".ghost", "surfaces.yml"), surfacesYml);
- }
if (nodes) {
- await mkdir(join(dir, ".ghost", "nodes"), { recursive: true });
- for (const [name, content] of Object.entries(nodes)) {
- await writeFile(join(dir, ".ghost", "nodes", name), content);
+ for (const [relPath, content] of Object.entries(nodes)) {
+ const full = join(dir, ".ghost", relPath);
+ await mkdir(dirname(full), { recursive: true });
+ await writeFile(full, content);
}
}
}
From bfc8258115dfb1b9584e84a613b5b613176eb3a2 Mon Sep 17 00:00:00 2001
From: Nahiyan Khan
Date: Sun, 28 Jun 2026 19:58:51 -0400
Subject: [PATCH 03/12] feat(graph): carry each node's file folder onto the
graph
The corridor model keys composition off a node's file folder (the directory its
source sits in), which diverges from graph-parent for index nodes: features/
bitcoin/index.md has folder features/bitcoin but parent features. Thread folder
through PlacedNode and GhostGraphNode; inherited nodes get a package-qualified
folder so they never sit on a local corridor. Purely additive.
---
.../ghost/src/ghost-core/graph/assemble.ts | 7 ++++
packages/ghost/src/ghost-core/graph/types.ts | 10 +++++
.../src/scan/fingerprint-package-layers.ts | 4 ++
packages/ghost/src/scan/node-tree.ts | 37 +++++++++++++------
4 files changed, 46 insertions(+), 12 deletions(-)
diff --git a/packages/ghost/src/ghost-core/graph/assemble.ts b/packages/ghost/src/ghost-core/graph/assemble.ts
index dc42f916..3cda91be 100644
--- a/packages/ghost/src/ghost-core/graph/assemble.ts
+++ b/packages/ghost/src/ghost-core/graph/assemble.ts
@@ -13,6 +13,12 @@ import {
export interface PlacedNode {
id: string;
parent?: string;
+ /**
+ * The node's file folder — the directory its source file sits in. For an
+ * index node this is its own id (`a/b/index.md` → `a/b`); for a leaf it is
+ * the parent (`a/b.md` → `a`); for the root `index.md` it is `""`.
+ */
+ folder: string;
doc: GhostNodeDocument;
}
@@ -52,6 +58,7 @@ export function assembleGraph(input: AssembleGraphInput): GhostGraph {
id,
...(fm.description !== undefined ? { description: fm.description } : {}),
...(placed.parent !== undefined ? { parent: placed.parent } : {}),
+ folder: placed.folder,
relates: fm.relates ?? [],
...(fm.incarnation !== undefined ? { incarnation: fm.incarnation } : {}),
body: placed.doc.body,
diff --git a/packages/ghost/src/ghost-core/graph/types.ts b/packages/ghost/src/ghost-core/graph/types.ts
index 8d64f62e..d59e24bb 100644
--- a/packages/ghost/src/ghost-core/graph/types.ts
+++ b/packages/ghost/src/ghost-core/graph/types.ts
@@ -28,6 +28,16 @@ export interface GhostGraphNode {
description?: string;
/** The containing directory's id; absent ⇒ this node is the `core` root. */
parent?: string;
+ /**
+ * The node's **file folder** — the directory the source file physically sits
+ * in, which is the unit of containment for corridor composition. This differs
+ * from `parent` for index nodes: `features/bitcoin/index.md` has folder
+ * `features/bitcoin` but parent `features`. A plain leaf shares its parent's
+ * value (`features/bitcoin/buy.md` → folder `features/bitcoin`). The root
+ * `core` node has folder `""`. Folders are walls: a node only cascades into
+ * surfaces whose folder chain includes this folder.
+ */
+ folder: string;
relates: GhostNodeRelation[];
incarnation?: string;
body: string;
diff --git a/packages/ghost/src/scan/fingerprint-package-layers.ts b/packages/ghost/src/scan/fingerprint-package-layers.ts
index 24ee6c7e..16baf9f5 100644
--- a/packages/ghost/src/scan/fingerprint-package-layers.ts
+++ b/packages/ghost/src/scan/fingerprint-package-layers.ts
@@ -87,6 +87,10 @@ async function loadInheritedNodes(
...(node.description !== undefined
? { description: node.description }
: {}),
+ // Inherited nodes carry a package-qualified folder so they never sit on
+ // a local corridor (folders are walls per package); they enter a slice
+ // only via an explicit cross-package `relates` edge.
+ folder: `${id}:${node.folder}`,
relates: [],
...(node.incarnation !== undefined
? { incarnation: node.incarnation }
diff --git a/packages/ghost/src/scan/node-tree.ts b/packages/ghost/src/scan/node-tree.ts
index 67ce4641..ab991b92 100644
--- a/packages/ghost/src/scan/node-tree.ts
+++ b/packages/ghost/src/scan/node-tree.ts
@@ -96,31 +96,44 @@ async function walk(
continue;
}
- const { id, parent } = locate(relPath);
- nodes.push({ id, ...(parent !== undefined ? { parent } : {}), doc: node });
+ const { id, parent, folder } = locate(relPath);
+ nodes.push({
+ id,
+ ...(parent !== undefined ? { parent } : {}),
+ folder,
+ doc: node,
+ });
}
}
/**
- * Compute a node's id and parent from its package-relative file path.
- * - `index.md` → id `core`, parent absent (the root node).
- * - `a/index.md` → id `a`, parent `core`.
- * - `a/b/index.md` → id `a/b`, parent `a`.
- * - `a.md` → id `a`, parent `core`.
- * - `a/b.md` → id `a/b`, parent `a`.
+ * Compute a node's id, parent, and file folder from its package-relative path.
+ * The folder is the directory the file sits in — the unit of corridor
+ * containment, which differs from `parent` for index nodes.
+ * - `index.md` → id `core`, parent absent, folder ``.
+ * - `a/index.md` → id `a`, parent `core`, folder `a`.
+ * - `a/b/index.md` → id `a/b`, parent `a`, folder `a/b`.
+ * - `a.md` → id `a`, parent `core`, folder ``.
+ * - `a/b.md` → id `a/b`, parent `a`, folder `a`.
*/
-function locate(relPath: string): { id: string; parent?: string } {
+function locate(relPath: string): {
+ id: string;
+ parent?: string;
+ folder: string;
+} {
const withoutExt = relPath.replace(/\.md$/, "");
const segments = withoutExt.split("/");
const isIndex = segments[segments.length - 1] === "index";
const idSegments = isIndex ? segments.slice(0, -1) : segments;
+ // The file folder: drop the filename segment (`index` or the leaf name).
+ const folder = segments.slice(0, -1).join("/");
if (idSegments.length === 0) {
- // Root index.md → the core node.
- return { id: "core" };
+ // Root index.md → the core node, folder is the package root ("").
+ return { id: "core", folder };
}
const id = idSegments.join("/");
const parent =
idSegments.length === 1 ? "core" : idSegments.slice(0, -1).join("/");
- return { id, parent: parent === "core" ? "core" : parent };
+ return { id, parent: parent === "core" ? "core" : parent, folder };
}
From 59acadf5abd87bf35ee779783071cc1425bb8bcd Mon Sep 17 00:00:00 2001
From: Nahiyan Khan
Date: Sun, 28 Jun 2026 19:59:01 -0400
Subject: [PATCH 04/12] feat(gather)!: corridor spine + hub-and-spoke spokes;
fix sibling leak
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Recompose resolveGraphSlice on the agreed model — folders are walls, files fill
the corridor:
- spine (full bodies): every node whose file folder is on the corridor from the
package root down to the surface's own folder. A sibling folder is a wall; its
nodes never enter the slice. This deletes the old leak where every child of an
ancestor (incl. every core-level sibling) cascaded into every surface.
- edges (full bodies, one hop): relates targets of every spine node, so a broad
rule authored once high in the corridor (relates: { to: arcade } on features/)
reaches every descendant.
- spokes (pointers): the surface's own descendants and each edge hub's subtree,
as id + description for the agent to pull on demand.
GraphSlice gains spokes[]; gather markdown gains an Available-to-pull section.
Tests encode the six confirmed behaviors against a synthetic feature-tree.
---
.changeset/corridor-gather.md | 14 +
packages/ghost/src/commands/gather-command.ts | 16 ++
packages/ghost/src/ghost-core/graph/slice.ts | 174 ++++++++----
.../ghost/test/ghost-core/check-route.test.ts | 3 +-
.../ghost/test/ghost-core/graph-fold.test.ts | 4 +
.../ghost/test/ghost-core/graph-slice.test.ts | 259 ++++++++++++------
6 files changed, 328 insertions(+), 142 deletions(-)
create mode 100644 .changeset/corridor-gather.md
diff --git a/.changeset/corridor-gather.md b/.changeset/corridor-gather.md
new file mode 100644
index 00000000..624cb891
--- /dev/null
+++ b/.changeset/corridor-gather.md
@@ -0,0 +1,14 @@
+---
+"@anarchitecture/ghost": minor
+---
+
+Recompose `gather` on a corridor + hub-and-spoke model and fix a sibling-surface
+context leak. A surface's slice is now: a **spine** of full-body nodes from every
+file on the corridor (the package root down to the surface's own folder — folders
+are walls, so sibling folders never leak in), the **edges** reachable in one hop
+from any spine node's `relates` (so a broad rule authored once high in the tree —
+e.g. `relates: { to: arcade }` on `features/` — reaches every descendant), and a
+set of **spokes**: pointer entries (id + description) for the surface's own
+descendants and any edge hub's subtree, which the agent pulls on demand. The
+`GraphSlice` JSON gains a `spokes` array; graph nodes carry their file `folder`.
+Grounding for `checks`/`review` remains the full-body spine + edges.
diff --git a/packages/ghost/src/commands/gather-command.ts b/packages/ghost/src/commands/gather-command.ts
index 38efbeab..5a2a9267 100644
--- a/packages/ghost/src/commands/gather-command.ts
+++ b/packages/ghost/src/commands/gather-command.ts
@@ -158,5 +158,21 @@ function formatSliceMarkdown(slice: GraphSlice): string {
}
}
+ // Spokes: pointers the agent may pull on demand (descendants + edge hubs).
+ if (slice.spokes.length > 0) {
+ lines.push(
+ "",
+ "## Available to pull",
+ "",
+ "Pointers to nearby context — run `ghost gather ` to expand.",
+ "",
+ );
+ for (const spoke of slice.spokes) {
+ const via = spoke.kind === "edge-hub" ? ` _(via \`${spoke.hub}\`)_` : "";
+ lines.push(`- \`${spoke.id}\`${via}`);
+ if (spoke.description) lines.push(` - ${spoke.description}`);
+ }
+ }
+
return `${lines.join("\n")}\n`;
}
diff --git a/packages/ghost/src/ghost-core/graph/slice.ts b/packages/ghost/src/ghost-core/graph/slice.ts
index fd94de51..60be1caa 100644
--- a/packages/ghost/src/ghost-core/graph/slice.ts
+++ b/packages/ghost/src/ghost-core/graph/slice.ts
@@ -3,10 +3,10 @@ import { ancestorChain } from "./assemble.js";
import { GHOST_GRAPH_ROOT_ID, type GhostGraph } from "./types.js";
/**
- * Why a node is present in a resolved slice.
- * - `own`: placed directly on the requested surface.
- * - `ancestor`: placed on an ancestor and cascaded down the tree.
- * - `edge`: contributed by a typed `relates` link from a slice node (one hop).
+ * Why a full-body node is present in a resolved slice.
+ * - `own`: a file in the requested surface's own folder.
+ * - `ancestor`: a file in a folder higher on the corridor (root → surface).
+ * - `edge`: contributed by a typed `relates` link from a spine node (one hop).
*/
export type GraphSliceProvenance =
| { kind: "own" }
@@ -20,6 +20,24 @@ export interface GraphSliceNode {
provenance: GraphSliceProvenance;
}
+/**
+ * A spoke: a node offered as a pointer (id + description, no body) for the agent
+ * to pull on demand. Spokes are navigable optionality, never authoritative
+ * context.
+ * - `descendant`: a node within or below the requested surface's own subtree.
+ * - `edge-hub`: a node within or below an `edge` target's subtree (a hub the
+ * surface `relates` to unfolds its menu).
+ */
+export type GraphSpokeKind = "descendant" | "edge-hub";
+
+export interface GraphSlicePointer {
+ id: string;
+ description?: string;
+ kind: GraphSpokeKind;
+ /** For an `edge-hub` spoke, the hub id it belongs to. */
+ hub?: string;
+}
+
export interface GraphSlice {
/** The requested node/surface id. */
surface: string;
@@ -27,7 +45,10 @@ export interface GraphSlice {
ancestors: string[];
/** The `--as` incarnation filter applied, if any. */
incarnation?: string;
+ /** Full-body context: the corridor spine plus one-hop `relates` edges. */
nodes: GraphSliceNode[];
+ /** Pointers (id + description) the agent may pull: descendants + edge hubs. */
+ spokes: GraphSlicePointer[];
}
export interface ResolveGraphSliceOptions {
@@ -36,17 +57,23 @@ export interface ResolveGraphSliceOptions {
}
/**
- * Compose a context slice for a surface by traversing the graph, deterministic
- * and with no I/O or LLM:
+ * Compose a context slice for a surface by traversing the graph — deterministic,
+ * no I/O, no LLM. The model is "folders are walls; files fill the corridor":
*
- * - own: nodes placed directly on the requested id;
- * - ancestor: nodes on each `under` ancestor up to `core` cascade down;
- * - edge: for each slice node's `relates`, the target node's body is included
- * once (one hop, no recursion), tagged by the relation qualifier.
+ * - **spine** (`own`/`ancestor`): every node whose **file folder** is on the
+ * surface's corridor — the chain of folders from the package root down to the
+ * surface's own folder. A sibling folder is a wall: its nodes never appear.
+ * Spine nodes are full bodies.
+ * - **edge**: for each spine node's `relates`, the target node's body is
+ * included once (one hop, no recursion), tagged by the relation qualifier.
+ * This is how a broad rule placed high in the corridor (e.g. "all feature UI
+ * draws on Arcade", authored once on `features`) reaches every descendant.
+ * - **spokes**: descendants of the surface's own folder, plus descendants of
+ * each edge target (a hub the surface draws on unfolds its menu), as pointers.
*
- * The `incarnation` option filters: a node with no incarnation (essence) is
- * always included; a tagged node is included only when it matches; absent
- * option means no filtering.
+ * The `incarnation` option filters full-body nodes: essence (untagged) always
+ * passes; a tagged node passes only when it matches. Spokes are unfiltered
+ * pointers.
*/
export function resolveGraphSlice(
graph: GhostGraph,
@@ -54,16 +81,13 @@ export function resolveGraphSlice(
options: ResolveGraphSliceOptions = {},
): GraphSlice {
const ancestorsFull = ancestorChain(graph, surfaceId);
- // Exclude the implicit root from the reported chain (parity with the old
- // resolver, which reported up to but not labeling core specially); keep it in
- // the cascade set so root/essence nodes still cascade.
const ancestors = ancestorsFull.filter((id) => id !== GHOST_GRAPH_ROOT_ID);
- const cascadeIds = new Set([
- surfaceId,
- ...ancestorsFull,
- GHOST_GRAPH_ROOT_ID,
- ]);
+ const surfaceNode = graph.nodes.get(surfaceId);
+ // The surface's own file folder anchors the corridor. For a bare tree
+ // position (a directory with no index node) the folder is the id itself.
+ const surfaceFolder = surfaceNode?.folder ?? surfaceId;
+ const corridor = corridorFolders(surfaceFolder);
const passesIncarnation = (incarnation?: string): boolean => {
if (options.incarnation === undefined) return true;
@@ -78,15 +102,16 @@ export function resolveGraphSlice(
? { incarnation: options.incarnation }
: {}),
nodes: [],
+ spokes: [],
};
- const seen = new Set();
- const add = (id: string, provenance: GraphSliceProvenance) => {
- if (seen.has(id)) return;
+ const seenBody = new Set();
+ const addBody = (id: string, provenance: GraphSliceProvenance): boolean => {
+ if (seenBody.has(id)) return false;
const node = graph.nodes.get(id);
- if (!node) return;
- if (!passesIncarnation(node.incarnation)) return;
- seen.add(id);
+ if (!node) return false;
+ if (!passesIncarnation(node.incarnation)) return false;
+ seenBody.add(id);
slice.nodes.push({
id: node.id,
body: node.body,
@@ -95,42 +120,89 @@ export function resolveGraphSlice(
: {}),
provenance,
});
+ return true;
};
- // Placement of a node: nodes attach to a surface via `under`; nodes whose id
- // *is* a surface in the cascade are themselves placed there. We resolve
- // placement as: a node belongs to surface S if its containment parent chain
- // reaches S directly (its `under` is S), or the node id equals S.
- const placementOf = (nodeParent?: string): string =>
- nodeParent ?? GHOST_GRAPH_ROOT_ID;
-
- // Own + ancestor: walk every node, place it, decide provenance by cascade.
+ // Spine: every node whose file folder is on the corridor. `own` when the
+ // node sits in the surface's own folder; `ancestor` when higher up.
for (const node of graph.nodes.values()) {
- const placement =
- node.id === surfaceId ? surfaceId : placementOf(node.parent);
- if (placement === surfaceId || node.id === surfaceId) {
- add(node.id, { kind: "own" });
- } else if (cascadeIds.has(placement)) {
- add(node.id, { kind: "ancestor", from: placement });
- }
+ if (node.origin === "inherited") continue;
+ if (!corridor.has(node.folder)) continue;
+ const provenance: GraphSliceProvenance =
+ node.folder === surfaceFolder
+ ? { kind: "own" }
+ : { kind: "ancestor", from: node.parent ?? GHOST_GRAPH_ROOT_ID };
+ addBody(node.id, provenance);
}
- // Edge contributions: one hop along `relates` from the nodes already in the
- // slice. The target's body is included, tagged by qualifier.
- const ownAndAncestor = [...slice.nodes];
- for (const sliceNode of ownAndAncestor) {
- const source = graph.nodes.get(sliceNode.id);
+ // Edges: one hop along `relates` from every spine node. The target's body is
+ // included; if the target is a hub, its subtree is offered as spokes.
+ const spineIds = slice.nodes.map((n) => n.id);
+ const edgeTargets: string[] = [];
+ for (const sourceId of spineIds) {
+ const source = graph.nodes.get(sourceId);
if (!source) continue;
for (const relation of source.relates) {
- // A `:` ref resolves to an inherited node, keyed the
- // same way in graph.nodes — `add` no-ops if it isn't present.
- add(relation.to, {
+ const added = addBody(relation.to, {
kind: "edge",
...(relation.as !== undefined ? { via: relation.as } : {}),
- from: sliceNode.id,
+ from: sourceId,
});
+ if (added) edgeTargets.push(relation.to);
+ }
+ }
+
+ // Spokes: descendants of the surface, plus descendants of each edge hub.
+ const seenSpoke = new Set(seenBody);
+ const addSpoke = (id: string, kind: GraphSpokeKind, hub?: string) => {
+ if (seenSpoke.has(id)) return;
+ const node = graph.nodes.get(id);
+ if (!node) return;
+ seenSpoke.add(id);
+ slice.spokes.push({
+ id: node.id,
+ ...(node.description !== undefined
+ ? { description: node.description }
+ : {}),
+ kind,
+ ...(hub !== undefined ? { hub } : {}),
+ });
+ };
+
+ for (const node of graph.nodes.values()) {
+ if (node.origin === "inherited") continue;
+ if (isWithinOrBelow(node.folder, surfaceFolder)) {
+ addSpoke(node.id, "descendant");
+ }
+ }
+ for (const hubId of edgeTargets) {
+ const hub = graph.nodes.get(hubId);
+ if (!hub) continue;
+ for (const node of graph.nodes.values()) {
+ if (isWithinOrBelow(node.folder, hub.folder)) {
+ addSpoke(node.id, "edge-hub", hubId);
+ }
}
}
return slice;
}
+
+/** The set of folders on the corridor from the package root down to `folder`. */
+function corridorFolders(folder: string): Set {
+ const set = new Set([""]); // root files reach everywhere
+ let current = folder;
+ while (current !== "") {
+ set.add(current);
+ const slash = current.lastIndexOf("/");
+ current = slash === -1 ? "" : current.slice(0, slash);
+ }
+ return set;
+}
+
+/** True when `folder` is `base` or nested below it (a descendant position). */
+function isWithinOrBelow(folder: string, base: string): boolean {
+ if (folder === base) return true;
+ if (base === "") return folder !== "";
+ return folder.startsWith(`${base}/`);
+}
diff --git a/packages/ghost/test/ghost-core/check-route.test.ts b/packages/ghost/test/ghost-core/check-route.test.ts
index 5d325477..fd2d66a9 100644
--- a/packages/ghost/test/ghost-core/check-route.test.ts
+++ b/packages/ghost/test/ghost-core/check-route.test.ts
@@ -19,7 +19,8 @@ function check(name: string, surface?: string): GhostCheckDocument {
}
function placed(id: string, parent: string): PlacedNode {
- return { id, parent, doc: { frontmatter: {}, body: "Prose." } };
+ // Directory/index node: its file folder is its own id.
+ return { id, parent, folder: id, doc: { frontmatter: {}, body: "Prose." } };
}
// The directory tree that establishes the surfaces:
diff --git a/packages/ghost/test/ghost-core/graph-fold.test.ts b/packages/ghost/test/ghost-core/graph-fold.test.ts
index c96e9814..e04536f9 100644
--- a/packages/ghost/test/ghost-core/graph-fold.test.ts
+++ b/packages/ghost/test/ghost-core/graph-fold.test.ts
@@ -6,15 +6,19 @@ import {
type PlacedNode,
} from "../../src/ghost-core/index.js";
+// Model an index/directory node: its folder is its own id (`a/b/index.md`).
+// A parentless node is the root `core` (folder ``).
function placed(
id: string,
parent: string | undefined,
frontmatter: PlacedNode["doc"]["frontmatter"] = {},
body = "Prose.",
): PlacedNode {
+ const folder = parent === undefined ? "" : id;
return {
id,
...(parent !== undefined ? { parent } : {}),
+ folder,
doc: { frontmatter, body },
};
}
diff --git a/packages/ghost/test/ghost-core/graph-slice.test.ts b/packages/ghost/test/ghost-core/graph-slice.test.ts
index 511c5c70..3b1650e9 100644
--- a/packages/ghost/test/ghost-core/graph-slice.test.ts
+++ b/packages/ghost/test/ghost-core/graph-slice.test.ts
@@ -5,132 +5,211 @@ import {
resolveGraphSlice,
} from "../../src/ghost-core/index.js";
-function placed(
+/**
+ * Model a node the way the loader does. `folder` is the file's directory — the
+ * unit of corridor containment:
+ * - a root file (`voice.md`) → parent `core`, folder ``.
+ * - a directory index (`a/index.md`)→ parent of `a`, folder `a`.
+ * - a leaf (`a/b.md`) → parent `a`, folder `a`.
+ */
+function root(
id: string,
- parent: string | undefined,
- frontmatter: PlacedNode["doc"]["frontmatter"] = {},
- body = "Prose.",
+ fm: PlacedNode["doc"]["frontmatter"] = {},
): PlacedNode {
- return {
- id,
- ...(parent !== undefined ? { parent } : {}),
- doc: { frontmatter, body },
- };
+ return { id, parent: "core", folder: "", doc: { frontmatter: fm, body: id } };
+}
+function dir(
+ id: string,
+ fm: PlacedNode["doc"]["frontmatter"] = {},
+): PlacedNode {
+ const slash = id.lastIndexOf("/");
+ const parent = slash === -1 ? "core" : id.slice(0, slash);
+ return { id, parent, folder: id, doc: { frontmatter: fm, body: id } };
+}
+function leaf(
+ id: string,
+ fm: PlacedNode["doc"]["frontmatter"] = {},
+): PlacedNode {
+ const slash = id.lastIndexOf("/");
+ const folder = slash === -1 ? "" : id.slice(0, slash);
+ const parent = folder === "" ? "core" : folder;
+ return { id, parent, folder, doc: { frontmatter: fm, body: id } };
}
function provenanceOf(slice: ReturnType, id: string) {
return slice.nodes.find((n) => n.id === id)?.provenance;
}
+const bodyIds = (slice: ReturnType) =>
+ slice.nodes.map((n) => n.id).sort();
+const spokeIds = (slice: ReturnType) =>
+ slice.spokes.map((s) => s.id).sort();
-describe("resolveGraphSlice", () => {
- it("tags own, ancestor, and edge provenance", () => {
- const graph = assembleGraph({
+describe("resolveGraphSlice — corridor + hub-and-spoke", () => {
+ // A cash-ios-shaped fixture: globals at root, a design-system hub (arcade),
+ // and two walled feature subtrees. The `features` module declares the Arcade
+ // dependency once, for all its children.
+ function cashGraph() {
+ return assembleGraph({
placedNodes: [
- placed("brand-voice", "core", {}, "Calm everywhere."),
- placed(
- "checkout/trust",
- "checkout",
- { relates: [{ to: "dashboard/density", as: "contrasts" }] },
- "Reduce felt risk.",
- ),
- placed("dashboard/density", "dashboard", {}, "Pack it in."),
+ root("core"), // root index.md
+ root("trust"),
+ root("accessibility"),
+ dir("arcade", { description: "Design system." }),
+ leaf("arcade/color", { description: "Color tokens." }),
+ leaf("arcade/motion", { description: "Motion." }),
+ dir("arcade/components", { description: "Components." }),
+ leaf("arcade/components/button", { description: "Button." }),
+ dir("features", { relates: [{ to: "arcade", as: "reinforces" }] }),
+ dir("features/bitcoin"),
+ leaf("features/bitcoin/invariants", {
+ description: "Non-negotiables.",
+ }),
+ dir("features/bitcoin/buy"),
+ leaf("features/bitcoin/buy/confirm"),
+ leaf("features/bitcoin/buy/review"),
+ dir("features/bitcoin/education"),
+ dir("features/lending"),
+ leaf("features/lending/invariants"),
+ dir("features/banking"),
+ leaf("features/banking/paychecks"),
],
});
- const slice = resolveGraphSlice(graph, "checkout");
+ }
- expect(provenanceOf(slice, "checkout/trust")).toEqual({ kind: "own" });
- expect(provenanceOf(slice, "brand-voice")).toEqual({
- kind: "ancestor",
- from: "core",
- });
- expect(provenanceOf(slice, "dashboard/density")).toEqual({
+ it("1. a sibling folder is a wall — its nodes never leak in", () => {
+ const slice = resolveGraphSlice(
+ cashGraph(),
+ "features/bitcoin/buy/confirm",
+ );
+ const ids = bodyIds(slice);
+ // Walled-off siblings: other features, the design system, sibling sub-areas.
+ expect(ids).not.toContain("features/banking");
+ expect(ids).not.toContain("features/banking/paychecks");
+ expect(ids).not.toContain("features/lending");
+ expect(ids).not.toContain("features/bitcoin/education");
+ // And not even as spokes — a wall is total.
+ expect(spokeIds(slice)).not.toContain("features/banking");
+ expect(spokeIds(slice)).not.toContain("features/lending");
+ });
+
+ it("2. an ancestor's relates propagates down to a deep leaf", () => {
+ const slice = resolveGraphSlice(
+ cashGraph(),
+ "features/bitcoin/buy/confirm",
+ );
+ // `features` declares the Arcade dependency; a screen 3 levels deeper
+ // inherits it via the corridor → edge path.
+ expect(provenanceOf(slice, "arcade")).toEqual({
kind: "edge",
- via: "contrasts",
- from: "checkout/trust",
+ via: "reinforces",
+ from: "features",
});
});
- it("cascades through multiple ancestor levels", () => {
- const graph = assembleGraph({
- placedNodes: [
- placed("brand-voice", "core", {}, "Calm."),
- placed("checkout", "core", {}, "Checkout surface."),
- placed("checkout/clarity", "checkout", {}, "Plain."),
- placed("checkout/payment", "checkout", {}, "Payment surface."),
- placed("checkout/payment/pay-now", "checkout/payment", {}, "One tap."),
- ],
- });
- const slice = resolveGraphSlice(graph, "checkout/payment");
- expect(provenanceOf(slice, "checkout/payment/pay-now")).toEqual({
- kind: "own",
+ it("3. an edge to a hub unfolds the hub's subtree as spokes", () => {
+ const slice = resolveGraphSlice(
+ cashGraph(),
+ "features/bitcoin/buy/confirm",
+ );
+ const hubSpokes = slice.spokes
+ .filter((s) => s.kind === "edge-hub")
+ .map((s) => s.id)
+ .sort();
+ expect(hubSpokes).toEqual([
+ "arcade/color",
+ "arcade/components",
+ "arcade/components/button",
+ "arcade/motion",
+ ]);
+ // The hub body itself is a full-body edge node, not a spoke.
+ expect(spokeIds(slice)).not.toContain("arcade");
+ });
+
+ it("4. a loose file in a corridor folder cascades full-body (invariants)", () => {
+ const slice = resolveGraphSlice(
+ cashGraph(),
+ "features/bitcoin/buy/confirm",
+ );
+ // `features/bitcoin/invariants` is a plain file in folder features/bitcoin,
+ // which is on the corridor — so it is inherited as a full body.
+ expect(provenanceOf(slice, "features/bitcoin/invariants")).toEqual({
+ kind: "ancestor",
+ from: "features/bitcoin",
});
- expect(provenanceOf(slice, "checkout/clarity")).toEqual({
+ // Globals (root files) reach everywhere.
+ expect(provenanceOf(slice, "trust")).toEqual({
kind: "ancestor",
- from: "checkout",
+ from: "core",
});
- expect(provenanceOf(slice, "brand-voice")).toEqual({
+ expect(provenanceOf(slice, "accessibility")).toEqual({
kind: "ancestor",
from: "core",
});
- expect(slice.ancestors).toEqual(["checkout"]);
});
- it("filters by incarnation: essence always in, matching in, mismatched out", () => {
- const graph = assembleGraph({
- placedNodes: [
- placed("brand-voice", "core", {}, "Calm."), // essence
- placed("checkout/web", "checkout", { incarnation: "web" }, "Inline."),
- placed(
- "checkout/mail",
- "checkout",
- { incarnation: "email" },
- "Subject.",
- ),
- ],
+ it("5. descendants appear as spokes (pointers), not spine", () => {
+ const slice = resolveGraphSlice(cashGraph(), "features/bitcoin");
+ const descendants = slice.spokes
+ .filter((s) => s.kind === "descendant")
+ .map((s) => s.id)
+ .sort();
+ expect(descendants).toEqual([
+ "features/bitcoin/buy",
+ "features/bitcoin/buy/confirm",
+ "features/bitcoin/buy/review",
+ "features/bitcoin/education",
+ ]);
+ // A descendant is a pointer, never a full body.
+ expect(bodyIds(slice)).not.toContain("features/bitcoin/buy/confirm");
+ // A descendant spoke carries its description for agent selection.
+ const buy = slice.spokes.find((s) => s.id === "features/bitcoin/buy");
+ expect(buy?.kind).toBe("descendant");
+ });
+
+ it("6. same-folder files co-occur as own (a folder is one surface)", () => {
+ const slice = resolveGraphSlice(
+ cashGraph(),
+ "features/bitcoin/buy/confirm",
+ );
+ // confirm.md and review.md share folder features/bitcoin/buy — both `own`.
+ expect(provenanceOf(slice, "features/bitcoin/buy/confirm")).toEqual({
+ kind: "own",
+ });
+ expect(provenanceOf(slice, "features/bitcoin/buy/review")).toEqual({
+ kind: "own",
});
- const slice = resolveGraphSlice(graph, "checkout", { incarnation: "web" });
- const ids = slice.nodes.map((n) => n.id).sort();
- expect(ids).toContain("brand-voice"); // essence
- expect(ids).toContain("checkout/web"); // matches
- expect(ids).not.toContain("checkout/mail"); // mismatched
- expect(slice.incarnation).toBe("web");
});
- it("includes every node when no incarnation filter is given", () => {
+ it("edges follow one hop only — no recursion", () => {
const graph = assembleGraph({
placedNodes: [
- placed("checkout/web", "checkout", { incarnation: "web" }, "x"),
- placed("checkout/mail", "checkout", { incarnation: "email" }, "y"),
+ leaf("checkout/a", { relates: [{ to: "dashboard/b" }] }),
+ leaf("dashboard/b", { relates: [{ to: "dashboard/c" }] }),
+ leaf("dashboard/c"),
],
});
- const slice = resolveGraphSlice(graph, "checkout");
- const ids = slice.nodes.map((n) => n.id).sort();
- expect(ids).toEqual(["checkout/mail", "checkout/web"]);
- expect(slice.incarnation).toBeUndefined();
+ const slice = resolveGraphSlice(graph, "checkout/a");
+ const ids = bodyIds(slice);
+ expect(ids).toContain("checkout/a"); // own
+ expect(ids).toContain("dashboard/b"); // one hop
+ expect(ids).not.toContain("dashboard/c"); // two hops — excluded
});
- it("follows relates edges one hop only (no recursion)", () => {
+ it("filters full-body nodes by incarnation; essence always passes", () => {
const graph = assembleGraph({
placedNodes: [
- placed(
- "checkout/a",
- "checkout",
- { relates: [{ to: "dashboard/b" }] },
- "node a",
- ),
- placed(
- "dashboard/b",
- "dashboard",
- { relates: [{ to: "dashboard/c" }] },
- "b",
- ),
- placed("dashboard/c", "dashboard", {}, "c"),
+ root("voice"), // essence
+ leaf("checkout/web", { incarnation: "web" }),
+ leaf("checkout/mail", { incarnation: "email" }),
],
});
- const slice = resolveGraphSlice(graph, "checkout");
- const ids = slice.nodes.map((n) => n.id);
- expect(ids).toContain("checkout/a"); // own
- expect(ids).toContain("dashboard/b"); // one hop from a
- expect(ids).not.toContain("dashboard/c"); // two hops — excluded
+ const slice = resolveGraphSlice(graph, "checkout/web", {
+ incarnation: "web",
+ });
+ const ids = bodyIds(slice);
+ expect(ids).toContain("voice"); // essence
+ expect(ids).toContain("checkout/web"); // matches
+ expect(ids).not.toContain("checkout/mail"); // mismatched
+ expect(slice.incarnation).toBe("web");
});
});
From 585a452c0aeb7a68930c2a3c9659e781359a07f2 Mon Sep 17 00:00:00 2001
From: Nahiyan Khan
Date: Sun, 28 Jun 2026 19:59:07 -0400
Subject: [PATCH 05/12] docs+skill: teach the corridor + spine/spokes gather
model
---
apps/docs/src/content/docs/cli-reference.mdx | 17 +++++++++++++----
packages/ghost/src/skill-bundle/SKILL.md | 16 +++++++++++++++-
.../src/skill-bundle/references/capture.md | 9 ++++++---
.../ghost/src/skill-bundle/references/schema.md | 17 +++++++++++++----
4 files changed, 47 insertions(+), 12 deletions(-)
diff --git a/apps/docs/src/content/docs/cli-reference.mdx b/apps/docs/src/content/docs/cli-reference.mdx
index e19c6b6d..abb99107 100644
--- a/apps/docs/src/content/docs/cli-reference.mdx
+++ b/apps/docs/src/content/docs/cli-reference.mdx
@@ -101,10 +101,19 @@ ghost validate --format json
### Compose a surface slice — `gather`
With no argument, list every node by id and description so an agent can match a
-task to one. With a surface, compose its context slice: the surface's own nodes,
-the ancestors it inherits from its parent directories, and one-hop `relates`
-edges. Use `--as` to filter to a single incarnation; untagged essence nodes
-always pass.
+task to one. With a surface, compose its context slice — folders are walls,
+files fill the corridor:
+
+- **spine** (full bodies): every file from the package root down to the
+ surface's own folder. A sibling folder is a wall — its nodes never appear.
+- **edges** (full bodies, one hop): each spine node's `relates` targets, so a
+ rule authored once high in the corridor reaches every descendant. A link to a
+ hub also unfolds the hub's subtree as spokes.
+- **spokes** (pointers): the surface's own descendants and any edge hub's
+ subtree, offered as `id` + `description` for the agent to pull on demand.
+
+Use `--as` to filter full-body nodes to a single incarnation; untagged essence
+nodes always pass.
diff --git a/packages/ghost/src/skill-bundle/SKILL.md b/packages/ghost/src/skill-bundle/SKILL.md
index f1a5705f..78a0107a 100644
--- a/packages/ghost/src/skill-bundle/SKILL.md
+++ b/packages/ghost/src/skill-bundle/SKILL.md
@@ -50,6 +50,20 @@ medium-bound expression (essence is untagged). Free-form keys (`audience`, …)
pass through. See [references/capture.md](references/capture.md) for the full
node shape.
+**How `gather` composes** — folders are walls; files fill the corridor:
+
+- **spine** (full bodies): every file from the package root down to the
+ surface's own folder is inherited — so a feature's `invariants.md` reaches
+ every screen in that feature, and root files reach everywhere. A **sibling
+ folder is a wall**: its nodes never appear, not even as a pointer.
+- **edges** (full bodies, one hop): each spine node's `relates` targets. Author
+ a broad rule once at the level it is true — e.g. `relates: { to: arcade }` on
+ `features/` — and every descendant inherits it. A link to a hub also unfolds
+ the hub's subtree as spokes.
+- **spokes** (pointers: id + description): the surface's own descendants and any
+ edge hub's subtree — navigable optionality the agent pulls on demand with a
+ follow-up `gather`.
+
Checks and review validate output; they are not generation input.
`manifest.yml` anchors the package with `schema: ghost.fingerprint-package/v1`.
@@ -83,7 +97,7 @@ ref). Inherited nodes are read-only and flow into gather/validate like local one
| `ghost validate [file-or-dir]` | Validate the package — artifact shape and the node graph (links resolve, one root, acyclic). |
| `ghost checks --surface ` | Select and ground the markdown checks governing the named surfaces. |
| `ghost review --surface [--diff ]` | Emit an advisory review packet: touched surfaces, routed checks, and fingerprint grounding (diff embedded verbatim). |
-| `ghost gather [surface] [--as ]` | Compose a surface's context slice (own + inherited + edge), or list the surface menu. |
+| `ghost gather [surface] [--as ]` | Compose a surface's context slice (corridor spine + relates edges, plus spoke pointers), or list the surface menu. |
| `ghost skill install` | Install this unified skill bundle. |
## Advanced CLI Verbs
diff --git a/packages/ghost/src/skill-bundle/references/capture.md b/packages/ghost/src/skill-bundle/references/capture.md
index c56234f6..61d2a16d 100644
--- a/packages/ghost/src/skill-bundle/references/capture.md
+++ b/packages/ghost/src/skill-bundle/references/capture.md
@@ -56,9 +56,12 @@ action beats completeness…
against those and names one. The body is the node's "implementation"; the
description is what makes it discoverable. Write one on any node worth
anchoring a task at.
-- **The directory places the node** — a node inherits everything in the
- directories above it. The brand soul lives in the package-root `index.md` (the
- `core` node), so it reaches every surface.
+- **The directory places the node** — folders are walls; files fill the
+ corridor. A node inherits every file in the folders above it, up to the root;
+ a sibling folder is invisible. The brand soul lives in the package-root files
+ (the `core` node and other root files), so it reaches every surface. Author a
+ broad rule at the broadest folder where it is true — a feature's
+ `invariants.md` reaches every screen in that feature and nowhere else.
- **`relates`** links laterally when a relationship carries rationale. When the
rationale is rich (e.g. "checkout and item-detail disagree on density on
purpose"), write a **relationship node** whose body explains the tension.
diff --git a/packages/ghost/src/skill-bundle/references/schema.md b/packages/ghost/src/skill-bundle/references/schema.md
index f793ec2c..c61c3e56 100644
--- a/packages/ghost/src/skill-bundle/references/schema.md
+++ b/packages/ghost/src/skill-bundle/references/schema.md
@@ -73,10 +73,19 @@ key), never by repo path.
## Gather
-`ghost gather ` composes a node's slice: its own body + inherited
-ancestors + one-hop `relates`, filtered by `--as `. With no
-argument, `gather` lists nodes by id + description for the agent to match the ask
-against. The agent names the node; Ghost never infers it from a path.
+`ghost gather ` composes a node's slice, filtered by `--as `:
+
+- **spine** (full bodies): every file on the corridor from the package root
+ down to the node's own folder. Folders are walls — sibling folders never
+ appear.
+- **edges** (full bodies, one hop): each spine node's `relates` targets. A rule
+ authored high in the corridor (e.g. `relates: { to: arcade }` on `features/`)
+ reaches every descendant.
+- **spokes** (pointers): the node's own descendants and any edge hub's subtree,
+ offered as id + description for the agent to pull with a follow-up `gather`.
+
+With no argument, `gather` lists nodes by id + description for the agent to match
+the ask against. The agent names the node; Ghost never infers it from a path.
## Checks
From f5735376e4bdfe284edd1567d4563056034ec2c8 Mon Sep 17 00:00:00 2001
From: Nahiyan Khan
Date: Sun, 28 Jun 2026 21:10:07 -0400
Subject: [PATCH 06/12] docs: frame Ghost as CLI + interpretive skill, not just
a calculator
The Architecture paragraph described Ghost as 'the deterministic calculator the
agent reaches for', which amputated the skill bundle. Ghost grounds agent work
with both a deterministic CLI and an interpretive skill bundle that teaches
authoring and use.
---
CLAUDE.md | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 57ff0084..2dbb977d 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -31,9 +31,11 @@ pnpm --filter @anarchitecture/ghost exec ghost
## Architecture
Ghost is **BYOA (bring your own agent)**. Claude Code, Codex, Cursor, Goose, or
-another host agent reads, decides, and writes. Ghost is the deterministic
-calculator the agent reaches for: schema and graph validation, repo-signal
-helpers, context composition, check routing, and advisory review packets.
+another host agent reads, decides, and writes. Ghost grounds that work with two
+things: a **deterministic CLI** — schema and graph validation, repo-signal
+helpers, context composition, check routing, and advisory review packets (no
+LLM, repeatable) — and an **interpretive skill bundle** that teaches the agent
+how to author and use the fingerprint.
The canonical root `.ghost/` package is a directory tree of prose nodes:
From ff9b61e9a9507e31329bd727d407a7ad831916bf Mon Sep 17 00:00:00 2001
From: Nahiyan Khan
Date: Sun, 28 Jun 2026 21:15:30 -0400
Subject: [PATCH 07/12] docs: retrack the docs site to the current command set
and corridor model
getting-started referenced a pile of removed commands (relay, lint, verify,
check, compare, stack, ack, track, diverge) and dead concepts (stacks,
selected_context, fingerprint.md). Rewrote its gather/govern sections onto the
real verbs (gather/checks/review), led the directory example with root nodes +
a marketing surface (no more bare checkout), and taught the corridor model.
fingerprint-authoring: drop lint/verify/stack; replace the nested-package
section with the extends-based shared-brand model. cli-reference: drop the
'surfaces spine' phrasing. Docs build, frontmatter, and terminology all pass.
---
apps/docs/src/content/docs/cli-reference.mdx | 4 +-
.../content/docs/fingerprint-authoring.mdx | 46 ++++++----
.../docs/src/content/docs/getting-started.mdx | 89 ++++++++-----------
3 files changed, 67 insertions(+), 72 deletions(-)
diff --git a/apps/docs/src/content/docs/cli-reference.mdx b/apps/docs/src/content/docs/cli-reference.mdx
index abb99107..3fe5447e 100644
--- a/apps/docs/src/content/docs/cli-reference.mdx
+++ b/apps/docs/src/content/docs/cli-reference.mdx
@@ -54,8 +54,8 @@ GHOST_PACKAGE_DIR=.agents/ghost ghost init
### Contribution — `scan`
-Report what the package contributes: presence of the manifest and surfaces
-spine, and the nodes and surfaces it carries.
+Report what the package contributes: presence of the manifest, and the nodes
+and surfaces (directories) it carries.
diff --git a/apps/docs/src/content/docs/fingerprint-authoring.mdx b/apps/docs/src/content/docs/fingerprint-authoring.mdx
index 72fa8d94..c61ffe45 100644
--- a/apps/docs/src/content/docs/fingerprint-authoring.mdx
+++ b/apps/docs/src/content/docs/fingerprint-authoring.mdx
@@ -34,8 +34,8 @@ weight to give human intent, existing code, and library evidence.
| Design system or UI library | Grammar-led. Describe primitives, tokens, component behavior, accessibility, and composition constraints. |
| Rebrand, redesign, or migration | Human-led transition. Capture current, target, and migration cautions. |
| Prototype becoming product | Ratification-led. Preserve only the emergent patterns humans want to keep. |
-| Fork, white label, or tenant variant | Shared base + local divergence. Keep common surface composition broad and local differences scoped. |
-| Monorepo or nested surfaces | Stack-aware. Use root guidance for broad composition and nested packages for surfaces assessed differently. |
+| Fork, white label, or tenant variant | Shared base + local divergence. Put the common surface composition in a base package and `extends` it from each variant. |
+| Monorepo or shared brand | Keep one package per product surface. A shared brand lives in its own package that the others `extends` by identity. |
@@ -71,8 +71,7 @@ Set up the Ghost fingerprint for this repo with auto-draft.
```bash
ghost scan --format json
ghost signals .
-ghost lint .ghost
-ghost verify .ghost --root .
+ghost validate
```
Raw repo signals are source evidence only. They can support curated inventory,
@@ -144,25 +143,38 @@ Write less like a brand book and more like a decision engine.
-
+
-Nested fingerprints are opt-in. Create a local `.ghost/` only when a surface
-should be assessed differently from the root product fingerprint.
+One contract per package: a repo's `.ghost/` is the whole fingerprint, and
+surfaces are directories within it. Reach for a second package only when a
+distinct product genuinely owns its own fingerprint — a separate app, a shared
+brand, a tenant variant.
-Use a nested package when a surface has distinct users, information density,
-trust or recovery posture, interaction rhythm, component grammar, UI library
-usage, or review criteria for the same UI decision.
+When several packages share a brand, put the common composition in its own
+package and `extends` it by identity:
-Keep broad product-family guidance at the root. Put local obligations in the
-nearest package that owns the surface.
+```yaml
+# product/.ghost/manifest.yml
+schema: ghost.fingerprint-package/v1
+id: acme-product
+extends:
+ brand: ../brand/.ghost # map the brand contract's id to where it lives
+```
-```bash
-ghost init --scope apps/checkout
-ghost stack apps/checkout
-ghost lint --all
-ghost verify --all
+Nodes then reference inherited context by identity, never by path:
+
+```yaml
+# product/.ghost/checkout/trust.md
+---
+relates:
+ - to: brand:core/trust
+ as: reinforces
+---
```
+Inherited nodes load read-only and flow into `gather` and `validate` like local
+ones. A variant inherits the brand without seeing its siblings.
+
diff --git a/apps/docs/src/content/docs/getting-started.mdx b/apps/docs/src/content/docs/getting-started.mdx
index 3d5e92ad..c1220ef2 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, author a product-surface composition fingerprint, and use it to generate, validate, compare, and govern product surfaces.
+description: Install Ghost, author a product-surface composition fingerprint, and use it to brief, validate, and review product surfaces.
kicker: Docs
section: guide
order: 10
@@ -18,18 +18,24 @@ The canonical portable fingerprint is a directory tree of prose nodes:
```text
.ghost/
manifest.yml # schema + package id
- index.md # the core node — true everywhere (optional)
- checkout/index.md # the `checkout` surface's own prose
- checkout/review.md # a node placed in the checkout surface
+ index.md # the core node — true everywhere
+ trust.md # another root node — also true everywhere
+ marketing/index.md # the `marketing` surface's own prose
+ marketing/email.md # a node in the marketing surface
checks/*.md # optional ghost.check/v1 deterministic checks
```
The directory tree _is_ the graph. A node's identity is its file path with
-`.md` dropped (`checkout/review.md` is the node `checkout/review`), and its
+`.md` dropped (`marketing/email.md` is the node `marketing/email`), and its
parent is the directory that contains it. A surface is simply a directory: its
own prose lives in that directory's `index.md`, and the package-root `index.md`
-is the implicit `core` node that is true everywhere. There is no spine file to
-maintain — a surface exists when its directory exists.
+is the implicit `core` node. There is no spine file to maintain — a surface
+exists when its directory exists.
+
+Folders are walls; files fill the corridor. A node inherits every file from the
+package root down to its own folder, so root nodes (`index.md`, `trust.md`)
+reach every surface while a sibling surface stays invisible. `relates` links
+nodes laterally across that boundary when a relationship carries rationale.
Every prose node is read through three lenses — intent, inventory, and
composition — and deterministic `checks/` validate the result afterward; they
@@ -93,8 +99,7 @@ core `index.md` node.
ghost init
ghost scan --format json
ghost signals .
-ghost lint .ghost
-ghost verify .ghost --root .
+ghost validate
```
Each node's prose records durable surface-composition guidance through three
@@ -117,64 +122,42 @@ workflow, read [Fingerprint Authoring](/docs/fingerprint-authoring).
-
-
-Before generating or revising UI, gather Relay JSON for the target path:
-
-```bash
-ghost relay gather apps/checkout/review/page.tsx --format json
-```
+
-`ghost.relay.gather/v2` is the agent contract. Agents should read `context`,
-`selected_context`, `targetPaths`, `source`, `stackDirs`, gaps, and trace fields
-from JSON. Plain `ghost relay gather ` remains a compact human preview.
-For prompt-shaped work where there is no clear path, host agents can create a
-`ghost.relay-request/v1` and run
-`ghost relay gather --request-stdin --format json`.
-Relay config controls the runtime. Omitted `base` uses the resolved fingerprint
-stack; `base.kind: none` lets frameworks provide declared request context
-without a `.ghost` package:
+Before generating or revising UI, gather the composed slice for the surface the
+work touches. The agent names the surface; Ghost composes its corridor of prose:
```bash
-GHOST_RELAY_CONFIG=.agents/ghost/relay.yml ghost relay gather --request-stdin --format json
-ghost relay gather stacks/portal.renewal-reminder.email.yml --config .agents/ghost/relay.yml --format json
+ghost gather marketing
+ghost gather marketing/email --as email
+ghost gather marketing --format json
```
-The package remains the approved product-surface context; review and check
-commands apply it after implementation.
+`gather` returns the **spine** (every node on the corridor from the root down to
+the surface, as full bodies), the **edges** reachable in one hop from any spine
+node's `relates`, and **spokes** — pointers to nearby nodes the agent can pull on
+demand. This is the pre-generation step: agents get surface-composition context
+before they build, not only after a review finds drift.
-After implementation, run Ghost against the same fingerprint:
+After implementation, run Ghost against the same fingerprint. The agent names
+the surfaces the change touches:
```bash
-ghost check --base main
-ghost review --base main
+ghost checks --surface marketing
+ghost review --surface marketing --base main
+git diff | ghost review --surface marketing --diff -
```
-`ghost check` applies active deterministic gates from the resolved fingerprint
-stack for each changed file. `ghost review` emits advisory context grounded in
-the same selected context as Relay, selected validation checks, and the diff.
-
-Wrappers should consume `ghost check --format json` and map Ghost severities
-outside Ghost. Ghost severities remain `critical`, `serious`, and `nit`.
-
-
-
-
-
-```bash
-ghost compare market/.ghost dashboard/.ghost
-ghost stack apps/checkout/review/page.tsx
-ghost ack --stance aligned --reason "Initial baseline"
-ghost track new-tracked.fingerprint.md
-ghost diverge typography --reason "Editorial product uses a different type scale"
-```
+`ghost checks` selects and grounds the markdown checks governing the named
+surfaces. `ghost review` emits an advisory packet: touched surfaces, routed
+checks, fingerprint grounding, and the diff embedded verbatim.
-Package comparison uses canonical `.ghost/` packages. `ack`,
-`track`, and `diverge` record stance for compatibility drift workflows that
-track direct fingerprint markdown references.
+Wrappers should consume `--format json` and map Ghost severities outside Ghost.
+Ghost severities remain `critical`, `serious`, and `nit`. Advisory review is
+never a CI gate on its own.
From 18c3b2fe421b6ec1c9e1ee5ae6499966152245f2 Mon Sep 17 00:00:00 2001
From: Nahiyan Khan
Date: Sun, 28 Jun 2026 21:17:47 -0400
Subject: [PATCH 08/12] docs: stop-slop pass on the gather prose
Cut adverb crutches ('simply'), a negative-contrast tail ('not only after a
review finds drift'), 'This is the pre-generation step' throat-clearing, and the
'navigable optionality' quotable. Plain, active phrasing.
---
apps/docs/src/content/docs/getting-started.mdx | 14 +++++++-------
packages/ghost/src/skill-bundle/SKILL.md | 4 ++--
2 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/apps/docs/src/content/docs/getting-started.mdx b/apps/docs/src/content/docs/getting-started.mdx
index c1220ef2..4d3de1cf 100644
--- a/apps/docs/src/content/docs/getting-started.mdx
+++ b/apps/docs/src/content/docs/getting-started.mdx
@@ -27,10 +27,10 @@ The canonical portable fingerprint is a directory tree of prose nodes:
The directory tree _is_ the graph. A node's identity is its file path with
`.md` dropped (`marketing/email.md` is the node `marketing/email`), and its
-parent is the directory that contains it. A surface is simply a directory: its
-own prose lives in that directory's `index.md`, and the package-root `index.md`
-is the implicit `core` node. There is no spine file to maintain — a surface
-exists when its directory exists.
+parent is the directory that contains it. A surface is a directory: its own
+prose lives in that directory's `index.md`, and the package-root `index.md` is
+the implicit `core` node. No spine file to maintain. A surface exists when its
+directory exists.
Folders are walls; files fill the corridor. A node inherits every file from the
package root down to its own folder, so root nodes (`index.md`, `trust.md`)
@@ -135,9 +135,9 @@ ghost gather marketing --format json
`gather` returns the **spine** (every node on the corridor from the root down to
the surface, as full bodies), the **edges** reachable in one hop from any spine
-node's `relates`, and **spokes** — pointers to nearby nodes the agent can pull on
-demand. This is the pre-generation step: agents get surface-composition context
-before they build, not only after a review finds drift.
+node's `relates`, and **spokes**, pointers to nearby nodes the agent can pull on
+demand. Run it before generation, so the agent builds with surface composition
+in hand rather than discovering the gaps in review.
diff --git a/packages/ghost/src/skill-bundle/SKILL.md b/packages/ghost/src/skill-bundle/SKILL.md
index 78a0107a..71bdb83a 100644
--- a/packages/ghost/src/skill-bundle/SKILL.md
+++ b/packages/ghost/src/skill-bundle/SKILL.md
@@ -61,8 +61,8 @@ node shape.
`features/` — and every descendant inherits it. A link to a hub also unfolds
the hub's subtree as spokes.
- **spokes** (pointers: id + description): the surface's own descendants and any
- edge hub's subtree — navigable optionality the agent pulls on demand with a
- follow-up `gather`.
+ edge hub's subtree. The agent reads the descriptions and pulls what it needs
+ with a follow-up `gather`.
Checks and review validate output; they are not generation input.
From f40a7a829c0ca04c4bf8b77e0eb1730ad9863196 Mon Sep 17 00:00:00 2001
From: Nahiyan Khan
Date: Sun, 28 Jun 2026 21:31:29 -0400
Subject: [PATCH 09/12] docs: strip em dashes from shipped prose (stop-slop)
Em dashes were repo-wide slop, not house style. Rewrote every one in the
user-facing surfaces as a colon, period, comma, or parenthetical, and recast two
negative-listing structures on the landing page. Covers README, CLAUDE.md, the
package README, the skill bundle (SKILL + references), the docs site pages, and
the ghost-ui marketing copy. Left typography specimens (the em dash is demo
content there). Patch changeset for the shipped skill-bundle/README prose.
---
.changeset/skill-bundle-prose.md | 6 ++++
CLAUDE.md | 20 +++++------
README.md | 18 +++++-----
apps/docs/src/App.tsx | 2 +-
apps/docs/src/app/page.tsx | 21 ++++++-----
apps/docs/src/app/tools/scan/page.tsx | 2 +-
apps/docs/src/app/tools/ui/page.tsx | 6 ++--
apps/docs/src/app/ui/components/page.tsx | 2 +-
apps/docs/src/app/ui/foundations/page.tsx | 2 +-
apps/docs/src/app/ui/page.tsx | 4 +--
apps/docs/src/components/docs/hero.tsx | 2 +-
apps/docs/src/content/docs/cli-reference.mdx | 28 +++++++--------
.../content/docs/fingerprint-authoring.mdx | 14 ++++----
.../docs/src/content/docs/getting-started.mdx | 16 ++++-----
packages/ghost/README.md | 12 +++----
packages/ghost/src/skill-bundle/SKILL.md | 36 +++++++++----------
.../references/authoring-scenarios.md | 9 ++---
.../src/skill-bundle/references/brief.md | 4 +--
.../src/skill-bundle/references/capture.md | 26 +++++++-------
.../src/skill-bundle/references/review.md | 4 +--
.../src/skill-bundle/references/schema.md | 18 +++++-----
21 files changed, 129 insertions(+), 123 deletions(-)
create mode 100644 .changeset/skill-bundle-prose.md
diff --git a/.changeset/skill-bundle-prose.md b/.changeset/skill-bundle-prose.md
new file mode 100644
index 00000000..9404422e
--- /dev/null
+++ b/.changeset/skill-bundle-prose.md
@@ -0,0 +1,6 @@
+---
+"@anarchitecture/ghost": patch
+---
+
+Clean em dashes out of the shipped skill bundle and package README prose,
+rewriting them as plain sentences, colons, or parentheticals.
diff --git a/CLAUDE.md b/CLAUDE.md
index 2dbb977d..c70a5f58 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -5,7 +5,7 @@ surface composition behind that UI: hierarchy, density, restraint, repetition,
trust, flow, and the choices that make a surface feel intentional.
Ghost keeps that surface composition in a repo-local `.ghost/` fingerprint
-package — a graph of prose nodes. The public npm shape is one package,
+package, a graph of prose nodes. The public npm shape is one package,
`@anarchitecture/ghost`, with one user-facing bin, `ghost`. The CLI validates
the node graph, composes context, routes checks, and emits deterministic
packets. The host agent does the interpretive BYOA work through the installed
@@ -32,16 +32,16 @@ pnpm --filter @anarchitecture/ghost exec ghost
Ghost is **BYOA (bring your own agent)**. Claude Code, Codex, Cursor, Goose, or
another host agent reads, decides, and writes. Ghost grounds that work with two
-things: a **deterministic CLI** — schema and graph validation, repo-signal
-helpers, context composition, check routing, and advisory review packets (no
-LLM, repeatable) — and an **interpretive skill bundle** that teaches the agent
+things. A **deterministic CLI** does the repeatable parts with no LLM: schema
+and graph validation, repo-signal helpers, context composition, check routing,
+and advisory review packets. An **interpretive skill bundle** teaches the agent
how to author and use the fingerprint.
The canonical root `.ghost/` package is a directory tree of prose nodes:
```text
manifest.yml # schema + id
-index.md # the core node — true everywhere (optional)
+index.md # the core node, true everywhere (optional)
/index.md # a surface's own prose (the directory is the surface)
/.md # a prose node placed in that surface
checks/*.md # optional ghost.check/v1 checks
@@ -50,14 +50,14 @@ checks/*.md # optional ghost.check/v1 checks
The **directory tree is the graph**. A node is a markdown file: descriptive
frontmatter (`description`, `relates`, `incarnation`) plus a prose body. A
node's identity is its path (`marketing/email.md` → `marketing/email`) and its
-parent is its containing directory — a surface is just a directory, and a
+parent is its containing directory. A surface is just a directory, and a
directory's own prose lives in its `index.md`. The package-root `index.md` is
the implicit `core` node. The body is written through three authoring lenses
(they guide what to capture, they are not fields):
-- **intent** — the why and the stance.
-- **inventory** — the materials, and pointers to code the agent can inspect.
-- **composition** — the patterns that make the surface feel intentional.
+- **intent**: the why and the stance.
+- **inventory**: the materials, and pointers to code the agent can inspect.
+- **composition**: the patterns that make the surface feel intentional.
`description` is the retrieval payload; `relates` links nodes laterally;
`incarnation` tags a medium-bound expression. Reserved at the package root:
@@ -147,7 +147,7 @@ Use `patch` for fixes and docs, `minor` for new commands/flags/exports, and
`workspace:*` runtime dependencies in the packed public artifact.
- The canonical on-disk form is a `.ghost/` directory tree: `manifest.yml` plus
prose nodes (`index.md` and `/.md`) and optional `checks/*.md`.
- The directory layout is the graph — ids and parents come from paths, never a
+ The directory layout is the graph; ids and parents come from paths, never a
spine file.
- Skill recipes live in `packages/ghost/src/skill-bundle/references/`; install
them with `ghost skill install`.
diff --git a/README.md b/README.md
index a0d2ce5d..7274885c 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# Ghost
**Agents can assemble UI. They can't reliably preserve the _composition_ behind
-it — the hierarchy, density, restraint, copy, trust, and flow that make a
+it: the hierarchy, density, restraint, copy, trust, and flow that make a
surface feel intentional.**
Ghost is a checked-in product-surface fingerprint your agent reads before it
@@ -18,7 +18,7 @@ writes, and decides.
```text
.ghost/
manifest.yml # ghost.fingerprint-package/v1 anchor: schema + id
- index.md # the core node — true everywhere (optional)
+ index.md # the core node, true everywhere (optional)
/index.md # a surface's own prose (the directory is the surface)
/.md # a prose node placed in that surface
checks/*.md # optional ghost.check/v1 checks
@@ -28,16 +28,16 @@ The fingerprint is a graph of **nodes**, and the **directory tree is the graph**
A node is a markdown file: descriptive frontmatter (`description`, `relates`,
`incarnation`) plus a prose body. A node's identity is its path
(`marketing/email.md` → `marketing/email`) and its parent is its containing
-directory — a surface is just a directory, and a directory's own prose lives in
+directory. A surface is just a directory, and a directory's own prose lives in
its `index.md`. The package-root `index.md` is the implicit `core` node, true
everywhere.
-The body is written through three authoring lenses — they guide what to capture,
+The body is written through three authoring lenses. They guide what to capture;
they are not fields:
-- **intent** — what the surface is trying to do and for whom.
-- **inventory** — the materials, and pointers to code the agent can inspect.
-- **composition** — the patterns that make the surface feel intentional.
+- **intent**: what the surface is trying to do and for whom.
+- **inventory**: the materials, and pointers to code the agent can inspect.
+- **composition**: the patterns that make the surface feel intentional.
`description` is the retrieval payload; `relates` links nodes laterally;
`incarnation` tags a medium-bound expression (essence is untagged). Reserved at
@@ -67,7 +67,7 @@ npx ghost --help
## Quick Start
```bash
-ghost init # scaffold .ghost/ — manifest + a core index.md node
+ghost init # scaffold .ghost/ with a manifest + a core index.md node
ghost validate # links resolve, one root, acyclic
ghost gather # list nodes; ghost gather composes a context slice
```
@@ -124,7 +124,7 @@ of truth; ordinary Git review is the approval boundary for fingerprint edits.
| Command | Description |
| --- | --- |
-| `ghost init` | Scaffold `.ghost/` — a manifest and a core `index.md` node. |
+| `ghost init` | Scaffold `.ghost/` with a manifest and a core `index.md` node. |
| `ghost scan` | Report node and surface contribution. |
| `ghost validate` | Validate the package: artifact shape and the node graph. |
| `ghost gather` | List nodes, or compose a surface's context slice. |
diff --git a/apps/docs/src/App.tsx b/apps/docs/src/App.tsx
index cb3515a5..1ce6a27b 100644
--- a/apps/docs/src/App.tsx
+++ b/apps/docs/src/App.tsx
@@ -51,7 +51,7 @@ export function App() {
} />
- {/* Tools — four-card index plus per-tool landings */}
+ {/* Tools: four-card index plus per-tool landings */}
} />
Agents can assemble UI. What they can't reliably preserve is the
- composition behind it — the hierarchy, density, restraint, copy,
+ composition behind it: the hierarchy, density, restraint, copy,
trust, and flow that make a surface feel intentional.
@@ -43,9 +43,9 @@ export default function Home() {
Ghost captures that composition and checks it into the repo, where
generation happens. It is a{" "}
- graph of prose nodes —
- one markdown file each — that your agent reads before it builds
- and checks after it changes.
+ graph of prose nodes, one
+ markdown file each, that your agent reads before it builds and
+ checks after it changes.
@@ -62,7 +62,7 @@ export default function Home() {
each node is written through intent,{" "}
- inventory, and composition — the why,
+ inventory, and composition: the why,
the materials, the patterns
@@ -100,15 +100,14 @@ export default function Home() {
Composition that can't be recalled or evaluated can't be
- delegated. A surface only its author can assess isn't transferable
- — not to agents, not to new engineers, not to forks. Ghost makes
- it transferable, and makes drift measurable: where generated UI
- diverges from the fingerprint, the gap is signal, and it is
- localized.
+ delegated. A surface only its author can assess won't transfer to
+ an agent, a new engineer, or a fork. Ghost makes it transferable,
+ and makes drift measurable: where generated UI diverges from the
+ fingerprint, the gap is signal, and it is localized.
Design systems were libraries for humans. Ghost is composition
- context for agents — every surface carries the fingerprint it
+ context for agents: every surface carries the fingerprint it
extends, and every deviation can carry evidence.
diff --git a/apps/docs/src/app/tools/scan/page.tsx b/apps/docs/src/app/tools/scan/page.tsx
index ee36b79d..6c1dbeb4 100644
--- a/apps/docs/src/app/tools/scan/page.tsx
+++ b/apps/docs/src/app/tools/scan/page.tsx
@@ -48,7 +48,7 @@ export default function GhostScanLanding() {
,
},
{
name: "MCP server",
href: "https://github.com/block/ghost/tree/main/packages/ghost-ui#mcp-server",
description:
- "ghost-mcp re-exposes the registry to AI assistants — five tools, two resources, so an agent can search components and pull source.",
+ "ghost-mcp re-exposes the registry to AI assistants with five tools and two resources, so an agent can search components and pull source.",
icon: ,
},
];
@@ -51,7 +51,7 @@ export default function GhostUiLanding() {
{/* Search */}
diff --git a/apps/docs/src/app/ui/foundations/page.tsx b/apps/docs/src/app/ui/foundations/page.tsx
index 1f648342..02711675 100644
--- a/apps/docs/src/app/ui/foundations/page.tsx
+++ b/apps/docs/src/app/ui/foundations/page.tsx
@@ -72,7 +72,7 @@ export default function FoundationsIndex() {
,
},
];
@@ -100,7 +100,7 @@ export default function DesignLanguageIndex() {
{[3, 4, 5].map((i) => {
const size = Math.pow(i, 1.6) * 12;
diff --git a/apps/docs/src/content/docs/cli-reference.mdx b/apps/docs/src/content/docs/cli-reference.mdx
index 3fe5447e..844f02d2 100644
--- a/apps/docs/src/content/docs/cli-reference.mdx
+++ b/apps/docs/src/content/docs/cli-reference.mdx
@@ -22,7 +22,7 @@ The canonical fingerprint is a `.ghost/` directory tree of prose nodes:
```text
.ghost/
manifest.yml # schema + id
- index.md # the core node — true everywhere (optional)
+ index.md # the core node, true everywhere (optional)
/index.md # a surface's own prose (the directory is the surface)
/.md # a prose node placed in that surface
checks/*.md # optional ghost.check/v1 checks
@@ -35,7 +35,7 @@ The command tables below are generated from the CLI source. Run
-### Initialize — `init`
+### Initialize: `init`
Scaffold a `.ghost/` package: a manifest and a core `index.md` node. Add
surfaces by adding directories (`checkout/index.md` is the `checkout` surface).
@@ -52,7 +52,7 @@ ghost init --package product-surface
GHOST_PACKAGE_DIR=.agents/ghost ghost init
```
-### Contribution — `scan`
+### Contribution: `scan`
Report what the package contributes: presence of the manifest, and the nodes
and surfaces (directories) it carries.
@@ -64,10 +64,10 @@ ghost scan
ghost scan --format json
```
-### Repo signals — `signals`
+### Repo signals: `signals`
Emit raw signals about a frontend repo as JSON. Use this as scratch evidence
-while authoring curated nodes — it does not contribute to the fingerprint by
+while authoring curated nodes. It does not contribute to the fingerprint by
itself.
@@ -80,9 +80,9 @@ ghost signals .
-### Validation — `validate`
+### Validation: `validate`
-Validate the package: artifact shape plus the node graph — every `relates` link
+Validate the package: artifact shape plus the node graph. Every `relates` link
resolves, there is exactly one root, and the graph is acyclic. Defaults to
`.ghost`; pass a file to validate a single node.
@@ -98,14 +98,14 @@ ghost validate --format json
-### Compose a surface slice — `gather`
+### Compose a surface slice: `gather`
With no argument, list every node by id and description so an agent can match a
-task to one. With a surface, compose its context slice — folders are walls,
+task to one. With a surface, compose its context slice. Folders are walls,
files fill the corridor:
- **spine** (full bodies): every file from the package root down to the
- surface's own folder. A sibling folder is a wall — its nodes never appear.
+ surface's own folder. A sibling folder is a wall; its nodes never appear.
- **edges** (full bodies, one hop): each spine node's `relates` targets, so a
rule authored once high in the corridor reaches every descendant. A link to a
hub also unfolds the hub's subtree as spokes.
@@ -131,7 +131,7 @@ before they build, not only after a review finds drift.
-### Route checks — `checks`
+### Route checks: `checks`
Select and ground the markdown checks governing the named surfaces. The agent
names the surfaces the change touches, then evaluates the returned checks. Use
@@ -145,7 +145,7 @@ ghost checks --surface checkout,billing
ghost checks --surface checkout --format json
```
-### Advisory review packet — `review`
+### Advisory review packet: `review`
Emit an advisory packet for a diff: touched surfaces, routed checks, and
fingerprint grounding, with the diff embedded verbatim. Diff against a git ref
@@ -168,7 +168,7 @@ gate on its own.
-### Install the skill — `skill`
+### Install the skill: `skill`
Install the unified Ghost skill bundle so a host agent knows how to author and
use the fingerprint.
@@ -181,7 +181,7 @@ ghost skill install --agent claude
ghost skill install --dest ~/.codex/skills/ghost
```
-### Migrate a legacy package — `migrate`
+### Migrate a legacy package: `migrate`
Migrate a legacy `.ghost/` package onto the node-graph surface model. Use
`--dry-run` to print the plan without writing.
diff --git a/apps/docs/src/content/docs/fingerprint-authoring.mdx b/apps/docs/src/content/docs/fingerprint-authoring.mdx
index c61ffe45..591df926 100644
--- a/apps/docs/src/content/docs/fingerprint-authoring.mdx
+++ b/apps/docs/src/content/docs/fingerprint-authoring.mdx
@@ -85,14 +85,14 @@ frequency may seed a draft, but it does not decide what the surface should do.
The fingerprint is a directory tree of prose nodes. The tree _is_ the graph: a
node's identity is its file path with `.md` dropped (`marketing/email.md` is the
node `marketing/email`), and its parent is the directory that contains it. A
-surface is just a directory — its own prose lives in that directory's
+surface is just a directory; its own prose lives in that directory's
`index.md` (`checkout/index.md` is the `checkout` surface), and the
package-root `index.md` is the implicit `core` node that is true everywhere.
There is no spine file. A surface exists when its directory exists. Reserved at
the package root are `manifest.yml` and the `checks/` subtree; every other
-`*.md` is a node. Moving a node to another directory is a rename — its id and
-parent change — and `ghost validate` reports any `relates` that no longer
+`*.md` is a node. Moving a node to another directory is a rename: its id and
+parent change, and `ghost validate` reports any `relates` that no longer
resolve.
Node frontmatter carries only descriptive properties:
@@ -108,8 +108,8 @@ Node frontmatter carries only descriptive properties:
-A node's prose body is written — and read — through three lenses. They shape
-how you write, never frontmatter fields:
+You write and read a node's prose body through three lenses. They shape how you
+write; they are never frontmatter fields:
| Lens | What belongs there |
| --- | --- |
@@ -147,8 +147,8 @@ Write less like a brand book and more like a decision engine.
One contract per package: a repo's `.ghost/` is the whole fingerprint, and
surfaces are directories within it. Reach for a second package only when a
-distinct product genuinely owns its own fingerprint — a separate app, a shared
-brand, a tenant variant.
+distinct product owns its own fingerprint: a separate app, a shared brand, a
+tenant variant.
When several packages share a brand, put the common composition in its own
package and `extends` it by identity:
diff --git a/apps/docs/src/content/docs/getting-started.mdx b/apps/docs/src/content/docs/getting-started.mdx
index 4d3de1cf..32077df3 100644
--- a/apps/docs/src/content/docs/getting-started.mdx
+++ b/apps/docs/src/content/docs/getting-started.mdx
@@ -18,8 +18,8 @@ The canonical portable fingerprint is a directory tree of prose nodes:
```text
.ghost/
manifest.yml # schema + package id
- index.md # the core node — true everywhere
- trust.md # another root node — also true everywhere
+ index.md # the core node, true everywhere
+ trust.md # another root node, also true everywhere
marketing/index.md # the `marketing` surface's own prose
marketing/email.md # a node in the marketing surface
checks/*.md # optional ghost.check/v1 deterministic checks
@@ -37,9 +37,9 @@ package root down to its own folder, so root nodes (`index.md`, `trust.md`)
reach every surface while a sibling surface stays invisible. `relates` links
nodes laterally across that boundary when a relationship carries rationale.
-Every prose node is read through three lenses — intent, inventory, and
-composition — and deterministic `checks/` validate the result afterward; they
-are not generation input.
+Write and read every prose node through three lenses: intent, inventory, and
+composition. Deterministic `checks/` validate the result afterward; they are not
+generation input.
One contract per package: a repo's `.ghost/` is the whole fingerprint, and
surfaces (directories) are the only locality.
@@ -113,9 +113,9 @@ lenses:
3. **Composition** - the patterns that make it intentional: rules, layouts,
structures, flows, states, content, behavior, and visual arrangements.
-These lenses are how the prose body is written, never frontmatter fields. Node
-frontmatter carries only descriptive properties — `description`, `relates`,
-`incarnation`, plus free-form passthrough keys. Raw repo signals are optional
+These lenses shape how you write the prose body; they are never frontmatter
+fields. Node frontmatter carries only descriptive properties: `description`,
+`relates`, `incarnation`, plus free-form passthrough keys. Raw repo signals are optional
authoring evidence. Curate durable intent, inventory, and composition into the
node prose, then use normal Git review for approval. For a fuller human-agent
workflow, read [Fingerprint Authoring](/docs/fingerprint-authoring).
diff --git a/packages/ghost/README.md b/packages/ghost/README.md
index 315c964e..093171c5 100644
--- a/packages/ghost/README.md
+++ b/packages/ghost/README.md
@@ -2,8 +2,8 @@
**A unified Ghost CLI for product-surface composition fingerprints.**
-Agents can assemble UI. They can't reliably preserve the _composition_ behind it
-— the hierarchy, density, restraint, copy, trust, and flow that make a surface
+Agents can assemble UI. They can't reliably preserve the _composition_ behind
+it: the hierarchy, density, restraint, copy, trust, and flow that make a surface
feel intentional. Ghost captures that composition in a repo-local `.ghost/`
package that a host agent reads before it builds and checks after it changes.
@@ -34,12 +34,12 @@ command index, and `ghost --help` shows flags for one command.
## The Shape
-A fingerprint is a directory tree of prose — a **graph of nodes**:
+A fingerprint is a directory tree of prose, a **graph of nodes**:
```text
.ghost/
manifest.yml # schema + id
- index.md # the core node — true everywhere (optional)
+ index.md # the core node, true everywhere (optional)
/index.md # a surface's own prose (the directory is the surface)
/.md # a prose node placed in that surface
checks/*.md # optional ghost.check/v1 checks
@@ -47,7 +47,7 @@ A fingerprint is a directory tree of prose — a **graph of nodes**:
The **directory tree is the graph**. A node is one markdown file: descriptive
frontmatter (`description`, `relates`, `incarnation`) plus a prose body written
-through three lenses — **intent** (the why), **inventory** (the materials), and
+through three lenses: **intent** (the why), **inventory** (the materials), and
**composition** (the patterns). A node's id is its path and its parent is its
directory; a surface is just a directory, and the package-root `index.md` is the
implicit `core` node that reaches every surface.
@@ -83,7 +83,7 @@ verify fingerprints:
ghost skill install
```
-Advanced and maintenance commands — `signals` and `migrate` — remain available
+Advanced and maintenance commands (`signals` and `migrate`) remain available
in the full command index.
No API key is required. `OPENAI_API_KEY` / `VOYAGE_API_KEY` are optional and
diff --git a/packages/ghost/src/skill-bundle/SKILL.md b/packages/ghost/src/skill-bundle/SKILL.md
index 71bdb83a..3ee5ae28 100644
--- a/packages/ghost/src/skill-bundle/SKILL.md
+++ b/packages/ghost/src/skill-bundle/SKILL.md
@@ -15,7 +15,7 @@ materials it draws from, and the patterns that make it feel intentional.
```text
.ghost/
manifest.yml # schema + id
- index.md # the core node — true everywhere (optional)
+ index.md # the core node, true everywhere (optional)
/index.md # a surface's own prose
/.md # a node placed in that surface
checks/*.md # optional ghost.check/v1 checks
@@ -30,18 +30,18 @@ design-system registry, or screenshot archive.
The fingerprint is a graph of **nodes**, and the **directory tree is the graph**.
A node is a markdown file: descriptive frontmatter (`description`, `relates`,
`incarnation`) + a prose body. A node's **identity is its path** (`marketing/email.md`
-→ `marketing/email`) and its **parent is its containing directory** — a surface
+→ `marketing/email`) and its **parent is its containing directory**. A surface
is just a directory, and a directory's own prose lives in its `index.md`
(`marketing/index.md` is the `marketing` surface; the package-root `index.md` is
-the implicit `core` node, true everywhere). **Intent + inventory + composition**
-are the authoring lenses the body is written through — they guide what to
-capture, they are not fields or node types:
+the implicit `core` node, true everywhere). You write the body through three
+authoring lenses, **intent + inventory + composition**. They guide what to
+capture; they are not fields or node types:
-- intent — the why and the stance.
-- inventory — the materials and pointers to implementation the agent can inspect.
-- composition — the patterns that make the surface feel intentional.
+- intent: the why and the stance.
+- inventory: the materials and pointers to implementation the agent can inspect.
+- composition: the patterns that make the surface feel intentional.
-`description` is the retrieval payload — a one-line "what this is / when to
+`description` is the retrieval payload, a one-line "what this is / when to
gather it" (like a tool's name + description); `ghost gather` with no argument
lists nodes by id + description for the agent to match against. The directory
places a node so it is inherited downward (`core` is the implicit root that
@@ -50,15 +50,15 @@ medium-bound expression (essence is untagged). Free-form keys (`audience`, …)
pass through. See [references/capture.md](references/capture.md) for the full
node shape.
-**How `gather` composes** — folders are walls; files fill the corridor:
+**How `gather` composes** (folders are walls; files fill the corridor):
-- **spine** (full bodies): every file from the package root down to the
- surface's own folder is inherited — so a feature's `invariants.md` reaches
- every screen in that feature, and root files reach everywhere. A **sibling
- folder is a wall**: its nodes never appear, not even as a pointer.
+- **spine** (full bodies): the package inherits every file from the root down to
+ the surface's own folder, so a feature's `invariants.md` reaches every screen
+ in that feature, and root files reach everywhere. A **sibling folder is a
+ wall**: its nodes never appear, not even as a pointer.
- **edges** (full bodies, one hop): each spine node's `relates` targets. Author
- a broad rule once at the level it is true — e.g. `relates: { to: arcade }` on
- `features/` — and every descendant inherits it. A link to a hub also unfolds
+ a broad rule once at the level it is true (say `relates: { to: arcade }` on
+ `features/`) and every descendant inherits it. A link to a hub also unfolds
the hub's subtree as spokes.
- **spokes** (pointers: id + description): the surface's own descendants and any
edge hub's subtree. The agent reads the descriptions and pulls what it needs
@@ -82,7 +82,7 @@ the child `ghost` process when they need repo-local Ghost files outside raw
one product in a monorepo). Ghost stays adapter-neutral: wrappers consume JSON
and map severities into their own review or check format.
-A package can **extend** another by identity — the shared-brand pattern. The
+A package can **extend** another by identity (the shared-brand pattern). The
manifest's `extends` maps a package id to where it lives:
`extends: { brand: ../brand/.ghost }`. Then nodes reference inherited context by
identity, never path: `relates: [{ to: brand:core/trust }]` (a `:`
@@ -94,7 +94,7 @@ ref). Inherited nodes are read-only and flow into gather/validate like local one
|---|---|
| `ghost init [--template ]` | Scaffold `.ghost/` with a manifest and a core `index.md` node. |
| `ghost scan [dir] [--format json]` | Report node/surface contribution. |
-| `ghost validate [file-or-dir]` | Validate the package — artifact shape and the node graph (links resolve, one root, acyclic). |
+| `ghost validate [file-or-dir]` | Validate the package: artifact shape and the node graph (links resolve, one root, acyclic). |
| `ghost checks --surface ` | Select and ground the markdown checks governing the named surfaces. |
| `ghost review --surface [--diff ]` | Emit an advisory review packet: touched surfaces, routed checks, and fingerprint grounding (diff embedded verbatim). |
| `ghost gather [surface] [--as ]` | Compose a surface's context slice (corridor spine + relates edges, plus spoke pointers), or list the surface menu. |
diff --git a/packages/ghost/src/skill-bundle/references/authoring-scenarios.md b/packages/ghost/src/skill-bundle/references/authoring-scenarios.md
index fd362645..6f89a708 100644
--- a/packages/ghost/src/skill-bundle/references/authoring-scenarios.md
+++ b/packages/ghost/src/skill-bundle/references/authoring-scenarios.md
@@ -83,11 +83,12 @@ content; scan frequency and raw signals do not establish guidance.
## 4. Draft The Nodes
-Write the smallest useful set of nodes — each a purpose-coherent prose body with
+Write the smallest useful set of nodes, each a purpose-coherent prose body with
a one-line `description`, placed by putting its file in the right surface
-directory and linked with `relates` where a relationship carries meaning. Write each body through the
-intent / inventory / composition lenses — the why, the material (with pointers
-to implementation), and how it is assembled. These are lenses, not fields.
+directory and linked with `relates` where a relationship carries meaning. Write
+each body through the intent / inventory / composition lenses: the why, the
+material (with pointers to implementation), and how it is assembled. These are
+lenses, not fields.
Label uncertain reasoning as provisional. Prefer a few high-confidence nodes
with evidence over a broad catalog. In auto-draft mode, write nodes directly
diff --git a/packages/ghost/src/skill-bundle/references/brief.md b/packages/ghost/src/skill-bundle/references/brief.md
index 69a62f0a..042c2917 100644
--- a/packages/ghost/src/skill-bundle/references/brief.md
+++ b/packages/ghost/src/skill-bundle/references/brief.md
@@ -27,10 +27,10 @@ json` as the agent interface.
The host agent owns natural-language matching: read the surface menu (each
surface's authored description) and pick the surface the ask belongs to. Ghost
-never infers a surface from a repo path — the agent names it.
+never infers a surface from a repo path; the agent names it.
When no surface is selected (or an unknown one is named), `gather` returns the
-surface menu, never the whole tree — choose a surface from it rather than
+surface menu, never the whole tree; choose a surface from it rather than
guessing.
Return a short human-facing brief synthesized from the slice: the relevant
diff --git a/packages/ghost/src/skill-bundle/references/capture.md b/packages/ghost/src/skill-bundle/references/capture.md
index 61d2a16d..a34d9fa6 100644
--- a/packages/ghost/src/skill-bundle/references/capture.md
+++ b/packages/ghost/src/skill-bundle/references/capture.md
@@ -19,7 +19,7 @@ is checked in, Ghost treats the fingerprint package as canonical.
```text
.ghost/
manifest.yml # schema + id
- index.md # the core node — true everywhere
+ index.md # the core node, true everywhere
checkout/ # a surface is a directory
index.md # the checkout surface's own prose
trust.md # a node placed under checkout
@@ -50,17 +50,17 @@ Near the moment of payment, reduce felt risk. Proximity of reassurance to the
action beats completeness…
```
-- **`description`** is how an agent finds the node — a one-line "what this is and
+- **`description`** is how an agent finds the node: a one-line "what this is and
when to gather it," exactly like a tool's name + description. `ghost gather`
with no argument lists nodes by id + description; the agent matches the ask
against those and names one. The body is the node's "implementation"; the
description is what makes it discoverable. Write one on any node worth
anchoring a task at.
-- **The directory places the node** — folders are walls; files fill the
+- **The directory places the node.** Folders are walls; files fill the
corridor. A node inherits every file in the folders above it, up to the root;
a sibling folder is invisible. The brand soul lives in the package-root files
(the `core` node and other root files), so it reaches every surface. Author a
- broad rule at the broadest folder where it is true — a feature's
+ broad rule at the broadest folder where it is true: a feature's
`invariants.md` reaches every screen in that feature and nowhere else.
- **`relates`** links laterally when a relationship carries rationale. When the
rationale is rich (e.g. "checkout and item-detail disagree on density on
@@ -72,15 +72,15 @@ action beats completeness…
Intent / inventory / composition are **authoring lenses**, not fields and not
node types. They are the things worth thinking through as you write a node's
-prose — a node may lean entirely on one:
+prose, and a node may lean entirely on one:
-- **intent** — the why and the stance.
-- **inventory** — the material you have (tokens, components, and pointers to the
+- **intent**: the why and the stance.
+- **inventory**: the material you have (tokens, components, and pointers to the
actual implementation in code).
-- **composition** — how it is assembled (the patterns that make it intentional).
+- **composition**: how it is assembled (the patterns that make it intentional).
A finding cites a node by id, so keep a node **purpose-coherent**: one purpose,
-any length. Split into a second node only when a handle diverges — a different
+any length. Split into a second node only when a handle diverges, say a different
directory (parent), a different `incarnation`, or a genuinely different
`relates` role.
@@ -105,14 +105,14 @@ ghost scan
```
`ghost init` is template-driven (`--template ` selects a starter). The
-default template seeds the package-root `index.md` — the `core` node —
+default template seeds the package-root `index.md` (the `core` node),
demonstrating the shape.
### 3. Shape the tree
Add a surface by adding a directory: `checkout/` is the `checkout` surface, and
`checkout/index.md` holds its prose. Nest surfaces by nesting directories. The
-tree is the layout itself — a node's id and parent come from where its file
+tree is the layout itself; a node's id and parent come from where its file
sits, never from a declared spine.
### 4. Orient
@@ -120,11 +120,11 @@ sits, never from a declared spine.
Read the product, not just the component library. Look for surfaces, docs,
tests, stories, routes, screenshots, or examples that reveal hierarchy,
behavior, copy, accessibility, trust, and flow. `ghost signals .` emits raw
-scratch observations — curate, never copy verbatim into a node.
+scratch observations; curate, never copy verbatim into a node.
### 5. Write sparse nodes
-Add the smallest useful set of nodes — each a purpose-coherent prose body
+Add the smallest useful set of nodes, each a purpose-coherent prose body
written through the lenses, placed by putting its file in the right directory
and linked with `relates` where a relationship carries meaning. Prefer a few
high-confidence nodes over a noisy
diff --git a/packages/ghost/src/skill-bundle/references/review.md b/packages/ghost/src/skill-bundle/references/review.md
index 668c7f29..91b65cff 100644
--- a/packages/ghost/src/skill-bundle/references/review.md
+++ b/packages/ghost/src/skill-bundle/references/review.md
@@ -22,7 +22,7 @@ in the surface's fingerprint slice. Use JSON as the agent contract. It includes:
- `touched_surfaces`: the surfaces the diff resolved to
- `checks`: the relevant checks per surface, with `relevance` (own or inherited)
- `grounding`: per surface, the slice's prose `nodes`, each with `provenance`
- (own / ancestor / edge). The why and the what live in each node's prose — read
+ (own / ancestor / edge). The why and the what live in each node's prose; read
the grounded nodes, own first, then inherited, then related.
Ghost selects and grounds the checks; it does not run them. Evaluate each
@@ -48,7 +48,7 @@ refs (principles/contracts as the why, exemplars as what good looks like), and a
repair or intentional-divergence rationale.
When a surface's grounding is silent, local evidence can still support advisory
-critique — label those findings provisional and non-Ghost-backed.
+critique; label those findings provisional and non-Ghost-backed.
Fingerprint edits are ordinary Git-reviewed edits to the split fingerprint
package. Do not silently rewrite the Ghost package during review unless the user
diff --git a/packages/ghost/src/skill-bundle/references/schema.md b/packages/ghost/src/skill-bundle/references/schema.md
index c61c3e56..5cdb0fb3 100644
--- a/packages/ghost/src/skill-bundle/references/schema.md
+++ b/packages/ghost/src/skill-bundle/references/schema.md
@@ -1,6 +1,6 @@
---
name: schema
-description: The Ghost fingerprint package shape — nodes, the spine, checks, and extends.
+description: The Ghost fingerprint package shape: nodes, the directory tree, checks, and extends.
---
# Ghost Fingerprint Package Reference
@@ -9,11 +9,11 @@ Canonical package:
```text
.ghost/
- manifest.yml ghost.fingerprint-package/v1 — id + optional extends
- index.md the core node — true everywhere (optional)
+ manifest.yml ghost.fingerprint-package/v1: id + optional extends
+ index.md the core node, true everywhere (optional)
/index.md a surface's own prose (the directory is the surface)
- /.md ghost.node/v1 — a node placed in that surface
- checks/*.md optional ghost.check/v1 — agent-evaluated output checks
+ /.md ghost.node/v1: a node placed in that surface
+ checks/*.md optional ghost.check/v1: agent-evaluated output checks
```
The **directory tree is the graph**: a node's id is its path and its parent is
@@ -27,7 +27,7 @@ paths and infers nothing from repo location.
## Nodes
-A node is the unit — a markdown file with descriptive frontmatter + a prose
+A node is the unit: a markdown file with descriptive frontmatter + a prose
body. Identity and containment are not in the frontmatter; they are where the
file sits. A node at `checkout/trust.md`:
@@ -56,7 +56,7 @@ There is no spine file. A surface exists when its directory exists; give it pros
with an `index.md`, place nodes inside it, and nest surfaces by nesting
directories. A surface that needs no prose of its own is simply a directory that
holds nodes. Moving a node to another directory changes its id (a rename) and
-its parent — `ghost validate` reports any `relates` that no longer resolve.
+its parent; `ghost validate` reports any `relates` that no longer resolve.
## Manifest + extends
@@ -68,7 +68,7 @@ extends:
```
A `brand:core/trust` ref in `relates` resolves into the extended package's nodes
-(read-only) — a `:` ref. Reference is by identity (the `extends`
+(read-only), a `:` ref. Reference is by identity (the `extends`
key), never by repo path.
## Gather
@@ -76,7 +76,7 @@ key), never by repo path.
`ghost gather ` composes a node's slice, filtered by `--as `:
- **spine** (full bodies): every file on the corridor from the package root
- down to the node's own folder. Folders are walls — sibling folders never
+ down to the node's own folder. Folders are walls; sibling folders never
appear.
- **edges** (full bodies, one hop): each spine node's `relates` targets. A rule
authored high in the corridor (e.g. `relates: { to: arcade }` on `features/`)
From e4bea42ee8e442cadc90e0e7cd61111a125f6eb4 Mon Sep 17 00:00:00 2001
From: Nahiyan Khan
Date: Sun, 28 Jun 2026 21:35:18 -0400
Subject: [PATCH 10/12] chore: delete dead fingerprint.md-era fossils
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Remove three orphaned artifacts from the pre-node-graph fingerprint.md format:
- schemas/fingerprint.schema.json: JSON Schema requiring palette/typography/
spacing frontmatter — a format that no longer exists. Not published, last
touched before the node-graph reset, referenced nowhere.
- scripts/emit-fingerprint-schema.mjs: its generator. Broken — reads from a
deleted dist/scan/schema.js and a toJsonSchema() that no longer exists.
- scripts/strip-signature.mjs: one-shot helper for stripping '# Signature'
blocks from fingerprint.md files. Dead format, referenced nowhere.
No code, build step, or CI references any of them.
---
schemas/fingerprint.schema.json | 383 ----------------------------
scripts/emit-fingerprint-schema.mjs | 27 --
scripts/strip-signature.mjs | 38 ---
3 files changed, 448 deletions(-)
delete mode 100644 schemas/fingerprint.schema.json
delete mode 100644 scripts/emit-fingerprint-schema.mjs
delete mode 100644 scripts/strip-signature.mjs
diff --git a/schemas/fingerprint.schema.json b/schemas/fingerprint.schema.json
deleted file mode 100644
index abc62175..00000000
--- a/schemas/fingerprint.schema.json
+++ /dev/null
@@ -1,383 +0,0 @@
-{
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "type": "object",
- "properties": {
- "name": {
- "type": "string"
- },
- "slug": {
- "type": "string"
- },
- "generator": {
- "type": "string"
- },
- "generated": {
- "type": "string"
- },
- "confidence": {
- "type": "number"
- },
- "extends": {
- "type": "string"
- },
- "metadata": {
- "type": "object",
- "propertyNames": {
- "type": "string"
- },
- "additionalProperties": {}
- },
- "id": {
- "type": "string"
- },
- "source": {
- "type": "string",
- "enum": ["registry", "extraction", "llm", "unknown"]
- },
- "timestamp": {
- "type": "string"
- },
- "sources": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "references": {
- "type": "object",
- "properties": {
- "specs": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "components": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "examples": {
- "type": "array",
- "items": {
- "type": "string"
- }
- }
- },
- "additionalProperties": false
- },
- "observation": {
- "type": "object",
- "properties": {
- "personality": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "resembles": {
- "type": "array",
- "items": {
- "type": "string"
- }
- }
- },
- "additionalProperties": false
- },
- "decisions": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "dimension": {
- "type": "string"
- },
- "dimension_kind": {
- "type": "string"
- },
- "embedding": {
- "type": "array",
- "items": {
- "type": "number"
- }
- }
- },
- "required": ["dimension"],
- "additionalProperties": false
- }
- },
- "checks": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string"
- },
- "canonical": {
- "type": "string"
- },
- "kind": {
- "type": "string",
- "enum": [
- "color",
- "radius",
- "spacing",
- "type-size",
- "type-family",
- "type-weight",
- "shadow",
- "motion"
- ]
- },
- "summary": {
- "type": "string"
- },
- "pattern": {
- "type": "string"
- },
- "enforce_at": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "severity": {
- "type": "string",
- "enum": ["critical", "serious", "nit"]
- },
- "match": {
- "type": "string",
- "enum": ["exact", "band", "percent", "structural"]
- },
- "tolerance": {
- "type": "number"
- },
- "presence_floor": {
- "type": "integer",
- "minimum": 0,
- "maximum": 9007199254740991
- },
- "observed_count": {
- "type": "integer",
- "minimum": 0,
- "maximum": 9007199254740991
- },
- "support": {
- "type": "number",
- "minimum": 0,
- "maximum": 1
- },
- "rationale": {
- "type": "string"
- }
- },
- "required": ["id", "pattern"],
- "additionalProperties": false
- }
- },
- "palette": {
- "type": "object",
- "properties": {
- "dominant": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "role": {
- "type": "string"
- },
- "value": {
- "type": "string"
- },
- "oklch": {
- "type": "array",
- "prefixItems": [
- {
- "type": "number"
- },
- {
- "type": "number"
- },
- {
- "type": "number"
- }
- ]
- }
- },
- "required": ["role", "value"],
- "additionalProperties": false
- }
- },
- "neutrals": {
- "type": "object",
- "properties": {
- "steps": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "count": {
- "type": "number"
- }
- },
- "required": ["steps", "count"],
- "additionalProperties": false
- },
- "semantic": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "role": {
- "type": "string"
- },
- "value": {
- "type": "string"
- },
- "oklch": {
- "type": "array",
- "prefixItems": [
- {
- "type": "number"
- },
- {
- "type": "number"
- },
- {
- "type": "number"
- }
- ]
- }
- },
- "required": ["role", "value"],
- "additionalProperties": false
- }
- },
- "saturationProfile": {
- "type": "string",
- "enum": ["muted", "vibrant", "mixed"]
- },
- "contrast": {
- "type": "string",
- "enum": ["high", "moderate", "low"]
- }
- },
- "required": [
- "dominant",
- "neutrals",
- "semantic",
- "saturationProfile",
- "contrast"
- ],
- "additionalProperties": false
- },
- "spacing": {
- "type": "object",
- "properties": {
- "scale": {
- "type": "array",
- "items": {
- "type": "number"
- }
- },
- "regularity": {
- "type": "number"
- },
- "baseUnit": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "null"
- }
- ]
- }
- },
- "required": ["scale", "regularity", "baseUnit"],
- "additionalProperties": false
- },
- "typography": {
- "type": "object",
- "properties": {
- "families": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "sizeRamp": {
- "type": "array",
- "items": {
- "type": "number"
- }
- },
- "weightDistribution": {
- "type": "object",
- "propertyNames": {
- "type": "string"
- },
- "additionalProperties": {
- "type": "number"
- }
- },
- "lineHeightPattern": {
- "type": "string",
- "enum": ["tight", "normal", "loose"]
- }
- },
- "required": [
- "families",
- "sizeRamp",
- "weightDistribution",
- "lineHeightPattern"
- ],
- "additionalProperties": false
- },
- "surfaces": {
- "type": "object",
- "properties": {
- "borderRadii": {
- "type": "array",
- "items": {
- "type": "number"
- }
- },
- "shadowComplexity": {
- "type": "string",
- "enum": ["deliberate-none", "subtle", "layered"]
- },
- "borderUsage": {
- "type": "string",
- "enum": ["minimal", "moderate", "heavy"]
- },
- "borderTokenCount": {
- "type": "number"
- }
- },
- "required": ["borderRadii", "shadowComplexity", "borderUsage"],
- "additionalProperties": false
- },
- "embedding": {
- "type": "array",
- "items": {
- "type": "number"
- }
- }
- },
- "required": [
- "id",
- "source",
- "timestamp",
- "palette",
- "spacing",
- "typography",
- "surfaces"
- ],
- "additionalProperties": false,
- "title": "Ghost Fingerprint Frontmatter",
- "description": "Schema for YAML frontmatter in Ghost fingerprint.md files."
-}
diff --git a/scripts/emit-fingerprint-schema.mjs b/scripts/emit-fingerprint-schema.mjs
deleted file mode 100644
index 9f9e01f7..00000000
--- a/scripts/emit-fingerprint-schema.mjs
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/usr/bin/env node
-/**
- * Emit schemas/fingerprint.schema.json from the zod source of truth.
- * Run after changes to packages/ghost/src/scan/schema.ts:
- *
- * pnpm --filter @anarchitecture/ghost build && node scripts/emit-fingerprint-schema.mjs
- */
-import { existsSync, mkdirSync, writeFileSync } from "node:fs";
-import { dirname, resolve } from "node:path";
-import { fileURLToPath } from "node:url";
-
-const here = dirname(fileURLToPath(import.meta.url));
-const root = resolve(here, "..");
-const { toJsonSchema } = await import(
- resolve(root, "packages/ghost/dist/scan/schema.js")
-);
-
-const schema = toJsonSchema();
-schema.title = "Ghost Fingerprint Frontmatter";
-schema.description =
- "Schema for YAML frontmatter in Ghost fingerprint.md files.";
-
-const outDir = resolve(root, "schemas");
-if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
-const outPath = resolve(outDir, "fingerprint.schema.json");
-writeFileSync(outPath, `${JSON.stringify(schema, null, 2)}\n`);
-console.log(`Wrote ${outPath}`);
diff --git a/scripts/strip-signature.mjs b/scripts/strip-signature.mjs
deleted file mode 100644
index 6d2f1bc5..00000000
--- a/scripts/strip-signature.mjs
+++ /dev/null
@@ -1,38 +0,0 @@
-#!/usr/bin/env node
-// One-shot script: remove `# Signature` body blocks from a list of
-// fingerprint.md files. The block runs from the `# Signature` line up to
-// (but not including) the next H1 heading or EOF. Idempotent.
-import { readFileSync, writeFileSync } from "node:fs";
-
-const files = process.argv.slice(2);
-if (files.length === 0) {
- console.error("usage: strip-signature.mjs ...");
- process.exit(2);
-}
-
-let touched = 0;
-for (const f of files) {
- const raw = readFileSync(f, "utf8");
- const lines = raw.split("\n");
- const out = [];
- let inSig = false;
- for (const line of lines) {
- if (/^# Signature\s*$/.test(line)) {
- inSig = true;
- continue;
- }
- if (inSig && /^# /.test(line)) {
- inSig = false;
- }
- if (!inSig) out.push(line);
- }
- const next = out.join("\n").replace(/\n{3,}/g, "\n\n");
- if (next !== raw) {
- writeFileSync(f, next, "utf8");
- touched++;
- console.log(`stripped: ${f}`);
- } else {
- console.log(`unchanged: ${f}`);
- }
-}
-console.log(`${touched}/${files.length} files modified`);
From fffadef9e0ed6d818c4fe7159d347bfbf33b5e06 Mon Sep 17 00:00:00 2001
From: Nahiyan Khan
Date: Sun, 28 Jun 2026 21:43:06 -0400
Subject: [PATCH 11/12] docs: omit the ghost-ui catalogue from the docs site
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Remove the Design Language catalogue (/ui/* pages, the /tools/ui landing) and
everything that only served it: the component/foundation demo components
(ai-elements, primitives, bento, examples, foundations), the registry loader
(lib/component-docs, registry-sidebar, demo-loader, component-page-shell,
open-in-v0), and the catalogue's theme editor (theme-panel, ThemePanelContext).
Drop the ghost-ui entries from the tools index and the dock search. Keep
ghost-ui as the docs site's runtime dependency (ThemeProvider, useTheme,
useStaggerReveal) — that is the shell the site is built with, not documented
content. 150 files removed; build, tests, and checks green.
---
apps/docs/src/App.tsx | 45 +-
apps/docs/src/app/tools/page.tsx | 12 +-
apps/docs/src/app/tools/ui/page.tsx | 84 ---
.../src/app/ui/components/[name]/page.tsx | 59 --
apps/docs/src/app/ui/components/page.tsx | 170 -----
.../src/app/ui/foundations/colors/page.tsx | 32 -
apps/docs/src/app/ui/foundations/page.tsx | 103 ---
.../app/ui/foundations/typography/page.tsx | 32 -
apps/docs/src/app/ui/page.tsx | 131 ----
.../docs/ai-elements/agent-demo.tsx | 89 ---
.../docs/ai-elements/artifact-demo.tsx | 67 --
.../docs/ai-elements/attachments-demo.tsx | 83 ---
.../docs/ai-elements/audio-player-demo.tsx | 53 --
.../docs/ai-elements/canvas-demo.tsx | 87 ---
.../ai-elements/chain-of-thought-demo.tsx | 52 --
.../docs/ai-elements/checkpoint-demo.tsx | 27 -
.../docs/ai-elements/code-block-demo.tsx | 79 --
.../docs/ai-elements/commit-demo.tsx | 122 ----
.../docs/ai-elements/confirmation-demo.tsx | 70 --
.../docs/ai-elements/connection-demo.tsx | 70 --
.../docs/ai-elements/context-demo.tsx | 44 --
.../docs/ai-elements/controls-demo.tsx | 50 --
.../docs/ai-elements/conversation-demo.tsx | 24 -
.../components/docs/ai-elements/edge-demo.tsx | 85 ---
.../environment-variables-demo.tsx | 74 --
.../docs/ai-elements/file-tree-demo.tsx | 35 -
.../docs/ai-elements/image-demo.tsx | 47 --
.../src/components/docs/ai-elements/index.tsx | 238 -------
.../docs/ai-elements/inline-citation-demo.tsx | 65 --
.../docs/ai-elements/jsx-preview-demo.tsx | 32 -
.../docs/ai-elements/message-demo.tsx | 49 --
.../docs/ai-elements/mic-selector-demo.tsx | 48 --
.../docs/ai-elements/model-selector-demo.tsx | 68 --
.../components/docs/ai-elements/node-demo.tsx | 83 ---
.../docs/ai-elements/open-in-chat-demo.tsx | 42 --
.../docs/ai-elements/package-info-demo.tsx | 57 --
.../docs/ai-elements/panel-demo.tsx | 61 --
.../docs/ai-elements/persona-demo.tsx | 55 --
.../components/docs/ai-elements/plan-demo.tsx | 63 --
.../docs/ai-elements/prompt-input-demo.tsx | 29 -
.../docs/ai-elements/queue-demo.tsx | 86 ---
.../docs/ai-elements/reasoning-demo.tsx | 29 -
.../docs/ai-elements/sandbox-demo.tsx | 84 ---
.../docs/ai-elements/schema-display-demo.tsx | 171 -----
.../docs/ai-elements/shimmer-demo.tsx | 21 -
.../docs/ai-elements/snippet-demo.tsx | 52 --
.../docs/ai-elements/sources-demo.tsx | 25 -
.../docs/ai-elements/speech-input-demo.tsx | 31 -
.../docs/ai-elements/stack-trace-demo.tsx | 65 --
.../docs/ai-elements/suggestion-demo.tsx | 18 -
.../components/docs/ai-elements/task-demo.tsx | 43 --
.../docs/ai-elements/terminal-demo.tsx | 63 --
.../docs/ai-elements/test-results-demo.tsx | 132 ----
.../components/docs/ai-elements/tool-demo.tsx | 63 --
.../docs/ai-elements/toolbar-demo.tsx | 71 --
.../docs/ai-elements/transcription-demo.tsx | 54 --
.../docs/ai-elements/voice-selector-demo.tsx | 137 ----
.../docs/ai-elements/web-preview-demo.tsx | 52 --
.../components/docs/bento/activity-goal.tsx | 63 --
.../src/components/docs/bento/calendar.tsx | 24 -
apps/docs/src/components/docs/bento/chat.tsx | 249 -------
.../components/docs/bento/cookie-settings.tsx | 60 --
.../components/docs/bento/create-account.tsx | 60 --
.../src/components/docs/bento/data-table.tsx | 322 ---------
apps/docs/src/components/docs/bento/index.tsx | 100 ---
.../docs/src/components/docs/bento/metric.tsx | 105 ---
.../components/docs/bento/payment-amount.tsx | 63 --
.../components/docs/bento/payment-method.tsx | 138 ----
.../components/docs/bento/report-issue.tsx | 93 ---
apps/docs/src/components/docs/bento/share.tsx | 121 ----
apps/docs/src/components/docs/bento/stats.tsx | 215 ------
.../components/docs/bento/team-members.tsx | 196 -----
.../components/docs/component-page-shell.tsx | 577 ---------------
apps/docs/src/components/docs/demo-loader.tsx | 536 --------------
apps/docs/src/components/docs/dock.tsx | 9 -
.../docs/examples/code-block/with-diff.tsx | 86 ---
.../examples/conversation/with-messages.tsx | 46 --
.../docs/examples/message/streaming.tsx | 24 -
.../docs/examples/message/with-actions.tsx | 47 --
.../prompt-input/with-attachments.tsx | 31 -
.../components/docs/foundations/colors.tsx | 444 ------------
.../docs/foundations/typography.tsx | 297 --------
.../src/components/docs/open-in-v0-button.tsx | 41 --
.../docs/primitives/accordion-demo.tsx | 72 --
.../components/docs/primitives/alert-demo.tsx | 109 ---
.../docs/primitives/alert-dialog-demo.tsx | 35 -
.../docs/primitives/aspect-ratio-demo.tsx | 18 -
.../docs/primitives/avatar-demo.tsx | 92 ---
.../components/docs/primitives/badge-demo.tsx | 60 --
.../docs/primitives/breadcrumb-demo.tsx | 47 --
.../docs/primitives/button-demo.tsx | 110 ---
.../docs/primitives/calendar-demo.tsx | 46 --
.../components/docs/primitives/card-demo.tsx | 187 -----
.../docs/primitives/carousel-demo.tsx | 79 --
.../docs/primitives/chart-area-demo.tsx | 91 ---
.../docs/primitives/chart-banded-demo.tsx | 117 ---
.../docs/primitives/chart-bar-demo.tsx | 77 --
.../docs/primitives/chart-bar-mixed.tsx | 100 ---
.../components/docs/primitives/chart-demo.tsx | 23 -
.../docs/primitives/chart-line-demo.tsx | 97 ---
.../docs/primitives/chart-pie-demo.tsx | 151 ----
.../docs/primitives/chart-posneg-bar-demo.tsx | 129 ----
.../docs/primitives/checkbox-demo.tsx | 40 --
.../docs/primitives/collapsible-demo.tsx | 45 --
.../docs/primitives/combobox-demo.tsx | 400 -----------
.../docs/primitives/command-demo.tsx | 86 ---
.../docs/primitives/component-wrapper.tsx | 165 -----
.../docs/primitives/context-menu-demo.tsx | 78 --
.../docs/primitives/date-picker-demo.tsx | 93 ---
.../docs/primitives/dialog-demo.tsx | 133 ----
.../docs/primitives/drawer-demo.tsx | 212 ------
.../docs/primitives/dropdown-menu-demo.tsx | 369 ----------
.../components/docs/primitives/form-demo.tsx | 419 -----------
.../components/docs/primitives/forms-demo.tsx | 227 ------
.../docs/primitives/hover-card-demo.tsx | 40 --
.../src/components/docs/primitives/index.tsx | 200 ------
.../components/docs/primitives/input-demo.tsx | 23 -
.../docs/primitives/input-otp-demo.tsx | 108 ---
.../components/docs/primitives/label-demo.tsx | 24 -
.../docs/primitives/menubar-demo.tsx | 129 ----
.../docs/primitives/navigation-menu-demo.tsx | 224 ------
.../docs/primitives/pagination-demo.tsx | 40 --
.../docs/primitives/popover-demo.tsx | 64 --
.../docs/primitives/progress-demo.tsx | 15 -
.../docs/primitives/radio-group-demo.tsx | 58 --
.../docs/primitives/resizable-demo.tsx | 66 --
.../docs/primitives/scroll-area-demo.tsx | 73 --
.../docs/primitives/select-demo.tsx | 92 ---
.../docs/primitives/separator-demo.tsx | 22 -
.../components/docs/primitives/sheet-demo.tsx | 95 ---
.../docs/primitives/skeleton-demo.tsx | 28 -
.../docs/primitives/slider-demo.tsx | 42 --
.../docs/primitives/sonner-demo.tsx | 131 ----
.../docs/primitives/switch-demo.tsx | 33 -
.../components/docs/primitives/table-demo.tsx | 87 ---
.../components/docs/primitives/tabs-demo.tsx | 105 ---
.../docs/primitives/textarea-demo.tsx | 39 -
.../docs/primitives/toggle-demo.tsx | 39 -
.../docs/primitives/toggle-group-demo.tsx | 71 --
.../docs/primitives/tooltip-demo.tsx | 43 --
.../src/components/docs/registry-sidebar.tsx | 187 -----
.../components/theme-panel/ColorControls.tsx | 157 ----
.../components/theme-panel/ColorSwatch.tsx | 29 -
.../components/theme-panel/ExportReset.tsx | 56 --
.../components/theme-panel/PresetSelector.tsx | 60 --
.../components/theme-panel/RadiusControls.tsx | 64 --
.../components/theme-panel/ShadowControls.tsx | 64 --
.../src/components/theme-panel/ThemePanel.tsx | 105 ---
.../theme-panel/ThemePanelTrigger.tsx | 20 -
.../theme-panel/TypographyControls.tsx | 194 -----
.../src/components/theme/ThemeControls.tsx | 18 -
apps/docs/src/contexts/ThemePanelContext.tsx | 248 -------
apps/docs/src/lib/component-docs.ts | 673 ------------------
153 files changed, 4 insertions(+), 15529 deletions(-)
delete mode 100644 apps/docs/src/app/tools/ui/page.tsx
delete mode 100644 apps/docs/src/app/ui/components/[name]/page.tsx
delete mode 100644 apps/docs/src/app/ui/components/page.tsx
delete mode 100644 apps/docs/src/app/ui/foundations/colors/page.tsx
delete mode 100644 apps/docs/src/app/ui/foundations/page.tsx
delete mode 100644 apps/docs/src/app/ui/foundations/typography/page.tsx
delete mode 100644 apps/docs/src/app/ui/page.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/agent-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/artifact-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/attachments-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/audio-player-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/canvas-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/chain-of-thought-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/checkpoint-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/code-block-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/commit-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/confirmation-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/connection-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/context-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/controls-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/conversation-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/edge-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/environment-variables-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/file-tree-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/image-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/index.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/inline-citation-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/jsx-preview-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/message-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/mic-selector-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/model-selector-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/node-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/open-in-chat-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/package-info-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/panel-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/persona-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/plan-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/prompt-input-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/queue-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/reasoning-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/sandbox-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/schema-display-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/shimmer-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/snippet-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/sources-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/speech-input-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/stack-trace-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/suggestion-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/task-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/terminal-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/test-results-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/tool-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/toolbar-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/transcription-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/voice-selector-demo.tsx
delete mode 100644 apps/docs/src/components/docs/ai-elements/web-preview-demo.tsx
delete mode 100644 apps/docs/src/components/docs/bento/activity-goal.tsx
delete mode 100644 apps/docs/src/components/docs/bento/calendar.tsx
delete mode 100644 apps/docs/src/components/docs/bento/chat.tsx
delete mode 100644 apps/docs/src/components/docs/bento/cookie-settings.tsx
delete mode 100644 apps/docs/src/components/docs/bento/create-account.tsx
delete mode 100644 apps/docs/src/components/docs/bento/data-table.tsx
delete mode 100644 apps/docs/src/components/docs/bento/index.tsx
delete mode 100644 apps/docs/src/components/docs/bento/metric.tsx
delete mode 100644 apps/docs/src/components/docs/bento/payment-amount.tsx
delete mode 100644 apps/docs/src/components/docs/bento/payment-method.tsx
delete mode 100644 apps/docs/src/components/docs/bento/report-issue.tsx
delete mode 100644 apps/docs/src/components/docs/bento/share.tsx
delete mode 100644 apps/docs/src/components/docs/bento/stats.tsx
delete mode 100644 apps/docs/src/components/docs/bento/team-members.tsx
delete mode 100644 apps/docs/src/components/docs/component-page-shell.tsx
delete mode 100644 apps/docs/src/components/docs/demo-loader.tsx
delete mode 100644 apps/docs/src/components/docs/examples/code-block/with-diff.tsx
delete mode 100644 apps/docs/src/components/docs/examples/conversation/with-messages.tsx
delete mode 100644 apps/docs/src/components/docs/examples/message/streaming.tsx
delete mode 100644 apps/docs/src/components/docs/examples/message/with-actions.tsx
delete mode 100644 apps/docs/src/components/docs/examples/prompt-input/with-attachments.tsx
delete mode 100644 apps/docs/src/components/docs/foundations/colors.tsx
delete mode 100644 apps/docs/src/components/docs/foundations/typography.tsx
delete mode 100644 apps/docs/src/components/docs/open-in-v0-button.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/accordion-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/alert-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/alert-dialog-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/aspect-ratio-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/avatar-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/badge-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/breadcrumb-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/button-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/calendar-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/card-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/carousel-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/chart-area-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/chart-banded-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/chart-bar-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/chart-bar-mixed.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/chart-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/chart-line-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/chart-pie-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/chart-posneg-bar-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/checkbox-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/collapsible-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/combobox-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/command-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/component-wrapper.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/context-menu-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/date-picker-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/dialog-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/drawer-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/dropdown-menu-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/form-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/forms-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/hover-card-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/index.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/input-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/input-otp-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/label-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/menubar-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/navigation-menu-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/pagination-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/popover-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/progress-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/radio-group-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/resizable-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/scroll-area-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/select-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/separator-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/sheet-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/skeleton-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/slider-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/sonner-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/switch-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/table-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/tabs-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/textarea-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/toggle-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/toggle-group-demo.tsx
delete mode 100644 apps/docs/src/components/docs/primitives/tooltip-demo.tsx
delete mode 100644 apps/docs/src/components/docs/registry-sidebar.tsx
delete mode 100644 apps/docs/src/components/theme-panel/ColorControls.tsx
delete mode 100644 apps/docs/src/components/theme-panel/ColorSwatch.tsx
delete mode 100644 apps/docs/src/components/theme-panel/ExportReset.tsx
delete mode 100644 apps/docs/src/components/theme-panel/PresetSelector.tsx
delete mode 100644 apps/docs/src/components/theme-panel/RadiusControls.tsx
delete mode 100644 apps/docs/src/components/theme-panel/ShadowControls.tsx
delete mode 100644 apps/docs/src/components/theme-panel/ThemePanel.tsx
delete mode 100644 apps/docs/src/components/theme-panel/ThemePanelTrigger.tsx
delete mode 100644 apps/docs/src/components/theme-panel/TypographyControls.tsx
delete mode 100644 apps/docs/src/components/theme/ThemeControls.tsx
delete mode 100644 apps/docs/src/contexts/ThemePanelContext.tsx
delete mode 100644 apps/docs/src/lib/component-docs.ts
diff --git a/apps/docs/src/App.tsx b/apps/docs/src/App.tsx
index 1ce6a27b..91dfb7cd 100644
--- a/apps/docs/src/App.tsx
+++ b/apps/docs/src/App.tsx
@@ -1,27 +1,15 @@
import { ThemeProvider } from "ghost-ui";
import { useEffect } from "react";
-import { Navigate, Route, Routes, useLocation, useParams } from "react-router";
+import { Navigate, Route, Routes, useLocation } from "react-router";
import DocsIndex from "@/app/docs/page";
import HomePage from "@/app/page";
import GhostDriftLanding from "@/app/tools/drift/page";
import GhostFleetLanding from "@/app/tools/fleet/page";
import ToolsIndex from "@/app/tools/page";
import GhostScanLanding from "@/app/tools/scan/page";
-import GhostUiLanding from "@/app/tools/ui/page";
-import ComponentPage from "@/app/ui/components/[name]/page";
-import ComponentsIndex from "@/app/ui/components/page";
-import ColorsPage from "@/app/ui/foundations/colors/page";
-import FoundationsIndex from "@/app/ui/foundations/page";
-import TypographyPage from "@/app/ui/foundations/typography/page";
-import DesignLanguageIndex from "@/app/ui/page";
import { Dock } from "@/components/docs/dock";
import { mdxDocsRoutes } from "@/routes/docs-routes";
-function ComponentRedirect() {
- const { name } = useParams<{ name: string }>();
- return ;
-}
-
function ScrollToHash() {
const { hash, pathname } = useLocation();
@@ -60,7 +48,6 @@ export function App() {
} />
} />
} />
- } />
{/* Cross-tool docs hub */}
} />
@@ -74,17 +61,6 @@ export function App() {
{/* MDX-authored doc pages under /docs/* */}
{mdxDocsRoutes()}
- {/* Design Language (ghost-ui catalogue) */}
- } />
- } />
- } />
- }
- />
- } />
- } />
-
{/* Redirects from the previous /tools/drift/{getting-started,cli} URLs */}
}
/>
-
- {/* Redirects from legacy root /foundations and /components URLs */}
- }
- />
- }
- />
- }
- />
- }
- />
- } />
diff --git a/apps/docs/src/app/tools/page.tsx b/apps/docs/src/app/tools/page.tsx
index 4ea48d66..6fd44c69 100644
--- a/apps/docs/src/app/tools/page.tsx
+++ b/apps/docs/src/app/tools/page.tsx
@@ -1,7 +1,7 @@
"use client";
import { useStaggerReveal } from "ghost-ui";
-import { FileText, Network, Orbit, Palette } from "lucide-react";
+import { FileText, Network, Orbit } from "lucide-react";
import type { ReactNode } from "react";
import { Link } from "react-router";
import { AnimatedPageHeader } from "@/components/docs/animated-page-header";
@@ -31,12 +31,6 @@ const tools: {
blurb: "Compare projects",
icon: ,
},
- {
- name: "ghost-ui",
- href: "/tools/ui",
- blurb: "Reference UI library",
- icon: ,
- },
];
function ToolStrip() {
@@ -49,7 +43,7 @@ function ToolStrip() {
return (
{tools.map((tool) => (
diff --git a/apps/docs/src/app/tools/ui/page.tsx b/apps/docs/src/app/tools/ui/page.tsx
deleted file mode 100644
index af688d82..00000000
--- a/apps/docs/src/app/tools/ui/page.tsx
+++ /dev/null
@@ -1,84 +0,0 @@
-"use client";
-
-import { useStaggerReveal } from "ghost-ui";
-import { Box, Component, Layers } from "lucide-react";
-import type { ReactNode } from "react";
-import { Link } from "react-router";
-import { AnimatedPageHeader } from "@/components/docs/animated-page-header";
-import { SectionWrapper } from "@/components/docs/wrappers";
-import { getAllComponents } from "@/lib/component-registry";
-
-const componentCount = getAllComponents().length;
-
-const cards: {
- name: string;
- href: string;
- description: string;
- icon: ReactNode;
-}[] = [
- {
- name: "Foundations",
- href: "/ui/foundations",
- description:
- "Color, typography, and the design tokens that underpin every Ghost UI component.",
- icon: ,
- },
- {
- name: `Components (${componentCount})`,
- href: "/ui/components",
- description:
- "Production-ready primitives + AI elements. Distributed via the shadcn registry.json, installed component-by-component, never wholesale.",
- icon: ,
- },
- {
- name: "MCP server",
- href: "https://github.com/block/ghost/tree/main/packages/ghost-ui#mcp-server",
- description:
- "ghost-mcp re-exposes the registry to AI assistants with five tools and two resources, so an agent can search components and pull source.",
- icon: ,
- },
-];
-
-export default function GhostUiLanding() {
- const ref = useStaggerReveal(".tool-card", {
- stagger: 0.06,
- y: 30,
- duration: 0.7,
- });
-
- return (
-
-
-
-
- {cards.map((item) => (
-
-
- {item.icon}
-
-
-
- {item.name}
-
-
-
-
- {item.description}
-
-
- ))}
-
-
- );
-}
diff --git a/apps/docs/src/app/ui/components/[name]/page.tsx b/apps/docs/src/app/ui/components/[name]/page.tsx
deleted file mode 100644
index 4291697f..00000000
--- a/apps/docs/src/app/ui/components/[name]/page.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-import { Navigate, useParams } from "react-router";
-import { ComponentPageShell } from "@/components/docs/component-page-shell";
-import { getComponentDoc } from "@/lib/component-docs";
-import {
- getCategory,
- getComponent,
- getComponentsByCategory,
-} from "@/lib/component-registry";
-import { getComponentSpec } from "@/lib/component-source";
-
-// ── Import demo source files as raw strings at build time ──
-
-const demoSourceModules = import.meta.glob(
- [
- "/src/components/docs/primitives/*-demo.tsx",
- "/src/components/docs/ai-elements/*-demo.tsx",
- ],
- { query: "?raw", eager: true },
-) as Record;
-
-function getDemoSource(
- slug: string,
- source: "primitives" | "ai-elements",
-): string | null {
- const key = `/src/components/docs/${source}/${slug}-demo.tsx`;
- return demoSourceModules[key]?.default ?? null;
-}
-
-export default function ComponentPage() {
- const { name } = useParams<{ name: string }>();
-
- if (!name) return ;
-
- const component = getComponent(name);
- if (!component) return ;
-
- const category = getCategory(component.primaryCategory);
- const siblings = getComponentsByCategory(component.primaryCategory);
- const currentIndex = siblings.findIndex((c) => c.slug === name);
- const prev = currentIndex > 0 ? siblings[currentIndex - 1] : null;
- const next =
- currentIndex < siblings.length - 1 ? siblings[currentIndex + 1] : null;
-
- const demoSource = getDemoSource(component.slug, component.demoSource);
- const spec = getComponentSpec(component.slug);
- const docs = getComponentDoc(name);
-
- return (
-
- );
-}
diff --git a/apps/docs/src/app/ui/components/page.tsx b/apps/docs/src/app/ui/components/page.tsx
deleted file mode 100644
index 83c42b64..00000000
--- a/apps/docs/src/app/ui/components/page.tsx
+++ /dev/null
@@ -1,170 +0,0 @@
-"use client";
-
-import { useStaggerReveal } from "ghost-ui";
-import { useMemo, useState } from "react";
-import { Link } from "react-router";
-import { AnimatedPageHeader } from "@/components/docs/animated-page-header";
-import { SectionWrapper } from "@/components/docs/wrappers";
-import {
- categories,
- getAllComponents,
- getComponentsByCategory,
-} from "@/lib/component-registry";
-
-/* ── Fuzzy match ─────────────────────────────────────────────────────── */
-
-function fuzzyMatch(query: string, target: string): number {
- const q = query.toLowerCase();
- const t = target.toLowerCase();
-
- // exact substring match scores highest
- if (t.includes(q)) return 1;
-
- // character-by-character fuzzy: every query char must appear in order
- let qi = 0;
- let score = 0;
- let lastIdx = -1;
-
- for (let ti = 0; ti < t.length && qi < q.length; ti++) {
- if (t[ti] === q[qi]) {
- // bonus for consecutive matches
- score += ti === lastIdx + 1 ? 2 : 1;
- lastIdx = ti;
- qi++;
- }
- }
-
- // all query characters must be found
- if (qi < q.length) return 0;
-
- // normalise to 0–1 range (below 1 so substring match always wins)
- return (score / (q.length * 2)) * 0.9;
-}
-
-/* ── Page ─────────────────────────────────────────────────────────────── */
-
-export default function ComponentsIndex() {
- const [query, setQuery] = useState("");
- const allComponents = useMemo(() => getAllComponents(), []);
-
- const filtered = useMemo(() => {
- if (!query.trim()) return null;
- return allComponents
- .map((c) => ({ ...c, score: fuzzyMatch(query, c.name) }))
- .filter((c) => c.score > 0)
- .sort((a, b) => b.score - a.score);
- }, [query, allComponents]);
-
- const isSearching = query.trim().length > 0;
-
- return (
-
-
-
- {/* Search */}
-
-
-
-
-
- You are a research assistant that helps users find and summarize
- academic papers. Use the provided tools to search databases and
- retrieve relevant publications. Always cite your sources.
-
-
-
-
-
-
-
-
-
-
-
-
-
- Review code for best practices, potential bugs, and performance
- issues. Provide actionable feedback with specific line references.
-
-
-
-
- Renders an AI-generated image from base64 data. The component
- automatically constructs a data URI from the provided media type and
- base64 string.
-
-
-
-
-
-
- PNG, landscape aspect ratio
-
-
-
-
-
-
- PNG, square aspect ratio
-
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/ai-elements/index.tsx b/apps/docs/src/components/docs/ai-elements/index.tsx
deleted file mode 100644
index 46e71e02..00000000
--- a/apps/docs/src/components/docs/ai-elements/index.tsx
+++ /dev/null
@@ -1,238 +0,0 @@
-"use client";
-
-// Code
-import { AgentDemo } from "@/components/docs/ai-elements/agent-demo";
-import { ArtifactDemo } from "@/components/docs/ai-elements/artifact-demo";
-// Chatbot
-import { AttachmentsDemo } from "@/components/docs/ai-elements/attachments-demo";
-// Voice
-import { AudioPlayerDemo } from "@/components/docs/ai-elements/audio-player-demo";
-// Workflow
-import { CanvasDemo } from "@/components/docs/ai-elements/canvas-demo";
-import { ChainOfThoughtDemo } from "@/components/docs/ai-elements/chain-of-thought-demo";
-import { CheckpointDemo } from "@/components/docs/ai-elements/checkpoint-demo";
-import { CodeBlockDemo } from "@/components/docs/ai-elements/code-block-demo";
-import { CommitDemo } from "@/components/docs/ai-elements/commit-demo";
-import { ConfirmationDemo } from "@/components/docs/ai-elements/confirmation-demo";
-import { ConnectionDemo } from "@/components/docs/ai-elements/connection-demo";
-import { ContextDemo } from "@/components/docs/ai-elements/context-demo";
-import { ControlsDemo } from "@/components/docs/ai-elements/controls-demo";
-import { ConversationDemo } from "@/components/docs/ai-elements/conversation-demo";
-import { EdgeDemo } from "@/components/docs/ai-elements/edge-demo";
-import { EnvironmentVariablesDemo } from "@/components/docs/ai-elements/environment-variables-demo";
-import { FileTreeDemo } from "@/components/docs/ai-elements/file-tree-demo";
-// Utilities
-import { ImageDemo } from "@/components/docs/ai-elements/image-demo";
-import { InlineCitationDemo } from "@/components/docs/ai-elements/inline-citation-demo";
-import { JsxPreviewDemo } from "@/components/docs/ai-elements/jsx-preview-demo";
-import { MessageDemo } from "@/components/docs/ai-elements/message-demo";
-import { MicSelectorDemo } from "@/components/docs/ai-elements/mic-selector-demo";
-import { ModelSelectorDemo } from "@/components/docs/ai-elements/model-selector-demo";
-import { NodeDemo } from "@/components/docs/ai-elements/node-demo";
-import { OpenInChatDemo } from "@/components/docs/ai-elements/open-in-chat-demo";
-import { PackageInfoDemo } from "@/components/docs/ai-elements/package-info-demo";
-import { PanelDemo } from "@/components/docs/ai-elements/panel-demo";
-import { PersonaDemo } from "@/components/docs/ai-elements/persona-demo";
-import { PlanDemo } from "@/components/docs/ai-elements/plan-demo";
-import { PromptInputDemo } from "@/components/docs/ai-elements/prompt-input-demo";
-import { QueueDemo } from "@/components/docs/ai-elements/queue-demo";
-import { ReasoningDemo } from "@/components/docs/ai-elements/reasoning-demo";
-import { SandboxDemo } from "@/components/docs/ai-elements/sandbox-demo";
-import { SchemaDisplayDemo } from "@/components/docs/ai-elements/schema-display-demo";
-import { ShimmerDemo } from "@/components/docs/ai-elements/shimmer-demo";
-import { SnippetDemo } from "@/components/docs/ai-elements/snippet-demo";
-import { SourcesDemo } from "@/components/docs/ai-elements/sources-demo";
-import { SpeechInputDemo } from "@/components/docs/ai-elements/speech-input-demo";
-import { StackTraceDemo } from "@/components/docs/ai-elements/stack-trace-demo";
-import { SuggestionDemo } from "@/components/docs/ai-elements/suggestion-demo";
-import { TaskDemo } from "@/components/docs/ai-elements/task-demo";
-import { TerminalDemo } from "@/components/docs/ai-elements/terminal-demo";
-import { TestResultsDemo } from "@/components/docs/ai-elements/test-results-demo";
-import { ToolDemo } from "@/components/docs/ai-elements/tool-demo";
-import { ToolbarDemo } from "@/components/docs/ai-elements/toolbar-demo";
-import { TranscriptionDemo } from "@/components/docs/ai-elements/transcription-demo";
-import { VoiceSelectorDemo } from "@/components/docs/ai-elements/voice-selector-demo";
-import { WebPreviewDemo } from "@/components/docs/ai-elements/web-preview-demo";
-import { ComponentWrapper } from "@/components/docs/primitives/component-wrapper";
-
-function CategoryLabel({ children }: { children: React.ReactNode }) {
- return (
-
- Large language models are neural networks trained on vast amounts of text
- data.{" "}
-
-
- They use transformer architectures to generate coherent text
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {" "}
- and have become a cornerstone of modern AI applications.
-
-
-
- Can you explain how React Server Components work?
-
-
-
-
-
-
- {`**React Server Components** (RSC) allow you to render components on the server, reducing the amount of JavaScript sent to the client.\n\n### Key Benefits\n\n- **Zero bundle size** — Server Components are not included in the client bundle\n- **Direct backend access** — You can query databases directly\n- **Automatic code splitting** — Client components are lazy-loaded\n\n\`\`\`tsx\n// This runs on the server\nasync function UserProfile({ id }: { id: string }) {\n const user = await db.user.findUnique({ where: { id } });\n return
- A dropdown menu that lets users open a query in various AI chat
- providers. Each item generates a provider-specific URL and opens it in a
- new tab.
-
-
-
-
-
- Open in a chat provider
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {`The user is asking about the performance implications of using React Server Components. Let me think through the key factors:\n\n1. **Bundle size reduction** - Since server components don't ship JavaScript to the client, the initial bundle can be significantly smaller.\n\n2. **Data fetching** - Server components can fetch data directly during rendering, eliminating client-side waterfalls.\n\n3. **Streaming** - The server can stream HTML progressively, improving Time to First Byte.`}
-
-
-
-
-
-
Streaming reasoning
-
-
-
- {`Analyzing the query about database optimization strategies. I should consider indexing, query planning, and caching...`}
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/examples/conversation/with-messages.tsx b/apps/docs/src/components/docs/examples/conversation/with-messages.tsx
deleted file mode 100644
index a7f1f2c9..00000000
--- a/apps/docs/src/components/docs/examples/conversation/with-messages.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import {
- Conversation,
- ConversationContent,
- ConversationScrollButton,
- Message,
- MessageContent,
- MessageResponse,
-} from "ghost-ui";
-
-const messages = [
- { id: "1", role: "user" as const, text: "What is TypeScript?" },
- {
- id: "2",
- role: "assistant" as const,
- text: "TypeScript is a strongly-typed superset of JavaScript developed by Microsoft. It adds optional static type checking, interfaces, enums, and other features that help catch errors at compile time rather than at runtime.",
- },
- { id: "3", role: "user" as const, text: "How does it compare to Flow?" },
- {
- id: "4",
- role: "assistant" as const,
- text: "Both TypeScript and Flow add static types to JavaScript, but they differ in key ways:\n\n- **Adoption**: TypeScript has much wider community adoption and tooling support.\n- **Type system**: TypeScript uses a structural type system; Flow also uses structural typing but with some nominal typing features.\n- **Tooling**: TypeScript ships its own compiler (`tsc`), while Flow relies on Babel for compilation.\n- **Ecosystem**: TypeScript has DefinitelyTyped with type definitions for thousands of packages.",
- },
-];
-
-export default function ConversationWithMessages() {
- return (
-
-
-
-
-
-
-
- {`Quantum entanglement is when two particles become linked so that measuring one instantly affects the other, no matter how far apart they are. Einstein called it "spooky action at a distance."`}
-
-
- Generating...
-
-
-
-
-
-
- Is it accessible?
-
- Yes. It adheres to the WAI-ARIA design pattern.
-
-
-
- Is it styled?
-
- Yes. It comes with default styles that matches the other
- components' aesthetic.
-
-
-
- Is it animated?
-
- Yes. It's animated by default, but you can disable it if you
- prefer.
-
-
-
-
-
-
- What are the key considerations when implementing a comprehensive
- enterprise-level authentication system?
-
-
- Implementing a robust enterprise authentication system requires
- careful consideration of multiple factors. This includes secure
- password hashing and storage, multi-factor authentication (MFA)
- implementation, session management, OAuth2 and SSO integration,
- regular security audits, rate limiting to prevent brute force
- attacks, and maintaining detailed audit logs. Additionally,
- you'll need to consider scalability, performance impact, and
- compliance with relevant data protection regulations such as GDPR or
- HIPAA.
-
-
-
-
- How does modern distributed system architecture handle eventual
- consistency and data synchronization across multiple regions?
-
-
- Modern distributed systems employ various strategies to maintain
- data consistency across regions. This often involves using
- techniques like CRDT (Conflict-Free Replicated Data Types), vector
- clocks, and gossip protocols. Systems might implement event sourcing
- patterns, utilize message queues for asynchronous updates, and
- employ sophisticated conflict resolution strategies. Popular
- solutions like Amazon's DynamoDB and Google's Spanner
- demonstrate different approaches to solving these challenges,
- balancing between consistency, availability, and partition tolerance
- as described in the CAP theorem.
-
-
-
-
-
-
- Success! Your changes have been saved
-
- This is an alert with icon, title and description.
-
-
-
- Heads up!
-
- This one has an icon and a description only. No title.
-
-
-
-
- This one has a description only. No title. No icon.
-
-
-
-
- Let's try one with icon and title.
-
-
-
-
- This is a very long alert title that demonstrates how the component
- handles extended text content and potentially wraps across multiple
- lines
-
-
-
-
-
- This is a very long alert description that demonstrates how the
- component handles extended text content and potentially wraps across
- multiple lines
-
-
-
-
-
- This is an extremely long alert title that spans multiple lines to
- demonstrate how the component handles very lengthy headings while
- maintaining readability and proper text wrapping behavior
-
-
- This is an equally long description that contains detailed information
- about the alert. It shows how the component can accommodate extensive
- content while preserving proper spacing, alignment, and readability
- across different screen sizes and viewport widths. This helps ensure
- the user experience remains consistent regardless of the content
- length.
-
-
-
-
- Something went wrong!
-
- Your session has expired. Please log in again.
-
-
-
-
- Unable to process your payment.
-
-
Please verify your billing information and try again.
-
-
Check your card details
-
Ensure sufficient funds
-
Verify billing address
-
-
-
-
-
-
- The selected emails have been marked as spam.
-
-
-
-
-
- Plot Twist: This Alert is Actually Amber!
-
- This one has custom colors for light and dark mode.
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/alert-dialog-demo.tsx b/apps/docs/src/components/docs/primitives/alert-dialog-demo.tsx
deleted file mode 100644
index a37afac5..00000000
--- a/apps/docs/src/components/docs/primitives/alert-dialog-demo.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import {
- AlertDialog,
- AlertDialogAction,
- AlertDialogCancel,
- AlertDialogContent,
- AlertDialogDescription,
- AlertDialogFooter,
- AlertDialogHeader,
- AlertDialogTitle,
- AlertDialogTrigger,
- Button,
-} from "ghost-ui";
-
-export function AlertDialogDemo() {
- return (
-
-
-
-
-
-
- Are you absolutely sure?
-
- This action cannot be undone. This will permanently delete your
- account and remove your data from our servers.
-
-
-
- Cancel
- Continue
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/aspect-ratio-demo.tsx b/apps/docs/src/components/docs/primitives/aspect-ratio-demo.tsx
deleted file mode 100644
index 3d9b838d..00000000
--- a/apps/docs/src/components/docs/primitives/aspect-ratio-demo.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import { AspectRatio } from "ghost-ui";
-
-export function AspectRatioDemo() {
- return (
-
-
-
-
-
- Meeting Notes
-
- Transcript from the meeting with the client.
-
-
-
-
- Client requested dashboard redesign with focus on mobile
- responsiveness.
-
-
-
New analytics widgets for daily/weekly metrics
-
Simplified navigation menu
-
Dark mode support
-
Timeline: 6 weeks
-
Follow-up meeting scheduled for next Tuesday
-
-
-
-
-
-
- NK
-
-
-
- BA
-
-
-
- SM
-
-
-
-
-
-
- Is this an image?
- This is a card with an image.
-
-
-
-
-
-
- 4
-
-
- 2
-
-
- 350m²
-
-
$135,000
-
-
-
-
- Content Only
-
-
-
- Header Only
-
- This is a card with a header and a description.
-
-
-
-
-
- Header and Content
-
- This is a card with a header and a content.
-
-
- Content
-
-
- Footer Only
-
-
-
- Header + Footer
-
- This is a card with a header and a footer.
-
-
- Footer
-
-
- Content
- Footer
-
-
-
- Header + Footer
-
- This is a card with a header and a footer.
-
-
- Content
- Footer
-
-
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do
- eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
- enim ad minim veniam, quis nostrud exercitation ullamco laboris
- nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
- reprehenderit in voluptate velit esse cillum dolore eu fugiat
- nulla pariatur. Excepteur sint occaecat cupidatat non proident,
- sunt in culpa qui officia deserunt mollit anim id est laborum.
-
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed
- do eiusmod tempor incididunt ut labore et dolore magna aliqua.
- Ut enim ad minim veniam, quis nostrud exercitation ullamco
- laboris nisi ut aliquip ex ea commodo consequat. Duis aute
- irure dolor in reprehenderit in voluptate velit esse cillum
- dolore eu fugiat nulla pariatur. Excepteur sint occaecat
- cupidatat non proident, sunt in culpa qui officia deserunt
- mollit anim id est laborum.
-
-
- Re-usable components built using Radix UI and Tailwind CSS.
-
-
- How to install dependencies and structure your app.
-
-
- Styles for headings, paragraphs, lists...etc
-
-
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed
- do eiusmod tempor incididunt ut labore et dolore magna
- aliqua. Ut enim ad minim veniam, quis nostrud exercitation
- ullamco laboris nisi ut aliquip ex ea commodo consequat.
- Duis aute irure dolor in reprehenderit in voluptate velit
- esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
- occaecat cupidatat non proident, sunt in culpa qui officia
- deserunt mollit anim id est laborum.
-