Files
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

65 lines
2.9 KiB
Markdown

# 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<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.