What compiles from a schema, and what does not

September 20, 2026 · View on GitHub

The full mapping behind jevc compile <schema.json>. The README carries the four rows that cover almost every real schema; this file is the rest.

fromJsonSchema is pure and property-tested — no model, no network. Zod, Anthropic tool input_schema, OpenAI strict json_schema and MCP inputSchema all normalize to JSON Schema first, so there is one mapper.

Schema constructMaps toNote
booleannouldescription becomes the instructions
string + enumchoiceenum members become criteria keys, with no per-member description (null). A non-string member is rendered with JSON.stringify, so {a: 1} becomes the option name {"a":1}; two members that render to the same name are dropped, not merged
oneOf/anyOf of constchoicea const union is an enum, and each member's own description becomes that option's criteria value — the one spelling that carries per-option prose
allOfmerged, then mappedmembers compose into one effective schema, which is what a $ref plus local overrides becomes once resolved
anyOf/oneOf of [X, null], or type: ["X", "null"]mapped as Xthe Pydantic v2 and OpenAI strict spelling of an optional field; a union of two decidable branches names no single decision and stays dropped
integer + minimum: 0/maximum, span 2..10scoreone level per value, labelled <dotted id> = i (a.risk = 0) — i is both the schema's value and the answer's level index. Those labels carry no meaning, and lintProgram says so on every one of them: score_levels_undescribed, a warning, with the two schema shapes that carry per-level prose instead. The draft-6+ numeric exclusiveMinimum/exclusiveMaximum are honoured, so {minimum: 0, maximum: 5, exclusiveMaximum: 5} is a 5-level score, not 6
integer + minimum other than 0, span 2..10droppeda score answer is a level index 0..n-1, so a threshold written in the schema's numbers fires minimum levels early and nothing in the Program records the offset; re-base the range, or bucket it into described levels
integer with gapped or unreadable bounds — multipleOf other than 1, or the draft-04 boolean exclusiveMinimum/exclusiveMaximumdroppeda score's levels are the contiguous indices 0..n-1, so a gap or a bound this mapper cannot read would offer the model a level the schema forbids
array of enumone noul per member (field.member)several labels may apply at once, and each noul answers its own label exactly once — which is why uniqueItems is ignored
array of enum with minItems > 0 or maxItems < member countdroppedone independent noul per label records no cardinality, so a declared single-select would ship as an unarbitrated multi-select. Spell a single-select as a plain enum
nested objectrecurse; ids flattened dotted (a.b)the questions map is flat, and the dotted id is load-bearing rather than cosmetic: it is what scopes the generated question text, and therefore what makes two sibling risk fields distinguishable to the model
a root with no properties — a $ref root, a bare-enum root, an array root, {}dropped, exit 1$ref is not resolved, and the drop names what it found rather than returning a silently empty Program
string (free)residualtext generation
array of objectresidualunbounded extraction
number (any)droppeda continuous range has no discrete-level equivalent; bucket it, or model a 0..1 probability as a noul
integer spanning > 10 valuesdroppeda score takes at most 10 levels
nulldroppedno decision to make
constignoredno decision to make, whatever it is attached to — {type: 'boolean', const: true} and {enum: […], const: 'a'} compile to nothing, not to a question with one answer
two things claiming one id — a property named "a.b" beside a nested a: {b}, or enum members 1 and "1"both dropped, exit 1the intent is unrepresentable as written rather than unsupported; renaming one of them fixes it

Notes

required and default are ignored: Jev answers every question in the map, always.

A dropped cell means one of two exit codes. An unsupported drop — no Jev equivalent — prints as dropped: on stderr and the compile still succeeds at exit 0; the artifact is everything Jev can represent of what you wrote. A collision drop prints as error: and exits 1 without writing anything, however many other decisions survived, because there is no artifact that asks what the schema asked. So does a schema that compiles to no decisions at all. fromJsonSchema returns a SchemaProgram, whose dropped entries carry kind: 'collision' | 'unsupported'; branch on kind, never on the reason prose.

A two-member enum whose values are yes/no-shaped could collapse to a noul. That heuristic is off by default (collapseBooleanEnums) — silently changing a declared output's shape is exactly the class of surprise this project exists to remove.