diff --git a/.changeset/gather-vocab.md b/.changeset/gather-vocab.md new file mode 100644 index 00000000..fef30ad9 --- /dev/null +++ b/.changeset/gather-vocab.md @@ -0,0 +1,9 @@ +--- +"@anarchitecture/ghost": major +--- + +Rename the `gather` slice vocabulary to plain language. The JSON `slice.spokes` +field is now `slice.pointers`, and the pointer `kind` `"edge-hub"` is now +`"related"` with its origin in a `from` field (was `hub`). Docs and the skill +bundle drop the spine/corridor/hub-and-spoke metaphors; `edge` provenance is +unchanged. diff --git a/apps/docs/src/content/docs/cli-reference.mdx b/apps/docs/src/content/docs/cli-reference.mdx index 34f82af8..324abdf5 100644 --- a/apps/docs/src/content/docs/cli-reference.mdx +++ b/apps/docs/src/content/docs/cli-reference.mdx @@ -130,9 +130,9 @@ A slice composes three things: surface's own folder. A sibling folder's nodes never appear. - **edges** (full bodies, one hop): the `relates` targets of every node on that path, so a rule authored once high in the tree reaches every descendant. A - link to a node also offers that node's subtree as spokes. -- **spokes** (pointers): the surface's own descendants and the subtree of any - node it relates to, offered as `id` + `description` for the agent to pull on + link to a node also offers that node's subtree as pointers. +- **pointers**: the surface's own descendants and the subtree of any node it + relates to, offered as `id` + `description` for the agent to pull on demand. Use `--as` to filter full-body nodes to a single incarnation; untagged essence diff --git a/apps/docs/src/content/docs/getting-started.mdx b/apps/docs/src/content/docs/getting-started.mdx index f62968a2..39d0b420 100644 --- a/apps/docs/src/content/docs/getting-started.mdx +++ b/apps/docs/src/content/docs/getting-started.mdx @@ -134,8 +134,8 @@ ghost gather marketing --format json ``` `gather` returns full bodies for every node from the root down to the surface, -the **edges** reachable in one hop from any of their `relates`, and **spokes**, -pointers to nearby nodes the agent can pull on demand. Run it before generation, +the **edges** reachable in one hop from any of their `relates`, and **pointers** +to nearby nodes the agent can pull on demand. Run it before generation, so the agent builds with surface composition in hand rather than discovering the gaps in review. diff --git a/docs/purposes.md b/docs/purposes.md index 5d20eda5..49d69484 100644 --- a/docs/purposes.md +++ b/docs/purposes.md @@ -49,7 +49,7 @@ Two resolution mechanisms, both read-only: | Consumer | CLI surface | Projection it needs | Reads | Changes the model? | | --- | --- | --- | --- | --- | | **Authoring** | `init`, `scan`, `signals`, `validate`, `migrate` | The raw nodes plus repo signals, for a human or agent writing the fingerprint. | the node graph, raw signals | **No**, this *is* the model. | -| **Generation** | `gather` | A narrow, task-scoped *slice* delivered before building: full bodies along the surface's path, one-hop edges, and spoke pointers. | the composed `gather` slice | **No** if selection stays a read-only narrowing pass. **Leak risk:** if retrieval needs are pushed back into the tree shape. | +| **Generation** | `gather` | A narrow, task-scoped *slice* delivered before building: full bodies along the surface's path, one-hop edges, and pointers. | the composed `gather` slice | **No** if selection stays a read-only narrowing pass. **Leak risk:** if retrieval needs are pushed back into the tree shape. | | **Governance** | `checks`, `review` | Every check offered, grounded in the touched surfaces' slice, evaluated against a diff. | offered checks plus the grounding slice | **No** if checks stay offered-and-grounded. **Leak risk:** making checks filter or route by surface instead of binding to prose via `source:`. | | **Fleet** | (`ghost-fleet`, private) | Many bundles at once: distances, cohorts, tracks-graph. | many fingerprints, read-only | **No**, consumes workspace exports read-only. | | **Discovery / pathless** | `gather ` | A ranked set of candidate nodes when there is no exact surface to name. | `description` payloads, ranked | **Leak risk:** inventing a routing model in the data instead of ranking on `description` and letting the agent pick. | diff --git a/packages/ghost/src/commands/gather-command.ts b/packages/ghost/src/commands/gather-command.ts index 948150c7..733b614c 100644 --- a/packages/ghost/src/commands/gather-command.ts +++ b/packages/ghost/src/commands/gather-command.ts @@ -67,7 +67,7 @@ export function registerGatherCommand(cli: CAC): void { // An inexact query (not an exact node id): rank the closest nodes // rather than dumping the whole menu. This is `gather`'s search front - // end — the same act as picking from the menu, done intelligently. + // end, the same act as picking from the menu, done intelligently. if (!known.has(surface)) { const matches = searchGraph(surface, loaded.graph); if (opts.format === "json") { @@ -131,12 +131,12 @@ function formatCandidatesMarkdown(query: string, matches: SearchHit[]): string { return `${lines.join("\n")}\n`; } lines.push( - `\`${query}\` is not a node id. Closest matches — run \`ghost gather \`:`, + `\`${query}\` is not a node id. Closest matches (run \`ghost gather \`):`, "", ); for (const hit of matches) { const kind = hit.surface ? "surface" : "node"; - lines.push(`- \`${hit.id}\` (${kind}) — ${matchLabel(hit)}`); + lines.push(`- \`${hit.id}\` (${kind}): ${matchLabel(hit)}`); if (hit.description) lines.push(` - ${hit.description}`); } return `${lines.join("\n")}\n`; @@ -207,26 +207,27 @@ function formatSliceMarkdown(slice: GraphSlice): string { const tag = node.incarnation ? ` _(as ${node.incarnation})_` : ""; lines.push( "", - `### \`${node.id}\` — ${provenanceLabel(node.provenance)}${tag}`, + `### \`${node.id}\` (${provenanceLabel(node.provenance)})${tag}`, "", node.body, ); } } - // Spokes: pointers the agent may pull on demand (descendants + edge hubs). - if (slice.spokes.length > 0) { + // Pointers the agent may pull on demand (descendants + related subtrees). + if (slice.pointers.length > 0) { lines.push( "", "## Available to pull", "", - "Pointers to nearby context — run `ghost gather ` to expand.", + "Pointers to nearby context. Run `ghost gather ` to expand.", "", ); - for (const spoke of slice.spokes) { - const via = spoke.kind === "edge-hub" ? ` _(via \`${spoke.hub}\`)_` : ""; - lines.push(`- \`${spoke.id}\`${via}`); - if (spoke.description) lines.push(` - ${spoke.description}`); + for (const pointer of slice.pointers) { + const via = + pointer.kind === "related" ? ` _(via \`${pointer.from}\`)_` : ""; + lines.push(`- \`${pointer.id}\`${via}`); + if (pointer.description) lines.push(` - ${pointer.description}`); } } diff --git a/packages/ghost/src/ghost-core/graph/slice.ts b/packages/ghost/src/ghost-core/graph/slice.ts index 60be1caa..da575454 100644 --- a/packages/ghost/src/ghost-core/graph/slice.ts +++ b/packages/ghost/src/ghost-core/graph/slice.ts @@ -5,8 +5,8 @@ import { GHOST_GRAPH_ROOT_ID, type GhostGraph } from "./types.js"; /** * Why a full-body node is present in a resolved slice. * - `own`: a file in the requested surface's own folder. - * - `ancestor`: a file in a folder higher on the corridor (root → surface). - * - `edge`: contributed by a typed `relates` link from a spine node (one hop). + * - `ancestor`: a file in a folder higher on the path (root → surface). + * - `edge`: contributed by a typed `relates` link from a node on the path (one hop). */ export type GraphSliceProvenance = | { kind: "own" } @@ -21,21 +21,20 @@ export interface GraphSliceNode { } /** - * A spoke: a node offered as a pointer (id + description, no body) for the agent - * to pull on demand. Spokes are navigable optionality, never authoritative - * context. + * A pointer: a node offered as id + description (no body) for the agent to pull + * on demand. Pointers are navigable optionality, never authoritative context. * - `descendant`: a node within or below the requested surface's own subtree. - * - `edge-hub`: a node within or below an `edge` target's subtree (a hub the - * surface `relates` to unfolds its menu). + * - `related`: a node within or below an `edge` target's subtree (a node the + * surface `relates` to offers its subtree as pointers). */ -export type GraphSpokeKind = "descendant" | "edge-hub"; +export type GraphPointerKind = "descendant" | "related"; export interface GraphSlicePointer { id: string; description?: string; - kind: GraphSpokeKind; - /** For an `edge-hub` spoke, the hub id it belongs to. */ - hub?: string; + kind: GraphPointerKind; + /** For a `related` pointer, the `relates` target whose subtree it belongs to. */ + from?: string; } export interface GraphSlice { @@ -45,10 +44,10 @@ export interface GraphSlice { ancestors: string[]; /** The `--as` incarnation filter applied, if any. */ incarnation?: string; - /** Full-body context: the corridor spine plus one-hop `relates` edges. */ + /** Full-body context: every node on the path plus one-hop `relates` edges. */ nodes: GraphSliceNode[]; - /** Pointers (id + description) the agent may pull: descendants + edge hubs. */ - spokes: GraphSlicePointer[]; + /** Pointers (id + description) the agent may pull: descendants + related subtrees. */ + pointers: GraphSlicePointer[]; } export interface ResolveGraphSliceOptions { @@ -57,23 +56,21 @@ export interface ResolveGraphSliceOptions { } /** - * Compose a context slice for a surface by traversing the graph — deterministic, - * no I/O, no LLM. The model is "folders are walls; files fill the corridor": + * Compose a context slice for a surface by traversing the graph. Deterministic, + * no I/O, no LLM. * - * - **spine** (`own`/`ancestor`): every node whose **file folder** is on the - * surface's corridor — the chain of folders from the package root down to the - * surface's own folder. A sibling folder is a wall: its nodes never appear. - * Spine nodes are full bodies. - * - **edge**: for each spine node's `relates`, the target node's body is + * - **path nodes** (`own`/`ancestor`): every node whose **file folder** is on + * the path from the package root down to the surface's own folder. A sibling + * folder's nodes never appear. Path nodes are full bodies. + * - **edge**: for each path node's `relates`, the target node's body is * included once (one hop, no recursion), tagged by the relation qualifier. - * This is how a broad rule placed high in the corridor (e.g. "all feature UI + * This is how a broad rule placed high in the tree (e.g. "all feature UI * draws on Arcade", authored once on `features`) reaches every descendant. - * - **spokes**: descendants of the surface's own folder, plus descendants of - * each edge target (a hub the surface draws on unfolds its menu), as pointers. + * - **pointers**: descendants of the surface's own folder, plus descendants of + * each edge target (a related node offers its subtree), as id + description. * * The `incarnation` option filters full-body nodes: essence (untagged) always - * passes; a tagged node passes only when it matches. Spokes are unfiltered - * pointers. + * passes; a tagged node passes only when it matches. Pointers are unfiltered. */ export function resolveGraphSlice( graph: GhostGraph, @@ -84,10 +81,10 @@ export function resolveGraphSlice( const ancestors = ancestorsFull.filter((id) => id !== GHOST_GRAPH_ROOT_ID); const surfaceNode = graph.nodes.get(surfaceId); - // The surface's own file folder anchors the corridor. For a bare tree - // position (a directory with no index node) the folder is the id itself. + // The surface's own file folder anchors the path. For a bare tree position + // (a directory with no index node) the folder is the id itself. const surfaceFolder = surfaceNode?.folder ?? surfaceId; - const corridor = corridorFolders(surfaceFolder); + const pathFolders = foldersOnPath(surfaceFolder); const passesIncarnation = (incarnation?: string): boolean => { if (options.incarnation === undefined) return true; @@ -102,7 +99,7 @@ export function resolveGraphSlice( ? { incarnation: options.incarnation } : {}), nodes: [], - spokes: [], + pointers: [], }; const seenBody = new Set(); @@ -123,11 +120,11 @@ export function resolveGraphSlice( return true; }; - // Spine: every node whose file folder is on the corridor. `own` when the + // Path nodes: every node whose file folder is on the path. `own` when the // node sits in the surface's own folder; `ancestor` when higher up. for (const node of graph.nodes.values()) { if (node.origin === "inherited") continue; - if (!corridor.has(node.folder)) continue; + if (!pathFolders.has(node.folder)) continue; const provenance: GraphSliceProvenance = node.folder === surfaceFolder ? { kind: "own" } @@ -135,11 +132,11 @@ export function resolveGraphSlice( addBody(node.id, provenance); } - // Edges: one hop along `relates` from every spine node. The target's body is - // included; if the target is a hub, its subtree is offered as spokes. - const spineIds = slice.nodes.map((n) => n.id); + // Edges: one hop along `relates` from every path node. The target's body is + // included; the target's subtree is then offered as pointers. + const pathNodeIds = slice.nodes.map((n) => n.id); const edgeTargets: string[] = []; - for (const sourceId of spineIds) { + for (const sourceId of pathNodeIds) { const source = graph.nodes.get(sourceId); if (!source) continue; for (const relation of source.relates) { @@ -152,35 +149,35 @@ export function resolveGraphSlice( } } - // Spokes: descendants of the surface, plus descendants of each edge hub. - const seenSpoke = new Set(seenBody); - const addSpoke = (id: string, kind: GraphSpokeKind, hub?: string) => { - if (seenSpoke.has(id)) return; + // Pointers: descendants of the surface, plus descendants of each edge target. + const seenPointer = new Set(seenBody); + const addPointer = (id: string, kind: GraphPointerKind, from?: string) => { + if (seenPointer.has(id)) return; const node = graph.nodes.get(id); if (!node) return; - seenSpoke.add(id); - slice.spokes.push({ + seenPointer.add(id); + slice.pointers.push({ id: node.id, ...(node.description !== undefined ? { description: node.description } : {}), kind, - ...(hub !== undefined ? { hub } : {}), + ...(from !== undefined ? { from } : {}), }); }; for (const node of graph.nodes.values()) { if (node.origin === "inherited") continue; if (isWithinOrBelow(node.folder, surfaceFolder)) { - addSpoke(node.id, "descendant"); + addPointer(node.id, "descendant"); } } - for (const hubId of edgeTargets) { - const hub = graph.nodes.get(hubId); - if (!hub) continue; + for (const targetId of edgeTargets) { + const target = graph.nodes.get(targetId); + if (!target) continue; for (const node of graph.nodes.values()) { - if (isWithinOrBelow(node.folder, hub.folder)) { - addSpoke(node.id, "edge-hub", hubId); + if (isWithinOrBelow(node.folder, target.folder)) { + addPointer(node.id, "related", targetId); } } } @@ -188,8 +185,8 @@ export function resolveGraphSlice( return slice; } -/** The set of folders on the corridor from the package root down to `folder`. */ -function corridorFolders(folder: string): Set { +/** The set of folders on the path from the package root down to `folder`. */ +function foldersOnPath(folder: string): Set { const set = new Set([""]); // root files reach everywhere let current = folder; while (current !== "") { diff --git a/packages/ghost/src/ghost-core/graph/types.ts b/packages/ghost/src/ghost-core/graph/types.ts index d59e24bb..85532771 100644 --- a/packages/ghost/src/ghost-core/graph/types.ts +++ b/packages/ghost/src/ghost-core/graph/types.ts @@ -29,13 +29,13 @@ export interface GhostGraphNode { /** The containing directory's id; absent ⇒ this node is the `core` root. */ parent?: string; /** - * The node's **file folder** — the directory the source file physically sits - * in, which is the unit of containment for corridor composition. This differs - * from `parent` for index nodes: `features/bitcoin/index.md` has folder + * The node's **file folder**: the directory the source file physically sits + * in, the unit of containment for slice composition. This differs from + * `parent` for index nodes: `features/bitcoin/index.md` has folder * `features/bitcoin` but parent `features`. A plain leaf shares its parent's * value (`features/bitcoin/buy.md` → folder `features/bitcoin`). The root - * `core` node has folder `""`. Folders are walls: a node only cascades into - * surfaces whose folder chain includes this folder. + * `core` node has folder `""`. A node only cascades into surfaces whose + * folder path includes this folder. */ folder: string; relates: GhostNodeRelation[]; @@ -47,7 +47,7 @@ export interface GhostGraphNode { /** * The in-memory fingerprint graph: prose nodes indexed by id, plus the * containment tree (parent edges from the directory layout, root = `core`) that - * is the traversal spine. This is the shape later phases (gather, checks, + * is traversed. This is the shape later phases (gather, checks, * review) traverse; the directory layout is just one serialization of it. */ export interface GhostGraph { diff --git a/packages/ghost/src/ghost-core/node/schema.ts b/packages/ghost/src/ghost-core/node/schema.ts index e23a7357..53cc3746 100644 --- a/packages/ghost/src/ghost-core/node/schema.ts +++ b/packages/ghost/src/ghost-core/node/schema.ts @@ -3,7 +3,7 @@ import { GHOST_NODE_RELATION_KINDS } from "./types.js"; /** * A node id is its path within the package, `.md` dropped (`marketing/email`). - * The directory tree is the containment spine: the containing directory is the + * The directory tree is the containment graph: the containing directory is the * parent, so the id *does* encode hierarchy by design. A segment is a permissive * lowercase slug (alphanumeric plus `.` `_` `-`); segments join with `/`. No * leading, trailing, or doubled slash. Ids are computed by the loader from the diff --git a/packages/ghost/src/scan/fingerprint-package-layers.ts b/packages/ghost/src/scan/fingerprint-package-layers.ts index 97cbe98c..e193eb3c 100644 --- a/packages/ghost/src/scan/fingerprint-package-layers.ts +++ b/packages/ghost/src/scan/fingerprint-package-layers.ts @@ -100,7 +100,7 @@ async function loadInheritedNodes( ? { description: node.description } : {}), // Inherited nodes carry a package-qualified folder so they never sit on - // a local corridor (folders are walls per package); they enter a slice + // a local path (folders are scoped per package); they enter a slice // only via an explicit cross-package `relates` edge. folder: `${id}:${node.folder}`, relates: [], diff --git a/packages/ghost/src/scan/node-tree.ts b/packages/ghost/src/scan/node-tree.ts index 2a312ae7..f41090b1 100644 --- a/packages/ghost/src/scan/node-tree.ts +++ b/packages/ghost/src/scan/node-tree.ts @@ -105,8 +105,8 @@ async function walk( /** * Compute a node's id, parent, and file folder from its package-relative path. - * The folder is the directory the file sits in — the unit of corridor - * containment, which differs from `parent` for index nodes. + * The folder is the directory the file sits in, the unit of containment for + * slice composition, which differs from `parent` for index nodes. * - `index.md` → id `core`, parent absent, folder ``. * - `a/index.md` → id `a`, parent `core`, folder `a`. * - `a/b/index.md` → id `a/b`, parent `a`, folder `a/b`. diff --git a/packages/ghost/src/scan/templates.ts b/packages/ghost/src/scan/templates.ts index 972210e2..288bf51a 100644 --- a/packages/ghost/src/scan/templates.ts +++ b/packages/ghost/src/scan/templates.ts @@ -29,8 +29,8 @@ function manifestFile(): TemplateFile { } /** - * The default starter: a manifest plus the package-root `index.md` — the `core` - * node whose prose cascades to every surface. The directory tree is the spine: + * The default starter: a manifest plus the package-root `index.md`, the `core` + * node whose prose cascades to every surface. The directory tree is the graph: * add a surface by adding a directory, give it prose with its own `index.md`, * and place nodes as `/.md`. */ diff --git a/packages/ghost/src/skill-bundle/SKILL.md b/packages/ghost/src/skill-bundle/SKILL.md index 1229608a..e7e1e364 100644 --- a/packages/ghost/src/skill-bundle/SKILL.md +++ b/packages/ghost/src/skill-bundle/SKILL.md @@ -60,7 +60,7 @@ node shape. path. Author a broad rule once at the level it is true (say `relates: { to: arcade }` on `features/`) and every descendant inherits it. A link to a node also offers that node's subtree as pointers. -- **spokes** (pointers: id + description): the surface's own descendants and the +- **pointers** (id + description, no body): the surface's own descendants and the subtree of any node it relates to. The agent reads the descriptions and pulls what it needs with a follow-up `gather`. @@ -105,7 +105,7 @@ ref). Inherited nodes are read-only and flow into gather/validate like local one | `ghost validate [file-or-dir]` | Validate the package: artifact shape and the node graph (links resolve, one root, acyclic). | | `ghost checks --surface ` | List the markdown checks and ground the named surfaces. | | `ghost review --surface [--diff ]` | Emit an advisory review packet: touched surfaces, the offered checks, and fingerprint grounding (diff embedded verbatim). | -| `ghost gather [node] [--as ]` | Compose a node's context slice (full bodies along its path + relates edges, plus spoke pointers), list the node menu, or rank the closest nodes for an inexact query. | +| `ghost gather [node] [--as ]` | Compose a node's context slice (full bodies along its path + relates edges, plus pointers), list the node menu, or rank the closest nodes for an inexact query. | | `ghost skill install` | Install this unified skill bundle. | ## Advanced CLI Verbs diff --git a/packages/ghost/src/skill-bundle/references/brief.md b/packages/ghost/src/skill-bundle/references/brief.md index c4d0d6ba..b3e31948 100644 --- a/packages/ghost/src/skill-bundle/references/brief.md +++ b/packages/ghost/src/skill-bundle/references/brief.md @@ -1,13 +1,19 @@ --- name: brief -description: Build a concise pre-generation brief from a surface's gather slice. +description: Build a pre-generation brief from a surface's gather slice — an instruction-plus-materials packet shaped as intent, inventory, and composition. --- # Recipe: Brief Work From Ghost Fingerprint +A brief turns a `gather` slice into a packet the generating agent can act on: +the materials (the grounded node prose) plus the instruction (how to read it), +organized through the three authoring lenses. The lenses are an output *view* +here, not fields or structure — synthesize across the whole slice; never chop +one node into three pieces. + 0. Before building, run the [self-check](self-check.md): if you cannot name the nodes you gathered, label each claim as Ghost-backed or provisional, and - point to where the fingerprint is silent, you are not grounded yet. Gather + point to where the fingerprint is silent, you are not grounded yet — gather first. 1. Match the ask to a surface in the menu (`ghost gather --format json` with no surface lists the surfaces and their descriptions), then run @@ -15,16 +21,15 @@ description: Build a concise pre-generation brief from a surface's gather slice. 2. Treat the gather slice as the agent contract: `surface`, `ancestors`, and the prose `nodes`, each with `provenance` (own, inherited from an ancestor, or contributed by a typed `relates` edge). The intent, the material, and the - composition live in each node's prose. + composition live in that node prose — surface them, do not add to them. 3. Add `--as ` (e.g. email, voice) to filter the slice to one output form; essence (untagged) nodes always pass. -4. Run `ghost signals ` when raw repo observations would help you find - evidence. -5. Run `ghost checks --surface ` (the surfaces you determined the change +4. Run `ghost checks --surface ` (the surfaces you determined the change touches) to ground them and see the offered checks, so generation avoids known failures. -6. When the slice is sparse, label local reasoning provisional rather than - inventing surface-specific rules. +5. When the slice is sparse, label local reasoning provisional rather than + inventing surface-specific rules. An empty section is a valid result: write + "the fingerprint is silent here" instead of manufacturing one. Plain `ghost gather ` is a compact human preview. Prefer `--format json` as the agent interface. @@ -37,9 +42,26 @@ When no surface is selected (or an unknown one is named), `gather` returns the surface menu, never the whole tree; choose a surface from it rather than guessing. -Return a short human-facing brief synthesized from the slice: the relevant -grounded nodes (their prose carries the why and what good looks like), checks to -avoid, and provisional assumptions when the surface is silent. +## The packet + +Return one short packet. Every claim is tagged: cite the node id for +Ghost-backed lines; mark the rest provisional. The packet is ephemeral working +context, never written back into `.ghost/`. + +- **Grounded in:** the node ids you pulled (`surface`, its `ancestors`, and any + `relates` edges), the incarnation if you filtered with `--as`, and the checks + `ghost checks` offered. +- **Intent** — the why and the stance the work must carry. +- **Inventory** — the concrete materials to build with, and pointers to the code + or inventory nodes the agent can inspect. Do not name components the + fingerprint never did. +- **Composition** — the patterns to hold so the surface feels intentional + (hierarchy, density, restraint, repetition, trust, flow). Fold in the offered + checks as constraints, but advisory taste is not a gate unless a check backs + it. +- **Silent / provisional:** where the slice does not cover the task and what + carries the reasoning there. -Fingerprint edits are ordinary Git-reviewed edits to the split fingerprint -package. +The lenses are this packet's output view — never reshape the slice into +intent/inventory/composition on disk or in `gather`. Fingerprint edits are +ordinary Git-reviewed edits to the split fingerprint package. diff --git a/packages/ghost/src/skill-bundle/references/schema.md b/packages/ghost/src/skill-bundle/references/schema.md index 2ee58107..8113879c 100644 --- a/packages/ghost/src/skill-bundle/references/schema.md +++ b/packages/ghost/src/skill-bundle/references/schema.md @@ -80,8 +80,8 @@ key), never by repo path. - **edges** (full bodies, one hop): the `relates` targets of every node on that path. A rule authored high in the tree (e.g. `relates: { to: arcade }` on `features/`) reaches every descendant. -- **spokes** (pointers): the node's own descendants and the subtree of any node - it relates to, offered as id + description for the agent to pull with a +- **pointers**: the node's own descendants and the subtree of any node it + relates to, offered as id + description for the agent to pull with a follow-up `gather`. With no argument, `gather` lists nodes by id + description for the agent to match diff --git a/packages/ghost/test/ghost-core/graph-slice.test.ts b/packages/ghost/test/ghost-core/graph-slice.test.ts index 3b1650e9..8af8e1b8 100644 --- a/packages/ghost/test/ghost-core/graph-slice.test.ts +++ b/packages/ghost/test/ghost-core/graph-slice.test.ts @@ -6,8 +6,8 @@ import { } from "../../src/ghost-core/index.js"; /** - * Model a node the way the loader does. `folder` is the file's directory — the - * unit of corridor containment: + * Model a node the way the loader does. `folder` is the file's directory, the + * unit of containment: * - a root file (`voice.md`) → parent `core`, folder ``. * - a directory index (`a/index.md`)→ parent of `a`, folder `a`. * - a leaf (`a/b.md`) → parent `a`, folder `a`. @@ -41,13 +41,13 @@ function provenanceOf(slice: ReturnType, id: string) { } const bodyIds = (slice: ReturnType) => slice.nodes.map((n) => n.id).sort(); -const spokeIds = (slice: ReturnType) => - slice.spokes.map((s) => s.id).sort(); +const pointerIds = (slice: ReturnType) => + slice.pointers.map((s) => s.id).sort(); -describe("resolveGraphSlice — corridor + hub-and-spoke", () => { - // A cash-ios-shaped fixture: globals at root, a design-system hub (arcade), - // and two walled feature subtrees. The `features` module declares the Arcade - // dependency once, for all its children. +describe("resolveGraphSlice: path nodes + related pointers", () => { + // A cash-ios-shaped fixture: globals at root, a design-system node (arcade), + // and two separate feature subtrees. The `features` module declares the + // Arcade dependency once, for all its children. function cashGraph() { return assembleGraph({ placedNodes: [ @@ -76,20 +76,20 @@ describe("resolveGraphSlice — corridor + hub-and-spoke", () => { }); } - it("1. a sibling folder is a wall — its nodes never leak in", () => { + it("1. a sibling folder never leaks in", () => { const slice = resolveGraphSlice( cashGraph(), "features/bitcoin/buy/confirm", ); const ids = bodyIds(slice); - // Walled-off siblings: other features, the design system, sibling sub-areas. + // Off-path siblings: other features, the design system, sibling sub-areas. expect(ids).not.toContain("features/banking"); expect(ids).not.toContain("features/banking/paychecks"); expect(ids).not.toContain("features/lending"); expect(ids).not.toContain("features/bitcoin/education"); - // And not even as spokes — a wall is total. - expect(spokeIds(slice)).not.toContain("features/banking"); - expect(spokeIds(slice)).not.toContain("features/lending"); + // And not even as pointers: the exclusion is total. + expect(pointerIds(slice)).not.toContain("features/banking"); + expect(pointerIds(slice)).not.toContain("features/lending"); }); it("2. an ancestor's relates propagates down to a deep leaf", () => { @@ -98,7 +98,7 @@ describe("resolveGraphSlice — corridor + hub-and-spoke", () => { "features/bitcoin/buy/confirm", ); // `features` declares the Arcade dependency; a screen 3 levels deeper - // inherits it via the corridor → edge path. + // inherits it via the path → edge route. expect(provenanceOf(slice, "arcade")).toEqual({ kind: "edge", via: "reinforces", @@ -106,32 +106,32 @@ describe("resolveGraphSlice — corridor + hub-and-spoke", () => { }); }); - it("3. an edge to a hub unfolds the hub's subtree as spokes", () => { + it("3. an edge to a node offers that node's subtree as pointers", () => { const slice = resolveGraphSlice( cashGraph(), "features/bitcoin/buy/confirm", ); - const hubSpokes = slice.spokes - .filter((s) => s.kind === "edge-hub") + const relatedPointers = slice.pointers + .filter((s) => s.kind === "related") .map((s) => s.id) .sort(); - expect(hubSpokes).toEqual([ + expect(relatedPointers).toEqual([ "arcade/color", "arcade/components", "arcade/components/button", "arcade/motion", ]); - // The hub body itself is a full-body edge node, not a spoke. - expect(spokeIds(slice)).not.toContain("arcade"); + // The related node's body itself is a full-body edge node, not a pointer. + expect(pointerIds(slice)).not.toContain("arcade"); }); - it("4. a loose file in a corridor folder cascades full-body (invariants)", () => { + it("4. a loose file in a path folder cascades full-body (invariants)", () => { const slice = resolveGraphSlice( cashGraph(), "features/bitcoin/buy/confirm", ); // `features/bitcoin/invariants` is a plain file in folder features/bitcoin, - // which is on the corridor — so it is inherited as a full body. + // which is on the path, so it is inherited as a full body. expect(provenanceOf(slice, "features/bitcoin/invariants")).toEqual({ kind: "ancestor", from: "features/bitcoin", @@ -147,9 +147,9 @@ describe("resolveGraphSlice — corridor + hub-and-spoke", () => { }); }); - it("5. descendants appear as spokes (pointers), not spine", () => { + it("5. descendants appear as pointers, not full bodies", () => { const slice = resolveGraphSlice(cashGraph(), "features/bitcoin"); - const descendants = slice.spokes + const descendants = slice.pointers .filter((s) => s.kind === "descendant") .map((s) => s.id) .sort(); @@ -161,8 +161,8 @@ describe("resolveGraphSlice — corridor + hub-and-spoke", () => { ]); // A descendant is a pointer, never a full body. expect(bodyIds(slice)).not.toContain("features/bitcoin/buy/confirm"); - // A descendant spoke carries its description for agent selection. - const buy = slice.spokes.find((s) => s.id === "features/bitcoin/buy"); + // A descendant pointer carries its description for agent selection. + const buy = slice.pointers.find((s) => s.id === "features/bitcoin/buy"); expect(buy?.kind).toBe("descendant"); });