sdk: drop filtered per-domain spec duplication — single source of truth #139

Open
opened 2026-05-14 10:04:21 +00:00 by timur · 1 comment
Owner

Problem

hero_aibroker_sdk currently ships a filtered copy of each per-domain OpenRPC spec at crates/hero_aibroker_sdk/specs/<domain>.openrpc.json (added in #131). The SDK's openrpc_client! macro reads from there instead of from the server's canonical crates/hero_aibroker_server/specs/<domain>.openrpc.json. Two reasons today:

  1. The openrpc_client! macro (hero_rpc_derive) doesn't support JSON-schema allOf, so the SDK copies strip the two methods that use it: memory.search (allOf on result item) and ai.transcribe_verbose (allOf on TranscriptionVerbose).
  2. Easier to iterate without touching the server crate.

Both reasons are workarounds. The hero_macro_openrpc skill is explicit (line 211): "Place the spec at src/<service_name>/openrpc.json inside the target crate, or use the one from the server crate." — single source of truth.

Cost of the current state

  • Two specs to keep in sync (silent drift risk).
  • memory.search and ai.transcribe_verbose aren't reachable through the typed SDK.
  • New consumers may copy the SDK pattern and entrench the duplication.

Proposal

Pick one of three:

Option A — flatten allOf in the server-side specs (simplest, smallest blast radius)

Two specs use allOf:

// crates/hero_aibroker_server/specs/speech.openrpc.json — TranscriptionVerbose
{
  "allOf": [
    { "$ref": "#/components/schemas/TranscriptionCompact" },
    { "type": "object", "properties": { "task": ..., "segments": ..., "words": ... } }
  ]
}
// crates/hero_aibroker_server/specs/memory.openrpc.json — memory.search result.items
{
  "allOf": [
    { "$ref": "#/components/schemas/MemoryEntry" },
    { "type": "object", "properties": { "score": { "type": "number" } } }
  ]
}

Inline the $ref'd schema's properties into a single object schema. No semantic change on the wire; the typed Rust output is the same shape.

Then SDK can do:

openrpc_client!("../hero_aibroker_server/specs/chat.openrpc.json", name = "GenClient");

and the crates/hero_aibroker_sdk/specs/ directory disappears.

Option B — build.rs filter

SDK has a build.rs that reads from ../hero_aibroker_server/specs/, transforms allOf (flatten or strip), writes to OUT_DIR, then the macro reads from there. Server specs stay authoritative; SDK doesn't commit copies.

Option C — add allOf support to openrpc_client!

The proper fix. Adds allOf → flat-struct generation to hero_rpc_derive so any spec using allOf works out of the box. Largest scope (touches hero_rpc).

Recommendation

A for this cycle (small, contained, ships fast). File C as a longer-term follow-up against hero_rpc.

Acceptance

  • crates/hero_aibroker_sdk/specs/ deleted
  • Each domain_client!() invocation in crates/hero_aibroker_sdk/src/lib.rs points at ../hero_aibroker_server/specs/<domain>.openrpc.json
  • memory::Client::memory_search and speech::Client::ai_transcribe_verbose exist and compile
  • cargo build -p hero_aibroker_sdk clean, cargo test -p hero_aibroker_sdk --doc green
  • Live RPC against running broker confirms both methods round-trip

Refs #127, #131.

## Problem `hero_aibroker_sdk` currently ships a filtered copy of each per-domain OpenRPC spec at `crates/hero_aibroker_sdk/specs/<domain>.openrpc.json` (added in [#131](https://forge.ourworld.tf/lhumina_code/hero_aibroker/pulls/131)). The SDK's `openrpc_client!` macro reads from there instead of from the server's canonical `crates/hero_aibroker_server/specs/<domain>.openrpc.json`. Two reasons today: 1. The `openrpc_client!` macro (`hero_rpc_derive`) doesn't support JSON-schema `allOf`, so the SDK copies strip the two methods that use it: `memory.search` (allOf on result item) and `ai.transcribe_verbose` (allOf on `TranscriptionVerbose`). 2. Easier to iterate without touching the server crate. Both reasons are workarounds. The `hero_macro_openrpc` skill is explicit ([line 211](https://forge.ourworld.tf/lhumina_code/hero_skills/src/branch/development/claude/skills/hero_macro_openrpc/SKILL.md#L211)): "Place the spec at `src/<service_name>/openrpc.json` inside the target crate, **or use the one from the server crate**." — single source of truth. ## Cost of the current state - Two specs to keep in sync (silent drift risk). - `memory.search` and `ai.transcribe_verbose` aren't reachable through the typed SDK. - New consumers may copy the SDK pattern and entrench the duplication. ## Proposal Pick one of three: ### Option A — flatten `allOf` in the server-side specs (simplest, smallest blast radius) Two specs use `allOf`: ```jsonc // crates/hero_aibroker_server/specs/speech.openrpc.json — TranscriptionVerbose { "allOf": [ { "$ref": "#/components/schemas/TranscriptionCompact" }, { "type": "object", "properties": { "task": ..., "segments": ..., "words": ... } } ] } ``` ```jsonc // crates/hero_aibroker_server/specs/memory.openrpc.json — memory.search result.items { "allOf": [ { "$ref": "#/components/schemas/MemoryEntry" }, { "type": "object", "properties": { "score": { "type": "number" } } } ] } ``` Inline the `$ref`'d schema's properties into a single object schema. No semantic change on the wire; the typed Rust output is the same shape. Then SDK can do: ```rust openrpc_client!("../hero_aibroker_server/specs/chat.openrpc.json", name = "GenClient"); ``` and the `crates/hero_aibroker_sdk/specs/` directory disappears. ### Option B — `build.rs` filter SDK has a `build.rs` that reads from `../hero_aibroker_server/specs/`, transforms `allOf` (flatten or strip), writes to `OUT_DIR`, then the macro reads from there. Server specs stay authoritative; SDK doesn't commit copies. ### Option C — add `allOf` support to `openrpc_client!` The proper fix. Adds `allOf` → flat-struct generation to `hero_rpc_derive` so any spec using `allOf` works out of the box. Largest scope (touches `hero_rpc`). ## Recommendation **A** for this cycle (small, contained, ships fast). File **C** as a longer-term follow-up against `hero_rpc`. ## Acceptance - [ ] `crates/hero_aibroker_sdk/specs/` deleted - [ ] Each `domain_client!()` invocation in `crates/hero_aibroker_sdk/src/lib.rs` points at `../hero_aibroker_server/specs/<domain>.openrpc.json` - [ ] `memory::Client::memory_search` and `speech::Client::ai_transcribe_verbose` exist and compile - [ ] `cargo build -p hero_aibroker_sdk` clean, `cargo test -p hero_aibroker_sdk --doc` green - [ ] Live RPC against running broker confirms both methods round-trip Refs [#127](https://forge.ourworld.tf/lhumina_code/hero_aibroker/issues/127), [#131](https://forge.ourworld.tf/lhumina_code/hero_aibroker/pulls/131).
Author
Owner

Implemented in #141#141.

Live-verified against a rebuilt broker (real Groq round-trip).

Implemented in #141 — https://forge.ourworld.tf/lhumina_code/hero_aibroker/pulls/141. Live-verified against a rebuilt broker (real Groq round-trip).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_aibroker#139
No description provided.