姚章浩
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
姚章浩
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
姚章浩
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
姚章浩
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
姚章浩
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
姚章浩
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
姚章浩
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
姚章浩
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
姚章浩
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
姚章浩
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
姚章浩
8da4666e76
test(library): 端到端编排在本机真实 PG/ES 上跑通
...
build / build (push) Successful in 1m14s
为什么:只验证单阶段写入还不够,完整链路(授权→预备份→四类写入→校验)也要在真实服务上跑。
本机 WeKnora 测试栈一次性库与随机索引已通过,CI 无这些服务故在 test-scope 声明为可选。
2026-10-28 15:58:06 +08:00
姚章浩
281699a431
feat(library): 生产整库真实读写/校验/恢复与本机真实服务验收
...
build / build (push) Successful in 1m21s
为什么:生产目标此前是"无条件失败"的占位,编排再完整也不能落地;本轮把它做成真实实现,
并在本机 WeKnora 测试栈的真实 PG/ES 上跑通导入、撤销、预备份与故障恢复。
- ProductionLibraryTarget:Current 读行数+身份摘要;Apply 按阶段真实写入(database/wiki
同事务先删本库再插、resources 传对象、index 搬运原始文档与向量);Verify 比对身份摘要;
Restore 按预备份快照回放。
- 撤销真正生效:索引阶段写入后 PruneScope 删除快照里没有的文档。
- ProductionBackupStore:导出前独立完整预备份,导出前后行数不一致即失败。
- worker 用注入配置装配目标与预备份;缺项把变量名写进阶段并失败,不再有占位对象。
- capacity 安全加法防溢出;保留策略组合语义明确(各维度都超限才候选)。
- 新增本机真实服务集成测试(一次性 kg_library_test_* 库 + 随机 ES 索引),
覆盖导入/校验/撤销/预备份/故障注入/恢复。
2026-10-28 15:54:01 +08:00
姚章浩
910c0fcd76
feat(library): 页面发起封存的最小权限主机侧执行边界
...
build / build (push) Failing after 16s
- libraryagent:0700 目录内 0600 unix socket + SO_PEERCRED 同 uid,
固定 binary/归档/日志/项目/写入者声明,请求只能选范围与作业号
- API:POST/GET /api/v1/library/captures(入队 library.capture + 读队列状态)
- worker:library.capture handler 走 socket;启动先 recover;不挂 Docker socket、
不持有来源凭据
- web:整库版本页显示真实作业状态并可发起封存(未接线/无权限给出原因)
- scripts/library-agent-install.sh:构建、安装 systemd user 服务并读回权限
2026-10-28 12:11:08 +08:00
姚章浩
045fc44f0e
feat(library): 接通整库封存真实入口与只读版本页
...
build / build (push) Failing after 19s
- libraryctl:status/preflight/capture/list/verify/recover,只读来源会话、
结清检查、契约推导、维护窗口内停写者、导出十表+索引+对象+Wiki、封存候选后恢复
- libraryfreeze.ProjectController:按 Compose 项目逐服务声明固定身份,
未声明服务/容器重建即拒绝,仅 SIGTERM,恢复后必须 healthy
- httpapi:GET /api/v1/library/snapshots[/{id}] 只读列出封存产物
- web:「整库版本」页(只读、未接线显示原因、无假按钮)
- api:LIBRARY_ARCHIVE_ROOT + 归档根只读挂载
- docs/library-capture.md:运维手册与 2026-10-28 两次真实封存实测
不代表整库业务验收通过:内容/问答/性能验收、跨系统切点、Wiki 删除语义重算、
生产预备份/恢复演练/双闸/同步协调仍未完成。
2026-10-28 11:39:50 +08:00
姚章浩
864a1632e0
test(snapshot): 增加真实测试源只读诊断并记录孤立索引阻塞
build / build (push) Successful in 1m11s
2026-10-28 08:32:23 +08:00
姚章浩
9f990158b9
test(snapshot): 联合验证真实隔离存储与暂停恢复链
build / build (push) Successful in 1m8s
2026-10-28 08:22:21 +08:00
姚章浩
9105f8316c
feat(snapshot): 增加整库独立快照及测试停写恢复基础
build / build (push) Successful in 1m20s
2026-10-27 23:44:12 +08:00
姚章浩
74aef1e373
fix(fetch): 修复钉钉来源身份与取件失败监控
build / build (push) Successful in 1m17s
2026-10-27 14:36:24 +08:00
姚章浩
e744114ff4
fix(catalog): 打通共享授权扫描与统一来源清单显示
build / build (push) Successful in 1m18s
2026-10-26 18:36:11 +08:00
hermes
43b1e423fe
fix(qa): 规范化邮件调度测试并声明其外部依赖
...
build / build (push) Successful in 1m11s
gofmt 修正;该集成测试需要一次性库与测试密钥,按仓库约定在 test-scope.json 声明为可选未运行。
2026-10-26 10:01:14 +08:00
姚章浩
533db535f1
feat(qaops): 接入真实模型补缺任务与总表看板
...
build / build (push) Successful in 1m12s
按已确认需求提供独立安全模型配置、真实探测、正式字典字段评估与原问题译中,持久批次支持只补空白、暂停恢复、有限重试、幂等和逐字段依据。覆盖固定规则、本地语言、组织关联与主表有效看板共用治理侧读模型,保留源生产和冻结历史。
验证:完整 make verify 退出0,702项Go与329项前端通过;默认118项外部/隔离集成明确跳过,另跑完整镜像及组织隔离PG竞态。12并发、输入漂移、过期租约、暂停/恢复及有限重试通过。用户授权的百炼 deepseek-v4.1-flash 完成真实后台与浏览器评估/翻译;552行、16列、11块看板及完整PNG、独立配置页测试/保存、重开恢复和零JS异常通过。仅非业务样本外发,自动处理关闭,原payload不变。
上线前治理库已做导出快照原生备份并真实恢复,60表行数与17个QA表内容哈希一致;不修改DobotAI生产或隧道。本提交时尚未部署,线上验收须独立完成。
2026-10-19 14:29:49 +08:00
姚章浩
f5e0f8d20f
feat(qa-ops): 建立独立正式主表并保真导入历史问答
...
build / build (push) Successful in 1m7s
将可维护总表与可替换2.0镜像分离,稳定来源关联和原始事件键保留;
源端更新/删除不覆盖主表或人工复核。首次收录保留原评估审计,
主表输入/复核独立持久化;历史原标注不冒充新AI或冻结证据。
历史导入绑定源哈希、逐行批准清单与真实恢复备份;三字段精确去重,
批次及原字段留档,重复执行不覆盖后续修改。仅操作本地治理库。
验证:完整make verify退出0;镜像与区域两组隔离PG race退出0;
真实恢复数据隔离导入12522、跳过792、暂缓1,总表15050;
重放保留模拟修改,结构down拒绝丢历史;真实浏览器第41周551条完整,
历史原标注和独立主表复核读回通过,移动布局及JS异常0。
2026-10-19 08:49:18 +08:00
姚章浩
3e7fcce0b1
feat(qa-ops): 持久本地问答副本与安全答案预览
...
build / build (push) Successful in 1m11s
新增迁移50与每5分钟/手动持久同步,API只读本地;同步失败保留、权限阻断、租约与质量失效原子激活。
保留全量稳定分页/固定列,已存质量批量读取,元数据不反复解压长答案;详情安全Markdown与原文。
已运行整仓 make verify、源与副本隔离PG -race、真实只读源及隔离浏览器。
实际同步2525条;固定窗口640条本地中位1.46秒,对照直接源33.10秒。
停worker、缺源凭据、重启API后仍可查询;未更新8088,未修改生产原问答。
2026-10-18 19:10:47 +08:00
姚章浩
74ba737b1e
fix(version): 按完整提交区分同号历史开发说明
...
build / build (push) Successful in 1m4s
历史分支中存在两条开发号 0.1.1;归档选择按 SHA 匹配,保留原编号与冲突标签。
增加同号前置说明遮蔽及错 SHA 不复用回归,标签计划使用相同精确选择方式。
已运行:定向 Go 测试、快照工具临时仓正反例及整仓 make verify;未部署。
2026-10-18 18:18:43 +08:00
姚章浩
013ce4a24e
fix(version): 补全开发快照记录与同步交付闭环
...
build / build (push) Successful in 1m3s
收录 main、注释开发快照和完整运行 SHA;开发说明绑定原提交,补记 0.1.1–0.1.25。
进行中同步按仓库合并,完成后重新执行;启动入队,部署脚本读回版本证据。
开发快照独立于正式标签与 Release,既有冲突标签不移动。
已运行:整仓 make verify,Go 673 通过、前端 274 通过,92 个已声明可选跳过,必需跳过 0。
隔离 PostgreSQL 重复同步队列用例及 -race 通过;未更新现有 8088、未修改生产问答。
2026-10-18 18:11:49 +08:00
姚章浩
4b3aaff321
feat(qa-ops): 完成复检留档与人工复核闭环
...
治理侧保存输入观测版本、自动复检及人工复核历史,输入变化失效、版本冲突和幂等重放不覆盖人工记录。批量质量数量使用完整筛选元数据,未采集原文不伪造快照,延迟旧扫描不回退新输入。
验证:全仓 make verify 退出码0,Go656及前端253通过;五包竞态、源/组织/质量隔离PG、真实只读源和隔离网页读回复核通过。默认91个可选集成跳过不计通过。未接AI模型,不写DobotAI源库,未推送、未合并、未部署。
2026-10-17 22:37:26 +08:00
姚章浩
9556db1ef0
feat(qaops): 增加个人自然月用量与零记录人员明细
2026-10-17 19:59:33 +08:00
姚章浩
56b8cabed9
feat(qaops): 接入问答组织补全与区域使用统计
2026-10-17 19:02:13 +08:00
姚章浩
cc4906b6a9
feat(qaops): 接通网页通讯录持久化同步与后台接管
2026-10-17 17:36:07 +08:00
姚章浩
5874134e04
feat(qaops): 接入真实通讯录采集并补全根部门
2026-10-17 16:11:29 +08:00
姚章浩
dab74b706b
feat(qaops): 增加版本化部门区域归属配置与预览
2026-10-17 11:39:38 +08:00
姚章浩
fd04bf7886
feat(qaops): 接入独立只读问答查询与治理覆盖复检
2026-10-17 09:04:19 +08:00
姚章浩
b7f7e4f121
fix(deploy): 上线前校验 .env 键值形状(两行粘成一行的坏值不再能上线)
...
build / build (push) Successful in 1m1s
事故复盘(2026-10-14,测试实例 p1 整体不可用约 6 分钟):
- 我用 `rstrip("\n") + 新行` 往 `.env` 追加 `VERSION_HISTORY_LINK_ORIGIN`,而 `.env` 最后一行
当时没有结尾换行 ⇒ 两行**粘成一行**:
`VERSION_HISTORY_GITEA_BASE_URL=http://192.168.50.223:3000VERSION_HISTORY_LINK_ORIGIN=https://gitea.zheromiku.top `
- 配置校验按设计拒绝这个非法 origin ⇒ `cmd/api`/`cmd/worker` 启动即退出 ⇒ 容器 crash-loop
(fail-closed 是对的,但代价是平台起不来)。
修复:
- `scripts/deploy-api-web.sh` 新增 **1a 步护栏**:上线前解析 `.env`,只要某行的值里又出现 `KEY=`
(两行粘连)或键名不合法 ⇒ 打印 `ENV_VALUE_LOOKS_MERGED` 并**拒绝部署**(不动线上);
- `internal/appconfig` 在校验失败时补线索:值里含 `=` ⇒ 提示"`.env` 里两行可能粘成了一行";
- 回归用例 `TestLoadRejectsMergedVersionHistoryEnvValue`(粘行必拒 + 线索文案 + 合法值仍放行)。
验证:护栏对"粘行样例/键名非法"退出码 1、对"正常样例/当前真实 .env"退出码 0;Go 用例通过;
make verify 15/15。线上已修 `.env` 并重启 api+worker(healthy),重新同步后页面链接已全部走公网 origin。
2026-10-14 11:17:35 +08:00
姚章浩
90db243a68
fix(web): 旧 index.html 的哈希资源回退到「上一版构建」,页面不再空白/裸奔
...
build / build (push) Successful in 56s
现象(主人反馈:页面**偶尔**变成"只有文字没样式"):旧入口 index.html 引用的是上一版的
`index-CddbQIqQ.js` / `index-DLg1Y9tN.css`,而新镜像里只剩当次构建的两个文件
⇒ JS 404 白屏、CSS 404 只有文字没样式。触发条件是"短时间内多次重建 web + CDN/浏览器还拿着旧入口"。
修复(三层,各管一段):
1. **入口不缓存**(上一笔已做):`index.html` → `no-cache, must-revalidate`,CDN/浏览器每次回源校验;
2. **旧哈希资源仍可取**(本笔):`/assets/` 未命中时先回退到只读挂载的
`deploy/legacy-assets/assets`(**上一版构建**的资源),再 404 ⇒ 还持有旧入口的客户端
不会因为一个文件 404 而白屏/裸奔;该目录由 `scripts/deploy-api-web.sh` 在**每次重建前**
从"正在运行的那个镜像"里落盘(`LEGACY_ASSETS=` 会打在部署日志里),并加进 .gitignore;
3. **未命中不再回退成 HTML**:避免浏览器把 index.html 当 JS 执行报一堆语法错误。
验证:旧入口的两个引用 `assets/index-CddbQIqQ.js`、`assets/index-DLg1Y9tN.css` 现在都是 200
(旧客户端可用);当前入口的 `assets/index-Dbnd-DW-.js`、`assets/index-Bgkt4A1i.css` 也是 200;
不存在的文件名仍明确 404;`nginx -t` 通过;本机 8088 与公网入口 kg.zheromiku.top 一致;make verify 15/15。
2026-10-14 10:58:01 +08:00
姚章浩
4d43a9386a
fix(version-history): tag 之前的提交不再被编号 + 部署脚本注入构建身份
...
build / build (push) Successful in 1m5s
真实数据发现(本机 p1 实例第一次同步后核对):
- **缺陷**:tag `v0.1.0` 之前的 20 个提交,提交内 CHANGELOG 顶部**也已经**是 0.1.0(旧分支遗留条目),
而它们的祖先集合被那个**更晚才打**的 tag 全覆盖 ⇒ 按提交数算得 0 ⇒ 被错标成「0.1.0 开发版」。
它们的正确判定是「历史提交」:那个 tag 是后来才打的,当时还没有发布基线。
- **修复**:基线 tag 必须是该提交的**祖先(或就是该提交)**,否则不给号;新增原因码
`release_tag_created_later`(页面文案:'该版本是在更晚的提交上发布的 —— 这个提交当时还没有 tag')。
回归用例两条:`TestDeriveVersionsDoesNotNumberCommitsThatPredateTheTag`、
`TestDeriveVersionsSideBranchWithoutTagStaysHistorical`(不含 tag 的侧分支同样不给号)。
- **部署脚本注入构建身份**:`scripts/deploy-api-web.sh` 原先不传 `-ldflags -X`,产物 `CommitSHA=unknown`
⇒ 页面只能显示"当前运行版本未知"(契约上诚实,但没用)。现在按 `scripts/version.py --identity`
(唯一来源,与 Makefile/CI 同规则)注入 VERSION/RELEASE_VERSION/BUILD_KIND/COMMITS_SINCE_RELEASE/
COMMIT_SHA/BUILD_TIME;取不到身份**拒绝继续**(宁可不上线,也不上线一个认不出自己的产物),
并在收尾核对注入的 SHA 真的进了产物。
- compose.yaml:把 VERSION_HISTORY_* 五项接进应用环境(api/worker 共享锚点),留空即未接线。
验证:真实仓库复验(114 条对账一致)⇒ 正式 1 / 开发 4 / 历史 109(原来错的 19 条现在都是
`release_tag_created_later`);p1 实例重新同步后当前快照分布一致,带号的记录只剩 1 正式 + 4 开发
(含真实存在的「同号不同 SHA」:950c1b4 与 3a1d32d 都是 0.1.1,各占一行);
make verify 15/15 success;前端 18 用例 + typecheck 通过。
2026-10-14 10:00:27 +08:00
姚章浩
0974d8dfa2
feat(version-history): 「版本记录」页 —— 追踪远端 main 全部提交(只读 + 管理员手动同步)
...
build / build (push) Successful in 57s
按已确认的契约实现(口径见 docs/VERSION_HISTORY.md):
- **版本号口径(只给有证据的)**:注释 tag + tag 指向该提交 + 该提交内 CHANGELOG 顶部三者一致 ⇒ 正式版本;
tag 之后的提交按 `<主>.<次>.<自该 tag 的提交数>` 显示开发号(与 scripts/version.py **同规则**);
证据不足 ⇒ 「历史提交 · 短SHA」+ 原因码,**不补造版本号**。完整 SHA 是唯一身份、版本号不是唯一键
(tag 之后两条并行分支各自的第 1 个提交都算 0.1.1 ⇒ 两条都保留,用短 SHA 区分)。
- **迁移 000046**:version_history_commits(提交记录,主键 repo_key+commit_sha)+ version_history_sync_runs
(同步状态)。快照 = 一次同步:抓完并与 X-Total-Count 对账通过才切换;失败不切换快照、不清空历史、
last_success_at 不动(页面显示"上次同步失败(原因)· 仍展示上次成功数据");「同步成功」≠「历史完整」。
- **只读客户端(匿名,不使用任何凭据)**:白名单 origin + 固定仓库、拒绝任意 URL 与跨主机重定向、
响应上限 5MB / 超时 15s、仅 5xx/429 退避重试(最多 3 次);逐页抓到短页为止;每次全量扫 tag
⇒ 旧提交后补的 tag 会更新既有记录(不新增第二行)。
- **接口**(三个都登记进 api/openapi.yaml):GET /api/v1/build-info(viewer)、
GET /api/v1/version-history(viewer;游标绑定快照,过期返回 409 cursor_expired;不返回伪造 total)、
POST /api/v1/version-history/sync(admin;并发同步复用同一作业,靠 IdempotencyKey)。
- **「当前运行」只认精确匹配**:产物注入的完整 SHA 命中某条记录才标该条;开发构建显式标注"开发构建";
没有可用标识 ⇒ "当前运行版本未知";**不拿最新 main / 最新 tag 顶替**,CI 成功不等于已部署。
- **前端**:一级菜单第 5 项(版本记录)+ 时间线页面(全部/正式/开发/历史四段筛选、正文展开、
加载更多、760px 单列无横向溢出、提交正文与变更记录按纯文本渲染);无打 tag / 发布 / 部署入口。
新增配置(留空即未接线,页面降级):VERSION_HISTORY_GITEA_BASE_URL / _REPO / _ALLOWED_ORIGINS /
_HTTP_TIMEOUT / _MAX_PAGES。
验证:`make verify` 15/15 success(tests-go 578 通过 / 0 失败;tests-web 209 通过 / 0 失败);
新增 Go 用例 30(版本口径与图遍历 12、客户端守卫 6、同步编排 5、接口 7);前端新增 18(组件 10 + 客户端 8);
真实仓库匿名快照拉取(`VERSION_HISTORY_LIVE_BASE_URL=… go test -run TestLiveSyncAgainstRealRepository`):
抓到 113 条与 X-Total-Count 一致、v0.1.0 是注释 tag 且目标与 `git rev-parse v0.1.0^{commit}` 一致、
开发号逐个等于 `git rev-list --count v0.1.0..<sha>`(正式 1 / 开发 22 / 历史 90)。
2026-10-13 19:40:19 +08:00
t
950c1b429b
feat(release): 抬档门禁(破坏性 ⇒ 0.x 抬次 / ≥1.0.0 抬主)+ 1.0.0 前置(稳定契约承诺)
...
原来预检只"打印破坏性条数",抬哪一档靠人自觉 ⇒ 改成硬门禁:
- **抬档门禁**:破坏性条数取自**目标提交里候选版本那一段**(发版事实看提交,不看工作区)。
`0.x` 阶段有破坏性 ⇒ 必须抬**次版本**(或主版本),只抬修订 ⇒ 阻断并报出该抬到哪一档;
`≥1.0.0` 之后有破坏性 ⇒ 必须抬**主版本**,抬次/修订都阻断。
另加"候选号必须大于上一个正式版本"(版本只增不减)。
- **1.0.0 前置**:上一版本是 `0.x` 时,候选 `1.0.0` = 对外承诺契约稳定 ⇒ 必须先有写实的
《稳定契约承诺》(覆盖 接口 / 数据模型 / 配置 / CLI 输出与退出码,不留 `待填`/`TODO`/`TBD`),
**且候选发布记录里链接它**,缺任一条 ⇒ 阻断;`1.x → 2.0.0` 不重复要求。
- 预检新增"版本走向"事实行(上一版本 → 候选 / 抬哪一档 / 破坏性 N 条 ⇒ 要求哪一档 ✅ ⛔ );
`docs/RELEASE_RUNBOOK.md` 新增"抬哪一档"对照表与《稳定契约承诺》段(故意留占位)。
验证:release_preflight --self-test 32 分支全过(含新增 9 条);反向夹具新增 §24 六条真实临时仓库用例
(0.x 只抬修订 ⇒ 阻断、0.x 抬次 ⇒ 放行、≥1.0.0 抬次 ⇒ 阻断、1.0.0 留占位 ⇒ 阻断、写实+链接 ⇒ 放行、
写实但没链接 ⇒ 仍阻断)⇒ 夹具 91/0;make verify 15/15。
于是 0.1.0 之后的第一个提交(本提交)开发号 = 0.1.1。
2026-10-13 17:13:44 +08:00
t
6de5d52363
feat(version): 开发号随提交自动递增 + 首个正式版本 0.1.0
...
build / build (push) Successful in 51s
按使用者要求定的版本规则(自动升的是**开发标识**,发布版本与 tag 仍由人批准):
- **开发号自动递增**:display_version(development) = `<正式版本的主.次>.<自 v<正式版本> 以来的提交数>`
(正式 0.1.0 之后第 1 个提交 0.1.1、第 12 个 0.1.12;恰在发布提交上 = 正式号)。
它**不写进 CHANGELOG**(发布记录只在发版时动),由 git 算出来 ⇒ 每个提交都有自己的号、数字只增。
- **浅克隆不猜号**:`--depth` 会把条数算少 ⇒ 浅仓库里拒绝算号,退回 `<版本>-dev+g<短SHA>`;
检出步骤在仓库有 tag 时 `--unshallow` 补全历史。
- 接线三处齐全:`version.py --identity` → 门禁 `-X …CommitsSinceRelease=<n>`;Makefile 的
`COMMITS_SINCE_RELEASE`;Dockerfile 同名 build-arg;Go 侧新增 `CommitsSinceRelease` 与
DisplayVersion() 新规则;契约测试**交叉核对**"注入的提交数 == git 算出的"(防注入错号)。
- **首个正式版本 0.1.0**:CHANGELOG 的 `[Unreleased]` 提成 `## [0.1.0] - 2026-10-13`;
头部补"版本怎么走"两行;RELEASE_RUNBOOK 补递增规则与"下一次发布把次版本加一"。
- 发版预检新增:打印**当前开发号**,并挡住"候选正式版本 <= 当前开发号"(会把开发号当正式号;建议次版本加一)。
验证:version.py --self-test(含 tag 后第 2 个提交 = 0.2.2、恰在 tag 上 = 0.2.0、无 tag 不猜号);
release_preflight --self-test(21 分支,含开发号两条);go test 本地三态 + 临时仓库 tag+2 提交实测
(Python 与 Go 都得出 0.2.2);反向夹具 85/0;make verify 15/15 success。
2026-10-13 16:45:56 +08:00
姚章浩
97f4b29661
fix(version): 就绪看目标提交的顶部版本 + 禁用自动发布路径
...
build / build (push) Successful in 52s
补两处残余(本轮不推、不打真实 tag、不部署;全部用临时仓库与 spy 验):
1) version_in_commit 必须比"目标提交 CHANGELOG 的**顶部**已发布版本",不是"历史里出现过":
- 新增 top_version_in_commit(),version_in_commit() 直接比它;预检输出把该提交的顶部版本打出来
(诊断可审计:例如"顶部是 `## [0.3.0]`,不是 0.2.0"),阻断信息里也带上真实顶部版本。
- 顺带兜住"最新在最上面 / 版本只增不减"的排序要求。
- 负例(自检 + 夹具各一遍):顶部 0.3.0、历史里 0.2.0 ⇒ 候选 0.2.0 拒绝;候选 0.3.0 通过。
2) 按只读约定**禁用 --execute 自动发布路径**:
- 删掉写路径(打 tag / 推 tag / 建 Release 的代码整段移除),`--execute` / `--approve-version` /
`--approve-sha` 只保留参数兼容(不报 unknown argument),传了就**明确拒绝**(rc=2)。
- 拒绝发生在收集事实之前 ⇒ **零写操作、零 API 调用**;自检用 spy 断言拒绝路径
"一个子进程都没跑、一个 API 都没发",并扫描生产路径不许出现直接执行 git 写命令的行。
- 输出里也不再推荐自动路径;RELEASE_RUNBOOK 第 4 步改为由人执行。
验证:release_preflight --self-test 绿(就绪 20 分支 + 顶部/历史两态 + --execute 拒绝零写 +
源码写路径扫描);反向夹具 84/0(含 --execute rc=2 且跑完仓里 tag 不变);make verify 15/15 success。
2026-10-13 15:48:50 +08:00
姚章浩
7a9d51cc70
fix(version): 正式构建核证据 + 预检分"建议/就绪" + CI 就绪按必需 context
...
按要求补三项(本轮不推、不打真实 tag、不部署;全部用临时仓库与假对象验):
1) 正式构建身份不能只认 --release:
- 新增 release_evidence():核①完整 40 位 SHA ②**指向该提交的注释 tag** v<版本号>
③该提交里 CHANGELOG 顶部版本一致。缺任一条 ⇒ rc=2,身份退回 development 并把
release_blocked 的原因写进 JSON(给机器读,也给人看)。
- version.py --self-test:六条负例(有版本无 tag / unknown SHA / 短 SHA / tag 指错提交 /
轻量 tag / 版本不一致)+ 证据齐正例,**tag 全部在 mktemp 临时仓库里创建**。
- Go 侧新增 TestReleaseBuildRequiresEvidence:release 构建必须 CommitSHA 完整且
v<ReleaseVersion> 是**注释 tag** 并指向该提交(独立于 Python 的二次复核)。
2) 发布预检区分"准备建议"与"发布就绪":
- 新增纯函数 readiness(facts) → (是否就绪, 阻塞项, 建议);输出分两段。
- **就绪要求候选版本已写进目标提交**(version_in_commit:查 git show <sha>:CHANGELOG.md,
工作区写了没提交不算);首发按**显式指定版本**处理,**不把"记录里有这个版本"当已发布**
(已发布的证据是 tag,不是记录条目)。
- 预检**不执行、也不在输出里推荐自动发布路径**(正式发版由人按 docs/RELEASE_RUNBOOK.md)。
3) CI 就绪判据:必需 context 集合来自 .gitea/required-contexts.txt(缺失/为空 ⇒ 没有策略 ⇒
阻断,不用观察到的状态顶替);**全部 success 才通过**,失败/缺项/pending/查询异常一律阻断。
Release 查询加 release_state():**只有 HTTP 404 才算不存在**,403/5xx/超时算查询异常 ⇒ 阻断。
验证:version.py --self-test 绿;release_preflight --self-test 绿(就绪判定 19 个分支 +
临时仓库验证 git show 语义 + 五个 HTTP 码语义);go test 三态(未注入 SKIP / 开发 PASS /
自称正式 RED);反向夹具 83/0(含临时仓库 go test 六例、预检"记录未进提交"两态);
make verify 15/15 success。
2026-10-13 15:26:49 +08:00