Commit Graph

201 Commits

Author SHA1 Message Date
姚章浩 d0c6d57283 fix(storage): 按提供方选择整库对象寻址方式
build / build (push) Successful in 1m23s
2026-10-31 06:22:20 +08:00
姚章浩 ee9d078794 fix(release): 分配目标分块序列并保持原值恢复
build / build (push) Successful in 1m26s
2026-10-31 06:08:05 +08:00
姚章浩 286f1fb375 fix(maintenance): 避免健康探针连接阻塞入口排空
build / build (push) Successful in 1m23s
2026-10-31 03:17:45 +08:00
姚章浩 56f8f4fd7e fix(library): 统一空目标概览与首次同步准入
build / build (push) Successful in 1m41s
2026-10-31 02:42:01 +08:00
姚章浩 7ab2eb1ccf fix(library): 按目标身份隔离版本授权与执行历史
build / build (push) Successful in 1m47s
2026-10-31 02:15:15 +08:00
姚章浩 323f9ae502 fix(handoff): 在执行器容器内读取云端归档清单
build / build (push) Successful in 1m23s
2026-10-30 22:45:25 +08:00
姚章浩 52c0186172 fix(handoff): 修复搬运默认上限及云端接收目录挂载
build / build (push) Successful in 1m29s
2026-10-30 22:37:18 +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
姚章浩 afe9affc2a style: gofmt 新增的三个文件
build / build (push) Successful in 1m27s
为什么:远端 CI 的格式门在推送后判红(run 258,job build,format=failure),
本地 `make verify` 那次跑的是"拉取前"的文件集,漏掉了随后新增/改写的三个文件。
失败项:internal/httpapi/library_intent_test.go、internal/jobs/lease_loss_test.go、
internal/platform/postgres/library_protection.go。

怎么验:`gofmt -l internal/ cmd/` 输出为空;`python3 scripts/check_format.py` rc=0
(Go:✅,前端:✅)。只改空白,不改逻辑。
2026-10-30 16:16:45 +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
姚章浩 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
姚章浩 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
姚章浩 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
姚章浩 402173263e fix(library): 预备份与归档类型按版本登记显示
两处契约错:prebackup 只认"与拟发布快照同内容的那一份",而恢复作业登记的是
恢复前备份(必然不同),于是刚备份完就显示"尚未创建";归档里同时有目标基线与
每次预备份,overview 不给登记类型,页面只能把每份都写成"测试封存"。

现在 prebackup 取最近一次已登记的那份,并给出所属作业与 bound_to_target;
overview 新增不含 evidence 的轻量版本清单供页面标注每份快照。闸口判定不变。
2026-10-30 01:33:50 +08:00
姚章浩 72ea9726e7 fix(library): 维护窗口起始时间只属于本次占用
acquired_at 没进 ON CONFLICT DO UPDATE,于是第一次建立基线写下的时间再也不会变:
恢复作业 16:59 才开窗口,页面却显示 13:37。库级与实例级是两份 SQL,都要改成
"同一作业重入(崩溃接管)保留旧值,新作业接管用本次时间"。

真实 PostgreSQL 用例覆盖两者:首开、同作业重入、释放后新作业接管、持有期间他人被拒。
2026-10-30 01:33:45 +08:00
姚章浩 168fc2ecb7 fix(release): 对象存储四个入口共用同一定位符解析
真实资源路径是 storage://<backend>/<provider>://<bucket>/<key>。Open/Put 用
validObjectKey 并拆出裸 key,而 Stat/Delete/Copy 用只认单层的 validObjectLocator
且把整串当对象键——对象在桶里却校验读不到。集成用例只喂裸 key,把这条不一致
藏了过去;写集合用例里那句"读不到也算通过"同样把断言关掉了。

现在四个入口共用 validObjectKey + providerPathFromLocator;集成用例喂真实定位符,
负例覆盖键不存在 / provider 不符 / 路径穿越 / 复制源不存在,并验证 Delete 后再读必失败。
2026-10-30 01:33:45 +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
姚章浩 c14dd01f08 fix(library): 封存读资源失败带上资源行与底层原因
build / build (push) Successful in 1m18s
真实封存卡在 `read library resource` 时没有任何可判断信息:对象缺失、桶写错、
宿主机解析不到端点都会走到同一个字符串。现在错误带资源行 id 与底层原因
(不含凭据),本轮就是用它在候选栈定位到"源对象被上一次 B02 演练删掉后没真正写回"。
2026-10-29 13:32:55 +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
姚章浩 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
姚章浩 c542a1333a fix(library): 资源行的存储后端列也改到目标实例
build / build (push) Successful in 1m17s
原版应用先按 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 全绿。
2026-10-29 07:08:39 +08:00
姚章浩 1aecd9d6af fix(library): 资源定位符同时改到目标存储后端
build / build (push) Successful in 1m16s
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 项全绿。
2026-10-29 07:04:45 +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
姚章浩 a6406209bd fix(httpapi): 恢复作业 id 改成维护窗口可接受的形状
build / build (push) Successful in 1m18s
librestore-<scope>-<时间戳.微秒> 带 ".",冻结协调器的 job 白名单只收
[A-Za-z0-9_-],所以每次恢复都在开窗口时被拒(invalid freeze request),
作业随后被接管扫成失败。改纳秒整数,并加测试锁住该约束。

试过:make verify 15 项全绿;真实恢复复现该错误后修复。
2026-10-29 06:28:29 +08:00
姚章浩 b061a9686c fix(v071): 资源换桶的变换在校验时同用
build / build (push) Successful in 1m17s
只改写入侧会让"行内容摘要"对不上:校验拿变换前的归档行与目标里的变换后行比较,
真实同步在 verify 阶段报"生产 resources 内容与快照不一致"并回滚。
把行变换抽成 rowTransform,写入与校验共用同一份。

试过:make verify 15 项全绿;真实同步复现该失败后修复(下一轮重跑验证)。
2026-10-29 06:24:14 +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
姚章浩 7d4ffdbfe5 fix(v071): 全新目标实例的空索引按空基线处理
build / build (push) Successful in 1m15s
全新 WeKnora 会用空映射({})先建好索引,范围字段等第一次写入才动态生成。
首次同步在读目标当前状态时就报 "missing release scope fields" 并失败,等于
"往空的生产实例做第一次同步"这条路根本走不通。

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

试过:make verify 15 项全绿;新增空/非空两种索引的用例;真实 sync 复现原错误。
2026-10-29 06:17:26 +08:00
姚章浩 84e8a0dfdc style(libraryagent): gofmt
build / build (push) Successful in 1m14s
上一个提交漏跑 gofmt,本地门禁 format 失败;只改空白。
2026-10-29 06:12:46 +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
姚章浩 f2d6f3f646 fix(httpapi): 封存队列作业 id 按 jobs.id 列宽拼短
build / build (push) Successful in 1m17s
真实请求 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)。
2026-10-29 06:05:16 +08:00
姚章浩 37a8370f7b fix(v071): 索引范围过滤接受未写 tenant_id 的文档
build / build (push) Successful in 1m15s
真实 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 条)。
2026-10-29 06:00:30 +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
姚章浩 1f482e4b49 feat(library): Wiki 撤销按贡献比对,维护窗口在页面如实呈现
build / build (push) Successful in 1m18s
为什么:删除一篇与别人共同贡献同一 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 项,无未运行项)。真实撤销语义验收见交付报告。
2026-10-29 04:45:34 +08:00
姚章浩 0ef79846e8 fix(library): 后台静默必须在关入口之后证明,并在停写者后复核
build / build (push) Successful in 1m19s
原顺序先证明后台空、再关入口排空:排空期间还在服务的在途请求会派生新任务,
那一刻的空到停写者时已经失效,而 app 收到 SIGTERM 会 exit(1) 把任务腰斩。

现在固定为:关入口并排空 → 证明后台静默(连续为空 + 稳定窗口 + 任务指纹)→
停写入者 → 停完再复核(队列非空/指纹变化即判 background_truncated)。
scheduled/retry 自行产生任务或任务卡住时 fail closed;旧 journal 的
background_idle 布尔不再当证明;入口关到一半失败时恢复入口并保持维护。
2026-10-29 04:02:59 +08:00