1. download.add.json had startMode "now" against a real, large (~6 GB)
Ubuntu ISO with saveDir hardcoded to /home/sami/Downloads/Programs.
Against a real veloxd (tests/conformance/run.sh) that's a real
download into the real user's real home, every single run — it had
already happened twice. startMode -> "later" (exercises the add path,
hands nothing to the engine) and saveDir is dropped entirely (resolves
to saveTo.defaultDir instead, checked against allowedRoots the same
way). Documented the rule this fixture was breaking in
contracts/fixtures/README.md so it doesn't happen a third time.
Auditing the rest for the same shape (now real: capture.offer, D7)
found a second, subtler instance: capture.offer.take.json's "take"
admits a real, immediately-started task the same way download.add
does, and the "Programs" category's saveDir is a migration-seeded
builtin (~/Downloads/Programs) that no isolated test setup can
redirect -- so even after pointing the URL at example.org (RFC 2606),
a real ~6 GB sparse .veloxpart still landed in the real home on the
declared Content-Length alone. Shrunk to a plausible-but-small 5 MiB.
Also scoped to "transport": "uds" -- a real "take" persists an active
task, so replaying the same fixture again on the second live transport
against the same shared daemon was hitting capture.offer's own
dedupe-by-URL and failing on a missing taskId, not a bug.
download.add's other real-URL siblings (errors/*.invalid-path,
*.invalid-params, *.disk-full) all fail before admission or are
requires-gated; left alone.
2. ADR 0018: DAEMON can set a nullable field through download.update /
settings.set but never clear it back to null, because the generated
C++ parser collapses "absent" and "explicit null" to the same
std::nullopt for every optional field (contracts/codegen/gen_cpp.py,
on purpose, and correct for create-style params -- just wrong for
patch-style ones, which is the only place the schema documents
"explicit null clears"). Decision: an opt-in x-clearable schema
annotation makes just those fields std::optional<std::optional<T>> in
C++ (TS already round-trips this natively); not a blanket rule
(would retype response fields like TaskSummary.effectiveUrl that have
no clear-vs-absent distinction to make), not an explicit clear-list
field (would redesign a wire contract DAEMON already built against
just to route around a generator gap). Recorded, not implemented here
-- that's its own PROTO PR (schema annotations + gen_cpp.py + gen_ts.py
+ regeneration + a minor VERSION bump per ADR 0015), not bundled into
a fixture-safety pass. Left a pointer to the ADR at the generator
comment it concerns.
3. Re-verified every xfail entry against current deferrals.md rather
than trust the reasons already on file: D7/D8 (capture.offer/
getRules), D3d/e/f/g/h/i (rules, queue.reorder, schedule, limiter,
download.update/refreshUrl) and D9 (settings) have all closed since
the list was last pruned, so most of it was stale. Removed everything
that now cleanly passes; kept and re-reasoned everything that doesn't:
- errors/download.provideAuth.not-found.json stays, as asked: real
bug, on_download_provideAuth never checks the task exists.
- category.list.json (mimeTypes -- documented D3a gap), schedule.set.json
(nextRunAt -- documented D3f gap): unchanged in substance, reason
text was already accurate.
- download.probe/get/list/update.json, session.hello.json,
queue.start/reorder.json, category.remove.json: not bugs -- each
golden depicts a richer lifecycle/config state (a probed download,
real queue or category membership, media/grabber capabilities) than
this harness's fresh, never-started bound tasks and empty isolated
DB can produce.
- limiter.get.json: real fixture bug, not a daemon one -- applyToRunning
is a write-only instruction on limiter.set, on_limiter_get never
returns it; the golden shouldn't have had it either. Fixed the
fixture and tools/mockd's own limiter.get, which had the same field
hardcoded into its in-memory state independent of the fixture file.
- grabber.*/media.*: still genuinely stub (M4 territory).
Only remaining unexpected-pass surfaced while re-verifying
(errors/capture.offer.ignore.json, always "take" instead of "ignore")
traced to capture.minSizeBytes defaulting to 0 on a fresh daemon,
making its below-minimum-size scenario unreachable -- not a bug, so
raised the setting in run.sh's isolated seeding instead of xfailing it.
ctest -L conformance: green, 100% (2/2), ~87s.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SFeUKLbdHizrJjLBeK7ffz
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.