| 项 | 内容 |
|---|---|
| 状态 | 已采纳(Accepted) |
| 日期 | 2026-10-19 |
| 背景 | 门禁执行体此前锁在 package main 里,宿主仓够不着——每个宿主要么各写一遍判据(各判一套,漂移到没人发现为止),要么干脆不写(policy 里的依赖拓扑表变成死条文:躺在文件里、看起来有人维护过,实际一条都不判)。 |
| 决策 | 把执行体下沉为可导入包 archcheck/(cmd/archcheck 退化为 CLI 装配),对外 API 三件套:Load / Run / WriteReport + Result.HasBlocking();判据与声明面分离(policy 是声明面,本包是执行面),宿主只写自己的声明面即可复用全部判定。 |
| 后果 | 宿主不必复制判据实现;判据的量纲与前提对齐(god object 判据改比「实质方法数」);泛型实例化各自成条;policy 自校验强制「规则须有执行体」。 |
| 依据 | archcheck/ 包(facade.go / deps.go / godobject.go / selfcheck.go);判据推导与实测数据见 issue #13 / #14。 |
> 一句话结论:机制面若要跨仓复用,就不能锁在 package main 里——判据与执行体分离,宿主只写声明面。
> 本文件正文原样搬自 README「门禁执行体可被宿主复用(2026-10-19)」一节。
门禁执行体可被宿主复用(2026-10-19)
执行体此前锁在 package main 里,宿主仓够不着——于是每个宿主要么各写一遍判据 (各判一套,漂移到没人发现为止),要么干脆不写(policy 里的依赖拓扑表变成死条文: 躺在文件里、看起来有人维护过,实际一条都不判)。现在它是可导入的包:
import (
"path/filepath"
"github.com/yi-nology/goagent-kernel/archcheck"
)
// ① 定位仓根:从当前目录向上找 architecture-policy.yaml
// (宿主仓把自己的 policy 放仓根,本仓同样如此——**不在 docs/ 下**)
root, err := archcheck.FindRoot(".")
if err != nil {
return err
}
// ② 加载声明面。policy 解析失败 = 门禁给不出结论,必须响亮失败
pol, err := archcheck.Load(filepath.Join(root, "architecture-policy.yaml"))
if err != nil {
return err
}
// ③ 跑判定
res, err := archcheck.Run(root, pol)
if err != nil {
return err
}
// ④ 出报告 + ⑤ 判阻断
archcheck.WriteReport(os.Stderr, res, archcheck.ReportOptions{Full: false})
if res.HasBlocking() {
os.Exit(1)
}
> archcheck.PolicyPath(root, pol) 不是加载路径——它返回的是报告里显示用的 > 相对路径(缺省即 "architecture-policy.yaml")。拿它去 Load 会读到错的地方。
宿主复用时要知道的六条:
policy 的
modules段要求每个 root 指向真实存在的包目录——陈旧的 root 会被 policy 自校验判红(这正是「机制包迁出当天即红」的落点)。跨仓依赖用
ext:<import 路径前缀>登记,且该前缀必须落在dependency_direction.ext_prefixes声明的受管包面之内。两层结构的理由见archcheck/deps.go包头:若「哪些外部包受管」也只由各模块自己声明,那么最后 一个声明它的模块撤掉声明时,整片包面随之消失,那条边就静默失管了。未登记包判定按「包目录」粒度,不是顶层目录——宿主的包根通常是嵌套的 (
internal/xxx),按顶层目录判会把整棵internal/误报成一个未登记的包。判据与实现分离:policy 是声明,本包是执行体。宿主可以只用自己的声明面 (只写
modules段 +dependency_direction段),复用本包的全部判定。判据面只有两种(import 路径 / 源码文本),没有
forbid_call_regex/allow_paths这类字段——常见诉求用现有字段表达:诉求 表达方式 某个第三方包只允许经某一个适配层引入(路径白名单) forbid_import_regex+scope_exclude某目录禁止裸 fetch(,但 api 客户端目录与两个 SSE 钩子除外forbid_regex(匹配调用形态)+scope_exclude写了不存在的字段会被 YAML 静默丢弃(那条规则于是变成「声明还在、一条都判 不了」),policy 自校验会把它报成「规则无执行体」并给出可执行的下一步。
max_methods_per_type比的是「实质方法数」,不是方法总数(v0.6.5 起)。 类型退化成 façade(实现下沉到子包、自己只做转发)之后,方法数 ≈ 公共 API 表面规模,不是复杂度集中度——拿它判 god object 是「把正确的工作判成错的」, 逼维护者在「撤销一次正确的重构」与「调高阈值(等于放弃判据)」之间二选一。 故「转发壳」(方法体只有一条转发语句、且剥掉注释与空白后长度 ≤forwarding_shell_max_chars,缺省 140)不计入阈值;报告里同时给出方法总数 与壳数,原始信号不丢。policy 写法 含义 不写 forwarding_shell_max_chars用缺省 140(老 policy 无需改动即可享受修正) forwarding_shell_max_chars: 0关闭该区分,所有方法都算实质方法,判定逐字退回旧行为 forwarding_shell_max_chars: <N>自定义刻度(方法体有效长度 ≤ N 才算壳) 泛型实例化各自成条(v0.9.0 起)。按 Go 规范,按实例特化的方法 (
func (m *Manager[int]) C())只属于Manager[int],不是Manager的方法。 剥掉实参会把它并进基类型——两个实例化各挂 20 个特化方法就被判成 「Manager 有 40 个方法」,而它其实不胖(方向是虚高 ⇒ 假红)。 故聚合键保留实参:报告里出现pkg.Manager[int]/pkg.Manager[string], 一眼看出是哪个实例化胖。 非泛型类型的键逐字不变(pkg.Report仍是pkg.Report)——这条必须成立, 否则改动会波及所有宿主,而不只是泛型用户(宿主仓实测无泛型接收者,零影响)。判据走
go/ast(不是逐行正则)——「一个函数体是不是只有一条转发语句」是结构性 问题,只有语法树能判。实现见archcheck/godobject.go,判据的由来与实测数据见 issue #13。