diff --git a/.github/scripts/check-ci-gates.py b/.github/scripts/check-ci-gates.py index 358a3194..8f34aa61 100755 --- a/.github/scripts/check-ci-gates.py +++ b/.github/scripts/check-ci-gates.py @@ -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 缓存)") diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 626a3abe..55688540 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/docs/zh/kernel-modularization-design.md b/docs/zh/kernel-modularization-design.md index 9c4a166b..2f814e06 100644 --- a/docs/zh/kernel-modularization-design.md +++ b/docs/zh/kernel-modularization-design.md @@ -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 步严格串行) diff --git a/internal/kernelboot/kernelstate.go b/internal/kernelboot/kernelstate.go index b7d7403b..0fe435ce 100644 --- a/internal/kernelboot/kernelstate.go +++ b/internal/kernelboot/kernelstate.go @@ -80,7 +80,9 @@ type MemoryBundle struct { // · 少一层间接 ⇒ 少一处可能出错的地方 // // 顺序正确性由**声明**驱动:`Build()` 校验缺依赖 / 环 / 重名, -// 判据断言关键顺序(见 internal/kernelboot/modules/registry_test.go 的 +// `Run()` 在执行每一层之前再核一次"实际已产出的设施"是否涵盖本层依赖 +// (声明对、但计划层顺序被改坏时也能拦住,见 DependencyNotReadyError)。 +// 判据断言关键顺序(见 internal/kernelmod/all/bootgraph_test.go 的 // TestLayerTopologyIsRespected:用真实模块登记表装配整机,逐层断言层间有序)。 // // ## ★ 并发 diff --git a/internal/kernelboot/modules/registry.go b/internal/kernelboot/modules/registry.go deleted file mode 100644 index f288e599..00000000 --- a/internal/kernelboot/modules/registry.go +++ /dev/null @@ -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 -} diff --git a/internal/kernelboot/modules/registry_test.go b/internal/kernelmod/all/bootgraph_test.go similarity index 93% rename from internal/kernelboot/modules/registry_test.go rename to internal/kernelmod/all/bootgraph_test.go index 604c09af..1b0bc01e 100644 --- a/internal/kernelboot/modules/registry_test.go +++ b/internal/kernelmod/all/bootgraph_test.go @@ -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("★ 源码扫描一个模块都没找到 —— 扫描逻辑失效(判据会永远绿)") diff --git a/internal/kernelmod/all/runtime_dep_test.go b/internal/kernelmod/all/runtime_dep_test.go index 0c8742d5..6f3eeb03 100644 --- a/internal/kernelmod/all/runtime_dep_test.go +++ b/internal/kernelmod/all/runtime_dep_test.go @@ -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) {