Skip to content

Grain, production, and military supply

A river-valley granary opens with 4,300 baskets of grain. Each month its manager picks one of four priorities (relief first, force first, balanced, or requisition for the force), and the granary must cover three demands: civilians (320 baskets), relief (120), and a garrison dispatch (1,200). The river route is closed in months 1 and 2, so nothing ships. The only shipment leaves in month 3; its river crossing is closed and it reaches the garrison by road. From month 4 the dispatch demand still claims grain each month but nothing ships, so when it ranks first in a lean month it crowds out civilians. The harvest arrives at the end of month 10.

The question the example answers: how can several domains draw on one stock so that every basket is accounted for, competing claims are settled in a recorded order, and the whole run replays exactly?

All quantities are synthetic values chosen for the example, not estimates of historical population, yields, rations, or prices.

  • The resource simulation extension (canwu-resource): accounts and demands, one allocation pass by priority, and a transfer held in escrow until acceptance.
  • Decision tickets (DecisionTicket): each month’s priority is a ticket option that carries an ordinary command.
  • A transport execution (TransportExecution) whose failed river leg is replaced by a reroute (ItineraryRevision) of the same shipment.
  • The force-supply reference consumer (canwu-force-supply-reference): the garrison eats from its own account by submitting a consumption intent, and the month 5 requisition runs through the full saga.
  • A reference content pack (canwu-economy-reference-content): synthetic_grain_fixture() supplies the garrison’s food requirement and the requisition policy, each bound to a model card. The civilian, relief, and harvest numbers are constants in grain.rs.
Terminal window
cargo run -p canwu-economy-reference --example grain_loop

The program prints a GrainLoopSummary as JSON: one frame per month (836 lines in all), then the totals. The output below is trimmed to part of the month 3 frame and the totals:

{
"frames": [
...
{
"economy": "canwu.economy-reference:economy:river-valley",
"month": 3,
...
"force_requested": 1200,
"force_fulfilled": 1200,
...
"force_operation": "canwu.force-supply-reference:operation:287:89280",
...
},
...
],
"final_stock": 2206,
"final_population_wellbeing_per_mille": 840,
"final_force_readiness_per_mille": 800,
"final_cooperation_per_mille": 859,
"total_harvest": 3966,
"transport_executions": 1,
"closed_route_months": [
1,
2
],
"rerouted_months": [
3
],
"conservation_closing": 2206,
"checkpoint_hash": "2e04345348f3d7b30ced46a653833a9e9be7403edf447badfc218fee1c8044f1"
}

The key fields of all fourteen frames from the same run:

Month Decision Opening stock Civilians (of 320) Relief (of 120) Garrison dispatch (of 1,200) Harvest Wellbeing ‰ Cooperation ‰
1 balanced 4300 320 120 0 0 1000 903
2 relief_first 3860 320 120 0 0 1000 906
3 balanced 3420 320 120 1200 0 1000 909
4 force_first 1780 320 120 0 0 1000 912
5 requisition_for_force 1340 140 0 0 0 820 832
6 balanced 1200 320 120 0 0 940 835
7 relief_first 760 320 120 0 0 1000 838
8 balanced 320 320 0 0 0 1000 841
9 force_first 0 0 0 0 0 680 844
10 balanced 0 0 0 0 3966 360 847
11 relief_first 3966 320 120 0 0 480 850
12 balanced 3526 320 120 0 0 600 853
13 force_first 3086 320 120 0 0 720 856
14 balanced 2646 320 120 0 0 840 859

Opening stock is the granary’s balance before the month’s allocation. Wellbeing falls by one per basket of civilian shortfall and rises by one per basket of relief, capped at 1000. Cooperation rises by 3 each month except month 5, when the requisition costs 80 instead.

Grain loop: tickets set priorities, one allocation pass splits granary stock, month 3 ships to the garrison, month 10 credits the harvest. View diagram source.
View diagram source
flowchart TD
  Ticket["Monthly decision tickets"] -->|priorities| Alloc["One allocation pass"]
  Demands["Demands: civilians, relief, garrison dispatch"] --> Alloc
  Granary["Granary account"] --> Alloc
  Alloc --> Consume["Consume: civilians and relief"]
  Alloc -->|"month 3 only"| Transfer["Transfer held in escrow"]
  Transfer --> Route["River leg fails, reroute by road"]
  Route -->|acceptance| Garrison["Garrison account"]
  Garrison --> Force["Force-supply consumption intent"]
  Harvest["Month 10 harvest credit"] --> Granary
  Consume --> Close["CloseMonth frame"]
  Force --> Close

The example is 23 lines: it passes fourteen GrainDecision values to GrainHarness::run_fourteen_months and prints the summary. The work happens in grain.rs.

GrainHarness::new compiles synthetic_grain_fixture() with compile_content_pack, installs the granary account (4,300 baskets, held by the manager) and an empty garrison account (held by the force), and stores the resource, economy, and force-supply state as initial domain records of the Scenario. new_canwu then registers the plugins:

let resource = ResourcePlugin::new([economy_kind, force_kind]);
let economy = EconomyReferencePlugin;
let force = ForceSupplyReferencePlugin;
let plugins: [&dyn SimulationPlugin; 3] = [&resource, &economy, &force];
Canwu::new_with_plugins(seed, scenario, &plugins)

ResourcePlugin::new takes the domain record kinds that the resource plugin reads to check adapter evidence: here, the economy and force-supply runtime records.

advance_month records the month’s choice in three decision tickets. record_decision opens a ticket with four options, each carrying an EconomyOperationV1::SelectDecision command, and resolves it with the option you passed in; this is the decision shown in the output, and it drives cooperation. record_g5_decision records the same choice as the granary’s resilience posture, and from month 3 record_force_decision records it as the garrison’s supply posture. The resilience posture sets each demand’s priority:

fn decision_priority(decision: GrainDecision, label: &str) -> i32 {
match (decision, label) {
(GrainDecision::ReliefFirst, "relief")
| (GrainDecision::ForceFirst | GrainDecision::RequisitionForForce, "force") => 120,
(_, "civilian") => 100,
(_, "relief") => 90,
_ => 80,
}
}

Resolving a ticket admits the command it carries, and the owning plugin applies that command at the next boundary. advance_month settles both boundaries before it reads the postures, so each month’s allocation uses that month’s choice. requisition_for_force sets the garrison’s posture to requisition_locally, which also changes how the garrison eats (step 5).

// All three uses are manager-owned demands for the same exact grain
// revision and become due at the same simulation instant. One
// canonical allocation ingress therefore decides scarcity by persisted
// priority/tie-break data rather than by Rust control flow.
let manager = holder(1);
let civilian = self.submit_demand(
month,
"civilian",
manager.clone(),
CIVILIAN_NEED,
decision_priority(resilience_decision, "civilian"),
None,
)?;

Relief (RELIEF_TARGET, 120) and the garrison dispatch (SHIPMENT_QUANTITY, 1,200) follow the same way. allocate_competing queues one enqueue_resource_allocation pass, and canwu-resource records a reservation for each demand it can serve. Each demand expires one month after it is due, which releases whatever it still holds.

The demands use the default source policy, ResourceDemandSourcePolicyV1::Pooled, which lets a demand draw on every grain account in ID order. In month 4 the dispatch demand ranks first, so it reserves the last 40 baskets in the garrison’s a-field-force account before 1,160 from z-granary.

record_route_availability(month, month >= 3, true) marks the river route closed in months 1 and 2, and those months cancel the dispatch demand ("force-before-readiness"). In month 3, deliver_to_force debits the reserved grain into transfer escrow with BeginTransfer, starts a river-boat leg, and fails it:

execution
.fail_current_leg("river crossing closed".to_owned(), self.canwu.time())
.map_err(transport_error)?;
let alternate = route_plan(self.canwu.time(), month, true)?;

The same execution is rerouted through ridge-post by road with ItineraryRevisionReason::Disaster. The carriers hand off at the relay, and the transfer advances to InTransit and then ArrivalPending. The garrison account is credited only when CompleteTransfer settles it with ResourceTransferDispositionV1::Accept. Each irreversible step (begin, accept, consume, credit) carries a completion lease certificate (proof that the holder activated its one-time right to perform that operation); see Production, resources, and transport: who owns what.

5. Feed civilians, relief, and the garrison

Section titled “5. Feed civilians, relief, and the garrison”

For civilians and relief, consume_local has the economy plugin authorize a consumption intent, then sends ResourceOperationRequestV1::Consume through adapter ingress. The garrison takes a separate path in service_force_if_due: the force-supply plugin submits a consumption intent against its own account, and the resource outcome is sent back to it through plugin ingress.

let operation = self.submit_force(ForceOperationV1::SubmitConsumptionIntent { intent })?;

The dispatch demand in months 4 to 14 still enters the allocation pass, but deliver_to_force runs only in month 3, so force_fulfilled stays 0. By month 5 only 40 baskets remain in the garrison account; the shortage costs the garrison 100‰ readiness (900 to 800). After that the account is empty, service_force_if_due returns early when there is nothing to issue, and readiness stays at 800.

Under the requisition_locally posture, consume_for_force attaches the requisition policy from synthetic_grain_fixture() to the garrison’s consumption intent, which opens a requisition saga in canwu-force-supply-reference. After the resource outcome and the readiness consequence, the force records an externality intent. The economy plugin applies it to the local economy (cooperation −80‰ and a 60‰ penalty on the next harvest; these values come from the fixture’s cooperation_cost_per_mille and next_harvest_input_cost_per_mille, while the economy profile’s own requisition block in grain.rs is recorded but not read) and publishes the outcome, the force acknowledges it, and FinalizeRequisition settles the saga. Month 5 runs this path with the garrison’s last 40 baskets. A requisition chosen once the garrison account is empty issues nothing, so no saga starts.

The economy plugin applies the externality only if the local economy still has the revision it had when the plugin joined the requisition’s completion lease. If another command changed the local economy in between, the outcome is Rejected and the local economy stays as it is; requisition_externality_rejects_a_local_economy_changed_after_its_lock tests this.

In month 10, credit_harvest computes

let quantity = HARVEST_BASE
.saturating_mul(u64::from(local.cooperation_per_mille))
.saturating_mul(u64::from(
1_000_u16.saturating_sub(local.pending_harvest_penalty_per_mille),
))
/ 1_000_000;

and credits it to the granary as ResourceCreditSourceV1::ExternalInflow. With cooperation at 844 after month 9 and the 60‰ penalty from month 5’s requisition, that is 5000 × 844 × 940 / 1,000,000 = 3966 (rounded down). Without the requisition, cooperation would be 927 and the harvest 4,635. Every month ends with EconomyOperationV1::CloseMonth, which checks the cited demand and outcome records and appends the frame you saw in the output. summary calls ResourceState::validate_conservation before it builds the totals.

  • Only canwu-resource changes a balance. The garrison submits an intent, and the harvest arrives as a credit with its evidence.
  • Arrival and acceptance are separate steps. The garrison account changes only when CompleteTransfer accepts the transfer.
  • One allocation pass settles the competing claims from stored priorities and tie-break keys. In month 5 requisition_for_force ranks the dispatch demand first: it reserves 1,200 of the 1,340 baskets, civilians get 140, and relief gets nothing. Nothing ships, so that grain stays out of reach until the demand expires. The granary is empty in months 9 and 10 until the harvest is credited at the end of month 10.
  • conservation_closing equals final_stock (2,206) because no transfer is left in escrow. At the end, the granary holds all 2,206 baskets, 1,200 of them reserved for the month 14 dispatch.
  • The requisition feeds the garrison no more than force_first would, because the garrison eats only from its own account. What it adds is the cost: cooperation falls from 912 to 832, and the harvest five months later is 3,966 instead of 4,635. requisition_branch_is_reproducible_and_carries_future_cost in grain_harness checks that cost, and requisition_keeps_resource_force_externality_and_ack_as_distinct_steps in canwu-force-supply-reference tests each saga step on its own.
  • Decisions. Edit the decisions array in grain_loop.rs and watch how civilian_fulfilled and population_wellbeing_per_mille change. Moving requisition_for_force to month 6 or later still ranks the dispatch first, so civilians can go short that month, but it starts no saga because the garrison account is already empty: the cooperation drop and harvest penalty never apply, and cooperation only misses that month’s +3.
  • Harvest. Set HARVEST_BASE to 6_000 in grain.rs. Month 10’s harvest_output becomes 6000 × 844 × 940 / 1,000,000 = 4760 (rounded down).
  • Seed floor. The economy profile’s seed_floor: 180 is recorded, but no code reads it, and neither grain account has a protected floor (protected_floor_policy: None), so allocation empties the granary by the end of month 8. Adding a protected stock floor (ProtectedFloorPolicyRevision) here takes more than pointing the granary at one. Allocation fails with VersionConflict whenever a demand’s protected_floor_policy differs from that of an account it draws on. The demands in this loop use the default Pooled source policy (step 3), so they draw on both grain accounts, and submit_demand in grain.rs always creates them with None. Both accounts and every demand would need the same policy revision. conservation_partial_minimum_and_protected_floor_are_enforced in canwu-resource shows a working single-account setup: the floor holds back stock, and a demand whose protection_override_class is in the policy’s override_classes still draws below it.
  • Persistence. Run cargo test -p canwu-economy-reference. snapshot_checkpoint_journal_and_fork_continue_identically snapshots the harness after four months, restores it, replays its checkpoint journal, and forks it, then checks that all four copies reach the same checkpoint_hash two months later.

Production, resources, and transport: who owns what has the full API rules.

The production simulation extension (canwu-production) owns processes, sites, work orders, and output settlement, and ProductionState::blockers_for names each requirement group that still lacks evidence (cargo run -p canwu-production --example process_constraints). Work in progress (WorkInProgress) records an execution’s progress and consumed inputs while the stock itself stays in canwu-resource; the grain loop runs no production process.

Reference content describes a deposit with a ResourceCapabilityRevision, whose resource capability stage (ResourceCapabilityStage) runs from Potential through RouteAccessible to DeliveredAccepted. Even at RouteAccessible, stock reaches an account only after a runtime route observation and destination acceptance.

Each month record_g5_decision puts the manager’s projection into the resilience ticket’s context. ProjectionProviderRegistryV1::project(canwu, holder, scope) builds it from the holder’s resource, economy, production, and force witnesses. It returns ProjectionQueryResultV1::Available with an EconomyProjectionV1, or Unavailable with a blocker code such as scope_unconfigured or provider_unavailable. Both parts of the projection are read models: building them changes no state and settles no trade.

  • The local scarcity projection (LocalScarcityProjection) covers the stock, open demand, and buffers the holder can observe. Only stock the holder can also reach counts as supply; remote stock with an unobserved or unreachable route goes into the excluded_unreachable_* fields. scarcity_per_mille is the shortfall of known supply against the known demand remainder, in thousandths of that remainder, plus route and security penalties, capped at 2,000. The projection lists its causes.
  • The price-pressure projection (PricePressureProjection) carries a pressure_per_mille only when the economy profile accepts price evidence and in-effect evidence exists: an executed exchange, a quote, an administered price, or a contract price, each with its interpretation rule and source versions. Its status is Observed for one factor and InferredPressure for several. Without evidence it is ExplicitUnknown, and a profile that excludes prices gives NotApplicable. Here it is NotApplicable, because the economy profile sets price_applicability: NotApplicable.

canwu-economy-reference-content also ships two source-cited fixtures that no example runs, a Ming-period cotton archetype for Songjiang and the Lower Yangzi (1450–1644) and an 1896–1911 archetype of the Hanyang ironworks, Daye iron mine, and Pingxiang coal mine; fixtures.rs holds them with their sources.

Open the runnable example

Read the grain harness tests

Read the force-supply tests

Browse the model cards and sources