Introduction
Jacquard is a deterministic routing system for ad hoc shaped networks. It provides a stable routing abstraction above the concrete routing algorithm. A host composes one or several engines behind the same contract, whether those engines ship with Jacquard or come from a 3rd party.
The docs are grouped numerically. The 200s cover the shared model and time. The 300s specify contract surfaces, simulator architecture, and experimental methodology. 400s contain one implementation spec per in-tree engine plus the reference client composition. 500s are 3rd-party developer guides for running simulations, assembling clients, and building custom components. Crate Architecture is the workspace architecture reference.
Scope
Jacquard owns the shared routing contract and seven in-tree routing engines. The router control plane, runtime adapters, and simulation harness are implemented crates. Protection-versus-connectivity policy may be supplied by a host, but Jacquard itself stays routing-engine-neutral at the contract layer.
The central split is between shared facts and local runtime state. Service descriptors, topology observations, admission checks, and route witnesses are explicit shared objects. Adaptive policy, selected routing actions, installed-route ownership, and engine-private runtime state stay local.
The routing model is shaped so admission, installation, maintenance, and replay remain explicit.
Problem
Jacquard is aimed at networks that are unstable, capacity-constrained, and potentially adversarial. Nodes may churn, links may degrade quickly, identities may be weak or partially authenticated, and local coordination may be necessary without any reliable global authority.
That creates two competing pressures. The system needs stronger coordination than naive flooding or purely local heuristics. It also cannot hard-code one routing doctrine such as GPS-based clique membership, singleton leaders, or full consensus on every routing transition.
It also needs to support more than one routing engine being present at once. A host such as Aura may want to run onion and pathway side by side, migrate traffic gradually from one to the other, or use one engine as a limited lower-layer carrier for another. Those are different cases and should not be collapsed into one mechanism.
System Shape
The top-level routing contract is routing-engine-neutral. A routing engine produces observational candidates, checks admission, admits a routing result, realizes it under router-provided canonical identity, publishes commitments, and handles engine-local maintenance. The control plane owns canonical route truth. The data plane forwards over already admitted truth.
When a routing engine needs local coordination, Jacquard allows it to expose a shared coordination result such as a committee selection. Jacquard does not require that every routing engine use committees, and it does not require that a committee have a distinguished leader. The shared layer standardizes the result shape, not the formation process. Formation may be engine-local, host-local, provisioned, or otherwise out of band.
Jacquard also allows a host-owned policy engine to compose routing engines through a neutral substrate contract. That means multiple routing engines may be used together, but the shared layer does not treat one canonical route as simultaneously owned by several unrelated engines. Composition happens through explicit carrier leases and layer parameters above the routing-engine boundary.
In-Tree Engines
Jacquard ships seven in-tree routing engines as concrete demonstrations of the contract:
pathwayfor explicit-path routingbatman-bellmanfor Bellman-Ford-enhanced next-hop routingbatman-classicfor spec-faithful BATMAN IV next-hop routingbabelfor RFC 8966 distance-vector routing with bidirectional ETX and feasibility distancesolsrv2for OLSRv2 link-state routingscatterfor bounded deferred-delivery diffusion routingmercatorfor hybrid corridor routing with stale-safe repair and bounded custody posture
These engines differ in what they publish. Pathway exposes an explicit path, Mercator publishes a corridor envelope, the proactive engines publish only next-hop visibility, and Scatter publishes an opaque viability claim. The shared routing contract carries canonical identity and lifecycle regardless of that published shape.
Design Commitments
Jacquard is fully deterministic. It uses typed time, typed ordering, explicit bounds, and explicit ownership objects instead of ambient runtime assumptions.
Jacquard supports distinct portability profiles. The default profile uses std for native host builds, the simulator, the reference client, and in-memory profiles. The wasm profile checks selected std crates on wasm32-unknown-unknown. The embedded profile builds the portable core, host-support, cast-support, router, and Mercator path with no_std plus alloc for MCU transport adapters.
Observation scopes are kept explicit. Local node state, peer estimates, link estimates, and neighborhood aggregates are separate model surfaces. This split keeps routing logic honest about what is known unequivocally, what is inferred about a peer, and what is an aggregate local view.
Jacquard is intentionally not opinionated about engine-local scoring, committee formation policy, or trust heuristics. The shared layer commits to the result shapes, evidence classes, ownership rules, and canonical transition path. The routing-engine layer owns the scoring rules, diversity logic, and misbehavior handling that depend on its routing semantics.
Lifecycle and Integration
The system is committed to one explicit service lifecycle: observation → candidate → admission → router-owned canonical identity allocation → engine realization → materialized route → maintenance, replacement, or teardown. Major transitions stay typed and explicit. Data-plane health stays observational until the control plane publishes a canonical change.
The composition boundary is intentionally narrow. The shared layer exposes substrate requirements, substrate leases, and layer parameters, but does not let routing engines leak their internals into one another.
Jacquard is also meant to be the integration point where multiple teams can contribute device-specific expertise without forking the routing model. One team may contribute a BLE node extension, another a Wi-Fi link extension, and another a platform-specific transport or service extension. The cooperative effect comes from merging those self-describing observations into one shared world picture above the routing-engine boundary, then letting routing engines incorporate observed nodes and links when their own criteria are met.
Core Types
This page focuses on the core primitives that other routing objects build on. See Crate Architecture for the internal directory layout of core.
Identity, Observation, And Fact
NodeId identifies one running Jacquard client. ControllerId identifies the cryptographic actor that authenticates for that node. NodeBinding makes that relationship explicit instead of assuming one node identity is enough for every deployment.
Jacquard uses an explicit epistemic ladder. Observation<T> is raw local input or a received report with provenance attached. Estimate<T> is a belief update derived from one or more observations. Fact<T> is the value the system is willing to treat as established routing truth.
This split matters because a recent topology sighting, a scored route candidate, and a published route witness are different kinds of claim.
#![allow(unused)]
fn main() {
pub struct NodeBinding {
pub node_id: NodeId,
pub controller_id: ControllerId,
pub binding_epoch: RouteEpoch,
pub proof: NodeBindingProof,
}
pub struct Observation<T> {
pub value: T,
pub source_class: FactSourceClass,
pub evidence_class: RoutingEvidenceClass,
pub origin_authentication: OriginAuthenticationClass,
pub observed_at_tick: Tick,
}
pub struct Estimate<T> {
pub value: T,
pub confidence_permille: RatioPermille,
pub updated_at_tick: Tick,
}
pub struct Fact<T> {
pub value: T,
pub basis: FactBasis,
pub established_at_tick: Tick,
}
}
This group of types shows two important boundaries. NodeBinding says who controls a node. Observation<T>, Estimate<T>, and Fact<T> say what kind of claim is being carried. Together they prevent the model from collapsing identity, evidence, inference, and publication into one opaque record.
FactSourceClass and OriginAuthenticationClass are intentionally separate. One says whether the fact is local or remote. The other says whether the origin is controlled, authenticated, or unauthenticated. That keeps provenance and authentication from collapsing into one mixed enum.
IdentityAssuranceClass is a second identity-facing qualifier. It says how strongly a node identity is grounded for routing-control decisions. That keeps “who claims to exist” separate from “how much committee or admission weight that identity should receive”.
Time And Qualifiers
Tick, DurationMs, OrderStamp, RouteEpoch, and ByteCount are the core scalar units. They keep local time, bounded duration, deterministic ordering, topology versioning, and byte quantities distinct at the type level. TimeWindow and TimeoutPolicy are the first compound objects built on those primitives. See Time Model for the full time-domain rules and the validated TimeWindow::new constructor.
Belief<T> and Limit<T> are the two main qualifier types. Belief<T> distinguishes Absent from Estimated(Estimate<T>), so the model can say both whether an estimate exists and how strong it is. Limit<T> says whether a budget is bounded or explicitly unlimited.
World Schema
Configuration is the shared graph-shaped world object the router reasons about. It wires together Node, Link, and Environment. World extensions emit Observation<ObservedValue> items that contribute to that picture.
Engine-specific heuristics do not live here. Novelty scoring, bridge detection, reach estimation, feasibility distances, and similar derived signals stay behind the engine trait boundary as engine-owned estimate types. core carries the world facts those heuristics are computed from, not the heuristics themselves.
Route Lifecycle Objects
RouteHandle, RouteLease, RouteMaterializationInput, RouteInstallation, RouteMaterializationProof, and RouteCommitment are the main runtime coordination objects in core. The router allocates canonical route identity through RouteHandle, RouteLease, and RouteMaterializationInput. The engine returns RouteInstallation and RouteMaterializationProof to describe what it realized under that identity.
Live routes are split into router-owned PublishedRouteRecord and engine-mutable RouteRuntimeState, composed as MaterializedRoute. Canonical route state does not come directly from a transport callback or raw health observation. Activation enforces the structural invariants. The admission decision must be admissible, the realized protection must satisfy the objective protection floor, and lease validity must be checked explicitly before publication or maintenance continues.
See Router Control Plane for the full lifecycle flow from objective through teardown.
Coordination And Layering
CommitteeSelection is the main shared coordination object. It carries a selected member set, role declarations, lease window, evidence basis, claim strength, and identity-assurance posture. core exposes only the coordination result shape. It does not define one universal committee-formation algorithm, require a leader, or encode engine-local scoring policy.
SubstrateRequirements, SubstrateCandidate, SubstrateLease, and LayerParameters are the shared layering objects. They exist so a host-level orchestrator can compose engines without teaching one engine about another’s internals. core exposes the carrier contract shape, not the host policy that decides when one engine should migrate to another.
DiscoveryScopeId is separate from the routing concept of a neighborhood. It is only a service-scope identifier used in ServiceScope::Discovery. It does not name a routing authority set or an engine-local topology object.
Pipeline And Observations
Jacquard keeps the shared routing pipeline explicit:
observation -> estimate -> fact -> candidate -> admission -> materialization -> publication
Only the first three stages live in the shared world model. Observation<T> carries raw local or remote input with provenance. Estimate<T> carries engine- or host-derived belief. Fact<T> carries the stronger claims the system is willing to treat as established routing truth. Candidate production, admission, materialization, and publication happen above this layer through the router and engine contracts.
This split matters because a recent link sighting, an engine-scored path preference, and a router-published route witness are not the same kind of statement. The type system keeps those boundaries visible.
World Extension Surface
World extensions contribute shared Node, Link, Environment, and related observation values without taking ownership of routing semantics.
- Extensions emit transport-neutral observations into the shared graph.
- Extensions do not publish canonical route truth.
- Engine-local heuristics such as novelty, relay value, bridge centrality, or next-hop scores stay private to the engine that derives them.
- Transport-specific authoring and handshake logic stays outside
jacquard-core.
This is the boundary that lets concrete transports, host integrations, and profile crates describe the world honestly while keeping route selection and publication above the shared model.
Time Model
Jacquard uses a typed deterministic time model. It does not treat wall clock as distributed truth. The routing core works with local monotonic time, bounded durations, deterministic ordering tokens, and topology epochs.
Time Domains
Tick is local monotonic time. It is used for expiry, replay checks, scheduling, and publication timestamps. DurationMs is a bounded duration type for timeout and backoff policy.
OrderStamp is a deterministic ordering token. RouteEpoch versions topology and reconfiguration state.
These domains are not interchangeable. Tick is not wall clock. OrderStamp is not an expiry. RouteEpoch is not elapsed time. Model field names should carry their domain when needed, such as *_tick, *_ms, and *_epoch.
When validity depends on time, Jacquard passes Tick explicitly. A topology or service epoch may version shared state, but it must not be reinterpreted as elapsed time just to satisfy a validity check.
#![allow(unused)]
fn main() {
pub struct Tick(pub u64);
pub struct DurationMs(pub u32);
pub struct OrderStamp(pub u64);
pub struct RouteEpoch(pub u64);
}
Each type is a newtype over a fixed-width integer. They are distinct at the type level so the compiler rejects accidental mixing.
Local Choice
Clock time is a local choice in Jacquard. It is valid for local waiting, retry, retention, and expiry decisions. It is not proof that another node observed the same event or reached the same conclusion.
Remote observation of another device clock must stay above the routing core. If a host needs to exchange time-related state, it should pass that state explicitly as application data. The routing core may carry the data, but it must not treat a remote clock as native routing truth.
Runtime Boundary
Jacquard accesses time and deterministic ordering through abstract effects. TimeEffects provides Tick. OrderEffects provides OrderStamp. This keeps production, tests, and simulation on one semantic model even when their underlying runtimes differ.
TimeWindow and TimeoutPolicy are the main compound time objects in the model. TimeWindow is used for bounded validity. TimeoutPolicy is used for bounded retries and local waiting policy. Both stay in the deterministic time domain and avoid raw timestamp fields.
TimeWindow is constructed through a validated constructor. Invalid windows with end_tick <= start_tick are rejected at the type boundary. This prevents them from leaking into leases, service validity, or route admission.
#![allow(unused)]
fn main() {
pub struct TimeoutPolicy {
pub attempt_count_max: u32,
pub initial_backoff_ms: DurationMs,
pub retry_multiplier_permille: RatioPermille,
pub backoff_ms_max: DurationMs,
pub overall_timeout_ms: DurationMs,
}
}
TimeoutPolicy governs all bounded retry and backoff behavior. The multiplier uses RatioPermille rather than a floating-point scale factor.
Routing Engines
This page describes the trait surface for adding a routing algorithm to Jacquard. It also captures the host capability boundary that engines consume and the in-tree engine shapes.
Routing Engine Contract
A routing engine is a routing algorithm that consumes the shared world picture and realizes routes under router-provided identity. Jacquard ships seven in-tree engines: pathway (explicit-path), mercator (hybrid corridor), batman-bellman (Bellman-Ford-enhanced next-hop), batman-classic (spec-faithful BATMAN IV next-hop), babel (RFC 8966 distance-vector), olsrv2 (OLSRv2 link-state), and scatter (bounded deferred-delivery diffusion). External engines can plug into the same contract without depending on any in-tree engine’s internals.
pub trait RoutingEnginePlanner {
#[must_use]
fn engine_id(&self) -> RoutingEngineId;
#[must_use]
fn capabilities(&self) -> RoutingEngineCapabilities;
#[must_use]
fn candidate_routes(
&self,
objective: &RoutingObjective,
profile: &SelectedRoutingParameters,
topology: &Observation<Configuration>,
) -> Vec<RouteCandidate>;
fn check_candidate(
&self,
objective: &RoutingObjective,
profile: &SelectedRoutingParameters,
candidate: &RouteCandidate,
topology: &Observation<Configuration>,
) -> Result<RouteAdmissionCheck, RouteError>;
fn admit_route(
&self,
objective: &RoutingObjective,
profile: &SelectedRoutingParameters,
candidate: RouteCandidate,
topology: &Observation<Configuration>,
) -> Result<RouteAdmission, RouteError>;
}
pub trait RoutingEngine: RoutingEnginePlanner {
fn materialize_route(
&mut self,
input: RouteMaterializationInput,
) -> Result<RouteInstallation, RouteError>;
fn route_commitments(&self, route: &MaterializedRoute) -> Vec<RouteCommitment>;
fn engine_tick(
&mut self,
tick: &RoutingTickContext,
) -> Result<RoutingTickOutcome, RouteError> {
Ok(RoutingTickOutcome::no_change_for(tick))
}
fn maintain_route(
&mut self,
identity: &PublishedRouteRecord,
runtime: &mut RouteRuntimeState,
trigger: RouteMaintenanceTrigger,
) -> Result<RouteMaintenanceResult, RouteError>;
fn teardown(&mut self, route_id: &RouteId);
}
RoutingEnginePlanner is pure. RoutingEngine is effectful. The split keeps candidate production deterministic and keeps runtime mutation inside explicit realization and maintenance methods.
The router allocates canonical route identity first. The engine realizes the admitted route under that identity and returns RouteInstallation. The final MaterializedRoute is assembled above the engine boundary as router-owned identity plus engine-owned runtime state. Maintenance only receives the mutable runtime portion.
Route choice and transition logic within a given engine is explicit and replayable. Engines project a small read-only planner snapshot from their private runtime state. Planner methods should read that snapshot rather than hidden mutable tables. Runtime methods should normalize inputs, call a pure reducer, then apply the reducer result through storage, transport, and logging effects.
The shared model-trait family in jacquard-traits captures that same split for deterministic model execution. RoutingEnginePlannerModel runs candidate generation and admission from an explicit planner snapshot. RoutingEngineRoundModel and RoutingEngineMaintenanceModel run pure reducers over explicit state and normalized input when an engine exposes those transitions. RoutingEngineRestoreModel reconstructs route-private runtime from the router-owned MaterializedRoute record.
The simulator consumes those engine-owned model traits directly instead of maintaining a parallel simulator-specific engine API.
This activation step also enforces the shared control-plane invariants. The admission decision must be admissible. The realized protection must satisfy the objective protection floor. Lease validity must be checked explicitly before maintenance or publication proceeds.
Engine Tick
engine_tick is the optional engine-wide bootstrap and convergence hook. The router or host owns cadence and passes a shared RoutingTickContext containing the authoritative merged topology observation for that step. The engine returns a small RoutingTickOutcome so the router can observe whether the tick changed engine-private state without standardizing engine internals. The hook itself does not publish canonical route truth directly.
RoutingTickOutcome.next_tick_hint is advisory scheduling pressure, not self-scheduling authority. Proactive engines such as Babel- or BATMAN-style implementations can report that more work is due soon. The host or router owns final cadence.
An engine may use a richer internal runtime model behind that hook. First-party pathway, for example, drives protocol-side ingress and bounded control-state refresh through a private choreography guest runtime while keeping the shared engine_tick signature unchanged.
That private choreography runtime does not replace the shared Jacquard effect traits. Generated Telltale effect interfaces remain engine-private implementation details, and the pathway interpreter adapts them onto the stable TimeEffects, OrderEffects, StorageEffects, RouteEventLogEffects, and TransportSenderEffects surfaces exposed by jacquard-traits. Host-owned TransportDriver implementations stop at the router or bridge layer, which delivers explicit ingress before each synchronous router round.
The migrated research engine now lives in DualTide. Jacquard keeps the shared route contract and host ownership rules available for external engines without carrying that engine as an in-tree implementation.
Router-led recovery follows the same split. The baseline engine hook restores route-private runtime from route identity. The router also supplies the current topology to a richer recovery hook so engines can rebuild topology-derived forwarding state without persisting that derived view in checkpoints.
Runtime Effect Boundary
The host capability surface stays narrow on purpose.
TransportSenderEffectsis the shared synchronous send capability engines use during a deterministic round.TransportDriveris the host-owned ingress and supervision surface.TimeEffects,OrderEffects,StorageEffects, andRouteEventLogEffectsremain capability traits, not runtime-owner traits.
Engines do not own async streams, driver supervision loops, or Jacquard time assignment. Hosts and bridges own those responsibilities and inject observations before the next synchronous router round.
Contract Rules
Two implementation rules are worth keeping explicit. If a planning or admission judgment depends on observations, the current topology must be passed into that method directly rather than read from ambient engine state. And if an engine keeps planner caches, those caches are memoization only: cache hits and misses must not change the semantic result for the same topology.
External routing engines should depend on jacquard-core and jacquard-traits. They should not depend on pathway internals, router internals, or simulator-private helpers. The stable shared contract includes RouteSummary, Estimate<RouteEstimate>, RouteAdmissionCheck, RouteWitness, RouteHandle, RouteLease, RouteMaterializationInput, RouteInstallation, RouteCommitment, RouteMaintenanceResult, CommitteeSelection, SubstrateRequirements, SubstrateLease, LayerParameters, Observation<T>, and Fact<T>. External engines must not assume pathway route shape, pathway topology structure, pathway-specific maintenance semantics, or any authority model outside those shared route objects.
Route Shape Visibility
Jacquard does not require every routing engine to expose a full hop-by-hop path.
ExplicitPath- engine can expose an actual route path shapeCorridorEnvelope- engine exposes a conservative end-to-end corridor envelope without claiming an explicit pathNextHopOnly- engine only claims best-next-hop visibility toward the destinationOpaque- engine does not expose useful route shape beyond viability
This matters for proactive engines. Pathway is ExplicitPath. Mercator is CorridorEnvelope. The batman engines (bellman and classic), babel, and olsrv2 are NextHopOnly. Scatter is Opaque: it can claim bounded deferred-delivery viability without claiming a stable next hop or explicit path shape.
In-Tree Engines
See Pathway Routing, Batman Routing, Mercator Routing Engine, Babel Routing, OLSRv2 Routing, and Scatter Routing for engine-specific models, capability assumptions, and maintenance behavior.
Policy And Coordination
Policy and coordination traits are separate from route realization. They cover host policy, optional local coordination results, and engine layering without direct engine-to-engine awareness.
#![allow(unused)]
fn main() {
pub trait PolicyEngine {
#[must_use]
fn compute_profile(
&self,
objective: &RoutingObjective,
inputs: &RoutingPolicyInputs,
) -> SelectedRoutingParameters;
}
pub trait CommitteeSelector {
type TopologyView;
fn select_committee(
&self,
objective: &RoutingObjective,
profile: &SelectedRoutingParameters,
topology: &Observation<Self::TopologyView>,
) -> Result<Option<CommitteeSelection>, RouteError>;
}
pub trait CommitteeCoordinatedEngine {
type Selector: CommitteeSelector;
fn committee_selector(&self) -> Option<&Self::Selector>;
}
pub trait SubstratePlanner {
#[must_use]
fn candidate_substrates(
&self,
requirements: &SubstrateRequirements,
topology: &Observation<Configuration>,
) -> Vec<SubstrateCandidate>;
}
pub trait SubstrateRuntime {
fn acquire_substrate(
&mut self,
candidate: SubstrateCandidate,
) -> Result<SubstrateLease, RouteError>;
fn release_substrate(&mut self, lease: &SubstrateLease) -> Result<(), RouteError>;
fn observe_substrate_health(
&self,
lease: &SubstrateLease,
) -> Result<Observation<RouteHealth>, RouteError>;
}
pub trait LayeredRoutingEnginePlanner {
#[must_use]
fn candidate_routes_on_substrate(
&self,
objective: &RoutingObjective,
profile: &SelectedRoutingParameters,
substrate: &SubstrateLease,
parameters: &LayerParameters,
) -> Vec<RouteCandidate>;
fn admit_route_on_substrate(
&self,
objective: &RoutingObjective,
profile: &SelectedRoutingParameters,
substrate: &SubstrateLease,
parameters: &LayerParameters,
candidate: RouteCandidate,
) -> Result<RouteAdmission, RouteError>;
}
pub trait LayeredRoutingEngine: RoutingEngine + LayeredRoutingEnginePlanner {
fn materialize_route_on_substrate(
&mut self,
input: RouteMaterializationInput,
substrate: SubstrateLease,
parameters: LayerParameters,
) -> Result<RouteInstallation, RouteError>;
}
pub trait LayeringPolicyEngine {
fn activate_layered_route(
&mut self,
objective: RoutingObjective,
outer_engine: RoutingEngineId,
substrate_requirements: SubstrateRequirements,
parameters: LayerParameters,
) -> Result<MaterializedRoute, RouteError>;
}
}
PolicyEngine, CommitteeSelector, CommitteeCoordinatedEngine, SubstratePlanner, and LayeredRoutingEnginePlanner are planning or read-only surfaces. SubstrateRuntime, LayeredRoutingEngine, and LayeringPolicyEngine are effectful. CommitteeSelector is optional. Jacquard standardizes the CommitteeSelection result shape, not one formation algorithm, and selectors may return None when no committee applies.
Selector implementations may be engine-local, host-local, provisioned, or otherwise out of band. The substrate and layering traits are forward-looking contract surfaces for host-owned composition.
Router Control Plane
jacquard-router is a generic middleware layer that owns the canonical control plane above the routing-engine boundary. The router registers one or more routing engines, orchestrates cross-engine candidate selection, and publishes the selected engine result as canonical. Routing engines plan, admit, and maintain route-private runtime state without touching canonical route identity or publication.
This includes proactive engines. The router does not own proactive routing tables. It only owns canonical publication over the evidence those engines return.
Ownership
The router owns canonical route-handle issuance, canonical lease publication, canonical commitment publication, explicit ingress queues, and router-owned round cadence. The router also dispatches maintenance triggers to engines.
Routing engines remain the owners of route-private runtime state and proof-bearing evidence. Profile implementations and test harnesses remain observational with respect to canonical route truth.
Cross-Engine Orchestration
The router coordinates multiple registered routing engines while keeping engine internals encapsulated. Engines are registered once and queried during activation and maintenance. Each engine returns candidates, evidence, and proofs through shared trait boundaries.
A policy engine computes the routing profile (protection class, connectivity posture, mode) from the current routing objective and local state. The router passes that profile to all registered engines. Engines return candidates ordered by cost and evidence. The router selects the best candidate, asks that engine to admit and materialize the route, and only then publishes canonical state.
The router remains oblivious to engine-specific scoring, topology models, or repair strategies. Engines remain oblivious to lease ownership, commitment publication, or multi-engine selection logic.
Activation Flow
The control-plane path is:
objective
-> policy profile
-> authoritative topology observation
-> explicit queued ingress
-> synchronous router round
-> cross-engine candidate ordering
-> selected-engine admission
-> router-owned handle + lease
-> engine materialization proof
-> canonical publication
-> router-published commitments
The engine does not mint the canonical handle, publish the canonical lease, or surface commitments as canonical truth. The router consumes RouteMaterializationProof, RouteWitness, RouteMaintenanceResult, and RouteSemanticHandoff to publish canonical state.
Route Lifecycle
The route lifecycle is owned by the control plane above the engine boundary.
- A host activates a
RoutingObjective. - The router computes policy and queries registered engines for candidates.
- The selected engine admits and materializes under router-owned identity.
- The router publishes canonical route state and commitments.
- Later rounds drive maintenance, replacement, handoff, expiry, or teardown.
Engines report proof-bearing maintenance outcomes such as continued health, repair, handoff, replacement pressure, or expiry. The router decides whether that engine result implies canonical mutation.
Tick and Maintenance
The router advances through synchronous rounds. Hosts feed topology, policy inputs, and transport observations into RoutingMiddleware, then call advance_round on the control plane. During that round the router drives RoutingTickContext into each registered engine and consumes RoutingTickOutcome.
Engines may refresh private control state and summarize previously ingested observations. They may run engine-private choreographies. Engines do not publish canonical truth directly during engine_tick.
RoutingTickOutcome.next_tick_hint lets proactive engines report scheduling pressure without taking ownership of cadence. The router or host may honor that hint, clamp it, or ignore it. The cadence decision remains router or host owned.
When maintenance returns a typed engine result, the router decides whether that implies canonical mutation. The outcome is a RouteMaintenanceOutcome variant. ReplacementRequired triggers router-owned reselection and replacement. HandedOff triggers router-owned lease transfer.
Continued and Repaired update the router-published commitment view without changing canonical identity. HoldFallback records a partition-tolerant hold state with the retained-object count. Failed(RouteMaintenanceFailure) removes the canonical route, with the failure sub-variant distinguishing LostReachability, CapacityExceeded, LeaseExpired, BackendUnavailable, InvalidEvidence, or PolicyRejected.
RoutingControlPlane returns typed router outcomes instead of collapsing everything to Result<(), E>.
The router also owns the durable publication sequence for canonical state:
typed engine evidence
-> router checkpoint update
-> router-stamped route event
-> in-memory canonical publication
Pathway may checkpoint route-private runtime payloads. Canonical route publication and canonical route-event emission happen in the router.
Configuration and State Updates
The router exposes RoutingMiddleware for hosts to update observable topology, policy inputs, and transport ingress without triggering route activation or maintenance. Hosts ingest topology observations when new world state arrives. Hosts ingest policy inputs when local conditions (capacity, churn, health) change. Hosts ingest transport observations explicitly instead of letting engines or routers poll transport adapters directly.
The router also exposes a recovery interface for checkpoint replay. Hosts call recover_checkpointed_routes after restart to restore the previous canonical route table and active materialized state.
Discovery Boundary
Shared discovery and coarse capability selection stay on ServiceDescriptor. Pathway nodes advertise route-capable surfaces through shared service descriptors. The router and test harness consume those shared descriptors. Jacquard does not introduce one universal handshake object for Discover, Activate, Repair, or Hold.
If a future engine needs stronger bilateral terms, add service-specific negotiation objects on that concrete path only.
Multi-Device Composition
A direct host/runtime composition harness exists outside the simulator. jacquard-mem-link-profile provides the shared in-memory carrier and effect adapters. jacquard-reference-client shows the minimum host bridge wiring for a new device target: one bridge-owned transport driver, one or more queue-backed transport senders handed to engines, explicit ingress stamping, and explicit synchronous router rounds. The end-to-end multi-device test exercises reference-client, router, pathway, batman-bellman, batman-classic, babel, and mem-link-profile across multiple runtimes.
This harness proves crate-boundary composition. It does not replace the simulator. The simulator remains the scenario/replay layer above these shared boundaries.
Minimal Host Wiring
The reference examples for a new deployment target are the split
reference-client end-to-end tests in
crates/reference-client/tests/e2e_pathway_shared_network.rs,
crates/reference-client/tests/e2e_batman_pathway_handoff.rs,
crates/reference-client/tests/e2e_olsrv2_shared_network.rs,
and
crates/reference-client/tests/e2e_olsrv2_pathway_handoff.rs,
backed by the shared scenarios in
crates/testkit/src/reference_client_scenarios.rs.
- build a shared
Observation<Configuration>with ordinaryServiceDescriptorvalues - attach one bridge-owned
TransportDriverper device runtime - construct one or more engines per device over queue-backed
TransportSenderEffects - wrap those engines in one router that owns canonical publication
- bind one host bridge owner per runtime, ingest topology and transport updates explicitly there, and advance the router through synchronous bridge rounds instead of minting route truth directly
The minimum composition surface for a new device includes world input, bridge-owned transport registration, router activation, and data-plane forwarding over admitted routes.
Profile Implementations
jacquard-mem-node-profile, jacquard-mem-link-profile, and jacquard-reference-client are Jacquard’s in-tree profile and composition crates. The two mem-* crates model node and link inputs without importing routing logic. jacquard-host-support sits beside them as the reusable support crate for transport/profile implementers. The reference client composes those profile implementations with jacquard-router and the in-tree routing engines to exercise the full shared routing path in tests.
Ownership Boundary
Profile crates are Observed. They model capability advertisement, transport carriage, and link-level state. They do not plan routes, issue canonical handles, publish route truth, or interpret routing policy.
Canonical route ownership remains on the router, and engine-private runtime state remains inside the routing engine. This keeps profile code reusable across routing engines and prevents observational fixtures from drifting into shadow control planes.
jacquard-core types flow through these crates unchanged. Node, NodeProfile, NodeState, Link, LinkEndpoint, LinkState, and ServiceDescriptor keep their shared-model shape end to end. The mem-* crates wrap builders around those shared objects instead of replacing or reshaping them, and the reference client hands the constructed world picture to the router as a plain Observation<Configuration>.
Crate Responsibilities
| Crate | Provides | Shared boundary it implements |
|---|---|---|
jacquard-host-support | TransportIngressSender, TransportIngressReceiver, TransportIngressNotifier, TransportIngressDrain, PeerDirectory, PendingClaims, ClaimGuard | none. It provides transport-neutral host support primitives over jacquard-core vocabulary |
jacquard-mem-node-profile | SimulatedNodeProfile, NodeStateSnapshot, SimulatedServiceDescriptor builders | none. It only emits jacquard-core model values |
jacquard-mem-link-profile | SimulatedLinkProfile, CastLinkPreset, SharedInMemoryNetwork, InMemoryTransport, InMemoryRetentionStore, InMemoryRuntimeEffects, transport-neutral reference defaults | TransportSenderEffects, TransportDriver, RetentionStore, TimeEffects, OrderEffects, StorageEffects, RouteEventLogEffects |
jacquard-reference-client | ClientBuilder, HostBridge, ReferenceRouter/ReferenceClient aliases, plus NodePreset, NodePresetOptions, NodeIdentity, LinkPreset, and LinkPresetOptions re-exported from the mem profile crates | none. It is pure composition over the crates above |
The mem-* crates stay routing-engine-neutral and transport-neutral. They carry frames, emit observations, and build shared model values. They do not mint route truth, interpret routing policy, or own BLE or IP-specific authoring helpers.
jacquard-host-support likewise stays transport-neutral. It owns generic ownership scaffolding only, not endpoint constructors, protocol state, or driver traits.
jacquard-mem-link-profile can adapt jacquard-cast-support delivery support into ordinary in-memory directed Link observations through CastLinkPreset. Endpoint authoring belongs to the caller through an explicit resolver closure.
Reference-client fixtures are the single place where a service descriptor picks up engine-specific routing-engine tags such as PATHWAY_ENGINE_ID, BATMAN_BELLMAN_ENGINE_ID, or BABEL_ENGINE_ID. That decision is composition, not profile. The reference-client bridge is also the only sanctioned place where transport ingress is drained and stamped before delivery to the router.
Composition
ClientBuilder is the wiring entry point. It attaches one bridge-owned InMemoryTransport driver to a SharedInMemoryNetwork, constructs queue-backed sender capabilities for each enabled engine, registers the engine set on a fresh MultiEngineRouter, and returns a ReferenceClient host bridge. The builder supports any combination of pathway, batman-bellman, batman-classic, babel, olsrv2, scatter, and mercator engines. The migrated research engine now lives in the sibling DualTide repository. Multiple clients built against the same network share one deterministic carrier while advancing routing state through explicit bridge rounds.
graph LR NodeProfile[jacquard-mem-node-profile<br/>SimulatedNodeProfile<br/>NodeStateSnapshot<br/>SimulatedServiceDescriptor] LinkProfile[jacquard-mem-link-profile<br/>SimulatedLinkProfile<br/>InMemoryTransport<br/>InMemoryRetentionStore<br/>InMemoryRuntimeEffects] Network((SharedInMemoryNetwork)) Ref[jacquard-reference-client<br/>fixtures + ClientBuilder + HostBridge] Router[MultiEngineRouter] Engines[PathwayEngine / BatmanBellmanEngine /<br/>BatmanClassicEngine / BabelEngine /<br/>OlsrV2Engine / ScatterEngine / MercatorEngine] NodeProfile --> Ref LinkProfile --> Ref Network --> LinkProfile Ref --> Router Ref --> Engines Router -- registers --> Engines
The reference end-to-end examples are the split reference-client tests in crates/reference-client/tests/client_builder.rs, crates/reference-client/tests/e2e_pathway_shared_network.rs, crates/reference-client/tests/e2e_batman_pathway_handoff.rs, crates/reference-client/tests/e2e_olsrv2_shared_network.rs, and crates/reference-client/tests/e2e_olsrv2_pathway_handoff.rs, plus the shared scenarios in crates/testkit/src/reference_client_scenarios.rs. They show how to add a new client runtime to the same in-memory network without bypassing the bridge-owned ingress path or the router-owned canonical path.
For custom link or node profile work, start from the in-tree builders in jacquard-mem-link-profile and jacquard-mem-node-profile, then validate the ownership boundaries against Crate Architecture.
Simulator Architecture
jacquard-simulator is the deterministic scenario and experiment harness for Jacquard. It sits above the shared routing boundaries and reuses the real reference-client host composition. It does not own canonical route truth. Canonical ownership remains on the router and engine crates.
The four core types are JacquardScenario, ScriptedEnvironmentModel, JacquardSimulator, and JacquardReplayArtifact. See Running Simulations for the step-by-step walkthrough a library consumer follows. For downstream experiment crates, the stable import path is the 500-series facade documented in Running Simulations and Running Experiments, not a crate-local external API file.
Internal Lanes
The simulator is a dual-lane harness. The full-stack lane drives the reference-client bridge and the real router and runtime composition. The model lane runs explicit planner snapshots, pure round reducers, pure maintenance reducers, and checkpoint fixtures without a host bridge.
The model lane does not replace the full-stack lane. It offers a cheaper path for deterministic planner and transition checks against engine-owned pure surfaces.
Execution has three modes. full-stack runs the maintained comparative families. model runs explicit fixture-driven planner, reducer, and restore checks. equivalence runs a model fixture and a full-stack replay for the same case and asserts that the visible decision matches.
Engine Selection Per Host
The simulator selects engines per host through EngineLane. Single-engine variants cover Pathway, BatmanBellman, BatmanClassic, Babel, OlsrV2, Scatter, and Mercator. Mixed-engine variants include PathwayAndBatmanBellman, PathwayAndBabel, PathwayAndOlsrV2, BabelAndBatmanBellman, OlsrV2AndBatmanBellman, AllEngines, and RouteVisibleEngines. The migrated research engine and paper now live in the sibling DualTide repository. Jacquard keeps only the simulator surfaces needed for the maintained routing report and multi-router comparison corpus.
All engines share one host bridge per node. The bridge owns ingress draining and Tick stamping. Engines keep private runtime state below the shared routing boundary.
Reused Surfaces
The simulator reuses existing Jacquard composition surfaces. It does not maintain a simulator-only stack.
jacquard-reference-client provides host bridge ownership and round advancement. jacquard-host-support provides queueing and host support primitives. jacquard-mem-link-profile provides in-memory transport composition. jacquard-mem-node-profile provides node profile authoring.
Environment Model
ScriptedEnvironmentModel schedules environment changes as EnvironmentHook values keyed to specific ticks. Applied hooks appear in each JacquardRoundArtifact for replay and inspection.
ReplaceTopologyswaps the full network configuration at a given tick.MediumDegradationadjusts delivery confidence and loss on a link between two nodes.AsymmetricDegradationadjusts forward and reverse confidence and loss independently on a directed link.Partitionremoves reachability between two nodes.CascadePartitionremoves multiple directed links simultaneously.MobilityRelinkreplaces one link with another to model node movement.IntrinsicLimitadjusts connection count or hold capacity constraints on a node.
Replay Artifacts
JacquardSimulator::run_scenario returns a JacquardReplayArtifact and a JacquardSimulationStats. The artifact captures the complete observable state of the run.
- environment traces and applied hooks per round
- ingress-batch boundaries and host-round outcomes
RouteEventandRouteEventStampedoutputsDriverStatusEventrecords for dropped ingress- deterministic checkpoints with host snapshots
- failure summaries for diagnostic inspection
Checkpoints carry InMemoryRuntimeEffects snapshots per host. These snapshots are needed to rebuild the bridge and recover checkpointed route state across all engines. Pathway is the only lane that exposes Telltale-native replay references. Checkpoint resume works across all engines.
Model-Lane Artifacts
Model-lane runs use their own fixture outputs instead of host-round replay artifacts. They record explicit planner snapshots, candidate counts, reducer summaries, restore outputs, and equivalence results in model_artifacts.jsonl. This makes equivalence checks against full-stack runs possible without introducing a simulator-only engine stack.
That file is additive. The maintained full-stack artifact contract remains the full-stack run log plus the aggregate and breakdown JSON outputs, plus the diffusion artifact set for deferred-delivery analysis. The report pipeline does not score model_artifacts.jsonl. It uses it for model-lane inspection and equivalence debugging only.
The model-lane selectors are babel-model-smoke, babel-equivalence-smoke, batman-bellman-model-smoke, batman-classic-model-smoke, olsrv2-model-smoke, pathway-model-smoke, and scatter-model-smoke in the tuning_matrix binary. They exercise engine-owned planner seeds, planner snapshots, reducer state, and restore inputs through the shared model-trait family.
Public Runner Boundary
ExperimentRunner is the external runner facade. It delegates built-in tuning
and diffusion execution to the existing artifact writers, and it runs custom
route-visible suites through ExperimentSuiteSpec without requiring access to
the maintained suite catalog. ArtifactSink::directory writes artifacts;
ArtifactSink::disabled supports in-memory smoke checks without report
generation.
The maintained corpus remains a consumer of the runner boundary through
builtin_suites and the tuning_matrix binary. That split keeps extraction
preparation additive: downstream crates can import the simulator and define
custom suites, while the current analysis/ report still receives the same
standard files.
Experimental Methodology
This page describes the methodology behind Jacquard’s maintained experiment corpus. It explains what an experiment is in this codebase, which variables the corpus varies deliberately versus measures as outcomes, why engines are tuned before comparative experiments, how tuned configurations flow into those experiments, and the pipeline from simulator run to final report.
Readers who want to run an experiment before reading the methodology should go directly to Running Experiments. For the simulator architecture the methodology sits on, see Simulator Architecture.
What An Experiment Is
An experiment in Jacquard is a maintained scenario family combined with a parameter sweep, a set of measured outputs, and a reduction into stable summary artifacts. A scenario family fixes the qualitative regime under test. A parameter sweep varies one or more engine or policy parameters inside that regime. A reduction aggregates per-run measurements into summary tables and boundary surfaces.
The unit of record is the scenario family, not a single scenario. Comparing two single runs invites noise from seed and regime idiosyncrasy. Comparing two families across multiple seeds averages over those idiosyncrasies and produces claims about engine or policy behavior rather than individual runs.
The simulator lanes described in Simulator Architecture are how these experiments execute. The full-stack lane drives the real router and engines for behavioral claims. The model lane runs pure planner and reducer fixtures for determinism checks that do not need host wiring.
Independent And Dependent Variables
A Jacquard experiment varies deliberate dimensions and measures separate outcomes. The dimensions and outcomes are kept disjoint so any correlation between them is a claim the experiment supports rather than a tautology.
The independent variables in the maintained corpus are topology and density, delivery pressure, medium pressure, directional mismatch, topology movement, local node pressure, and workload class. See Running Experiments for the exact bands each variable takes.
The dependent variables are run outcomes. The report surfaces activation success, route presence, first materialization, first loss, recovery timing, route churn, engine handoffs, stress boundary, and first breakdown family. Per-engine cost and replay metrics extend the outcome set where applicable.
Why Engines Are Tuned First
Every in-tree engine exposes parameters that strongly affect its behavior. Examples include the BATMAN decay window, the Pathway search budget, the Babel decay window, Scatter profile thresholds, and Mercator corridor scoring.
Running a comparative experiment before locking those parameters would conflate per-engine tuning choices with per-engine capability. A comparison that pits one engine against another at arbitrary parameter settings measures both the relative engine choice and the tuning gap between them.
The tuning sweep isolates parameter effects under one engine at a time. Its output is a representative parameter set per engine, chosen as a defensible operating point in the regimes the corpus targets. Subsequent comparative experiments hold those parameters fixed, so the contrast measures engines rather than tuning.
How Tuned Configurations Feed Into Experiments
Once the tuning phase selects a representative parameter set per engine, comparative and head-to-head experiments use those fixed settings. The corpus encodes the choice in the experiment suite definitions themselves. A reader auditing a comparison can trace which engines use which tuned parameters without a second lookup.
A 3rd party running an experiment that introduces a new engine should either contribute a tuning family for that engine first, or declare the fixed operating point it uses. The declared operating point must be defensible for the regimes in question. Skipping the tuning-first sequence is permitted only when the experiment is explicitly about tuning behavior itself.
The same discipline applies to policy parameters when a comparison covers policies that expose tuning knobs. A head-to-head that varies policy as well as engine should not simultaneously vary policy tuning unless tuning is the variable of interest.
The Pipeline From Experiment To Report
A simulator run produces a per-run log plus aggregate and breakdown JSON files under the run directory. Diffusion runs add their own per-run log plus diffusion aggregate and boundary summaries. Head-to-head reductions are exported into the generated report directory. Model-lane runs add model artifacts as validation companions rather than scoring inputs.
The analysis/ Python package reads route-visible router artifacts for the
maintained routing report. data.py loads them into Polars frames. scoring.py
derives per-run metrics. tables.py produces CSV tables. plots.py produces
vector plots. sections.py and document.py compose report sections and lay
them out. report.py is the entry that assembles the router PDF.
Report outputs are stable across releases subject to explicit schema versioning. A 3rd party can rely on the artifact shape to build custom reductions or alternate reports without waiting on changes to the included pipeline.
Batman Routing
Two BATMAN routing engines are provided. Each implements the proactive originator-message model over the shared Jacquard world picture.
-
jacquard-batman-classic(engine IDjacquard.batmanc) is a spec-faithful engine. It implements the BATMAN IV originator-message model without structural departures. TQ is carried in the OGM and updated by each re-broadcasting node. No candidate is emitted before receive-window data has accumulated. -
jacquard-batman-bellman(engine IDjacquard.batmanb) replaces the spec’s distributed TQ propagation with a local Bellman-Ford computation over a gossip-merged topology graph. It enriches TQ with Jacquard link beliefs and includes a bootstrap shortcut for tick-1 route availability. This is the engine measured in the tuning corpus.
Both engines declare RouteShapeVisibility::NextHopOnly and the same capability envelope. They are transport-neutral and operate alongside other engines on a shared multi-engine router. The router retains canonical route publication, handle issuance, and lease management. Batman owns proactive originator observations, neighbor ranking, and best-next-hop state within its own crate boundary.
Both BATMAN crates follow the same internal shape. Each projects a small planner snapshot from engine-private runtime state, runs candidate generation and admission against that snapshot, drives refresh and maintenance through pure reducers, and restores active route runtime from the router-owned MaterializedRoute record rather than from an engine-private checkpoint blob. Each crate also implements the shared RoutingEnginePlannerModel surface so the simulator can execute BATMAN planner cases from engine-owned seeds without assembling best-next-hop tables itself.
Shared Inputs
Both engines consume Observation<Configuration> from the shared Jacquard world model. Destination eligibility is checked against ServiceDescriptor before either engine produces a candidate. A destination node must declare support for the engine’s specific ID in its shared service surface before the engine emits a RouteCandidate toward it. See Pathway Routing for the shared planning contract both engines implement.
Classic BATMAN (jacquard.batmanc)
OGM Structure
The classic engine’s originator advertisement carries only the fields required by the spec:
OriginatorAdvertisement {
originator: NodeId,
sequence: u64,
tq: RatioPermille, // path quality from this node to originator; 1000 at source
remaining_hop_limit: u8, // hops remaining; decremented at each relay
}
No per-link state is included. Quality information travels as the tq scalar, which each re-broadcasting node updates before forwarding. Advertisements are framed with the eight-byte magic prefix JQBATMNC and postcard-serialized.
Flooding and TTL
flood_gossip runs each tick. It sends the local originator OGM (tq=1000, remaining_hop_limit=DEFAULT_OGM_HOP_LIMIT=50) to every direct neighbor. It also sends a re-broadcast copy of each learned OGM whose hop-limit has not reached zero.
Before forwarding a learned OGM, the engine computes its path quality to the originator and encodes it in the outgoing advertisement:
rebroadcast_tq = tq_product(link_state_tq_to_sender, received_tq)
rebroadcast_remaining_hop_limit = received_remaining_hop_limit - 1
OGMs with remaining_hop_limit=0 are discarded and not forwarded. This bounds propagation to at most DEFAULT_OGM_HOP_LIMIT relay hops from the originator. Stale OGMs cannot circulate without bound in large meshes.
Internal State Split
jacquard-batman-classic keeps route-choice projection separate from convergence state. The planner snapshot carries only local_node_id, the staleness window, and the current BestNextHop table. The round reducer owns receive-window pruning, echo-window pruning, originator observation derivation, neighbor ranking, and best-next-hop projection. The runtime wrapper is left with transport send, ingress decode, and router integration.
TQ Propagation
TQ degrades multiplicatively as an OGM hops through the network. An originator X sends tq=1000. Each relay node B applies tq_product(link_state_tq_to_sender, received_tq) before re-broadcasting. When node A receives X’s OGM via relay B, it reads B’s reported path quality directly from the received TQ field:
received_tq_via_B = link_B_to_prev × ... × link_Y_to_X × 1000 / 1000^n
A stores received_ogm_info[X][B] with the received TQ and a hop count derived from DEFAULT_OGM_HOP_LIMIT - received_remaining_hop_limit + 1. This data drives A’s local routing decision for X without any local path computation.
This is the classic distributed implicit computation. Each node contributes its local link observation. The flood assembles an end-to-end quality estimate without any node building a full topology graph.
Receive Window and Quality Scoring
A separate receive window is maintained per (originator, via_neighbor) pair. It counts unique sequence numbers received within the staleness window. The window occupancy permille is computed as:
occupancy_permille = received_count / window_span × 1000
This receive quality is applied as a third factor in the local routing decision alongside local_link_tq_to_B and received_tq_from_B, combined via two nested tq_product calls. The receive quality is not encoded in the re-broadcast TQ. Downstream nodes see only the link-state-based path quality in re-broadcast advertisements.
Echo-Only Bidirectionality
A neighbor B is confirmed bidirectional only when a local OGM has been received back via B. bidirectional_neighbor_valid checks the bidirectional_receive_windows table and returns false if no echo has been seen. There is no topology fallback. A neighbor for which no echo has been received does not contribute routing observations regardless of what the shared world model reports about the reverse link.
No Bootstrap
If no receive-window data has accumulated for a (originator, via_neighbor) pair, observation_tq is zero and no routing observation is produced for that path. The engine produces no RouteCandidate on tick 1 for any multi-hop destination. Routes emerge as sequence windows fill. This matches the spec’s behavior.
Enhanced BATMAN (jacquard.batmanb)
OGM Structure
The enhanced engine’s originator advertisement carries full link state rather than a TQ scalar:
OriginatorAdvertisement {
originator: NodeId,
sequence: u64,
links: Vec<AdvertisedLink>, // runtime_state, transport_kind, delivery_confidence
// no tq field, no ttl field
}
This advertisement is sufficient to reconstruct a topology graph. It does not encode a pre-computed path quality. Advertisements are framed with magic JQBATMAN. The absence of TTL means OGMs are flooded verbatim to all neighbors every tick without hop-count bounds.
Gossip Merging and Bellman-Ford
merge_advertisements folds learned advertisements into a copy of the current topology observation. It inserts synthesized Link entries for gossip-discovered edges not already present in the direct view. This produces a merged topology that may include nodes and links beyond the local one-hop view.
refresh_private_state then runs Bellman-Ford on this merged topology to compute (path_tq, hop_count) from each direct neighbor to every reachable originator. When a receive window exists for the (originator, neighbor) pair, the local routing decision uses three factors:
steady_state_tq = tq_product(tq_product(local_link_tq, bellman_ford_path_tq), receive_quality)
When no receive window exists (bootstrap), the decision uses two factors:
bootstrap_tq = tq_product(local_link_tq, bellman_ford_path_tq)
This substitutes a deterministic local computation for the spec’s distributed OGM-propagated TQ. The computation is reproducible from the topology snapshot. The spec’s TQ reflects whatever the neighborhood has recently observed.
Internal State Split
jacquard-batman-bellman uses the same snapshot-and-reducer structure as the classic engine. The planner sees only BatmanBellmanPlannerSnapshot. The pure round reducer owns OGM-window pruning, merged-topology route projection, ranking, and best-next-hop selection.
The pure maintenance reducer owns route-health refresh and replacement decisions. The runtime wrapper owns transport send, ingress decode, and router-facing publication hooks.
TQ Enrichment
derive_tq starts from the same ogm_equivalent_tq(LinkRuntimeState) baseline as the classic engine. When richer Jacquard link beliefs are present, it incorporates up to four additional terms in a running average.
| Enrichment | Normalization |
|---|---|
delivery_confidence_permille | Direct permille value |
symmetry_permille | Direct permille value |
transfer_rate_bytes_per_sec | Normalized against 128 kbps saturation ceiling |
stability_horizon_ms | Normalized against 4000 ms saturation ceiling |
The final TQ is the integer average over all contributing terms. With no beliefs present the result is identical to the classic engine’s baseline. This enrichment has no equivalent in the BATMAN protocol.
Topology Fallback for Bidirectionality
bidirectional_neighbor_valid first checks bidirectional_receive_windows as in the classic engine. If no echo window exists, it falls back to checking whether the shared topology contains a reverse link with usable state. This accelerates route availability on tick 1 before any echoes have been received. It admits routes the spec would withhold until echo confirmation.
Bootstrap Shortcut
In derive_originator_observations, if no receive-window data exists for a specific (originator, via_neighbor) pair, the engine uses the Bellman-Ford path TQ directly as the combined TQ: bootstrap_tq = tq_product(local_link_tq, path_tq). This is a two-factor formula. Once a receive window exists for that pair, the engine switches to the standard three-factor formula: tq = tq_product(tq_product(local_link_tq, path_tq), receive_quality).
The bootstrap check is per-originator-per-neighbor. Receiving an OGM for one originator does not disable bootstrap for other originators that have not yet accumulated window data. On tick 1, before any OGMs have been received, the engine produces routing candidates from topology-derived path quality for all reachable destinations. The spec produces no candidates until receive-window data has accumulated.
Shared Mechanisms
The following mechanisms are identical in both engines.
OGM Receive Window
OgmReceiveWindow tracks received sequence numbers per (originator, via_neighbor) pair using a sliding window of size stale_after_ticks. Occupancy is computed as received_count / window_span × 1000. Sequences outside the staleness window are pruned. The window becomes empty once the last observed sequence ages out.
Sequence numbers are accepted strictly monotonically. Earlier or equal sequences from the same originator via the same neighbor are discarded.
TQ Product
Both engines use the same compound quality formula:
tq_product(left, right) = (left × right) / 1000
The result is on the same 0–1000 permille scale as the inputs. A path through two links each at 900 TQ yields 810. Multi-hop paths accumulate tq_product in sequence, producing monotonically decreasing quality with hop count. Links with a derived TQ below 700 are classified RouteDegradation::Degraded.
Decay Window
DecayWindow governs observation freshness and refresh cadence. The default marks observations stale after 8 ticks and triggers a refresh within 4 ticks. Both engines accept with_decay_window at construction for tuning.
Neighbor Ranking and BestNextHop
Candidates for each originator are ranked in this order:
receive_qualitydescendingtqdescendingis_bidirectionaldescendingobserved_at_tickdescendinghop_countascendingvia_neighborascending (deterministic tie-break)
The top-ranked entry becomes BestNextHop. It carries the next-hop NodeId, TQ, receive quality, hop count, observation tick, topology epoch, transport kind, degradation status, bidirectionality flag, and a derived BackendRouteId.
Planning, Admission, and Lifecycle
Planning, admission, and route lifecycle use identical logic in both engines. The planner checks the destination’s ServiceDescriptor for the engine-specific ID before emitting any candidate, then validates admission against the projected snapshot rather than against hidden mutable tables.
candidate_routes emits at most one RouteCandidate per reachable destination. admit_route validates the candidate’s BackendRouteId against the current BestNextHop table entry. A stale or superseded reference is inadmissible. materialize_route records an active route and derives health from TQ: HealthScore = tq, PenaltyPoints = 1000 - tq.
maintain_route returns ReplacementRequired when the best next-hop has changed. It returns Failed(LostReachability) when the destination has no table entry or when is_bidirectional is false. Route replacement is the only reconfiguration path. Neither engine implements suffix repair or hold.
Route restore is also shared. On recovery, the router passes the full MaterializedRoute record back into the engine. The BATMAN engine reconstructs its active route entry from the router-owned backend route record and rebuilds any derived forwarding view from the current topology rather than persisting a second engine-local checkpoint source of truth.
Capabilities
Both engines declare the same capability envelope:
| Capability | Value |
|---|---|
max_protection | LinkProtected |
max_connectivity | ConnectedOnly |
repair_support | Unsupported |
hold_support | Unsupported |
decidable_admission | Supported |
quantitative_bounds | ProductiveOnly |
reconfiguration_support | ReplaceOnly |
route_shape_visibility | NextHopOnly |
Spec Compliance
Faithful Mechanisms (Classic)
| Mechanism | Implementation |
|---|---|
| OGM sequence-number freshness gating | Strictly monotonic: OGMs with equal or older sequence are discarded |
| Receive-window occupancy as route quality | occupancy_permille = received / window_span |
| TQ product formula | (left × right) / 1000 |
| TQ propagated via OGM | tq field updated at each relay hop |
| TTL-bounded propagation | DEFAULT_OGM_HOP_LIMIT=50, decremented at each hop |
| Bidirectionality via echo | Echo window required. No topology fallback. |
| Proactive flood | OGMs sent to all direct neighbors each tick |
| Staleness window pruning | Sequences outside window are dropped |
| Next-hop-only route shape | RouteShapeVisibility::NextHopOnly |
| Single best next-hop per destination | Top-ranked entry only |
One minor deviation exists. The re-broadcast TQ uses ogm_equivalent_tq(LinkRuntimeState) as the local quality factor. The strict BATMAN IV spec uses receive-window occupancy as this factor. The local routing decision correctly applies receive-window quality as a third factor. The deviation therefore affects downstream quality estimates in OGMs rather than local route selection.
Enhanced Engine Departures
| Mechanism | Change | Implication |
|---|---|---|
| TQ computation | Local Bellman-Ford on merged topology. No TQ field in OGM. | Path quality is deterministic and reproducible from the topology snapshot rather than reflecting recent neighborhood observation. The computation is closer to OLSR-style local SPF than DV gossip. |
| Link state in OGM | Full AdvertisedLink state per neighbor. No TQ scalar or TTL. | This is equivalent to distributing a topology database via gossip. It enables local path computation with no BATMAN protocol equivalent. |
| TTL | Absent. OGMs propagate without hop-count bounds. | OGMs circulate for as long as advertisements remain within the staleness window. The spec’s propagation depth control is lost. |
| TQ enrichment | Delivery confidence, symmetry, transfer rate, stability averaged into TQ. | TQ reflects richer signal quality than packet counts alone. No BATMAN protocol equivalent. |
| Bidirectionality | Echo window with topology fallback. | Routes are available on tick 1. The engine admits paths the spec would withhold until echo confirmation. |
| Bootstrap | Per-originator-per-neighbor: path TQ used as combined TQ (two factors) when no window exists for that pair. | Routing candidates are produced on tick 1. Receiving OGMs for one destination does not disable bootstrap for others. The spec produces no candidates until receive-window data has accumulated. |
| Full topology reconstruction | merge_advertisements builds a complete adjacency graph. | The implementation behaves closer to a link-state protocol than a pure DV-gossip protocol. Path computation is centralized and explicit rather than implicit in the OGM flood. |
Classic as Babel Comparison
The classic engine is the correct baseline when comparing against Babel (RFC 8966). Babel was designed to address specific weaknesses of classic DV-gossip protocols. These weaknesses are present in the spec-faithful implementation.
Babel addresses three gaps relative to classic BATMAN:
- Asymmetric-link handling. Classic batman’s bidirectionality gate excludes paths with poor incoming links entirely. Babel’s feasibility condition handles asymmetric metrics without excluding those paths.
- Loop freedom. Classic batman relies on sequence-number freshness for loop prevention. Babel’s feasibility condition provides a provable loop-freedom guarantee during transient topology changes.
- Triggered updates. Classic batman floods on a fixed tick schedule. Babel sends triggered updates immediately when a metric changes, reducing recovery latency.
Comparing classic batman against Babel measures what each mechanism contributes independently. Comparing the enhanced engine against Babel conflates the Bellman-Ford and topology-enrichment changes with the DV-gossip differences, making performance attribution unreliable.
Enhanced as OLSRv2 Comparison
The enhanced engine is the correct baseline when comparing against OLSRv2 (jacquard-olsrv2). Both use local shortest-path computation over a topology database distributed by gossip. The primary structural difference is TC messages with MPR election versus OGM flooding.
The enhanced engine also exhibits the partition-recovery weakness that OLSR directly addresses. The receive window used as receive_quality is the same window used to gate bidirectionality. Both indicators require the full window span to recover when a partition clears, which delays route restoration compared to OLSR’s explicit TC-flood response to topology changes.
That comparison therefore measures two distinct questions: the cost of MPR-suppressed flooding overhead versus the enhanced batman’s simpler model, and whether OLSR’s immediate topology-change response produces better recovery behavior under adverse conditions.
Babel Routing
jacquard-babel (engine ID jacquard.babel..) implements the Babel distance-vector routing protocol as described in RFC 8966. It uses bidirectional ETX link cost, additive path metric, and a feasibility distance table for loop-free route selection.
Protocol Overview
Babel is a distance-vector routing protocol designed for wireless mesh networks. Each node originates route updates advertising itself as a destination with metric 0. Relay nodes add their local link cost before re-advertising the best route. Downstream nodes select the path with the lowest total metric.
Three properties distinguish Babel from the batman engines in Jacquard. First, link cost uses bidirectional ETX rather than forward-only TQ. Second, path metric is additive rather than multiplicative. Third, route selection is gated by a feasibility condition that provides loop freedom during transient topology changes.
Shared Inputs
The Babel engine consumes Observation<Configuration> from the shared Jacquard world model. Destination eligibility is checked against ServiceDescriptor before the engine produces a candidate. A destination node must declare support for the engine’s specific ID in its shared service surface before the engine emits a RouteCandidate toward it. See Pathway Routing for the shared planning contract all engines implement.
Update Structure
The Babel update carries four fields:
BabelUpdate {
destination: NodeId,
router_id: NodeId,
seqno: u16,
metric: u16,
}
The originator sets metric=0 and assigns a monotonically increasing seqno. Each relay node adds the local link cost to the metric before re-advertising. The router_id identifies the originator of the route entry. Updates are framed with the eight-byte magic prefix JQBABEL. and postcard-serialized.
No TTL field is present. Propagation depth is controlled by the decay window: stale entries are pruned when observed_at_tick exceeds stale_after_ticks. No hop-count bound is needed because only the selected route per destination is re-advertised, and infeasible routes are rejected by the feasibility condition.
ETX Link Cost
Link cost uses the Expected Transmission Count formula:
cost = 256 * 1_000_000 / (fwd_delivery_permille * rev_delivery_permille)
This captures bidirectional link quality. A perfectly symmetric active link (1000 permille in both directions) yields cost 256. An asymmetric link where the forward direction is good (980 permille) but the reverse is poor (300 permille) yields cost 871. The formula penalizes asymmetric links more heavily than BATMAN’s forward-only TQ because poor reverse delivery means acknowledgments are lost, increasing true retransmission count.
If either direction is absent or faulted (delivery 0), cost equals BABEL_INFINITY (0xFFFF), making the route unusable. This replaces the echo-window bidirectionality gate used by batman-classic. No separate bidirectionality check is needed because asymmetry is encoded directly in the metric.
Additive Metric
Path metric is the sum of link cost and the neighbor’s reported metric:
compound_metric = link_cost + neighbor_metric
If either input equals BABEL_INFINITY, the result is BABEL_INFINITY. Otherwise the sum saturates at BABEL_INFINITY - 1 (0xFFFE). The metric scale runs from 0 (perfect local route) to 0xFFFF (unreachable). Values at or above 0xFFFF are treated as unreachable.
This additive model differs from BATMAN’s multiplicative TQ product. A single bad hop in a multi-hop path raises the total metric by its full link cost. In BATMAN, the same bad hop would reduce the multiplicative product less dramatically relative to other hops. Babel therefore discriminates more strongly against paths with one weak link among otherwise strong links.
Feasibility Distance Table
Every node maintains a per-destination feasibility distance FD[D] stored as a (seqno, metric) pair. A route entry for destination D is feasible if and only if:
seqno_is_newer(entry.seqno, FD[D].seqno)
OR (entry.seqno == FD[D].seqno AND entry.metric < FD[D].metric)
The seqno_is_newer function uses modular arithmetic over u16 as defined in RFC 8966 Section 3.5.1. A seqno is newer if the unsigned distance (candidate - reference) mod 2^16 falls in the range (0, 2^15).
When FD is absent for a destination (never selected, or all routes expired), any finite-metric route is feasible. The feasibility condition prevents routing loops during transient topology changes. It ensures that a node never selects a route whose metric has increased relative to its last feasibly selected route, unless a newer seqno proves that the originator has acknowledged the topology change.
Admission vs Selection
Updates are always admitted to the route table. The feasibility condition gates selection only. This matches RFC 8966: a node stores all received route entries and uses the FC only when choosing which route to select.
FD Update Rules
FD is updated to (seqno, metric) of the selected route only when the selection is feasible. Infeasible fallback selections leave FD unchanged. This preserves the loop-freedom guarantee: the FD ratchet never moves backward.
Infeasible Fallback
When no feasible route exists for a destination, the engine selects the best infeasible route to preserve connectivity. This selection does not update FD. The periodic seqno increment (every 16 ticks) propagates a fresh seqno from the originator.
When that update arrives, it satisfies the feasibility condition through the newer seqno and allows FD to be updated, ending the fallback period. This replaces the explicit SEQREQ mechanism from RFC 8966 with a bounded periodic refresh.
FD Expiry
When all routes to a destination expire from the route table, FD for that destination is removed. The next route learned will be treated as if FD is absent (any finite metric is feasible).
Sequence Number Management
The originator seqno is incremented every SEQNO_REFRESH_STEP ticks, which defaults to 16. This periodic increment serves as the mechanism for resolving infeasible-fallback states across the network. The seqno uses u16 with modular arithmetic and wraps at 2^16.
No explicit seqno request mechanism is implemented. In the full RFC 8966 protocol, a node can send a SEQREQ to the originator asking it to bump its seqno immediately. In the Jacquard tick model, the periodic increment bounds the infeasible-fallback window to at most 16 ticks without requiring asynchronous request handling.
Selected-Route Flooding
Each tick, the engine floods two types of updates to all direct neighbors. The first is the local node’s originated update with the current seqno and metric 0. The second is a re-advertisement of the best selected route per destination. Non-selected routes are not re-broadcast.
This differs from batman-classic, which re-broadcasts all received OGMs. Babel’s selected-route flooding reduces overhead and works in concert with the feasibility condition to provide loop freedom.
Decay Window
DecayWindow governs route entry freshness. The default marks entries stale after 8 ticks and expects the next refresh within 4 ticks. Both parameters are configurable via BabelEngine::with_decay_window. Stale entries are pruned during each refresh pass before route selection.
The decay window is identical in shape to the one used by the batman engines. All distance-vector engines accept with_decay_window at construction for tuning.
Quality Scoring
The engine converts Babel metric to a RatioPermille quality score using a linear mapping. Metric 0 maps to quality 1000 (perfect). Metric values at or above 1024 map to quality 0. Routes with metric at or above 512 are classified as degraded.
Planning, Admission, and Lifecycle
Planning, admission, and route lifecycle follow the shared contract used by all Jacquard engines. The planner checks the destination’s ServiceDescriptor for the Babel engine ID before emitting any candidate.
candidate_routes emits at most one RouteCandidate per reachable destination. admit_route validates the candidate’s BackendRouteId against the current best next-hop table. A stale or superseded reference is inadmissible. materialize_route records an active route and derives health from the quality score.
maintain_route returns ReplacementRequired when the best next-hop has changed. It returns Failed(LostReachability) when the destination has no table entry. Route replacement is the only reconfiguration path. The engine does not implement suffix repair or hold.
The internal design follows the same state-partitioning shape used across the other distance-vector engines. Babel projects a read-only planner snapshot from the best-next-hop table. Candidate generation and admission read that snapshot rather than hidden planner state. The refresh pass and maintenance path both reduce explicit state through pure helper functions before the runtime wrapper applies the result.
Recovery keeps the same shape. The durable fact is the router-owned MaterializedRoute record, not a Babel-private route checkpoint blob. During router-led recovery, Babel reconstructs the active route entry from that record and rebuilds the topology-derived forwarding view from the router’s current topology.
Babel implements the shared RoutingEnginePlannerModel, RoutingEngineRoundModel, RoutingEngineMaintenanceModel, and RoutingEngineRestoreModel traits. The simulator drives Babel planner seeds, explicit round-state reducers, maintenance reducers, and restore checks through that standardized contract, and the crate keeps the seed-to-state translation engine-owned.
Capabilities
The Babel engine declares the same capability envelope as the batman engines:
| Capability | Value |
|---|---|
max_protection | LinkProtected |
max_connectivity | ConnectedOnly |
repair_support | Unsupported |
hold_support | Unsupported |
decidable_admission | Supported |
quantitative_bounds | ProductiveOnly |
reconfiguration_support | ReplaceOnly |
route_shape_visibility | NextHopOnly |
Comparison with Batman Engines
vs Batman-Classic
Batman-classic is the correct comparison baseline for Babel. Both are pure distance-vector gossip protocols without local topology reconstruction. Babel addresses three gaps relative to batman-classic.
- Asymmetric-link handling. Batman-classic’s bidirectionality gate excludes poor-reverse paths entirely. Babel’s ETX cost encodes asymmetric quality as a finite metric.
- Loop freedom. Batman-classic relies on sequence-number freshness. Babel’s feasibility condition provides a formal guarantee.
- Propagation. Batman-classic re-broadcasts all received OGMs. Babel forwards only the selected route.
vs Batman-Bellman
Batman-bellman replaces the spec’s distributed TQ propagation with a local Bellman-Ford computation over a gossip-merged topology graph. Comparing babel against batman-bellman conflates the Bellman-Ford and topology-enrichment changes with the DV-gossip differences, making performance attribution unreliable. For protocol-level comparison, use batman-classic.
OLSRv2 Routing
jacquard-olsrv2 (engine ID jacquard.olsrv2.) implements a deterministic OLSRv2-class proactive link-state engine. It preserves the core OLSRv2 shape: HELLO-driven symmetric-neighbor learning, deterministic MPR election, TC-style topology flooding, and shortest-path next-hop derivation over the learned topology database.
The crate is not a wire-compatible RFC 7181 daemon clone. It is a Jacquard engine that consumes Observation<Configuration>, advances only during router-owned synchronous rounds, and publishes only next-hop route candidates through the shared engine traits.
Engine Overview
The engine owns five pieces of runtime state:
- one-hop neighbor state learned from HELLO exchange
- two-hop reachability learned from symmetric neighbors
- local MPR and MPR-selector sets
- topology tuples learned from TC advertisements
- a derived shortest-path tree and best-next-hop table
The router and host own ingress draining, tick cadence, and time attachment. jacquard-olsrv2 consumes explicit ingress through the shared runtime hook and returns router-visible NextHopOnly candidates.
Internally, the engine splits route choice from protocol progression. OlsrPlannerSnapshot carries the route-choice projection used by planning. A pure round reducer owns HELLO and TC expiry, two-hop reachability derivation, MPR selection, SPF refresh, and best-next-hop projection.
A separate pure maintenance reducer owns route-health refresh and replacement decisions. The runtime wrapper is limited to ingress decode, transport emission, and router-facing integration. jacquard-olsrv2 also implements the shared RoutingEnginePlannerModel surface. The simulator executes OLSRv2 planner checks from engine-owned planner seeds instead of building OLSR control-state fixtures itself.
Jacquard-Specific Simplifications
Jacquard keeps the OLSRv2 surface deterministic and auditable:
- one deterministic decay window controls HELLO and TC freshness
- one deterministic MPR election policy is used for all nodes
- link cost is integer-only and derived from shared link observations
- all sets and maps use canonical ordering with no ambient randomness
- no async protocol loop, no host-driver ownership, and no external RFC interoperability layer
- route publication remains router-owned
The result is an OLSRv2-class baseline for comparative routing work rather than a feature-complete NHDP implementation.
HELLO Semantics
Each round, the engine may originate one HELLO message carrying the local originator ID, a monotonically increasing local HELLO sequence number, the current symmetric-neighbor set, and the current local MPR set.
Inbound HELLO processing updates the one-hop neighbor table and the derived two-hop reachability map. A link is treated as symmetric only when the inbound HELLO confirms the local node inside the neighbor’s symmetric-neighbor set. The shared topology observation constrains whether the underlying link is usable. HELLO state alone does not override a failed transport observation.
HELLO state expires when the engine-local hold window passes. Expiry uses Tick, not wall-clock time.
MPR Election
MPR election is deterministic and local. The input surface is the symmetric one-hop neighbors, two-hop neighbors reachable through those one-hop neighbors, and the integer link metric derived from the shared observation model.
The algorithm chooses a minimal covering relay set for the known two-hop neighbors. Ties break first on lower metric cost, then on canonical node order. The elected set is exported only as engine-local control state plus the local HELLO advertisement. It is not promoted into shared core vocabulary.
TC Flooding
TC advertisements carry the originator ID, a monotonically increasing local TC sequence number, and the advertised-neighbor set selected for flooding.
The engine originates a fresh TC when the advertised-neighbor surface changes or when the local topology state needs refresh. Inbound TC processing accepts only strictly fresher sequence numbers per originator, replaces older topology tuples for that originator, and expires stale tuples by the same tick-based hold window.
Forwarding is constrained by MPR-selector state. A node forwards only when the sender has selected it as an MPR and the TC sequence has not already been forwarded for that originator.
Shortest-Path Computation
The topology database is a deterministic set of directed topology tuples. Shortest-path derivation runs over local symmetric edges, accepted TC tuples, and integer link cost derived from the shared link observation.
The shortest-path tree is recomputed when HELLO or TC ingestion changes the topology database. Best-next-hop derivation collapses the tree into one NodeId next hop per reachable destination. Only destinations that advertise support for jacquard.olsrv2. in the shared service surface are eligible for route candidates.
Capability Envelope
The OLSRv2 engine declares the same conservative next-hop envelope used by the other proactive engines:
| Capability | Value |
|---|---|
max_protection | LinkProtected |
max_connectivity | ConnectedOnly |
repair_support | Unsupported |
hold_support | Unsupported |
decidable_admission | Supported |
quantitative_bounds | ProductiveOnly |
reconfiguration_support | ReplaceOnly |
route_shape_visibility | NextHopOnly |
The engine keeps a full topology graph privately. It does not claim explicit-path visibility at the shared contract boundary.
Route Lifecycle And Maintenance
Planning and admission follow the standard Jacquard route lifecycle. candidate_routes emits next-hop candidates from the projected best-next-hop snapshot. check_candidate validates the candidate against the current snapshot projection. admit_route binds the candidate to router-owned identity. materialize_route installs the active next-hop record.
Maintenance returns Continued while the selected next hop remains valid. It returns ReplacementRequired when the shortest-path table selects a new next hop. It returns Failed(LostReachability) when no route remains.
There is no suffix repair or engine-owned hold mode. Route replacement is the only reconfiguration path.
Restore is router-led. On recovery, the router passes the full MaterializedRoute record back into the engine. jacquard-olsrv2 reconstructs its active route entry from the router-owned backend route record and rebuilds derived SPF state from the current topology and refreshed control tables rather than from an engine-private checkpoint payload.
Comparison Role
jacquard-olsrv2 is the in-tree full-topology proactive baseline. It answers a different question from the batman and Babel engines.
batman-classic and babel are distance-vector gossip baselines. batman-bellman is a topology-enriched BATMAN variant. OLSRv2 is the proactive link-state baseline with explicit topology flooding.
OLSRv2 is the primary comparison point for measuring how much full topology knowledge buys over gossip-style next-hop routing.
Related Pages
Pathway Routing
jacquard-pathway is Jacquard’s first-party routing-engine implementation. It consumes the shared world model from jacquard-core and implements the stable routing boundaries from jacquard-traits. Pathway-only heuristics, runtime caches, and repair state remain inside the pathway crate. Proactive engines such as babel, batman-bellman, batman-classic, and olsrv2 are separate routing-engine crates that do not change pathway’s explicit-path semantics.
Shared Inputs
Pathway planning consumes the shared world picture from jacquard-core. The engine reads Observation<Configuration>, Node, Link, Environment, ServiceDescriptor, and NodeRelayBudget without wrapping or reshaping them.
The pathway engine treats ServiceDescriptor as the shared capability-advertisement plane. Route-capable pathway nodes expose the default Jacquard routing surface, which includes the Discover, Move, and Hold services along with relay headroom, hold capacity, link-quality observations, and coarse environment posture. Pathway does not add a second advertisement protocol or a pathway-global algorithm handshake on top of that surface.
The static PATHWAY_CAPABILITIES envelope is exercised by contract tests. The in-tree pathway crate proves its Repair, Hold, partition-tolerance, decidable-admission, and explicit-route-shape claims against live planner and runtime behavior.
Deterministic Topology Model
DeterministicPathwayTopologyModel is the pathway-owned read-only query surface. It queries shared Configuration objects and then derives pathway-private estimate types such as PathwayPeerEstimate and PathwayNeighborhoodEstimate. Those estimates stay encapsulated in jacquard-pathway so engine-specific scoring does not leak into the shared cross-engine schema.
Peer and neighborhood estimates expose optional score components, so unknown and zero remain distinct without turning those pathway-private components into shared observed facts. The components are clamped to the crate’s HealthScore range so composition stays bounded. Where service validity matters, the topology model receives observed_at_tick explicitly rather than reinterpreting RouteEpoch as time. Pathway uses these estimates directly in candidate ordering and committee selection, so swapping the topology model changes pathway-private route preference and coordination behavior without changing the shared world schema.
Planning and Admission
The pathway engine implements the shared RoutingEnginePlanner contract, which produces candidates in five deterministic steps:
- read the current topology snapshot
- freeze a
PathwaySearchDomainover deterministicNodeIdgraph state for that snapshot - resolve the routing objective into one v13
SearchQuery:SearchQuery::SingleGoalfor one exact destination, orSearchQuery::CandidateSetfor selector-style service and gateway objectives over a deterministic acceptable-destination set - run Telltale’s canonical search machine once for that query under an explicit
SearchExecutionPolicyplus declared fairness bundle - derive deterministic backend references, route ids, costs, and estimates from the selected-result witness and the final authoritative search state, then sort by path metric, pathway-private topology-model preference, and deterministic route key
This algorithm produces a stable candidate ordering across replays. The search metric is integer-only and combines hop count, delivery confidence, loss-derived congestion, symmetry, pathway-private peer and neighborhood estimates, protocol-repeat penalties, protocol-diversity bonuses, and a deferred-delivery bonus when the destination is honestly hold-capable. The shared RouteCost surface then reflects the chosen path’s hop count, confidence, symmetry, congestion, protocol diversity, and deferred-delivery hold reservation without exposing the pathway-private estimate internals that shaped the search.
The direct per-link reliability inputs are delivery_confidence_permille, symmetry_permille, and loss_permille. Pathway turns these into weighted edge penalties during path search. Higher delivery confidence and better symmetry reduce path cost. Higher loss increases it.
These signals are then combined with pathway-private peer and neighborhood bonuses and penalties rather than collapsed into one shared reliability field.
median_rtt_ms is a field on the shared link observation surface. No current observation source populates it, so pathway treats it as absent and does not use it in path scoring.
Deferred-delivery classification is deliberately stricter than capability advertisement alone. A destination only qualifies for retention-biased routing when its Hold service advertisement is currently valid for pathway, the advertised capacity hint reports positive hold_capacity_bytes, and the node state separately reports positive hold_capacity_available_bytes. A stale advertisement, an empty capacity hint, or unknown live capacity is not enough.
Telltale Search Core
The generic search core lives in telltale-search, and Pathway supplies a domain adapter plus route-specific policy on top of it.
telltale-search owns:
- canonical batch extraction and search-machine semantics over weighted graph state
- generalized
SearchQueryhandling, selected-result semantics, and witness publication SearchExecutionPolicy/SearchRunConfigvalidation with explicit fairness assumptions- replay artifacts, canonical observation reconstruction, and observation comparison
- epoch reconfiguration through
EpochReconfigurationRequestwith explicit reseeding policy - theorem-backed exactness and fairness claims for the exposed runtime profiles
Pathway owns:
- topology interpretation and edge-cost policy
- heuristic policy (
Zeroor the current hop-lower-bound heuristic) - objective-to-
SearchQuerymapping forNode,Service, andGatewaydestinations - candidate-path derivation from the final search state, route class, connectivity posture, and route summary
- admission policy, witness generation, and committee handling
- opaque backend-token encoding and cache-miss re-derivation
This split is intentional. Pathway uses the generic search machine as a deterministic planning substrate. The published route semantics remain Pathway-owned.
Inherited Search Features
The Pathway engine inherits several capabilities directly from the v13 Telltale search system:
- canonical batch extraction instead of planner-local frontier bookkeeping
- one objective-scoped
SearchQueryexecution rather than a planner-local loop that rebuilds selector semantics out of repeated single-goal runs - selected-result and witness semantics exported directly by the search runtime
- explicit execution-policy control through
SearchExecutionPolicyandSearchRunConfig - replay artifacts that preserve epoch trace, batch schedule, fairness bundle, and final authoritative state
- explicit epoch reconfiguration with a real reseeding policy, where Pathway uses
PreserveOpenAndIncons
Pathway uses SearchQuery::SingleGoal for exact node destinations and SearchQuery::CandidateSet for service or gateway objectives that select among multiple acceptable destinations. For exact queries, the runtime can also emit the optional path-problem helper surfaces. Candidate-set queries stay on the generic selected-result surface and intentionally do not rely on a distinguished goal anchor.
Pathway exposes only exact run-to-completion profiles to the router. The supported public modes are canonical serial and threaded exact single-lane, both with batch_width = 1, SearchCachingProfile::EphemeralPerStep, and SearchEffortProfile::RunToCompletion.
Budgeted or bounded execution contracts remain part of the generic Telltale runtime surface. Pathway rejects them fail-closed for router-visible planning until it has a Pathway-owned policy for exposing them.
Proof and Assurance Surface
Pathway also inherits proof-oriented guarantees and trace surfaces from the Telltale runtime:
- fail-closed configuration validation before execution, including scheduler profile, batch width, executor compatibility, caching profile, effort profile, and fairness bundle
- explicit determinism and fairness claims tied to the selected scheduler profile rather than hidden host-runtime assumptions
- replay and observation-comparison surfaces that can reconstruct and compare the final observed selected result
- state and artifact traces that expose canonical batches, normalized commits, fairness certificates, epoch transitions, and final authoritative machine state
- theorem-backed exact observable equivalence between canonical serial and threaded exact single-lane execution for the current Pathway domain
These guarantees belong to the Telltale search substrate, not to Pathway’s route policy layer. Pathway relies on them to justify exactness, replayability, and debug visibility at the search boundary while owning topology freezing, route-objective mapping, candidate derivation, and router publication semantics.
Pathway defaults to canonical serial search with batch_width = 1, epsilon = 1.0, SearchCachingProfile::EphemeralPerStep, SearchEffortProfile::RunToCompletion, and the minimum exact fairness bundle required by the generic runtime. ThreadedExactSingleLane is available as an explicit opt-in planner mode. Batched parallel, budgeted, and bounded profiles are not exposed because the weaker fairness or approximation story is not acceptable for default routing behavior.
Admission Contract
Admission and witness generation operate on shared result objects. The pathway engine returns RouteCandidate, RouteAdmissionCheck, RouteAdmission, and RouteWitness values. This keeps pathway interoperable with the common router and layering surfaces. The routing-invariants check in toolkit/checks/rust/routing_invariants.rs enforces the planning rules below.
- if a planning judgment depends on observations, the current topology must be passed explicitly to the planner method that makes that judgment
BackendRouteRefstays opaque at the shared boundary, but in pathway it is a self-contained plan token rather than a cache key- pathway may memoize derived candidates internally, but cache hits and misses must produce the same result for the same topology
- admitted routes carry that opaque backend ref forward so
materialize_routecan decode the selected pathway plan without searching planner cache state - materialization revalidates that decoded plan against the latest observed topology, the shared topology epoch, and the plan validity window before issuing a proof
Pathway route ids are path identities. The stable route id is derived from source, destination, route class, and concrete segment path. Epoch stays in the plan token and proof instead of becoming part of the stable route identity. Pathway-private plan tokens, route-identity bytes, ordering keys, and runtime checkpoints all use the same versioned canonical binary encoding policy so replay, hashing, and checkpoint recovery stay aligned.
Planner cache state is advisory only. candidate_routes populates candidate_cache for reuse by check_candidate and admit_route, but cache misses re-derive the same candidate and admission result from the backend token plus explicit topology. Pathway does not let planner decisions depend on hidden mutable cache state.
The simulator consumes Pathway planning through the shared RoutingEnginePlannerModel contract. jacquard-pathway owns the planner seed and the seed-to-private-state translation, so simulator fixtures describe route objectives and expected outcomes rather than pathway-private search or repair internals.
Engine Middleware
RoutingEngine::engine_tick is the engine-wide progress hook for pathway. The router or host supplies a shared RoutingTickContext, and pathway returns a RoutingTickOutcome that reports whether the tick changed pathway-private state. Inside jacquard-pathway, this hook is the engine-internal middleware loop.
topology observation
-> refresh pathway-private estimates
-> summarize transport ingress
-> update bounded control state
-> clear stale candidate cache
-> checkpoint current pathway runtime state
Each tick ingests the latest topology observation, refreshes the pathway-private estimate caches, summarizes the latest bounded transport observations, and folds that evidence into a bounded control state. That control state carries transport stability, repair pressure, and anti-entropy pressure with deterministic decay. Pathway uses it to tighten route health, escalate repair posture under sustained pressure, make AntiEntropyRequired consume real anti-entropy debt rather than acting as pure bookkeeping, and drive the cooperative route-export, neighbor-advertisement, and anti-entropy protocol exchanges described below. The hook then evicts stale candidate entries and writes the scoped topology-epoch checkpoint.
Discovery enters the pathway engine through the shared world picture: nodes, links, environment, and service advertisements are already merged into Observation<Configuration> before the engine plans. Pathway then derives its route-export, neighbor-advertisement, and anti-entropy choreography payloads from those shared observations plus active shared route objects rather than maintaining a second hidden advertisement schema.
Internal Choreography Surface
Pathway carries a private Telltale choreography layer inside jacquard-pathway. This does not change the shared Jacquard routing contract. Router-facing planning, admission, materialization, maintenance, and tick flow use the shared RoutingEngine trait plus the pathway-owned PathwayRoutingEngine extension seam.
The internal split is:
- planner-local deterministic Rust:
- topology interpretation
- candidate search and ranking
- committee scoring
- route-health derivation
- choreography-backed cooperative protocols:
- forwarding hop
- activation handshake
- bounded suffix repair
- semantic handoff
- hold / replay exchange
- route export exchange
- neighbor advertisement exchange
- anti-entropy exchange
Pathway protocols live inline in the pathway crate as tell! definitions. That keeps the generated protocol/session code adjacent to the Rust host logic that enters those protocols and avoids a second file-based choreography source of truth.
Pathway also keeps one pathway-owned choreography interpreter surface above the shared runtime traits. That interpreter maps protocol-local requests onto the existing Jacquard boundaries:
TransportSenderEffectsfor endpoint-addressed payload sendsRetentionStorefor deferred-delivery payload storageRouteEventLogEffectsfor replay-visible route events- router-owned checkpoint orchestration for persisted pathway-private state
Host-owned ingress draining stops outside pathway itself. The router or bridge drains TransportDriver, converts raw ingress into shared observations, and feeds those observations into pathway through explicit router ingestion before a synchronous round. Inside pathway, those observations enter a bounded pending-ingress queue. A round consumes that queue deterministically and records a host-facing pathway round-progress snapshot that reports whether the round advanced state, waited quietly, or dropped excess ingress fail-closed.
This is intentionally pathway-private. The router should only observe shared route objects, shared tick context, shared round outcome, and shared checkpoint orchestration. It should not depend on pathway-private choreography payloads or generated effect interfaces.
The generated or protocol-local Telltale effect interfaces are not the shared Jacquard effect contract. They stay inside jacquard-pathway as implementation-facing protocol surfaces. Concrete host adapters implement the shared traits from jacquard-traits, and the pathway choreography interpreter translates protocol-local requests onto those stable cross-engine traits instead of replacing them.
At runtime, pathway entry points cross one private guest-runtime layer before touching transport send capability, retention, or route-event logging directly. forward_payload, materialization-side activation, maintenance-side repair and handoff, retained-payload replay, round-side ingress recording, route export, neighbor advertisement, and anti-entropy exchange all enter that pathway-local choreography boundary first.
The guest runtime resolves stable inline protocol metadata for the protocol being entered, fails closed if that metadata is unavailable, and then records small protocol checkpoints keyed by protocol kind plus route or tick session. Recovery does not depend on hidden in-memory sequencing state. Telltale session futures remain confined to choreography modules. The engine and runtime layer itself stays synchronous and driver-free.
Runtime and Repair
Materialization stores a pathway-private active-route object under the router-owned canonical identity. That object contains the explicit PathwayPath, optional CommitteeSelection, and a deterministic ordering key plus four route-private substates:
PathwayForwardingStatefor current owner, owner-relative next-hop cursor, in-flight frames, and last ackPathwayRepairStatefor bounded repair budget and last repair tickPathwayHandoffStatefor the last handoff receipt and handoff tickPathwayRouteAntiEntropyStatefor partition mode, retained objects, and last anti-entropy refresh
Canonical route identity, admission, and lease ownership remain outside this pathway-private runtime object.
Pathway decodes the admitted opaque backend ref during materialization instead of recovering route shape from planner cache state. Token decode alone is not enough. The runtime re-derives the candidate against the latest observed topology and fails closed if the plan epoch, handle epoch, witness epoch, latest topology epoch, or plan validity window do not agree. Materialization itself fails closed until the engine has observed topology through engine_tick, so pathway does not synthesize a pre-observation route health or an empty-world fallback.
Route Health
Route health is derived from the active route’s remaining suffix rather than from engine-global topology presence. Pathway validates the current owner-relative suffix against the latest observed topology and folds first-hop transport observations into that route-local view when available. It publishes ReachabilityState::Unknown when it lacks route-local validation data rather than pretending the route is generically reachable or unreachable.
The runtime route-health calculation combines three signal groups:
| Signal group | Inputs |
|---|---|
| First-hop transport summary | remote-link stability score, remote-link congestion penalty |
| Remaining-suffix topology view | delivery confidence, symmetry, loss-derived congestion penalty |
| Pathway control state | transport stability score, anti-entropy pressure |
As in planning, median_rtt_ms is not part of the published route-health calculation.
Lifecycle and Maintenance
Lifecycle sequencing is explicit and fail-closed. Pathway validates first, builds the next active-route state off to the side, persists the checkpoint, records the route event, and only then publishes the in-memory runtime mutation. If checkpoint or route-event logging fails, the new state is not committed.
Protocol checkpoints follow the same fail-closed rule. Pathway writes or updates the protocol checkpoint through the choreography guest runtime before treating that step as complete, and rollback paths remove route-scoped protocol checkpoints when materialization or teardown does not commit. Those checkpoints carry protocol metadata derived from the live inline protocol modules themselves, including the protocol name, declared roles, and source-path identity, so replay and recovery stay aligned with the live generated protocol surface rather than with a handwritten local label only.
Maintenance is expressed through the shared RouteMaintenanceResult surface. Repair means a bounded local suffix-repair algorithm over the latest observed topology. LinkDegraded and EpochAdvanced attempt to recompute the remaining suffix from the current owner to the final destination, consume one repair step on success, and escalate to typed replacement when no bounded patch is available or the repair budget is exhausted.
The maintenance path follows the same reducer split as the other engines. Pathway first normalizes one maintenance input from the active route, latest topology epoch, trigger, and handoff receipt. A pure transition planner then returns the next route or runtime projection plus ordered effect requests such as repair exchange, retained-payload flush, handoff exchange, and anti-entropy pressure consumption. The runtime wrapper executes those requested effects fail-closed and only checkpoints or publishes the projected route state after every requested effect succeeds.
CapacityExceeded returns ReplacementRequired without flipping partition mode, since it indicates replacement pressure rather than partition evidence. PartitionDetected enters partition mode and reports the current retained-object count through HoldFallback. PolicyShift performs handoff and AntiEntropyRequired flushes retained payloads to recover. Pathway exposes one current commitment per route, so repair, handoff, and deferred-delivery posture stay inside the route runtime state rather than becoming separate concurrent commitments.
Forwarding
A handoff advances the owner-relative cursor to the remaining suffix under the next owner. Forwarding then succeeds only for the current owner of that suffix.
Old owners fail closed with StaleOwner, exhausted owner-relative paths fail with Invalidated, and malformed admitted plan tokens fail the same way during materialization. Each case maps to a typed RouteMaintenanceResult value rather than a side-channel mutation.
Optional Committee Coordination
Pathway can attach a swappable CommitteeSelector, with DeterministicCommitteeSelector as the optional in-tree implementation. The selector returns Option<CommitteeSelection> rather than assuming a committee always exists. The in-tree selector reads the pathway neighborhood estimate for committee gating and the pathway peer estimate for ranking. Route ordering and local coordination stay on the same topology-model interpretation.
Committee eligibility is stricter than forwarding value alone. A member must be route-capable for pathway, must present a usable shared service surface, and may be disqualified by bounded behavior-history penalties before ranking happens. Selection is diversity-constrained: controller diversity is mandatory, and discovery-scope diversity is enforced when alternatives exist. If discovery-scope diversity would suppress the minimum viable committee, pathway falls back to controller-only diversity rather than silently disabling coordination.
None means no committee applies, and a selector error is not silently downgraded to None. Pathway surfaces a selector error as a typed inadmissible candidate using BackendUnavailable. The result is advisory coordination evidence only and does not replace canonical route admission, route witness, or route lease ownership.
Retention and Storage
The pathway engine uses the shared RetentionStore boundary for deferred-delivery payloads. While a route is in partition mode, forward_payload buffers payloads into the retention store instead of sending them immediately. Pathway then flushes those retained payloads on recovery or before handoff when a next hop becomes available. The typed partition fallback surface is RouteMaintenanceOutcome::HoldFallback, which carries the retained-object count visible on the route at the time the fallback was entered.
Retained payload identity flows through the shared Hashing boundary. Route and runtime checkpoints flow through the shared storage and route-event-log effects. Storage keys and runtime checkpoints are scoped by the local engine identity so multiple local pathway engines can share one backend without overwriting one another.
Pathway supports a scoped checkpoint round-trip for pathway-private route runtime and the latest topology epoch. The route checkpoint is intentionally narrower than the full active-route object: it stores the mutable forwarding, repair, handoff, anti-entropy, and current-epoch substate only. Path shape, committee selection, route cost, route identity, and lifecycle event are rebuilt from the router-owned MaterializedRoute record and the self-contained backend plan token during restore.
The choreography layer adds a second scoped recovery surface. Protocol checkpoints are keyed by protocol kind plus route session or tick session and round-trip through the same storage boundary.
Route recovery uses the reduced route checkpoint plus the router-owned materialized record. Protocol recovery uses the protocol checkpoint catalog. Neither requires ambient hidden state outside engine-owned checkpoint storage and router-owned canonical route truth.
Swappable Trait Surface
The pathway engine exposes its narrow read-only pathway seams as two traits in jacquard-pathway: PathwayTopologyModel and PathwayRoutingEngine. Substituting either one replaces a pathway subcomponent without forking the engine, and the coupling is pathway-specific rather than leaking into jacquard-traits. RetentionStore remains a shared runtime boundary on the neutral effect surface. For host runtime effects beyond these seams, the engine uses the shared TimeEffects, OrderEffects, StorageEffects, RouteEventLogEffects, and Hashing surfaces from jacquard-traits.
Topology Model
#![allow(unused)]
fn main() {
pub trait PathwayTopologyModel {
type PeerEstimate;
type NeighborhoodEstimate;
#[must_use]
fn local_node(&self, local_node_id: &NodeId, configuration: &Configuration) -> Option<Node>;
#[must_use]
fn neighboring_nodes(
&self,
local_node_id: &NodeId,
configuration: &Configuration,
) -> Vec<(NodeId, Node)>;
#[must_use]
fn reachable_endpoints(
&self,
local_node_id: &NodeId,
configuration: &Configuration,
) -> Vec<LinkEndpoint>;
#[must_use]
fn adjacent_links(&self, local_node_id: &NodeId, configuration: &Configuration) -> Vec<Link>;
#[must_use]
fn peer_estimate(
&self,
local_node_id: &NodeId,
peer_node_id: &NodeId,
observed_at_tick: Tick,
configuration: &Configuration,
) -> Option<Self::PeerEstimate>;
#[must_use]
fn neighborhood_estimate(
&self,
local_node_id: &NodeId,
observed_at_tick: Tick,
configuration: &Configuration,
) -> Option<Self::NeighborhoodEstimate>;
}
}
PathwayTopologyModel is read-only. The associated estimate types are the important boundary. If a pathway implementation wants novelty scores, reach estimates, bridge heuristics, or neighborhood flow signals, those stay pathway-owned behind PathwayTopologyModel. They are not promoted into jacquard-core as shared Node, Link, or Environment schema.
Engine Binding
#![allow(unused)]
fn main() {
pub trait PathwayRoutingEngine: RoutingEngine {
type TopologyModel: PathwayTopologyModel;
type Retention: RetentionStore;
fn topology_model(&self) -> &Self::TopologyModel;
fn retention_store(&self) -> &Self::Retention;
}
}
PathwayRoutingEngine binds one concrete topology model and one retention store to a pathway engine instance. It stays narrow on purpose: hosts can inspect the read-only pathway subcomponents without gaining a mutation hook into pathway-private runtime state. Transport send capability and transport ingress ownership are split cleanly: pathway consumes the shared TransportSenderEffects capability, while the host/router owns ingress supervision and delivers explicit observations before each round.
Shared Retention Boundary
#![allow(unused)]
fn main() {
pub trait RetentionStore {
fn retain_payload(
&mut self,
object_id: ContentId<Blake3Digest>,
payload: Vec<u8>,
) -> Result<(), RetentionError>;
fn take_retained_payload(
&mut self,
object_id: &ContentId<Blake3Digest>,
) -> Result<Option<Vec<u8>>, RetentionError>;
fn contains_retained_payload(
&self,
object_id: &ContentId<Blake3Digest>,
) -> Result<bool, RetentionError>;
}
}
RetentionStore is the storage boundary for opaque deferred-delivery payloads during partitions. It stays intentionally narrow so platform-specific persistence can substitute without forcing the rest of the pathway engine to know about it. It is not treated as a pathway-specific trait surface.
Scatter Routing
jacquard-scatter is Jacquard’s bounded deferred-delivery diffusion engine. It does not maintain a topology graph. It does not publish a best next hop. It does not compute an explicit end-to-end path or corridor envelope.
scatter publishes a narrow router claim. The claim states that an objective is supportable somewhere in the current world model. The claim is opaque, partition-tolerant, and hold-capable. After materialization, the engine moves data through engine-private transport packets under the standard RoutingEngine and RouterManagedEngine boundary.
The crate follows the same internal split used across the other in-tree engines. ScatterPlannerSnapshot carries the planner surface. A pure diffusion-planning reducer decides which retained messages should expire, replicate, or hand off.
A separate pure maintenance reducer owns route-health and hold-fallback decisions. The runtime wrapper is left with ingress decode, packet send, and router-facing installation and restore.
See Crate Architecture for the shared ownership and boundary rules that constrain this engine.
Core Model
The in-tree scatter implementation keeps a small deterministic model. The engine retains messages, summarizes peer observations, and tracks per-route progress. It does not assume stable endpoint identity beyond the router objective vocabulary already present in Jacquard.
- payloads carry a stable local message id
- expiry is local and typed through
created_tickplus boundedDurationMs - replication is bounded by hard copy budgets
- forwarding is local and opportunistic
- handoff is preferential rather than ack-driven custody transfer
- published route shape visibility is
Opaque
Policy Surface
ScatterEngineConfig defines the deterministic policy surface for the engine. It keeps the behavior-critical constants named and typed. It avoids anonymous literals in the runtime.
ScatterExpiryPolicyScatterBudgetPolicyScatterRegimeThresholdsScatterDecisionThresholdsScatterTransportPolicyScatterOperationalBounds
These policies cover message lifetime, replication budgets, regime detection, carrier thresholds, contact feasibility, and bounded runtime work.
Route Lifecycle
Planner behavior is conservative. candidate_routes emits at most one candidate for a supportable objective. The router remains the owner of canonical route truth.
- the planner confirms that the destination or service objective is supportable in the current observation
- the router admits an opaque and partition-tolerant
scatterclaim - the runtime materializes a route-local progress surface
- payloads are retained, carried, replicated, or handed off according to local regime and peer score
- maintenance can report hold-fallback viability even when no direct next hop exists
This split keeps route publication router-owned. It lets scatter own its private deferred-delivery mechanics.
Planner and admission run against the explicit planner snapshot rather than against hidden mutable runtime state. Route restore is router-led. On recovery, the router passes the full MaterializedRoute record back into the engine. scatter reconstructs its active route entry from the router-owned backend token and recomputes route progress from retained local messages.
The simulator consumes Scatter planning through the shared RoutingEnginePlannerModel contract. jacquard-scatter owns the planner seed, including the bounded policy surface. Any reduced transport or time scaffolding stays inside the engine crate rather than in simulator-side helpers.
Transport Boundary
scatter follows the standard Jacquard ownership split. The engine consumes explicit TransportObservation. The engine sends only through TransportSenderEffects. The engine does not own async transport streams or assign Tick.
Host bridges own ingress draining and time attachment. Transport choice stays a local contact-feasibility judgment. The implementation keeps that judgment reduced and deterministic. It does not build separate routing models per transport.
Diffusion planning is pure up to the point where packets are actually sent. The reducer emits forward intents as data. The wrapper performs the real TransportSenderEffects calls and then commits the resulting local progress updates.
Contrast With Other Engines
batman-bellman,batman-classic,babel, andolsrv2retain routing control state but do not buffer payloads for deferred deliverypathwaysupports deferred delivery through explicit path and retention boundaries plus full-route searchmercatorcarries forward bounded corridor evidence and limited custody posture rather than general payload custodyscatteris the in-tree opaque deferred-delivery baseline, so payload custody stays local, bounded, and diffusion-oriented
Current Non-Goals
The current engine does not attempt to provide a full DTN control plane. It keeps the surface intentionally narrow.
- topology reconstruction
- stable semantic identity routing beyond Jacquard objectives
- ack-driven authoritative custody transfer
- multipath planning
- distributed time agreement
- remote-clock freshness claims
Mercator Routing Engine
jacquard-mercator is Jacquard’s corridor routing engine for disrupted mesh networks, where connectivity is real but intermittent. It keeps a bounded view of recent network evidence, searches for a route with backup options, repairs around stale paths when it can, and falls back to limited carry-and-forward behavior when no connected route is safe.
Mercator is for regimes where a connected route may be valid for only part of a run. It does not replace router-owned publication. It produces one router-facing route candidate when connected support is good enough, and one bounded custody posture when connected support is not.
See Routing Engines for the shared trait contract and Router Control Plane for canonical route ownership.
Router Boundary
Mercator implements the same RoutingEngine and RouterManagedEngine surfaces as the other in-tree engines. Candidate production reads explicit topology observations plus Mercator’s private evidence graph. Materialization happens only after router admission and router identity allocation.
The engine does not own transport streams, drain ingress, or assign Tick. Host bridges attach time and advance router rounds. Mercator stays below that boundary: it maintains evidence, searches for a corridor, and exposes bounded diagnostics and router analysis snapshots.
Its published route shape is CorridorEnvelope. Internally, a corridor contains one primary realization plus bounded alternates. Externally, the router sees one candidate for one destination, service, or gateway objective.
Evidence Model
Mercator’s core state is a bounded evidence graph. It records link support, reverse-link support, route support, broker pressure, service support, custody opportunities, objective accounting, and disruption markers. Each record type carries a distinct role:
- link and reverse-link support describe whether a path is usable in both directions where that matters
- route support records which candidate routes still have evidence behind them
- broker pressure tracks whether a bridge-like node is becoming too central
- service support maps service objectives to providers
- custody opportunities describe possible store-carry-forward handoffs
Every record uses Jacquard’s typed time and ordering: Tick, DurationMs, OrderStamp, and RouteEpoch. Scores are integer-ranked. Pruning is deterministic and uses score first, then canonical identity.
The graph is bounded by configuration. Mercator caps neighbors, brokers, service evidence, alternates, and custody opportunities. Evidence expires by policy and can be withdrawn immediately when a disruption epoch invalidates it.
Candidate Publication
Mercator searches for a corridor instead of a single brittle path. The planner expands from local evidence, uses maintained topology evidence when available, and reserves search effort for underserved objectives so one high-demand objective does not monopolize planning.
A selected corridor yields one router-facing RouteCandidate. Alternates remain private. This lets Mercator repair small topology changes without publishing multiple canonical routes for the same objective.
Admission stays conservative. A candidate must satisfy the objective, pass freshness checks, clear reachability confidence thresholds, and respect broker-pressure limits.
Stale Safety
Mercator treats staleness as a normal operating condition, not an exceptional failure. Route support moves through private states: Fresh, Suspect, Repairing, Withdrawn, and CustodyOnly.
A disruption epoch invalidates dependent support immediately. Repair can reuse only corridor alternates whose support survives the current epoch. If no support survives, Mercator withdraws connected route support instead of continuing to publish an obsolete route as usable.
The stale metrics count active but unusable route support after disruption. Pre-disruption losses are not charged as post-disruption stale persistence.
Custody Fallback
When a connected route is not supportable, Mercator can enter bounded custody posture. Custody mode does not publish a connected route. It retains payload evidence only through the shared retention boundary and forwards only to carriers with strict deterministic improvement.
The custody policy uses copy budgets, protected bridge budget, same-cluster suppression, low-gain suppression, energy pressure, and leakage-risk checks. These controls keep disconnected delivery bounded by construction rather than relying on best-effort flooding.
This posture is closest to Scatter Routing, but Mercator uses it as a fallback beneath a route-visible corridor engine. Scatter remains the opaque deferred-delivery baseline.
Diagnostics
Mercator reports selected-result rounds, no-candidate attempts, inadmissible attempts, support withdrawals, stale persistence, repair attempts, recovery rounds, objective service, broker concentration, broker switching, and custody pressure.
Custody diagnostics include retained records, reproduction count, copy-budget use, protected-bridge use, transmission count, storage bytes, energy units, leakage risk, and suppression counts.
The simulator consumes these diagnostics in route-visible summaries, routing-fitness families, diffusion families, report tables, and recommendation scoring. In those reports, route-visible rows measure connected-route publication, while diffusion rows measure the bounded custody behavior.
Simulator Usage
The reference client exposes ClientBuilder::mercator for a single-engine client. ClientBuilder::all_engines also registers Mercator with the maintained mixed-engine set.
The simulator exposes a mercator engine lane and includes Mercator in local route-visible, head-to-head, routing-fitness, large-population, and diffusion suites. The tuning_matrix binary writes Mercator rows into aggregate, breakdown, comparison, routing-fitness, and diffusion artifacts where those families apply.
See Simulator Architecture for host bridge ownership during runs and Experimental Methodology for how maintained suites use fixed engine operating points.
Comparisons
Mercator shares explicit search goals with Pathway Routing. It differs by retaining bounded alternates and treating stale repair and weakest-flow service as first-class diagnostics.
Mercator publishes corridor envelopes while using a smaller bounded evidence graph, stale-safe repair state, and bounded custody posture.
Mercator shares custody pressure concerns with Scatter Routing. It differs by remaining route-visible whenever connected corridor support exists.
See Crate Architecture for the dependency and ownership rules that keep these engine-private mechanisms out of shared route truth.
Reference Client
jacquard-reference-client is the host-bridge composition that wires the router, engines, transport driver, and profile crates into a single runnable host. It is the canonical example of how a Jacquard client is assembled. Integration tests and the simulator use it as the default host. Downstream consumers can use it as a library with stock components or as a starting point for their own composition.
See Profile Implementations for the profile-boundary spec. See Client Assembly for a library-consumer walkthrough.
What The Reference Client Provides
ClientBuilder is the wiring entry point. It attaches one bridge-owned InMemoryTransport driver to a SharedInMemoryNetwork, constructs queue-backed sender capabilities for each enabled engine, registers the engine set on a fresh MultiEngineRouter, and returns a ReferenceClient host bridge.
The builder accepts any combination of in-tree engines: pathway, batman-bellman, batman-classic, babel, olsrv2, scatter, and mercator. The EngineKind enum names the selectable engines. Multiple clients built against the same network share one deterministic carrier. Each client advances routing state through its own explicit bridge rounds.
The bridge surface exposes HostBridge and BoundHostBridge for binding and round advancement. Round outcomes flow through BridgeRoundReport and BridgeWaitState, with BridgeQueueConfig controlling ingress queueing behavior. Together these let a caller drive synchronous rounds, inspect per-round outcomes, and observe the bridge’s waiting behavior.
Ownership Boundaries
The bridge owns three responsibilities the consumer must not bypass. It owns the transport driver, so engines never hold onto async I/O directly. It owns ingress draining, which converts raw transport input into the shared observation surface. It owns Tick stamping, which attaches Jacquard logical time at the ingress boundary.
Engines retain their private runtime state under the shared router contract. The router owns canonical route truth, handle issuance, and lease management. The reference client wires these pieces together but does not mutate canonical truth on their behalf.
Profile types flow through unchanged. NodeProfile, NodeState, ServiceDescriptor, Link, LinkEndpoint, and LinkState keep their shared-model shape end to end. The reference client only composes them into a runnable bridge.
Reference Tests
The reference client’s test suite is the canonical living example of host composition. The tests at crates/reference-client/tests/ exercise client builder options, pathway-on-shared-network flows, batman-pathway handoff, olsrv2 handoff, and shared scenarios from the testkit. They serve as executable documentation for how the builder and bridge fit together.
Shared scenario helpers live at crates/testkit/src/reference_client_scenarios.rs. A 3rd party composing their own scenarios can mirror the pattern there rather than reimplementing the builder plumbing.
Running Simulations
This guide is for Rust developers who depend on jacquard-simulator as a library and want to script their own deterministic routing scenarios. We assume developers may use the simulator for three main purposes: running an existing preset to observe engine behavior, authoring a custom scenario to probe a specific condition, and driving an experiment suite to sweep parameters across a scenario family. This guide covers the first two, plus the shared tools for inspecting and asserting on replay results.
See Simulator Architecture for the architecture this guide sits on top of, Reference Client for the host composition the default adapter uses, Running Experiments for the parameter sweep flow, and Crate Architecture for the ownership and boundary rules.
Adding the Dependency
jacquard-simulator tracks the workspace version 0.8.0. Add it alongside the core and trait crates a consumer typically imports types from.
[dependencies]
jacquard-simulator = "0.8.0"
jacquard-core = "0.8.0"
jacquard-traits = "0.8.0"
Building topology and profile observations also requires jacquard-mem-node-profile and jacquard-mem-link-profile. These crates provide the NodePreset, NodeIdentity, and LinkPreset builders documented in Profile Implementations.
Running a Preset Scenario
The fastest path to a running simulation is to pair a preset from jacquard_simulator::presets with the default ReferenceClientAdapter.
#![allow(unused)]
fn main() {
use jacquard_simulator::{
presets, JacquardSimulator, ReducedReplayView,
ReferenceClientAdapter, ScenarioAssertions,
};
use jacquard_traits::RoutingSimulator;
let (scenario, environment) = presets::pathway_line();
let mut simulator = JacquardSimulator::new(ReferenceClientAdapter);
let (replay, stats) = simulator
.run_scenario(&scenario, &environment)
.expect("run pathway scenario");
let reduced = ReducedReplayView::from_replay(&replay);
assert!(stats.executed_round_count > 0);
}
presets::pathway_line returns a three-node line topology, three Pathway hosts, and a scripted environment that applies degradation, intrinsic limits, partition, and mobility relink hooks across seven rounds. run_scenario advances the bridge round by round and returns a full replay artifact plus a compact stats record. ReducedReplayView::from_replay projects the replay into the analysis-facing surface used by assertions and post-run tooling.
The preset set also includes single-engine lines for every in-tree engine, mixed-engine variants such as all_engines_line and mixed_line, regression fixtures, and composition fixtures. See crates/simulator/src/presets/ for the full index.
Building a Custom Scenario
A scenario takes four pieces. It needs an initial topology observation, an ordered host roster, an ordered list of bound objectives, and a round limit. Topology comes from the mem profile crates. The other three are scenario-level constructs.
#![allow(unused)]
fn main() {
use jacquard_core::{NodeId, OperatingMode, SimulationSeed};
use jacquard_simulator::{BoundObjective, HostSpec, JacquardScenario};
let scenario = JacquardScenario::new(
"custom-pair",
SimulationSeed(42),
OperatingMode::FieldPartitionTolerant,
topology,
vec![
HostSpec::pathway(NodeId([1; 32])),
HostSpec::pathway(NodeId([2; 32])),
],
vec![BoundObjective::new(NodeId([1; 32]), move_objective(NodeId([2; 32])))],
8,
)
.with_checkpoint_interval(2);
}
JacquardScenario::new consumes the positional arguments above. The with_checkpoint_interval builder enables deterministic mid-run checkpoints used by resume_replay and by cross-run replay fidelity checks. The move_objective helper in the snippet is application code that populates a RoutingObjective struct. See crates/simulator/src/presets/common.rs for the connected_objective and service_objective patterns the in-tree presets use.
Additional builders cover less common shapes. with_initial_configuration and with_round_limit override their positional counterparts after construction. with_topology_lags models asymmetric observation delays per host. with_broker_nodes marks a subset of nodes as explicit brokers.
Scheduling Environment Changes
ScriptedEnvironmentModel::new accepts a list of ScheduledEnvironmentHook values, each pairing a Tick with an EnvironmentHook. Hooks fire deterministically at their scheduled tick and are recorded into the replay as AppliedEnvironmentHook artifacts.
#![allow(unused)]
fn main() {
use jacquard_core::{NodeId, RatioPermille, Tick};
use jacquard_simulator::{EnvironmentHook, ScheduledEnvironmentHook,
ScriptedEnvironmentModel};
let environment = ScriptedEnvironmentModel::new(vec![
ScheduledEnvironmentHook::new(Tick(3), EnvironmentHook::MediumDegradation {
left: NodeId([1; 32]),
right: NodeId([2; 32]),
confidence: RatioPermille(800),
loss: RatioPermille(150),
}),
ScheduledEnvironmentHook::new(Tick(5), EnvironmentHook::Partition {
left: NodeId([1; 32]),
right: NodeId([2; 32]),
}),
]);
}
The EnvironmentHook variants cover the bulk of practical perturbations. ReplaceTopology swaps the full configuration at a given tick. MediumDegradation and AsymmetricDegradation lower link quality either symmetrically or per direction.
Partition and CascadePartition remove one or several directed links. MobilityRelink simulates node movement by redirecting a link onto a new peer. IntrinsicLimit enforces per-node connection and hold-capacity ceilings.
Selecting Engines Per Host
Each HostSpec carries an EngineLane that names the engine or engine set that host runs. The scenario constructor accepts a mixed roster. The reference client adapter composes a router plus the selected engine crates per host.
#![allow(unused)]
fn main() {
use jacquard_simulator::HostSpec;
let roster = vec![
HostSpec::pathway(owner),
HostSpec::pathway_and_batman_bellman(relay),
HostSpec::batman_bellman(edge),
];
}
Each constructor returns a HostSpec with a sensible default overrides bundle. Single-engine constructors include pathway, batman_bellman, batman_classic, babel, olsrv2, scatter, and mercator. Multi-engine constructors include the maintained pairwise composites plus all_engines. Per-host knobs apply through .with_profile, .with_policy_inputs, .with_batman_bellman_decay_window, and similar builders.
Inspecting Replay Artifacts
JacquardReplayArtifact carries the full per-round record. It holds the scenario, the scripted environment, ordered round artifacts, route events, driver status events, failure summaries, and optional checkpoints. Each round artifact exposes the topology snapshot for that round, the applied environment hooks, and one host round artifact per host.
#![allow(unused)]
fn main() {
for round in &replay.rounds {
for host in &round.host_rounds {
for route in &host.active_routes {
println!(
"{:?} -> {:?} via {:?}",
host.local_node_id, route.destination, route.next_hop_node_id,
);
}
}
}
}
For analysis work, convert the full replay into the reduced surface through ReducedReplayView::from_replay(&replay). To trade detail for throughput at capture time, call run_scenario_with_capture with SimulationCaptureLevel::FullReplay, ReducedReplay, or SummaryOnly. Summary-only runs produce JacquardSimulationStats without materializing per-round artifacts.
Running Through The External Facade
External experiment crates should prefer the public facade when they want a
stable import story rather than direct access to the maintained in-tree tuning
catalog. The facade is intentionally small: SimulatorConfig selects capture
behavior, ExperimentSuiteSpec names a custom suite, RouteVisibleRunSpec
binds a scenario/environment pair to a run id, ExperimentRunner executes the
suite, and ArtifactSink decides whether artifacts are written.
#![allow(unused)]
fn main() {
use jacquard_core::SimulationSeed;
use jacquard_simulator::{
ArtifactSink, ExperimentRunner, ExperimentSuiteSpec, RouteVisibleRunSpec,
};
let suite = ExperimentSuiteSpec::route_visible(
"custom-route-visible",
vec![RouteVisibleRunSpec::new(
"custom-route-visible-seed-41",
"custom-family",
"batman-bellman",
SimulationSeed(41),
scenario,
environment,
)],
);
let artifacts = ExperimentRunner::default()
.run_route_visible_suite(
&suite,
&ArtifactSink::directory("artifacts/custom-route-visible"),
)
.expect("run custom route-visible suite");
assert_eq!(artifacts.manifest.run_count, 1);
}
The directory sink writes external_manifest.json and external_runs.jsonl.
These files are for downstream harnesses and smoke
checks; the maintained analysis/ report still consumes the standard
tuning_matrix artifact layout described in
Running Experiments. Use
ArtifactSink::disabled() when a library consumer wants an in-memory
determinism check without file output or Python report dependencies.
All facade validation happens before execution. Duplicate run ids, duplicate
(family, engine, seed) tuples, empty ids, and unknown engine ids fail with a
deterministic error. The default engine registry covers the route-visible
engines used by the maintained report: batman-classic, batman-bellman,
babel, olsrv2, scatter, mercator, pathway, and
pathway-batman-bellman.
Asserting Expectations
ScenarioAssertions is a builder that turns a reduced replay into a pass or fail outcome. Each .expect_* method accumulates one rule. The .evaluate(&reduced) call runs them all.
#![allow(unused)]
fn main() {
use jacquard_core::DestinationId;
use jacquard_pathway::PATHWAY_ENGINE_ID;
use jacquard_simulator::ScenarioAssertions;
ScenarioAssertions::new()
.expect_route_materialized(owner, DestinationId::Node(target))
.expect_engine_selected(owner, DestinationId::Node(target), &PATHWAY_ENGINE_ID)
.expect_distinct_engine_count(1)
.evaluate(&reduced)
.expect("pathway assertions");
}
The available rules are expect_route_materialized, expect_route_absent, expect_engine_selected, expect_distinct_engine_count, and expect_recovery_within_rounds. Failures surface as an AssertionFailure carrying a structured detail string, ready to bubble through Result chains or to drive integration-test diagnostics.
Resuming From A Checkpoint
Enabling checkpoints on the scenario through with_checkpoint_interval(n) tells the simulator to snapshot host state every n rounds. Given a completed replay, resume_replay reconstructs the bridge at the last checkpoint and advances again through the remaining rounds.
#![allow(unused)]
fn main() {
let mut simulator = JacquardSimulator::new(ReferenceClientAdapter);
let (replay, _) = simulator.run_scenario(&scenario, &environment)?;
let (resumed, _) = simulator.resume_replay(&replay)?;
assert_eq!(
replay.rounds.last().map(|r| &r.topology),
resumed.rounds.last().map(|r| &r.topology),
);
}
Resume is the primary determinism check. Identical replay tails across the original and resumed runs confirm that nothing in the composed engine state leaked ambient time or host-dependent ordering. The crates/simulator/tests/phase0_determinism.rs suite uses this pattern across every preset.
Swapping the Host Adapter
The default ReferenceClientAdapter wires the reference client host into the simulator. Implement JacquardHostAdapter when the scenario needs a different host composition, for example a host that carries a custom transport or a different engine set than the reference client exposes.
#![allow(unused)]
fn main() {
use jacquard_core::NodeId;
use jacquard_reference_client::ReferenceClient;
use jacquard_simulator::{JacquardHostAdapter, JacquardScenario, SimulationError};
use std::collections::BTreeMap;
struct MyAdapter;
impl JacquardHostAdapter for MyAdapter {
fn build_hosts(
&self,
scenario: &JacquardScenario,
) -> Result<BTreeMap<NodeId, ReferenceClient>, SimulationError> {
todo!()
}
}
let mut simulator = JacquardSimulator::new(MyAdapter);
}
Pass the custom adapter to JacquardSimulator::new instead of ReferenceClientAdapter. The rest of the scenario flow is identical. The canonical in-tree host wiring examples live under crates/reference-client/tests/.
Going Further
For parameter sweeps across scenario families, see Running Experiments. That guide covers both the in-tree tuning_matrix binary and custom suite assembly.
For composing the reference client outside the simulator harness, use crates/reference-client/tests/ as the canonical wiring reference and Profile Implementations for profile vocabulary.
For the boundary rules the simulator works within, see Crate Architecture. For the internal dual-lane architecture, see Simulator Architecture.
Running Experiments
This guide covers two kinds of work. The first half shows how to invoke the in-tree experiment suites through the tuning_matrix binary, where to find the artifacts, how to regenerate the report, and what the current corpus says about each engine. The second half shows how to assemble a custom suite programmatically when the in-tree families do not cover a 3rd party’s analytical question.
See Experimental Methodology for what an experiment is, why the tuning phase runs first, and which variables are independent versus dependent. See Simulator Architecture for the harness the suites sit on, Reference Client for the host composition, and Running Simulations for the base scenario flow each experiment builds on.
Running The In-Tree Suites
Run the smaller smoke sweep:
cargo run --bin tuning_matrix -- smoke
Smoke runs a single seed across every family and completes quickly enough for a pre-PR sanity pass. Use it to confirm the binary builds and the families produce non-empty artifacts.
Run the full local sweep and generate the report:
cargo run --bin tuning_matrix -- local
Local runs the full maintained seed set across every family, including diffusion and head-to-head corpora. The run writes artifacts to artifacts/analysis/local/{timestamp}/ and updates the artifacts/analysis/local/latest symlink. Expect a long-running job on modest hardware.
The matrix caps worker concurrency by default to avoid exhausting memory on developer machines. Override the cap explicitly when needed:
cargo run --bin tuning_matrix -- local --jobs 1
JACQUARD_TUNING_JOBS=2 cargo run --bin tuning_matrix -- local
Both the --jobs flag and the environment variable apply to the same concurrency pool.
Regenerate the report without rerunning the simulator:
nix develop --command python3 -m analysis.report artifacts/analysis/local/latest
The route-visible router report lands at
artifacts/analysis/{suite}/latest/router-tuning-report.pdf.
Engine Takeaways From The Current Corpus
Each paragraph below summarizes what the present corpus says about one engine. Treat them as observations rather than normative recommendations. Rerun the matrix and reread the report after any meaningful engine, router, or simulator change before updating defaults.
BATMAN Bellman
BATMAN Bellman separates most clearly in recoverable transition families. Route-presence plateaus alone are too flat, so the report also weighs stability accumulation, first-loss timing, and failure boundaries. The responsive range clusters around the short-window settings. batman-bellman-1-1 leads the balanced default ranking, with batman-bellman-2-1 and batman-bellman-3-1 close behind. Asymmetric bridge breakdown regimes remain hard failures across the tested window range.
BATMAN Classic
BATMAN Classic converges more slowly than BATMAN Bellman due to its echo-only bidirectionality and lack of bootstrap shortcut. The tested decay window settings cluster tightly. The recommendation reflects the spec-faithful model’s need for larger windows to allow receive-window accumulation.
Babel
Babel separates most clearly in the asymmetry-cost-penalty family, where the bidirectional ETX formula produces measurably different route selection. The partition-feasibility-recovery family shows the FD table’s bounded infeasible-fallback window. Decay window settings do not yet separate sharply, suggesting the FD table and seqno refresh interval dominate convergence timing.
Pathway
The Pathway matrix shows a clear minimum-budget boundary. Budget 1 remains the hard cliff in the maintained service-pressure families. Budgets at and above 2 form the viable floor. pathway-4-zero and pathway-4-hop-lower-bound lead the balanced default ranking.
The practical interpretation is that 2 is the minimum viable budget floor, 3 to 4 is the sensible default range, and larger budgets need a regime-specific justification.
Mercator
Mercator is now represented on both the route-visible and diffusion surfaces. In route-visible head-to-head runs, it behaves as a corridor-maintenance engine: strong in connected high-loss and corridor-continuity families, viable but visibly constrained by bridge-transition and stale-recovery windows. In diffusion runs, its custody fallback is bounded rather than flood-like; the current corpus shows it can keep the broadcast overload and large congestion-threshold families viable after the protected bridge budget tuning.
Mixed Comparison
The comparison regimes inform regime suitability rather than global winners. Low-loss connected-only cases favor the distance-vector stacks. Concurrent mixed workloads favor Pathway. High-loss bridge and corridor-continuity cases remain hard failure regimes across several tested stacks.
Head-To-Head Engine Sets
The head-to-head corpus runs the same regimes under explicit single-engine stacks: batman-bellman, batman-classic, babel, olsrv2, scatter, mercator, pathway, or pathway-batman-bellman. This is separate from the mixed-engine comparison, which asks which engine a router selects when several are available. Head-to-head asks what happens when only one is available.
Assembling A Custom Suite
A 3rd party who needs an experiment the in-tree corpus does not cover should
assemble it through the public runner facade. The maintained tuning corpus is
available under jacquard_simulator::builtin_suites, while custom suites use
ExperimentSuiteSpec, RouteVisibleRunSpec, ExperimentRunner, and
ArtifactSink.
#![allow(unused)]
fn main() {
use jacquard_core::SimulationSeed;
use jacquard_simulator::{
ArtifactSink, ExperimentRunner, ExperimentSuiteSpec, RouteVisibleRunSpec,
};
let suite = ExperimentSuiteSpec::route_visible(
"custom-connected-line",
vec![RouteVisibleRunSpec::new(
"custom-connected-line-seed-41",
"custom-connected-line-family",
"batman-bellman",
SimulationSeed(41),
scenario,
environment,
)],
);
let artifacts = ExperimentRunner::default()
.run_route_visible_suite(
&suite,
&ArtifactSink::directory("artifacts/custom-connected-line"),
)
.expect("run custom suite");
}
The call returns a RouteVisibleArtifacts handle with an in-memory manifest and
per-run summaries. With a directory sink, it writes external_manifest.json and
external_runs.jsonl. With ArtifactSink::disabled(), it executes without
writing files and without invoking Python.
The custom facade is deliberately separate from the maintained report writer. Use it for downstream experiments, minimal consumer tests, and extraction preparation. Use the built-in tuning suites when the output must feed the current router report without an adapter.
Catalog Extensibility Limits
The in-tree family catalog is intentionally separate from external suite
assembly. Built-in local, smoke, staged, comparison, head-to-head, diffusion,
and model-lane suites live behind builtin_suites and remain the source of the
standard tuning_matrix corpus. External suites do not import those modules
unless they intentionally want the maintained Jacquard corpus.
There is no crates/simulator/EXTERNAL_API.md. The external usage contract is documented here and in Running Simulations, so a downstream developer can work from the 500-series guides without chasing a crate-local API note.
Diffusion Suites
Diffusion suites use the same runner facade and the standard diffusion artifact
writer. A downstream crate builds a DiffusionSuite from
CustomDiffusionRunSpec values. Each run provides a
CustomDiffusionScenarioSpec, a DiffusionPolicyConfig, and an explicit seed.
#![allow(unused)]
fn main() {
use jacquard_simulator::{
ArtifactSink, CustomDiffusionRunSpec, DiffusionSuite, ExperimentRunner,
};
let suite = DiffusionSuite::from_custom_runs(
"custom-diffusion",
vec![CustomDiffusionRunSpec {
family_id: "external-diffusion-family".to_string(),
seed: 41,
policy,
scenario,
}],
)
.expect("valid diffusion suite");
let artifacts = ExperimentRunner::default()
.run_diffusion_suite(&suite, &ArtifactSink::directory("artifacts/custom-diffusion"))
.expect("run diffusion suite");
assert_eq!(artifacts.manifest.run_count, 1);
}
The writer emits diffusion_manifest.json, diffusion_runs.jsonl,
diffusion_aggregates.json, and diffusion_boundaries.json. The manifest
contains schema_version: 1. Existing analysis/ plots depend on this
standard diffusion layout, so extraction work must preserve these files or
provide a compatibility writer.
External diffusion families whose id starts with external- use a deterministic
default contact model. Maintained Jacquard families continue to use their
family-specific contact probabilities.
Artifact Compatibility
The route-visible report path consumes the standard in-tree files:
manifest.json, runs.jsonl, aggregates.json, breakdowns.json, optional
model_artifacts.jsonl, and the diffusion files listed above. Both
manifest.json and diffusion_manifest.json carry explicit schema versions.
Schema changes must be additive or accompanied by a compatibility writer before
the analysis/ report can move.
The external route-visible facade writes external_manifest.json and
external_runs.jsonl; those files are intentionally not consumed by the
current analysis/ report. They are for downstream harnesses, consumer tests,
and future extraction work.
The former paper-facing research artifacts now live in DualTide. Jacquard’s
simulator keeps the route-visible and diffusion artifact files consumed by the
maintained analysis/ report, including the multi-router comparison corpus.
Moving or pruning research code must not remove or rename artifacts required by
analysis/.
Review Guidance
When reviewing report output, prefer the PDF report and CSV tables over any single composite score. Use the transition table to distinguish robust settings from lucky averages. Use the boundary table to see where an engine stops being acceptable. Rerun the matrix after meaningful engine, router, or simulator changes before updating defaults.
Client Assembly
This guide covers using jacquard-reference-client as a library in a downstream application, and wrapping it in a small binary for standalone deployment. It also covers the two composition-time swaps a 3rd party is most likely to reach for: a custom PolicyEngine and a custom CommitteeSelector.
See Reference Client for the implementation spec this guide builds on. See Profile Implementations for the profile boundary and Crate Architecture for the workspace ownership rules.
Adding The Dependency
jacquard-reference-client tracks the workspace version 0.8.0. Add it alongside the core and trait crates a consumer typically imports types from.
[dependencies]
jacquard-reference-client = "0.8.0"
jacquard-core = "0.8.0"
jacquard-traits = "0.8.0"
The reference client re-exports node and link profile types from jacquard-mem-node-profile and jacquard-mem-link-profile, so most consumers do not need direct dependencies on those crates.
The reference client is a std host composition. Embedded integrations use the portable crates directly with default features disabled: jacquard-core, jacquard-traits, jacquard-host-support, jacquard-cast-support, jacquard-router, and jacquard-mercator. That path keeps round advancement and Tick stamping in the bridge while an executor-owned adapter, such as an Embassy LoRa task, owns radio I/O.
Building A Client As A Library
ClientBuilder exposes one constructor per engine choice. Call the constructor for the lane needed, apply optional overrides through the with_* builder methods, then call build to produce a ReferenceClient.
#![allow(unused)]
fn main() {
use jacquard_core::{Observation, Configuration, NodeId, Tick};
use jacquard_reference_client::{ClientBuilder, SharedInMemoryNetwork};
let network = SharedInMemoryNetwork::default();
let client = ClientBuilder::pathway(
NodeId([1; 32]),
topology,
network.clone(),
Tick(0),
)
.with_queue_config(Default::default())
.build()
.expect("build pathway client");
}
The single-engine constructors are pathway, batman_bellman, batman_classic, babel, olsrv2, scatter, and mercator. The multi-engine constructor all_engines registers every in-tree engine on one client. Per-engine parameter overrides flow through with_batman_bellman_decay_window, with_babel_decay_window, with_pathway_search_config, and similar builders.
The built ReferenceClient bundles a router, the configured engines, and the host bridge. Bind it once with .bind(), then advance synchronous rounds. Every round drains ingress, runs engine and router logic, and flushes the outbound queue through the bridge-owned transport driver.
Outbound transport sends carry explicit delivery intent. Existing point-to-point transports can continue using send_transport, which is treated as unicast. Transports with physical fanout, such as BLE GATT notification, should use TransportDeliverySupport and router delivery admission helpers so a multicast or broadcast carrier is not exposed as a node-targeted unicast link. After admission, the bridge flushes the resulting TransportDeliveryIntent to the driver without reinterpreting the mode.
Jacquard carries delivery-intent vocabulary, support shaping, effect-boundary plumbing, and admission helpers. The built-in engines produce ordinary node routes. They do not perform full multicast or broadcast route activation. Downstream integrations such as BLE profiles can depend on the vocabulary to model fanout honestly without assuming engine-level cast routing is complete.
Running A Client As A Binary
A minimal binary wraps the library. The main function constructs the builder, binds the bridge, and drives rounds on a cadence the application chooses.
use jacquard_core::Tick;
use jacquard_reference_client::{ClientBuilder, SharedInMemoryNetwork};
use std::thread::sleep;
use std::time::Duration;
fn main() -> anyhow::Result<()> {
let network = SharedInMemoryNetwork::default();
let mut client = ClientBuilder::pathway(node_id, topology, network, Tick(0))
.build()?;
let mut bound = client.bind();
loop {
bound.advance_round()?;
sleep(Duration::from_millis(100));
}
}
The cadence is application policy. A standalone node drives rounds on a wall-clock cadence. A test harness drives rounds as fast as the scenario allows. A batch integration advances rounds whenever new ingress arrives.
The bridge owns transport ingress and Tick stamping. A binary wrapper must not call the transport driver directly or advance Tick inside engine code. External transports must implement the shared effect traits described in Custom Transport. A binary that wires a non-default transport plugs it into the builder at construction time rather than patching it in later.
Customizing The Routing Policy
PolicyEngine converts a RoutingObjective plus local inputs into SelectedRoutingParameters. The default reference client uses a neutral policy appropriate for the enabled engine set. A consumer overrides protection, connectivity, or mode decisions by implementing the trait and passing the instance during construction.
#![allow(unused)]
fn main() {
use jacquard_core::{RoutingObjective, RoutingPolicyInputs, SelectedRoutingParameters};
use jacquard_traits::PolicyEngine;
struct StrictProtectionPolicy;
impl PolicyEngine for StrictProtectionPolicy {
fn compute_profile(
&self,
objective: &RoutingObjective,
inputs: &RoutingPolicyInputs,
) -> SelectedRoutingParameters {
// derive a profile that raises the protection floor
todo!()
}
}
}
The custom policy replaces the default during composition. The routing pipeline consults it once per route activation, so the trait stays stateless from the router’s perspective. Policies that need per-activation state carry that state inside the trait implementor.
Swapping The Committee Selector
CommitteeSelector chooses the committee for an objective that requires coordinated membership. Pathway consumes it through CommitteeCoordinatedEngine. A custom selector returns Option<CommitteeSelection> or None when no committee applies.
#![allow(unused)]
fn main() {
use jacquard_core::{CommitteeSelection, Observation, RoutingObjective, SelectedRoutingParameters};
use jacquard_traits::CommitteeSelector;
struct LocalityBiasedSelector;
impl CommitteeSelector for LocalityBiasedSelector {
type TopologyView = jacquard_core::Configuration;
fn select_committee(
&self,
objective: &RoutingObjective,
profile: &SelectedRoutingParameters,
topology: &Observation<Self::TopologyView>,
) -> Result<Option<CommitteeSelection>, jacquard_core::RouteError> {
todo!()
}
}
}
The selector attaches to the pathway engine through the composition API. For the pathway-specific committee coordination contract, see Pathway Routing. Engines that do not use committee coordination ignore the selector.
Going Further
For building a custom engine that plugs into this same ClientBuilder, see Custom Engine. For replacing the default in-memory transport, see Custom Transport. For authoring a new device profile, see Custom Device. For a capstone example that threads all three together, see Bringing It Together.
Custom Engine
This guide walks through implementing a custom routing engine from scratch and registering it with the router. The worked example is a minimal no-op engine that declares opaque route visibility and always reports lost reachability. It is short and unhelpful as a routing algorithm, but it exercises every trait a real engine must implement.
See Routing Engines for the engine contract spec. For the in-tree engines that demonstrate specific patterns, see Pathway Routing, Batman Routing, Babel Routing, OLSRv2 Routing, and Scatter Routing.
Required Traits
Every routing engine implements three traits from jacquard-traits. RoutingEnginePlanner is the pure surface: identity, capability advertisement, candidate enumeration, and admission check. RoutingEngine extends the planner with the effectful surface: materialization, maintenance, and teardown. RouterManagedEngine adds the hooks the generic router middleware needs for forwarding, restore, and ingress.
The model-trait family is optional. RoutingEnginePlannerModel, RoutingEngineRoundModel, RoutingEngineMaintenanceModel, and RoutingEngineRestoreModel let the simulator drive engine-owned pure reducers. A first pass can skip the model traits and implement them later when the engine integrates with experiment suites.
Define the engine struct and any private state first. The no-op example carries nothing beyond its identity and a handful of constants.
#![allow(unused)]
fn main() {
use jacquard_core::{NodeId, RoutingEngineId};
pub const OPAQUE_NOOP_ENGINE_ID: RoutingEngineId =
RoutingEngineId::from_contract_bytes(*b"jacquard.opnoop.");
pub struct OpaqueNoopEngine {
local_node_id: NodeId,
}
impl OpaqueNoopEngine {
pub fn new(local_node_id: NodeId) -> Self {
Self { local_node_id }
}
}
}
RoutingEngineId is a 16-byte identifier that distinguishes one engine from another in shared observation and service-descriptor surfaces. Keep it stable across releases so network peers can resolve the engine by id without a compatibility shim.
Declaring Identity And Capabilities
The planner surface starts with identity and capability advertisement. Engines declare what they can do so the router can filter candidates and skip engines that cannot satisfy a given objective.
#![allow(unused)]
fn main() {
use jacquard_core::{
RouteProtectionClass, RouteShapeVisibility, RouteRepairClass, RoutePartitionClass,
RoutingEngineCapabilities,
};
impl OpaqueNoopEngine {
fn capabilities_decl() -> RoutingEngineCapabilities {
RoutingEngineCapabilities {
max_protection: RouteProtectionClass::LinkProtected,
max_connectivity: RoutePartitionClass::ConnectedOnly,
repair_support: RouteRepairClass::Unsupported,
hold_support: RoutePartitionClass::Unsupported,
route_shape_visibility: RouteShapeVisibility::Opaque,
}
}
}
}
RouteShapeVisibility signals what shape of route the engine publishes. Pathway uses ExplicitPath, Mercator uses CorridorEnvelope, the distance-vector engines use NextHopOnly, and scatter uses Opaque. Choose the weakest shape the engine actually provides. Routers will not promise callers a richer shape than the engine advertises.
Capability choices cascade into admission. An engine that advertises Unsupported for repair_support will be skipped for repair-bearing objectives.
Minimum Planner Path
The planner surface takes a routing objective, a profile, and a topology observation. It returns candidate routes and admission decisions. The no-op example returns empty candidates, which is valid and leaves admission unreachable.
#![allow(unused)]
fn main() {
use jacquard_core::{
Configuration, Observation, RouteAdmission, RouteAdmissionCheck, RouteCandidate, RouteError,
RouteSelectionError, RoutingObjective, SelectedRoutingParameters,
};
use jacquard_traits::RoutingEnginePlanner;
impl RoutingEnginePlanner for OpaqueNoopEngine {
fn engine_id(&self) -> jacquard_core::RoutingEngineId { OPAQUE_NOOP_ENGINE_ID }
fn capabilities(&self) -> RoutingEngineCapabilities { Self::capabilities_decl() }
fn candidate_routes(
&self,
_objective: &RoutingObjective,
_profile: &SelectedRoutingParameters,
_topology: &Observation<Configuration>,
) -> Vec<RouteCandidate> {
Vec::new()
}
fn check_candidate(
&self,
_objective: &RoutingObjective,
_profile: &SelectedRoutingParameters,
_candidate: &RouteCandidate,
_topology: &Observation<Configuration>,
) -> Result<RouteAdmissionCheck, RouteError> {
Err(RouteError::Selection(RouteSelectionError::NoCandidate))
}
}
}
A real engine produces non-empty candidates from the current topology, tags each with a backend reference, and decides admission based on the engine-private assessment of that candidate. See Routing Engines for the planner contract rules, including the invariant that admission judgments must come from the current topology rather than hidden planner cache state.
Materialization And Maintenance
The effectful surface completes the engine. Materialization installs runtime state under the router-owned canonical identity. Maintenance reports route health each round and drives replacement decisions. Teardown releases engine-private state when the router retires a route.
use jacquard_core::{
MaterializedRoute, PublishedRouteRecord, RouteCommitment, RouteId, RouteInstallation,
RouteLifecycleEvent, RouteMaintenanceFailure, RouteMaintenanceOutcome,
RouteMaintenanceResult, RouteMaintenanceTrigger, RouteMaterializationInput,
RouteRuntimeState,
};
use jacquard_traits::RoutingEngine;
impl RoutingEngine for OpaqueNoopEngine {
fn materialize_route(
&mut self,
_input: RouteMaterializationInput,
) -> Result<RouteInstallation, RouteError> {
Err(RouteError::Selection(RouteSelectionError::NoCandidate))
}
fn route_commitments(&self, _route: &MaterializedRoute) -> Vec<RouteCommitment> {
Vec::new()
}
fn maintain_route(
&mut self,
_identity: &PublishedRouteRecord,
_runtime: &mut RouteRuntimeState,
_trigger: RouteMaintenanceTrigger,
) -> Result<RouteMaintenanceResult, RouteError> {
Ok(RouteMaintenanceResult {
event: RouteLifecycleEvent::Expired,
outcome: RouteMaintenanceOutcome::Failed(RouteMaintenanceFailure::LostReachability),
})
}
fn teardown(&mut self, _route_id: &RouteId) {}
}
A real engine materializes route-private runtime under the router’s canonical identity and returns a RouteInstallation that describes what it realized. Maintenance returns a typed RouteMaintenanceOutcome that drives router behavior: Continued and Repaired preserve the route, ReplacementRequired triggers reselection, HandedOff transfers the lease, and Failed surfaces the failure variant.
The RouterManagedEngine trait fills in the remaining router-side hooks. Implement local_node_id_for_router, forward_payload_for_router, and the restore methods. Default implementations apply for the ingress-observation hook when the engine does not consume transport observations directly.
Registering With The Router
An engine enters a live routing stack through either MultiEngineRouter::register_engine (manual composition) or through the engine-specific ClientBuilder entry point (reference client composition).
#![allow(unused)]
fn main() {
use jacquard_router::MultiEngineRouter;
// Manual composition: construct the router with the host's effects and
// policy, then register the engine on it.
let mut router = MultiEngineRouter::new(/* policy, effects, ... */);
router.register_engine(Box::new(OpaqueNoopEngine::new(local_node_id)))?;
// Reference-client composition expects the in-tree engine constructors.
// A custom engine either lives inside a downstream fork of ClientBuilder,
// or the caller composes the router and engines manually and wraps them
// in a custom host harness.
}
Nodes advertise engine eligibility in their ServiceDescriptor. A destination that tags the custom engine’s RoutingEngineId becomes eligible for candidate production from that engine. Tagging belongs in node profile authoring; see Profile Implementations.
If the engine ships in its own crate, consider implementing RoutingEnginePlannerModel as well. The simulator drives model-lane fixtures through that trait, which lets the engine participate in the maintained experiment corpus without full-stack wiring. See Simulator Architecture for the model-lane design.
Going Further
For contract enforcement, the toolkit/checks/rust/routing_invariants.rs policy check validates engine crates against the shared rules. Run cargo xtask check routing-invariants during development.
For integration into experiment suites, see Running Experiments. For host composition patterns, see Client Assembly and Reference Client.
Custom Transport
This guide walks through adding a custom link: the byte-carrying transport surface engines send through, plus the link-level profile that wraps it. It targets 3rd parties replacing the in-memory transport with something runtime-specific, for example a TCP, BLE, or raw LoRa P2P carrier.
See Profile Implementations for the shared profile boundary. See Reference Client for the host bridge composition the custom transport plugs into. See Crate Architecture for the ownership rules the transport layer must respect.
What A Transport Owns
A transport in Jacquard has two surfaces. Engines send payloads through TransportSenderEffects during a synchronous round. The host owns ingress and supervision through TransportDriver. The bridge attaches Tick to each ingress event before delivering it to the router.
Engines must not own async I/O directly. They must not poll the transport driver. They must not attach time. A 3rd party implementing a new transport replaces the two trait surfaces and hands the result to the bridge, which keeps those ownership boundaries.
Reuse jacquard-host-support for generic mailbox, peer-directory, or claim-ownership scaffolding when the transport needs those primitives. Its ingress mailbox keeps storage bounded and exposes both generation snapshots and a wakeable changed future, so host bridges can pair it with a native blocking loop or an executor-owned embedded loop. Do not introduce a pathway-specific or engine-specific transport trait. Keep the transport transport-neutral.
Embedded transports use the same ownership split. A LoRa adapter running under Embassy owns the radio task, queues tick-free ingress into the mailbox, wakes the bridge through the portable changed-future surface, and lets the bridge stamp Tick before router ingestion. jacquard-host-support does not depend on Embassy directly; the adapter supplies that executor integration at the edge.
Implementing TransportSenderEffects
TransportSenderEffects is the synchronous send capability the router hands to each engine. An implementation takes a LinkEndpoint and a payload, dispatches it through the runtime-specific carrier, and returns an error on failure.
#![allow(unused)]
fn main() {
use jacquard_core::LinkEndpoint;
use jacquard_traits::{TransportSenderEffects, TransportError};
use std::sync::mpsc::Sender;
pub struct ChannelSender {
outbound: Sender<(LinkEndpoint, Vec<u8>)>,
}
impl TransportSenderEffects for ChannelSender {
fn send_transport(
&mut self,
endpoint: &LinkEndpoint,
payload: &[u8],
) -> Result<(), TransportError> {
self.outbound
.send((endpoint.clone(), payload.to_vec()))
.map_err(|_| TransportError::Unavailable)
}
}
}
The sender runs inside a deterministic round, so the implementation must complete quickly. If the underlying carrier needs async dispatch, buffer the outbound payload here and flush it on the driver side. Do not block the round on runtime I/O.
Implementing TransportDriver
TransportDriver is the host-owned ingress surface. The bridge calls drain_transport_ingress once per round, attaches Tick to every returned event, and feeds the stamped events into the router as observations. The driver also owns lifecycle: shutdown_transport_driver releases runtime resources when the bridge is torn down.
#![allow(unused)]
fn main() {
use jacquard_traits::{TransportDriver, TransportError, TransportIngressEvent};
use std::sync::mpsc::Receiver;
pub struct ChannelDriver {
inbound: Receiver<TransportIngressEvent>,
}
impl TransportDriver for ChannelDriver {
fn drain_transport_ingress(
&mut self,
) -> Result<Vec<TransportIngressEvent>, TransportError> {
let mut batch = Vec::new();
while let Ok(event) = self.inbound.try_recv() {
batch.push(event);
}
Ok(batch)
}
}
}
The driver returns a bounded batch per round, not a stream. Unbounded draining would let one round consume arbitrarily much wall-clock time. A real driver caps the drain to a per-round limit and defers remaining events to the next round.
The bridge attaches Tick to each event before delivery. The driver must not populate a tick field on its own. Handing the driver ownership of time is the primary failure mode for custom transports.
Building A Link Profile
A link profile wraps the transport surfaces with the pieces the shared Link, LinkEndpoint, and LinkState vocabulary needs. jacquard-mem-link-profile is the canonical example. It provides SimulatedLinkProfile, SharedInMemoryNetwork, InMemoryTransport, InMemoryRetentionStore, and InMemoryRuntimeEffects.
TransportKind names the concrete carrier surface in the shared taxonomy. Use TransportKind::LoraP2p for raw LoRa peer-to-peer links because the variant names the link mode that Jacquard routes over, not the whole LoRa radio family. Use EndpointLocator::ScopedBytes { scope: "lora".to_owned(), bytes } for LoRa endpoint identity, with the byte payload defined by the transport-owned profile crate. Other LoRa operating modes remain custom transports until they have a shared, concrete routing surface.
#![allow(unused)]
fn main() {
use jacquard_core::{Link, LinkEndpoint};
use jacquard_traits::{RetentionStore, TransportDriver, TransportSenderEffects};
pub struct ChannelLinkProfile {
sender: ChannelSender,
driver: ChannelDriver,
retention: ChannelRetentionStore,
}
impl ChannelLinkProfile {
pub fn new(/* runtime-specific config */) -> Self {
todo!()
}
pub fn sender(&mut self) -> &mut ChannelSender { &mut self.sender }
pub fn driver(&mut self) -> &mut ChannelDriver { &mut self.driver }
pub fn retention(&mut self) -> &mut ChannelRetentionStore { &mut self.retention }
}
}
The profile does not itself implement the effect traits. It bundles individual implementors and hands them to the bridge when the client is composed. A 3rd party composes the sender, driver, and retention store through whatever builder pattern fits the runtime. The reference client uses one per-engine sender, one shared driver, and one retention store per host.
For the retention store, either reuse InMemoryRetentionStore from jacquard-mem-link-profile or implement RetentionStore against persistent storage. Retention is independent of transport, so a custom transport paired with the in-memory retention store is a reasonable first pass.
Shaping Cast Evidence
Custom transports own physical transport facts. A LoRa profile owns spreading factor, duty cycle, gateway behavior, and acknowledgement limits. A BLE profile owns scan windows and advertising behavior. A satellite profile owns contact schedules. These facts stay in the transport-owned profile crate.
Use jacquard-cast-support when the profile needs to shape those facts into bounded unicast, multicast, or broadcast evidence, or when profile/host integration needs deterministic route-neutral delivery support for an explicit delivery objective. The helper crate sorts receiver sets deterministically, enforces explicit bounds, carries typed freshness and capacity fields, and keeps directional support separate from reverse confirmation. The helper does not implement a transport, endpoint, retry loop, or send driver.
The in-memory profile shows the intended fixture path. jacquard-cast-support derives delivery support from explicit facts and an objective. jacquard-mem-link-profile can then adapt that support into ordinary directed Link observations, while the caller owns endpoint authoring:
#![allow(unused)]
fn main() {
use jacquard_cast_support::{
shape_unicast_delivery_support, CastDeliveryObjective, CastDeliveryPolicy, UnicastEvidence,
};
use jacquard_core::{ByteCount, LinkEndpoint, NodeId, TransportKind};
use jacquard_host_support::opaque_endpoint;
use jacquard_mem_link_profile::{CastLinkObservation, CastLinkPreset};
fn endpoint_for(node: NodeId, payload_bytes_max: ByteCount) -> LinkEndpoint {
// Transport-owned endpoint authoring stays here.
opaque_endpoint(TransportKind::WifiAware, vec![node.0[0]], payload_bytes_max)
}
fn links_from_unicast(evidence: &[UnicastEvidence], sender: NodeId, receiver: NodeId) -> Vec<CastLinkObservation> {
let objective = CastDeliveryObjective::unicast(sender, receiver);
let (support, _report) =
shape_unicast_delivery_support(evidence.iter(), &objective, CastDeliveryPolicy::default());
support
.iter()
.map(|support| CastLinkPreset::from_unicast_support(support, endpoint_for))
.collect()
}
}
jacquard-host-support remains host plumbing. Use it for mailbox, peer-directory, endpoint convenience, and claim-ownership support. Do not put profile evidence logic there.
Composing With A Host Bridge
The composed profile plugs into the reference client’s ClientBuilder or into a custom host bridge. The default builder expects the in-memory network, so a non-default transport requires composing the router and engines directly or forking the builder. See Reference Client for the composition the reference client exposes.
The minimum composition wires three things together. First, a router that owns canonical route publication. Second, one or more engines registered on that router, each holding a queue-backed TransportSenderEffects handle. Third, a host bridge that owns the TransportDriver, drains ingress, stamps Tick, and advances the router through synchronous rounds.
For end-to-end host composition patterns, see Client Assembly and Crate Architecture.
Custom Device
This guide walks through adding a custom node: the device-side profile that advertises capabilities, exposes node state, and emits service descriptors. It targets 3rd parties modeling a device that is not covered by the in-memory node profile, for example a physical BLE peripheral, a constrained IoT endpoint, or a heterogeneous mix of hosts with different service surfaces.
See Profile Implementations for the shared profile boundary. See Custom Transport for the companion link side. See Reference Client for the host bridge composition the custom profile plugs into.
What A Device Profile Owns
A device profile in Jacquard has three outputs. It emits a NodeProfile describing capability advertisement. It emits NodeState describing live state like relay budget and hold capacity. It emits one or more ServiceDescriptor values describing the services the node exposes.
A profile does not plan routes, issue canonical handles, or publish route truth. Those stay on the router and engines. The profile stays observational: it describes what the device is, not what routes it selects.
Reuse the core vocabulary unchanged. Node, NodeProfile, NodeState, Link, LinkEndpoint, LinkState, and ServiceDescriptor keep their shared shape end to end. A custom profile wraps builders around those shared objects rather than introducing a parallel schema.
Identity And Endpoint
NodeIdentity pairs a NodeId with a ControllerId. The node id is the routing identifier other hosts use to reach this device. The controller id is the cryptographic actor that authenticates for that node.
#![allow(unused)]
fn main() {
use jacquard_core::{ByteCount, ControllerId, LinkEndpoint, NodeId, Tick, TransportKind};
use jacquard_mem_node_profile::{NodeIdentity, NodePresetOptions};
let identity = NodeIdentity::new(
NodeId([1; 32]),
ControllerId([1; 32]),
);
let endpoint = jacquard_host_support::opaque_endpoint(
TransportKind::WifiAware,
vec![1],
ByteCount(256),
);
let options = NodePresetOptions::new(identity, endpoint, Tick(1));
}
TransportKind names the carrier the endpoint speaks. Built-in variants cover shared routing surfaces such as BLE, Wi-Fi, TCP relay, QUIC, and raw LoRa P2P. Use TransportKind::Custom when a runtime needs a carrier not already in core. opaque_endpoint keeps the endpoint opaque at the shared boundary; transport-specific endpoint builders belong in transport-owned profile crates.
Observation timing is explicit. Every built object carries observed_at_tick, which is the bridge-stamped time at which the profile observed this state. A profile that loses track of observation timing cannot drive deterministic scenarios.
Profile And Capabilities
A NodePreset wraps the options into a full Node. The in-tree route_capable helper registers a single routing engine. The route_capable_for_engines variant registers several at once.
#![allow(unused)]
fn main() {
use jacquard_mem_node_profile::NodePreset;
use jacquard_pathway::PATHWAY_ENGINE_ID;
let node = NodePreset::route_capable(options, &PATHWAY_ENGINE_ID).build();
}
Engine eligibility is encoded through the node’s service descriptors. Only engines whose RoutingEngineId appears in the node’s service surface are eligible to produce route candidates toward that node. A custom device that implements a custom engine tags the engine id in its service descriptor; see Custom Engine.
For a device profile to advertise multiple services or multi-engine eligibility, compose the descriptors manually through SimulatedServiceDescriptor builders. RouteServiceKind distinguishes Discover, Move, Hold, and other service classes.
Node State
NodeState carries live per-node state. This includes relay budget, hold capacity, and current resource pressure. The profile refreshes node state on each observation tick.
#![allow(unused)]
fn main() {
use jacquard_core::{ByteCount, Tick};
use jacquard_mem_node_profile::NodeStateSnapshot;
let state = NodeStateSnapshot::route_capable(Tick(1))
.with_hold_capacity(ByteCount(4096))
.build();
}
A device that conserves resources sets conservative budgets. A device with slack exposes higher budgets. Engines observing the state use it as input to admission decisions. An engine that sees hold_capacity_available_bytes = 0 will not treat the node as hold-capable for deferred delivery.
State changes are observations, not mutations. The profile emits a fresh snapshot each tick the state is relevant. It does not mutate a previously emitted snapshot in place. This keeps the observation flow consistent with the shared Observation<T> surface described in Core Types.
Integration
A full device profile composes the pieces into an Observation<Configuration> that the host bridge ingests. The observation carries the node map, the link map, and the environment. Custom profiles typically build this by combining the node and link outputs with the environment observed at the same tick.
The composed observation plugs into ClientBuilder through the topology argument. Every constructor accepts Observation<Configuration>, so a custom profile interoperates with the reference client and the simulator without extra wiring. See Client Assembly for the composition flow.
For provenance qualifiers on the emitted objects, FactSourceClass, OriginAuthenticationClass, and IdentityAssuranceClass let the profile describe where each observation came from and how strongly authenticated it is. See Core Types for the provenance surfaces the profile populates.
Bringing It Together
This is the capstone guide. It shows how the earlier guides compose into an end-to-end custom experiment: a fully custom host running a custom engine over a custom transport and device profile, driven by a custom suite, reduced through a custom report section. Its purpose is to thread the other guides together so a 3rd party can see how the pieces fit.
A fully custom experiment may replace six components: a routing engine, a transport and link profile, a device and node profile, the client composition, the experiment suite definition, and the report pipeline. Each has a dedicated guide. This capstone references them rather than re-explaining them.
Assembling The Client
The client composition pattern is already covered in Client Assembly. Reuse it here with the custom components in place. A ClientBuilder-shaped composition wires the custom engine from Custom Engine, the custom transport from Custom Transport, and the custom device profile from Custom Device into one ReferenceClient equivalent.
The current reference ClientBuilder expects the in-tree engines and the in-memory transport, so a fully custom stack bypasses it. Compose a router, register the custom engine on that router, and wire the custom transport surfaces into a host bridge directly. The minimum host composition is documented in Reference Client.
Driving The Custom Client From The Simulator
Lift the custom stack into the simulator harness by implementing JacquardHostAdapter. The adapter’s build_hosts method returns one host runtime per scenario host. Pass the adapter to JacquardSimulator::new instead of the default ReferenceClientAdapter. The walkthrough for the host adapter lives under Running Simulations in the “Swapping the Host Adapter” section.
#![allow(unused)]
fn main() {
use jacquard_simulator::{JacquardHostAdapter, JacquardScenario, JacquardSimulator, SimulationError};
use jacquard_core::NodeId;
use std::collections::BTreeMap;
struct CustomStackAdapter {
// runtime-specific wiring for the custom engine and transport
}
impl JacquardHostAdapter for CustomStackAdapter {
fn build_hosts(
&self,
scenario: &JacquardScenario,
) -> Result<BTreeMap<NodeId, jacquard_reference_client::ReferenceClient>, SimulationError> {
todo!("construct the custom client per host in the scenario")
}
}
let mut simulator = JacquardSimulator::new(CustomStackAdapter { /* ... */ });
}
The adapter is the only simulator-facing integration point the capstone introduces. Everything else about the simulation flow is identical to Running Simulations: scenarios, environment hooks, replay inspection, and assertions all work unchanged.
Wrapping In An Experiment Suite
Promote the custom scenario into an ExperimentSuite to sweep it across seeds and parameter sets. The suite assembly pattern is already documented in Running Experiments. Reuse it with the custom JacquardHostAdapter instead of the default.
An experiment over a fully custom stack should honor the methodology in Experimental Methodology. If the custom engine exposes tunable parameters, run a tuning sweep for that engine before any comparative run. Hold the resulting operating point fixed in comparative runs so the contrast measures engines, not tuning.
Artifacts land under artifacts/analysis/{suite}/{timestamp}/ like the in-tree suites. The Python report pipeline ingests them unchanged because the artifact shape is stable across releases.
Extending The Report Pipeline
The analysis/ Python package reads simulator artifacts and assembles the PDF report. report.py is the entry. data.py loads per-run and aggregate data into Polars frames. scoring.py derives per-run metrics. tables.py produces CSV tables. plots.py produces vector plots. sections.py composes report sections.
A 3rd party adding a custom metric or plot for a custom experiment touches three places. Add the metric derivation in scoring.py. Add the CSV or plot rendering in tables.py or plots.py. Add the section layout in sections.py and hook it into the report assembly in report.py.
# scoring.py
def custom_engine_stability(run):
return run["my_custom_metric"].mean()
# sections.py
def custom_engine_section(runs):
return [
Paragraph("Custom engine stability by regime"),
custom_engine_stability_table(runs),
]
The call shape and the data frames follow the conventions already used for the in-tree sections. Read the existing implementations of the BATMAN, Babel, or Pathway sections as living templates.
Artifact Schema Stability
The Rust simulator guarantees stable schemas for the per-run JSONL logs plus aggregate, breakdown, diffusion aggregate, and diffusion boundary JSON summaries. Schema changes go through explicit versioning. A 3rd party can rely on the shape across patch releases and plan migrations for minor releases.
Model-lane artifacts are additive and subject to looser guarantees. They serve as a validation companion rather than a scoring input. Consumers who need long-term stability of their custom reductions should target the full-stack artifacts first.
For contract rules the simulator and report pipeline live within, see Crate Architecture. For the methodology these artifacts ultimately support, see Experimental Methodology.
Walkthrough
Each section establishes the vocabulary the next section assumes. Some terms used in Jacquard mean something different from their use in other systems. Those terms are flagged where they appear.
1. What Jacquard Is
Jacquard is a deterministic routing system for ad hoc networks that are unstable, capacity-constrained, and potentially adversarial. Nodes churn, links degrade, identities may be weak, and there is often no reliable global authority. See Introduction for the longer framing.
Jacquard owns the routing contract: a stable interface above the concrete routing algorithm. A host composes one or several engines behind that contract. The in-tree engines are pathway, BATMAN, Babel, OLSRv2, scatter, and Mercator. Multiple engines can be live at once for different traffic without sharing canonical route truth.
Three commitments shape everything else. Determinism rules out floats, ambient randomness, and wall-clock time. Explicit ownership keeps each role’s allowed surface small and named. Explicit certainty levels make every routing input a typed object that records what was seen, what is believed, and what is established.
2. Shared World Model
Jacquard’s shared world model uses three types in increasing order of certainty: Observation<T>, Estimate<T>, and Fact<T>. An observation is a raw input plus source, auth, evidence class, and Tick. An estimate is a belief derived from observations and carries a confidence permille from 0 to 1000. A fact is established routing truth produced by admission or publication.
See Core Types for the full type definitions. The evidence class on each observation discriminates the kind of source. The enum lives at crates/core/src/base/qualifiers.rs.
#![allow(unused)]
fn main() {
pub enum RoutingEvidenceClass {
DirectObservation,
PeerClaim,
AdmissionWitnessed,
}
}
Engines use this class to weight a routing input. A peer claim is information but weaker than a direct observation. An admission witness is the strongest class and is only produced by the router on a successful admission.
3. Typed Time
Every observation carries a tick. Typed time is therefore part of the shared world model, not a transport concern. See Time Model for the definitions of:
Tick(u64): local monotonic round counter used for expiry, replay, and schedulingOrderStamp(u64): total ordering used for content-addressing and tiebreaksRouteEpoch(u64): versions topology reconfigurations so objects from different epochs are not interchangeableDurationMs(u32): bounded duration with no negatives and no overflow
Determinism is the reason for this layer. If routing borrowed from wall-clock or OS scheduling, the simulator could not replay a run. Bugs would not reproduce. Engine behavior could not be reasoned about at the contract level.
4. The Routing Pipeline
The system runs a seven-stage pipeline. The stages and their producer are summarized below.
observation → estimate → fact → candidate → admission → materialization → publication
Stages one through three live in core. Observations arrive at a node through a transport driver, the bridge stamps each one with a Tick, and they land in the shared world model. The world model derives estimates such as link quality and peer liveness. An estimate firms up or is witnessed by admission, at which point it becomes a fact.
Stages four through seven belong to engines and the router. An engine emits a RouteCandidate from current facts, then performs an admission check that satisfies the router’s objective and profile constraints and returns a route-shaped proof. The engine then materializes the admitted route under a router-issued canonical identity. The router publishes the canonical route. The engine produces proofs. The router publishes truth.
5. Roles and Ownership
There are four named roles with explicit ownership boundaries. See Router Control Plane for the full breakdown.
| Role | Owns | Does not own |
|---|---|---|
Engine (RoutingEngine impl) | Candidate production, admission checks, engine-private state | Ingress, time, canonical identity |
Router (jacquard-router) | Canonical handles, leases, publications, round advancement | Any routing algorithm |
Bridge (ReferenceClient and host-specific equivalents) | Ingress draining, Tick stamping, transport-to-router ferrying | Routing decisions |
Driver (TransportDriver impl) | Transport-specific ingress, supervision, capability | Anything above the transport boundary |
The router is a registrar. It grants identity, holds leases, and publishes what engines have proven. It does not know how to find routes.
The engine is a strategy. It consumes the shared world model and emits proof-bearing candidates. It does not know what time it is or where packets come from. This separation is what lets multiple routing algorithms coexist behind one contract.
6. The Seven Engines
Each engine produces candidates from the same shared world model. Engines differ mostly in the shape of route they publish.
| Engine | Family | Publishes |
|---|---|---|
pathway | Explicit-path source routing | ExplicitPath |
batman-bellman | Enhanced BATMAN with local Bellman-Ford over gossip-merged topology | NextHopOnly |
batman-classic | Spec-faithful BATMAN IV with OGM flooding and echo bidirectionality | NextHopOnly |
babel | RFC 8966 distance-vector with ETX and feasibility distance | NextHopOnly |
olsrv2 | Proactive link-state with deterministic MPR election and TC flooding | NextHopOnly |
scatter | Bounded deferred-delivery diffusion | Opaque (a viability claim) |
mercator | Hybrid corridor routing with stale-safe repair and bounded custody fallback | CorridorEnvelope |
Mercator is the only engine that publishes a CorridorEnvelope. The rest of this document explains why that shape is necessary for the regime Mercator addresses.
7. Mercator: Problem and Corridor Shape
Pathway assumes a path exists end-to-end at the time the packet is forwarded. BATMAN, Babel, and OLSRv2 maintain next-hop tables on a generally connected mesh and tolerate local churn. Scatter handles deeply disconnected delivery but publishes only an opaque viability claim with no route shape.
Mercator targets the regime in between. Connectivity to a destination might be good for one window and bad in the next. The engine publishes a useful description of how to reach the destination when connectivity is good and degrades through bounded states when connectivity is poor.
The published route shape is a corridor. The struct lives at crates/mercator/src/corridor.rs.
#![allow(unused)]
fn main() {
pub struct MercatorCorridor {
pub objective: MercatorObjectiveKey,
pub primary: MercatorRouteRealization,
pub alternates: Vec<MercatorRouteRealization>,
pub topology_epoch: RouteEpoch,
}
}
The router only sees a CorridorEnvelope worth of route shape. The alternates are engine-private and let Mercator perform fast repair without republishing a new canonical route every time the primary wobbles. The published object stays stable while the underlying topology churns.
8. Mercator: Support State and Custody
A single objective’s relationship with the network degrades along a named ladder. The enum lives at crates/mercator/src/evidence.rs.
#![allow(unused)]
fn main() {
pub enum MercatorSupportState {
Fresh,
Suspect,
Repairing,
Withdrawn,
CustodyOnly,
}
}
The states replace the implicit flapping that would otherwise occur when every blip triggers a withdraw and re-announce cycle. An objective slides smoothly from confirmed support, through degraded states, to a custody-only fallback in which no connected route is supportable.
The custody fallback is bounded. The relevant config struct lives at crates/mercator/src/public_state.rs.
#![allow(unused)]
fn main() {
pub struct MercatorOperationalBounds {
pub custody_copy_budget_max: u32,
pub custody_protected_bridge_budget: u32,
pub custody_payload_bytes_max: u32,
pub custody_low_gain_floor: u16,
pub custody_energy_pressure_threshold: u16,
pub custody_leakage_risk_threshold: u16,
}
}
The phrase bounded custody posture is a contraction of these budgets. Every custody behavior is gated by a named finite quantity: how many copies of a payload may exist, how many protected bridges may carry it, how large a payload qualifies, how much expected forwarding gain is required before storing, and how much energy or leakage pressure is tolerated. Classical DTN custody is unbounded eventual-delivery semantics. Mercator’s custody is policy-bounded and deterministic.
9. Mercator: Evidence Accumulation
Mercator maintains an internal bounded evidence graph at crates/mercator/src/evidence.rs. The graph records link support, reverse-link support, route support, broker pressure, service support, and custody opportunities. Each record carries Tick, DurationMs, OrderStamp, and RouteEpoch. Pruning is deterministic and uses score first, then canonical identity as tiebreak.
The evidence graph is engine-private. The router does not see it. It is the engine’s digest of the shared world model, organized for corridor planning and repair.
Per the engine description in AGENTS.md, the engine continues to evolve. The corridor shape, evidence model, and support-state names are stable surfaces. The planner is the area still under iteration.
10. Running a Demo
The simplest end-to-end test exercises the full bridge to router to engine to publication loop on a four-node topology for ten rounds. The relevant test is at crates/reference-client/tests/e2e_pathway_shared_network.rs. Run it with:
cargo test -p jacquard-reference-client --test e2e_pathway_shared_network -- --nocapture
Pathway is a useful starting point because it is the simplest engine to read top-to-bottom. The contract being exercised, including observations, candidate production, admission, materialization, and publication, is identical across engines. Once this loop is understood, the Mercator code reads against the same shape.
The simulator can run experiment matrices across all seven engines and emit a PDF report. Generate one with just tuning-local and open artifacts/analysis/<suite>/latest/router-tuning-report.pdf. The report contains per-engine recommendations, transition stability, failure boundaries, and diffusion coverage.
11. Glossary
The following definitions are compact restatements of terms used above.
| Term | Definition |
|---|---|
| admission | An engine’s check that a candidate satisfies a router objective and profile constraints, returning a route-shaped proof. |
| bridge | Host glue. Owns ingress draining and Tick stamping. Reference implementation is ReferenceClient. |
| candidate | A RouteCandidate. An engine’s advisory output before admission. |
| choreography | An engine-private protocol state machine. In pathway, expressed via Telltale session-type macros. |
| corridor | Mercator’s published route shape. A primary realization plus engine-private alternates sharing a topology epoch. |
| custody | Store-carry-forward of a payload when no connected route is supportable. |
| driver | A TransportDriver. Host-owned, transport-specific ingress and supervision. |
| engine | A RoutingEngine implementation. Owns a strategy and engine-private state. |
| epoch | A RouteEpoch. Versions a topology reconfiguration. Objects from different epochs are not interchangeable. |
| estimate | An Estimate<T>. A belief derived from observations with a confidence permille. |
| evidence | A provenance discriminator on routing inputs. |
| evidence class | A RoutingEvidenceClass, one of DirectObservation, PeerClaim, or AdmissionWitnessed. |
| fact | A Fact<T>. Established routing truth. |
| gateway | An objective variant. A destination role distinct from a plain node or service. |
| materialization | An engine realizing an admitted route under router-issued canonical identity. |
| objective | What a route is for. A node, a service, or a gateway. |
| observation | An Observation<T>. A raw input plus provenance and a tick. |
| OrderStamp | A deterministic total ordering used for content-addressing and tiebreaks. |
| pathway | The first-party explicit-path engine. Uses Telltale for choreography. |
| publication | The router announcing a canonical route and its commitments. |
| RouteEpoch | See epoch. |
| router | jacquard-router. Owns canonical identity, leases, publications, and round advancement. |
| support state | Mercator’s per-objective ladder of Fresh, Suspect, Repairing, Withdrawn, and CustodyOnly. |
| Tick | A local monotonic round counter. |
| viability claim | What scatter publishes. An opaque indication that an objective is reachable eventually. Carries no path or next hop. |
Crate Architecture
This page describes the crate layout, the boundary rules, and the implementation policies that keep the workspace consistent.
Boundary Rule
core defines what exists. traits defines what components are allowed to do.
core owns shared identifiers, data types, constants, error types, and the full model pipeline from world objects through observations, engine-neutral estimates, policy, and action. Derives, trivial constructors, and simple validation are allowed. Cross-crate behavioral interfaces belong in traits.
traits owns the cross-crate behavioral interfaces, grouped below by purpose. The layering subset is forward-looking. The shared shape is part of the stable design. In-tree coverage is contract-oriented rather than a mature production layering stack.
Shared transport vocabulary follows the same rule. core keeps a small observed-world transport schema in TransportKind, EndpointLocator, LinkEndpoint, TransportDeliveryIntent, TransportDeliverySupport, and RouteDeliveryObjective because those types appear in shared link, service, send-intent, and admission surfaces. Jacquard intentionally does not force those types fully opaque.
EndpointLocator keeps only the neutral locator families the shared model actually needs. Endpoint identity and delivery intent are separate: a LinkEndpoint names the carrier, while TransportDeliveryIntent says whether an admitted send is unicast, multicast, or broadcast. TransportKind::LoraP2p names raw LoRa peer-to-peer links in the shared taxonomy, and those endpoints use EndpointLocator::ScopedBytes with scope lora. Transport-specific endpoint builders belong in transport-owned profile crates rather than in core or the transport-neutral mem profile crates.
| Category | Traits |
|---|---|
| Routing contract | RoutingEnginePlanner, RoutingEngine, Router, RoutingControlPlane, RoutingDataPlane, PolicyEngine |
| Local coordination | CommitteeSelector, CommitteeCoordinatedEngine |
| Layering | SubstratePlanner, SubstrateRuntime, LayeredRoutingEnginePlanner, LayeredRoutingEngine, LayeringPolicyEngine |
| Runtime effects | TimeEffects, OrderEffects, StorageEffects, RouteEventLogEffects, TransportSenderEffects |
| Host-owned drivers | TransportDriver |
| Hashing and content | Hashing, ContentAddressable, TemplateAddressable |
| Simulator | RoutingScenario, RoutingEnvironmentModel, RoutingSimulator, RoutingReplayView |
Dependency Graph
The workspace contains repo-local policy tooling in jacquard-toolkit-xtask plus the routing crates jacquard-core, jacquard-traits, jacquard-host-support, jacquard-cast-support, jacquard-macros, jacquard-pathway, jacquard-mercator, jacquard-batman-bellman, jacquard-batman-classic, jacquard-babel, jacquard-olsrv2, jacquard-scatter, jacquard-router, jacquard-mem-node-profile, jacquard-mem-link-profile, jacquard-reference-client, jacquard-testkit, and jacquard-simulator. The migrated research engine moved to the sibling DualTide repository.
jacquard-core
↑ ↑
jacquard-traits jacquard-host-support jacquard-cast-support
↑ ↑ ↑
jacquard-mem-node-profile
│
jacquard-mem-link-profile
│
jacquard-pathway ─────────┐
jacquard-mercator ────────┤
jacquard-batman-bellman ──┤
jacquard-batman-classic ──┤
jacquard-babel ───────────┼──→ jacquard-router ←── jacquard-reference-client
jacquard-olsrv2 ──────────┤ │ ↑
jacquard-scatter ─────────┘ └──→ jacquard-simulator
jacquard-testkit provides shared test support (used by simulator and reference-client tests)
jacquard-reference-client composes mem-* + router + in-tree engines
jacquard-simulator reuses reference-client composition rather than a simulator-only stack
jacquard-toolkit-xtask
Every crate depends on jacquard-core. Every crate except jacquard-core depends on jacquard-traits only when they need behavioral boundaries. jacquard-host-support depends only on jacquard-core plus proc-macro and serialization support because it owns reusable mailbox, ownership, endpoint-convenience, and host-side observational projector helpers, not runtime traits or router semantics. jacquard-cast-support depends only on jacquard-core plus serialization support because it owns bounded cast evidence helper shapes, not transport implementations or router semantics. jacquard-router depends on registered engines only through shared traits, not through pathway or BATMAN internals.
jacquard-mem-node-profile depends on jacquard-core and jacquard-host-support plus serialization support. jacquard-mem-link-profile depends on jacquard-core, jacquard-traits, and jacquard-host-support because it implements shared transport, retention, and effect traits while reusing the canonical raw-ingress mailbox. jacquard-core and jacquard-traits remain runtime-free.
Crate Layout
Inside core, files are grouped into three areas. base/ holds cross-cutting primitives: identity, time, qualifiers, constants, and errors. model/ holds the world-to-action pipeline: world objects, observations, estimation, policy, and action. routing/ holds route lifecycle and runtime coordination objects.
core defines result shapes, not policies. It exposes coordination objects like CommitteeSelection, layering objects like SubstrateLease, and route lifecycle objects like RouteHandle, but it does not encode engine-local scoring, committee algorithms, leader requirements, layering decisions, or a parallel authority system above those route objects. Authority flows through the route contracts themselves: admitted routes, witnesses, proofs, leases, and explicit lifecycle transitions.
Purity And Side Effects
Jacquard treats purity and side effects as part of the trait contract.
Puretraits must be deterministic with respect to their inputs. They should not perform I/O, read ambient time, allocate order stamps, or mutate hidden state that changes outputs.Read-onlytraits may inspect owned state or snapshots, but they must not mutate canonical routing truth or perform runtime effects.Effectfultraits may perform I/O or mutate owned runtime state, but only through an explicit boundary with a narrow purpose.
Signature design follows the same split. Use &self for pure and read-only methods. Use &mut self only when the method has explicit state mutation or side effects. Do not mix pure planning and effectful runtime mutation in one trait unless the split is impossible and documented.
That is why Jacquard separates RoutingEnginePlanner from RoutingEngine, SubstratePlanner from SubstrateRuntime, and LayeredRoutingEnginePlanner from LayeredRoutingEngine. Engine-specific read-only seams such as pathway topology access stay in the owning engine crate rather than leaking into jacquard-traits. The shared round lifecycle follows the same rule: router-owned cadence and explicit ingress live at the contract layer, while engine-specific control loops and control-state contents stay inside the owning engine crate.
The same rule applies inside engine crates. Candidate generation and scoring consume an explicit planner snapshot rather than hidden mutable state. Round and maintenance logic run through pure reducers over explicit runtime state plus normalized input when an engine supports those transitions. Checkpoints persist only durable protocol facts, while derived caches are rebuilt during recovery.
jacquard-traits also carries the shared engine-model contract that the simulator consumes. RoutingEnginePlannerModel standardizes planner execution over typed planner snapshots. RoutingEngineRoundModel and RoutingEngineMaintenanceModel standardize pure transition execution where an engine exposes those reducers.
RoutingEngineRestoreModel standardizes route-private runtime reconstruction from router-owned route records. jacquard-simulator depends on that trait family rather than maintaining a separate engine-specific integration API.
Enforcement
Trait purity and routing invariants are enforced by the lint suite. The stable-toolchain check lane is split between the external toolkit runner and Jacquard’s local toolkit/xtask. Nightly compiler-backed coverage lives in the external toolkit lint suite plus toolkit/lints/model_policy and toolkit/lints/routing_invariants. Public trait definitions in jacquard-traits also carry #[purity(...)] or #[effect_trait] annotations that the proc macros validate at compile time.
Runtime Boundary
The routing core does not call platform APIs directly. Hashing, storage, route-event logging, transport send capability, host-owned transport drivers, time, and ordering all cross explicit shared boundaries in traits.
jacquard-host-support sits alongside that boundary, not inside it. Reusable host-side ingress mailboxes, unresolved and resolved peer bookkeeping, claim guards, transport-neutral endpoint conveniences, and host-side topology projectors live there so core stays data-only and traits stays contract-only. The ingress mailbox separates bounded storage from change notification. The default host path uses blocking notification where the target supports it, and the portable path exposes generation snapshots plus a Future wake surface for executor-owned scheduling. The router consumes explicit ingress and advances through synchronous rounds rather than polling transports ambiently. That is how native execution, tests, and simulation share one semantic model.
Portability Profiles
The default workspace profile is a std profile. It supports native host builds, the simulator, the reference client, and in-memory profiles.
The wasm profile is a target compatibility check for selected std crates on wasm32-unknown-unknown. It does not prove that a crate is no_std. A crate can compile for wasm while still using the standard library surface available on that target.
The embedded profile is a no_std plus alloc profile for the deterministic model, route, cast, host-support, and Mercator path needed by MCU transport adapters such as jq-lora. Mercator is the in-tree engine path for this profile. That profile avoids direct thread, blocking wait, wall-clock, filesystem, and host I/O APIs. Platform behavior enters through explicit host or executor adapters.
The portable embedded crate set is:
jacquard-corejacquard-traitsjacquard-cast-supportjacquard-host-supportjacquard-mercatorjacquard-router
Downstream embedded adapters depend on these crates with default features disabled. The profile still assumes alloc; it is not a no-allocation profile.
[dependencies]
jacquard-core = { version = "0.8.0", default-features = false }
jacquard-traits = { version = "0.8.0", default-features = false }
jacquard-host-support = { version = "0.8.0", default-features = false }
jacquard-cast-support = { version = "0.8.0", default-features = false }
jacquard-router = { version = "0.8.0", default-features = false }
jacquard-mercator = { version = "0.8.0", default-features = false }
Embedded verification uses thumbv7em-none-eabihf by default. Run the portable check to validate the crate set on the host with default features disabled and on the embedded target:
just no-std-check
Set the NO_STD_TARGET environment variable to use a different installed target. Run the wasm check separately when validating browser or wasm host compatibility:
just wasm-check
jacquard-cast-support sits alongside profile and host-integration crates as deterministic evidence and delivery support. It is part of the portable no_std plus alloc profile. It normalizes unicast, multicast, and broadcast cast inputs into bounded, ordered helper records, then can derive route-neutral delivery support from those records and an explicit delivery objective. Profiles can map that support into route-visible TransportDeliverySupport without flattening multicast or broadcast into fake unicast links. It leaves transport send/receive, endpoint authoring, retry scheduling, custody storage, and route publication to their owning crates.
Cast objective admission stays above engines. Profiles expose delivery support, router-owned compatibility helpers compare that support with RouteDeliveryObjective, and host effects receive the admitted TransportDeliveryIntent. Broadcast objectives name an explicit BroadcastDomainId plus receiver coverage requirements; there is no implicit default broadcast domain. Engines continue to produce generic route candidates and must not depend on jacquard-cast-support to understand multicast, broadcast, BLE fanout, or endpoint materialization.
The cast surface is integration vocabulary and helper plumbing, not complete multicast or broadcast route activation in the built-in engines. The in-tree engines materialize ordinary routes. A host or downstream profile can use the delivery-support and admission helpers to avoid lying about fanout transports while full cast route activation remains a router/profile integration concern.
The effect traits are narrower than the higher-level component traits. They model runtime capabilities, not whole subsystems. RoutingEngine, Router, and RetentionStore are larger behavioral contracts and should not be forced through the effect layer.
Recovery follows the same ownership split. The router persists canonical MaterializedRoute records. Engines restore route-private runtime through router-managed hooks. When the current topology is needed to rebuild a derived forwarding view, the router provides that topology during recovery rather than forcing the engine to persist the derived view itself.
First-party pathway keeps one additional internal layer above those shared effects: pathway-private choreography effect interfaces generated from Telltale protocols. Those generated interfaces are not promoted into jacquard-traits. Concrete host/runtime adapters implement the shared effect traits, and jacquard-pathway interprets its private choreography requests in terms of those stable shared boundaries.
Within jacquard-pathway itself, the async envelope is narrower still. Telltale session futures are driven to completion only inside choreography modules. The engine/runtime layer owns a bounded explicit ingress queue, consumes it during one synchronous round, and exposes a pathway round-progress snapshot for host-facing inspection. It does not own transport drivers, ambient async callbacks, or executor-shaped advancement.
Invariants
- No crate may use floating-point types in routing logic, routing state, routing policy, or simulator verdicts.
- No crate may treat wall-clock time as distributed semantic truth.
Tickis time andRouteEpochis configuration versioning. Crates must not convert between them by rewrapping the inner integer.- Canonical ordering must flow through shared ordering types. Crates must not invent crate-local tie-break schemes.
- Canonical hashing and content IDs must flow through the shared hash and content-addressing boundaries.
- Transport may observe links and carry bytes, but it must not invent route truth, publish canonical route health, or mutate materialized-route ownership.
- GPS, absolute location, clique grids, and singleton leaders are not shared routing truth. Spatial hints stay engine-private above the shared observation boundary.
- Multiple routing engines may coexist in one host runtime. Generic mixed-engine canonical route ownership is not a base-layer assumption.
Ownership
Each crate owns a narrow slice of runtime state.
| Crate | Owns |
|---|---|
jacquard-core | Shared vocabulary. No live state. |
jacquard-traits | Compile-time boundaries. No runtime state. |
jacquard-macros | Annotation-site validation and syntax-local code generation for effect, handler, and purity attributes. No runtime state. |
jacquard-host-support | Generic host-side ingress mailboxes, peer identity bookkeeping, claim ownership helpers, transport-neutral endpoint conveniences, and host-side observational read models. No route truth, no transport-specific protocol logic, no router actions, no time/order stamping. |
jacquard-cast-support | Deterministic bounded unicast, multicast, and broadcast evidence helper records plus route-neutral delivery support shaping. No transport implementation, endpoint constructors, retry scheduling, route truth, router actions, or time/order stamping. |
jacquard-pathway | Pathway-private forwarding state, topology caches, repair state, retention state, engine-local committee scoring, and the private choreography guest runtime plus its protocol checkpoints. |
jacquard-mercator | Mercator-private evidence graph, corridor planner, stale-safe repair state, weakest-flow accounting, broker-pressure accounting, bounded custody records, and route-visible diagnostics. |
jacquard-batman-bellman | BATMAN Bellman-private originator observations, gossip-merged topology, Bellman-Ford path computation, TQ enrichment, next-hop ranking tables, and active next-hop forwarding records. |
jacquard-batman-classic | BATMAN Classic-private OGM-carried TQ state, receive windows, echo-based bidirectionality tables, learned advertisement state, next-hop ranking tables, and active next-hop forwarding records. |
jacquard-babel | Babel-private route table, feasibility-distance state, additive-metric scoring, seqno management, and active next-hop forwarding records. |
jacquard-olsrv2 | OLSRv2-private HELLO state, symmetric-neighbor and two-hop reachability tables, deterministic MPR state, TC topology tuples, shortest-path derivation, and active next-hop forwarding records. |
jacquard-scatter | Scatter-private retained messages, peer observations, per-route progress, replication and handoff state, and deterministic regime, budget, and transport policy thresholds. |
jacquard-router | Canonical route identity, materialization inputs, leases, handle issuance, top-level route-health publication, delivery objective compatibility, and multi-engine orchestration state. |
jacquard-mem-node-profile | In-memory node capability and node-state modeling only. No routing semantics. |
jacquard-mem-link-profile | In-memory link capability, carrier, retention, route-visible delivery-support fixtures, and runtime-effect adapter state only. No canonical routing truth. |
jacquard-reference-client | Narrow host-side bridge composition of profile implementations, bridge-owned drivers, router, and one or more in-tree engine instances for tests and examples. Observational with respect to canonical route truth, but owner of ingress queueing and round advancement in the reference harness. |
jacquard-testkit | Shared test fixtures and scenario helpers consumed by the simulator and reference-client test suites. No canonical route truth. |
jacquard-simulator | Replay artifacts, scenario traces, post-run analysis, and model-lane orchestration over engine-owned planner, reducer, and restore surfaces. No canonical route truth during a live run. |
A host-owned policy engine above the router may own cross-engine migration policy and substrate selection.
Extensibility
core::Configuration is the shared graph-shaped world object. Engine-specific structure such as topology exports, peer novelty, bridge estimates, planning caches, and forwarding tables belongs in the engine crate behind its trait boundary rather than in core.
The extension surface is split across Core Types, Routing Engines, and Pathway Routing.
For first-party pathway specifically, Telltale stays an internal implementation substrate. Shared crates remain runtime-free. Router integration drives pathway through shared planning, tick, maintenance, and checkpoint orchestration. It must not depend on pathway-private choreography payloads, protocol session keys, or guest-runtime internals.
DualTide is now the canonical home for the migrated research line, paper, and theorem boundary. Jacquard keeps the deterministic routing runtime, simulator, and maintained routing report pipeline.