Docs / Developer / Add a content kind
Add a content kind
A kind is one folder. src/entities/<kind>/kind.ts describes it once (its id, labels, deck color and glyph, whether it
is a Library deck, the blank piece "New" saves, the lines bunzai inspect prints), plus, when they apply, the
pieces it links by id (links), where its name lives (namePath), where it keeps pictures (media), whether it is
too heavy to read whole (heavy), whether the Library offers "New" for it (libraryNew), which scene field
points at it (sceneField), which list in a scene's prompting links it (promptingList), the Workbench editor it opens in, and, for a kind in a family, the deck it joins (deck,
with the owning kind's deckTitle and deckBlurb) and how it reads as its family's base (asPreset andsendProblem for a writing setup, src/core/writing.ts). Then bun run kinds
(scripts/kind-manifest.ts) writes two static seams from every kind folder:
src/generated/kinds.ts:KINDS,KIND_IDS,kindOf,isKindId, andAnyKindEntity, the union of every kind's
canonical piece. No decoder code, so the browser bundle and the search worker load it cheaply.src/generated/kind-schemas.ts: every kind's whole-entity decoder (the parse boundary's discriminated union) and the
kinds whose bodies split into item parts in storage.
Both are checked by bun run kinds:check in verify:ci; never edit them by hand.
What reads the seam (so you edit none of it)
| What | Where |
|---|---|
| The closed set of studio folders | src/studio/path-policy.ts (STUDIO_ENTITY_KINDS) |
| The parse boundary | src/entities/runtime-schema.ts |
| Storage item parts | src/entities/piece-parts.ts |
| Format adapter contracts | src/core/adapter.ts (KindAdapter<K>; character and preset add their extras) |
| Deck labels, colors, glyphs, Library decks | src/ui/_shared/decks.ts |
| Panel, editor head and Press deck colors | components/panel, components/editor-ehead, apps/press (inline from deckMeta) |
Search kind: words |
src/core/search/kind-aliases.ts (plural, short tag, searchWords) |
| Import receipts | src/ui/receipt.ts (receiptWord) |
| Kit deck counts and name lookup | src/kit/bridge.ts, src/kit/tools/rail-open.ts |
| Library "New" and every other blank-piece door | src/ui/_shared/new-piece.ts (createBlank, from blank and blankId; canCreateBlank honors libraryNew) |
| Links: Press riders, export embedding, import renames, Kit delete warnings, Workbench node ports, archive link caveats | src/core/links.ts (from links and sceneField) |
| A piece's name, read and written | src/core/piece-name.ts (from namePath) |
| Which paths are pictures, which kinds carry art | src/entities/_shared/media-reference.ts (from media; Kit's gallery and art tools read ART_KINDS) |
| Duplicate, rename and the Press on heavy pieces | src/ui/apps/press, src/ui/apps/library/delete-flow.tsx (from heavy) |
bunzai inspect |
src/cli-inspect.ts (describe) |
| First-run deck titles | src/ui/setup/steps/first-deck |
| Format matrix and field docs | scripts/format-matrix.ts, scripts/docs-fields.ts |
Steps
- Make
src/entities/<kind>/withschema.ts(the canonical typeCanonicalEntity<"<kind>", Body>and an empty-body
factory) andruntime-schema.ts(the strict body decoder wrapped indefineExhaustiveShape, andexport const entitySchema = z.strictObject(defineExhaustiveShape<CanonicalX>()(entityShape("<kind>", bodySchema, profileSchema)))). - Write
src/entities/<kind>/kind.ts:export default defineKind<"<kind>", Body>({ ... }). The accent must be a--deck-*token insrc/ui/theme/tokens.css; the icon a glyph id fromsrc/generated/ic-glyphs.json. - Optional:
parts.tswhen the body has a long item list that should save item by item (seekind-parts.ts), andcapabilities/for Kit (bun run capabilities). - Run
bun run kinds. - Name the kind's Workbench editor in its
kind.ts(editor: an id, a label and how much of the piece it reads),
then add the component tosrc/ui/apps/workbench/editors.tsunder that id. The completeness test fails on a
named editor with no component, and on a component no kind names. - Write
docs/reference/entities/<kind>.mdandscripts/scale/fixtures/<kind>.ts. - Run
bun test src/entities/kind-completeness.test.ts. It lists, per kind, every missing part and the file to
create. A part a kind deliberately goes without (an editor, Kit capabilities) is named in the test's exemption list
with the reason.
Behavior that belongs to one kind alone (a preset's readiness line in the Press, a lorebook's scan) stays in the code
that owns that behavior; the hub is for what every kind has. A scene's own fields (its preset with toggles and samplers,
its lorebook choices, its regex rack, its backdrop) keep their own shapes and rules in the scene store; sceneField
only says that a scene can point at the kind.
src/entities/kind-lists.test.ts keeps it that way: a source line naming two or more kind ids is a hand-written list,
and every file holding one is listed there with its count and the reason it is not a kind list. Read the hub instead of
adding to it.