Files
samiandClaude Sonnet 5 5e3e21543a proto: give the generated C++ Dispatcher a real error channel (P1, 1.4.0)
DAEMON's daemon/docs/proto-requests-m1.md P1: velox::proto::Dispatcher's
on_* methods returned Result<T> = expected<T, ParseError>, and dispatch()
mapped every handler error to -32603 InternalError. A handler had no way to
return -32010 (download.get not-found), -32011 (download.add invalid-path)
or -32013 (probe-failed) with their data payloads -- three error fixtures a
conformant server must satisfy were unreachable, blocking DAEMON's
"conformance as a server" M1 DoD.

Two error channels now, kept separate on purpose:
  - parse: Result<T> / ParseError -- dispatch() failing to turn the wire into
    typed params. Always -32602, always structural.
  - handler: HandlerResult<T> / HandlerError -- a handler deciding the request
    can't be fulfilled. Carries any ErrorCode + message + free-form data.

    struct HandlerError {
        ErrorCode code{ErrorCode::InternalError};  // bare {} is a valid -32603
        std::string message;
        nlohmann::json data = nullptr;             // straight into the error's data
    };
    template <class T> using HandlerResult = std::expected<T, HandlerError>;

dispatch()'s handler branch is now
  make_error(id, r.error().code, r.error().message, r.error().data)
instead of a hard-coded InternalError. -32001/-32002/-32003 stay the server
layer's to raise around dispatch(), as DAEMON already does.

Verified end to end against the real dispatch() path: a handler returning
TaskNotFound/InvalidPath/ProbeFailed produces -32010/-32011/-32013 with the
data object intact, and a bare HandlerError{} still yields a clean -32603
with no data field. The `= nullptr` on the member (not `{nullptr}`) matters:
brace-init of nlohmann::json from nullptr is the array [null], not JSON null.

FixtureDispatcher regenerated to HandlerResult; conformance_main.cpp only
inspects dispatch()'s JSON and needed no change. TS side is untouched beyond
the version string -- no server Dispatcher is generated there.

P2 also handled: session.hello.version-mismatch's data.expected was a stale
"1.0.0"; now $any, with a note that the error-fixture compare is on `code`
only so a server echoing kProtocolVersion there is fine.

Version: minor, 1.3.0 -> 1.4.0. Wire is byte-identical (no schema, fixture,
or OpenRPC change) but every Dispatcher implementer must swap Result ->
HandlerResult on regen, and the bump is how lanes are told to. Not an ADR:
one lane consumes this binding, it's the one that asked, and the shape is
the one they proposed. Answered in contracts/proto-answers-daemon-m1.md.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012fgjnqFCS5h5L7gZTZo3rV
2026-09-10 15:10:13 +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.