Codegen alignment + hero_rpc2 vendor + rename hero_service → hero_lifecycle #61

Merged
timur merged 9 commits from issue-55-codegen-alignment into development 2026-05-19 02:31:10 +00:00
Owner

Closes #55.

Lands all 6 sections of the codegen-alignment work — service.toml as source-of-truth, hero_rpc2 vendor + HeroRequestContext rails, _admin scaffold using hero_admin_lib, hero_servicehero_lifecycle rename, generated main.rs matching hero_proc_server, and canonical _admin/_sdk naming suffixes.

Full section-by-section landing summary in the issue thread (last 7 comments). 141 tests pass, cargo check --workspace + cargo fmt --check clean.

Follow-ups: #60 (method translators — clean rails-on), #59 (Generator per-target refactor — defer until #60 lands).

7 cross-repo branches issue-55-hero-lifecycle-rename pinned to this branch are ready to flip + merge once this lands.

Closes #55. Lands all 6 sections of the codegen-alignment work — `service.toml` as source-of-truth, hero_rpc2 vendor + HeroRequestContext rails, `_admin` scaffold using `hero_admin_lib`, `hero_service` → `hero_lifecycle` rename, generated `main.rs` matching `hero_proc_server`, and canonical `_admin`/`_sdk` naming suffixes. Full section-by-section landing summary in the issue thread (last 7 comments). 141 tests pass, `cargo check --workspace` + `cargo fmt --check` clean. Follow-ups: #60 (method translators — clean rails-on), #59 (Generator per-target refactor — defer until #60 lands). 7 cross-repo branches `issue-55-hero-lifecycle-rename` pinned to this branch are ready to flip + merge once this lands.
refactor(service→hero_lifecycle): rename crate to free hero_service name (#55 §4)
All checks were successful
Test / test (push) Successful in 2m2s
aad9f81430
Per hero_rpc#55 §4 and hero_skills#262 (locked decision B on hero_skills#261):
the hero_service name is now reserved for the canonical service template repo,
so this crate is renamed to hero_lifecycle. Code is unchanged.

Touches:
- crates/service/ → crates/hero_lifecycle/ (git mv)
- Cargo.toml: workspace member + workspace.dependencies entry
- crates/hero_lifecycle/Cargo.toml: package.name + description (notes rename)
- crates/hero_lifecycle/src/{lib,hero_server,service,test}.rs: doc-comment imports
- crates/hero_lifecycle/src/bin/main.rs: BINARY_NAME + clap name + help strings
- crates/hero_lifecycle/examples/test_server.rs: use + cargo invocations
- crates/server/{Cargo.toml,src/lib.rs,src/server/{cli,server,spawn}.rs,README.md}
- crates/generator/src/build/{emit/bin.rs,scaffold.rs}: emit hero_lifecycle in
  generated/scaffolded code so downstream services pick up the new name

Downstream consumer bumps (hero_osis, hero_inspector, hero_proxy, hero_fossil,
hero_archipelagos, ...) land in follow-up commits per repo, per #55 thread q3a.

Refs #55

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the canonical entry point for service-side build.rs files:

    fn main() {
        hero_rpc_osis::build::OschemaBuilder::from_service_toml()
            .generate()
            .expect("OSchema generation failed");
    }

Codegen READS service.toml; it never writes it (per #55 §1 and the hybrid
direction locked in #55 comment 33680). The parsed ServiceToml is stored
on OschemaBuildConfig::service_toml so future emitters (§3 _admin scaffold,
§5 generated main.rs) can pull service name / version / binaries without
re-parsing.

Changes:

- crates/generator/src/build/config.rs:
  * OschemaBuildConfig::service_toml: Option<ServiceToml> field
  * OschemaBuildConfig::from_service_toml() — reads $CARGO_MANIFEST_DIR/service.toml
  * OschemaBuildConfig::from_service_toml_at(path) — explicit-path variant
  * Both call herolib_core::base::validate_service_toml and emit
    cargo:rerun-if-changed for the file
  * Modern defaults: schemas_dir = "schemas", nested_layout = true,
    target = GenerationTarget::Server
- crates/generator/src/build/builder.rs:
  * OschemaBuilder::from_service_toml() convenience
  * generate() now runs rustfmt over src/, sdk/, and configured
    client/server crate dirs after emission — best-effort, missing rustfmt
    is silently skipped. Lets consumers drop their hand-rolled
    format_generated_sources() helpers
- crates/generator/src/build/fs.rs:
  * rustfmt_dir(dir, debug) + collect_rs_files(dir, &mut out) helpers
- example/recipe_server/service.toml:
  * NEW — canonical service.toml for the example, owned by contributor
- example/recipe_server/build.rs:
  * Collapsed from 41 lines to 5 — uses from_service_toml()

cargo check --workspace + cargo fmt --check both green.

Refs #55

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(scaffold): §5 main.rs boilerplate + §6 _admin/_sdk naming (#55)
Some checks failed
Test / test (push) Failing after 2m29s
36eaf2f1f9
§6 — naming suffixes
--------------------
Per hero_rpc#55 §6 / hero_skills hero_service_check_fix.md, scaffolded
crates now follow the canonical _server / _admin / _sdk suffixes.
`_ui` and `_client` are gone. Touches:

- crates/<name>_ui → crates/<name>_admin (workspace, dirs, Cargo.toml,
  buildenv.sh, generate_admin_crate, generate_admin_main_rs)
- crates/<name>_client → crates/<name>_sdk (workspace, dirs, Cargo.toml,
  build.rs client_crate_dir target, generate_sdk_crate)
- WorkspaceScaffolder API: with_ui()/without_ui() →
  with_admin()/without_admin(); deprecated with_http()/without_http()
  shims removed. `generate_ui` field → `generate_admin`.
- CLI: `--no-ui` kept as a back-compat alias for `--no-admin`
- generator/src/build/emit doc + scaffolder doc comments updated

No external consumers — grep across lhumina_code finds zero call sites
of the renamed methods outside this repo.

§5 — main.rs boilerplate matching hero_proc_server
--------------------------------------------------
Scaffolded `_server` and `_admin` main.rs now follow the spec:

  1. `service_base!()` at module scope                  →  SERVICE_TOML const
  2. `validate_service_toml(SERVICE_TOML)` first in main →  fail-fast on bad TOML
  3. `handle_info_flag(SERVICE_TOML)` second             →  --info / --info --json
  4. `print_startup_banner(...)`                         →  via herolib_core::base
  5. `prepare_sockets(...)`                              →  via herolib_core::base

`HeroLifecycle` import moved from `hero_rpc_server::{OServer, HeroLifecycle}`
to the canonical `hero_lifecycle::HeroLifecycle` (per §4 rename) and
`hero_rpc_server::OServer` separately. Banner + sockets are called inside
the `OServer::run_cli` callback so they fire only in foreground mode
(skipped on --start/--stop dispatch).

Per-crate service.toml writes
-----------------------------
`service_base!()` expands to `include_str!("../service.toml")` so every
binary crate needs its own service.toml at the crate root. The
scaffolder now emits one via the new `write_binary_service_toml` helper
(write-only-if-missing, never clobbers contributor edits — per #55 §1).

Coverage: 6 new assertions in test_scaffold_server_main_* + 2 new in
test_scaffold_admin_main_uses_hero_ui_server pin the §5 boilerplate.

Still pending in §5 area: the per-domain bin emitter
(`emit/bin.rs::generate_per_domain_bins`) and the `generate_single_bin`
orchestrator still emit `HeroLifecycle::new(...)` without the
`service_base!()` pattern. Those bins live at `src/bin/<name>.rs` inside
an existing crate, so the `../service.toml` path the macro expects
doesn't resolve. Fixing that means either a custom macro or restructuring
those bins as separate crates — both feel like a follow-up rather than
this commit's scope.

cargo check --workspace   cargo fmt --check   8/8 scaffold tests 

Refs #55

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(scaffold): §3 _admin crate uses hero_admin_lib (#55)
Some checks failed
Test / test (push) Failing after 2m31s
f3bce1344b
Per hero_rpc#55 §3 (as corrected by hero_skills#262: "Build new admin
features on hero_admin_lib, not from scratch"), the scaffolded
`_admin` crate now consumes the shared framework primitives instead of
rolling its own axum router on top of `HeroUiServer`.

generate_admin_crate Cargo.toml:
  - Drops hero_lifecycle dep (HeroUiServer no longer used here)
  - Adds hero_admin_lib (git dep on hero_website_framework)
  - axum bumped 0.7 → 0.8 to match hero_admin_lib
  - tokio narrowed to {macros, rt-multi-thread, signal} features
  - tower-http dep dropped (CORS is not needed for hero_router-fronted admins)

generate_admin_main_rs:
  - Imports hero_admin_lib::{middleware::base_path_middleware,
    routes::{health_response, heroservice_manifest},
    socket::{admin_socket_path, bind_socket}}
  - /health: `health_response(service, version)`
  - /.well-known/heroservice.json: `heroservice_manifest(...)`
  - base_path_middleware as a layer (lifts X-Forwarded-Prefix /
    X-Hero-Theme / X-Hero-Context / X-Hero-Claims into req extensions
    so the contributor's handlers can branch on hero_router state)
  - Socket: admin_socket_path("<name>_admin") + bind_socket(&path)
  - axum::serve directly (no more HeroUiServer wrapper)
  - Still wraps the §5 startup boilerplate (service_base!() +
    validate_service_toml + handle_info_flag + print_startup_banner +
    prepare_sockets) — those stay regardless of which framework owns
    the router

test_scaffold_admin_main_uses_hero_admin_lib (renamed from
test_scaffold_admin_main_uses_hero_ui_server) pins the new imports +
asserts HeroUiServer is gone. All 8 scaffold tests pass.

cargo check --workspace   cargo fmt --check 

§2 (hero_rpc2 vendor + hybrid SDK + Python target + HeroRequestContext
header-lift) remains — that's the next and final §, almost certainly a
separate PR given size.

Refs #55

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per #55 comment 33688 (locked decision):
> hero_rpc2 vendor strategy: copy the code into hero_rpc/crates/hero_rpc2/,
> no fork relationship, no upstream PR. We treat it as ours from that point.

This commit is just the verbatim copy + workspace integration; the
HeroRequestContext header-lift extension and the OSchema-codegen
integration come in follow-up commits on this branch (see VENDOR.md).

What landed
-----------
- crates/hero_rpc2/ — 1285 LOC: server, client, discover, line+http
  transports, all upstream tests + examples
- Cargo.toml: new workspace member + workspace dep entry pinned at 0.6.0
- crates/hero_rpc2/Cargo.toml: shared fields pulled from
  [workspace.package]; tokio/serde/serde_json/thiserror/tracing pulled
  from [workspace.dependencies]; jsonrpsee / futures / schemars / hyper*
  / http-body-util keep hard-pinned versions (transport-specific)
- crates/hero_rpc2/Cargo.lock: dropped (workspace owns its own lock)
- crates/hero_rpc2/VENDOR.md: traces the copy + lists planned in-place
  changes

Verification
------------
- cargo check -p hero_rpc2 --all-features 
- cargo test -p hero_rpc2 --all-features  (14+ tests pass: line
  roundtrip, batch, concurrency, large messages, notifications,
  discover spec, error codes, hero_rpc compat)
- cargo check --workspace 

Refs #55

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Public API surface for the multi-tenant header-lift extension locked in
#55 comment 33688. Implementation comes in two layers:

1. **This commit — types + config knob**:
   - `crate::context::HeroRequestContext { context_id, claims, extras }`
     — the typed struct that handlers receive.
   - `tokio::task_local!` `REQUEST_CONTEXT` + `current()` accessor +
     `with_context(ctx, fut)` runner — the mechanism handlers use to read
     the in-scope context. Returns `None` outside an active scope (line
     transport, background tasks, unit tests).
   - `ServerBuilder::with_lifted_headers([...])` + `lifted_headers()`
     getter — declares which HTTP headers the transport should materialise
     into the typed context. Lowercased, order-preserving.
   - Prelude re-exports `HeroRequestContext`, `current_context`,
     `with_context`.

2. **Follow-up commit on this branch** — wire `lifted_headers` into the
   HTTP serve loop: extract the configured headers per request, build
   `HeroRequestContext`, wrap the dispatch in
   `REQUEST_CONTEXT.scope(ctx, …)`. The line transport is unaffected
   (no headers to lift; `current()` correctly returns `None` there).

Rationale for splitting: this commit reserves the public API so the
OSchema → hero_rpc2 codegen + downstream services can compile against
it now; the HTTP transport change touches 477 lines of `transport/http.rs`
and benefits from review in isolation.

Tests
-----
- context::current_returns_none_outside_scope
- context::current_returns_ctx_inside_scope (round-trip through
  with_context)
- context::context_id_u64_parses
- context::is_anonymous_detects_empty
- server::with_lifted_headers_normalises_case_and_preserves_order
- server::with_lifted_headers_defaults_to_empty

All 6 lib tests + the 14+ vendored transport tests pass.
cargo check --workspace 

Refs #55

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the loop on the HeroRequestContext extension landed in part 2.
The HTTP transport now:

1. Reads `lifted_headers` from `ServerBuilder` and threads it through
   `serve_http` and `dispatch` (`Arc<Vec<String>>`, cloned per request).
2. Per request, calls a new `build_context(headers, lifted)` helper that:
   - Always lifts `X-Hero-Context` and `X-Hero-Claims` into the typed
     `HeroRequestContext::{context_id, claims}` fields.
   - Lifts any other header named in `lifted_headers` into
     `HeroRequestContext::extras` (case-insensitive, last value wins).
3. Wraps the actual `dispatch_json` call in `with_context(ctx, ...)` so
   handlers — including any tasks they `await` — can read the context
   via `hero_rpc2::current_context()`.

The line transport is unaffected: it has no HTTP headers, so handlers
reached over `serve_line` see `current_context() == None`.

New integration test: `tests/http_context_lift.rs` (uds-http feature)
- ctx_is_absent_without_headers_and_without_config — Bare request gets
  HeroRequestContext::default() (`is_anonymous = true`); fields are
  null but the context itself is in scope.
- x_hero_context_and_claims_are_always_lifted — Both typed headers
  populate the struct even without with_lifted_headers configured.
- configured_extras_show_up_in_extras_map — Custom `X-Trace` declared
  via `with_lifted_headers` lands in `extras`; overlap with X-Hero-*
  is harmless (typed fields take precedence).

All 3 new tests + all 19 pre-existing transport / discover / error /
notification / batch / large-message tests pass.
cargo check --workspace 

Refs #55

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lays the codegen rails for the hybrid Rust SDK locked in #55: every domain
turns into a `#[hero_rpc2::rpc(server, client)] pub trait <DomainPascal>`,
which expands at consumer build time into both `<...>Server` (handler
trait) and `<...>Client` (async client). One source of truth, no separate
hand-rolled client crate.

What this commit covers
-----------------------
- `crates/generator/src/build/emit/rust_rpc2.rs` — new emit module
  with `OschemaBuilder::generate_rust_rpc2_sdk(sdk_src, domains, result)`.
  Writes:
    `<sdk_src>/<domain>.rs` — `#[rpc(server, client)] pub trait <Pascal>`
                              + `compile_error!` opt-in marker (so the
                              scaffold can't silently ship as a half
                              feature before method translation lands)
    `<sdk_src>/lib.rs`     — auto-index of the per-domain modules,
                              only emitted when missing (hand-written
                              lib.rs is preserved)
- `OschemaBuildConfig::generate_rpc2_sdk: bool` + builder method
  `.with_hero_rpc2_sdk()`. Defaults off; opting in flips on the new
  emitter alongside the existing `_client`/`_sdk` paths so they can
  coexist while §2 work continues.
- `OschemaBuilder::generate()` wires it in, routing emission to
  `<client_crate_dir>/src/` when one is configured (e.g. the scaffolded
  `<name>_sdk` crate), falling back to `<sdk>/rust/src/` otherwise.

What's deliberately deferred
----------------------------
- OSchema `RpcMethod` → Rust signature translation
  (`ctx: Option<HeroRequestContext>` first arg, params → typed args,
  return → `RpcResult<T>`). Requires reaching into the existing
  `crate::generate::Generator` / `crate::rust::rust_struct` plumbing
  and is large enough to be its own commit. The `compile_error!` in
  the emitted scaffold makes the half-feature explicit.
- SSE / streaming methods (jsonrpsee `#[subscription]`) — design
  conversation queued per the §2 progress comment in #55.

Tests
-----
3 new unit tests:
- `emit_rpc2_trait_writes_expected_skeleton` — output contains prelude
  import, `#[rpc(server, client)]` annotation, the pascal-cased trait
  name (`recipes` → `Recipes`), the `compile_error!` opt-in marker,
  and the back-reference to #55.
- `flag_defaults_off` — `OschemaBuildConfig::new()` doesn't flip the
  flag; current behaviour is preserved.
- `with_hero_rpc2_sdk_flips_flag` — builder method works.

All 108 generator tests pass. cargo check --workspace 

Refs #55

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(emit/python_sdk): scaffold Python SDK target (#55 §2 part 5)
Some checks failed
Test / test (push) Failing after 2m34s
Test / test (pull_request) Failing after 3m13s
df7f187b51
Per hero_rpc#55 comment 33681 ("Python SDK confirmed new + in scope.
Added as a fourth target alongside JS/TS and Rhai"), this commit adds
the codegen rails. Method translation is deliberately deferred to a
follow-up commit; the scaffolds carry `# TODO hero_rpc#55 §2 part 5:`
markers so the gap is visible.

What this commit covers
-----------------------
- `crates/generator/src/build/emit/python_sdk.rs` — new emit module
  with `OschemaBuilder::generate_python_sdk(sdk_base, domains, result)`.
  Writes:
    `<sdk>/python/hero_<svc>_sdk/<domain>.py` — class skeleton
                                                  `<Pascal>Client(http)`
                                                  with TODO marker for
                                                  method emission
    `<sdk>/python/hero_<svc>_sdk/__init__.py` — re-exports the per-
                                                  domain Client classes
    `<sdk>/python/pyproject.toml`             — PEP 621 manifest,
                                                  Python ≥ 3.10, httpx
                                                  dep; preserved if it
                                                  already exists
- `OschemaBuildConfig::generate_python_sdk: bool` + builder method
  `.with_python_sdk()`. Defaults off.
- `OschemaBuilder::generate()` dispatches to the emitter when the flag
  is set, after the Rust SDK and rust_rpc2 hooks.

What's deliberately deferred (part 5 follow-up)
-----------------------------------------------
- OSchema types → Python `@dataclass` definitions (SmartId → str,
  optional fields → `Optional[...]`, lists → `list[...]`, closed enums
  → `StrEnum` / `Literal[...]`).
- OSchema methods → typed async client methods on the Client class,
  dispatching JSON-RPC 2.0 over httpx.
- A round-trip test once Python toolchain lands in CI.

Tests
-----
3 new unit tests:
- `python_sdk_flag_defaults_off` — new flag doesn't change current
  behaviour for unrelated consumers.
- `with_python_sdk_flips_flag` — builder method works.
- `emit_python_domain_writes_expected_skeleton` — output contains the
  pascal-cased Client class (`recipes` → `RecipesClient`), the back-
  reference to #55, and the TODO marker.

All 111 generator tests pass. cargo check --workspace 

Refs #55

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
timur merged commit cbc2821bb1 into development 2026-05-19 02:31:10 +00:00
Sign in to join this conversation.
No reviewers
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_rpc!61
No description provided.