Migrate OSIS serving to hero_lib serve_rpc_domains and DELETE HeroOsisServer (no adapter, no deprecation) #154

Open
opened 2026-06-02 08:54:46 +00:00 by timur · 17 comments
Owner

Goal

Straight migration, no half-measures. OSIS multi-domain serving must move entirely onto Kristof's hero_lifecycle::serve_rpc_domains (hero_lib) and the old replica must be deletedhero_rpc_osis::rpc::bootstrap::HeroOsisServer, the hero_osis_server!() macro path, the rpc2_adapter-based serving, and the stopgap HeroOsisServer::serve_domains() (branch feature/osis-multidomain-serve). hero_osis (and the CRM example) must boot on the new serving.

Explicitly NOT wanted: no RpcModule → axum::Router adapter/shim, no deprecation window, no back-compat alias. Remove the old code; make OSIS work with the new directly.

Why

HeroOsisServer and serve_rpc_domains are two parallel implementations of the same control-plane + per-domain-socket topology. Kristof built serve_rpc_domains in hero_lib; we were replicating it in hero_blueprint over the hero_rpc2 transport. That replica (incl. the serve_domains() I added as a stopgap) is the duplication to delete.

Approach (straight migration)

OSIS domain handlers should be served natively by serve_rpc_domains, which means the OSIS generator emits domain servers the hero_lib way:

  • serve_rpc_domains(Vec<(name, openrpc_json, axum::Router)>) (hero_lib crates/hero_lifecycle/src/manifest.rs:733) wants one axum::Router (POST /rpc + GET /openrpc.json) per domain — produced today by openrpc_server!<domain>_rpc::router(impl <Domain>Api).
  • So regenerate the OSIS server codegen (hero_blueprint crates/generator/src/rust/rust_osis.rs + the hero_osis_server!/HeroOsisServer machinery) to emit/feed hero_lib-servable domain routers instead of jsonrpsee RpcModules behind hero_rpc2's own HTTP transport.
  • The storage-backed CRUD bodies stay (OSIS's unique value: DBTyped/OSIS-storage CRUD) — only the serving/transport plumbing changes from hero_rpc2 → hero_lib.

Delete

  • HeroOsisServer (crates/osis/src/rpc/bootstrap.rs) incl. serve() and the stopgap serve_domains().
  • The hero_osis_server!() macro (crates/osis/src/lib.rs) + the generator's emission of it (crates/generator/src/build/scaffold.rs server main.rs).
  • The hero_rpc2 serving path for OSIS (rpc2_adapter register-into-RpcModule + ServerBuilder::serve_http for multi-domain) — to the extent it's only used for OSIS serving.

Update

  • hero_osis (crates/hero_osis_server/src/bin/hero_osis.rs + service.toml) to boot via serve_rpc_domains (control plane + per-domain sockets come for free, incl. discover_domains).
  • CRM example (examples/crm) server main.rs to the new path.

Done-when

  • One serving implementation (serve_rpc_domains) — no HeroOsisServer, no adapter, no stopgap serve_domains().
  • hero_osis + CRM boot on it; per-domain sockets; same-named rootobjects across domains (e.g. Contact in identity + business) don't collide; discover_domains works.

Context: hero_osis re-architecture (Claude). Pairs with #155 (CRUD/openrpc struct shape). Supersedes the stopgap on hero_blueprint feature/osis-multidomain-serve + hero_osis feature/osis-hero-db-storage.

## Goal **Straight migration, no half-measures.** OSIS multi-domain serving must move entirely onto Kristof's `hero_lifecycle::serve_rpc_domains` (hero_lib) and the **old replica must be deleted** — `hero_rpc_osis::rpc::bootstrap::HeroOsisServer`, the `hero_osis_server!()` macro path, the `rpc2_adapter`-based serving, and the stopgap `HeroOsisServer::serve_domains()` (branch `feature/osis-multidomain-serve`). hero_osis (and the CRM example) must boot on the new serving. **Explicitly NOT wanted:** no `RpcModule → axum::Router` adapter/shim, no deprecation window, no back-compat alias. Remove the old code; make OSIS work with the new directly. ## Why `HeroOsisServer` and `serve_rpc_domains` are two parallel implementations of the same control-plane + per-domain-socket topology. Kristof built `serve_rpc_domains` in hero_lib; we were replicating it in hero_blueprint over the hero_rpc2 transport. That replica (incl. the `serve_domains()` I added as a stopgap) is the duplication to delete. ## Approach (straight migration) OSIS domain handlers should be **served natively by `serve_rpc_domains`**, which means the OSIS generator emits domain servers the hero_lib way: - `serve_rpc_domains(Vec<(name, openrpc_json, axum::Router)>)` (hero_lib `crates/hero_lifecycle/src/manifest.rs:733`) wants one `axum::Router` (POST `/rpc` + GET `/openrpc.json`) per domain — produced today by `openrpc_server!` → `<domain>_rpc::router(impl <Domain>Api)`. - So regenerate the OSIS server codegen (hero_blueprint `crates/generator/src/rust/rust_osis.rs` + the `hero_osis_server!`/`HeroOsisServer` machinery) to emit/feed hero_lib-servable domain routers instead of jsonrpsee `RpcModule`s behind hero_rpc2's own HTTP transport. - The **storage-backed CRUD bodies stay** (OSIS's unique value: `DBTyped`/OSIS-storage CRUD) — only the serving/transport plumbing changes from hero_rpc2 → hero_lib. ## Delete - `HeroOsisServer` (`crates/osis/src/rpc/bootstrap.rs`) incl. `serve()` and the stopgap `serve_domains()`. - The `hero_osis_server!()` macro (`crates/osis/src/lib.rs`) + the generator's emission of it (`crates/generator/src/build/scaffold.rs` server `main.rs`). - The hero_rpc2 serving path for OSIS (`rpc2_adapter` register-into-`RpcModule` + `ServerBuilder::serve_http` for multi-domain) — to the extent it's only used for OSIS serving. ## Update - hero_osis (`crates/hero_osis_server/src/bin/hero_osis.rs` + `service.toml`) to boot via `serve_rpc_domains` (control plane + per-domain sockets come for free, incl. `discover_domains`). - CRM example (`examples/crm`) server `main.rs` to the new path. ## Done-when - One serving implementation (`serve_rpc_domains`) — no `HeroOsisServer`, no adapter, no stopgap `serve_domains()`. - hero_osis + CRM boot on it; per-domain sockets; same-named rootobjects across domains (e.g. `Contact` in identity + business) don't collide; `discover_domains` works. _Context: hero_osis re-architecture (Claude). Pairs with #155 (CRUD/openrpc struct shape). Supersedes the stopgap on hero_blueprint `feature/osis-multidomain-serve` + hero_osis `feature/osis-hero-db-storage`._
timur changed title from Converge OSIS multi-domain serving onto hero_lib serve_rpc_domains (remove HeroOsisServer duplication) to Migrate OSIS serving to hero_lib serve_rpc_domains and DELETE HeroOsisServer (no adapter, no deprecation) 2026-06-02 09:08:33 +00:00
Author
Owner

Picking this up (together with the paired issue — same OSIS handler regeneration). Working branches: hero_blueprint feature/osis-multidomain-serve (rebased target) + hero_osis feature/osis-hero-db-storage.

Exploring the exact target first: how a storage-backed OSIS handler plugs into hero_lib openrpc_server! (<Domain>Api trait + per-domain router) and serve_rpc_domains. Will post the concrete execution plan here next, then implement in chunks with a comment per chunk so this issue stays a complete record.

**Picking this up** (together with the paired issue — same OSIS handler regeneration). Working branches: hero_blueprint `feature/osis-multidomain-serve` (rebased target) + hero_osis `feature/osis-hero-db-storage`. Exploring the exact target first: how a storage-backed OSIS handler plugs into hero_lib `openrpc_server!` (`<Domain>Api` trait + per-domain router) and `serve_rpc_domains`. Will post the concrete execution plan here next, then implement in chunks with a comment per chunk so this issue stays a complete record.
Author
Owner

Execution plan (picked up) — target architecture nailed

Goal restated: delete hero_blueprint's parallel OSIS serving (HeroOsisServer, hero_osis_server!, rpc2_adapter dispatch, OsisAppRpcHandler, the stopgap serve_domains()) and serve OSIS via hero_lib's openrpc_server! + serve_rpc_domains. No adapter, no deprecation.

Target shapes (verified against hero_lib origin/development)

herolib_derive::openrpc_server!(spec=<oschema dir>, service_toml=…) generates, per domain:

  • The Rust types (rootobject structs + per-method *Input/*Output wrappers) from the OpenRPC spec.
  • The <Domain>Api trait: async fn <type>_set(&self, ctx:&HeroRequestContext, input:<Type>SetInput) -> Result<<Type>SetOutput, RpcError> etc. CRUD = _get,_set,_delete,_list,_list_full,_exists,_find (no _new; _set is upsert, SetInput{ data:<Type> }, empty sid→create).
  • <domain>_rpc::router(impl) -> axum::Router, <domain>_rpc::OPENRPC_JSON, and free fns serve_domains(...)/serve_domains_with(...)manifest().serve_rpc_domains(...) (control plane + rpc_<domain>.sock per domain + discover_domains).
    Refs: hero_lib crates/derive/src/openrpc_server.rs:430-760, example crates/hero_lifecycle/examples/server/{main.rs,rpc/dom1_impl.rs}, crates/oschema/src/oschema/openrpc.rs:520-640.

What hero_blueprint's OSIS generator becomes

Emit, per domain: #[async_trait] impl <Domain>Api for Osis<Domain> whose bodies call the existing DBTyped storage (get/set/delete/list/exists) + indexer write-through, mapping errors Box<dyn Error>RpcError. _set body: empty/absent sid ⇒ create (mint sid, stamp times) ⇒ self.<t>_db.set(&mut obj); else load+update. The service main.rs is just serve_domains(OsisDomain::default()/::create(...), …).

DELETE (hero_blueprint)

crates/osis/src/rpc/bootstrap.rs (HeroOsisServer + serve/serve_domains), hero_osis_server! macro (crates/osis/src/lib.rs), the rpc2_adapter multi-domain serving + OsisAppRpcHandler/handle_rpc dispatch, and the generator emission of all the above in crates/generator/src/rust/rust_osis.rs (the bespoke CRUD methods + handle_rpc + OsisAppRpcHandler impl). The hero_osis_server!-based server main.rs template in crates/generator/src/build/scaffold.rs.

KEEP

DBTyped storage + SID/counter logic, OsisIndexer write-through, the trigger hooks, the rootobject types (modulo the type-ownership decision below).

Step order

  1. Resolve the type-ownership decision (below).
  2. Change the generator to emit impl <Domain>Api (+ run/emit openrpc_server! for trait/types/router) instead of the bespoke handler/serving.
  3. Convert the CRM example first (single domain — smallest surface to prove the converged pattern end-to-end: build→boot→driver).
  4. Regenerate hero_osis (15 domains) on the new pattern; bin main.rsserve_domains(...); drop the per-domain-socket service.toml hack (serve_rpc_domains owns sockets).
  5. Delete the dead code. Build + boot + CRUD round-trip + collision check.

⚠️ Gating decision: rootobject type ownership / storage-type impedance

openrpc_server! generates wire types from the spec: User { sid: Option<String>, created_at: Option<String>, … }. OSIS storage (DBTyped<T: OsisObject>) currently uses User { sid: SmartId, created_at: OTime } and OsisObject::sid()->&SmartId. These don't match. Options:

  • (A) One type model — storage adopts the spec/wire types. DBTyped/OsisObject operate on the openrpc_server!-generated types (String sid, String/OTime times); the generator emits impl OsisObject for <Type> (local to the consumer crate, no orphan issue). True convergence, one set of types, but OsisObject must accept String sids (small change to the trait + SID minting).
  • (B) Two type models + convert at the trait boundary. Keep hero_blueprint storage types (SmartId/OTime); the impl <Domain>Api converts spec-types↔storage-types per call. Preserves typing, but doubles the type set + adds conversion codegen.

Recommendation: (A) — it's the actual convergence (single source of truth = the OpenRPC spec) and matches Kristof's "one schema per object" direction (979c1781). Pending confirmation before the generator rewrite, since it shapes everything.

(Cross-refs #155 for the CRUD method/struct shape — same regeneration. Journaling continues here per chunk.)

## Execution plan (picked up) — target architecture nailed Goal restated: **delete** hero_blueprint's parallel OSIS serving (`HeroOsisServer`, `hero_osis_server!`, `rpc2_adapter` dispatch, `OsisAppRpcHandler`, the stopgap `serve_domains()`) and serve OSIS via hero_lib's `openrpc_server!` + `serve_rpc_domains`. No adapter, no deprecation. ### Target shapes (verified against hero_lib `origin/development`) `herolib_derive::openrpc_server!(spec=<oschema dir>, service_toml=…)` generates, per domain: - The Rust **types** (rootobject structs + per-method `*Input`/`*Output` wrappers) from the OpenRPC spec. - The **`<Domain>Api` trait**: `async fn <type>_set(&self, ctx:&HeroRequestContext, input:<Type>SetInput) -> Result<<Type>SetOutput, RpcError>` etc. CRUD = `_get,_set,_delete,_list,_list_full,_exists,_find` (no `_new`; `_set` is upsert, `SetInput{ data:<Type> }`, empty `sid`→create). - `<domain>_rpc::router(impl) -> axum::Router`, `<domain>_rpc::OPENRPC_JSON`, and free fns `serve_domains(...)`/`serve_domains_with(...)` → `manifest().serve_rpc_domains(...)` (control plane + `rpc_<domain>.sock` per domain + `discover_domains`). Refs: hero_lib `crates/derive/src/openrpc_server.rs:430-760`, example `crates/hero_lifecycle/examples/server/{main.rs,rpc/dom1_impl.rs}`, `crates/oschema/src/oschema/openrpc.rs:520-640`. ### What hero_blueprint's OSIS generator becomes Emit, per domain: `#[async_trait] impl <Domain>Api for Osis<Domain>` whose bodies call the existing **DBTyped** storage (get/set/delete/list/exists) + **indexer write-through**, mapping errors `Box<dyn Error>`→`RpcError`. `_set` body: empty/absent `sid` ⇒ create (mint sid, stamp times) ⇒ `self.<t>_db.set(&mut obj)`; else load+update. The service `main.rs` is just `serve_domains(OsisDomain::default()/::create(...), …)`. ### DELETE (hero_blueprint) `crates/osis/src/rpc/bootstrap.rs` (HeroOsisServer + serve/serve_domains), `hero_osis_server!` macro (`crates/osis/src/lib.rs`), the `rpc2_adapter` multi-domain serving + `OsisAppRpcHandler`/`handle_rpc` dispatch, and the generator emission of all the above in `crates/generator/src/rust/rust_osis.rs` (the bespoke CRUD methods + `handle_rpc` + `OsisAppRpcHandler` impl). The `hero_osis_server!`-based server `main.rs` template in `crates/generator/src/build/scaffold.rs`. ### KEEP DBTyped storage + SID/counter logic, OsisIndexer write-through, the trigger hooks, the rootobject types (modulo the type-ownership decision below). ### Step order 1. Resolve the type-ownership decision (below). 2. Change the generator to emit `impl <Domain>Api` (+ run/emit `openrpc_server!` for trait/types/router) instead of the bespoke handler/serving. 3. Convert the **CRM example first** (single domain — smallest surface to prove the converged pattern end-to-end: build→boot→driver). 4. Regenerate **hero_osis** (15 domains) on the new pattern; bin `main.rs` → `serve_domains(...)`; drop the per-domain-socket `service.toml` hack (serve_rpc_domains owns sockets). 5. Delete the dead code. Build + boot + CRUD round-trip + collision check. ### ⚠️ Gating decision: rootobject type ownership / storage-type impedance `openrpc_server!` generates wire types from the spec: `User { sid: Option<String>, created_at: Option<String>, … }`. OSIS storage (`DBTyped<T: OsisObject>`) currently uses `User { sid: SmartId, created_at: OTime }` and `OsisObject::sid()->&SmartId`. These don't match. Options: - **(A) One type model — storage adopts the spec/wire types.** `DBTyped`/`OsisObject` operate on the `openrpc_server!`-generated types (String sid, String/`OTime` times); the generator emits `impl OsisObject for <Type>` (local to the consumer crate, no orphan issue). True convergence, one set of types, but `OsisObject` must accept String sids (small change to the trait + SID minting). - **(B) Two type models + convert at the trait boundary.** Keep hero_blueprint storage types (SmartId/OTime); the `impl <Domain>Api` converts spec-types↔storage-types per call. Preserves typing, but doubles the type set + adds conversion codegen. Recommendation: **(A)** — it's the actual convergence (single source of truth = the OpenRPC spec) and matches Kristof's "one schema per object" direction (979c1781). Pending confirmation before the generator rewrite, since it shapes everything. (Cross-refs #155 for the CRUD method/struct shape — same regeneration. Journaling continues here per chunk.)
Author
Owner

Decision: (A) one type model — storage adopts the spec/wire types. DBTyped/OsisObject will operate on the openrpc_server!-generated rootobject types (String sid, String/OTime timestamps); the generator emits impl OsisObject for <Type> locally in the consumer crate. Single source of truth = the OpenRPC spec; matches 979c1781 (one schema per object).

Implication: OsisObject (in hero_rpc_osis::db) moves from sid()->&SmartId to a String-sid surface, and DBTyped<T> keys on the String sid. This revises the hero_db storage layer (the feature/osis-hero-db-storage work) — sid minting moves to the create path of _set.

Chunk order (each → its own commit + comment here)

  1. osis crate foundation: OsisObject String-sid surface + DBTyped<T> keying/minting on String sid; keep redb/OTOML + indexer. (base for everything)
  2. generator: emit #[async_trait] impl <Domain>Api for Osis<Domain> (DBTyped bodies, _set upsert, errors→RpcError) + adopt openrpc_server! for trait/types/router/spec; delete bespoke CRUD methods + handle_rpc + OsisAppRpcHandler emission.
  3. delete HeroOsisServer + hero_osis_server! + rpc2_adapter serving + stopgap serve_domains().
  4. CRM convert + prove end-to-end (single domain).
  5. hero_osis regenerate (15 domains) + serve_domains(...) main; build+boot+CRUD+collision check.

Starting chunk 1 on hero_blueprint feature/osis-multidomain-serve.

**Decision: (A) one type model — storage adopts the spec/wire types.** DBTyped/OsisObject will operate on the `openrpc_server!`-generated rootobject types (String `sid`, String/`OTime` timestamps); the generator emits `impl OsisObject for <Type>` locally in the consumer crate. Single source of truth = the OpenRPC spec; matches 979c1781 (one schema per object). Implication: `OsisObject` (in `hero_rpc_osis::db`) moves from `sid()->&SmartId` to a String-sid surface, and `DBTyped<T>` keys on the String sid. This revises the hero_db storage layer (the `feature/osis-hero-db-storage` work) — sid minting moves to the create path of `_set`. ### Chunk order (each → its own commit + comment here) 1. **osis crate foundation**: `OsisObject` String-sid surface + `DBTyped<T>` keying/minting on String sid; keep redb/OTOML + indexer. (base for everything) 2. **generator**: emit `#[async_trait] impl <Domain>Api for Osis<Domain>` (DBTyped bodies, `_set` upsert, errors→`RpcError`) + adopt `openrpc_server!` for trait/types/router/spec; delete bespoke CRUD methods + `handle_rpc` + `OsisAppRpcHandler` emission. 3. **delete** HeroOsisServer + `hero_osis_server!` + rpc2_adapter serving + stopgap `serve_domains()`. 4. **CRM** convert + prove end-to-end (single domain). 5. **hero_osis** regenerate (15 domains) + `serve_domains(...)` main; build+boot+CRUD+collision check. Starting chunk 1 on hero_blueprint `feature/osis-multidomain-serve`.
Author
Owner

Chunk 1 spec — OsisObject/DBTyped String-sid foundation (executable)

Base branch: hero_blueprint feature/osis-multidomain-serve (off newest dev). File: crates/osis/src/db/db.rs. (Storage backend stays dev's filesystem-OTOML here; re-landing the hero_db/redb backend from feature/osis-hero-db-storage is a separate orthogonal merge — note both branches touch this trait, reconcile during merge.)

OsisObject — before → after

Current (db.rs:~32):

pub trait OsisObject: Serialize + for<'de> Deserialize<'de> {
    fn sid(&self) -> &SmartId;
    fn set_sid(&mut self, sid: SmartId);
    fn type_name() -> &'static str;
    fn indexed_fields(&self)->Vec<(String,String)> {..}
    fn indexed_fields_json(&self)->Vec<(String,serde_json::Value)> {..}
    fn indexed_field_names()->&'static [&'static str] {..}
}

After (String-sid, matches openrpc_server!-generated wire types where sid: Option<String>):

pub trait OsisObject: Serialize + for<'de> Deserialize<'de> {
    /// Current sid ("" / None ⇒ unsaved → create on set).
    fn sid(&self) -> String;            // empty string when absent
    fn set_sid(&mut self, sid: String);
    fn type_name() -> &'static str;
    // indexed_* unchanged
}

(Generated impls read/write the type's sid: Option<String>: sid()self.sid.clone().unwrap_or_default(); set_sid(s)self.sid = Some(s).)

DBTyped changes (db.rs)

  • object_path(sid:&str)type_dir().join(format!("{sid}.otoml")).
  • get/delete/exists(&self, sid:&str), list(&self)->Result<Vec<String>> (return String sids; filter __osis_meta:-style as today / read dir entries).
  • set(&self, obj:&mut T): if obj.sid() empty ⇒ mint via id_new()obj.set_sid(new_sid_string); else use as-is; write OTOML at object_path(&obj.sid()).
  • id_new(&self) -> Result<String>: keep the SmartId compute/counter logic internally, return sid.as_str() (String). (SmartId stays the minting algorithm; the stored/keyed sid is its String form — same as today's as_str().)
  • NotFound(sid.to_string()).

Ripple (chunk 2 handles the generator side)

  • The generator's emitted impl OsisObject for <Type> (currently in rust_struct.rs / osis_impl) must switch to the String-sid bodies above, against the openrpc_server! types (sid: Option<String>, created_at/updated_at: Option<String>), and stamp created_at/updated_at as strings on create.
  • rust_osis.rs CRUD callers: covered by the chunk-2 rewrite to impl <Domain>Api (the old user_new/user_set(sid,..) go away; _set upsert reads obj.sid()).
  • rhai.rs DynamicObject impl + tests in db/tests.rs: update to String sid.

Acceptance

cargo check -p hero_rpc_osis --features rpc green after chunk 1 + the matching generated-impl change; db/tests.rs (String sid) pass.


Status / compaction boundary: Plan + target shapes + decision (A) + this chunk-1 spec are now fully journaled here — enough to execute chunks 1→5 cleanly in a fresh context. Resuming = start chunk 1 from this comment on feature/osis-multidomain-serve.

## Chunk 1 spec — `OsisObject`/`DBTyped` String-sid foundation (executable) Base branch: hero_blueprint `feature/osis-multidomain-serve` (off newest dev). File: `crates/osis/src/db/db.rs`. (Storage backend stays dev's filesystem-OTOML here; re-landing the hero_db/redb backend from `feature/osis-hero-db-storage` is a separate orthogonal merge — note both branches touch this trait, reconcile during merge.) ### OsisObject — before → after Current (`db.rs:~32`): ```rust pub trait OsisObject: Serialize + for<'de> Deserialize<'de> { fn sid(&self) -> &SmartId; fn set_sid(&mut self, sid: SmartId); fn type_name() -> &'static str; fn indexed_fields(&self)->Vec<(String,String)> {..} fn indexed_fields_json(&self)->Vec<(String,serde_json::Value)> {..} fn indexed_field_names()->&'static [&'static str] {..} } ``` After (String-sid, matches `openrpc_server!`-generated wire types where `sid: Option<String>`): ```rust pub trait OsisObject: Serialize + for<'de> Deserialize<'de> { /// Current sid ("" / None ⇒ unsaved → create on set). fn sid(&self) -> String; // empty string when absent fn set_sid(&mut self, sid: String); fn type_name() -> &'static str; // indexed_* unchanged } ``` (Generated impls read/write the type's `sid: Option<String>`: `sid()` → `self.sid.clone().unwrap_or_default()`; `set_sid(s)` → `self.sid = Some(s)`.) ### DBTyped<T> changes (`db.rs`) - `object_path(sid:&str)` → `type_dir().join(format!("{sid}.otoml"))`. - `get/delete/exists(&self, sid:&str)`, `list(&self)->Result<Vec<String>>` (return String sids; filter `__osis_meta:`-style as today / read dir entries). - `set(&self, obj:&mut T)`: if `obj.sid()` empty ⇒ mint via `id_new()` → `obj.set_sid(new_sid_string)`; else use as-is; write OTOML at `object_path(&obj.sid())`. - `id_new(&self) -> Result<String>`: keep the SmartId compute/counter logic internally, return `sid.as_str()` (String). (SmartId stays the minting algorithm; the stored/keyed sid is its String form — same as today's `as_str()`.) - `NotFound(sid.to_string())`. ### Ripple (chunk 2 handles the generator side) - The generator's emitted `impl OsisObject for <Type>` (currently in `rust_struct.rs` / osis_impl) must switch to the String-sid bodies above, against the `openrpc_server!` types (`sid: Option<String>`, `created_at/updated_at: Option<String>`), and stamp `created_at/updated_at` as strings on create. - `rust_osis.rs` CRUD callers: covered by the chunk-2 rewrite to `impl <Domain>Api` (the old `user_new/user_set(sid,..)` go away; `_set` upsert reads `obj.sid()`). - `rhai.rs` `DynamicObject` impl + tests in `db/tests.rs`: update to String sid. ### Acceptance `cargo check -p hero_rpc_osis --features rpc` green after chunk 1 + the matching generated-impl change; `db/tests.rs` (String sid) pass. --- **Status / compaction boundary:** Plan + target shapes + decision (A) + this chunk-1 spec are now fully journaled here — enough to execute chunks 1→5 cleanly in a fresh context. Resuming = start chunk 1 from this comment on `feature/osis-multidomain-serve`.
Author
Owner

Chunk 1 DONE — OsisObject/DBTyped String-sid foundation

Committed 0164e33 on feature/osis-multidomain-serve (pushed).

What landed (crates/osis/src/db/db.rs, db/tests.rs, rhai.rs):

  • OsisObject: sid(&self) -> String / set_sid(&mut self, sid: String). Empty string ⇒ unsaved (create on set). Matches the openrpc_server!-generated wire types where sid: Option<String> (generated impls will return self.sid.clone().unwrap_or_default()).
  • DBTyped<T>: object_path(&str), get/delete/exists(&self, sid: &str), list(&self) -> Result<Vec<String>> (filters to valid-SmartId stems), set() mints on empty sid, id_new() -> Result<String>. SmartId stays the minting algorithm; the stored/keyed sid is its string form (sid.as_str()), so distributed collision-free minting + the flock counter are unchanged.
  • rhai.rs DynamicObject (sid: Option<String>) and db/tests.rs (SimpleItem/ComplexItem now Option<String>) updated; gid() test helper decodes a minted sid back to its global id where tests asserted ordering.

Verified: cargo check -p hero_rpc_osis green on default / rpc / rhai; 39 db tests pass (1 ignored — needs PATH_ROOT), incl. both concurrency/no-collision tests and counter-persistence.

Note for the hero_db merge: this touches the same OsisObject/DBTyped trait that feature/osis-hero-db-storage (redb backend) revises — reconcile the String-sid surface during that merge. Backend here stays dev's filesystem-OTOML.

Next: chunk 2 — generator emits #[async_trait] impl <Domain>Api for Osis<Domain> (DBTyped bodies, _set upsert, errors→RpcError) and adopts hero_lib openrpc_server! for the types/trait/router/spec; drops the bespoke CRUD + handle_rpc + OsisAppRpcHandler emission (also resolves #155's _new/<Type>Input removal in the same pass).

## Chunk 1 DONE — `OsisObject`/`DBTyped` String-sid foundation ✅ Committed `0164e33` on `feature/osis-multidomain-serve` (pushed). **What landed** (`crates/osis/src/db/db.rs`, `db/tests.rs`, `rhai.rs`): - `OsisObject`: `sid(&self) -> String` / `set_sid(&mut self, sid: String)`. Empty string ⇒ unsaved (create on `set`). Matches the `openrpc_server!`-generated wire types where `sid: Option<String>` (generated impls will return `self.sid.clone().unwrap_or_default()`). - `DBTyped<T>`: `object_path(&str)`, `get/delete/exists(&self, sid: &str)`, `list(&self) -> Result<Vec<String>>` (filters to valid-SmartId stems), `set()` mints on empty sid, `id_new() -> Result<String>`. **SmartId stays the minting algorithm**; the stored/keyed sid is its string form (`sid.as_str()`), so distributed collision-free minting + the flock counter are unchanged. - `rhai.rs` `DynamicObject` (`sid: Option<String>`) and `db/tests.rs` (`SimpleItem`/`ComplexItem` now `Option<String>`) updated; `gid()` test helper decodes a minted sid back to its global id where tests asserted ordering. **Verified:** `cargo check -p hero_rpc_osis` green on default / `rpc` / `rhai`; **39 db tests pass** (1 ignored — needs `PATH_ROOT`), incl. both concurrency/no-collision tests and counter-persistence. **Note for the hero_db merge:** this touches the same `OsisObject`/`DBTyped` trait that `feature/osis-hero-db-storage` (redb backend) revises — reconcile the String-sid surface during that merge. Backend here stays dev's filesystem-OTOML. Next: **chunk 2** — generator emits `#[async_trait] impl <Domain>Api for Osis<Domain>` (DBTyped bodies, `_set` upsert, errors→`RpcError`) and adopts hero_lib `openrpc_server!` for the types/trait/router/spec; drops the bespoke CRUD + `handle_rpc` + `OsisAppRpcHandler` emission (also resolves #155's `_new`/`<Type>Input` removal in the same pass).
Author
Owner

Chunk 2 build sheet — generator adopts openrpc_server!, emits impl <Domain>Api (executable)

Mapped the full generator call graph + the openrpc_server! target. Chunk 2 is the keystone; it reshapes three layers (generator emission, scaffold templates, and the core/server crate split). Journaling the concrete plan + the one gating decision it surfaces, so it can be executed in a focused pass without leaving a broken tree.

Generator call graph (where the rewrite lands)

  • Consumer build.rshero_rpc_osis::build::OschemaBuilder::from_service_toml().generate().
  • OschemaBuilder::generate_domain() (crates/generator/src/build/emit/domain.rs) → Generator::from_dir(dir).server(domain).generate()crates/generator/src/generate/rust_server.rscrate::rust::generate_rust_osis(&domains) (rust_osis.rs:319/337).
  • Output is committed source (not OUT_DIR): core types in crates/<name>/src/<dom>/types_generated.rs; server handler in crates/<name>_server/src/<dom>/server/osis_server_generated.rs; spec at docs/openrpc.json.
  • Scaffold templates (Cargo.toml/main.rs/service.toml) in crates/generator/src/build/scaffold.rs.

⚠️ Gating decision (shapes everything): where is openrpc_server! invoked?

openrpc_server!(spec=<dir>, service_toml=…) generates the wire types itself (the <Type> structs with sid: Option<String>), the <Domain>Api trait, the <dom>_rpc::router/OPENRPC_JSON, and the top-level serve_domains(...). So today's rust_struct.rs type emission becomes redundant and must be retired in favour of the macro's types — otherwise two definitions collide.

Recommendation — invoke openrpc_server! in the CORE crate (crates/<name>/), not the server crate. Rationale + resulting layout (minimises ripple, keeps types where SDK/admin/web already import them):

  • CORE crate (crates/<name>): one openrpc_server!(spec="schemas", service_toml="…/service.toml") invocation ⇒ emits per-domain module <dom> { wire types + <Domain>Api trait + <dom>_rpc(router, OPENRPC_JSON) } + top-level serve_domains(...). Generator additionally emits impl OsisObject for <dom>::<Type> here (same crate as the type ⇒ no orphan issue; String-sid bodies from chunk 1 + indexed_fields_json).
  • SERVER crate (crates/<name>_server): generator emits the Osis<Dom> struct (holds DBTyped<<dom>::<Type>> per rootobject + OsisIndexer + triggers) and #[async_trait] impl <dom>::<Domain>Api for Osis<Dom> with DBTyped-backed bodies. main.rs = <name>::serve_domains(Osis<Dom1>::create(db,uid)?, Osis<Dom2>::create(...)?, …).await (trait is generic, impls passed in by value — they can live in the server crate while serve_domains lives in core).

This is "true convergence" (decision A): the OpenRPC spec is the single source of the types, generated once by the macro.

Target generated impl <Domain>Api body shape (per rootobject, DBTyped-backed)

Maps the macro's CRUD trait methods onto chunk-1 storage. No _new; _set is upsert (resolves #155). Errors Box<dyn Error>RpcError (not_found→-32002, invalid_params→-32602, else internal→-32603).

async fn company_get(&self, _ctx: &HeroRequestContext, input: CompanyGetInput)
    -> Result<CompanyGetOutput, RpcError> {
    let mut obj = self.company_db.get(&input.sid)
        .map_err(|e| RpcError::not_found(e.to_string()))?;
    Self::company_trigger_get_post(&mut obj);
    Ok(obj.into())                              // CompanyGetOutput is the full object schema
}

async fn company_set(&self, _ctx: &HeroRequestContext, mut input: CompanySetInput)
    -> Result<CompanySetOutput, RpcError> {
    let mut obj = input.data;                   // full object; sid empty ⇒ create, present ⇒ update
    let creating = obj.sid().is_empty();
    if creating { obj.created_at = Some(now_string()); Self::company_trigger_new_post(&mut obj); }
    obj.updated_at = Some(now_string());
    if !Self::company_trigger_save_pre(&mut obj) { return Err(RpcError::invalid_params("save cancelled")); }
    self.company_db.set(&mut obj).map_err(|e| RpcError::internal(e.to_string()))?;  // mints sid on empty
    let sid = obj.sid();
    let _ = self.indexer.index_document("company", &sid,
        <Company as OsisObject>::indexed_fields_json(&obj)).await;
    Self::company_trigger_save_post(&obj);
    Ok(CompanySetOutput { value: sid })
}
// _delete: db.delete(&input.sid) + indexer.delete_document; _list: db.list(); 
// _list_full: list()→get each; _exists: db.exists(&input.sid); _find: indexer.search(params→query)

Note: get/delete/exists now take a &str sid directly (chunk 1) — drop the old SmartId::parse(sid) step. _set no longer constructs from a partial <Type>Input (that struct is gone, #155) — it takes the full object.

DELETE (chunk 3, after this compiles)

rust_osis.rs bespoke CRUD (_new, two-arg _set, _rpc_*, handle_rpc, OsisAppRpcHandler impl emission); rust_struct.rs type + <Type>Input + From<&Type> emission; schemas/openrpc.rs _new/input-schema emission; the SDK new/<Type>NewInput. And the runtime infra: crates/osis/src/rpc/bootstrap.rs (HeroOsisServer + serve_domains stopgap), hero_osis_server! (crates/osis/src/lib.rs), rpc2_adapter + OsisAppRpcHandler (crates/osis/src/rpc/{rpc2_adapter,server}.rs).

Scaffold template changes (build/scaffold.rs)

  • Core Cargo.toml: add herolib_derive (for openrpc_server!) + herolib_oschema_server (HeroRequestContext/RpcError) + async-trait.
  • Server Cargo.toml: drop hero_rpc2 + jsonrpsee; keep hero_rpc_osis (storage/indexer) + add async-trait, herolib_oschema_server.
  • Server main.rs: replace hero_osis_server!()...serve() with <name>::serve_domains(...).
  • service.toml: emit control-plane rpc.sock + one rpc_<domain>.sock per domain (serve_rpc_domains Model A), matching the hero_lib example service.toml.
  1. Hand-write the CRM target (single workspace, examples/crm) to the shape above and get it to build + boot + driver-green — proves openrpc_server! works against an OSIS oschema (7 rootobjects + service) end-to-end. This is the exact template the generator must emit. (This is the old chunk-4 pulled forward as the proof.)
  2. Make generate_rust_osis + rust_struct + scaffold emit that template; regenerate CRM and confirm byte-for-byte-equivalent build+boot.
  3. Chunk 3 delete the dead serving.
  4. Chunk 5: regenerate hero_osis (15 domains) on feature/osis-hero-db-storage; drop the per-domain-socket service.toml hack; build+boot+CRUD+collision+discover_domains.

Acceptance (chunk 2)

examples/crm builds with openrpc_server!-generated types/trait/router; Osis<Dom> impls compile against the trait; serve_domains boots; CRM driver passes all 8 phases over per-domain sockets.


Status: chunk 1 landed (0164e33, verified). Chunk 2 is a focused cross-crate pass best executed start-to-finish in one sitting; this sheet (call graph + layout decision + body shapes + scaffold diffs + sequencing) makes it executable. Resume = step 1 above (hand-prototype CRM target on feature/osis-multidomain-serve).

## Chunk 2 build sheet — generator adopts `openrpc_server!`, emits `impl <Domain>Api` (executable) Mapped the full generator call graph + the `openrpc_server!` target. Chunk 2 is the keystone; it reshapes **three** layers (generator emission, scaffold templates, **and** the core/server crate split). Journaling the concrete plan + the one gating decision it surfaces, so it can be executed in a focused pass without leaving a broken tree. ### Generator call graph (where the rewrite lands) - Consumer `build.rs` → `hero_rpc_osis::build::OschemaBuilder::from_service_toml().generate()`. - → `OschemaBuilder::generate_domain()` (`crates/generator/src/build/emit/domain.rs`) → `Generator::from_dir(dir).server(domain).generate()` → `crates/generator/src/generate/rust_server.rs` → `crate::rust::generate_rust_osis(&domains)` (`rust_osis.rs:319/337`). - Output is **committed source** (not `OUT_DIR`): core types in `crates/<name>/src/<dom>/types_generated.rs`; server handler in `crates/<name>_server/src/<dom>/server/osis_server_generated.rs`; spec at `docs/openrpc.json`. - Scaffold templates (Cargo.toml/main.rs/service.toml) in `crates/generator/src/build/scaffold.rs`. ### ⚠️ Gating decision (shapes everything): where is `openrpc_server!` invoked? `openrpc_server!(spec=<dir>, service_toml=…)` **generates the wire types itself** (the `<Type>` structs with `sid: Option<String>`), the `<Domain>Api` trait, the `<dom>_rpc::router`/`OPENRPC_JSON`, and the top-level `serve_domains(...)`. So today's `rust_struct.rs` type emission becomes redundant and must be retired in favour of the macro's types — otherwise two definitions collide. **Recommendation — invoke `openrpc_server!` in the CORE crate** (`crates/<name>/`), not the server crate. Rationale + resulting layout (minimises ripple, keeps types where SDK/admin/web already import them): - **CORE crate** (`crates/<name>`): one `openrpc_server!(spec="schemas", service_toml="…/service.toml")` invocation ⇒ emits per-domain module `<dom>` { wire types + `<Domain>Api` trait + `<dom>_rpc`(router, `OPENRPC_JSON`) } + top-level `serve_domains(...)`. Generator additionally emits `impl OsisObject for <dom>::<Type>` here (same crate as the type ⇒ no orphan issue; String-sid bodies from chunk 1 + `indexed_fields_json`). - **SERVER crate** (`crates/<name>_server`): generator emits the `Osis<Dom>` struct (holds `DBTyped<<dom>::<Type>>` per rootobject + `OsisIndexer` + triggers) and `#[async_trait] impl <dom>::<Domain>Api for Osis<Dom>` with DBTyped-backed bodies. `main.rs` = `<name>::serve_domains(Osis<Dom1>::create(db,uid)?, Osis<Dom2>::create(...)?, …).await` (trait is generic, impls passed in by value — they can live in the server crate while `serve_domains` lives in core). This is "true convergence" (decision A): the OpenRPC spec is the single source of the types, generated once by the macro. ### Target generated `impl <Domain>Api` body shape (per rootobject, DBTyped-backed) Maps the macro's CRUD trait methods onto chunk-1 storage. No `_new`; `_set` is upsert (resolves #155). Errors `Box<dyn Error>` → `RpcError` (`not_found`→-32002, `invalid_params`→-32602, else `internal`→-32603). ```rust async fn company_get(&self, _ctx: &HeroRequestContext, input: CompanyGetInput) -> Result<CompanyGetOutput, RpcError> { let mut obj = self.company_db.get(&input.sid) .map_err(|e| RpcError::not_found(e.to_string()))?; Self::company_trigger_get_post(&mut obj); Ok(obj.into()) // CompanyGetOutput is the full object schema } async fn company_set(&self, _ctx: &HeroRequestContext, mut input: CompanySetInput) -> Result<CompanySetOutput, RpcError> { let mut obj = input.data; // full object; sid empty ⇒ create, present ⇒ update let creating = obj.sid().is_empty(); if creating { obj.created_at = Some(now_string()); Self::company_trigger_new_post(&mut obj); } obj.updated_at = Some(now_string()); if !Self::company_trigger_save_pre(&mut obj) { return Err(RpcError::invalid_params("save cancelled")); } self.company_db.set(&mut obj).map_err(|e| RpcError::internal(e.to_string()))?; // mints sid on empty let sid = obj.sid(); let _ = self.indexer.index_document("company", &sid, <Company as OsisObject>::indexed_fields_json(&obj)).await; Self::company_trigger_save_post(&obj); Ok(CompanySetOutput { value: sid }) } // _delete: db.delete(&input.sid) + indexer.delete_document; _list: db.list(); // _list_full: list()→get each; _exists: db.exists(&input.sid); _find: indexer.search(params→query) ``` Note: `get`/`delete`/`exists` now take a `&str` sid directly (chunk 1) — drop the old `SmartId::parse(sid)` step. `_set` no longer constructs from a partial `<Type>Input` (that struct is gone, #155) — it takes the full object. ### DELETE (chunk 3, after this compiles) `rust_osis.rs` bespoke CRUD (`_new`, two-arg `_set`, `_rpc_*`, `handle_rpc`, `OsisAppRpcHandler` impl emission); `rust_struct.rs` type + `<Type>Input` + `From<&Type>` emission; `schemas/openrpc.rs` `_new`/input-schema emission; the SDK `new`/`<Type>NewInput`. And the runtime infra: `crates/osis/src/rpc/bootstrap.rs` (HeroOsisServer + `serve_domains` stopgap), `hero_osis_server!` (`crates/osis/src/lib.rs`), `rpc2_adapter` + `OsisAppRpcHandler` (`crates/osis/src/rpc/{rpc2_adapter,server}.rs`). ### Scaffold template changes (`build/scaffold.rs`) - Core Cargo.toml: add `herolib_derive` (for `openrpc_server!`) + `herolib_oschema_server` (HeroRequestContext/RpcError) + `async-trait`. - Server Cargo.toml: drop `hero_rpc2` + `jsonrpsee`; keep `hero_rpc_osis` (storage/indexer) + add `async-trait`, `herolib_oschema_server`. - Server `main.rs`: replace `hero_osis_server!()...serve()` with `<name>::serve_domains(...)`. - `service.toml`: emit control-plane `rpc.sock` + one `rpc_<domain>.sock` per domain (serve_rpc_domains Model A), matching the hero_lib example service.toml. ### Recommended sequencing (de-risk before touching the generator) 1. **Hand-write the CRM target** (single workspace, `examples/crm`) to the shape above and get it to **build + boot + driver-green** — proves `openrpc_server!` works against an OSIS oschema (7 rootobjects + service) end-to-end. This is the exact template the generator must emit. (This is the old chunk-4 pulled forward as the proof.) 2. Make `generate_rust_osis` + `rust_struct` + scaffold emit that template; regenerate CRM and confirm byte-for-byte-equivalent build+boot. 3. **Chunk 3 delete** the dead serving. 4. **Chunk 5**: regenerate hero_osis (15 domains) on `feature/osis-hero-db-storage`; drop the per-domain-socket `service.toml` hack; build+boot+CRUD+collision+`discover_domains`. ### Acceptance (chunk 2) `examples/crm` builds with `openrpc_server!`-generated types/trait/router; `Osis<Dom>` impls compile against the trait; `serve_domains` boots; CRM driver passes all 8 phases over per-domain sockets. --- **Status:** chunk 1 landed (`0164e33`, verified). Chunk 2 is a focused cross-crate pass best executed start-to-finish in one sitting; this sheet (call graph + layout decision + body shapes + scaffold diffs + sequencing) makes it executable. Resume = step 1 above (hand-prototype CRM target on `feature/osis-multidomain-serve`).
Author
Owner

Decision: openrpc_server! invoked in the SERVER crate (timur)

Confirmed: per Kristof's hero_lib pattern + hero_skills best practices, the macro lives in crates/<name>_server (not core). Verified against hero_lib/crates/hero_lifecycle/examples/server/main.rsopenrpc_server!(spec=…, service_toml=…) + serve_domains(...) + #[async_trait] impl <Domain>Api all live in the server binary; the SDK is independent (openrpc_client! against the spec, its own types), so core needn't own the wire types.

Confirmed OSIS oschema format (Company = { name: str @index … } + service CrmService { … }) is shape-identical to the hero_lib demo oschema, so openrpc_server!(spec=<dir of domain subdirs>) consumes OSIS schemas as-is.

Revised chunk-2 layout (server crate owns the macro):

  • crates/<name>_server/src/main.rs: openrpc_server!(spec="<schemas dir>", service_toml="service.toml", save_openrpc_dir=…) → per-domain module {types + <Domain>Api + <dom>_rpc} + serve_domains(...); then serve_domains(OsisCrm::create(db,uid)?, …).await.
  • Generator emits into the server crate: impl OsisObject for <dom>::<Type> (String-sid, chunk 1), Osis<Dom> struct (DBTyped + OsisIndexer + triggers), #[async_trait] impl <dom>::<Domain>Api for Osis<Dom> with DBTyped bodies. <Domain>Api method names: CRUD <type>_<op> (e.g. company_get), service methods <service_snake>_<method> (e.g. crm_service_open_opportunities).
  • Body mapping: *_getdb.get(&input.sid) then field-by-field into <Type>GetOutput; *_list_fullVec<<Type>> into …Output{value}; *_set upsert (full object, mint on empty sid) + indexer write-through; errors → RpcError.
  • Scaffold deps: server Cargo.toml adds herolib_derive, herolib_oschema_server, async-trait; drops hero_rpc2/jsonrpsee; keeps hero_rpc_osis (storage/indexer). service.toml emits control-plane rpc.sock + per-domain rpc_<domain>.sock.

Approach: rewriting the generator directly (no separate hand-prototype). The hero_rpc_generator lib stays compilable throughout (it only templates strings); the broken stretch is contained to regenerate→build CRM, where I'll iterate to green.

## Decision: `openrpc_server!` invoked in the **SERVER crate** (timur) Confirmed: per Kristof's hero_lib pattern + hero_skills best practices, the macro lives in `crates/<name>_server` (not core). Verified against `hero_lib/crates/hero_lifecycle/examples/server/main.rs` — `openrpc_server!(spec=…, service_toml=…)` + `serve_domains(...)` + `#[async_trait] impl <Domain>Api` all live in the server binary; the SDK is independent (`openrpc_client!` against the spec, its own types), so core needn't own the wire types. Confirmed OSIS oschema format (`Company = { name: str @index … }` + `service CrmService { … }`) is shape-identical to the hero_lib demo oschema, so `openrpc_server!(spec=<dir of domain subdirs>)` consumes OSIS schemas as-is. **Revised chunk-2 layout (server crate owns the macro):** - `crates/<name>_server/src/main.rs`: `openrpc_server!(spec="<schemas dir>", service_toml="service.toml", save_openrpc_dir=…)` → per-domain module {types + `<Domain>Api` + `<dom>_rpc`} + `serve_domains(...)`; then `serve_domains(OsisCrm::create(db,uid)?, …).await`. - Generator emits into the server crate: `impl OsisObject for <dom>::<Type>` (String-sid, chunk 1), `Osis<Dom>` struct (DBTyped + OsisIndexer + triggers), `#[async_trait] impl <dom>::<Domain>Api for Osis<Dom>` with DBTyped bodies. `<Domain>Api` method names: CRUD `<type>_<op>` (e.g. `company_get`), service methods `<service_snake>_<method>` (e.g. `crm_service_open_opportunities`). - Body mapping: `*_get` → `db.get(&input.sid)` then field-by-field into `<Type>GetOutput`; `*_list_full` → `Vec<<Type>>` into `…Output{value}`; `*_set` upsert (full object, mint on empty sid) + indexer write-through; errors → `RpcError`. - Scaffold deps: server Cargo.toml adds `herolib_derive`, `herolib_oschema_server`, `async-trait`; drops `hero_rpc2`/`jsonrpsee`; keeps `hero_rpc_osis` (storage/indexer). service.toml emits control-plane `rpc.sock` + per-domain `rpc_<domain>.sock`. Approach: rewriting the generator directly (no separate hand-prototype). The `hero_rpc_generator` lib stays compilable throughout (it only templates strings); the broken stretch is contained to regenerate→build CRM, where I'll iterate to green.
Author
Owner

Chunk 2 — generator internals mapped (exact edit sites for the execution pass)

Read the full emission. The rewrite touches these exact spots; capturing so the focused pass is pure mechanics:

crates/generator/src/rust/rust_osis.rs (1756 lines) — generate_domain (159) calls, in order: generate_handler_trait, generate_oschema_constant, generate_domain_struct (227 — KEEP, holds *_db: DBTyped<T> + indexer + service handlers), generate_domain_impl (328 — KEEP ctor/indexer wiring; the CRUD methods it emits via generate_crud_methods (518) get rewritten), generate_rpc_handler (818 — DELETE: RpcRequest/Response + handle_rpc + per-ro generate_rpc_methods (1067) + generate_service_rpc_methods (1280)), generate_osis_app_rpc_handler (1393 — DELETE, incl. the OsisDomainInit impl which must be replaced by create() kept as a plain fn). New: emit #[async_trait] impl <dom>::<Domain>Api for Osis<Dom> with DBTyped bodies (the _get/_set/_delete/_list/_list_full/_exists/_find mapping from comment 38174) + the service-method trait fns.

  • Note: today _get does SmartId::parse(sid) then db.get(&smart_id); chunk 1 made db.get(&str), so drop the parse. obj.sid.as_str().to_string()obj.sid().

crates/generator/src/rust/rust_struct.rs (2316 lines):

  • generate_struct (899) emits <Name> (with pub sid: SmartId, created_at/updated_at: OTime) + <Name>Input + From<&Name> + <Name>FindParams. Under the new model the type + Input come from openrpc_server! (server crate) → stop emitting <Name>/<Name>Input/From (resolves #155); keep <Name>FindParams (the macro doesn't generate it) OR confirm the macro emits <Type>FindParams (it referenced TodoFindParams — verify before deleting our emission).
  • generate_osis_impl_block (51) currently emits impl OsisObject with fn sid(&self)->&SmartId { &self.sid } / set_sid(SmartId)rewrite to String-sid (self.sid.clone().unwrap_or_default() / self.sid = Some(sid)) against the macro types' sid: Option<String>, and stamp created_at/updated_at as Option<String>. This is the generator-side of chunk 1; it's the function that survives as the OsisObject bridge.

crates/derive — the #[derive(OsisObject)] macro is dead under the new model (can't derive on macro-generated types); generated code uses the explicit impl from generate_osis_impl_block. Remove its use in scaffolds.

Build orchestrationgenerate/rust_server.rs + build/emit/* + build/scaffold.rs: server crate gets the openrpc_server!(spec=<schemas>, service_toml=…, save_openrpc_dir=…) invocation in main.rs, #[path] mods per domain for the impl <Domain>Api, then serve_domains(OsisCrm::create(db,uid)?, …).await. service.toml → control-plane rpc.sock + per-domain rpc_<domain>.sock. Cargo.toml: +herolib_derive,+herolib_oschema_server,+async-trait; −hero_rpc2,−jsonrpsee.

Latent state after chunk 1 (expected)

generate_osis_impl_block + the OsisObject derive still emit &SmartId bodies, so a consumer regenerated today won't compile against chunk 1's String-sid trait until chunk 2 lands. This is by design (chunk 1 shipped knowing chunk 2 follows) and is contained — hero_rpc_osis itself + its tests are green; no consumer was regenerated.

Checkpoint: chunk 1 shipped+verified (0164e33); chunk 2 fully scoped to exact edit sites + macro-location decided (server crate). Chunk 2 is one atomic cross-crate pass (≈6 files + regenerate/debug CRM to green) with a long consumer-non-compiling window — to be executed as a dedicated focused sitting. Resume = gut rust_osis.rs serving emission + rewrite generate_osis_impl_block + wire openrpc_server! into the server scaffold, then regenerate CRM to green.

## Chunk 2 — generator internals mapped (exact edit sites for the execution pass) Read the full emission. The rewrite touches these exact spots; capturing so the focused pass is pure mechanics: **`crates/generator/src/rust/rust_osis.rs`** (1756 lines) — `generate_domain` (159) calls, in order: `generate_handler_trait`, `generate_oschema_constant`, `generate_domain_struct` (227 — **KEEP**, holds `*_db: DBTyped<T>` + `indexer` + service handlers), `generate_domain_impl` (328 — KEEP ctor/indexer wiring; **the CRUD methods it emits via `generate_crud_methods` (518) get rewritten**), `generate_rpc_handler` (818 — **DELETE**: RpcRequest/Response + `handle_rpc` + per-ro `generate_rpc_methods` (1067) + `generate_service_rpc_methods` (1280)), `generate_osis_app_rpc_handler` (1393 — **DELETE**, incl. the `OsisDomainInit` impl which must be replaced by `create()` kept as a plain fn). New: emit `#[async_trait] impl <dom>::<Domain>Api for Osis<Dom>` with DBTyped bodies (the `_get`/`_set`/`_delete`/`_list`/`_list_full`/`_exists`/`_find` mapping from comment 38174) + the service-method trait fns. - Note: today `_get` does `SmartId::parse(sid)` then `db.get(&smart_id)`; chunk 1 made `db.get(&str)`, so drop the parse. `obj.sid.as_str().to_string()` → `obj.sid()`. **`crates/generator/src/rust/rust_struct.rs`** (2316 lines): - `generate_struct` (899) emits `<Name>` (with `pub sid: SmartId`, `created_at/updated_at: OTime`) + `<Name>Input` + `From<&Name>` + `<Name>FindParams`. Under the new model the **type + Input come from `openrpc_server!`** (server crate) → stop emitting `<Name>`/`<Name>Input`/`From` (resolves #155); **keep `<Name>FindParams`** (the macro doesn't generate it) OR confirm the macro emits `<Type>FindParams` (it referenced `TodoFindParams` — verify before deleting our emission). - `generate_osis_impl_block` (51) currently emits `impl OsisObject` with `fn sid(&self)->&SmartId { &self.sid }` / `set_sid(SmartId)` — **rewrite to String-sid** (`self.sid.clone().unwrap_or_default()` / `self.sid = Some(sid)`) against the macro types' `sid: Option<String>`, and stamp `created_at/updated_at` as `Option<String>`. This is the generator-side of chunk 1; it's the function that survives as the OsisObject bridge. **`crates/derive`** — the `#[derive(OsisObject)]` macro is dead under the new model (can't derive on macro-generated types); generated code uses the explicit `impl` from `generate_osis_impl_block`. Remove its use in scaffolds. **Build orchestration** — `generate/rust_server.rs` + `build/emit/*` + `build/scaffold.rs`: server crate gets the `openrpc_server!(spec=<schemas>, service_toml=…, save_openrpc_dir=…)` invocation in `main.rs`, `#[path]` mods per domain for the `impl <Domain>Api`, then `serve_domains(OsisCrm::create(db,uid)?, …).await`. service.toml → control-plane `rpc.sock` + per-domain `rpc_<domain>.sock`. Cargo.toml: +`herolib_derive`,+`herolib_oschema_server`,+`async-trait`; −`hero_rpc2`,−`jsonrpsee`. ### Latent state after chunk 1 (expected) `generate_osis_impl_block` + the `OsisObject` derive still emit `&SmartId` bodies, so a consumer regenerated *today* won't compile against chunk 1's String-sid trait until chunk 2 lands. This is by design (chunk 1 shipped knowing chunk 2 follows) and is contained — `hero_rpc_osis` itself + its tests are green; no consumer was regenerated. **Checkpoint:** chunk 1 shipped+verified (`0164e33`); chunk 2 fully scoped to exact edit sites + macro-location decided (server crate). Chunk 2 is one atomic cross-crate pass (≈6 files + regenerate/debug CRM to green) with a long consumer-non-compiling window — to be executed as a dedicated focused sitting. Resume = gut `rust_osis.rs` serving emission + rewrite `generate_osis_impl_block` + wire `openrpc_server!` into the server scaffold, then regenerate CRM to green.
Author
Owner

Chunk 2 — openrpc_server! identifier rules pinned (no re-derivation needed)

Confirmed against hero_lib crates/derive/src/openrpc_server.rs + crates/oschema/src/oschema/openrpc.rs:

  • Domain module: to_snake_case(domain.name) → e.g. crm. Types/trait/<stem>_rpc live at crate::<mod>::….
  • Trait stem: to_pascal_case(spec.info.title); info.title = first service {} block name (openrpc.rs:478-487), else "OSchema API". CRM service CrmServiceinfo.title="CrmService" ⇒ trait crm::CrmServiceApi. (Domains with no service block get a degenerate stem — every OSIS domain should carry a service block.)
  • Per-method structs: to_pascal_case(fn_name)+{Input,Output}. CRUD fn = <type>_<op> (company_getCompanyGetInput/Output); service fn = <service_snake>_<method> (crm_service_open_opportunitiesCrmServiceOpenOpportunitiesInput/Output).
  • Shapes (from the example): …GetOutput=full object fields; …SetInput{ #[serde(flatten)] data: <Type> }; …SetOutput{value:String}; …ListOutput{value:Vec<String>}; …ListFullOutput{value:Vec<<Type>>}; …ExistsOutput{value:bool}; …DeleteOutput{value:bool}; …FindInput{params:Option<<Type>FindParams>},…FindOutput{value:Vec<String>}.

Starting the emission rewrite now, foundation-first: core type + OsisObject impl → Option<String> sid / Option<String> timestamps (matches the macro wire types + chunk-1 trait), generator lib kept green at each step.

## Chunk 2 — `openrpc_server!` identifier rules pinned (no re-derivation needed) Confirmed against hero_lib `crates/derive/src/openrpc_server.rs` + `crates/oschema/src/oschema/openrpc.rs`: - **Domain module**: `to_snake_case(domain.name)` → e.g. `crm`. Types/trait/`<stem>_rpc` live at `crate::<mod>::…`. - **Trait stem**: `to_pascal_case(spec.info.title)`; `info.title` = **first `service {}` block name** (openrpc.rs:478-487), else `"OSchema API"`. CRM `service CrmService` ⇒ `info.title="CrmService"` ⇒ trait **`crm::CrmServiceApi`**. (Domains with no service block get a degenerate stem — every OSIS domain should carry a service block.) - **Per-method structs**: `to_pascal_case(fn_name)+{Input,Output}`. CRUD fn = `<type>_<op>` (`company_get`→`CompanyGetInput/Output`); service fn = `<service_snake>_<method>` (`crm_service_open_opportunities`→`CrmServiceOpenOpportunitiesInput/Output`). - **Shapes** (from the example): `…GetOutput`=full object fields; `…SetInput{ #[serde(flatten)] data: <Type> }`; `…SetOutput{value:String}`; `…ListOutput{value:Vec<String>}`; `…ListFullOutput{value:Vec<<Type>>}`; `…ExistsOutput{value:bool}`; `…DeleteOutput{value:bool}`; `…FindInput{params:Option<<Type>FindParams>}`,`…FindOutput{value:Vec<String>}`. Starting the emission rewrite now, foundation-first: core type + `OsisObject` impl → `Option<String>` sid / `Option<String>` timestamps (matches the macro wire types + chunk-1 trait), generator lib kept green at each step.
Author
Owner

Chunk 2a DONE — String-sid wire types + OsisObject impl (8ba3eff)

Pushed on feature/osis-multidomain-serve. Generator lib compiles; 151 generator unit tests pass (also fixed 5 pre-existing stale to_openrpc().unwrap() test calls that were blocking the suite).

  • rust_struct.rs generate_struct: root-object fields → sid/created_at/updated_at: Option<String> (was SmartId/OTime) + skip_serializing_if; dropped the SmartId import and the root-object-driven OTime import.
  • rust_struct.rs generate_osis_impl_block: OsisObject impl → String-sid surface (sid()->self.sid.clone().unwrap_or_default(), set_sid(Some(..))), matching chunk-1's trait.

Refinement to the 2b plan (server emission)

Because the decision is openrpc_server! in the server crate, the macro generates the wire types there (crate::<dom>::<Type>). Therefore the DBTyped<T> fields and impl OsisObject for <Type> must be emitted on the macro types in the server crate — so:

  • generate_osis_impl_block (core crate) becomes dead for storage; the OsisObject-impl emission moves into rust_osis.rs (server emission), referencing crate::<dom>::<Type> and reusing the indexed_fields_json classifier (share it out of rust_struct.rs).
  • Osis<Dom>’s DBTyped<{ro.name}> fields resolve against the macro module (server use crate::<dom>::* rather than use super::core::*).

2b remaining (server emission rewrite — the large piece)

In rust_osis.rs: keep generate_domain_struct + ctor/indexer wiring; replace generate_crud_methods output with #[async_trait] impl <dom>::<Stem>Api for Osis<Dom> (CRUD bodies from comment 38174, _get/_set/_delete/_list/_list_full/_exists/_find; get/delete/exists take &str directly — no SmartId::parse; obj.sid() not obj.sid.as_str()); delete generate_rpc_handler/generate_rpc_methods/generate_service_rpc_methods/generate_osis_app_rpc_handler; emit impl OsisObject for <dom>::<Type>; replace OsisDomainInit::create with a plain create() fn. Service methods (e.g. crm_service_open_opportunities) bridge the generated trait method to the preserved CrmServiceHandler. Then scaffold (openrpc_server! in main.rs + #[path] impl mods + serve_domains, per-domain-socket service.toml, Cargo dep swap) and regenerate CRM to green.

Status: chunk 1 (0164e33) + chunk 2a (8ba3eff) shipped/verified. 2b is the next contiguous large pass (gut rust_osis.rs serving emission + scaffold + CRM regen).

## Chunk 2a DONE — String-sid wire types + OsisObject impl ✅ (`8ba3eff`) Pushed on `feature/osis-multidomain-serve`. Generator lib compiles; **151 generator unit tests pass** (also fixed 5 pre-existing stale `to_openrpc().unwrap()` test calls that were blocking the suite). - `rust_struct.rs generate_struct`: root-object fields → `sid/created_at/updated_at: Option<String>` (was `SmartId`/`OTime`) + `skip_serializing_if`; dropped the `SmartId` import and the root-object-driven `OTime` import. - `rust_struct.rs generate_osis_impl_block`: `OsisObject` impl → String-sid surface (`sid()->self.sid.clone().unwrap_or_default()`, `set_sid(Some(..))`), matching chunk-1's trait. ### Refinement to the 2b plan (server emission) Because the decision is **`openrpc_server!` in the server crate**, the macro generates the wire types there (`crate::<dom>::<Type>`). Therefore the `DBTyped<T>` fields **and** `impl OsisObject for <Type>` must be emitted on the **macro types in the server crate** — so: - `generate_osis_impl_block` (core crate) becomes dead for storage; the OsisObject-impl emission **moves into `rust_osis.rs`** (server emission), referencing `crate::<dom>::<Type>` and reusing the `indexed_fields_json` classifier (share it out of `rust_struct.rs`). - `Osis<Dom>`’s `DBTyped<{ro.name}>` fields resolve against the macro module (server `use crate::<dom>::*` rather than `use super::core::*`). ### 2b remaining (server emission rewrite — the large piece) In `rust_osis.rs`: keep `generate_domain_struct` + ctor/indexer wiring; **replace** `generate_crud_methods` output with `#[async_trait] impl <dom>::<Stem>Api for Osis<Dom>` (CRUD bodies from comment 38174, `_get`/`_set`/`_delete`/`_list`/`_list_full`/`_exists`/`_find`; `get/delete/exists` take `&str` directly — no `SmartId::parse`; `obj.sid()` not `obj.sid.as_str()`); **delete** `generate_rpc_handler`/`generate_rpc_methods`/`generate_service_rpc_methods`/`generate_osis_app_rpc_handler`; emit `impl OsisObject for <dom>::<Type>`; replace `OsisDomainInit::create` with a plain `create()` fn. Service methods (e.g. `crm_service_open_opportunities`) bridge the generated trait method to the preserved `CrmServiceHandler`. Then scaffold (`openrpc_server!` in `main.rs` + `#[path]` impl mods + `serve_domains`, per-domain-socket `service.toml`, Cargo dep swap) and regenerate CRM to green. **Status:** chunk 1 (`0164e33`) + chunk 2a (`8ba3eff`) shipped/verified. 2b is the next contiguous large pass (gut `rust_osis.rs` serving emission + scaffold + CRM regen).
Author
Owner

Chunk 2b-core DONE — generator emits impl <Stem>Api over openrpc_server! (760ed90)

rust_osis.rs rewritten: −1,389 / +389 lines. Generator lib compiles; 151 generator unit tests pass. Per domain it now emits:

  • impl OsisObject for <Type> on the macro wire types (String-sid; indexed_fields_json via serde_json::to_value — uniform numeric/bool/string/enum/Option, no classifier needed).
  • Osis<Domain> struct (per-rootobject DBTyped + per-domain indexer + service handlers) with new()/create()/init_handlers().
  • #[async_trait] impl <Stem>Api for Osis<Domain> — CRUD backed by DBTyped + indexer write-through (get→GetOutput field-map; set upsert with the full object, mint on empty sid; delete/list/list_full/exists), errors→RpcError; service methods bridge to the contributor <Service>Trait handler.

Deleted: handle_rpc, the _rpc_* dispatchers, RpcRequest/RpcResponse, OsisAppRpcHandler impl, the bespoke _new / _set(sid,data) CRUD, OSCHEMA_SOURCE emission (resolves #155's _new/<Type>Input removal).

Remaining in chunk 2 (scaffold + orchestration + regen — the next pass)

  1. Module layout: the macro expands mod <dom> at the crate root, so the generated impl file must live in a module that does NOT clash with a src/<dom>/ dir module — emit it as a distinct module (e.g. #[path] mod osis_<dom>) that does use crate::<dom>::*. The current emission’s use crate::<dom>::*; / use super::rpc::*; are provisional until this is fixed in build/emit/rust_server.rs + scaffold.rs.
  2. Scaffold main.rs (build/scaffold.rs generate_server_main_rs): replace hero_osis_server!()…serve() with herolib_derive::openrpc_server!(spec="<schemas>", service_toml="service.toml", save_openrpc_dir=…) + #[path] impl mods + serve_domains(OsisCrm::create(db,uid)?, …).await. Update the scaffold tests at scaffold.rs:3511+ (they assert the old macro).
  3. Cargo deps: +herolib_derive, +herolib_oschema_server, +async-trait; −hero_rpc2, −jsonrpsee. Ensure schemas reachable at server-crate compile time (the macro reads spec at compile time).
  4. service.toml: control-plane rpc.sock + per-domain rpc_<domain>.sock.
  5. Dead core emission: rust_struct.rs generate_osis_impl_block is now redundant (OsisObject moved to the server on macro types) — drop it (and the rpc.rs wrapper bits that referenced OSCHEMA_SOURCE).
  6. _find: currently returns empty (TODO) — lower <Type>FindParams→indexer query (off rust_type string: String→StrFilter, named→EnumFilter, numeric→NumFilter, bool→BoolFilter).
  7. Regenerate CRM (examples/crm/run.sh) → build → boot → driver green; then chunk 5 (hero_osis 15 domains).

Shipped so far: chunk 1 (0164e33), 2a (8ba3eff), 2b-core (760ed90) — all generator-lib/test-verified. Next pass is scaffold + orchestration + CRM regen (verified by the end-to-end run, which needs lab + full-workspace build).

## Chunk 2b-core DONE — generator emits `impl <Stem>Api` over `openrpc_server!` ✅ (`760ed90`) `rust_osis.rs` rewritten: **−1,389 / +389 lines**. Generator lib compiles; **151 generator unit tests pass**. Per domain it now emits: - `impl OsisObject for <Type>` on the macro wire types (String-sid; `indexed_fields_json` via `serde_json::to_value` — uniform numeric/bool/string/enum/Option, no classifier needed). - `Osis<Domain>` struct (per-rootobject `DBTyped` + per-domain indexer + service handlers) with `new()/create()/init_handlers()`. - `#[async_trait] impl <Stem>Api for Osis<Domain>` — CRUD backed by `DBTyped` + indexer write-through (`get`→GetOutput field-map; `set` upsert with the full object, mint on empty sid; `delete/list/list_full/exists`), errors→`RpcError`; service methods bridge to the contributor `<Service>Trait` handler. **Deleted**: `handle_rpc`, the `_rpc_*` dispatchers, `RpcRequest/RpcResponse`, `OsisAppRpcHandler` impl, the bespoke `_new` / `_set(sid,data)` CRUD, `OSCHEMA_SOURCE` emission (resolves #155's `_new`/`<Type>Input` removal). ### Remaining in chunk 2 (scaffold + orchestration + regen — the next pass) 1. **Module layout**: the macro expands `mod <dom>` at the **crate root**, so the generated impl file must live in a module that does NOT clash with a `src/<dom>/` dir module — emit it as a distinct module (e.g. `#[path] mod osis_<dom>`) that does `use crate::<dom>::*`. The current emission’s `use crate::<dom>::*;` / `use super::rpc::*;` are provisional until this is fixed in `build/emit/rust_server.rs` + `scaffold.rs`. 2. **Scaffold `main.rs`** (`build/scaffold.rs generate_server_main_rs`): replace `hero_osis_server!()…serve()` with `herolib_derive::openrpc_server!(spec="<schemas>", service_toml="service.toml", save_openrpc_dir=…)` + `#[path]` impl mods + `serve_domains(OsisCrm::create(db,uid)?, …).await`. Update the scaffold tests at `scaffold.rs:3511+` (they assert the old macro). 3. **Cargo deps**: +`herolib_derive`, +`herolib_oschema_server`, +`async-trait`; −`hero_rpc2`, −`jsonrpsee`. Ensure schemas reachable at server-crate compile time (the macro reads `spec` at compile time). 4. **service.toml**: control-plane `rpc.sock` + per-domain `rpc_<domain>.sock`. 5. **Dead core emission**: `rust_struct.rs generate_osis_impl_block` is now redundant (OsisObject moved to the server on macro types) — drop it (and the `rpc.rs` wrapper bits that referenced `OSCHEMA_SOURCE`). 6. **`_find`**: currently returns empty (TODO) — lower `<Type>FindParams`→indexer query (off `rust_type` string: String→StrFilter, named→EnumFilter, numeric→NumFilter, bool→BoolFilter). 7. **Regenerate CRM** (`examples/crm/run.sh`) → build → boot → driver green; then chunk 5 (hero_osis 15 domains). **Shipped so far:** chunk 1 (`0164e33`), 2a (`8ba3eff`), 2b-core (`760ed90`) — all generator-lib/test-verified. Next pass is scaffold + orchestration + CRM regen (verified by the end-to-end run, which needs `lab` + full-workspace build).
Author
Owner

Chunk 2 — two design refinements for the orchestration pass (record before bring-up)

Planning the scaffold/orchestration surfaced two decisions that revise 2b-core's service model:

  1. serve_domains takes impl <Stem>Api by value → drop the Arc<Handler> + init_handlers machinery. Implement <Stem>Api directly for Osis<Domain> (Send+Sync; passed by value into the router). create() returns Self (not Arc<Self>). Service-method logic becomes inherent async fn on Osis<Domain> (contributor-editable in rpc.rs), and the generated trait method just extracts the macro Input fields and calls self.<method>(..). This removes the back-reference problem and is simpler than the old <Service>Trait/<Service>Handler indirection — but it means rust_rpc.rs must emit inherent-method stubs on Osis<Domain> instead of the <Service>Trait+<Service>Handler (a rust_rpc rewrite), and rust_osis.rs's 2b-core service bridge + struct (handler fields/init_handlers) gets revised accordingly.

  2. Server-crate module naming: openrpc_server! expands mod <domain> at the crate root, which clashes with today's src/<domain>/ dir module. Fix: emit the OSIS impl + contributor rpc.rs under src/osis_<domain>/ (crate::osis_<domain>), leaving crate::<domain> to the macro. main.rs: openrpc_server!(spec="../<name>/schemas", service_toml="service.toml", save_openrpc_dir="generated/openrpc") + mod osis_<domain>; + serve_domains(osis_<domain>::Osis<D>::create(&format!("{root}/<domain>"),0)?, …) in macro (sorted-subdir) order. (Per-domain #[cfg(feature)] gating drops — the macro's serve_domains has fixed arity.)

These touch rust_osis.rs (revise service model), rust_rpc.rs (inherent stubs), build/scaffold.rs (main.rs/Cargo/service.toml templates + tests), build/emit/rust_server.rs + mod-wiring (relocate to osis_<domain>/), then _find lowering and the CRM regen. The pieces are interdependent and only fully validate when the CRM workspace builds+boots (lab + server/admin/web/sdk), so this is the contiguous bring-up pass.

Shipped/verified to date: chunk 1 (0164e33), 2a (8ba3eff), 2b-core (760ed90) — generator emission migrated, lib green, 151 tests pass.

## Chunk 2 — two design refinements for the orchestration pass (record before bring-up) Planning the scaffold/orchestration surfaced two decisions that revise 2b-core's service model: 1. **`serve_domains` takes `impl <Stem>Api` by value** → drop the `Arc<Handler>` + `init_handlers` machinery. Implement `<Stem>Api` directly for `Osis<Domain>` (Send+Sync; passed by value into the router). `create()` returns `Self` (not `Arc<Self>`). **Service-method logic becomes inherent `async fn` on `Osis<Domain>`** (contributor-editable in `rpc.rs`), and the generated trait method just extracts the macro Input fields and calls `self.<method>(..)`. This removes the back-reference problem and is simpler than the old `<Service>Trait`/`<Service>Handler` indirection — but it means **`rust_rpc.rs` must emit inherent-method stubs on `Osis<Domain>` instead of the `<Service>Trait`+`<Service>Handler`** (a rust_rpc rewrite), and `rust_osis.rs`'s 2b-core service bridge + struct (handler fields/init_handlers) gets revised accordingly. 2. **Server-crate module naming**: `openrpc_server!` expands `mod <domain>` at the **crate root**, which clashes with today's `src/<domain>/` dir module. Fix: emit the OSIS impl + contributor `rpc.rs` under **`src/osis_<domain>/`** (`crate::osis_<domain>`), leaving `crate::<domain>` to the macro. `main.rs`: `openrpc_server!(spec="../<name>/schemas", service_toml="service.toml", save_openrpc_dir="generated/openrpc")` + `mod osis_<domain>;` + `serve_domains(osis_<domain>::Osis<D>::create(&format!("{root}/<domain>"),0)?, …)` in macro (sorted-subdir) order. (Per-domain `#[cfg(feature)]` gating drops — the macro's `serve_domains` has fixed arity.) These touch `rust_osis.rs` (revise service model), `rust_rpc.rs` (inherent stubs), `build/scaffold.rs` (main.rs/Cargo/service.toml templates + tests), `build/emit/rust_server.rs` + mod-wiring (relocate to `osis_<domain>/`), then `_find` lowering and the CRM regen. The pieces are interdependent and only fully validate when the CRM workspace builds+boots (`lab` + server/admin/web/sdk), so this is the contiguous bring-up pass. **Shipped/verified to date:** chunk 1 (`0164e33`), 2a (`8ba3eff`), 2b-core (`760ed90`) — generator emission migrated, lib green, 151 tests pass.
Author
Owner

State of chunk 2 — server-side generator migration DONE & committed

Shipped on feature/osis-multidomain-serve (all generator-lib + test verified):

  • 0164e33 chunk 1 — String-sid storage (39 db tests).
  • 8ba3eff 2a — wire types + OsisObject impl → Option<String>.
  • 760ed90 2b-core — rust_osis.rs emits impl <Stem>Api + impl OsisObject + Osis<Domain>; deleted handle_rpc / _rpc_* / OsisAppRpcHandler / _new (−1389 lines).
  • 1fad2b7 2b-scaffold — server main.rsopenrpc_server! + serve_domains; Cargo deps swapped; tests updated.

Generator lib compiles; 151 generator unit tests pass at each step.

Remaining frontier to reach CRM-green (honest scope)

The generator's server-side emission is migrated. To get examples/crm/run.sh green still needs:

Generator (server path, generator-lib verifiable):

  1. build/emit/rust_server.rs + mod-wiring: write the generated OSIS impl + contributor rpc.rs under src/osis_<domain>/ (matching the new main.rs), not src/<domain>/generated/.
  2. rust_rpc.rs: revise the contributor rpc.rs to emit inherent async fn service methods on Osis<Domain> (returning Result<T, RpcError>) + the lifecycle triggers — replacing the <Service>Trait/<Service>Handler indirection (serve_domains takes the impl by value; no Arc back-ref). Revise rust_osis.rs's 2b-core service bridge + struct to call self.<method>(..) and return Self from create() (drop handler fields/init_handlers).
  3. rust_osis.rs: _find query lowering (currently returns empty) — lower <Type>FindParams→indexer query off rust_type (String→StrFilter, named→EnumFilter, numeric→NumFilter, bool→BoolFilter).
  4. rust_struct.rs: drop the now-dead generate_osis_impl_block (core); ensure schemas reachable at the server crate (spec="../<name>/schemas").

Then the full workspace (validated only by the end-to-end run; lab is available on the dev box):
5. SDK emitter — the generated hero_<name>_sdk must speak the new wire (Option<String> sid, _set upsert, no _new/<Type>Input). It currently uses jsonrpsee #[rpc]; align to the spec (ideally openrpc_client!). The CRM driver imports SDK types (CompanyNewInput, etc.) — those change (#155).
6. admin/web crates consume the SDK/types — must compile against the new shape.
7. Regenerate CRM → build (server first, then admin/web/sdk/driver) → boot → driver green. Then chunk 3 (delete crates/osis HeroOsisServer/rpc2/hero_osis_server!) and chunk 5 (hero_osis 15 domains).

Items 1–4 are the next contiguous generator pass (verifiable via generator tests + a server-only build). Items 5–7 are the workspace bring-up. This is a multi-pass effort; the server-emission core (the hard semantic translation) is done.

## State of chunk 2 — server-side generator migration DONE & committed Shipped on `feature/osis-multidomain-serve` (all generator-lib + test verified): - `0164e33` chunk 1 — String-sid storage (39 db tests). - `8ba3eff` 2a — wire types + OsisObject impl → `Option<String>`. - `760ed90` 2b-core — `rust_osis.rs` emits `impl <Stem>Api` + `impl OsisObject` + `Osis<Domain>`; deleted handle_rpc / `_rpc_*` / OsisAppRpcHandler / `_new` (−1389 lines). - `1fad2b7` 2b-scaffold — server `main.rs` → `openrpc_server!` + `serve_domains`; Cargo deps swapped; tests updated. Generator lib compiles; **151 generator unit tests pass** at each step. ### Remaining frontier to reach CRM-green (honest scope) The generator's **server-side** emission is migrated. To get `examples/crm/run.sh` green still needs: **Generator (server path, generator-lib verifiable):** 1. `build/emit/rust_server.rs` + mod-wiring: write the generated OSIS impl + contributor `rpc.rs` under `src/osis_<domain>/` (matching the new `main.rs`), not `src/<domain>/generated/`. 2. `rust_rpc.rs`: revise the contributor `rpc.rs` to emit **inherent `async fn` service methods on `Osis<Domain>`** (returning `Result<T, RpcError>`) + the lifecycle triggers — replacing the `<Service>Trait`/`<Service>Handler` indirection (serve_domains takes the impl by value; no Arc back-ref). Revise `rust_osis.rs`'s 2b-core service bridge + struct to call `self.<method>(..)` and return `Self` from `create()` (drop handler fields/init_handlers). 3. `rust_osis.rs`: `_find` query lowering (currently returns empty) — lower `<Type>FindParams`→indexer query off `rust_type` (String→StrFilter, named→EnumFilter, numeric→NumFilter, bool→BoolFilter). 4. `rust_struct.rs`: drop the now-dead `generate_osis_impl_block` (core); ensure schemas reachable at the server crate (`spec="../<name>/schemas"`). **Then the full workspace (validated only by the end-to-end run; `lab` is available on the dev box):** 5. **SDK emitter** — the generated `hero_<name>_sdk` must speak the new wire (`Option<String>` sid, `_set` upsert, no `_new`/`<Type>Input`). It currently uses jsonrpsee `#[rpc]`; align to the spec (ideally `openrpc_client!`). The CRM **driver** imports SDK types (`CompanyNewInput`, etc.) — those change (#155). 6. **admin/web** crates consume the SDK/types — must compile against the new shape. 7. Regenerate CRM → build (server first, then admin/web/sdk/driver) → boot → driver green. Then chunk 3 (delete `crates/osis` HeroOsisServer/rpc2/hero_osis_server!) and chunk 5 (hero_osis 15 domains). Items 1–4 are the next contiguous generator pass (verifiable via generator tests + a server-only build). Items 5–7 are the workspace bring-up. This is a multi-pass effort; the server-emission core (the hard semantic translation) is done.
Author
Owner

Chunk 2b — generator-side migration COMPLETE; end-to-end blocked upstream

Pushed a6f50d7 (osis_/ layout + inherent service methods). The server-side generator now fully emits the new model; generator lib + 151 tests green.

Validated via a locally-rebuilt lab

lab is built from hero_skills/crates/lab (depends on blueprinterhero_rpc_generator). Added a temp [patch."…hero_blueprint.git"] → local working tree in hero_skills/Cargo.toml, rebuilt lab, scaffolded CRM fresh. Confirmed the scaffolder (crates/generator/src/build/scaffold.rs generate_server_main_rs — the live path; the blueprints/service/.../main.rs template file is overwritten by it) now emits:

  • main.rs = herolib_derive::openrpc_server!(spec="../hero_crm/schemas", service_toml, save_openrpc_dir) + #[path="osis_crm/mod.rs"] mod osis_crm; + serve_domains(osis_crm::OsisCrm::create(&format!("{root}/crm"),0)?).await.
  • server Cargo.toml deps swapped (+herolib_derive/+herolib_oschema_server/+async-trait; −hero_rpc2/−jsonrpsee).

End-to-end build blocker (UPSTREAM of OSIS — not this generator)

Building the scaffolded workspace (local [patch] so build.rs runs the local generator) fails before OSIS codegen, in dependency resolution/compile:

  1. hero_indexer_sdk won't compile against current herolib_derive: its openrpc_client!("../hero_indexer_server/openrpc.json") has db.create(name, schema) (2 params) — violates the new one-input rule. Pulled in via hero_rpc_osis's rpc feature (dep:hero_indexer_sdk). hero_indexer needs its own one-input migration (separate repo/issue).
  2. hero_theme 301s on git fetch (a hero_crm_admin dep) — blocks workspace resolution; worked around by temporarily trimming the workspace to core+server.

So full CRM bring-up is gated on (1)+(2), independent of the OSIS serving work. I did not hand-edit hero_indexer's generated spec (out of scope, fragile).

Remaining (OSIS scope, after the upstream unblock)

  • _find: still returns empty — lower <Type>FindParams→indexer query (off rust_type).
  • Inspect the generated osis_crm/{osis.rs,rpc.rs,mod.rs} once build.rs can run; fix any emission bugs.
  • SDK (openrpc_client!) / admin / web to the new wire; driver type updates (#155).
  • Then chunk 3 (delete crates/osis HeroOsisServer/rpc2/hero_osis_server!) + chunk 5 (hero_osis 15 domains).

Shipped (feature/osis-multidomain-serve)

0164e33 ch1 · 8ba3eff 2a · 760ed90 2b-core · 1fad2b7 2b-scaffold · a6f50d7 2b-layout. All generator-lib/test-verified. Temp [patch] left in hero_skills/Cargo.toml for the next verification loop (drop when done).

## Chunk 2b — generator-side migration COMPLETE; end-to-end blocked upstream Pushed `a6f50d7` (osis_<domain>/ layout + inherent service methods). The **server-side generator now fully emits the new model**; generator lib + **151 tests green**. ### Validated via a locally-rebuilt `lab` `lab` is built from `hero_skills/crates/lab` (depends on `blueprinter` → `hero_rpc_generator`). Added a temp `[patch."…hero_blueprint.git"]` → local working tree in `hero_skills/Cargo.toml`, rebuilt `lab`, scaffolded CRM fresh. Confirmed the scaffolder (`crates/generator/src/build/scaffold.rs` `generate_server_main_rs` — the live path; the `blueprints/service/.../main.rs` template file is overwritten by it) now emits: - `main.rs` = `herolib_derive::openrpc_server!(spec="../hero_crm/schemas", service_toml, save_openrpc_dir)` + `#[path="osis_crm/mod.rs"] mod osis_crm;` + `serve_domains(osis_crm::OsisCrm::create(&format!("{root}/crm"),0)?).await`. - server `Cargo.toml` deps swapped (+herolib_derive/+herolib_oschema_server/+async-trait; −hero_rpc2/−jsonrpsee). ### End-to-end build blocker (UPSTREAM of OSIS — not this generator) Building the scaffolded workspace (local `[patch]` so `build.rs` runs the local generator) fails before OSIS codegen, in dependency resolution/compile: 1. **`hero_indexer_sdk` won't compile against current `herolib_derive`**: its `openrpc_client!("../hero_indexer_server/openrpc.json")` has `db.create(name, schema)` (2 params) — violates the new one-input rule. Pulled in via `hero_rpc_osis`'s `rpc` feature (`dep:hero_indexer_sdk`). **hero_indexer needs its own one-input migration** (separate repo/issue). 2. **`hero_theme` 301s on git fetch** (a `hero_crm_admin` dep) — blocks workspace resolution; worked around by temporarily trimming the workspace to core+server. So full CRM bring-up is gated on (1)+(2), independent of the OSIS serving work. I did **not** hand-edit hero_indexer's generated spec (out of scope, fragile). ### Remaining (OSIS scope, after the upstream unblock) - `_find`: still returns empty — lower `<Type>FindParams`→indexer query (off `rust_type`). - Inspect the generated `osis_crm/{osis.rs,rpc.rs,mod.rs}` once build.rs can run; fix any emission bugs. - SDK (`openrpc_client!`) / admin / web to the new wire; driver type updates (#155). - Then chunk 3 (delete `crates/osis` HeroOsisServer/rpc2/hero_osis_server!) + chunk 5 (hero_osis 15 domains). ### Shipped (feature/osis-multidomain-serve) `0164e33` ch1 · `8ba3eff` 2a · `760ed90` 2b-core · `1fad2b7` 2b-scaffold · `a6f50d7` 2b-layout. All generator-lib/test-verified. Temp `[patch]` left in `hero_skills/Cargo.toml` for the next verification loop (drop when done).
Author
Owner

⚠️ development diverged — rebase needed (2026-06-03)

Pulled all repos. development advanced +29 commits since this branch forked (merge-base bb95389): a friend's "refactor/hero-lib-base" effort. It did the surrounding hero_lib migration — removed hero_rpc2/jsonrpsee, rewrote admin/web → thin hero_lib shells, rewrote tests → openrpc_server! + herolib_openrpc client, "workspace fully green on hero_lib" (phase 6) — but left the core OSIS server emission on the OLD model: development's rust_osis.rs still emits handle_rpc; scaffold main.rs still hero_osis_server!; OsisObject still &SmartId; crates/osis/src/rpc/bootstrap.rs still present.

Conclusion: this branch's 6 commits (the OSIS server-emission migration to openrpc_server!) are complementary, not redundant — they're the piece the hero-lib-base refactor didn't do. But they sit on the stale base and must be rebased onto the updated development. Expect conflicts in build/scaffold.rs (server main.rs — keep my openrpc_server!+serve_domains version) and Cargo dep templates (both sides removed hero_rpc2/jsonrpsee — reconcile). The friend's admin/web shell + test rewrites should be KEPT (they cover part of my "SDK/admin/web" remaining frontier).

Also: my earlier hero_theme-301 / hero_indexer one-input blockers were against the stale base — development is green on hero_lib, so re-test after rebase; they may already be resolved. (Friend migrated hero_indexer→hero_lib; its db.create is still 2-param though.)

PR #156 is based on the stale base — refresh it after the rebase.

## ⚠️ `development` diverged — rebase needed (2026-06-03) Pulled all repos. `development` advanced **+29 commits** since this branch forked (merge-base `bb95389`): a friend's **"refactor/hero-lib-base"** effort. It did the *surrounding* hero_lib migration — removed `hero_rpc2`/`jsonrpsee`, rewrote **admin/web → thin hero_lib shells**, rewrote **tests → `openrpc_server!` + herolib_openrpc client**, "workspace fully green on hero_lib" (phase 6) — **but left the core OSIS server emission on the OLD model**: development's `rust_osis.rs` still emits `handle_rpc`; scaffold `main.rs` still `hero_osis_server!`; `OsisObject` still `&SmartId`; `crates/osis/src/rpc/bootstrap.rs` still present. **Conclusion:** this branch's 6 commits (the OSIS **server-emission** migration to `openrpc_server!`) are **complementary, not redundant** — they're the piece the hero-lib-base refactor didn't do. But they sit on the stale base and **must be rebased onto the updated `development`**. Expect conflicts in `build/scaffold.rs` (server `main.rs` — keep my `openrpc_server!`+`serve_domains` version) and Cargo dep templates (both sides removed hero_rpc2/jsonrpsee — reconcile). The friend's admin/web shell + test rewrites should be KEPT (they cover part of my "SDK/admin/web" remaining frontier). Also: my earlier `hero_theme`-301 / `hero_indexer` one-input blockers were against the stale base — `development` is green on hero_lib, so **re-test after rebase**; they may already be resolved. (Friend migrated hero_indexer→hero_lib; its `db.create` is still 2-param though.) PR #156 is based on the stale base — refresh it after the rebase.
Author
Owner

development HEAD was a BROKEN MERGE — rebased onto coherent base, workspace now builds

Picked this back up. The "rebase onto development" plan from the last comment was wrong in its premise: development HEAD 35d79a6 ("merge refactor/hero-lib-base … keep development additions") does not compile — for 4 independent reasons, all pre-existing (no OSIS commits involved):

  1. root Cargo.toml lists deleted crates/openrpc + crates/hero_rpc2 as members → workspace manifest won't load;
  2. workspace.package.rust-version dropped → crates can't inherit it;
  3. crates/osis + crates/derive inherit the deleted hero_rpc2 dep;
  4. scaffold.rs calls ui_emit::{admin,web}_*_html fns the refactor removed → 11 generator errors.

Root cause: the merge resolved "keep development additions" by resurrecting the pre-refactor manifests/source while keeping the refactor's directory deletions/renames — the two halves are inconsistent. The other merge parent, the refactor tip a87cac4, is internally coherent (osis on herolib_derive; derive/hero_rpc2/openrpc removed; rust-version present; builds clean). Everything 35d79a6 adds over a87cac4 is either broken-resurrection or redundant (crates/derive's openrpc_client!/openrpc_proxy! are also in herolib_derive).

Done

Rebased the OSIS chunks onto a87cac4 (dropping the now-pointless serve_domains stopgap — its bootstrap.rs was already deleted by the refactor). Resolved the conflicts keeping approach A (issue decision 38153/38174: one type model, String sid, OSIS owns a local String-sid OsisObject trait — not herolib's &SmartId one). Result: the entire hero_blueprint workspace now builds (cargo build --workspace green) and all tests pass — generator 148 unit tests, osis 44 db tests + doctests (default + index feature). Branch development (local) = a87cac4 + 5 OSIS commits.

Key finding for the remaining work

a87cac4 already contains an approach-B OSIS server emitter (generate/server_main.rs + scaffold emit_server_sources): spec-pure wire types + separate *Store types carrying sid: SmartId, #[derive(OsisObject)]. Our approach-A chunk-1 String-sid fork breaks approach B (*Store derives the canonical SmartId OsisObject, but DBTyped<T> now requires the local String one). So the live scaffold's server-source emission must switch B→Agenerate_server_crate still calls emit_server_sources. That switch + the end-to-end lab validation (scaffold CRM → cargo build the generated crate; approach-A osis.rs compilability vs the macro wire types was never validated — it was blocked upstream before) is the next focused pass. Then _find lowering + chunk 5 (hero_osis 15 domains).

Not pushed yet — remote development is the broken merge; superseding it needs a force-push.

## development HEAD was a BROKEN MERGE — rebased onto coherent base, workspace now builds Picked this back up. The "rebase onto development" plan from the last comment was wrong in its premise: **development HEAD `35d79a6` ("merge refactor/hero-lib-base … keep development additions") does not compile** — for 4 independent reasons, all pre-existing (no OSIS commits involved): 1. root `Cargo.toml` lists deleted `crates/openrpc` + `crates/hero_rpc2` as members → workspace manifest won't load; 2. `workspace.package.rust-version` dropped → crates can't inherit it; 3. `crates/osis` + `crates/derive` inherit the deleted `hero_rpc2` dep; 4. `scaffold.rs` calls `ui_emit::{admin,web}_*_html` fns the refactor removed → 11 generator errors. Root cause: the merge resolved "keep development additions" by resurrecting the pre-refactor manifests/source while keeping the refactor's directory deletions/renames — the two halves are inconsistent. The other merge parent, the **refactor tip `a87cac4`, is internally coherent** (osis on `herolib_derive`; `derive`/`hero_rpc2`/`openrpc` removed; `rust-version` present; builds clean). Everything `35d79a6` adds over `a87cac4` is either broken-resurrection or redundant (`crates/derive`'s `openrpc_client!`/`openrpc_proxy!` are also in `herolib_derive`). ### Done Rebased the OSIS chunks onto `a87cac4` (dropping the now-pointless `serve_domains` stopgap — its `bootstrap.rs` was already deleted by the refactor). Resolved the conflicts keeping **approach A** (issue decision 38153/38174: one type model, String `sid`, OSIS owns a local String-sid `OsisObject` trait — *not* herolib's `&SmartId` one). Result: **the entire hero_blueprint workspace now builds** (`cargo build --workspace` green) and all tests pass — **generator 148 unit tests, osis 44 db tests + doctests** (default + `index` feature). Branch `development` (local) = `a87cac4` + 5 OSIS commits. ### Key finding for the remaining work `a87cac4` already contains an **approach-B** OSIS server emitter (`generate/server_main.rs` + scaffold `emit_server_sources`): spec-pure wire types + separate `*Store` types carrying `sid: SmartId`, `#[derive(OsisObject)]`. Our **approach-A** chunk-1 String-sid fork **breaks approach B** (`*Store` derives the canonical SmartId `OsisObject`, but `DBTyped<T>` now requires the local String one). So the live scaffold's server-source emission **must switch B→A** — `generate_server_crate` still calls `emit_server_sources`. That switch + the end-to-end `lab` validation (scaffold CRM → `cargo build` the generated crate; approach-A `osis.rs` compilability vs the macro wire types was never validated — it was blocked upstream before) is the next focused pass. Then `_find` lowering + chunk 5 (hero_osis 15 domains). Not pushed yet — remote `development` is the broken merge; superseding it needs a force-push.
Author
Owner

Server emitter synced to current macro + compile-validated (pushed)

Followed up comment 38409. Re-synced the OSIS server emitter (generate/server_main.rs) to the current published openrpc_server! contract (pinned via cargo expand) and pushed to development:

  • 3c96ae7 — drop <type>_new; _set upsert via {data: <Type>} returning the minted sid; _get/_list_full Option-wrap sid/created_at/updated_at; service methods use the bare OpenRPC method name (ping) + <Pascal>Input/Output (no <service> prefix); real epoch-seconds now().
  • 39cb90e — fixed the fixture's /Volumes/T7 machine-path patch so the compile test is portable.

server_emit::emitted_main_compiles now PASSES — the generated server crate compiles against the real macro for the first time. Generator 149 + osis 44 tests green; cargo build --workspace green; reference fixture regenerated (multi-file) and a #[ignore]d regen_reference_fixture_main keeps it refreshable. The hero_indexer one-input blocker is resolved (hero_rpc_osis --features index compiles).

Chunk 5 (hero_osis 15+ domains) — scoped, deliberately NOT rushed

hero_osis_server has 18 feature-gated domains, each carrying hand-written business logic in src/<domain>/server/rpc.rs, on the OLD build.rs-emission path + the deleted hero_osis_server! macro + deleted hero_rpc2. a87cac4 built approach B only for the scaffold path; the build.rs path (generate_server_dir) still emits the old dispatcher model. Migrating hero_osis is therefore a careful per-domain effort (regenerate CRUD on approach B + port each domain's hand-written service logic into the new stubs), not a rebuild — re-scaffolding via lab would clobber the hand-written code. Tracked as the next focused effort; not pushed half-migrated.

## Server emitter synced to current macro + compile-validated (pushed) Followed up comment 38409. Re-synced the OSIS server emitter (`generate/server_main.rs`) to the current published `openrpc_server!` contract (pinned via `cargo expand`) and pushed to `development`: - `3c96ae7` — drop `<type>_new`; `_set` upsert via `{data: <Type>}` returning the minted sid; `_get`/`_list_full` Option-wrap `sid/created_at/updated_at`; service methods use the **bare** OpenRPC method name (`ping`) + `<Pascal>Input/Output` (no `<service>` prefix); real epoch-seconds `now()`. - `39cb90e` — fixed the fixture's `/Volumes/T7` machine-path patch so the compile test is portable. **`server_emit::emitted_main_compiles` now PASSES** — the generated server crate compiles against the real macro for the first time. Generator 149 + osis 44 tests green; `cargo build --workspace` green; reference fixture regenerated (multi-file) and a `#[ignore]`d `regen_reference_fixture_main` keeps it refreshable. The hero_indexer one-input blocker is resolved (`hero_rpc_osis --features index` compiles). ### Chunk 5 (hero_osis 15+ domains) — scoped, deliberately NOT rushed hero_osis_server has 18 feature-gated domains, each carrying **hand-written business logic** in `src/<domain>/server/rpc.rs`, on the OLD **build.rs-emission** path + the deleted `hero_osis_server!` macro + deleted `hero_rpc2`. a87cac4 built approach B only for the *scaffold* path; the *build.rs* path (`generate_server_dir`) still emits the old dispatcher model. Migrating hero_osis is therefore a careful per-domain effort (regenerate CRUD on approach B + port each domain's hand-written service logic into the new stubs), not a rebuild — re-scaffolding via lab would clobber the hand-written code. Tracked as the next focused effort; not pushed half-migrated.
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_blueprint#154
No description provided.