Files
vdm/contracts/codegen
samiandClaude Opus 5 53421d6cb8 proto: freeze the wire contract at 1.0.0
Schemas for the whole v1 surface: 38 methods, 9 events, 25 named types and the
JSON-RPC envelope, with x-privileged / x-transports / x-deadlineMs / x-errors
annotations that both generators emit as data rather than prose.

Four generators over one IR (contracts/codegen/schema_ir.py), so the C++ structs,
the TypeScript types and the OpenRPC document cannot disagree about what the
contract says:

  gen_cpp.py             -> core/generated/velox_proto.{hpp,cpp}
  gen_ts.py              -> extension/src/shared/protocol/
  gen_openrpc.py         -> contracts/openrpc.json
  gen_cpp_conformance.py -> tests/conformance/cpp/fixture_dispatcher.hpp

Inbound parsing never throws: parse<T>() returns std::expected<T, ParseError> and
nlohmann's throwing ADL from_json is deliberately not emitted. Schema constraints
(minimum, maxLength, pattern, ...) become real runtime checks in both languages —
the daemon does not trust the extension and the extension does not trust the
daemon.

59 golden fixtures: a success case per method, 12 error cases, 9 events. Replayed
by tests/conformance/ against both the generated C++ and a live server over both
transports. tools/mockd serves the same fixtures with unhappy-path flags so the
GUI and EXT lanes never wait for veloxd.

run.sh also proves capture.offer fails open: with a daemon answering slower than
750 ms the client gives up and lets Firefox take the download.

core/generated/ is libveloxproto, a separate target from libveloxcore, which
still never sees JSON — see docs/adr/0009.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012fgjnqFCS5h5L7gZTZo3rV
2026-09-09 19:55:54 +04:00
..

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:

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<std::string, T> / Record<string, T>
string + enum enum class / string-literal union
integer + enum + x-enum enum class : int32_t / as const object
array + items std::vector<T> / T[]
$ref to a types/*.schema.json the named type
["X", "null"], or oneOf: [X, {type: null}] std::optional<T> / 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<T>() -> std::expected<T, ParseError> 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.