diff --git a/.changeset/draft-fingerprint-boundary.md b/.changeset/draft-fingerprint-boundary.md new file mode 100644 index 00000000..a58fad5b --- /dev/null +++ b/.changeset/draft-fingerprint-boundary.md @@ -0,0 +1,5 @@ +--- +"@anarchitecture/ghost": major +--- + +Make checked-in fingerprint.yml entries canonical, remove fingerprint entry lifecycle fields and proposals, add first-class exemplars, and emit context bundles as prose + inventory + exemplars generation packets. diff --git a/.changeset/thresholded-proposal-guidance.md b/.changeset/thresholded-proposal-guidance.md deleted file mode 100644 index 66c913af..00000000 --- a/.changeset/thresholded-proposal-guidance.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@anarchitecture/ghost": patch ---- - -Clarify proposal and silent-memory guidance so agents threshold durable memory candidates and label provisional local reasoning. diff --git a/.ghost/fingerprint.yml b/.ghost/fingerprint.yml index f01ff6cd..aa79ac22 100644 --- a/.ghost/fingerprint.yml +++ b/.ghost/fingerprint.yml @@ -8,10 +8,12 @@ summary: goals: - Keep product-experience memory repo-local, portable, and easy for agents to read. - Preserve product identity across generation, review, and remediation. - - Separate durable judgment from generated inventory and cache. + - Use Git as the staging and approval boundary for memory edits. anti_goals: - Treat raw inventory as canonical product truth. + - Add special lifecycle files for the MVP. - Turn Ghost into a design-system snapshot or screenshot archive. + - Become a memory lifecycle manager or proposal system. - Let advisory review block work without deterministic checks. tradeoffs: - Prefer compact durable memory over exhaustive surveys. @@ -61,13 +63,27 @@ topology: - primitive-demo - theme-control - tool-doc - examples: - - path: README.md - surface_type: docs-home - note: Public entry point must describe fingerprint.yml as canonical memory. - - path: packages/ghost/src/scan/context/package-review-command.ts - surface_type: emitted-agent-prompt - note: Review command is generated from fingerprint.yml memory. +exemplars: + - id: public-readme-memory-model + path: README.md + title: Public README memory model + surface_type: docs-home + scope: docs-site + note: Public entry point describes fingerprint.yml as canonical memory. + why: Shows how Ghost explains repo-local prose memory, optional inventory, and Git-reviewed approval. + refs: + - principle:fingerprint-is-canonical + - principle:inventory-is-cache + - id: review-command-memory-packet + path: packages/ghost/src/scan/context/package-review-command.ts + title: Generated review command + surface_type: emitted-agent-prompt + scope: public-cli + note: Review command is generated from fingerprint.yml memory. + why: Shows how Ghost turns canonical memory into an agent-facing review instruction. + refs: + - experience_contract:review-cites-memory-and-diff + - pattern:compact-agent-handoff situations: - id: capturing-memory title: Capturing repo memory @@ -77,7 +93,7 @@ situations: - principle:fingerprint-is-canonical - principle:inventory-is-cache experience_contracts: - - experience_contract:agents-propose-before-promoting + - experience_contract:git-is-approval-boundary patterns: - pattern:fingerprint-first-bundle - id: reviewing-generated-ui @@ -100,36 +116,35 @@ situations: - pattern:editorial-workbench-docs principles: - id: fingerprint-is-canonical - status: accepted - principle: fingerprint.yml is canonical product-experience memory; other generated artifacts are source material or output. + principle: fingerprint.yml is canonical product-experience memory when checked in; generated artifacts are source material or output. guidance: - Describe the fingerprint as product experience memory, not a design-system snapshot. - Keep durable identity, hierarchy, behavior, copy, accessibility, trust, and flow in fingerprint.yml. + - Treat uncommitted or unmerged memory edits as drafts handled by Git, not by a Ghost-specific lifecycle. evidence: - path: docs/fingerprint-format.md note: Public format docs define fingerprint.yml as the source of truth. - path: packages/ghost/src/scan/fingerprint-package.ts - note: init writes fingerprint.yml, checks.yml, proposals, and cache. + note: init writes fingerprint.yml and checks.yml; cache appears only when inventory is explicitly generated. - id: inventory-is-cache - status: accepted principle: Generated inventory answers what exists; fingerprint memory answers what matters and why. guidance: - Store generated inventory under cache when useful. - - Promote only durable conclusions into fingerprint.yml. + - Curate only durable conclusions into fingerprint.yml. + - Do not let cache or implementation vocabulary become product authority. evidence: - path: README.md note: README frames inventory as optional cache/source material. - id: checks-are-executable-appendix - status: accepted principle: Deterministic checks stay outside fingerprint.yml and must be grounded in typed memory refs. guidance: - Active checks can block; advisory findings cannot block unless check-backed. - Use typed refs such as pattern:token-first-interface. + - Checks keep enforcement status because gates need lifecycle state. evidence: - path: packages/ghost/src/ghost-core/checks/lint.ts note: Active checks require typed derives_from grounding. - id: oss-language-is-portable - status: accepted principle: Public Ghost docs and generated prompts should work for arbitrary OSS repos without internal strategy language. guidance: - Use examples that fit docs sites, dashboards, SaaS apps, games, and component libraries. @@ -139,7 +154,6 @@ principles: - path: docs/generation-loop.md experience_contracts: - id: review-cites-memory-and-diff - status: accepted contract: Advisory review findings must cite the diff location and the relevant fingerprint memory. obligations: - Use active checks only when a finding should block. @@ -147,16 +161,15 @@ experience_contracts: evidence: - path: packages/ghost/src/scan/context/package-review-command.ts note: Emitted review command tells agents what to cite. - - id: agents-propose-before-promoting - status: accepted - contract: Agents create proposals for gaps or intentional divergence; humans promote durable memory. + - id: git-is-approval-boundary + contract: Memory edits use ordinary Git workflow; checked-in fingerprint.yml is truth. obligations: - Do not rewrite canonical memory silently during generation or review. - - Use proposals for missing-memory, intentional-divergence, experience-gap, and check-candidate cases. + - Treat uncommitted or unmerged memory edits as draft work. + - Record durable product memory in fingerprint.yml and deterministic gates in checks.yml; optional rationale files are secondary. evidence: - - path: packages/ghost/src/skill-bundle/references/propose.md + - path: packages/ghost/src/skill-bundle/references/capture.md - id: emitted-context-prefers-memory - status: accepted contract: Emitted context bundles and review commands should prefer fingerprint.yml over legacy survey or markdown artifacts. obligations: - Default emit paths load fingerprint.yml package memory. @@ -165,7 +178,6 @@ experience_contracts: - path: packages/ghost/src/scan/context/package-writer.ts - path: packages/ghost/src/scan-emit-command.ts - id: interfaces-preserve-operability - status: accepted contract: Ghost interfaces should preserve operability, readable examples, and responsive access before visual polish. obligations: - Preserve keyboard-reachable controls and readable code examples. @@ -177,26 +189,26 @@ experience_contracts: - path: apps/docs/src/components/docs/component-page-shell.tsx patterns: - id: fingerprint-first-bundle - status: accepted kind: composition - pattern: Root Ghost bundles start from fingerprint.yml, then attach checks, proposals, decisions, intent, and cache only when useful. + pattern: Root Ghost bundles start from fingerprint.yml and checks.yml, then attach optional rationale, config, or cache only when useful. guidance: - Do not require survey or inventory for a valid new project. + - Do not create cache during initialization; cache appears only after explicit inventory work. + - Allow sparse fingerprint.yml authoring; omitted top-level sections normalize to empty memory internally. - Keep optional cache outside canonical memory. + - Use Git review for memory approval instead of Ghost-specific draft files. evidence: - path: README.md - path: docs/fingerprint-format.md - id: compact-agent-handoff - status: accepted kind: content pattern: CLI and emitted prompts should tell the host agent exactly which memory to read and which findings can block. guidance: - Prefer short ordered workflows over broad conceptual lectures. - - Name proposal categories when memory is missing or contradictory. + - Name memory gaps plainly without creating a separate memory-change workflow. evidence: - path: packages/ghost/src/scan/context/package-review-command.ts - id: editorial-workbench-docs - status: accepted kind: visual pattern: Ghost docs combine restrained editorial explanation with concrete command and schema work surfaces. applies_to: @@ -215,7 +227,6 @@ patterns: - path: apps/docs/src/components/docs/page-header.tsx - path: apps/docs/src/components/docs/component-page-shell.tsx - id: token-first-interface - status: accepted kind: visual pattern: Ghost UI surfaces use semantic tokens for repeatable color and avoid ad hoc surface hex values. applies_to: @@ -255,15 +266,3 @@ implementation_vocabulary: notes: - Implementation vocabulary is replaceable material; product memory owns the judgment. - Correct components or tokens do not prove an experience is on-brand. -review_policy: - proposal_policy: - - Agents create proposals for missing memory, intentional divergences, experience gaps, and check candidates. - - Humans promote durable memory into fingerprint.yml or checks.yml. - experience_gap_categories: - - missing-memory - - intentional-divergence - - experience-gap - - check-candidate - memory_gap_policy: - - Report uncertainty instead of inventing product truth. - - Treat repeated composition failures as product signal. diff --git a/.ghost/intent.md b/.ghost/intent.md index 490d8f07..3f1eca2e 100644 --- a/.ghost/intent.md +++ b/.ghost/intent.md @@ -7,8 +7,7 @@ The package should make four boundaries easy to feel: - `fingerprint.yml` is durable product experience memory. - `checks.yml` is the deterministic appendix and only active checks can block. -- `proposals/` is where agents stage missing memory, intentional divergence, - experience gaps, and check candidates. +- Git is the staging and approval boundary for memory edits. - `cache/` is optional generated inventory; it can help capture but it is not canonical truth. @@ -16,5 +15,5 @@ For Ghost's own UI and docs, preserve the editorial/workbench tension: plain foundations, concrete command examples, restrained surfaces, strong display type where it clarifies hierarchy, compact borders, and token-first color usage. Divergence is welcome when it teaches the package something, but it -should be named through a proposal or decision instead of hidden inside +should be named through a memory edit or decision instead of hidden inside generated UI. diff --git a/.ghost/proposals/.gitkeep b/.ghost/proposals/.gitkeep deleted file mode 100644 index 8b137891..00000000 --- a/.ghost/proposals/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/CLAUDE.md b/CLAUDE.md index 7fca684d..3c1ea735 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,10 +44,14 @@ Optional memory lives beside it: - `intent.md` for human-authored or human-approved product intent. - `decisions/*.yml` for accepted/rejected product-experience rationale. -- `proposals/*.yml` for staged memory changes before promotion. - `cache/` for generated inventory. Cache answers what exists; fingerprint memory answers what matters and why. +Generation starts from product prose in `fingerprint.yml`, optional generated +inventory in `cache/`, and curated exemplars in `fingerprint.yml`. Checks remain +validation and enforcement, not generation memory. Ordinary Git review is the +approval boundary for memory edits. + Legacy `resources.yml`, `map.md`, `survey.json`, and `patterns.yml` may still appear in older repos or as migration source material. They are not canonical memory for new Ghost work. @@ -66,21 +70,21 @@ memory for new Ghost work. | Command | Description | | --- | --- | -| `ghost init` | Create `.ghost/{fingerprint.yml,checks.yml,proposals/,cache/}`. | -| `ghost scan` | Report scan state and BYOA next-step guidance. | +| `ghost init` | Create `.ghost/{fingerprint.yml,checks.yml}`. | +| `ghost scan` | Report fingerprint memory/readiness state and BYOA next-step guidance. | | `ghost inventory` | Emit raw repo signals as JSON for optional cache/source material. | | `ghost lint` | Validate a bundle or single artifact. | -| `ghost verify` | Validate fingerprint evidence paths, typed check refs, and optional memory. | +| `ghost verify` | Validate fingerprint evidence and exemplar paths, typed check refs, and optional memory. | | `ghost describe` | Print optional `intent.md` or direct markdown section ranges. | | `ghost diff` | Structural prose-level diff between direct fingerprints. | | `ghost survey ` | Legacy/cache helpers for optional `ghost.survey/v2` workflows. | | `ghost check` | Run active `ghost.checks/v1` deterministic gates against a diff. | -| `ghost review` | Emit an evidence-routed advisory review packet. | +| `ghost review` | Emit an evidence-routed advisory review packet grounded in memory, exemplars, checks, and the diff. | | `ghost compare` | Pairwise or composite comparison over bundles or direct fingerprints. | | `ghost ack` | Record stance toward the tracked fingerprint in `.ghost-sync.json`. | | `ghost track` | Shift the tracked fingerprint. | | `ghost diverge` | Declare intentional divergence on a dimension. | -| `ghost emit ` | Emit `review-command` or `context-bundle`. | +| `ghost emit ` | Emit `review-command` or the `context-bundle` generation packet. | | `ghost skill install` | Install the unified `ghost` agentskills.io bundle. | `ghost scan --format json` is deterministic handoff state for the host agent. @@ -131,8 +135,8 @@ first publish becomes `0.1.0`. - Keep publishable runtime code self-contained in `packages/ghost`; no `workspace:*` runtime dependencies in the packed public artifact. - The canonical on-disk form is `.ghost/fingerprint.yml` plus optional - `.ghost/checks.yml`, `.ghost/proposals/`, `.ghost/decisions/`, - `.ghost/intent.md`, and `.ghost/cache/`. + `.ghost/checks.yml`, `.ghost/decisions/`, `.ghost/intent.md`, and + `.ghost/cache/`. - Direct `fingerprint.md` remains only for legacy/direct compare workflows. - Skill recipes live in `packages/ghost/src/skill-bundle/references/`; install them with `ghost skill install`. diff --git a/README.md b/README.md index 5dacf263..e9f291bd 100644 --- a/README.md +++ b/README.md @@ -8,39 +8,50 @@ copy, accessibility, trust, and flow. Ghost stores that memory in a versioned `.ghost/` bundle that agents can read before generation and validate after changes. -The canonical bundle is intentionally small: - -- **`.ghost/fingerprint.yml`** is the source of truth for product experience - memory: summary, topology, situations, principles, experience contracts, - patterns, implementation vocabulary, and review policy. -- **`.ghost/config.yml`** optionally records implementation roots and - reference registries/libraries so agents know where to look without treating - reference defaults as product intent. -- **`.ghost/checks.yml`** optionally stores deterministic gates grounded in - fingerprint memory. -- **`.ghost/intent.md`** optionally records human-authored or human-approved - product intent. -- **`.ghost/decisions/*.yml`** optionally records accepted/rejected - product-experience rationale. -- **`.ghost/proposals/*.yml`** stages missing memory, intentional divergence, - experience gaps, and check candidates before human promotion. -- **`.ghost/cache/`** may hold generated inventory. Cache answers what exists; - `fingerprint.yml` answers what matters and why. +The MVP rule is intentionally small: + +- **`.ghost/fingerprint.yml`** is checked-in product-experience memory. +- **Generation uses prose + inventory + exemplars.** Fingerprint prose explains + what matters, optional inventory says what exists, and exemplars show what + good looks like. +- **`.ghost/checks.yml`** is optional deterministic enforcement grounded in + that memory. +- **Git is the approval boundary.** Uncommitted or unmerged edits are draft + work; checked-in `fingerprint.yml` memory is canonical truth for Ghost. + +`fingerprint.yml` can start sparse: + +```yaml +schema: ghost.fingerprint/v1 +``` + +Add only the sections that contain real memory. Ghost normalizes omitted +top-level sections internally, so agents and checks still receive the full +shape they expect. + +Ghost is not a memory lifecycle manager, proposal system, design-system +registry, or screenshot archive. It is a small repo-local contract agents can +read before work and deterministic tooling can validate after work. + +Optional material can sit beside the core files: + +- **`.ghost/config.yml`** routes implementation roots and reference + registries/libraries without making them product intent. +- **`.ghost/intent.md`** records human-authored or human-approved product + intent when useful. +- **`.ghost/decisions/*.yml`** records accepted/rejected product-experience + rationale when history matters. +- **`.ghost/cache/`** holds generated inventory only after you explicitly + create it. Cache answers what exists; `fingerprint.yml` answers what matters + and why. Older `resources.yml`, `map.md`, `survey.json`, and `patterns.yml` artifacts are legacy/cache material. They are not canonical Ghost memory. -Ghost also supports nested memory for real product surfaces. A repo may keep a -root `.ghost/` for broad product identity and a child bundle such as -`apps/checkout/.ghost/` for local checkout rules. For a file under -`apps/checkout`, Ghost resolves layers from root to leaf, merges them with -child entries winning by `id`, and normalizes child-relative paths back to the -repo root for routing and reports. - -Host tools can wrap Ghost without adopting the default directory name. Use -`--memory-dir ` on stack-aware commands to resolve memory from a -safe relative directory such as `.design/memory`; use `--package ` only for -exact single-bundle mode. +Advanced workflows can add nested memory for product areas, custom +`--memory-dir` locations for host wrappers, optional cache inventory, and fleet +comparison. Those features stay available, but the core loop is just +`fingerprint.yml`, optional active checks, and Git review. ## Install @@ -63,91 +74,118 @@ npx ghost skill install --dest ~/.codex/skills/ghost Then ask your agent in plain English: ```text -Capture a Ghost fingerprint for this repo. +Set up Ghost memory for this repo. Brief this work from the Ghost fingerprint. Review this PR for Ghost drift. Compare these two Ghost bundles. ``` -## Fingerprint Capture +## Fingerprint Memory -Fingerprint Capture is a BYOA workflow. Your agent reads, interprets, and -writes the memory artifacts; the CLI supplies deterministic status, -validation, checks, emitted prompts, and review packets. +Fingerprint Memory is a BYOA workflow. Your agent reads, interprets, and edits +checked-in product prose and exemplars through ordinary file changes. Optional +inventory can orient generation; active checks validate the result. Ask your agent: ```text -Capture a Ghost fingerprint for this repo. +Set up Ghost memory for this repo. ``` -During capture, the agent checkpoints with commands like: +During setup or memory edits, the agent checkpoints with commands like: ```bash -ghost init --with-intent -ghost init --with-config --reference packages/ghost-ui/.ghost -ghost init --scope apps/checkout --with-intent -ghost init --scope apps/checkout --memory-dir .design/memory +ghost init ghost scan --format json -ghost scan --include-nested --format json -ghost inventory > .ghost/cache/inventory.json ghost lint .ghost ghost verify .ghost --root . -ghost lint --all -ghost verify --all ``` +Use `--with-intent`, `--with-config`, `--reference`, `--scope`, or +`--memory-dir` only when the project needs those advanced files or routing +features. + For Ghost UI, `--reference packages/ghost-ui/.ghost` writes config that points at `registry:packages/ghost-ui/public/r/registry.json` plus the Ghost UI reference fingerprint. It does not create or require an installable Ghost UI package. -Inventory is optional source material. Durable conclusions belong in -`.ghost/fingerprint.yml`; implementation routing belongs in optional -`.ghost/config.yml`; executable gates belong in `.ghost/checks.yml`. +Inventory is optional source material: + +```bash +mkdir -p .ghost/cache +ghost inventory > .ghost/cache/inventory.json +``` + +Durable conclusions belong in `.ghost/fingerprint.yml`; executable gates belong +in `.ghost/checks.yml`; implementation routing belongs in optional +`.ghost/config.yml`. + +## Generation Packet + +`ghost emit context-bundle` writes a portable packet for agents. Its `prompt.md` +is organized around: + +- **Product Prose** from checked-in `fingerprint.yml`. +- **Inventory** from `.ghost/cache/inventory.json` when present. +- **Exemplars** from `fingerprint.yml` as curated anchors. +- **Active Checks** from `checks.yml` for validation, not generation memory. + +Checks and review validate output after generation. They do not replace the +prose, inventory, and exemplar inputs that help agents produce the right thing. ## Drift Workflow ```bash ghost check --base main ghost review --base main --include-memory -ghost stack apps/checkout/review/page.tsx ghost emit review-command ghost emit review-command --path apps/checkout/review/page.tsx ghost emit context-bundle +``` + +Advanced commands remain available for scoped memory, legacy migration, and +comparison: + +```bash +ghost stack apps/checkout/review/page.tsx ghost compare market/.ghost dashboard/.ghost ghost compare a.md b.md --semantic # legacy direct markdown compare ghost ack --stance aligned --reason "Initial baseline" ghost diverge typography --reason "Editorial product uses a different type scale" ``` -`ghost scan --format json` emits deterministic capture state: whether -`fingerprint.yml` is present, whether memory has accepted entries, which +`ghost scan --format json` emits deterministic memory state: whether +`fingerprint.yml` is present, whether it has product-experience entries, which optional files exist, and what the next BYOA step should be. It does not call an LLM. -## CLI Commands +## Core CLI Commands + +| Command | Description | +| --- | --- | +| `ghost init` | Create `.ghost/{fingerprint.yml,checks.yml}`; use options only when optional files or scoped memory are needed. | +| `ghost scan` | Report whether canonical fingerprint memory exists and whether it is ready to guide review. | +| `ghost lint` | Validate a bundle or individual artifact. | +| `ghost verify` | Validate fingerprint evidence and exemplar paths, typed check refs, and optional memory. | +| `ghost check` | Run active `ghost.checks/v1` gates against a diff, grouping changed files by memory stack unless `--package` is provided. `--format json` emits `ghost.check-report/v1` for wrappers. | +| `ghost review` | Emit an evidence-routed advisory packet grounded in fingerprint memory, active checks, and the diff. | +| `ghost emit ` | Emit `review-command` or the `context-bundle` generation packet from checked-in memory. | +| `ghost skill install` | Install the unified `ghost` agentskills.io bundle. | + +## Advanced And Legacy Commands | Command | Description | | --- | --- | -| `ghost init` | Create `.ghost/{fingerprint.yml,checks.yml,proposals/,cache/}`; use `--scope ` for scoped memory and `--memory-dir` for a non-default memory directory. | -| `ghost scan` | Report fingerprint capture progress and, with `--include-nested`, nested bundle readiness. Supports `--memory-dir`. | | `ghost inventory` | Emit raw repo signals as JSON for optional cache/material gathering. | -| `ghost lint` | Validate a bundle or individual artifact; `--all` validates nested bundles and stack merges. Supports `--memory-dir`. | -| `ghost verify` | Validate fingerprint evidence paths, typed check refs, optional memory, and with `--all` nested stack integrity. Supports `--memory-dir`. | | `ghost stack` | Inspect resolved root-to-leaf memory layers and merged output for one or more paths. Supports `--memory-dir`. | | `ghost describe` | Print optional `intent.md` or legacy direct markdown section ranges. | | `ghost diff` | Structural prose-level diff between legacy direct fingerprints. | | `ghost survey ` | Legacy/cache helpers for `ghost.survey/v2` files. Not canonical memory. | -| `ghost check` | Run active `ghost.checks/v1` gates against a diff, grouping changed files by memory stack unless `--package` is provided. `--format json` emits `ghost.check-report/v1` for wrappers. | -| `ghost review` | Emit an evidence-routed advisory packet with `stacks[]` unless `--package` is provided. Supports `--memory-dir`. | -| `ghost proposal ` | Create, list, or resolve scoped proposal files without auto-promoting canonical memory. Supports `--memory-dir`. | | `ghost compare` | Pairwise or composite comparison over bundles or direct fingerprints. | | `ghost ack` | Record stance toward the tracked fingerprint in `.ghost-sync.json`. | | `ghost track` | Shift the tracked fingerprint. | | `ghost diverge` | Declare intentional divergence on a dimension. | -| `ghost emit ` | Emit `review-command` or `context-bundle`; use `--path` for a merged stack or `--package` for exact bundle mode. Supports `--memory-dir` with `--path`. | -| `ghost skill install` | Install the unified `ghost` agentskills.io bundle. | ## Repo Layout @@ -156,7 +194,7 @@ workspace packages remain only for historical/development context. | Path | Role | Published? | | ---- | ---- | --- | -| [`packages/ghost`](./packages/ghost) | Unified public package. Ships the `ghost` CLI, fingerprint capture helpers, deterministic checks, advisory review packets, comparison, stance tracking, and the unified skill bundle. | yes: `@anarchitecture/ghost` | +| [`packages/ghost`](./packages/ghost) | Unified public package. Ships the `ghost` CLI, fingerprint memory helpers, deterministic checks, advisory review packets, advanced comparison/stance helpers, and the unified skill bundle. | yes: `@anarchitecture/ghost` | | [`packages/ghost-core`](./packages/ghost-core) | Private historical shared library. Runtime code is folded into `packages/ghost` for publishing. | no | | [`packages/ghost-fleet`](./packages/ghost-fleet) | Private fleet view across many members. | no | | [`packages/ghost-ui`](./packages/ghost-ui) | Reference design system: shadcn registry + `ghost-mcp` MCP server. | no | @@ -183,6 +221,6 @@ optional and only used by semantic embedding helpers when a host opts in. | --- | --- | | [docs/fingerprint-format.md](./docs/fingerprint-format.md) | Root `.ghost/` memory format | | [docs/generation-loop.md](./docs/generation-loop.md) | Brief, generate, check, review, and remediate loop | -| [docs/host-adapters.md](./docs/host-adapters.md) | Adapter-neutral JSON, severity mapping, proposals, and custom memory directories | +| [docs/host-adapters.md](./docs/host-adapters.md) | Adapter-neutral JSON, severity mapping, and custom memory directories | | [GOVERNANCE.md](./GOVERNANCE.md) | Project governance | | [LICENSE](./LICENSE) | Apache License, Version 2.0 | diff --git a/apps/docs/README.md b/apps/docs/README.md index 47687276..a9f1c75c 100644 --- a/apps/docs/README.md +++ b/apps/docs/README.md @@ -2,7 +2,7 @@ **Documentation site for the Ghost project.** -`ghost-docs` is the deployed docs for everything in this monorepo: the `ghost` CLI, the fingerprint format, the design language foundations, and the live `ghost-ui` component catalogue. A Vite + MDX app that consumes [`ghost-ui`](../../packages/ghost-ui) as a workspace dependency. +`ghost-docs` is the deployed docs for everything in this monorepo: the `ghost` CLI, the fingerprint memory format, the generation loop, and the live `ghost-ui` component catalogue. A Vite + MDX app that consumes [`ghost-ui`](../../packages/ghost-ui) as a workspace dependency. ## Run diff --git a/apps/docs/src/app/docs/page.tsx b/apps/docs/src/app/docs/page.tsx index e7ad367e..2aa66593 100644 --- a/apps/docs/src/app/docs/page.tsx +++ b/apps/docs/src/app/docs/page.tsx @@ -17,7 +17,7 @@ const sections: { name: "Getting Started", href: "/docs/getting-started", description: - "Install Ghost, capture a repo fingerprint, and learn the loop around .ghost.", + "Install Ghost, set up repo memory, and learn the loop around .ghost.", icon: , }, { diff --git a/apps/docs/src/app/page.tsx b/apps/docs/src/app/page.tsx index 4ab7b8d5..d176492d 100644 --- a/apps/docs/src/app/page.tsx +++ b/apps/docs/src/app/page.tsx @@ -30,33 +30,31 @@ export default function Home() {

Agents can write UI. What they cannot reliably preserve is the - thought behind the product experience they are changing. + product experience identity behind that UI.

The failure mode is structural. Large language models generate by matching local patterns. They reproduce components, tokens, and - layouts, but they do not consistently preserve the higher-order - decisions that make a surface feel intentional: its hierarchy, its - density, its restraint, the specific ways it repeats and refuses, - and the ways it earns trust. + layouts, but they do not consistently preserve the decisions that + make a surface feel intentional: hierarchy, density, restraint, + behavior, copy, accessibility, trust, and flow.

- Most design systems encode the inventory of a product: colors, - type scales, components. That inventory is necessary, but it is - not sufficient. The same system can produce many different - products. What is missing is the policy that governs how those - parts are composed. + Most design systems encode inventory: colors, type scales, + components, libraries, and examples. That inventory is necessary, + but it is not sufficient. Inventory answers what exists. It does + not answer what matters, why it matters, or how a product chooses + among valid possibilities.

- Ghost introduces a second layer: a fingerprint. + Ghost introduces repo-local product experience memory.

- The fingerprint is a repository-local, versioned artifact that - captures product experience memory: the situations, principles, - contracts, and patterns that shape how the system is actually - used. Components, tokens, and libraries remain replaceable - implementation material; Ghost gives agents the judgment that sits - around them. + That memory lives in a compact, versioned bundle that agents can + read before generation and deterministic tooling can validate + after changes. Components, tokens, libraries, and generated + inventory remain implementation material; Ghost preserves the + product judgment that sits around them.

The broader boundary is product experience: anything that shapes @@ -70,15 +68,15 @@ export default function Home() {

  • .ghost/fingerprint.yml stores canonical product - experience memory + experience memory and curated exemplars
  • - .ghost/checks.yml stores deterministic gates - grounded in that memory + .ghost/checks.yml stores optional deterministic + gates grounded in that memory
  • - .ghost/proposals stages missing memory, intentional - divergence, experience gaps, and check candidates + ordinary Git review separates draft memory edits from checked-in + truth
  • .ghost/decisions records optional accepted or @@ -90,63 +88,60 @@ export default function Home() {

- The distinction is deliberate. Inventory describes what exists. - The fingerprint describes how the product repeatedly chooses to - use what exists. + The distinction is deliberate. Fingerprint prose explains what + matters and why. Optional cache explains what exists. Exemplars + show concrete surfaces worth inspecting before generation or + review. Checks validate output; they are not generation memory.

- This makes the fingerprint closer to a behavioral prior than a - spec. It encodes: + Fingerprint memory may start sparse and grow only where the + product has durable memory to record. It can encode:

    -
  • preferred hierarchies over possible ones
  • -
  • constraints on density, spacing, and interaction patterns
  • -
  • allowed deviations from base components
  • -
  • explicit anti-patterns the surface avoids
  • +
  • the product, audience, goals, anti-goals, and tradeoffs
  • +
  • situations where user intent changes product obligations
  • +
  • principles and contracts for behavior, copy, and recovery
  • +
  • visual, content, behavior, and composition patterns
  • +
  • curated examples of what good looks like in practice

For an agent, this changes the task. UI generation is no longer unconstrained composition over a design system. It becomes a - constrained search guided by a product-specific policy. + product-specific brief grounded in checked-in memory, optional + inventory, and exemplars.

A typical loop becomes:

    -
  1. Condition on the fingerprint
  2. -
  3. Generate UI against the design system
  4. -
  5. Evaluate the result against fingerprint constraints
  6. -
  7. Revise until violations are reduced or resolved
  8. +
  9. + Brief from fingerprint memory, optional inventory, and exemplars +
  10. +
  11. Generate or edit with the host agent
  12. +
  13. Run active deterministic checks and advisory review
  14. +
  15. + Fix code, explain intentional divergence, or update memory + through Git +

- The fingerprint is therefore not just descriptive. It is partially - executable. It enables both generation and evaluation. -

-

- This is critical because style that cannot be evaluated cannot be - delegated. A design language that only its original author can - judge is not transferable: to agents, to new engineers, or to - forks of the product. -

-

- Ghost treats evaluation as a first-class concern. Parts of the - fingerprint are grounded in: + Ghost stays bring-your-own-agent. The agent reads, decides, and + writes. Ghost does the repeatable work: initialization, schema + validation, inventory, evidence verification, checks, advisory + review packets, comparison, and handoff packets.

-
    -
  • explicit rules: hard constraints
  • -
  • preference gradients: soft constraints
  • -
  • negative constraints: anti-patterns
  • -

- These allow an agent, or a reviewer, to check whether a surface - composes faithfully, not just whether it compiles. + This is critical because product judgment that cannot be recalled + or evaluated cannot be delegated. A product experience that only + its original author can judge is not transferable: to agents, to + new engineers, or to forks of the product.

Drift becomes measurable within this system. When generated or - modified UI diverges from the fingerprint, the failure is not just - error; it is signal. Drift can originate from: + modified UI diverges from checked-in memory, the failure is not + just error; it is signal. Drift can originate from:

  • incorrect generation: agent failure
  • -
  • incomplete fingerprint: under-specified policy
  • +
  • missing memory: under-specified product judgment
  • intentional product evolution

@@ -154,29 +149,26 @@ export default function Home() { system's boundary becomes visible where composition fails.

- The fingerprint bundle must live where generation happens: in the - repository, versioned alongside the code it governs, evolving - through the same pull requests that introduce new UI. As the - product changes, the package updates with it, maintaining - alignment between intent and implementation. + The memory bundle must live where generation happens: in the + repository, versioned alongside the code it governs. As the + product changes, memory changes through the same ordinary Git + review that introduces new UI.

- This leads to a different governance model. Instead of a single - centralized design authority, each repository owns its - fingerprint: its local expression of the design language. - Divergence across repositories is not hidden; it is made explicit - through declared stances (aligned, accepted,{" "} - diverging), turning fragmentation into observable - structure. + This leads to a practical governance model. Each repository owns + its product experience memory. Advanced workflows can add nested + bundles for product areas, custom memory directories for host + wrappers, comparison across systems, and declared drift stances.

- Across an organization, the collection of fingerprints forms a - higher-order map: a distributed model of the design language as it - is actually practiced, not as it is prescribed. + Across an organization, the collection of Ghost bundles forms a + higher-order map: a distributed model of product experience as it + is actually practiced, not as it is only described.

- Nothing is enforced globally. Nothing drifts silently. Every - surface declares its policy, and every deviation carries evidence. + Nothing is enforced globally. Nothing needs to drift silently. + Every surface can carry its memory, and every deviation can carry + evidence.

diff --git a/apps/docs/src/app/tools/drift/page.tsx b/apps/docs/src/app/tools/drift/page.tsx index e9074ebc..93e22a78 100644 --- a/apps/docs/src/app/tools/drift/page.tsx +++ b/apps/docs/src/app/tools/drift/page.tsx @@ -17,7 +17,7 @@ const cards: { name: "Ghost loop", href: "/docs/getting-started#the-simple-model", description: - "See how capture, review, comparison, and intent fit together.", + "See how memory, checks, review, comparison, and intent fit together.", icon: , }, { @@ -31,7 +31,7 @@ const cards: { name: "CLI reference", href: "/docs/cli#ghost--review-and-compare", description: - "Run checks, emit advisory review, compare fingerprints, and record intent.", + "Run checks, emit exemplar-guided advisory review, compare fingerprints, and record intent.", icon: , }, ]; @@ -48,7 +48,7 @@ export default function GhostDriftLanding() {
, }, { @@ -78,7 +78,7 @@ export default function ToolsIndex() { diff --git a/apps/docs/src/app/tools/scan/page.tsx b/apps/docs/src/app/tools/scan/page.tsx index 8f99e0ab..ac5fb78f 100644 --- a/apps/docs/src/app/tools/scan/page.tsx +++ b/apps/docs/src/app/tools/scan/page.tsx @@ -16,21 +16,20 @@ const cards: { { name: "Get started", href: "/docs/getting-started", - description: - "Install the skill bundle and ask your agent to capture a fingerprint.", + description: "Install the skill bundle and set up repo-local Ghost memory.", icon: , }, { name: "CLI reference", - href: "/docs/cli#ghost--capture-support-and-bundle-checks", - description: "Check capture progress, validate bundles, and emit context.", + href: "/docs/cli#ghost--memory-support-and-bundle-checks", + description: "Check memory readiness, validate bundles, and emit context.", icon: , }, { name: "Format spec", href: "https://github.com/block/ghost/blob/main/docs/fingerprint-format.md", description: - "The full bundle format for resources, map, survey, patterns, checks, and optional memory.", + "The full bundle format for fingerprint prose, exemplars, checks, and optional memory.", icon: , }, ]; @@ -46,8 +45,8 @@ export default function GhostScanLanding() {
-The CLI does the repeatable parts: initialize memory bundles, report capture -progress, validate files, emit raw inventory, compare bundles, check diffs, -emit handoff packets, and record intent. Your agent does the reading, writing, -and reviewing. +The CLI does the repeatable parts: initialize memory bundles, report +fingerprint readiness, validate files, emit raw inventory, compare bundles, +check diffs, emit handoff packets, and record intent. Your agent does the +reading, writing, and reviewing. Canonical Ghost memory starts here, with optional child bundles for scoped product areas: @@ -33,18 +33,18 @@ npx ghost skill install Ask your agent to run the workflow: ```text -Capture a Ghost fingerprint for this repo. +Set up Ghost memory for this repo. ``` Commands are grouped by job: -- **Create and check memory bundles**: `init`, `scan`, `stack`, `inventory`, `lint`, `verify`, `describe`, `diff`, `survey `, `proposal `, `emit`. +- **Create and check memory bundles**: `init`, `scan`, `stack`, `inventory`, `lint`, `verify`, `describe`, `diff`, `survey `, `emit`. - **Review and compare drift**: `check`, `review`, `compare`, `ack`, `track`, `diverge`. - **Install BYOA recipes**: `skill install`. - **View many projects**: `ghost-fleet members`, `ghost-fleet view`, `ghost-fleet emit skill`. Workflows like _capture_, _survey_, _patterns_, _recall_, _brief_, _critique_, -_propose_, _promote_, _review_, _verify_, _compare_, and _remediate_ are skill +_review_, _verify_, _compare_, and _remediate_ are skill recipes your host agent runs. They are installed by `ghost skill install`; they are not LLM-running CLI verbs. @@ -54,19 +54,20 @@ reference. - + -`ghost` supports Fingerprint Capture. It inventories the repo, reports capture -progress, validates memory, verifies evidence paths and typed refs, describes -legacy direct markdown when needed, diffs direct fingerprint files, runs -legacy/cache survey ops, and emits agent-ready outputs. +`ghost` supports repo-local fingerprint memory. It inventories the repo, +reports memory/readiness state, validates memory, verifies evidence, exemplar +paths, and typed refs, describes legacy direct markdown when needed, diffs +direct fingerprint files, runs legacy/cache survey ops, and emits agent-ready +outputs. Ordinary Git workflow is the staging layer for memory edits. ### Initialize - `init` -Create a `.ghost/` memory skeleton with `fingerprint.yml`, `checks.yml`, -`proposals/`, and `cache/`. Use `--scope ` for nested -`/.ghost/` memory. Use `--memory-dir ` when a host adapter -stores Ghost memory under a different safe relative directory. +Create a `.ghost/` memory skeleton with `fingerprint.yml` and `checks.yml`. +Use `--scope ` for nested `/.ghost/` memory. Use +`--memory-dir ` when a host adapter stores Ghost memory under a +different safe relative directory. @@ -77,11 +78,11 @@ ghost init --scope apps/checkout --with-intent ghost init --scope apps/checkout --memory-dir .design/memory ``` -### Capture progress - `scan` +### Memory readiness - `scan` -Report whether fingerprint memory is present, whether it has accepted entries, -which optional files exist, and which BYOA step the agent should run next. -`--format json` is the deterministic handoff. +Report whether canonical fingerprint memory is present, whether it has +entries, which optional files exist, and which BYOA step the agent should run +next. `--format json` is the deterministic handoff. @@ -140,9 +141,9 @@ ghost lint --all --memory-dir .design/memory ### Bundle fidelity - `verify` Validate that a root `.ghost` bundle is internally faithful: -`fingerprint.yml` evidence paths should resolve from `--root`, typed check refs -must point at known checks, and active checks must be grounded in fingerprint -memory. +`fingerprint.yml` evidence and exemplar paths should resolve from `--root`, +typed check refs must point at known checks, and active checks must be grounded +in fingerprint memory. With `--all`, Ghost verifies every discovered bundle and stack merge. @@ -203,7 +204,9 @@ ghost diff a.md b.md --format json Emit deterministic artifacts for agents and editors. Supported kinds are `review-command` and `context-bundle`; both default to `.ghost/fingerprint.yml` -memory. Legacy direct markdown emit is no longer supported. +memory. `context-bundle` is the generation packet: product prose, optional +inventory cache, curated exemplars, and active checks. Legacy direct markdown +emit is no longer supported. Use `--path` to emit from the merged stack for a repo path, or `--package` to stay in exact single-bundle mode. `--memory-dir` applies only to stack resolution through `--path`; `--package` remains exact-path mode. @@ -220,23 +223,6 @@ ghost emit context-bundle --prompt-only ghost emit context-bundle --out dist/context ``` -### Scoped proposals - `proposal ` - -Create, list, and resolve thresholded candidate memory updates in the nearest -applicable scoped bundle. Use proposals for durable gaps: repeated, -high-impact, explicitly human-stated, intentionally divergent, likely to recur, -or blocking confident future review. These commands do not promote prose into -`fingerprint.yml` or `checks.yml`; promotion remains explicit. - - - -```bash -ghost proposal list --path apps/checkout/review/page.tsx -ghost proposal create --path apps/checkout/review/page.tsx --id checkout-copy-memory --kind missing-memory --title "Checkout copy memory" --claim "Checkout copy needs local memory." --rationale "Payment review language is checkout-specific." --summary "Add checkout copy guidance." -ghost proposal resolve checkout-copy-memory --path apps/checkout/review/page.tsx --status accepted -ghost proposal list --path apps/checkout/review/page.tsx --memory-dir .design/memory --format json -``` - @@ -260,10 +246,10 @@ ghost check --base main --memory-dir .design/memory --format json ### Advisory packet - `review` Emit an evidence-routed advisory review packet grounded in -`.ghost/fingerprint.yml`, optional `.ghost/intent.md`, open proposals, accepted +`.ghost/fingerprint.yml`, exemplars, optional `.ghost/intent.md`, accepted decisions, checks, and the diff. Without `--package`, JSON output includes `stacks[]`, each with changed files, layer dirs, merged memory, checks, -proposals, and provenance. Use `--memory-dir` for adapter-owned memory +decisions, and provenance. Use `--memory-dir` for adapter-owned memory locations. @@ -280,7 +266,7 @@ Ghost does not emit host-specific review or check formats. A host adapter should consume `ghost check --format json`, map `critical | serious | nit` into its own severity vocabulary, and emit the host-native artifact outside Ghost. The repository doc `docs/host-adapters.md` describes the adapter contract and -proposal flow. +custom memory directories. ### Comparison - `compare` @@ -326,8 +312,8 @@ ghost diverge palette --reason "Dark-mode-first palette for this product" ### Skill bundle - `skill install` Install the unified `ghost` skill bundle into a host agent. It contains -Fingerprint Capture, survey/cache, patterns, schema, recall, brief, critique, -propose, promote, review, verify, compare, and remediate recipes. +fingerprint memory, survey/cache, patterns, schema, recall, brief, critique, +review, verify, compare, and remediate recipes. @@ -374,10 +360,10 @@ then ask your agent in plain English. | Recipe | Bundle | Trigger | | --- | --- | --- | -| `capture` | `ghost` | "Capture a Ghost fingerprint for this repo" | +| `capture` | `ghost` | "Set up Ghost memory for this repo" | | `survey` | `ghost` | "use survey cache" / "extract tokens as source material" | | `patterns` | `ghost` | "write fingerprint patterns" / "codify product-experience patterns" | -| `recall` / `brief` / `critique` / `propose` / `promote` | `ghost` | "brief this work" / "propose this decision" / "promote this proposal" | +| `recall` / `brief` / `critique` | `ghost` | "brief this work" / "critique this change" | | `review` | `ghost` | "review this PR for drift" | | `verify` | `ghost` | "verify generated UI against the bundle" | | `compare` | `ghost` | "why did these two fingerprints drift?" | diff --git a/apps/docs/src/content/docs/getting-started.mdx b/apps/docs/src/content/docs/getting-started.mdx index 80c6c4c1..2696b94d 100644 --- a/apps/docs/src/content/docs/getting-started.mdx +++ b/apps/docs/src/content/docs/getting-started.mdx @@ -1,6 +1,6 @@ --- title: Getting Started -description: Install Ghost, capture repo-local product experience memory, and review generated UI against it. +description: Install Ghost, author repo-local product experience memory, and review generated UI against it. kicker: Docs section: guide order: 10 @@ -9,9 +9,9 @@ slug: getting-started -Ghost captures a project's product experience memory in the repo, where your -agent can use it. The public package is `@anarchitecture/ghost`, and it -installs one CLI: `ghost`. +Ghost keeps a project's product experience memory in the repo, where your agent +can use it. The public package is `@anarchitecture/ghost`, and it installs one +CLI: `ghost`. The canonical fingerprint is small: @@ -28,13 +28,17 @@ Host adapters can use a different safe relative memory directory with `--memory-dir`, such as `.design/memory`. Ghost still emits adapter-neutral JSON; the wrapper maps findings into its own review or check format. -`intent.md`, `decisions/*.yml`, and `proposals/*.yml` are optional -human-approved or staged context. `cache/` can hold generated inventory, but it -is not canonical memory. +`intent.md` and `decisions/*.yml` are optional human-authored context. `cache/` +can hold generated inventory, but it is not canonical memory. Ordinary Git +workflow is the staging layer: uncommitted or unmerged memory edits are drafts, +and checked-in `fingerprint.yml` is truth for Ghost. + +Generation starts from product prose, optional generated inventory, and curated +exemplars. Checks validate the result afterward; they are not generation memory. | Surface | Job | Verbs | | --- | --- | --- | -| `ghost` | Create and check root or nested `.ghost/` memory bundles, review diffs, compare bundles, and record drift intent. | `init`, `scan`, `stack`, `inventory`, `lint`, `verify`, `describe`, `diff`, `survey `, `proposal `, `check`, `review`, `compare`, `ack`, `track`, `diverge`, `emit`, `skill install` | +| `ghost` | Create and check root or nested `.ghost/` memory bundles, review diffs, compare bundles, and record drift intent. | `init`, `scan`, `stack`, `inventory`, `lint`, `verify`, `describe`, `diff`, `survey `, `check`, `review`, `compare`, `ack`, `track`, `diverge`, `emit`, `skill install` | | `ghost-fleet` | See how many project fingerprints relate. | `members`, `view`, `emit skill` | | `ghost-ui` | Reference design system Ghost uses to test itself. | - | @@ -48,28 +52,29 @@ npx ghost --help npx ghost skill install ``` -Once the skill is installed, ask your agent in plain English: "Capture a Ghost -fingerprint for this repo" or "review this PR for drift". The recipe tells the +Once the skill is installed, ask your agent in plain English: "Set up Ghost +memory for this repo" or "review this PR for drift". The recipe tells the agent what to read, what to write, and which CLI checks to run. - + ```text -Capture a Ghost fingerprint for this repo. +Set up Ghost memory for this repo. ``` -Fingerprint Capture writes durable memory first: +Fingerprint memory records durable product-experience guidance: 1. **Summary and topology** - what product this is, who it serves, and where UI surfaces live. 2. **Situations** - user, task, and state moments that change obligations. 3. **Principles and contracts** - durable product judgment plus behavior, disclosure, failure, and recovery rules. -4. **Patterns and implementation vocabulary** - accepted visual, content, - behavior, and composition memory plus current tokens and components agents - may use to implement it. +4. **Patterns and exemplars** - visual, content, behavior, and composition + memory plus concrete surfaces that show what good looks like. +5. **Implementation vocabulary** - current tokens and components agents may use + to implement the memory. ```bash ghost init --with-intent @@ -84,8 +89,14 @@ ghost lint --all ghost verify --all ``` -Inventory is optional source material. Promote only durable conclusions into -`fingerprint.yml`. +Inventory is optional source material. Curate durable conclusions into +`fingerprint.yml`, then use normal Git review for approval. + +For generation, emit a packet that keeps these inputs separate: + +```bash +ghost emit context-bundle +``` @@ -112,37 +123,29 @@ Review this PR for design drift ``` `ghost check` applies active deterministic gates from the resolved memory stack -for each changed file. `ghost review` emits advisory context grounded in -merged `fingerprint.yml` memory, optional `intent.md`, open proposals, checks, -and the diff. Add `--include-memory` to include accepted decisions in the -advisory packet. Use `--package ` when you need exact single-bundle -behavior. +for each changed file. `ghost review` emits advisory context grounded in merged +`fingerprint.yml` memory, exemplars, optional `intent.md`, checks, and the diff. +Add `--include-memory` to include accepted decisions in the advisory packet. Use +`--package ` when you need exact single-bundle behavior. Wrappers should consume `ghost check --format json` and map Ghost severities outside Ghost. Ghost severities remain `critical`, `serious`, and `nit`. - + Use the installed `ghost` skill when work reveals durable memory Ghost should -consider adding to the fingerprint: +add to the fingerprint: ```text Brief this work from the Ghost fingerprint -Propose this product-experience decision as a fingerprint update -Promote this accepted proposal +Update the fingerprint with this product-experience decision ``` -The recipes recommend proposals only when the gap is repeated, high-impact, -explicitly human-stated, intentionally divergent, likely to recur, or blocks -confident future review. Humans promote durable context into `fingerprint.yml` -or `checks.yml`. - -For scoped product areas, use `ghost proposal create --path ...` to -write the proposal into the nearest applicable child bundle, then -`ghost proposal resolve --path --status accepted|rejected` after -the explicit promotion work is done. +Memory updates are ordinary edits to `fingerprint.yml`, `checks.yml`, +`intent.md`, or `decisions/*.yml`. Uncommitted or unmerged edits are drafts; +checked-in memory is canonical. diff --git a/apps/docs/src/generated/cli-manifest.json b/apps/docs/src/generated/cli-manifest.json index a65ca39e..99e2195f 100644 --- a/apps/docs/src/generated/cli-manifest.json +++ b/apps/docs/src/generated/cli-manifest.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-06-02T13:29:31.526Z", + "generatedAt": "2026-06-03T03:48:35.268Z", "tools": [ { "tool": "ghost", @@ -40,12 +40,12 @@ "tool": "ghost", "name": "init", "rawName": "init [dir]", - "description": "Create a root .ghost product experience memory skeleton (fingerprint.yml, checks.yml, proposals/, cache/)", + "description": "Create a root .ghost memory skeleton (fingerprint.yml and checks.yml)", "options": [ { "rawName": "--scope ", "name": "scope", - "description": "Create a scoped / product experience memory skeleton", + "description": "Create a scoped / memory skeleton", "default": null, "takesValue": true, "negated": false @@ -96,12 +96,12 @@ "tool": "ghost", "name": "verify", "rawName": "verify [dir]", - "description": "Verify a root Ghost memory bundle: fingerprint evidence paths and checks are grounded.", + "description": "Verify a root Ghost memory bundle: fingerprint evidence, exemplars, and checks are grounded.", "options": [ { "rawName": "--root ", "name": "root", - "description": "Optional target root used to resolve fingerprint.yml evidence paths (default: cwd)", + "description": "Optional target root used to resolve fingerprint.yml evidence and exemplar paths (default: cwd)", "default": null, "takesValue": true, "negated": false @@ -136,7 +136,7 @@ "tool": "ghost", "name": "scan", "rawName": "scan [dir]", - "description": "Report fingerprint capture progress: produced artifacts, evidence readiness, and the next BYOA step.", + "description": "Report fingerprint memory/readiness state: produced artifacts, review readiness, and the next BYOA step.", "options": [ { "rawName": "--include-scopes", @@ -275,115 +275,11 @@ } ] }, - { - "tool": "ghost", - "name": "proposal", - "rawName": "proposal [id]", - "description": "Create, list, or resolve scoped Ghost memory proposals.", - "options": [ - { - "rawName": "--path ", - "name": "path", - "description": "Repo path used to resolve the memory stack", - "default": ".", - "takesValue": true, - "negated": false - }, - { - "rawName": "--memory-dir ", - "name": "memoryDir", - "description": "Relative memory package directory for proposal stack resolution (default: .ghost)", - "default": null, - "takesValue": true, - "negated": false - }, - { - "rawName": "--id ", - "name": "id", - "description": "Proposal id for create", - "default": null, - "takesValue": true, - "negated": false - }, - { - "rawName": "--kind ", - "name": "kind", - "description": "Proposal kind for create", - "default": null, - "takesValue": true, - "negated": false - }, - { - "rawName": "--title ", - "name": "title", - "description": "Proposal title for create", - "default": null, - "takesValue": true, - "negated": false - }, - { - "rawName": "--claim <text>", - "name": "claim", - "description": "Proposal claim for create", - "default": null, - "takesValue": true, - "negated": false - }, - { - "rawName": "--rationale <text>", - "name": "rationale", - "description": "Proposal rationale for create", - "default": null, - "takesValue": true, - "negated": false - }, - { - "rawName": "--target <target>", - "name": "target", - "description": "Proposed target: fingerprint, checks, or review_policy", - "default": "fingerprint", - "takesValue": true, - "negated": false - }, - { - "rawName": "--summary <text>", - "name": "summary", - "description": "Proposed action summary for create", - "default": null, - "takesValue": true, - "negated": false - }, - { - "rawName": "--evidence <path-or-note>", - "name": "evidence", - "description": "Evidence path or note for create; repeat or comma-separate for multiple values", - "default": null, - "takesValue": true, - "negated": false - }, - { - "rawName": "--status <status>", - "name": "status", - "description": "Resolution status: accepted, rejected, or superseded", - "default": null, - "takesValue": true, - "negated": false - }, - { - "rawName": "--format <fmt>", - "name": "format", - "description": "Output format: cli or json", - "default": "cli", - "takesValue": true, - "negated": false - } - ] - }, { "tool": "ghost", "name": "emit", "rawName": "emit <kind>", - "description": "Emit a derived artifact from the fingerprint package (kinds: review-command, context-bundle)", + "description": "Emit a derived artifact from the fingerprint package (review command or context-bundle generation packet)", "options": [ { "rawName": "--path <path>", @@ -436,7 +332,7 @@ { "rawName": "--prompt-only", "name": "promptOnly", - "description": "Emit only prompt.md (context-bundle)", + "description": "Emit only prompt.md (context-bundle generation packet)", "default": null, "takesValue": false, "negated": false diff --git a/docs/cli-consolidation-plan.md b/docs/cli-consolidation-plan.md index cc023266..e8c5bc83 100644 --- a/docs/cli-consolidation-plan.md +++ b/docs/cli-consolidation-plan.md @@ -11,9 +11,9 @@ branch: refactor/fingerprint (or a follow-up branch) > `inventory`, `lint`, `verify`, `describe`, `diff`, `survey`, `check`, > `review`, `compare`, `ack`, `track`, `diverge`, `emit`, and `skill install`) > plus the private `ghost-fleet` CLI. Capture, map, survey, patterns, recall, -> brief, critique, propose, promote, verify, compare, and remediate are -> host-agent skill recipes installed by `ghost skill install`. Kept for history; -> don't treat the exploratory verb list below as current. See the +> brief, critique, review, verify, compare, and remediate are host-agent skill +> recipes installed by `ghost skill install`. Kept for history; don't treat the +> exploratory verb list below as current. See the > [README](../README.md) and the > [CLI reference](../apps/docs/src/content/docs/cli-reference.mdx) for the > current surface. diff --git a/docs/fingerprint-format.md b/docs/fingerprint-format.md index 59f2eb49..975485cc 100644 --- a/docs/fingerprint-format.md +++ b/docs/fingerprint-format.md @@ -1,29 +1,49 @@ # The Root Fingerprint Bundle Format A Ghost fingerprint is a repo-local product experience memory bundle rooted at -`.ghost/`. The canonical on-disk shape is: +`.ghost/`. The core on-disk shape is: ```text .ghost/ fingerprint.yml # canonical product experience memory - config.yml # optional implementation roots and reference registries/libraries checks.yml # optional deterministic gates +``` + +Git is the staging and approval boundary: uncommitted or unmerged edits are +draft work, and checked-in `fingerprint.yml` memory is canonical for Ghost. +Ghost is not a lifecycle manager, proposal system, design-system registry, or +screenshot archive. It validates checked-in memory and runs checked-in gates. + +`fingerprint.yml` may start with only: + +```yaml +schema: ghost.fingerprint/v1 +``` + +Add only top-level sections that contain real memory. Ghost normalizes omitted +sections internally to empty `summary`, `topology`, memory arrays, `exemplars`, and +`implementation_vocabulary` so existing checks, review packets, and stack +merges still see the full shape. + +Optional material can sit beside the core files: + +```text +.ghost/ + config.yml # optional implementation roots and reference registries/libraries intent.md # optional human-authored or human-approved intent decisions/ # optional ghost.decision/v1 rationale - proposals/ # optional ghost.proposal/v1 candidate memory updates cache/ # optional generated inventory and other ephemeral facts ``` -`fingerprint.yml` is the source of truth. `config.yml` routes implementation -and reference registry/library context without defining product intent. `checks.yml` is -the executable appendix. Proposals are unresolved candidate changes. Cache is -refreshable and may be deleted without losing canonical memory. +`config.yml` routes implementation and reference registry/library context +without defining product intent. `checks.yml` is the executable appendix. Cache +is refreshable and may be deleted without losing canonical memory. Legacy `resources.yml`, `map.md`, `survey.json`, and `patterns.yml` files may still appear in older repos or as migration/source material. They are not canonical Ghost memory. -## Nested Bundles +## Advanced: Nested Bundles Large repos can add scoped bundles below the root: @@ -38,31 +58,25 @@ For a path like `apps/checkout/review/page.tsx`, Ghost resolves every The merged stack is broad-to-local: 1. Root memory supplies product-wide identity, shared situations, principles, - contracts, patterns, checks, decisions, proposals, and intent. + contracts, patterns, exemplars, checks, decisions, and intent. 2. Child memory adds local product-area detail. 3. Entries with the same `id` are replaced by the nearest child entry. 4. Child-relative paths are normalized to repo-root paths in reports, routing, and emitted context. `summary.product` and other scalar summary fields use the nearest child value. -Summary arrays, topology surface types, implementation vocabulary, and review -policy arrays merge parent-to-child with de-dupe. Checks merge by `id`, so a -child check with `status: disabled` suppresses an inherited active check. -`intent.md` files concatenate with layer headings. Decisions and proposals also -merge by `id` with child entries winning. - -No `extends` field is needed in canonical `fingerprint.yml`; stack inheritance -is filesystem-based. - -The default memory directory is `.ghost`. Host adapters can use another safe -relative directory by passing `--memory-dir <relative-dir>` to stack-aware -commands. For example, `--memory-dir .design/memory` resolves -`.design/memory/fingerprint.yml` at the root and in nested product areas. +Summary arrays, topology surface types, exemplars, and implementation vocabulary merge +parent-to-child with de-dupe. Checks merge by `id`, so a child check with +`status: disabled` suppresses an inherited active check. `intent.md` files +concatenate with layer headings. Decisions merge by `id` with child entries +winning. ## `fingerprint.yml` `fingerprint.yml` uses `ghost.fingerprint/v1`. It should stay compact enough -for agents to read before generation and review. +for agents to read before generation and review. Fingerprint entries do not +have lifecycle status fields; if an entry is in checked-in `fingerprint.yml`, +Ghost treats it as memory. ```yaml schema: ghost.fingerprint/v1 @@ -94,50 +108,51 @@ situations: patterns: [pattern:reference-before-decoration] principles: - id: memory-before-inventory - status: accepted principle: Canonical memory should explain what matters and why; generated inventory only explains what exists. experience_contracts: - id: review-cites-memory - status: accepted contract: Advisory review findings must cite the diff and the relevant fingerprint memory. patterns: - id: reference-before-decoration - status: accepted kind: composition pattern: Reference pages prioritize the working surface before visual flourish. +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 how command docs stay inspectable before decorative framing. + refs: [pattern:reference-before-decoration] implementation_vocabulary: tokens: [--color-bg, --color-fg] components: [Button, CodeBlock] notes: - Use these as current implementation material, not as proof of product fit. -review_policy: - proposal_policy: - - Agents create proposals for missing memory, intentional divergence, experience gaps, and check candidates. - - Humans promote durable memory into fingerprint.yml or checks.yml. - experience_gap_categories: - - missing-memory - - intentional-divergence - - experience-gap - - check-candidate ``` -Canonical sections: +Top-level sections are optional on disk and default to empty when omitted: | Section | Purpose | | --- | --- | | `summary` | Product identity, audience, goals, anti-goals, tradeoffs, and tone. | -| `topology` | Repo scopes, paths, surface types, and examples. | +| `topology` | Repo scopes, paths, and surface types. | | `situations` | User/task/state moments that change product obligations. | | `principles` | Durable product experience rules and judgment. | | `experience_contracts` | How surfaces and capabilities speak, disclose, fail, and recover. | | `patterns` | Reusable visual, behavioral, content, or composition patterns. | +| `exemplars` | Curated paths that show what good looks like for generation and review. | | `implementation_vocabulary` | Current tokens, components, libraries, assets, and notes available for implementation. | -| `review_policy` | How agents handle gaps, proposals, and uncertainty. | + +`exemplars` are canonical generation anchors when checked into +`fingerprint.yml`. Entry-level `evidence` remains proof or citation for a +memory claim; exemplars are the concrete surfaces an agent should inspect. ## `checks.yml` `checks.yml` uses `ghost.checks/v1`. Active checks are deterministic and must -declare a typed `derives_from` reference into `fingerprint.yml`. +declare a typed `derives_from` reference into `fingerprint.yml`. Checks keep +`status: active | proposed | disabled` because enforcement still needs state. ```yaml schema: ghost.checks/v1 @@ -197,45 +212,28 @@ decided_at: "2026-05-17T00:00:00.000Z" `ghost review --include-memory` reads only decisions with `status: accepted`. -## `proposals/*.yml` +## Core Commands -Candidate memory updates use `ghost.proposal/v1`. Agents create proposals when -generation or review exposes missing memory, intentional divergence, -experience gaps, or possible deterministic checks. Proposals are never -canonical until a human promotes them. - -```yaml -schema: ghost.proposal/v1 -id: saved-payment-empty-state -status: open -kind: missing-memory -title: Saved payment empty state should teach recovery -claim: Empty states for saved payment methods should prioritize recovery. -rationale: The user is blocked from paying, not browsing product concepts. -evidence: - - path: apps/payments/empty-state.tsx -proposed_action: - target: fingerprint - summary: Promote into fingerprint.yml if repeated. +```bash +ghost init +ghost lint .ghost +ghost verify .ghost --root . +ghost check --base main --format json +ghost review --base main --include-memory +ghost emit review-command --path apps/checkout/review/page.tsx +ghost emit context-bundle ``` -## Commands +Advanced scoped-memory and wrapper commands remain available: ```bash ghost init --with-intent ghost init --with-config --reference packages/ghost-ui/.ghost ghost init --scope apps/checkout --with-intent ghost init --scope apps/checkout --memory-dir .design/memory -ghost lint .ghost -ghost verify .ghost --root . ghost lint --all ghost verify --all ghost stack apps/checkout/review/page.tsx -ghost check --base main --format json -ghost review --base main --include-memory -ghost proposal create --path apps/checkout/review/page.tsx --id checkout-copy-memory --kind missing-memory --title "Checkout copy memory" --claim "Checkout copy needs local memory." --rationale "The checkout surface has specific payment review language." --summary "Add checkout copy guidance." -ghost emit review-command --path apps/checkout/review/page.tsx -ghost emit context-bundle ``` When `--reference packages/ghost-ui/.ghost` is used, generated config points to @@ -244,5 +242,5 @@ When `--reference packages/ghost-ui/.ghost` is used, generated config points to vocabulary; it is not copied into the product's own memory. Use `ghost inventory > .ghost/cache/inventory.json` when observed repo facts are -useful source material. Promote only durable conclusions into -`fingerprint.yml`. +useful source material. Make `.ghost/cache/` first when it does not exist. +Curate durable conclusions into `fingerprint.yml`. diff --git a/docs/generation-loop.md b/docs/generation-loop.md index afd64308..6aba443a 100644 --- a/docs/generation-loop.md +++ b/docs/generation-loop.md @@ -1,15 +1,11 @@ # Product Fingerprint Loop Ghost gives UI generators and product-development agents local, auditable -product experience memory. The canonical input is the resolved Ghost memory -stack for the task path, starting with `.ghost/fingerprint.yml` and adding any -nested child bundles. +product experience memory. Generation starts from checked-in prose, +optional inventory, and exemplars. Checks validate the result afterward. ```text -.ghost/fingerprint.yml -apps/checkout/.ghost/fingerprint.yml -merged checks, intent, decisions, proposals -.ghost/cache/inventory.json +product prose + inventory + exemplars | v host agent or generator @@ -24,32 +20,37 @@ ghost check + ghost review deterministic gates + advisory product-experience findings ``` -Ghost prepares the input and checks the output. It does not own the generator. -Use any agent or tool that can read local context and apply changes. +Ghost prepares the input and checks the output. It does not own the generator, +memory lifecycle, approval workflow, or design-system registry. Use any agent or +tool that can read local context and apply changes. ## Before Generation -Build a brief from the resolved memory stack: +Build a brief from the generation packet: -1. Run `ghost stack <path>` or resolve the applicable `.ghost/` layers for the - task path. -2. Read broad-to-local merged `fingerprint.yml` memory. -3. Select the relevant `situations`. -4. Carry applicable `principles`, `experience_contracts`, and `patterns` into +1. Read `.ghost/fingerprint.yml` as canonical product prose and exemplar + anchors. +2. Select the relevant `situations`. +3. Carry applicable `principles`, `experience_contracts`, and `patterns` into the work. -5. Use `implementation_vocabulary` only as current material that may help - satisfy the selected product memory. -6. Read merged checks to know which deterministic rules can block. -7. Read open proposals from the stack as unresolved context, not truth. +4. Inspect relevant `exemplars` as concrete examples of what good looks like. +5. Use generated inventory and `implementation_vocabulary` only as material + that may help satisfy the selected product memory. +6. Read active checks in `.ghost/checks.yml` to know which deterministic rules + can block. +7. Use optional `intent.md`, accepted decisions, and nested stacks only when + the project has opted into those advanced inputs. Generated inventory can help orient an agent, but it is cache: ```bash +mkdir -p .ghost/cache ghost inventory > .ghost/cache/inventory.json ``` -Inventory answers what exists now. The fingerprint answers what matters, why, -and how agents should compose or review product experience. +Inventory answers what exists now. Fingerprint prose answers what matters and +why. Exemplars show concrete surfaces an agent should inspect before composing +or reviewing product experience. ## Generation @@ -59,13 +60,14 @@ The generator should preserve: - relevant user/task/state obligations - interface and capability behavior - copy, disclosure, failure, and recovery contracts -- restraint and pacing from accepted patterns +- restraint and pacing from patterns +- concrete precedent from exemplars - accessibility, responsive behavior, and visual choices when they are grounded in principles, contracts, or patterns -If the requested work intentionally diverges from memory, the agent should name -the divergence in its response or create a proposal. It should not rewrite -canonical memory silently. +If requested work intentionally diverges from memory, the agent should name the +divergence in its response. Memory changes are ordinary Git-reviewed edits to +`fingerprint.yml`, `checks.yml`, and optional rationale files when present. ## Review @@ -90,20 +92,20 @@ ghost review --base main --include-memory Without `--package`, advisory review packets include `stacks[]`, one for each changed-file memory stack. Each stack includes changed files, layer dirs, merged -fingerprint memory, merged checks, proposals, and provenance. +fingerprint memory, merged checks, decisions, and provenance. Advisory review packets include: - the current diff - `fingerprint.yml` memory +- relevant exemplars - active checks - optional accepted decisions -- open proposals - finding categories for fixes, intentional divergence, missing memory, experience gaps, and eval uncertainty -Review findings should cite the diff location, relevant fingerprint memory, any -active check when blocking, and open proposals when relevant. +Review findings should cite the diff location, relevant fingerprint memory, +relevant exemplars when useful, and any active check when blocking. ## Remediation @@ -111,9 +113,8 @@ When review flags drift, the host agent chooses the smallest useful response: - Fix the generated or changed code. - Explain why a divergence is intentional. -- Create a `missing-memory`, `intentional-divergence`, `experience-gap`, or - `check-candidate` proposal. -- Promote memory only when a human accepts the change. +- Update `fingerprint.yml`, `checks.yml`, or optional rationale files when the + user asks to change memory. The loop is: @@ -122,8 +123,7 @@ brief from fingerprint -> generate or edit -> run ghost check -> run ghost review - -> fix code or propose memory - -> human promotes durable memory + -> fix code or update memory through Git ``` ## CI @@ -137,7 +137,7 @@ ghost check --base main ghost review --base main --format markdown ``` -Wrappers that store memory outside `.ghost` can pass +Advanced wrappers that store memory outside `.ghost` can pass `--memory-dir <relative-dir>` to stack-aware commands. `--package <dir>` remains exact single-bundle mode and bypasses stack discovery. diff --git a/docs/host-adapters.md b/docs/host-adapters.md index 4389d651..b2f04627 100644 --- a/docs/host-adapters.md +++ b/docs/host-adapters.md @@ -10,11 +10,13 @@ are written. Ghost provides: -- `fingerprint.yml`, `checks.yml`, proposals, decisions, and stack merge rules. +- `fingerprint.yml` product prose and exemplars, optional `checks.yml`, + decisions, intent, and stack merge rules. - `ghost check --format json` as the stable `ghost.check-report/v1` contract. - `ghost review --format json` for advisory packets grounded in the resolved memory stack. -- `ghost proposal create/list/resolve` for deterministic draft memory files. +- `ghost emit context-bundle` for a generation packet that separates product + prose, optional inventory, exemplars, and active checks. - `--memory-dir <relative-dir>` for wrappers that store Ghost memory somewhere other than `.ghost`. @@ -24,11 +26,15 @@ Host adapters provide: - 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 -- any LLM or human approval flow for promoting proposals into canonical memory +- normal Git review for memory edits Ghost does not emit host-specific check formats. Consume JSON and translate it outside Ghost. +Inventory cache is optional source material. Adapters should not treat +`.ghost/cache/inventory.json` as canonical product memory; checked-in +`fingerprint.yml` remains the authority. + ## Check Flow Run deterministic checks and consume the JSON report: @@ -69,23 +75,14 @@ relative directory: ghost init --scope apps/checkout --memory-dir .design/memory ghost stack apps/checkout/review/page.tsx --memory-dir .design/memory --format json ghost check --base main --memory-dir .design/memory --format json -ghost proposal create --path apps/checkout/review/page.tsx --memory-dir .design/memory --id checkout-copy-memory --kind missing-memory --title "Checkout copy memory" --claim "Checkout copy needs local memory." --rationale "Payment review language is checkout-specific." --summary "Add checkout copy guidance." +ghost review --base main --memory-dir .design/memory --format json ``` `--package <dir>` remains exact single-bundle mode. Use it when the caller already knows the package directory and wants to bypass stack discovery. -## Proposal Flow - -Adapters should use proposals as the draft layer. Ghost intentionally does not -invent or promote final fingerprint memory. - -```bash -ghost proposal create --path apps/checkout/review/page.tsx --id checkout-copy-memory --kind missing-memory --title "Checkout copy memory" --claim "Checkout copy needs local memory." --rationale "Payment review language is checkout-specific." --summary "Add checkout copy guidance." --format json -ghost proposal list --path apps/checkout/review/page.tsx --format json -ghost proposal resolve checkout-copy-memory --path apps/checkout/review/page.tsx --status accepted --format json -``` +## Memory Edits -The JSON result for create and resolve always includes `package_dir`, `path`, -and `proposal`. Promotion into `fingerprint.yml` or `checks.yml` remains a -separate human-approved or host-agent action. +Adapters do not need a special Ghost draft layer. If memory work is uncommitted +or unmerged, it is draft work. Once `fingerprint.yml`, `checks.yml`, decisions, +or intent are checked in, Ghost treats them as truth for deterministic tooling. diff --git a/install/install.sh b/install/install.sh index 000d5bce..bca9e8fb 100755 --- a/install/install.sh +++ b/install/install.sh @@ -1,5 +1,5 @@ #!/bin/sh -# Ghost — install the unified design-memory skill bundle. +# Ghost — install the unified product-experience memory skill bundle. # # Usage: # curl -fsSL https://raw.githubusercontent.com/block/ghost/main/install/install.sh | sh @@ -14,10 +14,9 @@ # What gets installed: # <agent-skills-dir>/ghost/ # SKILL.md -# references/scan.md, map.md, survey.md, patterns.md, schema.md -# references/recall.md, brief.md, critique.md, capture.md, promote.md -# references/review.md, verify.md, compare.md, remediate.md -# assets/fingerprint.template.md +# references/brief.md, capture.md, compare.md, critique.md, map.md +# references/patterns.md, recall.md, remediate.md, review.md +# references/schema.md, survey.md, verify.md # # Exit codes: # 0 installed @@ -193,7 +192,8 @@ printf '\nInstalled %d files to %s\n' "$count" "$GHOST_DEST" printf '\n' printf 'Next:\n' printf ' cd <your-repo>\n' -printf ' Tell your agent: "Scan this project with ghost"\n' +printf ' Tell your agent: "Set up Ghost memory for this repo"\n' printf '\n' -printf 'The agent will produce resources.yml → map.md → survey.json → patterns.yml,\n' -printf 'then use the fingerprint to brief, review, or capture product-experience memory.\n' +printf 'The agent will use fingerprint.yml as checked-in product-experience memory,\n' +printf 'curate exemplars, keep checks.yml as deterministic gates, optionally gather\n' +printf 'inventory cache, and run ghost lint/verify/check/review.\n' diff --git a/install/manifest.json b/install/manifest.json index d7234d2c..c8491d39 100644 --- a/install/manifest.json +++ b/install/manifest.json @@ -1,27 +1,24 @@ { "$schema": "ghost.install-manifest/v1", "name": "ghost", - "description": "Ghost recipes — create, activate, review, and evolve the repo-local fingerprint bundle.", + "description": "Ghost recipes — create, brief, review, and preserve the repo-local fingerprint bundle.", "version": "0.1.0", "source": { "package": "packages/ghost/src/skill-bundle" }, "files": [ "SKILL.md", - "references/scan.md", - "references/map.md", - "references/survey.md", - "references/patterns.md", - "references/schema.md", - "references/recall.md", "references/brief.md", - "references/critique.md", "references/capture.md", - "references/promote.md", - "references/review.md", - "references/verify.md", "references/compare.md", + "references/critique.md", + "references/map.md", + "references/patterns.md", + "references/recall.md", "references/remediate.md", - "assets/fingerprint.template.md" + "references/review.md", + "references/schema.md", + "references/survey.md", + "references/verify.md" ] } diff --git a/package.json b/package.json index d4f30203..9b7dbe6f 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,10 @@ "test:watch": "vitest", "typecheck": "tsc --build", "build:ui": "pnpm --filter ghost-ui build", - "check": "biome check . && pnpm typecheck && pnpm check:file-sizes && pnpm check:docs && pnpm check:cli-manifest", + "check": "biome check . && pnpm typecheck && pnpm check:file-sizes && pnpm check:docs && pnpm check:install-bundle && pnpm check:cli-manifest", "check:file-sizes": "node scripts/check-file-sizes.mjs", "check:docs": "node scripts/check-docs-frontmatter.mjs", + "check:install-bundle": "node scripts/check-install-bundle.mjs", "check:cli-manifest": "node scripts/dump-cli-help.mjs --check", "dump:cli-help": "node scripts/dump-cli-help.mjs", "fmt": "biome format --write .", diff --git a/packages/ghost-core/src/checks/index.ts b/packages/ghost-core/src/checks/index.ts index 6ec7d7fa..6f54a656 100644 --- a/packages/ghost-core/src/checks/index.ts +++ b/packages/ghost-core/src/checks/index.ts @@ -6,18 +6,21 @@ export { routeGhostPathToScopes, } from "./routing.js"; export { + GhostCheckDerivesFromSchema, GhostCheckSchema, GhostChecksSchema, } from "./schema.js"; export type { GhostCheck, GhostCheckAppliesTo, + GhostCheckDerivesFrom, GhostCheckDetector, GhostCheckDetectorType, GhostCheckEvidence, GhostCheckSeverity, GhostCheckStatus, GhostChecksDocument, + GhostChecksFingerprintMemory, GhostChecksLintIssue, GhostChecksLintOptions, GhostChecksLintReport, diff --git a/packages/ghost-core/src/checks/lint.ts b/packages/ghost-core/src/checks/lint.ts index 3760bd2a..7bcd2368 100644 --- a/packages/ghost-core/src/checks/lint.ts +++ b/packages/ghost-core/src/checks/lint.ts @@ -9,6 +9,13 @@ import type { } from "./types.js"; const SUPPORT_FLOOR = 0.85; +const GROUNDING_PREFIXES = [ + "principle", + "situation", + "experience_contract", + "pattern", +] as const; +type GroundingPrefix = (typeof GROUNDING_PREFIXES)[number]; export function lintGhostChecks( input: unknown, @@ -65,6 +72,8 @@ function checkOne( ): void { const path = `checks[${index}]`; checkDetector(check, path, issues); + checkGrounding(check, path, options, issues); + checkAppliesToFingerprintTargets(check, path, options, issues); if (check.status === "disabled") return; @@ -78,7 +87,7 @@ function checkOne( }); } - if (options.map && check.applies_to?.scopes?.length) { + if (!options.fingerprint && options.map && check.applies_to?.scopes?.length) { const scopeIds = new Set( getEffectiveMapScopes(options.map).map((scope) => scope.id), ); @@ -139,6 +148,125 @@ function checkOne( } } +function checkAppliesToFingerprintTargets( + check: GhostCheck, + path: string, + options: GhostChecksLintOptions, + issues: GhostChecksLintIssue[], +): void { + if (!check.applies_to || !options.fingerprint) return; + + const severity = check.status === "active" ? "error" : "warning"; + const targets = collectFingerprintRoutingTargets(options.fingerprint); + + check.applies_to.scopes?.forEach((scope, scopeIndex) => { + if (targets.scopes.has(scope)) return; + issues.push({ + severity, + rule: "check-scope-unknown", + message: `Check references unknown fingerprint scope '${scope}'.`, + path: `${path}.applies_to.scopes[${scopeIndex}]`, + }); + }); + + check.applies_to.surface_types?.forEach((surfaceType, surfaceIndex) => { + if (targets.surfaceTypes.has(surfaceType)) return; + issues.push({ + severity, + rule: "check-surface-type-unknown", + message: `Check references unknown fingerprint surface type '${surfaceType}'.`, + path: `${path}.applies_to.surface_types[${surfaceIndex}]`, + }); + }); + + check.applies_to.pattern_ids?.forEach((patternId, patternIndex) => { + if (targets.patterns.has(patternId)) return; + issues.push({ + severity, + rule: "check-pattern-unknown", + message: `Check references unknown fingerprint pattern '${patternId}'.`, + path: `${path}.applies_to.pattern_ids[${patternIndex}]`, + }); + }); +} + +function checkGrounding( + check: GhostCheck, + path: string, + options: GhostChecksLintOptions, + issues: GhostChecksLintIssue[], +): void { + if (check.status === "active" && !check.derives_from) { + issues.push({ + severity: "error", + rule: "check-grounding-missing", + message: + "Active checks must declare derives_from with a typed fingerprint.yml reference.", + path: `${path}.derives_from`, + }); + return; + } + + if (!check.derives_from || !options.fingerprint) return; + + const parsed = parseGroundingRef(check.derives_from); + if (!parsed) return; + + const targets = collectFingerprintTargets(options.fingerprint); + if (targets[parsed.prefix].has(parsed.id)) return; + + issues.push({ + severity: check.status === "active" ? "error" : "warning", + rule: "check-grounding-unknown", + message: `Check derives_from references unknown fingerprint memory '${check.derives_from}'.`, + path: `${path}.derives_from`, + }); +} + +function collectFingerprintRoutingTargets( + fingerprint: NonNullable<GhostChecksLintOptions["fingerprint"]>, +): { + scopes: Set<string>; + surfaceTypes: Set<string>; + patterns: Set<string>; +} { + const surfaceTypes = new Set(fingerprint.topology?.surface_types ?? []); + for (const scope of fingerprint.topology?.scopes ?? []) { + for (const surfaceType of scope.surface_types ?? []) { + surfaceTypes.add(surfaceType); + } + } + return { + scopes: new Set( + fingerprint.topology?.scopes?.map((entry) => entry.id) ?? [], + ), + surfaceTypes, + patterns: new Set(fingerprint.patterns?.map((entry) => entry.id) ?? []), + }; +} + +function parseGroundingRef( + ref: string, +): { prefix: GroundingPrefix; id: string } | undefined { + const [prefix, id] = ref.split(":"); + if (!prefix || !id) return undefined; + if (!GROUNDING_PREFIXES.includes(prefix as GroundingPrefix)) return undefined; + return { prefix: prefix as GroundingPrefix, id }; +} + +function collectFingerprintTargets( + fingerprint: NonNullable<GhostChecksLintOptions["fingerprint"]>, +): Record<GroundingPrefix, Set<string>> { + return { + principle: new Set(fingerprint.principles?.map((entry) => entry.id) ?? []), + situation: new Set(fingerprint.situations?.map((entry) => entry.id) ?? []), + experience_contract: new Set( + fingerprint.experience_contracts?.map((entry) => entry.id) ?? [], + ), + pattern: new Set(fingerprint.patterns?.map((entry) => entry.id) ?? []), + }; +} + function checkDetector( check: GhostCheck, path: string, diff --git a/packages/ghost-core/src/checks/schema.ts b/packages/ghost-core/src/checks/schema.ts index 14387819..ed3cec2f 100644 --- a/packages/ghost-core/src/checks/schema.ts +++ b/packages/ghost-core/src/checks/schema.ts @@ -4,6 +4,17 @@ import { GHOST_CHECKS_SCHEMA } from "./types.js"; const GhostCheckStatusSchema = z.enum(["active", "proposed", "disabled"]); const GhostCheckSeveritySchema = z.enum(["critical", "serious", "nit"]); +export const GhostCheckDerivesFromSchema = z + .string() + .min(1) + .regex( + /^(principle|situation|experience_contract|pattern):[a-z0-9][a-z0-9._-]*$/, + { + message: + "derives_from must use a typed fingerprint ref, e.g. principle:dense-workflows", + }, + ); + const GhostCheckAppliesToSchema = z .object({ scopes: z.array(z.string().min(1)).optional(), @@ -58,6 +69,7 @@ export const GhostCheckSchema = z title: z.string().min(1), status: GhostCheckStatusSchema, severity: GhostCheckSeveritySchema, + derives_from: GhostCheckDerivesFromSchema.optional(), applies_to: GhostCheckAppliesToSchema.optional(), detector: GhostCheckDetectorSchema, evidence: GhostCheckEvidenceSchema.optional(), diff --git a/packages/ghost-core/src/checks/types.ts b/packages/ghost-core/src/checks/types.ts index 417340de..4321d3fc 100644 --- a/packages/ghost-core/src/checks/types.ts +++ b/packages/ghost-core/src/checks/types.ts @@ -5,6 +5,22 @@ export const GHOST_CHECKS_FILENAME = "checks.yml" as const; export type GhostCheckStatus = "active" | "proposed" | "disabled"; export type GhostCheckSeverity = "critical" | "serious" | "nit"; +export type GhostCheckDerivesFrom = + | `principle:${string}` + | `situation:${string}` + | `experience_contract:${string}` + | `pattern:${string}`; + +export interface GhostChecksFingerprintMemory { + topology?: { + scopes?: { id: string; surface_types?: string[] }[]; + surface_types?: string[]; + }; + principles?: { id: string }[]; + situations?: { id: string }[]; + experience_contracts?: { id: string }[]; + patterns?: { id: string }[]; +} export type GhostCheckDetectorType = | "forbidden-regex" @@ -38,6 +54,7 @@ export interface GhostCheck { title: string; status: GhostCheckStatus; severity: GhostCheckSeverity; + derives_from?: GhostCheckDerivesFrom; applies_to?: GhostCheckAppliesTo; detector: GhostCheckDetector; evidence?: GhostCheckEvidence; @@ -68,6 +85,7 @@ export interface GhostChecksLintReport { export interface GhostChecksLintOptions { map?: Pick<MapFrontmatter, "scopes" | "feature_areas">; + fingerprint?: GhostChecksFingerprintMemory; } export interface RoutedGhostCheck { diff --git a/packages/ghost-core/src/fingerprint-package.ts b/packages/ghost-core/src/fingerprint-package.ts index 5e947a48..4e4fd9bf 100644 --- a/packages/ghost-core/src/fingerprint-package.ts +++ b/packages/ghost-core/src/fingerprint-package.ts @@ -1,13 +1,17 @@ export const FINGERPRINT_PACKAGE_DIR = ".ghost" as const; export const RESOURCES_FILENAME = "resources.yml" as const; export const PATTERNS_FILENAME = "patterns.yml" as const; +export const FINGERPRINT_YML_FILENAME = "fingerprint.yml" as const; +export const CONFIG_FILENAME = "config.yml" as const; export const INTENT_FILENAME = "intent.md" as const; export const FINGERPRINT_FILENAME = "fingerprint.md" as const; export const DECISIONS_DIRNAME = "decisions" as const; -export const PROPOSALS_DIRNAME = "proposals" as const; +export const CACHE_DIRNAME = "cache" as const; export interface FingerprintPackagePaths { dir: string; + fingerprintYml: string; + config: string; resources: string; map: string; survey: string; @@ -17,5 +21,5 @@ export interface FingerprintPackagePaths { checks: string; intent: string; decisions: string; - proposals: string; + cache: string; } diff --git a/packages/ghost-core/src/index.ts b/packages/ghost-core/src/index.ts index c0cda361..ee21719f 100644 --- a/packages/ghost-core/src/index.ts +++ b/packages/ghost-core/src/index.ts @@ -9,6 +9,7 @@ export type { GhostCheckSeverity, GhostCheckStatus, GhostChecksDocument, + GhostChecksFingerprintMemory, GhostChecksLintIssue, GhostChecksLintOptions, GhostChecksLintReport, @@ -55,13 +56,15 @@ export { } from "./embedding/index.js"; // --- Map (ghost.map/v2) --- export { + CACHE_DIRNAME, + CONFIG_FILENAME, DECISIONS_DIRNAME, FINGERPRINT_FILENAME, FINGERPRINT_PACKAGE_DIR, + FINGERPRINT_YML_FILENAME, type FingerprintPackagePaths, INTENT_FILENAME, PATTERNS_FILENAME, - PROPOSALS_DIRNAME, RESOURCES_FILENAME, } from "./fingerprint-package.js"; // --- Map (ghost.map/v2) --- @@ -81,7 +84,7 @@ export { slugifyScopeId, type TopLevelEntry, } from "./map/index.js"; -// --- Memory (ghost.decision/v1 + ghost.proposal/v1) --- +// --- Memory (ghost.decision/v1) --- export type { GhostDecisionDocument, GhostDecisionStatus, @@ -90,24 +93,14 @@ export type { GhostMemoryLintIssue, GhostMemoryLintReport, GhostMemoryLintSeverity, - GhostProposalAction, - GhostProposalDocument, - GhostProposalKind, - GhostProposalStatus, - GhostProposalTarget, } from "./memory/index.js"; export { GHOST_DECISION_SCHEMA, GHOST_DECISIONS_DIRNAME, - GHOST_PROPOSAL_SCHEMA, - GHOST_PROPOSALS_DIRNAME, GhostDecisionSchema, GhostExperienceEvidenceSchema, GhostExperienceScopeSchema, - GhostProposalActionSchema, - GhostProposalSchema, lintGhostDecision, - lintGhostProposal, } from "./memory/index.js"; // --- Patterns (ghost.patterns/v1) --- export type { diff --git a/packages/ghost-core/src/memory/index.ts b/packages/ghost-core/src/memory/index.ts index 1f733024..2500ec31 100644 --- a/packages/ghost-core/src/memory/index.ts +++ b/packages/ghost-core/src/memory/index.ts @@ -1,10 +1,8 @@ -export { lintGhostDecision, lintGhostProposal } from "./lint.js"; +export { lintGhostDecision } from "./lint.js"; export { GhostDecisionSchema, GhostExperienceEvidenceSchema, GhostExperienceScopeSchema, - GhostProposalActionSchema, - GhostProposalSchema, } from "./schema.js"; export type { GhostDecisionDocument, @@ -14,15 +12,5 @@ export type { GhostMemoryLintIssue, GhostMemoryLintReport, GhostMemoryLintSeverity, - GhostProposalAction, - GhostProposalDocument, - GhostProposalKind, - GhostProposalStatus, - GhostProposalTarget, -} from "./types.js"; -export { - GHOST_DECISION_SCHEMA, - GHOST_DECISIONS_DIRNAME, - GHOST_PROPOSAL_SCHEMA, - GHOST_PROPOSALS_DIRNAME, } from "./types.js"; +export { GHOST_DECISION_SCHEMA, GHOST_DECISIONS_DIRNAME } from "./types.js"; diff --git a/packages/ghost-core/src/memory/lint.ts b/packages/ghost-core/src/memory/lint.ts index 7e79a07e..e6efb13e 100644 --- a/packages/ghost-core/src/memory/lint.ts +++ b/packages/ghost-core/src/memory/lint.ts @@ -1,5 +1,5 @@ import type { ZodIssue } from "zod"; -import { GhostDecisionSchema, GhostProposalSchema } from "./schema.js"; +import { GhostDecisionSchema } from "./schema.js"; import type { GhostMemoryLintIssue, GhostMemoryLintReport } from "./types.js"; export function lintGhostDecision(input: unknown): GhostMemoryLintReport { @@ -8,12 +8,6 @@ export function lintGhostDecision(input: unknown): GhostMemoryLintReport { return finalize([]); } -export function lintGhostProposal(input: unknown): GhostMemoryLintReport { - const result = GhostProposalSchema.safeParse(input); - if (!result.success) return finalize(zodIssues(result.error.issues)); - return finalize([]); -} - function zodIssues(issues: ZodIssue[]): GhostMemoryLintIssue[] { return issues.map((issue) => ({ severity: "error" as const, diff --git a/packages/ghost-core/src/memory/schema.ts b/packages/ghost-core/src/memory/schema.ts index eb9c24c3..30330331 100644 --- a/packages/ghost-core/src/memory/schema.ts +++ b/packages/ghost-core/src/memory/schema.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { GHOST_DECISION_SCHEMA, GHOST_PROPOSAL_SCHEMA } from "./types.js"; +import { GHOST_DECISION_SCHEMA } from "./types.js"; const SlugIdSchema = z .string() @@ -56,25 +56,3 @@ export const GhostDecisionSchema = z decided_at: z.string().datetime({ offset: true }), }) .strict(); - -export const GhostProposalActionSchema = z - .object({ - target: z.enum(["decisions", "patterns", "checks", "intent"]), - summary: z.string().min(1), - }) - .strict(); - -export const GhostProposalSchema = z - .object({ - schema: z.literal(GHOST_PROPOSAL_SCHEMA), - id: SlugIdSchema, - status: z.enum(["open", "accepted", "rejected", "superseded"]), - kind: z.enum(["decision", "pattern", "check", "intent"]), - title: z.string().min(1), - claim: z.string().min(1), - rationale: z.string().min(1), - scope: GhostExperienceScopeSchema.optional(), - evidence: z.array(GhostExperienceEvidenceSchema).min(1), - proposed_action: GhostProposalActionSchema, - }) - .strict(); diff --git a/packages/ghost-core/src/memory/types.ts b/packages/ghost-core/src/memory/types.ts index 09c91c85..c2575a70 100644 --- a/packages/ghost-core/src/memory/types.ts +++ b/packages/ghost-core/src/memory/types.ts @@ -1,21 +1,8 @@ export const GHOST_DECISION_SCHEMA = "ghost.decision/v1" as const; -export const GHOST_PROPOSAL_SCHEMA = "ghost.proposal/v1" as const; export const GHOST_DECISIONS_DIRNAME = "decisions" as const; -export const GHOST_PROPOSALS_DIRNAME = "proposals" as const; export type GhostDecisionStatus = "accepted" | "rejected" | "superseded"; -export type GhostProposalStatus = - | "open" - | "accepted" - | "rejected" - | "superseded"; -export type GhostProposalKind = "decision" | "pattern" | "check" | "intent"; -export type GhostProposalTarget = - | "decisions" - | "patterns" - | "checks" - | "intent"; export interface GhostExperienceScope { roles?: string[]; @@ -44,24 +31,6 @@ export interface GhostDecisionDocument { decided_at: string; } -export interface GhostProposalAction { - target: GhostProposalTarget; - summary: string; -} - -export interface GhostProposalDocument { - schema: typeof GHOST_PROPOSAL_SCHEMA; - id: string; - status: GhostProposalStatus; - kind: GhostProposalKind; - title: string; - claim: string; - rationale: string; - scope?: GhostExperienceScope; - evidence: GhostExperienceEvidence[]; - proposed_action: GhostProposalAction; -} - export type GhostMemoryLintSeverity = "error" | "warning" | "info"; export interface GhostMemoryLintIssue { diff --git a/packages/ghost-core/test/checks.test.ts b/packages/ghost-core/test/checks.test.ts index 3d869641..8d7cfc60 100644 --- a/packages/ghost-core/test/checks.test.ts +++ b/packages/ghost-core/test/checks.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; import { type GhostChecksDocument, + type GhostChecksFingerprintMemory, lintGhostChecks, type MapFrontmatter, routeGhostChecksForPath, @@ -36,6 +37,7 @@ function checks( title: "Use design tokens for UI color", status: "active", severity: "serious", + derives_from: "principle:tokenized-ui-color", applies_to: { scopes: ["lending"], paths: ["Code/Features/Lending"], @@ -64,6 +66,80 @@ describe("ghost.checks/v1", () => { expect(report.errors).toBe(0); }); + it("requires active checks to declare derives_from", () => { + const report = lintGhostChecks(checks({ derives_from: undefined })); + + expect(report.errors).toBe(1); + expect(report.issues[0]).toMatchObject({ + rule: "check-grounding-missing", + path: "checks[0].derives_from", + }); + }); + + it("accepts active checks grounded in fingerprint memory", () => { + const report = lintGhostChecks(checks(), { + fingerprint: fingerprintMemory(), + }); + + expect(report.errors).toBe(0); + expect(report.warnings).toBe(0); + }); + + it("reports active checks grounded in missing fingerprint memory", () => { + const report = lintGhostChecks( + checks({ derives_from: "principle:not-recorded" }), + { + fingerprint: fingerprintMemory(), + }, + ); + + expect(report.errors).toBe(1); + expect(report.issues[0]).toMatchObject({ + rule: "check-grounding-unknown", + path: "checks[0].derives_from", + }); + }); + + it("rejects untyped derives_from references at schema level", () => { + const report = lintGhostChecks( + checks({ derives_from: "tokenized-ui-color" as never }), + ); + + expect(report.errors).toBe(1); + expect(report.issues[0]?.rule).toBe("schema/invalid_format"); + }); + + it("rejects implementation-only derives_from references at schema level", () => { + const report = lintGhostChecks( + checks({ derives_from: "implementation_vocabulary:tokens" as never }), + ); + + expect(report.errors).toBe(1); + expect(report.issues[0]?.rule).toBe("schema/invalid_format"); + }); + + it("fails active checks that reference unknown fingerprint targets", () => { + const report = lintGhostChecks( + checks({ + applies_to: { + scopes: ["unknown-scope"], + surface_types: ["unknown-surface"], + pattern_ids: ["unknown-pattern"], + }, + }), + { fingerprint: fingerprintMemory() }, + ); + + expect(report.errors).toBe(3); + expect(report.issues).toEqual( + expect.arrayContaining([ + expect.objectContaining({ rule: "check-scope-unknown" }), + expect.objectContaining({ rule: "check-surface-type-unknown" }), + expect.objectContaining({ rule: "check-pattern-unknown" }), + ]), + ); + }); + it("fails invalid detector regex", () => { const report = lintGhostChecks( checks({ detector: { type: "forbidden-regex", pattern: "[" } }), @@ -106,3 +182,21 @@ describe("ghost.checks/v1", () => { expect(routed).toEqual([]); }); }); + +function fingerprintMemory(): GhostChecksFingerprintMemory { + return { + topology: { + scopes: [ + { + id: "lending", + surface_types: ["native-feature"], + }, + ], + surface_types: ["native-feature"], + }, + principles: [{ id: "tokenized-ui-color" }], + situations: [], + experience_contracts: [], + patterns: [{ id: "tokenized-ui-color" }], + }; +} diff --git a/packages/ghost-core/test/memory.test.ts b/packages/ghost-core/test/memory.test.ts index 1b92101c..28e6658c 100644 --- a/packages/ghost-core/test/memory.test.ts +++ b/packages/ghost-core/test/memory.test.ts @@ -1,13 +1,8 @@ import { describe, expect, it } from "vitest"; -import { - GHOST_DECISION_SCHEMA, - GHOST_PROPOSAL_SCHEMA, - lintGhostDecision, - lintGhostProposal, -} from "../src/index.js"; +import * as core from "../src/index.js"; const VALID_DECISION = { - schema: GHOST_DECISION_SCHEMA, + schema: core.GHOST_DECISION_SCHEMA, id: "checkout-reversibility", status: "accepted", title: "Reversibility before money movement", @@ -28,35 +23,15 @@ const VALID_DECISION = { decided_at: "2026-05-17T00:00:00.000Z", }; -const VALID_PROPOSAL = { - schema: GHOST_PROPOSAL_SCHEMA, - id: "saved-payment-empty-state", - status: "open", - kind: "decision", - title: "Saved payment empty state should teach recovery", - claim: - "Empty states for saved payment methods should prioritize recovery over education.", - rationale: "The user is blocked from paying, not browsing product concepts.", - scope: { - roles: ["design", "pm", "qa"], - surface_types: ["empty-state"], - }, - evidence: [{ path: "apps/payments/empty-state.tsx" }], - proposed_action: { - target: "decisions", - summary: "Promote into a product-experience decision if repeated.", - }, -}; - describe("Ghost product-experience memory schemas", () => { it("accepts a valid ghost.decision/v1 document", () => { - const report = lintGhostDecision(VALID_DECISION); + const report = core.lintGhostDecision(VALID_DECISION); expect(report.errors).toBe(0); }); it("rejects a decision without auditable evidence", () => { - const report = lintGhostDecision({ + const report = core.lintGhostDecision({ ...VALID_DECISION, evidence: [], }); @@ -65,24 +40,15 @@ describe("Ghost product-experience memory schemas", () => { expect(report.issues.map((issue) => issue.path)).toContain("evidence"); }); - it("accepts a valid ghost.proposal/v1 document", () => { - const report = lintGhostProposal(VALID_PROPOSAL); - - expect(report.errors).toBe(0); - }); - - it("rejects a proposal with an unknown proposed target", () => { - const report = lintGhostProposal({ - ...VALID_PROPOSAL, - proposed_action: { - target: "roadmap", - summary: "This should stay outside Ghost.", - }, - }); - - expect(report.errors).toBeGreaterThan(0); - expect(report.issues.map((issue) => issue.path)).toContain( - "proposed_action.target", - ); + it("does not expose proposal-era memory symbols from private core", () => { + for (const symbol of [ + "GHOST_PROPOSAL_SCHEMA", + "GHOST_PROPOSALS_DIRNAME", + "GhostProposalActionSchema", + "GhostProposalSchema", + "lintGhostProposal", + ]) { + expect(core).not.toHaveProperty(symbol); + } }); }); diff --git a/packages/ghost-ui/.ghost/fingerprint.yml b/packages/ghost-ui/.ghost/fingerprint.yml index 6afa072f..1b702bd3 100644 --- a/packages/ghost-ui/.ghost/fingerprint.yml +++ b/packages/ghost-ui/.ghost/fingerprint.yml @@ -42,6 +42,3 @@ implementation_vocabulary: - src/fonts notes: - Ghost UI is registry-distributed implementation vocabulary, not an installable npm package or consuming-product experience memory. -review_policy: - proposal_policy: - - Consumers create product-specific proposals in their own .ghost bundle. diff --git a/packages/ghost-ui/.ghost/proposals/.gitkeep b/packages/ghost-ui/.ghost/proposals/.gitkeep deleted file mode 100644 index 8b137891..00000000 --- a/packages/ghost-ui/.ghost/proposals/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/packages/ghost/README.md b/packages/ghost/README.md index 5def9d5c..d7eced43 100644 --- a/packages/ghost/README.md +++ b/packages/ghost/README.md @@ -1,10 +1,10 @@ # @anarchitecture/ghost -**Unified Ghost CLI for repo-local product fingerprints.** +**Unified Ghost CLI for repo-local product-experience memory.** -Ghost supports Fingerprint Capture for a root `.ghost/` bundle, checks diffs -against deterministic gates, emits advisory review packets, compares bundles, -and records intentional drift. It ships one CLI: `ghost`. +Ghost initializes root `.ghost/fingerprint.yml` memory, checks diffs against +deterministic gates, emits advisory review packets, compares bundles, and +records intentional drift. It ships one CLI: `ghost`. ## Install @@ -53,7 +53,7 @@ Ghost is bring-your-own-agent. The CLI performs deterministic work: inventory, lint, verify, compare, check, and handoff packet generation. The installed `ghost` skill teaches your host agent how to capture canonical `.ghost/fingerprint.yml` memory, brief work from it, review drift, verify -generated UI, remediate issues, and propose candidate fingerprint updates. +generated UI, remediate issues, and suggest memory edits when the user asks. ```bash ghost skill install @@ -62,7 +62,7 @@ ghost skill install Then ask your agent: ```text -Capture a Ghost fingerprint for this repo. +Set up Ghost memory for this repo. ``` ## License diff --git a/packages/ghost/src/ghost-core/fingerprint-package.ts b/packages/ghost/src/ghost-core/fingerprint-package.ts index 45e2797c..4e4fd9bf 100644 --- a/packages/ghost/src/ghost-core/fingerprint-package.ts +++ b/packages/ghost/src/ghost-core/fingerprint-package.ts @@ -6,7 +6,6 @@ export const CONFIG_FILENAME = "config.yml" as const; export const INTENT_FILENAME = "intent.md" as const; export const FINGERPRINT_FILENAME = "fingerprint.md" as const; export const DECISIONS_DIRNAME = "decisions" as const; -export const PROPOSALS_DIRNAME = "proposals" as const; export const CACHE_DIRNAME = "cache" as const; export interface FingerprintPackagePaths { @@ -22,6 +21,5 @@ export interface FingerprintPackagePaths { checks: string; intent: string; decisions: string; - proposals: string; cache: string; } diff --git a/packages/ghost/src/ghost-core/fingerprint/index.ts b/packages/ghost/src/ghost-core/fingerprint/index.ts index 385c052c..a5db74ed 100644 --- a/packages/ghost/src/ghost-core/fingerprint/index.ts +++ b/packages/ghost/src/ghost-core/fingerprint/index.ts @@ -1,26 +1,26 @@ export { lintGhostFingerprint } from "./lint.js"; export { GhostFingerprintEvidenceSchema, + GhostFingerprintExemplarSchema, GhostFingerprintExperienceContractSchema, GhostFingerprintImplementationVocabularySchema, + GhostFingerprintMemoryRefSchema, GhostFingerprintPatternKindSchema, GhostFingerprintPatternSchema, GhostFingerprintPrincipleSchema, GhostFingerprintRefPrefixSchema, GhostFingerprintRefSchema, - GhostFingerprintReviewPolicySchema, GhostFingerprintSchema, GhostFingerprintScopeSchema, GhostFingerprintSituationSchema, - GhostFingerprintStatusSchema, GhostFingerprintSummarySchema, - GhostFingerprintTopologyExampleSchema, GhostFingerprintTopologySchema, GhostFingerprintTopologyScopeSchema, } from "./schema.js"; export type { GhostFingerprintDocument, GhostFingerprintEvidence, + GhostFingerprintExemplar, GhostFingerprintExperienceContract, GhostFingerprintImplementationVocabulary, GhostFingerprintLintIssue, @@ -31,13 +31,10 @@ export type { GhostFingerprintPrinciple, GhostFingerprintRef, GhostFingerprintRefPrefix, - GhostFingerprintReviewPolicy, GhostFingerprintScope, GhostFingerprintSituation, - GhostFingerprintStatus, GhostFingerprintSummary, GhostFingerprintTopology, - GhostFingerprintTopologyExample, GhostFingerprintTopologyScope, } from "./types.js"; export { diff --git a/packages/ghost/src/ghost-core/fingerprint/lint.ts b/packages/ghost/src/ghost-core/fingerprint/lint.ts index 21d3d330..d47ae887 100644 --- a/packages/ghost/src/ghost-core/fingerprint/lint.ts +++ b/packages/ghost/src/ghost-core/fingerprint/lint.ts @@ -38,6 +38,7 @@ export function lintGhostFingerprint( checkDuplicateIds("principles", doc.principles, issues); checkDuplicateIds("experience_contracts", doc.experience_contracts, issues); checkDuplicateIds("patterns", doc.patterns, issues); + checkDuplicateIds("exemplars", doc.exemplars, issues); checkTopologyRefs(doc, issues); checkRefs(doc, issues); @@ -109,15 +110,6 @@ function checkTopologyRefs( }); }); - doc.topology.examples?.forEach((example, exampleIndex) => { - checkSurfaceTypeRef( - example.surface_type, - `topology.examples[${exampleIndex}].surface_type`, - topology, - issues, - ); - }); - doc.situations.forEach((situation, situationIndex) => { checkSurfaceTypeRef( situation.surface_type, @@ -151,6 +143,20 @@ function checkTopologyRefs( issues, ); }); + doc.exemplars.forEach((exemplar, index) => { + checkScopeIdRef( + exemplar.scope, + `exemplars[${index}].scope`, + topology, + issues, + ); + checkSurfaceTypeRef( + exemplar.surface_type, + `exemplars[${index}].surface_type`, + topology, + issues, + ); + }); } function checkRefs( @@ -199,6 +205,9 @@ function checkRefs( doc.patterns.forEach((pattern, index) => { checkCheckRefs(pattern.check_refs, `patterns[${index}].check_refs`, issues); }); + doc.exemplars.forEach((exemplar, index) => { + checkMemoryRefs(exemplar.refs, `exemplars[${index}].refs`, targets, issues); + }); } function collectTopology(doc: GhostFingerprintDocument): { @@ -279,6 +288,22 @@ function checkScopeRefs( }); } +function checkScopeIdRef( + scope: string | undefined, + path: string, + topology: ReturnType<typeof collectTopology>, + issues: GhostFingerprintLintIssue[], +): void { + if (!scope) return; + if (topology.scopes.has(scope)) return; + issues.push({ + severity: "error", + rule: "fingerprint-scope-unknown", + message: `Scope '${scope}' is not declared in topology.scopes.`, + path, + }); +} + function collectTargets( doc: GhostFingerprintDocument, ): Record<RefTargetPrefix, Set<string>> { @@ -338,6 +363,35 @@ function checkCheckRefs( }); } +function checkMemoryRefs( + refs: GhostFingerprintRef[] | undefined, + path: string, + targets: Record<RefTargetPrefix, Set<string>>, + issues: GhostFingerprintLintIssue[], +): void { + refs?.forEach((ref, index) => { + const parsed = parseRef(ref); + if (!parsed || parsed.prefix === "check") { + issues.push({ + severity: "error", + rule: "fingerprint-ref-prefix", + message: + "Expected principle:*, situation:*, experience_contract:*, or pattern:* reference.", + path: `${path}[${index}]`, + }); + return; + } + if (!targets[parsed.prefix].has(parsed.id)) { + issues.push({ + severity: "error", + rule: "fingerprint-ref-unknown", + message: `Reference '${ref}' does not exist in fingerprint.yml.`, + path: `${path}[${index}]`, + }); + } + }); +} + function parseRef( ref: GhostFingerprintRef, ): diff --git a/packages/ghost/src/ghost-core/fingerprint/schema.ts b/packages/ghost/src/ghost-core/fingerprint/schema.ts index 2fd6d516..0e1ae48a 100644 --- a/packages/ghost/src/ghost-core/fingerprint/schema.ts +++ b/packages/ghost/src/ghost-core/fingerprint/schema.ts @@ -9,12 +9,6 @@ const SlugIdSchema = z "id must be a slug (lowercase alphanumeric plus . _ -, leading alphanumeric)", }); -export const GhostFingerprintStatusSchema = z.enum([ - "accepted", - "proposed", - "deprecated", -]); - export const GhostFingerprintPatternKindSchema = z.enum([ "visual", "behavioral", @@ -41,6 +35,17 @@ export const GhostFingerprintRefSchema = z }, ); +export const GhostFingerprintMemoryRefSchema = z + .string() + .min(1) + .regex( + /^(principle|situation|experience_contract|pattern):[a-z0-9][a-z0-9._-]*$/, + { + message: + "ref must be typed as prefix:slug, e.g. principle:dense-workflows", + }, + ); + export const GhostFingerprintEvidenceSchema = z .object({ path: z.string().min(1).optional(), @@ -77,19 +82,23 @@ export const GhostFingerprintTopologyScopeSchema = z }) .strict(); -export const GhostFingerprintTopologyExampleSchema = z +export const GhostFingerprintTopologySchema = z .object({ - path: z.string().min(1), - surface_type: SlugIdSchema.optional(), - note: z.string().min(1).optional(), + scopes: z.array(GhostFingerprintTopologyScopeSchema).optional(), + surface_types: z.array(SlugIdSchema).optional(), }) .strict(); -export const GhostFingerprintTopologySchema = z +export const GhostFingerprintExemplarSchema = z .object({ - scopes: z.array(GhostFingerprintTopologyScopeSchema).optional(), - surface_types: z.array(SlugIdSchema).optional(), - examples: z.array(GhostFingerprintTopologyExampleSchema).optional(), + id: SlugIdSchema, + path: z.string().min(1), + title: z.string().min(1).optional(), + surface_type: SlugIdSchema.optional(), + scope: SlugIdSchema.optional(), + note: z.string().min(1).optional(), + why: z.string().min(1).optional(), + refs: z.array(GhostFingerprintMemoryRefSchema).optional(), }) .strict(); @@ -112,7 +121,6 @@ export const GhostFingerprintSituationSchema = z export const GhostFingerprintPrincipleSchema = z .object({ id: SlugIdSchema, - status: GhostFingerprintStatusSchema, principle: z.string().min(1), applies_to: GhostFingerprintScopeSchema.optional(), guidance: z.array(z.string().min(1)).optional(), @@ -125,7 +133,6 @@ export const GhostFingerprintPrincipleSchema = z export const GhostFingerprintExperienceContractSchema = z .object({ id: SlugIdSchema, - status: GhostFingerprintStatusSchema, contract: z.string().min(1), applies_to: GhostFingerprintScopeSchema.optional(), obligations: z.array(z.string().min(1)).optional(), @@ -137,7 +144,6 @@ export const GhostFingerprintExperienceContractSchema = z export const GhostFingerprintPatternSchema = z .object({ id: SlugIdSchema, - status: GhostFingerprintStatusSchema, kind: GhostFingerprintPatternKindSchema, pattern: z.string().min(1), applies_to: GhostFingerprintScopeSchema.optional(), @@ -158,24 +164,20 @@ export const GhostFingerprintImplementationVocabularySchema = z }) .strict(); -export const GhostFingerprintReviewPolicySchema = z - .object({ - proposal_policy: z.array(z.string().min(1)).optional(), - experience_gap_categories: z.array(z.string().min(1)).optional(), - memory_gap_policy: z.array(z.string().min(1)).optional(), - }) - .strict(); - export const GhostFingerprintSchema = z .object({ schema: z.literal(GHOST_FINGERPRINT_SCHEMA), - summary: GhostFingerprintSummarySchema, - topology: GhostFingerprintTopologySchema, - situations: z.array(GhostFingerprintSituationSchema), - principles: z.array(GhostFingerprintPrincipleSchema), - experience_contracts: z.array(GhostFingerprintExperienceContractSchema), - patterns: z.array(GhostFingerprintPatternSchema), - implementation_vocabulary: GhostFingerprintImplementationVocabularySchema, - review_policy: GhostFingerprintReviewPolicySchema, + summary: GhostFingerprintSummarySchema.optional().default({}), + topology: GhostFingerprintTopologySchema.optional().default({}), + situations: z.array(GhostFingerprintSituationSchema).optional().default([]), + principles: z.array(GhostFingerprintPrincipleSchema).optional().default([]), + experience_contracts: z + .array(GhostFingerprintExperienceContractSchema) + .optional() + .default([]), + patterns: z.array(GhostFingerprintPatternSchema).optional().default([]), + exemplars: z.array(GhostFingerprintExemplarSchema).optional().default([]), + implementation_vocabulary: + GhostFingerprintImplementationVocabularySchema.optional().default({}), }) .strict(); diff --git a/packages/ghost/src/ghost-core/fingerprint/types.ts b/packages/ghost/src/ghost-core/fingerprint/types.ts index 4e641b51..4946eafb 100644 --- a/packages/ghost/src/ghost-core/fingerprint/types.ts +++ b/packages/ghost/src/ghost-core/fingerprint/types.ts @@ -1,7 +1,6 @@ export const GHOST_FINGERPRINT_SCHEMA = "ghost.fingerprint/v1" as const; export const GHOST_FINGERPRINT_YML_FILENAME = "fingerprint.yml" as const; -export type GhostFingerprintStatus = "accepted" | "proposed" | "deprecated"; export type GhostFingerprintPatternKind = | "visual" | "behavioral" @@ -44,16 +43,20 @@ export interface GhostFingerprintTopologyScope { surface_types?: string[]; } -export interface GhostFingerprintTopologyExample { +export interface GhostFingerprintExemplar { + id: string; path: string; + title?: string; surface_type?: string; + scope?: string; note?: string; + why?: string; + refs?: GhostFingerprintRef[]; } export interface GhostFingerprintTopology { scopes?: GhostFingerprintTopologyScope[]; surface_types?: string[]; - examples?: GhostFingerprintTopologyExample[]; } export interface GhostFingerprintSituation { @@ -72,7 +75,6 @@ export interface GhostFingerprintSituation { export interface GhostFingerprintPrinciple { id: string; - status: GhostFingerprintStatus; principle: string; applies_to?: GhostFingerprintScope; guidance?: string[]; @@ -83,7 +85,6 @@ export interface GhostFingerprintPrinciple { export interface GhostFingerprintExperienceContract { id: string; - status: GhostFingerprintStatus; contract: string; applies_to?: GhostFingerprintScope; obligations?: string[]; @@ -93,7 +94,6 @@ export interface GhostFingerprintExperienceContract { export interface GhostFingerprintPattern { id: string; - status: GhostFingerprintStatus; kind: GhostFingerprintPatternKind; pattern: string; applies_to?: GhostFingerprintScope; @@ -111,12 +111,6 @@ export interface GhostFingerprintImplementationVocabulary { notes?: string[]; } -export interface GhostFingerprintReviewPolicy { - proposal_policy?: string[]; - experience_gap_categories?: string[]; - memory_gap_policy?: string[]; -} - export interface GhostFingerprintDocument { schema: typeof GHOST_FINGERPRINT_SCHEMA; summary: GhostFingerprintSummary; @@ -125,8 +119,8 @@ export interface GhostFingerprintDocument { principles: GhostFingerprintPrinciple[]; experience_contracts: GhostFingerprintExperienceContract[]; patterns: GhostFingerprintPattern[]; + exemplars: GhostFingerprintExemplar[]; implementation_vocabulary: GhostFingerprintImplementationVocabulary; - review_policy: GhostFingerprintReviewPolicy; } export type GhostFingerprintLintSeverity = "error" | "warning" | "info"; diff --git a/packages/ghost/src/ghost-core/index.ts b/packages/ghost/src/ghost-core/index.ts index 60a47532..bcbdc6a3 100644 --- a/packages/ghost/src/ghost-core/index.ts +++ b/packages/ghost/src/ghost-core/index.ts @@ -59,6 +59,7 @@ export { export type { GhostFingerprintDocument, GhostFingerprintEvidence, + GhostFingerprintExemplar, GhostFingerprintExperienceContract, GhostFingerprintImplementationVocabulary, GhostFingerprintLintIssue, @@ -69,33 +70,29 @@ export type { GhostFingerprintPrinciple, GhostFingerprintRef, GhostFingerprintRefPrefix, - GhostFingerprintReviewPolicy, GhostFingerprintScope, GhostFingerprintSituation, - GhostFingerprintStatus, GhostFingerprintSummary, GhostFingerprintTopology, - GhostFingerprintTopologyExample, GhostFingerprintTopologyScope, } from "./fingerprint/index.js"; export { GHOST_FINGERPRINT_SCHEMA, GHOST_FINGERPRINT_YML_FILENAME, GhostFingerprintEvidenceSchema, + GhostFingerprintExemplarSchema, GhostFingerprintExperienceContractSchema, GhostFingerprintImplementationVocabularySchema, + GhostFingerprintMemoryRefSchema, GhostFingerprintPatternKindSchema, GhostFingerprintPatternSchema, GhostFingerprintPrincipleSchema, GhostFingerprintRefPrefixSchema, GhostFingerprintRefSchema, - GhostFingerprintReviewPolicySchema, GhostFingerprintSchema, GhostFingerprintScopeSchema, GhostFingerprintSituationSchema, - GhostFingerprintStatusSchema, GhostFingerprintSummarySchema, - GhostFingerprintTopologyExampleSchema, GhostFingerprintTopologySchema, GhostFingerprintTopologyScopeSchema, lintGhostFingerprint, @@ -111,7 +108,6 @@ export { type FingerprintPackagePaths, INTENT_FILENAME, PATTERNS_FILENAME, - PROPOSALS_DIRNAME, RESOURCES_FILENAME, } from "./fingerprint-package.js"; // --- Map (ghost.map/v2) --- @@ -132,7 +128,7 @@ export { slugifyScopeId, type TopLevelEntry, } from "./map/index.js"; -// --- Memory (ghost.decision/v1 + ghost.proposal/v1) --- +// --- Memory (ghost.decision/v1) --- export type { GhostDecisionDocument, GhostDecisionStatus, @@ -141,24 +137,14 @@ export type { GhostMemoryLintIssue, GhostMemoryLintReport, GhostMemoryLintSeverity, - GhostProposalAction, - GhostProposalDocument, - GhostProposalKind, - GhostProposalStatus, - GhostProposalTarget, } from "./memory/index.js"; export { GHOST_DECISION_SCHEMA, GHOST_DECISIONS_DIRNAME, - GHOST_PROPOSAL_SCHEMA, - GHOST_PROPOSALS_DIRNAME, GhostDecisionSchema, GhostExperienceEvidenceSchema, GhostExperienceScopeSchema, - GhostProposalActionSchema, - GhostProposalSchema, lintGhostDecision, - lintGhostProposal, } from "./memory/index.js"; // --- Patterns (ghost.patterns/v1) --- export type { diff --git a/packages/ghost/src/ghost-core/memory/index.ts b/packages/ghost/src/ghost-core/memory/index.ts index 1f733024..2500ec31 100644 --- a/packages/ghost/src/ghost-core/memory/index.ts +++ b/packages/ghost/src/ghost-core/memory/index.ts @@ -1,10 +1,8 @@ -export { lintGhostDecision, lintGhostProposal } from "./lint.js"; +export { lintGhostDecision } from "./lint.js"; export { GhostDecisionSchema, GhostExperienceEvidenceSchema, GhostExperienceScopeSchema, - GhostProposalActionSchema, - GhostProposalSchema, } from "./schema.js"; export type { GhostDecisionDocument, @@ -14,15 +12,5 @@ export type { GhostMemoryLintIssue, GhostMemoryLintReport, GhostMemoryLintSeverity, - GhostProposalAction, - GhostProposalDocument, - GhostProposalKind, - GhostProposalStatus, - GhostProposalTarget, -} from "./types.js"; -export { - GHOST_DECISION_SCHEMA, - GHOST_DECISIONS_DIRNAME, - GHOST_PROPOSAL_SCHEMA, - GHOST_PROPOSALS_DIRNAME, } from "./types.js"; +export { GHOST_DECISION_SCHEMA, GHOST_DECISIONS_DIRNAME } from "./types.js"; diff --git a/packages/ghost/src/ghost-core/memory/lint.ts b/packages/ghost/src/ghost-core/memory/lint.ts index 7e79a07e..e6efb13e 100644 --- a/packages/ghost/src/ghost-core/memory/lint.ts +++ b/packages/ghost/src/ghost-core/memory/lint.ts @@ -1,5 +1,5 @@ import type { ZodIssue } from "zod"; -import { GhostDecisionSchema, GhostProposalSchema } from "./schema.js"; +import { GhostDecisionSchema } from "./schema.js"; import type { GhostMemoryLintIssue, GhostMemoryLintReport } from "./types.js"; export function lintGhostDecision(input: unknown): GhostMemoryLintReport { @@ -8,12 +8,6 @@ export function lintGhostDecision(input: unknown): GhostMemoryLintReport { return finalize([]); } -export function lintGhostProposal(input: unknown): GhostMemoryLintReport { - const result = GhostProposalSchema.safeParse(input); - if (!result.success) return finalize(zodIssues(result.error.issues)); - return finalize([]); -} - function zodIssues(issues: ZodIssue[]): GhostMemoryLintIssue[] { return issues.map((issue) => ({ severity: "error" as const, diff --git a/packages/ghost/src/ghost-core/memory/schema.ts b/packages/ghost/src/ghost-core/memory/schema.ts index 73ffe170..30330331 100644 --- a/packages/ghost/src/ghost-core/memory/schema.ts +++ b/packages/ghost/src/ghost-core/memory/schema.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { GHOST_DECISION_SCHEMA, GHOST_PROPOSAL_SCHEMA } from "./types.js"; +import { GHOST_DECISION_SCHEMA } from "./types.js"; const SlugIdSchema = z .string() @@ -56,30 +56,3 @@ export const GhostDecisionSchema = z decided_at: z.string().datetime({ offset: true }), }) .strict(); - -export const GhostProposalActionSchema = z - .object({ - target: z.enum(["fingerprint", "checks", "review_policy"]), - summary: z.string().min(1), - }) - .strict(); - -export const GhostProposalSchema = z - .object({ - schema: z.literal(GHOST_PROPOSAL_SCHEMA), - id: SlugIdSchema, - status: z.enum(["open", "accepted", "rejected", "superseded"]), - kind: z.enum([ - "missing-memory", - "intentional-divergence", - "experience-gap", - "check-candidate", - ]), - title: z.string().min(1), - claim: z.string().min(1), - rationale: z.string().min(1), - scope: GhostExperienceScopeSchema.optional(), - evidence: z.array(GhostExperienceEvidenceSchema).min(1), - proposed_action: GhostProposalActionSchema, - }) - .strict(); diff --git a/packages/ghost/src/ghost-core/memory/types.ts b/packages/ghost/src/ghost-core/memory/types.ts index 25677389..c2575a70 100644 --- a/packages/ghost/src/ghost-core/memory/types.ts +++ b/packages/ghost/src/ghost-core/memory/types.ts @@ -1,21 +1,8 @@ export const GHOST_DECISION_SCHEMA = "ghost.decision/v1" as const; -export const GHOST_PROPOSAL_SCHEMA = "ghost.proposal/v1" as const; export const GHOST_DECISIONS_DIRNAME = "decisions" as const; -export const GHOST_PROPOSALS_DIRNAME = "proposals" as const; export type GhostDecisionStatus = "accepted" | "rejected" | "superseded"; -export type GhostProposalStatus = - | "open" - | "accepted" - | "rejected" - | "superseded"; -export type GhostProposalKind = - | "missing-memory" - | "intentional-divergence" - | "experience-gap" - | "check-candidate"; -export type GhostProposalTarget = "fingerprint" | "checks" | "review_policy"; export interface GhostExperienceScope { roles?: string[]; @@ -44,24 +31,6 @@ export interface GhostDecisionDocument { decided_at: string; } -export interface GhostProposalAction { - target: GhostProposalTarget; - summary: string; -} - -export interface GhostProposalDocument { - schema: typeof GHOST_PROPOSAL_SCHEMA; - id: string; - status: GhostProposalStatus; - kind: GhostProposalKind; - title: string; - claim: string; - rationale: string; - scope?: GhostExperienceScope; - evidence: GhostExperienceEvidence[]; - proposed_action: GhostProposalAction; -} - export type GhostMemoryLintSeverity = "error" | "warning" | "info"; export interface GhostMemoryLintIssue { diff --git a/packages/ghost/src/review-packet.ts b/packages/ghost/src/review-packet.ts index 506a8e73..56ef86d8 100644 --- a/packages/ghost/src/review-packet.ts +++ b/packages/ghost/src/review-packet.ts @@ -4,11 +4,8 @@ import { resolve } from "node:path"; import { parse as parseYaml, stringify as stringifyYaml } from "yaml"; import { GHOST_DECISIONS_DIRNAME, - GHOST_PROPOSALS_DIRNAME, type GhostDecisionDocument, - type GhostProposalDocument, lintGhostDecision, - lintGhostProposal, } from "#ghost-core"; import { parseUnifiedDiff } from "./core/index.js"; import { @@ -42,9 +39,6 @@ async function buildSingleBundleReviewPacket(options: { intent: (await readOptional(paths.intent)) ?? null, checks: (await readOptional(paths.checks)) ?? null, config: (await readOptionalPackageConfig(paths.config)) ?? null, - open_proposals: await readOpenProposals( - resolve(paths.dir, GHOST_PROPOSALS_DIRNAME), - ), }; if (options.includeMemory) { packet.memory = { @@ -83,7 +77,6 @@ async function buildStackReviewPacket(options: { intent: first.merged.intent, checks: stringifyYaml(first.merged.checks, { lineWidth: 0 }), config: config ?? null, - open_proposals: first.merged.open_proposals, stacks, }; if (options.includeMemory) { @@ -111,17 +104,10 @@ function baseReviewPacket( "experience-gap", "eval-uncertainty", ], - proposal_types: [ - "missing-memory", - "intentional-divergence", - "experience-gap", - "check-candidate", - ], required_finding_citations: [ "diff location", "fingerprint.yml memory", "active check when blocking", - "open proposal when relevant", "repair or intentional-divergence rationale", ], }; @@ -142,8 +128,6 @@ function reviewStackFromMemoryStack( fingerprint: stack.merged.fingerprint, intent: stack.merged.intent, checks: stack.merged.checks, - proposals: stack.merged.proposals, - open_proposals: stack.merged.open_proposals, decisions: stack.merged.decisions, }, provenance: stack.provenance, @@ -155,7 +139,6 @@ interface ReviewPacketBase { package_dir: string; diff: string; finding_categories: string[]; - proposal_types: string[]; required_finding_citations: string[]; } @@ -166,12 +149,10 @@ interface ReviewPacket { intent: string | null; checks: string | null; config: GhostPackageConfig | null; - open_proposals: GhostProposalDocument[]; memory?: { decisions: GhostDecisionDocument[] }; stacks?: ReviewStackPacket[]; diff: string; finding_categories: string[]; - proposal_types: string[]; required_finding_citations: string[]; } @@ -185,8 +166,6 @@ interface ReviewStackPacket { fingerprint: unknown; intent: string | null; checks: unknown; - proposals: GhostProposalDocument[]; - open_proposals: GhostProposalDocument[]; decisions: GhostDecisionDocument[]; }; provenance: GhostMemoryStack["provenance"]; @@ -197,17 +176,13 @@ export function formatReviewPacketMarkdown(packet: ReviewPacket): string { Package: ${packet.package_dir} -Review this diff as a non-blocking design-language critic. Advisory findings must be evidence-routed and must cite: ${packet.required_finding_citations.join(", ")}. Do not fail the build unless the issue is tied to an active deterministic check in checks.yml. Keep findings grounded in fingerprint memory, human intent, open proposals, or active deterministic checks; do not expand the review into unrelated audit categories. +Review this diff as a non-blocking design-language critic. Advisory findings must be evidence-routed and must cite: ${packet.required_finding_citations.join(", ")}. Do not fail the build unless the issue is tied to an active deterministic check in checks.yml. Keep findings grounded in fingerprint memory, active deterministic checks, and optional rationale files when present; do not expand the review into unrelated audit categories. Use these finding categories: ${packet.finding_categories.join(", ")}. -When accepted fingerprint memory is silent, local evidence can still support advisory critique. Label those findings as provisional and non-Ghost-backed, and ground them in nearby product surfaces, local components, token or copy conventions, accepted decisions, or human intent. Ask the human before judging high-risk, irreversible, privacy/security/legal, or product-identity-defining choices. - -## Proposal Threshold +When fingerprint memory is silent, local evidence can still support advisory critique. Label those findings as provisional and non-Ghost-backed, and ground them in nearby product surfaces, local components, token or copy conventions, or optional rationale files when present. Ask the human before judging high-risk, irreversible, privacy/security/legal, or product-identity-defining choices. -Create or recommend a proposal only when the gap is repeated, high-impact, explicitly human-stated, intentionally divergent, likely to recur, or blocks confident future review. Do not propose for isolated implementation details, weak local context, duplicate open proposals, issues already fixable from accepted memory, vague taste concerns, or generic code quality. - -If the diff exposes missing or contradictory memory, report it as missing-memory or experience-gap only after applying the threshold. Include \`Memory action: none | recommend-proposal | create-proposal\` for each memory-gap finding. Default to \`recommend-proposal\`; use \`create-proposal\` only when the user explicitly asks to capture memory or when following the dedicated proposal workflow. Candidate proposal kinds: ${packet.proposal_types.join(", ")}. Do not silently rewrite canonical memory. +If the diff exposes missing or contradictory memory, report it as missing-memory or experience-gap. Do not silently rewrite memory during review; memory changes are ordinary Git-reviewed edits to fingerprint.yml, checks.yml, and optional rationale files when present. ${formatReviewStacksSection(packet.stacks ?? null)} @@ -227,8 +202,6 @@ ${formatMemorySection(packet.memory ?? null)} ${formatConfigSection(packet.config)} -${formatProposalSection(packet.open_proposals)} - ## Active Checks \`\`\`yaml @@ -259,7 +232,6 @@ function formatReviewStacksSection(stacks: ReviewStackPacket[] | null): string { { fingerprint: stack.merged.fingerprint, checks: stack.merged.checks, - open_proposals: stack.merged.open_proposals, provenance: stack.provenance, }, { lineWidth: 0 }, @@ -315,39 +287,6 @@ async function readAcceptedDecisions( return decisions; } -async function readOpenProposals( - dirPath: string, -): Promise<GhostProposalDocument[]> { - let entries: Dirent[]; - try { - entries = await readdir(dirPath, { withFileTypes: true }); - } catch { - return []; - } - - const proposals: GhostProposalDocument[] = []; - for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) { - if (!entry.isFile()) continue; - if (entry.name.startsWith(".")) continue; - if (!/\.ya?ml$/i.test(entry.name)) continue; - - const path = resolve(dirPath, entry.name); - const parsed = parseYaml(await readFile(path, "utf-8")); - const report = lintGhostProposal(parsed); - if (report.errors > 0) { - const first = report.issues.find((issue) => issue.severity === "error"); - const suffix = first?.path ? ` @ ${first.path}` : ""; - throw new Error( - `${path} failed proposal lint: ${first?.message ?? "invalid proposal"}${suffix}`, - ); - } - const proposal = parsed as GhostProposalDocument; - if (proposal.status === "open") proposals.push(proposal); - } - - return proposals; -} - function formatMemorySection( memory: { decisions: GhostDecisionDocument[] } | null, ): string { @@ -390,7 +329,7 @@ function formatConfigSection(config: GhostPackageConfig | null): string { if (!config) { return `## Implementation Config -_No config.yml present. Review uses fingerprint.yml memory and the provided diff only._ +_No config.yml present. Review uses canonical fingerprint.yml memory and the provided diff only._ `; } @@ -402,29 +341,6 @@ ${stringifyYaml(config)} `; } -function formatProposalSection(proposals: GhostProposalDocument[]): string { - if (proposals.length === 0) { - return `## Open Proposals - -_No open proposals found in .ghost/proposals._ -`; - } - - const lines = ["## Open Proposals", ""]; - for (const proposal of proposals) { - lines.push(`### ${proposal.title}`); - lines.push(""); - lines.push(`- **ID:** \`${proposal.id}\``); - lines.push(`- **Kind:** ${proposal.kind}`); - lines.push(`- **Claim:** ${proposal.claim}`); - lines.push(`- **Rationale:** ${proposal.rationale}`); - lines.push(`- **Proposed action:** ${proposal.proposed_action.summary}`); - lines.push(""); - } - - return lines.join("\n"); -} - function formatDecisionScope( scope: NonNullable<GhostDecisionDocument["scope"]>, ): string { diff --git a/packages/ghost/src/scan-commands.ts b/packages/ghost/src/scan-commands.ts index 0c9a73cf..64157fd1 100644 --- a/packages/ghost/src/scan-commands.ts +++ b/packages/ghost/src/scan-commands.ts @@ -37,7 +37,6 @@ import { verifyFingerprintPackage, } from "./scan/index.js"; import { registerEmitCommand } from "./scan-emit-command.js"; -import { registerProposalCommand } from "./scan-proposal-command.js"; import { registerStackCommand } from "./scan-stack-command.js"; /** @@ -132,11 +131,11 @@ export function registerScanCommands(cli: CAC): void { cli .command( "init [dir]", - "Create a root .ghost product experience memory skeleton (fingerprint.yml, checks.yml, proposals/, cache/)", + "Create a root .ghost memory skeleton (fingerprint.yml and checks.yml)", ) .option( "--scope <path>", - "Create a scoped <path>/<memory-dir> product experience memory skeleton", + "Create a scoped <path>/<memory-dir> memory skeleton", ) .option( "--memory-dir <relative-dir>", @@ -200,15 +199,13 @@ export function registerScanCommands(cli: CAC): void { ); } else { process.stdout.write( - `Initialized fingerprint package: ${paths.dir}\n`, + `Initialized Ghost memory skeleton: ${paths.dir}\n`, ); process.stdout.write(` fingerprint.yml: ${paths.fingerprintYml}\n`); process.stdout.write(` checks.yml: ${paths.checks}\n`); if (opts.withConfig || opts.reference) { process.stdout.write(` config.yml: ${paths.config}\n`); } - process.stdout.write(` proposals/: ${paths.proposals}\n`); - process.stdout.write(` cache/: ${paths.cache}\n`); if (opts.withIntent) { process.stdout.write(` intent.md: ${paths.intent}\n`); } @@ -226,11 +223,11 @@ export function registerScanCommands(cli: CAC): void { cli .command( "verify [dir]", - "Verify a root Ghost memory bundle: fingerprint evidence paths and checks are grounded.", + "Verify a root Ghost memory bundle: fingerprint evidence, exemplars, and checks are grounded.", ) .option( "--root <dir>", - "Optional target root used to resolve fingerprint.yml evidence paths (default: cwd)", + "Optional target root used to resolve fingerprint.yml evidence and exemplar paths (default: cwd)", ) .option("--format <fmt>", "Output format: cli or json", { default: "cli" }) .option( @@ -277,7 +274,7 @@ export function registerScanCommands(cli: CAC): void { cli .command( "scan [dir]", - "Report fingerprint capture progress: produced artifacts, evidence readiness, and the next BYOA step.", + "Report fingerprint memory/readiness state: produced artifacts, review readiness, and the next BYOA step.", ) .option( "--include-scopes", @@ -316,7 +313,7 @@ export function registerScanCommands(cli: CAC): void { } else { const fmt = (state: string) => state === "present" ? "present" : "missing"; - process.stdout.write(`capture dir: ${status.dir}\n\n`); + process.stdout.write(`memory dir: ${status.dir}\n\n`); process.stdout.write( ` fingerprint (fingerprint.yml): ${fmt(status.fingerprint.state)}\n`, ); @@ -326,9 +323,6 @@ export function registerScanCommands(cli: CAC): void { process.stdout.write( ` checks (checks.yml): ${fmt(status.checks.state)}\n`, ); - process.stdout.write( - ` proposals (proposals/): ${fmt(status.proposals.state)}\n`, - ); process.stdout.write( ` cache (cache/): ${fmt(status.cache.state)}\n`, ); @@ -341,7 +335,7 @@ export function registerScanCommands(cli: CAC): void { ); } else { process.stdout.write( - "next: fingerprint capture complete - all stages present\n", + "next: edit fingerprint.yml, then run ghost verify/check/review\n", ); } process.stdout.write(`readiness: ${status.readiness.state}\n`); @@ -669,7 +663,6 @@ export function registerScanCommands(cli: CAC): void { } }); - registerProposalCommand(cli); registerEmitCommand(cli); } @@ -685,7 +678,6 @@ async function nestedBundleStatus( ...pkg, fingerprint: status.fingerprint, checks: status.checks, - proposals: status.proposals, intent: status.intent, readiness: status.readiness, }; @@ -700,7 +692,6 @@ interface NestedBundleStatus { memory_dir: string; fingerprint: Awaited<ReturnType<typeof scanStatus>>["fingerprint"]; checks: Awaited<ReturnType<typeof scanStatus>>["checks"]; - proposals: Awaited<ReturnType<typeof scanStatus>>["proposals"]; intent: Awaited<ReturnType<typeof scanStatus>>["intent"]; readiness: Awaited<ReturnType<typeof scanStatus>>["readiness"]; } @@ -728,8 +719,6 @@ function initCommandOutput( fingerprintYml: paths.fingerprintYml, ...(options.includeConfig ? { config: paths.config } : {}), checks: paths.checks, - proposals: paths.proposals, - cache: paths.cache, ...(options.includeIntent ? { intent: paths.intent } : {}), }; } diff --git a/packages/ghost/src/scan-emit-command.ts b/packages/ghost/src/scan-emit-command.ts index c4b56555..04e59731 100644 --- a/packages/ghost/src/scan-emit-command.ts +++ b/packages/ghost/src/scan-emit-command.ts @@ -41,7 +41,7 @@ export function registerEmitCommand(cli: CAC): void { cli .command( "emit <kind>", - `Emit a derived artifact from the fingerprint package (kinds: ${SUPPORTED_KINDS.join(", ")})`, + `Emit a derived artifact from the fingerprint package (review command or context-bundle generation packet)`, ) .option("--path <path>", "Resolve a nested memory stack for this repo path") .option( @@ -62,7 +62,10 @@ export function registerEmitCommand(cli: CAC): void { ) // context-bundle flags: .option("--readme", "Include README.md (context-bundle)") - .option("--prompt-only", "Emit only prompt.md (context-bundle)") + .option( + "--prompt-only", + "Emit only prompt.md (context-bundle generation packet)", + ) .option( "--name <name>", "Override the skill name (default: fingerprint.yml product or first scope) (context-bundle)", diff --git a/packages/ghost/src/scan-proposal-command.ts b/packages/ghost/src/scan-proposal-command.ts deleted file mode 100644 index 9341952c..00000000 --- a/packages/ghost/src/scan-proposal-command.ts +++ /dev/null @@ -1,352 +0,0 @@ -import type { Dirent } from "node:fs"; -import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises"; -import { isAbsolute, join, relative, resolve, sep } from "node:path"; -import type { CAC } from "cac"; -import { parse as parseYaml, stringify as stringifyYaml } from "yaml"; -import { - GHOST_PROPOSAL_SCHEMA, - type GhostProposalDocument, - lintGhostProposal, -} from "#ghost-core"; -import { - type GhostMemoryStack, - loadMemoryStackForPath, - normalizeMemoryDir, -} from "./scan/index.js"; - -export function registerProposalCommand(cli: CAC): void { - cli - .command( - "proposal <op> [id]", - "Create, list, or resolve scoped Ghost memory proposals.", - ) - .option("--path <path>", "Repo path used to resolve the memory stack", { - default: ".", - }) - .option( - "--memory-dir <relative-dir>", - "Relative memory package directory for proposal stack resolution (default: .ghost)", - ) - .option("--id <id>", "Proposal id for create") - .option("--kind <kind>", "Proposal kind for create") - .option("--title <title>", "Proposal title for create") - .option("--claim <text>", "Proposal claim for create") - .option("--rationale <text>", "Proposal rationale for create") - .option( - "--target <target>", - "Proposed target: fingerprint, checks, or review_policy", - { default: "fingerprint" }, - ) - .option("--summary <text>", "Proposed action summary for create") - .option( - "--evidence <path-or-note>", - "Evidence path or note for create; repeat or comma-separate for multiple values", - ) - .option( - "--status <status>", - "Resolution status: accepted, rejected, or superseded", - ) - .option("--format <fmt>", "Output format: cli or json", { default: "cli" }) - .action(async (op: string, idArg: string | undefined, opts) => { - try { - if (op === "create") { - const result = await createScopedProposal(idArg, opts); - writeProposalCommandResult(result, opts.format); - process.exit(0); - return; - } - if (op === "list") { - const stack = await loadMemoryStackForPath( - typeof opts.path === "string" ? opts.path : ".", - process.cwd(), - { memoryDir: memoryDirFromOpts(opts) }, - ); - writeProposalList(stack, opts.format); - process.exit(0); - return; - } - if (op === "resolve") { - const result = await resolveScopedProposal(idArg, opts); - writeProposalCommandResult(result, opts.format); - process.exit(0); - return; - } - - console.error( - `Error: unknown proposal op '${op}'. Supported: create, list, resolve`, - ); - process.exit(2); - } catch (err) { - console.error( - `Error: ${err instanceof Error ? err.message : String(err)}`, - ); - process.exit(2); - } - }); -} - -async function createScopedProposal( - idArg: string | undefined, - opts: Record<string, unknown>, -): Promise<ProposalCommandResult> { - const targetPath = typeof opts.path === "string" ? opts.path : "."; - const stack = await loadMemoryStackForPath(targetPath, process.cwd(), { - memoryDir: memoryDirFromOpts(opts), - }); - const layer = stack.layers.at(-1); - if (!layer) throw new Error("No memory stack layer found for proposal path."); - - const id = requiredString(idArg ?? opts.id, "proposal id"); - const kind = requiredString(opts.kind, "proposal kind"); - const title = requiredString(opts.title, "proposal title"); - const claim = requiredString(opts.claim, "proposal claim"); - const rationale = requiredString(opts.rationale, "proposal rationale"); - const target = requiredString(opts.target, "proposal target"); - const summary = requiredString(opts.summary, "proposal action summary"); - if (!isProposalKind(kind)) - throw new Error(`Unsupported proposal kind: ${kind}`); - if (!isProposalTarget(target)) { - throw new Error(`Unsupported proposal target: ${target}`); - } - - const scopedPath = toLayerRelativePath(layer.root, targetPath); - const proposal: GhostProposalDocument = { - schema: GHOST_PROPOSAL_SCHEMA, - id, - status: "open", - kind, - title, - claim, - rationale, - ...(scopedPath !== "." ? { scope: { paths: [scopedPath] } } : {}), - evidence: proposalEvidence(opts.evidence, layer.root), - proposed_action: { - target, - summary, - }, - }; - assertProposalIsValid(proposal); - - const proposalDir = join(layer.dir, "proposals"); - await mkdir(proposalDir, { recursive: true }); - const outPath = join(proposalDir, `${id}.yml`); - if (await fileExists(outPath)) { - throw new Error(`Proposal already exists: ${outPath}`); - } - await writeFile(outPath, stringifyYaml(proposal, { lineWidth: 0 }), "utf-8"); - return { package_dir: layer.dir, path: outPath, proposal }; -} - -async function resolveScopedProposal( - idArg: string | undefined, - opts: Record<string, unknown>, -): Promise<ProposalCommandResult> { - const id = requiredString(idArg ?? opts.id, "proposal id"); - const status = requiredString(opts.status, "proposal status"); - if (!isResolveStatus(status)) { - throw new Error( - "Proposal resolve --status must be accepted, rejected, or superseded", - ); - } - - const targetPath = typeof opts.path === "string" ? opts.path : "."; - const stack = await loadMemoryStackForPath(targetPath, process.cwd(), { - memoryDir: memoryDirFromOpts(opts), - }); - const found = await findProposalFileInStack(stack, id); - if (!found) { - throw new Error(`No proposal '${id}' found in resolved memory stack.`); - } - const proposal = parseYaml( - await readFile(found.path, "utf-8"), - ) as GhostProposalDocument; - proposal.status = status; - assertProposalIsValid(proposal); - await writeFile( - found.path, - stringifyYaml(proposal, { lineWidth: 0 }), - "utf-8", - ); - return { package_dir: found.layer.dir, path: found.path, proposal }; -} - -function writeProposalCommandResult( - result: ProposalCommandResult, - format: unknown, -): void { - if (format === "json") { - process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); - return; - } - process.stdout.write( - `${result.proposal.id}: ${result.proposal.status} in ${result.path}\n`, - ); -} - -function writeProposalList(stack: GhostMemoryStack, format: unknown): void { - if (format === "json") { - process.stdout.write( - `${JSON.stringify( - { - target_path: stack.target_path, - memory_dir: stack.memory_dir, - layers: stack.layers.map((layer) => ({ - dir: layer.dir, - relative_root: layer.relative_root, - memory_dir: layer.memory_dir, - })), - open_proposals: stack.merged.open_proposals, - }, - null, - 2, - )}\n`, - ); - return; - } - - process.stdout.write(`target: ${stack.target_path}\n`); - if (stack.merged.open_proposals.length === 0) { - process.stdout.write("open proposals: none\n"); - return; - } - process.stdout.write("open proposals:\n"); - for (const proposal of stack.merged.open_proposals) { - process.stdout.write(` - ${proposal.id}: ${proposal.title}\n`); - } -} - -interface ProposalCommandResult { - package_dir: string; - path: string; - proposal: GhostProposalDocument; -} - -async function findProposalFileInStack( - stack: GhostMemoryStack, - id: string, -): Promise<{ layer: GhostMemoryStack["layers"][number]; path: string } | null> { - for (const layer of [...stack.layers].reverse()) { - const proposalDir = join(layer.dir, "proposals"); - const direct = [ - join(proposalDir, `${id}.yml`), - join(proposalDir, `${id}.yaml`), - ]; - for (const path of direct) { - if (await proposalFileHasId(path, id)) return { layer, path }; - } - - let entries: Dirent<string>[]; - try { - entries = await readdir(proposalDir, { withFileTypes: true }); - } catch { - continue; - } - for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) { - if (!entry.isFile() || !/\.ya?ml$/i.test(entry.name)) continue; - const path = join(proposalDir, entry.name); - if (direct.includes(path)) continue; - if (await proposalFileHasId(path, id)) return { layer, path }; - } - } - return null; -} - -async function proposalFileHasId(path: string, id: string): Promise<boolean> { - try { - const parsed = parseYaml(await readFile(path, "utf-8")) as { id?: unknown }; - return parsed.id === id; - } catch { - return false; - } -} - -function proposalEvidence( - evidence: unknown, - layerRoot: string, -): GhostProposalDocument["evidence"] { - const values = splitOptionValues(evidence); - if (values.length === 0) { - return [{ note: "Created by ghost proposal create." }]; - } - return values.map((value) => { - if (value.startsWith("note:")) { - return { note: value.slice("note:".length).trim() }; - } - return { path: toLayerRelativePath(layerRoot, value) }; - }); -} - -function splitOptionValues(value: unknown): string[] { - if (Array.isArray(value)) { - return value.flatMap(splitOptionValues); - } - if (typeof value !== "string") return []; - return value - .split(",") - .map((entry) => entry.trim()) - .filter(Boolean); -} - -function toLayerRelativePath(layerRoot: string, value: string): string { - const absolute = isAbsolute(value) ? value : resolve(process.cwd(), value); - const rel = relative(layerRoot, absolute); - if (rel && !rel.startsWith("..") && !isAbsolute(rel)) { - return rel.replaceAll(sep, "/"); - } - return value.replaceAll(sep, "/"); -} - -function requiredString(value: unknown, label: string): string { - if (typeof value !== "string" || value.trim() === "") { - throw new Error(`Missing required ${label}.`); - } - return value.trim(); -} - -function isProposalKind(value: string): value is GhostProposalDocument["kind"] { - return ( - value === "missing-memory" || - value === "intentional-divergence" || - value === "experience-gap" || - value === "check-candidate" - ); -} - -function isProposalTarget( - value: string, -): value is GhostProposalDocument["proposed_action"]["target"] { - return ( - value === "fingerprint" || value === "checks" || value === "review_policy" - ); -} - -function isResolveStatus( - value: string, -): value is Exclude<GhostProposalDocument["status"], "open"> { - return value === "accepted" || value === "rejected" || value === "superseded"; -} - -function assertProposalIsValid(proposal: GhostProposalDocument): void { - const report = lintGhostProposal(proposal); - if (report.errors === 0) return; - const first = report.issues.find((issue) => issue.severity === "error"); - const suffix = first?.path ? ` @ ${first.path}` : ""; - throw new Error( - `proposal failed lint: ${first?.message ?? "invalid proposal"}${suffix}`, - ); -} - -async function fileExists(path: string): Promise<boolean> { - try { - await stat(path); - return true; - } catch { - return false; - } -} - -function memoryDirFromOpts(opts: Record<string, unknown>): string { - return normalizeMemoryDir( - typeof opts.memoryDir === "string" ? opts.memoryDir : undefined, - ); -} diff --git a/packages/ghost/src/scan-stack-command.ts b/packages/ghost/src/scan-stack-command.ts index 746eca95..d573d803 100644 --- a/packages/ghost/src/scan-stack-command.ts +++ b/packages/ghost/src/scan-stack-command.ts @@ -64,15 +64,12 @@ function formatStackJson(stack: GhostMemoryStack): Record<string, unknown> { memory_dir: layer.memory_dir, fingerprint_id: layer.fingerprint.summary.product ?? null, checks: layer.checks?.checks.length ?? 0, - proposals: layer.proposals.length, })), merged: { fingerprint: stack.merged.fingerprint, checks: stack.merged.checks, intent: stack.merged.intent, decisions: stack.merged.decisions, - proposals: stack.merged.proposals, - open_proposals: stack.merged.open_proposals, }, provenance: stack.provenance, }; @@ -96,7 +93,6 @@ function formatStackCli(stack: GhostMemoryStack): string { stack.merged.checks.checks.filter((check) => check.status === "active") .length }`, - ` open proposals: ${stack.merged.open_proposals.length}`, "", ]; return `${lines.join("\n")}\n`; diff --git a/packages/ghost/src/scan/constants.ts b/packages/ghost/src/scan/constants.ts index 1f2b923a..3df94b6d 100644 --- a/packages/ghost/src/scan/constants.ts +++ b/packages/ghost/src/scan/constants.ts @@ -31,8 +31,5 @@ export const CHECKS_FILENAME = "checks.yml"; /** Optional directory containing accepted/rejected product-experience decisions. */ export const DECISIONS_DIRNAME = "decisions"; -/** Optional directory containing candidate product-experience memory changes. */ -export const PROPOSALS_DIRNAME = "proposals"; - /** Optional directory containing generated, non-canonical caches. */ export const CACHE_DIRNAME = "cache"; diff --git a/packages/ghost/src/scan/context/package-memory.ts b/packages/ghost/src/scan/context/package-memory.ts index 1763a237..d908aa45 100644 --- a/packages/ghost/src/scan/context/package-memory.ts +++ b/packages/ghost/src/scan/context/package-memory.ts @@ -1,20 +1,36 @@ -import type { Dirent } from "node:fs"; -import { readdir, readFile } from "node:fs/promises"; -import { resolve } from "node:path"; +import { readFile } from "node:fs/promises"; +import { join } from "node:path"; import { parse as parseYaml } from "yaml"; import { type GhostChecksDocument, GhostChecksSchema, type GhostFingerprintDocument, GhostFingerprintSchema, - type GhostProposalDocument, - GhostProposalSchema, lintGhostChecks, lintGhostFingerprint, - lintGhostProposal, } from "#ghost-core"; import type { FingerprintPackagePaths } from "../fingerprint-package.js"; +export interface PackageInventorySummary { + root?: string; + platform_hints: string[]; + build_system_hints: string[]; + language_histogram: Array<{ name: string; files: number }>; + package_manifests: string[]; + candidate_config_files: string[]; + registry_files: string[]; + top_level_tree: Array<{ path: string; kind: string; child_count: number }>; + config?: { + targets?: Array<{ id: string; platform?: string; roots?: string[] }>; + libraries?: Array<{ id: string; role?: string; source?: string }>; + }; +} + +export type PackageInventory = + | { state: "missing"; path: string } + | { state: "present"; path: string; summary: PackageInventorySummary } + | { state: "unreadable"; path: string; error: string }; + export interface PackageMemory { name: string; memoryDir?: string; @@ -23,18 +39,18 @@ export interface PackageMemory { checks?: GhostChecksDocument; checksRaw?: string; intent?: string; - openProposals: GhostProposalDocument[]; + inventory: PackageInventory; } export async function loadPackageMemory( paths: FingerprintPackagePaths, nameOverride?: string, ): Promise<PackageMemory> { - const [fingerprintRaw, checksRaw, intent, openProposals] = await Promise.all([ + const [fingerprintRaw, checksRaw, intent, inventory] = await Promise.all([ readFile(paths.fingerprintYml, "utf-8"), readOptional(paths.checks), readOptional(paths.intent), - readOpenProposals(paths.proposals), + loadPackageInventory(paths), ]); const fingerprint = parseFingerprint(fingerprintRaw); @@ -46,10 +62,27 @@ export async function loadPackageMemory( checks, checksRaw, intent, - openProposals, + inventory, }; } +export async function loadPackageInventory( + paths: Pick<FingerprintPackagePaths, "cache">, +): Promise<PackageInventory> { + const path = join(paths.cache, "inventory.json"); + const raw = await readOptional(path); + if (!raw) return { state: "missing", path }; + try { + return { state: "present", path, summary: summarizeInventory(raw) }; + } catch (err) { + return { + state: "unreadable", + path, + error: err instanceof Error ? err.message : String(err), + }; + } +} + function parseFingerprint(raw: string): GhostFingerprintDocument { const parsed = parseYamlSafe(raw, "fingerprint.yml"); const report = lintGhostFingerprint(parsed); @@ -93,45 +126,6 @@ function parseChecks( return result.data as GhostChecksDocument; } -async function readOpenProposals( - dirPath: string, -): Promise<GhostProposalDocument[]> { - let entries: Dirent[]; - try { - entries = await readdir(dirPath, { withFileTypes: true }); - } catch { - return []; - } - - const proposals: GhostProposalDocument[] = []; - for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) { - if (!entry.isFile()) continue; - if (entry.name.startsWith(".")) continue; - if (!/\.ya?ml$/i.test(entry.name)) continue; - - const path = resolve(dirPath, entry.name); - const parsed = parseYamlSafe(await readFile(path, "utf-8"), path); - const report = lintGhostProposal(parsed); - if (report.errors > 0) { - const first = report.issues.find((issue) => issue.severity === "error"); - const suffix = first?.path ? ` @ ${first.path}` : ""; - throw new Error( - `${path} failed proposal lint: ${first?.message ?? "invalid proposal"}${suffix}`, - ); - } - - const result = GhostProposalSchema.safeParse(parsed); - if (!result.success) { - throw new Error(`${path} failed proposal schema validation.`); - } - - const proposal = result.data as GhostProposalDocument; - if (proposal.status === "open") proposals.push(proposal); - } - - return proposals; -} - function parseYamlSafe(raw: string, label: string): unknown { try { return parseYaml(raw); @@ -144,6 +138,81 @@ function parseYamlSafe(raw: string, label: string): unknown { } } +function summarizeInventory(raw: string): PackageInventorySummary { + const parsed = JSON.parse(raw) as Record<string, unknown>; + return { + root: typeof parsed.root === "string" ? parsed.root : undefined, + platform_hints: stringArray(parsed.platform_hints).slice(0, 8), + build_system_hints: stringArray(parsed.build_system_hints).slice(0, 8), + language_histogram: recordArray(parsed.language_histogram) + .map((entry) => ({ + name: typeof entry.name === "string" ? entry.name : "", + files: typeof entry.files === "number" ? entry.files : 0, + })) + .filter((entry) => entry.name) + .slice(0, 8), + package_manifests: stringArray(parsed.package_manifests).slice(0, 12), + candidate_config_files: stringArray(parsed.candidate_config_files).slice( + 0, + 12, + ), + registry_files: stringArray(parsed.registry_files).slice(0, 8), + top_level_tree: recordArray(parsed.top_level_tree) + .map((entry) => ({ + path: typeof entry.path === "string" ? entry.path : "", + kind: typeof entry.kind === "string" ? entry.kind : "", + child_count: + typeof entry.child_count === "number" ? entry.child_count : 0, + })) + .filter((entry) => entry.path && entry.kind) + .slice(0, 12), + config: summarizeInventoryConfig(parsed.config), + }; +} + +function summarizeInventoryConfig( + value: unknown, +): PackageInventorySummary["config"] { + if (!value || typeof value !== "object" || Array.isArray(value)) { + return undefined; + } + const record = value as Record<string, unknown>; + const targets = recordArray(record.targets) + .map((entry) => ({ + id: typeof entry.id === "string" ? entry.id : "", + ...(typeof entry.platform === "string" + ? { platform: entry.platform } + : {}), + roots: stringArray(entry.roots), + })) + .filter((entry) => entry.id) + .slice(0, 8); + const libraries = recordArray(record.libraries) + .map((entry) => ({ + id: typeof entry.id === "string" ? entry.id : "", + ...(typeof entry.role === "string" ? { role: entry.role } : {}), + ...(typeof entry.source === "string" ? { source: entry.source } : {}), + })) + .filter((entry) => entry.id) + .slice(0, 8); + if (targets.length === 0 && libraries.length === 0) return undefined; + return { targets, libraries }; +} + +function stringArray(value: unknown): string[] { + return Array.isArray(value) + ? value.filter((entry): entry is string => typeof entry === "string") + : []; +} + +function recordArray(value: unknown): Array<Record<string, unknown>> { + return Array.isArray(value) + ? value.filter((entry): entry is Record<string, unknown> => + Boolean(entry && typeof entry === "object" && !Array.isArray(entry)), + ) + : []; +} + async function readOptional(path: string): Promise<string | undefined> { try { return await readFile(path, "utf-8"); diff --git a/packages/ghost/src/scan/context/package-review-command.ts b/packages/ghost/src/scan/context/package-review-command.ts index 0c360aca..63a25df2 100644 --- a/packages/ghost/src/scan/context/package-review-command.ts +++ b/packages/ghost/src/scan/context/package-review-command.ts @@ -1,5 +1,6 @@ import type { GhostCheck, + GhostFingerprintExemplar, GhostFingerprintExperienceContract, GhostFingerprintPattern, GhostFingerprintPrinciple, @@ -19,18 +20,11 @@ const REVIEW_FINDING_CATEGORIES = [ "eval-uncertainty", ] as const; -const REVIEW_PROPOSAL_TYPES = [ - "missing-memory", - "intentional-divergence", - "experience-gap", - "check-candidate", -] as const; - /** * Emit a repo-local slash command from fingerprint.yml memory. * * The command stays intentionally light: it tells the host agent which Ghost - * files and CLI packets to use, then includes a compact accepted-memory index. + * files and CLI packets to use, then includes a compact memory index. * Full canonical truth remains in fingerprint.yml and checks.yml. */ export function emitPackageReviewCommand( @@ -49,10 +43,9 @@ export function emitPackageReviewCommand( heading, packageModeSection(), packageWorkflowSection(memory), - packageFindingPolicySection(memory), + packageFindingPolicySection(), packageMemoryIndex(memory), packageChecksSection(activeChecks), - packageProposalSection(memory), packageReviewFooter(memory), ]; return `${parts.filter(Boolean).join("\n\n").trim()}\n`; @@ -77,15 +70,15 @@ function packageWorkflowSection(memory: PackageMemory): string { 1. Read \`${memoryDir}/fingerprint.yml\` as the canonical product-experience memory. 2. Select the relevant situation before judging UI, copy, flow, disclosure, recovery, trust, or interaction behavior. Keep findings grounded in resolved Ghost memory or active checks; do not expand the review into unrelated audit categories. -3. Apply accepted principles, experience contracts, and patterns before choosing implementation details. Treat proposed or deprecated memory as non-canonical unless the user explicitly asks to explore it. -4. Use implementation vocabulary only as replaceable material that may help satisfy the selected product memory. -5. Run \`ghost check${memoryDirFlag}\` when a diff is available. Active checks are deterministic and can block. -6. Run \`ghost review --include-memory${memoryDirFlag}\` for the advisory packet when you need full diff context, open proposals, and accepted decisions. -7. Cite the diff location, fingerprint.yml memory, any active check, and any relevant open proposal for every finding.`; +3. Apply principles, experience contracts, and patterns before choosing implementation details. +4. Inspect relevant exemplars as concrete anchors for what good looks like. +5. Use implementation vocabulary only as replaceable material that may help satisfy the selected product memory. +6. Run \`ghost check${memoryDirFlag}\` when a diff is available. Active checks are deterministic and can block. +7. Run \`ghost review${memoryDirFlag}\` for the advisory packet when you need full diff context and memory excerpts; add \`--include-memory\` only when optional decisions matter. +8. Cite the diff location, fingerprint.yml memory, relevant exemplars when useful, and any active check when a finding blocks.`; } -function packageFindingPolicySection(memory: PackageMemory): string { - const memoryDir = memory.memoryDir ?? ".ghost"; +function packageFindingPolicySection(): string { return `## Finding Policy Use these categories: ${REVIEW_FINDING_CATEGORIES.map((category) => `\`${category}\``).join(", ")}. @@ -94,13 +87,9 @@ Only findings backed by an active check should be treated as blocking. Everythin Review only what Ghost memory or active checks make relevant to the product experience. -When accepted fingerprint memory is silent, local evidence can still support advisory critique. Label those findings as provisional and non-Ghost-backed, and ground them in nearby product surfaces, local components, token or copy conventions, accepted decisions, or human intent. Ask the human before judging high-risk, irreversible, privacy/security/legal, or product-identity-defining choices. - -## Proposal Threshold +When fingerprint memory is silent, local evidence can still support advisory critique. Label those findings as provisional and non-Ghost-backed, and ground them in nearby product surfaces, local components, token or copy conventions, or optional rationale files when present. Ask the human before judging high-risk, irreversible, privacy/security/legal, or product-identity-defining choices. -Create or recommend a proposal only when the gap is repeated, high-impact, explicitly human-stated, intentionally divergent, likely to recur, or blocks confident future review. Do not propose for isolated implementation details, weak local context, duplicate open proposals, issues already fixable from accepted memory, vague taste concerns, or generic code quality. - -If the diff reveals missing or contradictory memory, report \`missing-memory\` or \`experience-gap\` only after applying the threshold. Include \`Memory action: none | recommend-proposal | create-proposal\` for each memory-gap finding. Default to \`recommend-proposal\`; use \`create-proposal\` only when the user explicitly asks to capture memory or when following the dedicated proposal workflow. Candidate proposal kinds: ${REVIEW_PROPOSAL_TYPES.map((kind) => `\`${kind}\``).join(", ")}. Do not silently rewrite \`${memoryDir}/fingerprint.yml\`, \`${memoryDir}/checks.yml\`, or proposal files.`; +If the diff reveals missing or contradictory memory, report \`missing-memory\` or \`experience-gap\` as a review finding. Do not silently rewrite memory during review; memory changes are ordinary edits that go through normal Git review.`; } function packageMemoryIndex(memory: PackageMemory): string { @@ -110,6 +99,7 @@ function packageMemoryIndex(memory: PackageMemory): string { const principles = formatPrinciples(fingerprint.principles); const contracts = formatExperienceContracts(fingerprint.experience_contracts); const patterns = formatPatterns(fingerprint.patterns); + const exemplars = formatExemplars(fingerprint.exemplars); const implementationVocabulary = formatImplementationVocabulary(memory); return `## Fingerprint Memory Index @@ -124,6 +114,8 @@ ${contracts} ${patterns} +${exemplars} + ${implementationVocabulary}`; } @@ -171,12 +163,11 @@ function formatSituations(situations: GhostFingerprintSituation[]): string { } function formatPrinciples(principles: GhostFingerprintPrinciple[]): string { - const accepted = principles.filter((entry) => entry.status === "accepted"); - if (accepted.length === 0) { - return "### Principles\n- No accepted principles recorded yet."; + if (principles.length === 0) { + return "### Principles\n- No principles recorded yet."; } const lines = ["### Principles"]; - for (const principle of accepted.slice(0, 10)) { + for (const principle of principles.slice(0, 10)) { lines.push(`- \`${principle.id}\` - ${principle.principle}`); for (const guidance of principle.guidance ?? []) { lines.push(` - ${guidance}`); @@ -188,12 +179,11 @@ function formatPrinciples(principles: GhostFingerprintPrinciple[]): string { function formatExperienceContracts( contracts: GhostFingerprintExperienceContract[], ): string { - const accepted = contracts.filter((entry) => entry.status === "accepted"); - if (accepted.length === 0) { - return "### Experience Contracts\n- No accepted experience contracts recorded yet."; + if (contracts.length === 0) { + return "### Experience Contracts\n- No experience contracts recorded yet."; } const lines = ["### Experience Contracts"]; - for (const contract of accepted.slice(0, 10)) { + for (const contract of contracts.slice(0, 10)) { lines.push(`- \`${contract.id}\` - ${contract.contract}`); for (const obligation of contract.obligations ?? []) { lines.push(` - ${obligation}`); @@ -203,12 +193,11 @@ function formatExperienceContracts( } function formatPatterns(patterns: GhostFingerprintPattern[]): string { - const accepted = patterns.filter((entry) => entry.status === "accepted"); - if (accepted.length === 0) { - return "### Patterns\n- No accepted patterns recorded yet."; + if (patterns.length === 0) { + return "### Patterns\n- No patterns recorded yet."; } const lines = ["### Patterns"]; - for (const pattern of accepted.slice(0, 12)) { + for (const pattern of patterns.slice(0, 12)) { lines.push(`- \`${pattern.id}\` (${pattern.kind}) - ${pattern.pattern}`); for (const guidance of pattern.guidance ?? []) { lines.push(` - ${guidance}`); @@ -234,6 +223,26 @@ function formatImplementationVocabulary(memory: PackageMemory): string { return lines.join("\n"); } +function formatExemplars(exemplars: GhostFingerprintExemplar[]): string { + if (exemplars.length === 0) { + return "### Exemplars\n- No curated exemplars recorded yet."; + } + const lines = ["### Exemplars"]; + for (const exemplar of exemplars.slice(0, 12)) { + const detail = exemplar.title ?? exemplar.note ?? exemplar.surface_type; + lines.push( + `- \`${exemplar.id}\` - \`${exemplar.path}\`${detail ? `: ${detail}` : ""}`, + ); + if (exemplar.why) lines.push(` - Why: ${exemplar.why}`); + } + if (exemplars.length > 12) { + lines.push( + `- ${exemplars.length - 12} more exemplar(s); inspect \`fingerprint.yml\` before deciding.`, + ); + } + return lines.join("\n"); +} + function packageChecksSection(activeChecks: GhostCheck[]): string { if (activeChecks.length === 0) { return `## Active Checks @@ -256,40 +265,11 @@ No active checks are recorded. Review remains advisory unless \`checks.yml\` add return lines.join("\n"); } -function packageProposalSection(memory: PackageMemory): string { - const { openProposals, fingerprint } = memory; - const policy = fingerprint.review_policy; - const lines = ["## Open Memory Gaps"]; - if (openProposals.length === 0) { - lines.push("- No open proposals recorded."); - } else { - for (const proposal of openProposals.slice(0, 8)) { - lines.push( - `- \`${proposal.id}\` (${proposal.kind}): ${proposal.claim} Proposed action: ${proposal.proposed_action.summary}`, - ); - } - if (openProposals.length > 8) { - lines.push( - `- ${openProposals.length - 8} more open proposal(s); read \`proposals/\` before judging related drift.`, - ); - } - } - pushJoined(lines, "Proposal policy", policy.proposal_policy); - pushJoined( - lines, - "Experience-gap categories", - policy.experience_gap_categories, - { code: true }, - ); - pushJoined(lines, "Memory-gap policy", policy.memory_gap_policy); - return lines.join("\n"); -} - function packageReviewFooter(memory: PackageMemory): string { const memoryDir = memory.memoryDir ?? ".ghost"; return `--- -Generated from \`${memoryDir}/fingerprint.yml\` for ${memory.name}. Re-run \`ghost emit review-command${stackMemoryDirFlag(memory)}\` after updating fingerprint.yml, checks.yml, or open proposals.`; +Generated from \`${memoryDir}/fingerprint.yml\` for ${memory.name}. Re-run \`ghost emit review-command${stackMemoryDirFlag(memory)}\` after updating fingerprint.yml, checks.yml, or optional rationale files.`; } function stackMemoryDirFlag(memory: PackageMemory): string { diff --git a/packages/ghost/src/scan/context/package-writer.ts b/packages/ghost/src/scan/context/package-writer.ts index 0510b033..efaf2556 100644 --- a/packages/ghost/src/scan/context/package-writer.ts +++ b/packages/ghost/src/scan/context/package-writer.ts @@ -1,6 +1,6 @@ import { mkdir, writeFile } from "node:fs/promises"; import { join } from "node:path"; -import type { GhostProposalDocument } from "#ghost-core"; +import { stringify as stringifyYaml } from "yaml"; import type { FingerprintPackagePaths } from "../fingerprint-package.js"; import { loadPackageMemory, type PackageMemory } from "./package-memory.js"; import type { WriteContextResult } from "./writer.js"; @@ -56,14 +56,6 @@ export async function writePackageContextBundleFromMemory( context.checksRaw, ); } - if (context.openProposals.length > 0) { - await writeContextFile( - options.outDir, - files, - "open-proposals.md", - formatOpenProposals(context.openProposals), - ); - } if (context.intent) { await writeContextFile(options.outDir, files, "intent.md", context.intent); } @@ -101,22 +93,24 @@ This skill grounds work in the **${context.name}** Ghost fingerprint. Read the files in this order: -1. \`fingerprint.yml\` - canonical product-experience memory. -2. \`checks.yml\` when present - deterministic gates; only \`active\` checks block. -3. \`open-proposals.md\` when present - unresolved missing memory, intentional divergences, experience gaps, or check candidates. -4. \`intent.md\` when present - supplemental human-approved context. - -When generating or reviewing UI, select the relevant situation, principles, -experience contracts, and patterns from \`fingerprint.yml\` before choosing -implementation details. Use implementation vocabulary only as replaceable -material that may help satisfy product memory. Treat proposals as unresolved -context, not canonical truth. - -When accepted fingerprint memory is silent, proceed from nearby product -surfaces, local components, token and copy conventions, accepted decisions or -human intent, and ordinary UX judgment when safe. Label that reasoning as -provisional and non-Ghost-backed. Ask a human before making high-risk, -irreversible, privacy/security/legal, or product-identity-defining choices. +1. \`prompt.md\` - generation packet: product prose, inventory, exemplars, and active checks. +2. \`fingerprint.yml\` - canonical product prose and exemplar anchors. +3. \`checks.yml\` when present - deterministic gates; only \`active\` checks block. +4. \`intent.md\` when present - supplemental human-authored context. + +When generating UI, combine product prose from \`fingerprint.yml\`, optional +inventory facts, and curated exemplars. Use implementation vocabulary and +inventory only as replaceable material that may help satisfy product memory. +When reviewing, use active checks for blocking validation and keep other +findings advisory. + +When fingerprint memory is silent, proceed from nearby product surfaces, local +components, token and copy conventions, optional rationale files when present, +and ordinary UX judgment when safe. Label that reasoning as provisional and +non-Ghost-backed. Ask a human before making high-risk, irreversible, +privacy/security/legal, or product-identity-defining choices. Memory changes +are ordinary Git-reviewed edits to \`fingerprint.yml\`, \`checks.yml\`, and +optional rationale files when present. `; } @@ -125,22 +119,43 @@ function buildPackagePromptMd(context: PackageMemory): string { `You are working inside the **${context.name}** product experience as captured by Ghost.`, ]; - parts.push(`# Fingerprint Memory + parts.push(`# Product Prose + +Canonical product memory lives in \`fingerprint.yml\`. Use situations, principles, experience contracts, and patterns as the source of product judgment. \`\`\`yaml ${context.fingerprintRaw.trim()} \`\`\``); - if (context.checksRaw?.trim()) { - parts.push(`# Active Checks + parts.push(`# Inventory + +${formatInventory(context)}`); + + parts.push(`# Exemplars + +${formatExemplars(context)}`); + + if (context.checks) { + const activeChecks = context.checks.checks.filter( + (check) => check.status === "active", + ); + if (activeChecks.length > 0) { + parts.push(`# Active Checks \`\`\`yaml -${context.checksRaw.trim()} +${stringifyYaml( + { + ...context.checks, + checks: activeChecks, + }, + { lineWidth: 0 }, +).trim()} \`\`\``); - } + } else { + parts.push(`# Active Checks - if (context.openProposals.length > 0) { - parts.push(formatOpenProposals(context.openProposals)); +No active checks are recorded. Proposed or disabled checks are not blocking validation.`); + } } if (context.intent?.trim()) { @@ -153,36 +168,19 @@ ${context.intent.trim()} parts.push(`# Use This Context +- Generate from product prose + inventory + exemplars. - Select the relevant situation before generating or reviewing UI. - Preserve applicable principles, experience contracts, and patterns. -- Use implementation vocabulary only when it supports the selected product memory. -- Only active checks are blocking. -- Treat open proposals as unresolved context that may explain gaps or intentional divergence. -- When accepted fingerprint memory is silent, proceed from nearby product surfaces, local components, token and copy conventions, accepted decisions or human intent, and ordinary UX judgment when safe. +- Inspect exemplars as concrete anchors for what good looks like. +- Use inventory and implementation vocabulary only when they support the selected product memory. +- Treat checks as validation; only active checks are blocking. +- When fingerprint memory is silent, proceed from nearby product surfaces, local components, token and copy conventions, optional rationale files when present, and ordinary UX judgment when safe. - Label silent-memory reasoning as provisional and non-Ghost-backed; ask the human before high-risk, irreversible, privacy/security/legal, or product-identity-defining choices. -- Proposal Threshold: create or recommend a proposal only when the gap is repeated, high-impact, explicitly human-stated, intentionally divergent, likely to recur, or blocks confident future review. -- Do not propose for isolated implementation details, weak local context, duplicate open proposals, issues already fixable from accepted memory, vague taste concerns, or generic code quality. -- If the task exposes missing or contradictory memory that meets the threshold, recommend a \`missing-memory\`, \`intentional-divergence\`, \`experience-gap\`, or \`check-candidate\` update instead of rewriting canonical memory silently; create it only when the user explicitly asks to capture memory.`); +- Treat memory changes as ordinary Git-reviewed edits to \`fingerprint.yml\`, \`checks.yml\`, and optional rationale files when present.`); return `${parts.join("\n\n")}\n`; } -function formatOpenProposals(proposals: GhostProposalDocument[]): string { - const lines = ["# Open Proposals", ""]; - for (const proposal of proposals) { - lines.push(`## ${proposal.title}`); - lines.push(""); - lines.push(`- **ID:** \`${proposal.id}\``); - lines.push(`- **Kind:** ${proposal.kind}`); - lines.push(`- **Target:** ${proposal.proposed_action.target}`); - lines.push(`- **Claim:** ${proposal.claim}`); - lines.push(`- **Rationale:** ${proposal.rationale}`); - lines.push(`- **Proposed action:** ${proposal.proposed_action.summary}`); - lines.push(""); - } - return lines.join("\n"); -} - function buildPackageReadmeMd(context: PackageMemory): string { return `# ${context.name} context bundle @@ -192,14 +190,111 @@ package. ## Files - \`SKILL.md\` - agent skill manifest. -- \`prompt.md\` - portable prompt distilled from \`fingerprint.yml\`. -- \`fingerprint.yml\` - canonical product-experience memory. -${context.checksRaw ? "- `checks.yml` - deterministic gates.\n" : ""}${context.openProposals.length > 0 ? "- `open-proposals.md` - unresolved candidate memory updates.\n" : ""}${context.intent ? "- `intent.md` - supplemental human-approved context.\n" : ""} -Regenerate this bundle when \`fingerprint.yml\`, active checks, or open -proposals change. +- \`prompt.md\` - portable generation packet: product prose, inventory, exemplars, and checks. +- \`fingerprint.yml\` - canonical product prose and exemplar anchors. +${context.checksRaw ? "- `checks.yml` - deterministic gates.\n" : ""}${context.intent ? "- `intent.md` - supplemental human-authored context.\n" : ""} +Regenerate this bundle when \`fingerprint.yml\`, active checks, or optional +rationale files change. `; } +function formatInventory(context: PackageMemory): string { + const { inventory } = context; + if (inventory.state === "missing") { + return `No generated inventory cache is present. Inventory is optional; generate it when useful with \`mkdir -p .ghost/cache && ghost inventory > .ghost/cache/inventory.json\`.`; + } + if (inventory.state === "unreadable") { + return `Inventory cache exists at \`${inventory.path}\`, but it could not be read: ${inventory.error}. Treat inventory as unavailable until the cache is regenerated.`; + } + + const { summary } = inventory; + const lines = [ + `Inventory cache: \`${inventory.path}\``, + "- Inventory is generated source material, not canonical product memory.", + ]; + pushJoined(lines, "Platform hints", summary.platform_hints, { code: true }); + pushJoined(lines, "Build hints", summary.build_system_hints, { code: true }); + if (summary.language_histogram.length) { + lines.push( + `- Languages: ${summary.language_histogram + .map((entry) => `${entry.name} (${entry.files})`) + .join(", ")}`, + ); + } + pushJoined(lines, "Package manifests", summary.package_manifests, { + code: true, + }); + pushJoined(lines, "Config candidates", summary.candidate_config_files, { + code: true, + }); + pushJoined(lines, "Registry files", summary.registry_files, { code: true }); + if (summary.top_level_tree.length) { + lines.push( + `- Top-level tree: ${summary.top_level_tree + .map((entry) => `\`${entry.path}\``) + .join(", ")}`, + ); + } + if (summary.config?.targets?.length) { + lines.push( + `- Config targets: ${summary.config.targets + .map((target) => `\`${target.id}\``) + .join(", ")}`, + ); + } + if (summary.config?.libraries?.length) { + lines.push( + `- Reference libraries: ${summary.config.libraries + .map((library) => `\`${library.id}\``) + .join(", ")}`, + ); + } + return lines.join("\n"); +} + +function formatExemplars(context: PackageMemory): string { + const { exemplars } = context.fingerprint; + if (exemplars.length === 0) { + return "No curated exemplars are recorded yet. Use nearby product surfaces as provisional anchors and label that reasoning as non-Ghost-backed."; + } + const lines: string[] = []; + for (const exemplar of exemplars.slice(0, 16)) { + const detail = [ + exemplar.title ?? exemplar.note, + exemplar.surface_type ? `surface: ${exemplar.surface_type}` : undefined, + exemplar.scope ? `scope: ${exemplar.scope}` : undefined, + ].filter(Boolean); + lines.push( + `- \`${exemplar.id}\` - \`${exemplar.path}\`${detail.length ? ` (${detail.join("; ")})` : ""}`, + ); + if (exemplar.why) lines.push(` - Why: ${exemplar.why}`); + if (exemplar.refs?.length) { + lines.push( + ` - Memory refs: ${exemplar.refs.map((ref) => `\`${ref}\``).join(", ")}`, + ); + } + } + if (exemplars.length > 16) { + lines.push( + `- ${exemplars.length - 16} more exemplar(s); read \`fingerprint.yml\` before generating.`, + ); + } + return lines.join("\n"); +} + +function pushJoined( + lines: string[], + label: string, + values: string[] | undefined, + options: { code?: boolean } = {}, +): void { + if (!values?.length) return; + const formatted = values + .map((value) => (options.code ? `\`${value}\`` : value)) + .join(", "); + lines.push(`- ${label}: ${formatted}`); +} + function ensureTrailingNewline(value: string): string { return value.endsWith("\n") ? value : `${value}\n`; } diff --git a/packages/ghost/src/scan/fingerprint-package.ts b/packages/ghost/src/scan/fingerprint-package.ts index 595fd045..8b00ffec 100644 --- a/packages/ghost/src/scan/fingerprint-package.ts +++ b/packages/ghost/src/scan/fingerprint-package.ts @@ -10,7 +10,6 @@ import { lintGhostChecks, lintGhostDecision, lintGhostFingerprint, - lintGhostProposal, MAP_FILENAME, SURVEY_FILENAME, } from "#ghost-core"; @@ -23,7 +22,6 @@ import { FINGERPRINT_YML_FILENAME, INTENT_FILENAME, PATTERNS_FILENAME, - PROPOSALS_DIRNAME, RESOURCES_FILENAME, } from "./constants.js"; import type { LintIssue, LintReport } from "./lint.js"; @@ -46,7 +44,6 @@ export interface FingerprintPackagePaths { checks: string; intent: string; decisions: string; - proposals: string; cache: string; } @@ -73,7 +70,6 @@ export function resolveFingerprintPackage( checks: join(dir, GHOST_CHECKS_FILENAME), intent: join(dir, INTENT_FILENAME), decisions: join(dir, DECISIONS_DIRNAME), - proposals: join(dir, PROPOSALS_DIRNAME), cache: join(dir, CACHE_DIRNAME), }; } @@ -86,8 +82,6 @@ export async function initFingerprintPackage( const paths = resolveFingerprintPackage(dirArg, cwd); await mkdir(paths.dir, { recursive: true }); await Promise.all([ - mkdir(paths.proposals, { recursive: true }), - mkdir(paths.cache, { recursive: true }), writeFile( paths.fingerprintYml, templateFingerprintYml(options.reference), @@ -132,13 +126,6 @@ export async function lintFingerprintPackage( lintGhostDecision, issues, ); - await lintMemoryDirectory( - paths.proposals, - "proposals", - "proposal", - lintGhostProposal, - issues, - ); let fingerprint: GhostFingerprintDocument | undefined; if (fingerprintRaw !== undefined) { @@ -184,8 +171,8 @@ export async function lintFingerprintPackage( async function lintMemoryDirectory( dirPath: string, - label: "decisions" | "proposals", - itemLabel: "decision" | "proposal", + label: "decisions", + itemLabel: "decision", lint: (input: unknown) => ReturnType<typeof lintGhostDecision>, issues: LintIssue[], ): Promise<void> { @@ -315,32 +302,15 @@ function templateFingerprintYml(reference?: string): string { const referenceInput = reference ? normalizeReferenceInput(reference) : undefined; - const implementationVocabulary = referenceInput - ? `implementation_vocabulary: + if (referenceInput) { + return `schema: ${GHOST_FINGERPRINT_SCHEMA} +implementation_vocabulary: libraries: - ${referenceInput.id} - notes: - - Product experience memory is intentionally blank until human-authored or human-approved. -` - : "implementation_vocabulary: {}\n"; +`; + } return `schema: ${GHOST_FINGERPRINT_SCHEMA} -summary: {} -topology: {} -situations: [] -principles: [] -experience_contracts: [] -patterns: [] -${implementationVocabulary}review_policy: - proposal_policy: - - Agents recommend or create thresholded proposals for durable missing memory, intentional divergences, experience gaps, and check candidates. - - Proposal candidates should be repeated, high-impact, explicitly human-stated, intentionally divergent, likely to recur, or blocking confident future review. - - Humans promote durable memory into fingerprint.yml and checks.yml. - experience_gap_categories: - - missing-memory - - intentional-divergence - - experience-gap - - check-candidate `; } diff --git a/packages/ghost/src/scan/memory-stack.ts b/packages/ghost/src/scan/memory-stack.ts index b88dc1e2..9aec8454 100644 --- a/packages/ghost/src/scan/memory-stack.ts +++ b/packages/ghost/src/scan/memory-stack.ts @@ -16,25 +16,24 @@ import { type GhostExperienceScope, type GhostFingerprintDocument, type GhostFingerprintEvidence, - type GhostFingerprintReviewPolicy, GhostFingerprintSchema, type GhostFingerprintSummary, type GhostFingerprintTopology, - type GhostFingerprintTopologyExample, type GhostFingerprintTopologyScope, - type GhostProposalDocument, - GhostProposalSchema, lintGhostChecks, lintGhostDecision, lintGhostFingerprint, - lintGhostProposal, type MapFrontmatter, } from "#ghost-core"; import { FINGERPRINT_PACKAGE_DIR, FINGERPRINT_YML_FILENAME, } from "./constants.js"; -import type { PackageMemory } from "./context/package-memory.js"; +import { + loadPackageInventory, + type PackageInventory, + type PackageMemory, +} from "./context/package-memory.js"; import type { FingerprintPackagePaths } from "./fingerprint-package.js"; import { lintFingerprintPackage, @@ -78,8 +77,8 @@ export interface GhostMemoryStackLayer extends GhostMemoryStackLayerRef { checks?: GhostChecksDocument; checks_raw?: string; intent?: string; + inventory: PackageInventory; decisions: GhostDecisionDocument[]; - proposals: GhostProposalDocument[]; } export interface GhostMemoryStack { @@ -92,8 +91,6 @@ export interface GhostMemoryStack { checks: GhostChecksDocument; intent: string | null; decisions: GhostDecisionDocument[]; - proposals: GhostProposalDocument[]; - open_proposals: GhostProposalDocument[]; }; provenance: { merge: "child-wins-by-id"; @@ -262,7 +259,6 @@ export function buildMemoryStack( ); const checks = mergeChecks(layers.map((layer) => layer.checks)); const decisions = mergeById(layers.flatMap((layer) => layer.decisions)); - const proposals = mergeById(layers.flatMap((layer) => layer.proposals)); const checkLint = lintGhostChecks(checks, { fingerprint, map: mapFromFingerprint(fingerprint), @@ -286,10 +282,6 @@ export function buildMemoryStack( checks, intent: mergeIntent(layers), decisions, - proposals, - open_proposals: proposals.filter( - (proposal) => proposal.status === "open", - ), }, provenance: { merge: "child-wins-by-id", @@ -306,13 +298,13 @@ export async function loadMemoryStackLayer( const paths = resolveFingerprintPackage(packageDir, process.cwd()); const normalizedMemoryDir = normalizeMemoryDir(memoryDir); const root = rootForMemoryPackageDir(paths.dir, normalizedMemoryDir); - const [fingerprintRaw, checksRaw, intent, decisions, proposals] = + const [fingerprintRaw, checksRaw, intent, inventory, decisions] = await Promise.all([ readFile(paths.fingerprintYml, "utf-8"), readOptional(paths.checks), readOptional(paths.intent), + loadPackageInventory(paths), readDecisionDirectory(paths.decisions), - readProposalDirectory(paths.proposals), ]); const fingerprint = normalizeFingerprintPaths( @@ -344,12 +336,10 @@ export async function loadMemoryStackLayer( ...(checks ? { checks } : {}), ...(checksRaw ? { checks_raw: checksRaw } : {}), ...(intent ? { intent } : {}), + inventory, decisions: decisions.map((decision) => normalizeDecisionPaths(decision, root, repoRoot), ), - proposals: proposals.map((proposal) => - normalizeProposalPaths(proposal, root, repoRoot), - ), }; } @@ -371,7 +361,10 @@ export function memoryStackToPackageMemory( checks: stack.merged.checks, checksRaw: stringifyYaml(stack.merged.checks, { lineWidth: 0 }), intent: stack.merged.intent ?? undefined, - openProposals: stack.merged.open_proposals, + inventory: stack.layers.at(-1)?.inventory ?? { + state: "missing", + path: `${stack.memory_dir}/cache/inventory.json`, + }, }; } @@ -545,19 +538,6 @@ async function readDecisionDirectory( return docs; } -async function readProposalDirectory( - dirPath: string, -): Promise<GhostProposalDocument[]> { - const parsed = await readYamlFiles(dirPath); - const docs: GhostProposalDocument[] = []; - for (const { path, value } of parsed) { - const report = lintGhostProposal(value); - if (report.errors > 0) throwMemoryLintError(path, report.issues); - docs.push(GhostProposalSchema.parse(value) as GhostProposalDocument); - } - return docs; -} - async function readYamlFiles( dirPath: string, ): Promise<Array<{ path: string; value: unknown }>> { @@ -604,8 +584,8 @@ function mergeFingerprints( principles: [], experience_contracts: [], patterns: [], + exemplars: [], implementation_vocabulary: {}, - review_policy: {}, }; for (const fingerprint of fingerprints) { @@ -624,6 +604,10 @@ function mergeFingerprints( ...fingerprint.experience_contracts, ]); merged.patterns = mergeById([...merged.patterns, ...fingerprint.patterns]); + merged.exemplars = mergeById([ + ...merged.exemplars, + ...fingerprint.exemplars, + ]); merged.implementation_vocabulary = { tokens: mergeStrings( merged.implementation_vocabulary.tokens, @@ -646,10 +630,6 @@ function mergeFingerprints( fingerprint.implementation_vocabulary.notes, ), }; - merged.review_policy = mergeReviewPolicy( - merged.review_policy, - fingerprint.review_policy, - ); } const report = lintGhostFingerprint(merged); @@ -686,50 +666,19 @@ function mergeTopology( ...(parent.scopes ?? []), ...(child.scopes ?? []), ]) as GhostFingerprintTopologyScope[]; - const examples = mergeByKey( - [...(parent.examples ?? []), ...(child.examples ?? [])], - (example) => example.path, - ) as GhostFingerprintTopologyExample[]; return { scopes, surface_types: mergeStrings( mergeStrings(parent.surface_types, child.surface_types), - collectSurfaceTypes(scopes, examples), + collectSurfaceTypes(scopes), ), - examples, }; } function collectSurfaceTypes( scopes: GhostFingerprintTopologyScope[], - examples: GhostFingerprintTopologyExample[], ): string[] | undefined { - return mergeStrings( - scopes.flatMap((scope) => scope.surface_types ?? []), - examples.flatMap((example) => - example.surface_type ? [example.surface_type] : [], - ), - ); -} - -function mergeReviewPolicy( - parent: GhostFingerprintReviewPolicy, - child: GhostFingerprintReviewPolicy, -): GhostFingerprintReviewPolicy { - return { - proposal_policy: mergeStrings( - parent.proposal_policy, - child.proposal_policy, - ), - experience_gap_categories: mergeStrings( - parent.experience_gap_categories, - child.experience_gap_categories, - ), - memory_gap_policy: mergeStrings( - parent.memory_gap_policy, - child.memory_gap_policy, - ), - }; + return mergeStrings(scopes.flatMap((scope) => scope.surface_types ?? [])); } function mergeChecks( @@ -780,12 +729,10 @@ function normalizeFingerprintPaths( ...scope, paths: scope.paths.map((path) => normalizePath(path, baseRoot, repoRoot)), })); - fingerprint.topology.examples = fingerprint.topology.examples?.map( - (example) => ({ - ...example, - path: normalizePath(example.path, baseRoot, repoRoot), - }), - ); + fingerprint.exemplars = fingerprint.exemplars.map((exemplar) => ({ + ...exemplar, + path: normalizePath(exemplar.path, baseRoot, repoRoot), + })); fingerprint.situations = fingerprint.situations.map((entry) => ({ ...entry, evidence: normalizeFingerprintEvidence(entry.evidence, baseRoot, repoRoot), @@ -864,23 +811,6 @@ function normalizeDecisionPaths( }; } -function normalizeProposalPaths( - input: GhostProposalDocument, - baseRoot: string, - repoRoot: string, -): GhostProposalDocument { - const proposal = clone(input); - return { - ...proposal, - scope: normalizeExperienceScopePaths(proposal.scope, baseRoot, repoRoot), - evidence: normalizeExperienceEvidence( - proposal.evidence, - baseRoot, - repoRoot, - ), - }; -} - function normalizeScopePaths<T extends { paths?: string[] }>( scope: T | undefined, baseRoot: string, diff --git a/packages/ghost/src/scan/scan-status.ts b/packages/ghost/src/scan/scan-status.ts index abd67b00..75b35063 100644 --- a/packages/ghost/src/scan/scan-status.ts +++ b/packages/ghost/src/scan/scan-status.ts @@ -16,7 +16,6 @@ import { CONFIG_FILENAME, FINGERPRINTS_DIRNAME, INTENT_FILENAME, - PROPOSALS_DIRNAME, SCOPE_SURVEYS_DIRNAME, } from "./constants.js"; @@ -73,7 +72,6 @@ export interface ScanStatus { config: ScanStageReport; checks: ScanStageReport; intent: ScanStageReport; - proposals: ScanStageReport; cache: ScanStageReport; scopes?: ScanScopeReport[]; scope_error?: string; @@ -85,7 +83,7 @@ export interface ScanStatus { * Inspect a Ghost memory directory and report whether the canonical * `fingerprint.yml` exists. Generated inventory is cache, not a prerequisite: * the durable product-experience memory is fingerprint.yml plus optional - * checks/proposals. + * checks. Other files are supplemental when present. */ export async function scanStatus( dirPath: string, @@ -96,7 +94,6 @@ export async function scanStatus( const configPath = resolve(dir, CONFIG_FILENAME); const checksPath = resolve(dir, GHOST_CHECKS_FILENAME); const intentPath = resolve(dir, INTENT_FILENAME); - const proposalsPath = resolve(dir, PROPOSALS_DIRNAME); const cachePath = resolve(dir, CACHE_DIRNAME); const [ @@ -104,14 +101,12 @@ export async function scanStatus( configPresent, checksPresent, intentPresent, - proposalsPresent, cachePresent, ] = await Promise.all([ pathExists(fingerprintPath, "file"), pathExists(configPath, "file"), pathExists(checksPath, "file"), pathExists(intentPath, "file"), - pathExists(proposalsPath, "directory"), pathExists(cachePath, "directory"), ]); @@ -131,10 +126,6 @@ export async function scanStatus( state: intentPresent ? "present" : "missing", path: intentPath, }; - const proposals: ScanStageReport = { - state: proposalsPresent ? "present" : "missing", - path: proposalsPath, - }; const cache: ScanStageReport = { state: cachePresent ? "present" : "missing", path: cachePath, @@ -146,7 +137,6 @@ export async function scanStatus( config, checks, intent, - proposals, cache, readiness: await scanReadiness(fingerprintPath, fingerprintPresent), recommended_next: fingerprintPresent ? null : "fingerprint", @@ -218,7 +208,7 @@ async function scanReadiness( principles?: unknown[]; experience_contracts?: unknown[]; patterns?: unknown[]; - topology?: { examples?: unknown[] }; + exemplars?: unknown[]; implementation_vocabulary?: { tokens?: unknown[]; components?: unknown[]; @@ -249,7 +239,7 @@ async function scanReadiness( if (productMemoryCount === 0 && implementationVocabularyCount === 0) { return readinessReport("memory-empty", { reasons: [ - "fingerprint.yml is valid but has no principles, situations, experience contracts, patterns, or implementation vocabulary yet.", + "fingerprint.yml is valid but has no product-experience entries or implementation vocabulary yet.", ], cannot_review: [ "product identity", @@ -279,7 +269,7 @@ async function scanReadiness( } return readinessReport("memory-ready", { - product_surface_count: fingerprint.topology?.examples?.length ?? 0, + product_surface_count: fingerprint.exemplars?.length ?? 0, implementation_vocabulary_rows: implementationVocabularyRows, reasons: ["fingerprint.yml contains product-experience memory."], can_review: [ diff --git a/packages/ghost/src/scan/verify-package.ts b/packages/ghost/src/scan/verify-package.ts index dc74d9a9..a7a9e53e 100644 --- a/packages/ghost/src/scan/verify-package.ts +++ b/packages/ghost/src/scan/verify-package.ts @@ -51,6 +51,7 @@ export async function verifyFingerprintPackage( if (fingerprint) { await verifyFingerprintEvidence(fingerprint, root, issues); + await verifyFingerprintExemplars(fingerprint, root, issues); } if (fingerprint && checks) { @@ -64,6 +65,27 @@ export async function verifyFingerprintPackage( return finalize(issues); } +async function verifyFingerprintExemplars( + fingerprint: GhostFingerprintDocument, + root: string, + issues: VerifyFingerprintIssue[], +): Promise<void> { + await Promise.all( + fingerprint.exemplars.map(async (entry, index) => { + const exemplarPath = isAbsolute(entry.path) + ? entry.path + : resolve(root, entry.path); + if (await pathExists(exemplarPath)) return; + issues.push({ + severity: "warning", + rule: "fingerprint-exemplar-unreachable", + message: `fingerprint exemplar path '${entry.path}' could not be resolved from ${root}.`, + path: `fingerprint.yml.exemplars[${index}].path`, + }); + }), + ); +} + async function readFingerprint( path: string, issues: VerifyFingerprintIssue[], diff --git a/packages/ghost/src/skill-bundle/SKILL.md b/packages/ghost/src/skill-bundle/SKILL.md index 6f52f485..6315f5d8 100644 --- a/packages/ghost/src/skill-bundle/SKILL.md +++ b/packages/ghost/src/skill-bundle/SKILL.md @@ -1,6 +1,6 @@ --- name: ghost -description: Capture, validate, review, and evolve a repo-local Ghost fingerprint. Use when the user wants to capture a product fingerprint, update .ghost, brief work from accepted product-experience context, review drift, verify generated UI, compare fingerprints, or record accepted divergence. +description: Author, validate, and review repo-local Ghost memory. Use when the user wants to set up a product fingerprint, update .ghost, brief work from product-experience context, review drift, verify generated UI, or use advanced comparison/drift stance workflows. license: Apache-2.0 metadata: homepage: https://github.com/block/ghost @@ -9,87 +9,101 @@ metadata: # Ghost - Product Fingerprints -Ghost captures product identity in a repo-local fingerprint bundle: +Ghost captures product identity in a repo-local memory contract: ```text .ghost/ - fingerprint.yml - config.yml # optional implementation roots and reference registries/libraries - checks.yml # optional deterministic gates - intent.md # optional human-approved intent - decisions/ # optional accepted/rejected rationale - proposals/ # optional candidate updates - cache/ # optional generated caches + fingerprint.yml # canonical product-experience memory + checks.yml # optional deterministic gates grounded in memory ``` -`fingerprint.yml` is the canonical product-experience memory. `config.yml` -maps implementation roots and reference UI registries/libraries without making -those references product intent. Checks are deterministic gates. Proposals -capture thresholded missing memory, intentional divergence, experience gaps, and -check candidates until a human promotes them. The host agent reads and writes -the fingerprint; the CLI provides deterministic validation, comparison, -routing, and handoff packets. +`fingerprint.yml` is the source of truth when it is checked in. Ordinary Git +workflow is the staging and approval boundary: uncommitted or unmerged changes +are drafts, and committed memory is canonical for Ghost. Checks are optional +deterministic gates. Ghost is not a lifecycle manager, proposal system, +design-system registry, or screenshot archive. -Repos may also contain nested bundles such as `apps/checkout/.ghost/`. Resolve -the memory stack for the task path and read layers broad-to-local. Child entries -with the same `id` override parent entries; child-relative paths are normalized -to repo-root paths by the CLI. +Generation uses **prose + inventory + exemplars**: -Host wrappers may store memory under another safe relative directory and pass -`--memory-dir <relative-dir>` to stack-aware commands. Ghost stays -adapter-neutral: consume JSON and let the wrapper map severities into its own -review or check format. +- Prose in `fingerprint.yml` explains what matters and why. +- Optional inventory in `cache/inventory.json` says what exists now. +- Exemplars in `fingerprint.yml` show concrete surfaces worth inspecting. -## CLI Verbs +Checks and review validate output; they are not generation memory. + +`fingerprint.yml` may start with only `schema: ghost.fingerprint/v1`. Add only +sections that contain real memory; Ghost normalizes omitted top-level sections +internally for checks, review, emit, and stack resolution. + +Optional material may sit beside the core files: `config.yml` for +implementation routing, `intent.md` for human-authored intent, `decisions/` for +historical rationale, and `cache/` for explicit generated inventory. Use these +only when present or requested. + +Advanced repos may contain nested bundles such as `apps/checkout/.ghost/`, and +host wrappers may use `--memory-dir <relative-dir>`. Ghost stays +adapter-neutral: wrappers consume JSON and map severities into their own review +or check format. + +## Core CLI Verbs + +| Verb | Purpose | +|---|---| +| `ghost init [dir]` | Create `.ghost/fingerprint.yml` and `.ghost/checks.yml`. | +| `ghost scan [dir] [--format json]` | Report fingerprint memory presence and readiness. | +| `ghost lint [file-or-dir]` | Validate a bundle or artifact. | +| `ghost verify [dir] --root <dir>` | Validate evidence paths, exemplar paths, and typed check refs. | +| `ghost check --base <ref>` | Run active deterministic gates against a diff. | +| `ghost review --base <ref>` | Emit an advisory review packet grounded in memory, exemplars, checks, and diff evidence. | +| `ghost emit <kind>` | Emit `review-command` or the `context-bundle` generation packet. | +| `ghost skill install` | Install this unified skill bundle. | + +## Advanced And Legacy CLI Verbs | Verb | Purpose | |---|---| -| `ghost init [dir] [--scope <path>] [--memory-dir <relative-dir>] [--with-intent] [--with-config] [--reference <path-or-registry>]` | Create a root or scoped memory skeleton. | -| `ghost scan [dir] [--include-nested] [--memory-dir <relative-dir>] [--format json]` | Report fingerprint memory presence and nested readiness. | -| `ghost stack [path...] [--memory-dir <relative-dir>]` | Inspect resolved broad-to-local memory layers and merged output. | +| `ghost init --scope <path>` / `--memory-dir <relative-dir>` | Create or resolve scoped/custom memory. | +| `ghost stack [path...]` | Inspect resolved broad-to-local memory layers and merged output. | | `ghost inventory [path]` | Emit raw repo signals for optional cache/source material. | -| `ghost lint [file-or-dir] [--all] [--memory-dir <relative-dir>]` | Validate a bundle, artifact, or all nested stack merges. | -| `ghost verify [dir] --root <dir> [--all] [--memory-dir <relative-dir>]` | Validate fingerprint evidence, checks, optional decisions/proposals, and stack integrity. | +| `ghost lint --all` / `ghost verify --all` | Validate nested stack merges. | | `ghost survey <op>` | Legacy/cache survey helpers for optional inventory workflows. | -| `ghost check --base <ref> [--memory-dir <relative-dir>] [--package <dir>]` | Run active deterministic gates against a diff; default groups files by memory stack. | -| `ghost review --base <ref> [--memory-dir <relative-dir>] [--package <dir>]` | Emit an advisory review packet grounded in resolved stack evidence. | -| `ghost proposal <create|list|resolve> [--memory-dir <relative-dir>]` | Create, list, or close scoped proposals without auto-promoting memory. | | `ghost compare <a> <b> [...more]` | Compare root bundles or direct fingerprints. | | `ghost ack` / `track` / `diverge` | Record stance toward tracked drift. | -| `ghost emit <kind>` | Emit `review-command` or `context-bundle`. | -| `ghost skill install` | Install this unified skill bundle. | ## Workflows -- Fingerprint Capture: follow [references/capture.md](references/capture.md). +- Fingerprint memory: follow [references/capture.md](references/capture.md). - Author fingerprint patterns: follow [references/patterns.md](references/patterns.md). -- Recall accepted product-experience context: follow [references/recall.md](references/recall.md). +- Recall product-experience context: follow [references/recall.md](references/recall.md). - Shape a pre-generation brief: follow [references/brief.md](references/brief.md). - Critique generated or changed work: follow [references/critique.md](references/critique.md). - Review drift: follow [references/review.md](references/review.md). - Verify generation: follow [references/verify.md](references/verify.md). -- Compare bundles: follow [references/compare.md](references/compare.md). - Remediate drift: follow [references/remediate.md](references/remediate.md). -- Propose a candidate fingerprint update: follow [references/propose.md](references/propose.md). -- Promote a human-approved proposal: follow [references/promote.md](references/promote.md). +- Advanced compare bundles: follow [references/compare.md](references/compare.md). ## Always -- Treat the resolved `.ghost/` memory stack as the source of truth. -- Use `.ghost/config.yml` for implementation/library routing; keep product - meaning in `fingerprint.yml` or approved memory. -- Validate with `ghost lint` and `ghost verify --root <target>` before declaring Fingerprint Capture complete; use `--all` when nested bundles exist. +- Treat checked-in `fingerprint.yml` as the source of truth. +- Generate from product prose, optional inventory, and curated exemplars. +- Run active checks from `checks.yml`; only active deterministic checks block. +- Use local evidence as provisional when fingerprint memory is silent. +- Treat memory changes as ordinary Git-reviewed edits. +- Validate with `ghost lint` and `ghost verify --root <target>` before declaring + fingerprint memory complete. - Run `ghost check` for deterministic gates and `ghost review` for advisory critique. -- Include accepted decisions with `ghost review --include-memory` when product-experience rationale matters. +- Use optional config, intent, decisions, cache, nested stacks, and custom memory + dirs only when present or requested. ## When Memory Is Silent -Silent fingerprint memory does not require stopping by default. When accepted -memory does not cover the task, proceed from nearby product surfaces, local -components, token and copy conventions, accepted decisions or human intent, and -ordinary UX judgment when safe. Label that reasoning as provisional and -non-Ghost-backed. Ask a human before making high-risk, irreversible, -privacy/security/legal, or product-identity-defining choices. +Silent fingerprint memory does not require stopping by default. When memory does +not cover the task, proceed from nearby product surfaces, local components, +token and copy conventions, optional rationale files when present, and ordinary +UX judgment when safe. Label that reasoning as provisional and +non-Ghost-backed. +Ask a human before making high-risk, irreversible, privacy/security/legal, or +product-identity-defining choices. ## Never @@ -97,4 +111,4 @@ privacy/security/legal, or product-identity-defining choices. - Never claim provisional judgment, local convention, or general UX reasoning as Ghost-backed memory. - Never treat `intent.md` as authoritative unless human-authored or human-approved. -- Never treat proposals or rejected decisions as canonical inputs. +- Never treat rejected decisions as canonical inputs. diff --git a/packages/ghost/src/skill-bundle/assets/fingerprint.template.md b/packages/ghost/src/skill-bundle/assets/fingerprint.template.md deleted file mode 100644 index f257814c..00000000 --- a/packages/ghost/src/skill-bundle/assets/fingerprint.template.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -# identity -id: PROJECT_ID -source: llm -timestamp: TIMESTAMP_ISO - -# narrative tags (prose lives in the body) -observation: - personality: - - adjective-1 - - adjective-2 - resembles: - - known-system - -# decision index: rationale and evidence live in matching body blocks -decisions: - - dimension: color-strategy - - dimension: spatial-system - -# concrete tokens -palette: - dominant: - - { role: primary, value: "#000000" } - neutrals: - steps: ["#ffffff", "#0a0a0a"] - count: 2 - semantic: [] - saturationProfile: muted - contrast: high - -spacing: - scale: [4, 8, 16, 24, 32] - regularity: 1.0 - baseUnit: 4 - -typography: - families: ["Inter"] - sizeRamp: [14, 16, 20, 24, 32] - weightDistribution: { "400": 1, "700": 1 } - lineHeightPattern: normal - -surfaces: - borderRadii: [4, 8] - shadowComplexity: deliberate-none - borderUsage: minimal ---- - -# Character - -2-4 sentences on the personality of this design language. Describe the language directly instead of introducing the project by name. Name what the system permits, not only what it avoids: scale contrast, shaped composition, semantic color, role-based elevation, functional motion, font sourcing, or themeable tokens. - -# Signature - -2-4 sentences on the final-picture posture: dominant moves, layout habits, and what generated output should feel like when the language comes together. - -# Decisions - -### color-strategy - -Prose rationale for the color-strategy decision. Implementation-agnostic: name the pattern, not the token. - -**Evidence:** -- `--color-primary: #000000` -- Survey color evidence: dominant observations cluster on the neutral palette - -### spatial-system - -Prose rationale for the spatial-system decision. - -**Evidence:** -- `--space-4: 16px` -- Survey spacing evidence: padding/gap/margin observations stay on the documented scale diff --git a/packages/ghost/src/skill-bundle/references/brief.md b/packages/ghost/src/skill-bundle/references/brief.md index cc9d3956..06e50be3 100644 --- a/packages/ghost/src/skill-bundle/references/brief.md +++ b/packages/ghost/src/skill-bundle/references/brief.md @@ -1,49 +1,32 @@ --- name: brief -description: Shape a pre-generation brief from the resolved Ghost memory stack. +description: Build a concise pre-generation brief from Ghost memory. --- -# Brief From The Ghost Fingerprint - -Use this before generating or implementing product UI. The goal is to turn -repo-local memory into a concise working brief: what the agent should preserve, -where it has freedom, and what needs human judgment. - -## Steps - -1. Resolve the memory stack for the task path with `ghost stack <path>` when a - path is known. -2. Read merged `fingerprint.yml` memory broad-to-local. -3. Select the relevant `situation` for the task, or state that none fits. -4. Pull applicable `principles`, `experience_contracts`, and `patterns`. -5. Read `implementation_vocabulary` only as current replaceable material. -6. Read merged checks for active deterministic gates. -7. Skim open proposals from the stack for gaps or intentional divergences. -8. Name missing or contradictory memory explicitly. - -## Output - -Produce: - -- Task framing and selected situation. -- Relevant principles and experience contracts. -- Product-native pattern guidance. -- Implementation vocabulary that may help satisfy the product memory. -- Active checks to run afterward. -- Open proposals or known gaps. -- Decisions the human should make before generation. - -When accepted fingerprint memory is silent, do not stop by default. Continue -from nearby product surfaces, local components, token and copy conventions, -accepted decisions or human intent, and ordinary UX judgment when safe. Label -that guidance as provisional and non-Ghost-backed, and ask the human only for -high-risk, irreversible, privacy/security/legal, or product-identity-defining -choices. - -Do not claim provisional guidance as fingerprint context. If memory is missing, -apply the Proposal Threshold before recommending memory action. A proposal -candidate should be repeated, high-impact, explicitly human-stated, -intentionally divergent, likely to recur, or blocking confident future review; -otherwise name the gap as local uncertainty. If it qualifies, say which proposal -type should be recorded after the work: `missing-memory`, -`intentional-divergence`, `experience-gap`, or `check-candidate`. +# Recipe: Brief Work From Ghost Memory + +1. Read checked-in `fingerprint.yml` product prose. +2. Select the relevant situations, principles, contracts, and patterns. +3. Inspect matching exemplars as concrete generation anchors. +4. Use optional generated inventory when present to understand what exists. +5. Skim active checks so generation avoids deterministic failures. +6. Use `ghost stack <path>`, accepted decisions, and `intent.md` only when the + repo has opted into those advanced inputs. + +Return a short brief with: + +- Relevant memory IDs. +- Product obligations for this task. +- Exemplars to inspect. +- Inventory facts when cache is present. +- Active checks to avoid. +- Local evidence or examples. +- Provisional assumptions when fingerprint memory is silent. + +When memory is silent, do not stop by default. Continue from nearby product +surfaces, local components, token and copy conventions, accepted decisions or +human intent, and ordinary UX judgment when safe. Label that reasoning as +provisional and non-Ghost-backed. + +Memory updates are ordinary Git-reviewed edits to `fingerprint.yml`, +`checks.yml`, and optional rationale files when present. diff --git a/packages/ghost/src/skill-bundle/references/capture.md b/packages/ghost/src/skill-bundle/references/capture.md index 9e61c42e..c7a1a4c3 100644 --- a/packages/ghost/src/skill-bundle/references/capture.md +++ b/packages/ghost/src/skill-bundle/references/capture.md @@ -1,6 +1,6 @@ --- name: capture -description: Capture repo-local Ghost product-experience memory in .ghost/fingerprint.yml. +description: Author repo-local Ghost product-experience memory. handoffs: - label: Inspect memory status command: ghost scan @@ -10,23 +10,39 @@ handoffs: prompt: Run ghost check against this bundle --- -# Recipe: Capture A Ghost Fingerprint +# Recipe: Author Ghost Fingerprint Memory -**Goal:** produce durable product-experience memory that helps agents build, -review, and repair on-brand work. +**Goal:** record durable product-experience memory in `.ghost/fingerprint.yml`. +If a change is uncommitted or unmerged, it is draft work. If it is checked in, +Ghost treats it as canonical memory. ```text .ghost/ - fingerprint.yml # canonical product-experience memory - checks.yml # optional deterministic gates - proposals/ # candidate memory changes - cache/ # optional generated inventory - intent.md # optional human-approved context + fingerprint.yml # canonical product-experience memory + checks.yml # optional deterministic gates ``` -`fingerprint.yml` answers what matters and why. Generated inventory answers -what exists right now. Keep those separate: inventory may be refreshed or -discarded, but canonical memory changes only through deliberate edits. +`fingerprint.yml` answers what matters and why. `checks.yml` contains only +optional active gates. Git is the approval boundary; Ghost does not manage a +separate memory lifecycle. + +Generation uses product prose, optional generated inventory, and curated +exemplars. Checks validate output after generation; they are not generation +memory. + +`fingerprint.yml` may start sparse: + +```yaml +schema: ghost.fingerprint/v1 +``` + +Add only sections that contain real memory. Ghost normalizes omitted top-level +sections internally, so an empty project does not need placeholder arrays or +objects. + +Optional files may appear beside the core files: `intent.md` for human-authored +context, `decisions/` for historical rationale, `config.yml` for implementation +routing, and `cache/` for explicit generated inventory. ## Steps @@ -42,47 +58,47 @@ to preserve. New projects may start with an empty but valid fingerprint and add memory as product choices become real. Use `--with-config --reference <path-or-registry>` when the product uses a -reference UI registry or library such as Ghost UI. This writes implementation -routing into `.ghost/config.yml` and records only implementation vocabulary in -the blank product fingerprint; it does not copy reference memory into product -intent. +reference UI registry or library. This writes implementation routing into +`.ghost/config.yml` and records only implementation vocabulary in the blank +product fingerprint; it does not copy reference memory into product intent. -For a monorepo or a deeply scoped product area, initialize local memory with -`ghost init --scope <path>`. Keep broad product identity in the root bundle and -put local situations, patterns, checks, decisions, and proposals in the child. +For a monorepo or deeply scoped product area, `ghost init --scope <path>` is an +advanced option. Keep broad product identity in the root bundle and put local +situations, patterns, and checks in the child only when scoped memory is needed. ### 2. Orient -Read the product, not just the component library. Look for the surfaces, docs, +Read the product, not just the component library. Look for surfaces, docs, tests, stories, routes, screenshots, or examples that reveal identity, hierarchy, behavior, copy, accessibility, and trust. -Optional helpers: +Optional helper: ```bash +mkdir -p .ghost/cache ghost inventory . > .ghost/cache/inventory.json ``` -Treat cache output as scratch material. Do not promote raw inventory into +Treat cache output as scratch material. Do not copy raw inventory into fingerprint memory without judgment. -### 3. Author `fingerprint.yml` +### 3. Write Memory -Fill the smallest useful memory: +Edit `fingerprint.yml` with the smallest useful durable memory. Omit sections +until they have real entries: - `summary`: product identity, audience, goals, anti-goals, tradeoffs, tone. -- `topology`: scopes, surface types, and representative examples. +- `topology`: scopes and surface types. - `situations`: user/task/state moments that change obligations. - `principles`: durable product-experience truths. - `experience_contracts`: behavior, disclosure, failure, recovery, and trust. - `patterns`: visual, behavioral, content, and composition patterns. +- `exemplars`: concrete surfaces that show what good looks like. - `implementation_vocabulary`: current tokens, components, libraries, assets, and notes that may help implement the product memory. -- `review_policy`: proposal and experience-gap handling. Prefer a few high-confidence entries over a comprehensive but noisy catalog. -Every accepted entry should be useful to a future agent making or reviewing a -product change. +Every entry should help a future agent make or review a product change. ### 4. Add Checks Sparingly @@ -93,47 +109,31 @@ typed `derives_from` reference into `fingerprint.yml`. derives_from: pattern:resource-index-stays-tabular ``` -Candidate checks belong in `.ghost/proposals/` as `kind: check-candidate` until -a human promotes them. +Keep speculative checks out of `checks.yml` until they have a deterministic +detector and evidence. Proposed checks may use `status: proposed` because check +enforcement still has its own lifecycle. ### 5. Validate ```bash ghost lint .ghost ghost verify .ghost --root <target> -ghost lint --all -ghost verify --all ghost check --base HEAD ``` -`lint` validates shape, `verify` validates evidence paths and typed check refs, -and `check` runs only active deterministic gates. +`lint` validates canonical shape, `verify` validates evidence paths and typed +check refs, and `check` runs only active deterministic gates. -## Gaps - -If the repo does not yet contain enough product experience to capture, say so. -For missing or contradictory memory, recommend or create a proposal only when -the gap is durable enough to help a future agent. It should be repeated, -high-impact, explicitly human-stated, intentionally divergent, likely to recur, -or blocking confident future review. - -When accepted fingerprint memory is silent, that silence does not block useful -work by itself. Continue from nearby product surfaces, local components, token -and copy conventions, accepted decisions or human intent, and ordinary UX -judgment when safe. Label that reasoning as provisional and non-Ghost-backed. +Use `ghost lint --all` and `ghost verify --all` only when nested memory bundles +exist. -Do not create proposals for isolated implementation details, weak local context, -duplicates of open proposals, issues already fixable from accepted memory, -vague taste concerns, or generic code quality. - -Use: - -- `missing-memory` -- `intentional-divergence` -- `experience-gap` -- `check-candidate` +## Gaps -Humans promote durable truth. Agents do not silently rewrite canonical memory. +If the repo does not yet contain enough product experience to record, say so. +When memory is silent, continue from nearby product surfaces, local components, +token and copy conventions, optional rationale files when present, and ordinary +UX judgment when safe. Label that reasoning as provisional and +non-Ghost-backed. ## Never diff --git a/packages/ghost/src/skill-bundle/references/critique.md b/packages/ghost/src/skill-bundle/references/critique.md index 49024bc7..691242ce 100644 --- a/packages/ghost/src/skill-bundle/references/critique.md +++ b/packages/ghost/src/skill-bundle/references/critique.md @@ -1,44 +1,22 @@ --- name: critique -description: Critique generated or changed work using .ghost/fingerprint.yml and the Ghost CLI. +description: Critique generated or changed UI using Ghost memory. --- -# Critique With The Ghost Fingerprint +# Recipe: Critique Generated Work -Use this after generated or changed UI exists. `ghost` emits deterministic -checks and advisory packets; `fingerprint.yml` supplies product-experience -memory. +1. Run `ghost review --base <ref>` or inspect the changed files directly. +2. Read checked-in `fingerprint.yml` and active checks. +3. Compare the work against the relevant situations, principles, contracts, and + patterns. +4. Inspect relevant exemplars as concrete anchors for what good looks like. +5. Lead with actionable findings. Cite diff locations, fingerprint memory, + exemplars, active checks, and repairs where relevant. -## Steps +When fingerprint memory is silent, you may use nearby product surfaces, local +components, token and copy conventions, accepted decisions, or human intent when +present. Label that reasoning as provisional and non-Ghost-backed. -1. Run `ghost check` for deterministic gates when a diff is available. -2. Run `ghost review --include-memory` for advisory critique. -3. Read the review packet, accepted decisions, and open proposals. -4. Separate findings by role: - - design: hierarchy, flow, density, tone, and Ghost-backed obligations - - engineering: implementation choices that preserve experience - - pm: product promise, tradeoffs, trust, disclosure - - qa: experience commitments and edge states -5. Classify each issue as `fix`, `intentional-divergence`, - `missing-memory`, `experience-gap`, or `eval-uncertainty`. - -## Output - -Lead with actionable findings. Cite diff locations, fingerprint memory, active -checks, open proposals, accepted decisions, and repairs where relevant. - -When accepted fingerprint memory is silent, you may use nearby product surfaces, -local components, token and copy conventions, accepted decisions or human intent, -and ordinary UX judgment for provisional critique. Label that reasoning as -non-Ghost-backed, and ask the human before judging high-risk, irreversible, -privacy/security/legal, or product-identity-defining choices. - -For memory-gap findings, include -`Memory action: none | recommend-proposal | create-proposal`. Default to -`recommend-proposal` only when the gap meets the Proposal Threshold: repeated, -high-impact, explicitly human-stated, intentionally divergent, likely to recur, -or blocking confident future review. Use `create-proposal` only when the user -explicitly asks to capture memory or when following `propose.md`. - -Never fail a build on advisory-only context. Only active `checks.yml` gates -block. +Do not make advisory taste judgment sound blocking unless an active check backs +it. If memory is missing or contradictory, name that as `missing-memory` or +`experience-gap`; update memory only when the user asks you to edit it. diff --git a/packages/ghost/src/skill-bundle/references/map.md b/packages/ghost/src/skill-bundle/references/map.md index c9358b92..15fd8237 100644 --- a/packages/ghost/src/skill-bundle/references/map.md +++ b/packages/ghost/src/skill-bundle/references/map.md @@ -2,7 +2,7 @@ name: map description: Use repo topology as optional source material for fingerprint.yml. handoffs: - - label: Capture fingerprint memory + - label: Update fingerprint memory skill: capture prompt: Use topology observations to update .ghost/fingerprint.yml --- @@ -15,13 +15,13 @@ handoffs: Use this recipe when an older workflow, existing repo, or migration still has a `.ghost/map.md`, or when you need to orient before writing `fingerprint.yml`. -## What To Capture +## What To Record Look for facts that help agents route product-experience judgment: - product scopes and owned paths - surface types -- representative examples +- exemplar surfaces worth inspecting - frameworks, platforms, and rendering constraints - design-system or component-library locations - places where UI can actually be observed @@ -35,9 +35,12 @@ topology: paths: [src/checkout] surface_types: [payment-review] surface_types: [payment-review, empty-state] - examples: - - path: src/checkout/review.tsx - surface_type: payment-review +exemplars: + - id: checkout-review + path: src/checkout/review.tsx + surface_type: payment-review + scope: checkout + why: Shows the payment-review surface worth preserving. ``` ## Optional Inventory diff --git a/packages/ghost/src/skill-bundle/references/patterns.md b/packages/ghost/src/skill-bundle/references/patterns.md index 3aa71ebe..2275e566 100644 --- a/packages/ghost/src/skill-bundle/references/patterns.md +++ b/packages/ghost/src/skill-bundle/references/patterns.md @@ -33,7 +33,6 @@ observations in `.ghost/cache/` or keep them in scratch notes. ```yaml patterns: - id: resource-index-stays-tabular - status: accepted kind: composition pattern: Resource index views stay tabular when comparison is the task. applies_to: @@ -70,7 +69,5 @@ ghost lint .ghost ghost verify .ghost --root . ``` -If a pattern is speculative, do not add it as accepted memory. Recommend or -create a proposal only when the speculation is durable enough to help future -generation or review: repeated, high-impact, explicitly human-stated, likely to -recur, or blocking confident review. +If a pattern is speculative, do not add it as canonical memory. Leave it in +scratch notes or ask the user whether to edit `fingerprint.yml`. diff --git a/packages/ghost/src/skill-bundle/references/promote.md b/packages/ghost/src/skill-bundle/references/promote.md deleted file mode 100644 index 40bdc0ac..00000000 --- a/packages/ghost/src/skill-bundle/references/promote.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -name: promote -description: Promote a human-approved proposal into the Ghost fingerprint. ---- - -# Promote A Fingerprint Proposal - -Use this only when a human has accepted a proposal or explicitly asks to record -the decision. - -## Steps - -1. Resolve the memory stack for the affected path, then read the proposal from - the matching `.ghost/proposals/` layer. -2. Choose the target from `proposed_action.target`. -3. For `fingerprint`, update the appropriate root or scoped - `fingerprint.yml` with the smallest durable principle, situation, - experience contract, or pattern addition. Use `implementation_vocabulary` - only for current materials that help agents implement the durable memory. -4. For `checks`, update the appropriate `checks.yml` only with deterministic - detectors and typed `derives_from` references. -5. For `review_policy`, update only the proposal or review rules in - `fingerprint.yml`. -6. Mark the proposal with `ghost proposal resolve <id> --path <path> --status accepted` or leave a note if it was superseded. -7. Run `ghost lint --all` and `ghost verify --all` when nested bundles exist. - -Canonical promotion should be deliberate. Keep rejected or unresolved ideas in -proposals, not in `fingerprint.yml` or `checks.yml`. diff --git a/packages/ghost/src/skill-bundle/references/propose.md b/packages/ghost/src/skill-bundle/references/propose.md deleted file mode 100644 index b074fa2b..00000000 --- a/packages/ghost/src/skill-bundle/references/propose.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -name: propose -description: Write a candidate ghost.proposal/v1 artifact from a session. ---- - -# Propose A Fingerprint Update - -Use this when a design review, implementation, QA finding, or PM discussion -reveals a product-experience decision that may belong in the Ghost fingerprint. - -## Proposal Threshold - -Create a proposal only when the observation is durable enough to help a future -agent generate or review work. Good candidates are repeated, high-impact, -explicitly human-stated, intentionally divergent, likely to recur, or blocking -confident future review. - -Do not create proposals for isolated implementation details, weak local context, -duplicates of open proposals, issues already fixable from accepted memory, -vague taste concerns, or generic code quality. - -## Steps - -1. Confirm the observation is about product experience: perceived, used, - trusted, understood, or safely changed. -2. Resolve the memory stack for the affected path with `ghost stack <path>`. -3. Check whether merged `fingerprint.yml`, active checks, or open proposals - already cover it. -4. Confirm it meets the Proposal Threshold and is not a duplicate of an open - proposal. -5. If it is new and thresholded, run `ghost proposal create --path <path> ...` - so the proposal lands in the nearest applicable scoped bundle. -6. Use schema `ghost.proposal/v1`. -7. Run `ghost lint --all` when nested bundles exist. - -## Proposal Shape - -```yaml -schema: ghost.proposal/v1 -id: saved-payment-empty-state -status: open -kind: missing-memory -title: Saved payment empty state should teach recovery -claim: Empty states for saved payment methods should prioritize recovery over education. -rationale: The user is blocked from paying, not browsing product concepts. -scope: - roles: [design, pm, qa] - surface_types: [empty-state] -evidence: - - path: apps/payments/empty-state.tsx -proposed_action: - target: fingerprint - summary: Promote into fingerprint.yml if repeated. -``` - -Use `missing-memory`, `intentional-divergence`, `experience-gap`, or -`check-candidate`. Do not rewrite `fingerprint.yml` or `checks.yml` without -human approval. diff --git a/packages/ghost/src/skill-bundle/references/recall.md b/packages/ghost/src/skill-bundle/references/recall.md index 21cbc1fe..2552df59 100644 --- a/packages/ghost/src/skill-bundle/references/recall.md +++ b/packages/ghost/src/skill-bundle/references/recall.md @@ -1,39 +1,24 @@ --- name: recall -description: Summarize relevant Ghost fingerprint context for a task. +description: Recall applicable Ghost memory for a task or file path. --- -# Recall Fingerprint Context +# Recipe: Recall Ghost Memory -Use this when the user asks what the fingerprint says, how a product usually -handles a surface, or what constraints matter before work begins. +1. Read checked-in `fingerprint.yml` entries. +2. Select relevant situations, principles, contracts, patterns, exemplars, and + active checks. +3. Use `ghost stack <path>`, accepted decisions, and intent only when the repo + has opted into those advanced inputs. +4. Summarize only memory that applies to the task. -## Steps +Return: -1. Resolve the memory stack for the task path with `ghost stack <path>` when a - path is known. -2. Read merged `fingerprint.yml` memory broad-to-local. -3. Identify matching topology scopes, surface types, situations, and examples. -4. Select relevant principles, experience contracts, and patterns. -5. Read implementation vocabulary only as current replaceable material. -6. Read merged checks for active deterministic gates. -7. Read decisions from the resolved stack; include only `status: accepted` as - supplemental rationale. -8. Skim proposals from the stack; include only open proposals as unresolved - context. +- Applicable memory IDs and short claims. +- Exemplars to inspect when generation or review needs a concrete anchor. +- Active checks that may affect the work. +- Optional decisions or intent that explain why, when present. +- Any gaps where local evidence must carry the reasoning. -## Output - -Return a short, cited recall packet: - -- Relevant situation. -- Product-experience principles. -- Applicable experience contracts. -- Matching patterns. -- Implementation vocabulary. -- Active checks. -- Accepted rationale. -- Open proposals or known gaps. - -Do not edit files during recall. If the fingerprint does not cover the task, -say that plainly and suggest the smallest proposal type to record later. +If the fingerprint is silent, say that plainly and continue with provisional +local reasoning when safe. Memory updates are ordinary Git-reviewed edits. diff --git a/packages/ghost/src/skill-bundle/references/remediate.md b/packages/ghost/src/skill-bundle/references/remediate.md index fe060e21..6c71ed99 100644 --- a/packages/ghost/src/skill-bundle/references/remediate.md +++ b/packages/ghost/src/skill-bundle/references/remediate.md @@ -1,117 +1,23 @@ --- name: remediate -description: Given drift findings and the offending diff, suggest the minimum sufficient fixes that close the gap. -handoffs: - - label: Re-review after applying the suggested fixes - skill: review - prompt: Re-run the review against the patched files to confirm the drift is closed - - label: Acknowledge the drift as accepted - command: ghost ack - prompt: Acknowledge that the current fingerprint no longer matches and accept the drift - - label: Declare a dimension intentionally divergent - command: ghost diverge - prompt: Record an intentional divergence on a specific dimension so it stops flagging +description: Suggest minimal code or memory changes after Ghost drift findings. --- -# Recipe: Remediate Drift +# Recipe: Remediate Ghost Drift -**Goal:** turn drift findings into the minimum sufficient patch that brings the -working tree back inside the resolved Ghost memory stack and active checks. +1. Read the review packet or check output. +2. Separate active-check failures from advisory findings. +3. For active-check failures, patch the smallest implementation change that + satisfies the detector and preserves product intent. +4. For advisory findings, cite the relevant fingerprint memory and suggest the + smallest product-aligned change, using exemplars as concrete anchors when + relevant. +5. If the finding is actually intentional divergence, say so and ask whether to + update checked-in memory. -Ghost has no `ghost remediate` CLI command. You, the host agent, read the -findings, weigh them against merged `fingerprint.yml`, active checks, accepted -rationale, open proposals, and stack provenance, then write the smallest patch -that actually satisfies the cited product-experience obligation. +Use `ghost check` after implementation changes. Use `ghost lint` and +`ghost verify` after memory changes. -## Steps - -### 1. Gather Inputs - -You need: - -- The drift output from `ghost review`, `ghost check`, or compare. -- The offending diff: `git diff <base> -- <file>`. -- The resolved stack from `ghost stack <path>` when the affected path is known. -- Merged `fingerprint.yml` memory. -- Merged checks for active gates. -- Open proposals from the stack for known gaps or accepted divergence candidates. -- `.ghost-sync.json` when present; anything stance:`diverging` is intentional - and must not be remediated as accidental drift. - -### 2. Match Findings To Memory - -For every finding, identify the relevant fingerprint entry: - -- Token drift -> related principle, pattern, or active check. -- Component drift -> related principle, pattern, or active check. -- Hierarchy/density drift -> principle, situation, or composition pattern. -- Disclosure/recovery drift -> experience contract. -- Copy/trust drift -> principle, experience contract, or review policy. - -If no entry applies, do not invent one inside the code patch. Apply the -Proposal Threshold: recommend a `missing-memory` or `experience-gap` proposal -only when the gap is repeated, high-impact, explicitly human-stated, likely to -recur, or blocking confident future review. Create it with -`ghost proposal create --path <path>` only when the user explicitly asks to -capture memory. - -Then classify the repair scope: - -- **Local**: a token, class, copy, import, component substitution, or small state - visibility change can satisfy the cited memory. -- **Structural**: layout, hierarchy, flow, action placement, component anatomy, - state model, disclosure, recovery, or trust behavior must change. -- **Unresolved**: the fingerprint is silent or contradictory, or the product is - intentionally changing. Silent memory does not block a safe local repair, but - local-evidence reasoning must be labeled provisional and non-Ghost-backed. - -Do not choose a local token or class patch when the cited memory is about -hierarchy, disclosure, recovery, trust, flow, or task structure. In those cases, -propose a structural patch or a short implementation plan. - -### 3. Score By Impact - -Rank findings by how much product experience they restore: - -- **Blocking**: active check failures. -- **Load-bearing**: violations of principles, contracts, or required patterns. -- **Local cleanup**: small implementation mismatches with obvious fixes. -- **Uncertain**: advisory drift that needs human judgment. - -If the finding is intentional for this change, suggest an -`intentional-divergence` proposal instead of a code patch. - -### 4. Propose The Patch - -For each finding, write a unified-diff suggestion in the form: - -```text -file:line before -> after (why this satisfies fingerprint memory) -``` - -Group patches by file. Keep changes as narrow as the obligation allows, but let -the scope match the finding. A structural product-experience failure may require -changing component anatomy, layout, state visibility, or action placement. Avoid -unrelated cleanup, but do not under-fix the issue just to keep the diff small. - -### 5. Surface What Cannot Be Remediated - -Some findings have no clean code fix: - -- The fingerprint is silent -> recommend `missing-memory` when it meets the - Proposal Threshold. -- The product is intentionally changing -> recommend `intentional-divergence`. -- The generated work failed to compose despite available memory -> propose - `experience-gap` when the gap is durable. -- A recurring deterministic issue can be detected -> propose `check-candidate`. -- The fix would cascade across many files -> stop and call out the separate - implementation plan. - -### 6. Record The Outcome - -After the user applies or rejects patches: - -- Re-run `ghost check` and `ghost review`. -- If the user accepts drift instead of fixing it, run `ghost ack` or - `ghost diverge <dimension>`. -- Never regenerate or rewrite `fingerprint.yml` to hide drift. +Do not broaden the patch into unrelated refactors. Do not edit memory silently +unless the user asks to update `fingerprint.yml`, `checks.yml`, or optional +rationale files. diff --git a/packages/ghost/src/skill-bundle/references/review.md b/packages/ghost/src/skill-bundle/references/review.md index 2678bdcd..fc25d050 100644 --- a/packages/ghost/src/skill-bundle/references/review.md +++ b/packages/ghost/src/skill-bundle/references/review.md @@ -1,13 +1,10 @@ --- name: review -description: Review PR or working-tree changes against resolved Ghost memory stacks. +description: Review PR or working-tree changes against checked-in Ghost memory. handoffs: - label: Suggest minimal fixes skill: remediate prompt: Given the drift findings, suggest the minimal code changes that bring the diff back inside the .ghost fingerprint - - label: Accept the drift - command: ghost ack - prompt: Acknowledge that the current fingerprint no longer matches and record the drift --- # Recipe: Review Code Changes For Experience Drift @@ -22,10 +19,10 @@ handoffs: ghost check --base <ref> ``` -Fix deterministic failures first. These come from active human-promoted -`checks.yml` rules in the resolved memory stack and are the only blocking -findings in v1. Use `--package <dir>` only when the user asks for exact -single-bundle behavior. +Fix deterministic failures first. These come from active `checks.yml` rules in +checked-in memory and are the only blocking findings in v1. Use +`--package <dir>` or stack-aware options only when the user asks for advanced +routing. ### 2. Build Advisory Context @@ -35,12 +32,11 @@ ghost review --base <ref> Use the emitted packet as context. It includes: -- `stacks[]` for changed files when nested bundles apply -- merged `fingerprint.yml` memory -- merged checks -- open proposals -- optional accepted decisions when requested with `--include-memory` -- layer provenance +- `fingerprint.yml` memory +- curated exemplars from `fingerprint.yml` +- active checks from `checks.yml` +- optional stack, config, intent, or accepted decision context when present or + requested - the diff ### 3. Write Advisory Findings @@ -58,8 +54,8 @@ Each finding must cite: - diff location - `fingerprint.yml` memory +- relevant exemplars when useful - active check when blocking -- open proposal when relevant - repair or intentional-divergence rationale Good advisory topics: @@ -70,8 +66,8 @@ Good advisory topics: - generic composition - awkward action placement - copy or trust-contract mismatch -- obligations grounded in fingerprint memory, human intent, open proposals, or - active checks +- obligations grounded in fingerprint memory, human intent, accepted decisions, + or active checks Bad advisory topics: @@ -80,44 +76,12 @@ Bad advisory topics: - enforcing a rule that is not in `checks.yml` - unrelated audit categories not grounded in Ghost memory -When accepted fingerprint memory is silent, local evidence can still support -advisory critique. Label those findings as provisional and non-Ghost-backed, and -ground them in nearby product surfaces, local components, token or copy -conventions, accepted decisions, or human intent. Ask the human before judging -high-risk, irreversible, privacy/security/legal, or product-identity-defining -choices. - -### 4. Apply The Proposal Threshold - -Do not create proposals for every ambiguity. A proposal is warranted only when -the gap is durable enough to help a future agent generate or review work: - -- repeated across a surface, pattern, or workflow -- high-impact for trust, safety, recovery, money, permissions, destructive - actions, or user confidence -- explicitly stated by a human -- intentionally divergent from accepted memory -- likely to recur in future reviews -- blocking confident classification as `fix`, `intentional-divergence`, or - `eval-uncertainty` - -Do not propose for isolated implementation details, weak local context, -duplicates of open proposals, issues already fixable from accepted memory, -vague taste concerns, or generic code quality. - -For memory-gap findings, include: - -```text -Memory action: none | recommend-proposal | create-proposal -``` - -Default to `recommend-proposal`. Use `create-proposal` only when the user -explicitly asks to capture memory or when following `propose.md`. Candidate -proposal kinds: - -- `missing-memory` -- `intentional-divergence` -- `experience-gap` -- `check-candidate` +When fingerprint memory is silent, local evidence can still support advisory +critique. Label those findings as provisional and non-Ghost-backed, and ground +them in nearby product surfaces, local components, token or copy conventions, +accepted decisions, or human intent. Ask the human before judging high-risk, +irreversible, privacy/security/legal, or product-identity-defining choices. -Humans promote durable memory into `fingerprint.yml` or `checks.yml`. +Memory changes are ordinary Git-reviewed edits to `fingerprint.yml`, +`checks.yml`, and optional rationale files when present. Do not silently rewrite +memory during a review unless the user asks to update memory. diff --git a/packages/ghost/src/skill-bundle/references/schema.md b/packages/ghost/src/skill-bundle/references/schema.md index d9405876..161a3801 100644 --- a/packages/ghost/src/skill-bundle/references/schema.md +++ b/packages/ghost/src/skill-bundle/references/schema.md @@ -1,18 +1,38 @@ # Root Fingerprint Bundle Schema Reference -Canonical package: +Core package: ```text .ghost/ fingerprint.yml ghost.fingerprint/v1 - checks.yml optional ghost.checks/v1 gates - intent.md optional human intent - decisions/ optional ghost.decision/v1 rationale - proposals/ optional ghost.proposal/v1 candidates - cache/ optional generated caches + checks.yml optional ghost.checks/v1 gates ``` -Nested packages use the same shape at any product-area root, for example +Optional files: + +```text +.ghost/ + intent.md optional human intent + decisions/ optional ghost.decision/v1 rationale + cache/ optional generated caches +``` + +Git is the approval boundary: checked-in `fingerprint.yml` is canonical memory, +and uncommitted or unmerged edits are draft work. Ghost validates memory and +runs gates; it is not a lifecycle manager, proposal system, or design-system +registry. + +`fingerprint.yml` may start sparse: + +```yaml +schema: ghost.fingerprint/v1 +``` + +Top-level sections are optional on disk and default internally to empty +`summary`, `topology`, memory arrays, `exemplars`, and +`implementation_vocabulary`. + +Advanced nested packages use the same shape at any product-area root, for example `apps/checkout/.ghost/`. Resolve the stack with `ghost stack <path>`; child entries with the same `id` override parent entries. @@ -37,27 +57,29 @@ situations: product_obligation: Keep status, owner, and recovery actions visible. principles: - id: density-supports-comparison - status: accepted principle: Dense work surfaces prioritize scanning and comparison. experience_contracts: - id: destructive-actions-disclose-consequence - status: accepted contract: Destructive actions disclose consequence and recovery path. patterns: - id: resource-index-stays-tabular - status: accepted kind: composition pattern: Resource index views stay tabular when comparison is the task. evidence: - path: src/orders/index.tsx +exemplars: + - id: orders-index + path: src/orders/index.tsx + title: Orders index + surface_type: resource-index + scope: orders + why: Shows the dense tabular precedent for order triage. + refs: [pattern:resource-index-stays-tabular] implementation_vocabulary: tokens: [color.background, color.text] components: [DataTable] notes: - Current vocabulary is replaceable implementation material. -review_policy: - proposal_policy: - - Agents recommend or create thresholded proposals; humans promote durable truth. ``` ## `checks.yml` @@ -94,6 +116,9 @@ Detector types remain deterministic only: - `banned-component` - `required-token` +Checks keep `status: active | proposed | disabled` because enforcement still +needs lifecycle state. Fingerprint entries do not have status fields. + ## `decisions/*.yml` ```yaml @@ -114,26 +139,6 @@ evidence: decided_at: "2026-05-17T00:00:00.000Z" ``` -## `proposals/*.yml` - -```yaml -schema: ghost.proposal/v1 -id: saved-payment-empty-state -status: open -kind: missing-memory -title: Saved payment empty state should teach recovery -claim: Empty states for saved payment methods should prioritize recovery over education. -rationale: The user is blocked from paying, not browsing product concepts. -scope: - roles: [design, pm, qa] - surface_types: [empty-state] -evidence: - - path: apps/payments/empty-state.tsx -proposed_action: - target: fingerprint - summary: Promote into fingerprint.yml if repeated. -``` - ## Validation ```bash @@ -146,5 +151,6 @@ ghost check --base main `lint` validates artifact shape. `verify` validates cross-artifact fidelity: fingerprint evidence paths resolve and checks reference known fingerprint -memory. Optional decisions/proposals are linted when present. `ghost check` is -the deterministic pass/fail gate. +memory. Exemplar paths are verified as generation anchors. Optional decisions +are linted when present. `ghost check` is the +deterministic pass/fail gate. diff --git a/packages/ghost/src/skill-bundle/references/survey.md b/packages/ghost/src/skill-bundle/references/survey.md index 2ab30dce..4a0c4107 100644 --- a/packages/ghost/src/skill-bundle/references/survey.md +++ b/packages/ghost/src/skill-bundle/references/survey.md @@ -5,7 +5,7 @@ handoffs: - label: Author fingerprint patterns skill: patterns prompt: Interpret observed facts into .ghost/fingerprint.yml patterns - - label: Capture fingerprint memory + - label: Update fingerprint memory skill: capture prompt: Use observed facts to update .ghost/fingerprint.yml --- @@ -40,9 +40,8 @@ Skip it when: - Keep generated output under `.ghost/cache/` unless a legacy command requires `.ghost/survey.json`. - Promote only useful, durable conclusions into `fingerprint.yml`. -- If observation is incomplete, say so. Recommend a proposal only when the gap - is durable enough to help future generation or review; otherwise leave it as - local uncertainty. +- If observation is incomplete, say so and leave the gap as local uncertainty + until the user asks to edit memory. ## Optional Legacy Helpers diff --git a/packages/ghost/src/skill-bundle/references/verify.md b/packages/ghost/src/skill-bundle/references/verify.md index 618cafaf..7297f8f5 100644 --- a/packages/ghost/src/skill-bundle/references/verify.md +++ b/packages/ghost/src/skill-bundle/references/verify.md @@ -1,42 +1,26 @@ --- name: verify -description: Confirm generated UI stays within the resolved Ghost memory stack; iterate if not. -handoffs: - - label: Remediate deterministic or advisory findings - skill: remediate - prompt: Given the verify findings, suggest minimal code changes that close the drift +description: Verify generated UI or memory against Ghost. --- -# Recipe: Verify Generated UI +# Recipe: Verify Ghost Work -**Goal:** run the generate -> check -> review -> repair loop against `.ghost/`. +1. Run `ghost lint .ghost` and `ghost verify .ghost --root <target>` after + memory changes. +2. Run `ghost check --base <ref>` after implementation changes. +3. For advisory review, run `ghost review --base <ref> --include-memory`. +4. For generation setup, run `ghost emit context-bundle` and inspect the + product prose, optional inventory, exemplars, and active checks. +5. Inspect generated UI manually or with screenshots when visual fidelity + matters. -## Steps +Report: -1. Generate the UI from a brief grounded in the resolved memory stack: - merged `fingerprint.yml`, merged checks, open proposals, nearest examples, - and human context. -2. Run the deterministic gate: +- Active-check failures and repairs. +- Advisory product-experience drift with citations. +- Missing or unreachable evidence and exemplar paths. +- Provisional local reasoning where fingerprint memory is silent. +- Any memory updates the user requested. - ```bash - ghost check --base <ref> - ``` - -3. Repair any active check failures. -4. Run advisory review: - - ```bash - ghost review --base <ref> - ``` - -5. Repair high-confidence advisory issues when they cite a diff location, - fingerprint memory, and a concrete repair. -6. If the review exposes missing or contradictory memory, apply the Proposal - Threshold before taking memory action. Recommend a proposal candidate only - when the gap is repeated, high-impact, explicitly human-stated, - intentionally divergent, likely to recur, or blocks confident future review. - Create it with `ghost proposal create --path <path>` only when the user - explicitly asks to capture memory. - -Only active `checks.yml` failures block. Advisory findings guide judgment and -may become proposals when they reveal durable memory gaps. +Memory edits should be validated before handoff. Implementation-only work does +not need memory edits unless the user asks for them. diff --git a/packages/ghost/test/checks-grounding.test.ts b/packages/ghost/test/checks-grounding.test.ts index fb5221d5..51cf73ff 100644 --- a/packages/ghost/test/checks-grounding.test.ts +++ b/packages/ghost/test/checks-grounding.test.ts @@ -238,15 +238,14 @@ function fingerprintDocument( principles: [ { id: "dense-workflows-prioritize-scanning", - status: "accepted", principle: "Dense workflows optimize for comparison, speed, and recovery.", }, ], experience_contracts: [], patterns: [], + exemplars: [], implementation_vocabulary: {}, - review_policy: {}, ...overrides, }; } diff --git a/packages/ghost/test/cli.test.ts b/packages/ghost/test/cli.test.ts index 608ad5c3..8017f9e1 100644 --- a/packages/ghost/test/cli.test.ts +++ b/packages/ghost/test/cli.test.ts @@ -129,6 +129,7 @@ describe("ghost CLI", () => { expect(result.code).toBe(0); expect(result.stdout).toContain("ghost"); expect(result.stdout).toContain("skill"); + expect(result.stdout).not.toContain("proposal <op>"); }); it("compares explicitly supplied fingerprint files", async () => { @@ -227,11 +228,82 @@ describe("ghost CLI", () => { expect(manifest.dimensions.typography.reason).toBe("editorial"); }); + it("initializes the default memory skeleton without cache", async () => { + const init = await runCli(["init", "--format", "json"], dir); + const scan = await runCli(["scan", "--format", "json"], dir); + await writeFile( + join(dir, "change.patch"), + lendingPatch("let color = CashTheme.primary"), + ); + + expect(init.code).toBe(0); + const initOutput = JSON.parse(init.stdout); + expect(Object.keys(initOutput).sort()).toEqual([ + "checks", + "dir", + "fingerprintYml", + ]); + await expect( + readFile(join(dir, ".ghost", "fingerprint.yml"), "utf-8"), + ).resolves.toBe("schema: ghost.fingerprint/v1\n"); + await expect( + readFile(join(dir, ".ghost", "checks.yml"), "utf-8"), + ).resolves.toContain("schema: ghost.checks/v1"); + const status = JSON.parse(scan.stdout); + expect(status.cache.state).toBe("missing"); + + const lint = await runCli(["lint"], dir); + const verify = await runCli(["verify", ".ghost", "--root", "."], dir); + const check = await runCli(["check", "--diff", "change.patch"], dir); + const review = await runCli(["review", "--diff", "change.patch"], dir); + const reviewCommand = await runCli(["emit", "review-command"], dir); + const contextBundle = await runCli(["emit", "context-bundle"], dir); + + expect(lint.code).toBe(0); + expect(verify.code).toBe(0); + expect(check.code).toBe(0); + expect(review.code).toBe(0); + expect(review.stdout).toContain("schema: ghost.fingerprint/v1"); + expect(reviewCommand.code).toBe(0); + expect(contextBundle.code).toBe(0); + }); + + it("rejects checks grounded in omitted sparse fingerprint memory", async () => { + await runCli(["init"], dir); + await writeFile( + join(dir, ".ghost", "checks.yml"), + `schema: ghost.checks/v1 +id: local +checks: + - id: missing-memory-check + title: Missing memory check + status: active + severity: serious + derives_from: principle:not-recorded + detector: + type: forbidden-regex + pattern: '#[0-9a-fA-F]{3,8}' +`, + ); + + const lint = await runCli(["lint", ".ghost", "--format", "json"], dir); + + expect(lint.code).toBe(1); + const report = JSON.parse(lint.stdout); + expect(report.issues[0]).toMatchObject({ + rule: "check-grounding-unknown", + path: "checks.yml.checks[0].derives_from", + }); + }); + it("initializes a bundle and reports fingerprint capture state as json", async () => { const init = await runCli(["init", "--with-intent"], dir); const scan = await runCli(["scan", "--format", "json"], dir); expect(init.code).toBe(0); + expect(init.stdout).toContain("fingerprint.yml:"); + expect(init.stdout).toContain("checks.yml:"); + expect(init.stdout).not.toContain("cache/:"); expect( await readFile(join(dir, ".ghost", "fingerprint.yml"), "utf-8"), ).toContain("schema: ghost.fingerprint/v1"); @@ -241,8 +313,8 @@ describe("ghost CLI", () => { expect(scan.code).toBe(0); const status = JSON.parse(scan.stdout); expect(status.fingerprint.state).toBe("present"); - expect(status.proposals.state).toBe("present"); - expect(status.cache.state).toBe("present"); + expect(status.proposals).toBeUndefined(); + expect(status.cache.state).toBe("missing"); expect(status.readiness.state).toBe("memory-empty"); }); @@ -278,6 +350,7 @@ describe("ghost CLI", () => { expect(init.code).toBe(0); const initOutput = JSON.parse(init.stdout); + expect(initOutput.cache).toBeUndefined(); expect(await realpath(initOutput.config)).toBe( await realpath(join(dir, ".ghost", "config.yml")), ); @@ -285,11 +358,11 @@ describe("ghost CLI", () => { const fingerprint = parseYaml( await readFile(join(dir, ".ghost", "fingerprint.yml"), "utf-8"), ) as Record<string, unknown>; - expect(fingerprint.situations).toEqual([]); - expect(fingerprint.principles).toEqual([]); - expect(fingerprint.experience_contracts).toEqual([]); - expect(fingerprint.patterns).toEqual([]); - expect(fingerprint.implementation_vocabulary).toMatchObject({ + expect(fingerprint).not.toHaveProperty("situations"); + expect(fingerprint).not.toHaveProperty("principles"); + expect(fingerprint).not.toHaveProperty("experience_contracts"); + expect(fingerprint).not.toHaveProperty("patterns"); + expect(fingerprint.implementation_vocabulary).toEqual({ libraries: ["ghost-ui"], }); @@ -351,7 +424,7 @@ describe("ghost CLI", () => { const status = JSON.parse(scan.stdout); expect(status.fingerprint.state).toBe("present"); expect(status.checks.state).toBe("present"); - expect(status.proposals.state).toBe("present"); + expect(status.proposals).toBeUndefined(); expect(status.cache.state).toBe("present"); expect(status.readiness.state).toBe("implementation-only"); expect(status.readiness.reasons[0]).toContain("implementation vocabulary"); @@ -441,6 +514,24 @@ describe("ghost CLI", () => { it("emits review commands and context bundles from the unified cli", async () => { await writeCheckPackage(dir); + await mkdir(join(dir, ".ghost", "cache"), { recursive: true }); + await writeFile( + join(dir, ".ghost", "cache", "inventory.json"), + JSON.stringify( + { + root: dir, + platform_hints: ["ios"], + build_system_hints: ["spm"], + language_histogram: [{ name: "swift", files: 12 }], + package_manifests: ["Package.swift"], + candidate_config_files: ["Code/Theme.swift"], + registry_files: [], + top_level_tree: [{ path: "Code/", kind: "dir", child_count: 3 }], + }, + null, + 2, + ), + ); await writeFile( join(dir, ".ghost", "fingerprint.md"), fingerprintWithId("local"), @@ -456,11 +547,11 @@ describe("ghost CLI", () => { "utf-8", ); expect(emittedReviewCommand).toContain("fingerprint.yml memory"); + expect(emittedReviewCommand).toContain("Exemplars"); + expect(emittedReviewCommand).toContain("lending-tokenized-screen"); expect(emittedReviewCommand).toContain("provisional and non-Ghost-backed"); - expect(emittedReviewCommand).toContain("Proposal Threshold"); - expect(emittedReviewCommand).toContain( - "Memory action: none | recommend-proposal | create-proposal", - ); + expect(emittedReviewCommand).not.toContain("Proposal Threshold"); + expect(emittedReviewCommand).not.toContain("recommend-proposal"); expect(emittedReviewCommand).toContain("experience-gap"); expect(emittedReviewCommand).toContain("no-hardcoded-ui-color"); expect(emittedReviewCommand).not.toContain( @@ -475,16 +566,67 @@ describe("ghost CLI", () => { ).resolves.toContain("schema: ghost.fingerprint/v1"); await expect( readFile(join(dir, "ghost-context", "prompt.md"), "utf-8"), - ).resolves.toContain("Fingerprint Memory"); + ).resolves.toContain("Product Prose"); + await expect( + readFile(join(dir, "ghost-context", "prompt.md"), "utf-8"), + ).resolves.toContain("Inventory cache"); + await expect( + readFile(join(dir, "ghost-context", "prompt.md"), "utf-8"), + ).resolves.toContain("Package.swift"); + await expect( + readFile(join(dir, "ghost-context", "prompt.md"), "utf-8"), + ).resolves.toContain("Exemplars"); + await expect( + readFile(join(dir, "ghost-context", "prompt.md"), "utf-8"), + ).resolves.toContain("no-hardcoded-ui-color"); + await expect( + readFile(join(dir, "ghost-context", "prompt.md"), "utf-8"), + ).resolves.not.toContain("candidate-density-check"); + await expect( + readFile(join(dir, "ghost-context", "prompt.md"), "utf-8"), + ).resolves.not.toContain("status: proposed"); await expect( readFile(join(dir, "ghost-context", "prompt.md"), "utf-8"), - ).resolves.toContain("Proposal Threshold"); + ).resolves.not.toContain("Proposal Threshold"); await expect( readFile(join(dir, "ghost-context", "prompt.md"), "utf-8"), ).resolves.toContain("Label silent-memory reasoning as provisional"); await expect( readFile(join(dir, "ghost-context", "SKILL.md"), "utf-8"), - ).resolves.toContain("provisional and non-Ghost-backed"); + ).resolves.toContain("provisional and\nnon-Ghost-backed"); + }); + + it("emits context bundles when inventory cache is malformed", async () => { + await writeCheckPackage(dir); + await mkdir(join(dir, ".ghost", "cache"), { recursive: true }); + await writeFile(join(dir, ".ghost", "cache", "inventory.json"), "{nope"); + + const contextBundle = await runCli(["emit", "context-bundle"], dir); + + expect(contextBundle.code).toBe(0); + await expect( + readFile(join(dir, "ghost-context", "prompt.md"), "utf-8"), + ).resolves.toContain("could not be read"); + }); + + it("warns when fingerprint exemplar paths are unreachable", async () => { + await writeCheckPackage(dir); + + const verify = await runCli( + ["verify", ".ghost", "--root", ".", "--format", "json"], + dir, + ); + + expect(verify.code).toBe(0); + const report = JSON.parse(verify.stdout); + expect(report.issues).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + rule: "fingerprint-exemplar-unreachable", + path: "fingerprint.yml.exemplars[0].path", + }), + ]), + ); }); it("rejects removed legacy direct markdown emit flags", () => { @@ -515,7 +657,6 @@ describe("ghost CLI", () => { for (const path of [ "SKILL.md", "references/capture.md", - "references/propose.md", "references/review.md", "references/remediate.md", "references/brief.md", @@ -534,18 +675,16 @@ describe("ghost CLI", () => { ); await expect( readFile( - join(dir, "skills", "ghost", "references", "propose.md"), + join(dir, "skills", "ghost", "references", "review.md"), "utf-8", ), - ).resolves.toContain("Proposal Threshold"); + ).resolves.toContain("memory is silent"); await expect( readFile( - join(dir, "skills", "ghost", "references", "review.md"), + join(dir, "skills", "ghost", "references", "propose.md"), "utf-8", ), - ).resolves.toContain( - "Memory action: none | recommend-proposal | create-proposal", - ); + ).rejects.toThrow(); }); it("check fails when an active deterministic check matches added lines", async () => { @@ -610,11 +749,9 @@ describe("ghost CLI", () => { expect(result.stdout).toContain("diff location"); expect(result.stdout).toContain("fingerprint.yml memory"); expect(result.stdout).toContain("active check when blocking"); - expect(result.stdout).toContain("Proposal Threshold"); + expect(result.stdout).not.toContain("Proposal Threshold"); expect(result.stdout).toContain("provisional and non-Ghost-backed"); - expect(result.stdout).toContain( - "Memory action: none | recommend-proposal | create-proposal", - ); + expect(result.stdout).not.toContain("recommend-proposal"); expect(result.stdout).toContain("missing-memory"); expect(result.stdout).toContain("experience-gap"); expect(result.stdout).toContain("repair or intentional-divergence"); @@ -671,8 +808,8 @@ libraries: const packet = JSON.parse(result.stdout); expect(packet.fingerprint.schema).toBe("ghost.fingerprint/v1"); expect(packet.finding_categories).toContain("experience-gap"); - expect(packet.proposal_types).toContain("check-candidate"); - expect(packet.open_proposals).toEqual([]); + expect(packet.proposal_types).toBeUndefined(); + expect(packet.open_proposals).toBeUndefined(); expect(packet.memory).toBeUndefined(); }); @@ -885,9 +1022,8 @@ libraries: "utf-8", ); expect(fingerprint).toContain("ghost.fingerprint/v1"); - expect(fingerprint).toContain( - "Agents recommend or create thresholded proposals", - ); + expect(fingerprint).not.toContain("review_policy"); + expect(fingerprint).not.toContain("proposal"); }); it("init --scope creates a nested bundle under a custom memory directory", async () => { @@ -917,155 +1053,6 @@ libraries: ).toContain("ghost.fingerprint/v1"); }); - it("proposal create/list/resolve targets the nearest scoped bundle", async () => { - await writeNestedCheckPackage(dir); - - const create = await runCli( - [ - "proposal", - "create", - "--path", - "apps/checkout/review/page.tsx", - "--id", - "checkout-copy-memory", - "--kind", - "missing-memory", - "--title", - "Checkout copy memory", - "--claim", - "Checkout review copy needs local memory.", - "--rationale", - "The checkout surface has specific payment review language.", - "--target", - "fingerprint", - "--summary", - "Add checkout copy guidance.", - "--evidence", - "apps/checkout/review/page.tsx", - "--format", - "json", - ], - dir, - ); - - expect(create.code).toBe(0); - const created = JSON.parse(create.stdout); - expect(await realpath(created.package_dir)).toBe( - await realpath(join(dir, "apps", "checkout", ".ghost")), - ); - expect(created.proposal.evidence[0].path).toBe("review/page.tsx"); - - const list = await runCli( - [ - "proposal", - "list", - "--path", - "apps/checkout/review/page.tsx", - "--format", - "json", - ], - dir, - ); - expect(JSON.parse(list.stdout).open_proposals[0].id).toBe( - "checkout-copy-memory", - ); - - const resolveResult = await runCli( - [ - "proposal", - "resolve", - "checkout-copy-memory", - "--path", - "apps/checkout/review/page.tsx", - "--status", - "accepted", - "--format", - "json", - ], - dir, - ); - expect(resolveResult.code).toBe(0); - expect(JSON.parse(resolveResult.stdout).proposal.status).toBe("accepted"); - }); - - it("proposal create/list/resolve supports custom memory directories", async () => { - await writeNestedCheckPackage(dir, ".design/memory"); - - const create = await runCli( - [ - "proposal", - "create", - "--path", - "apps/checkout/review/page.tsx", - "--memory-dir", - ".design/memory", - "--id", - "checkout-custom-memory", - "--kind", - "missing-memory", - "--title", - "Checkout custom memory", - "--claim", - "Checkout has wrapper-specific memory.", - "--rationale", - "The host wrapper stores memory outside .ghost.", - "--summary", - "Add wrapper-specific checkout guidance.", - "--evidence", - "apps/checkout/review/page.tsx", - "--format", - "json", - ], - dir, - ); - - expect(create.code).toBe(0); - const created = JSON.parse(create.stdout); - expect(await realpath(created.package_dir)).toBe( - await realpath(join(dir, "apps", "checkout", ".design", "memory")), - ); - expect(created).toMatchObject({ - path: expect.stringContaining("checkout-custom-memory.yml"), - proposal: { id: "checkout-custom-memory", status: "open" }, - }); - - const list = await runCli( - [ - "proposal", - "list", - "--path", - "apps/checkout/review/page.tsx", - "--memory-dir", - ".design/memory", - "--format", - "json", - ], - dir, - ); - const listed = JSON.parse(list.stdout); - expect(listed.memory_dir).toBe(".design/memory"); - expect(listed.open_proposals[0].id).toBe("checkout-custom-memory"); - - const resolveResult = await runCli( - [ - "proposal", - "resolve", - "checkout-custom-memory", - "--path", - "apps/checkout/review/page.tsx", - "--memory-dir", - ".design/memory", - "--status", - "accepted", - "--format", - "json", - ], - dir, - ); - expect(resolveResult.code).toBe(0); - expect(JSON.parse(resolveResult.stdout).proposal.status).toBe("accepted"); - }); - it("lint --all and verify --all include nested bundles", async () => { await writeNestedCheckPackage(dir); @@ -1130,20 +1117,25 @@ topology: situations: [] principles: - id: tokenized-ui-color - status: accepted principle: UI colors should come from the product token system. check_refs: [check:no-hardcoded-ui-color] experience_contracts: [] patterns: - id: tokenized-ui-color - status: accepted kind: visual pattern: Product UI color uses semantic tokens instead of literals. check_refs: [check:no-hardcoded-ui-color] +exemplars: + - id: lending-tokenized-screen + path: Code/Features/Lending/LendingUI + title: Lending tokenized UI + surface_type: native-feature + scope: lending + why: Shows semantic CashTheme color usage for native lending UI. + refs: [principle:tokenized-ui-color, pattern:tokenized-ui-color] implementation_vocabulary: tokens: [CashTheme.primary] components: [] -review_policy: {} `, ); await writeFile( @@ -1199,6 +1191,22 @@ checks: examples: - Code/Features/Lending/LendingUI repair: Replace literals with Arcade/Cash semantic tokens. + - id: candidate-density-check + title: Candidate density check + status: proposed + severity: nit + derives_from: principle:tokenized-ui-color + applies_to: + scopes: [lending] + paths: [Code/Features/Lending] + detector: + type: required-regex + pattern: 'CashTheme' + evidence: + support: 0.5 + observed_count: 1 + examples: + - Code/Features/Lending/LendingUI `, ); } @@ -1206,7 +1214,6 @@ checks: async function writeMemoryFiles(dir: string): Promise<void> { const pkg = join(dir, ".ghost"); await mkdir(join(pkg, "decisions"), { recursive: true }); - await mkdir(join(pkg, "proposals"), { recursive: true }); await writeFile( join(pkg, "decisions", "checkout-reversibility.yml"), `schema: ghost.decision/v1 @@ -1237,22 +1244,6 @@ rationale: Rejected decisions are non-canonical memory. evidence: - note: Rejected in design review. decided_at: "2026-05-17T00:00:00.000Z" -`, - ); - await writeFile( - join(pkg, "proposals", "saved-payment-empty-state.yml"), - `schema: ghost.proposal/v1 -id: saved-payment-empty-state -status: accepted -kind: missing-memory -title: Saved payment empty state should teach recovery -claim: Empty states for saved payment methods should prioritize recovery. -rationale: The user is blocked from paying, not browsing product concepts. -evidence: - - path: apps/payments/empty-state.tsx -proposed_action: - target: fingerprint - summary: Promote into fingerprint.yml if repeated. `, ); } @@ -1266,11 +1257,7 @@ async function writeNestedCheckPackage( join(dir, "apps", "checkout"), memoryDir, ); - await mkdir(join(rootMemory, "proposals"), { recursive: true }); await mkdir(join(rootMemory, "cache"), { recursive: true }); - await mkdir(join(checkoutMemory, "proposals"), { - recursive: true, - }); await mkdir(join(checkoutMemory, "cache"), { recursive: true, }); @@ -1294,12 +1281,10 @@ principles: [] experience_contracts: [] patterns: - id: root-token-pattern - status: accepted kind: visual pattern: Web UI color uses semantic product tokens. implementation_vocabulary: tokens: [RootTheme] -review_policy: {} `, ); await writeFile( @@ -1342,14 +1327,12 @@ principles: [] experience_contracts: [] patterns: - id: checkout-token-pattern - status: accepted kind: visual pattern: Checkout review uses checkout product tokens. applies_to: paths: [review] implementation_vocabulary: tokens: [CheckoutTheme] -review_policy: {} `, ); await writeFile( diff --git a/packages/ghost/test/fingerprint-yml-schema.test.ts b/packages/ghost/test/fingerprint-yml-schema.test.ts index 6e3068e3..af7d1eb5 100644 --- a/packages/ghost/test/fingerprint-yml-schema.test.ts +++ b/packages/ghost/test/fingerprint-yml-schema.test.ts @@ -10,6 +10,18 @@ describe("ghost.fingerprint/v1", () => { const result = GhostFingerprintSchema.safeParse(minimalFingerprint()); expect(result.success).toBe(true); + if (!result.success) throw new Error("minimal fingerprint should parse"); + expect(result.data).toEqual({ + schema: GHOST_FINGERPRINT_SCHEMA, + summary: {}, + topology: {}, + situations: [], + principles: [], + experience_contracts: [], + patterns: [], + exemplars: [], + implementation_vocabulary: {}, + }); }); it("accepts a full OSS-friendly fingerprint.yml document", () => { @@ -28,6 +40,20 @@ describe("ghost.fingerprint/v1", () => { expect(result.success).toBe(false); }); + it("rejects old topology examples", () => { + const input = fullFingerprint(); + (input.topology as Record<string, unknown>).examples = [ + { + path: "apps/dashboard/src/routes/orders/page.tsx", + surface_type: "dense-dashboard", + }, + ]; + + const result = GhostFingerprintSchema.safeParse(input); + + expect(result.success).toBe(false); + }); + it("rejects implementation vocabulary as a typed ref target", () => { const input = fullFingerprint(); input.situations[0].patterns = [ @@ -39,6 +65,20 @@ describe("ghost.fingerprint/v1", () => { expect(result.success).toBe(false); }); + it("rejects legacy status fields in canonical fingerprint.yml entries", () => { + const principle = fullFingerprint(); + principle.principles[0].status = "accepted" as never; + expect(GhostFingerprintSchema.safeParse(principle).success).toBe(false); + + const contract = fullFingerprint(); + contract.experience_contracts[0].status = "accepted" as never; + expect(GhostFingerprintSchema.safeParse(contract).success).toBe(false); + + const pattern = fullFingerprint(); + pattern.patterns[0].status = "accepted" as never; + expect(GhostFingerprintSchema.safeParse(pattern).success).toBe(false); + }); + it("reports unknown typed refs inside the fingerprint", () => { const input = fullFingerprint(); input.situations[0].principles = ["principle:missing-principle"]; @@ -96,6 +136,8 @@ describe("ghost.fingerprint/v1", () => { it("reports unknown topology scope and surface type references", () => { const input = fullFingerprint(); input.situations[0].surface_type = "unknown-surface"; + input.exemplars[0].scope = "unknown-scope"; + input.exemplars[0].surface_type = "unknown-surface"; input.principles[0].applies_to = { scopes: ["unknown-scope"], surface_types: ["unknown-surface"], @@ -104,7 +146,7 @@ describe("ghost.fingerprint/v1", () => { const report = lintGhostFingerprint(input); - expect(report.errors).toBe(4); + expect(report.errors).toBe(6); expect(report.issues).toEqual( expect.arrayContaining([ expect.objectContaining({ @@ -123,10 +165,31 @@ describe("ghost.fingerprint/v1", () => { rule: "fingerprint-situation-unknown", path: "principles[0].applies_to.situations[0]", }), + expect.objectContaining({ + rule: "fingerprint-scope-unknown", + path: "exemplars[0].scope", + }), + expect.objectContaining({ + rule: "fingerprint-surface-type-unknown", + path: "exemplars[0].surface_type", + }), ]), ); }); + it("reports unknown exemplar refs", () => { + const input = fullFingerprint(); + input.exemplars[0].refs = ["pattern:missing-pattern"]; + + const report = lintGhostFingerprint(input); + + expect(report.errors).toBe(1); + expect(report.issues[0]).toMatchObject({ + rule: "fingerprint-ref-unknown", + path: "exemplars[0].refs[0]", + }); + }); + it("requires check refs to use check:*", () => { const input = fullFingerprint(); input.principles[0].check_refs = ["pattern:compact-filter-toolbar"]; @@ -144,14 +207,6 @@ describe("ghost.fingerprint/v1", () => { function minimalFingerprint() { return { schema: GHOST_FINGERPRINT_SCHEMA, - summary: {}, - topology: {}, - situations: [], - principles: [], - experience_contracts: [], - patterns: [], - implementation_vocabulary: {}, - review_policy: {}, }; } @@ -175,14 +230,22 @@ function fullFingerprint() { }, ], surface_types: ["dense-dashboard", "docs"], - examples: [ - { - path: "apps/dashboard/src/routes/orders/page.tsx", - surface_type: "dense-dashboard", - note: "Order review table", - }, - ], }, + exemplars: [ + { + id: "orders-table", + path: "apps/dashboard/src/routes/orders/page.tsx", + title: "Order review table", + surface_type: "dense-dashboard", + scope: "dashboard", + note: "Dense filtering and comparison surface.", + why: "Shows the compact hierarchy future dashboard work should preserve.", + refs: [ + "principle:dense-workflows-prioritize-scanning", + "pattern:compact-filter-toolbar", + ], + }, + ], situations: [ { id: "user-is-filtering-an-operations-table", @@ -204,7 +267,6 @@ function fullFingerprint() { principles: [ { id: "dense-workflows-prioritize-scanning", - status: "accepted", principle: "Dense operational workflows should optimize for comparison, speed, and recovery before visual novelty.", applies_to: { @@ -226,7 +288,6 @@ function fullFingerprint() { experience_contracts: [ { id: "destructive-actions-require-clear-confirmation", - status: "accepted", contract: "Destructive actions need explicit confirmation and a clear recovery path.", obligations: ["confirm intent", "explain consequence"], @@ -235,7 +296,6 @@ function fullFingerprint() { patterns: [ { id: "compact-filter-toolbar", - status: "accepted", kind: "composition", pattern: "Filters stay visually attached to the table they affect.", guidance: ["keep primary filters before secondary actions"], @@ -248,10 +308,5 @@ function fullFingerprint() { assets: ["status icons"], notes: ["current vocabulary is replaceable implementation material"], }, - review_policy: { - proposal_policy: ["agents may propose but not promote memory"], - experience_gap_categories: ["missing-memory", "unclear-intent"], - memory_gap_policy: ["continue conservatively and propose durable memory"], - }, }; } diff --git a/packages/ghost/test/memory-stack.test.ts b/packages/ghost/test/memory-stack.test.ts index e6877101..3e35c189 100644 --- a/packages/ghost/test/memory-stack.test.ts +++ b/packages/ghost/test/memory-stack.test.ts @@ -66,6 +66,16 @@ describe("nested Ghost memory stacks", () => { (pattern) => pattern.id === "child-pattern", )?.evidence?.[0], ).toMatchObject({ path: "apps/checkout/review/page.tsx" }); + expect( + stack.merged.fingerprint.exemplars.find( + (exemplar) => exemplar.id === "shared-exemplar", + ), + ).toMatchObject({ + title: "Child review exemplar", + path: "apps/checkout/review/page.tsx", + scope: "checkout", + surface_type: "payment-review", + }); expect( stack.merged.checks.checks.find( (check) => check.id === "no-hardcoded-color", @@ -76,11 +86,6 @@ describe("nested Ghost memory stacks", () => { (decision) => decision.id === "shared-decision", )?.status, ).toBe("rejected"); - expect( - stack.merged.proposals.find( - (proposal) => proposal.id === "shared-proposal", - )?.claim, - ).toBe("Checkout has its own proposal."); }); it("groups changed files by resolved memory stack", async () => { @@ -97,6 +102,49 @@ describe("nested Ghost memory stacks", () => { ]); }); + it("merges sparse parent and child fingerprints with normalized defaults", async () => { + await mkdir(join(dir, ".ghost"), { recursive: true }); + await writeFile( + join(dir, ".ghost", "fingerprint.yml"), + "schema: ghost.fingerprint/v1\n", + ); + await mkdir(join(dir, "apps", "checkout", ".ghost"), { recursive: true }); + await writeFile( + join(dir, "apps", "checkout", ".ghost", "fingerprint.yml"), + `schema: ghost.fingerprint/v1 +summary: + product: Checkout +principles: + - id: checkout-review-stays-reversible + principle: Checkout review keeps reversal visible before payment. +`, + ); + + const stack = await loadMemoryStackForPath( + "apps/checkout/review/page.tsx", + dir, + ); + + expect(stack.layers).toHaveLength(2); + expect(stack.merged.fingerprint.summary.product).toBe("Checkout"); + expect(stack.merged.fingerprint.topology).toEqual({ + scopes: [], + surface_types: undefined, + }); + expect(stack.merged.fingerprint.situations).toEqual([]); + expect(stack.merged.fingerprint.principles).toHaveLength(1); + expect(stack.merged.fingerprint.experience_contracts).toEqual([]); + expect(stack.merged.fingerprint.patterns).toEqual([]); + expect(stack.merged.fingerprint.exemplars).toEqual([]); + expect(stack.merged.fingerprint.implementation_vocabulary).toEqual({ + tokens: undefined, + components: undefined, + libraries: undefined, + assets: undefined, + notes: undefined, + }); + }); + it("resolves root-to-leaf layers from a custom memory directory", async () => { await writeStackFixture(dir, ".design/memory"); @@ -141,7 +189,6 @@ async function writeRootBundle( ): Promise<void> { const ghost = memoryPackagePath(dir, memoryDir); await mkdir(join(ghost, "decisions"), { recursive: true }); - await mkdir(join(ghost, "proposals"), { recursive: true }); await writeFile( join(ghost, "fingerprint.yml"), `schema: ghost.fingerprint/v1 @@ -159,21 +206,24 @@ situations: product_obligation: preserve broad product continuity principles: - id: shared-principle - status: accepted principle: Parent product memory. experience_contracts: [] patterns: - id: root-pattern - status: accepted kind: visual pattern: Root pattern. - id: child-pattern - status: accepted kind: visual pattern: Parent version of child pattern. +exemplars: + - id: shared-exemplar + path: apps/root.tsx + title: Parent exemplar + surface_type: app-shell + scope: app + refs: [pattern:root-pattern] implementation_vocabulary: tokens: [RootTheme.color] -review_policy: {} `, ); await writeFile( @@ -210,22 +260,6 @@ rationale: Parent rationale. evidence: - note: Parent evidence. decided_at: "2026-05-17T00:00:00.000Z" -`, - ); - await writeFile( - join(ghost, "proposals", "shared-proposal.yml"), - `schema: ghost.proposal/v1 -id: shared-proposal -status: open -kind: missing-memory -title: Parent proposal -claim: Parent proposal. -rationale: Parent rationale. -evidence: - - note: Parent evidence. -proposed_action: - target: fingerprint - summary: Parent action. `, ); } @@ -236,7 +270,6 @@ async function writeChildBundle( ): Promise<void> { const ghost = memoryPackagePath(root, memoryDir); await mkdir(join(ghost, "decisions"), { recursive: true }); - await mkdir(join(ghost, "proposals"), { recursive: true }); await mkdir(join(root, "review"), { recursive: true }); await writeFile( join(ghost, "fingerprint.yml"), @@ -256,23 +289,27 @@ situations: surface_type: payment-review principles: - id: shared-principle - status: accepted principle: Checkout review must make reversal obvious. applies_to: paths: [review] experience_contracts: [] patterns: - id: child-pattern - status: accepted kind: behavioral pattern: Checkout keeps review controls visible. applies_to: paths: [review] evidence: - path: review/page.tsx +exemplars: + - id: shared-exemplar + path: review/page.tsx + title: Child review exemplar + surface_type: payment-review + scope: checkout + refs: [pattern:child-pattern] implementation_vocabulary: tokens: [CheckoutTheme.action] -review_policy: {} `, ); await writeFile( @@ -316,22 +353,6 @@ rationale: Child rationale. evidence: - path: review/page.tsx decided_at: "2026-05-18T00:00:00.000Z" -`, - ); - await writeFile( - join(ghost, "proposals", "shared-proposal.yml"), - `schema: ghost.proposal/v1 -id: shared-proposal -status: open -kind: experience-gap -title: Child proposal -claim: Checkout has its own proposal. -rationale: Child rationale. -evidence: - - path: review/page.tsx -proposed_action: - target: fingerprint - summary: Child action. `, ); } diff --git a/packages/ghost/test/scan-status.test.ts b/packages/ghost/test/scan-status.test.ts index bc4adda8..49056e9e 100644 --- a/packages/ghost/test/scan-status.test.ts +++ b/packages/ghost/test/scan-status.test.ts @@ -36,24 +36,36 @@ describe("scanStatus readiness", () => { const status = await scanStatus(dir); expect(status.fingerprint.state).toBe("present"); + expect(status.cache.state).toBe("missing"); expect(status.recommended_next).toBeNull(); expect(status.readiness.state).toBe("memory-empty"); expect(status.readiness.cannot_review).toContain("product identity"); }); - it("reports ready memory when fingerprint.yml has accepted experience entries", async () => { + it("reports ready memory when fingerprint.yml has experience entries", async () => { await writeFile( join(dir, "fingerprint.yml"), fingerprintFile(` principles: - id: dense-workflows-prioritize-scanning - status: accepted principle: Dense workflows optimize for comparison and recovery. +topology: + scopes: + - id: dashboard + paths: [apps/dashboard] + surface_types: [dense-dashboard] + surface_types: [dense-dashboard] patterns: - id: preserve-table-density - status: accepted kind: composition pattern: Keep dense operational tables scannable. +exemplars: + - id: orders-table + path: apps/dashboard/orders.tsx + surface_type: dense-dashboard + scope: dashboard + refs: + - pattern:preserve-table-density implementation_vocabulary: tokens: - color.background @@ -64,9 +76,11 @@ implementation_vocabulary: const status = await scanStatus(dir); + expect(status.cache.state).toBe("missing"); expect(status.readiness.state).toBe("memory-ready"); expect(status.readiness.implementation_vocabulary_rows.tokens).toBe(1); expect(status.readiness.implementation_vocabulary_rows.components).toBe(1); + expect(status.readiness.product_surface_count).toBe(1); expect(status.readiness.can_review).toContain("surface behavior"); }); @@ -95,21 +109,7 @@ implementation_vocabulary: function fingerprintFile(overrides = ""): string { if (overrides.trim()) { return `schema: ghost.fingerprint/v1 -summary: {} -topology: {} -situations: [] -experience_contracts: [] -review_policy: {} ${overrides}`; } - return `schema: ghost.fingerprint/v1 -summary: {} -topology: {} -situations: [] -principles: [] -experience_contracts: [] -patterns: [] -implementation_vocabulary: {} -review_policy: {} -${overrides}`; + return "schema: ghost.fingerprint/v1\n"; } diff --git a/packages/ghost/test/skill-bundle-manifest.test.ts b/packages/ghost/test/skill-bundle-manifest.test.ts new file mode 100644 index 00000000..e9ab0c57 --- /dev/null +++ b/packages/ghost/test/skill-bundle-manifest.test.ts @@ -0,0 +1,51 @@ +import { readdir, readFile } from "node:fs/promises"; +import { dirname, relative, resolve, sep } from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; + +const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../../.."); + +interface InstallManifest { + source?: { + package?: string; + }; + files?: string[]; +} + +describe("install manifest", () => { + it("lists exactly the files in the Ghost skill bundle", async () => { + const manifestPath = resolve(REPO_ROOT, "install", "manifest.json"); + const manifest = JSON.parse( + await readFile(manifestPath, "utf-8"), + ) as InstallManifest; + const packagePath = + manifest.source?.package ?? "packages/ghost/src/skill-bundle"; + const bundleRoot = resolve(REPO_ROOT, packagePath); + const actual = await listFiles(bundleRoot); + const listed = [...(manifest.files ?? [])].sort(); + + expect(listed).toEqual(actual); + }); +}); + +async function listFiles(root: string): Promise<string[]> { + const files: string[] = []; + + async function walk(dir: string): Promise<void> { + const entries = await readdir(dir, { withFileTypes: true }); + await Promise.all( + entries.map(async (entry) => { + const absolute = resolve(dir, entry.name); + if (entry.isDirectory()) { + await walk(absolute); + return; + } + if (!entry.isFile()) return; + files.push(relative(root, absolute).replaceAll(sep, "/")); + }), + ); + } + + await walk(root); + return files.sort(); +} diff --git a/scripts/check-install-bundle.mjs b/scripts/check-install-bundle.mjs new file mode 100644 index 00000000..f3c82138 --- /dev/null +++ b/scripts/check-install-bundle.mjs @@ -0,0 +1,99 @@ +#!/usr/bin/env node +import { execFileSync } from "node:child_process"; +import { + mkdtempSync, + readdirSync, + readFileSync, + rmSync, + statSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { join, relative, resolve } from "node:path"; +import { pathToFileURL } from "node:url"; + +const ROOT = process.cwd(); +const MANIFEST_PATH = resolve(ROOT, "install/manifest.json"); +const INSTALL_SCRIPT = resolve(ROOT, "install/install.sh"); +const EXPECTED_PACKAGE = "packages/ghost/src/skill-bundle"; +const BUNDLE_ROOT = resolve(ROOT, EXPECTED_PACKAGE); + +function fail(message) { + console.error(`check-install-bundle failed: ${message}`); + process.exit(1); +} + +function listFiles(dir, root = dir) { + const out = []; + for (const name of readdirSync(dir)) { + const full = join(dir, name); + const stat = statSync(full); + if (stat.isDirectory()) { + out.push(...listFiles(full, root)); + } else if (stat.isFile()) { + out.push(relative(root, full).replaceAll("\\", "/")); + } + } + return out; +} + +function skillBundleOrder(files) { + return files.sort((a, b) => { + if (a === "SKILL.md") return -1; + if (b === "SKILL.md") return 1; + return a.localeCompare(b); + }); +} + +function assertSameList(label, actual, expected) { + const actualJson = JSON.stringify(actual, null, 2); + const expectedJson = JSON.stringify(expected, null, 2); + if (actualJson === expectedJson) return; + fail( + `${label} mismatch.\nExpected:\n${expectedJson}\nActual:\n${actualJson}`, + ); +} + +const manifest = JSON.parse(readFileSync(MANIFEST_PATH, "utf8")); + +if (manifest?.source?.package !== EXPECTED_PACKAGE) { + fail( + `manifest source.package must be '${EXPECTED_PACKAGE}', got '${manifest?.source?.package}'`, + ); +} + +if (!Array.isArray(manifest.files) || manifest.files.length === 0) { + fail("manifest files must be a non-empty array"); +} + +const expectedFiles = skillBundleOrder(listFiles(BUNDLE_ROOT)); +assertSameList("manifest files", manifest.files, expectedFiles); + +const tmpRoot = mkdtempSync(join(tmpdir(), "ghost-install-bundle-")); +const dest = join(tmpRoot, "ghost"); + +try { + execFileSync( + "sh", + [ + INSTALL_SCRIPT, + "--source", + pathToFileURL(ROOT).href, + "--dest", + dest, + "--force", + "--agent", + "codex", + ], + { + cwd: ROOT, + stdio: "pipe", + }, + ); + + const installedFiles = skillBundleOrder(listFiles(dest)); + assertSameList("installed files", installedFiles, manifest.files); +} finally { + rmSync(tmpRoot, { recursive: true, force: true }); +} + +console.log(`check-install-bundle: ${manifest.files.length} files OK`);