Skip to content

Releases: oekit/matrix-tools

v0.1.20

Choose a tag to compare

@zy84338719 zy84338719 released this 27 Oct 09:58
48d9de8

matrix-tools v0.1.20 —— consumer 链两处修复。

fix(tools):consumer 探针判据粒度对齐

两处判据错误,都只在真实 org 数据上暴露(夹具看不出):

  • 假阳性:源码 import 的是子包路径(github.com/cloudwego/kitex/server),
    go.mod require 的是模块路径(github.com/cloudwego/kitex)⇒ 拿全等去比
    永远匹配不上 ⇒ kitex 父仓被误判「替 kitexotel / kitex-istio 背重依赖」,
    而它的 go.mod 是对的。修法:按模块前缀比(_imports_module)。
  • 假阴性:嵌套模块排除只覆盖第一层 ⇒ kitexotel 在深度 4 ⇒ 它的 import
    被算成父仓的 ⇒ 「父仓替子模块背依赖」这条判据对最深的嵌套模块完全失效。
    修法:逐级向上找 go.mod,覆盖任意深度(_under_nested_module)。

顺带修 stdout 行缓冲:重定向/管道时日志全程 0 字节 ⇒ 进程被杀/超时时一个字都留不下。

自测新增两条自带反证样本的判据用例;变异验证:前缀比退回全等 ⇒ 用例变红。

fix(ci):consumer job 跨仓写回必须用 PAT

该 job 用无凭据的 HTTPS URL clone oekit.github.io ⇒ git push 报
could not read Username for 'https://github.com' ⇒ 从未绿过 ⇒ 站点消费面画像
冻结在 2026-10-21。且 push 失败发生在 "Open consumer issue" 之前 ⇒ 步骤失败即中断
⇒ 真违规从来没人被报过。

修法:token 取 MATRIX_AUDIT_TOKEN || OEKIT_PROJECT_TOKEN || GITHUB_TOKEN
(GITHUB_TOKEN 只对本仓有效,跨仓必须用 classic PAT);push 显式塞
http.extraheader(GH_TOKEN/GITHUB_TOKEN 环境变量只对 gh CLI 生效,git 不认);
失败时 ::error:: 给出可执行诊断;set -euo pipefail 让管道里的失败不被吞。

影响面

consumer_probe 的判据只喂给 CI 的 consumer job 与本地画像生成,
不影响发版链(matrix.py / audit.py / release_post.py)。

对应 PR #111 / #112,issue #109 / #110。

v0.1.19

Choose a tag to compare

@zy84338719 zy84338719 released this 27 Oct 09:37
859ccb1

fix(tools):远端 tag 读取加重试。

matrix._ls_remote_tags(url, timeout=60, attempts=3) —— 每个 URL 最多试 3 次,退避 0.5s / 1s / 2s。2026-10-27 跑 gormx v0.6.2 的发版后置链时,一次 middleware 的网络抖动让链在「站点 versions.json」那步直接死掉(版本行 PR 开了、站点与 BOM 都没开),手动 ls-remote 当场可读 ⇒ 瞬时抖动。

⚠️ #79 口径不变:全试完仍失败照样抛 RemoteReadError,绝不降级成本地 tag(读到空 ≠ 读不到)。重试只吸收瞬时抖动。

remote_tags 是所有远端读的事实入口(审计 / BOM 生成 / 站点版本表 / Release 对账都走它)⇒ 一次抖动原本能打断其中任何一条链。

自测:tag_cache_selftest 补反证 7(瞬时失败必须重试)与反证 8(全失败仍抛错且确实重试过),变异验证(attempts=1 ⇒ 2 例变红)。

v0.1.18

Choose a tag to compare

@zy84338719 zy84338719 released this 25 Oct 14:35
430895f

matrix-tools v0.1.18:审计加实时进度行 + BOM「读不到」与「陈旧」分开报

修后台审计任务「Re-run audit for final truth」(bg_19b83328,900s 超时)暴露的两个问题。

一、超时后日志全丢
该任务的输出日志只有 59 字节(运行时那句 "Command timed out after 900 seconds"),
审计自己的输出一个字都没有。两个原因:

  • stdout 块缓冲 ⇒ 实测整个运行期间日志文件 0 字节,只在进程退出时一次性落盘;
  • 命令形状 ... | grep ... | tail -15 两端都缓冲,且开头白等 sleep 90。
    改法:stdout 改行缓冲;新增 progress(),进度行写 stderr(stderr 默认行缓冲)——
    写 stderr 不写 stdout 是刻意的:CI 里 audit.py > audit-report.txt 收集的是 stdout
    (那份文件会被贴进漂移 issue),进度进的是日志,报告文件格式一个字不变。
    进度分 7 个阶段,逐仓与 Release 对账逐仓报。

二、「读不到」混进了「漂移」
兄弟会话同名任务报出「❌ matrix.RemoteReadError: registry: 远端 tag 读不到…」——
那是 BOM 子进程崩了、audit 取它输出最后一行当漂移消息,读的人看不出这是读取失败
而不是产物陈旧(违反 #79)。
改法:gen_matrix_go.py 引入退出码契约(生产方是子进程、消费方是 audit.py):
0=一致 / 1=真漂移 / 2=读不到(判不了 ≠ 通过);读不到时给干净消息并返回 2,
不再抛裸 traceback;audit.py 用 classify_check() 分类,不靠猜消息文本。

三、该任务重跑(修正命令形状)
去掉 sleep 90、去掉管道过滤、日志直接落盘 ⇒ 54s、退出码 0、输出完整,
审计结论不变(24/24 仓成功 + ✓ 矩阵审计通过)。

详见 CHANGELOG v0.1.18。

v0.1.17

Choose a tag to compare

@zy84338719 zy84338719 released this 25 Oct 14:16
c310411

matrix-tools v0.1.17:远端 tag 读取加进程内缓存 + 并发预取

修掉后台审计任务反复以 900s 超时、输出为空的形态。

根因:同一份远端事实被重复读 ≈168 次(audit 的 4 个调用点 × 24 仓,加上
Release 对账与 BOM 子进程),单次读 1.0–2.1s(拥塞时 5–10s)⇒ 审计要 281s。

改动:

  • matrix.py 新增默认关的进程内远端 tag 缓存(enable/disable/clear_tag_cache)
    与并发预取 prefetch_remote_tags;
  • scripts/audit.py 接线:审计开头开启缓存 + 并发预取,读失败只记一条漂移;
  • scripts/gen_matrix_go.py 每仓只读一次远端 tag;
  • scripts/tag_cache_selftest.py(新,5 例反证)+ 接线进 audit.yml selftest job。

实测(同一台机器、同一网络条件):audit --remote-tags 281s → 48s / 49s(连跑两次,
5.8×);审计结论不变(24/24 仓远端读取成功 + ✓ 矩阵审计通过);BOM 产物零变化。

详见 CHANGELOG v0.1.17。

v0.1.16

Choose a tag to compare

@zy84338719 zy84338719 released this 25 Oct 13:19
d8670d7

变更记录

  • fix(tools): BOM 生成器 Go 地板取不到就报错,不许兜底出假地板——v0.1.42 曾把 hertz 的 1.26.0 写成 1.24.0(#100)

背景

gen_matrix_go.py 生成 BOM 时版本取远端 tag、Go 地板取本地 clone,两路口径分裂:

  • 版本:gen_versions.latest_version → _remote_tags(远端)
  • 地板:git -C <dir> show <tag>:go.mod(本地 clone 的 git 对象)

本地 clone 还没 fetch 到那个 tag 时(多仓并行发版、clone 刚建、网络不稳),
git show 直接失败 ⇒ 落到旧实现的兜底分支 return m.group(1) if m else "1.24.0"。

关键点:兜底值 1.24.0 不是「未知」而是一个具体的、看起来合法的地板 —— 它和
「真的是 1.24.0 的仓」在产物里长得一模一样,下游无从分辨。地板低报比不报更危险:
不报会让人去查,报一个具体数字会让人以为那就是事实。

已污染一个发布物

BOM v0.1.42 把 hertz 写成 Go: "1.24.0",而 hertz v0.8.4 / v0.8.5 的 go.mod
都是 go 1.26.0(触发条件:生成时本地 hertz clone 尚未 fetch 到 v0.8.5)。
v0.1.42 已发布、tag 不可撤,只能补发 v0.1.43 修正 —— 但根因仍在,下一个 BOM
版本只要再撞上「本地 clone 缺 tag」就会重犯。

改动

  • scripts/gen_matrix_go.py:go_floor() fail-loud —— 取不到 go.mod、或
    go.mod 里没有 go 指令 ⇒ 抛 SystemExit,错误消息里带可执行的修复步骤
    (git fetch --tags)与 issue 编号;不再有任何默认地板。这与
    matrix.remote_tags 已有的口径一致:「读到空」≠「事实为空」(#79)。
  • scripts/gen_matrix_go.py:新增 ensure_local_tag() —— 取地板前若本地缺该 tag,
    补一次 git fetch --tags(只在真缺时做,稳态零额外网络开销;fetch 失败不抛,
    真因仍由 go_floor 报,它的消息里带 git 的原始 stderr,更可诊断)。
  • scripts/gen_matrix_go_selftest.py(新):反证怎么构造 —— 把「取不到就报错」
    改回「兜底」,自测必须失败。5 例覆盖:正常路径取到真地板 / 本地缺 tag 必须抛错
    (且消息里不含任何看起来合法的地板值)/ go.mod 无 go 指令同样抛错 /
    ensure_local_tag 能从 origin 补取 / 稳态不做额外 fetch。无副作用、不联网、
    不依赖工作区布局(临时目录造 fixture 仓,「远端」用本地 bare 仓当 origin)。
    已接线进 .github/workflows/audit.yml 的 selftest job。

兼容性

  • happy path 零行为变化 —— gen_matrix_go.py --check 实跑仍报
    ✓ oekit BOM 与远端 tag 一致(v0.1.43,35 模块)。
  • 产物格式零变化(仍是 Go: 字段),只有「取不到」时从「写假值」变成「报错退出」。

变异验证(实跑)

把兜底改回 return m.group(1) if m else "1.24.0" ⇒ 反证 1、2 两条用例立刻 FAIL
(应当抛 SystemExit,却正常返回了);还原后 5 例全绿。

关联 #100(产物侧已由 BOM v0.1.43 修正)。

v0.1.15

Choose a tag to compare

@zy84338719 zy84338719 released this 25 Oct 01:51
c42cf87

这次修的是什么

维护轮门禁此前从未生效——月度 workflow maintenance.yml(2026-10-19 加入,cron 0 4 5 * *)
至今 0 次运行、0 个维护轮 issue,而真实地板漂移是 20 仓(阈值 8):门禁到期不报,
且没人知道信号丢了。三个根因互相叠加:

  1. maintenance_check.py 不认 MATRIX_BASE——CI 布局下 24 仓全部「读不到」。
    脚本硬编码 BASE = parent.parent.parent,而 matrix.repo_dir(repo, base) 的语义是
    base or os.environ.get("MATRIX_BASE", ...)——base 非空就把环境变量短路掉。
    CI 里仓克隆在 $GITHUB_WORKSPACE/repos/<name>,三个候选路径一个都不命中
    ⇒ 24 仓全抛 FileNotFoundError ⇒ 被 except Exception 吞掉 ⇒ MAINTENANCE_COUNT=0。
  2. 后果是反向的:count == '0' 走的正是「关闭维护轮 issue」分支
    ⇒ 门禁方向反了——到期不报、反而关单。
  3. 标签 matrix-maintenance 在本仓不存在 ⇒ 即使走对分支,issues.create 也会
    422 Validation Failed,issue 依然开不出来。(matrix-drift 存在,所以 audit.yml
    的漂移 issue 开得出来——这就是两个 workflow 表现不一致的原因。)

连带:align_floors.py 是同一行硬编码,而它是维护轮唯一的整改工具
⇒ 定位错 ⇒ 整改命令打到错误目录。

改了什么

  • scripts/maintenance_check.py:base 解析认 MATRIX_BASE(与 audit.py 同口径),
    --base 可显式覆盖,新增 --repos 单仓盘点;「读不到」与「真的没漂移」分开——
    读不到的仓计入 MAINTENANCE_UNDETERMINED,且不再打印「✓ 全矩阵地板零漂移」
    (与 audit 在 #79 对 remote tag 的处理同口径:读不到 ≠ 事实为空)。
  • scripts/align_floors.py:同样认 MATRIX_BASE / 新增 --base / --repos。
  • .github/workflows/maintenance.yml:显式传 --base "$PWD/repos";
    set -o pipefail 让脚本崩溃传导为步骤失败(而不是被 tee 吞成 0);
    关闭分支收紧为 count == '0' && undetermined == '0'——只有确定没有漂移才关单;
    计数缺失(脚本崩了)⇒ 明确失败、不改动任何维护轮 issue;
    开 issue 前确保标签存在(自愈,不依赖仓外的带外设置)。
  • scripts/maintenance_check_selftest.py(新):一道没人验证过的门禁,很可能自己就是
    个假绿。自测用临时目录 + 临时 git 仓(不联网、不碰真仓)复刻 CI 布局,证明仓能
    被认出、漂移能被算出来、仓读不到时不许输出「零漂移」、align_floors.py 同样认
    MATRIX_BASE。改前红 4/5、改后绿 5/5(同一命令行,失败原因只能是 bug 本身)。
  • .github/workflows/audit.yml:自测接入既有 selftest job。
  • README.md:补维护轮 workflow 说明与两个工具的 base 口径。

v0.1.14

Choose a tag to compare

@zy84338719 zy84338719 released this 24 Oct 04:11
200736c

这次加的是什么

嵌套模块的「多层子目录路径」支持——kitex v0.7.0 把 kitexotel / istio 拆成嵌套模块后,
工具链里五处「单层目录」假设全部算错。

Go 的子目录模块 tag 规则要求 tag 带完整子路径 ⇒ 这类嵌套模块的 tag 是
observability/tracing/instrumentation/kitexotel/v0.1.0,而工具链按「仓根下一层」解析:

位置 旧假设 修法
gen_versions.py tag.split("/", 1) 改 rsplit(split 会把多层路径切成 path=observability / ver=tracing/…/v0.1.0)
gen_matrix_go.py NESTED_ONLY_TAGS 正则只放行单层路径 放宽为允许多层,版本号改 rsplit 取最后一段
release_train.py 嵌套模块的 CHANGELOG 取 module_dir.parent/CHANGELOG.md 上溯到仓根——多层时原指向不存在的目录,发版被凭空拦下
verify_release_units.py llms.txt 嵌套条目正则只放行单层路径 名字与链接正则同步放宽
audit.py 嵌套模块父仓地板门禁只扫「仓根下一层」 放宽为递归 **/go.mod(排除仓根本身)

计数漏数也修了:count_units 用 r.glob("*/go.mod") 只扫一层 ⇒ 多层嵌套模块被漏数
(实测文件系统里有 11 个,单层 glob 只数出 10)⇒ 站点文案据此判漂移
(真实 11 / 36,误判 10 / 35)。改为递归扫 **/go.mod,排除仓根本身与
.git / vendor / third_party / testdata / examples(与共享 CI 的发现口径一致)。

口径变化

嵌套模块 9 → 11、发版单元 34 → 36(24 根 + 11 嵌套 + 1 伞仓)。

NESTED_MODULES 登记表补齐 kitex v0.7.0 拆出的两个嵌套模块——登记表是「应该有什么」的唯一声明,
缺登记会让三类判据失真(发版单元口径门禁判「口径分裂」红 / llms.txt 逐字节比对判站点段漂移红 /
嵌套模块安装行缺失 ⇒ Agent 装不上):

  • kitex/observability/tracing/instrumentation/kitexotel → kitexotel/v0.1.0
  • kitex/istio → istio/v0.1.0

顺带把 BOM_RELEASE_UNIT 旁过期的计数注释(「裁决定死 34」)改成只写裁决不写绝对数——
裁决本身(伞仓必须计入发版单元口径)没变,变的是总数随嵌套模块增减。

附带

matrix.py 的 DESC 同步已发布事实:pkg 补 v0.23.0 起 Peek 非删除式读
(可选接口 PeekGetter 外置——读到过期条目只算不命中、不删除它,stale-if-error 兜底需要那份过期真值留在表内,pkg#72)、
data 补 v0.18.0 起 cache 壳 PeekGetter 别名 + cachetest 壳 2 个 stale-if-error 契约入口。

验证

changelog_check ✓ · verify_release_units ✅ 36 个发版单元 · verify_site_copy ✅(24 / 25 / 11 / 36)·
MODULES 24 | NESTED 11 | UNITS 36

v0.1.13

Choose a tag to compare

@zy84338719 zy84338719 released this 23 Oct 14:46
3cdca43

这次加的是什么

「发布序列倒挂」门禁——同一版本线内,后发布的 tag 版本号反而更低。

全矩阵 24 个模块仓实测,4 个仓有 5 个已发布版本命中:

仓 旁支版本 发布时刻 此前已发布的更高版本 其发布时刻 间隔
kitex v0.3.9 10-15 19:32 v0.4.1 10-15 16:48 +2h43m
kitex v0.3.10 10-16 08:39 v0.4.1 10-15 16:48 +15h50m
redisx v0.3.1 10-16 10:20 v0.4.0 10-16 10:17 +3m
middleware v0.5.1 10-17 16:52 v0.6.0 10-17 16:08 +44m
network v0.1.1 10-17 16:53 v0.5.1 10-17 16:51 +2m

这些版本都已发布——tag 在远端、GitHub Release 在、Go module proxy 上可解析 ⇒ 删不得也挪不得(重推 = 已缓存消费方集体中毒,.github#5)。而矩阵(versions.json / llms.txt)只登记 latest 与 breakingIndex,不登记它们 ⇒ 对外口径与远端事实不一致。

为什么一直没被抓到

共享 CI 的 changelog_check.py 规则 1 查的是「CHANGELOG 小节按 semver 严格递减」——那是文档序;倒挂是发布序(事实序)。

network 的小节顺序是 v0.5.5 … v0.5.0 → v0.1.1,逐节递减 ⇒ 门禁绿;而事实序是 v0.5.1 先发布、v0.1.1 后发布。实测 4/4 仓的 changelog_check 全绿——两种序之间没有人守。

怎么加的

  • scripts/verify_version_line.py——独立 oracle:自己解析 semver、自己读 git 原始输出(git for-each-ref --sort=creatordate),刻意不复用任何现有解析逻辑(共享逻辑就有共同盲区,#31 复审原则)。
    版本线 = (路径前缀, module 路径)——module 路径变更是「新模块」,新旧两条线各自单调(redisx 活例:旧路径 github.com/oekit/redis 与新路径 …/redisx;不按路径分层,新线起点会被误判成回退)。
    某仓一个 tag 都没有 ⇒ 判不了,不当通过。
  • scripts/version_line_known.json——已登记的 5 处旁支版本,逐条写明事实与处置。未登记的新倒挂仍然判红——例外表是显式人工放行口,不是漏报口子。
  • scripts/verify_version_line_selftest.py——变异自测 7 例:回退必须抓到(且参照是此前已发布的最高版本)/ 正常递增不误报 / 嵌套版本线独立 / module 路径变更后新线起点不误判 / 同一路径内的回退仍抓到 / 例外表只放行登记的那一处 / 无 tag 判不了。
  • 接入 audit.py(聚合层 ⇒ 随之进 CI 的 PR 门禁与周巡检);audit.yml 的 selftest job 加入新自测。

明确不做

  • 不删、不重推任何已发布 tag(= 已缓存消费方集体中毒)
  • 不改已发布版本的内容语义——各仓只补事实声明:network#24 / kitex#33 / middleware#46 / redisx#31

关:#88

v0.1.12

Choose a tag to compare

@zy84338719 zy84338719 released this 23 Oct 11:44
73d5d01

这次修的是什么

「远端有没有这个 tag」这条事实,在 matrix-tools 里有三条读取路径,却有两种语义。

#79 把「读到空 ≠ 事实为空」在 GitHub API 侧(ghsafe)与 git 传输侧
(matrix.remote_tags)都做实了——读不到就抛 RemoteReadError。但另外两条
读取路径仍是旧语义:只看 git ls-remote 的 stdout 非空 ⇒ 读失败 = 「不存在」。

# 路径 用在哪 读失败时的行为
1 matrix.remote_tags audit / gen_versions 抛 RemoteReadError(#79)
2 release_train.tag_exists_remote 发版检查面 a「同名 tag 查重」 False =「不存在」⇒ 查重门禁静默放行
3 close_issue.tag_on_remote 关单守卫「tag 必须已存在于远端」 报「tag 不在远端」——把「读不到」说成「没有」

变异实测(修复前):同一个确实存在于远端的 tag,只因读法不同就从「存在」
翻成「不存在」——

读法 读成功 读失败
release_train.tag_exists_remote True False
matrix.tag_exists True False

怎么修的

  • matrix.remote_tag_exists:这条事实的唯一读法(走 matrix.remote_tags:
    canonical HTTPS 优先 → origin 兜底 → 失败抛 RemoteReadError);
    matrix.tag_exists 收口到它,不再自带第三种语义。
  • release_train.tag_exists_remote 改走它;check_module 接住 RemoteReadError
    ⇒ fail-closed(不进打 tag 环节),错误信息写明「读不到 ≠ 不存在」。
  • close_issue.tag_on_remote 改走它;读不到 ⇒ 报「读不到远端 tag(不是不存在)」,
    并明确不要用 --force 绕过(--force 是刻意的越权 ⇒ 显式跳过核对、不发读请求)。

为什么值得单独修

  • 发版查重形同虚设:release_train 的 docstring 写着这条检查的理由——
    「重推 = 代理/sumdb 缓存集体中毒,.github#5」——恰在最需要它的时刻(SSH 掉线 /
    网络抖动)放行。真正拦住重推的是 git push 自身(实测同名 tag 无 --force 推送被拒:
    ! [rejected] v1.0.0 -> v1.0.0 (already exists),远端 tag 指向不变)⇒ 不会真的
    重推成功
    ,但脚本会在 git tag -a 之后崩在 push、留下半截本地 tag,报错是
    「already exists」而不是「查重时读不到远端」——排查被带偏。
  • 关单守卫误报:--force 在脚本文案里就写着「确实要引用未发版本需 --force」
    ⇒ 读失败被误报成「不在远端」,会把人推向 --force,而真实原因可能只是网络抖了
    一下 ⇒ 守卫生效的路径被误诊成「刻意的越权」。

验证

  • 变异回归:M1 release_train 退回旧语义 ⇒ K 转红;M2 close_issue 退回旧语义
    ⇒ M 转红;M3 matrix 退回旧语义 ⇒ I 转红;3/3 转红,还原后 14/14 全绿。
  • scripts/remote_reads_selftest.py 加 6 组用例(I/J/K/L/M/N):三条读路径各自的
    「读失败必抛」与「读成功必判存在」。该脚本已接进 audit.yml 的 selftest job
    ⇒ 自动进 CI 覆盖(PR #86 的 selftest job 已绿)。
  • 其余四个 selftest(close_issue / dependabot_dirs / ghsafe / verify_release_units)
    全绿;audit.py --remote-tags 仍 ✓ 矩阵审计通过(远端 tag 读取 24/24 仓成功);
    PR #86 CI 全绿(含 audit job)。

Closes #85

v0.1.11

Choose a tag to compare

@zy84338719 zy84338719 released this 23 Oct 10:22
7aaf01c

这次修的是什么

站点承诺 llms.txt 是「34 个发版单元」的机器可读索引(24 根 + 9 嵌套 + 1 伞仓
oekit),canonical 口径 gen_tables.count_units 也这么算。但发版单元门禁
verify_release_units 只校验 24 + 9 = 33 —— 伞仓压根没进判据。

伞仓 oekit 是货真价实的独立发版单元:有独立 tag(latest v0.1.29)、独立 GitHub
Release,下游 go get github.com/oekit/oekit@latest 是一次独立的版本获取。

变异实测(修复前):把站点 docs/public/llms.txt 里伞仓那段
(go get github.com/oekit/oekit@latest)整段删掉后重跑门禁——

✅ 发版单元门禁通过(33 个发版单元 = 24 根 + 9 嵌套;9 条嵌套安装行齐全)
exit=0

同样的变异对嵌套模块是红的 ⇒ 门禁的牙没长在伞仓上。而站点
docs/guide/ai-agent.md 正对外承诺「llms.txt 覆盖 34 个发版单元(… + 1 个 BOM
伞仓 oekit
)」⇒ 这条承诺无人守(llms.txt 是 AI Agent 唯一会抓的机器可读索引)。

怎么修的

  • check_llms_units 增加伞仓 oracle:llms.txt 必须含
    go get github.com/oekit/oekit@latest 安装行,与嵌套模块同等要求。
    该函数已被 audit.py 导入 ⇒ 改动自动进 CI 覆盖面(audit.yml 的 audit job:
    PR 门禁 + 周巡检),无需新增 CI 接线。
  • check_filesystem_oracle 增加伞仓的物理证明:目录在 ⇒ 必须真有独立 vX.Y.Z tag
    (与嵌套模块同款判据);目录没检出不报(本地少 clone 一个不该假红)。
  • 口径统一到 canonical:新增 release_unit_count() = 24 根 + 9 嵌套 + 1 伞仓 = 34。
    伞仓数不写死常量(判定依据是 BOM_REPO 不在矩阵登记表里,它若哪天被收进矩阵模块,
    口径自动变 0,不会两处各说各话)。
  • 门禁额外做口径交叉核对:完整检出时拿文件系统推导(count_units)与 canonical
    口径对比,不一致即判红 —— 同一个术语不许对外报出两个数。

验证

  • 变异回归:修复前同一变异 exit=0(绿)→ 修复后 exit=1 并点名伞仓:
    ❌ 发版单元门禁未通过(应覆盖 34 个发版单元 = 24 根 + 9 嵌套 + 1 伞仓):
       - llms.txt 缺伞仓 BOM 仓 oekit 的 go get 安装行(`go get github.com/oekit/oekit@latest`)…
    
  • 新增 scripts/verify_release_units_selftest.py(纯内存 fixture;不联网、不依赖工作区
    布局、无副作用):合法 fixture 零问题 / 删掉伞仓安装行必红 / 删掉嵌套安装行必红 /
    删掉根模块条目必红;另钉住 34 这个口径。接入 audit.yml 的 selftest job
    (与 close_issue / dependabot_dirs / ghsafe / remote_reads 四个既有 selftest 并列)。
  • 回归:五个 selftest 全绿、py_compile 全仓通过、audit.py --remote-tags 仍
    ✓ 矩阵审计通过(远端 tag 读取 24/24 仓成功)、PR #83 CI 全绿(含 audit job)。

Closes #82