refactor(kernelboot,kernelmod): ID:13 删死包 kernelboot/modules,整机启动图判据迁到 kernelmod/all

## 决策
删除 internal/kernelboot/modules(registry.go + registry_test.go),
把整机启动图判据迁到 internal/kernelmod/all(真实聚合入口)。
文件头那条 TODO(死包清理) 随包一起消失。

## 为什么这个包该删
实测零生产调用:
  · 生产代码里 import 它的包数 = 0
  · Register() / BuildModules() / Names() 的外部调用点 = 0
  · 它的 Register/BuildModules 是 internal/kernelmod 那套注册表的**第二份实现**
    (留着只会让人下次接错口子)
  · kernelmod/all/facilities_sync_test.go 的注释里早已写明
    「kernelmod 才是真实注册表(模块编译期工厂加载)」

## 判据迁移(能力未削弱,逐条实测)
- 判据 1–4(整机装配/层拓扑/严格逆序关闭/无残留): 只换宿主,
  用 kernelmod.Registered() 作为装配输入不变,实测 5 层 / 17 模块 /
  关闭顺序 [4 3 2 1 0] / 3 轮 goroutine 差值 0 / 订阅者 2→2
- 判据 5(TestEveryRegisteredModuleIsCompiledIn): 真相源是扫 `..` 源码树的
  「目录+类型名 复合键 + 四方法齐全」,与宿主包无关。scanModuleTypes 的
  modRoot 从 "../../kernelmod" 改为 ".."(本文件现在在 kernelmod/all/ 下)
- 删掉 `_ ".../kernelmod/all"` 自引用(判据现在与聚合入口同包,
  本包被编译即聚合 import 生效 —— 反而更贴近生产同一条 import 链)

## 「新模块登记了但漏 import」仍咬得住(3 个变异亲测)
  A 从 all.go 删 multimodal 聚合 import(叶子模块,无人依赖)
    → TestEveryRegisteredModuleIsCompiledIn 红(指名 multimodal)
    → TestEveryModuleDirIsImported 红
  B 删 multimodal 的 init()(Register 调用点消失,真相源不能是被测入口)
    → TestEveryRegisteredModuleIsCompiledIn 仍红(源码侧多数一个类型)
  C supervisor 的 Name() 改成非字面量
    → 判据显式失败(不静默跳过 ⇒ 不会变空转)

## 悬空引用清零
全仓 grep kernelboot/modules:代码/文档/CI 只剩 ci.yml 里一条
「那个包零生产调用,已删除」的历史说明。同步修正:
  · ci.yml「整机启动图判据」步骤 → go test ./internal/kernelmod/all/
  · ci.yml -race 排除表 pkg 行目标包 → internal/kernelmod/all
    (该包现在装整机 ⇒ 同样会撞 KERNEL-REGISTRY-SETLOCK;
     实测不带 -race 6/6 绿,排除仍必要,单号不变)
  · ci.yml 三条说明文本(含证据步骤的提示语)
  · kernelboot/kernelstate.go 指向旧判据路径的注释
  · docs/zh/kernel-modularization-design.md §2.7 的 declarations.go 路径
  · check-ci-gates.py §4 断言目标包 → ./internal/kernelmod/all/
    (否则它盯的是一个已不存在的包)

## 验证
go build ./... = 0 error,go vet ./... = 0 error
go test ./internal/kernelboot/... ./internal/kernelmod/... = ok
go test -race ./internal/kernelmod/all/ = ok(连跑 6 次无 DATA RACE)
check-ci-gates.py EXIT=0;ID:15 的 verify_v4 矩阵复跑仍 5 变异全抓 + 误报项 EXIT=0 + 基线 EXIT=0
This commit is contained in:
JianFeeeee
2026-10-24 12:56:37 +08:00
parent e70c2b55b4
commit 3864f36992
7 changed files with 43 additions and 145 deletions

View File

@ -158,10 +158,15 @@ else:
if _run_body(_inject(_probe + "|合成探针原因|")).returncode == 0:
fails.append("合成行「reason 有效 / ticket 空」竟通过 ⇒ ticket 守卫已失效")
# ── 4. go job:整机启动图判据必须单列一步跑 kernelboot/modules ──
# ── 4. go job:整机启动图判据必须单列一步跑 kernelmod/all ──
#
# ★ 目标包随 ID:13 从 internal/kernelboot/modules(死包,已删)迁到
# internal/kernelmod/all(真实聚合入口)。本断言跟着改,否则它盯的
# 是一个**已不存在的包** —— 那会变成"永远红"或(更糟)"永远绿"的空转。
GO_BOOT_PKG = "./internal/kernelmod/all/"
gojob = step_runs("go")
boot = [r for _n, r in gojob if "kernelboot/modules" in r and "go test" in r]
check(bool(boot), "go job 缺少「整机启动图判据」步骤(go test ./internal/kernelboot/modules/)")
boot = [r for _n, r in gojob if GO_BOOT_PKG in r and "go test" in r]
check(bool(boot), "go job 缺少「整机启动图判据」步骤(go test " + GO_BOOT_PKG + ")")
for r in boot:
check("-count=1" in r, "整机启动图判据步骤必须带 -count=1(不许吃 go test 缓存)")

View File

@ -100,7 +100,7 @@ jobs:
- name: go test ./...
run: go test ./... -count=1 -timeout 20m
# 整机启动图判据(internal/kernelboot/modules/registry_test.go)。
# 整机启动图判据(internal/kernelmod/all/bootgraph_test.go)。
#
# ## 为什么单列一步(而不是只靠上面的 go test ./...)
#
@ -111,11 +111,14 @@ jobs:
# ② 它断言的是**跨模块的顺序与资源回收**,是"多跑几次才暴露"的那类,
# 单独一步便于在 flaky 时被识别(这里带 -count=1,不吃缓存)
#
# ★ 判据宿主是 internal/kernelmod/all(真实聚合入口),不是以前的
# internal/kernelboot/modules(那个包零生产调用,已删除)。
#
# ★ 不用 -race(那在 race job 里跑):本判据含真实端口/挂载,
# -race 的插桩会让时序假设变松,两者分开放更便于定位。
- name: 整机启动图判据(真实登记表装配 + 逆序关闭 + 无残留)
run: |
go test ./internal/kernelboot/modules/ \
go test ./internal/kernelmod/all/ \
-count=1 -timeout 10m -v
# 跨平台客户端版本一致性:内核 internal/meta 是唯一事实源,
@ -222,7 +225,7 @@ jobs:
# ║ ║
# ║ **整包永久排除不被接受。** 唯一出口是给 plugin.Registry 的 Set* ║
# ║ 加锁(修复单 KERNEL-REGISTRY-SETLOCK),修好后: ║
# ║ · 删掉本表里 kernelboot/modules 那一行 ║
# ║ · 删掉本表里 kernelmod/all 那一行 ║
# ║ · **复跑一次** -race 全仓确认整机判据在 -race 下也稳定 ║
# ║ · 删掉 evidence 步骤 ② 的静态断言 ║
# ║ 这三步没做完,本线不算收口。 ║
@ -234,7 +237,7 @@ jobs:
# 列名:粒度|目标|原因|修复单号 (用 | 分隔,原因内可含空格)
read -r -d '' TABLE <<'EOF' || true
test|internal/plugins/webui|TestOpenAIStreamIsActuallyStreaming|gzipResponseWriter 的 mode/wroteHeader/buf 无锁;handler goroutine(发 finish/[DONE])与增量写 goroutine 并发写同一 ResponseWriter(gzip.go:139 读 mode vs gzip.go:206 写 mode)|WEBUI-GZIP-RACE
pkg|internal/kernelboot/modules|*(整包)|该包把真实整机装起来 ⇒ 第 4 层 agent 与 plugin-host 并行 Init 时,双方都无锁直写同一 plugin.Registry 的 7 个 Set*(SetLuaVM/SetBaseAPIKey/SetSupervisor/SetTracker/SetConfig/SetStageHost/SetIndexer,registry.go:231-284)。竞争在**内核装配**里,不在本包代码里;任何装整机的用例都会撞上|KERNEL-REGISTRY-SETLOCK
pkg|internal/kernelmod/all|*(整包)|该包把真实整机装起来(bootgraph_test.go 的整机判据) ⇒ 第 4 层 agent 与 plugin-host 并行 Init 时,双方都无锁直写同一 plugin.Registry 的 7 个 Set*(SetLuaVM/SetBaseAPIKey/SetSupervisor/SetTracker/SetConfig/SetStageHost/SetIndexer,registry.go:231-284)。竞争在**内核装配**里,不在本包代码里;任何装整机的用例都会撞上。★ 目标包随 ID:13 迁到此(原 internal/kernelboot/modules 已删)|KERNEL-REGISTRY-SETLOCK
EOF
tests=""; pkgs=""; n=0
@ -291,7 +294,7 @@ jobs:
SKIP="${{ steps.skip.outputs.tests }}"
# 从全仓包列表里剔除 pkg 级排除项(按路径后缀精确匹配,
# 不用 grep 模糊匹配 —— 否则 kernelboot 会把 kernelboot/modules 一起剔掉)。
# 不用 grep 模糊匹配 —— 否则 kernelmod 会把 kernelmod/all 一起剔掉)。
PKGS=""
for p in ${{ steps.pkgs.outputs.pkgs }}; do
drop=0
@ -379,7 +382,7 @@ jobs:
done
echo " 仍为无锁直写的 Set*: $((total - locked))/$total"
if [ "$locked" -gt 0 ]; then
echo "::error title=内核 setter 已加锁::KERNEL-REGISTRY-SETLOCK 可能已被修复($locked/$total 个 Set* 形态已变)—— 请从 ci.yml 排除表删掉 kernelboot/modules 那一行"
echo "::error title=内核 setter 已加锁::KERNEL-REGISTRY-SETLOCK 可能已被修复($locked/$total 个 Set* 形态已变)—— 请从 ci.yml 排除表删掉 kernelmod/all 那一行"
fail=1
fi

View File

@ -736,8 +736,9 @@ loadOne(name):
### 2.7 声明层已落地(Step 4b 第一步,2026-10-16)
`internal/kernelboot/modules/declarations.go` 把 **main() 里隐含在语句顺序中的
依赖关系**变成显式声明。实测分层结果:
`internal/kernelmod/all` 的聚合 import 把**全部模块引入编译**后,各模块在自己
文件中用 Requires() 把 **main() 里隐含在语句顺序中的依赖关系**变成显式声明。
实测分层结果(2026-10-24 复核:`go test ./internal/kernelmod/all/ -run TestLayeringYieldsParallelism -v`):
```
6 层 / 17 模块,最宽层 9 个可并行(原来是 17 步严格串行)

View File

@ -80,7 +80,9 @@ type MemoryBundle struct {
// · 少一层间接 ⇒ 少一处可能出错的地方
//
// 顺序正确性由**声明**驱动:`Build()` 校验缺依赖 / 环 / 重名,
// 判据断言关键顺序(见 internal/kernelboot/modules/registry_test.go 的
// `Run()` 在执行每一层之前再核一次"实际已产出的设施"是否涵盖本层依赖
// (声明对、但计划层顺序被改坏时也能拦住,见 DependencyNotReadyError)。
// 判据断言关键顺序(见 internal/kernelmod/all/bootgraph_test.go 的
// TestLayerTopologyIsRespected:用真实模块登记表装配整机,逐层断言层间有序)。
//
// ## ★ 并发

View File

@ -1,104 +0,0 @@
// 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
}

View File

@ -1,29 +1,21 @@
package modules
package all
// 「真实登记表 → 整机启动图」的判据(2026-10-24)。
//
// ╔══════════════════════════════════════════════════════════════════════╗
// ║ ★★ 本包(internal/kernelboot/modules)当前**零生产调用**,是死包。 ║
// ║ ║
// ║ 实测(2026-10-24): ║
// ║ · 生产代码里 import 本包的包数 = 0 ║
// ║ · BuildModules() / Names() / Register() 的外部调用点 = 0 ║
// ║ · 真正的内核模块登记表是 internal/kernelmod(由 kernelmod/all ║
// ║ 聚合 import,cmd/homed/kmod_runtime.go 消费) ║
// ║ ║
// ║ ⚠ 不要把本包当成主路径。它里面的 Register/BuildModules 是 kernelmod ║
// ║ 那套注册表的**第二份实现**,留着只会让人下次接错口子。 ║
// ║ ║
// ║ 本文件为什么仍挂在这里:判据用真实登记表(kernelmod.Registered)装配 ║
// ║ 整机,是**占位**位置;挪去 internal/kernelmod/all/ 并删除本包, ║
// ║ 需要单独一单(避免与本线混在一起,见下方 TODO)。 ║
// ║ 去留决策与执行:另开一单(判据迁 kernelmod/all + 删本目录 + ║
// ║ 同步改 registry.go / kernelstate.go / docs 的指引)。 ║
// ╚══════════════════════════════════════════════════════════════════════╝
// ## 迁移记录(ID:13,2026-10-24)
//
// TODO(死包清理):本包零生产调用。判据已在此跑通并合入;把判据迁到
// internal/kernelmod/all/(真实聚合入口、已有同族判据)后删除整个目录。
// 触发条件:本线合入 main 后另开单。在那之前**不要**新增对本包的生产依赖。
// 本文件原先挂在 `internal/kernelboot/modules/registry_test.go`,那个包
// **零生产调用**(死包:Register/BuildModules 是 kernelmod 那套的**第二份
// 实现**,外部调用点 = 0),只为了安放这组判据才留着。
//
// 现迁移到**真实聚合入口** `internal/kernelmod/all`:本包本来就是
// 「把全部模块 import 进来」的那一层,判据放在这里,用的就是生产路径
// 同一条 import 链(all → 各模块 init → kernelmod.Register)。
// 死包与其第二份注册表实现已删除。
//
// ★ 迁移没有削弱任何判据的能力:下面判据 5 的真相源(扫 `..` 源码树的
// 类型大小)与宿主包无关,换宿主前后都照样咬得住「登记了但漏 import」。
// 变异亲测见本文件尾部「失败面」一节第 1–3 条。
//
// ## 这个文件补的是什么(此前完全没有的覆盖)
//
@ -136,11 +128,10 @@ import (
internalConfig "github.com/JianFeeeee/HologramAgent/internal/config"
"github.com/JianFeeeee/HologramAgent/internal/kernelboot"
"github.com/JianFeeeee/HologramAgent/internal/kernelmod"
// ★ 真实登记表的**唯一入口**:聚合 import 决定"哪些模块被编译进来"。
// 本判据刻意用与 `cmd/homed/kmod_runtime.go` 完全相同的入口
// (若那里漏了 import,这里同样会漏 ⇒ 两边一起红,而不是判据自说自话)。
_ "github.com/JianFeeeee/HologramAgent/internal/kernelmod/all"
// ★ 判据与被测对象现在同属一个包(internal/kernelmod/all):
// 本文件就在聚合 import 的**那一层**里。因此不需要(也不能)再
// 空白 import 自己 —— 只要本包被编译,all.go 的聚合 import 就已生效。
// 这正是迁移的收益之一:判据用的是生产同一条 import 链,而非旁路。
)
// ─────────────────────────── 整机夹具 ───────────────────────────
@ -668,7 +659,7 @@ func TestShutdownLeavesNoResidue(t *testing.T) {
// · 删 all/all.go 任一聚合 import ⇒ 红(该类型四方法仍在 ⇒ 源码侧多一个)
// · 删掉整个模块目录 ⇒ 反向断言红(登记表里有、源码树里没有)
func TestEveryRegisteredModuleIsCompiledIn(t *testing.T) {
const modRoot = "../../kernelmod"
const modRoot = ".."
fromSource := scanModuleTypes(t, modRoot)
if len(fromSource) == 0 {
t.Fatal("★ 源码扫描一个模块都没找到 —— 扫描逻辑失效(判据会永远绿)")

View File

@ -86,7 +86,7 @@ func emptyState(t *testing.T) *kernelboot.KernelState {
// 取真实 plan 的**第一层**单独跑:模块(config / kernel-api)本来就绪条件
// 为空,若能通过 ⇒ 校验没把"合法的第 0 层"当成缺依赖。
//
// (整机正序不误报由 internal/kernelboot/modules 的整机判据覆盖:
// (整机正序不误报由 internal/kernelmod/all 的整机判据覆盖(bootgraph_test.go):
// 它用真实 plan 跑完全部 5 层,装配成功后资产/关闭/泄漏全部断言。
// 这里只做最小正向基线,避免重复一份整机夹具。)
func TestRunRuntimeGuardDoesNotFireOnCorrectPrefix(t *testing.T) {