From 18b8be85a4437763aa9c786137c6cd4e4cc9c476 Mon Sep 17 00:00:00 2001 From: Nahiyan Khan Date: Thu, 25 Jun 2026 10:53:42 -0400 Subject: [PATCH 1/9] docs: reset Ghost around a clean Layer 2 design Stop circling. Three notes (purposes, ghost-layers, contract-and-binding) shared one diagnosis: the descriptive core is clean; selection/routing/merge leaked into the artifact's shape. reset.md fixes purpose, goals, layers, and separation of concerns, and schedules a single first cut. coordinate-space.md is the clean-room Layer 2 design: a surface is an author-named group with an optional description; topology is a strict containment tree plus cascade-from-ancestors plus rare explicit shared-edges; resolution is BYOA (Ghost emits a described menu, the agent matches); delete list covers inventory.topology, smeared applies_to, and ghost.map/v1. Focus pass: delete pre-redesign docs (fingerprint-format, generation-loop, host-adapters, ghost-fleet, language-fingerprints, relay-configs-and-context, prompt-first-relay-prd) that described the dead Relay-routing and topology/applies_to model. Port the one durable thesis (language maps onto the four facets) inline into the voice skill reference and fix its stale coordinate guidance. Update README and ideas/README to the reset arc. --- README.md | 7 +- docs/fingerprint-format.md | 274 ----------------- docs/generation-loop.md | 154 ---------- docs/ghost-fleet.md | 68 ----- docs/host-adapters.md | 139 --------- docs/ideas/README.md | 49 ++- docs/ideas/contract-and-binding.md | 210 +++++++++++++ docs/ideas/coordinate-space.md | 275 +++++++++++++++++ docs/ideas/ghost-layers.md | 136 +++++++++ docs/ideas/reset.md | 178 +++++++++++ docs/language-fingerprints.md | 186 ------------ docs/purposes.md | 91 ++++++ docs/relay-configs-and-context.md | 281 ------------------ .../src/skill-bundle/references/voice.md | 13 +- 14 files changed, 935 insertions(+), 1126 deletions(-) delete mode 100644 docs/fingerprint-format.md delete mode 100644 docs/generation-loop.md delete mode 100644 docs/ghost-fleet.md delete mode 100644 docs/host-adapters.md create mode 100644 docs/ideas/contract-and-binding.md create mode 100644 docs/ideas/coordinate-space.md create mode 100644 docs/ideas/ghost-layers.md create mode 100644 docs/ideas/reset.md delete mode 100644 docs/language-fingerprints.md create mode 100644 docs/purposes.md delete mode 100644 docs/relay-configs-and-context.md diff --git a/README.md b/README.md index f9a2c89a..3aa464c0 100644 --- a/README.md +++ b/README.md @@ -204,10 +204,7 @@ optional and only used by semantic embedding helpers when a host opts in. | Resource | Description | | --- | --- | -| [docs/fingerprint-format.md](./docs/fingerprint-format.md) | Portable `.ghost/` package format. | -| [docs/generation-loop.md](./docs/generation-loop.md) | Brief, generate, check, review, and remediate loop. | -| [docs/language-fingerprints.md](./docs/language-fingerprints.md) | Voice and language capture through existing fingerprint facets. | -| [docs/host-adapters.md](./docs/host-adapters.md) | Adapter-neutral JSON, severity mapping, and custom fingerprint directories. | -| [docs/ghost-fleet.md](./docs/ghost-fleet.md) | Current private fleet package model. | +| [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. | diff --git a/docs/fingerprint-format.md b/docs/fingerprint-format.md deleted file mode 100644 index 842faca5..00000000 --- a/docs/fingerprint-format.md +++ /dev/null @@ -1,274 +0,0 @@ -# The Portable Fingerprint Package Format - -A Ghost fingerprint is a checked-in, repo-local surface-composition contract -that humans can approve and agents can act from. The canonical portable package -lives under `.ghost/`: - -```text -.ghost/ - manifest.yml # ghost.fingerprint-package/v1 package anchor - intent.yml # core: surface intent - inventory.yml # core: curated material and source links - composition.yml # core: experience patterns - validate.yml # optional deterministic gates -``` - -Git is the staging and approval boundary: uncommitted or unmerged edits are -draft work, and checked-in facet files are canonical for Ghost. - -`manifest.yml` is intentionally small: - -```yaml -schema: ghost.fingerprint-package/v1 -id: local -``` - -The raw facet files can be sparse. Missing files or sections mean this package -contributes no local guidance for that facet; broader stack context may still -supply it. Ghost normalizes absent facets to empty values when it assembles the -internal `ghost.fingerprint/v1` document used by validation checks, review -packets, Relay briefs, compare, and stack merges. - -## Core Facets - -`intent.yml` captures the intent behind the surface: - -```yaml -summary: - product: Example Docs - audience: [contributors, maintainers] - goals: - - Preserve task-first documentation and product trust. -principles: - - id: intent-before-material - principle: Intent captures the intent behind the surface; inventory points to replaceable material. -experience_contracts: - - id: review-cites-memory - contract: Advisory review findings must cite the diff and relevant fingerprint refs. -``` - -Use intent for durable claims about audience needs, product obligations, -acceptable tradeoffs, what the surface refuses to become, and contracts that -should shape agent behavior. - -High-quality facet content makes generation choices explicit: what to preserve, -what to avoid, which tradeoffs win, which situations route guidance, and which -exemplars ground the claim. - -`inventory.yml` points to curated material and optional source links: - -```yaml -topology: - scopes: - - id: docs-site - paths: [apps/docs] - surface_types: [docs-home, reference-page] - surface_types: [docs-home, reference-page] -building_blocks: - tokens: [--color-bg, --color-fg] - components: [Button, CodeBlock] - libraries: [packages/ghost-ui] - assets: [apps/docs/public/placeholder.svg] - routes: [/docs, /docs/cli-reference] - files: [apps/docs/src/content/docs/cli-reference.mdx] -exemplars: - - id: cli-reference-page - path: apps/docs/src/content/docs/cli-reference.mdx - title: CLI reference page - surface_type: reference-page - scope: docs-site - why: Shows command facts and examples kept visually adjacent. - refs: [composition.pattern:reference-before-decoration] -sources: - - id: ghost-ui-registry - kind: registry - ref: packages/ghost-ui/public/r/registry.json -``` - -Supported `inventory.sources[].kind` values are `registry`, `file`, `url`, and -`package`. Source links are provenance and orientation; they do not -make generated material canonical by themselves. - -`composition.yml` captures the patterns that make a surface feel intentional: - -```yaml -patterns: - - id: reference-before-decoration - kind: structure - pattern: Reference pages prioritize the working surface before visual flourish. - applies_to: - surface_types: [reference-page] - guidance: - - Keep command names, flags, and examples visually adjacent. - - Put caveats near the command they modify. - evidence: - - path: apps/docs/src/content/docs/cli-reference.mdx - check_refs: [validate.check:no-hardcoded-brand-color] -``` - -Pattern `kind` can be `rule`, `layout`, `structure`, `flow`, `state`, -`visual`, `behavior`, or `content`. - -## References - -Use facet-qualified refs when one part of the fingerprint grounds another: - -- `intent.situation:` -- `intent.principle:` -- `intent.experience_contract:` -- `inventory.exemplar:` -- `composition.pattern:` -- `validate.check:` - -Facet refs without `validate.check:` are used where only fingerprint facet material is -valid, such as `inventory.exemplars[].refs`. - -## Enforcement - -`validate.yml` uses `ghost.validate/v1`. Checks are -deterministic validation, not generation input. - -```yaml -schema: ghost.validate/v1 -id: example-docs -checks: - - id: no-hardcoded-brand-color - title: Use semantic color tokens - status: active - severity: serious - derivation: - intent: - - intent.principle:reference-before-decoration - inventory: - - inventory.exemplar:cli-reference-page - composition: - - composition.pattern:reference-before-decoration - applies_to: - scopes: [docs-site] - paths: [apps/docs] - surface_types: [reference-page] - detector: - type: forbidden-regex - pattern: '#[0-9a-fA-F]{3,8}' - evidence: - support: 0.92 - observed_count: 12 - examples: - - apps/docs/src/styles/theme.css - repair: Move repeatable colors into semantic tokens. -``` - -Check `status` can be `active`, `proposed`, or `disabled`. `severity` can be -`critical`, `serious`, or `nit`. - -Detector `type` can be: - -- `forbidden-regex` -- `required-regex` -- `banned-import` -- `banned-component` -- `required-token` - -Ref-backed checks are preferred. Missing derivation refs lint as warnings, not -errors, so teams can draft gates while curation catches up. Promote only rules -that can be detected deterministically; taste stays in intent or composition -until there is a reliable detector. - -## Nested Packages - -Large repos can add scoped packages below the root: - -```text -.ghost/... -apps/checkout/.ghost/... -apps/checkout/review/page.tsx -``` - -For a path like `apps/checkout/review/page.tsx`, Ghost resolves each -`/manifest.yml` from root to leaf. Each package is a -sparse patch: it contributes only the facets it knows, and the resolved stack -supplies the working context. The merged stack is broad-to-local: - -- child entries with the same `id` replace parent entries; -- scalar summary fields use the nearest child value; -- arrays merge with de-dupe; -- child-relative paths normalize to repo-root paths in reports; -- checks merge by `id`, so a child check with `status: disabled` suppresses an - inherited active check; -- intent situations, principles, and experience contracts merge by `id`, with - child entries winning; -- composition patterns, inventory exemplars, and sources merge by `id`, with - child entries winning. - -Use nested packages when an area has genuinely different surface composition, -not just because it has different files. A nested package does not need to -restate inherited intent, inventory, composition, or validation checks. - -For workspace monorepos, start with a safe plan: - -```bash -ghost init --monorepo -``` - -This creates or preserves the root `.ghost/` package, detects child package -roots from workspace metadata, and prints proposed `ghost init --scope ...` -commands. Add `--apply` when you want Ghost to create the detected child -packages: - -```bash -ghost init --monorepo --apply -``` - -## Core Commands - -```bash -ghost init -ghost scan --format json -ghost lint .ghost -ghost verify .ghost --root . -ghost check --base main --format json -ghost review --base main -ghost emit review-command --path apps/checkout/review/page.tsx -ghost relay gather apps/checkout/review/page.tsx --format json -ghost relay gather --request request.yml --format json -``` - -`ghost scan` reports package contribution facets. Useful `intent` means any -non-empty summary field, situation, principle, or experience contract. Useful -`inventory` means topology scopes or surface types, curated building blocks, -exemplars, or source links. Useful `composition` means at least one pattern. -Useful `validate` means at least one deterministic check. Absent facets are -reported as absent contributions, not incomplete packages. - -Use raw repo signals when observed repo facts are useful authoring evidence: - -```bash -ghost signals . -``` - -Curate durable conclusions into `intent.yml`, `inventory.yml`, or -`composition.yml`. - -## Authoring Rules - -- Write durable surface intent in `intent.yml`. -- Write curated repo material and exemplars in `inventory.yml`. -- Write repeatable experience patterns in `composition.yml`. -- Write deterministic gates in `validate.yml`. -- Prefer typed refs over prose-only cross-links. -- Keep ids stable after review because refs and checks depend on them. -- Let Git review approve changes to canonical fingerprint facets. - -Do not: - -- describe root-level `fingerprint.md` or direct `fingerprint.yml` as the new - canonical package input; -- treat cache output as canonical surface guidance; -- promote subjective taste directly into a check without a deterministic - detector; -- put structural gate configuration in intent. - -Legacy `resources.yml`, `map.md`, `survey.json`, `patterns.yml`, direct -`fingerprint.md`, and direct `fingerprint.yml` files may appear in older repos -or explicit compatibility workflows. New Ghost work should target the split -portable package under `.ghost/`. diff --git a/docs/generation-loop.md b/docs/generation-loop.md deleted file mode 100644 index f6047af1..00000000 --- a/docs/generation-loop.md +++ /dev/null @@ -1,154 +0,0 @@ -# Fingerprint Generation Loop - -Ghost gives UI generators and product-development agents a local, auditable -product-surface composition fingerprint. Generation starts from checked-in -facets; checks and review govern the result afterward. - -```text -intent.yml + inventory.yml + composition.yml - | - v -host agent or generator - | - v -HTML / JSX / app code - | - v -ghost check + ghost review - | - v -deterministic gates + advisory surface-composition findings -``` - -## Before Generation - -Gather Relay JSON when a target path is known: - -```bash -ghost relay gather apps/checkout/review/page.tsx --format json -``` - -By default, Relay uses the resolved `.ghost` fingerprint stack as its base -runtime. A Relay config can add declared sources, request resolvers, or opt out -of the fingerprint base entirely with `base.kind: none`. - -For prompt-shaped work without a clear path, the host agent should first turn -the ask into a structured Relay request, then pass it to Ghost: - -```yaml -schema: ghost.relay-request/v1 -task: generate-interface -selectors: - customer: subscriber - brand: acme - system: portal - moment: renewal-reminder - medium: email - capability: billing -``` - -```bash -ghost relay gather --request-stdin --format json -``` - -If the host framework stores Relay config outside `.ghost/relay.yml`, keep the -same command and pass the config: - -```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 second form works for `base.kind: none` configs by synthesizing a minimal -`task: gather` Relay request from the target path. - -The full `ghost.relay.gather/v2` result is the agent contract. Agents should -read `context`, `selected_context`, `targetPaths`, `source`, `stackDirs`, gaps, -and trace fields from JSON rather than scraping the markdown preview. - -Use the JSON context in this order: - -1. Start from the selected context hits and their match reasons. -2. Apply intent and composition hits before choosing implementation details. -3. Inspect inventory hits as concrete anchors. -4. Use `inventory.building_blocks` as curated material. -5. Run `ghost signals` when raw repo observations would help find evidence. -6. Skim active checks in `.ghost/validate.yml` so generation avoids - deterministic failures. -7. Treat gaps as a signal to use local evidence provisionally or inspect the - full facet files. - -For quick terminal inspection, `ghost relay gather ` still prints a -compact human preview. The preview can omit projected Relay config sources that -are present in JSON. - -Raw repo signals can help orient an agent: - -```bash -ghost signals . -``` - -Signals answer what exists now and do not count as fingerprint contribution. -`intent.yml` captures the intent behind the surface. Curated inventory points to -building blocks and exemplars. `composition.yml` captures the patterns that make -the surface feel intentional. - -## Govern - -`ghost check` is deterministic: - -```bash -ghost check --base main --format json -``` - -Without `--package`, `ghost check` groups changed files by resolved fingerprint -stack and runs merged checks for each group. Only active checks can block. - -`ghost review` is advisory: - -```bash -ghost review --base main -``` - -Advisory review packets include the current diff, the same context-hit model as -Relay, active checks, and finding categories for fixes, intentional -divergence, missing fingerprint grounding, experience gaps, and eval -uncertainty. - -Review findings should cite the diff location, relevant fingerprint facet refs, -relevant exemplars when useful, any active check when blocking, and a -selected-context gap or local-evidence rationale when the fingerprint is silent. - -## Remediation - -When review flags drift from the fingerprint, the host agent chooses the -smallest useful response: - -- Fix the generated or changed code. -- Explain why a divergence is intentional. -- Update the split fingerprint package when the user asks to change the Ghost - fingerprint. - -## CI - -CI should run deterministic checks for UI-touching changes. Advisory review can -attach a packet or comment, but it should not fail the build unless a finding is -backed by an active check. - -```bash -ghost check --base main -ghost review --base main --format markdown -``` - -Advanced wrappers that store fingerprint packages outside `.ghost` can set -`GHOST_PACKAGE_DIR=` on stack-aware commands. `--package ` -remains exact single-bundle mode and bypasses stack discovery. Wrappers that -store Relay runtime config elsewhere should set `GHOST_RELAY_CONFIG` or pass -`ghost relay gather --config `. - -## Legacy Cache Helpers - -Older Ghost bundles used `resources.yml`, `map.md`, `survey.json`, -`patterns.yml`, and direct `fingerprint.yml` files under `.ghost/` as capture -material. Those files are now legacy/cache source material. Promote durable -conclusions into `intent.yml`, `inventory.yml`, and `composition.yml`. diff --git a/docs/ghost-fleet.md b/docs/ghost-fleet.md deleted file mode 100644 index 25aa2c9e..00000000 --- a/docs/ghost-fleet.md +++ /dev/null @@ -1,68 +0,0 @@ -# Ghost Fleet - -`ghost-fleet` is a private workspace package for read-only elevation views -across many design-system or product-surface fingerprints. It is not part of -the public `@anarchitecture/ghost` npm surface. - -Per-repo Ghost answers "is this repo using its fingerprint faithfully?" Fleet -answers "what does this set of systems look like together?" It computes -deterministic facts across member snapshots, then the fleet skill turns those -facts into a world-shape narrative. - -## Current Shape - -Fleet reads a local directory of member snapshots: - -```text -fleet/ - members/ - cash-web/ - map.md - fingerprint.md - .ghost-sync.json - fingerprints/ - checkout.md - dashboard/ - map.md - fingerprint.md - reports/ -``` - -Each member is read-only. Fleet does not fetch, refresh, regenerate, or author -member fingerprints. If a member is stale, refresh the source repo or snapshot -outside fleet, then run fleet again. - -## CLI - -```bash -ghost-fleet members -ghost-fleet view -ghost-fleet emit skill -``` - -- `members` lists loaded members and surfaces missing or malformed inputs. -- `view` writes `fleet.md` and `fleet.json` to `/reports/`. -- `emit skill` installs the fleet skill bundle into a host agent directory. - -The emitted `ghost.fleet/v1` frontmatter contains: - -- parent-member pairwise distances; -- scoped fingerprint nodes and node distances; -- track edges read from `.ghost-sync.json`; -- groupings by platform, build system, registry, rendering, and styling. - -The CLI intentionally does not write the narrative. The skill fills the body -sections `World shape`, `Cohorts`, and `Tracks` from the deterministic output. - -## Boundaries - -- Fleet consumes direct `map.md` and `fingerprint.md` snapshots for the private - fleet workflow. That compatibility shape does not change the public - `.ghost/` package model. -- Fleet may read scoped overlays from `fingerprints/.md`; those are - member snapshots, not nested package roots. -- Clusters are a narrative projection over distances and groupings. They are - deliberately not serialized into `ghost.fleet/v1` frontmatter. -- The current milestone supports `members`, `view`, and `emit skill`. Separate - temporal aggregation, refresh, and interactive browsing remain out of scope - until a real workflow needs them. diff --git a/docs/host-adapters.md b/docs/host-adapters.md deleted file mode 100644 index 32f4ca91..00000000 --- a/docs/host-adapters.md +++ /dev/null @@ -1,139 +0,0 @@ -# Host Adapter Integration - -Ghost is adapter-neutral. It owns the portable fingerprint package, -deterministic validation, stack resolution, and machine-readable packets. Host -tools consume that fingerprint contract and own display, severity mapping, and -review/check file formats. - -## Responsibilities - -Ghost provides: - -- `.ghost/` package loading and stack merging. -- `intent.yml`, `inventory.yml`, and `composition.yml` as generation context. -- Optional `validate.yml`. -- `ghost signals` stdout output for raw repo observations. -- `ghost check --format json` as the stable `ghost.check-report/v1` contract. -- `ghost review --format json` for advisory packets grounded in the resolved - fingerprint stack. -- `ghost relay gather [target] --format json` as the `ghost.relay.gather/v2` - contract for generation context. Host adapters should consume JSON fields - such as `context`, `selected_context`, `source`, `targetPaths`, `stackDirs`, - gaps, and trace data instead of scraping markdown. -- `ghost relay gather --request --format json` and - `ghost relay gather --request-stdin --format json` for prompt-shaped tasks - where the host adapter can provide a structured `ghost.relay-request/v1`. -- Relay configs as the execution contract for context gathering. Omitted - `base` means `base.kind: fingerprint`; explicit `base.kind: none` lets a - framework repo gather declared request context without a `.ghost` package. -- `GHOST_PACKAGE_DIR=` for wrappers that store Ghost package - roots somewhere other than `.ghost`. -- `GHOST_RELAY_CONFIG=` for wrappers that store Relay config - somewhere other than `.ghost/relay.yml`. - -Host adapters provide: - -- repo-specific installation workflows -- policies for when to capture, validate, generate from, govern, or compare a - fingerprint -- generated review/check files in the host's native format -- severity mapping from Ghost's `critical | serious | nit` -- policy for when a finding blocks, comments, or remains advisory -- normal Git review for fingerprint edits - -Raw repo signals are authoring evidence, not canonical inventory. The checked-in -facet files remain the authority. - -## Check Flow - -Run deterministic checks and consume the JSON report: - -```bash -ghost check --base main --format json -``` - -Wrappers should map severity externally. A typical mapping is: - -```text -critical -> blocking -serious -> blocking or high-confidence review finding -nit -> advisory -``` - -The exact labels belong to the host. - -## Custom Fingerprint Directories - -The default package root is `.ghost`. Wrappers can use any safe relative -package root by setting `GHOST_PACKAGE_DIR` on the Ghost process: - -```bash -GHOST_PACKAGE_DIR=.design/memory ghost init --scope apps/checkout -GHOST_PACKAGE_DIR=.design/memory ghost stack apps/checkout/review/page.tsx --format json -GHOST_PACKAGE_DIR=.design/memory ghost relay gather apps/checkout/review/page.tsx --format json -GHOST_PACKAGE_DIR=.design/memory ghost check --base main --format json -GHOST_PACKAGE_DIR=.design/memory ghost review --base main --format json -``` - -`--package ` remains exact single-bundle mode. Use it when the caller -already knows the package root and wants to bypass stack discovery. - -## Relay Context Flow - -Use JSON as the agent contract: - -```bash -ghost relay gather apps/checkout/review/page.tsx --format json -``` - -Relay first loads config by precedence: `--config`, `GHOST_RELAY_CONFIG`, -discovered `.ghost/relay.yml`, then the built-in default config. Source paths -inside a Relay config are resolved from the repo root/current working directory, -not from the config file directory. - -When the user prompt is not naturally tied to one repo path, the host adapter -should create a Relay request from the prompt and pass it to Ghost: - -```yaml -schema: ghost.relay-request/v1 -task: generate-interface -selectors: - customer: subscriber - brand: acme - system: portal - moment: renewal-reminder - medium: email - capability: billing -``` - -```bash -ghost relay gather --request-stdin --format json -``` - -Framework-owned contexts can keep the same command and provide their runtime -config explicitly: - -```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 -``` - -Use `base.kind: none` in that config when there is no base Ghost fingerprint -package. Ghost will return deterministic gaps such as `no-base-fingerprint` -instead of throwing a missing `.ghost/manifest.yml` error. - -The nested `context.schema` value is `ghost.relay-context/v1`. The top-level -`brief` field is display text for humans and compatibility. Plain markdown -output from `ghost relay gather ` is a compact human preview and may -omit projected Relay config sources that are present in JSON. - -Ghost resolves request selectors deterministically against declared Relay -config resolvers. Natural-language extraction belongs to the host adapter, not -Ghost core. - -## Fingerprint Edits - -Adapters do not need a special Ghost draft state. If fingerprint work is -uncommitted or unmerged, it is draft work. Once the split fingerprint package, -checks, decisions, or intent are checked in, Ghost treats them as truth for -deterministic tooling. diff --git a/docs/ideas/README.md b/docs/ideas/README.md index 6f1dd212..53e027ee 100644 --- a/docs/ideas/README.md +++ b/docs/ideas/README.md @@ -3,24 +3,47 @@ This folder is for live, non-authoritative exploration that should not be lost to chat history but is not ready to become public docs or a changeset. -Current public docs live one level up: +The one public doc one level up is `../purposes.md` (one model, many +projections). Older format / loop / adapter / fleet docs were deleted in a +focus pass: they described the pre-redesign Relay-routing and +`topology`/`applies_to` model that `coordinate-space.md` replaces. -- [Portable fingerprint format](../fingerprint-format.md) -- [Generation loop](../generation-loop.md) -- [Host adapter integration](../host-adapters.md) -- [Ghost Fleet](../ghost-fleet.md) - -Retained notes: +## The settled center - `fingerprint-first-architecture.md` records the settled product center: Ghost is fingerprint-first, and drift is one governance workflow over the - portable `.ghost/` package. + portable `.ghost/` package. Everything below is subordinate to it. + +## The reset arc (read in order) + +These notes form one continuous thread from "I overcomplicated this" to a +buildable Layer 2 design. They agree; read them as a sequence. + +- `../purposes.md` — one model, many projections. The artifact never bends to + serve a consumer. +- `ghost-layers.md` — the five layers Ghost actually has (description, map, + selection, governance, comparison), with each piece of code assigned to a + layer and each leak named. +- `contract-and-binding.md` — the portable-contract vs repo-binding split. + (Now mostly subsumed: the split falls out of `coordinate-space.md` for free.) +- `reset.md` — the stop-circling note. Fixes purpose, goals, layers, and + separation of concerns, and schedules a single first move with everything + else parked. +- `coordinate-space.md` — the clean-room design for Layer 2 (the first cut). A + surface is an author-named group with an optional description; topology is a + strict containment tree plus cascade-from-ancestors plus rare explicit + shared-edges; resolution is BYOA (Ghost emits a described menu, the agent + matches); delete list covers `inventory.topology`, smeared `applies_to`, and + `ghost.map/v1`. + +## Independent, still live + - `ghost-ui.md` explores additive registry metadata for the private Ghost UI - reference package. + reference package. Orthogonal to the coordinate redesign. - `guided-migration.md` explores a future host-agent workflow for migrating one - fingerprint toward another. + fingerprint toward another. Layer 5 (comparison); untouched by the redesign. -Conventions: +## Conventions - One file per idea, kebab-case slug. - Add frontmatter with `status: exploring`, `status: deferred`, or @@ -28,5 +51,5 @@ Conventions: - Keep idea notes explicitly subordinate to the current fingerprint package model. - Delete notes that only describe superseded package splits, removed commands, - or dead migration plans after their useful decisions are folded into current - docs. + or dead routing/coordinate models after their useful decisions are folded + into current docs. diff --git a/docs/ideas/contract-and-binding.md b/docs/ideas/contract-and-binding.md new file mode 100644 index 00000000..3ccd6838 --- /dev/null +++ b/docs/ideas/contract-and-binding.md @@ -0,0 +1,210 @@ +--- +status: exploring +--- + +# Contract and binding: the two durable artifacts + +> **Mostly subsumed.** The contract/binding split this note proposes now falls +> out of `coordinate-space.md` for free: designing the coordinate space +> medium-agnostically produces the portable-contract-vs-repo-binding split +> without a separate decision. Keep this note for the *sort* (which piece goes +> where) and the artifact rationale; treat `coordinate-space.md` as the live +> design. + +This note is subordinate to `fingerprint-first-architecture.md` (settled) and a +sibling to `ghost-layers.md` (exploring). It changes neither. The layers note +asks, of each file, *"which operation is this?"* and answers with five layers. +This note asks a different question along a different axis: + +> Of the durable, checked-in thing Ghost produces, **which job is it doing — +> describing a portable surface language, or binding one repo to that +> language?** + +It exists because of a confession, not a refactor: Ghost started as a repo-first +composition guard and was later stretched into a portable, possibly non-UI brand +contract. Both are good. The pain is that **one `.ghost/` artifact was asked to +be both at once.** This note names the seam between those two jobs so every +existing feature gets a home instead of getting cut. + +## The one-line diagnosis + +There are two durable artifacts hiding inside one folder. + +- **The contract** is repo-agnostic. It is Square's surface language: brand + intent, the surfaces it spans (email, web, product, pos, voice), the + coordinate space those surfaces live in, and the patterns that make each feel + intentional. It can be published, versioned, mounted over MCP, and consumed by + many repos — or by no repo at all. It need not be about UI. +- **The binding** is repo-native. It is a thin statement that *this* working + tree is an instance of *that* contract, and that these paths realize these + surfaces. It is where path-first resolution, drift, and checks live, because + those are inherently operations on a working tree. + +Every recent contradiction is this seam showing through. "How does Relay work +from the repo root?" is the seam: the contract answers *from the prompt +coordinate*; the binding answers *from the path*. The question felt +irreconcilable because two artifacts were answering it at once. + +## How this composes with the five layers + +`ghost-layers.md` slices Ghost by **operation** (Description, Map, Selection, +Governance, Comparison). This note slices the same code by **durable artifact**. +They are orthogonal axes over the same files, and they agree: + +| Layer (operation) | Contract (portable) | Binding (repo) | +| --- | --- | --- | +| 1 Description | Owns it. Brand intent/inventory/composition, scoped by surface. | References it. Adds none. | +| 2 Map | Owns it. The coordinate space *is* the contract's spine. | Maps paths → coordinates. | +| 3 Selection | Coordinate → contract slice (prompt-first). | Path → coordinate → slice (path-first). | +| 4 Governance | Declares checks against surfaces. | Runs checks/drift against a real diff. | +| 5 Comparison | The unit compared. | Not involved. | + +The crucial agreement: **Leak A in the layers note (the map trapped inside the +description) is the contract's spine that the binding has been impersonating with +filesystem paths.** Extracting the map (their highest-leverage cut) and splitting +contract from binding (this note's cut) are the same surgery seen from two +angles. Do one and the other falls out. + +## The shape, made concrete + +Contract — portable, lives anywhere, knows nothing about git: + +```text +square-brand/ # an npm package, an MCP resource, a folder + manifest.yml # ghost.contract/v1 (proposed) + map.yml # Layer 2: dimensions + surfaces (the spine) + core/ # true everywhere: brand intent, tokens, voice + intent.yml + inventory.yml + composition.yml + validate.yml + surfaces/ + email/lifecycle/ + intent.yml + inventory.yml + composition.yml + validate.yml + web/public/ ... + product/dashboard/ ... +``` + +Binding — repo-native, tiny, points rather than redefines: + +```text +apps/email-svc/.ghost.bind.yml +``` + +```yaml +schema: ghost.binding/v1 +contract: square-brand # path, npm name, or MCP id +surface: email/lifecycle # a coordinate into the contract map +paths: [apps/email-svc/src] +``` + +A monorepo opened at the root has several bindings, each naming a different +surface of the same contract. Resolution unifies: + +```text +prompt → host extracts coordinate → contract slice (no path needed) +path → binding → coordinate → same contract slice (path is evidence) +``` + +Both roads arrive at one coordinate. The contract returns `core + that surface` +and nothing else. When the coordinate is unknown, the contract returns the +**surface menu**, never the whole tree — which is the structural cure for the +brand-mixing global fallback. + +## The sort: every current piece gets a home + +The point of the exercise. **Contract** = belongs to the portable artifact. +**Binding** = belongs to the repo instance. **Kill** = remove; it serves neither +cleanly and survives only as legacy or as a duplicate of something better placed. + +| Piece | Home | Note | +| --- | --- | --- | +| `intent` / `inventory.building_blocks` / `composition` | **Contract** | Layer 1 core. Re-scoped from one flat bag to per-surface. Survives intact. | +| `inventory.topology` (scopes, surface_types) | **Contract** (as the map) | Leak A. Becomes `map.yml`, the contract spine — not a property of inventory. | +| `applies_to` smeared across nodes | **Contract** (resolved by map) | Leak A. A node's surface is its location in the tree, not a repeated tag. | +| `intent.situations` | **Contract** | Half-built coordinates (moment + surface_type). Folded into the map / surface nodes. | +| `validate.yml` checks (the *declaration*) | **Contract** | Surfaces declare their obligations. | +| `ghost check` / `review` / drift run against a diff | **Binding** | Inherently needs a working tree. Path-first. Stays repo-side. | +| `ack` / `track` / `diverge` (stance in `.ghost-sync.json`) | **Binding** | Stance is a repo-local relationship to a contract version. | +| Path → coordinate mapping (new `.ghost.bind.yml`) | **Binding** | The thin pointer. Replaces topology-as-path-matcher. | +| `relay gather` selection engine | **Both, unified** | One resolver: prompt→coordinate and path→binding→coordinate meet here. | +| `relay-config` `sources` / `request_resolvers` / stack resolvers | **Kill** | The *second* routing system. Collapses into map + binding. This is the core duplication. | +| `inventory.topology.scopes` as a runtime path-matcher | **Kill** | Path matching moves to the binding; the map keeps only the vocabulary. | +| `global fallback` (silent whole-graph dump) | **Kill** | Replaced by the explicit surface menu + "ask which surface." | +| `CAPS` truncation | **Kill** | Leak D. With a real map, the surface region is the budget. | +| nesting merge as ownership (`child-wins-by-id`) | **Binding** (as sugar) | Leak E. Demote to authoring convenience; ownership is git/CODEOWNERS. | +| `survey` / `ghost.survey/v1` | **Kill** | Legacy long tail. No home in either artifact. | +| `map.md` / `resources.yml` / `patterns.yml` / direct `fingerprint.md` | **Kill** | Migration museum. One canonical shape, no legacy formats. | +| `ghost diff` / `ghost describe` | **Kill** | Serve the dead direct-markdown path. | +| `signals` | **Binding** | Repo reconnaissance for authoring. Inherently working-tree-bound. | +| `compare` / `embedding/*` / `ghost-fleet` | **Contract** (consumer) | Layer 5. Compares contracts. Already clean; hold the line. | +| `ghost-ui` registry + MCP | **Contract** (delivery) | A way to ship a contract as a consumable resource. | + +If this sort feels right, the relief is real: **nothing built was wasted.** Most +pieces move or get re-scoped; the Kill column is legacy and duplication, not +capability. + +## What this buys (the relief, stated plainly) + +- The portable brand bundle has no idea git exists. Shippable, reusable across + many repos, works in the no-source-tree / MCP case. This is Job B, finally + freed from Job A's working-tree assumptions. +- The binding is tiny — it points, it does not redefine. The monorepo-root case + stops being a contradiction: many bindings, one contract, one coordinate space. +- "Non-UI composition" stops being scary scope creep. It is just a surface in the + contract that no binding maps to UI paths. The contract is allowed to describe + things no repo consumes. +- Net complexity goes **down**: one clarifying split (contract vs binding) + replaces three colliding concepts (topology vs situations vs relay resolvers). + +## The honest cost + +- One new idea — `manifest` gains "contract vs binding," and the skill must teach + *"are you authoring the brand, or binding a repo to it?"* That is one more + concept than today, but it replaces three that fight. +- Authoring asks "is this brand-universal or surface-specific?" That cost is real + — but it is the exact decision that prevents brand mixing, so it is a feature + with a price, not pure overhead. +- The contract↔binding reference (by path, npm, or MCP id) needs a resolution + contract. That is genuinely new surface area and the first thing to prototype. + +## The forks worth arguing before any code + +1. **Does the binding live in `.ghost.bind.yml`, or stays the contract embeddable + in-repo for the common single-repo case?** Many repos *are* their own + contract. The split must not tax the simple case: a lone repo should be able + to inline its contract and skip the binding entirely. +2. **Partial cross-cuts.** Email+web-but-not-product guidance does not fit a + strict surface tree. Medium-level intermediate surfaces absorb most of it; + genuinely diagonal sharing forces the tree toward a DAG. Pressure-test before + committing. +3. **Who owns the coordinate vocabulary** — `map.yml` as source of truth, or does + it derive from the surface tree itself? Lean: the tree *is* the vocabulary; + `map.yml` only adds aliases and descriptions. One source of truth (this is the + layers note's Leak C resolved). +4. **Versioning the reference.** A binding pins a contract version; `ack`/`track` + already model stance toward a moving reference. Does binding reuse that + machinery or get its own? + +## Not a plan + +This note assigns the two artifacts and sorts the pieces. It schedules no moves, +changes no schema, and renames no command today. Concrete extraction — the +contract manifest, `map.yml`, the binding schema, the unified resolver — should +each be proposed in its own note and linked back here for the artifact rationale, +exactly as `ghost-layers.md` asks for its layer rationale. + +Contracts to keep stable while sorting: `ghost.fingerprint/v1`, +`ghost.validate/v1`, `ghost.fingerprint-package/v1`, `ghost.relay-config/v1`, +`ghost.relay-request/v1`, `ghost.relay.gather/v2`, `ghost.check-report/v1`. + +## Read-back + +This note is successful if it converts a feeling into a list. You are not +serving too many purposes; you are serving **two** purposes with **one** +artifact. Name the two artifacts, sort each piece into Contract / Binding / Kill, +and the bundled mess becomes a portable contract plus a thin repo binding — with +nothing you built thrown away, only sorted. diff --git a/docs/ideas/coordinate-space.md b/docs/ideas/coordinate-space.md new file mode 100644 index 00000000..4a410086 --- /dev/null +++ b/docs/ideas/coordinate-space.md @@ -0,0 +1,275 @@ +--- +status: exploring +--- + +# The coordinate space (Layer 2), designed clean + +This note is subordinate to `fingerprint-first-architecture.md` (settled) and is +the first cut named by `reset.md`. It supersedes the Layer 2 framing in +`ghost-layers.md` and the "map" framing in `contract-and-binding.md`: both +correctly located the leak; neither had the design. This note has the design. + +It was written **clean-room**. The shape below was derived from Ghost's purpose, +the five layers, and a working session about real outcomes — deliberately +*without* reading the two existing coordinate implementations (`ghost.map/v1` +and `inventory.topology`). Those are read only in the final section, to confirm +what gets deleted. Nothing here is back-formed from what exists. + +## What stays constant + +This redesign touches **one layer only**. Held fixed: + +- **Layer 1 (Description):** intent / inventory / composition. Their *content* + does not change. (Their coordinate *annotations* do — see below.) +- **Layer 4 (Governance):** checks, drift, `ack` / `track` / `diverge`. +- **Layer 5 (Comparison):** compare, fleet, embeddings. + +The four-facet artifact and the projection rule from `purposes.md` are +untouched. This is a greenfield of the coordinate space, not the project. + +## The one-line definition + +> A **surface** is an author-named group, with an optional description, that +> holds a slice of the fingerprint and may contain sub-surfaces. + +That is the entire concept. Ghost ships the *mechanism* for authored groups. It +does **not** ship a taxonomy. `email`, `flyer`, `menu`, `settings`, `checkout`, +`modules/billing` are all author data, never Ghost vocabulary. The system has no +opinion about what surfaces exist — only that each is named, optionally +described, nestable, and the home of some rules and context. + +This is the point-1 fix from the reset session: the coordinate space is not +back-formed from tags the description happened to need. It is its own thing — +a vocabulary of *groups*, owned by authors, that the description is *placed +into*. + +## The four outcomes this must serve + +The design is validated against four real outcomes, not abstractions: + +1. **In-repo UI work (existing or new).** A builder prompts an agent to work on + UI. Ghost supplies the right slice before the agent builds. Result beats no + Ghost. +2. **Non-visual builder → PR gate.** A builder ships a feature; Ghost runs PR + checks as a governance gate against the right slice. +3. **Customer brand generation (no repo).** A customer prompts a product to make + a flyer / menu / sticker / email for their brand. Ghost — already compiled + for that brand — drives the generation context and self-heals. *No path, no + diff, no repo exists.* +4. **Portable brand package.** Internal teams maintain one brand fingerprint + centrally; it cascades to all systems; everyone edits one package so nothing + diverges. + +The unifying observation: **all four ask the same question — "the right slice, +at the right time."** They differ only in *how the slice is named*: a path, a +prompt, an explicit surface, a package id. That difference is **medium, not +model.** Outcome 3 is the forcing function: it has the least medium (no path, no +repo), so the coordinate space must be designed from *it* and treat path / diff +/ prompt as conveniences layered on top. No medium is privileged. + +## The topology: strict tree + cascade + rare explicit edges + +Accepted in the design session. Stated precisely, in graph terms: + +**Containment is a strict tree.** Every node — every principle, exemplar, +pattern, contract, check — has exactly **one home surface**. One parent. The +path is the identity. Storage, ownership, and the menu are all this one tree. +Trees lay out deterministically and read at a glance; that legibility *is* the +predictability goal (`reset.md` goal #4). + +**Sharing is resolved by altitude, not by multi-parent.** When a rule applies to +several surfaces, it is placed at the **lowest common ancestor** and **cascades +down**. The brand-wide color rule lives in `core` and flows everywhere. An +email-wide voice lives in `email` and flows to `email/marketing` and +`email/reminder`. Cascade replaces the DAG for the common case. Most "diagonal" +sharing is really "lives higher up." + +**The genuinely diagonal case gets a rare, explicit, visible edge.** "Applies to +email + web but not product" has no common ancestor short of root. That — and +only that — uses an explicit authored `shares` edge that the menu *shows* and a +human *writes on purpose*. It is never a smeared tag and never a silent second +parent. In diagram terms: a tidy tree with a small, countable set of labeled +overlay edges — never a force-directed hairball. + +This is the org-chart-plus-dotted-lines pattern: the tree carries the weight, +explicit edges handle the few exceptions, and one mechanism is never asked to do +both jobs. It gives DAG-level expressiveness for the common case (through +altitude) while keeping tree-level legibility. + +### Why this kills the old leak + +`applies_to` smeared across nodes (Leak A / Leak E) was exactly an implicit DAG: +every node carrying `applies_to: [a, b]` is a node with two parents. Replacing it +with **placement + cascade + rare explicit edge** is the death of that leak. +Inheritance returns, but disciplined: *down the containment tree only.* No +mixins, no priority weights, no union-merge-by-id — just "ancestors contribute to +descendants," the most predictable inheritance there is. This satisfies +`reset.md` goal #4 and the "model does not bend" rule in `purposes.md`. + +## Grouping is placement, not tags + +The point-1 coupling fix, made concrete: + +> A node's surface is **where it is stored**, not a property it carries. You +> *place* an exemplar into `email/marketing`. You do not stamp +> `surface_type: email` onto it. + +This is how Layer 1 content stays constant while its *coordinate annotations* are +removed. The grouping moves from smeared per-node fields +(`applies_to` / `surface_type` / `scope`) to **storage location in the surface +tree** plus an authored surface manifest. The description stops influencing the +coordinate space, because the description no longer carries coordinates at all. + +## The description is the keystone + +A surface carries an **optional description** authored in natural language: +*"a module is a self-contained sub-product; billing and payouts are modules."* + +This single field is what lets the system stay taxonomy-free and still resolve +fluid, author-invented vocabulary. The reasoning: + +- `email` resolves on its name alone — self-evident. +- `modules` is meaningless to a matching agent until an author *describes* it. +- The description is the bridge between author vocabulary and natural-language + asks. + +Descriptions are **optional but agent-draftable**. An agent can draft a +surface's description *from the content already grouped under it*; a human +approves it via git (git stays the approval boundary). The authoring burden is +"review a draft," not "write from scratch." Present a description when resolution +would otherwise be ambiguous; skip it when the name is self-evident. + +## Resolution is BYOA: Ghost emits a menu, the agent matches + +The resolution model, medium-agnostic: + +``` +any evidence ──> agent matches against the described menu ──> Ghost returns +(path|prompt| core + that + explicit|pkg-id) surface's slice +``` + +Division of labor (this is the BYOA boundary from +`fingerprint-first-architecture.md`): + +- **Ghost (deterministic, no LLM):** stores the surface tree; on request emits + the **menu** — surfaces with their descriptions and shapes. Once a coordinate + is chosen, deterministically returns `core + that surface's slice` (the slice = + the surface's own nodes + everything cascaded from its ancestors + any explicit + shared edges). Ghost does zero NLP. +- **Host agent (inference):** already holds the prompt. Reads the described menu, + picks the surface. Path / diff / explicit name / package id are *additional + evidence* it may use, never requirements. + +**Ambiguity returns the menu, never the whole tree.** When evidence does not +resolve to a surface, Ghost returns the **surface menu** and asks which one — +it never dumps the whole fingerprint. This is the structural cure for the +global-fallback brand-mixing failure: in outcome 3, mixing a customer's flyer +voice into their email is *the* failure mode, and a menu-instead-of-dump makes it +impossible. (`purposes.md` leaks #1 and #2, resolved.) + +## How the four outcomes resolve + +| Outcome | Evidence the agent uses | Resolves to | +| --- | --- | --- | +| 1 In-repo UI, existing file | path → surface | that surface's slice, before building | +| 1 In-repo UI, new work | prompt → surface (or menu) | chosen surface's slice | +| 2 PR gate | diff paths → surface(s) | checks for those surfaces, against the diff | +| 3 Customer flyer (no repo) | prompt → surface | `core + flyer`, never `email` | +| 4 Portable brand | package id → tree | the whole tree as a consumable resource | + +One model. The medium is just an adapter on the front. **This is also the +contract/binding split (`contract-and-binding.md`) falling out for free:** the +surface tree + the description it holds *is* the portable contract (outcomes 3 & +4, no repo needed); path→surface and diff→surface are the repo conveniences +(outcomes 1 & 2). We did not have to decide contract-vs-binding to design the +coordinate — designing it medium-agnostically produced the split, exactly as the +layers note predicted. + +## What a surface needs (the shape, in prose) + +Stated as obligations, not a schema (schema is a follow-on note): + +- **id / name** — the author's chosen label, slug-shaped. +- **description** — optional, natural language, agent-draftable. Present when the + name is not self-evident. +- **parent** — at most one (strict tree). Absent = top-level under `core`. +- **slice** — the nodes placed in this surface. Placement, not tags. +- **shares** — optional, rare, explicit edges to other surfaces for the + irreducibly-diagonal case. Menu-visible. + +Resolution against a surface yields: its own slice + cascaded ancestor slices + +shared-edge contributions. `core` is the root every surface inherits. + +## What each layer asks of the coordinate space + +Confirming the design serves all consumers (the layer rule, `reset.md`): + +- **Selection (3):** "evidence → which surface → its slice." Served by the menu + + deterministic slice resolution. No NLP in Ghost. +- **Governance (4):** "this diff touches which surfaces → run their checks." + Served by path→surface mapping over the tree. +- **Comparison (5):** "compare these surfaces / whole trees." Served by the tree + being a clean, portable structure. + +A new purpose still gets a new layer, never a new field on intent / inventory / +composition. + +## The delete list + +Only now, after the clean design exists, do we name what it replaces. These are +the back-formed coordinate systems the design above supersedes. (Read at this +point, not before, to avoid anchoring.) + +| Dead thing | Why it dies | Replaced by | +| --- | --- | --- | +| `inventory.topology` (scopes, surface_types) inside the fingerprint | Coordinate space trapped in the description (Leak A) | The surface tree, a Layer 2 artifact | +| `applies_to` on principles / contracts / patterns | Smeared tags = implicit DAG (Leak A/E) | Placement + cascade from ancestors | +| `surface_type` / `scope` on exemplars and situations | Same: nodes self-tagging coordinates | Placement (storage location) | +| `ghost.map/v1` / `map.md` (`ghost-core/map/`) | A *prior, richer* coordinate attempt, but repo-structure-shaped and medium-coupled (path/build-system/render-strategy baked in) | The medium-agnostic surface tree | +| `child-wins-by-id` union merge as ownership (Leak E) | Ownership is git/CODEOWNERS; merge did a governance job | Cascade down the containment tree | +| `global-fallback` whole-tree dump | Brand-mixing failure | Menu-instead-of-dump on ambiguity | + +The crucial honesty for the sadness that started all this: **the description core +is untouched, and the two dead coordinate systems are being unified into one +clean thing — not thrown away into a void.** This is a teardown of one layer +inside a frame that protects the three layers that work. Greenfield where it's +earned; foundation kept where it's solid. + +## Decisions locked in this note + +1. A surface = author-named group + optional description + sub-surfaces. Ghost + ships the mechanism, never a taxonomy. +2. Grouping is by placement (storage location), not tags. Layer 1 content stays + constant; its coordinate annotations are removed. +3. Topology = strict containment tree + cascade-from-ancestors + rare explicit + shared-edges. No silent multi-parent. +4. Resolution is BYOA: Ghost emits a described menu deterministically; the agent + matches; Ghost returns `core + surface slice`. Ghost does no NLP. +5. Ambiguity returns the menu, never the whole tree. (Brand-mixing cure.) +6. Descriptions are optional but agent-draftable, human-approved via git. +7. Medium-agnostic: path / prompt / explicit name / package id are evidence, not + privileged selectors. Designed from the no-repo case (outcome 3). + +## Not a plan + +This note designs the coordinate space and names what it replaces. It schedules +no moves, writes no schema, renames no command. Concrete extraction — the surface +schema, the slice resolver, the menu emitter, the migration off `topology` / +`applies_to` / `map.md` — should each be proposed in its own note and linked back +here for the design rationale, exactly as `reset.md` asks. + +Contracts to keep stable while this is explored: `ghost.fingerprint/v1`, +`ghost.validate/v1`, `ghost.fingerprint-package/v1`, `ghost.check-report/v1`. + +## Read-back + +This note succeeds if: + +- The coordinate space is defined without a taxonomy and without privileging any + medium. +- All four outcomes resolve through one model. +- The point-1 coupling is fixed: the description no longer carries coordinates. +- The topology is a clean tree a person can hold in their head, with cascade for + the common overlap and a handful of visible edges for the rare diagonal. +- Nothing in Layer 1, 4, or 5 had to change to make Layer 2 right. diff --git a/docs/ideas/ghost-layers.md b/docs/ideas/ghost-layers.md new file mode 100644 index 00000000..51ae6dde --- /dev/null +++ b/docs/ideas/ghost-layers.md @@ -0,0 +1,136 @@ +--- +status: exploring +--- + +# Ghost layers: a triage map + +This note is subordinate to `fingerprint-first-architecture.md` (settled). It +does not change that decision. It applies it. The settled memo says the +fingerprint is the durable *descriptive* artifact and everything else is a tool +that consumes, validates, governs, or compares it. This note takes that one +sentence and asks, file by file, **which layer is this, and is it where it +belongs?** + +It is a triage map, not a rewrite plan. The goal is to make the size of the +thing feel smaller by giving every piece a home. + +## The one-line diagnosis + +The descriptive center is clean. The operational rings leaked into its shape. + +Intent / inventory / composition is genuinely *one surface seen through three +angles*. It survived every refactor (`bd1ced5`, `f393720`, `7ecd13c`) because it +is coherent. The pain is not there. The pain is that **selection, routing, +merge, governance, and comparison** are operations *on* the fingerprint that +pressed back *into* its structure, because the fingerprint was the only durable +thing to hang them on. + +That is a hopeful diagnosis. The rot is in a ring, not the core. + +## The five layers + +| Layer | Name | One line | Owns | +| --- | --- | --- | --- | +| 1 | **Description** | What the surface is. | intent, inventory, composition | +| 2 | **Map** | The coordinate space the surface lives in. | dimensions, scopes, surface types | +| 3 | **Selection** | Path or prompt to a narrow view of 1. | relay gather, routing, request resolution | +| 4 | **Governance** | Whether a change stays faithful, and who owns what. | checks, drift, ownership | +| 5 | **Comparison** | Read-only analytics across many fingerprints. | distance, cohorts, fleet | + +The discipline rule that comes from this map: + +> A new purpose gets a new layer, never a new field on intent / inventory / +> composition. + +Most of the recent agony was the question "does this go in the fingerprint?" +having no answer. With the layers named, the question becomes "which layer is +this?" — and that almost always has an obvious answer. + +## Triage: current code to layer + +| Code | Layer | State | +| --- | --- | --- | +| `ghost-core/fingerprint/{schema,types,lint}.ts` (intent, composition, inventory minus topology) | 1 | **Clean.** The center. Leave it alone. | +| `inventory.topology` (scopes, surface_types) | 2 trapped in 1 | **Leak A.** A coordinate space living inside a description file. | +| `applies_to` on principles / contracts / patterns / exemplars / situations / checks | 2 trapped in 1 | **Leak A.** The same coordinate space smeared across every node. | +| `context/graph.ts` (`Applicability`, `buildScopes`, `matchScopes`, `pathsOverlap`) | 2 | Map logic, but reconstructed at runtime from the smeared fields above. | +| `fingerprint/lint.ts` (`checkTopologyRefs`, `fingerprint-surface-type-unknown`) | 2 | Already enforces the map vocabulary. This is the source of truth to protect. | +| `context/entrypoint.ts` (`buildContextEntrypoint`, `CAPS`, `relevanceScore`) | 3 | **Leak D.** `CAPS` is a truncation crutch from having no real map to narrow with. | +| `context/selection-reasons.ts` (`directSelectionReasons`, `expandOneHopWithReasons`, `globalFallbackRefs`) | 3 | Selection. Correct layer. One-hop expansion is not exclude-aware yet. | +| `context/selected-context.ts` (`SelectedContextGap`, hits, omissions) | 3 | Selection output contract. Correct layer. | +| `relay.ts`, `relay-command.ts`, `relay-runtime-helpers.ts` | 3 | Selection runtime. Correct layer. | +| `context/request-resolution.ts`, `relay-request.ts`, `request-stack-document.ts` | 3 | Prompt to view. Correct layer. Has its own selector matcher (see Leak C). | +| `context/relay-config*.ts`, `default-relay-config.ts`, `projection.ts`, `relay-context.ts`, `relay-modes.ts` | 3 | Selection plumbing. Correct layer. | +| proposed `relay.yml` / `routes` facet | 3 | **Leak B.** Wants a filename and a name Layer 3 already used. | +| `ghost-core/checks/{schema,lint,routing,types}.ts`, `validate.yml` | 4 | Governance gates. Correct layer. | +| check / review / ack / track / diverge commands | 4 | Drift governance. Correct layer. | +| `scan/fingerprint-stack.ts` (`mergeFingerprints`, `mergeById`, `mergeStrings`, `child-wins-by-id`) | 4 wearing a 1 costume | **Leak E.** A merge algorithm doing an ownership job. | +| `ghost-core/embedding/*`, `compare` command, `packages/ghost-fleet` | 5 | **Clean consumer.** Reads description, never writes back. Hold this line. | + +## The named leaks + +**Leak A — the map is trapped inside the description.** `inventory.topology` +plus every node's `applies_to` is a coordinate system masquerading as a property +of the surface. It is Layer 2 living inside Layer 1. This is the highest-leverage +cut because Layers 3, 4, and 5 all query it: fix it once, three consumers get +cleaner. Extracting an explicit surface map is the one structural change worth +making first. + +**Leak B — `relay.yml` collision.** `relay-config-loader.ts` already discovers +`.ghost/relay.yml` and hard-throws unless it validates as +`ghost.relay-config/v1`. The proposed routing facet wants the same path. Two +Layer 3 things fighting for one filename, and "Relay" already names the +subsystem. Resolution: name the facet for what it does (the map / routing), not +"relay." + +**Leak C — duplicate vocabulary.** A new `selectors` block would re-declare +`surface_type`, which `inventory.topology.surface_types` already owns with +*error*-level lint enforcement. Two sources of truth for one Layer 2 concept. +Resolution: the map owns the vocabulary; selection references it. Only genuinely +new axes (e.g. `medium`) are new. + +**Leak D — `CAPS`.** Hardcoded truncation (`intent: 6, composition: 6, ...`) in +selection is a crutch from when there was no good map to narrow with. With a real +Layer 2, the region is the budget. Keep caps only on the global-fallback path. + +**Leak E — nesting as ownership.** `mergeFingerprints` (union-by-id, child-wins) +performs a governance/ownership job with a data-model mechanism. It couples +failure domains across teams (a root edit can break a leaf's gather), allows +silent overrides, and supports union-only inheritance. Ownership is a git / +CODEOWNERS concern. Resolution: demote nesting to authoring sugar; let +governance live in Layer 4. + +## What is already clean (the reassurance) + +- **Layer 1** is coherent and battle-tested. You feel no pain here, and that is + the signal that the model is right. +- **Layer 5** (compare, fleet, embeddings) is already a well-behaved consumer. +- **Layer 4 checks/drift** is correctly separated; only nesting leaks. + +You did not lose the plot. The code drifted from a doc you already ratified. The +fix is alignment, not reinvention. + +## Why this happened (and why it is fine) + +You could not have drawn these boundaries up front. You had to build the bundled +version to discover where it wanted to split. Every collision in this exploration +— the `relay.yml` clash, the double vocabulary, the union merge — was not bad +design surfacing. It was a seam becoming visible enough to cut along. The mess is +the map you could not have drawn before you made it. + +## Not a plan + +This note assigns homes; it does not schedule moves. Any actual extraction +(surface map, routing facet name, nesting demotion) should be proposed in its own +note and linked back here for the layer rationale. Nothing here changes a schema, +a command, or a contract today. + +Contracts that exist and should be kept stable while triaging: `ghost.fingerprint/v1`, +`ghost.validate/v1`, `ghost.fingerprint-package/v1`, `ghost.relay-config/v1`, +`ghost.relay-request/v1`, `ghost.relay.gather/v2`, `ghost.check-report/v1`. + +## Read-back + +This note is successful if a contributor can take any file in +`packages/ghost/src` and answer two questions without guessing: which layer does +this serve, and is it currently living in that layer or leaking into another. diff --git a/docs/ideas/reset.md b/docs/ideas/reset.md new file mode 100644 index 00000000..04616369 --- /dev/null +++ b/docs/ideas/reset.md @@ -0,0 +1,178 @@ +--- +status: exploring +--- + +# Reset: how to approach Ghost + +This note is subordinate to `fingerprint-first-architecture.md` (settled). It +changes no decision in that memo. It exists for a different reason than the +others in this folder: not to explore a new idea, but to stop circling. + +Three notes — `purposes.md`, `ghost-layers.md`, and `contract-and-binding.md` — +were written from a real and honest discouragement: that Ghost had been +overcomplicated, that it tried to do too much, that the plot was lost. This note +takes those three seriously, reads what they actually concluded, and turns the +feeling into a single approach with a defined purpose, fixed goals, named +layers, and a clean separation of concerns. + +The short version: **you are not lost, and nothing you built was wasted.** The +three notes are not three problems. They are one diagnosis written three times, +and they agree on the first move. This note names that move and the discipline +that keeps it from being undone. + +## The diagnosis the three notes already share + +Read together, the circling notes say one thing: + +- **The descriptive core is right.** Intent / inventory / composition is *one + surface seen through three angles*. It survived every refactor because it is + coherent. `ghost-layers.md` calls this the clean center. `purposes.md` calls + it the model that does not bend. `contract-and-binding.md` calls it the part + that "survives intact." +- **The pain is leakage, not scope.** Selection, routing, merge, and governance + are *operations on* the fingerprint that pressed *into* its shape, because the + fingerprint was the only durable thing to hang them on. `ghost-layers.md` + names this Leak A. `contract-and-binding.md` names it "the map the binding + impersonates with filesystem paths." `purposes.md` names it "merge to + assemble, select to deliver." +- **They converge on one cut.** Extract the coordinate space — `topology` plus + the smeared `applies_to` — out of the description and give it its own home. + The layers note calls this "the one structural change worth making first." The + contract note says doing it *is* the contract/binding split seen from another + angle: "do one and the other falls out." + +That is the whole reset. The mess felt like five colliding concepts. It is one +seam, visible now because the bundled version was built first. The disorientation +is the ordinary feeling of standing right after the hard part, where a mess +turns back into a map. + +## Purpose (one sentence, does not move) + +> 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 — +> as a portable, checked-in contract that humans approve and agents act from. + +This is the `fingerprint-first-architecture.md` sentence, unchanged. Everything +below serves it. If a proposed feature does not serve this sentence, it is not a +Ghost feature; it is a projection, a tool, or scope creep. + +## Goals (what "right" means) + +1. **One durable artifact.** The checked-in fingerprint is the source of truth. + Every consumer reads it through a projection; no consumer changes its shape + or its merge semantics to suit itself. (`purposes.md`, the rule.) +2. **A clean descriptive core.** Intent / inventory / composition stays exactly + what it is. New purposes never become new fields on it. (`ghost-layers.md`, + the discipline rule.) +3. **Operations live in their own layer.** Selection, governance, and comparison + are rings around the core, not properties of it. +4. **The model is dumb on purpose.** Nesting is storage and ownership; + routing plus filtering is selection. No mixins, no priority weights, no + write access to the merge. Predictability is the feature. +5. **Portability is allowed.** The artifact may describe surfaces no repo + consumes (non-UI, multi-surface brand). That is not scope creep; it is the + contract being legitimately bigger than any one binding. + +## The layers (the separation of concerns) + +This adopts the five layers from `ghost-layers.md` verbatim, because they are +already correct. The reset is to *commit* to them as the answer to every "does +this go in the fingerprint?" question. + +| Layer | Name | One line | Owns | +| --- | --- | --- | --- | +| 1 | **Description** | What the surface is. | intent, inventory, composition | +| 2 | **Map** | The coordinate space the surface lives in. | dimensions, scopes, surface types | +| 3 | **Selection** | Path or prompt to a narrow view of layer 1. | relay gather, routing, request resolution | +| 4 | **Governance** | Whether a change stays faithful, and who owns what. | checks, drift, ownership | +| 5 | **Comparison** | Read-only analytics across many fingerprints. | distance, cohorts, fleet | + +The two rules that make these layers load-bearing instead of decorative: + +> **The layer rule.** A new purpose gets a new layer, never a new field on +> intent / inventory / composition. + +> **The projection rule.** A consumer may read through any projection it likes. +> It may not change the *shape* of the fingerprint or its *merge semantics* to +> suit itself. If serving a purpose requires bending the shape, that is a leak +> to fix at the boundary, not a redesign of the artifact. + +Layers 1, 4 (checks/drift), and 5 are already clean per the triage. The work is +entirely in extracting Layer 2 and letting Layer 3 stop improvising around its +absence. + +## The first cut (the only thing this note schedules) + +Everything above is stance. This is the move. Do exactly one thing first: + +> **Extract the map.** Lift the coordinate space — `inventory.topology` plus the +> `applies_to` smeared across nodes — out of the description and make it an +> explicit Layer 2 artifact that selection, governance, and comparison query. + +Why this one, before contract/binding, before any kill list, before renames: + +- It is the leak all three notes independently point at (Leak A / the spine / + the impersonated map). +- It is the highest leverage: Layers 3, 4, and 5 all query the coordinate space, + so fixing it once makes three consumers cleaner. +- It unblocks the bigger question without deciding it early. Once the map is its own + thing, the contract/binding split *falls out* of it rather than being a second + independent surgery. You can decide that question later, from a cleaner base. + +What this first cut explicitly does **not** require: + +- No decision on contract vs binding yet. +- No renames of commands or schemas. +- No removal of `survey`, `diff`, `describe`, or any legacy format yet. +- No new public interface. + +The map extraction gets its own proposal note with concrete schema, linked back +here for rationale. This note only fixes which cut is first and why. + +## What stays parked (so the circling stops) + +These are real and worth doing, but they are *downstream of the first cut* and +must not be started in parallel, because doing them now is what re-creates the +overwhelm: + +- **Contract vs binding** (`contract-and-binding.md`). Revisit *after* the map + exists; the split becomes mostly mechanical once the coordinate space is + extracted. +- **Kill lists** (legacy formats, `survey`, direct-markdown commands). These are + cleanup, not architecture. They do not block the core and can happen anytime. +- **Routing facet naming** (Leak B), **duplicate vocabulary** (Leak C), **CAPS** + (Leak D), **nesting-as-ownership** (Leak E). All resolve more obviously once + Layer 2 is real. Each gets its own note when its turn comes. + +Parking these is not deferral-as-avoidance. It is the direct remedy for the +specific feeling that started this: too many open fronts at once. There is one +front now. + +## How to hold the line (discipline going forward) + +When any future change is proposed, answer two questions before writing code: + +1. **Which layer is this?** If the honest answer is "it adds a field to intent / + inventory / composition," stop — it is almost certainly a leak from another + layer. +2. **Is this the model bending, or a projection reading?** If a consumer needs + the shape or merge to change, fix the boundary, not the artifact. + +If both questions have clean answers, the change is safe. If they don't, the +change is the next leak — write it down, don't build it yet. + +## Read-back + +This note succeeds if it replaces a feeling with a footing: + +- The purpose is one sentence and it has not changed since the settled memo. +- "Did I overcomplicate it?" has an answer: no — five operations leaked into one + file, and they are *sortable*, not wrong. +- "What do I do next?" has exactly one answer: extract the map. One cut, the one + all three notes already agree on. +- Everything else has a home (a layer) or a queue (parked), so nothing has to be + held in the head at once. + +You did not lose the plot. The code drifted from a doc you already ratified, and +three notes you already wrote found the seam. The reset is to believe them, make +the one cut, and let the rest fall out. diff --git a/docs/language-fingerprints.md b/docs/language-fingerprints.md deleted file mode 100644 index 026c1f30..00000000 --- a/docs/language-fingerprints.md +++ /dev/null @@ -1,186 +0,0 @@ -# Language In The Fingerprint - -Voice and language are part of a product surface. Copy drifts the same way -layout drifts: generated notifications, error messages, empty states, and -button labels slowly stop sounding like one product. - -Ghost does not need a new domain, schema, or dimension set to capture this. -Language flows through the same facets as every other -surface-composition concern: - -- `intent.yml` carries voice intent. -- `inventory.yml` points at copy material and external writing - standards. -- `composition.yml` carries copy patterns. -- `validate.yml` carries the deterministic subset. - -This document shows the mapping. Nothing here changes the -`ghost.fingerprint/v1` schema. - -## Voice Intent Lives In Intent - -`intent.summary.tone` already exists for tone words. Voice rules that need -rationale become principles. Surfaces with non-negotiable wording become -experience contracts. - -```yaml -# intent.yml -summary: - tone: - - plain - - direct - - warm -principles: - - id: action-first-copy - principle: Copy leads with what the reader can do next, not with what the system did. - applies_to: - paths: - - src/i18n/** - - src/components/** - surface_types: - - error-state - - empty-state - guidance: - - Prefer "Try again" over "The operation failed." - - Keep apology framing out of routine errors. - evidence: - - path: src/components/error-banner.tsx - note: Existing errors already lead with the recovery action. -experience_contracts: - - id: exact-wording-surfaces - contract: Designated surfaces (legal text, consent prompts, destructive confirmations) use approved exact wording and are never paraphrased. - applies_to: - surface_types: - - legal-text - - consent-prompt - - destructive-confirmation - obligations: - - Treat the approved string as the source of truth, not a style suggestion. - - Route wording changes through ordinary Git review. - check_refs: - - validate.check:no-banned-phrases -``` - -Scope every voice entry with `applies_to`. Selective context assembly uses -`applies_to` paths, scopes, and surface types to decide which fingerprint -entries reach an agent for a given piece of work. Scoped voice entries are -selected when copy work touches their surfaces and stay out of the way when -it does not; unscoped entries only surface through ref edges or the global -fallback. - -## Copy Material Lives In Inventory - -Most teams already maintain writing standards somewhere: a style guide, a -terminology list, a banned-phrase list. The fingerprint should point at that -source, not fork it. `inventory.sources` already supports this. - -```yaml -# inventory.yml -building_blocks: - files: - - src/i18n/en.json - notes: - - User-facing strings are centralized in the i18n catalog; copy edits happen there. -sources: - - id: writing-standards - kind: url - ref: https://example.com/your-org/writing-standards - note: Org voice rules and terminology. Normativity levels map to Ghost per the table in docs/language-fingerprints.md. -exemplars: - - id: refund-error-copy - path: src/components/refund-error.tsx - surface_type: error-state - why: Canonical error shape - states what happened, then the next step, in under two sentences. -``` - -External standards stay maintained in one place. The fingerprint records which -source applies and which surfaces it governs. - -## Copy Patterns Live In Composition - -`composition.yml` already has `kind: content` for exactly this. - -```yaml -# composition.yml -patterns: - - id: error-message-shape - kind: content - pattern: Error copy states what happened, then the next step the reader can take. - applies_to: - surface_types: - - error-state - guidance: - - Two sentences maximum for inline errors. - - Name the recovery action in the same breath as the failure. - anti_patterns: - - Passive apology framing ("we're sorry to inform you"). - - Blaming the reader ("you entered an invalid value"). - check_refs: - - validate.check:no-banned-phrases - evidence: - - path: src/components/error-banner.tsx - - id: button-labels-are-verbs - kind: content - pattern: Buttons label the action the reader takes, as a verb phrase. - anti_patterns: - - Generic labels ("OK", "Submit") where a specific verb exists. -``` - -## The Deterministic Subset Becomes Checks - -Writing standards commonly carry normativity levels, in the spirit of -RFC 2119: some rules are absolute, some are recommended, some are contextual. -Map them onto the existing check lifecycle instead of inventing a new one: - -| Standard's level | Ghost expression | Effect | -| --- | --- | --- | -| must (legal wording, banned phrases) | `validate.yml` entry with `status: active` | `ghost check` can fail the diff | -| should (strong recommendation) | `validate.yml` entry with `status: proposed` | Surfaces in `ghost review` as advisory | -| may / contextual | `composition.yml` guidance only | Generation input, never a gate | - -Only the mechanically detectable subset belongs in `validate.yml`. The -`forbidden-regex` and `required-regex` detectors cover banned phrases and -required boilerplate today: - -```yaml -# validate.yml -checks: - - id: no-banned-phrases - title: Banned phrases stay out of user-facing copy - status: active - severity: serious - derivation: - intent: - - intent.experience_contract:exact-wording-surfaces - composition: - - composition.pattern:error-message-shape - applies_to: - paths: - - src/i18n/** - - src/components/** - detector: - type: forbidden-regex - pattern: "we'?re sorry to inform you|per our policy" - repair: Lead with the recovery action; see the error-message-shape pattern and the linked writing standards. -``` - -Everything that needs reading comprehension - tone, register, audience fit - -stays advisory. `ghost review` routes it to the host agent with the relevant -intent and composition refs; it never blocks on its own. - -## What Ghost Deliberately Does Not Do - -- Ghost does not ship a voice ontology, tone scales, or scored language - dimensions. Voice rules are curated intent, owned by the team that writes - them, approved through Git review like every other fingerprint edit. -- Ghost does not embed any organization's style guide. The fingerprint points - at one through `inventory.sources`. -- Advisory copy critique never gates CI. Only active deterministic checks - block, exactly as with visual and structural drift. - -## Workflow - -Capturing language follows the same loop as any other facet content: inventory -the user-facing strings, read the standards source the inventory declares, -draft the smallest evidence-backed entries, and ask a human to curate the -claims. The `voice` recipe in the Ghost skill bundle walks an agent through it. diff --git a/docs/purposes.md b/docs/purposes.md new file mode 100644 index 00000000..93e3a032 --- /dev/null +++ b/docs/purposes.md @@ -0,0 +1,91 @@ +# What Fingerprints Are For + +Ghost has one artifact — the `.ghost/` fingerprint package — and several +consumers that read it. This page exists to keep them honest. + +## The rule + +> A consumer may read the fingerprint through any **projection** it likes. +> A consumer may **not** change the shape of the fingerprint or the merge +> semantics to suit itself. + +The fingerprint is a deliberately dumb source of truth. It does not know who is +asking. Every purpose lives in the projection, not in the artifact. + +The test for any feature that "feels bundled": + +> Does serving this purpose require changing the *shape* of the fingerprint or +> its *merge semantics*? +> - **No** → it's a projection. Fine. Keep it out of the model. +> - **Yes** → that's a leak. Write it down below and fix the boundary. + +## The model (does not bend) + +Four facets, one job each: + +| Facet | Job | +| --- | --- | +| `intent.yml` | Obligations: situations, principles, experience contracts. | +| `inventory.yml` | Material and evidence: topology, building blocks, exemplars, sources. | +| `composition.yml` | Assembly grammar: patterns. | +| `validate.yml` | Hard deterministic checks. Output validation, not generation input. | + +Plus one resolution mechanism: nested packages merged root→leaf, +`child-wins-by-id`. Nesting is the **storage and ownership** model. It is *not* +the selection model. + +## The consumers (each is a projection) + +| Consumer | CLI surface | Projection it needs | Reads | Changes the model? | +| --- | --- | --- | --- | --- | +| **Authoring** | `init`, `scan`, `signals`, `lint`, `verify` | The raw facets + repo signals, for a human/agent writing the fingerprint. | all facets, raw signals | **No** — this *is* the model. | +| **Generation** | `relay gather --mode generation` | A narrow, task-scoped *slice* of the merged stack, delivered before building. | merged stack → `selected-context` filtered by `applies_to` / route selectors | **No** if selection stays a read-only narrowing pass. **Leak risk:** if routing needs are pushed back into merge semantics. | +| **Governance** | `check`, `review`, `relay gather --mode review` | Active checks for the changed paths, evaluated against a diff. | `merged.checks` filtered to changed paths | **No** if check-scoping is pure projection. **Leak risk:** child `status: disabled` silently suppressing an inherited `critical` gate is governance policy living in the data merge. | +| **Comparison / drift** | `compare`, `diff`, `ack`, `track`, `diverge` | The whole structure, often across *bundles*, not one repo. | full fingerprint(s), structural | **No** — read-only structural views. | +| **Fleet** | (`ghost-fleet`, private) | Many bundles at once: distances, cohorts, tracks-graph. | many merged fingerprints | **No** — consumes workspace exports read-only. | +| **Prompt-shaped / pathless** | `relay gather --mode prompt`, `--request*` | A slice selected by prompt selectors when there is no meaningful target path. | route → stack candidates → merged → slice | **Leak risk:** this is where path-based nesting degrades to global fallback. The fix is routing as a selector, not new merge behavior. | + +## Known leaks (the `Yes` and `Leak risk` rows, restated) + +These are the places where a consumer's need has bent, or is bending, the model. +Each is a thing to fix at the boundary — not a reason to redesign the artifact. + +1. **Generation reads the merged stack too broadly.** Nothing should consume + `merged.fingerprint` directly for generation; everything should go through + `selected-context`. The merged stack is an *index*, not a *payload*. + *Fix: merge to assemble, select to deliver.* + +2. **Pathless tasks fall through to `global-fallback`.** Tree-walking is the + privileged selector, so when there's no path the mechanism stops + discriminating exactly when you want it to. + *Fix: path is one selector among several; route by prompt selectors too.* + +3. **A child can silently disable an inherited critical check.** Suppressing a + parent's hard gate from a child folder is governance action-at-a-distance + that is invisible in review. + *Fix: make the suppression explicit and surface it in `review` / `diff` with + per-layer provenance (already recorded in `provenance.layers`).* + +4. **id-coupling is silent.** Overrides only happen on exact id match; a typo + produces two contradictory entries that both merge in, with no error. + *Fix: warn on near-miss ids during `lint`.* + +5. **Low-value overlays accrue.** "Don't nest just because files differ" is + advice no one follows. + *Fix: `scan` / `lint --all` warns when a nested package contributes almost + nothing the parent didn't already say.* + +## What we are NOT doing + +- **Not** flattening to a single fingerprint — that kills ownership locality and + reintroduces drift. +- **Not** adding deeper inheritance (mixins, priority weights, multiple + inheritance) — cascade-fragility grows faster than the expressive payoff. The + whole value of the nesting model is that it is dumb and predictable. +- **Not** giving any consumer write access to the merge. + +## One line + +Nesting is how fingerprints are **stored and owned**; routing plus `applies_to` +filtering is how context is **selected**. One model, many projections, and the +model never bends to serve a projection. diff --git a/docs/relay-configs-and-context.md b/docs/relay-configs-and-context.md deleted file mode 100644 index 19a87a6f..00000000 --- a/docs/relay-configs-and-context.md +++ /dev/null @@ -1,281 +0,0 @@ ---- -title: Relay configs and context -description: How Ghost Relay gathers structured context and lets repos add declared sources. ---- - -# Relay configs and context - -`ghost relay gather` is the single command agents call to gather context. Relay -loads a Relay config, the config chooses the base runtime, and Ghost emits the -same `ghost.relay.gather/v2` JSON contract for agents. Markdown output remains a -compact human preview. - -```text -ghost relay gather --> load Relay config --> choose base runtime --> resolve target/request context --> emit ghost.relay.gather/v2 JSON -``` - -The default OSS shape remains the flat `.ghost/` package: - -```text -.ghost/ - manifest.yml - intent.yml - inventory.yml - composition.yml - validate.yml -``` - -That shape is the default runtime. A repo only needs a Relay config when it -wants extra project-owned context files, such as product questions, source -references, brand guidance, or declared request resolvers, to appear in Relay -JSON. Agent-framework repos can also provide a Relay config from another -location and opt out of a base fingerprint package. - -## Relay Configs - -A Relay config is loaded before Relay resolves context. Precedence is: - -1. `--config ` -2. `GHOST_RELAY_CONFIG` -3. discovered `.ghost/relay.yml` -4. the built-in default config - -OSS Ghost does not auto-discover framework-specific paths such as -`.agents/ghost/relay.yml`. Pass those paths explicitly or set -`GHOST_RELAY_CONFIG`. - -```bash -ghost relay gather app/settings/page.tsx --config .ghost/relay.yml --format json -GHOST_RELAY_CONFIG=.agents/ghost/relay.yml ghost relay gather --request-stdin --format json -``` - -Source paths and resolver globs are relative to the repo root/current working -directory, not to the directory containing the config file. - -Minimal fingerprint-based example: - -```yaml -schema: ghost.relay-config/v1 -id: acme.product-surface/v1 - -base: - kind: fingerprint - -sources: - - id: product-questions - path: product/questions.yml - section: questions - items: questions - summary: question - include: - - blocks - max_chars: 4000 -``` - -Omitting `base` is the same as: - -```yaml -base: - kind: fingerprint -``` - -Each source says: read this file, take these items, summarize each item with -this field, and optionally include a small set of fields as bounded content. -Relay does not read arbitrary project files. - -Request-only configs opt out of the base fingerprint runtime: - -```yaml -schema: ghost.relay-config/v1 -id: demo.agent-context/v1 - -base: - kind: none - -sources: [] - -request_resolvers: - - id: demo-stacks - kind: stack - files: - - stacks/*.yml - schema: demo.stack/v1 - unit_sources: - - id: unit-questions - path: "{unit}/questions.yml" - section: questions - items: questions - summary: question -``` - -With `base.kind: none`, Relay does not load `.ghost/manifest.yml`. It resolves -only declared config sources and request resolvers: - -```bash -ghost relay gather --request-stdin --config .agents/ghost/relay.yml --format json -ghost relay gather stacks/portal.renewal-reminder.email.yml --config .agents/ghost/relay.yml --format json -``` - -The second form synthesizes a minimal Relay request with `task: gather` and the -target path, so a stack file can still be gathered through the same command. - -## Relay Requests - -Agents often start from a natural-language prompt rather than a file path. The -host adapter should turn that prompt into a small `ghost.relay-request/v1` -object, then ask Ghost to resolve it deterministically: - -```bash -ghost relay gather --request request.yml --format json -ghost relay gather --request-stdin --format json -``` - -Example request: - -```yaml -schema: ghost.relay-request/v1 -task: generate-interface -prompt: Generate the right interface for a subscriber renewal reminder in email. -selectors: - customer: subscriber - brand: acme - system: portal - moment: renewal-reminder - medium: email - capability: billing -constraints: - output: interface -``` - -Ghost does not infer selectors from natural language. Codex, Claude, Goose, or -another host harness owns that extraction. Ghost receives selectors and resolves -declared context with exact/id-normalized matching. - -Relay configs can declare stack-style request resolvers: - -```yaml -schema: ghost.relay-config/v1 -id: demo.product-surface/v1 - -base: - kind: fingerprint - -sources: [] - -request_resolvers: - - id: demo-stacks - kind: stack - files: - - stacks/*.yml - schema: demo.stack/v1 - unit_sources: - - id: unit-questions - path: "{unit}/questions.yml" - section: questions - items: questions - summary: question - - id: unit-sources - path: "{unit}/sources.yml" - section: sources - items: sources - summary: summary - - id: unit-composition - path: "{unit}/composition.yml" - section: extra:composition - items: patterns - summary: pattern -``` - -A matching stack file can carry selector metadata: - -```yaml -schema: demo.stack/v1 -id: portal.renewal-reminder.email -title: Portal renewal reminder via email -task_context: - customer: subscriber - system: systems.portal - moment: moments.subscription-renewal-reminder - medium: media.email - capability: capabilities.billing -units: - - systems/portal - - media/email - - capabilities/billing -``` - -When a request matches exactly one stack, Relay projects the declared unit -sources into `questions`, `sources`, and `extra:*`. Unit sources cannot project -into canonical `intent`, `inventory`, `composition`, or `checks`; those remain -owned by fingerprint packages unless projected as explicit extras. If selectors -are missing, conflicting, or ambiguous, Relay records gaps and trace entries -instead of guessing. - -## Sections - -Core sections are: - -- `intent` -- `inventory` -- `composition` -- `checks` -- `questions` -- `sources` - -Extra sections use `extra:`, for example `extra:brand_voice`. - -Canonical `intent`, `inventory`, `composition`, and `checks` continue to come -from the Ghost package schemas. Custom Relay sources initially project into -`questions`, `sources`, and `extra:*`. - -## Relay Context - -JSON output from `ghost relay gather --format json` is the stable agent-facing -contract: - -```json -{ - "schema": "ghost.relay.gather/v2", - "selected_context": {}, - "source": {}, - "targetPaths": [], - "stackDirs": [], - "brief": "# Ghost Relay Brief...", - "context": { - "schema": "ghost.relay-context/v1" - } -} -``` - -The nested Relay context records: - -- target path, request, and mode; -- Relay config id, source, path, and `base.kind`; -- selected section items; -- source files for selected items; -- suggested reads; -- skipped context; -- gaps and trace information. - -Agents and host adapters should consume JSON fields such as `context`, -`selected_context`, `targetPaths`, `source`, `stackDirs`, gaps, and trace data. -The top-level `brief` field is preview text for display and compatibility; do -not scrape it as the primary agent interface. Plain markdown output may omit -projected Relay config sources that are present in JSON. - -## Non-Goals - -- Ghost does not become a generic YAML collector. -- OSS Ghost does not ship proprietary ontology. -- OSS Ghost does not auto-discover `.agents` paths; use `--config` or - `GHOST_RELAY_CONFIG`. -- Relay does not read arbitrary files without a Relay config source. -- Relay does not infer selectors from natural-language prompts. -- Existing `.ghost/` packages do not need migration. -- Relay does not summarize or interpret custom sources with an LLM. -- Visibility is deterministic filtering and trace metadata, not an access - control boundary. diff --git a/packages/ghost/src/skill-bundle/references/voice.md b/packages/ghost/src/skill-bundle/references/voice.md index 96d37c1e..107aac81 100644 --- a/packages/ghost/src/skill-bundle/references/voice.md +++ b/packages/ghost/src/skill-bundle/references/voice.md @@ -5,8 +5,10 @@ description: Capture voice and language guidance into existing Ghost fingerprint # Recipe: Capture Voice And Language -Language maps onto the existing facets; do not invent new schema. See -`docs/language-fingerprints.md` in the Ghost repo for the full mapping. +Language maps onto the existing facets; do not invent new schema. Voice and +language flow through `intent.yml` (tone, voice principles, wording contracts), +`inventory.yml` (copy material and writing-standard sources), +`composition.yml` (copy patterns), and `validate.yml` (the detectable subset). 1. Inventory the user-facing strings: i18n catalogs, error components, notifications, empty states, onboarding copy. Record durable locations in @@ -21,10 +23,9 @@ Language maps onto the existing facets; do not invent new schema. See `intent.experience_contracts`. - Copy shapes into `composition.patterns` with `kind: content`, including `anti_patterns` observed in the repo. - - Scope each entry with `applies_to` (paths, scopes, surface types) so - selective context assembly surfaces it for copy work on those surfaces - and omits it elsewhere. Unscoped entries reach agents only through ref - edges or the global fallback. + - Place each entry in the surface it belongs to so selective context + assembly surfaces it for copy work on that surface and omits it + elsewhere. Brand-wide voice lives in the root surface and cascades down. 4. Promote only the mechanically detectable subset into `validate.yml`: - Absolute rules (banned phrases, required boilerplate) become From 523a50e9433bcc7292578ee9c02fd97758808ad5 Mon Sep 17 00:00:00 2001 From: Nahiyan Khan Date: Thu, 25 Jun 2026 11:10:03 -0400 Subject: [PATCH 2/9] docs(coordinate-space): split containment tree from composition graph A real non-UI composition case showed the topology conflated two axes. Containment (where a node lives, who owns it) is a Layer 2 strict tree with cascade-from-ancestors. Composition (what combines to serve a request) is a Layer 3 typed reference graph laid over the tree. The explicit edges are not a rare exception; in composition-heavy, pathless cases they are the primary structure. The original 'tree + rare edges' framing over-fit the in-repo UI case and under-served the no-repo composition case. Amends the topology section, the layer-asks, decision 3, the surface shape, read-back, and the ideas README. --- docs/ideas/README.md | 4 +- docs/ideas/coordinate-space.md | 150 ++++++++++++++++++++++----------- 2 files changed, 101 insertions(+), 53 deletions(-) diff --git a/docs/ideas/README.md b/docs/ideas/README.md index 53e027ee..988a4fd5 100644 --- a/docs/ideas/README.md +++ b/docs/ideas/README.md @@ -31,8 +31,8 @@ buildable Layer 2 design. They agree; read them as a sequence. else parked. - `coordinate-space.md` — the clean-room design for Layer 2 (the first cut). A surface is an author-named group with an optional description; topology is a - strict containment tree plus cascade-from-ancestors plus rare explicit - shared-edges; resolution is BYOA (Ghost emits a described menu, the agent + two axes — a strict containment tree (Layer 2) plus a typed composition graph + over it (Layer 3); resolution is BYOA (Ghost emits a described menu, the agent matches); delete list covers `inventory.topology`, smeared `applies_to`, and `ghost.map/v1`. diff --git a/docs/ideas/coordinate-space.md b/docs/ideas/coordinate-space.md index 4a410086..131fd532 100644 --- a/docs/ideas/coordinate-space.md +++ b/docs/ideas/coordinate-space.md @@ -67,44 +67,83 @@ model.** Outcome 3 is the forcing function: it has the least medium (no path, no repo), so the coordinate space must be designed from *it* and treat path / diff / prompt as conveniences layered on top. No medium is privileged. -## The topology: strict tree + cascade + rare explicit edges - -Accepted in the design session. Stated precisely, in graph terms: - -**Containment is a strict tree.** Every node — every principle, exemplar, -pattern, contract, check — has exactly **one home surface**. One parent. The -path is the identity. Storage, ownership, and the menu are all this one tree. -Trees lay out deterministically and read at a glance; that legibility *is* the -predictability goal (`reset.md` goal #4). - -**Sharing is resolved by altitude, not by multi-parent.** When a rule applies to -several surfaces, it is placed at the **lowest common ancestor** and **cascades -down**. The brand-wide color rule lives in `core` and flows everywhere. An -email-wide voice lives in `email` and flows to `email/marketing` and -`email/reminder`. Cascade replaces the DAG for the common case. Most "diagonal" -sharing is really "lives higher up." - -**The genuinely diagonal case gets a rare, explicit, visible edge.** "Applies to -email + web but not product" has no common ancestor short of root. That — and -only that — uses an explicit authored `shares` edge that the menu *shows* and a -human *writes on purpose*. It is never a smeared tag and never a silent second -parent. In diagram terms: a tidy tree with a small, countable set of labeled -overlay edges — never a force-directed hairball. - -This is the org-chart-plus-dotted-lines pattern: the tree carries the weight, -explicit edges handle the few exceptions, and one mechanism is never asked to do -both jobs. It gives DAG-level expressiveness for the common case (through -altitude) while keeping tree-level legibility. - -### Why this kills the old leak - -`applies_to` smeared across nodes (Leak A / Leak E) was exactly an implicit DAG: -every node carrying `applies_to: [a, b]` is a node with two parents. Replacing it -with **placement + cascade + rare explicit edge** is the death of that leak. -Inheritance returns, but disciplined: *down the containment tree only.* No -mixins, no priority weights, no union-merge-by-id — just "ancestors contribute to -descendants," the most predictable inheritance there is. This satisfies -`reset.md` goal #4 and the "model does not bend" rule in `purposes.md`. +## The topology: two axes, two layers + +> **Amended** after a real non-UI proof case. The original framing here was +> "strict tree + cascade + rare explicit edges," which collapsed two distinct +> things into one. A composition-heavy case (a typed graph of units of several +> kinds resolved for a pathless request) showed the explicit edges are **not** a rare +> exception — they are a first-class structure that belongs to a *different +> layer*. The correction below makes the design stronger: it confirms Layer 2 +> (the map) and Layer 3 (selection) are genuinely separate, with different +> shapes. + +There are **two axes**, and they must not be conflated: + +1. **Containment — where a node lives and who owns it. This is Layer 2, and it + is a strict tree.** +2. **Composition — what combines to answer a request. This is Layer 3, and it is + a typed reference graph laid over the containment tree.** + +### Containment is a strict tree (Layer 2) + +Every node — every principle, exemplar, pattern, contract, check — has exactly +**one home surface**. One parent. The path is the identity. Storage, ownership, +and the menu are all this one tree. Trees lay out deterministically and read at a +glance; that legibility *is* the predictability goal (`reset.md` goal #4). + +**Sharing down the tree is resolved by altitude, not by multi-parent.** When a +rule applies to several surfaces that share an ancestor, it is placed at the +**lowest common ancestor** and **cascades down**. The brand-wide color rule lives +in `core` and flows everywhere. An email-wide voice lives in `email` and flows to +`email/marketing` and `email/reminder`. Cascade handles the common overlap +without giving any node two parents. Most "lives in two places" is really "lives +higher up." + +This kills the old leak. `applies_to` smeared across nodes (Leak A / Leak E) was +an implicit DAG: every node carrying `applies_to: [a, b]` is a node with two +parents. Placement + cascade replaces it. Inheritance returns, but disciplined: +*down the containment tree only.* No mixins, no priority weights, no +union-merge-by-id — just "ancestors contribute to descendants," the most +predictable inheritance there is. + +### Composition is a typed graph (Layer 3) + +The containment tree answers "where does this live." It does **not** answer "what +combines to serve this request." That second question is composition, and its +shape is a **typed reference graph** over the tree: a node of one kind references +nodes of other kinds by typed edge. + +In the simple UI case this graph is nearly invisible — a surface's slice is +mostly its own subtree plus cascaded ancestors, and composition collapses back +toward the tree. **But in the composition-heavy case the typed edges are the +primary structure, not a rare exception.** A request resolves to a unit that +references units of several other kinds, none of which are its parent or child. +That is not "a tidy tree with a few overlay lines" — it is a graph whose edges +are the point, and the tree underneath is only telling you where each referenced +node is stored. + +The discipline that keeps this from becoming a hairball is **typing**: edges are +not free-form "see also" links; each edge has a kind, and the legible set of edge +kinds is small and authored. The org-chart-plus-dotted-lines intuition still +holds — the difference the proof case revealed is that the dotted lines are +**typed and load-bearing**, and they belong to selection (Layer 3), not to the +map (Layer 2). + +### Why the split matters + +Collapsing composition into "rare tree edges" over-fit the in-repo UI case +(outcomes 1 & 2, where path → subtree does most of the work) and under-served the +no-repo composition case (outcomes 3 & 4, where a prompt resolves a typed +composition with no target path). The four outcomes are equals; the topology must +serve the composition case as a first-class shape, not an exception. Keeping the +tree for containment and a typed graph for composition serves all four without +bending either. + +Both axes satisfy `reset.md` goal #4 and the "model does not bend" rule in +`purposes.md`: the containment tree is dumb and predictable, and the composition +graph stays legible because its edges are typed and few. Neither axis introduces +mixins, priority weights, or union-merge-by-id. ## Grouping is placement, not tags @@ -193,24 +232,29 @@ Stated as obligations, not a schema (schema is a follow-on note): - **id / name** — the author's chosen label, slug-shaped. - **description** — optional, natural language, agent-draftable. Present when the name is not self-evident. -- **parent** — at most one (strict tree). Absent = top-level under `core`. +- **parent** — at most one (strict containment tree). Absent = top-level under + `core`. - **slice** — the nodes placed in this surface. Placement, not tags. -- **shares** — optional, rare, explicit edges to other surfaces for the - irreducibly-diagonal case. Menu-visible. +- **edges** — typed references to nodes in other surfaces (the composition graph, + Layer 3). Each edge has a kind from a small authored set; the menu shows them. + Sparse in UI cases, primary structure in composition-heavy cases. Resolution against a surface yields: its own slice + cascaded ancestor slices + -shared-edge contributions. `core` is the root every surface inherits. +typed-edge contributions. `core` is the root every surface inherits. ## What each layer asks of the coordinate space Confirming the design serves all consumers (the layer rule, `reset.md`): -- **Selection (3):** "evidence → which surface → its slice." Served by the menu + - deterministic slice resolution. No NLP in Ghost. +- **Selection (3):** "evidence → which surface → its composed slice." Served by + the menu + deterministic resolution of the typed composition graph (the + surface's subtree, its cascaded ancestors, and its typed edges). No NLP in + Ghost. **This is where the composition graph lives** — Layer 2 stores, Layer 3 + composes. - **Governance (4):** "this diff touches which surfaces → run their checks." - Served by path→surface mapping over the tree. -- **Comparison (5):** "compare these surfaces / whole trees." Served by the tree - being a clean, portable structure. + Served by path→surface mapping over the containment tree. +- **Comparison (5):** "compare these surfaces / whole trees." Served by the + containment tree being a clean, portable structure. A new purpose still gets a new layer, never a new field on intent / inventory / composition. @@ -242,8 +286,11 @@ earned; foundation kept where it's solid. ships the mechanism, never a taxonomy. 2. Grouping is by placement (storage location), not tags. Layer 1 content stays constant; its coordinate annotations are removed. -3. Topology = strict containment tree + cascade-from-ancestors + rare explicit - shared-edges. No silent multi-parent. +3. Topology has two axes. **Containment** (Layer 2) is a strict tree: one home + per node, cascade-from-ancestors for overlap, no silent multi-parent. + **Composition** (Layer 3) is a typed reference graph over the tree: nodes + reference nodes of other kinds by typed edge. The edges are first-class, not + rare exceptions — in composition-heavy cases they are the primary structure. 4. Resolution is BYOA: Ghost emits a described menu deterministically; the agent matches; Ghost returns `core + surface slice`. Ghost does no NLP. 5. Ambiguity returns the menu, never the whole tree. (Brand-mixing cure.) @@ -270,6 +317,7 @@ This note succeeds if: medium. - All four outcomes resolve through one model. - The point-1 coupling is fixed: the description no longer carries coordinates. -- The topology is a clean tree a person can hold in their head, with cascade for - the common overlap and a handful of visible edges for the rare diagonal. +- Containment is a clean tree a person can hold in their head, with cascade for + overlap; composition is a typed graph over it, legible because its edges are + typed and few. The two axes are not conflated. - Nothing in Layer 1, 4, or 5 had to change to make Layer 2 right. From 7a4c99405e2aacdbbe6a1be6d6712c6755f2cd49 Mon Sep 17 00:00:00 2001 From: Nahiyan Khan Date: Thu, 25 Jun 2026 11:18:07 -0400 Subject: [PATCH 3/9] docs(surface-schema): propose ghost.surfaces/v1 as the first extraction First concrete cut from coordinate-space.md. Proposes a new surfaces.yml facet (ghost.surfaces/v1) anchored by the existing manifest, expressing both axes: parent (the containment tree) and typed edges over edge_kinds (the composition graph). Specifies a field-by-field migration off inventory.topology, smeared applies_to, and exemplar surface_type/scope to a single surface: placement pointer, keeping Layer 1 prose constant and the flat facet files intact for the first cut. Includes a worked example from this repo's own dogfood .ghost/, lint obligations, and open forks (closed vs open edge_kinds, dotted ids, default placement, where path->surface mapping lives). Additive and backward compatible: absent surfaces.yml keeps single-core behavior. --- docs/ideas/README.md | 5 + docs/ideas/surface-schema.md | 246 +++++++++++++++++++++++++++++++++++ 2 files changed, 251 insertions(+) create mode 100644 docs/ideas/surface-schema.md diff --git a/docs/ideas/README.md b/docs/ideas/README.md index 988a4fd5..b8a998c5 100644 --- a/docs/ideas/README.md +++ b/docs/ideas/README.md @@ -35,6 +35,11 @@ buildable Layer 2 design. They agree; read them as a sequence. over it (Layer 3); resolution is BYOA (Ghost emits a described menu, the agent matches); delete list covers `inventory.topology`, smeared `applies_to`, and `ghost.map/v1`. +- `surface-schema.md` — the first concrete extraction. Proposes + `ghost.surfaces/v1` as a new `surfaces.yml` facet expressing both the + containment `parent` and typed composition `edges`, plus a field-by-field + migration off `topology` / `applies_to` / `surface_type` / `scope` to a + `surface:` placement pointer. ## Independent, still live diff --git a/docs/ideas/surface-schema.md b/docs/ideas/surface-schema.md new file mode 100644 index 00000000..867c9064 --- /dev/null +++ b/docs/ideas/surface-schema.md @@ -0,0 +1,246 @@ +--- +status: exploring +--- + +# Surface schema: the first extraction + +This note is subordinate to `fingerprint-first-architecture.md` (settled) and is +the first concrete cut named by `reset.md` and designed by `coordinate-space.md`. +It turns the prose shape in `coordinate-space.md` into a proposed schema, on +disk, with a migration path off `topology` / `applies_to` / `surface_type` / +`scope`. It proposes one new schema; it changes no code in this note. + +The design decisions are settled by `coordinate-space.md` and not reopened here: +a surface is an author-named group; grouping is placement not tags; containment +is a strict tree (Layer 2); composition is a typed reference graph over it +(Layer 3); resolution is BYOA. This note only asks: **what does that look like as +a file an author writes and the CLI validates?** + +## What must be expressed + +From `coordinate-space.md`, a surface needs: + +- **id / name** — author's slug-shaped label. +- **description** — optional, natural language, agent-draftable. +- **parent** — at most one (the containment tree, Layer 2). +- **slice** — the nodes placed in this surface (placement, not tags). +- **edges** — typed references to nodes in other surfaces (the composition + graph, Layer 3). + +The schema must hold both axes without conflating them: parent is containment, +edges are composition. + +## Where surfaces live on disk + +`coordinate-space.md` makes the coordinate space its own Layer 2 artifact, +distinct from the description facets. The on-disk choice should follow that: +**a new facet file, `surfaces.yml`, anchored by the existing `manifest.yml`.** + +```text +.ghost/ + manifest.yml # ghost.fingerprint-package/v1 (unchanged) + surfaces.yml # ghost.surfaces/v1 (new) — the coordinate space + intent.yml # ghost.intent/v1 — description content (unchanged) + inventory.yml # ghost.inventory/v1 — minus topology + composition.yml # ghost.composition/v1 — patterns minus applies_to + validate.yml # ghost.validate/v1 — checks minus applies_to +``` + +Rationale for a new file over extending an existing one: + +- It is a different layer; `purposes.md` says a new purpose gets its own home, + never a new field on a description facet. +- It keeps the description facets' content constant (the point-1 fix) — they + lose only their coordinate annotations. +- It is additive: a package with no `surfaces.yml` has a single implicit `core` + surface and behaves exactly as today. Migration is opt-in. + +New schema literal: `ghost.surfaces/v1`. (Sits alongside the existing facet +literals `ghost.intent/v1`, `ghost.inventory/v1`, `ghost.composition/v1`, +`ghost.validate/v1`.) + +## Proposed shape + +`surfaces.yml`: + +```yaml +schema: ghost.surfaces/v1 + +# Edge kinds this package allows. Small, authored, closed set. +# Typing is the discipline that keeps composition legible (coordinate-space.md). +edge_kinds: + - id: composes + description: This surface assembles the referenced surface into its output. + - id: governed-by + description: This surface must satisfy the referenced surface's obligations. + +surfaces: + # core is implicit and always present; declare it only to describe it. + - id: core + description: True everywhere. Brand-wide intent, inventory, and patterns. + + - id: email + description: Transactional and lifecycle email. + parent: core + + - id: email.marketing + description: Promotional email; campaign voice and offer framing. + parent: email + + - id: checkout + description: The purchase decision surface. + parent: core + # Composition graph: a typed edge to a peer surface, not a parent. + edges: + - kind: composes + to: payments + - kind: governed-by + to: consent +``` + +Field rules: + +- `id` — slug. Dots denote tree depth for readability; `parent` is the + authoritative containment link (the dotted id is sugar, the tree is truth). +- `parent` — optional; absent means a top-level surface under the implicit + `core` root. Exactly one parent (strict tree; no arrays). +- `description` — optional string. Present when the name is not self-evident. +- `edges` — optional; each has `kind` (must be in `edge_kinds`) and `to` (an + existing surface id). Edges are the composition graph; they never imply + containment and never cascade. +- `edge_kinds` — the closed vocabulary. An edge with an unknown `kind` is a lint + error (mirrors how `surface_types` are error-enforced today). + +## How nodes attach to surfaces (placement, not tags) + +`coordinate-space.md` says a node's surface is *where it is stored*. Two on-disk +options; this note recommends the first and flags the second as the larger +follow-on: + +1. **Per-surface placement field, minimal change (recommended first cut).** + Description nodes keep living in `intent.yml` / `inventory.yml` / + `composition.yml`, but their coordinate annotations (`applies_to`, + `surface_type`, `scope`) are removed and replaced by a single `surface: ` + placement key. Default when absent is `core`. This is the smallest honest + step: it deletes the smeared DAG and replaces it with one placement pointer, + without restructuring the facet files. + +2. **Storage-by-location, full model (follow-on note).** Nodes physically live + under the surface they belong to (nested facet files per surface). Truest to + "placement is location," but it restructures the package layout and should be + its own proposal. Do not attempt it in the first cut. + +The first cut keeps the flat facet files and changes annotations to a placement +pointer. That is enough to kill Leak A and validate the tree + graph model +before committing to a layout change. + +## The migration (off the dead coordinate systems) + +What `coordinate-space.md` deletes, expressed as concrete field moves: + +| Today (delete) | Becomes | +| --- | --- | +| `inventory.topology.scopes[]` | `surfaces.yml` surfaces with `parent` links | +| `inventory.topology.surface_types[]` | folded into surfaces (a surface *is* the type) | +| `exemplar.surface_type` / `exemplar.scope` | `exemplar.surface: ` placement | +| `principle.applies_to` / `pattern.applies_to` / `contract.applies_to` | node `surface: ` placement + cascade | +| `check.applies_to` | check `surface: ` placement | +| `ghost.map/v1` (`map.md`, `ghost-core/map/`) | `ghost.surfaces/v1` | + +Worked example, from this repo's own dogfood `.ghost/inventory.yml`: + +```yaml +# before — topology + smeared coordinates +topology: + scopes: + - id: docs-site + paths: [docs, README.md, apps/docs] + surface_types: [docs-home, docs-foundation, tool-doc] +exemplars: + - id: public-readme-fingerprint-model + surface_type: docs-home + scope: docs-site +``` + +```yaml +# after — surfaces.yml owns the tree; exemplar just places itself +# surfaces.yml +surfaces: + - id: docs + description: The docs site and public README. + parent: core +exemplars: # in inventory.yml, coordinates gone + - id: public-readme-fingerprint-model + surface: docs +``` + +Note `paths` disappeared from the surface in the no-repo model — path is +*evidence the host maps to a surface*, not part of the surface definition +(`coordinate-space.md`: medium-agnostic, designed from the no-repo case). Path → +surface mapping is a binding/governance concern (Layer 3/4), proposed separately; +the surface itself does not carry repo paths. + +## Validation (lint obligations, not code) + +`ghost lint` on `ghost.surfaces/v1` should enforce: + +- every `parent` references an existing surface id; no cycles (it is a tree); +- exactly one parent per surface (no parent arrays); +- every edge `kind` is in `edge_kinds`; every edge `to` is an existing surface; +- every node `surface:` placement references an existing surface (warn on + near-miss ids, per `purposes.md` leak #4); +- `core` is reserved as the implicit root. + +Composition edges are explicitly allowed to form a graph (including cross-links); +only `parent` is constrained to a tree. This is the two-axis rule made into lint. + +## What stays stable + +Per `coordinate-space.md` and `reset.md`, hold these contracts while extracting: +`ghost.fingerprint/v1`, `ghost.validate/v1`, `ghost.fingerprint-package/v1`, +`ghost.check-report/v1`. `ghost.surfaces/v1` is new and additive; absent +`surfaces.yml` keeps today's single-`core` behavior. + +Layer 1 content (intent / inventory / composition prose) does not change. Only +coordinate annotations move to a `surface:` placement pointer. + +## Open forks (decide before code) + +1. **`edge_kinds` closed vs. open.** Closed (lint-enforced) keeps the graph + legible; open lets authors invent edge kinds freely. Recommendation: closed, + small default set, matching the error-level discipline `surface_types` has + today. The composition-heavy proof case needs typed edges precisely *because* + the set is legible. +2. **Dotted ids vs. explicit parent only.** Allowing `email.marketing` as an id + is readable but creates two sources of truth for the tree. Recommendation: + `parent` is authoritative; dotted ids are display sugar validated to agree + with `parent`, or disallowed entirely. Lean toward disallowing to keep one + source of truth (the layers note's Leak C instinct). +3. **`surface:` default.** Absent placement defaults to `core`. Confirm that an + un-placed node cascading from root is the desired default rather than a lint + warning that forces explicit placement. +4. **Where path→surface mapping lives.** Out of scope here by design, but it is + the next note: the repo-side binding that turns a target path or diff into a + surface for outcomes 1 & 2. `surfaces.yml` stays repo-agnostic. + +## Not a plan + +This note proposes `ghost.surfaces/v1`, its on-disk home, the placement-pointer +migration, and the lint obligations. It writes no Zod, renames no command, and +moves no field today. Implementation — the schema module, the lint rules, the +`surfaces.yml` loader, the migration of this repo's own `.ghost/` — is the next +cut, proposed in its own note and linked back here. + +## Read-back + +This note succeeds if: + +- A surface is one file (`surfaces.yml`) an author can write and a human can + review in Git. +- The schema expresses both axes: `parent` (containment tree) and `edges` + (typed composition graph), without conflating them. +- The migration off `topology` / `applies_to` / `surface_type` / `scope` is a + concrete field-by-field move, not a rewrite. +- The first cut keeps flat facet files and changes annotations to a `surface:` + pointer — small enough to ship and prove before any layout change. +- Nothing in the stable contracts has to change to add `ghost.surfaces/v1`. From 82d337ddbb6fa82283d396f75d7442beef250810 Mon Sep 17 00:00:00 2001 From: Nahiyan Khan Date: Thu, 25 Jun 2026 11:44:06 -0400 Subject: [PATCH 4/9] docs(surface-schema): settle edge_kinds (closed) and flat ids Resolve two open forks into decisions. edge_kinds is a fixed, Ghost-owned, closed set referenced (never defined) per package: opening it would make Ghost a general-purpose graph database and lose the interface-composition focus; richer consumers extend edges consumer-side, not by opening Ghost's set. IDs are flat, unique slugs with no structural meaning; dotted-id-as-hierarchy is banned (lint error) so the tree lives only in parent, one source of truth (kills Leak C). Updates the example, field rules, lint obligations, and forks list; two forks remain (surface default, path->surface binding). --- docs/ideas/surface-schema.md | 93 ++++++++++++++++++++++++------------ 1 file changed, 63 insertions(+), 30 deletions(-) diff --git a/docs/ideas/surface-schema.md b/docs/ideas/surface-schema.md index 867c9064..f99c4c30 100644 --- a/docs/ideas/surface-schema.md +++ b/docs/ideas/surface-schema.md @@ -66,13 +66,8 @@ literals `ghost.intent/v1`, `ghost.inventory/v1`, `ghost.composition/v1`, ```yaml schema: ghost.surfaces/v1 -# Edge kinds this package allows. Small, authored, closed set. -# Typing is the discipline that keeps composition legible (coordinate-space.md). -edge_kinds: - - id: composes - description: This surface assembles the referenced surface into its output. - - id: governed-by - description: This surface must satisfy the referenced surface's obligations. +# Edge kinds are a fixed, Ghost-owned set (see "edge_kinds is closed" below). +# Not authored per-package; listed in the schema, not in surfaces.yml. surfaces: # core is implicit and always present; declare it only to describe it. @@ -83,9 +78,9 @@ surfaces: description: Transactional and lifecycle email. parent: core - - id: email.marketing + - id: email-marketing description: Promotional email; campaign voice and offer framing. - parent: email + parent: email # the tree lives here, not in the id - id: checkout description: The purchase decision surface. @@ -100,16 +95,52 @@ surfaces: Field rules: -- `id` — slug. Dots denote tree depth for readability; `parent` is the - authoritative containment link (the dotted id is sugar, the tree is truth). +- `id` — a flat, unique, slug-shaped label with **no structural meaning**. Dots + are not allowed as hierarchy: the tree lives only in `parent`, never in the + id. `email-marketing` is a name; `email.marketing` is banned because the dot + would pretend to be a `parent` link. One source of truth for the tree. - `parent` — optional; absent means a top-level surface under the implicit - `core` root. Exactly one parent (strict tree; no arrays). + `core` root. Exactly one parent (strict tree; no arrays). **This is the only + place containment is expressed.** - `description` — optional string. Present when the name is not self-evident. -- `edges` — optional; each has `kind` (must be in `edge_kinds`) and `to` (an - existing surface id). Edges are the composition graph; they never imply - containment and never cascade. -- `edge_kinds` — the closed vocabulary. An edge with an unknown `kind` is a lint - error (mirrors how `surface_types` are error-enforced today). +- `edges` — optional; each has `kind` (must be one of the Ghost-owned + `edge_kinds`) and `to` (an existing surface id). Edges are the composition + graph; they never imply containment and never cascade. + +`edge_kinds` are **not** declared per-package. They are a fixed, Ghost-owned set +(see below), so `surfaces.yml` references kinds but never defines them. + +## `edge_kinds` is closed (settled) + +The edge vocabulary is a **closed, Ghost-owned set**, not author-extensible. +This was an open fork; it is now decided, because opening it is the exact thing +that loses the plot. + +An open vocabulary means the *author* defines what an edge means, which means +Ghost has no opinion about edges, which means Ghost is a general-purpose graph +database. That is unbounded scope — the sprawl the reset exists to end. Closing +the set forces Ghost to commit to what edges are *for*, and for a fingerprint- +first, interface-composition tool the answer is small: edges express how +interface surfaces relate. A starting set: + +- `composes` — this surface assembles the referenced surface into its output. +- `governed-by` — this surface must satisfy the referenced surface's + obligations. + +The discipline rule that comes with closing it: + +> If you cannot name an edge kind from the interface-composition domain, it does +> not belong in Ghost. The temptation to add a non-interface edge kind is the +> signal that the work has drifted toward a general world-model graph — which is +> a consumer's job, not Ghost's. + +**The boundary for richer consumers.** A composition-heavy consumer (a typed +unit graph with many relationship kinds) will legitimately want edge kinds Ghost +does not ship. That is expected: such inputs are domain-shaped, Ghost is +interface-shaped. The resolution is that the consumer extends edges *in the +consumer*, not by opening Ghost's set. Ghost's closed set is the interface +vocabulary; anything beyond it is consumer-local extension. This keeps Ghost +small and keeps the consumer free. ## How nodes attach to surfaces (placement, not tags) @@ -186,7 +217,10 @@ the surface itself does not carry repo paths. - every `parent` references an existing surface id; no cycles (it is a tree); - exactly one parent per surface (no parent arrays); -- every edge `kind` is in `edge_kinds`; every edge `to` is an existing surface; +- every surface `id` is a flat slug with no dots (dots-as-hierarchy is a lint + error; the tree lives only in `parent`); +- every edge `kind` is one of the fixed Ghost-owned `edge_kinds`; every edge `to` + is an existing surface; - every node `surface:` placement references an existing surface (warn on near-miss ids, per `purposes.md` leak #4); - `core` is reserved as the implicit root. @@ -204,22 +238,21 @@ Per `coordinate-space.md` and `reset.md`, hold these contracts while extracting: Layer 1 content (intent / inventory / composition prose) does not change. Only coordinate annotations move to a `surface:` placement pointer. +## Settled in this note + +- **`edge_kinds` is closed and Ghost-owned.** See "`edge_kinds` is closed" + above. Authors reference kinds; they never define them. Richer consumers + extend edges consumer-side, not by opening Ghost's set. +- **IDs are flat; the tree lives only in `parent`.** Dotted ids as hierarchy are + banned (a lint error). One source of truth for containment, killing the Leak C + duplicate-vocabulary risk before it starts. + ## Open forks (decide before code) -1. **`edge_kinds` closed vs. open.** Closed (lint-enforced) keeps the graph - legible; open lets authors invent edge kinds freely. Recommendation: closed, - small default set, matching the error-level discipline `surface_types` has - today. The composition-heavy proof case needs typed edges precisely *because* - the set is legible. -2. **Dotted ids vs. explicit parent only.** Allowing `email.marketing` as an id - is readable but creates two sources of truth for the tree. Recommendation: - `parent` is authoritative; dotted ids are display sugar validated to agree - with `parent`, or disallowed entirely. Lean toward disallowing to keep one - source of truth (the layers note's Leak C instinct). -3. **`surface:` default.** Absent placement defaults to `core`. Confirm that an +1. **`surface:` default.** Absent placement defaults to `core`. Confirm that an un-placed node cascading from root is the desired default rather than a lint warning that forces explicit placement. -4. **Where path→surface mapping lives.** Out of scope here by design, but it is +2. **Where path→surface mapping lives.** Out of scope here by design, but it is the next note: the repo-side binding that turns a target path or diff into a surface for outcomes 1 & 2. `surfaces.yml` stays repo-agnostic. From 83fa8082f866d89e424b6119f15211fa95190024 Mon Sep 17 00:00:00 2001 From: Nahiyan Khan Date: Thu, 25 Jun 2026 14:19:19 -0400 Subject: [PATCH 5/9] docs(surface-schema): settle explicit placement; reframe binding as scoped ownership Resolve the remaining forks. Placement is explicit: defaulting un-placed nodes to core would rebuild global-fallback (the brand-mixing failure the redesign cures), so authoring drafts placement, lint warns-and-teaches on the gap, and un-placed never silently means core (warning not error, matching the derivation- ref convention). Reframe the path->surface fork as scoped ownership: in a repo, surfaces are owned by location (the checkout surface is realized under apps/checkout), which is ordinary nested-package/CODEOWNERS-shaped binding, not a new subsystem. The portable contract (surfaces.yml) carries no paths; paths live on the binding. One sub-fork remains for its own note: nested-package-as-binding vs explicit path declaration vs both. --- docs/ideas/README.md | 4 +- docs/ideas/surface-schema.md | 76 +++++++++++++++++++++++++++++++----- 2 files changed, 70 insertions(+), 10 deletions(-) diff --git a/docs/ideas/README.md b/docs/ideas/README.md index b8a998c5..a0d2c618 100644 --- a/docs/ideas/README.md +++ b/docs/ideas/README.md @@ -39,7 +39,9 @@ buildable Layer 2 design. They agree; read them as a sequence. `ghost.surfaces/v1` as a new `surfaces.yml` facet expressing both the containment `parent` and typed composition `edges`, plus a field-by-field migration off `topology` / `applies_to` / `surface_type` / `scope` to a - `surface:` placement pointer. + `surface:` placement pointer. Settles closed `edge_kinds`, flat ids, and + explicit placement (no silent global default); the one remaining fork — the + repo binding as scoped ownership — is reframed for its own note. ## Independent, still live diff --git a/docs/ideas/surface-schema.md b/docs/ideas/surface-schema.md index f99c4c30..3b78e0f9 100644 --- a/docs/ideas/surface-schema.md +++ b/docs/ideas/surface-schema.md @@ -152,9 +152,10 @@ follow-on: Description nodes keep living in `intent.yml` / `inventory.yml` / `composition.yml`, but their coordinate annotations (`applies_to`, `surface_type`, `scope`) are removed and replaced by a single `surface: ` - placement key. Default when absent is `core`. This is the smallest honest - step: it deletes the smeared DAG and replaces it with one placement pointer, - without restructuring the facet files. + placement key. Placement is explicit (see "Placement is explicit" below); an + absent `surface:` is not silently treated as global. This is the smallest + honest step: it deletes the smeared DAG and replaces it with one placement + pointer, without restructuring the facet files. 2. **Storage-by-location, full model (follow-on note).** Nodes physically live under the surface they belong to (nested facet files per surface). Truest to @@ -165,6 +166,30 @@ The first cut keeps the flat facet files and changes annotations to a placement pointer. That is enough to kill Leak A and validate the tree + graph model before committing to a layout change. +## Placement is explicit (settled) + +This was an open fork ("absent `surface:` defaults to `core`"); it is now +decided against the silent default. Defaulting un-placed nodes to `core` quietly +rebuilds global-fallback — a node that reaches every surface is exactly the +brand-mixing failure `coordinate-space.md` exists to cure. So placement is +explicit, made frictionless from three directions rather than enforced by nag: + +1. **Authoring drafts placement.** When a Ghost authoring skill captures a node, + it proposes a `surface:` from what is being captured. The author approves + rather than hand-writes, so most nodes are placed at birth. +2. **Lint catches and teaches.** An un-placed node is flagged — not silently + dropped into `core` — with a message that explains *why* placement matters + (un-placed reaches everywhere; that is brand-mixing) and what to do. Teaching, + not just erroring. +3. **No silent global default.** "Absent = core, move on" is explicitly not the + behavior. + +The un-placed lint is a **warning, not a hard error**, matching the existing +convention that missing derivation refs warn so teams can draft while curation +catches up. A hand-authored fingerprint can have un-placed nodes in draft; lint +surfaces them and teaches the fix, and they never reach production silently +global. + ## The migration (off the dead coordinate systems) What `coordinate-space.md` deletes, expressed as concrete field moves: @@ -223,6 +248,8 @@ the surface itself does not carry repo paths. is an existing surface; - every node `surface:` placement references an existing surface (warn on near-miss ids, per `purposes.md` leak #4); +- an un-placed node warns (not errors) and teaches placement — it is never + silently treated as global `core`; - `core` is reserved as the implicit root. Composition edges are explicitly allowed to form a graph (including cross-links); @@ -246,15 +273,46 @@ coordinate annotations move to a `surface:` placement pointer. - **IDs are flat; the tree lives only in `parent`.** Dotted ids as hierarchy are banned (a lint error). One source of truth for containment, killing the Leak C duplicate-vocabulary risk before it starts. +- **Placement is explicit; no silent global default.** See "Placement is + explicit" above. Authoring drafts placement, lint warns-and-teaches on the + gap, and un-placed never silently means `core`. + +## The repo binding is scoped ownership (reframed) + +An earlier draft called this "where path→surface mapping lives," which made it +sound like an exotic new subsystem. It is not. In a repo, **surfaces are owned +by location** — the `checkout` surface *is* the thing realized under +`apps/checkout/`. That is ordinary scoped ownership, CODEOWNERS- and +directory-shaped, and Ghost already has the mechanism: nested packages. + +The contract/binding split makes this clean: + +- **The portable contract** (`surfaces.yml`) is repo-agnostic and carries **no + paths**. This is what lets the no-repo cases (outcomes 3 & 4) work at all. +- **The repo binding** declares scoped ownership: *this surface is realized by + this scope here.* Path → surface resolution falls out of where the binding + sits in the tree, the way nested-package resolution already works. + +The hard rule that keeps the split honest: **the surface definition must never +carry `paths`.** The moment `surfaces.yml` gains a `paths:` field, the portable +contract is re-coupled to a repo and the no-repo cases break. Paths live on the +binding, never on the surface. This is exactly why `topology.scopes[].paths` is +on the delete list. + +This is a separate note, but it is the *familiar* part (nesting, ownership), not +a new abstraction. The one real sub-fork it must settle: + +> Does scoped ownership live in **nested `.ghost/` packages** (directory +> location = the binding), in an **explicit binding declaration** that names a +> surface and its paths, or **both**? Lean: directory location is the default +> binding; an explicit declaration is the escape hatch when ownership does not +> match the tree. ## Open forks (decide before code) -1. **`surface:` default.** Absent placement defaults to `core`. Confirm that an - un-placed node cascading from root is the desired default rather than a lint - warning that forces explicit placement. -2. **Where path→surface mapping lives.** Out of scope here by design, but it is - the next note: the repo-side binding that turns a target path or diff into a - surface for outcomes 1 & 2. `surfaces.yml` stays repo-agnostic. +1. **Scoped-ownership binding shape.** Nested-package-as-binding vs. explicit + path declaration vs. both. Its own note (see "The repo binding is scoped + ownership" above); `surfaces.yml` stays repo-agnostic regardless. ## Not a plan From 4a56f47283c94db3b4954b9285ef18a7e0dce83c Mon Sep 17 00:00:00 2001 From: Nahiyan Khan Date: Thu, 25 Jun 2026 14:36:52 -0400 Subject: [PATCH 6/9] docs(surface-binding): settle ghost.binding/v1, the repo binding layer Second concrete extraction. The contract (surfaces.yml) carries no paths; the binding owns all path matching. Directory location is the default binding (a scoped .ghost/ binds surfaces to its subtree, reframing nested-package resolution from data-merge to binding); an explicit .ghost.bind.yml is the escape hatch when ownership does not match the tree, and is the real home for the deleted topology.scopes[].paths. One resolver serves prompt, path, and diff roads, meeting at a surface id; no-repo cases need no binding. Reframes nesting-as-ownership to retire Leak E (no silent merge override, no silently disabled inherited critical check). Records open forks (external contract references, core fallback, bind-vs-redeclare) and the honest caution that this is the least proof-validated layer, so it ships smallest-first. --- docs/ideas/README.md | 6 + docs/ideas/contract-and-binding.md | 8 +- docs/ideas/surface-binding.md | 210 +++++++++++++++++++++++++++++ 3 files changed, 221 insertions(+), 3 deletions(-) create mode 100644 docs/ideas/surface-binding.md diff --git a/docs/ideas/README.md b/docs/ideas/README.md index a0d2c618..b9f113bd 100644 --- a/docs/ideas/README.md +++ b/docs/ideas/README.md @@ -42,6 +42,12 @@ buildable Layer 2 design. They agree; read them as a sequence. `surface:` placement pointer. Settles closed `edge_kinds`, flat ids, and explicit placement (no silent global default); the one remaining fork — the repo binding as scoped ownership — is reframed for its own note. +- `surface-binding.md` — the second concrete extraction. Settles `ghost.binding/v1`: + the contract carries no paths, the binding owns all path matching, with + directory location as the default binding and an explicit `.ghost.bind.yml` as + the escape hatch. Path / prompt / diff all resolve to a surface id through one + resolver; nesting is reframed from data-merge to binding (retiring Leak E). + Records that this is the least proof-validated layer, so it ships smallest-first. ## Independent, still live diff --git a/docs/ideas/contract-and-binding.md b/docs/ideas/contract-and-binding.md index 3ccd6838..7022681f 100644 --- a/docs/ideas/contract-and-binding.md +++ b/docs/ideas/contract-and-binding.md @@ -7,9 +7,11 @@ status: exploring > **Mostly subsumed.** The contract/binding split this note proposes now falls > out of `coordinate-space.md` for free: designing the coordinate space > medium-agnostically produces the portable-contract-vs-repo-binding split -> without a separate decision. Keep this note for the *sort* (which piece goes -> where) and the artifact rationale; treat `coordinate-space.md` as the live -> design. +> without a separate decision. The contract half is designed in +> `surface-schema.md` (`ghost.surfaces/v1`); the binding half in +> `surface-binding.md` (`ghost.binding/v1`). Keep this note for the *sort* +> (which piece goes where) and the artifact rationale; treat those two as the +> live design. This note is subordinate to `fingerprint-first-architecture.md` (settled) and a sibling to `ghost-layers.md` (exploring). It changes neither. The layers note diff --git a/docs/ideas/surface-binding.md b/docs/ideas/surface-binding.md new file mode 100644 index 00000000..71251f9e --- /dev/null +++ b/docs/ideas/surface-binding.md @@ -0,0 +1,210 @@ +--- +status: exploring +--- + +# Surface binding: connecting a repo to the contract + +This note is subordinate to `fingerprint-first-architecture.md` (settled), +designed by `coordinate-space.md`, and the second concrete cut after +`surface-schema.md`. It settles the one fork that note left open: how a real +working tree declares that it realizes a surface, and where. It builds on the +`ghost.binding/v1` vocabulary first sketched in `contract-and-binding.md`. + +The schema note defined the **portable contract** (`surfaces.yml`, +repo-agnostic, no paths). This note defines the other half of the contract/ +binding split: the **binding** — the thin repo-native statement that *this* +working tree is an instance of that contract, and these paths realize these +surfaces. + +## What stays constant + +- **The contract carries no paths.** `surfaces.yml` stays repo-agnostic. This is + the hard rule from `surface-schema.md` and it is non-negotiable here: the + binding exists precisely so the contract never has to know git exists. +- **The no-repo cases need no binding.** Outcomes 3 & 4 (customer brand + generation, portable brand package) resolve directly from a prompt to a + surface. The binding is **purely additive** for the repo cases (outcomes 1 & + 2). A lone prompt against a contract never touches a binding. +- **Resolution is BYOA.** Ghost resolves path → surface deterministically, no + LLM. The host agent still does any natural-language matching. + +## What the binding is for + +Two outcomes need a working tree connected to the contract: + +1. **In-repo work (outcome 1).** "I'm editing `apps/checkout/page.tsx` → which + surface's slice do I get before I build?" needs **path → surface**. +2. **PR gate (outcome 2).** "This diff touches these files → which surfaces' + checks run?" needs the same resolution over a **diff**. + +Both are the same primitive: **given a path, resolve the surface that owns it, +then compose that surface's slice** (its subtree + cascaded ancestors + typed +edges, per `coordinate-space.md`). The binding is the only thing that turns a +filesystem path into a surface id. Nothing else in the model knows about paths. + +This is also where three layers plug in: Selection (Layer 3) and Governance +(Layer 4) both consume path → surface; the contract/binding seam +(`contract-and-binding.md`) becomes concrete here; and it is the final home for +demoting nesting-as-ownership (Leak E). + +## The shape: directory by default, declaration as escape hatch + +`surface-schema.md` left the sub-fork as nested-package vs. explicit declaration +vs. both. **Decision: both, with directory location as the default binding and an +explicit declaration as the escape hatch.** + +### Directory location is the default binding + +The common case needs zero new ceremony. A scoped `.ghost/` package placed in a +directory **binds the surfaces it declares to that directory's subtree**. This +reuses the nested-package resolution Ghost already has — root-to-leaf discovery +along a path — but reframes what nesting *means*: not a data merge, a **binding**. + +```text +.ghost/ # root contract: surfaces.yml defines the tree +apps/checkout/.ghost/ # binds checkout surfaces to apps/checkout/** +apps/checkout/page.tsx # resolves to the checkout surface by location +``` + +For a path, Ghost walks from root to leaf, finds the nearest binding, and that +binding names the surface. Location *is* ownership. No `paths:` field is needed +in the common case because the directory already says where the binding applies. + +### Explicit declaration when ownership does not match the tree + +Sometimes the surface a path realizes is not where the directory tree would put +it — a flat repo, a surface realized across scattered paths, a monorepo whose +layout predates the surface model. For that, an explicit binding file: + +```text +apps/email-svc/.ghost.bind.yml +``` + +```yaml +schema: ghost.binding/v1 +contract: . # path, npm name, or resource id of the contract +bindings: + - surface: email-lifecycle # a surface id in the contract + paths: [apps/email-svc/src] + - surface: email-marketing + paths: [apps/email-svc/campaigns] +``` + +The explicit form names contract + surface + paths directly. It is the escape +hatch, not the default: most repos never write one. `paths:` lives **here, on +the binding**, never on the surface — this is exactly where the deleted +`topology.scopes[].paths` actually belonged. + +### Precedence + +When both exist, **the nearest binding along the path wins**, and an explicit +`.ghost.bind.yml` at a given level takes precedence over directory-implied +binding at that level. Precedence is positional and deterministic; there is no +merge of competing bindings, no priority weights. (Holds the `reset.md` +no-cascade-fragility line.) + +## Resolution + +One resolver serves both roads, meeting at a surface id: + +```text +prompt → host matches the described menu → surface id ─┐ + ├─→ compose slice +path → nearest binding → surface id ─────────────────┘ +diff → each changed path → binding → surface id(s) → union of slices/checks +``` + +- **Prompt road (no repo):** unchanged from `coordinate-space.md`. No binding + involved. The contract resolves a surface from the described menu. +- **Path road (repo):** walk root→leaf, nearest binding names the surface, + compose its slice. Path is *evidence that resolves to a coordinate*, not a + coordinate itself. +- **Diff road (PR gate):** resolve each changed path to its surface, take the + union, run those surfaces' checks against the diff. A path with no binding + resolves to the root contract's `core` (the diff still gets brand-wide checks). + +When a path resolves to **no surface and there is no root contract**, the result +is the explicit "which surface?" menu, never a whole-tree dump — the same +brand-mixing cure as the prompt road. + +## Nesting is binding, not data-merge (Leak E, resolved) + +`coordinate-space.md` put `child-wins-by-id` union merge on the delete list. +This note is its final home. Nested `.ghost/` packages stop being a data-merge +mechanism (root facets union-merged into child facets by id) and become a +**binding mechanism**: a nested package binds surfaces to its subtree. Ownership +is positional and git/CODEOWNERS-shaped, not a silent field-level override. + +Consequences, all of them improvements: + +- A root edit can no longer silently break a leaf's resolved slice through merge. +- A child can no longer silently disable an inherited critical check via + `status: disabled` in a merge (`purposes.md` leak #3) — checks live on + surfaces in the contract, and the binding only points. +- "Don't nest just because files differ" (`purposes.md` leak #5) becomes + structural: you nest to bind a different surface, not to override data. + +## What this buys + +- The monorepo-root case stops being a contradiction: many bindings, one + contract, one coordinate space. Opening the repo at root and editing a + checkout file resolves to the checkout surface via its binding. +- The portable contract is genuinely shippable: no binding, no git assumptions, + works over npm or a resource id for outcomes 3 & 4. +- Path matching has exactly one home (the binding), so the contract, selection, + and governance never re-grow their own path matchers (the Leak B/C instinct). + +## Open forks (decide before code) + +1. **Contract reference resolution.** `contract: .` (in-repo, the common case) + is trivial. Cross-repo references (npm name, resource id) need a resolution + contract and version pinning. Recommendation: ship in-repo `contract: .` + first; defer external references to their own note. `ack` / `track` already + model stance toward a moving reference and may supply the versioning + machinery. +2. **Implicit vs. explicit root contract for `core` fallback.** When a path has + no binding, does it resolve to root `core`, or to "no surface → menu"? + Recommendation: if a root contract exists, unbound paths resolve to `core` + (brand-wide checks still apply); if none exists, return the menu. +3. **Does a scoped `.ghost/` redeclare surfaces, or only bind existing ones?** + Recommendation: a binding references surface ids that exist in the root + contract; it does not define new surfaces. One source of truth for the tree + (the schema note's flat-id, single-`parent` discipline extends here). + +## What stays stable + +Hold these contracts while binding is explored: `ghost.fingerprint/v1`, +`ghost.validate/v1`, `ghost.fingerprint-package/v1`, `ghost.check-report/v1`, +and the proposed `ghost.surfaces/v1`. `ghost.binding/v1` is new and additive; +absent any binding, the contract behaves exactly as the no-repo case. + +## A caution worth recording + +This is the **least proof-validated layer** of the redesign. The live +composition proof case resolves context from a prompt with no repo binding at +all — it exercises the contract and the prompt road, not the path or diff roads. +So the binding is designed from outcomes 1 & 2 reasoning, not yet from a running +proof. Treat its first implementation as a hypothesis to validate against a real +in-repo case, and prefer the smallest version (directory-default binding, +in-repo `contract: .`, defer external references) until a working tree exercises +it. + +## Not a plan + +This note settles the binding shape (directory-default, explicit escape hatch), +the resolution roads, and the home for Leak E. It writes no Zod, renames no +command, and moves no field today. Implementation — the `ghost.binding/v1` +loader, path→surface resolution, diff→surfaces for the PR gate — is the next +cut, and it sits behind the surface-schema implementation it depends on. + +## Read-back + +This note succeeds if: + +- The contract still carries no paths; the binding owns all path matching. +- The no-repo cases need no binding, and the repo cases add one without changing + the contract. +- Path, prompt, and diff all resolve to a surface id through one resolver. +- Nesting is reframed from data-merge to binding, retiring Leak E. +- The honest caution is recorded: this layer is real but the least validated by + the live proof case, so it ships smallest-first. From b0897830658d0377a74f8a49c4414d167012ba8c Mon Sep 17 00:00:00 2001 From: Nahiyan Khan Date: Thu, 25 Jun 2026 14:46:15 -0400 Subject: [PATCH 7/9] docs(implementation-plan): sequence the hard-cutover surface-model build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plan the breaking change as eight dependency-ordered phases, each a green, committed cut: (0) freeze/baseline, (1) ghost.surfaces/v1 schema, (2) surfaces lint, (3) placement on nodes — the breaking line, removing topology/applies_to/ surface_type/scope, (4) delete ghost.map/v1, (5) slice resolver + menu (prompt road), (6) one-shot migration command + migrate this repo's .ghost/, (7) ghost.binding/v1 path/diff roads + retire child-wins-by-id (Leak E), (8) command/ skill/docs reconciliation. Measured blast radius (~38 src, ~16 map, ~20 test files). Additive phases 1-2 land first to de-risk; least-validated binding lands last. Records open planning decisions (relay survival, migrator permanence, delete-list commands). --- docs/ideas/README.md | 5 + docs/ideas/implementation-plan.md | 208 ++++++++++++++++++++++++++++++ 2 files changed, 213 insertions(+) create mode 100644 docs/ideas/implementation-plan.md diff --git a/docs/ideas/README.md b/docs/ideas/README.md index b9f113bd..074f2acc 100644 --- a/docs/ideas/README.md +++ b/docs/ideas/README.md @@ -48,6 +48,11 @@ buildable Layer 2 design. They agree; read them as a sequence. the escape hatch. Path / prompt / diff all resolve to a surface id through one resolver; nesting is reframed from data-merge to binding (retiring Leak E). Records that this is the least proof-validated layer, so it ships smallest-first. +- `implementation-plan.md` — sequences the hard-cutover (breaking, no parallel + model) build in dependency order across eight phases: schema → lint → + placement → delete `ghost.map/v1` → resolver/menu → migration command → + binding → command/skill/docs reconciliation. Marks Phase 3 (removing node + coordinate fields) as the breaking line, with additive Phases 1–2 landed first. ## Independent, still live diff --git a/docs/ideas/implementation-plan.md b/docs/ideas/implementation-plan.md new file mode 100644 index 00000000..111f9c1d --- /dev/null +++ b/docs/ideas/implementation-plan.md @@ -0,0 +1,208 @@ +--- +status: exploring +--- + +# Implementation plan: the surface-model cutover + +This note sequences the implementation of the surface model designed across +`reset.md`, `coordinate-space.md`, `surface-schema.md`, and `surface-binding.md`. +It is subordinate to `fingerprint-first-architecture.md` (settled). It schedules +work; it does not redesign anything. + +## Stance: hard cutover, breaking change + +Decided: **one hard breaking change, no parallel old/new model.** `topology`, +`applies_to`, `surface_type`/`scope` on nodes, `ghost.map/v1`, and +nesting-as-merge are removed, not deprecated-alongside. Rationale: + +- Ghost is pre-1.0 (`0.18.0`). One major bump is the cheapest moment to break. +- Carrying both models during a migration window is its own sprawl — the exact + thing the reset exists to end. A clean break is less total work than a bridge. +- One published package (`@anarchitecture/ghost`); a single `major` changeset + covers the whole cutover. + +The cost this accepts: any existing `.ghost/` package with `topology` / +`applies_to` / `surface_type` / `scope` must migrate. We provide a one-shot +migration command (Phase 6) and accept that old fingerprints fail `lint` loudly +until migrated. No silent compatibility shim. + +## Blast radius (measured) + +- ~38 `src` files touch `topology` / `applies_to` / `surface_type` / `scope`. +- ~16 `src` files touch `ghost.map/v1` (the entire `ghost-core/map/` module + plus its consumers in `scan`, `checks`, `core`). +- ~20 test files reference dead fields or the dead `relay` / `survey` surface. +- 19 CLI commands; several (`relay`, `survey`, `diff`, `describe`, `stack`) are + on the delete list from earlier notes and should be reconfirmed against the + new model rather than ported. + +This is large but bounded, and concentrated: `fingerprint/{schema,types,lint}`, +`scan/fingerprint-stack`, `context/*`, and `checks/*` carry most of it. + +## Sequencing principle + +Each phase is one PR-sized cut, lands green (`pnpm check` + `pnpm test`), and is +committed before the next starts. No two phases open at once — the same +discipline that kept the design notes clean. The order is **dependency-driven**: +schema before lint before loader before consumers before resolver before +binding. Nothing downstream is touched until its upstream lands. + +## Phases + +### Phase 0 — freeze and baseline + +- Tag the current green state on the branch (pre-cutover reference). +- Snapshot the current public-export surface (`test/public-exports.test.ts`) so + the breaking diff is explicit and reviewable. +- Write the `major` changeset stub now, filled in as phases land. + +No behavior change. This phase makes the break auditable. + +### Phase 1 — `ghost.surfaces/v1` schema module + +- New module `ghost-core/surfaces/` (`schema.ts`, `types.ts`, `index.ts`), + mirroring the `fingerprint/` module layout. +- Zod for `surfaces.yml`: `surfaces[]` with `id` (flat slug, no dots), optional + `description`, optional single `parent`, optional `edges[]` (`kind` from the + fixed Ghost-owned set, `to` an existing id). `edge_kinds` is a code constant, + not package data. +- Export from `@anarchitecture/ghost/core`. +- Tests: schema accepts the valid shape, rejects dotted ids, parent arrays, + unknown edge kinds. + +Depends on: nothing. Pure addition. Lands without touching existing behavior. + +### Phase 2 — surfaces lint + tree/graph validation + +- `ghost-core/surfaces/lint.ts`: parent references exist, no cycles (tree), + single parent, edge `to` references exist, edge `kind` is known, near-miss id + warnings, `core` reserved as implicit root. +- Composition edges may form a graph (cross-links allowed); only `parent` is + tree-constrained. +- Wire into `ghost lint` for `surfaces.yml`. +- Tests: each lint rule, valid graph passes, cyclic parent fails. + +Depends on: Phase 1. + +### Phase 3 — placement on description nodes + +- Remove `applies_to` from principles / patterns / experience_contracts; remove + `surface_type` / `scope` from exemplars and situations; remove + `topology` from `inventory`. +- Add a single optional `surface: ` placement key to each placeable node. +- Lint: placement references an existing surface; un-placed node **warns and + teaches** (never silent `core`); near-miss id warns. +- Update `fingerprint/{schema,types,lint}.ts` and `checks/schema.ts` (drop + `check.applies_to`, add `check.surface`). +- Tests: placement validation; the warn-not-error behavior; removed fields now + rejected by `.strict()`. + +Depends on: Phase 1–2. **This is the breaking core** — the schema no longer +accepts the old coordinate fields. + +### Phase 4 — delete `ghost.map/v1` + +- Remove `ghost-core/map/` entirely and its consumers' map dependencies in + `scan/` (`fingerprint-package`, `inventory`, `file-kind`, `lint-map`, + `scan-status`, `fingerprint-stack`), `checks/` (`routing`, `lint`, `types`), + and `core/` (`check`, `scope-resolver`). +- Replace map-derived scope resolution with surface resolution from Phase 1–3. +- Remove `map.md` handling and `MAP_FILENAME`. +- Tests: delete `map-scopes.test.ts` and `scope-resolver.test.ts` map paths; + retarget any still-valid assertions to surfaces. + +Depends on: Phase 3 (surfaces must own scope resolution before map is removed). + +### Phase 5 — slice resolver + menu emitter (Layer 3, prompt road) + +- Resolver: given a surface id, compose its slice = own placed nodes + cascaded + ancestor nodes + typed-edge contributions. Deterministic, no LLM. +- Menu emitter: surfaces + descriptions for the host agent to match against. +- Ambiguity returns the menu, never a whole-tree dump. +- Replace the old `context/entrypoint.ts` `CAPS` truncation and + `globalFallbackRefs` with surface-scoped composition. +- Tests: cascade correctness, edge contribution, menu shape, ambiguity → menu. + +Depends on: Phase 3–4. This is where selection stops improvising around a +missing map. + +### Phase 6 — migration command + +- `ghost migrate` (or `ghost surfaces init --from-legacy`): one-shot transform + of an existing `.ghost/` — derive `surfaces.yml` from old `topology.scopes`, + rewrite node `applies_to` / `surface_type` / `scope` into `surface:` + placement. Best-effort, prints what it could not place for human review. +- Migrate this repo's own dogfood `.ghost/` with it (the worked example in + `surface-schema.md`), and commit the migrated package. +- Tests: a legacy fixture migrates to a valid `surfaces.yml` + placed nodes. + +Depends on: Phase 1–5. The migrator needs the target shape to exist. + +### Phase 7 — `ghost.binding/v1` (path road + diff road) + +- New `ghost-core/binding/` schema + loader for `.ghost.bind.yml`. +- Reframe nested-package resolution from data-merge to binding: nearest binding + along a path names the surface; explicit `.ghost.bind.yml` overrides + directory-implied binding at its level. +- Wire path → surface and diff → surfaces into `check` / `review` (Layer 4) and + the path road of selection (Layer 3). +- Retire `child-wins-by-id` merge (`scan/fingerprint-stack.ts`) — Leak E. +- Ship smallest first: directory-default binding + in-repo `contract: .`; defer + external contract references. +- Tests: path resolves to nearest binding; diff → union of surfaces; no-binding + path → `core` when a root contract exists. + +Depends on: Phase 1–6. Lands last because it is the **least proof-validated** +layer (`surface-binding.md` caution) and depends on everything above. + +### Phase 8 — command + skill + docs reconciliation + +- Reconfirm delete-list commands against the new model: `relay`, `survey`, + `diff`, `describe`, `stack` — remove or rebuild, do not port blindly. +- Update the skill bundle references to teach placement + surfaces (the + `voice.md` fix was a preview of this). +- Regenerate the CLI manifest (`pnpm dump:cli-help`). +- Fill in the `major` changeset. +- Tests: `cli.test.ts`, `public-exports.test.ts` updated to the new surface. + +Depends on: Phase 1–7. + +## What lands when (the cutover line) + +Phases 1–2 are **additive and safe** — they can land without breaking anything. +**Phase 3 is the breaking line**: once node coordinate fields are removed, old +fingerprints fail lint. Everything from Phase 3 on is part of the single major +release. Plan to land 1–2 first to de-risk, then 3–8 as the breaking sequence. + +## Open decisions for planning + +1. **One mega-PR vs. phased merges to the branch.** Recommendation: phased + commits on `reset-coordinate-space` (as we have been), squash-reviewed as one + major release. Keeps each cut reviewable without shipping a half-cut model. +2. **`ghost migrate` permanence.** Is the migrator a permanent command or a + one-release transitional tool removed in the next major? Recommendation: + transitional, documented as such. +3. **Commands on the delete list.** `diff` / `describe` / `stack` / `survey` / + `relay` — confirm each is delete vs. rebuild *before* Phase 8, ideally noted + now so Phase 8 is execution not decision. +4. **Does `relay` survive at all?** The prompt road (Phase 5) replaces most of + what `relay gather` did. Decide whether `relay` is renamed, absorbed into a + new `gather`/`select` command, or removed. + +## Not the work itself + +This note sequences the cutover. It writes no code. Each phase becomes its own +commit (or small PR) on the branch, lands green, and is checked off here as it +completes. Implementation starts at Phase 0. + +## Read-back + +This plan succeeds if: + +- The cutover is one breaking major, not a compatibility bridge. +- Phases are dependency-ordered: schema → lint → placement → map-delete → + resolver → migrate → binding → commands. +- The breaking line (Phase 3) is explicit, with safe additive work (1–2) landed + first. +- Every phase lands green and committed before the next begins. +- The least-validated layer (binding) lands last. From 2222830f5bdbbf10f95695e9c93bb9e6e70d7848 Mon Sep 17 00:00:00 2001 From: Nahiyan Khan Date: Thu, 25 Jun 2026 14:55:46 -0400 Subject: [PATCH 8/9] docs(implementation-plan): settle command fate via the desire-survives test A command's desire survives if the new model serves it; its implementation survives only if it already is that. Relay's desire (right narrow context, right time, traceable) is realized by the Phase 5 resolver; relay's implementation (relay-config, request_resolvers, sources, ghost.relay-request/v1) is the second routing system on the delete list. So relay/stack/survey/diff/describe are deleted: relay and stack absorbed into a new gather/select command shipped in Phase 5, the rest dead. Phase 5 ships the new context command on the resolver (not old relay plumbing); Phase 8 deletes the dead commands as execution, not decision. Collapses three open planning decisions into one. --- docs/ideas/implementation-plan.md | 46 +++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/docs/ideas/implementation-plan.md b/docs/ideas/implementation-plan.md index 111f9c1d..e9d9ba36 100644 --- a/docs/ideas/implementation-plan.md +++ b/docs/ideas/implementation-plan.md @@ -113,12 +113,15 @@ accepts the old coordinate fields. Depends on: Phase 3 (surfaces must own scope resolution before map is removed). -### Phase 5 — slice resolver + menu emitter (Layer 3, prompt road) +### Phase 5 — slice resolver + menu emitter, as the new gather command (Layer 3, prompt road) - Resolver: given a surface id, compose its slice = own placed nodes + cascaded ancestor nodes + typed-edge contributions. Deterministic, no LLM. - Menu emitter: surfaces + descriptions for the host agent to match against. - Ambiguity returns the menu, never a whole-tree dump. +- **Ship this as the new context-gathering command** (working name `gather` / + `select`): relay's *desire* done right (see "Command fate"). Do not build it + on the old relay-config / request-resolution plumbing. - Replace the old `context/entrypoint.ts` `CAPS` truncation and `globalFallbackRefs` with surface-scoped composition. - Tests: cascade correctness, edge contribution, menu shape, ambiguity → menu. @@ -157,8 +160,10 @@ layer (`surface-binding.md` caution) and depends on everything above. ### Phase 8 — command + skill + docs reconciliation -- Reconfirm delete-list commands against the new model: `relay`, `survey`, - `diff`, `describe`, `stack` — remove or rebuild, do not port blindly. +- Delete the absorbed/dead commands per "Command fate" — `relay`, `stack`, + `survey`, `diff`, `describe` — and remove the relay-config loader, + request-resolution, and request-stack modules in `context/`. This is execution, + not decision. - Update the skill bundle references to teach placement + surfaces (the `voice.md` fix was a preview of this). - Regenerate the CLI manifest (`pnpm dump:cli-help`). @@ -174,6 +179,33 @@ Phases 1–2 are **additive and safe** — they can land without breaking anythi fingerprints fail lint. Everything from Phase 3 on is part of the single major release. Plan to land 1–2 first to de-risk, then 3–8 as the breaking sequence. +## Command fate: the desire-survives test (settled) + +Decided by one rule: **a command's desire survives if the new model serves it; the +command's implementation survives only if it already is that.** Relay the desire +("give an agent the right narrow context at the right time, traceably") is the +whole point of the coordinate space and is realized correctly by the Phase 5 +resolver. Relay the implementation (`relay-config`, `request_resolvers`, +`sources`, `ghost.relay-request/v1`, the selector-routing facet) is the second +routing system on the delete list. So the *desire* lives on under a truer name; +the *mechanism* dies. + +Applying the test to the whole delete list: + +| Command | Desire survives as | Implementation | +| --- | --- | --- | +| `relay` | Phase 5 slice resolver + menu emitter | **deleted** (absorbed into a new `gather` / `select` command) | +| `stack` | path → surface (Phase 7 binding) | **deleted** (absorbed) | +| `survey` | nothing in the new model | **deleted** | +| `diff` | the dead direct-markdown path | **deleted** | +| `describe` | the dead direct-markdown path | **deleted** | + +Consequence for sequencing: **Phase 5 ships the new context-gathering command** +(working name `gather` or `select`) as relay's desire done right, and **Phase 8 +deletes `relay` / `stack` / `survey` / `diff` / `describe` as execution, not +decision.** The relay config loader, request-resolution, and request-stack +modules in `context/` are removed with `relay`. + ## Open decisions for planning 1. **One mega-PR vs. phased merges to the branch.** Recommendation: phased @@ -182,12 +214,8 @@ release. Plan to land 1–2 first to de-risk, then 3–8 as the breaking sequenc 2. **`ghost migrate` permanence.** Is the migrator a permanent command or a one-release transitional tool removed in the next major? Recommendation: transitional, documented as such. -3. **Commands on the delete list.** `diff` / `describe` / `stack` / `survey` / - `relay` — confirm each is delete vs. rebuild *before* Phase 8, ideally noted - now so Phase 8 is execution not decision. -4. **Does `relay` survive at all?** The prompt road (Phase 5) replaces most of - what `relay gather` did. Decide whether `relay` is renamed, absorbed into a - new `gather`/`select` command, or removed. +3. **New command name.** `gather` vs. `select` vs. keeping `gather` as a verb on + a renamed noun. Cosmetic; decide at Phase 5. ## Not the work itself From 576dcb922a4fb9e29cc2a5cbb2ca248833ca471a Mon Sep 17 00:00:00 2001 From: Nahiyan Khan Date: Thu, 25 Jun 2026 15:00:58 -0400 Subject: [PATCH 9/9] docs(phase-1-plan): execution spec for the ghost.surfaces/v1 schema module First implementation cut, purely additive. Specs a new ghost-core/surfaces/ module mirroring fingerprint/ (types, schema, index) plus a schema test and one re-export in ghost-core/index.ts. Schema bans dotted ids via a dot-excluding slug regex (the tree lives only in parent); single parent falls out of parent being scalar; edge kinds restricted to the fixed Ghost-owned set. Draws an explicit schema/lint boundary: graph-level checks (cycles, dangling parent/edge refs, near-miss, reserved core) are deferred to Phase 2 lint, documented in a test case so they are not fixed in the wrong layer. Out-of-scope and acceptance criteria are enumerated; one commit, no changeset (no user-visible behavior). --- docs/ideas/README.md | 4 + docs/ideas/phase-1-plan.md | 227 +++++++++++++++++++++++++++++++++++++ 2 files changed, 231 insertions(+) create mode 100644 docs/ideas/phase-1-plan.md diff --git a/docs/ideas/README.md b/docs/ideas/README.md index 074f2acc..a3b5b9d9 100644 --- a/docs/ideas/README.md +++ b/docs/ideas/README.md @@ -53,6 +53,10 @@ buildable Layer 2 design. They agree; read them as a sequence. placement → delete `ghost.map/v1` → resolver/menu → migration command → binding → command/skill/docs reconciliation. Marks Phase 3 (removing node coordinate fields) as the breaking line, with additive Phases 1–2 landed first. +- `phase-1-plan.md` — execution spec for Phase 1: the additive + `ghost-core/surfaces/` module (`ghost.surfaces/v1` schema + types + index + + tests), mirroring the `fingerprint/` module. Bans dotted ids at the schema + layer; defers all graph-level validation (cycles, dangling refs) to Phase 2. ## Independent, still live diff --git a/docs/ideas/phase-1-plan.md b/docs/ideas/phase-1-plan.md new file mode 100644 index 00000000..dcef6a8f --- /dev/null +++ b/docs/ideas/phase-1-plan.md @@ -0,0 +1,227 @@ +--- +status: exploring +--- + +# Phase 1 plan: the `ghost.surfaces/v1` schema module + +This note is the execution spec for Phase 1 of `implementation-plan.md`. It is +the first line of real code in the surface-model cutover. Phase 1 is **purely +additive**: it introduces a new module and changes no existing behavior, so it +lands green without breaking anything (the breaking line is Phase 3). + +Scope is deliberately narrow: schema + types + a thin module export + tests. +**Lint validation (cycles, dangling refs) is Phase 2, not here.** Phase 1 only +proves the shape parses and the basic Zod constraints hold. + +## Deliverable + +A new module `packages/ghost/src/ghost-core/surfaces/` that mirrors the existing +`ghost-core/fingerprint/` module layout: + +```text +ghost-core/surfaces/ + types.ts # constants, TS interfaces, lint report types + schema.ts # Zod schema for surfaces.yml + index.ts # public re-exports (mirrors fingerprint/index.ts) +``` + +Plus a test file `packages/ghost/test/ghost-core/surfaces-schema.test.ts`, and a +one-line addition to `ghost-core/index.ts` re-exporting the new module under a +`// --- Surfaces (ghost.surfaces/v1) ---` section header (matching the existing +section-comment convention). + +No CLI wiring, no loader, no consumers. Those are later phases. + +## `types.ts` + +Constants and interfaces, following `fingerprint/types.ts` conventions exactly. + +```ts +export const GHOST_SURFACES_SCHEMA = "ghost.surfaces/v1" as const; +export const GHOST_SURFACES_YML_FILENAME = "surfaces.yml" as const; + +// The fixed, Ghost-owned edge vocabulary (surface-schema.md: closed set). +// A code constant, never package data. +export const GHOST_SURFACE_EDGE_KINDS = ["composes", "governed-by"] as const; +export type GhostSurfaceEdgeKind = (typeof GHOST_SURFACE_EDGE_KINDS)[number]; + +export const GHOST_SURFACE_ROOT_ID = "core" as const; + +export interface GhostSurfaceEdge { + kind: GhostSurfaceEdgeKind; + to: string; +} + +export interface GhostSurface { + id: string; + description?: string; + parent?: string; + edges?: GhostSurfaceEdge[]; +} + +export interface GhostSurfacesDocument { + schema: typeof GHOST_SURFACES_SCHEMA; + surfaces: GhostSurface[]; +} + +// Lint report types reuse the fingerprint 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[]; +} +``` + +## `schema.ts` + +Zod, following `fingerprint/schema.ts` conventions (`.strict()`, slug regex +reused, descriptive error messages). + +```ts +import { z } from "zod"; +import { + GHOST_SURFACE_EDGE_KINDS, + GHOST_SURFACES_SCHEMA, +} from "./types.js"; + +// Flat slug, NO dots-as-hierarchy. surface-schema.md: the tree lives only in +// `parent`; a dotted id would be a second, conflicting source of truth. +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 SurfaceEdgeSchema = z + .object({ + kind: z.enum(GHOST_SURFACE_EDGE_KINDS), + to: SurfaceIdSchema, + }) + .strict(); + +const SurfaceSchema = z + .object({ + id: SurfaceIdSchema, + description: z.string().min(1).optional(), + parent: SurfaceIdSchema.optional(), + edges: z.array(SurfaceEdgeSchema).optional(), + }) + .strict(); + +export const GhostSurfacesSchema = z + .object({ + schema: z.literal(GHOST_SURFACES_SCHEMA), + surfaces: z.array(SurfaceSchema).optional().default([]), + }) + .strict(); +``` + +Note the **deliberate boundary**: the slug regex *excludes the dot* (`.`), which +is what mechanically bans dotted-id hierarchy at the schema layer. That is a +Phase 1 guarantee. Structural rules that need the whole document — parent +references an existing id, no cycles, edge `to` exists, single root — are +**graph-level checks deferred to Phase 2 lint**, because Zod validates a node in +isolation and cannot see the rest of the tree. + +## `index.ts` + +Mirror `fingerprint/index.ts`: re-export schema, types, and constants. (No lint +export yet — that is Phase 2.) + +```ts +export { GhostSurfacesSchema } from "./schema.js"; +export { + GHOST_SURFACE_EDGE_KINDS, + GHOST_SURFACE_ROOT_ID, + GHOST_SURFACES_SCHEMA, + GHOST_SURFACES_YML_FILENAME, + type GhostSurface, + type GhostSurfaceEdge, + type GhostSurfaceEdgeKind, + type GhostSurfacesDocument, + type GhostSurfacesLintIssue, + type GhostSurfacesLintReport, + type GhostSurfacesLintSeverity, +} from "./types.js"; +``` + +And in `ghost-core/index.ts`, add under a new section comment: + +```ts +// --- Surfaces (ghost.surfaces/v1) --- +export { + GhostSurfacesSchema, + GHOST_SURFACES_SCHEMA, + GHOST_SURFACES_YML_FILENAME, + GHOST_SURFACE_EDGE_KINDS, + GHOST_SURFACE_ROOT_ID, + type GhostSurface, + type GhostSurfaceEdge, + type GhostSurfaceEdgeKind, + type GhostSurfacesDocument, +} from "./surfaces/index.js"; +``` + +## Tests + +`test/ghost-core/surfaces-schema.test.ts`, following +`fingerprint-yml-schema.test.ts` style. Cases: + +- **Accepts** a minimal document (`{ schema, surfaces: [] }`) and defaults + `surfaces` to `[]` when absent. +- **Accepts** a realistic tree: `core`, `email` (parent `core`), + `email-marketing` (parent `email`), `checkout` with two typed edges. +- **Rejects** a dotted id (`email.marketing`) with the slug message. +- **Rejects** a parent given as an array (single parent only — falls out of + `parent` being a scalar; assert the strict parse fails). +- **Rejects** an unknown edge kind (`kind: see-also`). +- **Rejects** an unknown top-level key (`.strict()` guard). +- **Accepts** an edge `to` that does not exist as a surface — and a comment in + the test notes this is intentionally a **Phase 2 lint** concern, not a schema + concern. This documents the schema/lint boundary so a future contributor does + not "fix" it in the wrong layer. + +## Acceptance + +Phase 1 is done when: + +- `pnpm build` and `pnpm typecheck` pass with the new module. +- `pnpm test` passes including the new test file. +- `pnpm check` passes (biome, file-size, terminology, docs, cli-manifest — the + manifest is unchanged because no CLI command was added). +- `@anarchitecture/ghost/core` exports the new symbols (extend + `public-exports.test.ts` only if it asserts core symbols; otherwise leave it). +- Nothing in existing behavior changed: no existing file's logic is edited, only + `ghost-core/index.ts` gains export lines. + +## Out of scope (explicitly) + +- Lint / graph validation (cycles, dangling parent/edge refs, near-miss ids, + reserved `core`) → **Phase 2**. +- Loading `surfaces.yml` from disk, CLI `lint`/`verify` wiring → Phase 2+. +- Node `surface:` placement on description facets → **Phase 3** (the breaking + line). +- Any removal of `topology` / `applies_to` / `ghost.map/v1` → Phase 3–4. + +## Commit + +One commit: `feat(surfaces): add ghost.surfaces/v1 schema module (additive)`. +No changeset yet — Phase 1 ships no user-visible behavior; the `major` changeset +is assembled across the breaking phases (3+) per `implementation-plan.md` +Phase 0. + +## Read-back + +Phase 1 succeeds if a contributor can import `GhostSurfacesSchema` from +`@anarchitecture/ghost/core`, parse a valid `surfaces.yml` shape, see dotted ids +and unknown edge kinds rejected at the schema layer, and understand from the +tests exactly which validation is deferred to Phase 2 lint — all without any +existing behavior changing.