Docs Decisions

Contents

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, not or patternProperties; no recursion; unions are anyOf + const discriminators.
  • No map-shaped objects; flat hierarchies with parent ids; an envelope of kind, schemaVersion, id, name, meta; kind-prefixed ids; units in field names; JSON Pointer addressing; descriptions written as prompts; MAJOR.MINOR versions; canonical files and JCS hashes; only the core writes pack documents.

Add for games:

  1. Edits and locks address list items by id. {doc, item, pointer} resolves at apply time, so inserting an item never shifts other edits.
  2. Expr fields. Wherever a number or boolean is accepted, the schema accepts number | string (or boolean | string), and the string is an L1 expression (ADR-0003), typechecked at bake. tune.<id> refers to a tuning knob.
  3. 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.
  4. Kit vocabulary is generated. Component, action, event, function, pass and task names, and their parameter objects, are generated into the schemas from kit registrations.
  5. Every pattern has a name. It sits on a named $def whose title is a human phrase, so diagnostics can say what was expected. A lint checks it.
  6. Case-insensitive enum matching in the validator, because constrained decoding doesn't guarantee case; no two enum values may differ only by case.
  7. 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.

From docs/adr/0005-schema-conventions.md in the Gamewright repository.