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
This commit is contained in:
2026-09-09 19:55:54 +04:00
co-authored by Claude Opus 5
parent a40585f419
commit 53421d6cb8
171 changed files with 29275 additions and 51 deletions
+110
View File
@@ -0,0 +1,110 @@
// ---------------------------------------------------------------------------
// GENERATED FILE — DO NOT EDIT.
//
// Source: contracts/schema/**
// Generator: contracts/codegen/gen_ts.py
// Contract: v1.0.0
//
// Hand-editing this file is a merge blocker. Fix the schema and regenerate:
// python3 contracts/codegen/gen_ts.py
// Only lane PROTO commits to contracts/.
// ---------------------------------------------------------------------------
import type {
AuthRequiredEvent,
GrabberProgressEvent,
NotifyEvent,
SettingsChangedEvent,
SpeedGlobalEvent,
TaskAddedEvent,
TaskProgressEvent,
TaskRemovedEvent,
TaskStateEvent,
} from './types.js';
/** Payload for each server-to-client notification, keyed by its wire name. */
export interface EventMap {
/**
* A server asked for credentials. The task sits in retry_wait until the client supplies
* them. Credentials travel to the Secret Service, never back through this event and never
* into a log.
*/
"event.auth.required": AuthRequiredEvent;
/**
* Crawl progress for the Site Grabber wizard. done true means the file list in
* grabber.status is final.
*/
"event.grabber.progress": GrabberProgressEvent;
/**
* Something the user should see: a completion, a failure, a queue finishing. The client
* decides between a toast, a tray balloon and a sound; the daemon does not assume a GUI is
* running.
*/
"event.notify": NotifyEvent;
/**
* Settings were written by some client. Carries only the key names; a client re-reads what
* it cares about. The extension watches for capture.* here and re-fetches capture.getRules
* so its rules never lag the daemon's.
*/
"event.settings.changed": SettingsChangedEvent;
/**
* Aggregate throughput for the status bar, the tray tooltip and the extension popup.
* Emitted at 1 Hz even when nothing is active, so a client can tell 'idle' from
* 'disconnected'.
*/
"event.speed.global": SpeedGlobalEvent;
/**
* A task entered the list. summary is always present so a client can insert the row
* without a follow-up download.get.
*/
"event.task.added": TaskAddedEvent;
/**
* Batched byte counters for every active task. Emitted at no more than 4 Hz as one array,
* never one notification per task: at twenty active downloads that is four messages a
* second instead of eighty. Clients apply a row patch and repaint the touched columns;
* rebuilding a model on this event is a bug.
*/
"event.task.progress": TaskProgressEvent;
/** A task left the list. The client deletes the row; there is nothing further to fetch. */
"event.task.removed": TaskRemovedEvent;
/**
* A task changed lifecycle state. Carries the summary so the row can be repainted in full
* without a round trip, and error whenever the new state is failed or retry_wait.
*/
"event.task.state": TaskStateEvent;
}
export type EventName = keyof EventMap;
export type EventPayload<E extends EventName> = EventMap[E];
/**
* Discriminated on `method`: narrowing an incoming notification gives the
* correctly typed params with no cast at the call site.
*/
export type ServerNotification = {
[E in EventName]: { jsonrpc: '2.0'; method: E; params: EventMap[E] };
}[EventName];
export interface EventMeta {
/** Upper bound on emission rate, where the contract sets one. */
readonly maxRateHz: number | null;
}
export const EVENTS: { readonly [E in EventName]: EventMeta } = {
"event.auth.required": { maxRateHz: null },
"event.grabber.progress": { maxRateHz: 4 },
"event.notify": { maxRateHz: null },
"event.settings.changed": { maxRateHz: null },
"event.speed.global": { maxRateHz: 1 },
"event.task.added": { maxRateHz: null },
"event.task.progress": { maxRateHz: 4 },
"event.task.removed": { maxRateHz: null },
"event.task.state": { maxRateHz: null },
} as const;
export const EVENT_NAMES = Object.keys(EVENTS) as EventName[];
export function isEventName(v: unknown): v is EventName {
return typeof v === 'string' && Object.prototype.hasOwnProperty.call(EVENTS, v);
}