Ming fiscal case
The scenario
Section titled “The scenario”A Ming revenue office assesses 100 units of tax, authorizes a collection of 70, and waits for proof that the 70 arrived. When a resource adapter reports the collection, the fiscal extension records a receipt, and the revenue minister receives an estimate of what is assessed, collected, and still owed. Three starts run this cycle in different settings: Hongwu 1391 (registered land, labor service, and grain obligations in the Lower Yangzi), Wanli 1581 (Single Whip reform at different stages across four regions), and Hongguang 1644 (five separate fiscal authorities of the Southern Ming).
The question the example answers: how does a game model a tax system whose rules change by period and region, while the bookkeeping stays separate from the grain and silver that actually move?
Rules, regions, periods, and adoption stages come from a source-cited content pack. The quantities, people, and institutions are illustrative values from a small synthetic reference world.
What this example shows
Section titled “What this example shows”- The fiscal simulation extension
canwu-fiscal(FiscalPlugin): fiscal rules, their regional adoption, assessments, execution authorizations, and receipts. Resource balances and transfers stay with the resource or logistics domain. - A reference content pack,
canwu-ming-fiscal: periods, regions, rules, reform transitions, and cited sources indata/pack.json, plus the three starts as fixture files. - A reference integration,
canwu-ming-fiscal-reference: builds each start oncanwu-reference-world, registers the plugins, runs the sample cycle, and writes a trace. - Fiscal actions sent as domain commands
(
Command::Plugin) and checked against aFiscalAuthorityBinding. - A fiscal execution receipt
(
FiscalExecutionReceiptPacket) settled from an exact domain record version written by an adapter. - The minister’s report, published as holder-relative knowledge with each total as a range.
Run it
Section titled “Run it”cargo run -p canwu-ming-fiscal-reference --example ming_fiscal_starter -- hongwu-1391cargo run -p canwu-ming-fiscal-reference --example ming_fiscal_starter -- wanli-1581cargo run -p canwu-ming-fiscal-reference --example ming_fiscal_starter -- hongguang-1644The fixture ID is the first argument; without it the starter runs
hongwu-1391. Each run prints one line (trace paths trimmed):
fixture=hongwu-1391 checkpoint=b282b58b82d8fbe7646fdb1cfbae1e64e1e2187a4bd84d014826351cb7ae5c0c frames=6 trace_manifest=... trace_steps=...fixture=wanli-1581 checkpoint=1c79fd79784d86ac285ad81e80e0f54dd7b0389edce99a8351012f0555f3255b frames=6 trace_manifest=... trace_steps=...fixture=hongguang-1644 checkpoint=8aa76890d6bdf198a8d1b620ee5db3597157b642f27e757bc9f120a5e451df50 frames=6 trace_manifest=... trace_steps=...The seed is fixed (DEFAULT_SEED), so each fixture prints the same checkpoint
hash on every run. frames=6 counts settled boundaries: two for each fiscal
action, one for the adapter’s evidence record, and one for the receipt. The
trace goes to artifacts/traces/ming-fiscal-reference/<fixture>/ in the
workspace: manifest.json, plus one JSON line per boundary in steps.jsonl.
In the last Hongwu line, the scope’s book under fiscal.state.ledgers and the
minister’s report under fiscal.projections hold the result (trimmed):
"totals":{...:{"assessed":100,"remission_granted":0,"collected":70,...,"outstanding":30}}"<cycle>:0000:0":{...,"assessed":{"minimum":100,"maximum":109},"collected":{"minimum":70,"maximum":71},"outstanding":{"minimum":30,"maximum":31}}fiscal.counts beside them counts what the fiscal records hold: books,
assessments, requests, receipts, and the recent outcomes of each record.
Each start assesses a different adoption:
| Fixture | Historical mode | Assessed rule and scope | Unit |
|---|---|---|---|
hongwu-1391 |
recorded_baseline |
canal_grain_tribute on scope.lower-yangzi.grain |
shi_grain_equivalent |
wanli-1581 |
recorded_baseline |
single_whip_lower_yangzi on scope.lower-yangzi.land |
liang_silver |
hongguang-1644 |
research_replay |
southern_ming_salt_finance on scope.southeast.salt |
liang_silver |
Options go after the fixture ID:
| Option | Effect |
|---|---|
--days <N> |
Continues for N simulation days after the sample cycle, settling one calendar boundary per step. |
--cadence <marker> |
Marks each step daily, monthly, or annual. Monthly and annual steps are 30 and 365 simulation days, because SimTime counts minutes and has no calendar. |
--step-days <N> |
Sets another step length. A shorter final step settles without a cadence marker. |
--trace-dir <path> / CANWU_TRACE_DIR |
Changes the trace root. |
--open-viewer / --viewer-port <N> |
Starts a trace viewer on 127.0.0.1 before the sample cycle and opens the default browser. The viewer loads steps.jsonl as it grows, and the process keeps serving until you press Ctrl+C. The port defaults to 0, which takes a free one. |
See Continuous-time game loop for the
stepping model. The viewer needs the workspace’s tools/trace-viewer/; outside
the workspace, set CANWU_WORKSPACE_ROOT.
Walkthrough
Section titled “Walkthrough”The starter,
ming_fiscal_starter.rs,
calls new_ming_fiscal_reference and run_ming_fiscal_sample_cycle_with_trace
from the integration’s
lib.rs
and writes a trace frame for every boundary receipt.
1. Compile the catalog for one fixture
Section titled “1. Compile the catalog for one fixture”ming_fiscal_reference_scenario loads the fixture JSON, such as
wanli-1581.json,
which names a year, a historical mode, regions, and each rule’s adoption
stage. It compiles the pack for those regions:
let catalog = compile_ming_fiscal(FiscalContentSelection { historical_year: fixture.historical_year, region_ids: fixture.region_ids.clone(), ..FiscalContentSelection::default()})?;The default selection leaves the mechanism set empty, which selects all eleven
mechanisms. The catalog keeps every
period and every rule for those regions; the year chooses the active period,
such as founding_reconstruction for 1391.
2. Build the starting fiscal state
Section titled “2. Build the starting fiscal state”The same function creates a FiscalState. reference_institutions gives
every start a court institution (the reference world’s government) under
authority.revenue-minister; hongguang-1644 adds a field commander, a
regional treasury, a salt administration, and a merchant-credit office. Each
gets a binding with one standing actor:
FiscalAuthorityBinding { id: institution.authority_id.to_owned(), institution: institution.entity.clone(), authorized_actor: Some(institution.actor), acting_actor: None, authority_basis: None, rebound_by: None,},rebound_by stays None until a RebindAuthority action replaces the
binding’s actors during a run; the starts never do that.
Each fixture adoption becomes a FiscalScopeBinding (one jurisdiction and
mechanism assigned to an institution at SimulationGranularity::Aggregate)
and an adoption record with the fixture’s stage; each scope gets its own
scope ledger record. The observer binding
observer.revenue-minister lets the minister see those totals with
confidence_per_mille: 850; it names the minister as actor and his person
as knowledge_holder, and controlled_by: None means no fiscal action can
re-point it. It leaves granularity and cadence at their defaults, one row
per scope and a report on every change; a world with many observers would
give each an Interval cadence and courts Institution granularity (see
how a report is published).
FiscalState::into_records then partitions the state into the records the
scenario carries.
3. Register the plugins
Section titled “3. Register the plugins”let canwu = Canwu::new_with_plugins(seed, reference.scenario, &[&world, &adapter, &fiscal])?;validate_ming_fiscal_reference(&canwu)?;new_ming_fiscal_reference registers the reference world, the execution
adapter, and ming_fiscal_plugin(), a FiscalPlugin that accepts one
evidence kind: the adapter’s fiscal_execution_evidence record.
validate_ming_fiscal_reference is the integration’s semantic check; it
re-matches every receipt against its evidence and checks every citation
using only state a seal cannot change, so it takes a Canwu or a sealed
CompactedCanwu and answers alike.
4. Open an assessment and authorize a collection
Section titled “4. Open an assessment and authorize a collection”sample_cycle_plan takes the first adoption, by ID, whose stage is
operational (implemented, audited, or entrenched) and whose rule’s legal
window contains the year. In hongguang-1644 the Taicang treasury rule ends
in 1643, so the cycle uses the salt administration. The first action:
action: FiscalAction::OpenAssessment { assessment_id: assessment_id.clone(), rule_id, scope_binding_id: scope_id, accounting_cycle_id: format!("{period_id}.{id_prefix}"), quantity: 100, unit: sample_unit(payment_form).to_owned(), payment_form, commutation_quote: None,},reference_batch reads the scope ledger’s revision with
fiscal_target_revision and puts the action in a one-entry
FiscalActionBatch against it; submit_reference_action_with_trace wraps the
batch with fiscal_action_command and issues it as the binding’s actor. At
the first boundary, admission checks the actor against the binding and the
binding’s institution against the scope. The next boundary checks the entry’s
expected_revision against the ledger’s revision when that boundary began,
settles the action, records its outcome in the ledger’s outcome ring, and
emits canwu.fiscal.action_settled.v1, which the helper reads back with
FiscalSettledAction::from_events. The sample submits one action at a time,
but a batch of several entries, or several batches built against the same
revision, would settle in that one boundary, in admission order. The
accounting cycle ID belongs to the host and is separate from the historical
period; the first assessment of a cycle opens the scope’s book for it. The
second action, AuthorizeExecution, permits collecting 70 of resource
ResourceId::new(1) from the actor to the institution; resource balances
stay as they are.
5. Record the collection and settle the receipt
Section titled “5. Record the collection and settle the receipt”settle_sample_execution_with_trace plays the resource side. It sends a
FiscalExecutionEvidence value (70 units, Fulfilled, with an
external_operation_id) through enqueue_reference_execution_result. At
boundary 5 the adapter checks it against the authorized request and writes a
create-only domain record. The function then submits that record’s exact
version:
enqueue_execution_receipts( canwu, now, &[canwu_fiscal::FiscalExecutionReceiptPacket { receipt_id: format!("{id_prefix}.receipt"), scope_binding_id, request_id, external_evidence: [evidence_version].into_iter().collect(), }],)?;At boundary 6 FiscalPlugin decodes each cited record, checks its kind and
that its scope, request, execution kind, payment form, resource, source,
target, and unit match the authorization, then derives the receipt’s quantity
(70) and disposition (fulfilled) from it. A receipt the plugin cannot accept
is refused with a canwu.fiscal.ingress_rejected.v1 event and never fails
the boundary.
6. Read the book totals and the report
Section titled “6. Read the book totals and the report”The scope’s book now shows 100 assessed, 70 collected, and 30 outstanding.
When a boundary changes a book an observer can see, FiscalPlugin publishes
that observer’s report as knowledge records held by the observer: one summary
record, and the parts of 16 scope rows whose rows changed. The minister’s
report has one row here, so it is a summary and one part. estimate in
derive.rs
rounds each total into a bucket sized by the observer’s confidence:
let precision_divisor = match confidence_per_mille { 900..=1_000 => 100, 750..=899 => 10, 500..=749 => 2, _ => 1,};let bucket_width = (magnitude / precision_divisor).max(2);let minimum = value / bucket_width * bucket_width;magnitude is the largest power of ten at or below the value. At 850 per
mille, 100 gets width 10 (100-109) and 70 gets the minimum width 2 (70-71).
The trace reads each observer’s report back from its holder:
collect_projections in trace.rs calls
fiscal_reports_of_holder(canwu, &binding.knowledge_holder), the trusted-host
form of fiscal_reports(&viewer), which puts the summary and parts back
together into one FiscalReport per observer binding:
let report = fiscal_reports_of_holder(canwu, &binding.knowledge_holder) .ok()? .remove(observer_id)?;Some((observer_id.clone(), report.projection))A player-facing client uses fiscal_reports(&canwu.viewer_for_actor(actor)?)
instead, which reads only the viewer’s own ledger.
7. Close the cycle and archive the book
Section titled “7. Close the cycle and archive the book”A game that runs for years closes each accounting cycle and archives its
books, so the fiscal records stay small. close_ming_fiscal_sample_cycle
submits CloseAccountingCycle for the sample scope and cycle; the closed book
takes only audit findings. archive_ming_fiscal_books(&mut canwu, &store)
then runs the archive protocol against a FiscalArchiveStore you supply:
prepare_fiscal_archive, enqueue_fiscal_archive, a boundary that settles
the commit, and finalize_fiscal_archive_retention, until no closed book is
left. The archive_century test runs one sample cycle a year for a hundred
years, closing every year and archiving every ten, and the fiscal records
stay a few kilobytes; no boundary ever reads the archive store.
What to notice
Section titled “What to notice”- The collected amount enters fiscal state only from the adapter’s evidence record, two boundaries after the authorization.
- The receipt’s quantity and disposition come from the cited evidence. This
adapter writes one evidence record per request of one scope, so an exact
evidence version settles at most one receipt; and, as always, a pair of
evidence kind and
external_operation_idsettles at most one receipt among the scope’s hot books. (Canwu also accepts aggregated execution evidence for an operation that serves several scopes; this integration does not use it, and its adapter refuses it.) - Each action names the revision of the one record it changes, the scope ledger here, so an action on another scope would never make it stale.
- One code path runs all three starts; the fixture JSON picks the rule, scope, and institution.
- The minister reads 100-109 while the authoritative state holds 100.
- In
wanli-1581the trace listsadopt_single_whip_northandadopt_single_whip_southwestas transition candidates. They stay candidates until anApplyTransitionaction settles.
Try changing
Section titled “Try changing”- Report precision. In
ming_fiscal_reference_scenario, set the observer’sconfidence_per_milleto 600. The divisor becomes 2, so the assessed range becomes 100-149 and the collected range 70-74. - Partial collection. In
settle_sample_execution_with_trace, set the evidence toquantity: 40anddisposition: FiscalReceiptDisposition::Partial. The book then shows 40 collected and 60 outstanding. - Apply a reform. In
wanli-1581, submitApplyTransitionforadopt_single_whip_northwithsingle_whip_northbound toscope.north.land, as the testsingle_whip_transition_atomically_suspends_the_superseded_ruledoes. The north’s silver-commutation adoption becomessuspended, asingle_whip_northadoption becomesimplemented, and the north candidate disappears.
Beyond the example
Section titled “Beyond the example”A game adds one more layer: a host adapter that performs the real operation in its resource, market, production, or logistics domain and then submits the evidence, as step 5 does.
Fiscal institutions covers the rest of the model: coverage cells and the Ming pack’s 704-cell breakdown, the partitioned records and their budgets, action batches and per-record revisions, accounting cycles and the archive, historical context and reform candidates, acting actors and auditor appointment, and the limits.
Periods and sources
Section titled “Periods and sources”The pack covers eight periods from 1368 to 1683 and cites ten sources, Ray
Huang (1974) most often; each source lists inferences it does not support, such
as reading registered households as actual population. The
case note lists the periods, and
data/pack.json lists every rule and source.
Source
Section titled “Source”Open the runnable example
Read the integration tests
Browse the content pack
Read the case note