Skip to content

Ming fiscal case

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.

  • 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 in data/pack.json, plus the three starts as fixture files.
  • A reference integration, canwu-ming-fiscal-reference: builds each start on canwu-reference-world, registers the plugins, runs the sample cycle, and writes a trace.
  • Fiscal actions sent as domain commands (Command::Plugin) and checked against a FiscalAuthorityBinding.
  • 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.
终端窗口
cargo run -p canwu-ming-fiscal-reference --example ming_fiscal_starter -- hongwu-1391
cargo run -p canwu-ming-fiscal-reference --example ming_fiscal_starter -- wanli-1581
cargo run -p canwu-ming-fiscal-reference --example ming_fiscal_starter -- hongguang-1644

The 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.

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.

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.

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.

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.

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.

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.

  • 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_id settles 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-1581 the trace lists adopt_single_whip_north and adopt_single_whip_southwest as transition candidates. They stay candidates until an ApplyTransition action settles.
  • Report precision. In ming_fiscal_reference_scenario, set the observer’s confidence_per_mille to 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 to quantity: 40 and disposition: FiscalReceiptDisposition::Partial. The book then shows 40 collected and 60 outstanding.
  • Apply a reform. In wanli-1581, submit ApplyTransition for adopt_single_whip_north with single_whip_north bound to scope.north.land, as the test single_whip_transition_atomically_suspends_the_superseded_rule does. The north’s silver-commutation adoption becomes suspended, a single_whip_north adoption becomes implemented, and the north candidate disappears.

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.

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.

Open the runnable example

Read the integration tests

Browse the content pack

Read the case note