Commit Graph

91 Commits

Author SHA1 Message Date
姚章浩 a98828dc4a fix(library): 续作在事务内校验阶段并原子入队
build / build (push) Successful in 1m29s
2026-11-01 22:05:02 +08:00
姚章浩 e292b6f674 fix(library): 续作改为单事务入队,回滚作业转维护恢复
build / build (push) Successful in 1m30s
2026-11-01 21:51:43 +08:00
姚章浩 044fb37b2c style(test): gofmt 补文件结尾换行
build / build (push) Successful in 1m31s
2026-11-01 19:35:30 +08:00
姚章浩 e34306b2db fix(ui): 已释放窗口不再渲染 held 文案,版本页不再把 500 说成未接线
build / build (push) Failing after 18s
发布管理:maintenanceSummary 只看 job.state === 'failed' 就给 held 处置文案;
窗口已 released 的旧作业(libsync-internal-1793399248431728863)因此自相矛盾。
改为仅 held 时给 recoveryError,已释放时给 releasedFailure(如实记录失败,不提供强制成功)。

版本记录:LoadVersionHistorySyncState 把可空的 finished_at 扫进 time.Time;
部署重启触发的同步在"进行中"时该列为 NULL,扫描失败使整页 500
(nginx access log:GET /api/v1/version-history?limit=30 → 500,74 字节错误体)。
改为指针接值再判空。前端同时区分"未配置/加载失败"并显示服务端 code。

RED/GREEN:新增 Go 集成用例(NULL finished_at)、两条前端用例;
make verify 16/16 通过(local 档案)。
2026-11-01 19:34:31 +08:00
姚章浩 8d838d9cb4 feat(library): 整库发布身份、写集合与收尾边界
build / build (push) Successful in 1m45s
执行时真实时钟与持久授权回读(撤销/过期即停并保留预备份)、写集合写前落盘、
租约与 attempt 身份、恢复队列与原发送身份分离、成功收尾必须释放维护后再登记终态。
2026-11-01 11:35:10 +08:00
姚章浩 8febe12d7d test(library): 隔离数据契约与 r11 身份自然键矩阵
真实本机 PG/ES/MinIO:v071 目录镜像 10 模式、结构引用/游标 15 模式、
8 表×2 外部范围身份矩阵、13 模式自然键(含软删除与合法同 parent 正例)、
备份期间三注入、收尾故障与授权撤销两个真实治理 PG 用例。

需隔离服务的用例在 test-scope.json 显式声明:普通 CI 跳过不算通过。
2026-11-01 11:35:09 +08:00
姚章浩 7ab2eb1ccf fix(library): 按目标身份隔离版本授权与执行历史
build / build (push) Successful in 1m47s
2026-10-31 02:15:15 +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
姚章浩 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
姚章浩 72ea9726e7 fix(library): 维护窗口起始时间只属于本次占用
acquired_at 没进 ON CONFLICT DO UPDATE,于是第一次建立基线写下的时间再也不会变:
恢复作业 16:59 才开窗口,页面却显示 13:37。库级与实例级是两份 SQL,都要改成
"同一作业重入(崩溃接管)保留旧值,新作业接管用本次时间"。

真实 PostgreSQL 用例覆盖两者:首开、同作业重入、释放后新作业接管、持有期间他人被拒。
2026-10-30 01:33:45 +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
姚章浩 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
姚章浩 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
姚章浩 f055fc174d style(library): gofmt 恢复重排文件
build / build (push) Successful in 1m19s
2026-10-29 01:38:46 +08:00
姚章浩 b007acd443 feat(library): 恢复失败后可以重新排队,不必手工改库
build / build (push) Has been cancelled
失败作业的幂等键把恢复入口锁死,运维修好故障也点不动第二次。
现在按窗口重排失败作业(同窗口有活作业则拒绝并发),CLI/API 都能真恢复。
2026-10-29 01:38:39 +08:00
姚章浩 3643e9bf82 fix(library): 维护收尾必须按预备份恢复并校验
build / build (push) Successful in 1m20s
删掉会自己开入口、只看服务健康的 FinalizeAfterOperatorRecovery。
新的 RecoverWindow 绑定原作业/目标指纹/持久化预备份,在维护关闭下
执行恢复与逐类校验,通过才恢复服务与入口;失败继续维护。
预备份身份与内容风险在第一笔写入前一起落盘;后台任务必须静默后才停写者;
写入者发布端口即拒绝开窗口;维护占用改为按实例互斥(000072)。
2026-10-29 00:50:32 +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
姚章浩 d1dbb2b945 style: gofmt library_maintenance.go
build / build (push) Successful in 1m21s
2026-10-28 20:47:58 +08:00
姚章浩 288e01a01a feat(library): 目标维护窗口(持久所有权/独占/release-on-verify)+ known-broken 记录
build / build (push) Failing after 16s
为什么:用户确认维护窗口方案,需要真实占有目标而不是 503 页面。

- 迁移 000070 library_maintenance:scope 唯一持有者、作业/目标身份、服务原状态、阶段。
- worker 在写目标前 acquire,验证+登记成功才 release;回滚失败/未验证保持 needs_restore。
- API GET /library/maintenance + 同步入口在他人持有时 409。
- 已知缺陷如实记录:候选实例该路由 404;kill 取证脚本竞态导致三项断言为 false。
2026-10-28 20:45:02 +08:00
姚章浩 c72b71d78a fix(library): 预备份硬门禁 + 崩溃接管 + 五项入口验收
build / build (push) Failing after 17s
为什么:父审指出预备份引用持久化失败仍继续写目标、接管依赖启动扫描且漏掉租约未过期、
恢复失败被标成"无需恢复"、版本登记与作业状态分叉。

- BackupPrepared 返回 error,持久化失败即零写入;预备份同时登记 production_prebackup 版本。
- 接管改为按队列租约判定 + 运行期 30s 扫描 + 原子抢占 + 固定幂等键 + attempt 归零。
- 接管场景保留预备份、不再宣称"未写无需恢复"。
- 恢复/重放同一快照重新激活 production_effective(清 superseded_at)。
- finalize 续作只做校验+登记(finalize_only),不重写生产。
- 索引映射基线缺失时采用首次观察值并登记;索引校验加孤儿文档检查(按 knowledge_id)。

真实验收(候选 API :18089 + 真实 PG/ES/MinIO):
A0 首次发布、A1 撤销、B 按版本恢复、C 写阶段 SIGKILL 接管、
D 跨 kind 互斥与多 worker 单次执行、E1 登记失败、E2 finalize 续作、E3 预备份失败零写入。
2026-10-28 19:11:37 +08:00
姚章浩 ac3b395102 fix(library): 对齐真实 WeKnora Schema + 入口级真实服务验收
build / build (push) Successful in 1m21s
为什么:候选环境改用真实 WeKnora 表结构后,暴露出多处"按猜的列名/形状"的假设,
以及若干 varchar 长度、对象定位符、空索引基线问题,导致真实链路跑不通。

- knowledge_tag_relations 无 tenant_id/KB 列 → 归属走父表;resources 作用域=绑定+chunk 引用。
- 写入顺序按真实外键;无代理键表用内容摘要核对。
- 队列作业 ID / 版本记录 ID 改固定长度摘要(列宽 varchar(36)/varchar(64))。
- 对象定位符拆 storage://<backend>/oss://<bucket>/<key>;provider 可配置。
- 空索引基线全链路一致(Current/Export/Backup/Archive),非空仍严格比对映射。
- finalize 阶段:版本登记失败不报成功;崩溃接管复用已持久化预备份。
- 迁移 000069:同一目标的 sync/restore 跨 kind 互斥。
2026-10-28 17:53:43 +08:00
姚章浩 a2d051f6b3 feat(library): 按版本恢复入口与失败作业续作
build / build (push) Successful in 1m25s
为什么:恢复此前只有"失败自动回滚",没有手动按版本恢复;失败作业也只能重发封存。
- 迁移 000068 允许 kind=restore;POST /library/restores(管理员 + 该快照恢复授权 + 闸口)。
- POST /library/jobs/{id}/resume 对失败/取消的同步/恢复作业按原参数续作。
- worker 统一 runLibraryRelease,同步与恢复共用链路;成功后登记 production_effective 版本。
2026-10-28 16:27:46 +08:00
姚章浩 7b8cc0384b feat(library): 生产发布授权与同步/恢复编排,worker 接线
build / build (push) Successful in 1m19s
为什么:同步入口此前只会拒绝;授权、预备份、四类写入、验证与失败恢复都没有代码。
本轮把编排与授权实现出来,真实生产执行仍按授权闸口关闭。

- internal/libraryrelease:预备份 → 数据库/资源/索引/Wiki → 验证 → 失败恢复;
  授权过期/目标不符/快照不符/参数变化在动生产前拒绝;恢复失败单独上报。
- 迁移 000067 library_release_authorizations(目标+快照+参数摘要+有效期,可撤销)。
- POST/GET /api/v1/library/authorizations 与 /{id}/revoke,仅管理员签发,5–1440 分钟。
- worker library.sync 接编排:先闸口再授权,阶段落库,只有编排成功才标成功。
- 生产目标适配器未配置时显式失败(不返回零值状态),预备份存储同。
- overview 返回授权状态,页面"生产保护与恢复"显示签发人与有效期。
- 验收矩阵区分"已实现·隔离测试通过"与"真实执行未授权"。
2026-10-28 15:11:27 +08:00
姚章浩 e313ae12f2 feat(library): 真实差异比对 + 孤儿作业回收 + 验收矩阵与台账
build / build (push) Successful in 1m18s
为什么:发布管理的差异一直显示"未接入",业务作业也可能因队列丢作业而永久占锁;
本轮把这两件做成真实实现,并把 30 个验收项与 CI/版本身份落成可审计文档。

- internal/librarydiff:稳定文档 ID + 排除易变列的整行摘要做增改撤销;overview 在
  生产权威版本与测试封存都在时给出 computed 差异并标注两侧快照,缺一侧仍是 unknown。
- 发布管理"本次变更"列文档身份与比对版本;无差异时明确写"没有文档差异"。
- ReconcileLibraryJobs:worker 启动清理队列已无对应作业的孤儿,避免永久互斥。
- docs/library-acceptance-matrix.md 与 docs/release-ledger.md 固定证据与版本事实。
2026-10-28 14:39:17 +08:00
姚章浩 e3db157ba8 test(postgres): 迁移清单跟随 000066 更新
build / build (push) Successful in 1m17s
为什么:新增 000066 整库作业迁移后,清单测试仍写死 65 条,CI tests-go 因此红。
不只改计数,同时断言 66 建出 library_versions 与 library_jobs_active 互斥索引。
2026-10-28 14:33:55 +08:00
姚章浩 dc9e827656 feat(library): 整库版本/作业持久化与状态读取契约,生产同步默认拒绝
build / build (push) Failing after 17s
- 新增 library_versions:测试封存、生产生效、生产预备份三类分开登记,
  生产生效版本唯一(旧版本必须先 supersede),测试快照不能顶替生产版本
- 新增 library_jobs + library_job_stages:业务作业身份、阶段、错误与恢复说明,
  分项计数用 NULL 表示"总量未知";互斥与幂等由数据库唯一索引保证
- 新增 GET /api/v1/library/overview、/jobs、/jobs/{id};POST /syncs 闸口未齐
  返回 503 缺项且不建作业
- 封存作业落库:先建业务作业再入队,worker 推进 export 阶段并登记 test_sealed
- 发布管理读真实状态:生产未知不再顶替、差异 unknown、启动同步按缺项禁用、
  同步明细/四类分项来自持久阶段;阶段勾选只认当前作业证据
- 准备信息标注封存时间与范围,不声称实时测试库已就绪
2026-10-28 13:58:51 +08:00
姚章浩 bb64c32654 feat(release): 发布管理整库视图 + R4/R5/R6/R7 修复
build / build (push) Failing after 16s
- R4:封存子进程 SIGTERM 宽限停、libraryctl 信号转 ctx;失败/超时后 agent 再做
  有界 recover,恢复失败即降级阻断封存并给出原因;启动先恢复、关闭等恢复完成
- R5:封存闸口以已存 payload 为准,同作业号换范围 409 且不入新队;重放回显真实状态
- R6:作业列表按类型查询(ListByType),不再先取全局再过滤
- R7:根目录 libraryagent 二进制移出版本库,保留运行产物并精确忽略
- UI:发布管理只保留对内/对外两目标,整合快照/完整性/作业/存储;链路未接线项禁用
  并标原因;整库版本独立导航并入;文档级旧流程折叠保留
2026-10-28 12:28:04 +08:00
姚章浩 74aef1e373 fix(fetch): 修复钉钉来源身份与取件失败监控
build / build (push) Successful in 1m17s
2026-10-27 14:36:24 +08:00
姚章浩 1b09f7d35b feat(monitor): 卡片概况与统一文件处理表
build / build (push) Successful in 1m17s
2026-10-27 12:14:19 +08:00
姚章浩 7448d130ec feat(settings): 汇总全部数据源配置操作审计
build / build (push) Successful in 1m13s
2026-10-27 10:42:30 +08:00
hermes f87d3fad9b fix(qa): 授权码按写入版本解密,改其它设置不再失效
build / build (push) Successful in 1m13s
只调整收件人或时间也会递增设置版本,导致已保存的授权码无法解密、测试发送报存储不可用。
新增 password_revision 记录加密时的版本,迁移回填现有行。
2026-10-26 16:09:00 +08:00
hermes 23b19350cc feat(qa): 周报邮件新增测试/审核邮箱
build / build (push) Successful in 1m13s
测试发送只投递到该地址,不打扰主送与抄送名单;未填时退回主送首位。
2026-10-26 16:06:17 +08:00
hermes 822fb652ff feat(qa): 周报邮件区分主送抄送并附统计看板图片
build / build (push) Successful in 1m21s
设置新增抄送列表,MIME 增加 Cc 头、envelope 同时投递主送与抄送,同一地址不得同时出现在两边。
确认后可保存统计看板 PNG(multipart/related + cid),发送时放在正文末尾内联显示;正文改动使确认失效后不再使用旧图。
2026-10-26 15:59:06 +08:00
hermes 6816289552 feat(qa): 周报确认后按设定时段自动发邮件
build / build (push) Successful in 1m23s
周报页新增自动发送区:管理员配置 SMTP/收件人/发送星期与小时,授权码用主密钥加密保存。
确认绑定当前草稿版本与正文哈希,正文再改则确认失效;worker 每分钟仅在启用、到点、周已结束且未发送过时发一次。
邮件同时提供渲染后的 HTML 与可读纯文本;SMTP 仅允许 TLS,拒绝明文。
迁移 000060 新增设置、确认与发送记录三张表,唯一键防重复发送。
2026-10-26 09:49:23 +08:00
姚章浩 2c0bb32183 fix(qaops): 点踩问题统一判为不正确
build / build (push) Successful in 1m11s
通过持久来源关联读取最新本地反馈,强制总表有效正确性为否,保留原问答和人工证据。统一列表、详情、指标和两阶段读取的反馈快照,拒绝冲突选择并保留其他字段编辑。已通过完整本地门禁、隔离PG竞态和真实恢复库52条点踩逐条读回、桌面及手机验收;未调用模型或连接源生产库。
2026-10-20 08:18:03 +08:00
姚章浩 87f97a2f9b feat(qaops): 完成详情下拉与每周自动保存草稿
build / build (push) Successful in 1m7s
详情使用共享主表字典,稀疏修改所选字段并保留原始问答及旧复核审计。
月周统计提供文本和下拉双入口;每租户每周一份持久草稿,按技能六节双语模板真实预填。
加入幂等自动保存、冲突保留、未知响应重试、切周及离开保护和schema58非破坏降级保护。

验证:完整make verify、隔离PostgreSQL全mirror竞态回归、真实恢复库浏览器验收通过。
浏览器实测字段精确读回、月周双输入、模板预填、MD/CSV、自动保存、冲突、失败重试与API重启恢复。
未连接或改写DobotAI生产源库,未运行付费模型;提交时尚未部署。
2026-10-20 05:44:02 +08:00