diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 27995ce..af0dec8 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -6,6 +6,22 @@ All notable changes to this repository should be documented in this file. The format loosely follows Keep a Changelog and can be adapted to the team's habits. 本文档参考了 Keep a Changelog 的思路,也可以根据团队习惯调整。 +## [0.3.7] - 2026-10-03 + +### Done / 完成 + +- P14 splits the `ContentService` god object in crearte-server (0.18.0, merge `fe810dd`, merge-base `dbf7fe5`): an 856-line file carrying five unrelated responsibility lines becomes five sub-service files plus an 82-line composition root, and the five handlers each narrow from 22 visible methods to a consumer-defined interface of 4/5/2/5/6 with zero over-declaration and zero orphaned methods. `cmd/catalog.go` stops paying the god object's tax (dependency slots 4 → 2, the `nil` bundle placeholder gone). Pure structural refactor with no behaviour change — 25 methods moved byte-for-byte, `NewContentService`'s four-parameter signature unchanged character for character, `cmd/serve.go` untouched, and not one existing `*_test.go` modified. +- P14 拆分 crearte-server 的 `ContentService` god object(0.18.0,merge `fe810dd`,merge-base `dbf7fe5`):一个承载五条互不相干职责线的 856 行文件,变成五个子 service 文件加一个 82 行的组合根;五个 handler 各自从 22 个可见方法收窄到 4/5/2/5/6 个消费方定义的接口,零过声明、零孤儿方法。`cmd/catalog.go` 不再为 god object 交税(依赖槽 4 → 2、`nil` bundle 占位消失)。纯结构重构、零行为变更——25 个方法逐字节迁移、`NewContentService` 的四参数签名逐字不变、`cmd/serve.go` 未动、无任何既有 `*_test.go` 被修改。 + +- The review ladder ran two rounds and **both caught the controller's own errors**, which is the batch's main result rather than the split itself. The task review (PASS with notes, 7 findings from 10 self-designed mutations) found that the guard purpose the spec states — "prevent methods migrating back to the composition root" — was covered by **no assertion at all**: adding a method to the root left all three assertions PASS, `go build`/`go vet` clean, and the full 11-package suite green. It also found that the spec's own positive control had a bypass: mutation (a) turns red because it *removes* the method from the sub-service, not because anything detects the extra root method, so rewriting it as a copy would have passed green. The branch review (APPROVE with notes, 6 findings from 16 mutation/spike rounds, all run on a HEAD-exported copy proven byte-identical by 140 blob hashes) then found a hole **in the first round's own fix**: the source-scan pattern required a *named* receiver, but Go permits omitting an unused one and a shadowing body is exactly the case that often needs none — `func (*ContentService) CoverURL(k string) string` left build, vet and all seven assertions green while the shadow was effective. One token fixed it. Thirteen notes were adjudicated before merge: four fix-forward commits (`e195be0`, `e04694b`) and the rest accepted-with-reason, spec-wording corrections, or backlog. +- 审查阶梯跑了两轮,**两轮都抓到了控制者自己的错误**——这才是本批的主要产出,而非拆分本身。任务级审查(PASS with notes,10 条自设 mutation 挖出 7 条 findings)发现 spec 自述的守卫目的「防方法迁回组合根」**没有任何断言覆盖**:在组合根上加方法会让三条断言全 PASS、`go build`/`go vet` 全绿、全量 11 包测试也全绿。它还发现 spec 自己的 positive control 有绕行路径:mutation (a) 之所以红,是因为它把方法从子 service **拿走**了,而不是因为任何断言侦测到组合根多出方法,故把它改写成「复制」就会误绿。全分支终审(APPROVE with notes,16 轮 mutation/spike 挖出 6 条 findings,全部跑在以 140 个 blob hash 证明与 HEAD 逐字节一致的导出副本上)接着在**第一轮的修复自身**里找到洞:源码扫描的正则要求接收者**有名**,但 Go 允许省略不用的接收者名,而遮蔽体恰好常不需要它——`func (*ContentService) CoverURL(k string) string` 会让 build、vet 与七条断言全绿,而遮蔽确实生效。一个 token 就修好了。13 条 note 在合并前逐条裁定:4 条 fix-forward(`e195be0`、`e04694b`),其余为 accepted-with-reason、spec 措辞纠正或挂账。 + +- Two fixes a reviewer proposed were rejected on measurement, and the branch review confirmed both rejections on the real repo: asserting `NumMethod() == 0` on the value type is unsatisfiable because embedding `*T` puts its methods in both the value and pointer method sets — the legal tree already reports 22, making it a vacuously *false* assertion, the mirror image of the vacuously *true* one P13 shipped; and comparing `Method.Func` identity cannot detect shadowing because promoted methods are forwarding wrappers that already differ on the legal tree (`9683808 != 9511104` unshadowed, `9515680 != 9511104` shadowed). Two lessons went into the spec: a vacuously false guard is as worthless as a vacuously true one and is *more* likely to pass review by looking strict; and a claim that some check is "the only way" must enumerate the forms it covers (named/unnamed receiver, value/pointer, exported/unexported) or it becomes the next reviewer's counterexample — the review overturned two such claims I had already committed. +- 审查者提议的两条修法经实测被否决,全分支终审在真仓复核确认两条驳回都正确:断言值类型的 `NumMethod() == 0` 不可满足,因为嵌入 `*T` 会使其方法进入值类型与指针类型两者的方法集——合法树上已经报 22,故那是恒**假**断言,正是 P13 交付的恒**真**断言的镜像形态;而比较 `Method.Func` 同一性无法侦测遮蔽,因为提升方法本身是 forwarding wrapper,合法树上两者已经不同(未遮蔽 `9683808 != 9511104`、遮蔽 `9515680 != 9511104`)。两条教训写入 spec:恒假守卫与恒真守卫同样无价值,且**更容易因看起来严格而通过审查**;声称某个检查是「唯一手段」时必须枚举它覆盖的形态(有名/匿名接收者、值/指针、导出/未导出),否则它会成为下一个审查者的反例——本轮审查推翻了我两处已入库的这类声明。 + +- Spec re-pinned twice (`11fa1e2` after the task review, `33c2d8d` after the branch review) and ROADMAP updated: D-J amended from three assertions to seven, the mutation list extended to a–h with (a)'s bypass annotated, line counts pinned to the `wc -l` convention, D-D recording that `ErrSubmissionConflict` is also used at `account.go:136` outside the two lines that share it, and the risk table gaining two Route C properties — the root has no named fields so `s.objects` is an ambiguous selector rejected at compile time (a barrier earlier than any guard), and `go vet` stays silent on a root method shadowing a promoted one. The third wave table gains the P14 row and the doc index marks it executed against the merge commit; backlog gains `Approve` at 169 lines (`moderation.go:47-215`, seven phases already mapped), `memory_content.go`'s 814-line test double, handler error-mapping dedup, the `now`-field audit for `AccountService`/`CleanupService`, assertion 6 pinning field names rather than types, the lexical scan's deliberate over-strictness on block comments, and orphan private helpers. It also logs a dark-mode gap found while surveying the next batch: P13's token work covered only the `src/index.html` entry and missed the second Vite entry `bootstrap` (`vite.config.ts:21`), whose loading screen hardcodes light values — so dark-theme users see a white flash on every virtual game launch. +- spec 两次 re-pin(任务级审查后 `11fa1e2`、全分支终审后 `33c2d8d`)并更新 ROADMAP:D-J 从三类断言增订为七类、mutation 清单扩到 a–h 并注解 (a) 的绕行路径、行数口径钉为 `wc -l`、D-D 补记 `ErrSubmissionConflict` 还被两线之外的 `account.go:136` 使用、风险表新增两条 Route C 性质——组合根零具名字段故 `s.objects` 是编译期即拒绝的 ambiguous selector(比任何守卫都更早的屏障),以及 `go vet` 对组合根方法遮蔽提升方法保持沉默。第三波表新增 P14 行、文档索引按 merge commit 标记已执行;挂账新增 169 行的 `Approve`(`moderation.go:47-215`,七阶段已测绘)、`memory_content.go` 的 814 行测试替身、handler 错误映射去重、`AccountService`/`CleanupService` 的 `now` 字段复核、断言 6 只钉字段名不钉类型、词法扫描对块注释的刻意过严、孤儿私有方法。另登记一条在为下一批取证时发现的暗色缺口:P13 的令牌化只覆盖了 `src/index.html` 入口,漏了第二个 Vite 入口 `bootstrap`(`vite.config.ts:21`),其加载屏硬编码亮色值——故暗色用户每启动一个虚拟游戏都会看到一次白闪。 + ## [0.3.6] - 2026-10-03 ### Done / 完成 diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index ed9f132..2c935e3 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -40,6 +40,7 @@ | P11 | 服务端安全硬化 | 限流表硬上界(`maxEntries` 失败关闭:满时先过期清理、仍满则对新面孔 429 且不插入,既有键语义逐字不变;闭合缺陷 B「同窗口 map 无界增长」)+ `LOG_LEVEL`/`LOG_FORMAT` 入口层小写化与 trim(闭合 P8 deferred 缺陷 C)+ `TrustedProxies` 空时启动 `slog.Warn`(D-C 大声留痕)+ 两个 compose-SNAT 网关陷阱反面钉桩 + deploy `TRUSTED_PROXIES` **空默认**与 README 专节(真实 prod 按实际反代 IP/网段配)+ nginx `X-Real-IP` 与九处 `nosniff`/`Referrer-Policy`(`always`)。CSP 与 HSTS 有意不做(前者需独立设计批,后者待 TLS 批) | **完成**(2026-10-02 合并推送:server `9da39b6` 0.17.0(merge-base `d627e12`)/ deploy `740958a` 0.7.0(`12f7c10`)/ crearte `d25c497` 0.24.1(`338acbf`)。五任务各经实现→任务级审查→裁定全 PASS(零关键零重要,9 条次要转打磨批),另加全分支终审(qwen3.8-max)裁定「需修复后合并」——N1(WARN hint 竟建议 subnet 信任,即 D-A′ 判定的绕过配置)/N2(spec 五处 subnet 线未随 re-pin 更新)/N3-N5 均已闭合,N6(validate.yml 加 `TRUSTED_PROXIES` 空默认机器守卫)转打磨批。**本批最重要事件:端到端验收抓出设计级错误并 re-pin D-A→D-A′**——原 D-A 信任整个 compose 子网,但真实 prod 栈实测:docker 对发布端口做 SNAT,nginx 看到的源恒为网桥网关且网关也在子网内,gin 跳过可信网关后**采纳客户端预置 XFF**(`X-Forwarded-For: 8.8.8.8` → `ip=8.8.8.8`,限流可自选桶绕过);而真实浏览器(无 XFF)在 compose 下恒塔缩到网关——缺陷 A 对合法流量根本修不了(SNAT 固有)。spike_snat 六用例信任矩阵钉死后改为:空信任默认(XFF 整体忽略、无绕过、D-C 告警如实提示单桶)+ subnet 固定回退 + README 重写真实 prod 配法 + server 两个反面钉桩。修正后 prod 真实栈回归五项全中(告警在位、伪造 XFF 解析为 nginx 容器 IP 绝非 8.8.8.8、单桶 429 正常、404 上安全头完好、卷全留存)。验收全链:server gofmt/vet/build 净 + test 11 包 ok + `-race` 12 包 ok(Debian 镜像 CGO=1)+ 真库 db-test 集成 ok(skip 守卫 0);crearte vitest **626**/typecheck 零错/build OK/主 e2e **80+1skip**/noauth 4;终审独立复跑六腿与控制者数字逐条一致。教训入账:隔离 spike 证明组件语义、不证明配置在真实拓扑下正确——安全/网络类修复合并前必须走真实流量路径的端到端验证;验收测错路径(用伪造 XFF 当回归)比不测更危险。CSP / TLS+HSTS 挂账待启动(打磨批已于 P12 部分清偿:P9-B 四条中三条(路由弱断言 / 撤评语义 / 播报重念)与 P11-N5/N6 已闭合;P9-B 第四条「spec 口径」在 P9-B 波次内已修;P11 其余九条次要为纯留档、无可动手改动) | | P12 | 窄屏溢出修复与打磨批 | 四个实测窄屏溢出缺陷 + 途中发现的一个:**缺口 A** 页头长 `display_name`(60 字为**合法上限**,非敌意构造)撑破全站每个路由(320px **+380px** / 375px **+325px**);**缺口 B** 账号页昵称 dd 逃逸(`ddRight=592`);**缺口 C** 目录排序 select 的 `shrink-0`(`/games` 320px **+3px**,短名短数据也复现);**缺口 D** 五个 admin 表格零 overflow 包裹层(`/admin/users` 320px **+76px**,**短数据也复现**);**缺口 E** 用户下拉菜单逃逸视口(`menu right=485`,由页头修复一并闭合)。另加两层机器守卫(`e2e/responsive.spec.ts` 12 测试零配置改动进 CI 主腿 + `app/lib/tableOverflow.test.ts` 源码级逐表格父元素判定)与三项打磨(P9B-1 路由弱断言 / P9B-2 撤评语义进可访问名 / P9B-4 播报重念)。零新功能、零后端行为变更 | **完成**(2026-10-02 合并推送:crearte `e59a171` 0.25.0(merge-base `d25c497`)/ server `d56c593` 0.17.1(`9da39b6`)/ deploy `529a988` 0.7.1(`740958a`)。派发遵循一仓一写者:T1/T2/T3/T5 同动 crearte 单一工作树,故交**一个**子代理而非四路争用 `.git/index.lock`;T6(server)/T7(deploy)由控制者本人写。**守卫任务提前为 TDD 第一步**而非第二波:先写守卫看它变红并逐字复现 spec 实测数字(+380/+325/+3px/五处缺失包裹层),再实现到绿——这个顺序使 RED 输出本身成为「有牙」证明,免去事后 revert 自证。任务级审查三份(前端 PASS with 8 notes、server PASS、deploy PASS with 3 notes + 1 条 spec 勘误)+ 全分支终审裁定 **APPROVE with notes(零关键零重要)**,全部 notes 合并前逐条裁定(FE-1/2/6 fix-forward `20215e7`、SD-N1 `fa39d27`、N-F1/2/4 改 spec/FINDINGS/注释,余为 accepted-with-reason)。**终审独立复现三处 mutation**(不采信控制者与审查者结果):回退 T1 → 6 腿 e2e 变红;注释掉表格包裹层 → 源码守卫变红;错误文案回显原始 env → server 两条断言同时变红。**两个挂账项由实测定夺**:①「移动端页头 / 汉堡菜单」**证伪删除**——nav 内容宽仅 74–158px,320/360/375/412/768/1280px 实测零溢出;② nav 链接逐字换行(375px = iPhone 12/13/14 同样如此)**park 待设计决策**——纯外观(无溢出无裁剪无功能损失),`whitespace-nowrap` 在 320px 引入 +11px,七种 gap 收缩组合在「320px + 长名」下全灭(+19~+75px),因 logo(77px)+nowrap nav(138–158px)+截断名(96px) 物理放不下、与页头修复争同一份像素。**诊断关键**:八种一行修法全停在残差 **+32px**(含 `overflow:hidden`)→ 改用烧蚀法定位到 P9-B 自己那个 1px `sr-only` span(`absolute` 无 `top/left`,`static` 包裹层不构成包含块故逃出滚动裁剪)→ 包裹层必须 `relative`。**归因冲突用运行时注入 2×2 消融定案**:768px `/account` 的 +40px 实由缺口 A(页头)驱动而非 dd——回退 dd @768 仍 `over=+0`、回退页头 → `over=+50`;dd 固有右缘恒 592px 故牙口在 <592px 视口。两个修复独立必要、缺一不可,但不是同一视口的同一症状;spec 已 re-pin 五处下游。验收:crearte vitest **635**/74(基线 626/73)/ vue-tsc 零错 / build+build-runtime OK / 主 e2e **92+1skip**(基线 80+1skip)/ noauth 4——控制者复跑两次 + 任务级审查者与终审者各复跑一次,四方零偏差;server gofmt/vet 净 11 包 ok;deploy 四 profile `config -q` 绿 + 守卫三态验证(正向绿 / 注入 CIDR 红 / 删注入行红)。教训入账:① 挂账项实现前必须先证伪或证实;② 所有候选修法失败在同一残差上说明**诊断错了**(换烧蚀法比再加候选有效);③ 运行时样式注入无法验证模板级修法(Vue 把插值编译成单文本节点);④ **源码级 mutation 可能读到 stale `dist/`**(`reuseExistingServer: !CI`)而产生假 GREEN,关键归因须改用运行时注入;⑤ **守卫自身要受与被守卫代码同等的审视**——审查发现注释掉包裹层会使新守卫假绿,与 deploy 守卫里那个恒不匹配的死 CIDR 断言同属假信心类,两者均合并前修掉。CSP / TLS+HSTS / OG 卡片 / 播放计数 / D4 nav 换行设计决策 挂账待启动(UX C 暗色模式已于 P13 交付清偿) | | P13 | 暗色模式 | 两态主题(light ↔ dark)全站落地:首次访问跟随 `prefers-color-scheme`,显式点击后持久化到 `localStorage['crearte.theme.v1']` 并从此压过系统偏好(**无第三态**——有意不做)。每主题 **12 个设计令牌**;暗色在 `html[data-theme="dark"]`(特异性 (0,1,1),无 `!important`/`@apply`)下整体覆盖亮色 `@theme` 块 = **方案 B**,故只需 **3 处 usage 迁移**(方案 A 需 32+ 处)。`index.html` 内联 **pre-paint 脚本**(IIFE + 仅 `var`,在任何 CSS 与 Vue module 之前)设 `data-theme` 防闪白,判定优先级与 `useTheme.effectiveTheme()` 逐字一致;页头 32px 开关(登出态可用)同步翻转 `data-theme` + `theme-color` meta + localStorage;`color-scheme` 由 `data-theme` 驱动使原生控件跟随。五个 `--shadow-hard*` 改 `var()` 传导;新增 `--color-skeleton` 与 `--color-scrim`(**两主题同值不翻转**——暗色 `color-mix(…ink 60%)` 实测 `oklab(L=0.962)` 近白会毁遮罩)。**零生产逻辑变更** | **完成**(2026-10-03 合并推送:crearte `983e7c4` 0.26.0(merge-base `e59a171`;记账 commit `4cfabdd`+`d1aa8c0`)。**一仓一写者**:T1–T5 交一个实现者子代理(同动 crearte 单一工作树),守卫任务 TDD 先行看红。任务级审查 **PASS with notes**(自设 10 条 mutation 挖出 5 条 findings)+ 全分支终审 **APPROVE with notes**(零关键 / 2 重要 / 5 次要 / 3 建议),10 条 note 合并前逐条裁定:4 条 fix-forward(`2433ec7`/`481574c`/`6ec6170`)+ 4 条(`bb2cb42`)+ 5 条 spec 文档纠正 + 1 条 pre-existing 延后 + 2 条 accepted-with-reason。**本批最重要事件:审查阶梯抓出控制者自己写错两次的守卫**——scrim 分离断言两次反转:spec 原文「`ink vs scrim ≥3` 两主题」会让亮色腿误红(亮色实测仅 1.07,两个深色互不分离);控制者第一次改成 `max(填充侧,边框侧) ≥3` **数学恒真**(两侧互补 → 下界 = √cr(paper,ink) = **4.0621 > 3**,50653 采样暴力验证),故把亮色 scrim 改纯白(正是该令牌要防的泛白)时填充侧 1.1165 而 max() 读 18.42、守卫仍绿(任务级审查 M1 抓到);交付形态改为**按主题钉实际分离侧**(亮=填充侧 paper、暗=边框侧 ink),mutation 现能变红。**两条教训入 spec**:① `max(a,b) ≥ k` 是恒真断言高发区(互补时下界非平凡),且**修守卫必须两方向都验**(对合法值不误红 + mutation 下不误绿)——同一错误在 alpha 兜底分支重演(只对当前 Chromium 输出验,漏了 CSS Color 4 百分比/指数 alpha 被解析成 80/1);② **Tailwind v4 扫描全部源文件含 `.test.ts`/`.spec.ts`**——测试注释里的类名字面量会被当候选、烧死 utility 进产物(实现者用拼接修对一处,控制者八分钟后在自己裁定 commit 里重新引入,靠重建抓出 → spec §8-5b)。**终审另暴露一处 pre-existing 缺陷并延后不忽视**:`runtime/host/GameHost.vue:39` 降级外链徽标(`bg-accent text-paper`,11px bold)实测**亮色 `paper on accent` = 3.2590 < 4.5 FAIL**(暗色 7.1218 过),根因是 spike-0 的 grep 与 `noHardcodedColor` 守卫都只覆盖 `app/`、**漏了与 `app` 同级的 `runtime/`**;该违规自 P9-B 就在、P13 未使其变差且不属 P13 范围 → 三方面处置(守卫扫描根已含 `runtime/`、spec §3.2「accent 全仓唯一/纯非文本令牌」的假声明已纠正、违规本身登记可访问性打磨批)。守卫体系:`contrast.test.ts` **重构为按主题分块解析**(旧单 Map 解析器逐字保留为 `legacySingleMapParse` **反面钉桩**,防后人「简化」回去——旧解析器在暗色块存在后会把九个旧键静默解析成暗色值而断言标签仍写 light palette = 守卫悄悄变只守暗色)+ 新增 `noHardcodedColor.test.ts`(沿用 P12 `tableOverflow` 屏蔽范式,扫 `app/`+`runtime/`,含 2 positive + 1 negative control 防「扫 0 文件也报 0 违规」的假信心)+ `themeBootstrap.test.ts`(钉双写:同键名、两侧 stored 白名单、判定序、IIFE+var、theme-color、color-scheme meta)+ `dark.spec.ts` **10 腿**(含 pre-paint 归因腿:拦 JS 包后 `data-theme` 仍 dark,证明是内联脚本而非挂载后设置;反方向 stored 压过 system 腿;垃圾 stored 被忽略腿——后两条是 finding #2/N2 的行为解药,因文本位置守卫不鉴别语义重排)。验收:crearte vitest **688/79**(基线 635/74)/ vue-tsc 零错 / build+build-runtime OK / 主 e2e **102+1skip**(基线 92+1skip,P12 的 12 条 responsive 腿未动全绿)/ noauth 4——控制者与两位审查者各自独立复跑;产物 `main-C7966Gm-.css`:`var(--color-*)`=75、内联 `#141414`=2、暗色块在位、死 utility 双双归零、`--tw-shadow` 含 `var(--color-ink)`。挂账新增:第三态「恢复跟随系统」(spike 3 证明 header 在 @320px 最坏格无像素容纳带标签控件)、CSP 落地时内联 pre-paint 脚本需 `nonce`、死令牌 `--color-info`(`bg-info`/`text-info` 零实例)、SFC `