# contracts/codegen — the generators Four generators, one IR. Everything is derived from `contracts/schema/`; nothing here is a second source of truth. ``` schema_ir.py loads schema/ and lowers it to a small IR ├── gen_cpp.py -> core/generated/velox_proto.{hpp,cpp} ├── gen_ts.py -> extension/src/shared/protocol/*.ts ├── gen_openrpc.py -> contracts/openrpc.json └── gen_cpp_conformance.py -> tests/conformance/cpp/fixture_dispatcher.hpp ``` Regenerate everything: ```sh for g in gen_cpp gen_ts gen_openrpc gen_cpp_conformance; do python3 contracts/codegen/$g.py done ``` `tests/conformance/check_contract.py` re-runs all four and fails if any committed output differs, so stale generated code cannot be merged. ## The supported JSON Schema subset The generators refuse to guess. Anything outside this subset raises `SchemaError` at generation time rather than emitting subtly wrong code — a schema that cannot be generated from is a contract bug, and it should stop the build. | Supported | Emitted as | |---|---| | `object` + `properties` | struct / interface | | `object` + typed `additionalProperties` | `std::map` / `Record` | | `string` + `enum` | `enum class` / string-literal union | | `integer` + `enum` + `x-enum` | `enum class : int32_t` / `as const` object | | `array` + `items` | `std::vector` / `T[]` | | `$ref` to a `types/*.schema.json` | the named type | | `["X", "null"]`, or `oneOf: [X, {type: null}]` | `std::optional` / `T \| null` | | `{}` | `nlohmann::json` / `unknown` | | `minimum` `maximum` `minLength` `maxLength` `pattern` `minItems` `maxItems` | runtime checks in both languages | Deliberately unsupported: `allOf`, `anyOf`, general `oneOf`, `patternProperties`, tuple `items`, recursive types. If the contract needs one, extend `schema_ir.py` in the same PR that needs it. ## Two rules the generated code follows **Nothing throws on the inbound path.** `gen_cpp.py` emits `parse() -> std::expected` and deliberately does *not* emit nlohmann's ADL `from_json`, whose failure mode is an exception. A malformed frame off the wire is an ordinary value the RPC loop handles, not a throw unwinding through the daemon. **Constraints are checked, not just documented.** A `maximum` in a schema becomes an `if` in both languages. The daemon is not allowed to trust the extension and the extension is not allowed to trust the daemon — `ws://127.0.0.1` is reachable by every local process, so a type declaration proves nothing at runtime. ## Absent and null mean the same thing Both generators treat a missing field and an explicit `null` identically. A client that omits a nullable field and one that sends `null` get the same result, in both languages. This is stated here because it is the kind of asymmetry that otherwise surfaces as a cross-language bug six months later.