跳转到内容

明代财政案例

明代一个财政衙门开出 100 单位的应征额,授权征收其中 70,然后等待这 70 实际入库的 证明。资源适配器报告征收完成后,财政扩展记下一张凭证,户部尚书随即收到一份估计: 应征多少、已征多少、还欠多少。三个起点在不同的历史背景下跑同一套流程:洪武 1391 (江南的登记土地、徭役和漕粮负担)、万历 1581(一条鞭法在四个地区处于不同阶段)、 弘光 1644(南明的五个独立财政权责方)。

这个示例回答的问题是:法规随时期和地区变化的税制,怎样在游戏里建模,同时让账目 和实际流动的粮食、白银分开?

法规、地区、时期和采纳阶段来自带出处的参考内容包。数量、人物和机构都是示意值,取自 一个小型的合成参考世界。

  • 财政模拟扩展 canwu-fiscal(FiscalPlugin): 财政法规及其地区采纳、核算、执行授权和凭证。资源余额和实际转移仍归资源或物流 领域管理。
  • 参考内容包 canwu-ming-fiscal:data/pack.json 中的时期、地区、法规、改革转型和出处,以及作为 fixture 文件的三个起点。
  • 参考整合包 canwu-ming-fiscal-reference:在 canwu-reference-world 上搭建每个起点,注册插件,运行示例流程并写出 trace。
  • 以领域命令(Command::Plugin)提交的财政行动, 由 FiscalAuthorityBinding 校验权限。
  • 财政执行凭证(FiscalExecutionReceiptPacket): 依据适配器写入的领域记录的精确版本结算。
  • 户部尚书的报告:以持有人相对知识发布,每个总数都是 一个区间。
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

第一个参数是 fixture ID,省略时运行 hongwu-1391。每次运行打印一行(trace 路径 已删减):

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

种子固定为 DEFAULT_SEED,所以同一 fixture 每次运行打印的 checkpoint 哈希都相同。 frames=6 是结算过的边界数:每个财政行动两个,适配器的证据记录一个,凭证一个。 trace 写到工作区的 artifacts/traces/ming-fiscal-reference/<fixture>/: manifest.json,外加 steps.jsonl 中每个边界一行 JSON。洪武最后一行里, fiscal.state.aggregates 和 fiscal.projections 记录了结果(已删减):

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

每个起点核算的采纳记录不同:

Fixture 历史模式 核算所用法规与范围 单位
hongwu-1391 recorded_baseline scope.lower-yangzi.grain 上的 canal_grain_tribute shi_grain_equivalent
wanli-1581 recorded_baseline scope.lower-yangzi.land 上的 single_whip_lower_yangzi liang_silver
hongguang-1644 research_replay scope.southeast.salt 上的 southern_ming_salt_finance liang_silver

选项写在 fixture ID 之后:

选项 作用
--days <N> 在示例流程之后继续运行 N 个模拟日,每一步结算一个日历边界。
--cadence <marker> 把每一步标记为 daily、monthly 或 annual。SimTime 以分钟计、不带日历,所以月度和年度步长分别取 30 和 365 个模拟日。
--step-days <N> 改用别的步长。最后不足一步的部分按不带 cadence 标记的边界结算。
--trace-dir <path> / CANWU_TRACE_DIR 改变 trace 根目录。
--open-viewer / --viewer-port <N> 在示例流程开始前于 127.0.0.1 上启动 trace viewer 并打开默认浏览器。viewer 随 steps.jsonl 增长而加载新内容,进程一直提供服务,直到按下 Ctrl+C。端口默认为 0,即自动选择空闲端口。

步进方式参见连续时间游戏循环。viewer 依赖工作区中的 tools/trace-viewer/,在工作区外运行时需设置 CANWU_WORKSPACE_ROOT。

示例程序 ming_fiscal_starter.rs 调用参考整合包 lib.rs 中的 new_ming_fiscal_reference 和 run_ming_fiscal_sample_cycle_with_trace, 并为每张边界回执写一帧 trace。

ming_fiscal_reference_scenario 读取 fixture JSON,例如 wanli-1581.json。 fixture 给出年份、历史模式、地区以及各条法规的采纳阶段。函数按这些地区编译内容包:

let catalog = compile_ming_fiscal(FiscalContentSelection {
historical_year: fixture.historical_year,
region_ids: fixture.region_ids.clone(),
..FiscalContentSelection::default()
})?;

默认选择的机制集合为空,即选中全部 11 种机制。编译后的目录保留所选地区的全部时期和全部法规; 年份只决定当前活动时期,例如 1391 年对应 founding_reconstruction。

同一个函数创建 FiscalState。reference_institutions 给每个起点一个朝廷机构 (参考世界中的政府),由 authority.revenue-minister 负责;hongguang-1644 另外 加入前线将领、地区财库、盐务衙门和商人信用机构。每个机构得到一个只有一名常设角色的 权限绑定:

FiscalAuthorityBinding {
id: institution.authority_id.to_owned(),
institution: institution.entity.clone(),
authorized_actor: Some(institution.actor),
acting_actor: None,
authority_basis: None,
},

fixture 中的每条采纳记录变成一个 FiscalScopeBinding(把一个辖区和一种机制交给 某个机构,粒度为 SimulationGranularity::Aggregate)和一条带 fixture 阶段的采纳 状态。观察者绑定 observer.revenue-minister 让户部尚书以 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 注册参考世界、执行适配器和 ming_fiscal_plugin()。 后者是一个只接受一种证据的 FiscalPlugin,这种证据就是适配器的 fiscal_execution_evidence 记录。validate_ming_fiscal_reference 是参考整合包的 语义校验:重新计算聚合和改革候选,并把每张凭证与其证据重新核对一遍。

sample_cycle_plan 按 ID 顺序取第一条满足条件的采纳记录:阶段可执行 (implemented、audited 或 entrenched),且法规的法定年份范围包含当前年份。 hongguang-1644 中太仓银库的法规止于 1643 年,所以流程改用盐务衙门。第一个行动:

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 用 fiscal_action_command 包装 FiscalActionRequest,以绑定中的角色身份提交。第一个边界上,准入过程核对角色与 绑定、绑定机构与范围,以及 expected_procedure_revision 与财政状态是否一致;下一个 边界把行动结算进 action_outcomes。核算周期 ID 由上层应用决定,与历史时期相互独立。 第二个行动 AuthorizeExecution 允许把资源 ResourceId::new(1) 中的 70 从该角色 征收给机构,资源余额保持不变。

settle_sample_execution_with_trace 扮演资源一方。它通过 enqueue_reference_execution_result 发出一条 FiscalExecutionEvidence(70 单位、 Fulfilled,带 external_operation_id)。第 5 个边界上,适配器按已授权的请求核对 这条结果,并写成一条只可创建的领域记录。随后函数提交该记录的精确版本:

enqueue_execution_receipt(
canwu,
now,
&canwu_fiscal::FiscalExecutionReceiptPacket {
receipt_id: format!("{id_prefix}.receipt"),
request_id,
external_evidence: [evidence_version].into_iter().collect(),
},
)?;

第 6 个边界上,FiscalPlugin 解码每条被引用的记录,检查记录种类,并逐项核对请求、 执行类型、支付形态、资源、来源、目标和单位是否与授权一致,再从证据推出凭证的数量 (70)和处置结果(fulfilled)。

此时聚合显示应征 100、已征 70、未结 30。每当某个边界写入新的财政状态版本, FiscalPlugin 就为每个观察者绑定发布一份报告,作为该观察者持有的知识记录。 derive.rs 里的 estimate 按观察者的置信度把每个总数归入一个区间:

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 是不超过该值的最大 10 的幂。置信度为 850‰ 时,100 的区间宽度为 10 (100-109),70 取最小宽度 2(70-71)。trace 通过 canwu.viewer_for_actor 和针对 fiscal_report_knowledge_schema_id() 的 KnowledgeQuery,以户部尚书的视角读回报告 (见 trace.rs 中的 collect_projections)。

  • 已征数量只能经由适配器的证据记录进入财政状态,比授权晚两个边界。
  • 凭证的数量和处置结果来自所引用的证据。在同一份财政状态中,每个精确证据版本、每个 证据种类与 external_operation_id 的组合,最多结算一张凭证。
  • 三个起点共用一套代码,由 fixture JSON 决定流程使用的法规、范围和机构。
  • 户部尚书看到的是 100-109,权威状态中的值是 100。
  • wanli-1581 的 trace 把 adopt_single_whip_north 和 adopt_single_whip_southwest 列为转型候选;在 ApplyTransition 行动结算之前, 它们一直只是候选。
  • 报告精度。 在 ming_fiscal_reference_scenario 中把观察者的 confidence_per_mille 改为 600。除数变为 2,应征区间变成 100-149,已征区间变成 70-74。
  • 部分征收。 在 settle_sample_execution_with_trace 中把证据改为 quantity: 40 和 disposition: FiscalReceiptDisposition::Partial。聚合随之显示 已征 40、未结 60。
  • 实施改革。 在 wanli-1581 中提交 adopt_single_whip_north 的 ApplyTransition,把 single_whip_north 绑定到 scope.north.land,写法可参照测试 single_whip_transition_atomically_suspends_the_superseded_rule。华北的折银采纳 变为 suspended,出现一条 implemented 的 single_whip_north 采纳,华北的候选 随之消失。

真正的游戏还要补上一层:上层应用的适配器在自己的资源、市场、生产或物流领域执行实际 操作,再像第 5 步那样提交证据。

模型的其余部分见财政制度:覆盖单元及明代内容包 704 个单元的构成、历史语境与改革候选、代理角色,以及状态上限。

内容包覆盖 1368 年至 1683 年的 8 个时期,引用 10 条出处,其中引用最多的是 Ray Huang (1974);每条出处都列出它不支持的推断,例如把登记户数当作实际人口。 案例说明列出了各个时期,data/pack.json列出了全部法规和出处。

打开可运行示例

阅读参考整合测试

浏览参考内容包

阅读案例说明