docs: re-pin the P14 spec after branch-review adjudication

The branch review approved with notes but overturned two universal claims I had
already committed to the spec. Both overturns were independently reproduced
before acting on them, and both were correct.

FR-1 (important): assertion 5's source scan required a NAMED receiver, but Go
permits omitting an unused one, and a shadowing body is exactly the case that
often needs none. Under func (*ContentService) CoverURL(...) the old pattern
matched nothing, so build, vet and all seven assertions stayed green while the
shadow was effective. The regex literal in §5-T1.5 is corrected to the optional
group now in the code, and the claim that assertion 5 is the only mechanical way
to catch shadowing is scoped: it was false before the fix, and after it the
assertion's reach is wider than the original text said, because NumMethod()
exposes only exported names — so an unexported root method is also caught by the
source scan alone.

FR-5: my adjudication of F4 said the two layers 'cover completely'. That is a
universal claim and an orphan private helper on the wrong line is its
counterexample (assertion 2 filters on IsExported, and Go does not report unused
methods). The wording is narrowed to what the layers actually cover, with the
reason the verdict still holds: the third layer can only ever produce dead code.

Both are recorded as instances of the same defect I keep committing — stating a
conclusion before establishing its range. §8 rule 5 already required universal
claims to have universal-range evidence; these were two places I did not follow
my own rule.

The lesson is written into the spec rather than only the ledger: when claiming a
guard is the only thing that catches a failure mode, enumerate the forms first
(named/unnamed receiver, value/pointer, exported/unexported), or the claim
becomes the next reviewer's counterexample.
This commit is contained in:
2026-10-03 12:26:55 +08:00
parent 11fa1e2d26
commit 33c2d8d4b8
@@ -129,7 +129,7 @@ python AST 级扫描 `content.go` 内部调用图:同线内部调用 9 处,*
> ⚠️ **RE-PIN(2026-10-03,任务级审查 F1 important)**:原 D-J 的**三条断言与其自述目的不匹配**——三项里的「防方法迁回组合根」**没有任何一条断言覆盖**(断言①只数字段、②只看子 service、③只查字段名)。实测(审查者 M-3b + 控制者独立复现):在组合根上新增方法后**三断言全 PASS、`go build`/`go vet` 绿、全量 11 包测试也全绿** → god object 可静默回潮,零机械信号。另实测 `go vet` **不报告**组合根方法对提升方法的遮蔽(M-9,`vet_exit=0`)。 > ⚠️ **RE-PIN(2026-10-03,任务级审查 F1 important)**:原 D-J 的**三条断言与其自述目的不匹配**——三项里的「防方法迁回组合根」**没有任何一条断言覆盖**(断言①只数字段、②只看子 service、③只查字段名)。实测(审查者 M-3b + 控制者独立复现):在组合根上新增方法后**三断言全 PASS、`go build`/`go vet` 绿、全量 11 包测试也全绿** → god object 可静默回潮,零机械信号。另实测 `go vet` **不报告**组合根方法对提升方法的遮蔽(M-9,`vet_exit=0`)。
> >
> **D-J 增订为七类断言**(④–⑦ 为 re-pin 后补,已由 commit `e195be0` 交付):④ `ContentService` 的**指针**方法名集合恰为五线导出方法之并(22 个)——抓「组合根新增方法」⑤ **源码扫描**:包内非测试 `.go` 里不得存在任何以 `ContentService` 为接收者的方法声明——**这是唯一能抓「遮蔽提升方法」的机械手段**(遮蔽后反射方法名集合不变、`go vet` 沉默)⑥ 五个子 service 的**字段名集合**逐字等于其依赖面(§1.4)——把「无可变内部状态」这个 §0 赖以论证可拆分的不变量变成机器契约(原断言③只禁字面名 `now`,故 `clock`/`logger`/`cache` 任何名字都能静默通过:钉的是名字而非不变量)⑦ 实例化 `NewContentService(nil, nil, nil, nil)` 后断言五个嵌入指针**均非 nil**——原断言①是纯静态类型检查,故构造器漏装某条线时 build/vet/守卫全绿,故障以运行时 nil 解引用 panic 出现(M-7)。 > **D-J 增订为七类断言**(④–⑦ 为 re-pin 后补,已由 commit `e195be0` 交付):④ `ContentService` 的**指针**方法名集合恰为五线导出方法之并(22 个)——抓「组合根新增方法」⑤ **源码扫描**:包内非测试 `.go` 里不得存在任何以 `ContentService` 为接收者的方法声明——这是唯一能抓「遮蔽提升方法」的机械手段(遮蔽后反射方法名集合不变、`go vet` 沉默),也是唯一能抓「**未导出**的组合根方法」的手段(`NumMethod()` 只暴露导出名,故断言④看不见它们)。⚠️ **RE-PIN(全分支终审 FR-1,important)**:④–⑦ 原由 `e195be0` 交付,但⑤的正则当时把接收者名写成**必需**分组(`\s*\w+\s+`),而 Go 允许省略不用的接收者名——遮蔽体恰好常不需它(`func (*ContentService) CoverURL(...)`),故该形态下 build/vet/七断言全绿而遮蔽确实生效(终审实测:经组合根调用返被接管值、直调子 service 返原值)。已由 `e04694b` 改为可选分组 `(?:\w+\s+)?` 并双向验证(合法树 7/7 绿;匿名指针/匿名值/有名三形态均 A5 红;负控制不过度捕获 `ContentServiceFoo`)。⑥ 五个子 service 的**字段名集合**逐字等于其依赖面(§1.4)——把「无可变内部状态」这个 §0 赖以论证可拆分的不变量变成机器契约(原断言③只禁字面名 `now`,故 `clock`/`logger`/`cache` 任何名字都能静默通过:钉的是名字而非不变量)⑦ 实例化 `NewContentService(nil, nil, nil, nil)` 后断言五个嵌入指针**均非 nil**——原断言①是纯静态类型检查,故构造器漏装某条线时 build/vet/守卫全绿,故障以运行时 nil 解引用 panic 出现(M-7)。
> >
> **两条被 Go 提升规则 spike 实测否决的修法**(勿再尝试,理由已写入 `architecture_test.go` 注释):(i) 「断言 `reflect.TypeOf(ContentService{}).NumMethod() == 0`」**不成立**——嵌入的是 `*T`,其方法**同时进入值类型与指针类型的方法集**,故合法树上值类型 NumMethod 已是 22,该断言**恒假**(P13 恒真断言的镜像形态)。(ii) 「比 `Method(i).Func` 的同一性以侦测遮蔽」**不成立**——提升方法本身是 forwarding wrapper,合法树上 `root.CoverURL.Func`(5153408) 与 `CatalogService.CoverURL.Func`(5148192) **已不同**,遮蔽后为 5148288,两侧都 false → **无法区分遮蔽与否**。 > **两条被 Go 提升规则 spike 实测否决的修法**(勿再尝试,理由已写入 `architecture_test.go` 注释):(i) 「断言 `reflect.TypeOf(ContentService{}).NumMethod() == 0`」**不成立**——嵌入的是 `*T`,其方法**同时进入值类型与指针类型的方法集**,故合法树上值类型 NumMethod 已是 22,该断言**恒假**(P13 恒真断言的镜像形态)。(ii) 「比 `Method(i).Func` 的同一性以侦测遮蔽」**不成立**——提升方法本身是 forwarding wrapper,合法树上 `root.CoverURL.Func`(5153408) 与 `CatalogService.CoverURL.Func`(5148192) **已不同**,遮蔽后为 5148288,两侧都 false → **无法区分遮蔽与否**。
| **D-K** | 派发形态 | **一个实现者子代理**做完 T1→T3。所有任务同动 `crearte-server` 单一工作树与 git index | AGENTS.md / `sdd-parallel-dispatch` §1「一仓一写者」;P12/P13 同款 | | **D-K** | 派发形态 | **一个实现者子代理**做完 T1→T3。所有任务同动 `crearte-server` 单一工作树与 git index | AGENTS.md / `sdd-parallel-dispatch` §1「一仓一写者」;P12/P13 同款 |
@@ -280,7 +280,8 @@ func NewGames(svc catalogPort) *Games {
- `ModerationService` = `{ListQueue, Approve, Reject, Unpublish, Republish, UpdateWorkFeatures}` - `ModerationService` = `{ListQueue, Approve, Reject, Unpublish, Republish, UpdateWorkFeatures}`
3. **死字段不复活**:`ContentService` 与五个子 service **都不得有名为 `now` 的字段**(D-H)。 3. **死字段不复活**:`ContentService` 与五个子 service **都不得有名为 `now` 的字段**(D-H)。
4. **组合根方法集恰为提升之并**:`reflect.TypeOf(&ContentService{})` 的方法名集合逐字等于断言 2 五个集合的并(22 个)。**必须用指针类型**(子 service 方法全是指针接收者)。抓「组合根新增方法」。 4. **组合根方法集恰为提升之并**:`reflect.TypeOf(&ContentService{})` 的方法名集合逐字等于断言 2 五个集合的并(22 个)。**必须用指针类型**(子 service 方法全是指针接收者)。抓「组合根新增方法」。
5. **源码不得声明组合根方法**:扫描包内非测试 `.go`,任何匹配 `^func\s+\(\s*\w+\s+\*?ContentService\s*\)` 的行都是违规。**这是唯一能抓「遮蔽提升方法」的手段**(遮蔽后断言 4 的名字集合不变、`go vet` 也沉默)。仓内有先例:`bundle/vector_test.go`、`cmd/catalog_test.go` 都用 `os.ReadFile`。 5. **源码不得声明组合根方法**:扫描包内非测试 `.go`,任何匹配 `^func\s+\(\s*(?:\w+\s+)?\*?ContentService\s*\)` 的行都是违规。**接收者名必须是可选分组**(全分支终审 FR-1,important,已由 commit `e04694b` 修正):Go 允许省略不使用的接收者名,而遮蔽体恰好常常不需要它(`func (*ContentService) CoverURL(k string) string { return "X" }`);旧写法把名字当必需,故该形态下 build/vet/七条断言全绿而遮蔽**确实生效**。本扫描是**词法级**的:块注释内部以 `func (…ContentService)` 开头的行也会命中(FR-2 实测假红)——这是**刻意的过严**,不要为此放宽正则;若真被规约注释误伤,改法是剥掉 `/* */` 与 `//` 后再扫,而不是删掉这条断言。仓内有先例:`bundle/vector_test.go`、`cmd/catalog_test.go` 都用 `os.ReadFile`。
- ⚠️ **范围限定(终审 FR-1 推翻了我原先的全称声明)**:原文曾写「这是**唯一**能抓『遮蔽提升方法』的手段」。在 FR-1 修正**之前**这句为假(匿名接收者形态连它也抓不到)。修正后它在**导出与未导出方法的全部接收者形态**上成立,且范围比原文更宽:因 `reflect.Type.NumMethod()` **只暴露导出方法**,断言 4 的视野仅 22 个导出名,故**未导出的组合根方法也只有本断言能抓**(四轮 mutation 仅变方法名大小写即实测坐实:未导出两轮 A4 绿/A5 红,导出两轮 A4 红/A5 红)。
6. **各线字段名集合**:五个子 service 的字段名逐字等于 §1.4 的依赖面——`CatalogService={objects,store}`、`ReactionService={store}`、`UploadService={bundle,objects,store,users}`、`SubmissionService={objects,store}`、`ModerationService={objects,store,users}`。抓「重新长出内部状态」与「依赖面被悄悄放宽」。 6. **各线字段名集合**:五个子 service 的字段名逐字等于 §1.4 的依赖面——`CatalogService={objects,store}`、`ReactionService={store}`、`UploadService={bundle,objects,store,users}`、`SubmissionService={objects,store}`、`ModerationService={objects,store,users}`。抓「重新长出内部状态」与「依赖面被悄悄放宽」。
7. **构造器不漏装**:`NewContentService(nil, nil, nil, nil)` 后五个嵌入指针均非 nil。传 `nil` 依赖是安全的:构造器只做纯组装、不解引用参数(既有测试已有多处以 `nil` bundle 构造,已证安全)。 7. **构造器不漏装**:`NewContentService(nil, nil, nil, nil)` 后五个嵌入指针均非 nil。传 `nil` 依赖是安全的:构造器只做纯组装、不解引用参数(既有测试已有多处以 `nil` bundle 构造,已证安全)。
@@ -366,7 +367,7 @@ docker run --rm -v "$PWD/src:/src" -w /src \
| **测试被迫修改 = 设计失败的信号** | §4 末条 + T3 验收硬指标「既有 `*_test.go` 修改文件数 = 0」。若实现者发现必须改测试,**停下来报告**而不是改 | | **测试被迫修改 = 设计失败的信号** | §4 末条 + T3 验收硬指标「既有 `*_test.go` 修改文件数 = 0」。若实现者发现必须改测试,**停下来报告**而不是改 |
| **架构守卫本身写成败 assertion** | T1 的 mutation 自查(a–h)必须附红/绿输出。P13 教训:`max(a,b) ≥ k` 形态恒真、positive control 只钉一个子树 → **守卫自身要受同等审视**。**P14 实证了这条风险的两种形态**:(i) 恒真(P13)与恒假(本批被否决的 `NumMethod()==0` 修法)都要防;(ii) **守卫覆盖不全**比恒真更隐蔽——本批三条断言各自都真有牙(M-1b/M-2/M-5 双向验证实证),但合起来漏掉了「组合根长方法」整个维度,且 spec 自己的 positive control (a) 有绕行路径(见 §5-T1 的 RE-PIN) | | **架构守卫本身写成败 assertion** | T1 的 mutation 自查(a–h)必须附红/绿输出。P13 教训:`max(a,b) ≥ k` 形态恒真、positive control 只钉一个子树 → **守卫自身要受同等审视**。**P14 实证了这条风险的两种形态**:(i) 恒真(P13)与恒假(本批被否决的 `NumMethod()==0` 修法)都要防;(ii) **守卫覆盖不全**比恒真更隐蔽——本批三条断言各自都真有牙(M-1b/M-2/M-5 双向验证实证),但合起来漏掉了「组合根长方法」整个维度,且 spec 自己的 positive control (a) 有绕行路径(见 §5-T1 的 RE-PIN) |
| **RE-PIN:组合根上直接访问依赖会编译失败(Route C 的额外性质,非缺陷)** | 组合根 `ContentService` **零具名字段**,故 `s.store` / `s.users` / `s.objects` / `s.bundle` 在组合根上都是 **ambiguous selector**(`catalog`/`upload`/`submission`/`moderation` 四个子 service 都有 `objects`,Go 提升规则下深度相同即歧义)。要访问须限定为 `s.CatalogService.objects` 这种形式。**这是比架构守卫更早的一道防线**:往组合根直接摸依赖的新代码编译期即失败。反过来也解释了为何 mutation (b) 加**具体字段**后守卫是唯一防线——加具体字段本身在 Go 里可编译(不与嵌入冲突) | | **RE-PIN:组合根上直接访问依赖会编译失败(Route C 的额外性质,非缺陷)** | 组合根 `ContentService` **零具名字段**,故 `s.store` / `s.users` / `s.objects` / `s.bundle` 在组合根上都是 **ambiguous selector**(`catalog`/`upload`/`submission`/`moderation` 四个子 service 都有 `objects`,Go 提升规则下深度相同即歧义)。要访问须限定为 `s.CatalogService.objects` 这种形式。**这是比架构守卫更早的一道防线**:往组合根直接摸依赖的新代码编译期即失败。反过来也解释了为何 mutation (b) 加**具体字段**后守卫是唯一防线——加具体字段本身在 Go 里可编译(不与嵌入冲突) |
| **RE-PIN:`go vet` 不报告组合根方法对提升方法的遮蔽** | 审查者 M-9 实测 `vet_exit=0`:在组合根上写一个与提升方法同名同签名的方法(Go 深度规则下 depth 0 胜出)会**静默接管**全部调用,lint 层零信号。故断言 5(源码扫描)不可省——反射方法名集合在遮蔽后不变、断言 4 会保持绿(已由 mutation (e) 实证)。**这两条同属「组合根一旦长出代码就静默出问题」的同一风险面**,也是 §2.1 方案 B(将来若删除组合根)的收尾条件之一:只要组合根存在,就必须有断言 4+5 守着它 | | **RE-PIN:`go vet` 不报告组合根方法对提升方法的遮蔽** | 审查者 M-9 实测 `vet_exit=0`:在组合根上写一个与提升方法同名同签名的方法(Go 深度规则下 depth 0 胜出)会**静默接管**全部调用,lint 层零信号。故断言 5(源码扫描)不可省——反射方法名集合在遮蔽后不变、断言 4 会保持绿(已由 mutation (e) 实证)。**这两条同属「组合根一旦长出代码就静默出问题」的同一风险面**,也是 §2.1 方案 B(将来若删除组合根)的收尾条件之一:只要组合根存在,就必须有断言 4+5 守着它。⚠️ **RE-PIN(终审 FR-1):这句曾是全称声明而范围为假**——断言 5 原正则要求接收者**有名**,故匿名接收者形态(`func (*ContentService) M(...)`)连它也抓不到,而遮蔽确实生效。现已由 `e04694b` 改为可选分组,该全称声明在**全部接收者形态**上成立;另因 `NumMethod()` 只暴露导出名,断言 4 对**未导出**的组合根方法是盲的,那部分同样只能靠断言 5。**教训:「唯一手段」这类全称声明写下时就要枚举形态(有名/匿名、值/指针、导出/未导出),否则它会成为下一个审查者的反例靶子** |
--- ---