Commit Graph

93 Commits

Author SHA1 Message Date
姚章浩 3e4020c327 fix(drill): 支持空目标并在探针写入前落盘恢复证据
build / build (push) Successful in 1m21s
2026-10-31 02:56:44 +08:00
姚章浩 7ab2eb1ccf fix(library): 按目标身份隔离版本授权与执行历史
build / build (push) Successful in 1m47s
2026-10-31 02:15:15 +08:00
姚章浩 06c2db19fc fix(library): 删除 handoff 路径残留的无条件 stage 写
build / build (push) Successful in 1m31s
BeginLibraryHandoff 事务里已经把 handoff 阶段推进为 running,函数返回后又补了一次
无条件 UpdateLibraryStage:既重复事务里的写,又在 Begin 返回与这次写之间给交接留出
竞态窗口(此时所有权可能已被 release 拿走)。删掉这次调用,handoff 路径的
running/succeeded/failed 三类阶段写只发生在 Begin/Complete/Fail 三个持有 job 行锁的
事务方法里。

回归:新增源码契约测试(library_handoff.go 里不得再出现 store.UpdateLibraryStage,
且必须走三个事务方法),以及行为测试(所有权不属于本次 handoff 时零阶段行、作业零改动;
用真实 handoff ID 重放则幂等成功且同样零写入)。既有事务行为测试保持不变。

验证:隔离 fixture PostgreSQL + 本 SHA libraryctl 实跑 cmd/worker 全包通过;完整门禁见提交日志。
未部署、未推送;云 SSH 与 UI 仍未验证。
2026-10-30 19:42:13 +08:00
姚章浩 70c181370d fix(library): handoff 阶段事务化、回传空页重置与目标基线原产地
父复审确认的三个残余(原链路,不扩架构),本包按真实调用链修完。

1 回传空页不重置游标:引用数正好是 pageSize 整数倍时,最后一页为空,
len(items)==0 直接 break 而不清游标 ⇒ 游标永远停在最老处,下一轮仍为空,
之后新增的快照再也取不到。抽出单轮函数 pullRound:空页显式重置该 scope 游标,
游标跨轮保留(单轮 20 页只是预算)。测试用真实分页语义的假数据源跑 100/2000/2100
三档,遍历完再新增引用并断言下一轮能看到(不再只测 advance helper)。

2 目标权威基线被误拒:library_baseline 是目标侧真实捕获,但只登记
production_effective(证据 action=target_baseline_init / origin=target_baseline),
原实现把"只有 effective"一概拒绝。现在原产地按**捕获证据**判定:封存=源、
预备份=目标、带基线证据的 effective=目标;普通发布成功留下的 effective 与
recovery_verified 只是引用、不决定原产地;无捕获证据或源/目标同时成立仍拒绝。
测试走真实基线登记形状(内部为业务真实字段),并保留"纯普通 effective 仍拒绝"。

3 handoff 阶段写入不完整:SyncStages 没有 handoff 行,UpdateLibraryStage 只是 0 行;
初始 running 是"读 owner 再写"(有竞态);成功后按旧 owner 写阶段必 0 行。
现改为三个事务方法:BeginLibraryHandoff(建/推进 running 且在同一事务内确认所有权)、
CompleteLibraryHandoff(同一事务:投递发布作业 + 交接 queue_job_id + 阶段 succeeded)、
FailLibraryHandoff(同一事务:阶段 failed + 作业 failed,不留半条记录)。
阶段行 (job_id, stage) upsert。测试断言交接后 handoff 阶段行真实存在且为 succeeded、
迟到执行对 stage 与 job 零改写(用屏障确定性把所有权切给 release)。

清理:删除被上述事务方法取代的 AdvanceLibraryJobIfActive 与 FailLibraryHandoffIfOwner,
避免同一含义两套实现。

验证:隔离 fixture PostgreSQL(真实迁移/队列/完成事件)+ 本 SHA 的 libraryctl 实跑
cmd/worker 全包通过;internal 全包与 go vet 通过。未部署、未推送;云 SSH/UI 仍未验证。
2026-10-30 19:32:28 +08:00
姚章浩 8a68ed1819 fix(library): 交接所有权 CAS、回传游标跨轮与快照原产地
父审查确认四项是原 finding 未闭环,本包按真实调用链修完(先 RED 再修)。

R4a 正常流程必现阻断:API 先入 handoff 队列作业并关联,交接却只接受"未关联",
每次正常发布都被自己的所有权检查挡住。新增 governance.LibraryHandoffQueueJobID
(jobs.id 为 varchar(36),原实现拼后缀超长,插入即失败);EnqueueAndAttachLibraryJob
增加"声明的前驱队列作业号",只允许当前无人持有或正是该 handoff 时替换,任意替换拒绝。
测试从 API 真实前驱(已关联)开始,覆盖正常交接、错误前驱拒绝、交接后重放、
以及 release 与 handoff 两条队列行都存在。

R4b 迟到 handoff 污染已交接作业:失败出口(含 Push 错误)改为所有权 CAS
(FailLibraryHandoffIfOwner),只在业务作业仍由本次 handoff 持有时写 state/stage/recovery;
已交接/已终态零修改。所有权判定改为只看 library_jobs.queue_job_id(阶段行是否存在
与所有权无关,原实现会误判)。阶段写入也带所有权。用测试屏障确定性地复现"失败出口
到来时所有权已切给 release",验证 release running 与 succeeded 两种状态下零修改。

R2 回传游标每轮重置:改为跨轮保留(单轮仍 20 页预算),读到不足一页才回到起点;
新增 pullCursor 与跨轮覆盖测试(2100 条引用、每轮 5 页仍全部覆盖,不重复不漏取)。

R3 原产地误判:封存成功后会再登记 production_effective(同一快照),原实现把
sealed+target 判成冲突,正常再发布/历史恢复被误拒。现在原产地只由捕获事件决定
(test_sealed=源 / production_prebackup=目标),effective/recovery_verified 只是引用;
二者冲突或都没有捕获事件仍拒绝。测试覆盖正常成功链、真冲突、无捕获事件三种。

验证:隔离 fixture PostgreSQL 实跑 cmd/worker 全包通过(含上述四条真实调用链);
libraryhandoff/librarysnapshot/httpapi/governance/platform-postgres/libraryctl 全绿;
完整 ci_checks 见提交时日志。未部署、未推送、未构建镜像;真实跨主机 SSH 链路仍未验证。
2026-10-30 19:22:48 +08:00
姚章浩 9300865d23 fix(library): 搬运复用前复验字节、回传游标分页与发布队列事务交接
父独立审查退回的四个 finding,逐项修在本包内,不改架构。

1 复用路径没复验字节:remoteEvidence 只读清单+摘要就宣布可用。现在新增
libraryctl snapshot-verify(整版逐部件重算)并在复用前调用;blob 缺失/篡改必须
在入发布队列之前失败。新增真实 local 跨进程用例:清单不动、删 blob / 改 blob 后
再 Push 必须拒绝。

2 回传枚举被固定 LIMIT 50 截断:LibraryJobSnapshots 只取成功作业且按摘要字典序。
改为 LibrarySnapshotCandidates:合并 library_versions、library_jobs.snapshot_id、
library_jobs.target_snapshot_id 三处引用,按 (created_at, snapshot_id) 游标分页;
回传循环翻页到不足一页为止。失败/待恢复作业留下的预备份、第 51 条以后、历史恢复
输入快照都能被枚举。另修"本地只有清单、blob 已损坏就永远跳过":改用整版 Load 判定,
并给 Unpack 增加显式 RepairCorruptBlobs(只在权威云端读回时开启)。

3 身份映射错误:搬运拿目标 tenant/KB 去核对封存快照,而封存清单写的是**源**身份
(云测试验收目标 a8e9c8ad/2e980686 与源 KB 必然不同)。现在 expectedSnapshotIdentity
按版本台账判定:test_sealed → 用部署注入的源绑定(LIBRARY_SOURCE_TENANT_ID /
LIBRARY_SOURCE_KB_*);预备份/生效/恢复版本 → 用目标绑定;没有登记或两类冲突一律拒绝。
身份永远来自独立绑定与台账,不接受包自证。

4 两步入队有竞态:Enqueue 成功后云执行器可能立刻领取并开始维护,Attach 失败却把作业
标失败并声称"未维护"。改为 EnqueueAndAttachLibraryJob 单事务(锁行 + 插入 + 关联 +
状态守卫),要么都成功要么都不发生;重复执行幂等,已关联别的队列作业则拒绝。
另修终态重放:AdvanceLibraryJob 无状态守卫,迟到的失败出口会把已成功作业改写;
新增 AdvanceLibraryJobIfActive(仅 pending/running 可推进),拿不到就明确记录"未覆盖"。

测试:libraryhandoff 用例覆盖跨租户/错误 scope/清单在而字节坏/本地损坏自愈;
隔离 fixture 库覆盖"校验通过才入队、失败不入队"、>50 条分页与失败作业预备份、
幂等重放不重复投递、迟到失败不改写终态。

未部署、未推送、未构建镜像;真实跨主机 SSH 链路仍未验证。
2026-10-30 19:10:02 +08:00
姚章浩 6400fd1bbe feat(library): 快照搬运先于维护、双向校验与云端版本回传
为什么改:云端 release-executor 与目标同机后,本机封存的快照必须"先到、先验证"
才允许开维护窗口。此前只有 pack/unpack 两条人工命令:UI 点发布时执行器读到的是
本机归档路径,云端根本没有这份快照,作业只能在装配阶段失败;而预备份落在云端,
本机页面也读不到。用户已批准补齐这条链路。

搬运(新包 internal/libraryhandoff):本机 Load 逐 blob 重算 → Pack → SSH 流式写入
云端暂存 → 原子改名 → 云端 snapshot-unpack 再逐 blob 校验 → 读回版本清单摘要自证 →
scope/tenant/知识库身份与治理库作业一致。只有全部通过才投递 library.sync|restore,
失败时发布作业根本不存在(目标不进维护、不写知识)。远端命令是固定 argv,容器名与
路径只来自部署配置;同 ID 异内容拒绝;同一快照本机 flock 串行化;暂存 bundle 用后即删,
归档本体永不自动清理。

回传:capture 角色按已成功作业的 snapshot_id 扫描,把本机缺失的云端快照 Fetch 回来
(云端 pack → SSH 流回 → 本机 Unpack 逐 blob 校验)。回传成功后 API/页面既有的列表、
详情、差异、存储与历史恢复路径读到的就是真实字节;失败则版本不出现在本机,不会被误报成可恢复。

角色:搬运与回传只注册在 capture;若接线到 full(同时消费发布作业)启动即失败,
避免与云端执行器抢同一批作业。release-executor 仍不拥有治理库 DDL,也不消费
QA/邮件/来源扫描。

验证:internal/libraryhandoff 全绿(跨进程 push/fetch、身份不符拒绝、幂等复用、
并发串行、篡改与未知快照不落版本、路径穿越拒绝、远端错误不当作"没有");
cmd/worker 在隔离 fixture 库(独立测试 PG)上验证"校验通过才入队发布、失败不入队";
httpapi 队列决策单测;相关包 go test 全绿;go vet 全仓通过。

未部署、未推送、未构建镜像:云端接线与验收仍由父执行。真实云链路(含 SSH 通道)
本轮没有云侧写操作,只有本机跨进程与隔离 fixture 证据。
2026-10-30 18:58:34 +08:00
姚章浩 e6472b0ec6 feat(library): 云测试发布目标接入的角色分离、快照搬运与图谱门禁
为什么改:发布执行链是"与目标同机"假设写死的(本机 docker.sock、本机 drain.json、
本机 Redis/PG),把目标换成云测试不能只改展示 URL;直接搬整套 worker 又会抢走
本机封存/QA/周报队列并按本机配置写错目标。所以先把"谁消费哪段队列"变成显式角色,
再补齐跨主机搬运与目标侧门禁。

角色(LIBRARY_WORKER_ROLE=full/release-executor/capture)不只影响 handler 列表,
还决定是否拥有 DDL(release-executor 只读校验迁移版本,落后即拒绝启动)、
收拾哪些 library_jobs.kind、是否跑 release 接管扫描、是否起 scheduler/QA/周报/探测。
capture 保留本机原有全部业务;release-executor 只挂 sync/restore/recover/baseline。

快照跨主机:新增内容寻址的 bundle(版本清单先落、blob 按 sha256 命名、清单最后
Link 作提交点),pack/unpack 幂等且拒绝覆盖与同名不同内容;执行器使用前仍逐 blob
重算 sha256,因此传输不可变有两道证据。

目标侧门禁:目标知识库 graph_enabled=true 时拒绝发布(发布只写 PG/ES/OSS,不写
Neo4j;未知也拒绝),只读检测、不改目标配置、不做图谱迁移。

云端入口:新增 drain 感知的 ingress 闸门(建连超时与读超时分离,半关闭单向传播,
SIGTERM 排空后写 drain.json 并 exit 0/2)、compose 叠加片段(app 不再发布端口、
frontend 与 adapter 的入口收敛到闸门、指向真实网络 weknora-test-v071-network)、
执行器启动脚本与治理 PG 反向隧道脚本(默认 unix socket,不占 TCP)。

验证:deploy/cloud-test/test_gate.py 13 项全 PASS(含 12 秒静默流、半关闭、2MiB 完整、
排空 exit 0/active_at_close=1、排空超时 exit 2、上游不可达不挂住);go test 相关包全绿;
scripts/ci_checks.py 16 项全部 success(EXIT=0,1241 用例通过/0 失败/191 已声明可选集成跳过);
云测试只读预检 17 项 / 6 FAIL,FAIL 项即当前部署缺口(无闸门、app 仍发布端口、
adapter 与 frontend 仍直连 app、云端归档根未建),未部署、未改云端任何状态。
2026-10-30 18:38:38 +08:00
姚章浩 a7d643635a fix(library): 守卫按行取作业身份、同作业多代次、原作业续作与执行域准入
build / build (push) Successful in 1m27s
R1 维护/发布守卫硬编码 j.id=$1:维护类入口的 $1 是 scope/instance,作业号在 $2,
导致 update/release/ingress 六个入口正常路径全部匹配不到作业。改为按被更新行自己的
owner_job_id 生成条件,并同时要求该代次在 library_attempts 里仍活跃。

R2 认领只改 jobs.attempt_epoch 不改 attempts.epoch,第二次接管即错位;唯一索引
(job_id) 与"每次接管新 attempt 且历史不可变"矛盾。改为同作业多代次:锁作业行与
operation 行、同 owner 幂等续租、活跃未过期者让路、过期/本作业旧代次标 abandoned、
新代次 epoch=max+1,并清同 operation 其它作业行租约。迟到 write/renew/finish 一律
ErrLibraryEpochStale(finish 补了"只能在活跃态进来一次")。

R3 续作不再新建 -resume 作业:PrepareLibraryJobResume 让原业务作业回 pending 并显式
结束上一代,维护窗口/journal/预备份/写集合仍按原作业 id 归属,代次由 worker 起手递增。

R4 执行域准入改为注册 handler 前读事实:目标实况(KB/租户/schema/索引)、旧 release
路由绑定的 KB、未释放维护窗口持有者;未知或重叠即拒绝执行(不做维护动作、不停既有服务)。

同时自纠 5 项设计偏离:current 次序键取意图创建时间(旧意图迟到事实被拒)、首次锚点
改事务 FOR UPDATE + 回读且 NULL≠0、版本事件冲突只做加法且 xmax=0 才算新事实、
恢复作为新 kind=recover 意图推进 current、隔离测试加测试库护栏并清理残留 schema。

测试:真实 PG(kg-pgtest-writeset,一用例一 schema)新增 8 条集成用例覆盖上述正负例;
go test ./... 全绿(50 包)、go vet/build rc=0、web tsc + vitest 485 通过、make verify 16 项通过。
2026-10-30 17:20:40 +08:00
姚章浩 98294b9583 fix(library): 统一执行身份、终态收尾、准入与数据身份
build / build (push) Failing after 24s
why:审计 19 项(12 P1 / 7 P2)来自同一批根因,逐点试错会把协议撕成十几份:
  · 意图与执行混为一谈(F01/F15):`library.sync:<scope>:<快照>` 是终身幂等键,
    A→B→A 的第三次发布拿到第一次的历史成功作业,什么都不做;
  · 版本行既是实体又是事件(F02/F09):主键只由 (kind,scope,快照) 决定、写入是
    upsert,第二个作业覆盖 `source_job_id`/`evidence`/`created_at`;
  · 接管没有代次(F13):两个 worker 都能认领同一 pending 作业,失租的旧执行还能
    写业务状态、释放别人的维护锁;
  · 早退与终态没有统一政策(F03/F04/F05/F06):半写窗口的前置检查失败会重新开门、
    服务起不来也照样开入口、journal 终态后收尾不可重建、恢复成功不回写 current;
  · 数据身份与准入各写一套(F07/F08/F14/F17):接管丢了复用语义、全库搬运不改
    location_hash、通用入口可造 library.* 作业、目标模型从没被问过;
  · 存储/容量/UI/CI 判据各自为政(F10/F11/F12/F16/F18/F19)。

what:按批准的一套协议集中实施(详见 handoffs/release-consolidated-implementation/
current-contract.md):
  · operation(一次明确意图)/ attempt(epoch 代次)/ 不可变版本事件 / current 指针;
    治理库迁移 76,只动治理库,不碰 WeKnora Schema/源码/镜像;
  · 所有业务状态、阶段、版本事件、维护锁写入带 epoch 守卫(失租代次写不进去);
    心跳失租立刻取消 handler;
  · 半写窗口早退保持关闭;服务未全部健康绝不开入口;journal 终态可从持久事实重建
    收尾;恢复成功登记 recovery 事件并推进 current;
  · worker 在动维护前核对业务行/kind/scope/队列关联;通用入口拒绝 library.*;
  · 全库搬运重算 location_hash 并与文档级导入共用实现;接管消费写集合的复用决定;
    发布前核目标实时模型契约、容量门禁前置;
  · 存储保护闭包改为全量依赖、类型只认显式登记、保护未知则不出可删候选;
  · 页面核 HTTP 状态与结构、恢复纳入轮询、Wiki 差异覆盖标题/摘要/父级。

how:`make verify` rc=0(16 项必需检查全绿,含 go test 全量与前端 485 例);
`go vet ./...` rc=0;`npx tsc -p tsconfig.json --noEmit` rc=0;逐项负例见各包测试
(F03/F04/F08/F12/F13/F14/F16/F18/F19 都有先红后绿记录)。
未执行:治理库迁移到任何已有库、真实 PG/ES/S3/WeKnora 集成、故障注入、部署。
2026-10-30 16:14:54 +08:00
姚章浩 d0310761b6 fix(worker): 接管复用预备份不再抹掉写集合证据
BackupPrepared 每轮都新造一份证据,而版本登记是 evidence=EXCLUDED.evidence
整份替换。接管复用时 BackupRef 只带快照 id(没有字节数)、写集合预检又只在
首轮执行,于是已登记的 writeset_digest/writeset_rows 被抹掉、backup_bytes
记成 0;同一作业第二次中断后再接管就取不回写集合,恢复链彻底断开
(2026-10-30T00:58Z 演练 …-1793321916382871758-retry,摘要 fddd98cf70… → 空)。

现在证据按「同一作业 + 同一份预备份」合并进已持久化那一行,身份不符一律失败;
接管路径用 prebackupBytesOf 从首次登记的证据取回真实字节,缺失即拒绝(不再写 0);
生效版本证据统一由 effectiveEvidence 构造;同一作业的写集合只允许落定一次。

验证:真实 PG(一次性库 127.0.0.1:25471)上先复现再修
- 修前红:cmd/worker 两条新集成用例分别停在「第 1 次接管后写集合丢失」
  与「复用未登记的预备份竟然成功」
- 修后绿:两轮中断+接管+第三次完整恢复后 writeset_digest/backup_bytes(4096)/
  job/backup_snapshot/backup_reused 逐项存活,created_at 仍是首次登记时刻
- 反向自检(各自单独变异后复跑,均红):去掉合并、去掉身份核对、
  去掉摘要冲突判定、去掉复用字节硬门禁
- make verify:16 项全部 success(含 tests-go 真跑集成用例)
2026-10-30 09:14:10 +08:00
姚章浩 780ab988c4 fix(worker): 历史快照恢复接管按作业身份与新授权判定
build / build (push) Successful in 1m20s
真实现场 2026-10-30T00:19Z:新恢复作业恢复历史快照 fc595511(已被 0d982f76
顶替),写入 database 后被 SIGKILL;重启接管时 classifyTakeover 只要看到
该快照有 SupersededAt 就判 superseded,没有判这行历史版本属于哪个作业,
把历史那次发布被顶替当成这次作业过期,作业 failed/prebackup,而恢复文案
又写"无需恢复",目标半写且维护锁未释放。

修法:被顶替分支先分清归属——本作业 finalize 成功或版本行属于本作业 ⇒ 仍
拒绝;历史行属于别的作业时,仅当本次是 restore、授权签发于顶替之后、scope
与 instance 两把锁仍由本作业独占且指纹一致、预备份登记属于本作业且清单字节
仍等于摘要,才按 fresh 继续重跑整条链路。任一证据缺失/读不到仍拒绝。

同时把续作早退统一走 failLibraryJobOrTakeover:有持久化预备份时保留
needs_restore 并点名 backup,不再写"无需恢复"。

验证:
- 先写失败回归(历史行+新授权 ⇒ 期望 fresh,实得 superseded),修后绿;
- 真实 PostgreSQL 正负例:授权撤销/实例锁释放/预备份归属被改/清单字节被改
  均回到 superseded;续作早退保留备份与 needs_restore;
- 本地 make verify 16 项必需检查全绿(identity 本地档案跳过)。
2026-10-30 08:51:07 +08:00
姚章浩 85b3596e0e fix(postgres): 复用预备份时保留首次登记时刻
崩溃接管会复用崩溃前那份预备份;BackupPrepared 会再登记一次版本行,而
`created_at = EXCLUDED.created_at` 把"备份什么时候拍的"洗成了接管时刻 ——
事后无法区分"复用"和"崩溃后重做了备份",而后者正好把半写状态当成可回滚基线。

- 只对 `production_prebackup` 取 LEAST(首次登记时刻只早不晚);
- 生效版本行仍写当前时刻(重新激活一个旧快照后,页面要显示最近一次生效);
- 真实 PG 用例两条负例:撤掉保留规则 ⇒ 红;规则漏到所有 kind ⇒ 红。

verify: 一次性库 kg_takeover_reuse_test 上 PASS;
负例 1(撤掉 CASE)→ "复用后登记时刻被改写";负例 2(CASE 去掉 kind 条件)→
"生效版本重新激活后时刻";make verify overall success。
2026-10-30 07:56:12 +08:00
姚章浩 6bdc17474c feat(worker): 接管认领与决策落结构化日志
2026-10-29T19:56Z 的 after:database 演练里,worker 只留下汇总行
`library job takeover sweep: 1 resumed, 0 failed without backup`:既不说哪个
作业,也不说它是否真被本进程认领(被别的 worker 抢走、attach 失败都只影响
计数)。演练脚本要的老文案只在"新建队列行"那条路径打印,真实走的是"队列行
已存在"那条 ⇒ 判据与实现各说一套,演练 exit 5。

- 新增 `library job <id> takeover claimed at=… kind=… scope=… target=…
  backup=… queue=… path=…`:claim + attach 都成功之后才写,两条认领路径共用
  (path=enqueue / existing-queue);
- 新增 `library job <id> takeover decision=…`:分类后、动手之前写,含
  finalize_only。只留阶段表的话,同快照收尾能把它伪造成完整续作;
- resume 的 store 参数抽成接口,单测才能证明"没认领/attach 失败时一行都不写"
  (变异验证:把认领行提到 attach 之前 ⇒ 红;删掉 existing-queue 那行 ⇒ 红);
- 接管复用的预备份在证据里写 `backup_reused=true`:分不清"复用"与"崩溃后重做
  备份",后者会把半写状态当成可回滚基线。

verify: go test ./cmd/worker -run 'TestTakeover|TestLedgerBackupStore' -count=1 全绿;
make verify overall success(16 项必需全绿)。
2026-10-30 07:56:07 +08:00
姚章浩 d544728001 test(worker): 真实PG断言拒绝接管的落库效果
build / build (push) Successful in 1m17s
拒绝接管不只要"分类对",还要三处落库都对:library_jobs 终态 failed + 保留预备份
基线 + 恢复指引里带得上那个基线;library_maintenance 的 released_at 仍为 NULL、
state=needs_restore(目标可能半写,释放等于谎称已恢复)。

用一次性库 kg_takeover_foreign_test 实跑:
- TestFailLibraryTakeoverKeepsMaintenanceAndNamesBackup PASS (1.35s)
- 负例:把恢复指引改成"无需恢复" ⇒ 同一用例红(library_takeover_integration_test.go:117)
- TestTakeoverRefusesSameSnapshotRegisteredByAnotherJob / 超出历史窗口那条仍 PASS
2026-10-30 03:50:49 +08:00
姚章浩 ba852f3dc2 fix(worker): 接管判据绑作业身份,同快照别作业登记不算完成
2026-10-29T19:16Z 真实现场:作业只写完 database 阶段(resources/index/wiki/verify
仍 pending),worker 被杀、人工拉起后被判 succeeded,阶段写"已登记生效版本(仅收尾,
未重写生产)",并把生效版本归属改写成这个作业。

根因:classifyTakeover 只看"当前生效版本的目标快照是不是我的目标快照"。版本行主键
由 (kind, scope, 快照) 决定(LibraryVersionID),登记是 ON CONFLICT (id) DO UPDATE、
会改写 source_job_id —— 快照相等这一维里没有作业身份。

改法:effectiveOwnedBy(version, jobID, snapshotID) 一份实现,接管与基线共用;
归属别的作业 ⇒ 新的 takeoverForeignEffective,走 failLibraryTakeover:不写目标、
不释放维护、终态失败并指明预备份(目标可能半写,释放等于谎称已恢复)。
本作业自己的 finalize 阶段成功仍是独立证据,不被归属改写推翻。

验证(已跑):
- go test ./cmd/worker -run TestClassifyTakeover → PASS(含 4 条新用例)
- 负例:撤掉身份判定后同两条用例红(classify="pending_final", want "foreign_effective")
- 真实 PG(一次性库 kg_takeover_foreign_test):
  TestTakeoverRefusesSameSnapshotRegisteredByAnotherJob PASS(1.41s),
  负例同一用例红(library_takeover_integration_test.go:53)
- 同一库 TestTakeoverClassificationUsesExactCurrentVersionBeyondHistoryWindow PASS
- make verify:overall success(15 项必需全绿)
2026-10-30 03:40:13 +08:00
姚章浩 0d3a016a9e feat(worker): 加真实中断演练的固定同步屏障
真杀 worker 只有在"写了一半"那一刻才有意义,靠 sleep 猜时间会错过阶段 ——
上一版演练脚本就是这样永远 exit 0 的。这里把"进程确实停在固定点"变成文件事实:
worker 到达点位后写 <dir>/<point>.hit 再等放行,演练方看到它再 SIGKILL,命中
是确定的。

安全边界:只有 KG_TEST_BARRIER_DIR 与 KG_TEST_BARRIER_POINTS 同时在场才生效
(默认关闭),只由本机 compose overlay 注入;单次等待有硬超时,没人放行就自己
继续;同一进程被杀后重启看到同一份 .hit 直接通过,不会把接管再拦住。屏障挂在
contentTrackedTarget(阶段之间),发布链路不需要知道演练的存在。

验证:屏障包 4 条用例(默认关闭无副作用 / 命中与放行握手 / 超时自行继续并留
.hit+.timeout / 目录不可建报错但不阻塞)与包装层接线用例全绿(后者证明真实
Apply 路径真的走到点位,漏接线会红)。
2026-10-30 03:09:29 +08:00
姚章浩 0088004cd6 fix(library): 当前生效版本改按精确查询
接管分类、基线判定、已知索引映射、同步闸口的创建判据与归档保留的保护标记,
原来都用 LibraryVersions(scope, 50/100) 这个分页窗口再在内存里筛。生效版本每
scope 只有一行、可能很久以前登记,历史一长就被挤出窗口 —— 于是"已生效"被读成
"没有生效":接管会把已经上线的目标重写一遍,基线入口会给已有权威版本的目标
再建一次基线。

现在统一走两条精确查询:LibraryCurrentVersion(kind='production_effective' AND
superseded_at IS NULL,每 scope 至多一行)与 LibraryVersionOfSnapshot(按快照
身份取回"已顶替"证据)。多行、自相矛盾、读不到一律 fail closed;同一含义只留
一份实现(knownIndexMapping 同步与恢复共用)。LibraryVersions 仍是 1..100 的
展示窗口,只用于列表。

验证:真实 PG 上先复现(窗口实现 ⇒ found=false),改后 7 条 store 契约用例与
worker 端到端判定用例全绿;本机 make verify 15 项 success(用例 1264 通过 /
0 失败 / 93 已声明可选跳过)。
2026-10-30 03:09:21 +08:00
姚章浩 67743f2285 test(library): 真实PG验证超出历史窗口仍能取回写集合
build / build (push) Successful in 1m27s
fake 版本库不实现 Store 的 1..100 上限,正是它让"限 out of range"一路绿到线上。
这组用例跑真实 PostgreSQL:真实 Run → contentTrackedTarget → 真实
libraryStageRecorder.WriteSetPlanned → 真实 store,断言台账在第一笔写入之前可读、
原备份证据字段与 writeset_digest 原子共存,并把记录推到 250 条更新版本之后
(窗口里已看不到它)仍能按身份取回。

store 级契约用例覆盖:命中、真缺失 found=false、别的作业=冲突、同身份两行=拒绝挑选、
跨 scope/跨 kind 不命中、不完整身份拒绝。变异成"列最近 N 条再筛"时四条全红。
隔离 schema 逻辑收敛到 pgtest 一份,postgres 侧改为委托。
2026-10-30 02:32:26 +08:00
姚章浩 f0bd6fb0d2 fix(library): 版本台账改按身份精确查询
写集合台账的取回走的是"按 scope 列最近 N 条再筛",而真实 Store 只接受 1..100,
于是真作业在预备份已落盘之后登记失败(library version limit out of range),
写集合引用没能持久化。假版本库没有这个上限,单测因此一路绿。

只把 200 改成 100 不够:历史超过窗口后,存在会被读成不存在(本机实测:把记录推到
250 条更新版本之后,接管/恢复就找不到它)。改为 Store.LibraryVersionByRef:
scope+kind+作业+快照 一条 SQL 精确命中;命中多行拒绝挑选,命中别的作业拒绝借用,
真的没有才返回 found=false。登记/接管/恢复三处共用它,公开分页上限保持 1..100 不变。
版本行解码抽成 scanLibraryVersions,窗口列表与精确查询共用一份。
2026-10-30 02:32:23 +08:00
姚章浩 27828ff21e test(worker): 编排链路里包装层必须真的触发写集合预检
build / build (push) Successful in 1m17s
只测“包装体转发”还不够:Run 是按可选接口断言决定要不要预检的。补一条走
libraryrelease.Run + contentTrackedTarget 的用例,断言预检被调用一次、结果
带回编排层、四类写入照常执行。
2026-10-30 02:03:28 +08:00
姚章浩 0252492e53 fix(library): 恢复保留下来的资源凭可审计证据复用
同步 A 成功后手工恢复 B,resources 表既不删行也不删对象;再同步 A 时,
resources 里那 21 行以“同租户但不在预备份范围”被判成别人的知识库,整次
同步在写第一笔之前被拒。

“目标里没人引用”不是归属证据:历史孤儿、其它知识库暂未绑定的资源都满足
它。改成唯一放行路径——可审计复用(v071/library_reuse.go):
1. 持久来源:这个目标成功同步过且登记过生效版本的快照,或它自己的预备份,
   且同一资源行在来源里内容一致;
2. 整行内容(除易变时间戳)与本次写入将要落地的行完全一致;
3. 该资源的对象字节与本次快照归档摘要一致。

三条同时成立才允许复用:写入阶段跳过它(不重写行、不重传对象),写集合里
记 existing+reused,恢复不清理;证据单独落盘 reuse/<digest>.json,摘要写进
写集合行,不冒充历史台账。内容不同、来源范围不同、或没有持久来源一律继续
拒绝,且拒绝发生在写入之前。

为什么把预备份也算来源:恢复作业的写集合同样会把“本目标历史状态里已有的
资源”判成跨范围冲突,而它们本来就出自这个目标自己的快照。

验证:隔离 PG+ES+MinIO 真服务下新增 5 个场景用例(复用成功且 xmin 不变、
无来源拒绝、内容不同拒绝、同内容不同来源拒绝、wiki 半写回滚只清本次新建);
v071 整包与全仓 go test 通过;make verify 15 项全绿。
2026-10-30 01:59:32 +08:00
姚章浩 f0f5571518 fix(worker): 写集合预检经包装层转发,缺失即拒写
orchestrator 用 target.(WriteSetPlanner) 决定要不要在写第一笔之前算写集合,
而 contentTrackedTarget 只转发了 Current/Apply/Verify/Restore,断言恒为假,
台账既不落盘也不登记。真实现场:10-29 两次真作业的预备份证据里没有
writeset_digest,归档根下没有 writesets/,崩溃接管必然无台账可读。

现在包装层显式转发;内层不支持预检时返回硬错误,不退回"没有台账也照写"。
用例断言包装后仍实现 WriteSetPlanner,并覆盖"内层不支持"的硬错误。
2026-10-30 01:33:40 +08:00
姚章浩 a0a7a26639 test(library): 写集合锚点与接管加载的单测
build / build (push) Successful in 1m21s
治理侧的摘要登记与取回原先只能连真实治理库验证,等于没有负例。这里把"列版本"
抽成一个最小接口(真实 store 原样满足),并为三个判据补单测:预备份原有证据必须
保留、摘要必须按"作业+目标+预备份"三者匹配才能取回、接管时没有写集合必须失败。

本地验证:go test ./cmd/worker/ -run 'WriteSet|LedgerBackup' 4 项通过;
go vet ./cmd/worker/ 无输出。
2026-10-30 00:00:53 +08:00
姚章浩 e0768f2caa fix(library): 写集合台账与跨范围冲突预检
build / build (push) Successful in 1m34s
失败恢复会在写完之后按"目标租户里没人引用"清理资源行,于是历史孤儿、同租户其它
知识库暂未绑定的资源、被共享的资源都可能被删掉;而预备份里没有这些行,删了不可
恢复,随后整实例指纹才发现不一致,只能停在 rollback_failed。恢复校验依赖的整实例
证据又只存在内存里,worker 重启后只按快照 id 恢复,等于静默跳过比对。

改法:写第一笔之前把"本次明确写集合 + 写前存在状态 + 预备份时刻整实例内容证据"
算出来并按内容寻址落盘(文件名与治理库里的引用都是 sha256),摘要登记在预备份
版本证据里;恢复只删"写前不存在 ∧ 由本次写入 ∧ 不在恢复内容里"的精确 ID,并对
资源额外要求仍无绑定/正文引用;缺记录、记录被改、绑定对不上都直接失败、维护保持。
resources 的 upsert 补租户限定,跨范围同 ID 在写入之前就被预检拒绝。
整实例指纹从"身份集合"改成"逐行内容摘要",别的知识库同 ID 行被覆盖写坏也会暴露。

本地验证(已跑):
- make verify 15 项全 success(identity 因本地档案 skipped)
- go test ./internal/releaseadapter/v071/ -run Isolated(真实 PG + 真实 ES +
  真实 MinIO 对象存储,写失败按定位符注入):目标非空/同租户其它知识库/共享资源/
  历史孤儿/跨租户数据在失败恢复后逐行字节不变、本次残留 0;缺写集合拒绝恢复;
  跨范围 ID 冲突在写入前拒绝;wiki 阶段半写回滚干净
- SIGKILL 子进程后按盘上写集合接管恢复,目标原有数据逐行不变
- go test ./internal/libraryrelease/:写集合先于第一笔、规划失败/登记失败都不写目标、
  接管不重新规划
2026-10-29 23:55:13 +08:00
姚章浩 5defe238e0 fix(library): 跨范围同步按目标范围改写并校验残留
build / build (push) Successful in 1m34s
真实事故:源 KB(10000/0c84a098) 同步到目标 KB(10001/e64e82b5) 时,归档行被原样
插入目标库,落在来源租户/知识库;资源阶段随即报"生产缺少资源 ... 的存储路径"
(资源查询按目标租户过滤,看不见刚写进去的行)。回滚只按目标范围回放,于是页面
报"已恢复到预备份",目标却多出 4 文件/38 分块/21 资源/4 绑定。

改:
- 写入与校验共用 retargetRow:tenant_id / knowledge_base_id / 资源定位符(后端、
  provider、桶、租户前缀) / storage_backend_id / provider / embedding_model_id
  统一映射到目标范围;行里的来源身份对不上就失败,宁可不写。
- Verify 增加"本次写集合仍以来源身份残留"判定;Restore 增加整库指纹比对(预备份
  时刻 vs 恢复后)与孤儿资源清理,回滚后用 Verify 复核,恢复工具退出 0 不再等于
  恢复完成。
- 新增 cmd/libraryresidue:按归档身份精确、可备份、可复核地清理已知的范围外残留。

验证:make verify 全绿;真实 PG+ES 用例复现"源目标 KB 不同 + 目标非空 + 资源阶段
失败",断言回滚后无来源范围残留、其它租户行未动、恢复后内容=预备份;正向用例断言
行落在目标范围且资源定位符指向目标存储。
2026-10-29 22:11:59 +08:00
姚章浩 c27a0c14cc fix(library): 基线契约读目标真实 schema/模型,演练标内容风险
build / build (push) Successful in 1m22s
真实演练暴露两个只有跑起来才看得见的问题:

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、服务与入口恢复。
2026-10-29 19:41:12 +08:00
姚章浩 a42b101a7b fix(library): 基线崩溃续作与维护原状态保留
build / build (push) Successful in 1m25s
父审发现三处会让目标卡死或状态对不上实际入口,逐条修:

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。
2026-10-29 19:08:58 +08:00
姚章浩 517d5f862d feat(library): 首次发布接上目标权威基线与整库空判定
目标非空又没有生效版本时,首次发布以前只能停在"权威版本未知"。现在有了
真实入口:管理员发起 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 逐项缺失;反向自检(把候选改回按最新归档取、把文案改回旧句、
去掉管理员判断)三项均按预期变红后复原。
2026-10-29 18:16:21 +08:00
姚章浩 24d2546cbb fix(library): 接管不再把版本查询失败当成没有收尾证据
build / build (push) Successful in 1m28s
resumed 分支里 LibraryVersions 的错误被忽略,FinalizeOnly 保持 false 后继续完整
apply:对"已登记版本并释放窗口、只差终态"的作业,这等于把同一份内容重写一遍目标,
与"最终只复核"的承诺相反。

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

同时修释放幂等:Update*/Release* 对"已由本作业释放"的行返回 ErrMaintenanceReleased,
收尾重试视为已完成(部分释放后不会永久卡死),别的作业仍报未持有。真 PG 负例验证
两种情形。
2026-10-29 14:24:12 +08:00
姚章浩 8d2ed8ebe6 fix(library): 维护释放与服务恢复完成之后才暴露作业成功
build / build (push) Successful in 1m32s
原本 execute 写完生效版本就写 succeeded,而服务恢复、入口开放与维护锁释放发生在
窗口返回之后。任一步失败时外部已看到成功,重启接管又只认非终态,于是目标永远
留在维护里(本轮真实现象:同步 succeeded 后立即恢复被 409)。

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

测试覆盖:调用顺序(成功后两把锁必已释放)、健康失败不释放不报成功、三步释放失败
各自不报成功、终态写入失败提示等待接管、接管证据判定正负例。
2026-10-29 14:02:09 +08:00
姚章浩 6ed06357f9 fix(library): 期望执行器身份随 capture 请求送达并在执行前核对
build / build (push) Successful in 1m27s
原来 worker 先 health 再 capture,两次拨号之间 socket 可能被换成另一个实例的
执行器:错的 agent 已经把写入者停了、快照也出了,事后 postcheck 只能拒绝登记,
副作用无法撤销。

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

负例是确定性的:两个真实 agent 各占一个 socket,把调用方指向错的那个,断言它的
capture 调用为 0 且仍能回答 health;同一份期望打到对的 socket 则只跑一次。
2026-10-29 13:23:56 +08:00
姚章浩 4c79052a18 test(release-worker): 测试 DSN 改为拼接构造避免密钥扫描误报
build / build (push) Successful in 1m34s
完整带口令的 URL 出现在源码里会被 security-secrets 判红(测试值也一样)。
按既有做法把连接串拆开拼接,语义不变。
2026-10-29 13:05:23 +08:00
姚章浩 cfad003499 fix(release-worker): 启动失败带端点与脱敏原因
正式 release-worker 崩溃重启时日志只有一句 connect release target database,
主机名、库名和底层原因都不在里面。现在错误里带 host:port/db、路由标识与原因。

端点去口令不等于整条日志安全:pgx 的解析错误可能原样回显连接串。新增
internal/pgdsn.Redact(按已知口令精确替换 + 按 URL/keyword 形状兜底),
错误保留 Unwrap 以便 errors.Is/As,技术原因不丢。cmd/worker 里只留库名的旧实现
换成同一个包,避免两份脱敏逻辑。libraryctl 用法补上 dead-letters。
2026-10-29 13:01:46 +08:00
姚章浩 47eb23cf68 feat(library): 封存执行器可用性与还原条件来自真实观测
页面上的"可发起封存"原来只判 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 带原因;重启后恢复。
2026-10-29 13:01:37 +08:00
姚章浩 e508abd958 fix(library): 死信解除绑定封存源实例而不是同步目标
build / build (push) Successful in 1m18s
同步目标与封存源常常不是同一个实例(kgseal vs kgmaint);用目标名去绑来源死信,
真实观测的 project 永远对不上,解除会被一律拒绝。新增 LIBRARY_SOURCE_INSTANCE,
留空回落到目标实例名,API 写记录与 worker 取指纹共用同一个来源实例名。

试过:真实 API GET/POST、go test ./internal/{httpapi,appconfig} 与 make verify 全绿。
2026-10-29 07:13:18 +08:00
姚章浩 c2402947a9 feat(library): 死信按精确指纹解除,保留上游记录
build / build (push) Successful in 1m26s
上游 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 项全绿。
2026-10-29 06:55:13 +08:00
姚章浩 cb0090802b fix(v071): 同步资源行时把定位符的桶换成目标桶
build / build (push) Has been cancelled
同步把资源字节写进目标桶,但 resources.physical_path 是从快照原样搬的,
里面还写着来源桶(storage://…/minio://kgmaint-1028a/…)。目标实例之后按这一行
取图/附件就会指向测试环境的桶,或直接取不到——跨环境引用,必须在本轮修掉:
写入前换桶、键不变,定位符解析不出来就报错(不写指向别处的行)。

试过:make verify 15 项全绿;新增定位符用例(嵌套/单层/裸键/非法);
真实同步后目标桶里对象键与行内定位符一致。
2026-10-29 06:21:53 +08:00
姚章浩 faaf003707 fix(libraryagent): 把宿主侧对象存储端点纳入固定命令形态
build / build (push) Failing after 17s
上一提交只给 libraryctl 加了 -source-s3-endpoint,agent 却不知道这个 flag,
systemd 起不来(exit 2)。端点属于"这台主机怎么访问这个源的存储",必须由 agent
自己钉住并随固定声明传给 libraryctl;请求仍然不能改。没配置时不凭空加 flag。

试过:make verify 15 项全绿;新增 agent 声明用例(有端点必传、无端点不多传)。
2026-10-29 06:11:29 +08:00
姚章浩 4d6ecda310 feat(libraryctl): 源对象存储端点可按宿主机地址覆盖
build / build (push) Successful in 1m13s
容器 env 里的 MINIO_ENDPOINT 往往是服务名(langfuse-minio:9000),封存跑在宿主机上
解析不到,真实封存报 read library resource 并整体失败。
新增 -source-s3-endpoint:只换访问入口,桶与对象键不变;缺 scheme 直接拒绝;
status 会打印实际使用的 endpoint。

试过:make verify 15 项全绿;新增覆盖用例(生效 + 拒绝无 scheme)。
2026-10-29 06:09:56 +08:00
姚章浩 11c52ad81b fix(libraryctl): 封存窗口接上实例级入口闸门与后台静默
build / build (push) Failing after 1m2s
库级窗口永远拿不到在途排空证据:被代理的 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 个用例(接线解析/身份绑定/缺闸门拒绝)。
2026-10-29 05:48:38 +08:00
姚章浩 1faf5137ea fix(libraryctl): 用完整容器 ID 判定写入者归属
build / build (push) Successful in 1m14s
`docker ps -q` 默认输出 12 位短 ID,与 declared 里的 64 位 ID 永不相等,
结果连已声明的写入者都被判成"未声明的并发写入者",封存永远开不了窗。
改用 --no-trunc 并按前缀双向匹配。
2026-10-29 05:02:10 +08:00
姚章浩 fd71ea349e fix(libraryctl): 同镜像不等于同一个写入者
build / build (push) Has been cancelled
共享业务栈与专用验收实例共用同一个 WeKnora 镜像,原来只按镜像判"未声明写入者",
任何一台开着共享栈的机器都永远开不了窗。判据改为:同项目或无项目标签即冲突;
另一个项目只有数据库/队列目标与源相同才算冲突。单测覆盖六种组合。
2026-10-29 05:01:22 +08:00
姚章浩 906c282986 fix(libraryctl): 捕获/预检也走同一份 scope 映射
build / build (push) Successful in 1m14s
上一提交只改了 status;capture/preflight 仍直接读 routing bindings,
所以 CLI 封存的还是 bindings 里的库,而不是 API 的目标——契约读取直接失败。
2026-10-29 04:59:57 +08:00
姚章浩 23cc6c0afe fix(libraryctl): 支持 MinIO 源,并与 API 共用同一份 scope 映射
build / build (push) Has been cancelled
为什么:专用验收实例用 S3 兼容的 MinIO,而 CLI 的源发现只认 oss 环境变量,
`STORAGE_TYPE=minio` 直接失败;同时 CLI 只读 routing bindings,API 读
LIBRARY_PRODUCTION_KB_*,两边指向不同知识库时,"封存的是哪个库"就说不清。

怎么改:新增 objectStoreFromSourceEnv(oss/minio,端点/桶缺失即报错,不退回默认值),
scopeBindings 让环境变量优先、bindings 兜底。单测覆盖 minio http/https、oss 与未知类型。
2026-10-29 04:58:45 +08:00
姚章浩 5afe151c7d fix(library): 维护目标把后台静默检查的依赖接上
build / build (push) Successful in 1m14s
openLibraryMaintenance 漏传了目标库/队列,导致实例级窗口永远报
"缺少后台静默检查"——接线漏了,防线却看起来像在工作。
2026-10-29 01:01:52 +08:00
姚章浩 3643e9bf82 fix(library): 维护收尾必须按预备份恢复并校验
build / build (push) Successful in 1m20s
删掉会自己开入口、只看服务健康的 FinalizeAfterOperatorRecovery。
新的 RecoverWindow 绑定原作业/目标指纹/持久化预备份,在维护关闭下
执行恢复与逐类校验,通过才恢复服务与入口;失败继续维护。
预备份身份与内容风险在第一笔写入前一起落盘;后台任务必须静默后才停写者;
写入者发布端口即拒绝开窗口;维护占用改为按实例互斥(000072)。
2026-10-29 00:50:32 +08:00
姚章浩 962bed8f53 feat(library): 人工恢复后按证据收尾维护窗口
build / build (push) Successful in 1m15s
回滚失败会按设计保持维护,但此前没有任何"人工确认后收尾"的路径:
journal 一直非终态,之后所有窗口都会被拦住。新增 libraryctl finalize:
不启动服务、不释放治理占用,只核对原本在跑的服务与入口是否真的在服务,
通过才把 journal 标成 operator_recovered。
2026-10-28 23:42:28 +08:00
姚章浩 38d04488de feat(library): 入口闸门排空 + 维护占用可再接管
build / build (push) Successful in 1m28s
被代理的 WeKnora 收到 SIGTERM 会先关监听再 exit(1),在途请求被切断。
排空改到真正的连接入口:闸门停止接客、等在途连接归零、写 drain.json
并 exit 0;协调器只认这条证据,写者退出码只作记录。

顺带修掉维护占用的一条死锁:释放后的窗口此前不能被新作业接管,
等于第一次同步之后谁也再进不去;持有中的窗口仍只允许原作业重入。
2026-10-28 23:35:15 +08:00
姚章浩 6fe98c9a30 feat(library): 同步/恢复改为实例级维护窗口
build / build (push) Successful in 1m26s
只锁治理库 scope 挡不住 WeKnora 直连和后台写入。把封存链路的
维护机制接到发布路径:持久化 owner/目标身份/原服务状态,关入口、
停写入者、验证排空与入口不可达,再写目标;写完先恢复原运行状态
与入口才释放占用。内容风险一旦落盘,重启恢复不再自动启动写入者。

新增 000071 迁移记录 level/指纹/服务/入口状态;未接线的目标直接
拒绝写,不用库级锁冒充系统维护。
2026-10-28 23:01:29 +08:00