Skip to content

refactor(framework)!: one phoxal library with consumer profiles, a Rust-declared api tree, and phoxal::session - #466

Merged
jBernavaPrah merged 25 commits into
mainfrom
refactor/single-crate
Aug 19, 2026
Merged

refactor(framework)!: one phoxal library with consumer profiles, a Rust-declared api tree, and phoxal::session#466
jBernavaPrah merged 25 commits into
mainfrom
refactor/single-crate

Conversation

@jBernavaPrah

Copy link
Copy Markdown
Contributor

What

The framework's six library packages (phoxal-protocol, phoxal-bus, phoxal-bundle, phoxal-model, phoxal-manifest, phoxal-runtime-contract) become real modules of the one phoxal crate.
phoxal is now the only external Rust dependency, the framework version owner and the sole compatibility carrier; phoxal-macros stays as the proc-macro package.

  • One crate, consumer profiles. Features participant (default), session, simulator, supervisor, authoring, test-harness decide which supported surface is public; cfg(feature) lives only in lib.rs and Cargo.toml (policy-checked). Every module compiles in every profile; the compat aggregate is profile-independent.
  • API layout by family. phoxal::api (robot, unchanged for participants), phoxal::runtime::api, phoxal::supervisor::api.
  • protocol_tree! deleted. The api tree is declared in Rust: private nodes!/endpoints! declarations beside the payloads; the payload type is the endpoint; one path tree with side selection at the leaf (api::topics().drive().state().client()); compatibility records derive from the same declarations; one owned Topic; sealed Endpoint/EndpointSemantics; the marker-trait zoo, descriptor ZSTs and ApiFamily are gone.
  • phoxal::session replaces the CLI's phoxal-client (Session, SessionHandle, ConnectOptions, ConnectedExecution, phase-specific errors; frozen bootstrap + compatibility-line refusal unchanged).
  • phoxal::supervisor::host is the supervisor implementation (feature supervisor); phoxal-supervisor is a main.rs. Raw bus ownership (BusOwner, Router, TimelineAuthority, world-clock minting) is pub(crate).
  • phoxal::simulator::SimulatorSession / WorldTime: the narrow external-simulator host SDK (feature simulator).
  • phoxal::authoring (ex phoxal-manifest, feature authoring).
  • cargo xtask compatibility resolves one published phoxal baseline; record identity is carrier-independent; a below-floor legacy read (xtask/src/legacy.rs, deletable after 0.66.x publishes) unions the five 0.65 packages for the one comparison that crosses the topology floor.
  • Policy gate gains: feature gates only at the crate root; official participants never take a host profile; no retired framework libraries; raw Zenoh only inside the bus; no hand-authored endpoints or family-rooted keys.

Wire-neutral by construction and by proof: phoxal::__compat::contract_surface() renders the same 64 records as the union of the five published 0.65 surfaces, byte for byte.

Evidence

  • cargo fmt --check, cargo clippy --workspace --all-targets -D warnings, cargo clippy -p phoxal --all-features, cargo hack check -p phoxal --each-feature, cargo test --workspace (861 tests), cargo test -p phoxal --all-features (630 tests), cargo doc --all-features with RUSTDOCFLAGS=-D warnings (0 warnings), cargo xtask policy (17 rules, all hold), cargo xtask compatibility rehearse-v1 (20 drilled, 20 held).
  • Byte-identical proof: PHOXAL_COMPAT_BASELINE_DIR=<0.65 surfaces> cargo test -p phoxal --all-features --test api -- --ignored -> the_aggregate_is_the_0_65_surface_byte_for_byte ... ok (54 endpoint + 7 bus + 1 bundle + 1 identity/version + 1 launch records on both sides).
  • Live compat report against crates.io: cargo xtask compatibility report -> baseline: 0.65.0, contracts: unchanged, source: compatible (3 authored projects), toolchain: 1.88 (unchanged).
  • Live cross-implementation attach: the new phoxal::session::Session (session profile) attaches to the released phoxal-supervisor 0.65.0 process over the frozen bootstrap: CONNECTED: remote framework 0.65.0 robot rgbd-imu-diff-drive; with the workspace labelled 0.66.0 the same attach is refused structurally: REFUSED: remote 0.65.0 local 0.66.0 refusal LocalNewer.
  • In-repo: session_attaches_to_the_supervisor (host + session on one execution), simulator world-session tests (delegated presence, outputs-before-clock, timeline replacement, deterministic close), 26 trybuild fixtures (family/semantics/side refusals, participant cannot reach host families or open a bus).
  • Profile proof (throwaway consumers): a participant compiles with default features; phoxal::session, phoxal::simulator, phoxal::participant::metadata are unreachable there and reachable under their profiles.

Consumers

phoxal-cli and phoxal/simulator-webots cut over in their own PRs against this train (phoxal = "0.66" with session,authoring / simulator); the org canon follows.

Review

Deep-track class (wire contracts, session lifecycle, release plumbing). Reviewed by the integrating agent with the evidence above at the maintainer's direction (no Codex counterpart for this campaign).

…/src

Pure file moves so history follows the files; nothing compiles yet.
Rewrites every path, folds the six manifests into phoxal/Cargo.toml, adds the
authoring/router features, and points the protocol tree macro at the new
in-crate module root. Workspace check and clippy are green; tests next.
… canon

Reduces the policy gate's library graph to phoxal + phoxal-macros, moves the
authoring feature check onto the one library, drops the six release-plz
packages and the crates.io wait list, and renames the module docs that still
named the retired packages.
The library-grammar tests named packages the merge deleted; they are pure
string rules, so they now use the surviving package and hypothetical names.
The component editor schema's one changed description follows the doc comment
that stopped naming the retired phoxal-model package.
…e from record identity

The framework is one library, so a baseline is the newest non-yanked `phoxal`
and nothing else is read.
Deletes the newest-common-version resolution, the partially published train
diagnostic, the two-floor refusal, and the predecessor-package fallback.
Record identity is now the record kind plus its own key: nothing on the wire
carries a Cargo package name, so a record that moved between modules is the same
record.

A baseline below the `0.66.0` topology floor is still read through the five
packages that carried the surface then, and through `phoxal-manifest` for the
authored-source leg, at that same exact version.
Their records are unioned, which is what lets the merge release prove itself as
no wire change.
`xtask/src/legacy.rs` holds all of it and is deletable once a `0.66.x` `phoxal`
is published.

The authored-source probe now depends on `phoxal` with the `authoring` profile
and reads the corpus through `phoxal::authoring::probe`; the rehearsal pins the
Stable-line tests under their merged module paths in `phoxal`.
`feature gates live only in the framework crate root` keeps a consumer profile
a visibility decision: it changes which modules `phoxal/src/lib.rs` declares as
public, never what a module compiles.
`official participants reach for no host profile` holds this repository's own
services, drivers and examples to the participant surface, in source and in the
features they enable.
`raw transport access stays inside the bus module` and `endpoints and topic keys
are never authored by hand` keep the second copies out: of the transport, and of
the topic tree.

`retired framework libraries stay absent from the dependency graph` refuses a
dependency on any of the six merged packages, whose last published versions stay
resolvable forever.

The authoring-feature check for official participants moves into the new
consumer-profile rule, which already covers that package set; the dependency
rule keeps it for the framework executable, which is not a participant. The
retired-surface self-exemption becomes a list, because the consumer-profile rule
spells `phoxal::simulator` in order to hunt for it.
The runbook described a baseline resolved from five carriers with a renamed
predecessor, and a source leg read through `phoxal-manifest`.
Restates both against the one library, records the one baseline still read
through the retired topology and when that section can go, and lists the four
new policy rules.
Two CI comments counted the packages a train publishes; they now count two.
… one

The bus module owns key grammar - liveliness and presence keys, execution roots
- so a literal there is the owner stating its own key rather than a second copy
of the tree; it is exempt from the topic-literal half exactly as it is from the
transport rule. The `impl Endpoint` half still admits only `bus/tree.rs`.

A literal beginning `"robot/meshes` is the bundle's asset layout. The asset root
and the robot family root collide as text and only their meaning separates them,
so the rule reads past that one prefix and says why; a line holding both an asset
path and a real key is still a finding.

`crates/fixture` joins `xtask` as a tree neither rule reads: it is unpublished
dev-only test support that no robot links.
…l_tree!

The dynamic endpoint tree stops being a global procedural catalogue and becomes
two private declarations that sit in the modules they describe: `crate::nodes!`
owns a module's child nodes and their typed dynamic segments, `crate::endpoints!`
owns its endpoint leaves beside the payload types they carry. Static segments are
spelled by their child module's identifier, dynamic placeholders by their declared
variable's, and the leading segment by the family marker, so no complete topic
string is authored anywhere.

The payload (or request) type is now the endpoint type. Descriptor ZSTs, the
generated `endpoint`/`topic` modules, the duplicated per-side builder trees,
`ApiFamily`, `ParticipantSpec::ContractApi`, and the marker-trait zoo are gone,
replaced by one sealed `Endpoint` with a `Family` and an `EndpointSemantics`, one
path tree with `.client()`/`.owner()` chosen at the leaf, and one owned `Topic`.

`phoxal::api`, `phoxal::runtime::api` and `phoxal::supervisor::api` are real
modules; `phoxal::__compat::contract_surface()` is the crate aggregate the three
families and the other owners state their records into.
Two rules the tree cannot enforce inside one crate: an endpoint's family and
semantics come from the `endpoints!` declaration beside its payload and nowhere
else, and a family-rooted key is rendered by walking the tree rather than
spelled by whoever needed it. Committed test sources are outside both, because
the bus is the ABI floor and its own tests declare stand-in endpoints and pin
rendered keys against the literals they are expected to be.

`protocol_tree` joins the retired-surface table: there is no global procedural
catalogue to bring back.
… with the api tree

Keep one implementation of the authored-endpoint rule (bus_boundary), drop
the duplicate api_tree module, exempt test code from the impl half, treat a
cfg(test) test_support.rs as test code, allow the frozen presence lease
literal beside the bootstrap, repin the six bootstrap rehearsal tests at
their merged module path, and stop spelling a runtime key in the world-clock
subscriber's error prose.
…e the supervisor, the client and the simulator into it

The crate stops being one overwhelming default API and becomes one product
with six named consumer roles. `default = ["participant"]` is a robot
developer's surface; `session`, `simulator`, `supervisor` and `authoring` are
the host roles around it, and `test-harness` is explicit participant test
support. A profile decides which modules `lib.rs` declares as `pub` - it never
forks a code path, and `cargo xtask policy` keeps it that way by rejecting
`cfg(feature` anywhere but the crate root.

Three implementations move in behind those profiles:

- `phoxal::supervisor::host` is the whole supervisor (`framework/supervisor/src`
  minus `main.rs`), so `phoxal-supervisor` is now a binary that parses one
  operand and calls `run()`. With the implementation in-crate, `BusOwner`,
  `BusConfig`, the embedded router, `TimelineAuthority`, `WorldClockPublisher`
  and every step minter become `pub(crate)`: no consumer profile receives raw
  transport, world or fabric ownership.
- `phoxal::session` is the CLI's `phoxal-client`, rewritten over the dynamic
  api tree. `Session` owns the transport and the lifecycle, `SessionHandle`
  performs typed operations, and the family wrapper modules are gone -
  consumers name `api`, `runtime::api` and `supervisor::api` directly.
- `phoxal::simulator::SimulatorSession` is the external world adapter's whole
  surface: typed component IO, delegated presence, deterministic close, and a
  `WorldTime` taken once and moved to the step thread.

`phoxal::testing` grows `TestBus` and `step_token`, because opening a bus is
now something the framework does on a test's behalf rather than something a
test does for itself.
`phoxal::simulator` is now the external simulator host SDK, and it means the
opposite of the role attribute that used to be spelled the same way: a world
owned from outside the graph rather than one participant inside it. The rule
matches the attribute spelling, so the retirement it guards stays guarded and
the new module path is ordinary source.

The consumer-profile rule's exemption goes with it - it only existed because
that module names `phoxal::simulator` - and the bus's two failure-injection
hooks stop riding `test-harness`: they are `cfg(test)` and crate-private, which
is what their only callers ever needed, and what removes the last Cargo feature
named outside the crate root.
… and pin the surfaces a profile hides

The crate root carried a second doc fragment for most modules - one on the
`mod` line and one in the module's own header - and rustdoc merged the two from
different scopes, which quietly broke every intra-doc link in the merged half.
Each module's `//!` header is now its documentation, and what `lib.rs` says
about a module is a plain comment about the profile decision. That, plus
spelling rather than linking anything a profile hides, takes `cargo doc` to zero
warnings for `default`, each host profile, and `--all-features` alike.

The compile-fail fixtures move with the surfaces they judge. A participant
cannot name `BusOwner`, `BusConfig` or the router, and cannot name the runtime
or supervisor families at all - the two new `fail/` cases - while the cases that
need a host family to exist before they can be refused move to `host_fail/`,
run by a second trybuild pass under `session`.

`phoxal::participant::launch` gains the encoder half of the launch contract:
the CLI writes participant argv locally and into systemd units, so the flag
spellings would otherwise live in another repository and drift from the parser
that has to accept them. The round-trip test is what keeps them one contract.

CI gains the two jobs the profiles need: every canonical profile built and
documented explicitly (a workspace build unifies features and can say nothing
about any single one), and `cargo semver-checks --all-features`, because
release-plz runs that tool with default features and exposes no way to pass a
feature selection through.
… that earn them

An `impl`-wide allow on `BusOwner`'s liveliness block and on `BusConfig` covered
methods that are alive in every profile, which would have hidden a real dead one
later. Each allow now names the item whose only caller is a module one profile
declares: the supervisor's own presence key, delegated participant presence, and
the external session configuration.
…and hide the participant engine from the profile that is one

The role attributes already emitted `$crate::__private::…` for every item but
one: the const-eval metadata writer reached `concatcp!` through
`$crate::participant::metadata`, which forced that module to stay public in
every profile. `__private::meta` already carried the same `const_format`
re-export for the config-schema path, so the second one beside the record was a
duplicate of one macro. Deleting it leaves `__private` as the whole ABI, exactly
as the module says it is.

With that, `phoxal::participant` takes the visibility the profile table gives
it. Its two public children are process contracts a *host* reads - the metadata
document a staged binary embeds, and the argv a launcher writes - so it is
public for `session`, `simulator`, `supervisor` and `authoring`, and private for
the profile that *is* a participant, whose authoring surface is the crate-root
facade.

Which half of that module is unreachable depends on the profile, so the
dead-code allows follow the halves rather than the module: a host profile allows
the engine below the two contracts, the two contracts allow their own reader and
encoder, and a build that selected no role at all allows both. The participant
profile allows neither, which is where the engine is alive and therefore where
the lint has something to say.

`xtask`'s bus-boundary fixture named `supervisor/src/router.rs`, a path the
supervisor move deleted; it now names a file that exists.
…rofile too

The table publishes `participant::metadata` for session, supervisor and
authoring only; a simulator stands in for drivers but never launches or
inspects a participant binary. Also spell the model path by its module name
in one authoring comment.
The shared workspace test unifies only the features the members enable, which
never includes session or simulator; the profile-gated tests and the
required-features integration tests need one all-features run. Rustdoc per
profile now fails on warnings, matching the local gate.
A simulator's step loop holds a WorldTime and the only source of one is a
session; an adapter proving its own stepping and parking discipline needs a
session no supervisor is running. Same constructor the framework's own proofs
use; there is no router, so it is not a second way to attach. The four runtime
examples state the participant profile they need.
… and state the branch-or-leaf tree rule

close() now revokes delegated Ready leases, drops the world's time, closes the
transport and returns the BusCloseReport as SimulatorCloseError when any stage
left evidence; the struct's field order is the same teardown order, so a
session dropped without close tears down safely. The api docs and crate root
state that a tree module is a branch (nodes!) or a leaf (endpoints!), never
both.
@jBernavaPrah

Copy link
Copy Markdown
Contributor Author

Review corrections applied (ae69376):

  • SimulatorSession::close now revokes the delegated Ready leases, drops the world's time, closes the transport and returns Result<(), SimulatorCloseError> carrying the BusCloseReport when any stage left evidence; the struct's field order is the same teardown order, so a session dropped without close tears down safely.
  • The branch-or-leaf rule of the api tree (a module is nodes! or endpoints!, never both; node-level endpoints are self: leaves) is stated in phoxal::api's module docs and the crate root, matching the implementation.
    All gates re-run green (clippy workspace + all-features, cargo test -p phoxal --all-features, rustdoc -D warnings for all-features and simulator profiles, policy 17/17).

… default profile

The example is pinned to the published train on purpose and its lockfile is
part of the proof; naming the participant feature explicitly only resolves once
0.66 is published, and the default profile is participant anyway.
…rk repository

The example robot project is phoxal/robot-rover; an in-repo copy pinned to a
published train can only lag the tree or stop being a runnable project. Its
CI job goes with it; the authoring-corpus tests and the authored-source
compatibility leg read the in-repo fixture robot, which mounts every component
type the repository authors.
@jBernavaPrah

Copy link
Copy Markdown
Contributor Author

Per review: the hello-rover example and its CI job are removed (caa4542); the example robot project is phoxal/robot-rover. The authoring-corpus tests and the authored-source compatibility leg read the in-repo fixture robot (source: compatible (2 authored projects), contracts: unchanged). The main ruleset now also requires workspace policy, consumer profiles and rust api surface (and no longer names the example).

@jBernavaPrah
jBernavaPrah merged commit 9990f3e into main Aug 19, 2026
10 checks passed
@jBernavaPrah
jBernavaPrah deleted the refactor/single-crate branch August 19, 2026 15:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant