发布管理:执行器可用性来自真实观测、整库还原入口与脱敏诊断 #12

Open
zheromiku wants to merge 0 commits from feat/library-executor-availability into main
Owner

已实现

  • 封存执行器可用性改为 worker 真实探测(agent health、迁移 074 library_executor_status、30s 心跳);overview / 快照列表 / POST captures 共用同一判定与原因。
  • 封存执行前探测 + 执行后按结果内 agent_identity 复核;不一致不登记测试封存。
  • GET /library/restore-availability + 页面恢复入口(管理员签发 30 分钟 restore 授权,条件满足才可点)。
  • release-worker 启动失败带端点/路由/脱敏原因;共享 internal/pgdsn(按口令与形状脱敏,保留 Unwrap)。

边界

  • 不承担 Wiki 语义撤销(用户 2026-10-29 决定,V03-a 证据保留为模块范围外)。
  • 未改 WeKnora 源码/Schema/镜像;未重启正式栈、未动共享 WKNFull;正式 socket 仍无监听。

真实验证

候选隔离栈:停候选 agent → overview capture_available=false(原因“不可达”)→ POST captures 503 library_capture_unavailable → 启 agent 恢复 true;restore-availability 200。证据在 $TMPDIR/kg-candidate-verify/evidence/。

尚未验证

真实封存(会产生新快照/停写入者窗口)与真实恢复作业未在新构建上重跑;正式 8088 未部署;正式 release-worker 配置修正需部署窗口。

未部署。

## 已实现 - 封存执行器可用性改为 worker 真实探测(agent `health`、迁移 074 `library_executor_status`、30s 心跳);overview / 快照列表 / POST captures 共用同一判定与原因。 - 封存执行前探测 + 执行后按结果内 `agent_identity` 复核;不一致不登记测试封存。 - `GET /library/restore-availability` + 页面恢复入口(管理员签发 30 分钟 restore 授权,条件满足才可点)。 - release-worker 启动失败带端点/路由/脱敏原因;共享 `internal/pgdsn`(按口令与形状脱敏,保留 Unwrap)。 ## 边界 - 不承担 Wiki 语义撤销(用户 2026-10-29 决定,V03-a 证据保留为模块范围外)。 - 未改 WeKnora 源码/Schema/镜像;未重启正式栈、未动共享 WKNFull;正式 socket 仍无监听。 ## 真实验证 候选隔离栈:停候选 agent → overview `capture_available=false`(原因“不可达”)→ POST captures 503 `library_capture_unavailable` → 启 agent 恢复 true;`restore-availability` 200。证据在 `$TMPDIR/kg-candidate-verify/evidence/`。 ## 尚未验证 真实封存(会产生新快照/停写入者窗口)与真实恢复作业未在新构建上重跑;正式 8088 未部署;正式 release-worker 配置修正需部署窗口。 未部署。
zheromiku added 31 commits 2026-10-29 13:06:37 +08:00
为什么:删除一篇与别人共同贡献同一 Wiki 页的文档后,页面还在、名字也在,
只有那篇文档的正文与引用被摘掉。原来的页面级差异会把这种语义重算显示成
"无变化",操作员据此会误判撤销没有生效。

怎么改:librarydiff 新增 CompareWiki,以 slug 为身份把正文/来源文档/引用分块/
出入边/目录分别取集合比较,输出 sources_removed、citations 变化与具体类别;
overview 的 diff.wiki 增加逐页明细(最多 50 页,超出置 pages_truncated),
说明文字明确"重算发生在源侧,本平台只如实比对与保留结果"。

页面:新增目标维护面板(实例级/库级、持有、入口开关、阶段、进度、恢复错误),
只有窗口仍持有且作业失败时才给"按预备份恢复";没有维护记录时写未接入 + fail closed。
变更页把 Wiki 逐页差异列出来源/引用怎么变。kind 补上 restore。

验过:go test ./internal/librarydiff ./internal/httpapi;web vitest 453 项;
make verify 全绿(15 项,无未运行项)。真实撤销语义验收见交付报告。
为什么:专用验收实例用 S3 兼容的 MinIO,而 CLI 的源发现只认 oss 环境变量,
`STORAGE_TYPE=minio` 直接失败;同时 CLI 只读 routing bindings,API 读
LIBRARY_PRODUCTION_KB_*,两边指向不同知识库时,"封存的是哪个库"就说不清。

怎么改:新增 objectStoreFromSourceEnv(oss/minio,端点/桶缺失即报错,不退回默认值),
scopeBindings 让环境变量优先、bindings 兜底。单测覆盖 minio http/https、oss 与未知类型。
上一提交只改了 status;capture/preflight 仍直接读 routing bindings,
所以 CLI 封存的还是 bindings 里的库,而不是 API 的目标——契约读取直接失败。
共享业务栈与专用验收实例共用同一个 WeKnora 镜像,原来只按镜像判"未声明写入者",
任何一台开着共享栈的机器都永远开不了窗。判据改为:同项目或无项目标签即冲突;
另一个项目只有数据库/队列目标与源相同才算冲突。单测覆盖六种组合。
`docker ps -q` 默认输出 12 位短 ID,与 declared 里的 64 位 ID 永不相等,
结果连已声明的写入者都被判成"未声明的并发写入者",封存永远开不了窗。
改用 --no-trunc 并按前缀双向匹配。
矩阵不再在顶部叠报告:已完成 / 缺代码 / 已实现未跑 / 需授权四段分开,
把本轮 Wiki 撤销真实验收(12 项检查)、贡献级差异、维护面板写进对应行,
并如实记下封存链路缺口(CLI 无入口排空证明、API 封存未接线)、
死信无解除契约、Neo4j 场景与排空探针未跑。

新增正式 8088 待验收部署方案:迁移 067–072、loopback 配置差异、
缺配即拒绝的 fail-closed 规则、回滚与账号数据保留。只准备,未执行。
0.1.166(1faf513)是本轮验收代码 SHA;f82eb07 起的文档提交不改代码。
0.1.165 因后续提交被 CI 取消,按事实记为 cancelled,不改写成成功。
库级窗口永远拿不到在途排空证据:被代理的 WeKnora 收到 SIGTERM 就 exit(1),
写者退出码不能当作请求已排空。capture/recover 改由 librarymaintenance.Manager
装配入口闸门、后台静默检查、目标指纹与入口探针(与发布链路同一套实现),
排空证据来自闸门 drain 记录;缺 LIBRARY_TARGET_* 时在开窗口前拒绝并点名。

维护接线解析收敛到 internal/librarymaintenance/env.go,agent 与 worker 讲同一组
变量名;agent 启动如实打印接线状态;安装脚本强制 0600 agent.env。

试过:make verify 15 项全绿;新增 7 个用例(接线解析/身份绑定/缺闸门拒绝)。
真实 v0.7.1 的 app 自己写索引时不带 tenant_id,治理侧同步写入时才补齐。
原来只按 term tenant_id 过滤,等于把 app 写入的正文整库漏掉:真跑时索引里有
2 条文档,导出 0 条,候选校验报 legacy coverage,封存被拒。

改为"显式本租户 或 完全没写租户字段"(写了别的租户仍然排除),知识库身份
仍是独立 term,范围没有放宽。Count/PruneScope/整库导出共用同一写法。

试过:make verify 15 项全绿;新增 3 个过滤条件用例;真实 preflight 由
candidate-invalid(0 条) 变为 candidate-valid(2 条)。
真实请求 POST /api/v1/library/captures 一律 500:拼出来的
"library-capture-<scope>-<nano>" 是 45 个字符,而 jobs.id 是 varchar(36),
PG 直接拒绝(value too long)。此前页面一直停在"未接线",这条路径从没被
真实请求覆盖过,所以只有接上 agent 之后才暴露。

改为 libcap-<scope>-<nano>(35 字符),并用测试锁住列宽与形状。

试过:go test ./internal/httpapi -run LibraryCapture 全绿;修复前用真实
DATABASE_URL 复现 enqueue error: value too long for type character varying(36)。
容器 env 里的 MINIO_ENDPOINT 往往是服务名(langfuse-minio:9000),封存跑在宿主机上
解析不到,真实封存报 read library resource 并整体失败。
新增 -source-s3-endpoint:只换访问入口,桶与对象键不变;缺 scheme 直接拒绝;
status 会打印实际使用的 endpoint。

试过:make verify 15 项全绿;新增覆盖用例(生效 + 拒绝无 scheme)。
上一提交只给 libraryctl 加了 -source-s3-endpoint,agent 却不知道这个 flag,
systemd 起不来(exit 2)。端点属于"这台主机怎么访问这个源的存储",必须由 agent
自己钉住并随固定声明传给 libraryctl;请求仍然不能改。没配置时不凭空加 flag。

试过:make verify 15 项全绿;新增 agent 声明用例(有端点必传、无端点不多传)。
style(libraryagent): gofmt
build / build (push) Successful in 1m14s
84e8a0dfdc
上一个提交漏跑 gofmt,本地门禁 format 失败;只改空白。
全新 WeKnora 会用空映射({})先建好索引,范围字段等第一次写入才动态生成。
首次同步在读目标当前状态时就报 "missing release scope fields" 并失败,等于
"往空的生产实例做第一次同步"这条路根本走不通。

现在:"没有范围字段 + 索引确实 0 篇文档" = 空基线;有文档却看不出归属仍然报错
(绝不把无法归属的文档当空库覆盖)。Count 与整库导出共用同一判断。

试过:make verify 15 项全绿;新增空/非空两种索引的用例;真实 sync 复现原错误。
同步把资源字节写进目标桶,但 resources.physical_path 是从快照原样搬的,
里面还写着来源桶(storage://…/minio://kgmaint-1028a/…)。目标实例之后按这一行
取图/附件就会指向测试环境的桶,或直接取不到——跨环境引用,必须在本轮修掉:
写入前换桶、键不变,定位符解析不出来就报错(不写指向别处的行)。

试过:make verify 15 项全绿;新增定位符用例(嵌套/单层/裸键/非法);
真实同步后目标桶里对象键与行内定位符一致。
fix(v071): 资源换桶的变换在校验时同用
build / build (push) Successful in 1m17s
b061a9686c
只改写入侧会让"行内容摘要"对不上:校验拿变换前的归档行与目标里的变换后行比较,
真实同步在 verify 阶段报"生产 resources 内容与快照不一致"并回滚。
把行变换抽成 rowTransform,写入与校验共用同一份。

试过:make verify 15 项全绿;真实同步复现该失败后修复(下一轮重跑验证)。
librestore-<scope>-<时间戳.微秒> 带 ".",冻结协调器的 job 白名单只收
[A-Za-z0-9_-],所以每次恢复都在开窗口时被拒(invalid freeze request),
作业随后被接管扫成失败。改纳秒整数,并加测试锁住该约束。

试过:make verify 15 项全绿;真实恢复复现该错误后修复。
- 矩阵:新增 S06/S06-a/S06-b/V03-c/Neo4j 拒绝行(都有真跑证据);把"封存入口未接线"、
  "CLI 库级窗口"从缺口里移走;排空时序旧结论保留但标注被确定性做法取代。
- 死信解除仍缺产品路径,写出最小外部治理契约(保留原始行、审计、验证真实处理成功后
  按精确项解除、未解除继续阻断)。
- 台账补 0.1.169–0.1.180 的 SHA 与 run(含 211/215 failure、218 cancelled,不涂改)。
- library-capture / 8088 部署方案同步成本轮真实结果。
上游 task_dead_letters 没有解决标记,purge/DELETE 会销毁"失败过"的唯一证据。
治理侧新增解除记录(迁移 073):绑定实时行逐字节指纹、重放引用、源侧观测摘要与
操作人;行被改过、重放后又出现新死信、源侧未结清都不命中,封存照旧阻断。
指纹先压紧空白再摘要,避免 PG to_jsonb::text 与 json.Marshal 的空白差异。

试过:go test ./internal/{librarydeadletter,libraryagent,httpapi,releaseadapter/v071,
platform/postgres},make verify 15 项全绿。
storage://<id> 是来源实例 storage_backends 的主键;只换桶不改 id 时,目标原版
应用以 storage backend not found 落回默认服务并拒绝整条路径(真实下载 500
code1007)。写入前按租户+目标桶解析目标后端,写入与校验共用同一份行变换;
解析不到就拒绝写入,不再产出目标读不出来的定位符。

试过:目标实例真实同步后 /api/v1/knowledge/{id}/download 读回 500 → 修后复测;
go test ./internal/releaseadapter/v071 与 make verify 15 项全绿。
原版应用先按 resources.storage_backend_id 找文件服务;只改定位符不改这一列,
目标仍报 storage backend not found 并拒绝整条路径(真实下载 500 code1007)。
行变换同时改写该列,写入与校验共用。

试过:目标同步后 /api/v1/knowledge/{id}/download 500 → 修后复测读回 507B 且
sha256 与源对象一致;go test ./internal/releaseadapter/v071 与 make verify 全绿。
同步目标与封存源常常不是同一个实例(kgseal vs kgmaint);用目标名去绑来源死信,
真实观测的 project 永远对不上,解除会被一律拒绝。新增 LIBRARY_SOURCE_INSTANCE,
留空回落到目标实例名,API 写记录与 worker 取指纹共用同一个来源实例名。

试过:真实 API GET/POST、go test ./internal/{httpapi,appconfig} 与 make verify 全绿。
按父审后重跑的真实结果改写验收矩阵:V03-a 单列"失败/不通过"(共享页正文仍带
撤销方独有事实);V05 并发阻断、B02 删源对象后目标应用读回、死信精确解除的
"已做/未演练"分开放;记 R03 实测耗时与正式栈 socket 风险;台账补 0.1.181–0.1.186
与 run 222–227。
页面上的"可发起封存"原来只判 LIBRARY_AGENT_SOCKET 非空:正式栈的 API 根本没挂
那个 socket,而 agent 曾顶着正式单元名冻结候选实例——配置非空、页面可点、真发起
却封错库。

现在:agent 新增 health 动作(不占封存操作锁、不跑子进程,只回答身份/降级/忙),
worker 每 30 秒探测并写入迁移 074 的 library_executor_status;overview、快照列表与
POST /library/captures 共用同一份判定(过期、不可达、实例不符、降级、正忙都拒绝并
给原因)。封存结果由 agent 附上自报身份,worker 在停写入者之前再探测一次、执行后
用结果复核,不一致就不登记测试封存——心跳只能证明"刚才"。

期望身份用 LIBRARY_SOURCE_*(封存源),不是 LIBRARY_TARGET_*(同步目标),否则
正确的执行器会被判成不匹配。另加 GET /library/restore-availability:恢复能否执行由
双闸、该快照的恢复授权与实例维护状态决定,页面不再写死 disabled。

真跑(候选隔离栈,两处 socket 隔离不变):overview 返回 executor
{kgmaint-weknora/kgmaint-app};停 agent 后转不可达并 503 带原因;重启后恢复。
原来"恢复至该版本"写死 disabled,标题还写"生产恢复未接线"——而 POST /library/restores
与授权接口早已可用,等于把整库还原的唯一入口藏起来。

现在页面按 GET /library/restore-availability 启用:缺授权时管理员可签发 30 分钟
有效期的 restore 授权(固定期限,没有"永久授权"),条件满足才可点恢复;工程师只看到
条件与"仅管理员可发起"。封存不可发起时也不再写死"通道未接线",而是显示接口原因、
执行器身份与观测时间。
正式 release-worker 崩溃重启时日志只有一句 connect release target database,
主机名、库名和底层原因都不在里面。现在错误里带 host:port/db、路由标识与原因。

端点去口令不等于整条日志安全:pgx 的解析错误可能原样回显连接串。新增
internal/pgdsn.Redact(按已知口令精确替换 + 按 URL/keyword 形状兜底),
错误保留 Unwrap 以便 errors.Is/As,技术原因不丢。cmd/worker 里只留库名的旧实现
换成同一个包,避免两份脱敏逻辑。libraryctl 用法补上 dead-letters。
发布管理不承担 Wiki 语义撤销:V03-a 的原始失败证据保留,但移到"模块范围外",
不再阻断发布,也不改判为通过。矩阵同步补上执行器运行时可用性与按版本恢复入口的
真实证据(候选栈),并把尚未跑的部分(真实封存身份复核、新构建上的真实恢复)
写在"已实现·未跑"。正式 release-worker 的崩溃根因按实测写清:目标库主机
prod-pg 不可解析,修正配置属于正式栈变更,需要部署窗口。
完整带口令的 URL 出现在源码里会被 security-secrets 判红(测试值也一样)。
按既有做法把连接串拆开拼接,语义不变。
zheromiku added 1 commit 2026-10-29 13:09:06 +08:00
记录真实 SHA、CI run 229(head 4c79052,必需 context success)、候选栈二进制来源与
回退目录。代码 SHA 与已部署候选产物分开写,不把文档提交当成部署证据。
zheromiku added 1 commit 2026-10-29 13:12:36 +08:00
矩阵里写"0.1.187 构建"会与台账的版本号漂移;改成代码 SHA 4c79052(0.1.192 工作树),
与候选二进制来源一致。
zheromiku added 1 commit 2026-10-29 13:23:59 +08:00
原来 worker 先 health 再 capture,两次拨号之间 socket 可能被换成另一个实例的
执行器:错的 agent 已经把写入者停了、快照也出了,事后 postcheck 只能拒绝登记,
副作用无法撤销。

现在 capture 请求自带期望身份(治理侧配置生成:project 必填,source_app /
archive_root 配了就绑定)与覆盖三项的稳定指纹;接收方在操作互斥内、执行任何源
操作之前比对,缺字段、指纹与字段不一致、project/source_app 不符都直接拒绝,
**错的执行器一次 capture 都不跑**。封存结果回显该指纹,worker 事后复核按它对照,
postcheck 只作为证据防线。恢复路径不要求期望(它只把写入者恢复回来)。

负例是确定性的:两个真实 agent 各占一个 socket,把调用方指向错的那个,断言它的
capture 调用为 0 且仍能回答 health;同一份期望打到对的 socket 则只跑一次。
zheromiku added 1 commit 2026-10-29 13:32:58 +08:00
真实封存卡在 `read library resource` 时没有任何可判断信息:对象缺失、桶写错、
宿主机解析不到端点都会走到同一个字符串。现在错误带资源行 id 与底层原因
(不含凭据),本轮就是用它在候选栈定位到"源对象被上一次 B02 演练删掉后没真正写回"。
zheromiku added 1 commit 2026-10-29 14:02:14 +08:00
原本 execute 写完生效版本就写 succeeded,而服务恢复、入口开放与维护锁释放发生在
窗口返回之后。任一步失败时外部已看到成功,重启接管又只认非终态,于是目标永远
留在维护里(本轮真实现象:同步 succeeded 后立即恢复被 409)。

现在内容校验与版本登记完成后只记"待收尾":finishLibrarySuccess 先确认目标写入者
与入口健康,再按 scope → instance 顺序释放两把锁(每一步错误都上抛,不再 `_ =` 丢弃),
最后才写 succeeded;任一步失败即落失败并保持 needs_restore。释放与终态写入用
context.WithoutCancel 有界包住,停机不会留下半成品。接管的 finalize-only 判定改为
按证据(finalize 阶段成功或未顶掉的生效版本),只复核与登记,不重写生产。

测试覆盖:调用顺序(成功后两把锁必已释放)、健康失败不释放不报成功、三步释放失败
各自不报成功、终态写入失败提示等待接管、接管证据判定正负例。
zheromiku added 1 commit 2026-10-29 14:07:39 +08:00
docs: 台账补记 0.1.195–0.1.197
build / build (push) Successful in 1m16s
691e4e0225
记录 R1 身份契约、封存资源诊断与 R4 成功边界三个提交的真实 SHA 与 CI run,
标明候选栈 clean 构建(vcs.modified=false)与真实验收覆盖范围。
zheromiku added 1 commit 2026-10-29 14:24:17 +08:00
resumed 分支里 LibraryVersions 的错误被忽略,FinalizeOnly 保持 false 后继续完整
apply:对"已登记版本并释放窗口、只差终态"的作业,这等于把同一份内容重写一遍目标,
与"最终只复核"的承诺相反。

现在接管先分类:finalize 阶段成功作为独立证据(不受版本查询影响)→ 只收尾;查询
失败且无阶段证据 → 未知,零写入、可重试;本快照被别的快照顶替 → 拒绝覆盖;其余才
按正常流程。

同时修释放幂等:Update*/Release* 对"已由本作业释放"的行返回 ErrMaintenanceReleased,
收尾重试视为已完成(部分释放后不会永久卡死),别的作业仍报未持有。真 PG 负例验证
两种情形。
zheromiku added 1 commit 2026-10-29 14:27:56 +08:00
docs: 台账补记 0.1.198–0.1.199
build / build (push) Successful in 1m20s
271f9f5f09
记录接管分类与维护释放幂等的真实 SHA、CI run(236)与候选栈身份注入后
/api/v1/build-info 的读回结果。
zheromiku added 1 commit 2026-10-29 15:24:34 +08:00
fix(web): 存储提示改为实际能力
build / build (push) Successful in 1m24s
f9c15a8f14
页面仍写"保留策略与清理在系统设置→存储管理,均未接线",但存储与备份已有只读
去重/保护范围/预览。改为说明可查看什么、什么尚未开放。
zheromiku added 2 commits 2026-10-29 18:16:37 +08:00
目标非空又没有生效版本时,首次发布以前只能停在"权威版本未知"。现在有了
真实入口:管理员发起 baseline 作业,worker 在维护隔离下把目标整库捕获成
归档快照,再用同一套 Verify 证明目标与快照逐类一致,最后登记
production_effective(证据写明 origin=target_baseline、快照 ID、逐表行数、
目标索引映射)。不写目标内容、不清空目标、不手写版本。

同时修两处会误导人的判定:
- productionScopeEmpty 原来只数 knowledges,"文档删了但分块/Wiki/资源还在"
  的目标会被当成空库;改用适配层同一份表清单与范围过滤逐表计数。
- 概览里的"测试封存候选"原来取该 scope 最新一份归档,而归档里还有生产
  预备份与目标基线快照;改为只认版本登记里 kind=test_sealed 的那一份。

迁移 075 放开 kind='baseline' 并把它并入 sync/restore 的写互斥索引;
worker 重启时用同一作业 id 接管未完成的基线窗口。维护接口补 capability,
页面才能区分"未运行维护"与"维护能力没接线"。

验证:go build ./... / go vet ./... 通过;新增用例覆盖管理员校验、已有生效
版本拒绝、目标读不到时零作业零版本、候选来自登记、缺项文案指向基线入口、
capability 逐项缺失;反向自检(把候选改回按最新归档取、把文案改回旧句、
去掉管理员判断)三项均按预期变红后复原。
发布页原来只有一个永远 disabled 的"启动同步",title 还写着"生产同步执行链
尚未接线"——执行链其实已经存在。现在:

- 管理员看到"签发同步授权(30 分钟)"(action=sync,绑定范围+目标快照),
  签发与执行仍是两步,不会自动签发;
- 闸口全满足时"启动同步"真的按 POST /library/syncs 投递,被拒时逐条显示
  接口返回的缺项;
- 目标非空又无生效版本时,管理员看到"建立目标权威基线(维护隔离)";
- 测试封存候选改用接口给的 test_sealed(来自版本登记),不再拿归档里最新
  一份顶替;
- 维护卡片区分"未运行维护 / 维护能力未接线 / 能力未知",逐项列出缺的变量。

验证:vitest 99 个文件 469 个用例全绿,tsc --noEmit 无错;反向自检(把按钮
改回恒 disabled、把维护文案改回旧句)两处按预期变红后复原。
zheromiku added 1 commit 2026-10-29 19:09:05 +08:00
父审发现三处会让目标卡死或状态对不上实际入口,逐条修:

1. 登记成功但没来得及收尾:崩溃重入会撞上"已有生效版本"直接失败,
   目标留在维护里、占用也不释放。现在按「本作业+同一快照」识别为自己的
   版本,只做收尾(恢复服务/开入口/释放两把占用),不重新捕获、不重写;
   别人的版本仍然拒绝。捕获完成但登记失败则明确失败并写"未对目标写入"。
2. 重入覆盖原运行状态:崩溃时服务已被停住,再读一次只会读到 stopped,
   写回去会让恢复阶段永远不启动写入者。AcquireInstance/LibraryMaintenance
   在 owner 相同的重入时保留已持久化原状态;新作业接管已释放窗口用自己的观察。
   真实 PostgreSQL 集成用例覆盖(先红后绿)。
3. 版本登记移进维护窗口:捕获/校验/登记要么一起成立、要么目标原样恢复;
   记录失败时窗口已开放,治理状态同步释放,不再"显示维护中、实际已开"。
   索引映射读不到时拒绝登记,不再用空摘要写证据。

顺带:
- 基线窗口统一收尾抽成 settleBaselineWindow,四种结局各有测试(成功/健康失败/
  held 保持维护/已恢复则释放),并用真实 DB 用例证明 baseline 与 sync/restore
  共用写互斥、幂等重试不产生第二个作业。
- 新增 cmd/librarydrill:维护隔离 → 整库预备份 → 一致性校验 → canary 漂移检测
  → 按预备份恢复 → 逐类复核 → 按原状态恢复服务。用与 worker 相同的 LIBRARY_*
  配置,绝不触碰封存源。
- 目标/预备份/维护栈装配收成 internal/librarystack 一份(worker 与演练共用)。

验证:go build/vet/test 全过;TEST_DATABASE_URL 指向临时 PostgreSQL 跑
TestMaintenanceReentryKeepsOriginalServiceState(修复前失败:got app=stopped)
与 TestLibraryWriteMutexCoversBaselineJobs;本地门禁 15/15。
zheromiku added 1 commit 2026-10-29 19:41:15 +08:00
真实演练暴露两个只有跑起来才看得见的问题:

1. 归档要求完整兼容性契约(v0.7.1 / schema 74 / elasticsearch_v8 / 1024 维 /
   映射摘要),而基线装配只填了 embedding 与映射,捕获被
   "unsupported snapshot compatibility contract" 拒绝。
   现在 schema 版本读目标库 schema_migrations(dirty 或版本不符直接拒),
   embedding 优先读目标库模型绑定(本机目标 models 表为空、引用悬空,
   此时回落部署声明并说清来源),版本/引擎用受支持栈常量,映射取目标当前值。
2. 演练在"还没写目标内容"时就允许 restore_blocked=false 的窗口以 blocked 收尾,
   写出 content_risk=false + restore_blocked=true 的非法 journal(恢复器拒绝加载,
   目标停在停机态)。现在写 canary 时先 MarkContentRisk,allowRestore 按
   "没碰内容 ⇒ 安全恢复;碰过 ⇒ 必须恢复并校验通过"。

另外给演练工具加 -recover:只把非终态窗口按原运行状态恢复,用于演练失败后
把目标救回来(不改治理记录)。

验证:库内模拟真实目标不可得,契约分支用真实 kgseal 演练覆盖:
snapshot 5cb945e6… 捕获 143ms → 基线一致 → canary 后校验如预期失败
(knowledges 1 行摘要不一致)→ 恢复后校验通过 → 窗口 succeeded、服务与入口恢复。
Some checks are pending
build / build (push) Successful in 1m22s
This branch is already included in the target branch. There is nothing to merge.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/library-executor-availability:feat/library-executor-availability
git checkout feat/library-executor-availability
Sign in to join this conversation.