Files
vdm/docs/adr/0009-generated-protocol-library.md
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

2.6 KiB

ADR 0009 — libveloxproto: generated protocol code is not part of libveloxcore

Status: accepted · Date: 2026-09-09 · Lane: PROTO

Context

Two rules in CLAUDE.md appear to collide, and a later agent will notice:

  • the layering rule says core → no JSON, no SQL, no Qt, no RPC. Ever.
  • the PROTO brief says gen_cpp.py emits core/generated/velox_proto.{hpp,cpp}, with to_json/from_json built on nlohmann.

Read together they say core must not know about JSON, and also that a JSON serialiser goes in core/. Left unresolved, someone eventually "fixes" it by moving the generated code, or by deleting the layering rule.

Decision

core/generated/ is its own CMake target, libveloxproto, and is not part of libveloxcore. The layering rule constrains libveloxcore — the engine — which continues to know nothing about JSON, SQL, Qt or RPC. libveloxproto is the wire types, which are by definition JSON, and it is linked by veloxd, the CLI, the GUI and the conformance runner.

The directory is core/generated/ because the PROTO brief and the roadmap both name that path, and moving it would break a written interface for a cosmetic gain.

libveloxcore must not link libveloxproto. The daemon's job is to project the engine's state into the contract's types; if the engine ever needs to know what a TaskSummary is, the layering has been violated and the fix is in the daemon, not here.

Consequences

  • add_subdirectory(core) must produce two targets. Lane PKG/QA owns the root build files; the conformance runner's CMakeLists.txt shows the linkage it expects.
  • A grep for nlohmann under core/ is no longer automatically a review failure — but one under core/src/ or core/include/ still is. That is the line, and it is worth stating because the old grep was a nice bright one.
  • The generated code deliberately does not emit nlohmann's ADL from_json, which throws. Inbound parsing is parse<T>() -> std::expected<T, ParseError>, so a malformed frame is a value the RPC loop handles rather than an exception unwinding through the daemon. Only the outbound direction is implicit.

Alternatives rejected

Put the generated code in daemon/. It is also needed by the CLI, the GUI and the conformance runner, and daemon/ belongs to a different lane than the one that generates it. A shared artifact owned by one consumer is how ownership disputes start.

Relax the layering rule to allow JSON in core. The rule is the reason the engine stays testable and the reason a GUI bug can never be an engine bug. It is worth more than the convenience of one directory.