结算系统
结算系统决定权威状态何时、以何种方式变化。它的基本单位是结算边界(简称边界):某个模拟时间点上的一次结算过程。边界先准入到期的工作,再按固定的十四个阶段运行各插件的系统,最后把所有变化一起提交,或者一起回滚。编写边界系统,或者需要弄清某次写入何时对其他系统可见时,可以参考本页。
上层应用用 settle_boundary(BoundaryRequest) 发起一次边界,也可以调用 advance_canonical 和 step_canonical,按模拟时钟结算队列中的输入。输入(ingress)、准入、持有人等术语的定义见术语表。
一次边界的完整流程
Section titled “一次边界的完整流程”查看图表源码
flowchart LR
Host["上层应用<br/>提交命令入队"] --> Queue["规范化输入队列"]
Queue -- "到达边界时间" --> Admit["第 1 阶段<br/>准入到期工作"]
Admit --> Snapshot["第 2 阶段<br/>固定读取基线"]
Snapshot --> Systems["第 3–14 阶段<br/>读取、分配、暂存、<br/>校验、提交、计算哈希"]
Systems --> Check{"所有阶段<br/>都成功?"}
Check -- "是" --> Commit["边界提交,<br/>settle_boundary 返回回执"]
Check -- "否" --> Rollback["整个边界回滚,<br/>settle_boundary 返回错误"]
- 引擎先执行请求时间之前到期的内部调度工作,再按队列顺序取出已到期的规范化输入。规范化输入(canonical ingress)是持久化、有确定顺序的输入队列,详见事件系统。
- 建立本边界的准入集合:命令尝试、已接受的命令、已准入的输入和事件,以及本次适用的日历节奏(例如
Daily或Monthly)和历法标记。 - 复制当前状态,作为边界快照。第 1 至第 8 阶段的系统都读这份快照,因此它们都从同一个基线出发。
- 依次执行十四个阶段。同一阶段内的系统按
(plugin name, system name)排序执行。请求中列出了某个系统的节奏,该系统才会运行;EventDriven系统则在本边界准入了任何事件或输入时运行。 - 记录边界证据:已准入的输入、资源分配、状态变化、随机抽样、转移审计、评估轨迹、知识发布和边界哈希。
- 任一步骤失败时,引擎把时间、队列、状态、日志、随机流、计数器和边界记录恢复到本边界之前的值,并返回错误。
阶段规定了工作的先后顺序。插件把每个边界系统注册到某一个阶段。只有第 7、10、12、13 阶段的系统可以声明状态写入;第 9 和第 11 阶段是内核提交暂存写入的地方。
| 顺序 | 阶段 | 做什么 |
|---|---|---|
| 1 | EventIngress |
准入到期的命令、事件和插件输入 |
| 2 | BoundarySnapshot |
固定本边界的读取基线 |
| 3 | DerivedFieldSolve |
计算已声明的派生字段 |
| 4 | PerceptionAndAttentionRefresh |
更新角色的感知与注意力 |
| 5 | DecisionAndAcceptedEffectIntake |
供插件使用的只读决策与效果接收阶段;决策输入在第 1 阶段准入 |
| 6 | ReservationAndAllocation |
提供资源、提出需求,由内核分配 |
| 7 | DomainDeltaProposal |
暂存组件、记录和事件的变化 |
| 8 | InvariantValidation |
用不变量检查暂存的变化 |
| 9 | AtomicDomainCommit |
一次性提交第 7 阶段的变化 |
| 10 | HistoricalCandidateEvaluation |
评估历史转移;转移清单的参与者暂存写入 |
| 11 | ConditionalTransitionCommit |
审计已就绪的转移清单,再提交第 10 阶段的变化 |
| 12 | StrategicAggregation |
生成战略层聚合结果;读取转移审计 |
| 13 | PerspectiveAndReportMaterialization |
生成角色视角和报告 |
| 14 | SaveReplayAndDiagnosticHashing |
记录证据,更新重放与诊断哈希 |
四个基本构件
Section titled “四个基本构件”阶段只是顺序。在阶段之下,结算由四个基本构件组成:
| 构件 | 作用 | 是否写入状态 |
|---|---|---|
| 立即写入 | 命令处理器或旧式事件响应器(reactor)立即生效,不经过边界的暂存与提交。旧的兼容代码(例如 Command::OrderMovement 路径)仍在使用它,少数插件命令(例如 canwu-society 的 set_institutional_policy)也是如此。 |
是 |
| 资源预留与分配 | 第 6 阶段的系统提供供给、提出需求,由内核算出每个请求分到多少。 | 否,只产出分配结果 |
| 分阶段批量提交 | 系统基于边界快照暂存变化,内核统一校验后作为一个批次提交。新的机制都走这条路径。 | 是 |
| 可见性 | 每个边界系统为自己暂存的写入声明 SameBoundary 或 NextBoundary,以此决定其他系统何时能读到。 |
否,只控制时机 |
一个状态键只能属于一条写入路径。立即处理器和边界系统声明写入同一个 StateKey 时,注册会失败。
资源分配把“谁先运行”和“谁拿到资源”拆开。在第 6 阶段,系统声明自己提供什么、需要什么,然后内核一次性结算同一资源池的全部请求。请求依次按资源池、优先级(从高到低)、平局决胜键(tie-break key)和资源预留标识排序,结果只取决于这几个键。
例如,一个粮仓资源池提供 100 单位,三个请求争用它:
| 请求 | 优先级 | 数量 | 结果 |
|---|---|---|---|
| A | 5 | 80 | Fulfilled:分到 80 |
| B | 1 | 50 | Partial:分到 20 |
| C | 1 | 10 | Rejected:分到 0 |
B 和 C 优先级相同,由平局决胜键决定先后;这里 B 的键排在 C 前面。系统通过 SimulationView::reservation 读取分配结果,并且必须在 reservation_reads 中列出它。
移动生命周期扩展 canwu-movement 用另一种方式分配运输运力:它在第 7 阶段对每个运力池调用一次 canwu-transport 提供的纯函数 allocate_capacity_bookings。一项运力预订要么全额确认,要么失败。预订依次按优先级(从高到低)、窗口开始时间、平局决胜键、准入序号和预订标识处理。每个结果都记录为 CapacityBookingAllocationEvidenceV1,已确认的运力始终归属原预订。
可见性与提交
Section titled “可见性与提交”边界系统返回一个 BoundaryProposal,其中的指令暂存写入,之后由内核提交。暂存的写入何时可读,取决于所在阶段和系统声明的可见性:
| 暂存阶段 | SameBoundary 写入 |
NextBoundary 写入 |
|---|---|---|
| 7 | 从第 8 阶段起可通过边界的暂存值覆盖层读到;在第 9 阶段提交 | 在边界结束时提交;从下一个边界起可读 |
| 10 | 在第 11 阶段提交;从第 11 阶段起可读 | 在边界结束时提交 |
| 12 或 13 | 在该阶段结束时提交 | 在边界结束时提交 |
第 8 阶段的不变量系统可以通过 proposed_component 和 proposed_domain_record 查看所有暂存值,包括 NextBoundary 写入,但仍须在读取声明中列出对应的键。
边界运行期间产生的事件,由下一个边界按正常流程准入。
人物与输入指令
Section titled “人物与输入指令”大多数指令写入插件拥有的状态。下面三种指令修改内核拥有的状态,各有固定规则:
SetPersonAvailability替换一个人物的生存状态和人身状态。只有声明写入StateKey::core_person_availability()的第 7 或第 10 阶段系统可以提出。同一边界内对同一人物写入两次,边界失败。边界结束时,在随机决策落地之后,内核取消决策者或控制者权限人物已不可用的未关闭票据,并列入边界记录。CreatePerson创建人物。只有声明写入StateKey::core_people()的第 7 阶段系统可以提出。ID 由引擎分配,回执中的created_persons把这个 ID 绑定到提出创建的插件、系统和相关标识(correlation)。新人物从下一个边界起对系统可见。PersonDraft可以带一个PersonPlacement,即人物的落位:一个政府和一个地域,两者都必须已经存在于WorldSnapshot投影中。这样创建的人物在投影中有一条Person记录。- 不带落位的草稿创建的是无定位人物:它在实体注册表(模拟运行的实体身份列表)中有身份,有人物可用性和创建回执,但没有
Person记录,SimulationView::person和Canwu::world里都查不到它。如果你的应用把政府和地点保存在自己的领域记录里,就用这种方式。草稿中的name和roles只保留在创建证据上,人物的属性请保存在你自己的记录里。 - 在命令权限、决策票据、人身状态和持有人知识这些方面,无定位人物与其他人物没有区别。内置的移动命令、信件和军队到达报告只读投影,所以不会涉及它。
CancelPluginIngress在输入到期之前撤回本系统所属插件安排的排队输入。撤回目标从SimulationView::cancellable_plugin_ingress获取,这个方法要求声明读取StateKey::core_ingress()。如果目标属于其他插件,已经到期、已准入或已撤回,或者同一边界里另一个提案也在撤回它,整个边界都会失败。
多个系统可以同时声明这两个人物核心键,所以内核在结算时按人物检查冲突写入。
有些历史转移需要几个插件在同一个边界里一起写入。转移清单(transition manifest)让协调插件预先列出所有参与者,这样某个参与者没有写入时,内核就能发现。内核在第 11 阶段、第 10 阶段的写入提交之前审计每份清单。
查看图表源码
flowchart LR
Register["第 7、10 或 12 阶段<br/>协调插件登记清单"] --> Stage["就绪边界的第 10 阶段<br/>清单所列参与者暂存写入"]
Stage --> Audit{"第 11 阶段审计"}
Audit -- "全部暂存且版本相符" --> Committed["Committed<br/>暂存写入随之提交"]
Audit -- "无人暂存" --> Expired["Expired<br/>不写入任何内容"]
Audit -- "部分暂存或版本不符" --> Failed["整个边界失败<br/>并回滚"]
Committed --> Read["第 11 阶段起<br/>协调者与参与者读取审计"]
Expired --> Read
- 登记。 协调插件在第 7、10 或 12 阶段的某个系统声明写入
StateKey::core_transitions(),并提出BoundaryDirective::RegisterTransitionManifest。TransitionManifest包含lineage_id、尝试序号attempt、就绪边界ready_at,以及每个参与插件的一项条目。每项条目列出该参与者在转移前(expected_pre)和转移后(expected_post)预期的记录版本。清单身份是TransitionManifestId { coordinator, lineage_id, attempt },其中协调者由内核按登记插件填写。第 7 阶段登记的清单可以在同一边界就绪;第 10 或第 12 阶段登记的清单必须指定之后的边界。就绪边界最多只能在 1,024 个边界之后。ready_at由协调者选择,不由引擎决定;它是边界 ID,不是日期。暂存和提交从不跨越两个边界:参与者在就绪边界暂存,写入也在同一个边界提交,不会推迟到下一个边界。 - 暂存。 在就绪边界的第 10 阶段,清单所列的每个参与者用声明了同一写入的第 10 阶段系统提出
StageTransitionWrite { manifest_id, writes }。暂存的写入就是普通指令,所以该系统声明的写入、所有权、阶段规则和可见性照常适用。writes为空表示参与者到场但不写入。未列入清单的插件尝试暂存时,边界以InvalidAuthority失败。 - 审计。 第 11 阶段提交之前,内核检查本边界就绪的每份清单。所有参与者都已暂存、所有预期版本都相符时,结果为
Committed。没有任何参与者暂存时,结果为Expired,不为这份清单写入任何内容。只有部分参与者暂存时,整个边界以TransitionParticipantMissing失败;预期版本不符时,以TransitionVersionMismatch失败。 - 读取。 内核保存一条
TransitionAuditRecord,记录结果和每个参与者暂存的写入数量,出现在BoundaryRecord::transition_audits和BoundaryReceipt::transition_audits中。协调者和参与者在第 11 阶段及之后运行的系统,通过SimulationView::transition_audits读取它。
expected_pre 指第 10 阶段读取的已提交状态中的版本。expected_post 在第 11 阶段检查:此时本边界 SameBoundary 的第 10 阶段写入已经提交,待处理的 NextBoundary 第 7、第 10 阶段写入也已计入。同一边界中第 12 或第 13 阶段的写入之后仍可能改变这条记录。
清单有数量上限,违反任何规则都会让边界失败:
- 每个协调者对同一
lineage_id最多有一份待处理清单,总共最多 32 份;所有协调者合计最多 128 份。一份清单最多列出 16 个参与者,预期版本合计最多 64 个。每个参与者都必须是已注册插件,并拥有声明core_transitions的第 10 阶段系统;每条预期记录都必须属于已注册的记录种类。 - 只有协调者和参与者能看到清单:它们在声明
canwu.core.transitions读取后,从登记后的下一个阶段起通过SimulationView::transition_manifests读取。上层应用用Canwu::pending_transition_manifests读取待处理清单。 - 没有撤回清单的指令。无人暂存时清单会过期,所以如果就绪边界每次重试都失败,可以用一个让所有参与者系统都不运行的节奏来结算这个边界,清单随之过期,协调者再登记一次新的尝试。清单 ID 在结算后可以复用,因此审计由
(manifest_id, ready_at)唯一确定。 - 待处理清单保存在快照中,并随失败的边界一起回滚。快照校验时,内核根据登记和审计证据重建它们。从未登记清单的运行,哈希与没有这项功能时相同。
- 不属于任何清单的第 10 阶段指令不受影响。要发现缺席的参与者,清单至少需要两个参与者;只有一个参与者时,它不写入只会让清单过期。
统计一段时间内的边界
Section titled “统计一段时间内的边界”有些系统会把工作带到之后的边界:把一个数据包重新发送到更晚的时间,或者处理某件事,而它的结果要在同一时刻的后续边界里结算。要把一段模拟时间内的边界数控制在预算之内,这类系统不仅要数自己开启的边界,还要数其他来源开启的边界。声明了 canwu.core.boundary_schedule 读取的边界系统可以调用 SimulationView::boundaries_between(from, to),读取左闭右开的时间窗口 from..to。返回的 BoundaryWindow 有三个列表:
settled:窗口内已经结算的边界。current:正在结算的边界;仅当读取者是边界系统,且该边界的时间落在窗口内时才有值。queued:窗口内在规范化输入队列和调度工作队列中仍在排队的每个不同到期时间。
每个 BoundarySummary 给出边界的时间、节奏、历法标记,以及按类别、种类和来源计数的输入。种类分为命令、插件数据包类型、日历工作、决策、维护和席位改绑。来源(BoundaryIngressOrigin)区分上层应用的普通输入(Host)、由边界系统或命令处理器安排的工作(BoundarySystem、CommandHandler)、以事件为起因记录的工作(Event),以及引擎自身或上层应用标注的工作,例如日历请求(System,附带名称)。摘要从不包含载荷。上层应用按自己的规则给边界分类,例如排除只由玩家命令开启的边界。席位改绑也属于命令类别,所以要单独挑出玩家命令时,应按种类而不是按类别筛选。
- 系统在自己的
BoundarySystemContract上用boundary_schedule_horizon声明要往回读多远的已结算边界,就像声明读取和随机流一样。它最多只能往回读这么远。窗口起点早于边界时刻减去这个时长时,返回EvidenceUnavailable。其他插件的声明不会放宽这个限制,所以任何插件都不会依赖别的插件声明的时长。窗口终点早于起点时,返回ValueOutOfRange。声明了非零时长的系统必须同时声明canwu.core.boundary_schedule读取。 - 模拟运行按所有已注册系统中最长的声明保留已结算边界,最长为
MAX_BOUNDARY_SCHEDULE_HORIZON(366 天);没有任何系统声明时,只保留当前时刻及之后的边界。上层应用可以往回读到这个时长,Canwu::boundary_schedule_horizon会给出这个时长。 - 引擎不内置历法,但会携带上层应用的历法标记(calendar mark)。运行多套历法的上层应用用
schedule_calendar_marks(due_at, marks)为一个日历边界标出周期在此开始的各个历法,每个CalendarMark是一个节奏加一个历法键(最长 64 字节的规范化标签);同一时刻到期的日历输入仍合并成一个边界,标记也合并。在BoundarySystemContract::calendar上写了历法键的边界系统,只在带有自己(cadence, calendar key)标记的边界上运行;没有历法键的系统在该节奏的每个边界上运行,不论有无标记,所以没有标记的schedule_calendar_boundary只唤醒没有历法键的系统。SimulationView::calendar_marks读出正在结算的边界的标记,不需要读取声明;BoundaryRecord和BoundarySummary也带着它们。历法键属于插件描述。SimTime::day从时间零点起按每天 1,440 分钟给模拟日编号,方便按天取窗口;回合、星期、从其他时刻开始的一天,以及某个标记是第几个月,由上层应用自行计算。 - 只有分阶段的边界系统可以声明这项读取。它会列出其他插件排队中的数据包类型(从不包含载荷),所以与
canwu.core.ingress一样,声明它的插件命令或同步事件驱动系统在注册时会被拒绝。 - 排队列表只是下限。当前边界提出的工作要等边界提交后才进入队列,所以零延迟的后续工作(它会在同一时刻开启第二个边界)要从下一个边界起才会列出。尚未入队的工作,以及上层应用直接结算的边界,也不会列出。
- 已结算列表在实时运行、封存证据后的运行、恢复后的运行及其精确重放中完全相同。引擎把它作为由边界日志、输入日志和已保存的插件描述派生的索引:边界提交时更新,封存证据时保留,恢复时根据完整日志重建。它不会给快照或哈希增加任何内容。
- 已结算的摘要来自边界证据,而边界证据不记录旧式的已调度动作,所以只有排队中的摘要会统计它们(
scheduled_actions)。 - 上层应用通过
Canwu::boundaries_between和CompactedCanwu::boundaries_between读取同一份账,结果中没有当前边界。结果会列出所有插件排队中的数据包类型,所以不要把它交给玩家、智能体、观察者或远程客户端。
评估轨迹逐项说明一条应用规则如何为某个对象算出一个数值。第 7 或第 12 阶段的系统提出 BoundaryDirective::RecordEvaluationTrace { trace } 即可记录一条轨迹,系统契约无需额外声明。EvaluationTraceRecord 包含规则 ID 与版本、对象实体、一组 EvaluationTerm(项 ID、整数贡献值和该项读取的证据)、规则算出的结果,以及当前边界。内核检查结构、对象是否存在,以及每条证据引用是否为该提案可见的已提交证据;各项之和是否等于结果,内核不检查。
- 轨迹是边界证据,与状态分开保存。内核把它连同产生它的插件、系统和阶段记录在
BoundaryRecord::evaluation_traces中,纳入边界哈希链,并随边界记录一起封存和归档。系统、命令处理器和决策策略都不能读回轨迹来决定结果。 - 运行配置通过
RunConfiguration::with_evaluation_limits设置EvaluationLimitsV1上限。默认每个边界所有系统合计 4,096 条轨迹、每条 32 项;最大可设为 65,536 条和 256 项。每项最多引用 16 条证据,规则 ID、规则版本和项 ID 各自最多 256 字节。提案超过任一上限时,边界以EvaluationTraceLimitExceeded失败;成功提交的边界保留它的全部轨迹。上限设为零即禁止本次运行记录轨迹。 - 第 13 阶段不能记录轨迹,应由算出该值的第 7 或第 12 阶段系统记录。轨迹计入日志大小。
- 玩家和智能体通过
CanwuViewer::evaluation_traces读取轨迹。这个视图不含证据,并受持有人知识范围限制,详见读取状态。
边界耗时明细
Section titled “边界耗时明细”上层应用可以自己给整个 settle_boundary 调用计时,但看不到这段时间在各个插件之间是怎么分的。边界耗时明细(boundary profile)给出的正是这一点,方便你按时间预算检查每个插件。
耗时明细默认关闭。在 Canwu 或 CompactedCanwu 上调用 set_boundary_profiling(true) 开启后,每个成功结算的边界都会留下一份 BoundaryProfile;take_boundary_profiles() 取走上次调用以来积累的全部耗时明细:
canwu.set_boundary_profiling(true);canwu.settle_boundary(BoundaryRequest::at(canwu.time()).with_cadence(SystemCadence::Daily))?;
let batch = canwu.take_boundary_profiles();for profile in &batch.profiles { println!( "boundary at {:?}: {} µs in total, {} µs in my-plugin, {} µs in the engine", profile.settled_at, profile.total_micros, profile.plugin_micros("my-plugin"), profile.remaining_micros(), );}一份耗时明细包含:
boundary_id和settled_at:是哪个边界,以及它的模拟时间。total_micros:整个结算花费的现实时间,单位为微秒。handlers:边界每调用一次插件处理器,就有一条BoundaryHandlerTiming,按调用顺序排列,带有kind、phase、plugin、name和elapsed_micros。engine:每个引擎阶段(BoundaryEnginePhase)一条BoundaryEngineTiming,把模拟内核自身的时间按工作种类拆开:Admission(检查请求、记录回滚点、准入到期的输入、命令、决策、维护和已调度动作)、SystemDispatch(为每个系统准备视图并检查它的提案)、Staging(阶段之间的叠加层、资源分配和转移审计;当第 7 阶段的全部记录写入都在同一边界可见时,大部分记录写入的工作算在这里)、Commit、Knowledge、Journal和Hashing。engine_micros(phase)读取其中一项。处理器和引擎阶段把total_micros分完:它们的和与总耗时的差距,每个列出的值不到一微秒。
边界会运行三种插件处理器,各对应一个 BoundaryHandlerKind:
| 种类 | 计时对象 | phase |
|---|---|---|
BoundarySystem |
边界系统 | 它所在的阶段 |
CommandHandler |
本边界准入的排队插件命令的处理器 | 无 |
EventSystem |
由这类命令或本边界到期的已调度动作触发的同步事件驱动插件系统 | 无 |
命令处理器和事件系统在边界准入输入时运行,早于第一个阶段,所以没有阶段。plugin_micros(plugin) 把某个插件所有处理器的耗时加起来;remaining_micros() 是剩下的部分,也就是引擎自身的时间,在同样的误差范围内等于各引擎阶段之和。
要知道精确重放的时间花在哪里,调用 Canwu::replay_from_journal_with_boundary_profiles(plugins, &journal, archive_provider, on_profile):它从第一个记录的边界起就开着计时重放,每个边界一结算就按日志顺序把它的耗时明细交给 on_profile,所以保留数量的上限不会限制一次很长的重放。返回的运行计时是关闭的。与日志不符的重放和不计时的重放一样失败,on_profile 此前已收到直到不符边界为止的明细。
- 耗时明细是给上层应用看的诊断数据,不进入模拟。它不属于权威状态、证据、快照、检查点或重放日志,插件也读不到它。开启计时的运行与不开启的运行,回执、证据和哈希完全相同;计时关闭时,结算不读时钟。同样的输入运行两次,数值也会不同,所以不要把这些数值再写回命令或插件输入。
- 新建、恢复、重放得到的模拟运行和派生分支,计时都是关闭的;加载存档之后需要重新开启。进入紧凑模式和封存证据都不会改变开关,也不会丢掉尚未取走的明细。
- 失败的边界不留下耗时明细。
- 模拟运行最多保留
MAX_RETAINED_BOUNDARY_PROFILES(1,024)份尚未取走的明细。如果在你取走之前又结算了更多边界,最旧的会被丢弃,BoundaryProfileBatch::dropped给出丢弃的数量。 - 插件归档维护由上层应用另行安排,在结算之外运行,不出现在任何明细里。边界只负责应用维护产生的提交,这部分时间算作引擎耗时。
扩展系统的运行位置
Section titled “扩展系统的运行位置”下列第一方扩展也使用同样的阶段。对照这张表,可以判断它们的状态相对于你自己的系统何时变化。
| 扩展 | 阶段与节奏 | 系统与工作 |
|---|---|---|
canwu-movement |
第 7 阶段,事件驱动 | movement_lifecycle_apply_v1 按准入顺序应用已准入的操作和事故,对每个运力池运行一次分配,处理到期路段,并退休已关闭的运输执行。 |
canwu-movement |
第 8 阶段,事件驱动 | movement_lifecycle_validate_v1 校验暂存的移动运行时状态。 |
canwu-movement |
第 13 阶段,事件驱动 | movement_report_publish_v1 发布有变化的移动报告,每份报告只发给有权看到它的持有人。 |
canwu-culture(CultureBoundaryPlugin) |
第 12 阶段,事件驱动 | culture_exposure_intake_v1 把已准入的 culture_exposure_v1 批次排入队列。 |
canwu-culture(CultureBoundaryPlugin) |
第 7 阶段,每月 | culture_lifecycle_settle_v1 消费队列和已接受的制度决策,基于 canwu-society 状态的边界快照结算生命周期,保存文化状态,并把 canwu-society 变化和信号批次安排为下一边界的输入。 |
canwu-society |
第 12 阶段,事件驱动 | intake-society-ingress 把已准入的 cohort_headcount_rebase_v1、society_lifecycle_delta_v1 和 society_pressure_revision_v1 输入排入队列。 |
canwu-society |
第 7 阶段,society 结算节奏(默认每日) | settle-social-transitions 在 cohort 转移之后、制度决策与转移规则之前应用队列,然后重新算出因它的变化而失效的动员候选、聚合状态和观察者投影,所以一次结算只写一次根记录。 |
canwu-society |
第 7 阶段,事件驱动 | intake-intergroup-relations 把已准入的 intergroup_relation_revision_v1 数据包应用到群体间关系记录上,每个数据包要么整体应用、要么完全不应用;被拒绝的数据包记为一条 society_intergroup_relation_rejected_v1 事件。 |
移动路段的时序由内部调度输入 movement_leg_due_v1 驱动:移动命令安排首次出发,出发按路段计划时长安排到达,到达再安排下一次出发。到期输入只有与移动命令上保存的到期时间一致时才生效,因此显式操作会让旧的到期输入失效。到期工作不会让边界失败。
文化和 canwu-society 之所以使用输入队列,是因为同一阶段里一个状态键只能有一个写入系统,而它们的写入系统按更粗的节奏运行。所有第 7 阶段系统读的是同一份边界快照,所以文化的每次交接都落在之后的边界:信号批次在下一个边界准入,canwu-society 变化在准入后的第一次 society 结算时应用。因此,文化步骤最早要到两个边界之后才影响 canwu-society 状态;人口校准则在准入它的边界之后的第一次 society 结算时生效。society 插件始终是 canwu.society:state 的唯一写入者。
society 默认在带 Daily 节奏的边界结算;上层应用也可以用 SocietyPlugin::with_settlement_cadence 注册成别的节奏。节奏更粗时,队列里的一切都要等到下一个带这个节奏的边界,见换一种结算节奏。
每个边界系统注册一个 BoundarySystemContract,声明:
- 名称、所属阶段和执行节奏;
- 读取和写入的
StateKey; - 提供和申请的资源池,以及要读取的分配结果(
reservation_reads); - 自己拥有的随机流;
- 会发出的事件类型(
emits)、可以发布的知识 schema(knowledge_writes),以及可以发送输入的其他插件(plugin_ingress_targets); - 写入的可见性。
内核拥有的键遵循同样的规则:转移指令需要 StateKey::core_transitions(),人物指令需要 StateKey::core_person_availability() 或 StateKey::core_people()。评估轨迹不写入状态,所以无需声明。
读取发布预算同样不需要声明。一个边界只有一份知识记录正文、持有人条目和载荷字节的预算,由所有发布知识的系统按调度顺序消耗(第 4 阶段先于第 13 阶段,同一阶段内按注册表顺序)。发布知识的系统在提案之前先用 SimulationView::knowledge_publication_budget() 读剩余的预算,因为超出预算的提案会让边界失败。
模拟内核拒绝未声明的读取、写入、分配读取和随机抽样。插件注册、快照恢复和精确重放都要求插件名称、版本和语义哈希一致。