财政制度
canwu-fiscal,即财政模拟扩展,把财政制度建模为程序:版本化的法规、逐地区的采纳、核算、减免、经授权的征收与转移、审计、有证据支撑的凭证、会关闭并归档的核算周期,以及发给观察者的报告。粮食、白银等余额留在你的资源或物流领域;财政扩展只记录应缴多少、批准了什么、证据证明了什么。它的状态拆分成多条记录,一个边界只读写它的数据包涉及的记录,所以几万个范围的世界与三个范围的明代起点使用同一套约定。当你的游戏需要随时期和地区变化的税收与国库规则时阅读本页;canwu-ming-fiscal 中的明代内容和可运行的 canwu-ming-fiscal-reference 整合包是完整示例。
crate 与职责
Section titled “crate 与职责”| crate | 负责内容 | 发布状态 |
|---|---|---|
canwu-fiscal |
与时期无关的模型:内容包 schema 与编译器(compile_fiscal_content)、目录和拆分后的状态记录、FiscalPlugin、权限与任命检查、凭证校验、核算周期、财政归档和持有人报告 |
已发布到 crates.io |
canwu-ming-fiscal |
明代内容:内嵌的 data/pack.json(1368–1683 年)、三个起点 fixture 和 compile_ming_fiscal |
已发布到 crates.io |
canwu-ming-fiscal-reference |
可运行的上层应用:场景构造、执行适配器插件、语义校验器、trace 写入与查看器、带关闭和归档辅助函数的示例周期,以及 ming_fiscal_starter 示例 |
仅在仓库中 |
查看图表源码
flowchart TB
Host["你的上层应用<br/>场景、适配器、命令"]
Ref["canwu-ming-fiscal-reference<br/>可运行的明代上层应用"]
Ming["canwu-ming-fiscal<br/>明代数据包"]
World["canwu-reference-world<br/>示例世界"]
Fiscal["canwu-fiscal<br/>与时期无关的模型"]
Resource["canwu-resource<br/>归档查找索引"]
Api["canwu-api<br/>对外 API"]
Host -.->|"可以从它起步"| Ref
Host --> Fiscal
Ref --> Ming
Ref --> World
Ref --> Fiscal
Ming --> Fiscal
Ming --> Api
Fiscal --> Api
Fiscal --> Resource
Resource --> Api
World --> Api
箭头从 crate 指向它所依赖的 crate。在参伍的 crate 中,canwu-fiscal 依赖 canwu-api,并且只为归档的查找索引函数依赖 canwu-resource;财政 API 中不出现任何资源类型,引擎内核也没有财政代码。canwu-ming-fiscal 只包含数据、fixture 加载器和编译辅助函数,没有运行时行为。参考整合包从 canwu-reference-world 借用一个政府、一支军队和若干人物,充当机构和官员。
哪些与时期无关,哪些由内容包提供
Section titled “哪些与时期无关,哪些由内容包提供”| 层次 | 提供内容 | 示例 |
|---|---|---|
canwu-fiscal(与时期无关) |
内容包 schema、校验、覆盖解析、封闭词汇表、财政记录、行动、权限与任命检查、凭证校验、核算周期、归档、报告 | compile_fiscal_content、FiscalPlugin |
| 内容包(与时期相关) | 时期、地区、机构、带法定年份范围的法规、改革转型、覆盖声明,以及带禁止推断的出处 | canwu-ming-fiscal 的 data/pack.json |
| 上层应用(与具体运行相关) | 年份和模式、各类绑定、任命政策、初始采纳阶段、机构实体、执行适配器、归档存储和命令 | canwu-ming-fiscal-reference |
词汇表是 canwu-fiscal 中的封闭 Rust 枚举:11 个 FiscalMechanism 值(例如 LandTax、SaltMonopoly 和 MerchantCredit)、7 个 FiscalPaymentForm 值(Grain、Silver、Labor、SaltCertificate、Coin、Goods、Credit)、10 个 FiscalAssessmentBasis 值和 4 个 FiscalCommutationPolicy 值。内容包在清单中列出它使用的机制,并把它的机构映射到这些值上。
compile_fiscal_content(&pack, selection) 校验整个内容包并返回 CompiledFiscalCatalog。它要求 ID 规范且唯一、引用都能解析、年份范围落在内容包的历史范围之内、pack_version 符合 SemVer、许可证为 Apache-2.0、每个机构、法规和转型都有出处、转型前置条件无环,并且每个覆盖单元都有状态。在 FiscalContentSelection 中,地区集合和机制集合为空表示全选。目录保留全部时期,所以一次运行可以跨越多年;selected_period_ids 列出覆盖所选年份的时期,content_hash 是整个内容包的哈希。
| 类型 | 表示的内容 |
|---|---|
FiscalContentPack |
编写好的内容包:清单、时期、地区、机构、法规、转型、覆盖声明、出处 |
CompiledFiscalCatalog |
不可变的运行目录,保存为只可创建的记录 canwu.fiscal:catalog |
FiscalRuleDefinition |
一条法规:机制、法定年份范围、辖区、核算依据、支付形态、折纳政策、出处、置信度 |
FiscalTransitionDefinition |
一项改革:来源与目标法规、观测与适用年份范围、辖区、取代或停用的法规、前置条件 |
FiscalState |
整个财政状态的逻辑模型和编写模型:根、归档头,以及按键存放的财政机构记录、范围明细、辖区记录、观察者记录和报告队列。into_records 把它拆分成运行中保存的记录,from_records 把记录拼回来 |
FiscalRoot |
canwu.fiscal:root 记录:历史语境及其修订号、获准的证据种类、报告队列最早的到期时间、是否有被搁置的条目,以及搁置轮询的游标 |
FiscalScopeLedger、FiscalCycleBook |
一个范围绑定的记录:它的采纳、每个常驻周期各一本核算周期账册(核算、减免、请求、凭证、审计结论)、修订号、最近结果和周期水位线 |
FiscalOffice |
一个机构的记录:权限绑定和审计绑定、任命政策和审计人提案、各周期的记账、它各范围的机制,以及能看到它的观察者,每个观察者带首行、粒度和节律是否为间隔 |
FiscalJurisdiction |
一个目录地区的记录:范围数、采纳数和转型候选 |
FiscalObserver |
一个观察者绑定的记录:报告的行布局、周期槽位,以及每个已发布分段的摘要值 |
FiscalReportSchedule |
canwu.fiscal:report-schedule 记录:每个报告队列页最早的到期时间,以及含有被搁置条目的页 |
FiscalHistoricalContext |
当前历史年份和 FiscalHistoricalMode |
FiscalAuthorityBinding |
谁可以代表某个机构行动 |
FiscalAuditBinding、FiscalAppointmentPolicy、FiscalAuditProposal |
可以对某个机构记录审计结论的机构外审计人;谁任命和罢免审计人;一条待决的提名或罢免请求 |
FiscalActionIssuer、FiscalRebinding |
已结算行动的签发者,及其绑定和身份;最近一次改绑或改指某个绑定的行动 |
FiscalScopeBinding |
一个机构对一个辖区、对象范围和机制的职责 |
FiscalObserverBinding |
谁接收报告(一名人物,或没有角色的政府)、看哪些机构、置信度多少、粒度是什么、按什么节律到期,以及哪个机构可以改指或移除它 |
FiscalReportGranularity、FiscalReportCadence |
报告的一行是什么(默认 Scope,或 Institution);报告何时到期(默认 OnChange,或 Interval { every, offset },可用 staggered 错开) |
FiscalAdoptionState |
一条法规在一个范围内的阶段,带一个记录变更次数的代次 |
FiscalActionBatch、FiscalActionEntry、FiscalAction |
一条命令中同一绑定下最多 4,096 个程序步骤;带 ID 和预期修订号的一个步骤;步骤本身 |
FiscalActionBatchSet |
一条命令中同一签发者之下最多 64 个批次,让几个机构的批次在一轮内结算 |
FiscalActionTarget |
行动写明修订号的那条记录:范围明细、财政机构记录、辖区记录或观察者记录 |
FiscalExecutionRequest |
征收、汇解、支出、储备或退还某个数量的授权 |
FiscalExecutionEvidence、FiscalExecutionEvidencePart |
适配器在实际操作之后写入的类型化结果,写明它结算的范围和请求;或者作为汇总证据,每个范围一个分项,各带请求和数量 |
FiscalExecutionReceiptPacket、FiscalExecutionReceipt |
引用精确证据版本的凭证请求,以及结算后的凭证 |
FiscalArchiveKeyV1、FiscalArchiveStore |
已归档账册、机构合计或维护凭证的键;归档所在的、由上层应用拥有的存储 |
FiscalReport、FiscalProjection、FiscalReportFact |
读者看到的一位观察者的报告:每个总数都是一个区间,每条事实按周期、行和位置标记 |
FiscalReportRecord |
一条已发布的知识记录:报告的摘要,或一个周期的一个分段(16 行,范围行或机构行) |
FiscalAdoptionStage 依次为 Promulgated、Communicated、Accepted、Implemented、Audited、Entrenched、Suspended 和 Repealed。只有 Implemented、Audited 和 Entrenched 是可执行的(is_operational),核算只能在可执行的采纳之下开立。一条法规可以已对某省颁布,却尚未在那里可执行。
覆盖单元与显式未知
Section titled “覆盖单元与显式未知”财政覆盖单元(FiscalCoverageCell)是一个时期、地区和机制的组合。编译器生成全部组合,并根据内容包的 FiscalCoverageDeclaration 条目逐个解析:匹配的声明中 priority 最高者胜出。没有匹配的单元会导致编译失败,最高优先级出现并列也会,所以文件顺序永远不会决定历史解读。
FiscalCoverageStatus |
含义 | 声明规则 |
|---|---|---|
Supported |
有出处的定义支持直接模拟 | 需要出处,以及该地区内这个机制的法规或转型 |
ArchetypeFallback |
用一个比较性的原型代替,并说明限制 | 需要出处,以及这个机制的法规或转型,可以来自其他地区 |
ExplicitUnknown |
内容包声明它没有这个单元的行为 | 不带定义 |
NotApplicable |
这个机制在此不适用 | 不带定义 |
明代内容包有 8 个时期、8 个地区和 11 种机制,共 704 个单元:160 个支持,101 个原型回退,443 个显式未知。未知单元中,425 个来自优先级为 0 的默认声明,18 个来自 zheng_other_mechanisms,这是一条优先级为 60 的声明,覆盖郑氏时期两个地区内的九种机制。每条 FiscalProvenance 都写明引文、URL、论断范围、置信度,以及至少一条禁止推断,例如“不要直接从法定额度推断实际收入”。
覆盖、出处、置信度和折纳政策是给上层应用、工具和审阅者看的目录元数据。插件的准入检查读取法规、法定年份范围、辖区、机制、支付形态和采纳阶段。
记录与字节预算
Section titled “记录与字节预算”一次运行把财政状态保存为 canwu.fiscal 命名空间中的若干记录。除了根对只可创建的目录的引用之外,财政记录之间互不引用,所以写一条记录不会让引擎重新校验其他记录:
| 种类 | 标识 | 内容 | 何时写入 |
|---|---|---|---|
catalog |
primary |
编译好的目录 | 创建后不再写入 |
root |
primary |
历史语境及其修订号、获准的证据种类、报告队列最早的到期时间、是否有被搁置的条目、搁置轮询的游标 | 语境变更、报告记账 |
archive |
primary |
归档头(FiscalArchiveHeadV1):每次已应用提交归档了什么、维护凭证、尚未确认的保留句柄 |
归档提交或确认 |
office |
fiscal_office_key(&institution) |
权限绑定和审计绑定、任命政策和提案、修订号与结果环,每个常驻周期的常驻账册数、分区键和已归档合计,周期水位线,它各范围的机制,能看到它的观察者及各自的首行、粒度和节律种类 | 绑定、提案或任命行动;它的某个范围开立某周期的第一本账册或第一个分区键;归档提交 |
ledger |
范围绑定 ID | 范围绑定及其机构键和名次、修订号与结果环、采纳、每个常驻核算周期一本 FiscalCycleBook、周期水位线 |
针对该范围的行动、凭证、关闭或归档提交 |
jurisdiction |
目录地区 ID | 修订号与结果环、各机制的范围数、各法规的采纳数、转型候选 | 该地区的采纳变化或转型、移动了候选的语境变更 |
observer |
观察者绑定 ID | 绑定、修订号与结果环、行布局、周期槽位、每个已发布分段的摘要值 | 向该观察者发布、改指、移除 |
directory |
dir. 加 3 位十六进制 |
摘要落在这个桶里的每个绑定、政策和提案 ID 所属的机构 | 绑定、政策或提案创建、移动或移除 |
index |
index.、所有者摘要和页码 |
一个机构按名次排列的范围,或一个地区的范围,每页 1,024 个 | 创建后不再写入 |
report-queue |
queue. 加 3 位十六进制 |
绑定 ID 摘要落在这一页的每个观察者被欠的报告工作,每个条目带到期时间 | 观察者看到的账册有变化;发布;按间隔发布的报告在下一个到期时刻重新排队 |
report-schedule |
primary |
每个报告队列页最早的到期时间,以及含有被搁置条目的页 | 某页最早的到期时间或搁置条目数变化 |
用 FiscalState::new 和各个 add_* 辅助函数(add_scope、add_adoption、add_assessment、add_authority_binding、add_audit_binding、add_appointment_policy、add_audit_proposal、add_observer)构造状态,然后调用 FiscalState::into_records(&catalog):它把所有派生字段算到最新(名次、计数、候选、机构周期记账、观察者布局、报告日程记录中各页的到期时间和根记录中最早的到期时间),校验状态,再拆分成记录,包括目录桶、索引页、队列页和日程记录。你自己填的派生字段会被重新计算,不会被采信。激活时对每个新场景和恢复的快照运行 audit_fiscal_records,所以不是有效状态的精确拆分的记录集合,在创建运行时就会被拒绝。load_fiscal_state 为上层应用拼出运行中已提交的状态,开销与财政记录数成正比;fiscal_root_reference、fiscal_office_reference、fiscal_ledger_reference、fiscal_jurisdiction_reference、fiscal_observer_reference、fiscal_archive_reference 和 fiscal_report_schedule_reference 指向单条记录。
每条记录都有字节预算,结算时按扣除 FISCAL_RECORD_RING_RESERVE_BYTES(4 KiB)后的预算检查,这部分预留给记录的结果环:范围明细为 MAX_FISCAL_LEDGER_RECORD_BYTES(256 KiB,按载荷精确测量),财政机构记录或辖区记录为 MAX_FISCAL_RECORD_BYTES(1 MiB);其余记录由各自的数量上限约束。结果放不下的行动作为它自己的结果被拒绝,原因是 FISCAL_RECORD_BUDGET_EXHAUSTED,这条拒绝记录能放进预留空间;放不下的凭证或历史语境变更以 state_budget_exhausted 被拒绝。一本常驻年度账册的范围明细约 1 到 2.5 KiB,所以只有多年账册不归档,范围明细才会触及预算:及时关闭并归档。财政机构记录按它的字节数加上常驻周期为归档提交预留的空间来检查,所以归档提交永远不会让它超出预算,已结算的运行总能恢复。
每个上层应用辅助函数都通过 canwu_api::CanwuRun 接受任一运行句柄,Canwu 或封存过的 CompactedCanwu:fiscal_action_target、fiscal_target_revision、fiscal_recent_outcome、enqueue_execution_receipts、load_fiscal_state、audit_fiscal_records、fiscal_reports_of_holder、enqueue_fiscal_archive 和 finalize_fiscal_archive_retention。它们只读当前记录和当前版本索引,所以封存过证据的运行与它未封存的派生分支读取和结算完全一致。
运行过程:从应征义务到凭证
Section titled “运行过程:从应征义务到凭证”查看图表源码
flowchart TB
Adopt["法规在范围内可执行<br/>FiscalAdoptionState"] --> Assess["OpenAssessment<br/>开立本周期的账册"]
Assess --> Remit["GrantRemission<br/>减少应征额"]
Assess --> Auth["AuthorizeExecution<br/>FiscalExecutionRequest"]
Auth --> Adapter["上层应用适配器在自己的领域<br/>转移物资"]
Adapter --> Evidence["证据记录<br/>FiscalExecutionEvidence 载荷"]
Evidence --> Packet["FiscalExecutionReceiptPacket<br/>引用精确版本"]
Packet --> Receipt["FiscalExecutionReceipt<br/>数量与处置取自证据"]
Remit --> Book["范围明细<br/>FiscalCycleBook 合计"]
Receipt --> Book
Book --> Report["第 13 阶段<br/>以知识形式发布的持有人报告"]
Book --> Close["CloseAccountingCycle"]
Close --> Archive["财政归档<br/>上层应用的存储"]
法规在某个范围内被采纳,就形成了应征义务;核算把一个核算周期内应缴多少固定下来,并在范围明细里开立该周期的账册。引擎按下面的顺序运行这条流程:
-
安装。 场景带上编译好的目录(
CompiledFiscalCatalog::into_record)和初始状态的记录(FiscalState::into_records)。注册FiscalPlugin::new(evidence_kinds);如果绑定用到代理角色或审计人,再加上.with_authority_basis_kinds(kinds);如果你的边界可能没有任何事件就过去了,加上.with_report_cadence(cadence),即唤醒节奏(wake-fiscal-reports-on-cadence-v1运行所用的SystemCadence,不是绑定的报告节律);要改报告份额或报告记录的留存策略,用.with_report_budget(...)和.with_report_retention(...)(见报告如何发布)。这几个选项都记在插件身份里:请在第一个边界之前定下来,换了设置再恢复或重放会被拒绝。激活时审计每条财政记录,所以财政记录集合无效或不完整的模拟运行在创建或恢复时就会被拒绝,不会等到某个边界才出错。绑定引用的权限依据如果属于插件不会读取的种类,激活同样会被拒绝;首次报告派生放不下的状态也会被拒绝。 -
提交。 用
fiscal_target_revision读取行动目标的修订号(或先用fiscal_action_target找到某个绑定和行动的目标),在同一绑定下构造包含一个或多个条目的FiscalActionBatch(单个条目用FiscalActionBatch::single),用fiscal_action_command包装成apply_fiscal_action_v1的领域命令,再用Canwu::enqueue_command排队;旧式的直接命令路径会以MixedCommandIngress失败。每个条目带有行动 ID、目标的expected_revision和行动本身。一个机构要在同一个核算步骤里核算许多范围时,把它们放进一个批次。一个人在同一时刻替几个机构行事时,用fiscal_action_set_command(apply_fiscal_action_set_v1)把这些批次作为一个FiscalActionBatchSet提交,它们在同一个边界准入,并在下一个边界全部结算(见批次组)。 -
准入。 在准入该命令的结算边界上,处理函数读取目录桶、机构记录和每个条目的目标记录,并在遇到以下情况时拒绝整个批次,指出第一个不能准入的条目的序号和原因(
entry 3: ...):- 条目超过
MAX_FISCAL_BATCH_ENTRIES(4,096)个,批次超过 4 MiB,单个条目超过 32 KiB,行动 ID、绑定 ID 或新建条目的 ID 不规范,自由文本超过 2,048 字节,单位或被引用的 ID 超过 160 字节,或者同一个目标下同一个行动 ID 出现两次(InvalidPayload); - 行动 ID 的结果还在目标的结果环中(
IdempotencyConflict),修订号高于目标的修订号(ValueOutOfRange,FISCAL_REVISION_UNKNOWN),或不高于目标的结果下限(ValueOutOfRange,FISCAL_REVISION_EXPIRED),这就是已被挤出结果环的行动重发时被拒绝的方式; - 签发者在绑定之下没有任何身份,或者所持身份不能发起这种行动;绑定的机构不拥有行动涉及的范围,包括
ApplyTransition中不存在的目标范围;权限依据已不是其记录的当前版本;或者任命行动不被该机构的任命政策允许(以上均为InvalidAuthority); - 核算、行动范围、审计目标或折纳报价不存在,改绑或提名中写到的人物在模拟运行中不存在,或者新的权限依据属于插件不会读取的种类(
EntityNotFound)。报价必须是其记录的当前精确版本或初始场景中的版本,且必须属于插件会读取的种类。初始场景之外的已被取代版本按不存在处理,因此用seal_evidence封存过的模拟运行与其精确重放接受的报价相同。
引擎把每次拒绝都记为
Canwu::command_attempts中被拒绝的尝试,边界照常提交,也不占用任何行动 ID。准入不把修订号与条目实际结算时的状态比较,这项检查在结算时进行。随后处理函数安排一条内部的fiscal_action_v1规范化输入(类别Decision),其中带着批次和签发者当时所持的身份。上层应用不能自己排入这种数据包:这个类型是内部输入,并且数据包只有在它的输入记录把它所携带的命令记为原因时才会结算。 - 条目超过
-
结算(第 7 阶段)。 下一个边界上,
settle-fiscal-ingress-v1在DomainDeltaProposal阶段运行。它先结算归档提交和保留确认,再按准入顺序结算其他已准入的数据包。它把批次条目按目标分组,每个目标只解码一次;对每个条目,判断是否过期(见行动批次与修订号),对照机构记录重新检查签发者的身份和权限依据,在上一个条目留下的记录副本上执行行动,再校验它改动的每条记录。结果以Applied或Rejected记入目标的结果环,目标的修订号加一,每个条目发出一个canwu.fiscal.action_settled.v1事件。下一个条目从最近一组有效记录继续,无论各条目结果如何,边界都照常提交。 -
凭证与语境。 同一个系统还结算
fiscal_execution_receipts_v1(用enqueue_execution_receipts提交、每包最多 4,096 张凭证的数据包,类别Acknowledgement,事件canwu.fiscal.execution_receipt_recorded.v1)和fiscal_historical_context_v1(用fiscal_historical_context_ingress构造、enqueue_plugin_ingress排队,事件canwu.fiscal.historical_context_changed.v1)。每张凭证各自结算或各自被拒绝,被拒绝的凭证或语境变更什么也不改变:它留下的唯一记录是一个canwu.fiscal.ingress_rejected.v1事件,事件摘要写明数据包,并以稳定的原因结尾(见凭证)。上层应用能发送的任何内容都不会让边界失败。 -
派生字段。 结算写入每条涉及的记录时,派生字段都是最新的:范围明细的账册带着合计,机构记录带着周期记账,辖区记录带着计数和转型候选。没有单独的派生系统,也没有保存的聚合;报告行直接从范围的账册读出。
-
报告(第 13 阶段)。
materialize-fiscal-reports-v1在PerspectiveAndReportMaterialization阶段运行:对报告已到期、排队的工作又能放进边界剩余发布预算的每个观察者,发布摘要和各行有变化的分段,详见知识与可见性。
两个系统都是事件驱动的,只要边界准入了事件或任何输入就会运行,并且都先用开销很小的读取判断自己有没有事可做:结算系统在本边界没有准入财政输入时直接返回,报告系统读根记录,没有报告到期、也没有搁置条目就直接返回。因此,与财政程序无关的边界只读根记录,不管这个世界有多大。如果你的边界可能没有任何事件就过去了,注册 FiscalPlugin::with_report_cadence(cadence):这一类别中没有事件的每个边界上,小系统 wake-fiscal-reports-on-cadence-v1 读根记录,有报告到期时就安排一条立即到期的内部排空数据包,让报告系统在同一时刻的下一个边界运行。这里不接受 SystemCadence::EventDriven;这个唤醒节奏会记入插件描述;报告系统本身仍是事件驱动的,唤醒节奏也不是绑定的报告节律,后者决定的是一份报告何时到期。
在明代示例程序中,核算命令在第 1 个边界准入、第 2 个边界结算,授权占用第 3、4 个边界,适配器在第 5 个边界写入证据,凭证在第 6 个边界结算。
FiscalAction |
目标 | 效果 | 主要检查 |
|---|---|---|---|
ChangeAdoption |
范围明细 | 新建采纳或改变其阶段;每次变更,代次加一 | 法规的机制与范围一致,且法规覆盖该范围的辖区;新建采纳要求当前年份在法规的法定年份范围内,也不能让转型目标直接以可执行阶段起步;每个范围明细最多 64 项采纳 |
ApplyTransition |
辖区记录 | 在一个辖区内把目标法规设为 Implemented,并在同一辖区停用被该转型取代的法规,一次行动完成;采纳发生变化的每个范围明细的修订号加一 |
每条目标法规恰好绑定一次;目标范围同属一个辖区;该转型当前是这个辖区的候选;绑定拥有全部目标范围 |
OpenAssessment |
范围明细 | 记录数量、单位、支付形态、核算周期和可选的折纳报价;范围还没有该周期的账册时开立一本 | 年份在法规的法定年份范围内;采纳可执行;法规允许该支付形态;同一范围、核算周期、单位和支付形态只能有一笔核算;周期高于该范围的两条水位线;每个范围明细最多 64 本常驻账册,每个机构最多 64 个常驻周期;每个机构、机制和周期最多 4 个分区键 |
GrantRemission |
范围明细 | 在打开的账册中减少应征额 | 减免总额不超过应征数量;每本账册最多 256 笔 |
AuthorizeExecution |
范围明细 | 在打开的账册中创建一条 FiscalExecutionRequest,种类为 Collect、Remit、Disburse、Reserve 或 Return |
单位与核算一致;来源和目标不同;机构是其中一方;Collect 请求合计不超过应征减去减免;每本账册最多 256 条 |
RecordAudit |
范围明细 | 在打开或已关闭的账册上记录一条带 FiscalAuditSeverity 和证据的审计结论;结论写明由谁在哪个绑定下录入,以及审计人由谁任命和提名 |
目标是该范围的一笔核算、一条请求或一张凭证;在拥有该范围的机构的权限绑定之下发起,或在针对该机构的审计绑定之下发起;每本账册最多 256 条 |
CloseAccountingCycle |
范围明细 | 关闭一个范围的一本账册,此后它只接受审计结论,并可以归档 | 账册存在且处于打开状态;在该范围所属机构的权限绑定之下发起 |
RebindAuthority |
财政机构记录 | 同时更换一个权限绑定的常设角色、代理角色和权限依据 | 见权限改绑 |
RebindAudit |
财政机构记录 | 作为被审计机构自己的直接决定,安装、更换或移除审计绑定 | 只有该机构的任命政策让它自己担任任命机构时才允许 |
NominateAuditor、PetitionAuditorRemoval、DecideAuditAppointment |
财政机构记录 | 提名候选人、请求罢免审计人,以及对提案作出决定或直接决定 | 见审计绑定 |
RebindObserver、RemoveObserver |
观察者记录 | 把观察者绑定改指到另一名角色或持有人,或移除它 | 在绑定的 controlled_by 机构的权限绑定之下发起,签发者是该绑定的常设角色或以整体名义作决定的机构;没有控制机构的绑定不能改变 |
GrantRemission、AuthorizeExecution 和 RecordAudit 都在 scope_binding_id 中写明范围,因为对象 ID 只在一个范围的常驻对象中唯一,不是全状态唯一。账册中的每个对象都在 source_action_id 中记录创建它的行动。
FiscalExecutionReceiptPacket 包含凭证 ID、它结算的范围、请求 ID 和 1 到 32 个精确证据版本。插件逐个检查:版本的种类在根记录获准的证据种类中;这个精确版本是其记录的当前版本,或初始场景中的版本;载荷能解码为 FiscalExecutionEvidence,并且其中的范围、请求、单位、支付形态、执行种类、资源、来源和目标都与请求一致。所有被引用记录的处置结果相同,数量合计不超过请求数量,建立时间都不早于请求。凭证的数量和 FiscalReceiptDisposition 取自这些证据。Fulfilled 和 Partial 计入合计,Rejected 和 Excused 的数量为零。一个精确版本在它写明的每个范围内最多结算一张凭证,一个 (evidence kind, external_operation_id) 组合在该范围的常驻账册内最多结算一张。以相同内容重复提交同一张凭证,状态保持不变。
汇总执行证据。 一次操作服务几个范围时,例如一个机构在一个征收日跨它的各个范围征收,可以只写一条证据记录。它的 scope_binding_id 和 request_id 留空,改为列出 parts:最多 MAX_FISCAL_EVIDENCE_PARTS(4,096)个证据分项(FiscalExecutionEvidencePart),每个范围至多一个,各写明它结算的请求和数量;各分项数量之和等于记录的 quantity,记录的其余字段(例如单位、支付形态、执行种类、资源、来源、目标和处置结果)适用于每个分项。每个分项发一张凭证,各写自己的范围和请求,引用同一个精确版本;这些凭证可以放在同一个数据包里。每张凭证按上面的规则结算写明其范围的分项,数量取该分项的数量。分项写明的范围或请求没有对应的常驻请求时,以 request_unknown 拒绝;记录里没有该凭证范围的分项,或分项写的是另一个请求时,以 claim_mismatch 拒绝;分项数量超过请求尚可授权的数量时,以 quantity_exceeded 拒绝。记录在分项之外还写了顶层范围或请求、同一范围有两个分项、分项过多或分项合计不等于其数量时,它不能结算任何凭证(claim_mismatch)。每个分项的凭证各自结算或被拒绝。
凭证通过 enqueue_execution_receipts 以每包最多 MAX_FISCAL_RECEIPTS_PER_PACKET(4,096)张的数据包提交,按顺序结算,每张对照它所在范围的范围明细。结算不能接受的凭证以 canwu.fiscal.ingress_rejected.v1 被拒绝,后面的凭证照常结算:
| 原因 | 何时 |
|---|---|
malformed_payload |
数据包无法解码 |
request_unknown |
该范围没有这个 ID 的常驻请求(从来没有,或者它的账册已归档) |
cycle_closed |
请求所在的账册已关闭 |
evidence_count_invalid、evidence_kind_unapproved、evidence_not_current |
没有证据或超过 32 个版本;根记录未获准的种类;被引用的版本不是其记录的当前版本,例如记录在数据包排队之后发生了变化 |
claim_mismatch、quantity_exceeded |
声明与范围、请求或其他声明不一致,或早于请求,或汇总证据没有该范围的分项、不合其规则;凭证合计会超过请求授权的数量 |
operation_reused、evidence_reused、receipt_id_conflict |
已被该范围常驻账册中的凭证结算过;某张常驻凭证已占用这个 ID 且内容不同 |
state_budget_exhausted、boundary_limit |
范围明细会超出字节预算;本边界已写了 65,536 个范围明细(请重新发送) |
context_outside_pack、invalid_packet |
语境年份超出内容包范围;其他任何会让状态无效的情况 |
被拒绝的凭证不消耗任何东西,所以改正后的凭证可以稍后结算;被引用的记录变了,就引用它的新版本。引用只可创建的证据种类,待结算的凭证才能保持当前。同一个范围内每个请求 ID 在整个运行期内只用一次:在之后的账册里重复使用某个请求 ID,而证据的模拟时间又与已归档的同 ID 请求的证据相同时,可能再次拿到那份证据。
行动批次与修订号
Section titled “行动批次与修订号”每条范围明细、财政机构记录、辖区记录和观察者记录都有自己的修订号。记录上的每个结果(无论应用还是拒绝)和每张凭证都让该记录的修订号加一;转型让采纳发生变化的每个范围明细的修订号加一。归档维护、报告记账和历史语境变更不改变任何行动目标的修订号,所以都不会让待决的决策过期。
只有当条目的 expected_revision 等于结算它的边界开始时目标的修订号,条目才会应用。因此,只有行动自己的目标在签发者查看之后发生了变化,行动才算过期:其他范围或机构上的活动不影响它,一个机构把它各范围的核算放在一个批次里提交时,无论别处发生了什么,所有条目都一起结算。同一个边界内,条目按准入顺序结算,每个条目都在前面的条目留下的记录上校验。后面的条目可以建立在前面条目创建的内容之上(前一个条目刚让某项采纳可执行,后一个条目就可以据此开立核算),但如果它要覆盖本边界更早的行动改动过的采纳、绑定、政策、提案或观察者(包括转型改动的),就会作为过期被拒绝。只新建内容的行动彼此不冲突:同一笔核算上的两项减免,只要合计仍能通过校验,就都会应用。在同一边界结算的凭证也不会让条目过期。
行动 ID 按目标计。每个目标在结果环中保留最近 FISCAL_RECENT_OUTCOMES(8)个结果,并记一个结果下限,即最近被挤出结果环的结果的修订号。准入拒绝还在结果环中的 ID 和不高于结果下限的修订号;结算则跳过目标结果环已持有、或本边界已结算过的行动副本,既不记结果也不发事件;所以一个行动跨边界和边界内都最多应用一次。每个结果都永久记录在它的 canwu.fiscal.action_settled.v1 事件中,FiscalSettledAction::from_events 可以读回,fiscal_recent_outcome 回答仍在结果环中的结果。过期或被拒绝的行动请换一个新的行动 ID,按你读到的修订号重新提交;永远不要预测修订号。
任何行动都不会连累同一边界的其他行动。每个上限都把越过它的那个行动作为它自己的结果拒绝,带稳定的原因,边界照常提交:
| 上限 | 值 | 原因 |
|---|---|---|
MAX_FISCAL_ACTIONS_PER_BOUNDARY |
65,536 个已应用的行动 | FISCAL_BOUNDARY_ACTION_LIMIT |
MAX_FISCAL_LEDGER_WRITES_PER_BOUNDARY |
65,536 个范围明细,行动和凭证合计 | FISCAL_BOUNDARY_ACTION_LIMIT(凭证为 boundary_limit) |
MAX_FISCAL_ACTIONS_PER_RECORD_PER_BOUNDARY |
同一目标上 64 个已应用的行动 | FISCAL_RECORD_ACTION_LIMIT |
MAX_FISCAL_PARTITION_KEYS |
每个机构、机制和周期 4 个分区键 | FISCAL_PARTITION_KEYS_EXHAUSTED |
MAX_FISCAL_HOT_BOOKS_PER_LEDGER、MAX_FISCAL_HOT_CYCLES_PER_OFFICE |
每个范围明细 64 本账册,每个机构 64 个周期 | FISCAL_HOT_BOOKS_EXHAUSTED |
| 记录字节预算 | 见记录与字节预算 | FISCAL_RECORD_BUDGET_EXHAUSTED |
过期或未通过权限检查的行动被拒绝时不占用任何上限。这些都不是整个运行期的总量上限:关闭并归档账册就能释放它们。
一个 FiscalActionBatchSet 带 1 到 MAX_FISCAL_BATCH_SET_BATCHES(64)个批次,合计至多 MAX_FISCAL_BATCH_SET_JSON_BYTES(16 MiB)、65,536 个条目,每个批次各自不超过批次的上限。准入像 apply_fiscal_action_v1 准入一个批次那样,在批次组唯一的签发者之下逐个准入各批次,并按顺序为每个批次排一个 fiscal_action_v1 数据包;跨批次为同一目标两次写明同一个行动 ID,或者任何一个批次的准入会拒绝的情形,都会拒绝整组,消息以 batch N: 开头。下一个边界结算这些批次的方式,与结算同一边界准入的几条批次命令完全相同:每个条目对照目标在该边界开始时的修订号,受同样的单边界上限约束。所以改动不同记录的批次,留下的账册和结果与这些批次一轮接一轮发送时相同,只是总共只用两个边界,而不是每个批次各用两个。签发者必须在每个批次的绑定下都有身份;本人名义决策可以做到,见权限。
核算周期与归档
Section titled “核算周期与归档”每个范围的范围明细为每个常驻核算周期保存一本 FiscalCycleBook,每个机构的财政机构记录按周期统计常驻账册数。核算周期 ID 由上层应用决定,与历史时期相互独立;它们按字节顺序比较,所以请让它们按时间排序("1644"、"1644-03")。
OpenAssessment开立某个周期的账册。CloseAccountingCycle关闭一个范围的一本账册;关闭的账册只接受审计结论(核算、减免或授权为FISCAL_CYCLE_CLOSED,凭证为cycle_closed)。账册可以按任意顺序关闭和归档。- 归档提交把已关闭的账册从范围明细移入财政归档。它把每个范围的水位线提高到它归档的最大周期;在归档某个机构某周期最后一本常驻账册的那次提交中,还把该机构这个周期的合计归档一次,并提高机构水位线。不在任何一条水位线及以下开立新账册(
FISCAL_CYCLE_ARCHIVED),所以一个核算分区(范围、周期、单位、支付形态)在整个运行期内最多核算一次,而无需读取归档。低于机构水位线的常驻账册不受影响。 - 归档由上层应用完成,协议与资源归档和生产归档相同:
prepare_fiscal_archive(&run, book_limit, &store)按(范围、周期)顺序选出已关闭的账册,最多MAX_FISCAL_ARCHIVE_COMMIT_RECORDS(4,096)条记录、每个 blob 64 条,构造PreparedFiscalArchiveBatchV1的 blob、成员页、时间页、查找索引页和目录;enqueue_fiscal_archive(&mut run, &prepared, &store)重新准备一次并要求两者相同,把它们写入你的FiscalArchiveStore并校验,持久化保留句柄,再把提交排入队列(fiscal_archive_commit_v1);提交结算后,finalize_fiscal_archive_retention排入确认。准备和排队要在同一个边界间隙内完成。这几个辅助函数接受任何运行句柄(CanwuRun),只读一次提交需要的记录:根记录、归档头、每条范围明细中足以判断它是否有已关闭账册的那一部分,以及本次提交选中账册所在的范围明细和机构记录;确认时只读归档头。归档头持有MAX_FISCAL_ARCHIVE_PENDING_HANDLES(64)个未确认句柄时,准备在读取任何范围明细之前就被拒绝(FiscalArchiveError::Backpressure)。 - 封存切点来自模拟运行本身。引用了运行最近一次封存之后建立的证据版本的账册,会在准备时被拒绝(
FiscalArchiveError::UnsealedCitations),直到某次封存覆盖它为止,因为归档账册会结束范围明细对它引用的折纳报价和凭证证据的载荷保留声明。辅助函数通过sealed_evidence_cursor()(Canwu、CompactedCanwu和任何CanwuRun都有)读取这个切点:运行最近一次封存的终点;运行在创建或恢复之后尚未封存时为None。尚未封存的运行(每个Canwu、从未封存的CompactedCanwu、带完整日志恢复或从接续检查点恢复且尚未再次封存的运行)不会拒绝任何账册:接续恢复不持有任何已封存的日志段,所以它的起点切点不算封存。会封存的上层应用要想让每处已归档的引用都落在已封存的日志段里,就应当在每次存档时先封存,再写接续检查点;只用接续检查点存档、从不封存的上层应用,在每个会话里归档都不会遇到这种拒绝。 - 提交先于所在边界的其他财政数据包结算,并且只对照它绑定的记录检查:归档头、每本选中账册的修订号,以及它们所属机构受影响的周期条目。其他账册上的行动、凭证或提交不影响它。从旧账册准备的提交结算为失效的空操作,留下凭证和一个由上层应用确认的句柄;归档头已持有其句柄的提交副本什么也不记录;两者都记在各自的
canwu.fiscal.archive_maintenance.v1事件里。确认把它的句柄退掉一次。两者都不改变任何修订号。超过归档头余量(64 个未确认的失效句柄)的失效提交,以及不匹配的提交,以拒绝事件被拒绝,什么也不改变;这样的提交在状态中没有保留句柄,所以finalize_fiscal_archive_retention对它返回错误,由上层应用自己放弃存储中的那份副本。 - 没有任何边界读取归档存储。准入、结算、幂等和分区唯一性都只用常驻记录,所以无论存储能否读取,运行的结算完全相同,恢复和精确重放也不需要存储。只有上层应用的归档操作读取存储:
archived_fiscal_record通过累积的归档查找索引按FiscalArchiveKeyV1查找已归档的账册、某机构某周期的合计或维护凭证,audit_fiscal_archive对照归档头检查整个归档。()是一个什么都不存的存储。
每本已关闭账册的周期合计随账册一起归档,机构在某周期的合计恰好归档一次。明代财政案例展示了 close_ming_fiscal_sample_cycle 和 archive_ming_fiscal_books,它的 archive_century 测试按归档节奏运行一个世纪,财政记录始终只有几 KB。
历史语境与改革
Section titled “历史语境与改革”历史财政语境包含 year 和 mode。RecordedBaseline 和 ResearchReplay 看转型的 observed_window,Counterfactual 看它的 eligibility_window。满足以下条件时,转型成为某个辖区的 FiscalTransitionCandidate:年份符合;该辖区有目标机制对应的范围;如果转型列出了来源法规,其中一条已在该辖区采纳且未废止;目标法规尚未全部可执行;前置转型的目标在该辖区已可执行。结算让每条辖区记录的候选保持最新,语境变更只重写候选发生移动的辖区记录。候选只有通过经授权的 ApplyTransition 才会生效。超出内容包范围或状态不接受的语境变更以事件被拒绝,什么也不改变;被接受的语境变更让根记录的 context_revision 加一,不会让任何行动过期。
FiscalAuthorityBinding 指定一个 institution(EntityRef)和一名常设的 authorized_actor,还可以指定一名 acting_actor 及其 authority_basis。准入接受两种签发者:
| 签发者 | 准入条件 |
|---|---|
Issuer::Actor(actor),没有决策控制者 |
actor 是绑定中的常设角色或代理角色,或者是适用的任命政策列出的人物 |
Issuer::Human 或 Issuer::Ai,且等于已校验的 decision_controller_id |
CommandAuthority 中的命令主体是绑定的机构,并且决策来源是指向某个绑定角色的 DecisionOrigin::Actor,或者是该机构的 DecisionOrigin::Institution,其负责角色已在绑定中 |
批次组还接受本人名义决策:来源是 DecisionOrigin::Actor { actor }、命令主体是 EntityRef::Person(actor) 的已校验决策。准入在每个批次的绑定之下都把这种决策当作此人直接签发,所以身为几个机构常设角色或代理角色的人,用一个批次组就能涵盖这些机构。在权限绑定下,它持有此人所持的常设或代理身份;在审计绑定或任命政策下,它以此人作为审计人或名单所列人物的身份行事;它绝不持有机构身份。批次命令不接受这种决策。只有当这个控制者可以动用此人的全部财政身份时,才注册这样的控制者(DecisionAuthority::Actor,命令主体为此人本人):只要某张票据向它提供批次组命令,它就能在此人任职的每个机构中行事。
绑定的机构还必须拥有行动涉及的范围;对 ApplyTransition 来说,要拥有全部目标范围;对观察者行动来说,它必须是绑定的 controlled_by 机构。代理角色需要一个精确的权限依据版本,而且必须与常设角色不同。权限依据是一条外部记录,其种类必须是插件会读取的:用 with_authority_basis_kinds 声明的种类,或者传给 FiscalPlugin::new 的证据种类。只要这个版本仍是其记录的当前版本,代理角色就能被准入。一旦记录更新到新版本或已退役,代理角色会以 FISCAL_ACTING_BASIS_NOT_CURRENT(InvalidAuthority)失败,常设角色仍可准入。
每个被准入的批次都记下签发者当时所持的身份(FiscalIssuerRole:Principal、Acting、Institution、Auditor 或 ListedActor)。结算时对每个条目都对照机构记录把这个身份再检查一遍,本边界更早的条目改动了该机构记录之后也会重新检查:角色在此期间已被换掉的,条目以 FISCAL_ISSUER_NOT_BOUND 被拒绝;代理角色的权限依据已不是当前版本的,以 FISCAL_ACTING_BASIS_NOT_CURRENT 被拒绝。是否为当前版本,读的是一份封存证据也不会改变的当前版本索引,所以封存过的模拟运行与其重放的判断完全一致。
范围绑定来自初始场景,没有哪个财政行动能修改它们。权限绑定、审计绑定和观察者绑定同样从场景开始,运行中只能通过下面的行动改变。
授权续期或职位换人时,用 FiscalAction::RebindAuthority 在一次提交内同时更换绑定的常设角色、代理角色和权限依据:
FiscalAction::RebindAuthority { authority_binding_id, authorized_actor, acting_actor, authority_basis,}- 谁可以发起。 请求必须在被更换的那个绑定之下发起,签发者只有两种。一种是绑定的常设角色,本人直接发起或作为决策来源均可。另一种是以整体名义作决定的机构:一个决策控制者,其命令主体和决策来源都是绑定的机构,并且没有负责角色。代理角色不能改绑,其他任何人也不能。
- 机构身份就是任命权。 这样的控制者可以指定任何角色,也可以一个都不指定;如果绑定没有常设角色,它自己就以常设身份行动。所以,只应为真正负责任命这些角色的机构注册这种控制者。
- 人物与依据。 行动中写到的每个人物都必须存在,缺少任何一个,命令都会以
EntityNotFound被拒绝。acting_actor和authority_basis必须同时设置。依据在命令准入时必须是其记录的当前版本,在结算边界开始时的状态中也必须仍是当前版本,否则行动以FISCAL_AUTHORITY_BASIS_NOT_CURRENT被拒绝。 - 交接在一次提交内完成。 此前准入的条目仍按准入顺序结算:排在改绑之前的按原来的角色结算,排在之后的被拒绝。新角色从提交之后的第一次准入起生效。任何一个已提交的状态里,都不会出现新旧角色同时有效或同时无效的情况。
- 留痕。 绑定在
rebound_by(FiscalRebinding)中记下这次改绑:行动 ID、时间,以及签发者所用的绑定、身份和人物。
FiscalAuditBinding 让机构之外的审计人可以对这个机构记录审计结论。它保存在被审计机构的财政机构记录中,与权限绑定并列,写明被审计的 institution、审计人 auditor,以及审计人自己的 authority_basis。请求仍在 authority_binding_id 字段中写审计绑定的 ID;绑定 ID 和政策 ID 共用一个命名空间。
在审计绑定之下:
- 只准入
RecordAudit,而且目标所在的范围必须归被审计机构所有; - 签发者必须是审计人本人,或者是针对被审计机构的一项已校验决策,由审计人作出或由审计人担任负责角色;
- 依据在准入时必须是其记录的当前版本,在结算边界开始时的状态中也必须仍是当前版本,否则以
FISCAL_AUTHORITY_BASIS_NOT_CURRENT被拒绝。
审计人不会因此获得其他财政权限:判断谁能代表机构行动时,根本不会查看审计绑定。每条结算后的结论都在 FiscalAuditFinding::entered_by 中写明由谁录入(绑定、身份、人物和依据),并在 appointed_by 和 nomination 中保留审计人由谁任命、由谁提名,与绑定当时所记一致。
谁任命和罢免一个机构的审计人,由它的审计任命政策决定:随状态用 FiscalState::add_appointment_policy 声明的 FiscalAppointmentPolicy,每个被审计机构最多一份:
- 任命机构是其决定负责任命和罢免的机构。它在自己的某个权限绑定之下,以该绑定的常设角色或整体名义,用
FiscalAction::DecideAuditAppointment { audit_binding_id, decision }作决定;代理角色永远不能作决定。决定可以是AcceptNomination(按提名时的依据任命某条待决提名的候选人,依据必须仍是当前版本)、Appoint(直接任命,需要appoints_without_nomination)、AcceptPetition(罢免请求所指的审计人,此人必须仍持有该绑定)、Remove(直接罢免,需要removes_without_petition)或Dismiss(驳回一条待决的提名或请求)。 - 提名方和请议方是在自己的某个权限绑定之下行动的机构,或政策按姓名列出、在政策自身 ID 之下行动的人物(
FiscalIssuerRole::ListedActor)。提名方用NominateAuditor { nomination_id, audit_binding_id, institution, candidate, authority_basis }提出候选人和权限依据,绑定可以尚不存在。请议方用PetitionAuditorRemoval { petition_id, audit_binding_id, grounds }根据证据请求罢免某个绑定当前的审计人。 - 任期。 用另一人替换在任的审计人既是罢免也是任命,所以需要
removes_without_petition(否则为FISCAL_PETITION_REQUIRED);政策保护在任审计人时,任命机构先接受针对他的罢免请求,再填补空缺。向空缺任命,或按续期的依据再次任命在任的审计人,不罢免任何人,只需要任命规则(需要提名而没有提名时为FISCAL_NOMINATION_REQUIRED)。 - 提案。 每条提名和罢免请求都是被审计机构记录中的一条
FiscalAuditProposal,记有提出它的一方和签发者,在结案之前一直待决,并且只结案一次:被任命机构接受或驳回,或者被取代。同一方对同一绑定提出的同类新提案取代它原来的待决提案;一次任命取代该绑定上待决的其他提名,只有在它替换了请求所指的审计人时才取代请求;一次罢免取代其他待决请求,提名则继续等待空缺。每一方在一份政策下最多有MAX_FISCAL_PENDING_PROPOSALS_PER_PARTY(4)条待决提案,所以谁也占不了别人的名额(超过时为ValueOutOfRange);每份政策保留最近MAX_FISCAL_RESOLVED_PROPOSALS_PER_POLICY(64)条已结案提案。 - 拒绝。 每次拒绝都有记录:准入时记为
InvalidAuthority(FISCAL_ISSUER_NOT_LISTED、FISCAL_ISSUER_NOT_APPOINTER、FISCAL_NOMINATION_REQUIRED、FISCAL_PETITION_REQUIRED、FISCAL_APPOINTMENT_POLICY_MISSING)、InvalidPayload(FISCAL_PROPOSAL_NOT_PENDING)、EntityNotFound或ValueOutOfRange;结算时,签发者身份已经消失,或所引用的提案已被本边界更早的条目结案的,记为Rejected结果。
FiscalAction::RebindAudit { audit_binding_id, auditor, authority_basis } 作为被审计机构自己的直接决定,安装一个绑定、更换其中的审计人和依据,或在两者都为 None 时移除它。只有政策让被审计机构自己担任任命机构、并允许它不经提名(安装)、不经请求(罢免)或两者都不经(替换)行动时才会准入,效果与对应的决定相同。其他情况下,被审计机构不能改变自己的审计人(FISCAL_ISSUER_NOT_APPOINTER);没有政策的机构只有随状态安装的审计绑定(FISCAL_APPOINTMENT_POLICY_MISSING)。
通过席位、投票和程序阶段作决定的机构,参见社会、文化与法律。
知识与可见性
Section titled “知识与可见性”财政记录属于权威状态。上层应用可以通过受信任读取访问它们,例如 typed_domain_record(&fiscal_ledger_reference(scope)),或用 load_fiscal_state 拼出整个状态。玩家和智能体应当读取财政报告,它是持有人相对知识。
FiscalObserverBinding 在 knowledge_holder 中指定谁持有它的报告:actor 本人,或者在没有角色时指定一个政府,这样朝廷或某个部门就能持有不属于任何个人的报告。它列出观察者能看到的机构(最多 4,096 个,其中的行合计最多 16,384 个)、不超过 1,000 的 confidence_per_mille、可选的 controlled_by(可以改指或移除它的机构),以及绑定的 granularity(粒度)和 cadence(报告节律),两者都有默认值,所以不写这两项的场景行为与 0.16.0 相同。请在 FiscalState::add_observer 之前设定它们;改指绑定不改变它们。
FiscalReportGranularity::Scope(默认):报告为它看到的各机构的每个范围各设一行,按绑定中机构的顺序和各机构范围的名次排列。每行列出该范围在某周期账册中的事实,每个单位和支付形态对应一条FiscalReportFact,其中应征、已征和未结各是一个FiscalAmountEstimate区间。FiscalReportGranularity::Institution:它看到的每个机构和该机构各范围的每种机制各设一行,事实是该机构在该周期、该机制下的常驻账册按单位和支付形态求和(每个数额在u64::MAX处饱和,所以求和绝不会让边界失败),再按绑定的置信度呈现。看 21 个各有一种机制的机构的朝廷只读 21 行,每个周期两个分段,不管这些机构有多少范围。求和在每个发布这类分段的边界读每本账册一次。机构行的分段无法在一条知识记录能带的证据之内引用它求和的每个范围明细版本,所以它引用各行所属机构记录的当前版本,并在books_summed中记下求和的账册数:出处比范围分段的弱。
confidence_per_mille |
量级为 M 的数值对应的区间宽度 |
|---|---|
| 900 至 1,000 | M / 100 |
| 750 至 899 | M / 10 |
| 500 至 749 | M / 2 |
| 低于 500 | M |
M 是不超过该值的最大 10 的幂,区间宽度至少为 2,所以每个总数都以区间呈现。置信度为 850 时,应征 100 显示为 100–109,已征 70 显示为 70–71。
用 fiscal_reports 读取报告。视图所属持有人持有的每份观察者绑定报告对应一份 FiscalReport,以绑定 ID 为键;作为受信任的上层应用,fiscal_reports_of_holder(&canwu, &holder) 可以读取任何持有人(包括政府)的报告:
let viewer = canwu.viewer_for_actor(actor)?;for (binding_id, report) in fiscal_reports(&viewer)? { for (fact_id, fact) in &report.projection.facts { // fact.assessed.minimum ..= fact.assessed.maximum, and so on. let may_be_older = report.is_stale(fact_id); }}report.projection 是一个 FiscalProjection:角色(政府持有的报告为 None)、as_of、置信度,以及标为 周期:行:事实 的各条事实。对一份完整的报告来说,它与 compute_projection(&state, binding_id, as_of) 从发布这份报告时的财政记录算出的结果相同。如果你手里已经有记录的载荷,assemble_fiscal_reports 可以直接把它们拼成报告。
报告如何发布
Section titled “报告如何发布”一份报告保存为若干条知识记录,schema 为 fiscal_report_knowledge_schema_id()(种类 canwu-fiscal / fiscal_report,版本 6):一条摘要记录(写明绑定的粒度和节律),加上报告显示的每个常驻周期的若干分段,每段 FISCAL_REPORT_ROWS_PER_PART(16)行(FiscalReportRecord)。分段的载荷把各行涉及的机构列成一张表、只写一次,每一行和每条事实都编码为数组,只有原来三分之一大小;FiscalReportPart 负责解码,单独的 FiscalReportRow 仍是对象形式。单独一条记录只是一条摘要或一个分段,所以整份报告请通过 fiscal_reports 读取。
- 报告行落在观察者记录上。 每条报告记录的主体都是该绑定自己的观察者记录,报告行由这条记录上的角色命名:摘要在第 0 行,槽位
s中周期的分段p在第1 + s * 1,024 + p行。改指或移除一个绑定,其他绑定的报告行都留在原处。 - 每条记录取代本条线上前面的那一条。 报告系统用
SimulationView::knowledge_heads在已提交的知识账本里找到那条记录,所以封存过证据的模拟运行与其重放取代的记录完全相同。范围行的分段把它各行读自的范围明细版本作为证据引用。 - 报告何时到期由绑定的节律决定。默认的
OnChange在改变了它所显示内容的那个边界到期。Interval { every, offset }在自SimTime::EPOCH起第offset + k * every分钟(k >= 0)到期,由每个到期时刻当时或之后第一个运行报告系统的边界发布,内容是上次发布以来的变化;它绝不会因为变化而发布,所以两个到期时刻之间,它显示的是as_of那个边界时的记录。到期时若没有任何变化,只要报告已经发布过至少一次,就只重新发布摘要,这样as_of会推进,没有消息也绝不等于没有报告。FiscalReportCadence::staggered(every, index, stride)用来错开偏移:三十天的间隔配 30 的步幅,观察者i落在第i % 30天。every是固定时长,所以按历法记时的上层应用用OnChange,或者用接近它历法月份的间隔。 - 只发布有变化的部分。 账册有变化时,看到该机构的每个观察者的报告队列页上都会排入账册所在行的分段,这一步只读机构记录里的观察者列表,不打开观察者记录。报告系统只发布排队分段中摘要值有变化的那些,连同摘要一起;观察者记录保存每个已发布分段按绑定置信度算出的摘要值,所以低于观察者精度的变动不会传到他那里。
OnChange观察者的工作在变化发生的时刻到期,边界没有改变他能看到的任何内容时就不向他发布,所以他的as_of是报告最后一次变化的时间。Interval观察者始终有一个队列条目,到期时间是它的下一个到期时刻;结算把工作并进去而不移动它,报告系统发完条目所欠的内容之后,不管有没有发布任何东西,都把它重新排到再下一个到期时刻。不会安排任何未来到期的数据包。没有常驻周期的报告不发布。 - 日程。 报告日程记录保存每个队列页最早的到期时间和含有搁置条目的页,根记录保存所有页中最早的到期时间(
FiscalRoot::earliest_due)。在这个时间之前、又没有搁置条目的边界只读根记录;到了这个时间的边界只解码最早到期时间已到的页。audit_fiscal_records检查从各页到根记录的这条链,并检查每个活动的Interval绑定在它的某个到期时刻上恰有一个条目。 - 周期来去。 一份报告最多显示
MAX_FISCAL_REPORT_CYCLE_SLOTS(64)个周期;超出的周期计入摘要的omitted_cycles,等第一个空出的槽位。观察者看到的某周期最后一本账册归档后,这个周期离开报告:它的各分段在原来的行上发布为空,摘要不再列出它,槽位随之空出。 - 改指与移除。 绑定改指之后,新持有人在下一次发布时拿到整份报告,旧持有人的报告被撤回:它的摘要被一条标为
withdrawn的摘要取代,读取函数不再列出它。对Interval绑定,这两件事都在它的下一个到期时刻发生,所以新持有人最多等every才拿到第一份报告,旧持有人在此之前保留自己的报告。被移除的绑定的报告以同样方式立即撤回,记录本身保留,所以它的标识不会被重复使用。被撤回的持有人保留已经拿到的记录,那是历史。 - 账本里留下什么。
fiscal_reportschema 按上层应用在FiscalPlugin::with_report_retention中选的留存策略注册:默认RetireWhenSuperseded,持有人每条线只保留一条记录,已关闭周期的数字在空分段取代它们之后离开持有人的账本;Keep则让每个被取代的分段仍可用FullHistory读到。要从旧报告记录里读已关闭周期数字的上层应用,请用Keep,或者自己归档收到过的报告。
报告系统在它的报告份额之内发布,即引擎发布预算中给财政的那一份,同时也不超过本边界更早的发布方留下的预算。份额(FiscalReportBudget)按记录正文、条目和载荷字节计。默认是 KnowledgeLimitsV1::CURRENT 的一半:16,384 个正文、16,384 个条目和 32 MiB,足以在一个边界内发完 fiscal-world-profile 基准世界(见限制与预算)中一个有变动的月度周期。FiscalPlugin::with_report_budget 可以在 FiscalReportBudget::MINIMUM(两个正文、两个条目、128 KiB)和引擎上限之间上调或下调。自定义的份额写在报告系统的名称里,所以是插件身份的一部分。每个持有人的记录是指令中的一个组,各组按引擎单条指令的上限打包进指令,所以向几千个持有人发布的边界只有几条指令和几个 KnowledgePublished 事件;分段的切法保证摘要始终有位置。放不下的留在队列里,边界安排一条立即到期的内部 fiscal_report_drain_v1 数据包,同一时刻的下一个边界继续发布;advance_canonical 和 step_canonical 会先结算这个边界再前进,settle_boundary 则拒绝更晚的时间,直到这一时刻的某个边界准入了这个数据包。为此财政记录中只有报告记账发生变化,没有任何修订号前进。队列排空之前,摘要列出待发布的分段,FiscalReport::is_complete 为 false,is_stale(fact_id) 可以告诉你某条事实属于待发布的分段,它可能比 as_of 旧,也可能缺失。保留直接命令历史的运行不能加入规范化输入,这时不安排排空,被欠的分段等下一个运行财政系统的边界。
推迟发布会泄露一点观察者绑定权限之外的信息:哪些分段待发布,取决于排在它前面的报告。
引擎不向已死亡的人物发布知识。观察者的持有人是已死亡的人物时,它排队的工作被搁置,不算到期,也不向它发布任何记录。报告系统每次运行检查根记录游标之后的 FISCAL_PARKED_CHECKS_PER_BOUNDARY(16)个被搁置条目的持有人,按队列页和绑定 ID 顺序循环,把恢复存活的持有人所欠的内容发布出去,所以恢复存活的持有人最多在 ceil(被搁置数 / 16) 次运行内收到报告,每次运行的开销固定;搁置条目不超过 16 个时每次都全部检查。改指或移除被搁置的绑定会解除搁置;只有搁置条目不会唤醒报告系统。政府持有人始终可以接收。持有人不存在、不是其角色本人、或者在没有角色时不是政府的状态,在创建运行时就会被拒绝。
场景的报告队列是它的记录的一部分:add_observer 把绑定的整份报告排到它的第一个到期时刻(对 Interval 绑定,是不早于状态的 historical_context.updated_at 的第一个对齐时刻,所以请先把它设为运行的起点),到期之后第一个运行报告系统的边界在它看到的某本账册常驻之后把报告发布出去。
随机性、持久化与重放
Section titled “随机性、持久化与重放”财政扩展不抽取随机数,也不声明随机流。报告区间来自整数分桶,部分征收之类的结果由你的适配器作为证据带进来。
财政状态存放在记录与字节预算所列的记录中。目录记录只可创建。每条范围明细把它常驻账册中的凭证和折纳报价引用的、运行中产生的精确证据版本标记为需要保留载荷,所以账册常驻期间引擎会让这些载荷始终可以加载;归档账册会结束这项声明,这正是会封存的运行上 prepare_fiscal_archive 拒绝未封存引用的原因。观察者记录引用持有人的人物或政府,所以如果状态写到了不存在的持有人,引擎会拒绝它。改绑、审计绑定、提名和结论的 entered_by 所写的记录版本则没有这样保留:之后不会再有判断读取它们,seal_evidence 之后,其中已被取代的版本可能无法再证明。load_fiscal_state 和 audit_fiscal_records 校验目录、每条记录和跨记录不变量。
每个边界判断都只读常驻财政记录、目录、本边界自己准入的输入、当前版本索引、不受封存影响的记录版本、人物可用性和 knowledge_heads,从不读取保留的证据或归档存储,所以封存过的运行、未封存的派生分支、从已封存日志段恢复的运行和精确重放结算完全一致。插件只安排零延迟的数据包(报告排空),规范化步进会在同一次调用中准入它,所以它不会长时间阻止 seal_evidence;报告的到期时间写在队列条目里,从不写成未来到期的数据包。默认留存策略下,报告记录在发布时清退,实时运行和重放一致,与封存无关。快照和精确重放要求插件身份一致:名称 canwu-fiscal、版本 0.6.0-experimental 和它的语义哈希,哈希涵盖你选定的唤醒节奏(with_report_cadence)、报告份额和留存策略。重放消费记录下来的命令和输入,不会重新运行玩家或 AI 的选择。参考整合包的 restore_ming_fiscal_reference 和 replay_ming_fiscal_reference 注册同样的三个插件,然后运行 validate_ming_fiscal_reference:它接受任一运行句柄,只用封存无法改变的状态把每张凭证与其证据重新核对一遍。
上层应用需要提供什么
Section titled “上层应用需要提供什么”- 内容包:你自己的
FiscalContentPack(可以用 serde 读取 JSON)或明代内容包,以及FiscalContentSelection。 - 初始
FiscalState:年份和模式、证据种类、权限绑定、审计绑定、范围绑定、观察者绑定、任命政策,以及初始采纳阶段,用into_records拆分成记录。 - 场景中的机构实体和角色,例如政府、军队或组织。
- 执行适配器:在你的资源、市场、生产或物流领域完成实际转移,写入写明范围和请求的类型化证据记录,再调用
enqueue_execution_receipts。 - 使用代理角色或审计人时,还要提供权限依据记录及其种类;如果希望机构能够改绑,还要为负责任命绑定角色的机构注册决策控制者。
- 各项决定:提交哪些行动、何时提交、分成哪些批次、数量、单位、核算周期 ID、何时关闭周期,以及何时改变历史语境。
- 如果运行长到已关闭的账册会成为负担,还要提供
FiscalArchiveStore和归档节奏;会封存证据的运行还要提供封存切点。 - 如果需要
validate_ming_fiscal_reference那样的检查,提供恢复时运行的语义校验;此外还有展示报告的界面。
| 常量 | 值 | 限制对象 |
|---|---|---|
MAX_FISCAL_SCOPE_BINDINGS |
1,048,576 | 范围绑定,每个一条范围明细 |
MAX_FISCAL_AUTHORITY_BINDINGS_PER_OFFICE、MAX_FISCAL_AUDIT_BINDINGS_PER_OFFICE |
256 | 一个机构的绑定,两类分别计数 |
MAX_FISCAL_OBSERVERS |
65,536 | 观察者绑定;每个最多看 4,096 个机构、16,384 个范围行,一个机构最多被 4,096 个观察者看到 |
| 每本账册 | 4 笔核算(每个分区键一笔)、256 笔减免、256 条执行请求、1,024 张凭证、256 条审计结论 | 一个范围在一个周期内的对象 |
MAX_FISCAL_ADOPTIONS_PER_LEDGER |
64 | 一个范围内采纳的法规 |
MAX_FISCAL_HOT_BOOKS_PER_LEDGER、MAX_FISCAL_HOT_CYCLES_PER_OFFICE |
64 | 一个范围的常驻账册;一个机构的常驻周期 |
MAX_FISCAL_PARTITION_KEYS |
4 | 每个机构、机制和周期的分区键 |
MAX_FISCAL_BATCH_ENTRIES、MAX_FISCAL_BATCH_JSON_BYTES |
4,096、4 MiB | 一个行动批次的条目数和字节数 |
MAX_FISCAL_BATCH_SET_BATCHES、MAX_FISCAL_BATCH_SET_JSON_BYTES |
64、16 MiB | 一个批次组的批次数和字节数;一组合计至多 65,536 个条目 |
MAX_FISCAL_ACTIONS_PER_BOUNDARY、MAX_FISCAL_LEDGER_WRITES_PER_BOUNDARY |
65,536 | 一个边界应用的行动数和写入的范围明细数 |
MAX_FISCAL_ACTIONS_PER_RECORD_PER_BOUNDARY |
64 | 一个边界内同一目标上应用的行动数 |
FISCAL_RECENT_OUTCOMES |
8 | 一个目标保留的常驻结果数 |
MAX_FISCAL_RECEIPTS_PER_PACKET、MAX_FISCAL_EVIDENCE_PARTS |
4,096 | 一个数据包中的凭证数;一条汇总证据记录的分项数 |
MAX_FISCAL_EVIDENCE_KINDS、MAX_FISCAL_EVIDENCE_PER_RECORD |
32 | 获准的证据种类;每张凭证或每条审计的证据数 |
MAX_FISCAL_ACTION_JSON_BYTES、MAX_FISCAL_ACTION_TEXT_BYTES、MAX_FISCAL_IDENTIFIER_BYTES |
32 KiB、2,048 字节、160 字节 | 一个条目;审计的 finding、减免的 reason 或执行授权的 purpose;单位或被引用的 ID、核算的单位和核算周期,以及领域记录机构的命名空间、名称和 ID 的合计长度 |
MAX_FISCAL_APPOINTMENT_PARTIES、MAX_FISCAL_PENDING_PROPOSALS_PER_PARTY、MAX_FISCAL_RESOLVED_PROPOSALS_PER_POLICY |
32、4、64 | 一份政策的提名方或请议方;一方的待决提案;一份政策保留的已结案提案 |
MAX_FISCAL_LEDGER_RECORD_BYTES、MAX_FISCAL_RECORD_BYTES、FISCAL_RECORD_RING_RESERVE_BYTES |
256 KiB、1 MiB、4 KiB | 一条范围明细;一条财政机构记录或辖区记录;每条记录为结果环预留的空间 |
FISCAL_REPORT_ROWS_PER_PART、MAX_FISCAL_REPORT_CYCLE_SLOTS |
16、64 | 一个分段中的行数;一份报告显示的周期数 |
报告份额(FiscalReportBudget、with_report_budget) |
默认 16,384 个正文、16,384 个条目、32 MiB | 报告系统在一个边界内发布的量,同时不超过引擎剩余的发布预算 |
FISCAL_PARKED_CHECKS_PER_BOUNDARY |
16 | 报告系统每次运行重新检查的搁置条目数 |
MAX_FISCAL_ARCHIVE_COMMIT_RECORDS、MAX_FISCAL_ARCHIVE_RECORDS_PER_BLOB、MAX_FISCAL_ARCHIVE_PENDING_HANDLES |
4,096、64、64 | 一次归档提交的记录数和 blob 大小;未确认的保留句柄数 |
目录的各项上限(时期、地区、定义、覆盖单元、单个定义内的引用、内容包大小)见 model.rs。
常驻上限都不是整个运行期的总量上限:关闭并归档账册就能释放它们,运行归档多少,就能产生多少对象。边界的开销只随它的数据包涉及的记录增长:在记录的 fiscal-world-profile 运行中(50,000 个范围、4,200 个机构、4,000 个观察者,按错开 30 天的月度间隔发布,朝廷用机构粒度;在一台 AMD Ryzen 7 7800X3D 台式机上测得,你的机器上数值会不同),没有报告到期的边界 0.63 ms 结算完毕,134 个观察者到期的一天 43 ms,一个边界内集中到来的 50,000 笔核算约 4.6 s,并且因为这一时刻没有报告到期而不需要排空;全部 4,000 个观察者在同一天到期、各欠一个周期时,9,603 条报告记录(15.7 MB 载荷)在一个 2.5 s 的边界内发完,而 0.16.0 需要 62 个排空边界。默认留存策略下,持有的知识在模拟的年月中保持平稳(从第 2 个月到第 24 个月都是 14,138 条记录),不再随每次发布增长。财政观察者规模化提案给出了测量结果,世界规模财政提案给出了记录与上限。
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-1644cargo run -p canwu-ming-fiscal-reference --example ming_fiscal_starter -- hongwu-1391 --days 365 --cadence monthly每次运行都会打印检查点哈希,并把 trace 写到 artifacts/traces/ming-fiscal-reference/<fixture>/。最后一条命令在示例流程之后再运行 365 个模拟日,每 30 天一个月度边界。明代财政案例逐步讲解示例流程、三个起点、trace 和各个选项。要验证本页描述的约定,可以运行这些测试:
cargo test -p canwu-ming-fiscal-reference --test referencecargo test -p canwu-ming-fiscal --test packcargo test -p canwu-fiscalreference 测试覆盖过期拒绝、一条鞭法转型、持有人报告、伪造输入、凭证复用以及快照和重放;archive_century 按归档节奏运行一个世纪的周期,seal_stable_validation 和 compact_host_helpers 在封存过的运行上驱动整合包;pack 覆盖覆盖声明与编译。在 canwu-fiscal 中,fiscal_partitioned_state 在随机运行的每个边界之后审计记录,fiscal_record_revisions 和 fiscal_action_batches 覆盖按记录的过期判断和批次,fiscal_action_idempotency 覆盖结果环,fiscal_cycle_archive 和 fiscal_archive_lifetime 覆盖周期与归档,fiscal_receipt_refusals 覆盖拒绝事件,fiscal_audit_appointment 和 gap_g57_fiscal_audit_binding 覆盖任命与审计绑定,gap_g48_fiscal_rebind_authority 覆盖权限改绑,gap_g13_fiscal_acting_actor 覆盖代理角色,gap_g41_fiscal_idle_early_exit 覆盖无事可做的边界,fiscal_report_lines、fiscal_report_parts、fiscal_observer_bindings 和 fiscal_reports_after_seal 覆盖报告记录,fiscal_report_cadence 覆盖按间隔发布的报告、日程和搁置轮询,fiscal_report_institutions 覆盖机构行。
- 结算系统:十四个阶段,以及写入何时可见
- 安全读取与呈现状态:视图与持有人知识查询
- 存档、重放与派生分支:快照、日志、接续检查点和精确重放
- 通过插件扩展:插件、领域记录和边界系统
- 粮食、生产与军事补给:执行适配器可以在其中完成实际转移的资源领域
- 仓库文档:canwu-fiscal README 和明代财政案例说明