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.
Terminal window
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=02fa72339b855af10464430523413b67d9d748be200eb0375d57b919d69aa72c frames=6 trace_manifest=... trace_steps=...
fixture=wanli-1581 checkpoint=d1d6ebabd9b8b38f8fe1a73d4708dbee999b596990942299ae7b4ea2229d7d2d frames=6 trace_manifest=... trace_steps=...
fixture=hongguang-1644 checkpoint=4c93454351280141b4709717b8cad275ceb853b89459115ef9d73392a4cff7b1 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, fiscal.state.aggregates and fiscal.projections hold the result (trimmed):

"aggregate.0000":{...,"assessed":100,"remission_granted":0,"collected":70,...,"outstanding":30}
"fact.0000":{...,"assessed":{"minimum":100,"maximum":109},"collected":{"minimum":70,"maximum":71},"outstanding":{"minimum":30,"maximum":31}}

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,
},

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. The observer binding observer.revenue-minister lets the minister see those totals with confidence_per_mille: 850.

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 recomputes aggregates and reform candidates and re-matches every receipt against its evidence.

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,
},

submit_reference_action_with_trace wraps the FiscalActionRequest with fiscal_action_command and issues it as the binding’s actor. At the first boundary, admission checks the actor against the binding, the binding’s institution against the scope, and expected_procedure_revision against the state. The next boundary settles the action into action_outcomes. The accounting cycle ID belongs to the host and is separate from the historical period. 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_receipt(
canwu,
now,
&canwu_fiscal::FiscalExecutionReceiptPacket {
receipt_id: format!("{id_prefix}.receipt"),
request_id,
external_evidence: [evidence_version].into_iter().collect(),
},
)?;

At boundary 6 FiscalPlugin decodes each cited record, checks its kind and that its 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.

The aggregate now shows 100 assessed, 70 collected, and 30 outstanding. At each boundary that writes a new fiscal state version, FiscalPlugin publishes one report per observer binding as a knowledge record held by that observer. 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 the report back through canwu.viewer_for_actor with a KnowledgeQuery for fiscal_report_knowledge_schema_id() (collect_projections in trace.rs).

  • 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. Within one fiscal state, each exact evidence version and each pair of evidence kind and external_operation_id settles at most one receipt.
  • 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 aggregate 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, historical context and reform candidates, acting actors, and state 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