Skip to content

Route planning and delivery execution

This page follows a letter from Wuxi to Beijing. You will run a routed delivery, see how canwu-routing picks a route from what one observer knows, and see how canwu-transport and canwu-movement record the journey leg by leg, including failures and reroutes.

Your application supplies the network and timetable data. canwu-routing and canwu-transport are plain Rust libraries that compute and check values; your plugins store those values as domain records and change them through canonical ingress. All of their types are re-exported from canwu-api.

The correspondence example plans two deliveries from the same sender, one to a neighbor in Wuxi and one to Beijing:

Terminal window
cargo run -p canwu-correspondence --example routed_correspondence
Wuxi local delivery: 1 leg(s), arrival minute 30
wuxi/hub -> wuxi/delivery/recipient via Horse
Wuxi to Beijing: 2 leg(s), arrival minute 3060
wuxi/hub -> beijing/station via Rail
beijing/station -> beijing/delivery/recipient via Horse

The sender’s network is defined in examples/support/mod.rs. The local letter takes one 30-minute horse leg. For Beijing the router compares a direct train (departs at minute 60, two days), a train through Nanjing (three days), and two final-mile horse legs (two and three hours). It picks the direct train and the two-hour leg: 60 + 2,880 + 120 = minute 3,060. Routed correspondence from Wuxi walks through that example in full. This page explains the planning and execution contracts underneath it.

“How does a letter get from Wuxi to Beijing?” is two questions:

  1. Planning: given the network, timetables, risk, and capacity this observer knows about, which route arrives earliest within the limits?
  2. Execution: has the letter departed, reached a relay, changed hands, been rerouted after a flood, and reached the recipient?

canwu-routing answers the first and canwu-transport records the second. The recipient’s knowledge changes only when the information extension completes the delivery attempt.

Plan from an observer's knowledge, install an itinerary, book capacity, run legs, and reroute after a failure. View diagram source.
View diagram source
flowchart LR
  Snap["PlanningSnapshot<br/>what the observer knows"] --> Plan["plan_route()<br/>RoutePlan"]
  Plan --> Install["TransportExecution<br/>initial ItineraryRevision"]
  Install --> Book["CapacityBooking<br/>(optional)"]
  Book --> Legs["Legs: start, arrive<br/>Handoff at relays"]
  Legs -->|"leg fails"| Replan["ReplanPending"]
  Replan -->|"new snapshot"| Reroute["Successor<br/>ItineraryRevision"]
  Reroute --> Legs
  Legs -->|"final leg arrives"| Done["ArrivalPending<br/>or Settled"]
let request = RoutingRequest {
origin: RoutingNodeRef::new("wuxi/hub"),
destination: RoutingNodeRef::new("beijing/delivery/recipient"),
departure_at: SimTime::EPOCH + SimDuration::hours(1),
policy: RoutingPolicy {
allowed_modes: [TransferMode::Rail, TransferMode::Horse]
.into_iter()
.collect(),
..RoutingPolicy::default()
},
};
let plan = plan_route(&planning_snapshot, &request)?;

A PlanningSnapshot is the network as one observer knows it at one moment. It records the observer, observed_at, knowledge_cut, topology_version, an optional timetable_version, and an optional valid_until expiry. Someone with a current railway timetable and someone who has only heard a courier rumor get different plans, and both plans are valid for what they knew. Two helpers build snapshots:

  • canwu_reference_world::planning_snapshot_from_world builds one from the reference world’s territories and routes.
  • canwu_correspondence::planning_snapshot_from_holder_knowledge builds one from exactly what one holder’s ledger asserts; see Plan from a holder’s knowledge.

The resulting RoutePlan lists legs, each with a mode and planned departure and arrival, plus estimated_arrival_at and a digest. A nearby recipient may need one short leg; Beijing may need several relays, stations, and custody transfers.

Rail, air, telegraph, and relay stations share one algorithm; the differences live in network data:

  • Rail: TransferMode::Rail, station endpoints, and departure slots. A 1900 network and a 1940 network are two content packages with different timetables.
  • Air: TransferMode::Air, airports, and flight slots.
  • Telegraph and other signals: TransferMode::Signal. Office hours, latency, risk, and interception come from data or your own extension’s policy.
  • Courier networks: relay-station endpoints, Horse or Foot connections, and custody handoffs between legs.

Each connection has a TraversalModel: Fixed for a constant duration, Departures for scheduled slots, and Piecewise for durations that change over time. The default algorithm, FifoDijkstraV1, assumes that leaving later never means arriving earlier. When your time-dependent data breaks that rule, set algorithm: RoutingAlgorithm::BoundedLabelCorrectingV1 in the policy.

RoutingPolicy also sets max_expanded_nodes, max_transfers, max_risk_per_mille, and max_arrival_at. Failures such as NoKnownRoute and ExpansionBudgetExceeded are deterministic results, so replay reproduces them.

RoutingCache stores plans keyed by the snapshot digest and the request, including its policy. It is a host-side cache: you can drop it at any time and rebuild it, and replay works without it.

RoutePlan.estimated_arrival_at is an estimate for execution. The delivery attempt’s due_at is the deadline in the information lifecycle. When the letter will arrive late, the system records a failure or creates a new attempt; the original deadline stays as it was.

Rerouting and retrying are different operations:

  • A reroute creates a new immutable ItineraryRevision for the same delivery attempt.
  • A delivery retry creates a new delivery attempt, which may use a new plan and mode.

After planning, the host creates a TransportExecution, installs the plan as its first itinerary revision, and links it to the delivery attempt it must complete:

let mut execution = TransportExecution::new(
TransportExecutionId(7),
Some(delivery_attempt_version.clone()),
);
execution.install_initial_itinerary(initial_revision)?;
execution.begin_saga(
delivery_attempt_version,
delivery_completion_operation_key(
TransportExecutionId(7),
ItineraryRevisionId(1),
1,
),
)?;

delivery_attempt_version is the exact domain-record version (DomainRecordVersionRef) of the delivery attempt, and initial_revision is an ItineraryRevision holding the RoutePlan with reason ItineraryRevisionReason::Initial. begin_saga moves the execution to Executing.

Then each leg runs in order:

  • start_current_leg(at) departs, and complete_current_leg(at, endpoint) arrives. Each leg records its actual times.
  • At a relay, record_handoff records who hands custody to whom before the next leg starts.
  • When a flood, war, bad weather, or a line outage blocks the route, a domain system calls fail_current_leg(reason, at). The execution enters ReplanPending. A new planning snapshot produces a successor revision, installed with reroute(revision, at), and record_handoff records custody passing from the failed leg to the new revision’s first leg. The router only plans; your systems decide that the disaster happened.
  • When the final leg arrives, the execution enters ArrivalPending. completion_request() returns a DeliveryCompletionRequest with a stable operation key and evidence. The host submits it through canonical ingress to complete the delivery attempt in the information extension. Submitting the same key twice completes the attempt once.

For execution 7, itinerary revision 1, and attempt version 1, the key is transport/7/revision/1/delivery-completion/attempt-version/1. An execution without a delivery attempt ends with settle_arrival(at, endpoint) in state Settled.

These records cover common strategy-game movement:

  • MovementSubjectRole::PersonsGroup moves a persons group: several people travelling as one subject, usually identified by an application domain record. Like cargo, it carries a positive quantity, here the head count; requires_quantity() returns true for both roles.
  • HandoffKind::Seizure { by } records a seizure handoff: someone outside the itinerary, such as bandits or a hostile garrison, takes custody. Planned handoffs use HandoffKind::Planned. A seizure handoff follows the same leg rules as a planned handoff.
  • A terminal seizure names the failed current leg as both ends, so custody leaves the itinerary. An execution allows one terminal seizure. After it, the execution accepts no more handoffs, leg failures, reroutes, arrivals, successful settlement, or bookings, and only its owner can close it, as failed or cancelled.
  • ItineraryRevisionReason::ExternalCondition { record, version, kind } explains a reroute by citing an application-owned condition record, such as a flood or border-closure record, at an exact positive version, with an application label in kind. It is validated on the initial itinerary and on every reroute.

Transport records these facts. Whether a seizure or hazard happened, and who had the authority, is decided by your application systems.

canwu-transport holds the records. The movement lifecycle extension canwu-movement runs them through one MovementPlugin: your host sends orders with the tracked command apply_movement_operation_v1, application systems report incidents through movement_incident_v1, and the plugin settles each leg at its due time, allocates capacity pools, and publishes holder-relative reports. The plugin draws no random numbers; your systems decide whether a hazard happens. How it runs on the design page gives the full rules for orders, legs, incidents, bookings, and reports.

Trains, aircraft, relay horses, and telegraph lines can all be booked with CapacityBooking, a persistent record for a time window. Its status is one of Requested, Confirmed, Consumed, Released, Expired, Cancelled, or Failed. A booking can be confirmed, failed, or cancelled before its window opens, and consumed only inside the window.

Movement executions can also draw on capacity pools; see Capacity pools. The correspondence extension currently offers only CorrespondenceCapacityAdmission::Unconstrained, so correspondence legs run without capacity limits.