Calendar Event
0. 规范语言
本文中的规范关键字(MUST / SHOULD / MAY 等)按 conformance/normative-language.md 解释;仅大写形式具有规范约束力。
1. 模型与激活
Calendar event 是一个带 Calendar schedule 子树的 Strand,而不是新的顶层对象 kind。事件标题、描述、议程和附件继续由 Strand / Message / Morph / Relation 表达;日程语义由 Strand metadata 中的 schedule 子树表达。实现 MUST NOT 新增 calendar 专用 typed id。可视化日历、gantt 或 agenda 是 View renderer / projection,而不是新的真相源。
ak.profile.calendar_event.v1 只表示实现 conformance profile,MUST NOT 被写入对象 metadata 充当对象 kind 或 schema 激活标记。
唯一激活路径(normative):Calendar Strand 的 schema_refs[] MUST 包含 ak.schema.calendar_event.v1,schedule 数据 MUST 位于单一命名空间 metadata.fields.calendar,且该子树 MUST 完整通过 calendar-event.schema.json 校验。
- 双向共现:schema ref 与
metadata.fields.calendar子树 MUST 在 post-patch 对象上同时出现或同时不出现。只删 ref 保留子树、或只删子树保留 ref,均 MUST 以schema_violationreason=calendar_activation_mismatch拒绝。 - 容器 self-schema
ak.schema.strand.v1MUST NOT 出现在schema_refs[](与morph.md§4 同一规则)。 - 扁平
metadata.fields.start/end/timezone/all_day/recurrence/attendees,以及metadata.fields.profile/profile_refs,都 MUST 被 Strand schema 拒绝。实现 MUST NOT 通过”存在若干字段”推断 Calendar 语义。 - 写入 Calendar 子树的
ak.strand.create/ak.strand.updateMUST 由 event-kind registry 选择其封闭 payload class,并在治理 Station 接纳时对 post-patch Strand 的schema_refs[]↔metadata.fields.calendar双向共现与完整 Calendar 子树执行校验;replay 重跑同一已登记 reducer/schema 版本,不读取不存在的 per-Eventrequirementscarrier。 - profile-aware validator MUST 对 create 与 post-patch 的完整子树做全对象校验,而不是只校验被 patch 触及的字段。
schema_refs 是公开、被签名、可路由的对象 schema 激活信息。它会暴露”这是一个 Calendar Strand”这一分类事实:加密部署 MUST 在 privacy disclosure 中把它列为有意的 routing metadata,MUST NOT 声称 schedule 完全不可观察。metadata 加密时,producer MUST 在加密前校验、授权客户端 MUST 在解密后校验;不能解密的服务只校验 schema_refs 与 envelope,MUST NOT 声称验证过 schedule plaintext。
2. Schedule 字段
metadata.fields.calendar 的 required core 是 start、end、timezone、tzdb_version、all_day、status 六项;可选 profile fields 是 recurrence、location、call_id、attendees。字段顺序在 schema 和 prose 中 MUST 保持下列顺序:
start(required):区间下界,同时是 recurrence 锚点。end(required):区间上界(排他)。timezone(required):canonical IANA Zone name;all-day 事件也 MUST 保留。tzdb_version(required):解释timezone与全部派生 instant 的 IANA TZDB release tag。all_day(required):boolean,选择 §3 的两个时间分支之一。status(required):confirmed | tentative | cancelled,无隐式默认,MUST NOT 省略。recurrence(optional):§4 的 RFC 8984 子集。location(optional):加密 envelope 或封闭的 plaintextcalendar_location对象。call_id(optional):Arkret 通话 / 会议 session ID。attendees(optional):§7 的 canonical roster。
status 与通用 Strand stage / state 正交:state 是对象物理生命周期,stage 是业务进度轴,status 只表达该日程本身是否成立。实现 MUST NOT 把 stage 私自解释成 calendar status,也 MUST NOT 用 status 表达对象生命周期。
3. 时间语义与区间
Calendar schedule 不携带绝对 instant。timed 事件的 start / end 是 RFC 8984 LocalDateTime(time.schema.json#/$defs/local_date_time,整秒、无 offset、无 Z、无括号 Zone、无小数秒),按 timezone + tzdb_version 解释。这保证”固定本地会议时间”在 TZDB 规则变化时不会被 UTC instant 反推出另一个 local anchor。
- 半开区间:无论哪个分支,事件区间都是
[start, end)。 - all-day 分支(
all_day=true):start/end都是 Gregoriandate。endMUST 严格晚于start,单日事件写成次日日期;end == startMUST 拒绝。 - timed 分支(
all_day=false):start/end都是整秒LocalDateTime。producer / reducer 按 §5 的 discontinuity 规则把 base start/end 转成 instant,并 MUST 要求base_end_instant > base_start_instant。 - recurring duration:v1 不引入 ISO duration wire 字段。recurring timed event 的 duration 固定为
base_end_instant - base_start_instant;每个 occurrence 的 local start 转成 instant 后加同一 elapsed duration 得到该 occurrence 的 end。recurring all-day event 的每个 occurrence 保留end_date - start_date个 local calendar day。实现 MUST NOT 自行在 elapsed duration、local wall-clock delta 与包含式日期之间选择。 - JSON Schema 无法表达跨字段时序比较,故
end与start的比较由 reducer / profile 承载:producer 写入前 MUST 校验,reducer 在不满足时 MUST 以schema_violation拒绝(与 §7 attendee 唯一性同一模式)。
4. Recurrence
v1 recurrence 是 RFC 8984 JSCalendar RecurrenceRule 的 snake_case 子集:frequency、interval、by_day、by_month、by_month_day、by_set_position、first_day_of_week、count、until。字段逐一映射到 JSCalendar 的同名 camelCase 字段;没有 Arkret 自创的 RRULE 语义。frequency 使用 JSCalendar 小写值 daily | weekly | monthly | yearly。by_day[] 是 NDay[]:每项包含小写 day(mo..su)和可选非零 nth_of_period,因此 1MO / -1FR 分别写作 {day:"mo",nth_of_period:1} / {day:"fr",nth_of_period:-1}。
4.1 默认值与隐式 filter
- 省略
interval等价于1;省略first_day_of_week等价于mo。 rscale固定 Gregorian,skip固定omit;二者不出现在 wire 上。- JSCalendar
@type由 Arkret schema context 隐含,wire 上 MUST NOT 携带。 - base
start总是第一个 occurrence,并计入count,即使它不匹配任何by_*。 - v1 子集不暴露
by_hour/by_minute/by_second,因此每个 occurrence 的 hour/minute/second MUST 继承 base start;weekly缺by_day时取 base weekday;monthly同时缺by_day/by_month_day时取 base month-day;yearly缺省时按 RFC 8984 隐式补 base month 与 month-day。所有隐式 filter MUST 进入同一 expansion 算法与ak.vector.calendar.recurrence_expansion.v1,MUST NOT 由 UI 预填字段模拟。
4.2 终止条件与收窄
count与untilMUST NOT 同时出现;同时出现一律schema_violation。until与 occurrence 的 local start 做<=比较,并与start同分支:all_day=true时是 Gregoriandate,否则是整秒LocalDateTime。untilMUST NOT 早于 base start。timed 分支既然拒绝小数秒,parser 就 MUST NOT 私自接受.0/.000。count收窄为1..=10000;by_set_position与nth_of_period收窄为-366..=-1 | 1..=366;by_month_day收窄为-31..=-1 | 1..=31;by_month只允许无前导零的"1".."12"。- 所有出现的
by_*数组 MUST 非空且元素唯一;空数组一律schema_violation,MUST NOT 等价于缺省。 - 搭配前置条件:
nth_of_period只允许monthly/yearly;by_month_dayMUST NOT 与weekly搭配;by_set_positionMUST 至少与一个by_day/by_month/by_month_day同时出现。不满足前置条件 MUST 直接拒绝,MUST NOT 接受后忽略。 excludedRecurrenceRules、recurrenceOverrides、RDATE / EXDATE、逐实例 reschedule 或 cancel,以及任何未列出的 RecurrenceRule 成员,在 v1 baseline 中 MUST 以 schema reject 或unsupported_feature失败,MUST NOT 静默忽略。
4.3 展开边界
无 count / until 的 recurrence MUST NOT 提供”展开全部”操作。每次 expansion MUST 携带有限 [range_start, range_end) 与 limit,并受 ../conformance/scalability-constraints.md 的单次上限与 candidate-period 扫描预算约束;预算耗尽时 MUST 返回带 continuation 的 limit_exceeded 或 partial result。continuation MUST 绑定 schedule revision、tzdb_version、range 与排序键;任一 basis 改变后旧 cursor MUST 失效,MUST NOT 在新 schedule 上续跑。
5. Timezone 与 TZDB 版本
timezone MUST 是 calendar-timezone-registry.json 中该 schedule 所 pin 的那一行 release 的 canonical Zone name。registry 的 required_normalizations 是逐 release 冻结的:某个后续 IANA release 改变了 Link target 时,由它自己的行携带新映射,早先的行保持签名当时生效的映射。实现 MUST 用 tzdb_version 选中行后再查表,MUST NOT 使用跨 release 的全局映射。producer MUST 在签名前把 Link alias 归一化为 canonical Zone;receiver MUST NOT 对已签名的值再做归一化——出现在 wire 上的 alias 是 producer 缺陷,MUST 拒绝而不是修复。occurrence key 复用该 exact signed value。
tzdb_version 是被签名覆盖的 IANA release tag(形如 2026a)。receiver MUST 用该 release 的规则解释 timezone 与全部派生 instant,MUST NOT 依赖本地安装版本。服务在 ServiceDescribe.calendar_tzdb_versions 声明可执行版本集合;无法执行某 schedule 的版本时 MUST 返回 calendar_tzdb_mismatch / unsupported_feature,或把 instant projection 标为 unresolved,MUST NOT 回退到相近或更新的 release 后静默产生另一结果。conformance 比较 MUST 在 schedule 所绑定的同一 TZDB version 下进行。
显式更新 tzdb_version MAY 改变派生 instant,但 MUST NOT 改变已签名的 timezone,也 MUST NOT 改变任何 local occurrence key;只有显式 timezone patch 才改变 occurrence identity。若任一 occurrence 的 instant 或 end 因此变化,§6 的 significant-change 规则 MUST 触发 RSVP needs_reconfirmation 与 schedule notification。
DST 不连续:重叠(fold)与不存在(gap)的 local time MUST 直接镜像 RFC 8984——一律以 transition 之前的 offset 解释。实现 MUST NOT 各自选择拒绝、取后 offset 或产生两个 occurrence。
6. Schedule revision 与 occurrence 身份
创建携带 Calendar 子树的 Strand,或显式写入该子树(含其祖先路径 metadata、metadata.fields 的替换/删除)的 Event,构成该 Strand 的 schedule revision 序列。写入与原值相同仍是一次显式 revision;是否需要 RSVP 重新确认则按下文 significant-change 表比较日程值,不能把“新 revision”直接等同于“日程已改变”。仅写入 metadata.title 等不包含 Calendar 子树的路径不产生新的 schedule revision。此定义按签名写入意图求值;序列的次序由治理 Station 在该 stream 上给出的 stream_position 决定,不依赖 receiver 的到达顺序。
当前 schedule revision 是该 Strand 所属 stream 上最后一个被接受的 eligible schedule revision。次序只由治理 Station 在该 stream 上给出的 stream_position 决定:位置更大的已接纳写入取代位置更小的。中间任意不写 Calendar 子树的更新既不产生 schedule revision,也不影响该次序。实现 MUST NOT 用 HLC、created_at、墙钟或本地接收顺序选值;同一段已提交前缀在任何重放顺序下 MUST 给出同一个当前 revision。
Calendar patch 与标题/正文 patch 使用同一对象基线规则,不添加额外 schedule base。
受限历史的 current 来源(normative):既有 current 体系的 calendar_schedule_source selector 以 strand_id 精确选择 closed calendar_schedule_source_value,字段顺序固定为 effective_scope, source, strand_revision, metadata_context。每个 Strand writer MUST 同事务更新此结果与 Strand current;两结果的 revision/stream MUST 相同,value.strand_revision MUST 等于该 revision。accepted_current_revision 派生本次 accepted Commit 的 {commit_id,stream_position};calendar_source_effective_scope 从 create-locked Strand scope 派生。来源 source 是最后 eligible 写入的 exact committed ref 或 JSON null;calendar_schedule_source_ref 按完整原接纳前缀与签名写入空间求值,title-only/tracks/生命周期后继保留来源。不得以本次对象 revision 补造来源。whole encrypted_metadata set/unset 是 metadata 祖先写入,不按明文差异或密文相等判断。
calendar_metadata_context 在当前 encrypted_metadata 存在时保存其原加密上下文:closed {source,event_kind,signer_id,payload_digest},source 为产生当前 envelope 的 exact committed ref,event_kind 只允许 Strand create/update,signer_id 为原已验证 Event 的 actual_signer(执行代署取 executed_by),payload_digest 为该 exact envelope 的既有摘要。未加密时 metadata_context MUST 为 null。不能用最后对象写入的 kind/signer覆盖旧加密上下文。客户端 MUST 与同 cut Strand ciphertext、effective scope、来源 stream/position 和完整请求账号/会话绑定;sender domain 使用完整 ActorId 的 canonical JCS,复用现有 AEAD context 重建。该材料是 own Station 认证 current 投影,不是 producer 对裁剪 Event 的签名,不交付原 Event 的无关旧标题或字段。
current 来源/最小上下文 MAY 早于 caller history floor,但只有当前 Strand scope 的获授权读者能取得;该例外不开放历史 scan、runtime 私有材料或历史解密密钥。现有 exact current 读取登记该 selector,不枚举对象;无权/隐藏目标保持既有不披露结果,缺准确配对/来源/上下文保持 unavailable。本站和审计者仍完整验证原接纳事实;普通客户端按既有 own Station 角色消费,重启与新设备不要求本机旧 Event 缓存。服务只判公开 intent/envelope,available 与 encrypted_unresolved 由持钥客户端解密并校验完整日程后求值。
Calendar schedule projection MUST 暴露 canonical schedule_revision_source 与 schedule_resolution_state:
available:唯一 winner 的 Calendar schedule 已解密并通过 schema;只有此状态 MAY 展开 occurrence、发送 RSVP 或派生精确 schedule notification。encrypted_unresolved:当前 revision 无法解密或校验;不得从被取代的旧值、方便投影或本地缓存制造替代 schedule。
并发 schedule 写由 Station 在该 stream 上串行化,无需人工解决;用户后续 ak.strand.update 在当前 revision 之上正常后继。被取代的历史 Event 仍保留 provenance,但不进入 current schedule 候选列表。
calendar_schedule_unavailable 是 authoring 侧与投影侧的错误,MUST NOT 成为服务端 admission 条件:判定可读性需要读取 Calendar 子树明文,E2EE 部署的服务端做不到。authoring client 在本地无法读取 winner 时 MUST 拒绝构造 RSVP;若某个 client 仍然发出,服务端按 §8.4 照常接受(它只看得到 shape 与 envelope),由授权投影把该 winner 归入 §9 的相应类别。这条与 §8.1 第三层同一原则:任何需要 schedule 明文的判定都不得进入 admission,否则 plaintext Realm 会比 E2EE Realm 多拒绝一批 Event,两者 accepted set 分叉。
significant-change 封闭表(normative):下表覆盖 §2 的全部 schedule 字段。表外字段 MUST NOT 触发实现私有的 RSVP 或通知行为;表内字段 MUST 恰好按本表处理,实现 MUST NOT 加严或放宽。
| 变化字段 | instance RSVP | series RSVP | schedule notification |
|---|---|---|---|
start | stale_orphaned | needs_reconfirmation | 是 |
end | needs_reconfirmation | needs_reconfirmation | 是 |
timezone | stale_orphaned | needs_reconfirmation | 是 |
tzdb_version | 仅当该 occurrence 的派生 instant 或 end 实际变化时 needs_reconfirmation,否则不变 | 同左 | 仅在同一条件下 |
all_day | stale_orphaned | needs_reconfirmation | 是 |
status | needs_reconfirmation | needs_reconfirmation | 是 |
recurrence | stale_orphaned | needs_reconfirmation | 是 |
location | needs_reconfirmation | needs_reconfirmation | 是 |
call_id | needs_reconfirmation | needs_reconfirmation | 是 |
attendees | 不变 | 不变 | 是,但只对被增删的 actor |
start、timezone、all_day、recurrence 四行是 identity-affecting fields:它们改变 occurrence key 本身,因此旧 instance RSVP MUST 保留审计并标记 stale_orphaned,MUST NOT 自动迁移到新 key;series RSVP 的 subject 不含 occurrence,故不 orphan,只需重新确认。
投影 MUST 暴露 current schedule source、每个 RSVP 当前值的 entry basis、逐 ref 的解析结果与 stale / reconfirmation 原因,MUST NOT 把旧回应静默显示为新 occurrence 的当前回应。
7. Attendees
calendar.attendees[] 是唯一 canonical schedule roster。membership / invite projection 只能作为 producer 生成 roster patch 的输入,MUST NOT 成为第二个投影真相源。v1 不注册裸 invited Relation kind;未来若需要显式邀请边,MUST 注册明确的 calendar_invite relation kind 并给出 cardinality、授权和隐私规则,MUST NOT 复用 attendees 或 RSVP 暗造邀请投递。
attendees[]长度上限 1000,同一数组内actor_idMUST 唯一。同一 actor 出现多条时 producer MUST 在写入前合并,无法合并时 reducer MUSTschema_violation。uniqueItems只捕获整对象重复,不能替代 actor 级唯一性。role省略时等价于required。organizer只是展示 / 日程语义,MUST NOT 自动产生 capability;同一 roster 至多一个organizer(由 schemamaxContains强制)。需要多 organizer 时另开 profile。display_name_snapshot是可泄露身份的信息:metadata 加密时 MUST 一并加密。- roster 增删 MUST NOT 创建 Realm / Circle membership,也 MUST NOT 扩大 history access。能看到日历事件不等于能看到其 private discussion 或 Circle-scoped 内容。
8. RSVP
RSVP 通过 ak.rsvp.set 写入,payload 是 {event_ref, occurrence, entry}。
occurrence:JSONnull表示整个 series;实例级 RSVP 使用 canonical instance key——all-day 写成YYYY-MM-DD,timed 写成YYYY-MM-DDTHH:mm:ss[Zone](Zone 为已签名的 canonical IANA Zone name)。两种分支的日期部分都 MUST 是真实的 proleptic-Gregorian date;只匹配数字正则但实际不存在的日期(如2026-02-30)MUST 以rsvp_occurrence_not_canonical拒绝。该判定只需解析本 Event 的签名字符串,属于 shape admission,不依赖 Calendar 子树明文。producer MUST 在签名前生成 canonical key;receiver MUST 拒绝非 canonical key,MUST NOT “接受后修复”自己的缓存地址。非 recurring Calendar 只允许occurrence=null,避免同一单次事件同时出现 series 与 base-instance 两个 typed current result。entry是封闭对象{schedule_basis_refs, response | encrypted_response}。
8.1 schedule_basis_refs
entry.schedule_basis_refs[] 精确列出 responder 实际观察到的唯一 schedule revision,series 与 instance RSVP
均必填且长度固定为 1。保留数组 wire 形状是既有 payload 合同,不表示多头;其 item 是 ak:event: typed id:
- schema 将
minItems=maxItems=1固定为单来源;空数组、多项或重复项都以rsvp_basis_malformed拒绝; - item MUST 是格式合法的
ak:event:typed id;不满足时以rsvp_basis_malformed拒绝,MUST NOT 进入 pending。 该判定只看本 Event 自身的这一个字段,是纯 byte 级判定,无需解析被引用的 Event,因此服务端在 E2EE 下 同样 MUST 执行; - 客户端不得把已被取代的 schedule Event 填入 basis。该字段是这条关联的唯一载体:
ak.rsvp.set的 payload 恰好携带一个entry,因此不存在需要 envelope 侧镜像来区分的逐项关联,协议也不定义这样的镜像。
判定分层(normative):basis 的校验严格分成三层,且每层的可判定材料在 E2EE 与 plaintext Realm 中完全相同,因此两类部署 MUST 得到同一个 canonical accepted set:
| 层 | 判定材料 | 未通过时 |
|---|---|---|
| shape admission | 只看本 Event 自身的 schedule_basis_refs(恰好一项、格式合法的 ak:event: typed id) | rsvp_basis_malformed 或 schema_violation 拒绝,MUST NOT pending |
| target admission | 被引用 Event 的明文 envelope:realm_id、kind、target ref | 引用 Event 尚未到达时按 event-and-patch.md §4.3.1 的 dependency_missing 保持 pending;已到达但 Realm / kind / target 不符时拒绝 |
| basis 有效性 | 需要读取 Calendar 子树明文:唯一引用来源是否构成目标 schedule revision、是否等于 authoring 时实际观察的 winner | 不是 admission 条件,只在授权投影中判定,见 §9 的 unresolved_basis / stale_orphaned |
Envelope 在 E2EE 下同样是明文,因此 target admission 不构成解密要求;只有第三层需要明文 schedule,故它 MUST 留在投影侧。服务端 MUST NOT 因为自己恰好能读明文 schedule 就在 admission 阶段追加第三层判定——那会让 E2EE 与 plaintext Realm 分叉出两套 accepted set。
8.2 response 与隐私
response / encrypted_response 是 oneOf 互斥、必居其一的两个分支,其合法性由目标 Strand scope 是否已有 accepted ak.mls.genesis 唯一判定:
- 该 scope 已激活(存在 accepted
ak.mls.genesis)时只有encrypted_response合法,出现明文分支 MUST 以mls_activation_required拒绝; - 该 scope 尚未激活时两分支均可,但明文 response 需要双重授权,缺任一侧 MUST 返回
unsupported_feature:Realm policy 侧在event-payload.schema.json#/$defs/plaintext_data_class的封闭枚举中授予rsvp_response,服务侧在 ServiceDescribe 的plaintext_visibility.data_classes中声明同一rsvp_response。只有 ServiceDescribe 单侧声明不构成合法授权; - 激活是不可逆的:已激活 scope 的任何回到明文分支的写入 MUST 以
mls_activation_irreversible拒绝;已激活但部署拿不到 scope encryption key 时 MUST 返回unsupported_feature,MUST NOT 静默降级为明文分支。
实现 MUST NOT 用 Calendar 专属开关覆盖 scope floor;v1 不存在”Realm policy 例外允许 plaintext RSVP comment”这一说法。
明文分支的 response 与 encrypted_response 解密后的 plaintext 共用同一封闭 schema rsvp_response:{status, comment?}。status 是封闭集 accepted | declined | tentative,未知值 MUST schema_violation;comment 若出现 MUST 复用 arkret_short_text、非空、最多 2000 code points,空回应通过省略字段表达。status 与 comment 始终共享同一个加密边界,“status 明文 + comment 密文”不是合法形态。
encrypted_response MUST 使用目标 Strand effective scope 的既有 encryption profile / key epoch。其 content_type 由 schema 以 const 固定为 application/vnd.arkret.calendar-rsvp-response+json(event-payload.schema.json#/$defs/rsvp_encrypted_response),使 receiver 能把该 ciphertext 唯一路由到 rsvp_response 一个解密 schema;携带其它 content_type MUST schema_violation。MUST NOT 为 RSVP 引入服务端可读的旁路 key。
event_ref、occurrence、schedule_basis_refs、responder actor 与 ciphertext size 仍是 routing-visible metadata,MUST 进入 privacy disclosure 与对应向量。实现 MUST NOT 用 strand_content、profile_private_field 或自由文本 notes 覆盖 RSVP 语义。
8.3 收敛
RSVP projection 按 accountable actor 对 (event_ref, occurrence) 使用 current-value projection 收敛;typed current result subject 固定为 encoding.md §9.5.2 的 [payload.event_ref, payload.occurrence, envelope.actor_id]。schedule_basis_refs 不进入 subject,但 MUST 进入 typed current result value。
registry 为该 typed current result write 登记 result_projection = set(payload.entry):整个 entry 是 projection set value,因此 winner 独立携带 basis 与 response。receiver MUST 从 Event payload 重算 reducer projection;无法唯一投影、写目标数量错误或投影值与 payload entry 不一致,MUST 以 reducer_projection_failed 拒绝整个 Event。Event wire 不携带 reducer write。
同一 responder 的 RSVP 当前值是该 stream 上最后一个被接受的 ak.rsvp.set,次序只由 stream_position 决定;并发提交因此被 Station 串行化,不存在两个同时生效的回应。MUST NOT 由 HLC、created_at、深度或到达顺序选边,也不得因为解密后的 plaintext 相同而折叠不同 Event 身份。用户下一次回应正常取代当前值,不存在 RSVP 专用冲突修复流程。
ak.rsvp.set 只表达回应,不修改 Strand schedule,不创建 attendees,也不赋予访问权。
8.4 Admission
admission 输入边界(normative):服务端 admission MUST 只使用 (a) 本 Event 自身的 shape、(b) Event envelope 的明文字段、(c) reducer 拥有的对象顶层字段(state、schema_refs、realm_id 等),以及 (d) capability / policy 状态。它 MUST NOT 使用任何需要读取 Calendar 子树明文的事实。这样 E2EE Realm 与 plaintext Realm 对同一组 Event 产生逐项相同的 canonical accepted set;需要明文的判定一律下沉到 authoring client 的前置条件与 §9 的授权投影。
据此,ak.rsvp.set 的服务端 admission predicate 是:
event_refMUST 解析到同 Realm 的 Strand,且该 Strandstate=active、schema_refs激活 Calendar schema。这三项都是顶层明文字段,E2EE 下同样可判定;- capability resource MUST 覆盖该精确 Strand / effective scope。attendee 身份不自动授予
ak.rsvp.set:持有显式 capability 才是授权真源;反之,持有精确 capability 的非 attendee 允许 RSVP,因为 attendees 是 schedule 数据而非授权真源。产品若只允许 attendees 回应,应通过 grant materialization / profile policy 实现,MUST NOT 在 reducer 中暗加身份判断; occurrenceMUST 过 canonical 语法校验(纯语法,不需要 schedule);entry.schedule_basis_refs按 §8.1 的三层表处理——shape admission 直接拒绝,target admission 在引用 Event 未到达时dependency_missingpending,basis 有效性不进 admission;- target 不存在、不可见、跨 Realm、非 Calendar、非 active 与 capability denied 的对外错误 MUST 服从既有反枚举策略;内部 reason code 可以更细,但 MUST NOT 形成 target existence oracle。
下列判定明确不属于服务端 admission,因为它们都需要 Calendar 子树明文:
| 判定 | authoring client 侧 | 投影侧 |
|---|---|---|
calendar.status=cancelled | MUST 拒绝构造新 RSVP(calendar_event_cancelled),历史 projection 保留 | 已存在的 RSVP 继续按 §9 显示,取消后到达的新 winner 标注为对已取消事件的回应 |
| schedule winner 不可读 | MUST 拒绝构造 RSVP(calendar_schedule_unavailable) | 该 winner 按 §9 的 basis 轴归类 |
| 该 occurrence 当前是否存在 | SHOULD 只对展开得到的 canonical key 构造 RSVP | 按 basis 判定 current / stale_orphaned |
| 非 recurring Calendar 携带非 null occurrence | MUST 拒绝构造(rsvp_occurrence_not_canonical) | 该 instance winner 不参加有效 RSVP fold,归入 diagnostics=non_recurring_occurrence;不得影响 occurrence=null 的 series winner |
服务端 MUST NOT 因为自己恰好能读明文 schedule 就把上表任一行提升为 admission 条件。
9. RSVP 投影
授权实现 MUST 能从已授权事件集本地计算 CalendarRsvpProjection;profile 不要求新增远端 Calendar API。
每个 typed current result 的唯一 winner 沿两条正交轴分类,二者都通过才可参加 effective response;任一轴不通过的 winner MUST 单列并保留审计。projection 只处理已被 accept 的 Event,因此错误 basis 与非 canonical occurrence 不会出现在这里——它们已在 §8.4 的 shape admission 被拒。
| 轴 | 取值 | 参加 effective response |
|---|---|---|
| basis 轴(沿 schedule revision 序列判定) | current | 是 |
effective_needs_reconfirmation | 是,但 MUST 标注需重新确认 | |
stale_orphaned(identity-affecting 修改后的旧 instance winner) | 否 | |
unresolved_basis(任一 ref 不可解析、不可见、或不在目标 Strand 的 schedule revision 序列中) | 否 | |
| response 轴(按 §8.2 分支取值后校验) | resolved | 是 |
response_invalid(解密认证失败,或 plaintext 不满足 rsvp_response) | 否 | |
encrypted_unresolved(缺 key) | 否,且 MUST NOT 伪造 status |
规则:
- basis 轴 MUST 按上表判定,MUST NOT 猜测;无法判定即
unresolved_basis。 - 若存在可参加的 instance winner,则 effective response 只取 instance winner;否则回退到
occurrence=null的可参加 series winner。 - instance 与 series winner MUST NOT 做 union,避免把 fallback 与 override 误显示成两个回应。
- canonical projection 暴露每个相关 typed current result 的 winner 及其 provenance。读取端先验证 envelope,再按分支取 response(
encrypted_response解密后、response直接),均按同一rsvp_responseschema 校验,得到 response 轴取值。 - 被排除的 winner MUST 连同其排除轴与原因一并暴露,供 UI 解释”为什么这条回应不算数”。
- archived / redacted /
calendar.status=cancelled目标的 projection 状态与历史 RSVP 显示规则 MUST 明确;redacted target MUST NOT 继续暴露 roster 或 response 内容。 - projection 输出 MUST 受与 Calendar Strand 相同的 effective scope / history visibility 约束,MUST NOT 用 RSVP 存在性泄露不可见事件或 attendee 身份。
10. Schedule notification
Calendar 的 notification 派生是 server 侧职责,由独立 profile ak.profile.calendar_notification_dispatch.v1(role=server)承载;ak.profile.calendar_event.v1(role=client)只要求生产、验证、展开与本地投影。一个 profile MUST NOT 同时标注 server 与 push 两种 role;provider 投递继续由既有 Push Gateway blind / visible profile 约束,Calendar server profile 只产生最小化 notification 或 wakeup intent。
Calendar schedule 变更通过 ak.strand.update 修改 §2 字段。metadata.fields.calendar 的 start / end / timezone / tzdb_version / all_day / status / recurrence / location / call_id / attendees 都是 schedule-relevant field;这组字段不属于 core notification。
- plaintext schedule 的 receiver 候选集是
pre_state.attendees ∪ post_state.attendees ∪ assigned_to ∪ watch_all,使被移除但仍有 scope 读取权的 attendee 也能获知变更。哪些字段变化真正触发通知由 §6 的 significant-change 表决定——特别是tzdb_version只在派生 instant 或 end 实际变化时触发,仅仅重签同一结果 MUST NOT 产生通知。 - 最终 MUST 按 post-state access / history visibility、mute / block / DND / push rule 过滤,沿用
private-objects.md§3.6 的”生成前过滤”语义。失去访问权者 MUST NOT 收到 notification、stub 或 push wakeup;实现 MUST NOT 把 DND 只解释为 provider transport filter。 - encrypted schedule MUST NOT 为通知扩大解密权:服务端不能从密文推导 attendee diff,也 MUST NOT 把普通 opaque metadata update 谎称为已验证的 schedule notification。它只能按可见的 assigned / watch 关系发送 profile 定义的 blind wakeup,由授权客户端解密后本地派生 schedule inbox。若要让服务端识别 schedule change,MUST 另有显式、签名、最小泄露且带负向量的 routing hint。
- 去重键是
(actor_id, source_event_id, notification_kind=schedule);同一 patch 同时改多个字段只产生一条 notification。 - Calendar attendees 是 receiver set 输入,不是访问权真源。RSVP 变更默认不产生 schedule notification;RSVP 自身的 UI 状态由 §9 的投影展示。
call_id是不扩大访问权的软引用;可解析时 MUST 同 Realm / effective scope,不可见或不存在 MUST NOT 泄露通话状态。
11. 一致性与非目标
两个 profile 的 fixture 必须分开,因为 conformance-suite.md §2 的向量适用性闭包规定profile 的认证集合包含其 required_fixtures[] 所映射向量的并集:client profile 要求 calendar-rsvp-fixture.json(§1–§9 的 12 条向量),server notification profile 只要求 calendar-notification-fixture.json(§10 的 1 条向量)。若让 server profile 引用 client fixture,它会被闭包规则要求通过 recurrence authoring 与 RSVP 投影等它根本不声明的向量,声明块自身即不可满足。只有 submit / scan endpoint 可达而未通过对应向量的实现 MUST NOT verified-claim 任一 profile。局部实现只 MAY 逐项列入 supported_features / supported_features。
v1 明确不包含下列能力;它们是 non-goal / unsupported extension,未实现时 MUST NOT 用”完整日历系统”表述 profile 能力:
- UI 月 / 周 / 日视图、拖拽或甘特图布局;
- free/busy 与会议室资源预订;
- iCalendar / CalDAV / JMAP Calendar 导入导出;
- 提醒器 / 闹钟 / 到点通知;
- recurring instance override、单次取消或改期;
- 外部邮件邀请与 iTIP / iMIP。
若产品目标需要其中任一项,MUST 新增独立 profile / schema / vector,MUST NOT 扩大当前字段的隐含语义。