@xdxer/dingtalk-agent 0.1.5-beta.4 → 0.1.5-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/bin/dingtalk-agent.js +339 -124
  3. package/dist/bin/dingtalk-agent.js.map +1 -1
  4. package/dist/src/actions.js +3 -2
  5. package/dist/src/actions.js.map +1 -1
  6. package/dist/src/doctor.js +1 -1
  7. package/dist/src/doctor.js.map +1 -1
  8. package/dist/src/dws.js +2 -1
  9. package/dist/src/dws.js.map +1 -1
  10. package/dist/src/init.js +2 -1
  11. package/dist/src/init.js.map +1 -1
  12. package/dist/src/map.js +157 -0
  13. package/dist/src/map.js.map +1 -0
  14. package/dist/src/skill-manager.js +5 -0
  15. package/dist/src/skill-manager.js.map +1 -1
  16. package/dist/src/tui.js +369 -0
  17. package/dist/src/tui.js.map +1 -0
  18. package/dist/src/upgrade.js +113 -33
  19. package/dist/src/upgrade.js.map +1 -1
  20. package/dist/src/waits.js +2 -1
  21. package/dist/src/waits.js.map +1 -1
  22. package/package.json +3 -2
  23. package/skills/core/dingtalk-agent-compose/SKILL.md +19 -1
  24. package/skills/core/dingtalk-people-group-memory/COMPLETENESS.md +36 -0
  25. package/skills/core/dingtalk-people-group-memory/SKILL.md +69 -0
  26. package/skills/core/dingtalk-people-group-memory/references/adapters.md +273 -0
  27. package/skills/core/dingtalk-people-group-memory/references/assembly-guidance.md +40 -0
  28. package/skills/core/dingtalk-people-group-memory/references/binding.md +110 -0
  29. package/skills/core/dingtalk-people-group-memory/references/cold-start.md +70 -0
  30. package/skills/core/dingtalk-people-group-memory/references/config-binding.md +89 -0
  31. package/skills/core/dingtalk-people-group-memory/references/consent-and-visibility.md +83 -0
  32. package/skills/core/dingtalk-people-group-memory/references/consolidation.md +162 -0
  33. package/skills/core/dingtalk-people-group-memory/references/event-ingest.md +103 -0
  34. package/skills/core/dingtalk-people-group-memory/references/guided-setup.md +70 -0
  35. package/skills/core/dingtalk-people-group-memory/references/model.md +148 -0
  36. package/skills/core/dingtalk-people-group-memory/references/storage-port.md +107 -0
@@ -0,0 +1,148 @@
1
+ # 建模 —— 人与群的信息模型(存储无关)
2
+
3
+ 本篇**不出现任何介质概念**:没有页、没有文件夹、没有物理地址。这里定义的是「记什么」,不是「存哪」。存储怎么落见 `storage-port.md`,介质特有的坑见 `adapters.md`。
4
+
5
+ 判断本篇是否真的解耦,用这个检验:**如果这世界上只有本地 Markdown 文件这一种存储,这个模型会长成同样的样子吗?** 会,才算解耦。
6
+
7
+ ## 一、两个一等实体
8
+
9
+ | 实体 | 主键 | 主键从哪来 | 说明 |
10
+ |---|---|---|---|
11
+ | **Person** | `personKey` | IM 源适配器提供的稳定人标识 | **不是**显示名、不是花名、不是员工编号(员工编号在有些渠道缺失,或属于另一个命名空间)。具体取哪个字段见 `adapters.md` |
12
+ | **Conversation** | `conversationKey` | IM 源适配器提供的稳定会话标识 | 单聊与群聊**都是** Conversation,用 `kind` 区分。整串使用,不得截断或当路径片段 |
13
+
14
+ 🔴 **群不是人的集合。** 群独有、无法由成员并集推导的八类:隐私档位、生命周期(建群时间/规模变化)、治理角色、群内专属称呼、**群约定与口径**、自动化装配、**群作为权限主体**、"这事在哪讨论过"的话题地址与沉默节奏。判据:删掉全部成员档案,群档案依然可读可用。
15
+
16
+ 🔴 **单聊也是 Conversation,但不给它建群档案。** 单聊的价值全部归到对方那个 Person 上;`kind=dm` 的 Conversation 只用来承载水位与去重键。
17
+
18
+ ## 二、Fact —— 唯一的落盘单位
19
+
20
+ 模型层只往存储里放一种东西:**Fact**。所有的"页""段落""条目"都是存储层的表现形式。
21
+
22
+ ```yaml
23
+ fact:
24
+ subject: { type: person|conversation, key: <主键> }
25
+ layer: raw | derived # 原始层只增不删;派生层可整体重建
26
+ section: <语义分区名> # 见下表;不是文件名
27
+ occurredAt: <绝对时间, 事件真实发生时间>
28
+ recordedAt: <绝对时间, 落盘时间>
29
+ scope: <发生地> # dm:<key> | conv:<key> | todo | doc
30
+ audience: A0 | A1 | A2 | A3
31
+ provenance: said | observed | agreed | inferred
32
+ evidenceId: <源系统主键> # 全体系唯一,即天然去重键
33
+ body: <正文>
34
+ supersedes: <evidenceId?> # 纠正时指向被替代项,不改写原件
35
+ ```
36
+
37
+ **`evidenceId` 是整个模型的去重基石**:同一条源事件无论被哪条路径处理到,都产出同一个 `evidenceId`,因此重放安全、并发安全、跨渠道不双写。没有 `evidenceId` 的事实(如人工观察)用 `hash(subject+occurredAt+body)` 合成一个。
38
+
39
+ **`layer` 只有两种,别造第三种**:
40
+
41
+ - `raw`:丢了不可重建,所以**只增不删**,纠错只能靠 `supersedes` 并列新增。
42
+ - `derived`:从 `raw` 编译出来的,**永远可整体重建、可 curate(去重/合并/退 superseded)**。
43
+
44
+ 🔴 **没有"derived·只增"这种东西。** derived 若又只增又不许 curate,就会可重建却无泄压阀——一年后累积过百条越过读墙、fail-open。derived section 的"一条仍生效的结论不会静默消失"是**语义保证**,靠 curate 时"每次收缩都能从 raw 重放复现"来兑现,不是靠禁止收缩。详见 `consolidation.md` §7。
45
+
46
+ 这条不变式由模型层保证,不下放给适配器。
47
+
48
+ ## 三、Section —— 两个正交维度:layer × kind
49
+
50
+ 每个 section 有两个正交属性:
51
+
52
+ - **`layer`**(上一节):`raw`(只增)/ `derived`(可重建)——决定能不能改写。
53
+ - **`kind`**:`narrative`(叙事)/ `registry`(注册表)——**决定该落哪种介质**。
54
+
55
+ 🔴 **kind 是这个模型最容易做错的一处,也是"索引类不该塞进文档"的根**:
56
+
57
+ | kind | 长什么样 | 访问模式 | 该落哪 |
58
+ |---|---|---|---|
59
+ | `narrative` | 一段会长大的正文,人打开就能读整份 | 读整页、drill-down;不按字段查询 | **文档类介质**(markdown 放得开、有排版) |
60
+ | `registry` | 一行一条、按 key 定位的结构化记录 | **按 key 查存在、枚举、否定查询、按 key 改一行** | **结构化介质**(有真分页/按字段查/按行改) |
61
+
62
+ 把 registry 塞进文档,就会撞上一整套本不必存在的病理——341 行截断、"判存在只能翻完页"、分桶防 fail-open、count 对账——这些**全是在给"结构化数据被迫上叙事介质"擦屁股**。放到有真分页、按字段查、按行定位的结构化介质上,这些病理**直接消失**(具体介质与病理对照见 `adapters.md`)。
63
+
64
+ **每个 raw 家族配一个 `*-digest`(derived, narrative)**:raw 逐次累积、按月归档;digest 是编译层读取底座。跑一年的压缩机制见 `consolidation.md`。
65
+
66
+ ### Person
67
+
68
+ | section | layer | kind | 装什么 |
69
+ |---|---|---|---|
70
+ | `identity` | derived | narrative | 他是谁、称呼与别名、组织位置。稳定层,单调粘滞 |
71
+ | `working-on` | derived | narrative | 当前在推什么、卡在哪。**时效层,会 decay** |
72
+ | `collaboration` | derived | narrative | 他负责什么、该找他确认什么、偏好、明确纠正。稳定层 |
73
+ | `public-facts` | derived | narrative | **唯一可对第三方取材**,每条带源会话与日期。可 curate |
74
+ | `interaction` | **raw** | narrative | 单聊蒸馏,逐条带归因。按月归档 |
75
+ | `activity` | **raw** | narrative | 机器抄录、可重放:待办三元、文档编辑、引用边。按月/周归档 |
76
+ | `interaction-digest`/`activity-digest` | derived | narrative | 封月月度摘要,每行携 evidenceId 锚点 |
77
+
78
+ ### Conversation(仅 `kind=group`)
79
+
80
+ | section | layer | kind | 装什么 |
81
+ |---|---|---|---|
82
+ | `charter` | derived | narrative | 这群干嘛的、在推什么、谁说了算、**发言档** |
83
+ | `conventions` | derived | narrative | 群公开口径/流程/术语/禁忌,每条带拍板人日期。可 curate |
84
+ | `chronicle` | **raw** | narrative | 议题级纪事+群生命事件。**留足**(议题级完整,别压成薄摘要)。按月归档 |
85
+ | `chronicle-digest` | derived | narrative | 封月月度摘要 |
86
+
87
+ ### Registry(索引与元数据 —— 走结构化介质,不进叙事文档)
88
+
89
+ 这些是 A0(仅 Agent),一行一条、按 key 查:
90
+
91
+ | registry | 主键 | 一行装什么 |
92
+ |---|---|---|
93
+ | `person-index` | `personKey` | 显示名、profile 位置、最后活跃、状态。**"这人有没有档案" = 按 personKey query,查不到就是没有** |
94
+ | `conversation-index` | `conversationKey` | 群名、groupType、memberCount、myRole、位置。取代文档版 `00-群索引` |
95
+ | `watermark` | `(subjectKey, channel)` | 水位值。按 key O(1) 取,不再"读整页 parse" |
96
+ | `count` | `(subjectKey, section, partition)` | 完整性对账条数(见 `consolidation.md` §0) |
97
+ | `roster` | `(conversationKey, personKey)` | 成员在本群的位置与角色。可丢弃可重拉 |
98
+ | `tombstone` | `(subjectKey, scope)` | 遗忘账本:范围+要求人+日期。**结构化介质无读墙 → 不会 fail-open** |
99
+ | `proposal-ledger` | `subjectKey` | 已提议建档的对象,防重复骚扰 |
100
+
101
+ 🔴 registry 走结构化介质后,`consolidation.md` §0 的 count 对账、`storage-port.md` 里 `lookup.negative=list-only` 与分桶那套,**对这些 section 不再需要**——它们是叙事介质的补丁,不是模型的本质要求。narrative section 仍需要它们。
102
+
103
+ ## 四、Audience —— 写入时定,读取时只收窄
104
+
105
+ | 档 | 谁能看 |
106
+ |---|---|
107
+ | `A0` | 仅 Agent 自己(索引、绑定档案、停用账本) |
108
+ | `A1` | 仅本人 + Agent owner |
109
+ | `A2` | 该会话的成员 |
110
+ | `A3` | 任意同事 |
111
+
112
+ **回答任何人之前先算交集**:引用事实的 `scope` 圈子不含提问者 → 不可用,**宁可答"不清楚"**,不要给一个模糊版本。
113
+
114
+ 🔴 `audience` **不由 Agent owner 代替本人授权**。「Agent 的 owner」与「事实的当事人」不是等价授权主体,否则一个主管就能在下属不知情时给每个下属建含立场与卡点的长期档案。
115
+
116
+ 🔴 **会话的隐私档位走白名单**:只有明确判定为"内部/常规"的会话走正常档,**其余一切**(外部群、合作群、未知类型、以及将来新增的任何类型)一律最小披露档。枚举值是平台的、会变;黑名单写法必然漏。
117
+
118
+ ## 五、交叉归属(三条铁律)
119
+
120
+ 1. **原始层按发生地只写一处。** 群里说的只进该会话的 `chronicle`/`conventions`;单聊说的只进那个人的 `interaction`;无会话标识的(待办/文档)只进人的 `activity`,**对会话贡献为零,不许伪造归属**。
121
+ 2. **派生层各写各的结论,跨实体只留引用不复制原文。** 判据:*把这句删掉,变的是「以后对这个人的预期」还是「这个会话以后怎么办事」*——前者写人,后者写会话,都变则写会话 `conventions` + 人 `public-facts` 一行引用。
122
+ 3. **唯一允许的上浮方向:会话公开 → 人的 `public-facts`,audience 继承为 A2。** 写死的红线:单聊 → 任何会话或第三方档案;A 会话 → B 会话;会话里对某人的评价/吐槽 → **任何档案**(连他自己的也不写)。
123
+
124
+ ## 六、Watermark —— 增量的唯一判据
125
+
126
+ 每个 `(subject, channel)` 一个水位,值是**已消化到的最后一条源事件的 `occurredAt`**。
127
+
128
+ ```yaml
129
+ watermark: { subject: <key>, channel: im|todo|doc, value: <绝对时间> }
130
+ ```
131
+
132
+ 🔴 **水位是"消化到哪",不是"哪天跑过"。** 用"今天刷没刷"当判据会退化成全量重刷模型:过午夜全体变旧 → 每轮全量重编 → 窗口装不下 → 永远跑不完、永远降级。
133
+
134
+ 🔴 **先写事实、后推水位。** 写入或回读任一失败即本批作废、水位不动。水位一旦虚高,这批源事件永远不会被再消化。
135
+
136
+ 🔴 **同一条源事件在会话侧推进会话水位,在人侧只是被引用、不推进人的水位。** 人的 im 水位只由单聊推进。任一侧重放都不漏、不双写。
137
+
138
+ **收敛判据**:`某 subject 收敛 ⟺ 它的水位 ≥ 它最新一条源事件的 occurredAt`;整体收敛 ⟺ backlog 为空。
139
+
140
+ ## 七、模型层必须自己保证的不变式(不下放给适配器)
141
+
142
+ 1. `raw` 层只增不删,纠错用 `supersedes` 并列新增。
143
+ 2. `evidenceId` 全局唯一即去重键。
144
+ 3. `audience` 写入时定、读取时只收窄不放宽。
145
+ 4. 交叉归属三铁律。
146
+ 5. 先写事实、后推水位。
147
+ 6. 会话隐私档位白名单默认拒绝。
148
+ 7. **"读失败 ≠ 不存在"**:任何读取失败、分页未完、授权缺失,一律记欠账并跳过本轮,**绝不写成"没有"**,也绝不据此新建重复实体。
@@ -0,0 +1,107 @@
1
+ # 存储端口契约
2
+
3
+ 本篇定义 Fact 怎么落地。**模型层(`model.md`)与事件层(`event-ingest.md`)只对本端口编程,不认识钉钉。** 介质特有的坑全部收在 `adapters.md`。
4
+
5
+ **解耦检验**:把某适配器的能力位调成最宽松档后,通用算法里的完整性校验、写后回读、判重读、翻页判存在四条分支应**自动全灭,Skill 正文一字不改**。
6
+
7
+ ## 零、先用端口形状消解,剩下的才用能力位描述
8
+
9
+ 三条病理不进能力位,靠端口形状让它**说不出来**:
10
+
11
+ - **没有 delete / update 操作** → `raw` 只增不删由形状强制,遗忘只能 append tombstone。
12
+ - **水位是 meta 键空间的一等对象,不是任何页里的一行** → 「水位必须放短页」这件事无从发生。
13
+ - **模型层词汇里没有页、分区、seg、月份** → 切的是 Fact,不是页;物理分区如何滚动是适配器私事。
14
+
15
+ **适配器私有地址空间**:适配器可在自己的地址空间保存分区表、哨兵、物理 ID。模型层不可读不可写;端口签名里**不得出现任何介质地址类型**(nodeId / URL / 路径),它们只出现在 `resolve` 返回的不透明句柄与绑定档案里。
16
+
17
+ 🔴 **用户可见链接的锚点必须是 `(subjectKey, section, evidenceId)` 三元**,永不含物理分区名——否则换介质即死链。
18
+
19
+ ## 一、操作集(10 个)
20
+
21
+ | 操作 | 语义 / 前置 | 返回与失败模式 |
22
+ |---|---|---|
23
+ | `resolve(agentIdentity)` | Boot 期一次解析两个 root 与能力位;同 Session 缓存 | `{roots[], capabilities}`;`NOT_CONFIGURED`(确证未建,可引导)/`UNAVAILABLE`(读失败,**禁止折成 NOT_CONFIGURED**)/`DRIFT(level, observed, expected)`(只报双边真实值,不自愈)。两 root 各自独立 |
24
+ | `probeWrite(root)` | **显式、昂贵、绑定期一次** | `writable` / `read-only` / `write-unverified`。结果写进绑定档案,运行期不重复探测 |
25
+ | `ensureEntity(root, subjectKey, displayHint)` | 唯一的建档动作。**禁止在 ingest/消息触发路径调用** | `exists` / `created` / `ambiguous`(同键多实例=残片)→ `ambiguous` 一律 HALT,不自愈、不猜正本 |
26
+ | `listEntities(root, cursor)` | 判「不存在」的唯一权威来源 | `{keys[], cursor, complete}`;`complete=false` 的结果**只能用于发现,不能用于否定** |
27
+ | `appendFacts(subject, section, facts[])` | 只增追加;facts 自带 `evidenceId` | `receipt{accepted[], skipped[]}`;`PARTIAL` / `UNVERIFIED` / `TOO_LARGE` / `READ_ONLY` |
28
+ | `readFacts(subject, section, range)` | 跨物理分区连续读 | `{facts[], cursor, complete}`;`complete=false` **等同读失败**,不是空 |
29
+ | `putView(subject, section, content, {expectVersion?})` | 整体重写;端口按 section 的 `layer` **拒绝对 `raw` 调用** | `receipt{version}`;`STALE` / `TOO_LARGE`(降级为摘要视图,溢出内容降为 Fact) |
30
+ | `readView(subject, section)` | 召回主路径 | `{content, version, complete}`;`NOT_FOUND` 是**合法初态**,必须与 `UNAVAILABLE` 严格可区分 |
31
+ | `metaGet / metaPut(scope, key, value, {expectVersion?})` | A0 键空间:水位、writeState、绑定档案、停用账本、tombstone 索引、欠账指针 | `{value, version}` / `NOT_FOUND` / `CONFLICT`。🔴 **水位键固定为 `watermark:(subjectKey, channel)`,与 section 正交** |
32
+ | `confirm(receipt)` | 通用算法**无条件调用**;`write.confirm=implicit` 时 O(1) 返回、零往返 | `confirmed` / `unconfirmed` / `failed`。未 confirmed 不得声称已记 |
33
+
34
+ 🔴 **水位键必须是 `(subject, channel)`,绝不能是 `(subject, section)`。** 一个 section 可能同时吃多个渠道的事件(`activity` 同时吃待办与文档),两渠道追平进度天然不同;用 section 当键会让追平待办后推进的水位把文档的未消化区间一起判成已消化,**那批事件永远不会被再消化**——正好踩中「水位虚高不可逆」。
35
+
36
+ ## 二、能力位(8 位 + 容量参数)
37
+
38
+ **准入规则(硬性)**:新增一位必须能**同时改变 ≥2 个适配器的行为**,否则它是适配器内部细节,不许上浮。**位数上限 8。**
39
+
40
+ | 位 | 取值 | 通用算法据此改变什么 |
41
+ |---|---|---|
42
+ | `read.completeness` | `exact` / `verified-partial` | `verified-partial` 时 `complete=false` 按读失败处理:记欠账、跳过本轮。**完整性如何自证全在适配器内部,模型层不知道「截断」这个词**。🔴 **静默截断介质(doc read 不报错)上,唯一可靠的自证是 count 对账**:写侧在 meta 记 `count:(subject,section,partition)`,读回条数 ≠ 记录条数即 `complete=false`。哨兵尾行会被静默截断吃掉,不可靠。详见 `consolidation.md` §0 |
43
+ | `limits.maxFactBytes` / `maxViewBytes` | 整数 / ∞ | 超限时**切 Fact**(同 `evidenceId` 拆多条)或把 View 降级为摘要 |
44
+ | `write.confirm` | `implicit` / `required` | `required` 时每次写后真回读 |
45
+ | `write.cas` | `none` / `revision` | `none`:append → confirm → metaPut 三步,任一失败整批作废、水位不动。`revision`:两步原子提交,`CONFLICT` 靠 `evidenceId` 幂等重放 |
46
+ | `dedupe.enforcement` | `server` / `client` | `client` 时重放前必须 `readFacts` 尾窗判重,且**要求 `complete=true` 才敢重放**;否则不写、记欠账、水位不动 |
47
+ | `identity.collision` | `reject` / `silent-rename` | `silent-rename` 时 `ensureEntity` 内部回读核对,检出残片返回 `ambiguous` |
48
+ | `lookup.negative` | `immediate` / `list-only` | `list-only` 时任何搜索的 0 结果**不构成否定证据**;否定结论只能来自 `listEntities(complete=true)` |
49
+ | `access.scope` | `container` / `leaf` | `leaf` 时只接受叶子锚点 seed;容器枚举缺席一律映射为 `UNAVAILABLE`,**禁止**推出 `NOT_CONFIGURED` |
50
+ | `access.probe` | `declarative` / `empirical` | `empirical` 时 `resolve` 不承诺写权,须显式 `probeWrite` 一次并缓存 |
51
+
52
+ ## 三、病理逐条映射
53
+
54
+ | 病理 | 归宿 | 通用算法的行为 |
55
+ |---|---|---|
56
+ | 原始层按月分页 | `read.completeness` + `limits.*` | 只调 `appendFacts/readFacts(range)`。「按月」降为适配器把一个 section 映射到多个物理对象的**内部分区策略**;本地文件上就是一个文件 |
57
+ | 水位必须放短页 | **端口形状消解** | `metaGet/Put` 独立寻址,落在哪是适配器的事 |
58
+ | 判存在只认列父目录翻完页 | `lookup.negative=list-only` | 先读模型层自持的索引 View 快判(有界成本);索引未命中且真要建时才付 `listEntities` 全量代价;`complete=false` → 记欠账、本拍跳过,**绝不新建** |
59
+ | 建后必回读核对残片 | `identity.collision=silent-rename` + `write.confirm` | 回读关进 `ensureEntity`,模型层只看三态;`ambiguous` → HALT 报人 |
60
+ | 先追加后推水位、任一失败整批作废 | `write.cas` + `write.confirm` | 这是 `cas=none` 分支的标准应对,不是某介质专属规则 |
61
+ | 写权用经验判定 | `access.probe=empirical` | 不查权限表、不解析显示名,只消费三态;`read-only` → 只召回不沉淀且禁说「我记住了」 |
62
+ | 容器级探测漏落点 | `access.scope=leaf` | `resolve` 只走 seed 一跳;容器级失败 = `UNAVAILABLE` 而非未配置 |
63
+
64
+ ## 四、适配器取值表
65
+
66
+ 🔴 **一个部署会同时用多个适配器**:narrative 类 section 走 `dingtalk-doc`,registry 类 section 走 `aitable`(见 `model.md` §三 kind、`adapters.md`)。端口的解耦价值正在这里——同一套模型层操作,registry 换介质就把一半病理关掉。
67
+
68
+ | 位 | `dingtalk-doc`(narrative·生产) | `aitable`(registry·生产,🔬实测) | `local-md`(开发/评测) |
69
+ |---|---|---|---|
70
+ | `read.completeness` | `verified-partial` | **`exact`** | `exact` |
71
+ | `maxFactBytes / maxViewBytes` | 4KB / 24KB | 单元格上限(故不放长正文) | ∞ / ∞ |
72
+ | `write.confirm` | `required` | `required` | `implicit` |
73
+ | `write.cas` | **`none`(raw 与 view 都是)** | `revision`(按 recordId 改行) | `revision`(临时文件 rename) |
74
+ | `dedupe.enforcement` | `client` | `server`(按 key 查重) | `client`(读 exact,故可靠) |
75
+ | `identity.collision` | `silent-rename` | `reject`(recordId 唯一) | `reject` |
76
+ | `lookup.negative` | `list-only` | **`immediate`**(`--filters` 查不到就是没有) | `immediate` |
77
+ | `access.scope` | `leaf` | `leaf`(baseId 直达) | `container` |
78
+ | `access.probe` | `empirical` | `declarative` | `declarative` |
79
+
80
+ > 🔬 **`aitable=exact/immediate` 是实测结论**(2026-07-23 核对 `dws aitable record query --help`):`--all`+`--cursor` 真分页无 341 行墙、`--filters` 按字段查、`record update --records` 按 recordId 精准改。**这就是"registry 类不该塞进文档"的凭据**:把 `00-群索引`/水位台账/tombstone 从 doc 挪到 aitable,`read.completeness=exact` 让 count 对账分支自动全灭、`lookup.negative=immediate` 让翻页判存在分支自动全灭。
81
+
82
+ > 🔬 **`dingtalk-doc` 的 `write.cas=none` 是实测结论**:`dws doc update` 只有 `--mode overwrite|append`,**没有任何 revision / expectVersion / CAS 参数**(2026-07-23 核对 `--help`)。所以编译层的整体重写同样没有并发保护,只能靠「写后回读 + 冲突时以最后一次为准」,并接受两个并发写者会互相覆盖。不要以为编译层比原始层安全。
83
+
84
+ `local-md` 落地形态:`<root>/people/<personKey>/` 为目录(**主键即目录名**,显示名进 front-matter),raw section = 一个只增 `.md`,view = 临时文件 rename,meta = `_meta/*.json`。
85
+
86
+ 🔴 **`local-md --chaos` 是契约的组成部分,不是建议**:强制 `complete=false`、随机静默改名、随机 `UNVERIFIED`、注入索引延迟与判重读截断。CI 必须让 chaos 档与真实档跑同一组用例——否则这套抽象只是把 bug 挪到了适配器边界之外,第一次执行仍然发生在生产。
87
+
88
+ ## 五、不能下放给适配器的模型层不变式
89
+
90
+ 1. `raw` 只增不删、纠错用 `supersedes`;`derived` 可重建。**View 里不得存在唯一信息。**
91
+ 2. `evidenceId` 全局唯一即去重键,必须是**源系统主键**(不得用内容哈希或时间戳)。端口只做机械去重,选错它无从发现。
92
+ 3. `audience` 写入时定、读取时只收窄。**收窄必须 fail-closed**:读该实体事实时 `complete=false`,则本轮不引用它的任何事实,宁可答「不清楚」。
93
+ 4. 交叉归属三铁律;唯一上浮方向 = 会话公开 → 人的 `public-facts`。
94
+ 5. 水位键 `(subject, channel)` 单调不回退;**先写事实、后推水位**的因果顺序**即使有 CAS 也不得反转**(CAS 只允许合并两步,不允许先推)。
95
+ 6. 「读失败 ≠ 不存在」的翻译权在模型层。**任何适配器不得把 `UNAVAILABLE` / `complete=false` 补成空集。**
96
+ 7. section 的 `layer` 归属由模型层定义并交给端口;会话隐私档位白名单默认拒绝。
97
+ 8. 知情同意:未启用态零写调用、未 `confirmed` 不说「我记住了」、首次落盘回读念一遍并给链接。
98
+ 9. **有界降级**:同一 root 连续 N 次 `unconfirmed` ⇒ 作废 `probeWrite` 缓存、降级 `read-only`、显式报人。否则表现为「一直在记但什么都没记」。
99
+
100
+ ## 六、这个抽象做不到什么(诚实清单)
101
+
102
+ - **并发建重复无法根除。** 无 CAS 介质上,端口只能把撞车压到绑定期一次并在事后 `ambiguous` → HALT。「建档只在绑定期由单写者触发」是**流程约束,不是端口能力**——这是抽象的公开缺口。
103
+ - **`client` 去重 + `verified-partial` 读存在真实双写窗口。** 判重读本身可能不全;缓解是「`complete=false` 即不重放」,代价是欠账增长而非双写。这两位的组合应在 `resolve` 阶段被显式标记为高风险档。
104
+ - **能力位大多是为 `dingtalk-doc` 的病理而设。** `aitable` 与 `local-md` 上这些位几乎全取宽松档、对应的通用算法分支全灭。这本是"抽象没做干净"的嫌疑——但反过来看,registry 走 aitable 后一半病理消失,恰恰证明**问题不在模型、在"把结构化数据塞进叙事介质"**。准入规则(≥2 适配器行为差异、上限 8 位)仍须执行,否则第 N 个介质接入时会退化成「每个适配器一个 if」。
105
+ - **判存在的读放大只被压小、没被消除。** 索引 View 可能陈旧,真要新建时仍需付全量翻页代价,且它落在「新人首次建档」这条常见路径上。
106
+ - **授权门不在本端口。** 它是消息**源侧**的门,归 ingest 源端口的 `AUTH_REQUIRED` 一态;放进存储端口即是端口变宽的开始。
107
+ - **端口窄度需要持续维护。** 任何一次「我只要再加一个 listFiles / getUrl」,都会把它推回通用文件系统。