diff --git a/.changeset/adapting-a-starter.md b/.changeset/adapting-a-starter.md new file mode 100644 index 00000000..362e661a --- /dev/null +++ b/.changeset/adapting-a-starter.md @@ -0,0 +1,5 @@ +--- +"@design-intelligence/ghost": minor +--- + +The skill bundle gains the `adapting-a-starter` recipe: the seven-step transplant procedure for turning an installed starter — a body like vessel-light or the naked skeleton — into your own brand's fingerprint, plus guidance for consuming an unadapted starter honestly. diff --git a/.changeset/median-headings-vessel-docs.md b/.changeset/median-headings-vessel-docs.md new file mode 100644 index 00000000..6067b73c --- /dev/null +++ b/.changeset/median-headings-vessel-docs.md @@ -0,0 +1,5 @@ +--- +"@design-intelligence/ghost": patch +--- + +Reframe the median as Ghost-stamped model truth and align Vessel's median check with heading anchors. diff --git a/.changeset/median-one-home.md b/.changeset/median-one-home.md new file mode 100644 index 00000000..388757b3 --- /dev/null +++ b/.changeset/median-one-home.md @@ -0,0 +1,5 @@ +--- +"@design-intelligence/ghost": minor +--- + +The median floor gets one authored home: `anti-goal.median` rules become markdown headings with per-rule check references, so `ghost validate` warns on every flag orphaned by pruning. All init templates (`skeleton`, `minimal`, `composition`) now stamp the median node; `ghost checks init` copies the paired check from the packaged payload and skips it when the node is absent. diff --git a/.changeset/review-locator-matching.md b/.changeset/review-locator-matching.md new file mode 100644 index 00000000..c966d0fe --- /dev/null +++ b/.changeset/review-locator-matching.md @@ -0,0 +1,5 @@ +--- +"@design-intelligence/ghost": patch +--- + +`ghost review` now resolves package-relative `materials/…` locators to their repo-relative form before matching diff paths, so packages living below the repo root offer their value checks correctly. diff --git a/apps/docs/src/content/docs/cli-reference.mdx b/apps/docs/src/content/docs/cli-reference.mdx index d24ed68d..26d17987 100644 --- a/apps/docs/src/content/docs/cli-reference.mdx +++ b/apps/docs/src/content/docs/cli-reference.mdx @@ -51,7 +51,7 @@ a finding (exit `1`). ### Initialize: `init` -Scaffold a `.ghost/` package with the skeleton starter: a manifest, a `glossary.md` declaring the grammar/signature/register/anti-goal kinds, a core `index.md`, six grammar nodes of value-free decision logic, four signature nodes as unanswered dials awaiting your brand's values, and `anti-goal.median` — the model's measured defaults as reject→replace rules, pruned per brand. Nothing in the skeleton is fiction: grammar and the median floor are safe to consume verbatim; the dials tell the agent to ask or flag, never freehand. Use `--template minimal` when you only want the small manifest/glossary/index starter. Add truths by adding files (`principle.density.md`). Use +Scaffold a `.ghost/` package. Every template and body includes `anti-goal.median`; the skeleton starter also includes a manifest, a `glossary.md` declaring the grammar/signature/register/anti-goal kinds, a core `index.md`, six grammar nodes of value-free decision logic, and four signature nodes as unanswered dials awaiting your brand's values. The median node is Ghost's measured knowledge of unsteered model behavior — model truth, not brand truth — stamped at init and owned (pruned, adapted) by you thereafter. Nothing in the skeleton is fiction: grammar and the median floor are safe to consume verbatim; the dials tell the agent to ask or flag, never freehand. Use `--template minimal` when you only want the small manifest/glossary/index starter. Add truths by adding files (`principle.density.md`). Use `--package ` for an exact directory, or set `GHOST_PACKAGE_DIR` when a host wrapper stores Ghost files outside the default `.ghost`. Core `init` scaffolds the fingerprint only; pass `--with checks` to also add the checks directory in diff --git a/apps/docs/src/content/docs/getting-started.mdx b/apps/docs/src/content/docs/getting-started.mdx index fa994988..3dd3cebe 100644 --- a/apps/docs/src/content/docs/getting-started.mdx +++ b/apps/docs/src/content/docs/getting-started.mdx @@ -80,7 +80,7 @@ by decision. One high-confidence truth beats an empty catalog. The CLI handles the deterministic package work. Your agent handles the authoring: interviewing you, working with the material you show it, drafting node prose, -and asking you to curate the claims. `ghost init` scaffolds the skeleton starter: `manifest.yml`, a `glossary.md` declaring the grammar/signature/register/anti-goal kinds, a core `index.md`, six grammar nodes of value-free decision logic, four signature nodes as unanswered dials, and `anti-goal.median` — the model's measured defaults as reject→replace rules headed by a prune instruction. Nothing in the skeleton is fiction: grammar and the median floor hold for any brand, and each dial tells the agent to ask you or flag the gap rather than freehand a value. Answering the dials is how the skeleton becomes your brand. Use `ghost init --template minimal` when you only want the small manifest/glossary/index starter. +and asking you to curate the claims. `ghost init` stamps `anti-goal.median` into every template and body; the median node is Ghost's measured knowledge of unsteered model behavior — model truth, not brand truth — owned (pruned, adapted) by you after init. The skeleton starter also includes `manifest.yml`, a `glossary.md` declaring the grammar/signature/register/anti-goal kinds, a core `index.md`, six grammar nodes of value-free decision logic, and four signature nodes as unanswered dials. Nothing in the skeleton is fiction: grammar and the median floor hold for any brand, and each dial tells the agent to ask you or flag the gap rather than freehand a value. Answering the dials is how the skeleton becomes your brand. Use `ghost init --template minimal` when you only want the small manifest/glossary/index starter. ```bash ghost init diff --git a/install/manifest.json b/install/manifest.json index 2595f730..951098a7 100644 --- a/install/manifest.json +++ b/install/manifest.json @@ -8,6 +8,7 @@ }, "files": [ "SKILL.md", + "references/adapting-a-starter.md", "references/authoring-scenarios.md", "references/blocks.md", "references/brief.md", diff --git a/packages/ghost/src/commands/checks-command.ts b/packages/ghost/src/commands/checks-command.ts index e1dd4b32..2fa22d7a 100644 --- a/packages/ghost/src/commands/checks-command.ts +++ b/packages/ghost/src/commands/checks-command.ts @@ -35,7 +35,11 @@ export function registerChecksCommand(cli: CAC): void { if (opts.format === "json") { process.stdout.write( `${JSON.stringify( - { dir: result.dir, written: result.written }, + { + dir: result.dir, + written: result.written, + skipped: result.skipped, + }, null, 2, )}\n`, @@ -45,6 +49,9 @@ export function registerChecksCommand(cli: CAC): void { for (const file of result.written) { process.stdout.write(` ${file}\n`); } + for (const file of result.skipped) { + process.stdout.write(` skipped ${file}\n`); + } } process.exit(0); } catch (err) { diff --git a/packages/ghost/src/commands/init-command.ts b/packages/ghost/src/commands/init-command.ts index f9a7c9cf..02946d3c 100644 --- a/packages/ghost/src/commands/init-command.ts +++ b/packages/ghost/src/commands/init-command.ts @@ -78,7 +78,12 @@ export function registerInitCommand(cli: CAC): void { dir: result.paths.dir, written: result.written, ...(addedChecks !== undefined - ? { checks: { written: addedChecks.written } } + ? { + checks: { + written: addedChecks.written, + skipped: addedChecks.skipped, + }, + } : {}), }, null, @@ -97,6 +102,9 @@ export function registerInitCommand(cli: CAC): void { for (const file of addedChecks.written) { process.stdout.write(` checks/${file}\n`); } + for (const file of addedChecks.skipped) { + process.stdout.write(` skipped checks/${file}\n`); + } } } process.exit(0); diff --git a/packages/ghost/src/commands/review-command.ts b/packages/ghost/src/commands/review-command.ts index eb314d3b..730e027c 100644 --- a/packages/ghost/src/commands/review-command.ts +++ b/packages/ghost/src/commands/review-command.ts @@ -53,6 +53,7 @@ export function registerReviewCommand(cli: CAC): void { diff: opts.diff, }); const packet = await buildReviewPacket(fingerprint, diffText, { + packageDir: paths.packageDir, runProbes: opts.probes !== false, cwd: process.cwd(), }); diff --git a/packages/ghost/src/init-payloads/skeleton/anti-goal.median.md b/packages/ghost/src/init-payloads/median/anti-goal.median.md similarity index 82% rename from packages/ghost/src/init-payloads/skeleton/anti-goal.median.md rename to packages/ghost/src/init-payloads/median/anti-goal.median.md index 4e1dadf6..a0a5cefb 100644 --- a/packages/ghost/src/init-payloads/skeleton/anti-goal.median.md +++ b/packages/ghost/src/init-payloads/median/anti-goal.median.md @@ -4,81 +4,82 @@ description: "The model's median defaults this fingerprint refuses — gather fo This is the model's median, not your brand. Each rule is reject→replace. Delete every line your brand legitimately violates — `ghost validate` will -surface any check the deletion orphans. +warn on any check reference the deletion orphans — delete the paired flag too. These are not aesthetic opinions. Where a count is given, it is the measured convergence of 300 unsteered generations across three frontier models (the antimedian experiment): the defaults a model reaches for when nobody hands it a brand. An output showing several of these tells reads as generated, whatever else it does right. A deterministic floor rides alongside these -rules in checks — contrast, reduced motion, sane z-index, no overshoot -easing — verified at review, never steered in prose. +rules in the paired check — mechanical tells like gradient-filled text, +emoji as interface icons, em-dash pileups, and aphoristic cadence — +verified at review, never steered in prose. - +### Hover-lift Reject hover-lift (`translateY` + growing shadow) as the default interaction → confirm with color and background change at the fingerprint's fast duration. (measured: 341) - +### Indigo accent Reject the indigo/blue/purple default accent (`#4f46e5`, `#2563eb`, `#8b5cf6` family; purple/violet hue 260–310) → the fingerprint's declared palette; absent one, monochrome plus a single functional accent. - +### Dark theme Reject unprompted dark theme → the fingerprint's declared surface. Dark-to-look-cool and light-to-be-safe are the same retreat from a decision. (measured: 271) - +### Gradients Reject gradient page and section backgrounds and gradient-filled CTAs → flat surfaces in semantic roles. - +### Side-stripe Reject side-stripe borders (a thick colored border on one side of a rounded card) — the single most recognizable tell of AI-generated UI → a full hairline border, a 4–8% surface tint, or a leading glyph. - +### Cream surface Reject the cream/sand/beige default surface (warm off-white; token names like `--cream`, `--sand`, `--parchment` are the tell) → a true off-white, the brand's own hue, or a committed color; warmth via accent and type, not the ground. - +### Glassmorphism Reject glassmorphism (decorative backdrop blur) → flat surfaces with real borders and the fingerprint's elevation tiers. - +### Chat bubbles Reject chat bubbles with initials-circle avatars for assistant turns → assistant text plain on the page surface; user turns marked compactly. - +### Stock copy Reject stock template copy ("Simple, transparent pricing") → headings that state what this product specifically does. - +### Celebration Reject celebration copy and UI (exclamation success, confetti language) → quiet factual confirmation. - +### Nested cards Reject nested cards and everything-in-cards → one surface level per region; interior hierarchy from spacing and type; spacing and alignment group without card overhead. - +### Hero metric Reject the hero-metric template (big number + small label + supporting stats as default proof) → evidence specific to the product, or nothing. A prominent metric showing real user data is fine. - +### Every button primary Reject every button a primary button → one primary per view; the rest step down the fingerprint's emphasis ladder. - +### Eyebrow kicker Reject an eyebrow kicker on every section → at most one, where the register sanctions it; one named kicker is voice, every-section is AI grammar. - +### Decorative motion Reject decorative looping animation (pulse/float/shimmer) and uniform fade-and-rise on every scrolled section → motion as evidence of state change; loops only for genuine loading; decoration never compensates for diff --git a/packages/ghost/src/init-payloads/median/median-tells.md b/packages/ghost/src/init-payloads/median/median-tells.md new file mode 100644 index 00000000..fd8e8acd --- /dev/null +++ b/packages/ghost/src/init-payloads/median/median-tells.md @@ -0,0 +1,117 @@ +--- +name: Median tells +description: Flags the measured defaults of unsteered generation, the deterministic floor, and current model-signature tells — hover-lift, default accents, unprompted dark theme, gradient text, contrast, frequency tells, and per-model signatures. +severity: high +references: + - anti-goal.median > Hover-lift + - anti-goal.median > Indigo accent + - anti-goal.median > Dark theme + - anti-goal.median > Gradients + - anti-goal.median > Glassmorphism + - anti-goal.median > Side-stripe + - anti-goal.median > Cream surface + - anti-goal.median > Chat bubbles + - anti-goal.median > Stock copy + - anti-goal.median > Celebration + - anti-goal.median > Hero metric + - anti-goal.median > Eyebrow kicker +--- + +These flags target the measured convergence patterns of unsteered model +generation, the deterministic floor the median node licenses, and tells +specific to individual models. Each is mechanically detectable in a diff. +Pruning a rule from `anti-goal.median` orphans its paired reference here — +`ghost validate` warns; delete the flag and its reference together. + +Flag `transform` with `translateY` inside a `:hover` rule on cards, +buttons, or list items, especially paired with a shadow increase. Hover +confirmation in this fingerprint is color and background change, not lift. +(`anti-goal.median > Hover-lift`) + +Flag accent values in the indigo/blue/purple default family (`#4f46e5`, +`#6366f1`, `#2563eb`, `#3b82f6`, `#8b5cf6`, and close neighbors) +unless the diff shows the user asked for them. They are model defaults, not +palette members. (`anti-goal.median > Indigo accent`) + +Flag whole-page dark backgrounds when the ask did not request dark mode. +Dark surfaces are a declared brand choice or an explicit theme, never an +unprompted default. (`anti-goal.median > Dark theme`) + +Flag `linear-gradient` or `radial-gradient` as page or section +backgrounds, and gradient-filled buttons. (`anti-goal.median > Gradients`) + +Flag `backdrop-filter: blur` used for glassmorphism cards. +(`anti-goal.median > Glassmorphism`) + +Flag `background-clip: text` (with or without the `-webkit-` prefix) +paired with a gradient. Emphasis comes from weight or size in a single +solid color. + +Flag a thick colored border on one side of an element (`border-left` or a +`border-l-*` utility at 2px or more in a non-neutral color) while the +other sides stay thin. (`anti-goal.median > Side-stripe`) + +Flag warm off-white page backgrounds in the cream/sand/beige band, and token +names like `--cream`, `--sand`, `--parchment`, `--linen` introduced +by the diff. (`anti-goal.median > Cream surface`) + +Flag assistant messages rendered as bubbles with initials-circle avatars. +(`anti-goal.median > Chat bubbles`) + +Flag emoji used as icons or imagery in interface chrome. Text labels carry +meaning. + +Flag stock template copy in headings: "Simple, transparent pricing", +"Welcome back", and interchangeable-with-a-competitor phrasing. Recommend +copy that states what this product specifically does. +(`anti-goal.median > Stock copy`) + +Flag exclamation-marked success copy, confetti language, and celebratory UI +("You did it!", "Awesome!"). Confirmation is quiet and factual. +(`anti-goal.median > Celebration`) + +Flag the hero-metric template — a big number, small label, and supporting +stats as default proof — unless the metric shows real user data. Recommend +evidence specific to the product, or nothing. +(`anti-goal.median > Hero metric`) + +Deterministic floor — licensed by the median node, verified here, never +steered in prose: + +Flag text/background pairs below WCAG AA contrast: 4.5:1 for body text, +3:1 for large text (24px and up, or 18.7px bold and up). Check the worst +stop when the background is a gradient. + +Flag animation or transition without a `prefers-reduced-motion: reduce` +alternative — a crossfade or instant state change. + +Flag ad-hoc z-index values (`999`, `9999`, or any value outside a +declared scale). Layering wants a semantic scale, not an arms race. + +Flag `cubic-bezier` easings whose control points overshoot the 0–1 range, +and keyframe names matching bounce, elastic, wobble, or jiggle. + +Frequency tells — the crime is repetition, not the move (advisory): + +Flag three or more uppercase, tracked eyebrow kickers above section headings +in one page. One named kicker is voice; a kicker on every section is model +grammar. (`anti-goal.median > Eyebrow kicker`) + +Flag five or more em-dashes in body copy in one view. + +Flag three or more instances of the aphoristic rebuttal cadence ("Not X. Y." +/ "Sentence. No qualifiers.") in one page's copy. + +Model-signature tells — skip any block whose model did not produce the diff +(advisory): + +Codex: flag a 1px border paired with a box-shadow of 16px blur or more on +the same element — pick a solid border or a tight shadow, not both. Flag +border-radius of 32px or more on cards, sections, or inputs unless an +answered shape dial (`signature.shape`) sanctions large radii — then it is +fidelity, not a tell. Flag 1px linear-gradient grid or repeating-stripe +backgrounds used as decoration. + +Gemini: flag `transform` (scale, rotate, translate) on `img` inside a +`:hover` rule, including group-hover utilities targeting a child image. +Animate the card's background, border, or shadow instead. diff --git a/packages/ghost/src/init-payloads/skeleton/glossary.md b/packages/ghost/src/init-payloads/skeleton/glossary.md index 8cb384e1..053ffb08 100644 --- a/packages/ghost/src/init-payloads/skeleton/glossary.md +++ b/packages/ghost/src/init-payloads/skeleton/glossary.md @@ -10,8 +10,8 @@ kinds: # grammar The brand's decision logic: closed sets, role vocabularies, and assembly -rules, stated in token roles and never in literal values. Grammar survives a -rebrand unchanged — swap every value and these nodes still hold. Gather +rules, stated in token roles and never in literal values. Grammar survives an +adaptation unchanged — swap every value and these nodes still hold. Gather grammar before inventing structure. # signature @@ -39,5 +39,6 @@ anti-goals before styling anything greenfield. --- -The ghost skill bundle teaches authoring: how to answer the dials, add -registers, and grow this skeleton into a real fingerprint. +The ghost skill bundle teaches authoring — its `adapting-a-starter` recipe is +the procedure for growing this skeleton into a real fingerprint: answer the +dials, prune the median floor, edit your values, then generate refs and tells. diff --git a/packages/ghost/src/init-payloads/skeleton/index.md b/packages/ghost/src/init-payloads/skeleton/index.md index b8a5b3b2..303364f0 100644 --- a/packages/ghost/src/init-payloads/skeleton/index.md +++ b/packages/ghost/src/init-payloads/skeleton/index.md @@ -2,13 +2,13 @@ description: "Always read first — the trust tiers of this starter fingerprint, the unanswered dials, and how to work before they are answered." --- -This is a skeleton fingerprint: the rebrand-safe law of good interface work, +This is a skeleton fingerprint: the adaptation-safe law of good interface work, with every brand decision left explicitly open. It steers an agent away from the model's median from the first generation, without pretending to be a brand it is not. The corpus carries two trust tiers. Grammar and the median floor are law: -safe to consume verbatim, unchanged by any future rebrand — they speak in +safe to consume verbatim, unchanged by any adaptation — they speak in token roles and closed sets, never in literal values. Signature nodes are dials, and in this starter every dial is unanswered: each states the fixed relationship worth keeping and the question only a human can answer. Do not diff --git a/packages/ghost/src/review/resolve.ts b/packages/ghost/src/review/resolve.ts index d7e79043..10c6c909 100644 --- a/packages/ghost/src/review/resolve.ts +++ b/packages/ghost/src/review/resolve.ts @@ -1,11 +1,12 @@ import { classifyMaterialLocator, type GhostCatalog, + type MaterialTransportOptions, + materialLocatorClaimsPath, parseSourceRef, } from "#ghost-core"; import type { LoadedCheck } from "../scan/check-files.js"; import { parseTouchedFiles, type TouchedFile } from "./diff.js"; -import { matchesGlob } from "./glob.js"; export interface MatchedMaterialNode { id: string; @@ -39,6 +40,7 @@ export function resolveReview( catalog: GhostCatalog, checks: Map, diffText: string, + transport: MaterialTransportOptions, ): ReviewResolution { const touchedFiles = parseTouchedFiles(diffText); const materialNodeIds = new Set(); @@ -55,8 +57,13 @@ export function resolveReview( if (localLocators.length === 0) continue; materialNodeIds.add(node.id); for (const file of touchedFiles) { + // Resolve each locator the same way validate does: package-relative + // `materials/…` locators expand to their repo-relative form before + // matching diff paths (which git emits repo-relative). Matching the + // raw locator text silently missed every exact-path locator whenever + // the package lives below the repo root. const locators = localLocators.filter((locator) => - matchesGlob(locator, file.path), + materialLocatorClaimsPath(locator, file.path, transport), ); if (locators.length === 0) continue; claimedFiles.add(file.path); diff --git a/packages/ghost/src/review/review-packet.ts b/packages/ghost/src/review/review-packet.ts index 9ed87017..34f72895 100644 --- a/packages/ghost/src/review/review-packet.ts +++ b/packages/ghost/src/review/review-packet.ts @@ -1,5 +1,8 @@ +import { join } from "node:path"; import type { GhostCatalogNode } from "#ghost-core"; +import { GHOST_MATERIALS_DIR } from "../scan/constants.js"; import type { LoadedFingerprintPackage } from "../scan/fingerprint-package.js"; +import { resolveGitRoot } from "../scan/package-paths.js"; import { type BaselineProse, resolveBaseline } from "./baseline.js"; import { type ProbeEvidence, runProbe } from "./probes.js"; import type { CoverageGap } from "./resolve.js"; @@ -38,6 +41,8 @@ export interface ReviewPacket { } export interface BuildReviewPacketOptions { + /** Absolute path of the fingerprint package directory (default: cwd/.ghost). */ + packageDir?: string; runProbes?: boolean; cwd?: string; probeTimeoutMs?: number; @@ -48,10 +53,16 @@ export async function buildReviewPacket( diffText: string, options: BuildReviewPacketOptions = {}, ): Promise { + const cwd = options.cwd ?? process.cwd(); const resolution = resolveReview( fingerprint.catalog, fingerprint.checks, diffText, + { + repoRoot: await resolveGitRoot(cwd), + packageDir: options.packageDir ?? join(cwd, ".ghost"), + materialsDir: GHOST_MATERIALS_DIR, + }, ); const materialNodes: PacketMaterialNode[] = resolution.materialNodes.map( diff --git a/packages/ghost/src/scan/check-scaffold.ts b/packages/ghost/src/scan/check-scaffold.ts index d646edc5..4e23f9d9 100644 --- a/packages/ghost/src/scan/check-scaffold.ts +++ b/packages/ghost/src/scan/check-scaffold.ts @@ -2,109 +2,11 @@ import { access, mkdir, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { UsageError } from "#ghost-core"; import { GHOST_CHECKS_DIR } from "./check-files.js"; +import { loadPayloadFile } from "./packed-payloads.js"; const EXAMPLE_CHECK_FILENAME = "example.md.example"; const MEDIAN_TELLS_FILENAME = "median-tells.md"; -const MEDIAN_TELLS_CONTENT = `--- -name: Median tells -description: Flags the measured defaults of unsteered generation, the deterministic floor, and current model-signature tells — hover-lift, default accents, unprompted dark theme, gradient text, contrast, frequency tells, and per-model signatures. -severity: high -references: - - anti-goal.median - - grammar.motion - - grammar.conversation ---- - -These flags target the measured convergence patterns of unsteered model -generation, the deterministic floor the median node licenses, and tells -specific to individual models. Each is mechanically detectable in a diff. -Pruning a rule from \`anti-goal.median\` orphans its paired flag here — -delete both together. - -Flag \`transform\` with \`translateY\` inside a \`:hover\` rule on cards, -buttons, or list items, especially paired with a shadow increase. Hover -confirmation in this fingerprint is color and background change, not lift. -(\`anti-goal.median > rule:median-hover-lift\`) - -Flag accent values in the indigo/blue/purple default family (\`#4f46e5\`, -\`#6366f1\`, \`#2563eb\`, \`#3b82f6\`, \`#8b5cf6\`, and close neighbors) -unless the diff shows the user asked for them. They are model defaults, not -palette members. (\`rule:median-indigo-accent\`) - -Flag whole-page dark backgrounds when the ask did not request dark mode. -Dark surfaces are a declared brand choice or an explicit theme, never an -unprompted default. (\`rule:median-dark-theme\`) - -Flag \`linear-gradient\` or \`radial-gradient\` as page or section -backgrounds, and gradient-filled buttons. (\`rule:median-gradients\`) - -Flag \`backdrop-filter: blur\` used for glassmorphism cards. -(\`rule:median-glassmorphism\`) - -Flag \`background-clip: text\` (with or without the \`-webkit-\` prefix) -paired with a gradient. Emphasis comes from weight or size in a single -solid color. - -Flag a thick colored border on one side of an element (\`border-left\` or a -\`border-l-*\` utility at 2px or more in a non-neutral color) while the -other sides stay thin. (\`rule:median-side-stripe\`) - -Flag warm off-white page backgrounds in the cream/sand/beige band, and token -names like \`--cream\`, \`--sand\`, \`--parchment\`, \`--linen\` introduced -by the diff. (\`rule:median-cream-surface\`) - -Flag assistant messages rendered as bubbles with initials-circle avatars. -(\`rule:median-chat-bubbles\`) - -Flag emoji used as icons or imagery in interface chrome. Text labels carry -meaning. - -Flag stock template copy in headings: "Simple, transparent pricing", -"Welcome back", and interchangeable-with-a-competitor phrasing. Recommend -copy that states what this product specifically does. -(\`rule:median-stock-copy\`) - -Deterministic floor — licensed by the median node, verified here, never -steered in prose: - -Flag text/background pairs below WCAG AA contrast: 4.5:1 for body text, -3:1 for large text (24px and up, or 18.7px bold and up). Check the worst -stop when the background is a gradient. - -Flag animation or transition without a \`prefers-reduced-motion: reduce\` -alternative — a crossfade or instant state change. - -Flag ad-hoc z-index values (\`999\`, \`9999\`, or any value outside a -declared scale). Layering wants a semantic scale, not an arms race. - -Flag \`cubic-bezier\` easings whose control points overshoot the 0–1 range, -and keyframe names matching bounce, elastic, wobble, or jiggle. - -Frequency tells — the crime is repetition, not the move (advisory): - -Flag three or more uppercase, tracked eyebrow kickers above section headings -in one page. One named kicker is voice; a kicker on every section is model -grammar. (\`rule:median-eyebrow-kicker\`) - -Flag five or more em-dashes in body copy in one view. - -Flag three or more instances of the aphoristic rebuttal cadence ("Not X. Y." -/ "Sentence. No qualifiers.") in one page's copy. - -Model-signature tells — skip any block whose model did not produce the diff -(advisory): - -Codex: flag a 1px border paired with a box-shadow of 16px blur or more on -the same element — pick a solid border or a tight shadow, not both. Flag -border-radius of 32px or more on cards, sections, or inputs. Flag 1px -linear-gradient grid or repeating-stripe backgrounds used as decoration. - -Gemini: flag \`transform\` (scale, rotate, translate) on \`img\` inside a -\`:hover\` rule, including group-hover utilities targeting a child image. -Animate the card's background, border, or shadow instead. -`; - const EXAMPLE_CHECK_CONTENT = `--- name: logo-clearspace-holds description: Logo usage preserves clearspace, lockup integrity, and glyph rules. @@ -121,6 +23,7 @@ is used when the full lockup is required. export interface AddChecksResult { dir: string; written: string[]; + skipped: string[]; } /** Scaffold the flat `.ghost/checks/` directory with an example check. */ @@ -132,20 +35,32 @@ export async function addChecksDir( throw new UsageError(`checks/ already exists at ${checksDir}.`); } + const written: string[] = []; + const skipped: string[] = []; + await mkdir(checksDir, { recursive: true }); - await writeFile( - join(checksDir, MEDIAN_TELLS_FILENAME), - MEDIAN_TELLS_CONTENT, - "utf-8", - ); + if (await exists(join(packageDir, "anti-goal.median.md"))) { + await writeFile( + join(checksDir, MEDIAN_TELLS_FILENAME), + await loadPayloadFile("median", MEDIAN_TELLS_FILENAME), + "utf-8", + ); + written.push(MEDIAN_TELLS_FILENAME); + } else { + skipped.push(`${MEDIAN_TELLS_FILENAME} (no anti-goal.median node)`); + } + await writeFile( join(checksDir, EXAMPLE_CHECK_FILENAME), EXAMPLE_CHECK_CONTENT, "utf-8", ); + written.push(EXAMPLE_CHECK_FILENAME); + return { dir: checksDir, - written: [MEDIAN_TELLS_FILENAME, EXAMPLE_CHECK_FILENAME], + written, + skipped, }; } diff --git a/packages/ghost/src/scan/fingerprint-package.ts b/packages/ghost/src/scan/fingerprint-package.ts index 5f02bcd2..2255e023 100644 --- a/packages/ghost/src/scan/fingerprint-package.ts +++ b/packages/ghost/src/scan/fingerprint-package.ts @@ -425,7 +425,7 @@ function lintCheckReferences( issues.push({ severity: "warning", rule: "check-reference-unresolved", - message: `check reference '${raw}' does not resolve to a fingerprint node`, + message: `check reference '${raw}' does not resolve to a fingerprint node — if you pruned this rule from the node, delete its paired flag in the check too`, path: `checks/${check.id}.md.references`, }); continue; @@ -437,7 +437,7 @@ function lintCheckReferences( issues.push({ severity: "warning", rule: "check-reference-heading-missing", - message: `check reference '${raw}' names a heading that was not found`, + message: `check reference '${raw}' names a heading that was not found — if you pruned this rule from the node, delete its paired flag in the check too`, path: `checks/${check.id}.md.references`, }); } diff --git a/packages/ghost/src/scan/packed-payloads.ts b/packages/ghost/src/scan/packed-payloads.ts index 9c06ff58..2c7142cb 100644 --- a/packages/ghost/src/scan/packed-payloads.ts +++ b/packages/ghost/src/scan/packed-payloads.ts @@ -19,10 +19,7 @@ const INIT_PAYLOAD_ROOTS = [ const BINARY_EXTENSIONS = new Set([".woff", ".woff2"]); export async function loadPackedPayload(name: string): Promise { - const payloadDir = - INIT_PAYLOAD_ROOTS.map((root) => join(root, name)).find((dir) => - existsSync(dir), - ) ?? join(INIT_PAYLOAD_ROOTS[0], name); + const payloadDir = resolvePayloadDir(name); const files = await listPayloadFiles(payloadDir); return Promise.all( @@ -33,6 +30,21 @@ export async function loadPackedPayload(name: string): Promise { ); } +export async function loadPayloadFile( + payload: string, + relativePath: string, +): Promise { + return readFile(join(resolvePayloadDir(payload), relativePath), "utf-8"); +} + +function resolvePayloadDir(name: string): string { + return ( + INIT_PAYLOAD_ROOTS.map((root) => join(root, name)).find((dir) => + existsSync(dir), + ) ?? join(INIT_PAYLOAD_ROOTS[0], name) + ); +} + async function listPayloadFiles(dir: string): Promise { const entries = await readdir(dir, { withFileTypes: true }); const files = await Promise.all( diff --git a/packages/ghost/src/scan/templates.ts b/packages/ghost/src/scan/templates.ts index 0c842459..8de78dac 100644 --- a/packages/ghost/src/scan/templates.ts +++ b/packages/ghost/src/scan/templates.ts @@ -3,7 +3,7 @@ import { GHOST_EVENTS_FILENAME, LEGACY_PULL_HISTORY_FILENAME, } from "./constants.js"; -import { loadPackedPayload } from "./packed-payloads.js"; +import { loadPackedPayload, loadPayloadFile } from "./packed-payloads.js"; /** * A single seed file an `init` template writes, relative to the package dir. */ @@ -52,7 +52,8 @@ const MINIMAL_TEMPLATE: GhostInitTemplate = { name: "minimal", description: "Minimal node package: manifest + glossary + a starter index node.", - files() { + async files() { + const medianFile = await medianTemplateFile(); return [ manifestFile(), gitignoreFile(), @@ -87,6 +88,8 @@ evidence unless the node says the sample itself is normative. What this brand must never look, sound, or feel like — named generic patterns and rejected neighbors. Always-on, like a principle, but stated as the thing to steer away from. +\`anti-goal.median\` is the model's floor, not the brand's taste. Gather +anti-goals before styling anything greenfield. # asset @@ -127,6 +130,7 @@ When a truth is narrower, state the condition in the prose — the situation whe it applies — never a filing destination. `, }, + medianFile, ]; }, }; @@ -146,7 +150,8 @@ const COMPOSITION_TEMPLATE: GhostInitTemplate = { name: "composition", description: "Composition starter: minimal files + an invariants floor and a worked bound/open pattern.", - files() { + async files() { + const medianFile = await medianTemplateFile(); return [ manifestFile(), gitignoreFile(), @@ -185,6 +190,8 @@ render travels with the prose. What this brand must never look, sound, or feel like — named generic patterns and rejected neighbors. Always-on, like a principle, but stated as the thing to steer away from. +\`anti-goal.median\` is the model's floor, not the brand's taste. Gather +anti-goals before styling anything greenfield. # asset @@ -297,6 +304,7 @@ When a blessed render of this pattern exists, add an \`exemplar.*\` node with \`materials\` pointing at the screenshot and the implementation path. `, }, + medianFile, ]; }, }; @@ -324,7 +332,10 @@ const SKELETON_TEMPLATE: GhostInitTemplate = { description: "Naked skeleton: the median floor + grammar law, with the signature dials left unanswered.", async files() { - const skeletonFiles = await loadPackedPayload("skeleton"); + const skeletonFiles = [ + ...(await loadPackedPayload("skeleton")), + await medianTemplateFile(), + ]; skeletonFiles.sort( (a, b) => (SKELETON_FILE_ORDER.get(a.relativePath) ?? Number.MAX_SAFE_INTEGER) - @@ -336,6 +347,13 @@ const SKELETON_TEMPLATE: GhostInitTemplate = { }, }; +async function medianTemplateFile(): Promise { + return { + relativePath: "anti-goal.median.md", + content: await loadPayloadFile("median", "anti-goal.median.md"), + }; +} + const TEMPLATES = new Map([ [MINIMAL_TEMPLATE.name, MINIMAL_TEMPLATE], [COMPOSITION_TEMPLATE.name, COMPOSITION_TEMPLATE], diff --git a/packages/ghost/src/skill-bundle/SKILL.md b/packages/ghost/src/skill-bundle/SKILL.md index 414bfcc0..a54843df 100644 --- a/packages/ghost/src/skill-bundle/SKILL.md +++ b/packages/ghost/src/skill-bundle/SKILL.md @@ -110,6 +110,7 @@ against the unpacked package with `--package `. - Author or update the fingerprint: follow [references/capture.md](references/capture.md). - Author material-backed nodes: follow [references/blocks.md](references/blocks.md). - Choose the right human-agent authoring workflow: follow [references/authoring-scenarios.md](references/authoring-scenarios.md). +- Adapt a starter (a body or the skeleton) to your brand: follow [references/adapting-a-starter.md](references/adapting-a-starter.md). - Gather applicable truths for a task: follow [references/recall.md](references/recall.md). - Shape a pre-generation brief: follow [references/brief.md](references/brief.md). - Explore deliberate provocations with wild nodes: follow [references/wild.md](references/wild.md). diff --git a/packages/ghost/src/skill-bundle/references/adapting-a-starter.md b/packages/ghost/src/skill-bundle/references/adapting-a-starter.md new file mode 100644 index 00000000..31792f16 --- /dev/null +++ b/packages/ghost/src/skill-bundle/references/adapting-a-starter.md @@ -0,0 +1,102 @@ +--- +name: adapting-a-starter +description: Transplant a starter fingerprint — a body like vessel-light or the naked skeleton — into your own brand, one procedure from manifest id to regenerated refs. +handoffs: + - label: Validate the adapted package + command: ghost validate --format json + prompt: Does the adapted fingerprint validate, and did any pruned rule orphan a check reference? +--- + +# Recipe: Adapt A Starter Fingerprint + +**Goal:** turn an installed starter — a full body (`ghost init --body +vessel-light`) or the naked skeleton (`ghost init`) — into *your* brand's +fingerprint without shipping a self-contradicting package. + +A starter is factored by rate of change under adaptation. Knowing which stratum a +file belongs to tells you what to do with it: + +| Stratum | Files | On adaptation | +| --- | --- | --- | +| Grammar | `grammar.*` | Keep unchanged — value-free decision logic that survives any adaptation. | +| Median floor | `anti-goal.median` | Prune, never rewrite — Ghost stamps this measured model truth into every initialized package; you own it after init. | +| Signature | `signature.*` | Answer — each is a dial; restate it with your brand's answer. | +| Values | `materials/tokens.css` | Edit — the single injection point for every literal value. | +| Registers | `register.*` | Re-tune — conditions referencing signature ids; revisit after the dials change. | +| Derived artifacts | `materials/ref/*.html`, `anti-goal.tells` | Regenerate — they demonstrate the values and near-misses of a *specific* signature. | + +Do the steps **in order and in one sitting** where possible. A half-adapted +package is worse than an unadapted one: stale refs steer harder than any prose +you rewrote, so stopping after step 4 ships a fingerprint that contradicts +itself. + +## The procedure + +1. **Change the manifest id.** Edit `id:` in `manifest.yml` to your brand's + name. This is deliberately first: it is the explicit act that marks the + adaptation as begun. Until it changes, the package honestly claims to be the + starter, and every consuming agent cites it as a starter default. +2. **Prune `anti-goal.median`.** Each rule is a `###` heading section; delete + the whole section for every rule your brand legitimately violates (a brand + built on gradients deletes the Gradients section — that is the node working, + not failing). Do not rewrite surviving rules; they are the model's measured + floor, not your taste. Then run `ghost validate`: every check reference + orphaned by a pruned heading surfaces as its own warning — delete the paired + flag and its reference from the check. +3. **Answer the signature dials.** Walk each `signature.*` node as a + questionnaire item. Keep the fixed relationship (the part the node marks + as worth keeping); replace the starter's answer — or the open question — + with your brand's: restate the node as "this brand's current answer is …". + Ask the human for any dial they have not decided; never freehand a value + and present it as brand-backed. +4. **Edit `materials/tokens.css`.** Every literal value lives here — radii, + palette, type sizes, durations, eases. Change the values; keep the role + names. The role names are the grammar's vocabulary and the reason the + grammar nodes survive untouched. +5. **Regenerate the refs.** This is the step that decides whether the adaptation + succeeded. Exemplars dominate prose: a prose rule contradicted by a stale + ref loses. Rebuild each `materials/ref/*.html` against the new tokens and + answered dials, keep the annotation headers (`normative-for` / + `incidental`) current, and make each ref demonstrate its closed sets + completely. If the starter shipped no refs (the skeleton), generate them + now — a fingerprint with no exemplars steers at half strength. +6. **Rewrite `anti-goal.tells`.** The tells are near-misses of the *starter's* + signature; yours are different. For each answered dial, name the failure + mode one step away from your answer and its replacement. If the starter + shipped no tells node (the skeleton), author one. +7. **Re-run the checks — including against the refs.** `ghost validate` for + package shape, then review the regenerated refs against the median and + value checks (stage the ref changes and run `ghost review`). The floor + only holds if the refs hold it too: a ref that trips a median flag will + teach every future generation the violation. Rewrite the body's + `checks/values.md` alongside the dials it references. + +## Consuming an unadapted starter + +Work does not block on adaptation. Before the procedure runs (or midway +through it), cite starter content honestly: + +- Grammar: **Ghost-backed** — value-free decision logic that holds for any + brand. +- Surviving median rules: **owner-backed after init** — Ghost stamps this + measured model truth into every initialized package; you own the pruning and + any adaptation thereafter. +- The starter's signature values (a body) or your provisional choices (the + skeleton): **Ghost-backed (starter default, unadapted)** or **provisional** + — never plain brand truth. The manifest id tells you which state you are + in: a starter id means unadapted. +- In a brief, `anti-goal.median` sits in the anti-goals slot — after intent, + inventory, and composition, never before the brand truths. If a median rule + conflicts with an answered signature node, the signature wins and the + median line is a prune candidate to report. + +## Never + +- Never rewrite grammar nodes to taste — if a grammar rule is wrong for your + brand, it was never grammar; move it to a signature node and answer it. +- Never leave the starter's refs alongside your new tokens — regenerate or + delete; a stale exemplar outweighs your rewritten prose. +- Never prune a median rule without deleting its paired check flag, or keep a + check that asserts an obligation no node states. +- Never pre-write the manifest id change into automation — it is the human's + act of ownership, the one step an agent should not take alone. diff --git a/packages/ghost/src/skill-bundle/references/authoring-scenarios.md b/packages/ghost/src/skill-bundle/references/authoring-scenarios.md index f244ae32..3bef7fb1 100644 --- a/packages/ghost/src/skill-bundle/references/authoring-scenarios.md +++ b/packages/ghost/src/skill-bundle/references/authoring-scenarios.md @@ -86,7 +86,7 @@ and enforced in review, not repeated as the model's main example. ## 4. Draft The Nodes Write the smallest useful set of nodes, each a purpose-coherent prose truth with -a one-line `description`, named `..md` (or a bare slug when no kind is present). Ask three questions of each body: why (the stance), with what +a one-line `description`, named `..md` (or a bare slug when no kind is present). Ask three questions of each node body: why (the stance), with what (the materials), and how it is assembled (the patterns). These are drafting prompts, not fields. diff --git a/packages/ghost/src/skill-bundle/references/blocks.md b/packages/ghost/src/skill-bundle/references/blocks.md index b6a2df40..6d6f31e0 100644 --- a/packages/ghost/src/skill-bundle/references/blocks.md +++ b/packages/ghost/src/skill-bundle/references/blocks.md @@ -57,7 +57,7 @@ Neither is correct. A concrete block node is a deliberate trade, not a leak. reasoning, sources…) earns **one short prose body**. This is what the method is for. - The **composer middle** (card, table, form, sidebar…) is a call to weigh. Give - it a body when its arrangement carries a stance worth matching. + it a prose body when its arrangement carries a stance worth matching. If a primitive ever seems to need stance guidance, that is a signal it is doing a composer's job. Promote the pattern into a node; do not write a body on the diff --git a/packages/ghost/src/skill-bundle/references/capture.md b/packages/ghost/src/skill-bundle/references/capture.md index 2e4324fd..76655ebb 100644 --- a/packages/ghost/src/skill-bundle/references/capture.md +++ b/packages/ghost/src/skill-bundle/references/capture.md @@ -181,7 +181,7 @@ action beats completeness... `materials`, a substantial fenced example, or a `## Skeleton`. You do not declare a separate type. -## What a body answers +## What a node body answers While drafting, ask three questions of every truth — *why* (the stance), *with what* (the materials), and *how it is assembled* (the patterns). These are @@ -204,7 +204,7 @@ genuinely a different truth. ## Node prose stances -Node prose is steering payload. A generic sentence in a body averages every +Node prose is steering payload. A generic sentence in a node body averages every future generation toward the median, so hold drafts to these stances before the human sees them. @@ -226,13 +226,13 @@ Everywhere else: fits a competitor's brand, it is retrieval-dead; rewrite it until it could belong to no one else. - **Cut unratified hedges.** "Generally," "where possible," and "consider" in a - body mean the human never picked a side. Get the ratification or cut the + node body mean the human never picked a side. Get the ratification or cut the sentence. - **Ban brand-deck filler.** "Elevate," "delight," "seamless," "best-in-class," "empower." When a brand doc supplies these words, they are testimony to distill, never prose to keep. - **Settle the altitude on purpose.** Every truth is either claimed universal - or given its condition in the prose. A body that does neither was never + or given its condition in the prose. A node body that does neither was never curated for altitude; ask the human which it is. ## Score drafts before curation @@ -244,7 +244,7 @@ dimension: | --- | --- | | Testimony | Can you quote the human words or artifact this node came from? | | Discrimination | Does the description fit only this brand? | -| Force | Does the body decide something, or merely describe something? | +| Force | Does the node body decide something, or merely describe something? | | Altitude | Is it universal on purpose, or given its condition? | | Residue | Is it free of starter-demo prose and brand-deck filler? | diff --git a/packages/ghost/test/cli.test.ts b/packages/ghost/test/cli.test.ts index b9913e9e..ea313adb 100644 --- a/packages/ghost/test/cli.test.ts +++ b/packages/ghost/test/cli.test.ts @@ -281,7 +281,7 @@ describe("ghost CLI", () => { expect(median.stdout).toContain( "This is the model's median, not your brand.", ); - expect(median.stdout).toContain(""); + expect(median.stdout).toContain("### Side-stripe"); // The dials ship unanswered and forbid freehanding. const shape = await runCli(["pull", "signature.shape"], dir); @@ -331,11 +331,15 @@ describe("ghost CLI", () => { expect(initOutput.written).toContain("manifest.yml"); expect(initOutput.written).toContain("glossary.md"); expect(initOutput.written).toContain("index.md"); + expect(initOutput.written).toContain("anti-goal.median.md"); expect(initOutput.written).not.toContain("principle.stance.md"); expect(initOutput.written).not.toContain("decision.tradeoff.md"); - const validate = await runCli(["validate"], dir); + const validate = await runCli(["validate", "--format", "json"], dir); expect(validate.code).toBe(0); + const report = JSON.parse(validate.stdout); + expect(report.errors).toBe(0); + expect(report.warnings).toBe(0); }); it("keeps default and steering as aliases for the skeleton template", async () => { @@ -370,10 +374,14 @@ describe("ghost CLI", () => { expect(initOutput.written).toContain("index.md"); expect(initOutput.written).toContain("principle.composition.md"); expect(initOutput.written).toContain("pattern.status-with-next-step.md"); + expect(initOutput.written).toContain("anti-goal.median.md"); // The scaffolded package is valid as written. - const validate = await runCli(["validate"], dir); + const validate = await runCli(["validate", "--format", "json"], dir); expect(validate.code).toBe(0); + const report = JSON.parse(validate.stdout); + expect(report.errors).toBe(0); + expect(report.warnings).toBe(0); // The ladder nodes surface in the gather menu with their kinds. const gather = await runCli(["gather", "--format", "json"], dir); @@ -908,13 +916,13 @@ a deliberate provocation past the fingerprint — surfaced only on request const gather = await runCli(["gather", "--format", "json"], dir); expect(gather.code).toBe(0); expect(JSON.parse(gather.stdout).coverage).toEqual({ - nodes: 4, + nodes: 5, concrete: 1, - guards: 1, + guards: 2, }); const markdown = await runCli(["gather"], dir); expect(markdown.stdout).toContain( - "4 nodes · 1 carry concrete material · 1 guards", + "5 nodes · 1 carry concrete material · 2 guards", ); const steering = await runCli( @@ -1603,6 +1611,54 @@ a deliberate provocation past the fingerprint — surfaced only on request }); }); + it("review resolves package-relative locators when the package sits below the repo root", async () => { + // Regression: exact-path `materials/…` locators were matched as raw text + // against repo-relative diff paths, so a package below the repo root + // (e.g. packages/vessel-light/.ghost) never matched them — its value + // checks were silently dropped from the packet. + const packageDir = join("nested", "app", ".ghost"); + await runCli(["init", "--package", packageDir], dir); + await mkdir(join(dir, packageDir, "materials"), { recursive: true }); + await writeFile( + join(dir, packageDir, "materials", "tokens.css"), + ":root{}\n", + ); + await writeFile( + join(dir, packageDir, "asset.tokens.md"), + "---\ndescription: Tokens.\nmaterials:\n - materials/tokens.css\n---\n\nTokens prose.\n", + ); + await mkdir(join(dir, packageDir, "checks"), { recursive: true }); + await writeFile( + join(dir, packageDir, "checks", "token-discipline.md"), + "---\nname: token-discipline\ndescription: Tokens hold.\nseverity: high\nreferences:\n - asset.tokens\n---\n\nGrade token discipline.\n", + ); + const touched = `${packageDir.replaceAll("\\", "/")}/materials/tokens.css`; + const diff = [ + `diff --git a/${touched} b/${touched}`, + `--- a/${touched}`, + `+++ b/${touched}`, + "@@ -1 +1 @@", + "-old", + "+new", + ].join("\n"); + + const result = await runCli( + ["review", "--package", packageDir, "--diff=-", "--format", "json"], + dir, + { stdin: diff }, + ); + + expect(result.code).toBe(0); + const packet = JSON.parse(result.stdout); + expect(packet.materialNodes.map((n: { id: string }) => n.id)).toContain( + "asset.tokens", + ); + const check = packet.checks.find( + (c: { id: string }) => c.id === "token-discipline", + ); + expect(check).toMatchObject({ offered: "matched" }); + }); + it("review runs check probes as evidence and supports --no-probes", async () => { await runCli(["init", "--with", "checks"], dir); await writeFile( @@ -1828,6 +1884,7 @@ a deliberate provocation past the fingerprint — surfaced only on request expect(add.code).toBe(0); const added = JSON.parse(add.stdout); expect(added.written).toEqual(["median-tells.md", "example.md.example"]); + expect(added.skipped).toEqual([]); await expect( readFile(join(dir, ".ghost", "checks", "example.md.example"), "utf-8"), ).resolves.toContain("references:"); @@ -1838,9 +1895,11 @@ a deliberate provocation past the fingerprint — surfaced only on request "utf-8", ); expect(median).toContain("anti-goal.median"); - expect(median).toContain("rule:median-hover-lift"); + expect(median).toContain("anti-goal.median > Hover-lift"); expect(median).toContain("prefers-reduced-motion"); - expect(median).toContain("delete both together"); + expect(median).toContain( + "`ghost validate` warns; delete the flag and its reference together.", + ); expect(median).not.toContain("Vessel"); // Running init twice is a usage error. @@ -1859,6 +1918,53 @@ a deliberate provocation past the fingerprint — surfaced only on request expect(unresolved).toEqual([]); }); + it("checks init skips median tells when the median node is absent", async () => { + await runCli(["init", "--template", "minimal"], dir); + await rm(join(dir, ".ghost", "anti-goal.median.md")); + + const add = await runCli(["checks", "init"], dir); + expect(add.code).toBe(0); + expect(add.stdout).toContain( + "skipped median-tells.md (no anti-goal.median node)", + ); + + await expect( + readFile(join(dir, ".ghost", "checks", "median-tells.md"), "utf-8"), + ).rejects.toThrow(); + + const validate = await runCli(["validate", "--format", "json"], dir); + expect(validate.code).toBe(0); + const report = JSON.parse(validate.stdout); + expect(report.errors).toBe(0); + expect(report.warnings).toBe(0); + }); + + it("validate warns when a pruned median heading orphans its paired check", async () => { + await runCli(["init"], dir); + await runCli(["checks", "init"], dir); + const path = join(dir, ".ghost", "anti-goal.median.md"); + const median = await readFile(path, "utf-8"); + await writeFile( + path, + median.replace(/### Side-stripe\n[\s\S]*?(?=\n### Cream surface)/, ""), + ); + + const validate = await runCli(["validate", "--format", "json"], dir); + expect(validate.code).toBe(0); + const report = JSON.parse(validate.stdout); + expect(report.warnings).toBe(1); + expect(report.issues).toEqual([ + expect.objectContaining({ + severity: "warning", + rule: "check-reference-heading-missing", + message: expect.stringContaining("anti-goal.median > Side-stripe"), + }), + ]); + expect(report.issues[0].message).toContain( + "if you pruned this rule from the node, delete its paired flag in the check too", + ); + }); + it("checks rejects unknown actions", async () => { await runCli(["init"], dir); const result = await runCli(["checks", "remove"], dir); diff --git a/packages/vessel-light/.ghost/anti-goal.median.md b/packages/vessel-light/.ghost/anti-goal.median.md index 68e2bfd0..a0a5cefb 100644 --- a/packages/vessel-light/.ghost/anti-goal.median.md +++ b/packages/vessel-light/.ghost/anti-goal.median.md @@ -4,79 +4,82 @@ description: "The model's median defaults this fingerprint refuses — gather fo This is the model's median, not your brand. Each rule is reject→replace. Delete every line your brand legitimately violates — `ghost validate` will -surface any check the deletion orphans. +warn on any check reference the deletion orphans — delete the paired flag too. These are not aesthetic opinions. Where a count is given, it is the measured convergence of 300 unsteered generations across three frontier models (the antimedian experiment): the defaults a model reaches for when nobody hands it a brand. An output showing several of these tells reads as generated, -whatever else it does right. +whatever else it does right. A deterministic floor rides alongside these +rules in the paired check — mechanical tells like gradient-filled text, +emoji as interface icons, em-dash pileups, and aphoristic cadence — +verified at review, never steered in prose. - +### Hover-lift Reject hover-lift (`translateY` + growing shadow) as the default interaction → confirm with color and background change at the fingerprint's fast duration. (measured: 341) - +### Indigo accent Reject the indigo/blue/purple default accent (`#4f46e5`, `#2563eb`, `#8b5cf6` family; purple/violet hue 260–310) → the fingerprint's declared palette; absent one, monochrome plus a single functional accent. - +### Dark theme Reject unprompted dark theme → the fingerprint's declared surface. Dark-to-look-cool and light-to-be-safe are the same retreat from a decision. (measured: 271) - +### Gradients Reject gradient page and section backgrounds and gradient-filled CTAs → flat surfaces in semantic roles. - +### Side-stripe Reject side-stripe borders (a thick colored border on one side of a rounded card) — the single most recognizable tell of AI-generated UI → a full hairline border, a 4–8% surface tint, or a leading glyph. - +### Cream surface Reject the cream/sand/beige default surface (warm off-white; token names like `--cream`, `--sand`, `--parchment` are the tell) → a true off-white, the brand's own hue, or a committed color; warmth via accent and type, not the ground. - +### Glassmorphism Reject glassmorphism (decorative backdrop blur) → flat surfaces with real borders and the fingerprint's elevation tiers. - +### Chat bubbles Reject chat bubbles with initials-circle avatars for assistant turns → assistant text plain on the page surface; user turns marked compactly. - +### Stock copy Reject stock template copy ("Simple, transparent pricing") → headings that state what this product specifically does. - +### Celebration Reject celebration copy and UI (exclamation success, confetti language) → quiet factual confirmation. - +### Nested cards Reject nested cards and everything-in-cards → one surface level per region; interior hierarchy from spacing and type; spacing and alignment group without card overhead. - +### Hero metric Reject the hero-metric template (big number + small label + supporting stats as default proof) → evidence specific to the product, or nothing. A prominent metric showing real user data is fine. - +### Every button primary Reject every button a primary button → one primary per view; the rest step down the fingerprint's emphasis ladder. - +### Eyebrow kicker Reject an eyebrow kicker on every section → at most one, where the register sanctions it; one named kicker is voice, every-section is AI grammar. - +### Decorative motion Reject decorative looping animation (pulse/float/shimmer) and uniform fade-and-rise on every scrolled section → motion as evidence of state change; loops only for genuine loading; decoration never compensates for diff --git a/packages/vessel-light/.ghost/checks/median-tells.md b/packages/vessel-light/.ghost/checks/median-tells.md index 1570e0ed..c15e44e5 100644 --- a/packages/vessel-light/.ghost/checks/median-tells.md +++ b/packages/vessel-light/.ghost/checks/median-tells.md @@ -3,49 +3,60 @@ name: Median tells description: Flags the measured defaults of unsteered generation and current model-signature tells — hover-lift, default accents, unprompted dark theme, gradient text, frequency tells, and per-model signatures. severity: high references: - - anti-goal.median - - grammar.motion - - grammar.conversation + - anti-goal.median > Hover-lift + - anti-goal.median > Indigo accent + - anti-goal.median > Dark theme + - anti-goal.median > Gradients + - anti-goal.median > Glassmorphism + - anti-goal.median > Side-stripe + - anti-goal.median > Cream surface + - anti-goal.median > Chat bubbles + - anti-goal.median > Stock copy + - anti-goal.median > Celebration + - anti-goal.median > Hero metric + - anti-goal.median > Eyebrow kicker --- -These flags target the measured convergence patterns of unsteered model -generation, plus tells specific to individual models. Each is mechanically -detectable in a diff. Pruning a rule from `anti-goal.median` orphans its -paired flag here — delete both together. +This is Vessel's adaptation of the shared median check; the fidelity carve-outs +below are the body's own. These flags target the measured convergence patterns +of unsteered model generation, plus tells specific to individual models. Each +is mechanically detectable in a diff. Pruning a rule from `anti-goal.median` +orphans its paired reference here — `ghost validate` warns; delete the flag and +its reference together. Flag `transform` with `translateY` inside a `:hover` rule on cards, buttons, or list items, especially paired with a shadow increase. Hover confirmation in Vessel is color and background change, not lift. -(`anti-goal.median > rule:median-hover-lift`) +(`anti-goal.median > Hover-lift`) Flag accent values in the indigo/blue/purple default family (`#4f46e5`, `#6366f1`, `#2563eb`, `#3b82f6`, `#8b5cf6`, and close neighbors) unless the diff shows the user asked for them. They are model defaults, not palette -members. (`rule:median-indigo-accent`) +members. (`anti-goal.median > Indigo accent`) Flag whole-page dark backgrounds when the ask did not request dark mode. Dark surfaces in Vessel are the editorial dark band or an explicit `.dark` -theme, never an unprompted default. (`rule:median-dark-theme`) +theme, never an unprompted default. (`anti-goal.median > Dark theme`) Flag `linear-gradient` or `radial-gradient` as page or section backgrounds, -and gradient-filled buttons. (`rule:median-gradients`) +and gradient-filled buttons. (`anti-goal.median > Gradients`) Flag `backdrop-filter: blur` used for glassmorphism cards. -(`rule:median-glassmorphism`) +(`anti-goal.median > Glassmorphism`) Flag `background-clip: text` (with or without the `-webkit-` prefix) paired with a gradient. Emphasis comes from weight or size in a single solid color. Flag a thick colored border on one side of an element (`border-left` or a `border-l-*` utility at 2px or more in a non-neutral color) while the other -sides stay thin. (`rule:median-side-stripe`) +sides stay thin. (`anti-goal.median > Side-stripe`) Flag warm off-white page backgrounds in the cream/sand/beige band, and token names like `--cream`, `--sand`, `--parchment`, `--linen` introduced by the -diff. (`rule:median-cream-surface`) +diff. (`anti-goal.median > Cream surface`) Flag assistant messages rendered as bubbles with initials-circle avatars. -(`rule:median-chat-bubbles`) +(`anti-goal.median > Chat bubbles`) Flag emoji used as icons or imagery in interface chrome. Text labels carry meaning. @@ -53,13 +64,22 @@ meaning. Flag stock template copy in headings: "Simple, transparent pricing", "Welcome back", and interchangeable-with-a-competitor phrasing. Recommend copy that states what this product specifically does. -(`rule:median-stock-copy`) +(`anti-goal.median > Stock copy`) + +Flag exclamation-marked success copy, confetti language, and celebratory UI +("You did it!", "Awesome!"). Confirmation is quiet and factual. +(`anti-goal.median > Celebration`) + +Flag the hero-metric template — a big number, small label, and supporting +stats as default proof — unless the metric shows real user data. Recommend +evidence specific to the product, or nothing. +(`anti-goal.median > Hero metric`) Frequency tells — the crime is repetition, not the move (advisory): Flag three or more uppercase, tracked eyebrow kickers above section headings in one page. One named kicker is voice; a kicker on every section is model -grammar. (`rule:median-eyebrow-kicker`) +grammar. (`anti-goal.median > Eyebrow kicker`) Flag five or more em-dashes in body copy in one view. diff --git a/packages/vessel-light/.ghost/checks/motion-restraint.md b/packages/vessel-light/.ghost/checks/motion-restraint.md index 0de32a48..52040bfb 100644 --- a/packages/vessel-light/.ghost/checks/motion-restraint.md +++ b/packages/vessel-light/.ghost/checks/motion-restraint.md @@ -4,13 +4,14 @@ description: Flags non-token motion, looping decoration, and keyframes that do n severity: medium references: - grammar.motion + - signature.temperature --- Review changed transitions and animations for vocabulary first. Flag durations that do not use `--duration-fast`, `--duration-normal`, or `--duration-slow`. A hard-coded millisecond value is a drift even when it matches the token today. -Flag custom easing unless it uses the approved spring ease or a browser default already required by the primitive. +Flag custom easing unless it uses the fingerprint's one ease or a browser default already required by the primitive. Flag looping animations outside explicit loading states. Loading must be tied to ongoing work, not ambient motion. diff --git a/packages/vessel-light/.ghost/checks/relationships.md b/packages/vessel-light/.ghost/checks/relationships.md index 29ecb88b..1dfe6391 100644 --- a/packages/vessel-light/.ghost/checks/relationships.md +++ b/packages/vessel-light/.ghost/checks/relationships.md @@ -1,15 +1,16 @@ --- name: Relationship discipline -description: Flags structural violations of the grammar — emphasis-ladder breaks, sibling margins, decorative borders, nested cards. These rules survive any rebrand. +description: Flags structural violations of the grammar — emphasis-ladder breaks, sibling margins, decorative borders, nested cards. These rules survive any adaptation. severity: high references: - grammar.hierarchy - grammar.rhythm - grammar.surfaces + - anti-goal.median > Nested cards --- These assertions test relationships between token roles, not the values behind -them. They hold for any brand built on this grammar; a fork keeps this check +them. They hold for any brand built on this grammar; an adapted package keeps this check unchanged. Flag more than one primary-variant button per view. Secondary actions step diff --git a/packages/vessel-light/.ghost/checks/values.md b/packages/vessel-light/.ghost/checks/values.md index 031e2d40..8587251c 100644 --- a/packages/vessel-light/.ghost/checks/values.md +++ b/packages/vessel-light/.ghost/checks/values.md @@ -1,17 +1,18 @@ --- name: Value discipline -description: Flags off-signature values — non-pill controls, off-palette hues, raw color literals, expression over budget. A fork rewrites this check alongside the signature nodes. +description: Flags off-signature values — non-pill controls, off-palette hues, raw color literals, expression over budget. Adapting the dials rewrites this check alongside the signature nodes. severity: high references: - signature.shape - signature.palette + - grammar.color-roles - register.data-density - register.editorial - register.email --- These assertions test Vessel's current answers to the signature dials. A -rebrand that changes the dials rewrites this check with them; the paired +adaptation that changes the dials rewrites this check with them; the paired relationship check stays. Review changed HTML and CSS by view, not just by file. Classify the register diff --git a/packages/vessel-light/.ghost/glossary.md b/packages/vessel-light/.ghost/glossary.md index 04718897..8017688a 100644 --- a/packages/vessel-light/.ghost/glossary.md +++ b/packages/vessel-light/.ghost/glossary.md @@ -10,15 +10,15 @@ kinds: # grammar The brand's decision logic: closed sets, role vocabularies, and assembly -rules, stated in token roles and never in literal values. Grammar survives a -rebrand unchanged — fork the package, swap every value, and these nodes still +rules, stated in token roles and never in literal values. Grammar survives an +adaptation unchanged — adapt the package, swap every value, and these nodes still hold. Gather grammar before inventing structure. # signature The dials: the choices that make this brand this brand, each stated as a current answer that stands until you replace it. Signature nodes name real -values because they are the values — on fork, edit the token roles in +values because they are the values — on adaptation, edit the token roles in `materials/tokens.css` and restate the node. Gather signature before setting any value a dial governs. @@ -39,7 +39,7 @@ anti-goals before styling anything greenfield. --- -To adapt this package to another brand, follow the transplant procedure — -it ships with the ghost skill bundle. In short: new manifest id, prune the +To adapt this package to another brand, follow the `adapting-a-starter` +recipe in the ghost skill bundle. In short: new manifest id, prune the median rules your brand legitimately violates, answer each signature node, edit `materials/tokens.css`, regenerate the refs, rewrite the tells. diff --git a/packages/vessel-light/.ghost/grammar.color-roles.md b/packages/vessel-light/.ghost/grammar.color-roles.md index 99e28261..01f24754 100644 --- a/packages/vessel-light/.ghost/grammar.color-roles.md +++ b/packages/vessel-light/.ghost/grammar.color-roles.md @@ -21,9 +21,9 @@ brand accents, and they never moonlight as atmosphere, in any register. One view should not perform a color palette. If a status color is present, let the rest of the view stay on the base roles. Richness beyond this is -register-gated: the expression roles (`--expression-1` through -`--expression-5`) exist, but their volume ladder is a brand answer — see the -palette signature — and each register caps how loud they may be. +register-gated: a closed expression set (`--expression-*`) exists, but its +size, members, and volume ladder are a brand answer — see the palette +signature — and each register caps how loud they may be. The constant that holds across every register: expression never touches what you click. Buttons, inputs, and links stay on the base roles everywhere. A diff --git a/packages/vessel-light/.ghost/grammar.motion.md b/packages/vessel-light/.ghost/grammar.motion.md index 406ea1c4..d7cbfbfd 100644 --- a/packages/vessel-light/.ghost/grammar.motion.md +++ b/packages/vessel-light/.ghost/grammar.motion.md @@ -22,6 +22,6 @@ keyframes are off-language. Prefer opacity and small transform changes. If removing an animation does not reduce comprehension, the animation was decoration. -Condition: editorial surfaces may stage entrances — scroll reveals and +Condition: marketing and editorial surfaces may stage entrances — scroll reveals and section transitions are part of editorial rhythm, still built from the three durations and the one ease. In product UI the same staging is decoration. diff --git a/packages/vessel-light/.ghost/index.md b/packages/vessel-light/.ghost/index.md index 37753ab5..7b01cbc2 100644 --- a/packages/vessel-light/.ghost/index.md +++ b/packages/vessel-light/.ghost/index.md @@ -12,7 +12,7 @@ closed sets the grammar enumerates. Imitate the refs when the task matches them — they are worked examples to copy from, not a framework to import. The corpus carries three trust tiers. Grammar and the median floor are law: -safe to consume verbatim, unchanged by any rebrand. Signature nodes are +safe to consume verbatim, unchanged by any adaptation. Signature nodes are dials awaiting your values: each states Vessel's current answer and stands until you replace it. Registers are conditions: editorial, email, and data-density each name the situation where parts of the default contract diff --git a/packages/vessel-light/.ghost/signature.type.md b/packages/vessel-light/.ghost/signature.type.md index c0c7b5b2..f5d59003 100644 --- a/packages/vessel-light/.ghost/signature.type.md +++ b/packages/vessel-light/.ghost/signature.type.md @@ -14,8 +14,7 @@ outside the text variants — and product UI never mixes the two vocabularies in one view. Vessel's current answer: HK Grotesk. vessel-light ships it as its embedded -voice; the React package currently falls back to system-ui. This fingerprint -is the intended future — here, `tokens.css` is canonical. +voice — here, `tokens.css` is canonical. The heading scale is editorial: display, section, sub, and card each carry their own rhythm (`--heading-display-*`, `--heading-section-*`, diff --git a/packages/vessel-light/README.md b/packages/vessel-light/README.md index 8f03e0fd..7df8c5dc 100644 --- a/packages/vessel-light/README.md +++ b/packages/vessel-light/README.md @@ -25,7 +25,7 @@ ghost review ## Structure -The corpus is factored by rate of change under rebrand: `grammar.*` nodes are value-free decision logic that survives any fork; `signature.*` nodes are the identity dials (shape, palette, type, temperature), each stating Vessel's current answer; `register.*` nodes are named conditions that re-tune the contract; `anti-goal.median` is the model's measured defaults (prune lines your brand legitimately violates) and `anti-goal.tells` guards near-misses of Vessel's own signature. Every literal value lives in `materials/tokens.css`. +The corpus is factored by rate of change under adaptation: `grammar.*` nodes are value-free decision logic that survives any adaptation; `signature.*` nodes are the identity dials (shape, palette, type, temperature), each stating Vessel's current answer; `register.*` nodes are named conditions that re-tune the contract; `anti-goal.median` is the model's measured defaults (prune lines your brand legitimately violates) and `anti-goal.tells` guards near-misses of Vessel's own signature. Every literal value lives in `materials/tokens.css`. ## Curation diff --git a/packages/vessel-light/demo/.gitignore b/packages/vessel-light/demo/.gitignore index ba1fd25c..e362c4c5 100644 --- a/packages/vessel-light/demo/.gitignore +++ b/packages/vessel-light/demo/.gitignore @@ -1,3 +1,3 @@ -# Eval run artifacts are not committed on mainline; the 2026-07-08 matrix run -# is parked on the `vessel-eval-artifacts` branch. Regenerate via steering-eval. +# Eval run artifacts are never committed; regenerate via steering-eval +# (prompt → generate → shoot → score → report) from asks.md + ballast.md. out/ diff --git a/scripts/check-release-tarball.mjs b/scripts/check-release-tarball.mjs index c30b59f9..ed8aa742 100644 --- a/scripts/check-release-tarball.mjs +++ b/scripts/check-release-tarball.mjs @@ -66,7 +66,8 @@ try { "package.json", "dist/bin.js", "dist/cli.js", - "dist/init-payloads/skeleton/anti-goal.median.md", + "dist/init-payloads/median/anti-goal.median.md", + "dist/init-payloads/median/median-tells.md", "dist/init-payloads/vessel-light/manifest.yml", "dist/init-payloads/vessel-light/materials/fonts/HKGrotesk-Regular.woff2", "node_modules/cac",