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:

    -
  1. Brief from the fingerprint facets and exemplars
  2. -
  3. Generate or edit with the host agent
  4. -
  5. Run active deterministic checks and advisory review
  6. - Fix code, explain intentional divergence, or update the Ghost - package through Git + Gather the composed context for the surface you're touching +
  7. +
  8. Generate or edit with your agent
  9. +
  10. Route checks and emit an advisory review against the diff
  11. +
  12. + 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: -

-
    -
  • incorrect generation: agent failure
  • -
  • missing-fingerprint: under-specified surface context
  • -
  • intentional product evolution
  • -
-

- 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() {
      - {/* Concentric circles — fixed backdrop, persists through page scroll */} + {/* Concentric circles: fixed backdrop, persists through page scroll */}
      {[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 */} -
      - setQuery(e.target.value)} - placeholder="Search components…" - className="w-full max-w-md rounded-full border border-border-card bg-card px-5 py-2.5 text-sm text-foreground placeholder:text-muted-foreground/50 outline-none focus:border-foreground/25 transition-colors duration-200" - /> -
      - - {/* Search results */} - {isSearching && ( -
      - {filtered && filtered.length > 0 ? ( -
      - {filtered.map((item) => ( - - ))} -
      - ) : ( -

      - No components match "{query}" -

      - )} -
      - )} - - {/* Category sections */} - {!isSearching && ( -
      - {categories.map((cat) => { - const items = getComponentsByCategory(cat.slug); - if (items.length === 0) return null; - return ( - - ); - })} -
      - )} -
      - ); -} - -/* ── Pill ─────────────────────────────────────────────────────────────── */ - -function ComponentPill({ slug, name }: { slug: string; name: string }) { - return ( - - - - {name} - - - ); -} - -/* ── Category section ─────────────────────────────────────────────────── */ - -function CategorySection({ - name, - description, - items, -}: { - name: string; - description: string; - items: { slug: string; name: string }[]; -}) { - const ref = useStaggerReveal(".component-card", { - stagger: 0.04, - y: 24, - duration: 0.6, - }); - - return ( -
      -

      - {name} -

      -

      {description}

      -
      - {items.map((item) => ( - - ))} -
      -
      - ); -} diff --git a/apps/docs/src/app/ui/foundations/colors/page.tsx b/apps/docs/src/app/ui/foundations/colors/page.tsx deleted file mode 100644 index ddaa3355..00000000 --- a/apps/docs/src/app/ui/foundations/colors/page.tsx +++ /dev/null @@ -1,32 +0,0 @@ -"use client"; - -import { useScrollReveal } from "ghost-ui"; -import { AnimatedPageHeader } from "@/components/docs/animated-page-header"; -import { ColorsDemos } from "@/components/docs/foundations/colors"; -import { SectionWrapper } from "@/components/docs/wrappers"; - -export default function ColorsPage() { - const contentRef = useScrollReveal({ - y: 50, - duration: 0.9, - ease: "expo.out", - }); - - return ( - <> - - - - - -
      - -
      -
      - - ); -} diff --git a/apps/docs/src/app/ui/foundations/page.tsx b/apps/docs/src/app/ui/foundations/page.tsx deleted file mode 100644 index 02711675..00000000 --- a/apps/docs/src/app/ui/foundations/page.tsx +++ /dev/null @@ -1,103 +0,0 @@ -"use client"; - -import { useStaggerReveal } from "ghost-ui"; -import { type ReactNode } from "react"; -import { Link } from "react-router"; -import { AnimatedPageHeader } from "@/components/docs/animated-page-header"; -import { SectionWrapper } from "@/components/docs/wrappers"; - -function ColorsVisual() { - return ( -
      - {[ - "bg-foreground", - "bg-foreground/80", - "bg-foreground/60", - "bg-foreground/40", - "bg-foreground/20", - "bg-foreground/10", - ].map((bg, i) => ( -
      - ))} -
      - ); -} - -function TypographyVisual() { - return ( -
      -
      -
      -
      -
      -
      - ); -} - -const foundations: { - name: string; - href: string; - description: string; - visual: ReactNode; -}[] = [ - { - name: "Colors", - href: "/ui/foundations/colors", - description: - "A pure monochromatic scale with selective semantic color for status and utility.", - visual: , - }, - { - name: "Typography", - href: "/ui/foundations/typography", - description: - "Magazine-grade hierarchy. Display for headers, Regular for body, Mono for data.", - visual: , - }, -]; - -export default function FoundationsIndex() { - const ref = useStaggerReveal(".foundation-card", { - stagger: 0.06, - y: 30, - duration: 0.7, - }); - - return ( - - - -
      - {foundations.map((item) => ( - -
      {item.visual}
      - - - {item.name} - - - -

      - {item.description} -

      - - ))} -
      -
      - ); -} diff --git a/apps/docs/src/app/ui/foundations/typography/page.tsx b/apps/docs/src/app/ui/foundations/typography/page.tsx deleted file mode 100644 index d6a8f53c..00000000 --- a/apps/docs/src/app/ui/foundations/typography/page.tsx +++ /dev/null @@ -1,32 +0,0 @@ -"use client"; - -import { useScrollReveal } from "ghost-ui"; -import { AnimatedPageHeader } from "@/components/docs/animated-page-header"; -import { TypographyDemos } from "@/components/docs/foundations/typography"; -import { SectionWrapper } from "@/components/docs/wrappers"; - -export default function TypographyPage() { - const contentRef = useScrollReveal({ - y: 50, - duration: 0.9, - ease: "expo.out", - }); - - return ( - <> - - - - - -
      - -
      -
      - - ); -} diff --git a/apps/docs/src/app/ui/page.tsx b/apps/docs/src/app/ui/page.tsx deleted file mode 100644 index 3010c412..00000000 --- a/apps/docs/src/app/ui/page.tsx +++ /dev/null @@ -1,131 +0,0 @@ -"use client"; - -import { useStaggerReveal } from "ghost-ui"; -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"; - -function ColorsVisual() { - return ( -
      - {[ - "bg-foreground", - "bg-foreground/80", - "bg-foreground/60", - "bg-foreground/40", - "bg-foreground/20", - "bg-foreground/10", - ].map((bg, i) => ( -
      - ))} -
      - ); -} - -function TypographyVisual() { - return ( -
      -
      -
      -
      -
      -
      - ); -} - -function ComponentsVisual() { - const count = getAllComponents().length; - return ( -
      - {Array.from({ length: 8 }).map((_, i) => ( -
      - ))} - - {count} components - -
      - ); -} - -const sections: { - name: string; - href: string; - description: string; - visual: ReactNode; -}[] = [ - { - name: "Foundations", - href: "/ui/foundations", - description: - "Color, typography, and the design tokens that underpin every Ghost UI component.", - visual: ( -
      -
      - -
      -
      - -
      -
      - ), - }, - { - name: "Components", - href: "/ui/components", - description: - "Production-ready building blocks. Every component follows Ghost UI: pill-first, monochromatic, accessible.", - visual: , - }, -]; - -export default function DesignLanguageIndex() { - const ref = useStaggerReveal(".dl-card", { - stagger: 0.06, - y: 30, - duration: 0.7, - }); - - return ( - - - -
      - {sections.map((item) => ( - -
      {item.visual}
      - - - {item.name} - - - -

      - {item.description} -

      - - ))} -
      -
      - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/agent-demo.tsx b/apps/docs/src/components/docs/ai-elements/agent-demo.tsx deleted file mode 100644 index 0b340865..00000000 --- a/apps/docs/src/components/docs/ai-elements/agent-demo.tsx +++ /dev/null @@ -1,89 +0,0 @@ -"use client"; - -import { - Agent, - AgentContent, - AgentHeader, - AgentInstructions, - AgentOutput, - AgentTool, - AgentTools, -} from "ghost-ui"; - -export function AgentDemo() { - return ( -
      - - - - - 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. - - - -
      - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/artifact-demo.tsx b/apps/docs/src/components/docs/ai-elements/artifact-demo.tsx deleted file mode 100644 index ab5105a5..00000000 --- a/apps/docs/src/components/docs/ai-elements/artifact-demo.tsx +++ /dev/null @@ -1,67 +0,0 @@ -"use client"; - -import { - Artifact, - ArtifactAction, - ArtifactActions, - ArtifactClose, - ArtifactContent, - ArtifactDescription, - ArtifactHeader, - ArtifactTitle, -} from "ghost-ui"; -import { CopyIcon, DownloadIcon, ShareIcon } from "lucide-react"; - -export function ArtifactDemo() { - return ( -
      - - -
      - React Component - - A reusable button component with variants - -
      - - - - - - -
      - -
      -            {`export function Button({ variant = "primary", children }) {
      -  return (
      -    
      -  );
      -}`}
      -          
      -
      -
      - - - -
      - SVG Illustration - - Generated logo design concept - -
      - - - - -
      - -
      - AI -
      -
      -
      -
      - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/attachments-demo.tsx b/apps/docs/src/components/docs/ai-elements/attachments-demo.tsx deleted file mode 100644 index a5caa72c..00000000 --- a/apps/docs/src/components/docs/ai-elements/attachments-demo.tsx +++ /dev/null @@ -1,83 +0,0 @@ -"use client"; - -import { - Attachment, - AttachmentEmpty, - AttachmentInfo, - AttachmentPreview, - AttachmentRemove, - Attachments, -} from "ghost-ui"; - -const mockAttachments = [ - { - id: "1", - type: "file" as const, - mediaType: "image/png", - filename: "screenshot.png", - url: "https://picsum.photos/seed/attach1/200/200", - }, - { - id: "2", - type: "file" as const, - mediaType: "application/pdf", - filename: "quarterly-report.pdf", - url: "", - }, - { - id: "3", - type: "file" as const, - mediaType: "audio/mp3", - filename: "recording.mp3", - url: "", - }, -]; - -export function AttachmentsDemo() { - return ( -
      -
      -

      Grid variant

      - - {mockAttachments.map((file) => ( - {}}> - - - - ))} - -
      - -
      -

      Inline variant

      - - {mockAttachments.map((file) => ( - {}}> - - - - - ))} - -
      - -
      -

      List variant

      - - {mockAttachments.map((file) => ( - {}}> - - - - - ))} - -
      - -
      -

      Empty state

      - -
      -
      - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/audio-player-demo.tsx b/apps/docs/src/components/docs/ai-elements/audio-player-demo.tsx deleted file mode 100644 index eb65a58e..00000000 --- a/apps/docs/src/components/docs/ai-elements/audio-player-demo.tsx +++ /dev/null @@ -1,53 +0,0 @@ -"use client"; - -import { - AudioPlayer, - AudioPlayerControlBar, - AudioPlayerDurationDisplay, - AudioPlayerElement, - AudioPlayerMuteButton, - AudioPlayerPlayButton, - AudioPlayerSeekBackwardButton, - AudioPlayerSeekForwardButton, - AudioPlayerTimeDisplay, - AudioPlayerTimeRange, - AudioPlayerVolumeRange, -} from "ghost-ui"; - -export function AudioPlayerDemo() { - return ( -
      -
      -

      Full audio player

      - - - - - - - - - - - - - -
      - -
      -

      - Minimal player (play, time, scrub) -

      - - - - - - - - - -
      -
      - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/canvas-demo.tsx b/apps/docs/src/components/docs/ai-elements/canvas-demo.tsx deleted file mode 100644 index 27fc23c1..00000000 --- a/apps/docs/src/components/docs/ai-elements/canvas-demo.tsx +++ /dev/null @@ -1,87 +0,0 @@ -"use client"; - -import { type NodeTypes, ReactFlowProvider } from "@xyflow/react"; -import { - Canvas, - Controls, - Node, - NodeContent, - NodeDescription, - NodeHeader, - NodeTitle, -} from "ghost-ui"; -import { useMemo } from "react"; - -const InputNode = () => ( - - - User Input - Text prompt - - -

      - Accepts a natural language query from the user. -

      -
      -
      -); - -const ProcessNode = () => ( - - - LLM Processing - GPT-4o - - -

      - Processes the input and generates a response. -

      -
      -
      -); - -const OutputNode = () => ( - - - Response - Markdown output - - -

      - Displays the generated response to the user. -

      -
      -
      -); - -const initialNodes = [ - { id: "1", type: "input", position: { x: 0, y: 100 }, data: {} }, - { id: "2", type: "process", position: { x: 500, y: 100 }, data: {} }, - { id: "3", type: "output", position: { x: 1000, y: 100 }, data: {} }, -]; - -const initialEdges = [ - { id: "e1-2", source: "1", target: "2" }, - { id: "e2-3", source: "2", target: "3" }, -]; - -export function CanvasDemo() { - const nodeTypes: NodeTypes = useMemo( - () => ({ - input: InputNode, - output: OutputNode, - process: ProcessNode, - }), - [], - ); - - return ( - -
      - - - -
      -
      - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/chain-of-thought-demo.tsx b/apps/docs/src/components/docs/ai-elements/chain-of-thought-demo.tsx deleted file mode 100644 index 5238cc15..00000000 --- a/apps/docs/src/components/docs/ai-elements/chain-of-thought-demo.tsx +++ /dev/null @@ -1,52 +0,0 @@ -"use client"; - -import { - ChainOfThought, - ChainOfThoughtContent, - ChainOfThoughtHeader, - ChainOfThoughtSearchResult, - ChainOfThoughtSearchResults, - ChainOfThoughtStep, -} from "ghost-ui"; -import { DatabaseIcon, FileTextIcon, SearchIcon } from "lucide-react"; - -export function ChainOfThoughtDemo() { - return ( - - Researching climate data - - - - IPCC 2024 - - NASA Climate - - NOAA Data - - - - - - - - - - - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/checkpoint-demo.tsx b/apps/docs/src/components/docs/ai-elements/checkpoint-demo.tsx deleted file mode 100644 index 5073b3d3..00000000 --- a/apps/docs/src/components/docs/ai-elements/checkpoint-demo.tsx +++ /dev/null @@ -1,27 +0,0 @@ -"use client"; - -import { Checkpoint, CheckpointIcon, CheckpointTrigger } from "ghost-ui"; - -export function CheckpointDemo() { - return ( -
      - - - - Checkpoint 1 — Initial draft - - - -
      - Some conversation content between checkpoints... -
      - - - - - Checkpoint 2 — After revisions - - -
      - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/code-block-demo.tsx b/apps/docs/src/components/docs/ai-elements/code-block-demo.tsx deleted file mode 100644 index 9fa3d01f..00000000 --- a/apps/docs/src/components/docs/ai-elements/code-block-demo.tsx +++ /dev/null @@ -1,79 +0,0 @@ -"use client"; - -import { - CodeBlock, - CodeBlockActions, - CodeBlockCopyButton, - CodeBlockFilename, - CodeBlockHeader, - CodeBlockTitle, -} from "ghost-ui"; - -const typescriptCode = `interface User { - id: string; - name: string; - email: string; - role: "admin" | "user" | "guest"; -} - -async function getUser(id: string): Promise { - const response = await fetch(\`/api/users/\${id}\`); - - if (!response.ok) { - throw new Error(\`Failed to fetch user: \${response.statusText}\`); - } - - return response.json(); -}`; - -const cssCode = `.container { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); - gap: 1.5rem; - padding: 2rem; -} - -.card { - border-radius: 0.75rem; - background: var(--card-bg); - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); -}`; - -const jsonCode = `{ - "name": "@acme/design-system", - "version": "2.4.0", - "dependencies": { - "react": "^19.0.0", - "tailwindcss": "^4.0.0" - } -}`; - -export function CodeBlockDemo() { - return ( -
      - - - - lib/api/users.ts - - - - - - - - - - - styles/layout.css - - - - - - - - -
      - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/commit-demo.tsx b/apps/docs/src/components/docs/ai-elements/commit-demo.tsx deleted file mode 100644 index 73cb521c..00000000 --- a/apps/docs/src/components/docs/ai-elements/commit-demo.tsx +++ /dev/null @@ -1,122 +0,0 @@ -"use client"; - -import { - Commit, - CommitActions, - CommitAuthor, - CommitAuthorAvatar, - CommitContent, - CommitCopyButton, - CommitFile, - CommitFileAdditions, - CommitFileChanges, - CommitFileDeletions, - CommitFileIcon, - CommitFileInfo, - CommitFilePath, - CommitFileStatus, - CommitFiles, - CommitHash, - CommitHeader, - CommitInfo, - CommitMessage, - CommitMetadata, - CommitSeparator, - CommitTimestamp, -} from "ghost-ui"; - -export function CommitDemo() { - return ( -
      - - - - - - - - feat: add user authentication with OAuth2 support - - - a1b2c3d - - - - - - - - - - - - - - - src/lib/auth/oauth.ts - - - - - - - - - - - src/middleware.ts - - - - - - - - - - - src/lib/auth/legacy.ts - - - - - - - - - - - src/config/auth.config.ts - - - - - - - - - - - - - - - - - - fix: resolve race condition in data fetching - - - f8e9d0c - - - - - - - - - -
      - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/confirmation-demo.tsx b/apps/docs/src/components/docs/ai-elements/confirmation-demo.tsx deleted file mode 100644 index 772fc14f..00000000 --- a/apps/docs/src/components/docs/ai-elements/confirmation-demo.tsx +++ /dev/null @@ -1,70 +0,0 @@ -"use client"; - -import { - Confirmation, - ConfirmationAccepted, - ConfirmationAction, - ConfirmationActions, - ConfirmationRejected, - ConfirmationRequest, - ConfirmationTitle, -} from "ghost-ui"; - -export function ConfirmationDemo() { - return ( -
      -
      -

      Approval requested

      - - - The assistant wants to execute rm -rf ./build - - -

      - This action will delete the build directory. Do you want to - proceed? -

      -
      - - Deny - Approve - -
      -
      - -
      -

      Accepted

      - - - Executed rm -rf ./build - - -

      - Action was approved and completed successfully. -

      -
      -
      -
      - -
      -

      Rejected

      - - - Blocked rm -rf ./build - - -

      - Action was denied by the user. -

      -
      -
      -
      -
      - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/connection-demo.tsx b/apps/docs/src/components/docs/ai-elements/connection-demo.tsx deleted file mode 100644 index 629c0c58..00000000 --- a/apps/docs/src/components/docs/ai-elements/connection-demo.tsx +++ /dev/null @@ -1,70 +0,0 @@ -"use client"; - -import { type NodeTypes, ReactFlowProvider } from "@xyflow/react"; -import { - Canvas, - Connection, - Node, - NodeContent, - NodeHeader, - NodeTitle, -} from "ghost-ui"; -import { useMemo } from "react"; - -const SourceNode = () => ( - - - Source - - -

      - Drag from the handle to see the custom connection line. -

      -
      -
      -); - -const TargetNode = () => ( - - - Target - - -

      Drop a connection here.

      -
      -
      -); - -const initialNodes = [ - { id: "1", type: "source", position: { x: 0, y: 80 }, data: {} }, - { id: "2", type: "target", position: { x: 500, y: 80 }, data: {} }, -]; - -export function ConnectionDemo() { - const nodeTypes: NodeTypes = useMemo( - () => ({ - source: SourceNode, - target: TargetNode, - }), - [], - ); - - return ( -
      -

      - Drag from the source handle to see the animated bezier connection line - with a circular endpoint indicator. -

      - -
      - -
      -
      -
      - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/context-demo.tsx b/apps/docs/src/components/docs/ai-elements/context-demo.tsx deleted file mode 100644 index 6b11729b..00000000 --- a/apps/docs/src/components/docs/ai-elements/context-demo.tsx +++ /dev/null @@ -1,44 +0,0 @@ -"use client"; - -import { - Context, - ContextContent, - ContextContentBody, - ContextContentFooter, - ContextContentHeader, - ContextInputUsage, - ContextOutputUsage, - ContextTrigger, -} from "ghost-ui"; - -export function ContextDemo() { - return ( - - - - - -
      - - -
      -
      - -
      -
      - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/controls-demo.tsx b/apps/docs/src/components/docs/ai-elements/controls-demo.tsx deleted file mode 100644 index 2d845c1f..00000000 --- a/apps/docs/src/components/docs/ai-elements/controls-demo.tsx +++ /dev/null @@ -1,50 +0,0 @@ -"use client"; - -import { type NodeTypes, ReactFlowProvider } from "@xyflow/react"; -import { - Canvas, - Controls, - Node, - NodeContent, - NodeHeader, - NodeTitle, -} from "ghost-ui"; -import { useMemo } from "react"; - -const SampleNode = () => ( - - - Sample Node - - -

      - Use the controls in the bottom-left to zoom, fit view, and lock - interactions. -

      -
      -
      -); - -const initialNodes = [ - { id: "1", type: "sample", position: { x: 0, y: 0 }, data: {} }, - { id: "2", type: "sample", position: { x: 400, y: 150 }, data: {} }, -]; - -export function ControlsDemo() { - const nodeTypes: NodeTypes = useMemo( - () => ({ - sample: SampleNode, - }), - [], - ); - - return ( - -
      - - - -
      -
      - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/conversation-demo.tsx b/apps/docs/src/components/docs/ai-elements/conversation-demo.tsx deleted file mode 100644 index 32a994ac..00000000 --- a/apps/docs/src/components/docs/ai-elements/conversation-demo.tsx +++ /dev/null @@ -1,24 +0,0 @@ -"use client"; - -import { - Conversation, - ConversationContent, - ConversationEmptyState, -} from "ghost-ui"; -import { MessageSquareIcon } from "lucide-react"; - -export function ConversationDemo() { - return ( -
      - - - } - /> - - -
      - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/edge-demo.tsx b/apps/docs/src/components/docs/ai-elements/edge-demo.tsx deleted file mode 100644 index ec292976..00000000 --- a/apps/docs/src/components/docs/ai-elements/edge-demo.tsx +++ /dev/null @@ -1,85 +0,0 @@ -"use client"; - -import { - type EdgeTypes, - type NodeTypes, - ReactFlowProvider, -} from "@xyflow/react"; -import { Canvas, Edge, Node, NodeHeader, NodeTitle } from "ghost-ui"; -import { useMemo } from "react"; - -const SimpleNode = ({ data }: { data: { label: string } }) => ( - - - {data.label} - - -); - -const initialNodes = [ - { - id: "a1", - type: "simple", - position: { x: 0, y: 0 }, - data: { label: "Start" }, - }, - { - id: "a2", - type: "simple", - position: { x: 450, y: 0 }, - data: { label: "Animated" }, - }, - { - id: "b1", - type: "simple", - position: { x: 0, y: 150 }, - data: { label: "Draft" }, - }, - { - id: "b2", - type: "simple", - position: { x: 450, y: 150 }, - data: { label: "Temporary" }, - }, -]; - -const initialEdges = [ - { id: "e-animated", source: "a1", target: "a2", type: "animated" }, - { id: "e-temporary", source: "b1", target: "b2", type: "temporary" }, -]; - -export function EdgeDemo() { - const nodeTypes: NodeTypes = useMemo( - () => ({ - simple: SimpleNode, - }), - [], - ); - - const edgeTypes: EdgeTypes = useMemo( - () => ({ - animated: Edge.Animated, - temporary: Edge.Temporary, - }), - [], - ); - - return ( -
      -

      - Two edge variants: Animated (top, with a traveling dot) - and Temporary (bottom, dashed line). -

      - -
      - -
      -
      -
      - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/environment-variables-demo.tsx b/apps/docs/src/components/docs/ai-elements/environment-variables-demo.tsx deleted file mode 100644 index 8c0d8ce4..00000000 --- a/apps/docs/src/components/docs/ai-elements/environment-variables-demo.tsx +++ /dev/null @@ -1,74 +0,0 @@ -"use client"; - -import { - EnvironmentVariable, - EnvironmentVariableCopyButton, - EnvironmentVariableGroup, - EnvironmentVariableName, - EnvironmentVariableRequired, - EnvironmentVariables, - EnvironmentVariablesContent, - EnvironmentVariablesHeader, - EnvironmentVariablesTitle, - EnvironmentVariablesToggle, - EnvironmentVariableValue, -} from "ghost-ui"; - -export function EnvironmentVariablesDemo() { - return ( -
      - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/file-tree-demo.tsx b/apps/docs/src/components/docs/ai-elements/file-tree-demo.tsx deleted file mode 100644 index c2ce9e49..00000000 --- a/apps/docs/src/components/docs/ai-elements/file-tree-demo.tsx +++ /dev/null @@ -1,35 +0,0 @@ -"use client"; - -import { FileTree, FileTreeFile, FileTreeFolder } from "ghost-ui"; - -export function FileTreeDemo() { - return ( -
      - - - - - - - - - - - - - - - - - - - - - - -
      - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/image-demo.tsx b/apps/docs/src/components/docs/ai-elements/image-demo.tsx deleted file mode 100644 index 6511c174..00000000 --- a/apps/docs/src/components/docs/ai-elements/image-demo.tsx +++ /dev/null @@ -1,47 +0,0 @@ -"use client"; - -import { Image } from "ghost-ui"; - -// A tiny 1x1 transparent PNG placeholder -const PLACEHOLDER_BASE64 = - "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="; - -export function ImageDemo() { - return ( -
      -

      - Renders an AI-generated image from base64 data. The component - automatically constructs a data URI from the provided media type and - base64 string. -

      - -
      -
      - AI-generated landscape placeholder - - PNG, landscape aspect ratio - -
      - -
      - AI-generated portrait placeholder - - 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 ( -
      -

      - {children} -

      -
      - ); -} - -export function AIElementDemos() { - return ( -
      - {/* Chatbot */} - Chatbot - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {/* Code */} - Code - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {/* Voice */} - Voice - - - - - - - - - - - - - - - - - - - - {/* Workflow */} - Workflow - - - - - - - - - - - - - - - - - - - - - - - {/* Utilities */} - Utilities - - - - - - -
      - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/inline-citation-demo.tsx b/apps/docs/src/components/docs/ai-elements/inline-citation-demo.tsx deleted file mode 100644 index 32219032..00000000 --- a/apps/docs/src/components/docs/ai-elements/inline-citation-demo.tsx +++ /dev/null @@ -1,65 +0,0 @@ -"use client"; - -import { - InlineCitation, - InlineCitationCard, - InlineCitationCardBody, - InlineCitationCardTrigger, - InlineCitationCarousel, - InlineCitationCarouselContent, - InlineCitationCarouselHeader, - InlineCitationCarouselIndex, - InlineCitationCarouselItem, - InlineCitationCarouselNext, - InlineCitationCarouselPrev, - InlineCitationSource, - InlineCitationText, -} from "ghost-ui"; - -const sources = [ - "https://en.wikipedia.org/wiki/Large_language_model", - "https://arxiv.org/abs/2303.08774", -]; - -export function InlineCitationDemo() { - 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. -

      - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/jsx-preview-demo.tsx b/apps/docs/src/components/docs/ai-elements/jsx-preview-demo.tsx deleted file mode 100644 index ffa7d6f9..00000000 --- a/apps/docs/src/components/docs/ai-elements/jsx-preview-demo.tsx +++ /dev/null @@ -1,32 +0,0 @@ -"use client"; - -import { JSXPreview, JSXPreviewContent, JSXPreviewError } from "ghost-ui"; - -const validJsx = `
      -

      Welcome Back

      -

      Your dashboard is ready to explore.

      -
      - - -
      -
      `; - -const errorJsx = `
      - -
      `; - -export function JsxPreviewDemo() { - return ( -
      - - - - - - - - - -
      - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/message-demo.tsx b/apps/docs/src/components/docs/ai-elements/message-demo.tsx deleted file mode 100644 index 40d6673e..00000000 --- a/apps/docs/src/components/docs/ai-elements/message-demo.tsx +++ /dev/null @@ -1,49 +0,0 @@ -"use client"; - -import { - Message, - MessageAction, - MessageActions, - MessageContent, - MessageResponse, -} from "ghost-ui"; -import { - CopyIcon, - RefreshCwIcon, - ThumbsDownIcon, - ThumbsUpIcon, -} from "lucide-react"; - -export function MessageDemo() { - return ( -
      - - - 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
      {user.name}
      ;\n}\n\`\`\``} -
      -
      - - - - - - - - - - - - - - -
      -
      - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/mic-selector-demo.tsx b/apps/docs/src/components/docs/ai-elements/mic-selector-demo.tsx deleted file mode 100644 index 06786dd3..00000000 --- a/apps/docs/src/components/docs/ai-elements/mic-selector-demo.tsx +++ /dev/null @@ -1,48 +0,0 @@ -"use client"; - -import { - MicSelector, - MicSelectorContent, - MicSelectorEmpty, - MicSelectorInput, - MicSelectorItem, - MicSelectorLabel, - MicSelectorList, - MicSelectorTrigger, - MicSelectorValue, -} from "ghost-ui"; - -export function MicSelectorDemo() { - return ( -
      -

      - Opens a popover listing available audio input devices. Requires - microphone permission to show device names. -

      - - - - - - - - {(devices) => - devices.length === 0 ? ( - - ) : ( - devices.map((device) => ( - - - - )) - ) - } - - - -
      - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/model-selector-demo.tsx b/apps/docs/src/components/docs/ai-elements/model-selector-demo.tsx deleted file mode 100644 index df2e74b1..00000000 --- a/apps/docs/src/components/docs/ai-elements/model-selector-demo.tsx +++ /dev/null @@ -1,68 +0,0 @@ -"use client"; - -import { - Button, - ModelSelector, - ModelSelectorContent, - ModelSelectorEmpty, - ModelSelectorGroup, - ModelSelectorInput, - ModelSelectorItem, - ModelSelectorList, - ModelSelectorLogo, - ModelSelectorLogoGroup, - ModelSelectorName, - ModelSelectorTrigger, -} from "ghost-ui"; - -export function ModelSelectorDemo() { - return ( - - - - - - - - No models found. - - - - - - GPT-4o - - - - - - GPT-4o Mini - - - - - - - - Claude Sonnet 4 - - - - - - Claude Opus 4 - - - - - - - - Gemini 2.5 Pro - - - - - - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/node-demo.tsx b/apps/docs/src/components/docs/ai-elements/node-demo.tsx deleted file mode 100644 index 922f9323..00000000 --- a/apps/docs/src/components/docs/ai-elements/node-demo.tsx +++ /dev/null @@ -1,83 +0,0 @@ -"use client"; - -import { ReactFlowProvider } from "@xyflow/react"; -import { - Badge, - Button, - Node, - NodeContent, - NodeDescription, - NodeFooter, - NodeHeader, - NodeTitle, -} from "ghost-ui"; - -export function NodeDemo() { - return ( - -
      -
      - - - Full Node - - A node with header, content, and footer - - - -

      - This node demonstrates all available sub-components arranged - together. It has both target (left) and source (right) handles. -

      -
      - - Ready - - -
      - - - - Source Only - Starting node in a workflow - - -

      - This node only has a source handle on the right side. -

      -
      -
      - - - - Target Only - Terminal node in a workflow - - -

      - This node only has a target handle on the left side. -

      -
      -
      - - - - Standalone - No handles - - -

      - A standalone card-style node with no connection handles. -

      -
      - - Idle - -
      -
      -
      -
      - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/open-in-chat-demo.tsx b/apps/docs/src/components/docs/ai-elements/open-in-chat-demo.tsx deleted file mode 100644 index f5e1c694..00000000 --- a/apps/docs/src/components/docs/ai-elements/open-in-chat-demo.tsx +++ /dev/null @@ -1,42 +0,0 @@ -"use client"; - -import { - OpenIn, - OpenInChatGPT, - OpenInClaude, - OpenInContent, - OpenInCursor, - OpenInLabel, - OpenInScira, - OpenInSeparator, - OpenInT3, - OpenInTrigger, - OpenInv0, -} from "ghost-ui"; - -export function OpenInChatDemo() { - 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 - - - - - - - - - - -
      - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/package-info-demo.tsx b/apps/docs/src/components/docs/ai-elements/package-info-demo.tsx deleted file mode 100644 index d6d393dd..00000000 --- a/apps/docs/src/components/docs/ai-elements/package-info-demo.tsx +++ /dev/null @@ -1,57 +0,0 @@ -"use client"; - -import { - PackageInfo, - PackageInfoContent, - PackageInfoDependencies, - PackageInfoDependency, - PackageInfoDescription, -} from "ghost-ui"; - -export function PackageInfoDemo() { - return ( -
      - - - A JavaScript library for building user interfaces - - - - - - - - - - - - - - - - - - -
      - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/panel-demo.tsx b/apps/docs/src/components/docs/ai-elements/panel-demo.tsx deleted file mode 100644 index ec110916..00000000 --- a/apps/docs/src/components/docs/ai-elements/panel-demo.tsx +++ /dev/null @@ -1,61 +0,0 @@ -"use client"; - -import { type NodeTypes, ReactFlowProvider } from "@xyflow/react"; -import { - Canvas, - Node, - NodeContent, - NodeHeader, - NodeTitle, - Panel, -} from "ghost-ui"; -import { useMemo } from "react"; - -const PlaceholderNode = () => ( - - - Workflow Node - - -

      - Panels float above the canvas at fixed positions. -

      -
      -
      -); - -const initialNodes = [ - { id: "1", type: "placeholder", position: { x: 100, y: 80 }, data: {} }, -]; - -export function PanelDemo() { - const nodeTypes: NodeTypes = useMemo( - () => ({ - placeholder: PlaceholderNode, - }), - [], - ); - - return ( -
      -

      - Panels are floating overlays positioned at the edges of the canvas. -

      - -
      - - - Top Left Panel - - - Top Right Panel - - - Bottom Center Panel - - -
      -
      -
      - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/persona-demo.tsx b/apps/docs/src/components/docs/ai-elements/persona-demo.tsx deleted file mode 100644 index f5e48571..00000000 --- a/apps/docs/src/components/docs/ai-elements/persona-demo.tsx +++ /dev/null @@ -1,55 +0,0 @@ -"use client"; - -import type { PersonaState } from "ghost-ui"; -import { Button, Persona } from "ghost-ui"; -import { useState } from "react"; - -const variants = [ - "obsidian", - "glint", - "halo", - "command", - "mana", - "opal", -] as const; -const states: PersonaState[] = [ - "idle", - "listening", - "thinking", - "speaking", - "asleep", -]; - -export function PersonaDemo() { - const [currentState, setCurrentState] = useState("idle"); - - return ( -
      -
      - {states.map((s) => ( - - ))} -
      - -
      - {variants.map((variant) => ( -
      - - {variant} -
      - ))} -
      -
      - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/plan-demo.tsx b/apps/docs/src/components/docs/ai-elements/plan-demo.tsx deleted file mode 100644 index ce00bc8d..00000000 --- a/apps/docs/src/components/docs/ai-elements/plan-demo.tsx +++ /dev/null @@ -1,63 +0,0 @@ -"use client"; - -import { - Plan, - PlanAction, - PlanContent, - PlanDescription, - PlanFooter, - PlanHeader, - PlanTitle, - PlanTrigger, -} from "ghost-ui"; -import { CheckCircleIcon, CircleIcon } from "lucide-react"; - -export function PlanDemo() { - return ( -
      - - -
      - Build a Landing Page - - Create a responsive landing page with hero section, features, and - footer. - -
      - - - -
      - -
        -
      • - - - Set up project structure - -
      • -
      • - - - Design hero section - -
      • -
      • - - Build features grid -
      • -
      • - - Add footer and navigation -
      • -
      -
      - -

      - 2 of 4 steps completed -

      -
      -
      -
      - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/prompt-input-demo.tsx b/apps/docs/src/components/docs/ai-elements/prompt-input-demo.tsx deleted file mode 100644 index 47132fcf..00000000 --- a/apps/docs/src/components/docs/ai-elements/prompt-input-demo.tsx +++ /dev/null @@ -1,29 +0,0 @@ -"use client"; - -import { - PromptInput, - PromptInputButton, - PromptInputFooter, - PromptInputSubmit, - PromptInputTextarea, - PromptInputTools, -} from "ghost-ui"; -import { PaperclipIcon } from "lucide-react"; - -export function PromptInputDemo() { - return ( -
      - {}}> - - - - - - - - - - -
      - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/queue-demo.tsx b/apps/docs/src/components/docs/ai-elements/queue-demo.tsx deleted file mode 100644 index b6f9b370..00000000 --- a/apps/docs/src/components/docs/ai-elements/queue-demo.tsx +++ /dev/null @@ -1,86 +0,0 @@ -"use client"; - -import { - Queue, - QueueItem, - QueueItemContent, - QueueItemDescription, - QueueItemIndicator, - QueueList, - QueueSection, - QueueSectionContent, - QueueSectionLabel, - QueueSectionTrigger, -} from "ghost-ui"; -import { CheckCircleIcon, ListTodoIcon } from "lucide-react"; - -export function QueueDemo() { - return ( -
      - - - - } - /> - - - - -
      - - - Refactor authentication module - -
      - - Extract shared logic into a reusable hook - -
      - -
      - - - Write unit tests for API routes - -
      -
      -
      -
      -
      - - - - } - /> - - - - -
      - - - Set up project scaffolding - -
      -
      - -
      - - - Configure database schema - -
      -
      -
      -
      -
      -
      -
      - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/reasoning-demo.tsx b/apps/docs/src/components/docs/ai-elements/reasoning-demo.tsx deleted file mode 100644 index 0ca165bd..00000000 --- a/apps/docs/src/components/docs/ai-elements/reasoning-demo.tsx +++ /dev/null @@ -1,29 +0,0 @@ -"use client"; - -import { Reasoning, ReasoningContent, ReasoningTrigger } from "ghost-ui"; - -export function ReasoningDemo() { - return ( -
      -
      -

      Completed reasoning

      - - - - {`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/ai-elements/sandbox-demo.tsx b/apps/docs/src/components/docs/ai-elements/sandbox-demo.tsx deleted file mode 100644 index 55604449..00000000 --- a/apps/docs/src/components/docs/ai-elements/sandbox-demo.tsx +++ /dev/null @@ -1,84 +0,0 @@ -"use client"; - -import { - CodeBlock, - CodeBlockActions, - CodeBlockCopyButton, - CodeBlockFilename, - CodeBlockHeader, - CodeBlockTitle, - Sandbox, - SandboxContent, - SandboxHeader, - SandboxTabContent, - SandboxTabs, - SandboxTabsBar, - SandboxTabsList, - SandboxTabsTrigger, -} from "ghost-ui"; - -const codeSnippet = `import { useState } from "react"; - -export default function Counter() { - const [count, setCount] = useState(0); - - return ( -
      -

      {count}

      - -
      - ); -}`; - -const outputText = `> next dev --turbo - Ready in 1.2s - Local: http://localhost:3000 - Network: http://192.168.1.100:3000 - - Compiled /page in 340ms`; - -export function SandboxDemo() { - return ( -
      - - - - - - - Code - Output - - - - - - - counter.tsx - - - - - - - - -
      {outputText}
      -
      -
      -
      -
      - - - - -
      - Executing test suite... -
      -
      -
      -
      - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/schema-display-demo.tsx b/apps/docs/src/components/docs/ai-elements/schema-display-demo.tsx deleted file mode 100644 index 12c4c54b..00000000 --- a/apps/docs/src/components/docs/ai-elements/schema-display-demo.tsx +++ /dev/null @@ -1,171 +0,0 @@ -"use client"; - -import { SchemaDisplay } from "ghost-ui"; - -export function SchemaDisplayDemo() { - return ( -
      - - - - - - - -
      - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/shimmer-demo.tsx b/apps/docs/src/components/docs/ai-elements/shimmer-demo.tsx deleted file mode 100644 index 2eb428f5..00000000 --- a/apps/docs/src/components/docs/ai-elements/shimmer-demo.tsx +++ /dev/null @@ -1,21 +0,0 @@ -"use client"; - -import { Shimmer } from "ghost-ui"; - -export function ShimmerDemo() { - return ( -
      - - Generating response... - - - - Analyzing your code and preparing suggestions - - - - Thinking... - -
      - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/snippet-demo.tsx b/apps/docs/src/components/docs/ai-elements/snippet-demo.tsx deleted file mode 100644 index 56d45b7b..00000000 --- a/apps/docs/src/components/docs/ai-elements/snippet-demo.tsx +++ /dev/null @@ -1,52 +0,0 @@ -"use client"; - -import { - Snippet, - SnippetAddon, - SnippetCopyButton, - SnippetInput, - SnippetText, -} from "ghost-ui"; - -export function SnippetDemo() { - return ( -
      - - - $ - - - - - - - - - - $ - - - - - - - - - - - - - - - - - $ - - - - - - -
      - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/sources-demo.tsx b/apps/docs/src/components/docs/ai-elements/sources-demo.tsx deleted file mode 100644 index 598afb1c..00000000 --- a/apps/docs/src/components/docs/ai-elements/sources-demo.tsx +++ /dev/null @@ -1,25 +0,0 @@ -"use client"; - -import { Source, Sources, SourcesContent, SourcesTrigger } from "ghost-ui"; - -export function SourcesDemo() { - return ( - - - - - - - - - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/speech-input-demo.tsx b/apps/docs/src/components/docs/ai-elements/speech-input-demo.tsx deleted file mode 100644 index 9d75873d..00000000 --- a/apps/docs/src/components/docs/ai-elements/speech-input-demo.tsx +++ /dev/null @@ -1,31 +0,0 @@ -"use client"; - -import { SpeechInput } from "ghost-ui"; -import { useState } from "react"; - -export function SpeechInputDemo() { - const [transcript, setTranscript] = useState(""); - - return ( -
      -

      - Click the microphone button to begin recording. Uses the Web Speech API - when available, with a MediaRecorder fallback. -

      - - - setTranscript((prev) => (prev ? `${prev} ${text}` : text)) - } - /> - -
      -

      Transcription output

      -

      - {transcript || "Transcribed text will appear here..."} -

      -
      -
      - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/stack-trace-demo.tsx b/apps/docs/src/components/docs/ai-elements/stack-trace-demo.tsx deleted file mode 100644 index b8863d15..00000000 --- a/apps/docs/src/components/docs/ai-elements/stack-trace-demo.tsx +++ /dev/null @@ -1,65 +0,0 @@ -"use client"; - -import { - StackTrace, - StackTraceActions, - StackTraceContent, - StackTraceCopyButton, - StackTraceError, - StackTraceErrorMessage, - StackTraceErrorType, - StackTraceExpandButton, - StackTraceFrames, - StackTraceHeader, -} from "ghost-ui"; - -const typeErrorTrace = `TypeError: Cannot read properties of undefined (reading 'map') - at UserList (/src/components/UserList.tsx:24:18) - at renderWithHooks (node_modules/react-dom/cjs/react-dom.development.js:14985:18) - at mountIndeterminateComponent (node_modules/react-dom/cjs/react-dom.development.js:17811:13) - at beginWork (node_modules/react-dom/cjs/react-dom.development.js:19049:16) - at HTMLUnknownElement.callCallback (node_modules/react-dom/cjs/react-dom.development.js:3945:14)`; - -const referenceErrorTrace = `ReferenceError: fetchData is not defined - at loadDashboard (/src/pages/dashboard.ts:15:3) - at async handleRequest (/src/server/router.ts:42:12) - at async processMiddleware (/src/server/middleware.ts:28:5) - at node:internal/process/task_queues:95:5`; - -export function StackTraceDemo() { - return ( -
      - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/suggestion-demo.tsx b/apps/docs/src/components/docs/ai-elements/suggestion-demo.tsx deleted file mode 100644 index feea6511..00000000 --- a/apps/docs/src/components/docs/ai-elements/suggestion-demo.tsx +++ /dev/null @@ -1,18 +0,0 @@ -"use client"; - -import { Suggestion, Suggestions } from "ghost-ui"; - -export function SuggestionDemo() { - return ( -
      -

      Suggested prompts

      - - - - - - - -
      - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/task-demo.tsx b/apps/docs/src/components/docs/ai-elements/task-demo.tsx deleted file mode 100644 index fd752437..00000000 --- a/apps/docs/src/components/docs/ai-elements/task-demo.tsx +++ /dev/null @@ -1,43 +0,0 @@ -"use client"; - -import { - Task, - TaskContent, - TaskItem, - TaskItemFile, - TaskTrigger, -} from "ghost-ui"; - -export function TaskDemo() { - return ( -
      - - - - - Found src/auth/login.ts — contains the - main login handler with JWT token generation. - - - Found src/middleware/auth.ts — - validates tokens on protected routes. - - - Found src/lib/session.ts — manages - session creation and expiration. - - - - - - - - - Reviewed prisma/schema.prisma for user - model relationships. - - - -
      - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/terminal-demo.tsx b/apps/docs/src/components/docs/ai-elements/terminal-demo.tsx deleted file mode 100644 index 6142dca7..00000000 --- a/apps/docs/src/components/docs/ai-elements/terminal-demo.tsx +++ /dev/null @@ -1,63 +0,0 @@ -"use client"; - -import { - Terminal, - TerminalActions, - TerminalContent, - TerminalCopyButton, - TerminalHeader, - TerminalTitle, -} from "ghost-ui"; - -const buildOutput = `\x1b[32m$\x1b[0m next build -\x1b[36minfo\x1b[0m - Linting and checking validity of types... -\x1b[36minfo\x1b[0m - Creating an optimized production build... -\x1b[36minfo\x1b[0m - Compiled successfully -\x1b[36minfo\x1b[0m - Collecting page data... -\x1b[36minfo\x1b[0m - Generating static pages (8/8) -\x1b[36minfo\x1b[0m - Finalizing page optimization... - -Route (app) Size First Load JS -\x1b[37m+\x1b[0m / 5.2 kB 89.3 kB -\x1b[37m+\x1b[0m /about 1.8 kB 85.9 kB -\x1b[37m+\x1b[0m /dashboard 12.4 kB 96.5 kB -\x1b[37m+\x1b[0m /api/health 0 B 0 B - -\x1b[32m\u2713\x1b[0m Build completed in 14.2s`; - -const gitOutput = `\x1b[33mOn branch main\x1b[0m -Your branch is up to date with 'origin/main'. - -Changes to be committed: - \x1b[32mnew file: src/components/avatar.tsx\x1b[0m - \x1b[32mmodified: src/lib/utils.ts\x1b[0m - \x1b[31mdeleted: src/old-component.tsx\x1b[0m - -Untracked files: - \x1b[31m.env.local\x1b[0m`; - -export function TerminalDemo() { - return ( -
      - - - Build Output - - - - - - - - - - git status - - - - - - -
      - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/test-results-demo.tsx b/apps/docs/src/components/docs/ai-elements/test-results-demo.tsx deleted file mode 100644 index e3811432..00000000 --- a/apps/docs/src/components/docs/ai-elements/test-results-demo.tsx +++ /dev/null @@ -1,132 +0,0 @@ -"use client"; - -import { - Test, - TestError, - TestErrorMessage, - TestErrorStack, - TestResults, - TestResultsContent, - TestResultsDuration, - TestResultsHeader, - TestResultsProgress, - TestResultsSummary, - TestSuite, - TestSuiteContent, - TestSuiteName, -} from "ghost-ui"; - -export function TestResultsDemo() { - return ( -
      - - - - - - - - - - - - - - - - - - - - - - - - - - - Expected status 200 but received 500 - - - {`at Object. (tests/api/user.test.ts:45:10) - at processTicksAndRejections (node:internal/process/task_queues:95:5)`} - - - - - - - AssertionError: expected 'invalid' to match - /^[^@]+@[^@]+$/ - - - - - - - - - - - - - - - - - -
      - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/tool-demo.tsx b/apps/docs/src/components/docs/ai-elements/tool-demo.tsx deleted file mode 100644 index db1d9e3d..00000000 --- a/apps/docs/src/components/docs/ai-elements/tool-demo.tsx +++ /dev/null @@ -1,63 +0,0 @@ -"use client"; - -import { Tool, ToolContent, ToolHeader, ToolInput, ToolOutput } from "ghost-ui"; - -export function ToolDemo() { - return ( -
      - - - - - - - - - - - - - - - - - - - - - - -
      - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/toolbar-demo.tsx b/apps/docs/src/components/docs/ai-elements/toolbar-demo.tsx deleted file mode 100644 index 0d6a2cf5..00000000 --- a/apps/docs/src/components/docs/ai-elements/toolbar-demo.tsx +++ /dev/null @@ -1,71 +0,0 @@ -"use client"; - -import { type NodeTypes, ReactFlowProvider } from "@xyflow/react"; -import { - Button, - Canvas, - Node, - NodeContent, - NodeHeader, - NodeTitle, - Toolbar, -} from "ghost-ui"; -import { CopyIcon, PencilIcon, Trash2Icon } from "lucide-react"; -import { useMemo } from "react"; - -const ToolbarNode = () => ( - - - Select this node - - -

      - Click to select and reveal the toolbar below. -

      -
      - - - - - -
      -); - -const initialNodes = [ - { - id: "1", - type: "toolbar", - position: { x: 100, y: 60 }, - data: {}, - selected: true, - }, -]; - -export function ToolbarDemo() { - const nodeTypes: NodeTypes = useMemo( - () => ({ - toolbar: ToolbarNode, - }), - [], - ); - - return ( -
      -

      - A floating toolbar that appears below a selected node, providing - contextual actions. -

      - -
      - -
      -
      -
      - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/transcription-demo.tsx b/apps/docs/src/components/docs/ai-elements/transcription-demo.tsx deleted file mode 100644 index 43c17faf..00000000 --- a/apps/docs/src/components/docs/ai-elements/transcription-demo.tsx +++ /dev/null @@ -1,54 +0,0 @@ -"use client"; - -import { Button, Transcription, TranscriptionSegment } from "ghost-ui"; -import { useState } from "react"; - -const mockSegments = [ - { text: "Welcome to the demo.", startSecond: 0, endSecond: 2 }, - { text: " Today we are looking at", startSecond: 2, endSecond: 4 }, - { text: " the transcription component,", startSecond: 4, endSecond: 6 }, - { text: " which highlights words", startSecond: 6, endSecond: 8 }, - { text: " as audio plays.", startSecond: 8, endSecond: 10 }, - { text: " Each segment is clickable", startSecond: 10, endSecond: 12 }, - { text: " and can seek to", startSecond: 12, endSecond: 14 }, - { text: " the corresponding position", startSecond: 14, endSecond: 16 }, - { text: " in the audio track.", startSecond: 16, endSecond: 18 }, -]; - -export function TranscriptionDemo() { - const [currentTime, setCurrentTime] = useState(0); - - return ( -
      -
      - - - {currentTime.toFixed(1)}s - - -
      - - - {(segment, index) => ( - - )} - -
      - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/voice-selector-demo.tsx b/apps/docs/src/components/docs/ai-elements/voice-selector-demo.tsx deleted file mode 100644 index a5fe8b04..00000000 --- a/apps/docs/src/components/docs/ai-elements/voice-selector-demo.tsx +++ /dev/null @@ -1,137 +0,0 @@ -"use client"; - -import { - Button, - VoiceSelector, - VoiceSelectorAccent, - VoiceSelectorAge, - VoiceSelectorAttributes, - VoiceSelectorBullet, - VoiceSelectorContent, - VoiceSelectorDescription, - VoiceSelectorEmpty, - VoiceSelectorGender, - VoiceSelectorGroup, - VoiceSelectorInput, - VoiceSelectorItem, - VoiceSelectorList, - VoiceSelectorName, - VoiceSelectorPreview, - VoiceSelectorSeparator, - VoiceSelectorTrigger, -} from "ghost-ui"; -import { useState } from "react"; - -const voices = [ - { - id: "alloy", - name: "Alloy", - gender: "non-binary" as const, - accent: "american" as const, - age: "Young Adult", - description: "Versatile, balanced tone", - }, - { - id: "echo", - name: "Echo", - gender: "male" as const, - accent: "american" as const, - age: "Adult", - description: "Warm, resonant baritone", - }, - { - id: "fable", - name: "Fable", - gender: "female" as const, - accent: "british" as const, - age: "Adult", - description: "Expressive storyteller", - }, - { - id: "onyx", - name: "Onyx", - gender: "male" as const, - accent: "american" as const, - age: "Mature", - description: "Deep, authoritative", - }, - { - id: "nova", - name: "Nova", - gender: "female" as const, - accent: "australian" as const, - age: "Young Adult", - description: "Bright, energetic", - }, - { - id: "shimmer", - name: "Shimmer", - gender: "female" as const, - accent: "irish" as const, - age: "Adult", - description: "Soft, calming presence", - }, -]; - -export function VoiceSelectorDemo() { - const [selected, setSelected] = useState(undefined); - - return ( -
      -

      - A dialog-based voice picker with search, gender, accent, and preview - controls. -

      - - - - - - - - - - No voices found. - - {voices.map((voice) => ( - setSelected(voice.id)} - > -
      -
      -
      - {voice.name} - - - - - - {voice.age} - -
      - - {voice.description} - -
      - { - /* no-op in demo */ - }} - /> -
      -
      - ))} -
      - -
      -
      -
      -
      - ); -} diff --git a/apps/docs/src/components/docs/ai-elements/web-preview-demo.tsx b/apps/docs/src/components/docs/ai-elements/web-preview-demo.tsx deleted file mode 100644 index 76c8253b..00000000 --- a/apps/docs/src/components/docs/ai-elements/web-preview-demo.tsx +++ /dev/null @@ -1,52 +0,0 @@ -"use client"; - -import { - WebPreview, - WebPreviewBody, - WebPreviewConsole, - WebPreviewNavigation, - WebPreviewNavigationButton, - WebPreviewUrl, -} from "ghost-ui"; -import { ArrowLeftIcon, ArrowRightIcon, RefreshCwIcon } from "lucide-react"; - -export function WebPreviewDemo() { - return ( -
      - - - - - - - - - - - - - - - - -
      - ); -} diff --git a/apps/docs/src/components/docs/bento/activity-goal.tsx b/apps/docs/src/components/docs/bento/activity-goal.tsx deleted file mode 100644 index f7f1c2d4..00000000 --- a/apps/docs/src/components/docs/bento/activity-goal.tsx +++ /dev/null @@ -1,63 +0,0 @@ -"use client"; - -import { - Button, - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "ghost-ui"; -import { Minus, Plus } from "lucide-react"; -import * as React from "react"; - -export function CardsActivityGoal() { - const [amount, setAmount] = React.useState(350); - - function onClick(adjustment: number) { - setAmount(Math.max(200, Math.min(400, amount + adjustment))); - } - - return ( - - - Payment Amount - Set your payment amount. - - -
      - -
      -
      ${amount}
      -
      - USD -
      -
      - -
      -
      - - - -
      - ); -} diff --git a/apps/docs/src/components/docs/bento/calendar.tsx b/apps/docs/src/components/docs/bento/calendar.tsx deleted file mode 100644 index e1971caf..00000000 --- a/apps/docs/src/components/docs/bento/calendar.tsx +++ /dev/null @@ -1,24 +0,0 @@ -"use client"; - -import { addDays } from "date-fns"; -import { Calendar, Card, CardContent } from "ghost-ui"; - -const start = new Date(2023, 5, 5); - -export function CardsCalendar() { - return ( - - - - - - ); -} diff --git a/apps/docs/src/components/docs/bento/chat.tsx b/apps/docs/src/components/docs/bento/chat.tsx deleted file mode 100644 index 85663470..00000000 --- a/apps/docs/src/components/docs/bento/chat.tsx +++ /dev/null @@ -1,249 +0,0 @@ -"use client"; - -import { - Avatar, - AvatarFallback, - AvatarImage, - Button, - Card, - CardContent, - CardFooter, - CardHeader, - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, - cn, - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - Input, - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "ghost-ui"; -import { Check, Plus, Send } from "lucide-react"; -import * as React from "react"; - -const users = [ - { - name: "Olivia Martin", - email: "m@example.com", - }, - { - name: "Isabella Nguyen", - email: "isabella.nguyen@email.com", - }, - { - name: "Emma Wilson", - email: "emma@example.com", - }, - { - name: "Jackson Lee", - email: "lee@example.com", - }, - { - name: "William Kim", - email: "will@email.com", - }, -] as const; - -type User = (typeof users)[number]; - -export function CardsChat() { - const [open, setOpen] = React.useState(false); - const [selectedUsers, setSelectedUsers] = React.useState([]); - - const [messages, setMessages] = React.useState([ - { - role: "agent", - content: "Hi, how can I help you today?", - }, - { - role: "user", - content: "Hey, I'm having trouble with my account.", - }, - { - role: "agent", - content: "What seems to be the problem?", - }, - { - role: "user", - content: "I can't log in.", - }, - ]); - const [input, setInput] = React.useState(""); - const inputLength = input.trim().length; - - return ( - <> - - -
      - - OM - -
      -

      Sofia Davis

      -

      m@example.com

      -
      -
      - - - - - - New message - - -
      - -
      - {messages.map((message, index) => ( -
      - {message.content} -
      - ))} -
      -
      - -
      { - event.preventDefault(); - if (inputLength === 0) return; - setMessages([ - ...messages, - { - role: "user", - content: input, - }, - ]); - setInput(""); - }} - className="flex w-full items-center space-x-2" - > - setInput(event.target.value)} - /> - -
      -
      -
      - - - - New message - - Invite a user to this thread. This will create a new group - message. - - - - - - No users found. - - {users.map((user) => ( - { - if (selectedUsers.includes(user)) { - return setSelectedUsers( - selectedUsers.filter( - (selectedUser) => selectedUser !== user, - ), - ); - } - - return setSelectedUsers( - [...users].filter((u) => - [...selectedUsers, user].includes(u), - ), - ); - }} - > - - {user.name[0]} - -
      -

      - {user.name} -

      -

      - {user.email} -

      -
      - {selectedUsers.includes(user) ? ( - - ) : null} -
      - ))} -
      -
      -
      - - {selectedUsers.length > 0 ? ( -
      - {selectedUsers.map((user) => ( - - {user.name[0]} - - ))} -
      - ) : ( -

      - Select users to add to this thread. -

      - )} - -
      -
      -
      - - ); -} diff --git a/apps/docs/src/components/docs/bento/cookie-settings.tsx b/apps/docs/src/components/docs/bento/cookie-settings.tsx deleted file mode 100644 index 515542a8..00000000 --- a/apps/docs/src/components/docs/bento/cookie-settings.tsx +++ /dev/null @@ -1,60 +0,0 @@ -"use client"; - -import { - Button, - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, - Label, - Switch, -} from "ghost-ui"; - -export function CardsCookieSettings() { - return ( - - - Cookie Settings - Manage your cookie settings here. - - -
      - - -
      -
      - - -
      -
      - - -
      -
      - - - -
      - ); -} diff --git a/apps/docs/src/components/docs/bento/create-account.tsx b/apps/docs/src/components/docs/bento/create-account.tsx deleted file mode 100644 index 236868cd..00000000 --- a/apps/docs/src/components/docs/bento/create-account.tsx +++ /dev/null @@ -1,60 +0,0 @@ -"use client"; - -import { - Button, - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, - Input, - Label, -} from "ghost-ui"; -import { Icons } from "@/components/docs/icons"; - -export function CardsCreateAccount() { - return ( - - - Create an account - - Enter your email below to create your account - - - -
      - - -
      -
      -
      - -
      -
      - - Or continue with - -
      -
      -
      - - -
      -
      - - -
      -
      - - - -
      - ); -} diff --git a/apps/docs/src/components/docs/bento/data-table.tsx b/apps/docs/src/components/docs/bento/data-table.tsx deleted file mode 100644 index 239bd1c7..00000000 --- a/apps/docs/src/components/docs/bento/data-table.tsx +++ /dev/null @@ -1,322 +0,0 @@ -"use client"; - -import { - ColumnDef, - ColumnFiltersState, - flexRender, - getCoreRowModel, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - SortingState, - useReactTable, - VisibilityState, -} from "@tanstack/react-table"; -import { - Button, - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, - Checkbox, - DropdownMenu, - DropdownMenuCheckboxItem, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, - Input, - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "ghost-ui"; -import { ArrowUpDown, ChevronDown, MoreHorizontal } from "lucide-react"; -import * as React from "react"; - -const data: Payment[] = [ - { - id: "m5gr84i9", - amount: 316, - status: "success", - email: "ken99@example.com", - }, - { - id: "3u1reuv4", - amount: 242, - status: "success", - email: "Abe45@example.com", - }, - { - id: "derv1ws0", - amount: 837, - status: "processing", - email: "Monserrat44@example.com", - }, - { - id: "bhqecj4p", - amount: 721, - status: "failed", - email: "carmella@example.com", - }, -]; - -export type Payment = { - id: string; - amount: number; - status: "pending" | "processing" | "success" | "failed"; - email: string; -}; - -export const columns: ColumnDef[] = [ - { - id: "select", - header: ({ table }) => ( - table.toggleAllPageRowsSelected(!!value)} - aria-label="Select all" - /> - ), - cell: ({ row }) => ( - row.toggleSelected(!!value)} - aria-label="Select row" - /> - ), - enableSorting: false, - enableHiding: false, - }, - { - accessorKey: "status", - header: "Status", - cell: ({ row }) => ( -
      {row.getValue("status")}
      - ), - }, - { - accessorKey: "email", - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) =>
      {row.getValue("email")}
      , - }, - { - accessorKey: "amount", - header: () =>
      Amount
      , - cell: ({ row }) => { - const amount = parseFloat(row.getValue("amount")); - - // Format the amount as a dollar amount - const formatted = new Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - }).format(amount); - - return ( -
      {formatted}
      - ); - }, - }, - { - id: "actions", - enableHiding: false, - cell: ({ row }) => { - const payment = row.original; - - return ( - - - - - - Actions - navigator.clipboard.writeText(payment.id)} - > - Copy payment ID - - - View customer - View payment details - - - ); - }, - }, -]; - -export function CardsDataTable() { - const [sorting, setSorting] = React.useState([]); - const [columnFilters, setColumnFilters] = React.useState( - [], - ); - const [columnVisibility, setColumnVisibility] = - React.useState({}); - const [rowSelection, setRowSelection] = React.useState({}); - - const table = useReactTable({ - data, - columns, - onSortingChange: setSorting, - onColumnFiltersChange: setColumnFilters, - getCoreRowModel: getCoreRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - getFilteredRowModel: getFilteredRowModel(), - onColumnVisibilityChange: setColumnVisibility, - onRowSelectionChange: setRowSelection, - state: { - sorting, - columnFilters, - columnVisibility, - rowSelection, - }, - }); - - return ( - - - Payments - Manage your payments. - - -
      - - table.getColumn("email")?.setFilterValue(event.target.value) - } - className="max-w-sm" - /> - - - - - - {table - .getAllColumns() - .filter((column) => column.getCanHide()) - .map((column) => { - return ( - - column.toggleVisibility(!!value) - } - > - {column.id} - - ); - })} - - -
      -
      - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext(), - )} - - ); - })} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext(), - )} - - ))} - - )) - ) : ( - - - No results. - - - )} - -
      -
      -
      -
      - {table.getFilteredSelectedRowModel().rows.length} of{" "} - {table.getFilteredRowModel().rows.length} row(s) selected. -
      -
      - - -
      -
      -
      -
      - ); -} diff --git a/apps/docs/src/components/docs/bento/index.tsx b/apps/docs/src/components/docs/bento/index.tsx deleted file mode 100644 index bf01514f..00000000 --- a/apps/docs/src/components/docs/bento/index.tsx +++ /dev/null @@ -1,100 +0,0 @@ -"use client"; - -import { useIsMobile } from "ghost-ui"; -import { lazy, Suspense } from "react"; -import { CardsChat } from "@/components/docs/bento/chat"; -import { CardsCookieSettings } from "@/components/docs/bento/cookie-settings"; -import { CardsCreateAccount } from "@/components/docs/bento/create-account"; -import { CardsPaymentMethod } from "@/components/docs/bento/payment-method"; -import { CardsReportIssue } from "@/components/docs/bento/report-issue"; -import { CardsShare } from "@/components/docs/bento/share"; -import { CardsTeamMembers } from "@/components/docs/bento/team-members"; - -const CardsStats = lazy(() => - import("@/components/docs/bento/stats").then((m) => ({ - default: m.CardsStats, - })), -); -const CardsCalendar = lazy(() => - import("@/components/docs/bento/calendar").then((m) => ({ - default: m.CardsCalendar, - })), -); -const CardsActivityGoal = lazy(() => - import("@/components/docs/bento/activity-goal").then((m) => ({ - default: m.CardsActivityGoal, - })), -); -const CardsMetric = lazy(() => - import("@/components/docs/bento/metric").then((m) => ({ - default: m.CardsMetric, - })), -); -const CardsDataTable = lazy(() => - import("@/components/docs/bento/data-table").then((m) => ({ - default: m.CardsDataTable, - })), -); - -function CalendarMetricGroup() { - return ( -
      - - - -
      - - - -
      -
      - - - -
      -
      - ); -} - -export function BentoDemo() { - const isMobile = useIsMobile(); - - return ( -
      -
      - - - - {isMobile && } -
      -
      - - - -
      -
      - - -
      - -
      -
      -
      -
      -
      - {!isMobile && ( - <> - - - - - - )} - -
      - -
      -
      -
      - ); -} diff --git a/apps/docs/src/components/docs/bento/metric.tsx b/apps/docs/src/components/docs/bento/metric.tsx deleted file mode 100644 index 17d3c52f..00000000 --- a/apps/docs/src/components/docs/bento/metric.tsx +++ /dev/null @@ -1,105 +0,0 @@ -"use client"; - -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, - ChartConfig, - ChartContainer, - ChartTooltip, - ChartTooltipContent, -} from "ghost-ui"; -import { Line, LineChart } from "recharts"; - -const data = [ - { - average: 400, - today: 240, - }, - { - average: 300, - today: 139, - }, - { - average: 200, - today: 980, - }, - { - average: 278, - today: 390, - }, - { - average: 189, - today: 480, - }, - { - average: 239, - today: 380, - }, - { - average: 349, - today: 430, - }, -]; - -const chartConfig = { - today: { - label: "Current Value", - color: "hsl(var(--primary))", - }, - average: { - label: "Average Value", - color: "hsl(var(--primary))", - }, -} satisfies ChartConfig; - -export function CardsMetric() { - return ( - - - Portfolio Value - - Your portfolio is performing above its 7-day average. - - - - - - - - } /> - - - - - ); -} diff --git a/apps/docs/src/components/docs/bento/payment-amount.tsx b/apps/docs/src/components/docs/bento/payment-amount.tsx deleted file mode 100644 index d2b84774..00000000 --- a/apps/docs/src/components/docs/bento/payment-amount.tsx +++ /dev/null @@ -1,63 +0,0 @@ -"use client"; - -import { - Button, - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "ghost-ui"; -import { Minus, Plus } from "lucide-react"; -import * as React from "react"; - -export function PaymentAmount() { - const [amount, setAmount] = React.useState(350); - - function onClick(adjustment: number) { - setAmount(Math.max(200, Math.min(400, amount + adjustment))); - } - - return ( - - - Payment Amount - Set your payment amount. - - -
      - -
      -
      ${amount}
      -
      - USD -
      -
      - -
      -
      - - - -
      - ); -} diff --git a/apps/docs/src/components/docs/bento/payment-method.tsx b/apps/docs/src/components/docs/bento/payment-method.tsx deleted file mode 100644 index 7df3c97e..00000000 --- a/apps/docs/src/components/docs/bento/payment-method.tsx +++ /dev/null @@ -1,138 +0,0 @@ -"use client"; - -import { - Button, - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, - Input, - Label, - RadioGroup, - RadioGroupItem, - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "ghost-ui"; -import { Icons } from "@/components/docs/icons"; - -export function CardsPaymentMethod() { - return ( - - - Payment Method - - Add a new payment method to your account. - - - - -
      - - -
      -
      - - -
      -
      -
      - - -
      -
      - - -
      -
      - - -
      -
      -
      - - -
      -
      - - -
      -
      - - -
      -
      -
      - - - -
      - ); -} diff --git a/apps/docs/src/components/docs/bento/report-issue.tsx b/apps/docs/src/components/docs/bento/report-issue.tsx deleted file mode 100644 index 4cdf6b29..00000000 --- a/apps/docs/src/components/docs/bento/report-issue.tsx +++ /dev/null @@ -1,93 +0,0 @@ -"use client"; - -import { - Button, - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, - Input, - Label, - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, - Textarea, -} from "ghost-ui"; -import * as React from "react"; - -export function CardsReportIssue() { - const id = React.useId(); - - return ( - - - Report an issue - - What area are you having problems with? - - - -
      -
      - - -
      -
      - - -
      -
      -
      - - -
      -
      - -