原来 worker 先 health 再 capture,两次拨号之间 socket 可能被换成另一个实例的 执行器:错的 agent 已经把写入者停了、快照也出了,事后 postcheck 只能拒绝登记, 副作用无法撤销。 现在 capture 请求自带期望身份(治理侧配置生成:project 必填,source_app / archive_root 配了就绑定)与覆盖三项的稳定指纹;接收方在操作互斥内、执行任何源 操作之前比对,缺字段、指纹与字段不一致、project/source_app 不符都直接拒绝, **错的执行器一次 capture 都不跑**。封存结果回显该指纹,worker 事后复核按它对照, postcheck 只作为证据防线。恢复路径不要求期望(它只把写入者恢复回来)。 负例是确定性的:两个真实 agent 各占一个 socket,把调用方指向错的那个,断言它的 capture 调用为 0 且仍能回答 health;同一份期望打到对的 socket 则只跑一次。
22 KiB
整库同步验收矩阵(真实证据对照,2026-10-29 重排)
本文件是唯一口径:状态只写实际发生的事,四类严格分开,不用"部分通过"含糊过去。
2026-10-29 用户决定(覆盖历史验收口径):发布管理不承担 Wiki 语义撤销—— 它只负责把测试端已确认的整库快照原样还原生产。因此撤销后正文里是否残留 撤销方独有事实,属于上游知识质量问题,不在本模块验收范围内:V03-a 保留原始 失败证据(见「一·补」下方的"模块范围外"表),不改成通过,也不再阻断发布。 同时不新增 LLM 重写、人工逐页复核闸门,也不把升级 WeKnora 作为前置条件。 保留的门槛是:整库文档/分块/索引/资源/Wiki 正文·来源·链接·目录一致、稳定切点、 实际维护与排空、准确快照授权、独立预备份与恢复、失败回滚、作业互斥与重启接管、 真实 UI 进度与存储安全。效果问答/性能/A06 不是门槛。
| 口径 | 含义 |
|---|---|
| 已完成·已验证 | 有代码 + 真实服务(真实 PG/ES/对象存储/原版 WeKnora API)跑出来的证据,或既有 CI 检查覆盖 |
| 缺代码 | 功能/契约本身没有实现;界面按"未接入"显示,不冒充 |
| 已实现·未跑 | 代码与测试在位,缺真实环境执行证据(不是权限问题) |
| 需授权/需环境 | 实现所需的动作属于生产写入/读取/恢复,或需要本会话拿不到的资源 |
A06(内容、问答、引用图片效果、性能报告)已由用户取消,不作为门槛,也不在本表。
一、已完成·已验证
| ID | 要点 | 承载位置 | 证据(本轮或前轮,均可回看) |
|---|---|---|---|
| V01 | 只有对内/对外两个目标,身份来自配置映射 | internal/httpapi/library_*.go;web/src/ReleaseTargets.tsx |
overview 对非法 scope 返回 400;真实归档 internal/external 各自解析到唯一 KB |
| V02 | 解析/索引/Wiki 就绪,验收绑定本版 | v071/library_source_settlement.go |
结清检查在封存前执行、缺项阻断;页面注明依据哪次快照 |
| V04 | 完整固定版本:文档+处理结果+资源+索引+完整 Wiki | internal/librarysnapshot/* |
真实快照 f992d64e…:4 文档/38 分块/122 索引/21 资源/31 Wiki 页/55,309,044 字节,部件摘要逐项校验 |
| V06 | 只同步知识数据,不含账号/会话/凭据 | 导出白名单(10 知识表 + 知识域索引 + 资源 + Wiki) | 部件清单无 users/sessions/api_keys |
| B01 | 每次生产写入前完整预备份,失败零写入 | v071.ProductionBackupStore.Prepare + prebackup 阶段 |
真实 PG/ES 上预备份 3111e823…;故障注入后用它恢复并校验 |
| B03 | 每版保存完整 Wiki 与引用关联并校验 | library_snapshot wiki 部件 + 摘要 |
真实快照 31 页逐页摘要;manifest 记录数量/字节 |
| S01 | 数据库/资源/索引/Wiki 真实落地并校验 | v071.ProductionLibraryTarget.Apply/Verify |
本轮 sync libsync-internal-…1793221776816380302:database 67 行、resources 21、index 122、wiki 63 行全部 succeeded + verify succeeded;目标库 wiki 31 页 |
| S02 | 生产不再解析/Embedding/生成 Wiki | index 阶段直接搬运归档 embedding | 真实 ES 写入归档 embedding,未调用模型 |
| S04 | 各阶段失败/超时/重启可追溯可恢复 | library_jobs/library_job_stages + ReconcileLibraryJobs |
阶段与错误持久化;worker 清理"队列无对应作业"的孤儿 |
| S05 | 幂等与互斥(多 worker 安全) | 幂等表 + active 唯一索引 | 真实 PG:同目标第二作业被拒;同键同 digest 复用;换 digest 冲突 |
| R01 | 按指定版本手工恢复 | POST /library/restores + library.restore |
真实跑通:撤旧授权→503;签发 restore 授权→202→succeeded,内容回到目标版本(evidence/B_restore.json) |
| R02 | 恢复失败显式异常、不回退测试库 | Outcome.State=rollback_failed + 独立阶段 |
隔离测试 + 前轮真半写场景(partial.json) |
| P01 | 作业阶段与整体状态 | library_jobs.state/stage;页面阶段条 |
阶段勾选只认当前作业证据;无作业显示"尚未开始" |
| P02 | 四类分项计数/字节/错误 | stageSizesFrom(manifest) + library_job_stages |
真实数据:database 67/88086B、resources 21/53490166B、index 122/1624880B、wiki 63/105912B |
| P03 | 同步明细与异常 | 页面"同步明细"读阶段行 | 行来自持久阶段,刷新/重启可复现 |
| U01 | 发布管理为主要入口(两库+单作业区) | web/src/ReleaseTargets.tsx |
按确认稿布局,父会话已在登录浏览器核验 |
| U02 | 权限边界 | canWrite/canCapture,写操作需 engineer+ |
无权限不给封存入口;未接线显示通道状态 |
| U03 | 操作确认 | 封存按钮 + 作业号 + notice | 同一作业号重放返回已存作业并提示 |
| D01 | 保留策略(数量/天数) | internal/libraryretention |
未设置=0 按"全部保留",不编默认值 |
| D02 | 占用测量 | internal/librarysnapshot/usage.go |
版本数/去重内容/blob/未引用/清单;两份真实清单 55,309,693 字节 |
| D03 | 依赖保护 | libraryretention.Build 的 Protected/ProtectedBy |
真实归档两版均因"最近测试封存"受保护 |
| D04 | 清理预览 | Build 的 Candidates/ReclaimPreviewBytes |
keep=1 时仍 0 候选;回收预览只计独占字节 |
| D05 | 容量门禁 | CheckCapacity + sync gate |
预算未设置→阻断;超预算→阻断并给预计字节 |
| D06 | 测试残留口径 | 未引用 blob 统计 | 真实归档未引用 blob 为 0(口径,不等于扫过全部残留) |
| S03 | 实例级维护窗口:关入口→排空→静默证明→停写者→写入→校验→开入口 | internal/libraryfreeze + librarymaintenance + 迁移 070–072 |
前轮 7 场景真跑全绿(normal/kill/applyfail/partial/crossscope/recover_refuse/recover_ok),本轮 sync/drain 场景继续复用;quiesce 证据、post_stop_*、journal、gate drain.json 均可回看 |
| V03-b | 治理侧能把上述语义变化说清楚(不是页面级"无变化") | internal/librarydiff/wiki.go CompareWiki;GET /library/overview 的 diff.wiki |
逐页比对正文/来源/引用/出入边/目录,输出 sources_removed、citations_before→after、changed;单测 wiki_test.go、视图单测 library_wiki_diff_test.go;页面逐行展示(wikiContributionLines + 测试) |
| S06 | 页面/API 发起封存 → 主机 agent → 实例级窗口封存(真实冻结) | POST /library/captures + cmd/libraryagent(0600 socket、固定命令)+ libraryctl capture(实例级:闸门/后台静默/指纹) |
本轮终态(0.1.186 e508abd)真跑:POST /api/v1/library/captures(job lib-internal-20261029T0716)202 → worker → agent → CLI → 关闸门排空 → 等后台静默 → 停 kgmaint-app(exit=1、drain_verified=true、stop_verified=true、restored=true)→ 导出 → 恢复写入者与入口;作业 libseal-lib-internal-20261029T0716 succeeded,快照 0f0a3b68…。证据 kg-final-gaps/evidence/04-inflight-v05-final.json、journal freeze-kgmaint/lib-internal-20261029T0716.json |
| S06-a | 排空证明取自真实在途请求(不靠 202 后抢时序) | 闸门 drain.json + 客户端真实上传握手 |
本轮终态真跑:先发一半 multipart 正文 → 闸门 conn:open active=1 → 再发起封存 → 入口关闭 → 补完正文,应用 200 并完成解析/嵌入 → drain.json phase=drained active_at_close=1 served=143 drain_ms=352;journal quiesce.samples=18 stable_ms=1617 且停写者后指纹不变(post_stop_digest == digest)。窗口内派生任务真的跑完(探针文档 f7fed988… completed / 7 分块),且该文档出现在被封存的快照里 |
| V03-c | 带"撤销后共享页"的整库封存 → 独立目标同步 → 恢复,逐项真实读回 | libraryctl 封存 + ProductionLibraryTarget + libraryrelease |
本轮终态真跑(字节一致性,不是撤销语义):KB e64e82b5(9 Wiki 页、1 文档、2 分块、1 资源)→ 快照 0f0a3b68… 与源逐页一致(05-snapshot-compare.json ok=True)→ 同步到独立实例 kgseal libsync-internal-1793229249632461601 succeeded → 目标逐页/索引/资源字节一致(07-target-compare.json ok=True,含目标桶对象 sha256)→ librestore-internal-1793229274077866298 succeeded → 目标仍逐项一致。注意:该共享页正文仍带撤销方残留(见 V03-a 不通过),本行只证明"搬运/恢复逐字节一致" |
| S06-b | 主机侧封存/恢复必须与 worker 讲同一份维护声明 | internal/librarymaintenance/env.go(LIBRARY_TARGET_* 唯一解析处) |
缺项时 capture 在开窗口前拒绝并点名;agent 启动打印维护目标;单测 5 项(逐项缺失/空白/口令空格/队列库号) |
| Neo4j 启用场景 | 启用图谱的库必须被真实拒绝 | v071/library_export.go 读 indexing_strategy.graph_enabled |
本轮真跑:在 kgmaint 建 graph_enabled=true 的真实 KB 8de41ca7…,用真实 CLI 预检 → 退出码 1、LIBRARYCTL_FAILED: Neo4j graph snapshot is not supported(evidence/10-neo4j-refuse.txt)。不是 skip |
| 执行器可用性(运行时观测) | 页面的"可发起封存"来自 worker 的真实探测,不是配置字符串 | internal/libraryexecutor + agent health + 迁移 074 library_executor_status + worker 30s 心跳 |
本轮真跑(候选栈,代码 SHA 4c79052 / 0.1.192 的工作树构建):GET /library/overview 返回 capture_available=true 且 executor={instance:kgmaint-weknora, project:kgmaint-weknora, source_app:kgmaint-app, observed_at:…};停候选 agent 后同一接口转 false、原因 主机侧封存执行器不可达:library agent is unreachable,此时 POST /library/captures 返回 503 library_capture_unavailable;重启 agent 后自动恢复 true。证据 $TMPDIR/kg-candidate-verify/evidence/{01,03,04,05}-*.json |
| 按版本恢复入口 | 页面不再把"恢复至该版本"写死 disabled;条件来自双闸/授权/维护 | GET /api/v1/library/restore-availability + web/src/ReleaseTargets.tsx |
接口真实返回 200(候选栈,缺授权时逐条列出原因);页面按 allowed 启用、管理员才见"签发恢复授权(30 分钟)",授权与执行是两步。证据 02-restore-availability.json + 前端用例 libraryRestore.test.ts / ReleaseTargets.test.tsx |
| U04 | 维护级别/进度/恢复错误真实呈现 | MaintenanceCard(web/src/ReleaseTargets.tsx)、/api/v1/library/maintenance |
显示实例级/库级、持有 scope+作业、阶段、入口开/关、服务、开始/释放、进度(阶段名中文化)、恢复错误与处理记录;仅"窗口仍持有且作业失败"时给"按预备份恢复";无记录时"未接入 + fail closed"。组件测试 6 项 |
| V05 | 导出中源变更必须被入口/写入者阻断,切点一致 | 闸门 drain.json + 真实并发客户端 | 本轮终态真跑:入口关闭的同一刻(固定协调点,非 sleep)对专用源发 8 写 + 4 读 → 8 写全部失败(7 URLError/1 RemoteDisconnected)、4 读 RemoteDisconnected;写入者被停下的那一刻(轮询容器状态捕获 running→exited→running)再发 4 写 + 2 读 → 全部 URLError。事后源库 v05 文档 0 / 分块 0,快照与源逐项一致(ok=True),在途文档 f7fed988… completed/7 分块且出现在快照里。证据 04-inflight-v05-final.json |
| B02 | 源测试桶删对象后,归档自包含且目标应用能读回同样字节 | 归档 resource 部件 + 目标应用下载接口 | 本轮终态真跑:源对象 10001/d4fbe69d…/ab2b628f….md(507B,sha256 25a55cd8…)先备份再 mc rm(stat 已 404)→ 同步仍 succeeded(字节只可能来自归档)→ 目标桶 kgseal-1029a 同键 507B、sha256 一致 → 目标应用自己的 GET /api/v1/knowledge/{id}/download 恢复前后都 200 / 507B / sha256 25a55cd8…。修复前该接口真实 500(code 1007 invalid MinIO file path),见 0.1.184/0.1.185 两条修复。证据 11-b02-delete-and-sync.json、12-b02-restore-download.json、07-target-compare.json |
| 死信精确解除(治理侧) | 保留上游死信行,只按精确指纹解除 | 迁移 073 + internal/librarydeadletter + POST/GET /library/dead-letter-releases + libraryctl dead-letters + -released-dead-letters | 已实现并有真实验证的部分:libraryctl dead-letters 在真实源上读出 0 条死信与观测摘要;治理 API GET 返回 instance=kgmaint-weknora、releases=[];POST 无写意图头 403;带真实观测但引用不存在的死信 → 409「观测里没有这条死信」。单测覆盖:摘要被改写、源侧未结清、重放早于失败、重放后又出现同键新死信、实例不一致、通配指纹。未演练:真实死信→重放→解除→放行(本轮没有死信,也没有源侧管理员凭据) |
一·补 失败 / 不通过(不得标通过)
| ID | 要点 | 承载位置 | 本轮真实结果 |
|---|---|---|---|
| (无) | 本模块没有"不通过"项:撤销语义已明确移出范围(见上表下方"模块范围外"),不在此改写为通过 | — | — |
模块范围外(用户 2026-10-29 决定;证据保留,不改判)
| ID | 要点 | 本轮真实结果(原样保留) |
|---|---|---|
| V03-a | 撤销一份共享贡献后,清除撤销方独有正文/引用/实体边 | 上游不通过,模块范围外:真实 DELETE /api/v1/knowledge/{id} 后,共享页 entity/fa-bu-zha-men 的来源/引用/边/目录都按上游 retract 更新(source_refs 只剩保留文档、引用 2→1、9 页/18 边无死链),但正文仍保留撤销方独有的两句事实 ZEBRA-A-7731/ZEBRA-A-9910(version=3、3733 字节、sha256 5fd27348…)。这是上游 retract 走 LLM 重写的非确定性结果,治理侧只按字节搬运,不能也不应改写它。证据 kg-final-gaps/evidence/14-wiki-residue-evidence.json + 01-wiki-source-after-r3.json,不涂改 |
二、缺代码
| ID | 缺口 | 现状与影响 |
|---|---|---|
| R03 | 存快/恢复快对比 | A06 性能报告已由用户取消,不作为门槛。已记实测耗时(小测试库,不是压测):封存 15.8s(含停写~5s)、同步 13.6s(预备份 13.4s 占主要)、恢复 13.8s;证据 13-r03-durations.json。没有做高负载压测,也不写没有测量的数字 |
| 死信解除·真实重放演练 | 没有可解除的真实死信,也没有源侧系统管理员凭据 | 契约与执行侧已实现(见下节新增行):上游保留原行、治理侧审计+指纹绑定、未结清/变化/新死信仍阻断。本轮没有真实死信(dead_letters=0),无法演练"真实重放成功→解除→放行"整条链路;上游只有 系统管理 runtime run_now(需要源侧管理员权限)与整队列 purge(销毁证据,不采用)。治理平台故意不持有源侧管理员凭据(放大授权),因此这一步阻塞在真实死信 + 重放执行方,不伪造成功 |
三、已实现·未跑
| ID | 要点 | 为什么未跑 |
|---|---|---|
| S03-b | 死信/孤儿任务阻断的负向演练(本轮只验证了真实死信阻断) | 真实阻断已发生并记录;系统化的负向矩阵未跑 |
| 排空时序(历史结论) | 旧做法"202 之后抢时序上传"在 1faf513 上重跑 6 次全部 Broken pipe(drain-task-final-1faf513*.json 保留,不记通过) |
已被 S06-a 的确定性做法取代:先证明请求已被闸门接收,再启动维护。旧证据保留,不覆盖 |
| 封存执行期身份复核(真实封存) | 期望身份随 capture 请求送进 agent,由它在操作互斥内、任何源操作之前比对(swap_test.go 用两个真实 agent 的 socket 替换做负例,断言错误 agent 的 capture 调用 0 次);真实封存尚未在最终构建上跑,本轮没有在候选栈发起真实封存(会产生新快照与停写入者窗口),因此"换 socket 被挡住"只有单测证据 |
|
| 按版本恢复的真实执行(新构建) | restore-availability 真实返回、恢复投递路径有单测;真实恢复作业仍在旧构建上验过(R01),本轮未重跑,不把旧证据算作新构建证据 |
|
| U05 | 正式 8088 页面验收 | 正式栈仍是 0.1.114,本轮未替换;维护面板/逐页 Wiki 差异只在测试与候选构建里验证过 |
四、需授权 / 需环境
| ID | 事项 | 缺什么 |
|---|---|---|
| D07 | 实际回收(真删对象/版本) | 自动清理默认关闭;删除属于真实清理,需单独授权 |
| 远程生产 | 生产写入/恢复/清理 | 用户授权范围只到本机专用验收实例;远程生产一律未执行 |
| 生产目标维护 | 对共享业务栈开维护窗口 | 本轮明确不停 WKNFull-app;维护只在 kgmaint 专用实例上跑 |
| 生产封存/同步真实链路 | 需要在生产机部署 libraryagent 并接线 |
需要用户批准的部署窗口与凭据录入 |
四·补 运维风险(本轮核对,未擅自处理)
正式 8088 栈(knowledge-governance-p1-api-1 / -worker-1)当前引用同一个 agent socket:
api env LIBRARY_AGENT_SOCKET=/run/library-agent/agent.sock,worker 挂载
/home/zheromiku/.local/share/knowledge-governance/agent -> /run/library-agent。而主机侧 user 单元
~/.config/systemd/user/knowledge-governance-library-agent.service 本轮已把目标改成 kgmaint 验收实例
(-source-app kgmaint-app、-project kgmaint-weknora、-archive /tmp/kg-e2e/archive,
原 WKNFull 版备份为 *.wkfull.bak)。后果:从正式 8088 发起封存会冻结测试实例,并把快照写进
正式 API 看不到的归档目录(正式 api 的 LIBRARY_ARCHIVE_ROOT=/var/lib/knowledge-governance/library)。
本轮没有启动任何共享维护,也没有改回该单元——需要用户决定:正式上线前把单元恢复为 WKNFull 版
(备份已在位),或给候选构建单独的 socket 路径。证据 00-identity-final.json 的 formal_stack 段。
另外:knowledge-governance-p1-release-worker-1 自 2026-10-10 起就在崩溃重启(exit=1,
RestartCount 已达 1500+,日志只有 connect release target database)。本轮只读定位到根因:
config/release.sim.json 的 target.database_url_file 指向 prod-pg:25432,而该主机名在本机与
容器网络里都不存在(getent hosts prod-pg 无结果、容器内 NXDOMAIN);source 指向的
WKNFull-postgres 可解析,所以是目标库配置遗留,不是代码缺陷。错误文本吞掉了主机与原因,
本轮已改为带端点 + 路由 + 脱敏原因(可在下次重启时直接看到),但修正正式配置或停用该 worker
属于正式栈变更,需要部署窗口授权,本轮没有执行。
五、本轮新增结论(写在这里而不是报告顶部)
- Wiki 撤销语义本轮不通过,且不是治理侧能自己补的:上游 retract 走 LLM 重写,重跑一次留下两句撤销方独有事实(
ZEBRA-A-7731/ZEBRA-A-9910),另有 r1 轮次把两句清掉的记录——同一输入两次结果不同。原版 v0.7.1 的 Wiki API 只有页 CRUD、rebuild-links(只重算[[slug]]边,metadata-only)、auto-fix(只修 lint 可修项),没有"按剩余来源重算页面正文"的安全 API(internal/router/router.go:2354起的路由表 +service/wiki_page.go:759)。因此:①不得把 V03 标通过;②不得用关键词硬删掩盖;③可选方案见交付报告(外部最小治理:保留旧版本+关联撤销作业+受影响页重建或阻断发布+验证与审计,需用户拍板产品语义)。 - 治理侧不能只报页面级差异:撤销共享贡献时页面仍是"更新",页面级计数会显示成"无变化"。已改为逐页语义比对,能指出"来源 -1、引用 2→1、正文变了、出边少了一条"。
- 封存链路本轮真的打通了,而且不是放松校验:CLI 改为实例级窗口(闸门 + 后台静默 + 目标指纹),"写者 exit=1"由闸门的
drained/active_at_close证据解释,而不是被忽略;排空证明改成"先在途、再关入口"的确定性顺序,不再靠 202 之后抢时序。 - 接线后暴露的四个真缺陷(都已修,都有用例):
jobs.id列宽 36 让封存作业根本投不进去;v0.7.1 的 app 写索引不带tenant_id导致导出 0 条;全新实例的空索引被当成"索引被改坏";恢复作业 id 带.被维护窗口拒。另有资源定位符仍写来源桶——同步过去的目标取不到图/附件。 - 死信解除已从"缺代码"变成"有代码、真实验证了一半":迁移 073 记录精确解除(保留上游行、绑定实时行指纹、重放引用、源侧观测摘要、操作人);封存时每条实时死信都必须被精确解除命中,行变化/新行/未结清照旧阻断;
libraryctl dead-letters生成只读观测,libraryctl -released-dead-letters逐字节复核。没有演练的部分是"真实死信→重放→解除→放行":本轮源侧dead_letters=0,且重放需要源侧系统管理员权限(/system/admin/runtime/queues/{queue}/tasks/{id}/actions/run_now),治理平台故意不持有该凭据。不伪造成功,也不 SQL 删行冒充。 - 同步过去的资源"能被应用读出来"是这轮才真正验的:资源行里既有
physical_path也有独立的storage_backend_id列,两者都指向来源实例的storage_backends主键;只换桶或不换列,目标应用都 500(code 1007 invalid MinIO file path)。已在同一份行变换里同时改后端 id 与桶,解析不到就拒绝写入。