Manifests
An asset-generation test case version lives under test-cases/<type>/<difficulty>/<slug>/<version>/
and declares its contents in a test-case.toml manifest, the same versioned,
immutable catalog layout every
test type uses. Unlike an end-to-end manifest,
it does not describe a build that produces a static site; it describes the
canvas the model draws on, the drawing tool it draws with, and where the
recorded actions are collected. There is no target image: an
asset-generation case is human-reviewed against its brief, so it declares no
[[reference]] at all.
# test-cases/<type>/<difficulty>/<folder>/<version>/test-case.tomlslug = "imp-sprite" # stable identity (required); the store key + recorded in every runname = "Imp Sprite" # human-readable display name (site-facing)difficulty = "medium" # relative difficulty: easy | medium | hard (required)experimental = false # optional; true hides the case from the UI unless the deployment enables experimental cases (default false)tags = ["asset-generation", "2d", "sprite"] # classification tags (site-facing, required)summary = "..." # optional one- or two-sentence abstract for the site cards (inline; NOT seeded)description = "description.md" # optional site-facing prose (relative path; NOT seeded)changelog = "changelog.md" # REQUIRED per-version changelog entry (relative path; NOT seeded)prompt = "prompt.hbs" # the prompt template handed to the harness (required)max_runtime_hours = 0.5 # cap on the harness session before it's stopped (default 1)type = "asset-generation" # the test type (required for this type; defaults to "end-to-end")asset_kind = "sprite" # "sprite" (one sprite, the default) | "sprite-sheet" (per-frame files) # | a high-res painted kind: "ui" (interface art) — see "UI cases" # | "material" (tileable PBR material) — see "Material cases" # | a 3D voxel kind: "voxel-model"/"voxel-animation" (cube cells), # "mc-model"/"mc-animation", "sn-model"/"sn-animation", # "dc-model"/"dc-animation" (meshed) — see "Voxel cases" below # | a skinned character: "mc-skinned"/"sn-skinned"/"dc-skinned" # — see "Skinned cases" # | Blender-authored (headless Blender + tcab-blend): # "blender-character" (skinned) | "blender-prop" (static) | # "blender-mechanism" (rigid) — see "Blender cases" # | a particle effect: "particle-2d"/"particle-3d" — see "Particle cases" # | audio: "sfx-synth"/"sfx-sample"/"music" — see "Audio cases"
# Variants: an ORDERED list of paths to standalone variant files (first = default).# Because `variants` is a root key, it must appear BEFORE the first table header# (here `[canvas]`). Each path is relative to the version folder; by convention the# files live under `variants/`. See the variant-file example below.variants = ["variants/base.toml"]
# The image the model draws on. For a single sprite this is the whole canvas; for# a sprite sheet it is ONE frame (every frame is a separate file of this size).[canvas]width = 64 # canvas width in pixels (required)height = 64 # canvas height in pixels (required)background = "transparent" # initial canvas state: transparent | a hex color
# The drawing tool the model is given. The binary is the only way to make a mark:# it renders the current image after each call so the model can read its progress,# and records every call it receives. There is NO operations schema — the binary's# `--help` is the contract. Use `draw` for a sprite, `draw-sheet` for a sheet.[tool]binary = "draw" # the drawing binary available in the environment (required)preview = "canvas.png" # where the binary writes the current image to read # (a {frame} template for a sprite sheet, e.g. "frames/{frame}.png")
# Where the recorded operation log is collected and returned to The Test Cabinet.# This action log — not the pixels on disk — is the authoritative output. A sprite# sheet records one log per frame, so its path is a {frame} template.[output]actions = "actions.json" # the ordered record of every operation (required) # (a {frame} template for a sprite sheet, e.g. "frames/{frame}.actions.json")
# Only for asset_kind = "sprite-sheet": the frames the model draws (each a separate# file the size of [canvas]) and the named sequences a reviewer plays back. The# number of frames is just how many are declared.[sheet]
[[sheet.frame]] # >=1 required; one declared frameindex = 6 # the index it is written to (draw-sheet --frame 6), unique
[[sheet.frame]]index = 7
[[sheet.sequence]] # >=1 required; one named animation the UI plays backslug = "walk-right" # stable slug (required, unique within the sheet)name = "Walk Right" # display name (optional; default humanizes the slug)frames = [6, 7] # ordered frame indices (required, non-empty, each a declared frame)fps = 4 # playback rate in frames per second (required, > 0)
# An asset-generation case declares NO [[reference]]: it is reviewed against its# brief, with no target image to score the regenerated asset against. (Declaring# one — common or per-variant — is rejected.)
# Common specs, seeded for EVERY variant (the brief describing what to draw and# how the tool behaves). Same `source` → `dest` mapping as end-to-end, and `dest`# likewise defaults to `source` with a trailing `.hbs` removed — so most briefs# just name the source they seed.[[spec]]source = "specs/brief.md" # dest defaults to "specs/brief.md"
# The single scoring domain every asset-generation case declares — and its whole# review. A produced asset is judged as a WHOLE against its brief, so a case# declares NO [[review_item]]s: the one rating the reviewer gives here is the run's# overall rating (see "Judged on one overall rating" below).[[domain]]id = "overall"name = "Overall"description = "How good the produced asset is overall, judged against the brief." # requiredEach variants entry points at a standalone variant file, exactly as for an
end-to-end case — a TOML document whose top-level
keys are the variant’s own fields, with every path resolving against the version
folder. Here a variant varies the brief (an additive spec) the model draws
toward — a tighter palette, an operation budget, a required technique — not a
different reference (an asset-generation case declares none). A voxel case’s
variant may additionally declare its own [voxel] volume, overriding the case’s
so the same subject is sculpted at a different size (see Variants vary the volume,
too):
# test-cases/<type>/<difficulty>/<slug>/<version>/variants/base.tomlslug = "base" # stable slug, recorded in the run recordname = "Base" # display name (optional; default humanizes the slug)spec = [] # ADDITIVE specs on top of the common specs (dest defaults to source)# [voxel] # VOXEL cases only: OVERRIDE the case's bounding volume for this variant-
type = "asset-generation"is the explicit test-type discriminator. It is required for an asset-generation case; omitting it defaults to"end-to-end", which then rejects the[canvas]/[tool]/[output]tables. Resolution validates the tables against the declared type: an asset-generation case must declare[canvas],[tool], and[output], and must not declare a[build]table, any[[check]], or any[[reference]](it has no target to score against — declaring one, common or per-variant, is rejected). -
asset_kindchooses the shape of the asset within an asset-generation case:"sprite"(the default — one sprite drawn onto the whole canvas),"sprite-sheet"(a set of animation frames, each a separate file); the high-res painted kinds"ui"(an interface asset or kit — see UI cases) and"material"(a tileable PBR material — see Material cases); or one of the 3D voxel kinds — the cube kinds"voxel-model"(static) and"voxel-animation"(rigged, animated), or the meshed kinds"mc-model","sn-model","dc-model"(static) and"mc-animation","sn-animation","dc-animation"(rigged, animated); the skinned character kinds"mc-skinned","sn-skinned","dc-skinned"(see Skinned cases); the particle kinds"particle-2d","particle-3d"(see Particle cases); or the audio kinds"sfx-synth","sfx-sample","music"(see Audio cases). It is a property of the whole version, not a variant axis — a case is exactly one kind, never a mix, and a variant cannot change it.asset_kind(and the[sheet],[ui],[material],[voxel],[model],[particle], and[audio]tables) are only valid for an asset-generation case; an explicit value on any other type is rejected. The two 2D pixel kinds anduideclare a[canvas](auikit adds a[ui]table of elements); amaterialcase declares a[material]table; every voxel, meshed, and skinned kind declares a[voxel]volume instead (see Voxel cases); a particle kind declares a[particle]field; and an audio kind declares an[audio]table — a case declares exactly the one(s) its kind requires. -
The
[sheet]table is required for — and only for —asset_kind = "sprite-sheet". It declares the case’s frames as[[sheet.frame]]entries — each just theindexit is written to (passed asdraw-sheet --frame) — plus one or more[[sheet.sequence]]entries. The number of frames is just how many are declared; for a sprite sheet[canvas]describes one frame (every frame is a separate file of that size). Resolution validates that frame indices are unique, and that each sequence has a unique non-emptyslug, at least oneframesindex, every index a declared frame, andfps > 0. Each frame is regenerated independently, with no whole-sheet aggregate; the named sequences are surfaced to the reviewer and played back as live animations from the per-frame regenerated images (the sheet layout travels in the run record so the verdict page can animate from the run alone). -
The site-facing metadata (
name,difficulty,tags,summary,description), the requiredchangelog,prompt,max_runtime_hours, and the[[spec]]andvariantsseeding rules behave as they do for an end-to-end case: the case seeds a brief, renders a prompt, lists its variants as standalone files (the first the default), and each[[spec]]destdefaults to itssource. The difference is references: an asset-generation case declares none (unlike end-to-end, where the common set and each variant may add reference mockups). So a variant here varies only the seeded brief — an additive[[spec]]— that the model draws toward. There is no[build]table — an asset-generation run produces a recorded action log, not a static site. One rendering difference from end-to-end: a shared quality directive — the brief is the floor, not the goal; produce the best-looking asset you can within its constraints — is prepended to every asset-generation prompt at render time (the same wording for every case; it is not added for other test types), so a case’s ownprompt.hbsstays factual and need not restate it. -
reference_implementationis an optional per-variant key, declared in a variant file exactly as it is for an end-to-end case — but what it points at is different, because an asset-generation case has no[build]table and produces no site. Here it names a directory (by conventionreference-impl/<variant>/) holding adraw.sh: a script of nothing but calls to the case’s own drawing binary, drawing the authored correct sheet the same one-operation-at-a-time way a model must. Running it reproduces both the images and the action logs exactly, which is why neither is committed — the script is the only source of truth, and a committed image could silently drift from it.The script runs against a workspace seeded from this manifest (the same seeding a run gets), so it must never write
draw.config.json, never calldraw-sheet init, and never restate the[canvas]size or the declared frames: a script that disagrees with its case fails against the seeded config instead of quietly producing an off-size reference. Like an end-to-end one it is never seeded into a run — it is the answer. It is published out-of-band bytcab publish-reference, which runs the script and uploads the frames and logs it produced, and then appears on the case page’s Reference tab as playable sequences and per-frame images rather than an embedded site. Do not confuse it with[[reference]]— an asset-generation case still declares none of those (see above); the two keys are unrelated despite the shared word.Because the images are never committed, there is nothing in the repository to look at while authoring one. Render them locally instead:
Terminal window node scripts/preview-asset-reference.mjs <slug># or point it straight at a script you are iterating on:node scripts/preview-asset-reference.mjs path/to/reference-impl/<variant>/draw.shIt seeds a workspace from the manifest exactly as a publish does, runs the script, and writes the per-frame images, the action logs, and one animated GIF per declared sequence to
tmp/asset-previews/<slug>/<variant>/(ignored, but inside the repo so it is one click away). Open theindex.htmlit writes to see everything at once. The GIFs use the same encoder and settings as the console’s own download button, so a preview shows what a reviewer will actually see. -
The
[canvas]table fixes the image the model works on: itswidthandheightin pixels and its initialbackground. For a single sprite this is the whole canvas; for a sprite sheet it is one frame. Fixing it keeps runs comparable, the same way an end-to-end build interface does. -
The
[tool]table describes the drawing binary.binaryis the executable available in the run environment, baked into the shared run-container image —drawfor a single sprite,draw-sheetfor a sheet;previewis the path the binary re-renders the current image to after each call (a{frame}template for a sheet, one preview per frame), so the model can read a real image to see its progress. There is no operations schema — the binary’s--helpis the contract, and the brief tells the model to read it (see The drawing binaries). The binary is the only channel for drawing — anything produced outside it is discarded (see Overview). -
The
[output]table names theactionslog the binary records and returns (a{frame}template for a sheet, one log per frame). This ordered list of operations is the authoritative output; The Test Cabinet regenerates the reviewed image from it. -
There are no targets. An asset-generation case is judged by a human against its brief, so it seeds only the brief (and the blank canvas/config the binary writes into) — no goal image, and no
[[reference]].
Judged on one overall rating
Section titled “Judged on one overall rating”Unlike an end-to-end or
full-stack case, an asset-generation case
declares no reviewer checklist — no [[review_item]] on the case and none on
any variant. Whether a sprite reads as the creature the brief describes, whether a
walk cycle has weight, whether a material sits right under light: these are
judgments about the asset as a whole, too subjective to decide as a list of
pass/fail points without inventing false precision.
So the whole review is one rating. Every asset-generation case declares the same single scoring domain:
[[domain]]id = "overall"name = "Overall"description = "How good the produced asset is overall, judged against the brief."The reviewer looks at the regenerated asset (and, for a sheet, plays back every
declared [[sheet.sequence]]), reads the brief, and gives it one
rating. Because a run’s overall rating is the
worst across its domains and there is exactly one, that rating is the run’s
rating. The run’s point score — the earned-over-available weight a checklist
produces — is simply empty for these cases; the rating carries the whole verdict.
That puts the weight on the brief: every requirement a checklist item would have named must be stated there, since the brief is both what the model is asked to satisfy and what the reviewer rates it against.
UI cases
Section titled “UI cases”A ui case produces a high-resolution interface
asset — one image or a
kit of named elements — painted with the
paint and ui binaries. It reuses
[canvas] for the base element size and adds an optional [ui] table declaring
the kit’s elements; omit [ui] for a single full-canvas image. Everything else on
the page — type, variants, [[spec]], the single [[domain]], the
no-[[reference]]/no-[build]/no-[[check]] rules — behaves exactly as above.
asset_kind = "ui"
# The base element size (and single-image size) and initial background — the same# [canvas] table a sprite uses.[canvas]width = 512height = 512background = "transparent"
# The drawing tools. `binary` names the PRIMARY tool (`paint`); the companion `ui`# binary (vector shapes, text, nine-slice) ships in the SAME run-container image and# is on PATH — the brief directs the model to both. `preview` is a {element} template# for a kit, a single file for a single-image case.[tool]binary = "paint"preview = "elements/{element}.png" # or "canvas.png" for a single-element case
# The recorded op log — a SINGLE interleaved record for the whole asset (each op# carries --element). Core emits the flattened per-element PNG(s) and ui.json# automatically (not manifest-declared).[output]actions = "actions.json"
# OPTIONAL: a KIT of named elements (omit for a single full-canvas image). Each# element is its own document of its own size — the interface analogue of a sheet's# frames.[ui]
[[ui.element]] # >=1 when [ui] is present; a declared elementname = "panel" # stable, unique name (draw with --element panel)width = 512 # element width in pixels (required)height = 320 # element height in pixels (required)nine_slice = { left = 24, right = 24, top = 24, bottom = 24 } # OPTIONAL fixed insets
[[ui.element]]name = "button-primary"width = 256height = 72- The
[ui]table is optional and valid only forasset_kind = "ui". When present it declares one or more[[ui.element]]entries — each aname(unique) and itswidth/height; anine_slice(left/right/top/bottom) may fix the stretchable insets, otherwise the model authors them withui set-nine-slice. When[ui]is absent, the case has a single implicit element (the whole[canvas]). Resolution validates that element names are unique and that any fixednine_sliceinsets fit within the element’s bounds. [tool].binarynames the primary painter (paint); the companionuibinary is baked into the sameuiimage and available onPATH.[tool].previewcarries the{element}token when[ui]declares elements, and is a single file otherwise (as a sheet’spreviewcarries{frame}).[output].actionsis a single interleaved op log — not an{element}template — since the two binaries share one recorded stream. The emitted per-element PNGs and theui.json(element sizes, nine-slice insets, atlas rectangles) are produced automatically by core, so they are not manifest-declared (see the output contract).
Material cases
Section titled “Material cases”A material case produces a tileable PBR
material — a set of maps
(base color, and any of normal, roughness, metallic, ambient occlusion, emissive) —
painted with the texture and pbr
binaries. It replaces
[canvas]/[voxel] with a [material] table and declares no [model] — a
material is judged subjectively against its brief, with no required-animation
contract, and a case authors one material (as a single sprite is one image).
asset_kind = "material"
# The maps the material carries and how they are baked.[material]size = 512 # square map resolution in pixels, a power of two (required)tile = true # seamless authoring: brushes/gradients/filters wrap across # the map edges so it tiles without a seam (default true)maps = ["base-color", "normal", "roughness", "metallic", "ao"] # the channels the material emits; "base-color" is REQUIRED, # the rest optional — a subset of: base-color | normal | # roughness | metallic | ao | emissivebackground = "transparent" # preview clear color only
# The tools. `binary` names the PRIMARY painter (`texture`); the companion `pbr`# binary (bake normal/AO, uniforms, assemble, 3D preview) ships in the SAME image and# is on PATH. `preview` is a {map} template — one preview per map, shown 2×2-tiled.[tool]binary = "texture"preview = "maps/{map}.png"
# The recorded op log — a SINGLE interleaved record (each op carries --map). Core# emits the per-map PNGs and material.json automatically (not manifest-declared).[output]actions = "actions.json"- The
[material]table fixes the material’s output: its squaresize(a power of two), whether it is authoredtileable (seamless — the default, required for triplanar application), and themapsit emits.mapsmust includebase-colorand is otherwise any subset ofnormal,roughness,metallic,ao,emissive. (Theheightchannel a case bakes relief from is an authoring aid, not an emitted map, so it is not declared here.) It is required for — and only for — a material case, and replaces[canvas]/[voxel]. [tool].binarynames the primary painter (texture); the companionpbrbinary is baked into the samematerialimage and onPATH.[tool].previewcarries the{map}token (one preview per declared map);[output].actionsis a single interleaved op log — not a{map}template — since the two binaries share one recorded stream. Core emits one PNG per declared map (maps/{map}.png) plus thematerial.json(paths, per-map color space, and the world-space tiling scale) automatically; neither is manifest-declared (see the output contract).
Voxel cases
Section titled “Voxel cases”A voxel case produces a 3D asset instead of a 2D image. There are eight voxel
kinds in two families: the cube kinds (asset_kind = "voxel-model" or
"voxel-animation"), which sculpt discrete opaque cells, and the meshed kinds
("mc-model"/"mc-animation", "sn-model"/"sn-animation",
"dc-model"/"dc-animation"), which extract a surface from a signed-distance field
(see Meshed voxel models).
Every voxel kind replaces the [canvas] table with a [voxel] table and reuses
[tool] and [output] unchanged in shape; every animated kind (any
-animation) adds a [model] table declaring the
required animations
the model must author (the rig’s parts and joints are the model’s to invent).
Everything else
on the page — type, variants, [[spec]], the single [[domain]], the
no-[[reference]]/no-[build]/no-[[check]] rules — behaves exactly as above.
# A static voxel model (asset_kind = "voxel-model").asset_kind = "voxel-model"
# The bounding volume the model sculpts into — the 3D analog of [canvas]. Cells# are OPAQUE #rrggbb (no alpha) and the volume starts EMPTY.[voxel]width = 32 # extent along x, in voxels (required)height = 32 # extent along y — up — in voxels (required)depth = 32 # extent along z, in voxels (required)background = "transparent" # PNG preview clear color only: transparent | a hex color # (it never places a voxel; the volume is always empty to start)
# The building binary. `voxel` for a static model, `voxel-anim` for an animated# one. `preview` is where the binary writes the wgpu PNG preview when the model runs# `render` (voxel rendering is on request, not after every op).[tool]binary = "voxel" # the voxel binary in the environment (required)preview = "model.png" # where the preview PNG is written (a {part} template for voxel-animation)
# Where the recorded operation log is collected. A cube case names its op log here;# a meshed case names its emitted mesh instead (see below).[output]actions = "actions.json" # the ordered op record (a {part} template for voxel-animation)For an animated case, [tool].preview and [output].actions become
{part} templates (one preview and one log per part, e.g.
parts/{part}.png and parts/{part}.actions.json), and the case adds a
[model] table:
# A rigged, animated voxel model (asset_kind = "voxel-animation").asset_kind = "voxel-animation"
[voxel]width = 32height = 24depth = 32background = "transparent"
[tool]binary = "voxel-anim" # required for voxel-animationpreview = "parts/{part}.png" # {part} REQUIRED for voxel-animation
[output]actions = "parts/{part}.actions.json" # {part} REQUIRED for voxel-animation
# The REQUIRED rig contract — and the ONLY thing the case fixes about the rig: the# set of animations the model must author. A case declares NO parts and NO joints —# the model INVENTS whatever skeleton the subject needs (parts, joints, pivots,# ranges) at run time and is scored on whether it worked out the right pieces and# animated them. [model] holds ONLY [[model.animation]] entries. Required for — and# only for — asset_kind = "voxel-animation".[model]
# A REQUIRED animation the model must author, declared by IDENTITY ALONE — its name# and whether it loops / self-plays. The case fixes NO parts, joints, period, or# keyframes: the model invents the rig it needs and authors the motion (the F-curve# keyframes, and the period) at run time with the voxel-anim# `define-animation`/`add-keyframe` subcommands, the produced rig.json carries it, and# the reviewer scores the motion it produced against the brief.[[model.animation]]name = "walk" # stable, unique name a game plays this animation by (required)loop = true # loop (true, the default) or play once and hold the last pose (false)auto_play = false # false (default) = a named playable a game triggers (walk, recoil); # true = plays continuously on its own (a decorative idle, e.g. a radar spin)A meshed case (asset_kind beginning mc-, sn-, or dc-) is identical in
manifest shape to a cube case — it frames the same [voxel] volume, and its
animated kinds carry the same [model] rig — but its [tool].binary is the
corresponding meshing binary. Its [output] names the op log the binary
records — exactly like a cube case — while the extracted .glb (binary glTF)
triangle geometry the surface extractor produces is emitted to a path core provides
automatically (it is not declared in the manifest):
# A static meshed model (asset_kind = "dc-model"; mc-model / sn-model are identical# in shape — only the binary differs).asset_kind = "dc-model"
[voxel]width = 48 # the field bounds — the same volume table as a cube case,height = 48 # here framing the signed-distance field the surface isdepth = 48 # extracted from (see "Meshed voxel models")background = "transparent"
[tool]binary = "dc" # the meshing binary: mc | sn | dc (static), # mc-anim | sn-anim | dc-anim (animated)preview = "model.png" # where the wgpu preview PNG is written (a {part} template for an animated kind)
[output]actions = "actions.json" # the recorded op log (as for a cube case); the extracted # .glb geometry is emitted automatically by coreAn animated meshed case (mc-animation, sn-animation, dc-animation) uses the
-anim binary and, exactly like voxel-animation, makes [tool].preview and
[output].actions {part} templates and adds the [model] rig table:
# A rigged, animated meshed model (asset_kind = "sn-animation").asset_kind = "sn-animation"
[voxel]width = 40height = 32depth = 40background = "transparent"
[tool]binary = "sn-anim" # required for sn-animation (mc-anim / dc-anim for the others)preview = "parts/{part}.png" # {part} REQUIRED for an animated kind
[output]actions = "parts/{part}.actions.json" # {part} REQUIRED for an animated kind
# The REQUIRED rig contract — the required animations, declared EXACTLY as for# voxel-animation (the [[model.animation]] entries above; NO parts or joints — the# model invents those at run time). Required for — and only for — an animated kind.[model]- The
[voxel]table fixes the bounding volume: itswidth,height(up), anddepthin voxels, and thebackgroundused only as the preview PNG’s clear color — it never places material, because the volume always starts empty. For a cube case it bounds the sculpted cells; for a meshed case it frames the signed-distance field the surface is extracted from. It is required for — and only for — a voxel case, and replaces[canvas]: a voxel case declaring[canvas], or a 2D case declaring[voxel], is rejected. Voxel material is opaque#rrggbb(there is no alpha). The volume is the case’s default: a variant may declare its own[voxel]to override it (see Variants vary the volume, too), so the same subject can be sculpted at more than one size. - The
[tool]and[output]tables work exactly as for a sprite, with the voxel binaries.[tool].binaryis the binary for the kind:voxel/voxel-animfor the cube kinds, andmc/sn/dc(static) ormc-anim/sn-anim/dc-anim(animated) for the meshed kinds (see The voxel binaries). Which binary a case names fixes the output’s character — a cube volume, or anmclow-poly,snsmooth, ordcsharp-edged surface — it is not a manifest knob. Every voxel case’s[output]names itsactionsop log; for a meshed case core also emits the extracted geometry automatically (mesh.glb, ormeshes/{part}.glbper part for an animated kind), so it is not manifest-declared. For an animated kind — where each part is authored, previewed, and emitted separately —previewand the[output]path must carry the{part}token (as a sheet’s carry{frame}); for a static kind they name single files and must not carry{part}. - The
[model]table is required for — and only for — an animated voxel kind (voxel-animationor any meshed-animation:mc-animation,sn-animation,dc-animation). It declares only the case’s required[[model.animation]]declarations — the set of animations the model must author. It declares no parts and no joints: the rig’s parts, joints, pivots, and ranges are model-invented at run time (with thedefine-part/define-jointsubcommands), never fixed by the case. Resolution validates only that every[[model.animation]]has a uniquename; there is nothing else on the table to validate. [[model.animation]]entries declare the animations the model must author — the timeline motions a game plays (a walk, a recoil, a decorative idle) and the reviewer scores. Each declares just its identity: a uniquename, aloopflag (loop vs. play once and hold, defaulttrue), and anauto_playflag (defaultfalse; whether it plays continuously on its own — a decorative idle such as a radar spin — versus a named playable a game triggers). It fixes no parts, joints, period, or keyframes: the model invents whatever rig realizes the animation and authors the motion as F-curves (per-keyframeconstant/linear/bezierinterpolation plusease-in/ease-out/ease-in-outpresets — see The voxel binaries), choosing the period, with thevoxel-animanimation subcommands. The produced animations are carried inrig.json, exported to glTF for a game to play, and reconciled against these declarations — a required animation that is missing, or that never actually animates, is a contract gap.- The required animations are the contract; the rig is the model’s to invent.
[model]fixes only the animations the model must produce — a case says WHAT the thing is and HOW it must move, never the pieces that move. Every part, joint, and pivot is model-invented at run time with thevoxel-animrig subcommands; the producedrig.jsoncarries the whole rig the model built. A rig’scallerjoints are the procedural interface a game drives per frame (turret yaw, gun pitch), exported as machine-readable metadata; its animations are the baked clips a game plays. The review UI reconciles the produced animations against the required set, surfaces caller joints as controls, and plays the produced animations; the 3D viewer poses the full rig. See Evaluation.
Variants vary the volume, too
Section titled “Variants vary the volume, too”For a voxel case the bounding volume is a variant axis: a variant may declare
its own [voxel] table, which replaces the case’s [voxel] for runs of
that variant (like a variant’s workspace, it overrides rather than layers). A
variant with no [voxel] inherits the case’s volume. This is how a case offers the
same subject at several sizes — the idiomatic set is a base variant (no
override, so it inherits the case volume), a half variant, and a double variant,
each with its own [voxel]:
# variants/double.toml — the same subject in a doubled volume.slug = "double"name = "Double Size"
[voxel]width = 100 # the case's width, doubledheight = 40depth = 152background = "transparent"Resolution validates a variant’s [voxel] exactly as the case’s — every extent
> 0, the background parses — and rejects a [voxel] on a variant of any
non-voxel case (only a voxel case has a volume to vary). The size a variant runs
at flows to everything that reads the volume: the tool config the binary is seeded
with, the volume the produced model is scored against, and the brief.
So a voxel brief should not hardcode its dimensions. Its [[spec]] is written as
a Handlebars template (a .hbs source; the seeded dest drops the .hbs), and the
spec-template context exposes the
effective volume as {{voxel}}: {{voxel.width}}/{{voxel.height}}/{{voxel.depth}}
for the extents and {{voxel.maxX}}/{{voxel.maxY}}/{{voxel.maxZ}} for the highest
index on each axis (so an inclusive coordinate range reads `0`–`{{voxel.maxX}}`).
The same {{voxel}} context is available in the case’s prompt.hbs. One brief then
reads correctly at every size. (Because coordinates are size-dependent, a voxel case’s
brief and [[domain]] text should describe the form without citing specific
coordinates or exact extents — a reviewer judges the shape, not numbers.)
Skinned cases
Section titled “Skinned cases”A skinned case (asset_kind = "mc-skinned", "sn-skinned", or "dc-skinned")
produces a character:
a single continuous skin bound to a model-invented skeleton, deforming across its
joints. Its manifest is a meshed animated case with one difference — the model
builds one whole-body field, not a field per part — so its [tool].preview and
[output].actions are single files (not {part} templates), even though it is
an animated kind carrying a [model] table:
# A skinned character (asset_kind = "sn-skinned"; mc-skinned / dc-skinned differ# only in the binary and its surface character).asset_kind = "sn-skinned"
[voxel]width = 40 # the field bounds — the same volume table a meshed caseheight = 48 # frames, here bounding the one whole-body field the skindepth = 24 # is extracted frombackground = "transparent"
[tool]binary = "sn-skin" # the skinned binary: mc-skin | sn-skin | dc-skinpreview = "model.png" # a SINGLE file — NOT a {part} template (one field, one mesh)
[output]actions = "actions.json" # a SINGLE op log — NOT a {part} template; the skinned # mesh.glb + rig.json are emitted automatically by core
# The REQUIRED animations, declared EXACTLY as for voxel-animation — by identity# alone. The skeleton, its bones, joints, and per-vertex binding are all# model-invented at run time; the case fixes only the animations.[model]
[[model.animation]]name = "walk"loop = trueauto_play = false- A skinned case declares a
[voxel]volume (the field bounds), like a meshed case, and a[model]table of required animations, like any animated kind — the skeleton, joints, and weights are the model’s to invent. Because it builds a single field,[tool].previewand[output].actionsare single files and must not carry{part}— this is the one animated kind that does not. Core emits the skinnedmesh.glb(geometry plus the glTF skin — per-vertex bone weights and inverse-bind matrices) andrig.json(the skeleton, the joint interface, and the F-curve animations) automatically; neither is manifest-declared. See The skinned binaries.
Blender cases
Section titled “Blender cases”The Blender kinds — blender-character, blender-prop, and
blender-mechanism — are authored by driving headless Blender through its Python
API instead of a constrained op-log tool, and each emits a native glTF the validator
decodes (see Blender binaries). All three
share one channel: the model writes a build.py (a bpy script) and runs the
tcab-blend runner, which exports the glTF and a model.png preview. The emitted
glTF is the authoritative, judged output; there is no operation log — build.py
is the recorded authoring trace, re-run for provenance. All three reuse [voxel] as a
bounding box. They differ in what they emit and whether they animate:
asset_kind | rig | [model] | emitted glTF |
|---|---|---|---|
blender-character | skinned (armature + weights) | required | character.glb |
blender-mechanism | rigid (parented node clips) | required | model.glb |
blender-prop | none (static) | forbidden | model.glb |
A blender-character produces a rigged, animated skinned
character — like the CSG
skinned kinds. Its build.py builds the character mesh, an armature it
invents, the skin weights, an empty weapon_socket bone, and one Action per required
animation, exporting a skinned + animated character.glb:
asset_kind = "blender-character"
# The character's BOUNDING BOX — the volume the whole character must fit within, in world# units (width x, height y-up, depth z; forward is +z). This is the [voxel] table reused# as a bounds box (a Blender character is a real mesh, not a voxel field); `background` is# the preview clear color only. A blender-character case must NOT declare [canvas].[voxel]width = 24height = 48depth = 20background = "transparent"
# The authoring tool. `tcab-blend` runs the model's `build.py` under headless Blender# (`blender --background --python build.py -- blender.config.json`), exports the glTF, and# renders the preview. `preview` is a SINGLE file (one mesh — no `{part}` token).[tool]binary = "tcab-blend"preview = "model.png"
# The authored `build.py` IS the recorded output/trace — NOT an op log. The validator# re-runs it for provenance. The emitted skinned `character.glb` is produced by the runner# and is NOT manifest-declared (core provides its path).[output]actions = "build.py"
# The required animations, declared EXACTLY as for the skinned/voxel-animation kinds — by# identity alone. The skeleton, weapon socket, and weights are the model's to invent.[model]
[[model.animation]]name = "idle"loop = trueauto_play = true
[[model.animation]]name = "run"loop = trueauto_play = false
# The self-contained brief, plus the `build.py` STARTER STUB seeded to the workspace root# (the path `[output].actions` names) so the model edits it in place.[[spec]]source = "specs/brief.md"
[[spec]]source = "specs/build.py"dest = "build.py"kind = "script" # tag it "Script" (not "Spec") on the Inputs tab-
The
[voxel]table is the character’s bounding box (not a voxel field): itswidth/height/depthin world units and abackgroundused only as the preview clear color. It replaces[canvas]. Like a voxel case it is a variant axis — a variant may declare its own[voxel]to author the same character at another size. -
[tool].binaryistcab-blendand[output].actionsis the authoredbuild.py— the recorded trace, re-run for provenance, not an operation log. Both are single files and must not carry a{part}token. Becausebuild.pyis authored by the model from a seeded starter stub, the stub is seeded as the case’s own[[spec]]withdest = "build.py"(landing at the run root), and it is the one case where a specdestdeliberately coincides with[output].actions. That spec setskind = "script"so the run’s Inputs tab tags the starterScriptrather thanSpec— a presentation-only marker that does not change how the file is seeded (see the end-to-end[[spec]]reference). -
[model]fixes the required animations (each a uniquename, aloopflag, and anauto_playflag), exactly as for the skinned cases. The skeleton, theweapon_socketbone, the per-vertex weights, and the keyframes are all model-invented inbuild.py. -
[model]also fixes the required caller DOFs — the runtime-drivable joints a game sets each frame to aim the asset (a turret’sturret_yaw, a character’saim_pitch) — as[[model.joint]]entries. Each declares aname, akind(rotation/translation), anaxis(x/y/z, named in the emitted Y-up glTF frame), andmin/max/restlimits (rotation limits in degrees, translation in world units). The model builds the driven node and tags its glTFextraswith atcab_jointdescriptor (via a Blender custom property +export_extras), so the interface travels in the emitted glTF — read by a game asuserData, and driven live in the review UI.[[model.joint]]is optional (a prop declares none); the clips a game plays and the DOFs a game drives are the two halves of the game-facing contract. See Runtime control.[[model.joint]]name = "turret_yaw" # a game drives this by namekind = "rotation" # "rotation" | "translation"axis = "y" # in the emitted Y-up glTF frame (yaw=y, pitch=x)min = -170.0 # degrees (rotation); world units (translation)max = 170.0rest = 0.0 -
character.glbandmodel.pngare runner-emitted, not declared. The skinned, animated glTF (character.glb) and the preview (model.png) are produced bytcab-blend, never named in the manifest. -
No
[[reference]], no[build], no[[check]]. Judged on the emitted glTF plus a reviewer’s judgment. There is no cheat-divergence check (that is only for thedraw/draw-sheetsprite kinds); instead the validator re-runsbuild.pyand records any divergence from the emitted glTF — the Blender analogue, recorded not gated (see Blender validation). -
The orchestrator seeds a
blender.config.json(the bounding box, the axes, the output paths, and the required animation names) the runner andbuild.pyread.
blender-prop (static) and blender-mechanism (rigid)
Section titled “blender-prop (static) and blender-mechanism (rigid)”The other two Blender kinds are identical in shape — [voxel] bounding box, tcab-blend
tool, build.py output, the seeded starter stub — and differ from the character only as
the table above shows:
- A
blender-propis a static hard-surface model (a weapon, crate, pickup). It declares no[model]table (it is unrigged — no armature, no animations), and itsbuild.pyjust builds geometry. The runner emitsmodel.glb(a native, unrigged glTF). The validator confirms a well-formed glTF with at least one mesh; there is no skin requirement and no animation to reconcile. - A
blender-mechanismis a rigidly-articulated model (a turret, door, crane). Itsbuild.pybuilds separate parented parts and authors motion as object transforms, exported as native glTF node-hierarchy animations (not skin deformation and not arig.json). It requires a[model]table of required animations, declared exactly as for the character, and emitsmodel.glb. The validator reconciles the emitted glTF’s animations against the required set (as the character does) but does not require a skin.
# A static prop: no [model], emits model.glb.asset_kind = "blender-prop"
[voxel]width = 8height = 16depth = 48background = "transparent"
[tool]binary = "tcab-blend"preview = "model.png"
[output]actions = "build.py"# (no [model] — a prop is static)# A rigid mechanism: [model] required (node-hierarchy clips), emits model.glb.asset_kind = "blender-mechanism"
[voxel]width = 24height = 30depth = 24background = "transparent"
[tool]binary = "tcab-blend"preview = "model.png"
[output]actions = "build.py"
[model]
[[model.animation]]name = "idle"loop = trueauto_play = true
[[model.animation]]name = "fire"loop = falseauto_play = falseParticle cases
Section titled “Particle cases”A particle case (asset_kind = "particle-2d" or "particle-3d") produces a
particle effect: the model
authors an emitter system the review UI and a game simulate live. It replaces
[canvas]/[voxel] with a [particle] table and declares no [model] — a
particle effect is judged subjectively against its brief, with no required-animation
contract, and a case authors one effect (as a single sprite is one image).
# A 3D particle effect (asset_kind = "particle-3d").asset_kind = "particle-3d"
# The field the effect plays in and how it is baked. A particle-2d case gives# width/height only (a 2D field, like [canvas]); particle-3d adds depth (a volume,# like [voxel]).[particle]width = 48 # extent along x (required)height = 48 # extent along y — up (required)depth = 48 # extent along z (required for particle-3d; omitted for particle-2d)duration_ms = 1500 # the effect's length in milliseconds (required)fps = 60 # the preview/playback frame rate (required, > 0)loop = false # one-shot (an explosion, default) or looping (fire, smoke)background = "transparent" # preview clear color only
[tool]binary = "particle-3d" # the particle binary: particle-2d | particle-3dpreview = "effect.gif" # where the binary writes the preview animation
[output]actions = "actions.json" # the recorded op record; the emitted system.json is # emitted automatically by core- The
[particle]table fixes the field the effect plays in —width/height(and, forparticle-3d,depth), itsduration_msand playbackfps, and whether the effectloops — and, like every other kind, abackgroundused only as the preview’s clear color. It is required for — and only for — a particle case, and replaces[canvas]/[voxel]. There is no simulation seed: a particle effect is simulated live (not baked), so it varies slightly from one play to the next, exactly as a real particle editor plays a system. - Core emits the authored
system.json(the emitter/force/curve definition the review UI and a game simulate live) automatically; it is not manifest-declared. See The particle binaries.
Audio cases
Section titled “Audio cases”An audio case (asset_kind = "sfx-synth", "sfx-sample", or "music") produces
a short audio clip. It replaces
[canvas]/[voxel] with an [audio] table, declares no [model] (a clip is
judged subjectively against its brief), and authors one clip per case.
# A sample-library sound effect (asset_kind = "sfx-sample").asset_kind = "sfx-sample"
[audio]sample_rate = 44100 # output sample rate in Hz (required)channels = "stereo" # "mono" | "stereo" (required)max_duration_ms = 5000 # cap on the rendered clip's length in ms (required, positive)sample_pack = "naval-weapons@1" # for sfx-sample: the baked sample pack (name@version) # instrument_bank = "orchestral@1" # for music: the baked instrument bank
[tool]binary = "sfx-sample" # the audio binary: sfx-synth | sfx-sample | musicpreview = "waveform.png" # where the binary writes the waveform + spectrogram # (a piano-roll as well, for music)
[output]actions = "actions.json" # the recorded op record; the rendered clip.wav (and, for # music, the portable clip.mid) are emitted automatically- The
[audio]table fixes the output format: itssample_rate,channels, andmax_duration_ms(any positive clip-length cap). Asfx-samplecase additionally names thesample_packit mixes over, and amusiccase names theinstrument_bankit plays — each aname@versionidentifying the palette baked into the run-container image, never a path in this repo (see the sample library). Themusicimage bakes every instrument bank, soinstrument_bankselects which one; the banks available today aregm-lite@0.1.0(broad general-MIDI),cinematic@0.1.0(epic orchestral — strings, brass, choir, orchestral percussion), andsynthwave@0.1.0(analog synths, pads, FM bells, an electronic drum machine). Asfx-synthcase names neither — it synthesizes from oscillators alone. - Core emits the rendered
clip.wav(and, formusic, a portableclip.midscore) automatically; neither is manifest-declared. Because the asset is a finished waveform, an audio case has no produced rig or system a runtime plays — the clip is simply played. See The audio binaries.