姚章浩
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
姚章浩
a50cd73423
fix(deploy): 校验云执行器授权配置与稳定启动
build / build (push) Successful in 1m21s
2026-10-31 04:26:54 +08:00
姚章浩
286f1fb375
fix(maintenance): 避免健康探针连接阻塞入口排空
build / build (push) Successful in 1m23s
2026-10-31 03:17:45 +08:00
姚章浩
3e4020c327
fix(drill): 支持空目标并在探针写入前落盘恢复证据
build / build (push) Successful in 1m21s
2026-10-31 02:56:44 +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
姚章浩
657fd251fb
fix(deploy): 显式启动发布worker并声明受控运行身份
build / build (push) Successful in 1m21s
2026-10-30 20:48:00 +08:00
姚章浩
468ae64918
fix(deploy): 修正云执行器套接字与凭据挂载契约
build / build (push) Successful in 1m19s
2026-10-30 20:37:18 +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
姚章浩
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
姚章浩
4f0852a94d
fix(ci): 契约门同时核进程退出码与负例归因
...
为什么:版本契约门只看 `--- PASS: X` 两行,rc1 取到却没用。同一进程里别的测试
失败时 go test 仍会打印这两个 PASS 行、退出码非 0——那种"绿"证明不了这一轮测试
成功,负构建因编译/网络错误非 0 也会被当成"断言还活着"。审计 F19 的原始触发即此。
改了什么:判据抽成 `judge_contract_positive` / `judge_contract_negative`,正向要求
rc=0 + 每个必需测试真 PASS + 无 FAIL 事件 + 未跳过;负例要求非 0 且失败在预期断言上,
命中编译/网络/工具链标记即判红。判据的反例自检 `contract_judgement_problems()` 同时在
`make verify` 的 contracts 步骤与 `--self-test` 里跑,判据被改坏在本地就红。
POLICY_REVISION 提到 ci-checks/4。
怎么验:`python3 scripts/ci_checks.py --self-test` rc=0(10 汇总分支 + 10 契约反例);
`--internal-contracts $(command -v go)` rc=0;反例探针(假 go 打印两个 PASS 后 exit 1)
现在 rc=1,修复前同一输入 rc=0。
2026-10-30 16:14:46 +08:00
姚章浩
c2ab658033
docs: 记录写集合证据被整份覆盖的陷阱
...
build / build (push) Successful in 1m19s
新增 trap 20:复用预备份时重新造一份证据,会让已登记的写集合锚点被整份
抹掉(现场 backup_bytes:0 + writeset_* 全丢)。写明症状、根因、负例优先的
判据与修法,含预备份版本行「一条只属于首次登记的作业」这一归属语义。
AGENTS.md 索引表那一行受写入保护、需主人批准,本次未改(见交接单待批准项)。
2026-10-30 09:14:14 +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
姚章浩
c57c22c65e
docs: 记录接管证据陷阱与判据变化
...
build / build (push) Successful in 1m34s
- traps §19:汇总日志与阶段文案都不是接管证据;认领/决策行必须带作业 id 且晚于
杀点;prebackup 允许 succeeded,但要证明复用同一份而不是重做;
- traps 索引第 19 行待主人批准(AGENTS.md 受写入保护),草稿在
docs/agents/traps-index-pending.md;
- 验收矩阵:S04-b 记"写链路已实测、脚本级验收未成立",并补一节
"这一轮到底验到了什么/还没成立什么"。旧 FAIL 证据不改写。
verify: make verify overall success(16 项)。文档不含会漂移的统计数字。
2026-10-30 07:56:20 +08:00
姚章浩
8a3aa800a3
fix(scripts): 演练判据改用认领与决策行并校验预备份复用
...
v4 判据(都带作业身份,且时间戳必须晚于杀点):
- `scripts/takeover_log_contract.py` 是唯一判据:认领行 + 决策行都要有本作业 id、
target/backup 一致;只有汇总行、别的作业的认领行、杀之前的旧日志三种一律红;
`--self-test` 11 条(正例 2 / 负例 9)进 `make verify`(新增第 16 项必需检查);
- `prebackup` 不再强制 `skipped`:真实接管是复用后重跑整条链路,阶段记 succeeded。
但**不取消备份校验**,改为证明复用同一份——快照 id、清单字节与 mtime、
版本行首次登记时刻都不变,且证据里有 `backup_reused=true`;
- 杀之前把预备份清单 sha256/mtime 与版本行登记时刻拍进 evidence;
- summary.json 增加 takeover_decision / claim_at / backup_registered_at。
verify: 判据自检 11 条全过;用真实 Go 输出行形状的正例日志 rc=0;旧形状(只有
汇总行)rc=2 且理由点名"汇总";`--check` 前置体检 13 项全 pass(含新增
preflight_log_contract);make verify overall success。
2026-10-30 07:56:16 +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
姚章浩
4a70c2a848
fix(scripts): 杀之前等 docker events 监听就位
...
build / build (push) Successful in 1m17s
SIGKILL 与事件监听抢在同一秒时,die 事件可能早于 attach 被错过,症状是误报
"没有 exitCode=137 证据"(假红)。加 1s 等待,并用注释写清它只会假红、不会假绿。
2026-10-30 03:46:52 +08:00
姚章浩
d0fe257531
docs: 记录接管身份陷阱与演练判据变化
...
build / build (push) Successful in 1m20s
trap-18:快照相等 ≠ 本作业完成。写清真实现场(只写 database 阶段却被判成功)、
根因(版本行主键由 kind+scope+快照决定,登记是 upsert 会改写 source_job_id)、
判据(负例优先:内存 + 真实 PG 两个方向)与修法(effectiveOwnedBy 一份实现,
foreign_effective 走拒绝接管出口)。
AGENTS.md 是受保护文件,索引表那一行没有落盘:批准稿放在
docs/agents/traps-index-pending.md,列为已知未完成项。
CHANGELOG 记录本轮 Fixed/Changed 两条。
2026-10-30 03:40:23 +08:00
姚章浩
1d70059d32
fix(scripts): 中断演练改用进程崩溃语义并按快照推导期望
...
上一轮为什么空等 420s:docker kill 走 Docker API 的"手动停止",unless-stopped
也不拉起,容器一直躺着,最后由人工 docker start 才接管 —— 那不是崩溃自恢复。
改法:
- 命中后从宿主机向容器 PID1 发 SIGKILL(同 uid 才允许,先证明能杀);退出码证据取
docker events 的 die exitCode=137 —— restart 后 State.ExitCode 会被重置成 0,
事后 inspect 读不到 137。
- 限期等 restart policy 拉起(同容器/同镜像才算);策略没起来才显式 docker start,
并记 restart_source=manual_start,绝不当自动恢复。
- 期望内容按目标快照 manifest 推导(knowledges/chunks/wiki_pages),resources 不进
默认期望(同租户其它知识库资源会保留,trap-16)。
- --kind sync|restore + --snapshot 强校验;目标快照等于中断前生效版本时**杀之前
就拒绝**(同快照收尾不能当完整续作验收)。
- 新增 verify_stages_complete:database/resources/index/wiki/verify 全部 succeeded
才算完整续作(上一轮这四个还是 pending,却被判成功)。
- 独立写集合校验器缺失即 fail(旧版记 canonical=not_run 还 pass)。
- save_evidence 读实例维护用 owner_scope(旧版读 scope 列,SQL 报错被吞)。
验证(已跑):
- bash -n 通过;--check 全绿(READY_FOR_PARENT,含 preflight_verifier)
- 负例:WRITESET_VERIFIER=/nonexistent → preflight_verifier fail / exit 2
- 期望推导:fc5955→4/38/31、0d982→1/2/9、0fea→4/38/31;不存在的快照 → exit 1
- 宿主 SIGKILL 能力实测(scratch 容器,非本栈):kill -9 PID1 → 策略自动重启,
RestartCount 0→1,docker events 记 exitCode=137
2026-10-30 03:40:18 +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
姚章浩
a19cf819fc
fix(scripts): 修演练脚本的撤注入与 restore 自锁
...
build / build (push) Successful in 1m17s
两处实测出来的缺陷:
- --restore 也走完整前置体检,而体检里"不许已经带注入"这一条会把它自己锁死
(上一轮没收尾时它就永远跑不了)。restore 只校验 docker/worker/归档根。
- 注入用的 overlay 被 compose 记进容器的 config_files 标签,restore 时原样重放
等于把注入又加回去(实测 KG_TEST_BARRIER_DIR 撤不掉)。按文件名排除演练 overlay。
另外演练中断(not_run/接管失败)时明确提示"屏障仍在注入,先 --restore",
不让父以为栈已经干净。
2026-10-30 03:13:35 +08:00
姚章浩
465cae80bd
docs: 记录"窗口当索引"陷阱与本次改动
...
build / build (push) Successful in 1m26s
trap-17:分页窗口当索引 —— "当前生效版本"不能按最近 N 条找。它比 trap-14 更
隐蔽:读成"没有"不报错,只是让错误的选择看起来合理(重写已上线的目标、给已有
权威版本的目标再建基线)。写清判据(250 条历史 + 窗口里看不到 + 精确查询仍命中)
与修法(身份/当前状态一律精确查,窗口只用于列表)。AGENTS.md 的陷阱索引同步加
一行;CHANGELOG 记录当前生效版本查询修复与固定同步屏障;test-scope 声明新增的
真实 PG 用例为"可选未运行"并写清理由(未声明 skip 判红)。
2026-10-30 03:09:37 +08:00
姚章浩
ed307f3c20
feat(scripts): 真中断演练脚本改成父可直接执行
...
旧 kill-takeover.sh 的判据自相矛盾:要求"作业已 running 且 ledger 齐"才启动,
可它又要先启动再等作业;维护窗口里要求业务 health 200(那时 app 是被有意停掉
的);没杀到任何进程也 exit 0。
这一版把"做成"定义清楚:真 SIGKILL 真容器 → 同一作业被接管 → 目标按期望内容
恢复 → 服务/入口恢复,四条全成立才 exit 0;missed/超时/接管失败各有非零退出码
并写成 not_run。流程:前置体检(目标拓扑、无在飞作业、无未释放维护窗口、镜像
里真的带屏障代码、只允许本机栈)→ 注入屏障 → ARMED 后由人在页面发起作业 →
看到 .hit 锁住这一个作业 → 读回写集合台账(文件名/自述摘要/作业绑定,可选独立
校验器)→ SIGKILL → 等同一作业接管 → 核对 prebackup:skipped、生效版本、写集合
摘要不变、目标计数、维护释放与容器恢复 → 撤掉注入。每一步有界,不做后台 sleep
轮询假装工作。
2026-10-30 03:09:33 +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
姚章浩
18683ca63c
docs(agents): 记录资源范围冲突与可审计复用判据
...
build / build (push) Successful in 1m26s
trap-16:resources 是多知识库共享表,“不在预备份范围”不等于“属于别的
知识库”;恢复保留下来的行是本目标上次成功同步的结果。判据、修法与反例
写进全文,索引表加一行。
2026-10-30 01:59:35 +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
姚章浩
cf3f4662cc
docs: 记录写集合与定位符陷阱并归档真作业证据
...
build / build (push) Successful in 1m25s
两条陷阱都是这次真作业里查出来的,且都被"只测内层/只喂裸 key"的用例盖住过:
包装层丢掉可选接口导致写集合静默跳过;对象存储四个入口吃不同定位符形状。
docs/evidence/2026-10-30-real-sync-restore 只放脱敏结果:作业/阶段/版本/维护窗口、
22 个资源与桶内字节、归档三处 sha256 对拍、以及"归档根下没有 writesets/"这个事实。
2026-10-30 01:34:41 +08:00
姚章浩
74fb04b2d1
fix(web): 整库页按作业类型与终态显示并补全轮询
...
- 成功的作业不再写"尚未启动/此时尚未运行",阶段条对恢复/基线也读自己的阶段行;
- 基线/恢复不再被写成"生产同步""封存进行中",改为逐类映射;
- 抽屉按版本登记标注每份归档,未登记的不冒充候选;
- 轮询覆盖投递→终态读回:投递中动作、封存队列、任一 scope 在飞作业都算在跑,
读到终态再读回一次就停,失败作业不会被无限轮询。
2026-10-30 01:33:51 +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
姚章浩
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