Files
knowledge-governance/docs/library-data-contract.md
T
姚章浩 90ab1c3a5e
build / build (push) Successful in 1m24s
docs: 记录已接受的游标与 clean 边界
写入用户明确接受的两项边界:发布前旧分页游标在发布后失效(刷新重新分页,不改
WeKnora、不声称后端可识别无代次游标);ES 已写成功但回执丢失且无法证明归属时
保持维护锁定并走人工核验/恢复,不承诺自动收敛,禁止凭相同正文或 seq_no 推断归属、
补写 receipt、跳过校验、强制开门或删除未知内容。可证明归属的自动补偿不回退。
2026-11-01 17:21:42 +08:00

90 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# v0.7.1 整库发布的数据契约与覆盖边界
这是候选代码的契约,不是部署许可。真实隔离执行证据在交付目录 `data-contract-r10.md`、`test-summary-r10.json` 及其 raw 索引。PG/ES/MinIO 均为本机隔离真实服务;发布/恢复测试直接调用适配器;B22 另走实际 `librarystack.OpenProductionStack` 和 HTTP 网关。网关的独占审核是测试夹具,不代表生产已隔离;B22 控制读取角色是真实 SELECT-only PG 角色。
## 1. 源 schema、历史身份与准入
- 新 `ExportLibraryWithReleases` 在读取内容的同一 REPEATABLE READ / READ ONLY 事务中读取 columns、constraints、indexes、triggers、relations、sequences;非普通表、RLS/继承/分区、自定义触发器或未知入边 FK 拒绝。
- 新归档增加一个内容寻址的 `schema/migration-catalog` 部件。归档核 JSON 目录结构、计数、大小和 SHA256;来源清单摘要绑定该部件。PlanWriteSet 在分配序列或执行任何业务写前要求其摘要与当前目标目录一致。序列定义属于目录,last_value/is_called 不属于 DDL 指纹。
- 旧快照没有该部件:不追加字段、不重新 seal、不改历史 hash;继续走原目标 catalog 和逐行字段/身份检查。它不能被称为“原捕获时已有 schema 证明”。旧V1台账的 canonical 规则不变。
- 目录严格一致不是自动迁移方案。源/目标定义不同会拒绝,需要单独评审兼容性,不能在业务库上临时 DDL 绕过。
- 实际 stack 准入读取目标模型、维度、schema 和 graph 状态。同维不同模型、模型行缺失、空模型名、graph 开启、schema dirty 均拒绝;不重算 embedding、不关闭 graph。正例和负例都比较 PG、ES正文/CAS、对象精确版本与网关 journal,证明准入无业务副作用。
## 2. 逐表身份、闭包和保真规则
|表|身份及本轮已执行的边界|保真/未闭合范围|
|---|---|---|
|knowledges|稳定字符串 ID;租户与KB按计划映射;正文/file_path/metadata 中规范资源引用进入闭包|其余原始列保留;模型/存储定位只按已冻结目标策略改写。不声称已穷尽全部业务 metadata 中非资源ID的语义|
|chunks|稳定 ID;knowledge_id 必须存在且同租户同KB;源 scoped 孤儿不能被 JOIN 静默丢掉;seq_id 使用固定数值映射|chunk_index 排序、按tenant+seq_id查询实跑;规范pre_chunk_id/next_chunk_id/parent_chunk_id要求同knowledge;tag_id及relation_chunks/indirect_relation_chunks要求在范围内,父树拒绝环;不把任意metadata数字猜成seq引用|
|knowledge_tags|稳定 ID;租户/KB必须匹配;seq_id按源数值大小分配并固定|sort_order ASC、created_at DESC、seq_id DESC 的并列排序实跑;原数值在恢复时回原,不回拨共享序列|
|knowledge_tag_relations|精确(knowledge_id,tag_id)元组;任一父可证明范围;缺knowledge、缺tag、跨KB/租户在源、入站及目标规划处拒绝|不通过内连接删除异常关系、不把元组拼成单列。非空正常/既存元组由完整隔离套件执行|
|resources|稳定 ID/22字符handle;引用资源必须存在,原对象字节、精确版本保真;目标正文引用也进入before-image和验证范围|共享资源仍按原不可变before-image/来源证明判断复用,不放宽成“字节相同即认领”。无本次归属的历史孤儿保持不动|
|resource_bindings|稳定 ID;resource_id必须在来源闭包;owner仅knowledge/chunk,必须可解析且租户一致|未知owner_type、NULL owner_type、缺owner及跨租户拒绝。被本次引用资源上的所有绑定都核验,包括指向其它KB的共享绑定;合法共享允许、未知共享归属拒绝|
|wiki_folders|稳定 ID、tenant/KB保真;整行JSON的规范资源字符串参与识别|parent_id必须在本次目录闭包且无环;目录ID碰撞写前拒绝;未知外部持久路径仍不声明支持|
|wiki_pages|稳定 ID;正文和嵌套metadata资源在无binding时也捕获/备份/发布/恢复|页面内容原样保留;按真实字段source_refs(含knowledgeID|title旧形状)、chunk_refs、folder_id检查闭包;不按不存在的source_knowledge_ids字段推断支持|
|wiki_page_issues|稳定 ID、tenant/KB保真;整行资源字符串参与识别|原v071按slug引用/展示的问题记录原样保留;未知扩展字段不自动映射数值ID,不能当作任意插件引用支持|
|wiki_log_entries|BIGINT正整数,不经float64;按源数值大小映射(2在100之前),映射绑定原计划|id DESC和cursor id<实际SQL实跑;恢复原id。旧外部持久游标不能跨映射直接复用,原A04此项仍需决策/验收|
|resource_access_grants(非迁移表)|唯一已审核的resources外部FK入边;不搬运、不删除授权|新建资源清理/Restore前若出现授权,持父键锁拒绝删除;恢复入口另核未知绑定/计划外绑定和外库正文metadata引用,有引用时在ES/对象/PG写前held,锁内再次核全部正文/绑定;当前完整隔离测试读回grant、PG、ES、原对象版本均未变。其它未知入边/触发器拒绝|
既有目标历史软删除行按原 destructive predicate 进入 before-image,不用来源活跃行过滤规则裁剪恢复数据。来源导出只包含活跃内容;可归属于本库的异常边在过滤前检查。完全没有任何父或范围锚的历史记录不能猜成属于本库:不全局扫出后删除、不自动迁移;这不是“所有历史孤儿已修复”。
## 3. 资源引用的实际格式
只读参考源码:`WeKnora-v0.7.1-dev/internal/types/resource.go` 中 `ResourceHandleLength=22` 与 `ParseResourcePath`;本轮没有修改WeKnora源码。
支持并真实执行:
- knowledge或Wiki正文里的 `resource://` 加22个URL-safe字符,包含Markdown图片语法;
- 嵌套JSON对象/数组字符串值里的同一规范引用,例如metadata.image_info[].url;
- 无binding的上述四种组合:源捕获、目标before-image、发布、恢复;完整PG及精确对象版本含历史孤儿比对。
实现先解码JSON再递归扫描字符串,不将JSON数字转为float64,也不重写正文。所有迁移行的字符串参与资源发现。目标 SQL 选择覆盖所有带tenant/KB的迁移表整行JSON,并与严格解析/完整引用存在性检查配合,不再只认chunk.content。
明确拒绝本轮实测的未知表示:短handle、过长handle、URI query/path/fragment后缀、再次转义的resource协议串;不截取22字符前缀冒充合法引用。未知owner独立拒绝。
覆盖边界:这不是任意富文本、任意编码或所有插件格式解析器。不承诺识别不含规范协议标识的opaque/base64内容、插件自定义裸UUID引用或全部非资源语义ID。新增格式需给出真实生产/读消费者契约、负例与保真证据后再支持;当前总体验收保持INCOMPLETE。
## 4. 数值序列与消费者
- chunks.seq_id、knowledge_tags.seq_id、wiki_log_entries.id:正常分配、落后于已占值、耗尽、源显式NULL分别真实执行。后3类在业务阶段前拒绝;所有序列的last_value/is_called、PG全文、ES正文/CAS、S3版本/删除标记不变。
- 正常分配是明确副作用,会推进序列;必须先固化P0/预备份,不能称只读。新target接管同一WriteSet再Apply不再取号;恢复原行但不rewind序列,合法空洞不等于身份丢失。
- v071消费者位置:repository/tag.go(sort_order/created_at/seq_id排序)、repository/chunk.go(seq_id读取及chunk_index排序)、repository/wiki_log_entry.go(id DESC、cursor id<)。本轮执行其SQL语义,不声称启动了完整WeKnora UI或所有外部消费者。
## 5. 后续门与安全边界
- 真实v071历史catalog镜像对照及UUID/自然键矩阵在r11执行,隔离结论与外部当前目录采样分开。A04已识别嵌套引用有有限支持,旧外部cursor仍不自动转换,不因内部结构正例而升级整项。
- r8/r9进程恢复历史证据只在原发送协议、不可变计划/receipt及相应场景前提不变时复用。本轮对schema/引用准入有收紧,不能把历史执行日志称为当前源码正式进程全链重跑;完整当前隔离适配器回归单列。
- clean-finalization unknown保持held是安全行为,但未满足全部自动收敛要求。若要求自动完成,需要能绑定原写入、完整内容和代次的充分服务端证据;相同干净正文、人工猜测归属或放宽共享规则均不可替代。
- 最终提交/push、远端CI、父复核和本机部署为明确下一门;整体安全验收未齐不部署。生产独占/ACL网络凭据、网关OSS业务端到端与宿主重启/断电是另行授权的部署/破坏性验收,不能作为停止本机授权开发的借口。
## 6. 目录与唯一键的有限支持矩阵
`testdata/catalog-v071-r11.json` 来自既存只读取证 `source-schema-live-r3b.json`,不是当前线上目录新采样。隔离镜像按原列、约束、索引、关系和触发器定义重建并逐项比较;sequence定义和OWNED BY归属独立绑定,不将last_value伪装成只读DDL。未知外部入边、外部出边、迁移表内部数值FK扩展不能因为源/目标同样扩展就默认放行。
稳定主键:knowledges、chunks、knowledge_tags、resources、resource_bindings、wiki_folders、wiki_pages、wiki_page_issues按同租户异KB/异租户碰撞检查;knowledge_tag_relations使用父身份元组;三个本地数值身份按固定WriteSet映射而非稳定UUID处理。资源共享的合法同ID复用仍依赖原before-image和所有权证明。
自然键以实际catalog的unique/partial indexes为基准:资源handle、有效tenant/location_hash、资源绑定复合键、tag tenant/KB/name、目录KB/parent/name、wiki KB/slug,以及provider/physical_path的应用层排他。wiki_page_issues在该目录中只有主键,没有额外自然唯一键。partial predicate按有效/软删除判定,并验证不同parent的正常目录;不能无差别拒绝全部正常资料。具体逐键DDL、正负例对应表见交付 `data-contract-r11.md`。
Wiki日志pages_affected仅支持消费者已有的字符串slug数组和{slug,title}对象数组;已删除页面/knowledge的历史日志是展示事实,不以缺现存父强删/拒绝历史日志。数字数组、对象中新增log_id等未知结构拒绝。普通metadata数字原样保存,不推断为日志ID。
旧外部cursor是明确剩余风险:目标新游标可正确分页,但源数字cursor或发布前旧cursor不能直接带入目标新映射。原A04“改本地ID不破坏引用/语义”没有豁免条款,所以仍未整项通过;本模块不擅改上游WeKnora,也不把“刷新页面即可”当作已经实现的自动失效协议。
授权以执行时真实时钟为准;完成预备份及持久计划登记后均再验。目标实现 `GrantRevalidator` 时按治理库授权行回读:`ActiveLibraryAuthorization` 只返回未撤销、未过期且参数摘要一致的记录,撤销后连写集合都不计算(避免序列分配),预备份保留待重新授权。真实发布 wrapper 无条件接线;未接线不等于安全,只是本机历史证据的边界。
## 7. r12 追加的真实边界
- **软删除 before-image**:destructive predicate 能碰到的 7 张带 `deleted_at` 的表(knowledges/chunks/knowledge_tags/resources/wiki_folders/wiki_page_issues/wiki_pages)逐表核对备份行数与删除谓词命中数;晚阶段失败后的恢复把这些行连同 `deleted_at` 原值一起放回(r12 真实 PG)。
- **索引独立 oracle**:发布成功后,正文、向量(同维不同值)、metadata(knowledge_id)任一被改都会让校验变红;改回后必须重新通过,说明合法本地列映射没有被这项收紧误杀。
- **外部索引文档**:同 `_id` 属于其它知识库时整轮在写前拒绝,且那份文档的 `_source`、`_seq_no`、`_primary_term` 与对象版本逐字节不变。
- **共享资源差异**:同 ID 共享资源在 handle / 定位 / 字节三类差异下都写前拒绝,共享对象版本不变;完全相同的共享资源按 no-op 复用,行 `xmin`/整行 JSON 与对象精确版本均不变。
- **晚阶段失败与对象**:Wiki 阶段失败回滚后,本次新建对象不残留,**已有对象的精确版本**(versionId + SHA256 + 字节数)不变。
- **clean 归属能力(实测)**:两个不同操作写入完全相同的字节后,索引侧只剩同一份 `_source`;`_seq_no`/`_version` 是分片提交计数,不绑定原 operation。因此"相同正文"或"seq_no+1"都不能证明是同一次 clean,缺服务端归属证据时继续 held(探针:`b19-capability-probe.json`)。
- **旧外部游标**:游标是裸数字 id(`next_cursor` → `id < cursor`),发布按源数值升序重排本地 id。目标自己发出的游标在同代内正确;发布前或来自其它实例的游标没有代次信息,无法自动翻译,会静默错位而非报错。详见交付 `a04-cursor-semantics-r12.md` 的 A/B 决策。
## 8. 已接受的可用性边界(r13,用户明确接受)
- **旧分页游标跨发布失效**:`next_cursor` 是裸数字 id(`id < cursor`),发布按源数值升序重排本地 id,请求里没有代次信息。发布前/跨实例游标无法翻译,回传会静默错位而非报错。已接受做法:发布或恢复后刷新页面、重新从第一页分页;不改 WeKnora 源码/Schema。同代内游标与引用重映射仍然正确。
- **clean-finalization 缺归属证据时保持 held**:真实 ES 探针显示两个不同操作写入相同字节后只剩同一份 `_source`,`_seq_no`/`_version` 是分片提交计数、不绑定 operation。此分支保持维护锁定并走人工核验/恢复,不承诺自动收敛;**不允许**凭相同正文或 seq_no+1 推断归属、补写 receipt、跳过校验、强制开门或删除未知内容。带原 marker/before-image/CAS/不可变 receipt 的 unknown 仍走既有自动补偿,不因本边界回退。
配套只读诊断:`GET /api/v1/library/jobs/{id}` 在作业失败且目标仍被本作业持有时返回 `maintenance`(held/owner/state/ingress/target_identity/note)与 `next_step` 文本,明确"按预备份与校验证据人工核验后再放行",且不提供任何强制成功入口;作业本身仍带 operation_id/attempt_epoch/backup/target 快照与错误、恢复说明。