Files
HomeAgent/internal/kernelboot/modules/registry.go
JianFeeeee 49141fe6f0 ci: 把 check-ci-gates.py 接进 go job,CI 自己检查自己的配置不许静默退化
背景:.github/scripts/check-ci-gates.py 上一轮已写好并入仓(ed8f28e),
但它**没被任何 workflow 调用** —— `grep check-ci-gates .github/workflows/ci.yml`
零命中。等于判据只活在评审口径里,CI 实际上不跑它。本次把它接上。

## 一、ci.yml:新增 2 步(go job,插在 go vet 之后、go test 之前)

- `actions/setup-python@v7`(3.12)
- `CI 配置门禁自检(动态枚举 / 排除表 / 启动图判据不许静默退化)`

位置为什么在 go test ./... **之前**而不是之后:

  · 该步骤是纯静态检查 —— 只读 ci.yml,秒级,无网络、无 cgo、
    不建 SQLite、不占端口;而 go test ./... 要跑 20 分钟。
  · 「门禁本身坏掉」应该第一时间红。放后面会让一次坏配置白白消耗
    20 分钟机器时间才发现,也让失败信号埋在 93 个包的输出后面。

runner 的 pyyaml **不假设自带**:先 `python3 -c 'import yaml'` 探测,
缺了才 `pip install pyyaml`(幂等,几秒)。本机实测 pyyaml 6.0.3 已有,
但本机的 Python 环境不等于 runner —— 所以做探测而不是赌。

## 二、顺手纳管 internal/kernelboot/modules/registry.go 的注释改动

该文件 +6/−3 的改动是**纯注释**,内容为 11:08 那轮已拍板要记进文件头的说明:
真相源为什么刻意不扫 `kernelmod.Register(...)` 调用点 ——
删掉某模块的 init() 后调用点消失,源码侧与登记表侧会同时少一个,
比对照样相等 ⇒ 假绿。手写模块名清单同理。故真相源取
「目录+类型名复合键 + 四方法齐全」的类型,而不是要检测对象的入口。

★ 这点在 commit message 里点明,是为了不让它变成夹带:它不是本次新写的代码,
  是上一轮已定但未提交的说明文字,与本次「门禁入 CI」同一主题(判据不许假绿)。

## 三、验证(先证能红,再证能绿,都不靠嘴说)

从 ci.yml 里**抽出该 step 的 run 块原样执行**(不是手敲等价命令):
  · 变色前(动态枚举改回写死 ./internal/agent/core/ ./cmd/waiter/)
    → EXIT=1,报 `race job 应恰有一步用 go list ./... 动态枚举包(实际 0 步)`
  · 还原后 → EXIT=0,报「CI 门禁自检通过:动态枚举 / 排除表校验 / 启动图判据步骤 / 交叉枚举 / 全部 run 步骤语法」

另在临时副本里抽查 5 个变异,4 个如期变红(写死枚举 / 去掉 -count=1 /
-race 不吃枚举 / cross 写死 cmd)。第 5 个「删掉排除表的原因校验」**没红** ——
已定位为脚本自身缺口,见下条,不在本次授权范围故未改。

## 四、已知遗留(未修,留给后续)

check-ci-gates.py 第 100 行用的是 `any("没有写原因" in r for ...)` ——
它只检查**提示文案还在**,不检查那段 `if [ -z "${reason// /}" ]` 校验逻辑本身。
于是把条件改成 `if false;` 而保留文案,断言仍然绿(实测 EXIT=0)。
判据被注释/文案顶包,与它自己在 docstring 里批判的「注释骗过 grep」是同一类错误。
属于该脚本的缺陷,本次任务授权只到「接进 CI」,故只记录不修。
2026-10-24 11:55:28 +08:00

105 lines
3.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package modules 是内核模块的**登记表**(不含任何具体模块的实现)。
//
// ## 设计意图(用户设计)
//
// > 每个内核模块显式各自声明依赖 … 最先初始化的 core 自然不依赖任何模块,
// > 下一步初始化的依赖 core 以此类推
//
// ## 怎么加一个新模块(这是本设计的**验收标准**)
//
// ① 新建一个文件(如 modules/mymod.go)
// ② 写一个类型,实现 kernelboot.Module(Name/Requires/Provides/Init)
// ③ 在同文件的 init() 里调 modules.Register(&MyMod{})
// ④ 在 `internal/kernelmod/all/all.go` 加一行空白 import —— **仅此而已**
//
// **不需要**编辑任何中央清单:依赖写在模块**自己**的 Requires() 里。
//
// ★ 为何要"防漏 import"的判据(见 `internal/kernelboot/modules/registry_test.go`):
// 忘了 import ⇒ 模块**根本不被编译进来** ⇒ 它不参与装配 ⇒ 依赖它的模块报
// "缺依赖"(响亮),但**不依赖它的模块会静默少了那个设施**
// (如插件注册表没建 ⇒ 插件全部加载不了,而错误信息指向插件而非根因)。
//
// 判据有两条(缺一不可):
//
// · `TestEveryRegisteredModuleIsCompiledIn` —— 用 AST 扫源码树里
// 「目录+类型名 复合键 + Name/Requires/Provides/Init 四方法齐全」的类型,
// 取其 Name() 与登记表做集合比对(独立真相源)。
// ★ 刻意**不**扫 `kernelmod.Register(...)` 调用点:删掉某模块的 init() 后
// 调用点消失,源码侧与登记表侧会同时少一个 ⇒ 比对照样相等 ⇒ 假绿。
// 手写模块名清单同理(清单会跟着漏),两者都不用。
// · `TestRealRegistryAssemblesWholeMachine` 等 —— 用真实登记表装配整机、
// 断言每个模块都进图且设施就绪、并严格逆序关闭(见同目录测试文件)
package modules
import (
"fmt"
"sort"
"sync"
"github.com/JianFeeeee/HologramAgent/internal/kernelboot"
)
var (
regMu sync.Mutex
registry []kernelboot.Module
seenName = map[string]bool{}
)
// Register 由模块在自己的 init() 里调用(自注册)。
//
// 重复注册同一模块名会**记录**并在 BuildModules 时报错 —— 不用 panic:
// panic 发生在 init 阶段,栈信息对使用者毫无意义(他只想知道哪个模块重名)。
func Register(m kernelboot.Module) {
if m == nil {
return
}
regMu.Lock()
defer regMu.Unlock()
if seenName[m.Name()] {
dupErr = append(dupErr, m.Name())
return
}
seenName[m.Name()] = true
registry = append(registry, m)
}
// dupErr 累积重名模块(BuildModules 时统一报告)。
var dupErr []string
// BuildModules 返回全部已登记模块(按名字排序,保证可复现)。
//
// ## 为什么排序
//
// 分层结果的**层内顺序**若依赖 map/切片顺序,同一份声明两次构建会得到
// 不同顺序 ⇒ 不可审计(设计 §2.5:图要能进 PR diff 比对)。
// 排序让结果稳定。
func BuildModules() ([]kernelboot.Module, error) {
regMu.Lock()
defer regMu.Unlock()
if len(dupErr) > 0 {
names := append([]string(nil), dupErr...)
sort.Strings(names)
return nil, fmt.Errorf("kernelboot/modules: 模块名重复注册: %v"+
"(同一个模块被 import 了两次?或两个模块取了同名)", names)
}
out := append([]kernelboot.Module(nil), registry...)
sort.Slice(out, func(i, j int) bool { return out[i].Name() < out[j].Name() })
if len(out) == 0 {
return nil, fmt.Errorf("kernelboot/modules: 没有任何模块被登记 —— " +
"通常是没有 import 模块包(见本文件顶部的'怎么加一个新模块')")
}
return out, nil
}
// Names 返回已登记模块名(诊断/判据用)。
func Names() []string {
regMu.Lock()
defer regMu.Unlock()
out := make([]string, 0, len(registry))
for _, m := range registry {
out = append(out, m.Name())
}
sort.Strings(out)
return out
}