跳转到内容

知识与信息系统

参伍把世界中的真值和每个角色知道的内容分开保存。canwu-knowledge 为每个持有人(即知识归属的人物或机构)维护一本知识账本;canwu-information 记录一条信息如何被写成、发出、阅读、读懂和发布;canwu-correspondence 根据承运人所知规划路线,投递写明收件人的信件。如果玩家、智能体或机构要随着报告和信件陆续送达而行动,并且各自只看到已经到达自己的内容,就需要用到这三个 crate。

crate 负责什么 发布情况
canwu-knowledge 持有人账本(记录正文只存一份,每个持有人各一个持有人条目,读出时拼成 KnowledgeRecord)、持有人查询、读取切面(一次读取所看到的账本位置)、游标和留存策略,以及为兼容保留的、每个角色对军队的旧式观测(ActorKnowledge) 已发布到 crates.io
canwu-information canwu.information 命名空间下以领域记录表示的信息生命周期:渠道、内容、表现形式、实物载体、发送记录、投递尝试、访问、解读、受众、发布和操作;模拟插件 InformationPlugin 已发布到 crates.io
canwu-correspondence canwu.correspondence 命名空间下的通信:通信操作、通信机会、知识种子和承运人委托;模拟插件 CorrespondencePlugin;持有人规划快照的构建函数 已发布到 crates.io
crate 依赖关系:上层应用使用两个扩展和对外 API;两个扩展依赖 canwu-api;模拟内核保存 canwu-knowledge 的账本. 查看图表源码.
查看图表源码
flowchart TB
  Host["上层应用<br/>场景、命令、输入、知识种子"]
  Corr["canwu-correspondence<br/>信件、路线、承运人"]
  Info["canwu-information<br/>信息生命周期"]
  Api["canwu-api<br/>对外 API、CanwuViewer"]
  Route["canwu-routing、canwu-transport<br/>路线规划、运输记录"]
  Sim["canwu-sim<br/>校验并提交知识发布"]
  Know["canwu-knowledge<br/>持有人账本与查询"]
  Host --> Corr
  Host --> Info
  Host --> Api
  Corr --> Info
  Corr --> Api
  Info --> Api
  Api --> Route
  Api --> Sim
  Api --> Know
  Sim --> Know

箭头从一个 crate 指向它依赖的 crate。canwu-knowledge 属于模型层:模拟内核(canwu-sim)保存它的账本并校验每一次知识发布,canwu-api 重新导出它的类型。两个扩展只依赖 canwu-api,canwu-correspondence 另外依赖 canwu-information。上层应用注册插件,提供内容和概率,并通过对外 API 读取结果。

发布知识和拥有状态是两种独立的能力。插件即使拥有某条记录,也要先注册知识 schema,并在自己的某个边界系统(插件在各结算阶段注册的函数)上声明 KnowledgeWriteGrant,才能把这条记录告诉任何持有人。

类型 表示什么
KnowledgeHolderRef 谁知道:Person(PersonId) 或 Entity(EntityRef)。定义在 canwu-core 中,由 canwu-api 重新导出。
KnowledgeRecord 读取方看到的、某个持有人账本中的一条事实:schema、带类型的 subjects、JSON payload、as_of(事实成立的时间,可以为空)、learned_at(持有人得知的时间)、confidence_per_mille、origin(获取方式和证据),以及同一持有人被它取代(supersedes)或与它矛盾(contradicts)的旧记录。as_of 和 learned_at 之差,就是这条消息传到持有人手中时已经过去的时间。
KnowledgeRecordBody、HolderEntry 账本实际保存记录的方式:知识记录正文(除持有人和关联之外的全部内容)无论几个持有人持有都只存一份;每个持有人各有一个持有人条目,带有持有人自己的本地记录号和自己的 supersedes、contradicts。一条 KnowledgeRecord 就是一个记录正文配上一个条目;没有任何持有人持有的记录正文离开账本。
KnowledgeRecordDraft 边界系统提出的记录草稿。内核分配记录 ID,并把 learned_at 设为边界时间。payload_bytes() 给出发布预算为它计费的字节数。
KnowledgeRecordView 面向持有人的记录副本:带持有人本地记录号(HolderKnowledgeRecordId,来自持有人自己的计数器,随条目保存),不含 origin。
KnowledgeQuery、KnowledgeQueryResult、KnowledgeCursor 某个持有人账本中的一页记录,可以按 schema、主体和得知时间过滤,带有翻到下一页的游标,以及查询涉及的每个 schema 的留存策略。
KnowledgeReadCut 一次读取所看到的账本位置:边界,加上该持有人全部记录的哈希。
KnowledgeSnapshot 所有持有人账本,保存在快照或场景中:按 ID 排列的记录正文,以及每个持有人的下一个本地记录号和它的条目。它还带有为兼容保留的 ActorKnowledge,即每个角色对各支军队的旧式观测;每条观测都带有一个 KnowledgeSource(直接观察、指挥职责、报告或场景记录)。
PluginKnowledgeSchema、KnowledgeWriteGrant 某个插件注册并拥有的带版本知识 schema,带有它的留存策略 retention(KnowledgeRetention::Keep 或 RetireWhenSuperseded),以及某个边界系统以指定可见性发布这个 schema 的许可。
KnowledgePublicationGroup、KnowledgeSupersession PublishKnowledge 指令中的一个组:这组草稿要发给的持有人(已排序、不重复);以及条目如何指明它取代的记录:Explicit(用草稿自己的列表,只允许单个持有人的组)或 CurrentOnSubjects(引擎为每个持有人找出该种类、带这些主体的当前记录)。
KnowledgePublicationBudget 到某个系统运行时,边界还能发布多少:记录正文、持有人条目和载荷字节,以及每条指令的上限。用 SimulationView::knowledge_publication_budget() 读取。

下表中每种信息记录类型都有对应的载荷类型,例如 Content 对应 ContentPayload。

类型 crate 表示什么
Content information 说了什么:内联 JSON 正文或外部资源的摘要;可以由其他内容衍生,并标明每个来源的角色,例如引用或更正
Representation information 内容在某种格式下的一种表现形式,带来源谱系、可选的 ClaimedSourceRef(它自称的来源),以及解读它所需的能力
Instance information 实物载体,带保管人和所在位置
Channel information 传递路径的配置及其能力,例如 AddressedDelivery、AudienceDelivery 或 OpenReception
Dispatch、DeliveryAttempt information 一次发往指定持有人、受众或公开接收的发送;一次把信息送到某位收件人的尝试
Access information 某个持有人读了某个表现形式,带阅读方式和以千分比计的阅读程度
Interpretation、AuthenticityFinding information 持有人试图读懂所访问的内容,状态为 Failed、Partial 或 Succeeded;可选的真伪判定记录它是否接受自称的来源
Audience、Release information 绑定成员根的固定成员名单;面向这个受众或公开可得的发布,最终为 Withdrawn 或 Expired
InformationOperationEnvelope information 一次幂等操作,以 InformationOperationId 为键,包含一条 LifecycleRequest
CorrespondenceOperation correspondence 一封有收件人的信件:通信意图、已解析的地址及其读取切面、RoutePlan、TransportExecution、规划历史、意外事件,以及最终为 Settled、DeadlineMissed、CompensationPending 或 Failed 的 CorrespondenceStatus
NetworkKnowledgeSeed correspondence 要写入某个持有人账本的路由端点、连接和收件人地址
CommunicationOpportunity correspondence 经过抽样得到的一次写信机会,发件人可以写给候选收件人中的一位
CorrespondenceIncidentKind correspondence Disaster、Interception 或 CarrierSeized
CarrierAuthority correspondence 承运人已接受的、替某位发件人承运的委托,保存为它当前的 CarrierDelegationRecord
从命令或输入经过第 7 阶段生命周期系统和第 13 阶段知识发布,到达持有人账本、KnowledgePublished 事件和 CanwuViewer 的路径. 查看图表源码.
查看图表源码
flowchart TB
  Input["上层应用的命令和输入<br/>apply_information_operation_v1、<br/>initiate_correspondence_v1、知识种子"]
  Corr["第 7 阶段:correspondence-lifecycle-v1<br/>读取承运人账本、规划路线"]
  Info7["第 7 阶段:information_lifecycle_v1<br/>校验并写入领域记录"]
  Info13["第 13 阶段:information_publication_v1<br/>PublishKnowledge,每个持有人一组"]
  Seed["第 13 阶段:correspondence-knowledge-ingress-v1<br/>知识种子和尝试报告"]
  Kernel["模拟内核<br/>检查 schema、授权、持有人和预算<br/>边界结束时提交"]
  Ledger["持有人知识账本<br/>一个记录正文,每个持有人一个条目"]
  Event["KnowledgePublished 事件<br/>受众:指令列出的持有人"]
  Viewer["CanwuViewer<br/>query_knowledge、visible_changes_since"]
  Input --> Corr
  Input --> Info7
  Input --> Seed
  Corr -- "information_operation_v1<br/>在之后的边界准入" --> Info7
  Info7 --> Info13
  Info13 --> Kernel
  Seed --> Kernel
  Kernel --> Ledger
  Kernel --> Event
  Ledger --> Viewer
  Event --> Viewer
  Ledger -. "下一次规划读取" .-> Corr
  1. 输入到达。上层应用提交 apply_information_operation_v1 命令,每条命令带一个 InformationOperationEnvelope。命令处理器检查幂等性,然后排入 information_operation_v1 规范化输入;命令本身不写任何记录。信息提供方也可以直接排入这条输入。通信通过 initiate_correspondence_v1、delegate_carrier_v1 和 resolve_correspondence_v1 三条命令,以及 install_correspondence_knowledge_v1、communication_opportunity_v1 和 correspondence_incident_v1 三种输入进入。

  2. 第 7 阶段运行信息生命周期。事件驱动的系统 information_lifecycle_v1 让每个已准入的操作依次进入 Accepted、ApplyingDomainChanges,最后应用变更,每个边界前进一步。应用之前,它检查解读权限;如果带有真伪判定,还要检查判定引用的是某个带自称来源的表现形式的当前版本。检查失败时,操作保存为 Rejected,错误码为 invalid_authority 或 invalid_lifecycle。InformationLifecycle::plan 返回记录变更,以及这次变更引出的知识发布。操作按先后顺序规划,最早的在前;计划会改动同一轮中更早的操作已经改动的记录的操作,等到下一个边界再规划。无法解码的数据包,以及对另一个数据包或已持久化的操作已占用的操作 ID 带来不同输入的数据包,以 canwu.information.ingress_rejected.v1 事件被跳过,绝不会让边界失败。同一个系统还为每个命令授权维护一条带版本的 DelegatedAuthority 记录,即委托账:按命令 ID 存放该授权下仍然有效的委托,并带一个到期索引。之后的 InterpretationAuthority::Delegated 按这本账而不是按命令检查,所以命令封存之后判断不变。系统在它运行的每个边界都会移除 expires_at 已到的委托,准入时就已经过期的委托声明不会记入;expires_at 已到的委托即使还没被移除也不再授权任何事,所以解读必须在委托仍然有效时准入,就算它的日期落在委托窗口之内也一样(否则为 invalid_authority)。没有 expires_at 的委托声明在整个运行期内保留。

  3. 第 7 阶段同时运行通信。correspondence-lifecycle-v1 结算启动、推进、意外事件、通信机会、恢复和委托。处理启动时,它读取承运人当前的规划知识,从中解析收件人地址,调用 plan_route,保存 CorrespondenceOperation,并为 canwu-information 安排信息操作。之后的 progress_correspondence_v1 输入等待这些操作结束,再开始和完成各段路程。

  4. 第 13 阶段发布知识。information_publication_v1 把一个操作要发布的知识转为 PublishKnowledge 指令,每条指令一个单持有人的组,每个操作在一个边界内最多发布 64 条,并且绝不超过边界的发布预算(第一条记录就放不下的一批整批推迟,同一时刻的下一个边界重试);每个持有人、种类和主体集合在一个边界内最多发布一次,引擎不接受的持有人(已死亡的人物、不存在的军队或政府、已退休的实体)留着不发布,操作带着其余部分完成。下一个边界,内部输入 information_operation_finalize_v1 记录已发布的 ID(通过 knowledge_heads 查到,而不是从保留的证据里找),并放出下一批。correspondence-knowledge-ingress-v1 发布知识种子和承运人的尝试报告:持有人被引擎拒绝、一次发布装不下,或陈述了规划网络不接受的连接的种子什么也不发布,其回执记下原因(KnowledgeSeedRefusal,知识种子拒收);发给引擎拒绝的承运人(例如扣押后死亡的人物)的尝试报告不发布,改为发出 canwu.correspondence.attempt_report_withheld.v1,边界照常提交。

  5. 模拟内核校验并提交。内核只接受满足以下全部条件的 PublishKnowledge 指令:

    • 来自第 4 或第 13 阶段的系统,且该系统在 knowledge_writes 中以相应的可见性声明了这个 schema;
    • schema 可写,并由同一个插件注册;
    • 每个组的每个持有人都符合资格,证据引用有效;
    • 条目取代或标为矛盾的每条记录,都是同一持有人、同一 schema 种类下可见的记录(见取代先前的记录);若用 CurrentOnSubjects,草稿不带关联,由引擎为每个持有人算出;
    • 指令不超过单条指令的上限,整个提案不超过预算留给这个系统的部分。

    随后内核为每个记录正文分配一个全局记录 ID,设定 learned_at,让同一边界中后续运行的系统读到 SameBoundary 记录,并在边界结束时把每个记录正文存一份、给组内每个持有人一个带该持有人下一个本地记录号的条目;在 RetireWhenSuperseded 下,这些条目取代的条目在同一次提交中离开各自的持有人(见留存)。

  6. 产出结果。每条指令成为 BoundaryRecord::knowledge_changes 中的一条 BoundaryKnowledgeChange(它的记录正文与账本共享而不是复制,加上每个持有人的条目;records_of(&holder) 拼出某个持有人收到的记录),并产生一个 KnowledgePublished 事件,列出它发给的全部持有人,每个持有人都在事件的受众之内。此后该持有人读取账本时就能看到新记录。

一个组里有多个持有人时,同一批记录正文发给所有人:账本、日志和每个哈希中各只有一份记录正文,每个持有人各一个条目。每个持有人只看到自己的本地记录号和自己的关联,没有任何字段告诉它别的持有人也持有同一个记录正文。取代关系按持有人各自判定:用 KnowledgeSupersession::CurrentOnSubjects 时,引擎为组内每个持有人执行 knowledge_heads 的规则,把该持有人在草稿种类下、带有草稿全部主体的当前记录写进它的条目,所以一条共享的记录线会取代每个持有人各自的旧记录。某个持有人的匹配结果超过 64 条当前记录时,边界失败:共享的记录线理应只有一个当前记录。Explicit 沿用草稿自己的列表,只允许单个持有人的组;单持有人的组配 Explicit 正是 0.16.0 的那条指令,第一方发布方都是这样迁移的。

一个边界最多保存 32,768 个记录正文、新增 32,768 个持有人条目、接收 64 MiB 载荷(KnowledgeLimitsV1::CURRENT),一条指令最多 1,000 个记录正文和 4,096 个条目。本边界所有发布知识的系统共用这份预算,按调度顺序消耗:第 4 阶段先于第 13 阶段,同一阶段内按注册表运行系统的顺序。系统在发布之前先读剩余的预算:

let mut budget = view.knowledge_publication_budget();
let bytes = draft.payload_bytes()?;
if budget.take(1, holders.len(), bytes) {
// 把这份草稿发给这些持有人
} else {
// 推迟它,或记为一次拒绝,让插件自己的状态能看到
}
  • KnowledgePublicationBudget 带有剩余的 records(记录正文)、holder_entries 和 payload_bytes,以及单条指令的上限 records_per_batch 和 holder_entries_per_batch。admits 判断再加一条给定大小的指令是否放得下;take 在放得下时扣减,并返回是否扣减了。
  • 这次读取不需要声明。答案是精确的:上限减去更早阶段和本阶段更早系统的提案已经用掉的部分,所以实时运行、封存过的运行、恢复后的运行和精确重放看到的余额相同。
  • KnowledgeRecordDraft::payload_bytes() 就是引擎为草稿载荷计费的字节数(它的紧凑 JSON 长度),发布方不必猜。
  • 先读预算、把放不下的推迟的发布方,永远不会让边界失败。超出预算发布的,边界以 KnowledgeLimitExceeded 失败:引擎不会把发布截断到刚好放下,因为同一提案中插件自己的记录写入会以为记录已经发出去了。第一方发布方(军事、移动、信息、技术和财政报告系统)都先读预算。
  • 默认没有按系统划分的份额。两个插件争用预算时,调度顺序靠前的先拿它需要的;谁靠前,取决于你的组合方式。
  • 发布预留可以为一个插件保证一份份额。用 KnowledgeReservedPlugin::new(&plugin, KnowledgePublicationReservation { records, holder_entries, payload_bytes }) 注册插件,或者让插件在自己的 register 里调用 registrar.reserve_knowledge_publication(reservation)。边界在第一个阶段之前根据自己的节奏和已准入事件判断哪些插件的写知识系统(带知识写入授权的第 4 或第 13 阶段系统)会在本边界运行,并为这些插件保留份额;其他插件的系统看不到、也拿不走这份尚未用掉的份额,与调度顺序无关,而预留份额的插件仍通过同一个 knowledge_publication_budget() 读取自己的份额。同时在第 4 和第 13 阶段发布的插件只有一份份额,整个边界内一直保留。注册时会拒绝空的预留、重复的预留、没有写知识边界系统的插件的预留,以及加起来超过每边界上限的预留;预留记在插件描述里,所以运行只能用声明了相同预留的插件恢复和重放。没有预留时,数字就是普通预算的数字。超出系统所看到的份额发布,仍然会让边界失败。

每个注册的 schema 都声明一个 KnowledgeRetention:

  • Keep(默认):每个条目在持有人账本中保留整个运行期。被取代的记录仍可用 KnowledgeHistoryView::FullHistory 读到。
  • RetireWhenSuperseded:持有人的新条目取代它的某个条目时,被取代的条目在发布它的那个边界提交时从该持有人那里清退。该持有人所有指向它的关联都被去掉,所以每个关联仍然能解析;没有持有人再持有它的记录正文时,记录正文离开账本。持有人每条记录线只保留一个条目,持有的知识不再随历史增长。

清退是边界的一次状态变化,实时运行和精确重放完全一致,并且与封存无关,所以封存过和没封存过的运行得到相同的状态哈希。留存策略是插件描述的一部分,恢复和重放都要求插件声明相同的策略。读取方看到的是:CurrentHeads 查询不变;留存策略为清退的 schema,FullHistory 只显示当前条目,KnowledgeQueryResult::retention 说明每个 schema 的策略;knowledge_changes_by_correlation 仍从保留的证据返回边界当时发布的内容,不管之后是否清退;快照只保存当前条目;被清退到空的持有人保留它的本地记录号计数器,所以本地记录号不会重新开始。已死亡人物的条目永远不会被清退,因为不可能再向他发布任何记录去取代它。

结算系统列出了全部十四个阶段。从无锡发出、按路线投递的通信逐步讲解了意外事件和恢复:灾害让同一次尝试改道,截取让截取者得到一条 Access 记录、投递照常继续,承运人扣押则结束这次尝试。

插件为持有人发布某件事的新版本(例如一份新的报告)时,新记录在 supersedes 中写明它取代的旧记录。此后,持有人的默认查询只显示新记录。

被取代或被标为矛盾的目标,必须是同一持有人、同一 schema 种类的记录,并且在发布者的读取切面上可见:要么已由更早的边界提交(不论它的 learned_at 是哪个时刻),要么由同一个边界在更早的阶段以 SameBoundary 可见性发布。同一时刻可以结算两个边界,后一个边界可以取代前一个边界发布的记录。加载存档时使用的是同一条规则,所以只要写出存档的模拟运行能够继续,这份存档就能加载。

要找到该取代哪些记录,声明了 StateKey::core_knowledge() 读取的边界系统可以调用 SimulationView::knowledge_heads:

let heads = view.knowledge_heads(&holder, &schema.kind, &[subject])?;
draft.supersedes = heads.records;
  • 它返回该持有人在这一 schema 种类下当前记录的全局 ID,也就是没有被该持有人任何条目在 supersedes 中指名的条目(当前知识记录)。只有带有所列全部主体的记录才算匹配,空列表则选中整个种类。如果你的插件为同一持有人维护多条记录线,要给每条线一个其他线没有的主体。指令用 KnowledgeSupersession::CurrentOnSubjects 时,引擎用草稿自己的主体为组内每个持有人做这次读取。
  • 返回的每个 ID,都可以作为该系统向同一持有人发布同种类记录时的有效目标,并且已经按草稿要求的升序排列。
  • 最多返回 64 个 ID,这也是一条记录最多能取代的数量。匹配的记录更多时,返回最新的那些,并设置 truncated。这不是错误:其余较旧的当前记录会在之后的发布中陆续被取代。
  • 结果来自已提交状态和正在结算的边界,所以实时运行、封存过证据的运行、恢复后的运行和精确重放得到的 ID 相同。
  • 这次读取的开销只与该种类下带有所列最窄主体的当前记录成正比,与持有人账本的大小无关:每个持有人账本都按种类和主体维护一份当前记录索引,由记录派生,从不保存。不发布任何内容的边界不会为运行持有的知识做任何工作,发布的边界只为它自己的记录付出开销。

账本属于一个持有人:未死亡的人物(KnowledgeHolderRef::Person)、军队、政府,或者 schema 把 holder_policy 设为 KnowledgeHolderPolicy::Allowed 的领域记录实体。向其他持有人发布知识会以 InvalidKnowledgeHolder 失败。账本没有继承关系:继任机构的账本从空开始,直到有插件向它发布知识。

读取方 接口 得到什么
绑定到人物或机构的玩家或智能体 CanwuViewer::query_knowledge 自己的账本,形式为 KnowledgeRecordView,不含来源证据
研究主体或开发者主体 CanwuViewer::query_holder_knowledge、CanwuViewer::audit_knowledge_record 任意现存持有人的账本;审计调用返回一条带 origin 的完整 KnowledgeRecord
公开主体 无 query_knowledge 以 InvalidKnowledgeAuthority 失败
受信任的上层应用代码 Canwu::admin_query_knowledge、Canwu::knowledge,以及只读当前记录时的 knowledge_heads 和 knowledge_current_records 任意持有人的账本,或完整的 KnowledgeSnapshot
边界系统或命令处理器 SimulationView::knowledge_records 视图读取切面上任意持有人的账本;系统契约必须声明 StateKey::core_knowledge() 读取

用 Canwu::viewer() 打开查看器,它遵循本次运行的观察策略;角色席位也可以用 viewer_for_actor。安全读取与呈现状态演示了两种用法,并说明如何构建客户端载荷。

查询结果按 learned_at 排序。默认的 KnowledgeHistoryView::CurrentHeads 会隐藏已被后续记录取代的记录,FullHistory 则返回 schema 的留存策略保留下来的全部记录,结果中的 retention 说明各 schema 适用哪种策略。持有人本地记录号来自持有人自己的计数器并随条目保存,所以不会改变、不会重新开始,读取方也无法据此推断其他持有人有多少条事实。游标绑定持有人、查询和读取切面。持有人得知新内容后,读取切面随之变化,旧游标会以 KnowledgeReadCutUnavailable 失败;其他持有人收到新消息,这个游标仍然有效。

从 0.22.1 起,读取一页的开销只取决于这一页,不再随持有人的全部历史增长。每个持有人账本都把自己的条目,以及其中的当前条目,按 schema、learned_at 和本地记录号排好顺序。读取一页时,在查询列出的每个 schema 里直接跳到 learned_after 或游标所在的位置,按顺序往下读,读到比上限多一条匹配,或越过 learned_at_or_before,就停下。它读到的是要返回的记录,加上被主体过滤跳过的记录,所以只按主体过滤的查询仍可能读遍所列 schema 的大部分条目。读取切面来自账本在条目进出时随时更新的哈希,推导它不必读任何条目,得到的值也与 0.22.0 相同。开销转到了提交上:条目因留存或移除离开持有人时,这次提交会把该持有人在它之后的条目重新哈希一遍,所以在很长的历史里清退一条旧条目,要为这段历史付出一次开销。每条新条目多花几微秒,加载存档时会重新哈希每一条条目,账本每条条目也多占约一成内存。

只需要某个持有人在某一 schema 种类下当前记录的受信任的上层应用,不必分页读取整本账本。Canwu、CompactedCanwu 和任何 CanwuRun 上的 knowledge_heads(holder, kind, subjects),对同一份已提交状态,返回边界里 SimulationView::knowledge_heads 返回的那些全局 ID(最多 64 个,升序,匹配更多时返回最新的;所列主体超过 64 个会以 KnowledgeLimitExceeded 失败)。knowledge_current_records(holder, kind, subjects) 则按发布顺序、最新在前,遍历同样的记录,形式是 CurrentHeads 页返回的持有人相对 KnowledgeRecordView。两者都读取持有人的当前记录索引,不推导读取切面,所以开销随读到的记录数增长;需要绑定读取切面的游标时,用 admin_query_knowledge 分页,它的开销同样只取决于这一页。它们会返回任意持有人的载荷,所以只供上层应用使用:CanwuViewer 没有这样的调用,记录要先由上层应用挑出属于该角色的部分,才能送到角色面前。

CanwuViewer::visible_changes_since(since) 为之后每个受众包含该查看者的事件返回一个 VisibleChange(时间、摘要和来源事件)。每次知识发布都产生一个 KnowledgePublished 事件,列出它发给的全部持有人(holders,连同这条指令的 record_count 和 entry_count),其中每个持有人都是它的受众,客户端可以借此发现新知识,再去查询账本。研究主体和开发者主体能看到所有事件。其他受众类型见事件系统。

两个扩展发布以下 schema:

schema 发给谁 何时发布
canwu.information representation_available 投递尝试的收件人 尝试变为 Delivered
canwu.information access_recorded 阅读者本人 记录一次访问
canwu.information interpretation_recorded 解读所服务的持有人 记录一次解读
canwu.information release_available 受众中的每个成员 面向受众的发布变为 Active
canwu.correspondence routing_endpoint、routing_connection、address 种子中指定的持有人 准入一个 NetworkKnowledgeSeed
canwu.correspondence attempt_report 承运人 承运人扣押结束了一次投递尝试,除非承运人此时已不接受知识

信息 schema 的载荷只有 record_version,主体字段指明是哪些记录。持有人得知某条记录存在之后,要展示哪些内容由上层应用决定。每条事实只发给一个持有人,所以经手人私下的阅读不会传给别人,受托承运人的路线知识不会进入发件人的账本,扣押报告也不写出扣押者。

canwu-knowledge 和 canwu-information 不抽取随机数。canwu-correspondence 在 correspondence-lifecycle-v1 上声明了一条随机流:operation-resolution,版本 1。它进行以下操作定址随机抽样:

操作类型 决定什么
communication_opportunity 通信机会是否被选中:在 0 到 999 之间抽样,小于应用提供的 probability_per_mille 即选中
communication_recipient 被选中的机会指向哪位候选收件人:在候选名单中均匀抽取一位
correspondence_incident 意外事件是否发生:在 0 到 999 之间抽样,小于应用提供的 probability_per_mille 即发生

0 到 999 的抽样结果以 roll_per_mille 保存在通信机会或意外事件记录上。持有人能否识破伪造的自称来源,由应用自己抽样决定;信息扩展只记录由此得出的 AuthenticityFinding。

持有人账本保存在 SimulationSnapshot::knowledge 中:按 ID 排列的记录正文,以及每个持有人的下一个本地记录号和它按本地记录号排列的条目。加载时,没有持有人持有的记录正文、指向不存在记录正文的条目、乱序或不低于持有人计数器的本地记录号、在持有人内解析不了的关联,以及重复的记录正文或持有人都会被拒绝。持有人本地记录号随条目保存;读取切面哈希和游标绑定由条目推导,从不保存,分页顺序和读取切面所用的哈希在加载存档时重建。知识的承诺按块计算:每个条目一个叶哈希,每 256 个本地记录号一个块根,每个持有人一个根,整本账本一个根,所以一次发布只重算它触及的块,条目也可以离开。信息和通信的状态都是普通的领域记录。

每条边界记录都保存本边界的 knowledge_changes,精确重放会重新计算并与之比较;恢复时引擎按边界从日志重建账本,套用取代规则、预算和留存策略,账本对不上的存档会被拒绝。信息操作按 ID 和规范输入哈希保证幂等:完全相同的重试不改变任何状态,同一 ID 配上不同输入则以 IdempotencyConflict 失败。解码之类的判断在你的代码中完成,结果以已记录的内容和解读记录进入模拟,所以重放直接复用这些结果,不会再次运行你的解码器。

  • 持有人及其初始知识:通过 Scenario::knowledge 提供;通信规划所需的知识也可以用 NetworkKnowledgeSeed 写入。
  • 你自己的知识 schema,以及根据你的机制决定谁得知什么的第 4 或第 13 阶段系统。
  • 信息内容:正文、格式、自称来源、渠道配置和受众成员名单。
  • 解读结果,包括 AuthenticityFinding。
  • 权限:发信和恢复所需的控制者与决策票据(可用 correspondence_decision_ticket 和 correspondence_recovery_decision_ticket 构建)、承运人委托命令;委托解读还需要一个名为 canwu-authority、接受 delegate_interpretation_v1 命令的插件。
  • 通信机会、意外事件和伪造识别的概率。
  • 根据 query_knowledge 和 visible_changes_since 的结果构建的客户端 DTO。

知识账本(DEFAULT_KNOWLEDGE_PAGE_SIZE、MAX_KNOWLEDGE_PAGE_SIZE 和 KnowledgeLimitsV1::CURRENT):

限制 数值
查询的默认和最大分页大小 100 条和 1,000 条
每个插件的知识 schema 数(schemas_per_plugin) 256
一条指令的记录正文数,各组合计(records_per_batch) 1,000
一条指令的持有人条目数,各组的持有人数 × 草稿数合计(holder_entries_per_batch) 4,096
一个边界的记录正文数,所有发布方合计(records_per_boundary) 32,768
一个边界的持有人条目数(holder_entries_per_boundary) 32,768
一个边界新记录正文的载荷字节数(payload_bytes_per_boundary) 64 MiB
每条记录的载荷字节数(payload_bytes_per_record) 65,536
每条记录的主体、证据引用、取代关联和矛盾关联数(relations_per_record) 各 64
摘要和获取方式文本(text_bytes) 1,024 字节

按边界计的几行是所有发布方共用的一份预算,用 knowledge_publication_budget() 读取;没有按系统计数的上限,只有发布预留为某个插件保留的份额。信息生命周期(InformationLimitsV1::canonical())限定每次发送的指定收件人数和显式受众成员数各为 10,000、每位收件人的投递尝试数为 256、内联正文为 65,536 字节,其余上限见 rustdoc。

一次通信机会列出 1 到 64 位候选收件人。

终端窗口
cargo run -p canwu-information --example confidential_copy_release
cargo run -p canwu-information --example encoded_interception
cargo run -p canwu-correspondence --example routed_correspondence
cargo test -p canwu-knowledge -p canwu-information -p canwu-correspondence