Grain, production, and military supply
The scenario
Section titled “The scenario”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.
What this example shows
Section titled “What this example shows”- 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 ingrain.rs.
Run it
Section titled “Run it”cargo run -p canwu-economy-reference --example grain_loopThe 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.
How grain moves
Section titled “How grain moves”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
Walkthrough
Section titled “Walkthrough”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.
1. Compose three plugins
Section titled “1. Compose three plugins”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.
2. Choose the month’s priority
Section titled “2. Choose the month’s priority”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).
3. Submit three demands and allocate once
Section titled “3. Submit three demands and allocate once”// 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.
4. Ship to the garrison in month 3
Section titled “4. Ship to the garrison in month 3”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.
6. Credit the harvest and close the month
Section titled “6. Credit the harvest and close the month”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.
What to notice
Section titled “What to notice”- Only
canwu-resourcechanges 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
CompleteTransferaccepts the transfer. - One allocation pass settles the competing claims from stored priorities and
tie-break keys. In month 5
requisition_for_forceranks 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_closingequalsfinal_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_firstwould, 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_costingrain_harnesschecks that cost, andrequisition_keeps_resource_force_externality_and_ack_as_distinct_stepsincanwu-force-supply-referencetests each saga step on its own.
Try changing
Section titled “Try changing”- Decisions. Edit the
decisionsarray ingrain_loop.rsand watch howcivilian_fulfilledandpopulation_wellbeing_per_millechange. Movingrequisition_for_forceto 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_BASEto6_000ingrain.rs. Month 10’sharvest_outputbecomes6000 × 844 × 940 / 1,000,000 = 4760(rounded down). - Seed floor. The economy profile’s
seed_floor: 180is 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 withVersionConflictwhenever a demand’sprotected_floor_policydiffers from that of an account it draws on. The demands in this loop use the defaultPooledsource policy (step 3), so they draw on both grain accounts, andsubmit_demandingrain.rsalways creates them withNone. Both accounts and every demand would need the same policy revision.conservation_partial_minimum_and_protected_floor_are_enforcedincanwu-resourceshows a working single-account setup: the floor holds back stock, and a demand whoseprotection_override_classis in the policy’soverride_classesstill draws below it. - Persistence. Run
cargo test -p canwu-economy-reference.snapshot_checkpoint_journal_and_fork_continue_identicallysnapshots the harness after four months, restores it, replays its checkpoint journal, and forks it, then checks that all four copies reach the samecheckpoint_hashtwo months later.
Beyond the example
Section titled “Beyond the example”Production, resources, and transport: who owns what has the full API rules.
Production
Section titled “Production”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.
Resource capability stages
Section titled “Resource capability stages”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.
Scarcity and price
Section titled “Scarcity and price”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 theexcluded_unreachable_*fields.scarcity_per_milleis 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 apressure_per_milleonly 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 isObservedfor one factor andInferredPressurefor several. Without evidence it isExplicitUnknown, and a profile that excludes prices givesNotApplicable. Here it isNotApplicable, because the economy profile setsprice_applicability: NotApplicable.
Historical archetypes
Section titled “Historical archetypes”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.
Source
Section titled “Source”Open the runnable example
Read the grain harness tests
Read the force-supply tests
Browse the model cards and sources