From 4e1c3242178d146642e33a8ebb4fa786827763be Mon Sep 17 00:00:00 2001 From: Corey Thomas Date: Fri, 14 Aug 2026 11:46:39 -0400 Subject: [PATCH] docs(designbot): source materials for AuthZed brand generation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Defines what "on-brand" means in a form a generation model can consume, bridged from Sandworm's design canon and ground-truthed against Burrow's live brand_config. Organizing idea is a two-layer split. Composer rules (read by the text model assembling a prompt) never reach the image model; renderer facts (hexes, identity, style locks) are never paraphrased by the composer. Burrow's worst generation bug lived exactly at that seam — a blending preamble and a character-fidelity block in the same payload produced the wrong character, silently. Scope: diagrams, blog/social imagery, mascot. Product UI is out of scope. Two routing calls that cut against reflex: - Diagrams generate SVG against the token spec, not raster. They are label-dense with exact 1.5px strokes and a 7 3 dash; image models misspell text and approximate strokes. - Headline-bearing OG cards are templated renders for the same reason. Also stages a reviewable, unapplied SQL fix for the Dibs reference set. Co-Authored-By: Claude Opus 5 (1M context) --- designbot/01-composer-contract.md | 121 +++++++++ designbot/02-source-material-spec.md | 162 ++++++++++++ designbot/03-diagrams.md | 149 +++++++++++ designbot/04-social-blog.md | 121 +++++++++ designbot/05-mascot-character.md | 203 +++++++++++++++ designbot/LEARNINGS.md | 232 ++++++++++++++++++ designbot/README.md | 93 +++++++ .../staged-sql/01-fix-dibs-references.sql | 193 +++++++++++++++ 8 files changed, 1274 insertions(+) create mode 100644 designbot/01-composer-contract.md create mode 100644 designbot/02-source-material-spec.md create mode 100644 designbot/03-diagrams.md create mode 100644 designbot/04-social-blog.md create mode 100644 designbot/05-mascot-character.md create mode 100644 designbot/LEARNINGS.md create mode 100644 designbot/README.md create mode 100644 designbot/staged-sql/01-fix-dibs-references.sql diff --git a/designbot/01-composer-contract.md b/designbot/01-composer-contract.md new file mode 100644 index 0000000..a8d042d --- /dev/null +++ b/designbot/01-composer-contract.md @@ -0,0 +1,121 @@ +# The Composer Contract + +Rules for the model that **assembles** a generation request. These never reach the image +model. If you are writing the renderer-facing prompt, you want a per-type file instead. + +The contract exists because assembly is where the expensive bugs live. An image model +given a coherent prompt fails gracefully — it produces something a bit off. An image model +given a *self-contradicting* prompt fails confidently: it picks one side, ignores the +other, and returns something that looks deliberate. Nobody files a bug against output that +looks deliberate. + +--- + +## 1. Assembly order is fixed + +Emit blocks in this order. Order is load-bearing: multimodal models weight early tokens +more heavily, and identity constraints buried under a spec body get overridden by the +spec. + +1. **Identity / character block** — only if the request involves a locked character +2. **Reference-attribute block** — only if references carry per-reference notes +3. **Type instruction** — "Render this `` specification as a high-fidelity image" +4. **Payload** — the JSON spec or the user's prose prompt +5. **Quality locks** — negative constraints, from the per-type file + +Burrow's `buildRecipePrompt` follows 1 → 3 → 4. The camera clause is injected at the +*front* of the prompt for exactly this reason (`injectCameraAngle` prepends rather than +appends). Treat that as the precedent: anything that must not be overridden goes early. + +## 2. Mutual exclusion — the rules that matter most + +These pairs must never appear in the same payload. This table is the distilled form of our +worst production bug. + +| A | B | Why they cannot co-occur | +|---|---|---| +| Blending preamble ("contribute ONE attribute, ignore character/costume") | Character-fidelity block ("render the character EXACTLY") | Direct contradiction. The blend instruction tells the model to discard identity; the fidelity block tells it to preserve identity. Result: wrong or blended characters. | +| Character-fidelity | Character-preservation | Different base assumptions. *Fidelity* means "references define the character, synthesize new." *Preservation* means "the attached image IS the work, modify it." Emitting both leaves the model guessing which image is canonical. | +| "Uniform flat color, no gradients" | Any brand spec that defines an intended gradient | We shipped this. The instruction contradicted the brand's own documented belly→back gradient. Always read the brand spec before emitting a color-uniformity constraint. | +| Multi-image / variant-sheet language | Single-image request | Produces a contact sheet of variants instead of one image. | +| Label or field names from the spec | The rendered payload | Spec labels leak into the image as literal text. Strip keys; emit values. | + +**When the request genuinely needs both sides** — e.g. "keep the character, but pull the +background from this other reference" — do not emit both blocks. Emit the fidelity block, +and scope the blend instruction to the *specific attribute* and the *specific image index*: +"Image 3: extract ONLY the background palette. Ignore its character, pose, and +composition." Burrow's `buildBlendingPreamble` does this correctly at the per-reference +level; the bug was that the global preamble fired alongside it. + +## 3. Precedence when something still conflicts + +Resolve in this order, highest wins: + +1. **Character identity** — species, anatomy, signature features. Never negotiable. +2. **Brand palette and quality locks** — the hexes and the prohibitions. +3. **Explicit user instruction** for this request. +4. **Type defaults** from the per-type file. +5. **Model preference** — whatever it would have done anyway. + +If a user instruction conflicts with character identity, the user is asking for a +different character. Say so; do not silently produce a hybrid. + +## 4. Reference budget + +The API slot budget is **4 images**. Everything below follows from that. + +- **Sort by weight before you slice.** Never let insertion order decide. Burrow shipped a + bug where dropped (100) and gallery (80) references filled all four slots and pushed the + canonical mascot out entirely — the code took the first four in insertion order. +- **Weights must be distinct.** A reference pool where every entry sits at the same weight + is not a ranking, and the slice becomes arbitrary. Burrow's live config has six mascot + references all at weight 70, which means two of them — deterministically the last two — + never reach the model. Nobody noticed, because output was merely slightly worse. +- **Suggested bands.** Explicit user selection 100 · canonical brand reference 90 · + session pins 80 · vocabulary auto-attach 70 · general style pool 50. +- **Re-apply boosts after any recalculation.** If a layer rebuilds weights from stored + values, caller-applied boosts are silently lost. Either re-boost downstream or move the + boost into the stored field. Do not assume a boost survives a merge. +- **Log what got dropped.** A silent top-N truncation reads as "we used your references." + +## 5. Guard the data boundary + +Types describe schema *intent*. They do not describe what the database actually returns +after years of nullable columns and enum drift. Three production crashes in Burrow shared +this exact shape: the type claimed a value was present and in-enum, `tsc` saw nothing, and +it blew up at runtime. + +Before composing, at the load boundary: + +- **Coerce nullable arrays to `[]`.** `colors`, `vocabulary`, `assetProfiles`, + `referenceImages` are all nullable in practice. Burrow's live `colors` is empty, and a + downstream `.map` crashed production. +- **Guard enum-keyed lookups.** Never index a config object with a value that came from + the database without a fallback. Live data contains `mascotView: "left-side"`, which is + not a member of the `MascotView` union — it crashed a `VIEW_CONFIG[x].label` lookup. +- **Treat missing as missing, not as default.** If the canonical reference is absent, say + so and degrade explicitly. Do not quietly substitute the highest-weighted alternative + and present the result as on-brand. + +## 6. What the composer must not do + +- **Do not paraphrase hex values.** Emit `#6242e0`, never "a deep violet." Paraphrase is + how palettes drift. +- **Do not invent facts to fill a gap.** If the brand config has no voice guidelines, the + request goes out without voice guidelines. An invented rule becomes canon the moment + someone reads the output and believes it. +- **Do not narrate.** "I will now render…" ends up in the image as text. +- **Do not emit more than one type file's rules.** They carry competing palettes and + composition defaults; a model holding all of them averages them. + +## 7. Pre-flight checklist + +Before dispatch, verify: + +- [ ] Exactly one character block (fidelity **or** preservation **or** neither) +- [ ] No blend preamble if a character block is present, unless scoped per-image-index +- [ ] Color-uniformity constraints checked against the brand spec's own gradients +- [ ] References sorted by weight, sliced to 4, with distinct weights and drops logged +- [ ] Nullable config arrays coerced; enum lookups guarded +- [ ] Quality locks appended from exactly one per-type file +- [ ] No spec keys, no narration, no paraphrased hexes in the payload diff --git a/designbot/02-source-material-spec.md b/designbot/02-source-material-spec.md new file mode 100644 index 0000000..8073be1 --- /dev/null +++ b/designbot/02-source-material-spec.md @@ -0,0 +1,162 @@ +# Authoring Source Material + +How to add a reference image, a vocabulary keyword, or an asset profile so that it +actually changes output. This is the file to read before uploading anything. + +--- + +## The problem this file exists to solve + +Burrow's schema is excellent and almost entirely unpopulated. Measured against the live +`brand_config` on 2026-08-04: + +| Field | Schema supports | Live value | +|---|---|---| +| `mascot_prompt` | free text | **2,073 chars** | +| `reference_images` | rich per-ref metadata | 6 refs, **all metadata null** | +| `colors` | `BrandColor[]` | **0 entries** | +| `vocabulary` | `BrandKeyword[]` | **0 entries** | +| `asset_profiles` | `AssetProfile[]` | **0 entries** | +| `identity` · `voice` · `color_scales` | full objects | **null** | + +One prose blob is carrying the entire brand. Every structured field that prompt assembly +already reads — and it does read them, `buildStyleWeightedReferences` copies nine metadata +fields onto every weighted reference — arrives empty. + +**That is the opportunity.** These fields are already wired end to end. Populating them is +pure source-material work with no code change and no deploy. + +--- + +## A reference is inert until it has four things + +Uploading an image gets you a slot in the pool. It does not get you influence. Every +reference needs: + +**1. A distinct weight.** Not the default. If everything is 70, the top-4 slice is +arbitrary and the last entries never reach the model. Pick from the bands in +`01-composer-contract.md` §4 and make sure no two references in the same category tie. + +**2. `isCanonical` set on exactly one reference per subject.** This is the fallback the +resolver uses when no explicit default is configured (`getDefaultReference` looks for a +canonical before falling back to highest-weight). Leaving it null everywhere means the +fallback path is guesswork. Live config: null on all six. + +**3. A real `description`.** It ships to the model as semantic context — this is the field +that tells the renderer *what it is looking at*. Live config: empty string on all six. +"Mascot Dibs Refrence Front" is a filename, not a description. (It is also misspelled, and +that misspelling is now load-bearing in the data.) + +**4. A `promptInjection`, if the reference implies a rule.** Free text appended when this +reference is used — e.g. `"maintaining the flat vector style with bold uniform outlines +and no visible fur texture"`. This is where a reference stops being a picture and starts +being a constraint. Live config: null on all six. + +Then, per category, fill the matching characteristics object — `mascotCharacteristics`, +`diagramCharacteristics`, `sceneCharacteristics`, `objectCharacteristics`, +`textureCharacteristics`. The two that earn their keep fastest: + +- **`alwaysInclude`** — features that must appear. For Dibs: oversized pink ears, large + dark circular eyes, pink tail with orange tuft. +- **`neverInclude`** — features to avoid. This is a negative constraint attached to the + asset rather than to the prompt, which means it survives every prompt rewrite. + +## Enum discipline + +Off-enum values crash the app and silently disable conditioning. Both are live right now. + +`MascotView` accepts exactly: `front` · `side` · `back` · `three-quarter` · `expression` · +`action` · `other`. + +Live data violates this three ways: + +- `"left-side"` is **not a member** — it crashed a `VIEW_CONFIG[x].label` lookup in + production. +- `"Front Third"` is tagged `front`; it is a **three-quarter** view. +- `"Back Third"` is tagged `back`; also **three-quarter**. + +The mislabels do not crash anything. They just quietly mean the model never receives a +correctly-labeled three-quarter reference, so three-quarter renders are conditioned on +front and back images. That is the more dangerous failure of the two, because it has no +symptom. + +**Rule: pick the enum member, or extend the enum in code first. Never invent a value at +data-entry time.** + +## Vocabulary and alias mining + +The matcher (`matchVocabulary`) is deliberately simple: case-insensitive, **word-boundary** +regex against `keyword` plus `aliases`. It will not match substrings, so "Dibs" correctly +fails to fire on "distribution" — good. But it also means it is **exactly as good as the +aliases you write, and no better.** + +For every keyword, list the phrases a teammate would actually type: + +```jsonc +{ + "keyword": "Dibs", + "aliases": ["dibs", "the mascot", "our mascot", "the jerboa", "the character"], + "referenceIds": ["", "", ""], + "category": "mascot", + "autoInclude": true, + "promptPrefix": "the brand mascot character" +} +``` + +Four things worth knowing: + +- **`autoInclude` defaults to true.** Omitting it means the references attach. Set it + `false` explicitly if you want the keyword to add context without pulling images. +- **`promptPrefix` is wrapped as `[Context: …]` and prepended.** Keep it to a noun phrase. + It is not a sentence and not an instruction. +- **Aliases are the whole ballgame.** A teammate who types "make the worm wave" gets + nothing unless "the worm" is an alias. Write aliases for how people talk, not for how + the asset is named. +- **`promptPrefixes` can be dropped by the caller.** `matchVocabulary` returns both an + `enhancedPrompt` and a bare `referenceIds` list; assembly paths that consume only the + IDs discard the prefix. Verify your path uses `enhancedPrompt` if you rely on prefixes. + +## Asset profiles + +Use a profile when several references share one identity — the six views of one character +are the canonical case. Author the characteristics **once** on the profile and link the +references by `profileId`; `applyProfile` merges profile characteristics onto each ref. + +This is strictly better than repeating characteristics per reference, because it makes the +identity single-sourced. When the character's canon changes, one row changes. + +## Mapping to `brand_config` columns + +| Package concept | Column | Notes | +|---|---|---| +| Character identity prose | `mascot_prompt` | Already rich. Keep, but promote its facts into structured fields. | +| Palette | `colors` | Empty. Fill from `sandworm/design/palette.md`. | +| Color ramps | `color_scales` | Null. Sandworm's 7 families × 15 stops map directly onto `ColorScale`. | +| Semantic roles | `semantic_colors` | Null. Sandworm's light/dark token sets map onto `SemanticColorMapping`. | +| Brand name, tagline, logos | `identity` | Null. | +| Voice do/don't | `voice` | Null. Sandworm `DESIGN.md` § Do's and Don'ts is the source. | +| Keywords | `vocabulary` | Empty. | +| Shared character facts | `asset_profiles` | Empty. | + +**Three columns in the TypeScript type have no database column at all:** +`defaultDiagramRef`, `defaultSceneRef`, `defaultObjectRef`. The type declares them; +`brand_config` has only `default_mascot_ref` and `default_texture_ref`. + +This matters directly for diagrams — our first-priority output type — because **you +cannot currently set a default diagram reference.** Setting it appears to work in the type +system and vanishes on write. That is the same double-silent-drop shape that disabled +vocabulary and asset profiles before migration 0003: a field present in the UI and the +type, but missing from both the DB column set and the PUT allowlist. No error, no +persistence, `tsc` clean. + +Adding those three columns is a small migration and it unblocks diagram defaults. Until +then, diagram references must be attached explicitly per request. + +## Before you commit a change here + +- [ ] Weight is distinct within its category +- [ ] Exactly one `isCanonical` per subject +- [ ] `description` is a description, not a filename +- [ ] Enum values are real members +- [ ] Aliases cover how people actually talk +- [ ] Any new rule traces to shipped code or an explicit brand decision diff --git a/designbot/03-diagrams.md b/designbot/03-diagrams.md new file mode 100644 index 0000000..69c1d7d --- /dev/null +++ b/designbot/03-diagrams.md @@ -0,0 +1,149 @@ +# Diagrams + +Source: `sandworm/design/diagrams.md` (status: stable, ground-truthed 2026-06-09 against +the shipped brand reference set). Load `sandworm/DESIGN.md` alongside it for primitive +tokens. + +--- + +## Route diagrams to SVG, not to an image model + +This is the most important call in the package, and it goes against the reflex. + +AuthZed diagrams are **hand-rolled SVG and Canvas** — no mermaid, no reactflow, no d3-graph. +They are dense with mono-caps text labels (`RAG`, `YOUR DATA`, `accessible doc ids`), exact +1px and 1.5px strokes, an exact `7 3` dash array, and exact hex fills. Every one of those +is something raster image generation is bad at: + +- **Text.** Image models misspell labels. A diagram is mostly labels. This alone disqualifies + the approach for anything shipping. +- **Exact stroke weights and dash arrays.** A model approximates `7 3`; the brand *is* `7 3`. +- **Editability.** A shipped diagram gets revised. A PNG gets regenerated from scratch and + drifts every time. + +So the designbot's diagram job is **generate SVG source against the token spec**, which a +text model does well and which is diffable, reviewable, and exactly on-token. Use image +generation for diagrams only for throwaway concept exploration that will be redrawn. + +Everything below is the spec that SVG output must satisfy. It is written as constraints +rather than prose so it can be checked mechanically. + +--- + +## Grammar — the rule that carries the most weight + +**Solid rounded-rect = a thing. Dashed rounded-rect = a grouping of things.** Never cross +them. Dashed is *only* for scope containers, never for a connector. + +## Nodes + +| Property | Value | +|---|---| +| Shape | rounded-rect, `rounded-lg` (8px) → `rounded-2xl` (16px); hub/identity nodes may be `rounded-full` | +| Border | **solid**, 1px, `stone-700` dark / `stone-200` light | +| Fill | `#0D0D10` dark (shipped HeroDiagram, ~85% alpha) · `#ffffff` light. Alt: `rgba(23,13,28,0.5)` + backdrop blur | +| Icon | one Lucide line-icon, `strokeWidth: 2`, centered, `stone-025` dark / `stone-900` light | +| Label | mono-caps, `stone-400`, placed **above** the node | + +Process/verb nodes (`Embed Model`, `re-rank`, `Environment`) instead use a **filled stone +pill below** with a sentence-case label. + +## Scope containers + +Dashed rounded-rect, `strokeDasharray: "7 3"`, 1px, `stone-700` dark / `stone-200` light. +Mono-caps label inset top-left, riding the dashed edge. Containers **nest** — `partition ▸ +prefix ▸ docs` — with the inner dash dimming as it goes. + +## Connectors + +Solid, 1.5px, simple chevron/triangle arrowhead in the line's own color. Routing is +**orthogonal** with ~16px corner radius, or dead-straight for pipeline flows. Bezier is +allowed only for organic hub-and-spoke fans. + +Color carries meaning — `stone-400` neutral flow · `teal-400` allowed/active/accessible · +`red-500` denied/blocked. Optional junction dot `r=3` at `rgba(114,177,173,0.35)`. + +Signal an active edge by **teal** and/or by dimming siblings to ~0.1 opacity. + +## The checkpoint beam — the signature device + +The most ownable primitive: AuthZed drawn as the authorization layer everything crosses. + +- **Bar** — `gradients.warm-hero` (sand-300 → red-400 → violet/magenta) with a magenta + bloom, `0 0 24px rgba(165,49,138,0.25)` +- **Logomark** — the Saturn mark on a `stone-950` coin with a subtle `stone-800` ring, + sitting **on** the beam. The mark *is* the checkpoint. The dark disc is required — the + color glyph needs it to pop off the gradient (decided 2026-06-10) +- **Threads** — fine 1px lines crossing the beam, one per permission check. `teal-400` + permitted, `red-500` denied +- **Orientation** — horizontal (data fans down through it) or vertical (subject ↔ resources) +- **Label** — mono-caps, warm/muted, attached to the beam not the nodes: `accessible doc + ids`, `permission management`, `functionality control` + +Use it when the teach is *"authorization happens here."* **Exactly one beam per diagram.** + +## Entity state + +Folded-corner Lucide file icon. `teal-400` accessible · `red-500` denied (add a padlock) · +`stone-400` neutral/unscoped. + +## Category eyebrows + +For capability taxonomies: mono-caps eyebrow tinted to a family, over solid thin-border +pills inheriting that tint. `sand-300` Permissions Model · `magenta-600` Authorization Data +· `teal-500` Evaluation Engine. `Grant/Deny` is a sparkle pill with a violet border; +`Access Decision` is the violet-bordered terminal output. + +## One color language + +| Color | Connector | Entity | Beam | +|---|---|---|---| +| `teal-400` | allowed / active | accessible doc | permitted thread | +| `red-500` | denied | locked doc + padlock | blocked thread | +| `stone-400` | neutral flow | unscoped data | — | +| warm-hero gradient | — | — | the AuthZed layer itself | + +A red doc and a red edge mean the same thing. That consistency is what makes a Sandworm +diagram legible at a glance. + +--- + +## Quality locks + +Copy verbatim into the negative-constraint slot. + +``` +- Node borders are SOLID. Never draw a node with a dashed border. +- Dashed strokes are ONLY for scope containers, using dasharray 7 3. Never dash a connector. +- Active or allowed state is teal (#72b1ad family) or opacity-dimming of siblings. + NEVER recolor an edge to magenta to indicate active state. +- Exactly one checkpoint beam per diagram. Never render multiple gradient beams. +- The state vocabulary is teal (allowed), red (denied), stone (neutral). Never introduce + a fourth state color. +- Node labels are mono-caps and sit above the node. Process labels sit in a filled pill. +- Icons are Lucide line icons at 2px stroke, one per node. +- Do not use mermaid, reactflow, or d3-graph conventions or default styling. +``` + +## Shipped references + +Ground these against real code rather than memory: + +- `projects/web/src/home/UseCaseTree.tsx` — hub-and-fan; solid teal bezier at + `rgba(114,177,173,0.18)`, `rounded-full` hub, junction dots +- `projects/web/src/home/HeroDiagram.tsx` — permission-check flow; `#0D0D10` nodes, + canvas flow dots, active = sibling opacity-dim +- `projects/web/src/mdx/custom/SpiceBoxDiagram.tsx` — the canonical `7 3` container +- `projects/web/src/products/authzed-cloud/IfStatementsToCheckPermission.tsx` — code-as-diagram + +## Open + +`defaultDiagramRef` has **no database column** (see `02-source-material-spec.md`). Diagram +references must be attached per request until that migration lands. + +## Correction carried forward + +An earlier hub token described the connected-system motif as a dashed `4 4` stone connector +with a magenta active state. Ground-truthing disproved it: the UseCaseTree fan is **solid +teal**, the only dashed element that ships is the SpiceBox **container** at `7 3`, and +active state is never a magenta edge. If you find the old form anywhere, it is wrong. diff --git a/designbot/04-social-blog.md b/designbot/04-social-blog.md new file mode 100644 index 0000000..60769db --- /dev/null +++ b/designbot/04-social-blog.md @@ -0,0 +1,121 @@ +# Blog & Social Imagery + +Source: `sandworm/DESIGN.md` § Colors, Typography, Do's and Don'ts. Hex values live in +`sandworm/design/palette.md` — this file references token names, matching the hub's own +anti-drift rule. + +--- + +## Two lanes, and the split is about text + +**Lane A — text-bearing cards** (OG images, blog headers with a headline, quote cards). +These are **templated renders**, not image generations. An AuthZed OG generator already +exists for this. Reasons, in order of how much they matter: + +1. Image models misspell headlines, and the headline is the entire payload of an OG card. +2. The typographic register is precise — Inter `font-light` (300) headlines with + `font-semibold text-magenta-600` emphasis spans. A model approximates weight; the brand + *is* the weight. +3. OG cards are regenerated constantly as titles change. A template is deterministic; a + generation drifts on every re-roll. + +**Lane B — non-text imagery** (atmospheric backgrounds, illustrative spot art, abstract +brand texture, section headers with no baked-in copy). This is genuine image-generation +territory, and the rest of this file is the spec for it. + +If a request has a headline in it, it is Lane A. Do not compromise by generating an image +and hoping the text lands. + +--- + +## Palette + +**Brand spine.** `magenta-600` is the AuthZed primary — wordmark, in-prose emphasis, hover +borders on dark cards. `sand-300` is the warm counterweight and the link color on dark. +`teal-500` is the cool tertiary for secondary CTAs and success. + +**Stone is the neutral spine and it is purple-tinted, not gray.** This is the single most +common way generated imagery goes off-brand: a model reaches for a neutral dark and +produces a brown or blue-black, which reads instantly wrong next to real surfaces. Dark +page surface is `stone-975`; dark cards are `stone-950` at 90% opacity so gradients breathe +through. Light page surface is `stone-025`, cards pure white. + +**Gradients are a temperature matrix, not one gradient.** + +| Role | Warm | Cool | +|---|---|---| +| Hero (3-stop) | `warm-hero` — sand-300 → red-400 → **violet-600** | `cool-hero` — violet-500 → teal-400 | +| Accent (2-stop) | `warm-accent` — sand-300 → red-500 | `cool-accent` — violet-400 → blue-300 | +| Brand-landing (4-stop) | `warm-brand` — sand-100 → sand-300 → magenta-600 → magenta-900 | `cool-brand` — blue-150 → teal-300 → violet-600 → violet-900 | + +`warm-hero` is **the** brand gradient. Its violet end-stop is `violet-600` (`#6242e0`), +settled 2026-06-10 against the canonical CTA — if you see `violet-500` terminating a warm +gradient, that is known drift, not canon. + +Use `cool-hero` for cloud/data/product surfaces where warm over-saturates. + +**Do not put the brand gradient on everything.** Over-application kills the signal. It +belongs on hero emphasis and primary CTAs, not as a default background wash. + +## Typography (Lane A, and for any generated mock that implies type) + +- **Inter** carries ~95% of the surface. **Brand weight is light** — headlines and marketing + body both run `font-light` (300). The lightness is the warm-authority register. +- **Bold is for emphasis spans only.** The AuthZed pattern is `font-light` headline with a + `font-semibold text-magenta-600` phrase inside it. +- **JetBrains Mono** for code, terminal chrome, and mono-caps section labels (`text-xs`, + `letterSpacing: 0.08em`, uppercase), paired with horizontal rules. +- **Prose is never mono.** Inter for narrative, mono for code. + +## Composition + +- **Left-align by default.** Centered text is for announcement-style sections only. + Defaulting to centered is what makes imagery read as generic SaaS. +- **Translucency or gradient border — never both.** Layering a translucent background + inside a gradient-bordered container reads muddy. +- The asymmetric 4/8 grid is the house pattern for breaking out of centered layouts on + laptop and up. + +## Semantic color + +`teal` success · `sand` warning · `red` error · `violet` creative/highlight. Same language +as diagrams, so a reader carries one meaning across surfaces. + +--- + +## Quality locks + +Copy verbatim into the negative-constraint slot for Lane B. + +``` +- Neutrals must be purple-tinted (Sandworm stone family). Never render neutral grays, + browns, or blue-blacks. Never use generic Tailwind palette colors. +- Never use arbitrary hex values for brand surfaces. Use only Sandworm family colors. +- The warm brand gradient runs sand → red → violet-600. Never terminate it on a different hue. +- Do not apply the brand gradient as a full-bleed background wash. It is for emphasis. +- Do not render headline text, wordmarks, or UI copy into the image. Text is composited + separately. +- Do not render a solid-magenta button. No solid-magenta CTA exists in this brand. +- Do not center-align composition by default. +- Do not combine a translucent background with a gradient border. +- No fake dashboards, live tickers, or implied backend activity. +``` + +That last one is a brand-integrity rule, not an aesthetic one: the system explicitly +forbids implying activity that isn't happening. + +## Known drift — do not clone + +- **Solid-magenta buttons.** These appear where vendor forms (HubSpot, Calendly) force a + solid background. That is vendor drift. There is no canonical solid-magenta CTA. +- **`violet-500` warm-gradient end-stops.** Three shipped sites still carry it; canon is + `violet-600`. + +The hub's own rule applies here: document rejected drift, don't bless it. + +## Open + +`identity` and `voice` are both null in the live `brand_config`. Voice guidelines matter for +any generated copy and for choosing register in illustrative work. `DESIGN.md` § Do's and +Don'ts is the nearest source and should be promoted into the `voice` field — see +`02-source-material-spec.md`. diff --git a/designbot/05-mascot-character.md b/designbot/05-mascot-character.md new file mode 100644 index 0000000..4057bc2 --- /dev/null +++ b/designbot/05-mascot-character.md @@ -0,0 +1,203 @@ +# Mascot & Character + +**Dibs is the AuthZed mascot** — a jerboa, named for *Muad'Dib*, the desert mouse of Dune. +The naming is part of a coherent set: **SpiceDB** the product, **Sandworm** the design +system, **Dibs** the character. Dibs is referred to as **they**. + +That lineage is usable source material, not trivia. It places the character in a desert +register that the palette already speaks (`sand`, `stone`), and it means Dibs is a +*desert* creature — which should inform environment, lighting, and prop choices in a way +that a generic "cute rodent" prompt never will. + +Separately and strictly: the **Saturn logomark** is not the mascot and follows different +rules. Part 1 covers the mark, Part 2 covers Dibs. + +--- + +## Part 1 — The logomark (applies now) + +The logomark gradient uses canonical Sandworm tokens: **magenta-600 → red-500 → sand-300**. + +**The shipped SVG assets carry the wrong hexes.** `#A43189` / `#F0546C` / `#FFB371` are +legacy near-matches that predate the current palette. They are **un-reconciled drift, not a +separate brand palette.** Use the tokens, not the values you find in the files. + +Canonical assets live at `projects/web/public/assets/brand/` and +`projects/design/sandworm/public/` (synchronized), in five variants. + +In diagrams, the mark sits on a `stone-950` coin with a subtle `stone-800` ring — the dark +disc is required for the color glyph to read against the gradient beam +(see `03-diagrams.md`). + +``` +- Never recolor the logomark. Its gradient is magenta-600 → red-500 → sand-300. +- Never regenerate or redraw the logomark with an image model. Composite the canonical + asset. A generated approximation of a logo is a wrong logo. +- Never use the legacy hexes #A43189, #F0546C, or #FFB371. +``` + +That second lock matters more than it looks. A logomark is the one asset where "close" is +strictly worse than absent. + +--- + +## Part 2 — Generating Dibs + +### Pick exactly one mode + +The two modes make different assumptions about what the attached image *is*. Emitting both +leaves the model guessing which image is canonical — see `01-composer-contract.md` §2. + +**Character fidelity** — the references *define* the character; synthesize something new. +Use for new poses, scenes, compositions. + +> The first reference image(s) ARE the canonical character. Render the character EXACTLY +> as shown — same species, face shape, ear shape/size/color, eye shape, fur color, +> proportions. Do NOT use a training-data prior of "what the species looks like" — the +> references are the source of truth. + +That last sentence is load-bearing. Without it the model renders its idea of a jerboa +rather than *this* jerboa. + +**Character preservation** — the attached image *is* the work; modify it in place. Use for +refinement passes on an existing render. + +Fidelity pairs with a **reference strength** dial that changes the instruction, not just a +number: ≥80 "lock to references, minimal interpretation" · ≥50 "strong adherence, small +stylistic adjustments OK" · below 50 "treat references as inspiration only." + +### Bind attributes to identity — do not decouple them + +The single most useful prompt-craft finding from the Burrow arc, and it was learned by +getting it wrong first. + +An early revision separated identity from pose and attributes, reasoning that decoupling +would let pose vary freely while identity held. **It did the opposite** — renders drifted +off-character, because attributes floating free of the identity gave the model permission +to re-derive the character to fit the pose. + +The fix was to **bind pose and attributes to the identity first**, then vary them. State +who the character is, then say what they are doing, as one continuous constraint. For a +fixed character, binding-first beats decoupling. + +Corollary, from the same arc: **schema fields govern costume, composition, palette, and +ornament — never character anatomy.** Say so explicitly in the prompt. + +### Ask for one image, and no text + +Two artifacts show up otherwise, both fixed by being explicit: + +- **The sticker sheet** — the model returns a contact sheet of variants instead of one + image. Request a single image. +- **The label leak** — spec keys and field labels bleed into the render as literal text. + Request no text, and strip keys from the payload. + +### Read the brand spec before constraining color + +We shipped an instruction demanding "uniform flat color, no gradients" against a character +whose own brand spec defined an intended belly→back gradient. The instruction and the spec +fought; the spec lost. + +**Always read the character's own definition before emitting a color-uniformity +constraint.** Where the spec defines a gradient, the correct instruction is "head matches +body, follow the spec's intended gradient" — not a blanket flattening. + +### Where identity actually lives + +In Burrow, character identity lives in **`brand_config.mascot_prompt`** — a single long +prose field. The per-reference `mascotCharacteristics` and `visualCharacteristics` fields +are **empty on every reference.** + +So: prompt craft must **read from the brand config**, not expect per-reference +characteristics to carry the signal. If you build against the per-ref fields, you will +build against nulls. + +The better end state is to promote those prose facts into structured fields — especially +`alwaysInclude` and `neverInclude`, which attach the constraint to the *asset* rather than +the prompt, so it survives every prompt rewrite. See `02-source-material-spec.md`. + +### Reference set discipline + +A character needs a turnaround, and the turnaround needs to be correctly labeled and +correctly weighted. The live Dibs set fails on both counts: + +- Six references, **all at weight 70** — with a 4-slot budget and ties broken by insertion + order, two views never reach the model. +- **Three-quarter views tagged `front` and `back`**, so three-quarter renders are + conditioned on front and back images. No symptom, permanently worse output. +- One reference carries `mascotView: "left-side"`, which is **not a member of the + `MascotView` union** — it crashed a config lookup in production. + +Author a set as: one canonical front (weight 90, `isCanonical: true`), a correctly-tagged +three-quarter, a side, a back, and at least one expression — each with a distinct weight, a +real `description`, and a `promptInjection` carrying the style rule. + +### The durable fix is a LoRA, not a better prompt + +Everything above is prompt craft approximating character consistency. The structural fix +for a recurring character is a trained LoRA. Notes carried forward from that research: + +- LoRAs can't run on Gemini — this forces a FLUX/fal backend alongside the Gemini one. +- **Caption in full sentences, not tags**, for FLUX.2 (single VLM text encoder). +- 20–30 images, **variety beats count**. A turnaround alone is risky: a flat consistent + backdrop makes the LoRA bind the character to the backdrop instead of generalizing. +- Caption only variable elements; leave fixed identity traits unstated so they bind to the + trigger token. +- **One render style per LoRA.** Never mix two visual styles of the same character in one + training set, or the style lock breaks. +- Text-heavy shots route to a model with better text fidelity, or get composited. + +--- + +## Dibs — the canonical identity + +From the live `mascot_prompt`. This is the character definition of record. Note how much +of it is *identity anchors* rather than description — that ratio is what makes it work: + +- **Anatomy** — very large round head on a small plump oval torso, top-heavy but balanced; + small slender limbs, dainty rounded feet +- **Eyes** — enormous, perfectly circular, solid deep dark purple (`#330066`), **no pupils + or highlights** +- **Ears** — two exceptionally large leaf-shaped ears, broad at base, warm pink (`#FF6699`) +- **Body** — soft orange with an intended gradient, lighter peach belly (`#FFC18C`) to + deeper orange back (`#FF8000`) +- **Tail** — long slender, accent pink, ending in a bushy **orange** tuft +- **Surface** — smooth, clean, **no visible fur texture**; flat vector +- **Always** — oversized pink ears, large dark circular eyes, simple facial features, pink + tail with orange tuft +- **Render** — bright clean 2D cartoon, bold outlines, flat color with minimal smooth + gradients + +The "always" line is the one doing the most work, and it is exactly what belongs in +`alwaysInclude`. + +--- + +## Quality locks + +``` +- Render exactly one image. Never produce a contact sheet, variant grid, or sticker sheet. +- Render no text, labels, watermarks, or field names anywhere in the image. +- The reference images are the source of truth for the character. Do not substitute a + generic version of the species from training data. +- Schema fields control costume, composition, palette, and ornament — never character anatomy. +- Do not humanize the character or alter its species. +- Follow the character spec's own gradients. Do not flatten a documented gradient. +``` + +## Open + +The identity prose is strong; everything structured around it is missing. In rough order of +payoff: + +1. **Promote the prose into structured fields** — especially `alwaysInclude` (oversized + pink ears, large dark circular eyes, pink tail with orange tuft) and `neverInclude`. + These attach to the asset, so they survive every prompt rewrite. +2. **Fix the reference set** — distinct weights, one `isCanonical`, real descriptions, + correct the two mislabeled three-quarter views, and replace the off-enum `"left-side"`. + Today two of the six references never reach the model. +3. **Decide the Dune register explicitly.** Dibs is a *desert* creature by lineage. Whether + that shows up in environments and props — or whether Dibs floats on brand-gradient + backgrounds with no world at all — is a brand call worth making on purpose rather than + letting each generation improvise. +4. **The LoRA.** Still the durable fix for consistency, still not started. diff --git a/designbot/LEARNINGS.md b/designbot/LEARNINGS.md new file mode 100644 index 0000000..665b67a --- /dev/null +++ b/designbot/LEARNINGS.md @@ -0,0 +1,232 @@ +# What Building Burrow Taught Us + +Burrow is an internal brand-asset generation tool — a DAM with a structured "mixing board" +front end over Gemini image models. It has been in real use long enough to fail in +instructive ways. This is what we'd want to know before building a designbot, ordered by +how much it cost to learn. + +Every claim here is traceable to shipped code or live data. Where something was believed +and later disproved, the correction is included rather than the tidy version. + +--- + +## 1. The expensive failures are *confident*, not broken + +An image model given a coherent-but-mediocre prompt produces something slightly off, and +someone files a bug. An image model given a **self-contradicting** prompt picks a side, +ignores the other, and returns something that looks deliberate. Nobody files a bug against +output that looks deliberate. + +Burrow's worst generation bug was exactly this. The payload could contain a blending +preamble — *"each reference contributes ONE attribute; ignore its character and costume"* — +in the same prompt as a character-fidelity block — *"the character MUST MATCH the references +EXACTLY."* Both instructions were individually correct and individually well-written. Together +they produced the wrong character. + +The fix was not better wording. It was **gating**: the blend preamble is now suppressed +whenever character fidelity is requested, and per-reference attribute extraction is scoped +to a specific image index instead of stated globally. + +**Generalizes to:** any system that assembles prompts from independently-authored fragments. +The failure isn't in any fragment; it's in the combination, which nobody owns. Write down +which fragments may never co-occur, and enforce it at assembly. We put that table in +[`01-composer-contract.md`](01-composer-contract.md) §2 because it's the highest-value thing +we know. + +## 2. Prompt craft has a direction, and it's counter-intuitive + +For a fixed character, we assumed decoupling identity from pose would let pose vary while +identity held. We shipped it. Renders drifted **off-character** — attributes floating free +of identity gave the model permission to re-derive the character to fit the pose. + +The correction was **binding-first**: state who the character is, then bind pose and +attributes to that identity as one continuous constraint. + +Two smaller findings from the same arc, both fixed by being explicit rather than clever: +asking for **one image** eliminated a contact-sheet-of-variants artifact, and asking for +**no text** eliminated spec labels leaking into renders as literal type. + +**Generalizes to:** resist the instinct that more structural separation gives more control. +With generative models, separation often reads as permission. + +## 3. A rich schema is not a populated one + +Burrow's `brand.ts` is genuinely well-designed — `MascotCharacteristics` with +`alwaysInclude`/`neverInclude`, per-category characteristics for diagrams and scenes and +objects, `UsageContext` with `useWhen`/`avoidWhen`, a keyword vocabulary with aliases and +prompt prefixes. Prompt assembly reads all of it; `buildStyleWeightedReferences` copies nine +metadata fields onto every weighted reference. + +Here is the live config, measured 2026-08-04: + +| Field | Live value | +|---|---| +| `mascot_prompt` | 2,073 chars of prose | +| `reference_images` | 6 refs, **all metadata null** | +| `colors` | **0** | +| `vocabulary` | **0** | +| `asset_profiles` | **0** | +| `identity` · `voice` · `color_scales` | **null** | + +One prose blob carries the entire brand. Everything structured is empty, and the code that +would use it runs faithfully against nulls. + +**Generalizes to:** schema design is the easy half and it feels like progress. The bottleneck +on generation quality is almost always **populated source material**, which is unglamorous, +requires domain judgment, and cannot be done by the person who wrote the schema in an +afternoon. Budget for the authoring, not just the modeling. + +This is the entire reason the designbot contribution is framed as source materials rather +than tooling. + +## 4. The silent-drop class + +Vocabulary and asset profiles had UI, had types, and **silently discarded every save.** Two +independent causes stacked: no database column existed, *and* both fields were missing from +the PUT allowlist. No error. No persistence. `tsc` clean. + +This is why earlier attempts to define brand vocabulary "didn't stick" — the work was done, +more than once, and thrown away on write. + +The shape is worth naming: **a field present in the UI and the type but absent from both the +DB column set and the write allowlist is a double-silent drop.** Every layer looks correct in +isolation. + +It is still live. `defaultDiagramRef`, `defaultSceneRef`, and `defaultObjectRef` are declared +in `BrandConfig` and have **no columns** in `brand_config`. Diagrams are our first-priority +output type, and you currently cannot set a default diagram reference. + +**Generalizes to:** for any config surface, test the round trip — write, reload, assert. Type +checking cannot see this class of bug, and neither can a code reviewer looking at one layer. + +## 5. Types describe intent; databases describe history + +Three production crashes shared one shape: the database held a nullable or off-enum value, +the TypeScript type claimed otherwise, and it blew up at runtime. + +- `brand_config.colors` was `null` but typed `BrandColor[]` → `.map` crashed. +- `mascotView: "left-side"` is not in the `MascotView` union → a `VIEW_CONFIG[x].label` + lookup crashed. **That value is still in live data.** + +The durable fix is **coercing and guarding at the data-load boundary**, not at each use +site. Types describe schema *intent*, not what Postgres returns after years of nullable +columns and enum drift. The load boundary is the only place to make the types honest. + +## 6. Silent quality loss beats loud failure, and it's worse + +The live reference set has six mascot images, **all at weight 70**. The API budget is four +slots. With uniform weights, the top-4 slice breaks ties by insertion order — so two +references, deterministically the last two, **never reach the model.** + +Separately, the two three-quarter views are tagged `front` and `back`, so three-quarter +renders are conditioned on front and back images. + +Neither of these crashes. Neither produces an error. Output is just quietly worse, forever, +and there is no signal that would ever prompt investigation. We found them by querying the +database, not by noticing bad output. + +A related bug did have teeth: `buildWeightedReferences` recalculates weights from stored +values, silently discarding caller-applied boosts, so the canonical mascot got pushed out of +the top four entirely by lower-priority references. + +**Generalizes to:** any weighted top-N path needs an explicit sort before the slice, distinct +weights to sort by, and a log line for what got dropped. A silent truncation reads as "we +used your references." + +## 7. Never hardcode a guessed error cause + +A reported "Generation failed" turned out to be Gemini returning +`429 RESOURCE_EXHAUSTED — monthly spending cap exceeded`. The real message never reached +anyone, because the UI threw **four hardcoded copies** of *"you may have hit a rate limit"* +whenever generation returned nothing — regardless of the actual provider status. + +The guess was even close. It still cost a debugging session, because the real message named +the actual cause and the guess didn't. + +Compounding it: the retry layer retried blindly with no status check, so a 429 burned three +retries across four parallel calls — twelve attempts against an already-capped project. + +**Generalizes to:** surface the provider's real status and message. A guessed error cause is +a lie that survives into every future debugging session. And **never retry a 4xx** — retries +are for transient failures, and a spend cap is not transient. + +## 8. Some constraints have no API, and mirrors are then legitimate + +We verified against Google's billing documentation: there is **no endpoint** to read or set +the Gemini monthly spend cap, and none to read month-to-date spend. The Cloud Billing Budget +API manages a different object that does not gate the Gemini API. + +So an accurate spend meter is *impossible* by pull. What shipped instead is a real usage +ledger from `response.usageMetadata` token counts, plus a hand-reconciled baseline, with the +429 as ground truth. The drift is permanent by design and documented as such. + +Also worth knowing: Google's cap enforcement **lags the setting by ~10 minutes**, which is +documented behavior. We spent real time debugging a cap raise that had in fact already +worked. + +**Generalizes to:** confirm whether an API exists before designing around it, and when it +doesn't, say plainly that the mirror is a mirror. A number presented as authoritative that +is actually a local estimate is worse than an obviously-manual one. + +## 9. Dials are for the author; presets are for everyone else + +Burrow's organizing metaphor is "dials not dreams" — a JSON spec as a mixing board where +every field is an independent fader. It is genuinely good, and it is tuned for the person +who built it. + +Comparing it against a peer tool that reads noticeably nicer, the difference was +**presets-first**: a small number of named levers plus auto-context, with the dials +underneath for power users. + +For a **team** designbot this is the whole ergonomic question. Teammates do not want eight +knobs; they want "make me a blog header" and a good default. The cheap version is a preset +layer *over* the existing dials — no architecture change — and it is probably the highest +-leverage UX work available. + +## 10. Not everything should be an image + +Two of our three in-scope output types shouldn't primarily use image generation at all, and +this only became clear from reading the brand canon. + +**Diagrams** are hand-rolled SVG, dense with mono-caps labels, with exact stroke weights and +an exact `7 3` dash array. Image models misspell text, approximate strokes, and produce +un-editable output that drifts on every regeneration. Generating **SVG source against the +token spec** is better on every axis — diffable, reviewable, exactly on-token. + +**OG and blog cards** are headline-bearing, and the headline is the entire payload. They +should be templated renders. + +That leaves image generation for what it's genuinely good at: atmospheric, illustrative, and +character work. + +**Generalizes to:** "we have an image model" is not a reason to make images. Pick the +representation the brand is actually authored in. + +## 11. Ground-truth beats memory, and write down what you disproved + +The best thing about AuthZed's existing design canon isn't the tokens — it's that it records +its own corrections. A plausible motif ("dashed connectors with a magenta active state") was +written down, believed, then disproved by grepping shipped code. The real pattern was a solid +teal fan. The canon now carries the correction *and* the note that the earlier version was +aspirational. + +We hit the same class of error from the other direction: a schema documented as existing +turned out never to have landed, and a code path assumed to be the live one turned out to be +a different surface entirely. + +**Generalizes to:** any rule written from memory is load-bearing fiction. Before writing a +rule, grep for it. When shipped code diverges from canon, record it as rejected drift rather +than silently canonizing it — the legacy logomark hexes and the vendor solid-magenta buttons +are both documented as drift, which is why nobody clones them. + +--- + +## The short version + +If you read one thing: **the tooling was never the bottleneck.** Burrow can already read +nine metadata fields per reference, match brand vocabulary with aliases, weight and rank +references, and freeze a reusable recipe. Almost all of it runs against empty configuration. + +The work that makes a designbot good is authoring the source material, in the fields that +already exist, with enough specificity that a model can't drift — and writing down which +instructions may never appear in the same breath. diff --git a/designbot/README.md b/designbot/README.md new file mode 100644 index 0000000..d878260 --- /dev/null +++ b/designbot/README.md @@ -0,0 +1,93 @@ +# AuthZed Designbot — Source Materials + +This package is the **source material** a designbot needs to produce on-brand AuthZed +imagery. It is not the tooling. The tooling exists; what it lacked was a definition of +what "on-brand" means in a form a model can consume. + +Scope: **diagrams**, **blog & social imagery**, **mascot / character**. Product UI mockups +are explicitly out of scope — `sandworm/design/product-ui.md` and `web-ui.md` already +cover that surface for humans and code, and generating UI is a different problem. + +--- + +## The one idea: two layers, never blurred + +A generation request has two kinds of content in it, and the single most expensive class +of bug we hit came from mixing them. + +| Layer | Who reads it | What it contains | Failure mode when wrong | +|---|---|---|---| +| **Composer rules** | The text model assembling the prompt (Claude, backend) | Assembly order, precedence, what may never co-occur, how many references to attach | Self-contradicting payloads → confidently wrong output | +| **Renderer facts** | The image model receiving the prompt (Gemini, via Burrow) | Hexes, identity descriptions, style locks, negative constraints | Drift, off-palette output, off-model characters | + +The composer decides *what to say*. The renderer facts are *what gets said*. A rule like +"never emit the blending preamble alongside a character-fidelity block" belongs to the +composer and must never reach the image model. A hex value belongs to the renderer and +should never be paraphrased by the composer. + +We learned this the hard way — see [`LEARNINGS.md`](LEARNINGS.md) §1. Burrow shipped a +prompt that told the model to "ignore character and costume, blend freely" in the same +payload as "character MUST MATCH EXACTLY." Both instructions were individually correct. +Together they produced the wrong character, silently, for weeks. + +--- + +## Files + +| File | Layer | Read it when | +|---|---|---| +| [`01-composer-contract.md`](01-composer-contract.md) | Composer | Building or reviewing the assembly step | +| [`02-source-material-spec.md`](02-source-material-spec.md) | Both | Authoring a new reference, keyword, or profile | +| [`03-diagrams.md`](03-diagrams.md) | Renderer | Generating any system/flow/authorization diagram | +| [`04-social-blog.md`](04-social-blog.md) | Renderer | Generating OG cards, blog headers, social posts | +| [`05-mascot-character.md`](05-mascot-character.md) | Renderer | Generating character work | +| [`LEARNINGS.md`](LEARNINGS.md) | — | Before changing any of the above | + +--- + +## Consuming this package + +**As a prompt/knowledge corpus (Claude backend).** Load `01-composer-contract.md` plus the +one per-type file that matches the request. Do not load all three type files at once — +they contain competing palettes and composition rules, and a model holding all three will +average them. One request, one type file. + +**As Burrow brand-config rows.** `02-source-material-spec.md` maps each field in this +package onto the `brand_config` columns that Burrow's prompt assembly already reads. +Most of those columns are currently empty; populating them is the highest-leverage work +available and requires no code change. + +**As a standalone tool.** The per-type files are self-contained. Each one ends with a +`Quality locks` section — copy those verbatim into your negative-constraint slot. They are +phrased as instructions to an image model, not as prose for a human. + +--- + +## Provenance, and why it matters + +Nothing in the per-type files is invented. It is bridged from Sandworm's design canon — +`sandworm/DESIGN.md` and the `sandworm/design/*.md` spokes — which is itself ground-truthed +against shipped code. Where Sandworm records a correction (it does, in several places), +this package carries the corrected form and cites it. + +That lineage is the reason this package can be trusted, and it sets the bar for changes: +**if you add a rule here, it must be traceable to shipped code or an explicit brand +decision.** A rule someone remembered is a rule that will drift. Sandworm's diagram spoke +learned this when a plausible "dashed-border connector" motif was written down, believed, +and then disproved by ground-truthing — the real shipped pattern was a solid teal fan. + +--- + +## Status + +Bridged and reviewed against `sandworm/` and Burrow's live `brand_config` on 2026-08-04. + +The brand is **Dune-coded end to end**, and the naming is load-bearing rather than +decorative: **SpiceDB** the product, **Sandworm** the design system, and **Dibs** the +mascot — a jerboa, after *Muad'Dib*, the desert mouse Paul Atreides takes his name from. +That coherence explains the palette's desert register (`sand`, `stone`) and is itself +source material: it tells a model what world this brand lives in. + +**Dibs is richly defined and structurally inert.** The identity prose is 2,073 characters +of genuinely good specification; every structured field around it is empty and every +reference carries null metadata. See `05-mascot-character.md`. diff --git a/designbot/staged-sql/01-fix-dibs-references.sql b/designbot/staged-sql/01-fix-dibs-references.sql new file mode 100644 index 0000000..b3c4f1a --- /dev/null +++ b/designbot/staged-sql/01-fix-dibs-references.sql @@ -0,0 +1,193 @@ +-- ============================================================================ +-- STAGED — NOT APPLIED. Review before running. +-- +-- Target: Burrow Postgres on Sandtrap (SHARED infrastructure, not local). +-- ssh sandtrap +-- docker exec -i burrow-postgres psql -U brandai -d burrow < 01-fix-dibs-references.sql +-- +-- What it fixes (all three are live problems, see docs/designbot/LEARNINGS.md §6): +-- 1. All six Dibs refs sit at weight 70. The API budget is 4 slots and ties +-- break by insertion order, so two refs NEVER reach the model. +-- 2. Both three-quarter views are mislabeled ('Front Third' => front, +-- 'Back Third' => back), so 3/4 renders condition on front/back images. +-- 3. 'Left Side' carries mascotView 'left-side', which is NOT in the +-- MascotView union — it crashed a VIEW_CONFIG[x].label lookup in prod. +-- +-- Also populates description + promptInjection, which are empty on every ref. +-- +-- Runs in a transaction and ends with ROLLBACK. Read the verification output, +-- then change the last line to COMMIT and re-run to apply for real. +-- ============================================================================ + +BEGIN; + +-- --- Backup the current value into app_setting so this is reversible -------- +INSERT INTO app_setting (key, value) +SELECT 'backup-reference-images-2026-08-06', reference_images::text +FROM brand_config +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +-- --- The patch ------------------------------------------------------------- +-- Weight bands (see docs/designbot/01-composer-contract.md §4): +-- canonical 90 > turnaround spread 82/80/78/76 > situational 74 +-- +-- Top-4 by weight resolves to: Front, Front-Third, Side, Back-Third — +-- a deliberate spread AROUND the character (front, 3/4, profile, rear-3/4) +-- rather than a front-heavy set. This is a character TURNAROUND; its whole +-- purpose is defining Dibs in the round. Starving the rear views means any +-- back-facing render has no reference for what Dibs looks like from behind, +-- and the model invents it. +-- +-- Expression drops to reserve at 74 because it is SITUATIONAL — pull it when +-- the brief calls for enthusiasm, not as a standing member of the set. +-- +-- !! THIS IS A STOPGAP. Static weights are the wrong mechanism entirely. !! +-- The 4-slot budget is a PER-REQUEST constraint; a global ranking cannot +-- express "which views matter for THIS camera angle." See the note at the +-- bottom of this file — the real fix is angle-aware selection, and these +-- weights only decide what happens until that exists. + +UPDATE brand_config +SET reference_images = ( + SELECT jsonb_agg(patched ORDER BY (patched->>'weight')::int DESC) + FROM ( + SELECT + CASE r->>'id' + + -- Canonical front. The identity anchor. + WHEN '0fd1L4gVBua3D7t-J7XQf' THEN r || jsonb_build_object( + 'weight', 90, + 'isCanonical', true, + 'mascotView', 'front', + 'artStyle', 'vector', + 'description', 'Dibs, canonical front view. The identity anchor: oversized leaf-shaped pink ears, large dark-purple circular eyes each carrying a small violet highlight, soft orange body with peach belly, pink tail with orange tuft.', + 'promptInjection', 'flat vector illustration, bold uniform outlines, smooth clean surface with no visible fur texture' + ) + + -- Front three-quarter. Was mislabeled 'front'. + WHEN 'wzLD4oESUI-mhrPiJsCmx' THEN r || jsonb_build_object( + 'weight', 82, + 'mascotView', 'three-quarter', + 'artStyle', 'vector', + 'description', 'Dibs, front three-quarter view. Shows ear depth and body volume that the flat front view flattens.', + 'promptInjection', 'flat vector illustration, bold uniform outlines, smooth clean surface with no visible fur texture' + ) + + -- Expression reference. Carries the personality register. + WHEN 'atVZAdmJJrz-ZQeFp8V33' THEN r || jsonb_build_object( + 'weight', 74, + 'mascotView', 'expression', + 'artStyle', 'vector', + 'description', 'Dibs cheering — the canonical happy/energetic expression. Arms raised, wide open smile. Use when the brief calls for enthusiasm.', + 'promptInjection', 'flat vector illustration, bold uniform outlines, cheerful open expression' + ) + + -- Side profile. Was the off-enum 'left-side' that crashed prod. + WHEN 'DRIPUDgrvkmRVj7asmnqu' THEN r || jsonb_build_object( + 'weight', 80, + 'mascotView', 'side', + 'artStyle', 'vector', + 'description', 'Dibs, left side profile. Defines the tail silhouette and the head-to-body proportion.', + 'promptInjection', 'flat vector illustration, bold uniform outlines, smooth clean surface with no visible fur texture' + ) + + -- Rear three-quarter. Was mislabeled 'back'. See NOTE below on 'other'. + WHEN 'C4tYPgm6HVus909Xejt1U' THEN r || jsonb_build_object( + 'weight', 78, + 'mascotView', 'other', + 'artStyle', 'vector', + 'description', 'Dibs, rear three-quarter view. Shows how the tail attaches and how the ears read from behind.', + 'promptInjection', 'flat vector illustration, bold uniform outlines, smooth clean surface with no visible fur texture' + ) + + -- Straight back. + WHEN 'sVSUpz75ItpG2SBRQzNj4' THEN r || jsonb_build_object( + 'weight', 76, + 'mascotView', 'back', + 'artStyle', 'vector', + 'description', 'Dibs, straight back view. Rear tail and ear placement.', + 'promptInjection', 'flat vector illustration, bold uniform outlines, smooth clean surface with no visible fur texture' + ) + + ELSE r + END AS patched + FROM jsonb_array_elements(reference_images) r + ) s +); + +-- --- Verify ---------------------------------------------------------------- +SELECT + r->>'name' AS name, + r->>'mascotView' AS view, + r->>'weight' AS weight, + r->>'isCanonical' AS canonical, + left(r->>'description', 40) AS description_starts +FROM brand_config, jsonb_array_elements(reference_images) r +ORDER BY (r->>'weight')::int DESC; + +-- Expect: 6 rows, weights 90/82/80/78/76/74, exactly one canonical=true, +-- views front/three-quarter/side/other/back/expression, no 'left-side', +-- every description non-empty. +-- Top-4 by weight = Front, Front-Third, Side, Back-Third (a turnaround spread). + +ROLLBACK; -- <<< change to COMMIT to apply + +-- ============================================================================ +-- NOTE — 'other' is a compromise, not the right answer. +-- +-- MascotView is: front | side | back | three-quarter | expression | action | other +-- +-- There is exactly one 'three-quarter' member, but Dibs has TWO three-quarter +-- views (front-facing and rear-facing). Tagging the rear one 'three-quarter' +-- too would make the pair indistinguishable to any view-specific logic, so this +-- patch parks it on 'other' and puts the real meaning in the description. +-- +-- The better fix is a code change: extend the union to distinguish +-- 'three-quarter-front' / 'three-quarter-back' (or add a 'facing' field). +-- Do that and this row should be re-tagged. +-- ============================================================================ + +-- ============================================================================ +-- THE REAL FIX — angle-aware reference selection +-- +-- Everything above is a stopgap. The underlying problem is that `mascotView` is +-- NEVER READ BY SELECTION. Verified across src/: the field is set in the UI +-- (BrandGuidelines, ReferenceAssetEditor, BrandOnboarding), displayed in the UI +-- (AssetPicker, BrandGuidelines), and copied onto the outgoing payload +-- (GenerationStudio, vocabulary-matcher, prompt.ts). Nothing filters, ranks, or +-- selects on it. +-- +-- Camera angle is equally disconnected. `selectedCameraAngle` produces a text +-- clause via CAMERA_ANGLES and gets PREPENDED to the prompt string. It never +-- reaches assembleReferences(). +-- +-- Net effect: ask for a back view and you get the four highest-weighted +-- references — which under any static ranking are mostly front-facing — plus a +-- sentence saying "back view". The words say back, the images say front, and in +-- a multimodal model the images win. That is the same self-contradiction class +-- documented in docs/designbot/01-composer-contract.md §2, arriving through the +-- reference channel instead of the text channel. +-- +-- Proposed selection, replacing pure weight-sort for mascot refs: +-- +-- 1. ALWAYS include the canonical identity anchor (isCanonical = true). +-- It defines who the character is, independent of camera. +-- 2. Fill remaining slots by matching the requested angle to mascotView: +-- +-- requested angle -> preferred views, in order +-- --------------------------------------------------------------- +-- front -> front, three-quarter +-- three-quarter -> three-quarter, front, side +-- side -> side, three-quarter, front +-- back -> back, rear-three-quarter, side +-- low-angle/top-down-> front, three-quarter (no dedicated refs) +-- default/auto -> three-quarter, front, side (best all-round) +-- +-- 3. Fill any leftover slots by weight descending. +-- 4. Log which references were dropped and why. +-- +-- Note CAMERA_ANGLES itself needs attention: the CameraAngle union declares 10 +-- members but the array defines only 6. 'back', 'close-up', 'full-body' and +-- 'portrait' are typed-but-absent, so getCameraAnglePrompt() returns '' for +-- them silently. A 'back' angle cannot even be requested today. +-- ============================================================================