ADR-0005: Schema conventions
- Status: Proposed
- Date: 2026-09-30
Context
Documents are written by the Director (through Claude strict tools or structured outputs), by
generators, by people in a text editor, and later by editor UIs. They are read by TypeScript in
the core and GDScript in the engine. Reelwright's ADR-0005 solved most of this, and its schema
machinery is copied into core/src/foundation/ (research/reelwright-reuse.md). Games add long
lists, grids, expressions and a vocabulary that grows with kits.
Decision
Inherit Reelwright's ADR-0005 in full:
- JSON Schema 2020-12 in
schemas/v1/, which are the source of truth; TypeScript types and GDScript classes are generated from them, and CI fails when they're stale. - Structured-output compatible, enforced by a lint: closed objects; no
oneOf,if/then,notorpatternProperties; no recursion; unions areanyOf+constdiscriminators. - No map-shaped objects; flat hierarchies with
parentids; an envelope ofkind,schemaVersion,id,name,meta; kind-prefixed ids; units in field names; JSON Pointer addressing; descriptions written as prompts;MAJOR.MINORversions; canonical files and JCS hashes; only the core writes pack documents.
Add for games:
- Edits and locks address list items by id.
{doc, item, pointer}resolves at apply time, so inserting an item never shifts other edits. Exprfields. Wherever a number or boolean is accepted, the schema acceptsnumber | string(orboolean | string), and the string is an L1 expression (ADR-0003), typechecked at bake.tune.<id>refers to a tuning knob.- Grids are strings or assets. Tile layers are run-length or base64 strings, or binary assets referenced by hash, never arrays of thousands of numbers.
- Kit vocabulary is generated. Component, action, event, function, pass and task names, and their parameter objects, are generated into the schemas from kit registrations.
- Every
patternhas a name. It sits on a named$defwhose title is a human phrase, so diagnostics can say what was expected. A lint checks it. - Case-insensitive enum matching in the validator, because constrained decoding doesn't guarantee case; no two enum values may differ only by case.
- The engine validates what didn't come through a bake. Codegen also emits a GDScript
validate()for the lint-enforced subset, for saves and dev-loaded documents. It reports the same diagnostic shape: severity, code, message, pointer, hint.
Alternatives considered
- Godot resources (
.tres) as the content format. They can embed scripts, only Godot reads them, and LLM tools can't be constrained to them. - Idiomatic JSON with maps and nesting. Shorter, but not writable through strict tools with guarantees, and nested trees make diffs and locks non-local.
Consequences
- One schema serves validation, editor autocompletion, the Director's tool definitions and codegen in two languages.
- Whole-document authoring tools exceed Claude's strict-tool budget (20 tools, 24 optional parameters, 16 unions per request). They stay non-strict with a validation and repair loop; small inputs are strict. Each pipeline stage sends only its own tools.