现象:共享桶部署下网关启动即报 gateway_auditor_key_required,残留过期 审计文件则每次 Execute 报 gateway_exclusive_writer_evidence_unverified; 用户已改为“停目标写入容器→更新→启动”的维护式更新。 根因:Gateway.Ready 强制 Isolation.Verify,main 强制 Ed25519 公钥,等于 把整桶独占证明当作发布前置;这与共享桶、沿用原 AK 的实际部署矛盾。 取舍:删除审计校验与相关 env 依赖,保留 journal、治理 PG 只读 authority、 plan/owner/attempt/epoch 绑定、intent-before-send、单次写与精确版本回滚。 create 仅在精确版本读回且仍为 current 时提交;发现外部 latest 只返回冲突, 不在 confirm 阶段删除(journal create intent 已提交,删除会变成无 delete intent/receipt 的未记账副作用并让 intent 永久 held)。同 key 不自动合并。 证据:真实二进制无审计/残留审计两环境通过;kg-pgtest-writeset 一次性库 + 随机 MinIO 桶跑 14 个网关用例(含共享桶共存写者、同 key 既有对象拒绝、 精确回滚)全过;umask 022 下 make verify 本地必需项全 success。
14 KiB
v0.7.1 整库发布的数据契约与覆盖边界
这是候选代码的契约,不是部署许可。真实隔离执行证据在交付目录 data-contract-r10.md、test-summary-r10.json 及其 raw 索引。PG/ES/MinIO 均为本机隔离真实服务;发布/恢复测试直接调用适配器;B22 另走实际 librarystack.OpenProductionStack 和 HTTP 网关。网关按限定 key 工作,不要求整桶独占审核;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 |
| 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、父复核和本机部署为明确下一门;整体安全验收未齐不部署。网关真实 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 快照与错误、恢复说明。