Expand description
The single API layer (D60/D61).
This crate is the dated API contract tree. It depends only on the
phoxal-bus ABI floor (the contract primitive traits and the
typed-topic builders) and the phoxal-macros proc-macros; it
does not depend on the phoxal engine. A runtime depends on both crates
and imports the API tree directly with use phoxal_api::y2026_1 as api;, so
phoxal_api::y2026_1, phoxal_api::ApiVersion, and phoxal_api::ContractBody
are the canonical paths.
§Dated API versions
An API version is a dated module (phoxal_api::y2026_1, …) generated by
phoxal_api_tree!. Each version module carries:
- a zero-variant marker
enum Api {}implementingApiVersion, whoseApiVersion::IDis the dated module name ("y2026_1") - the canonical version identity; - the version-local wire bodies, one
pub modper contract node holding plain serde structs/enums and theirContractBodyimpls; - an api-local
topicbuilder rooted attopic::new().
Versions can build on earlier ones with version y2026_2 extends y2026_1:
every inherited node and type is re-emitted fresh under the new module - same
ContractBody::FAMILY and ContractBody::TOPIC, a different Api - so an
unchanged contract stays wire-identical while the compile-time Api bound stays
distinct. See phoxal_api_tree! for the inheritance and shape-identity rules.
§One API version per runtime, one per graph
A runtime authors against exactly one version module
(use phoxal_api::y2026_1 as api;) and declares it on the derive
(#[phoxal(api = y2026_1)]). Every handle body is bound
ContractBody<Api = R::Api>, so passing a body from another API version is a
compile error (D59/D60). At deploy time a single graph runs a single API
version: all of its runtimes share one Api, so contracts on the bus are
mutually consistent by construction (D59).
§Plain serde wire bodies, version in metadata
A wire body is just its serde encoding - there is no {"v":…} envelope or any
other version tag inside the payload (D62). The three pieces of routing
identity travel as bus metadata alongside the encoded body, not in it:
- the version (
ApiVersion::ID, e.g."y2026_1"), - the family (
ContractBody::FAMILY, e.g."drive::State"), - the codec that produced the bytes.
Keeping these out of the payload means the body bytes for an unchanged contract are identical across versions and codecs; the envelope/metadata layer owns version and family.
§Family and topic
Both identifiers are derived from the contract node’s path in the tree, never written by hand:
ContractBody::FAMILYis the::-joined node path plus the body type name, e.g.component::motor::Command. It excludes dynamic variables, so every instance of a per-instance node shares one family.ContractBody::TOPICis the versionless/-joined key with each dynamic node contributing a{var}placeholder, e.g.component/{instance}/motor/{capability}/command. A fully static path has a literal key (drive/state).
§The api-local topic builder
Each version module exposes a topic builder that mirrors the node tree:
api::topic::new() returns a root, one method per top-level node walks down the
tree, a dynamic node’s method takes its variable as impl Display, and a leaf
method binds the topic’s side-branded kind to its version-local body. For
example api::topic::new().drive().state() yields a
Topic<Subscribe<drive::State>> (the CLIENT observes the owner’s state) over
the static key drive/state, and
api::topic::new().component("base").motor("left").command() fills the dynamic
segments to produce component/base/motor/left/command. Because the builder is
generated from the same tree as FAMILY/TOPIC, the built key and the
documented key stay in lockstep.
§Owner side: topic::internal
The PUBLIC topic::new()... chain above is the client side. The matching
owner side lives at api::topic::internal::new(cap)... (L1 + L2, plan #00):
the same node tree and keys, but the leaf brands flip so the owner gets the side
it must take - api::topic::internal::new(cap).drive().state() is
Topic<Publish<drive::State>> (the owner publishes its telemetry), and
api::topic::internal::new(cap).drive().target() is Topic<Subscribe<drive::Target>>
(the owner reads its command input). A query owner reaches its ServeQuery
brand the same way. The internal chain is the deliberate, greppable owner
opt-in; a runtime acquires the topics of its OWN node through it and everything
it consumes through the public chain.
The internal::new entry requires the runner-minted owner capability
(OwnerCap, Layer 2): a runtime obtains it from
phoxal::SetupContext::owner_capability() and passes it in. On the documented
surface, owning a topic therefore cannot happen by accident - only with a
capability the runner mints.
Modules§
- y2026_1
- Dated API version
#id— version-local wire bodies + topics.
Traits§
- ApiVersion
- The contract primitive traits, re-exported from the
phoxal-buscrate (the ABI floor) so they stay addressable atphoxal_api::ApiVersion/phoxal_api::ContractBody. - Contract
Body - The contract primitive traits, re-exported from the
phoxal-buscrate (the ABI floor) so they stay addressable atphoxal_api::ApiVersion/phoxal_api::ContractBody.