docs: re-pin both P15 specs after task-level review adjudication

Twenty-four pinned corrections across the two specs and their plans, every one
traced to a measured finding. Both task-level reviewers overturned claims I had
written into the specs, and I reproduced each against the base tree before
accepting it (git show, never the working tree, which already carries the fix).

P15-A spec gains two sections. T1.4 records the reviewer's candidate guard for
phase-independence: the spec called "do not merge phases four and six" the most
important constraint in the batch, yet its only stated mitigation was the
byte-level comparison, which is one-shot evidence — after the report is filed,
nothing reddens when someone merges them. The reviewer built CG1/CG2/CG3 and
verified them both ways: green on the legal tree, red on four distinct merge and
degeneration forms, all assertion-red rather than compile-red, while the three
shipped guards stayed green throughout. T4 records the adjudication of nine
findings, including two that undercut the guard the batch itself added: a
half-finished dir parameter that redirects which files are counted but not which
are read, so a scan pointed elsewhere can satisfy its own precondition while the
169-line function it exists to catch stays invisible; and a span scanner keyed to
line-initial func, which means a 125-line package-level closure passes all four
gates and all three guards. Also pinned: the gofmt gate in the four-gate recipe.
gofmt -l lists unformatted files and still exits zero, so the recipe everyone ran
reported gofmt_exit=0 as evidence of formatting cleanliness. Implementer and I
shared that recipe, which is why the same blind spot was computed twice and
caught by neither.

Phase intervals are corrected to the real base-tree line numbers. The prior text
told the implementer to move lines 212-213 into a helper; 213 is the slog.Info
the same spec requires to stay in the caller, so following it would have swapped
what moves with what stays. Phases six and seven also overlapped, each claiming
the transaction error mapping. The skeleton now passes a pointer where its own
note eighteen lines later demanded a pointer receiver, and the transaction
helper's signature carries submissionID, which removes the batch's dependence on
a cross-repository equality argument for the only acceptance criterion it has.

P15-B spec corrects the artifact criterion I had backwards. I wrote that deleting
a token should change the var(--color-*) count; that count measures usage, the
token had zero usage across seven utility suffixes, and it stayed at 75. Had it
moved, that would have meant something referenced the token, contradicting the
dead-token premise. Definition-side and usage-side are separate measures and the
spec now says so.

Leg five is re-pinned as critical. It was the sole enforcement point for the
non-reactive injection decision, and it only pinned literal form: two
type-legal variants that establish the dependency elsewhere in the computed body
pass all ten legs with vue-tsc clean, and the reviewer proved by effectScope
evaluation counting that both really do make the iframe src flip on a theme
change, reloading a running game. The narrowing was introduced by the
implementer's own fix, to avoid a trap comment that spells out the forbidden
literal; but comment masking already neutralizes that comment, so the narrowing
was unnecessary and the second line of defense created the gap. Same shape as
the P14 final review finding a hole in the first round's fix.

Also pinned: the rejected deviation five, with the corrected root cause (all four
freeze attempts used page.route, which does not intercept service worker
registration, worker-issued requests, or navigations synthesized by respondWith;
context.route holds the loading screen past 25 seconds); the missing
dark-system-times-light-hash grid that one mutation slipped past all ten source
legs and all five e2e legs simultaneously; the fourth tautology class the spec's
three warnings omitted, where emptying a loop's driving collection makes it
vacuously true; legs seven and eight, which respectively substring-searched a
hex that occurs three times in the file and matched only double-quoted style
attributes; the copyable code block's comment, which now avoids the three
literals that would false-redden a correct implementation, with the annotation
moved outside the block; and three new discipline entries covering untested
method scope, measurement units, and retaining one-off verification scripts.

Every correction was checked by a script asserting both directions — the pinned
text present and the superseded text gone — plus table column integrity, fence
pairing, and code-block cleanliness. Three earlier attempts at this re-pin failed
on my own errors and were fixed before commit; the working tree was never left in
a half-edited state.
This commit is contained in:
2026-10-03 19:48:48 +08:00
parent 82f0a6d360
commit 02188eb3e0
4 changed files with 87 additions and 36 deletions
+2 -2
View File
@@ -32,10 +32,10 @@
cd crearte-server && docker run --rm -v "$PWD/src:/src" -w /src \ cd crearte-server && docker run --rm -v "$PWD/src:/src" -w /src \
-v crearte_gomod:/go/pkg/mod -e GOCACHE=/gocache -v crearte_gocache:/gocache \ -v crearte_gomod:/go/pkg/mod -e GOCACHE=/gocache -v crearte_gocache:/gocache \
-e GOPROXY=https://goproxy.cn,direct -e GOSUMDB=sum.golang.google.cn \ -e GOPROXY=https://goproxy.cn,direct -e GOSUMDB=sum.golang.google.cn \
golang:1.24-alpine sh -c 'gofmt -l . ; go vet ./... ; go build ./... ; go test ./... -count=1' golang:1.24-alpine sh -c 'gofmt -l . ; test -z "$(gofmt -l .)" ; echo gofmt_gate=$? ; go vet ./... ; go build ./... ; go test ./... -count=1'
``` ```
基线(spec §0,已实测):`gofmt -l .` 空 · `go vet` exit 0 · `go build` exit 0 · `go test ./... -count=1` **11 包 ok**(api ~11.0s 真库集成、service ~3.7s)。 基线(spec §0,已实测):**`test -z "$(gofmt -l .)"` exit 0**(⚠️ 审查者 F3:`gofmt -l` 的语义是「列出需要格式化的文件」,**列出时仍 exit 0**,只在解析失败时 exit 2 → `gofmt_exit=0` **不是证据**;须同时存证 `gofmt -l .` 的**原始输出为空**。控制者与实现者共用旧配方,故同一盲点被复算两次而未被发现)
⚠️ **host Go 1.18 不可用**;**必须带 `GOPROXY=https://goproxy.cn,direct`**(`proxy.golang.org` 本机不可达,默认下载**挂死并杀代理**);冷构建 1–3 min → `background: true` + 耐心 poll,**不要 sleep 循环、不要因没立刻返回就断定失败**。 ⚠️ **host Go 1.18 不可用**;**必须带 `GOPROXY=https://goproxy.cn,direct`**(`proxy.golang.org` 本机不可达,默认下载**挂死并杀代理**);冷构建 1–3 min → `background: true` + 耐心 poll,**不要 sleep 循环、不要因没立刻返回就断定失败**。
⚠️ **`-count=1` 强制实跑**(禁缓存)。 ⚠️ **`-count=1` 强制实跑**(禁缓存)。
+1 -1
View File
@@ -29,7 +29,7 @@
| **T1** | `app/lib/bootstrapTheme.test.ts`(**新增**) | **RED 先行**(spec §5-T1)。**10 腿**:①pre-paint 脚本在 `<style>` 与 `<body>` 之前 ②判定序 hash→prefers→light(`indexOf` 比较,**先断言三者都 `> -1`**)③白名单校验 hash theme(只接受 `light`/`dark`)④IIFE 且只用 `var` ⑤**`GameHost.vue` 注入的是 `effectiveTheme()` 而非 `useTheme().theme.value`** ⑥bootstrap 暗色块六个 hex 与 `main.css` 暗色调色板**逐值一致**(字符串相等,**不得写成 `max(a,b) ≥ k`**)⑦bootstrap 亮色六个 hex **未被改动** ⑧`bootstrap/index.html` 内**不存在 inline `style` 里的 `color:`** ⑨**前提检查**(读到 ≥3 个文件且 bootstrap html 长度 >500)⑩`AA_PAIRS` 含 `['ink','surface']` 且 `REQUIRED_KEYS` 不含 `info`、长度 `toBe(11)` | RED 输出**逐字存证** `.superpowers/sdd-p15b/impl-evidence/t1-red.txt`,并**逐腿标注红/绿**:腿 1/2/5/6/8/10 须红,**腿 7/9 应当已绿**(它们是"不得回退"钉桩,不是新功能断言——spec §5-T1 明写这是有意的) | | **T1** | `app/lib/bootstrapTheme.test.ts`(**新增**) | **RED 先行**(spec §5-T1)。**10 腿**:①pre-paint 脚本在 `<style>` 与 `<body>` 之前 ②判定序 hash→prefers→light(`indexOf` 比较,**先断言三者都 `> -1`**)③白名单校验 hash theme(只接受 `light`/`dark`)④IIFE 且只用 `var` ⑤**`GameHost.vue` 注入的是 `effectiveTheme()` 而非 `useTheme().theme.value`** ⑥bootstrap 暗色块六个 hex 与 `main.css` 暗色调色板**逐值一致**(字符串相等,**不得写成 `max(a,b) ≥ k`**)⑦bootstrap 亮色六个 hex **未被改动** ⑧`bootstrap/index.html` 内**不存在 inline `style` 里的 `color:`** ⑨**前提检查**(读到 ≥3 个文件且 bootstrap html 长度 >500)⑩`AA_PAIRS` 含 `['ink','surface']` 且 `REQUIRED_KEYS` 不含 `info`、长度 `toBe(11)` | RED 输出**逐字存证** `.superpowers/sdd-p15b/impl-evidence/t1-red.txt`,并**逐腿标注红/绿**:腿 1/2/5/6/8/10 须红,**腿 7/9 应当已绿**(它们是"不得回退"钉桩,不是新功能断言——spec §5-T1 明写这是有意的) |
| **T2** | `app/lib/contrast.test.ts`(改)+ `app/styles/main.css`(改) | 按 spec §3.2/§3.3:`AA_PAIRS` 插入 `['ink','surface','卡片/表格/表单底(bg-surface 62 处,58 靠 body 继承 text-ink)亮18.42/暗14.85']`(**不得改其它 10 对**);删 `main.css:12`(亮)与`:53`(暗) 两行 `--color-info`;`REQUIRED_KEYS` 删 `'info',`;四处 "12 令牌" 文案改 "11 令牌"(`:47,74,119,121`)。⚠️ **`LEGACY_KEYS`(`:197`)不动**(九键不含 `info`,已实测) | `npx vitest run` **80 文件 / 688+N 全绿**(N = T1 腿数 ≥10);`--color-info` 全仓命中 **0**;`AA_PAIRS` **11** 对;`REQUIRED_KEYS` **11** 项;`LEGACY_KEYS` **9** 项不动 | | **T2** | `app/lib/contrast.test.ts`(改)+ `app/styles/main.css`(改) | 按 spec §3.2/§3.3:`AA_PAIRS` 插入 `['ink','surface','卡片/表格/表单底(bg-surface 62 处,58 靠 body 继承 text-ink)亮18.42/暗14.85']`(**不得改其它 10 对**);删 `main.css:12`(亮)与`:53`(暗) 两行 `--color-info`;`REQUIRED_KEYS` 删 `'info',`;四处 "12 令牌" 文案改 "11 令牌"(`:47,74,119,121`)。⚠️ **`LEGACY_KEYS`(`:197`)不动**(九键不含 `info`,已实测) | `npx vitest run` **80 文件 / 688+N 全绿**(N = T1 腿数 ≥10);`--color-info` 全仓命中 **0**;`AA_PAIRS` **11** 对;`REQUIRED_KEYS` **11** 项;`LEGACY_KEYS` **9** 项不动 |
| **T3** | `bootstrap/index.html`(改)+ `runtime/host/adapters.ts`(改)+ `runtime/host/GameHost.vue`(改) | 按 spec §3.1(a)–(e):head 内 `<style>` **之前**插 pre-paint 脚本(**逐字对齐 spec 给的代码块**,含 CSP 注释与 `catch` 兜底);`<style>` 加 `html[data-theme="dark"]` 覆盖块(**亮色值逐字不变**,`color-scheme: dark` 随块声明一次);**删两处 inline `color`**(`:25` 的 `#a3a3a3`、`:27` 的 `#f87171`,各保留 `font-size`);`resolveRuntimeTargets` 的 `opts` 加**可选**键 `theme?: 'light' \| 'dark'`,**仅当存在时**才写 `fragment.theme`;`GameHost.vue` 注入 **`effectiveTheme()`** 并写明陷阱注释 | `npx vitest run` 全绿;`npm run typecheck`(`vue-tsc --noEmit`)**零错**;**`adapters.test.ts` 4 处既有调用零修改仍绿**(验证 `theme` 是可选键);既有 `*.test.ts` 修改数 = **1**(仅 `contrast.test.ts`,须在报告显式声明) | | **T3** | `bootstrap/index.html`(改)+ `runtime/host/adapters.ts`(改)+ `runtime/host/GameHost.vue`(改) | 按 spec §3.1(a)–(e):head 内 `<style>` **之前**插 pre-paint 脚本(**逐字对齐 spec 给的代码块**,含 CSP 注释与 `catch` 兜底);`<style>` 加 `html[data-theme="dark"]` 覆盖块(**亮色值逐字不变**,`color-scheme: dark` 随块声明一次);**删两处 inline `color`**(`:25` 的 `#a3a3a3`、`:27` 的 `#f87171`,各保留 `font-size`);`resolveRuntimeTargets` 的 `opts` 加**可选**键 `theme?: 'light' \| 'dark'`,**仅当存在时**才写 `fragment.theme`;`GameHost.vue` 注入 **`effectiveTheme()`** 并写明陷阱注释 | `npx vitest run` 全绿;`npm run typecheck`(`vue-tsc --noEmit`)**零错**;**`adapters.test.ts` 4 处既有调用零修改仍绿**(验证 `theme` 是可选键);既有 `*.test.ts` 修改数 = **1**(仅 `contrast.test.ts`,须在报告显式声明) |
| **T4** | e2e / 产物验证 + mutation 自查 | 按 spec §5-T4:`npm run build` exit 0(⚠️ 它会类型检查 `e2e/*.spec.ts`,P13 spike 6 曾因此 `BUILD_EXIT=2`);`npm run e2e` **≥102+1skip** 不回退(`dark.spec.ts` 10 腿全绿);**新增一腿覆盖 hash 注入**(e2e 断言 iframe `src` hash 含 `theme=dark`;**若 fixture 无虚拟游戏可跑,改为 vitest 腿**用 `@vue/test-utils` 挂 `GameHost` 断言 `targets[0].url` 含 `theme=dark`——**必须有一腿覆盖**,不接受"fixture 不支持所以不测");**产物验证**:`dist/` 里 bootstrap HTML 含暗色块、**不含** `#a3a3a3` 与 `#f87171`,主应用 CSS 里 `--color-info` 零命中、`var(--color-*)` 计数与 P13 交付值 **75** 比对并**解释差异**;**mutation (a)–(j) 逐条**附输出 | 全部输出存证 `.superpowers/sdd-p15b/impl-evidence/`;产物 grep **用 `grep -F`**(spec §8-6:手写转义与带引号模式对 minified 产物假阴性) | | **T4** | e2e / 产物验证 + mutation 自查 | ⚠️ **RE-PIN 2026-10-03(审查裁定后)**:**偏离 5 已驳回**——端到端腿**可**确定性化,用 **`context.route`(不是 `page.route`)** + inert sw.js(`page.route` 拦不到 SW 注册请求 / SW 发起的请求 / 被 SW `respondWith` 合成的导航,实测命中 0);断言须含 **`snap.url === 父侧 iframe src`**(因果链闭合);另加**行为腿**(游戏页切主题 → iframe `src` 不变,D-B 的唯一行为级防线)与**反方向格**(子侧 `[系统暗色] × [hash=light]` → 期望 `light`,H1/H3 的唯一鉴别格)。产物验证**必须在 e2e 之后重跑生产 `npm run build`** 之上做,并用 `grep -c -F 'localhost:4173'` = **0** 证明量的是生产构建(控制者当日量了 e2e 残留 → 7069 vs 7056 的假矛盾)。计数须**同时给 raw 与屏蔽注释后两个数**、字节用 `wc -c`(详见 spec §5-T4.3/§5-T4.4 与 `adjudication.md` §三) | **端到端腿 + 行为腿 + 反方向格三类都跑绿**;mutation 复验须跑审查者自设的 **G1/G2/H1/H3/F1/F2/H4 七条**(spec 的 (a)–(j) 对这七条**全部无牙**)+ 全部对照组 + 合法树 |
--- ---
@@ -11,7 +11,7 @@
## §0 基线(已实测,2026-10-03 12:50) ## §0 基线(已实测,2026-10-03 12:50)
**四门**(dockerized `golang:1.24-alpine`,AGENTS.md 硬红线;host Go 1.18 不可用):`gofmt -l .` 空 · `go vet ./...` exit 0 · `go build ./...` exit 0 · `go test ./... -count=1` **11 包 ok**(`internal/api` ~11.0s 含真库集成、`internal/service` ~3.7s)。 **四门**(dockerized `golang:1.24-alpine`,AGENTS.md 硬红线;host Go 1.18 不可用):**`test -z "$(gofmt -l .)"` exit 0**(⚠️ **不能用 `gofmt -l . ; echo $?`**:`gofmt -l` 的语义是「列出需要格式化的文件」,**列出时仍 exit 0**,只在解析失败时 exit 2 → 旧配方的 `gofmt_exit=0` 不是证据。审查者 F3 实测。同时须把 `gofmt -l .` 的**原始输出**一并存证以证明为空)· `go vet ./...` exit 0 · `go build ./...` exit 0 · `go test ./... -count=1` **11 包 ok**(`internal/api` ~11.0s 含真库集成、`internal/service` ~3.7s)。
**目标函数**:`internal/service/moderation.go:47-215` = **169 行**(`wc -l` 口径,P14 spec §5 已钉该口径)。 **目标函数**:`internal/service/moderation.go:47-215` = **169 行**(`wc -l` 口径,P14 spec §5 已钉该口径)。
@@ -38,11 +38,11 @@
|---|---|---|---|---| |---|---|---|---|---|
| ① 加载 + 状态校验 + 解析 | 48–62 | `Submissions().GetByID` → `ErrNotFound` 映射 `ErrContentNotFound`;`sub.Status != Pending` → `ErrSubmissionConflict`;`ParseSubmissionEnvelope(sub.Payload)`;`p := env.Work` | `ErrContentNotFound` / `ErrSubmissionConflict` / envelope 解析错 | 否 | | ① 加载 + 状态校验 + 解析 | 48–62 | `Submissions().GetByID` → `ErrNotFound` 映射 `ErrContentNotFound`;`sub.Status != Pending` → `ErrSubmissionConflict`;`ParseSubmissionEnvelope(sub.Payload)`;`p := env.Work` | `ErrContentNotFound` / `ErrSubmissionConflict` / envelope 解析错 | 否 |
| ② owner 查取 | 64–70 | **仅** `Kind == NewWork` 时 `s.users.GetByID(sub.SubmitterID)` | `service: resolve submitter: %w` | 否 | | ② owner 查取 | 64–70 | **仅** `Kind == NewWork` 时 `s.users.GetByID(sub.SubmitterID)` | `service: resolve submitter: %w` | 否 |
| ③ 上传解析 | 72–85 | `BundleUploadID`/`CoverUploadID` 各 `Uploads().GetByID`,并校验 `up.ConsumedBy == sub.ID` | `ErrUploadUnavailable` | 否 | | ③ 上传解析 | **72–86** | `BundleUploadID`/`CoverUploadID` 各 `Uploads().GetByID`,并校验 `up.ConsumedBy == sub.ID` | `ErrUploadUnavailable` | 否 |
| ④ **乐观预检** switch | 87–100 | `NewWork`:`Works().GetByID` 已存在 → `ErrWorkIDTaken`;`NewVersion`:`Versions().Get` 已存在 → `ErrVersionExists`;其它 kind 无 case | `ErrWorkIDTaken` / `ErrVersionExists` / **`service: approve precheck: %w`** | 否(用 `s.store`) | | ④ **乐观预检** switch | **88–101** | `NewWork`:`Works().GetByID` 已存在 → `ErrWorkIDTaken`;`NewVersion`:`Versions().Get` 已存在 → `ErrVersionExists`;其它 kind 无 case | `ErrWorkIDTaken` / `ErrVersionExists` / **`service: approve precheck: %w`** | 否(用 `s.store`) |
| ⑤ 对象 Copy | 102–123 | `finalBundleKey = "bundles/"+sub.WorkID+"/"+p.Version+".bin"`;`finalCoverKey = "covers/"+sub.WorkID+"/"+coverUpload.SHA256+path.Ext(...)`;各 `s.objects.Copy` | `ErrUploadUnavailable`(`storage.ErrObjectNotFound`)/ `service: copy bundle|cover: %w` | 否(用 `s.objects`) | | ⑤ 对象 Copy | 102–123 | `finalBundleKey = "bundles/"+sub.WorkID+"/"+p.Version+".bin"`;`finalCoverKey = "covers/"+sub.WorkID+"/"+coverUpload.SHA256+path.Ext(...)`;各 `s.objects.Copy` | `ErrUploadUnavailable`(`storage.ErrObjectNotFound`)/ `service: copy bundle\|cover: %w` | 否(用 `s.objects`) |
| ⑥ **`WithTx` 事务** | 125–206 | `GetByIDForUpdate` 锁行 → **重校** `Status == Pending` → **悲观重检** switch(三分支)→ 建 Work / Version / Touch / UpdateMetadata → `MarkReviewed` | 见 §1.2 | **是**(`tx repository.ContentRepository`) | | ⑥ **`WithTx` 事务** | **125–201** | `GetByIDForUpdate` 锁行 → **重校** `Status == Pending` → **悲观重检** switch(三分支)→ 建 Work / Version / Touch / UpdateMetadata → `MarkReviewed` | 见 §1.2 | **是**(`tx repository.ContentRepository`) |
| ⑦ pending 清理 + slog | 207–214 | 事务错误映射(`repository.ErrConflict` → `ErrSubmissionConflict`);`pendingKeys(bundleUpload, coverUpload)` 逐个 `s.objects.Delete`(**只 `slog.Error` 不返回**,best-effort);`slog.Info("approve", …)`;`return nil` | `ErrSubmissionConflict` / 裸 `err` | 否 | | ⑦ 事务错误映射 + pending 清理 + slog | **202–214** | 事务错误映射(`repository.ErrConflict` → `ErrSubmissionConflict`);`pendingKeys(bundleUpload, coverUpload)` 逐个 `s.objects.Delete`(**只 `slog.Error` 不返回**,best-effort);`slog.Info("approve", …)`;`return nil` | `ErrSubmissionConflict` / 裸 `err` | 否 |
**`WithTx` 签名**(`internal/repository/content.go:38`):`WithTx(ctx context.Context, fn func(ContentRepository) error) error`。 **`WithTx` 签名**(`internal/repository/content.go:38`):`WithTx(ctx context.Context, fn func(ContentRepository) error) error`。
@@ -138,7 +138,7 @@ func ParseSubmissionEnvelope(raw []byte) (SubmissionEnvelope, error) // 值返
|---|---|---|---| |---|---|---|---|
| **D-A** | 拆分粒度 | **按七阶段拆 5 个 helper**(①②③ 合为一个"输入装载"、④ 一个、⑤ 一个、⑥ 一个(内部再拆 3 个 kind 分支)、⑦ 一个),不逐行拆 | §1.1 的阶段边界是**语义边界**(副作用发生点),不是任意切点。①②③ 都只读、都产出 plan 的字段、失败时零副作用 → 合为一个 helper 不损失可读性;④⑤⑥⑦ 各自有独立副作用(预检/Copy/事务/清理)→ 必须分开 | | **D-A** | 拆分粒度 | **按七阶段拆 5 个 helper**(①②③ 合为一个"输入装载"、④ 一个、⑤ 一个、⑥ 一个(内部再拆 3 个 kind 分支)、⑦ 一个),不逐行拆 | §1.1 的阶段边界是**语义边界**(副作用发生点),不是任意切点。①②③ 都只读、都产出 plan 的字段、失败时零副作用 → 合为一个 helper 不损失可读性;④⑤⑥⑦ 各自有独立副作用(预检/Copy/事务/清理)→ 必须分开 |
| **D-B** | plan 结构体 | 新增**包级私有 struct** `approvePlan`,字段:`sub model.Submission`、`p WorkPayload`、`owner model.User`、`bundleUpload *model.Upload`、`coverUpload *model.Upload`、`finalBundleKey string`、`finalCoverKey string` | 七阶段之间传递的就是这七样东西(实测自函数体)。用 struct 而非 7 个返回值:Go 的多返回值超过 4 个即难读,且 ⑤ 要往 plan 里写 `finalBundleKey`/`finalCoverKey` 供 ⑥⑦ 用 | | **D-B** | plan 结构体 | 新增**包级私有 struct** `approvePlan`,字段:`sub model.Submission`、`p WorkPayload`、`owner model.User`、`bundleUpload *model.Upload`、`coverUpload *model.Upload`、`finalBundleKey string`、`finalCoverKey string` | 七阶段之间传递的就是这七样东西(实测自函数体)。用 struct 而非 7 个返回值:Go 的多返回值超过 4 个即难读,且 ⑤ 要往 plan 里写 `finalBundleKey`/`finalCoverKey` 供 ⑥⑦ 用 |
| **D-C** | helper 形态 | ①②③④⑤⑦ → **未导出方法**(需 `s.store`/`s.users`/`s.objects`);⑥ 事务体 → **包级函数** `applyApprovalInTx(ctx, tx repository.ContentRepository, plan approvePlan, adminID, note string) error`;⑥ 内三个 kind 分支 → **包级函数** | §1.3 实测两种形态都不触守卫;选择依据是"是否需要接收者字段"。事务体不碰 `s.*`(只用 `tx`)→ 包级函数更纯,且签名自证"事务内不得访问 store 外的东西"。体例分别同 `deleteAccountLocked` 与 `versionFromUpload` | | **D-C** | helper 形态 | ①②③④⑤⑦ → **未导出方法**(需 `s.store`/`s.users`/`s.objects`);⑥ 事务体 → **包级函数** `applyApprovalInTx(ctx, tx repository.ContentRepository, plan approvePlan, submissionID, adminID, note string) error`;⑥ 内三个 kind 分支 → **包级函数** | §1.3 实测两种形态都不触守卫;选择依据是"是否需要接收者字段"。事务体不碰 `s.*`(只用 `tx`)→ 包级函数更纯,且签名自证"事务内不得访问 store 外的东西"。体例分别同 `deleteAccountLocked` 与 `versionFromUpload` |
| **D-D** | ④与⑥**不合并** | 各自独立 helper,不共享代码 | §1.2:四处实质差异 + 乐观/悲观语义不同 + 错误文案不同。表面重复是**有意的 TOCTOU 双层校验** | | **D-D** | ④与⑥**不合并** | 各自独立 helper,不共享代码 | §1.2:四处实质差异 + 乐观/悲观语义不同 + 错误文案不同。表面重复是**有意的 TOCTOU 双层校验** |
| **D-E** | 事务边界与 Copy 顺序 | **一行不动**:⑤ 的两次 `s.objects.Copy` 必须在 ⑥ `WithTx` **之外**(故 ⑦ 需要清理);⑥ 的全部写入必须在 `WithTx` **之内**;`MarkReviewed` 必须是事务内**最后一个**调用 | 改变顺序即行为变更:Copy 进事务会让长耗时 I/O 持锁;`MarkReviewed` 提前会让"实体建成但状态未改"的中间态可被并发观察到 | | **D-E** | 事务边界与 Copy 顺序 | **一行不动**:⑤ 的两次 `s.objects.Copy` 必须在 ⑥ `WithTx` **之外**(故 ⑦ 需要清理);⑥ 的全部写入必须在 `WithTx` **之内**;`MarkReviewed` 必须是事务内**最后一个**调用 | 改变顺序即行为变更:Copy 进事务会让长耗时 I/O 持锁;`MarkReviewed` 提前会让"实体建成但状态未改"的中间态可被并发观察到 |
| **D-F** | `Features` 三态语义 | `if p.Features == nil { work.Features = existing.Features }` **连注释一起搬**进 MetadataChange helper,**不得"简化"** | 该注释原文:「旧提交(features 采集上线前创建)的 payload 无 features 键:保留现值,避免审批通过时静默清空已发布作品的运行权限;显式 `{}` 才是清空」。三态(nil=保留 / 空 map=清空 / 有值=覆盖)由 `content_test.go:237` 精确钉住,简化即红 | | **D-F** | `Features` 三态语义 | `if p.Features == nil { work.Features = existing.Features }` **连注释一起搬**进 MetadataChange helper,**不得"简化"** | 该注释原文:「旧提交(features 采集上线前创建)的 payload 无 features 键:保留现值,避免审批通过时静默清空已发布作品的运行权限;显式 `{}` 才是清空」。三态(nil=保留 / 空 map=清空 / 有值=覆盖)由 `content_test.go:237` 精确钉住,简化即红 |
@@ -172,11 +172,11 @@ func (s *ModerationService) Approve(ctx context.Context, adminID, submissionID,
if err := s.precheckApprove(ctx, plan); err != nil { // ④(乐观、无锁、Copy 之前) if err := s.precheckApprove(ctx, plan); err != nil { // ④(乐观、无锁、Copy 之前)
return err return err
} }
if err := s.copyApprovalObjects(ctx, plan); err != nil { // ⑤(写 plan.finalBundleKey/finalCoverKey) if err := s.copyApprovalObjects(ctx, &plan); err != nil { // ⑤(写 plan.finalBundleKey/finalCoverKey)
return err return err
} }
err = s.store.WithTx(ctx, func(tx repository.ContentRepository) error { err = s.store.WithTx(ctx, func(tx repository.ContentRepository) error {
return applyApprovalInTx(ctx, tx, plan, adminID, note) // ⑥ return applyApprovalInTx(ctx, tx, plan, submissionID, adminID, note) // ⑥
}) })
if err != nil { if err != nil {
if errors.Is(err, repository.ErrConflict) { if errors.Is(err, repository.ErrConflict) {
@@ -211,14 +211,14 @@ func (s *ModerationService) Approve(ctx context.Context, adminID, submissionID,
- `storage.ErrObjectNotFound` → `ErrUploadUnavailable` 的映射逐字保留;`service: copy bundle|cover: %w` 两条文案逐字保留。 - `storage.ErrObjectNotFound` → `ErrUploadUnavailable` 的映射逐字保留;`service: copy bundle|cover: %w` 两条文案逐字保留。
**⑥ `applyApprovalInTx`**(包级函数): **⑥ `applyApprovalInTx`**(包级函数):
- 搬 126–205 行(`WithTx` 闭包体)。签名收 `(ctx, tx repository.ContentRepository, plan approvePlan, adminID, note string) error`。 - 搬 **126–200** 行(`WithTx` 闭包体)。⚠️ **`:201` 是 `})`,`:202–207` 的事务错误映射属阶段⑦,都不得搬进 helper**(照抄旧区间「126–205」会把 `})` 与闭包外半截搬进去 → 编译红或行为变更)。签名收 `(ctx, tx repository.ContentRepository, plan approvePlan, submissionID, adminID, note string) error`(**含 `submissionID`,见 F9 裁定**)。
- **`GetByIDForUpdate` 锁行 + 重校 `Status == Pending` 必须在最前**(125–134),且 `errors.Is(err, repository.ErrNotFound)` → `ErrSubmissionConflict`(**注意与①不同**:① 映射 `ErrContentNotFound`,⑥ 映射 `ErrSubmissionConflict`——这个差异是刻意的,事务内消失意味着并发删除,语义是冲突而非未找到)。 - **`GetByIDForUpdate` 锁行 + 重校 `Status == Pending` 必须在最前**(125–134),且 `errors.Is(err, repository.ErrNotFound)` → `ErrSubmissionConflict`(**注意与①不同**:① 映射 `ErrContentNotFound`,⑥ 映射 `ErrSubmissionConflict`——这个差异是刻意的,事务内消失意味着并发删除,语义是冲突而非未找到)。
- 三个 kind 分支各自拆包级函数(`createWorkInTx` / `createVersionInTx` / `applyMetadataChangeInTx`),或保留为一个 switch——**由实现者按可读性定**,但 `switch` 的分支顺序与 `MarkReviewed` 在末尾的位置**不得变**。 - 三个 kind 分支各自拆包级函数(`createWorkInTx` / `createVersionInTx` / `applyMetadataChangeInTx`),或保留为一个 switch——**由实现者按可读性定**,但 `switch` 的分支顺序与 `MarkReviewed` 在末尾的位置**不得变**。
- `MetadataChange` 分支的 `Features` 三态**连注释一起搬**(D-F)。 - `MetadataChange` 分支的 `Features` 三态**连注释一起搬**(D-F)。
- 事务内错误**裸返**,不加 `service: approve precheck` 之类包装(§1.2)。 - 事务内错误**裸返**,不加 `service: approve precheck` 之类包装(§1.2)。
**⑦ `cleanupApprovalUploads`**(未导出方法): **⑦ `cleanupApprovalUploads`**(未导出方法):
- 搬 212–213 行。`for _, key := range pendingKeys(plan.bundleUpload, plan.coverUpload)` + `s.objects.Delete` + `if err != nil && !errors.Is(err, storage.ErrObjectNotFound) { slog.Error("approve: pending delete failed", "key", key, "error", err) }` **逐字保留**。 - 搬 **208–212** 行(pending 循环)。⚠️ **`:213` 的 `slog.Info` 与 `:214` 的 `return nil` 留在 `Approve` 本体,不得搬走**(旧区间「212–213」会把该搬走的和该留下的正好搞反)。`for _, key := range pendingKeys(plan.bundleUpload, plan.coverUpload)` + `s.objects.Delete` + `if err != nil && !errors.Is(err, storage.ErrObjectNotFound) { slog.Error("approve: pending delete failed", "key", key, "error", err) }` **逐字保留**。
- **不得返回 error**(best-effort 语义:清理失败不能让已成功的审批变失败)。 - **不得返回 error**(best-effort 语义:清理失败不能让已成功的审批变失败)。
- ⚠️ `slog.Info("approve", …)` 与 `return nil` **留在 `Approve` 本体**,不进 helper——helper 名是 `cleanup`,把成功日志塞进去会让职责不清。 - ⚠️ `slog.Info("approve", …)` 与 `return nil` **留在 `Approve` 本体**,不进 helper——helper 名是 `cleanup`,把成功日志塞进去会让职责不清。
@@ -265,16 +265,43 @@ P14 的七条断言钉的是**类型形态**,没有一条钉**函数长度**
**mutation 自查(实现者必做并附输出,每条测"预期红 + 其余绿 + 非编译红 + 恢复证明")**: **mutation 自查(实现者必做并附输出,每条测"预期红 + 其余绿 + 非编译红 + 恢复证明")**:
- (a) 把 `Approve` 的某阶段**内联回去**(helper 存在但 `Approve` 不调它,改为原地展开)→ `TestApproveIsSmall` 必须红 - (a) 把 `Approve` 的某阶段**内联回去**(helper 存在但 `Approve` 不调它,改为原地展开)→ `TestApproveIsSmall` 必须红
- (b) 给某个 helper 塞代码使 `Approve` 仍 <60 但**某函数 >100**(例如把⑥的三个 kind 分支全塞进 `applyApprovalInTx` 而不拆)→ `TestNoOversizedFunction` 必须红 - (b) 给某个 helper 塞代码使 `Approve` 仍 <60 但**某函数 >100**(例如把⑥的三个 kind 分支全塞进 `applyApprovalInTx` 而不拆)→ `TestNoOversizedFunction` 必须红
> ⚠️ **(b) 的可行性须先实测**:若⑥整体(126–205 = 80 行)搬进 `applyApprovalInTx` 而不再拆,该函数是 80 行 < 100 → **mutation (b) 在这个具体形态下不会红**。这是**阈值 100 的已知盲区**,不是守卫失效:它钉的是"不再有 169 行的巨函数",不是"每个 helper 都短"。实现者须在报告里写明这一点,**不得声称 (b) 覆盖了所有塞代码形态**。若要在 (b) 上取得真红,须构造 >100 行的 helper(例如把 ④⑥ 两个 switch 都塞进一个 helper)。 > ⚠️ **(b) 的可行性须先实测**:若⑥整体(126–200 = **77 行**)搬进 `applyApprovalInTx` 而不再拆 → **不会红**;**更宽的实测盲区:仅合并④⑥(正是 §2.1 被否方案 A 的形态)= 91 行,仍不红**;真红下界实测 >100 行。这是**阈值 100 的已知盲区**,不是守卫失效:它钉的是"不再有 169 行的巨函数",不是"每个 helper 都短"。实现者须在报告里写明这一点,**不得声称 (b) 覆盖了所有塞代码形态**。若要在 (b) 上取得真红,须构造 >100 行的 helper(例如把 ④⑥ 两个 switch 都塞进一个 helper)。
- (c) 删掉一个 helper(把其内容合并进 `Approve`)→ `TestApproveHelpersExist` 必须红 - (c) 删掉一个 helper(把其内容合并进 `Approve`)→ `TestApproveHelpersExist` 必须红
- (d) 把 helper 名改成别的(如 `loadApproveInputs`)→ `TestApproveHelpersExist` 必须红(**这条是 (c) 的对照**:证明断言钉的是名字集合而非"存在任意 5 个函数") - (d) 把 helper 名改成别的(如 `loadApproveInputs`)→ `TestApproveHelpersExist` 必须红(**这条是 (c) 的对照**:证明断言钉的是名字集合而非"存在任意 5 个函数")
- (e) 把 `TestNoOversizedFunction` 的扫描范围改成一个空目录 → 必须红(**防"扫 0 文件也报 0 违规"的假信心**,P13 审查者 M5 的同族教训;断言内须含"至少扫到 N 个 `.go` 文件"的前提检查) - (e) 把 `TestNoOversizedFunction` 的扫描范围改成一个空目录 → 必须红(**防"扫 0 文件也报 0 违规"的假信心**,P13 审查者 M5 的同族教训;断言内须含"至少扫到 N 个 `.go` 文件"的前提检查)
- (f) 把阈值从 100 改成 1000 → 必须红(同 (e),防阈值被放宽;断言须自证阈值字面量) - (f) 把阈值从 100 改成 1000 → 必须红(同 (e),防阈值被放宽;断言须自证阈值字面量)
- (g) **给 `ModerationService` 加字段**(如 `clock func() time.Time`)→ P14 的断言 6 `TestLineFields` 必须红(**跨批回归检查**:证明本批新增文件没有削弱 P14 守卫) - (g) **给 `ModerationService` 加字段**(如 `clock func() time.Time`)→ P14 的断言 6 `TestLineFields` 必须红(**跨批回归检查**:证明本批新增文件没有削弱 P14 守卫)
- (h) **在 `ContentService` 上加方法** → P14 的断言 5 必须红(同 (g),且**须同时测有名与匿名接收者两种形态**——P14 FR-1 的教训:只测有名会漏掉 `func (*ContentService) M()`) - (h) **在 `ContentService` 上加方法** → P14 的断言 5 必须红(同 (g),且**须同时测有名与匿名两种接收者形态**——P14 FR-1 的教训:只测有名会漏掉 `func (*ContentService) M()`)
- ⚠️ **(RE-PIN 2026-10-03)spec 的 (a)–(h) 对审查者发现的缺口全部无牙**,fix-forward 轮须复跑审查者自己的轮次:**RV-A / RV-B / RV-C / RV-J**(四条合并形态,修后应由 CG 守卫红)· **RV-I / RV-S**(修后应由正向存在性前提红)· **RV-E**(修后应给出合理错误而非 `read memory.go: no such file`)· **RV-O2**(修后应由 F2 新断言红)· **RV-P / RV-Q**(修后应由自证钉红)· **RV-R**(修后**仍应全绿** = 固有上限,**须在报告里明写「未能防住,靠 review 兜」,不得声称已修**)· **RV-N**(泛型形态修后应命中)· **RV-F / RV-G / RV-H**(跨批回归,应仍与实现者报告一致)
每次 mutation 后**只回滚触及的文件**(`git checkout -- <file>`,**绝不用 `git checkout -- .`**——P14 控制者第 24 次自伤:全量回滚会抹掉未提交的守卫编辑,导致后续各轮全在测没有修复的树),并核 `git diff` 空 + 守卫文件 `func Test` 计数不变。 每次 mutation 后**只回滚触及的文件**(`git checkout -- <file>`,**绝不用 `git checkout -- .`**——P14 控制者第 24 次自伤:全量回滚会抹掉未提交的守卫编辑,导致后续各轮全在测没有修复的树),并核 `git diff` 空 + 守卫文件 `func Test` 计数不变。
### T1.4 —— ④⑥ 独立性**常驻**守卫(⚠️ RE-PIN 2026-10-03,审查者 F10 裁定采纳)
spec §7 风险表第 1 行原写「T3 字节级比对会暴露合并」——**但 T3 是一次性证据,报告归档后下一个贡献者合并 ④⑥ 时没有任何东西会红**。审查者实测确认这个状态不可接受(§1.2 把 ④⑥ 不合并列为「本批最重要的约束」,却零常驻机械信号)。
**新增 `internal/service/approve_phases_test.go`**(新文件;不动 `architecture_test.go`),三条子断言:
- **CG1**:`fmt.Errorf("service: approve precheck: %w"` 只能出现在 `precheckApprove` 的 span 内
- **CG2**:`.Submissions().GetByIDForUpdate(` 只能出现在 `applyApprovalInTx` 的 span 内
> ⚠️ needle **必须收紧成 `.Submissions().GetByIDForUpdate(`**:`moderation.go` 内唯一命中 `:196`;而 `account.go:46`、`auth.go:122` 是 **`users.GetByIDForUpdate`**(不同接收者)。不收紧则将来 `moderation.go` 新增别的 `GetByIDForUpdate` 调用会误红。
- **CG3**:两个 helper 体内各自含 `switch …Kind`,且**互不调用**
**关键设计**:语料**先过 `stripComments`(只掩注释、保留字符串字面量)**。`moderation.go:141` 与 `:191` 的注释里**合法地**提到 precheck 文案 → 不掩注释会假红;而字符串字面量必须保留,否则 `fmt.Errorf("...")` 这个 needle 根本匹配不到。span 复用 `function_length_test.go` 的 `scanFuncs`(共享同一分词器,也就共享 F2 的闭包盲区,但闭包形态与 ④⑥ 合并无关)。
**审查者已双向验证**:合法树 **绿**;**4 种合并/退化形态全红**且全部 `compile_red=False`(是断言红不是编译红),三条 shipped 守卫在这些形态下**全绿**:
| 轮 | 形态 | shipped 三条 | CG 守卫 |
|---|---|---|---|
| RV-A | **活的** ④⑥ 语义合并:两个 helper 都委托给同一个 `checkKindConflicts(ctx, store repository.ContentRepository, plan)`(= §2.1 被否方案 **B** 的实现;比实现者 (b-iii) 的静态拼接更强——它真的会跑) | 全绿 | **红**(2 条) |
| RV-B | 掏空 ④:`precheckApprove` → `return nil`(声明保留,名字守卫满足) | 全绿 | **红** |
| RV-C | 把 ⑥ 的 `GetByIDForUpdate` 搬进 ④(摧毁 TOCTOU 分层,**标识符计数完全不变**) | 全绿 | **红**(CG2 越界) |
| RV-J | 「最自然的 DRY」:④ 的两个 case 折进 ⑥ 的 switch、⑥ 持有 precheck 文案、④ 变空壳 | 全绿 | **红**(CG1 越界 + CG3) |
> ⚠️ **RV-C 同时推翻了控制者的核实方法**:把 `GetByIDForUpdate` 从 ⑥ 搬进 ④,标识符计数不变、错误文案不变,而 TOCTOU 分层被摧毁(乐观预检变成持锁预检,失败时机与锁语义全变)。**「计数一致」证明不了控制流没变**——必须配 **顺序** 与 **位置** 证明(T3 的 order-violation 检查 + 审查者的 per-destination 单调性检查)。
**⚠️ 已知边界(须原样写进守卫注释;纪律 #4:不写全称声明)**:若合并者把 precheck 文案**留在** `precheckApprove` 内、同时把 ⑥ 的逻辑**复制**进去(复制而非移动),CG1 不红——此时需要靠「两处 `switch …Kind` 的 case 集合不同」这类更强断言。**该形态未验证,故不声称 CG1–CG3 覆盖所有合并形态。**
### T2 —— 执行拆分(T1 由红转绿) ### T2 —— 执行拆分(T1 由红转绿)
按 §3 执行。**完成后 `go test ./...` 必须 11 包全绿且既有测试文件零修改**。 按 §3 执行。**完成后 `go test ./...` 必须 11 包全绿且既有测试文件零修改**。
@@ -288,13 +315,31 @@ P14 的七条断言钉的是**类型形态**,没有一条钉**函数长度**
- **单行 `var X = errors.New(...)` 须有提前终止分支**,否则会把后续声明并入同一块(P14 F6 的残留局限)。 - **单行 `var X = errors.New(...)` 须有提前终止分支**,否则会把后续声明并入同一块(P14 F6 的残留局限)。
- 若工具给出意外值(如某方法报 CHANGED),**先怀疑自己的模式**,不要据此指控实现者(P14 F6:第一版工具误报 `CoverURL` 为 CHANGED,若照它写报告会产生一条完全虚假的 critical finding)。 - 若工具给出意外值(如某方法报 CHANGED),**先怀疑自己的模式**,不要据此指控实现者(P14 F6:第一版工具误报 `CoverURL` 为 CHANGED,若照它写报告会产生一条完全虚假的 critical finding)。
### T4 —— 审查裁定后的守卫加固(fix-forward 轮,⚠️ RE-PIN 2026-10-03)
任务级审查(`.superpowers/sdd-p15a/task-review-report.md`,18 轮自设 mutation)裁决 **PASS with notes**,控制者裁定合并前修 6 项(`.superpowers/sdd-p15a/adjudication.md` §二):
| # | finding | 改什么 | 审查者的实测证据 |
|---|---|---|---|
| **F1** 🔴 | `readNonTestSources` 的 `dir` 参数是**半成品**:`os.ReadDir(dir)` 但 `os.ReadFile(name)` → `dir` 只重定向「数哪些文件」,不重定向「读哪些内容」 | `os.ReadFile(filepath.Join(dir, name))` + import `path/filepath` | **RV-I**:base 树(`Approve` 169 行)+ `dir="zzscan"`(12 个同名文件、不含 `moderation.go`)→ 前提满足、`TestNoOversizedFunction` **PASS**,**而它唯一存在的理由(拦住 169 行 `Approve`)完全失效**。这恰好是守卫自己注释声称能防住的形态 |
| **F2** 🔴 | `scanFuncs` 只对 `^func` 开 span → **`var f = func(...) {…}` 的函数体永不被测量** | 新断言:service 包内 `^var .* = func(` 形态的**包级闭包数量 = 0**(现状 0) | **RV-O2**:真实可编译、gofmt 干净的包内文件含 **125 行闭包** → 四门全绿、三条守卫全 PASS(`scanned 16 files / 86 funcs`——文件被数进去但一个函数都没测出来)。**比实现者自曝的 (b) 盲区更宽:那是「≤100 行的合并形态」,这是「任意长度的非 `func` 形态」** |
| **F3** 🔴 | **四门的 gofmt 门是退出码盲的**:`gofmt -l .` 列出未格式化文件时**仍 exit 0**(只在解析失败时 exit 2) | 配方改 `test -z "$(gofmt -l .)"`,并同时存证原始输出为空 | 构造含缩进顶层声明的文件:`gofmt -l .` **列出** `main.go` 而 `gofmt_l_exit=0`。**控制者与实现者共用旧配方 → 同一盲点被复算两次而没被发现**(记为控制者错误 #32)。**与 F2 叠加后缩进逃逸路径完全敞开**:` func …` Go 编译器接受、`scanFuncs` 扫不到、三条守卫全绿,唯一防线就是这个不会红的 gofmt 门。**这是 P14「管道吞退出码」教训的镜像形态:命令本身不产生非零退出码,比管道吞码更隐蔽,因为 `$?` 看起来可信** |
| **F4** 🔴 | 三个守卫常量里**两个无自证钉**,有钉的那个**只防单边改** | (a) **必修**:前提检查从「文件数 ≥ N」改成**正向存在性断言** `if _, ok := srcs["moderation.go"]; !ok { t.Fatalf(...) }`(不依赖魔法数字,同时治 RV-I 与 RV-S 的根);(b) 给 `approveLineBudget`/`minScannedFiles` 补自证钉;(c) 注释写明「同步改值 + 同步改钉」是**固有上限**、只能靠 review 兜 | **RV-P**(`approveLineBudget` 60→**600**)全绿、日志 `Approve = 24 lines (< 600)`,**静默失去全部牙齿**;**RV-Q**(`minScannedFiles` 12→**0**)全绿,前提变 `len(srcs) < 0` **恒假 → 永不触发**;**RV-R**(**同步**改阈值 100→1000 **且**同步改自证钉)全绿 → **自证钉防不住阈值放宽**;**RV-S**(RV-Q + `dir="zzempty"` 0 个 .go)→ **PASS**、日志 `scanned 0 files / 0 funcs`,**精确复现守卫注释声称能防住的「扫 0 文件也报 0 违规的假信心」** |
| **F7** 🟡 | helper 正则要求名字后**紧跟 `(`** → **泛型声明形态** `HELPER[T any](` 不命中 | `HELPER\(` → `HELPER(?:\[[^\]]*\])?\(` | RV-N 实测三个 helper 均 `false`。**失败方向是红(过严)不是绿(绕过)**,故不是安全洞而是脆弱性;但与 F2 的缩进逃逸**同源**:都源于 `^func` / `HELPER\(` 这类**行首锚定的词法匹配** |
| **F9** 🟢 | `applyApprovalInTx` 签名不含 `submissionID`,实现者用 `plan.sub.ID` 替代(附跨两个 repository 实现的同值论证) | **加 `submissionID` 形参**,⑥内两处(`GetByIDForUpdate`、`MarkReviewed`)恢复逐字用 `submissionID` | 审查者独立复核**同值成立**、并发安全验证通过;但本批唯一验收判据是「零行为变更」,而该替换让判据**依赖一条跨实现论证** → 加形参后这两处逐字一致、**彻底消除依赖**,且 T3 的 changed 从 **15 降到 13**(证据更强) |
| **F6** 🟡 | 实现者报告 §6 的 changed 分类表**与它自己的证据对不上账**(stated `9+3+2+2 = 16`,证据标签合计 **15**) | 更正为 `多值返回 7 · 声明折叠 2 · 零值折叠 2 · 赋值形态 1 · =→:= 1 · 实参来源 2 = 15` | **实质无问题**(15 条逐条有理由、162 = 147+15+0 对账闭合、审查者独立 differ 精确复现归属),是**汇总表算术/誊写错误**非证据造假。但 USER.md 明写「报数字要报实跑输出」,且 P14 有过「控制者报出 83/299 与 82/298 两套数字」的先例 |
**DEFER(另开批 + 挂账)**:**F5** —— P14 断言 5 有一条未封死的绕过:**类型别名接收者**(`type X = ContentService` + `func (c *X) CoverURL(...)`)。审查者 RV-M 硬证据:**遮蔽确实生效**(组合根返回 `RV-M-SHADOW`),而**用与 `architecture_test.go` 逐字相同的正则**实测命中数 = **0**;RV-K:全部 **10 条守卫 + vet + build 全绿**。与 P14 FR-1(匿名接收者)**同族**:FR-1 的修法把接收者名改成可选分组,但仍要求 `ContentService` 这个字面 token 出现在 `(` 之后,别名形态恰好把这个 token 换掉了。→ `architecture_test.go:173` 的「**唯一**能侦测遮蔽提升方法的机械手段」这个全称声明**在别名形态下被推翻**(纪律 #4)。**pre-existing(P14 遗留),不算本批扣分项**;且修它必须动 `architecture_test.go`(本批禁触)→ 另开批。最小修法:禁止包内出现指向 `ContentService` 的类型别名,一条正则 `^type\s+\w+\s*=\s*\*?ContentService\b`(合法树 0 命中、RV-K/RV-M 红);更强修法:改用 `go/ast` 判定接收者类型是否 resolve 到 `ContentService`(含别名),**一次性解决 F2/F5/F7 三条的词法根因**。
**偏离 1 的裁定(审查者 §10)**:`5fd12e5` 给 `readNonTestSources` 加 `dir` 参数——**动机成立**(spec §5 要求每条 mutation 测「预期红 + 其余绿」,共享扫描入口无法隔离)、**「RED 输出前后逐字同款」验证成立**(两份存证失败内容完全一致,只有行号整体 +3)、**但实现有 bug(F1)且换来的收益是虚的**(审查者:「为了演示而给生产代码加参数、且加出 bug,是净损失」)。审查者倾向回退;**控制者裁定保留参数化并按 F1+F4(a) 修**——理由:(e) 的隔离演示有独立价值(它是「预期红 + **其余绿**」这个硬要求的唯一实现路径),F1 是一行修,而 F4(a) 的正向存在性断言比回退更能治根。
### 四门验收 ### 四门验收
```bash ```bash
cd crearte-server && docker run --rm -v "$PWD/src:/src" -w /src \ cd crearte-server && docker run --rm -v "$PWD/src:/src" -w /src \
-v crearte_gomod:/go/pkg/mod -e GOCACHE=/gocache -v crearte_gocache:/gocache \ -v crearte_gomod:/go/pkg/mod -e GOCACHE=/gocache -v crearte_gocache:/gocache \
-e GOPROXY=https://goproxy.cn,direct -e GOSUMDB=sum.golang.google.cn \ -e GOPROXY=https://goproxy.cn,direct -e GOSUMDB=sum.golang.google.cn \
golang:1.24-alpine sh -c 'gofmt -l . ; go vet ./... ; go build ./... ; go test ./... -count=1' golang:1.24-alpine sh -c 'gofmt -l . ; test -z "$(gofmt -l .)" ; echo gofmt_gate=$? ; go vet ./... ; go build ./... ; go test ./... -count=1'
``` ```
⚠️ **`-count=1` 强制实跑**(禁缓存)。⚠️ **落盘证据用 `setsid nohup` 脱离 + 宿主侧重定向**(容器只挂 `/src` 写不了 `.superpowers/`;exec session 一断容器就被杀——P14 实现者与任务级审查者都踩过)。⚠️ **`grep -c` 输出 0 时 exit 1**,放 `&&` 链里会静默中断后续步骤(含 `git commit`)。⚠️ **`pkill -f <pattern>` 会匹配到自己的命令行**(P14 控制者把自己 SIGKILL)→ 用 `[p]attern`。 ⚠️ **`-count=1` 强制实跑**(禁缓存)。⚠️ **落盘证据用 `setsid nohup` 脱离 + 宿主侧重定向**(容器只挂 `/src` 写不了 `.superpowers/`;exec session 一断容器就被杀——P14 实现者与任务级审查者都踩过)。⚠️ **`grep -c` 输出 0 时 exit 1**,放 `&&` 链里会静默中断后续步骤(含 `git commit`)。⚠️ **`pkill -f <pattern>` 会匹配到自己的命令行**(P14 控制者把自己 SIGKILL)→ 用 `[p]attern`。
@@ -318,7 +363,7 @@ cd crearte-server && docker run --rm -v "$PWD/src:/src" -w /src \
- `crearte-server/docs/CHANGELOG.md` 新增 **`## [0.19.0] - 2026-10-03`**(插 `## [0.18.0]` 前)。格式:**同条目英文行紧跟中文行、无空行**;不同条目**空一行**;小节标题双语(`### Changed / 变更`、`### Tests / 测试`、`### Context / 背景`)。 - `crearte-server/docs/CHANGELOG.md` 新增 **`## [0.19.0] - 2026-10-03`**(插 `## [0.18.0]` 前)。格式:**同条目英文行紧跟中文行、无空行**;不同条目**空一行**;小节标题双语(`### Changed / 变更`、`### Tests / 测试`、`### Context / 背景`)。
- wrapper `docs/CHANGELOG.md` 新增 **`## [0.3.8] - 2026-10-03`**(插 `## [0.3.7]` 前),小节 `### Done / 完成`。 - wrapper `docs/CHANGELOG.md` 新增 **`## [0.3.8] - 2026-10-03`**(插 `## [0.3.7]` 前),小节 `### Done / 完成`。
- `docs/ROADMAP.md`:第三波表新增 **P15-A 行**(P14 行之后)+ 文档索引表新增一行。**哈希引 merge commit**(P12/P13/P14 惯例),记账 commit 另列。 - `docs/ROADMAP.md`:第三波表新增 **P15-A 行**(P14 行之后)+ 文档索引表新增一行。**哈希引 merge commit**(P12/P13/P14 惯例),记账 commit 另列。
- **挂账新增**:六个 >60 行函数(`deleteAccountLocked` 83 / `UpdateSubmission` 76 / `ValidateWorkFields` 75 / `ImportDir` 70 / `CleanupService.Run` 70 / `checkSubmissionRules` 66)· `TestNoOversizedFunction` 阈值 100 的已知盲区(80 行的 helper 不会被拦,见 §5 mutation (b) 的警告)。 - **挂账新增**:① 六个 >60 行函数(`deleteAccountLocked` 83 / `UpdateSubmission` 76 / `ValidateWorkFields` 75 / `ImportDir` 70 / `CleanupService.Run` 70 / `checkSubmissionRules` 66)② **阈值 100 的盲区扩写为「≤100 行的任何合并形态」**(实测:⑥整体搬进 `applyApprovalInTx` = **77 行**不红;**仅合并④⑥ = 91 行仍不红**;真红下界 >100 行)③ **`TestApproveIsSmall` 的镜像盲区**:须内联四个阶段才把 `Approve` 推到 79 ≥ 60;单内联⑦(28 行)时三条守卫全绿(由 (c)/(d) 的名字集合断言兜)→ 守卫钉的是「编排体小 + 五个名字存在 + 无巨函数」,**不钉「每个阶段必须经由 helper」** ④ **非 `func` 形态的任意长度逃逸**(F2:125 行包级闭包四门全绿)⑤ **三个字面量常量的自证缺口**(F4:RV-S 复现「0 文件 0 违规 PASS」)⑥ **F3:四门配方的 gofmt 门退出码盲**(影响**所有**批次,不只 P15-A)⑦ **F5:P14 断言 5 的类型别名接收者绕过**(pre-existing)
- **`go.mod` / 任何 version 文件不 bump**。 - **`go.mod` / 任何 version 文件不 bump**。
- **纪律**:显式 `git add <file>`,**禁 `git add -A`**。wrapper 的 `IDENTITY.md`/`SOUL.md`/`USER.md` 三个未跟踪文件**非本批产物,绝不 stage**。 - **纪律**:显式 `git add <file>`,**禁 `git add -A`**。wrapper 的 `IDENTITY.md`/`SOUL.md`/`USER.md` 三个未跟踪文件**非本批产物,绝不 stage**。
@@ -328,7 +373,7 @@ cd crearte-server && docker run --rm -v "$PWD/src:/src" -w /src \
| 风险 | 缓解 | | 风险 | 缓解 |
|---|---| |---|---|
| **实现者"顺手 DRY"合并④⑥的 switch** | §1.2 已写明四处实质差异 + 语义差别(乐观/悲观)+ 错误文案差别;D-D 明令禁止;T3 字节级比对会暴露合并(base 的 `service: approve precheck: %w` 文案会消失或移位);既有 `TestApproveSameWorkIDConcurrent` 断言的错误集合会变 | | **实现者"顺手 DRY"合并④⑥的 switch** | §1.2 已写明四处实质差异 + 语义差别(乐观/悲观)+ 错误文案差别;D-D 明令禁止;T3 字节级比对会暴露合并(⚠️ **但 T3 是一次性证据,报告归档后下一个贡献者合并 ④⑥ 时没有任何东西会红** → 由 `approve_phases_test.go` 的 **CG1–CG3 常驻断言**拦截,见 §5-T1.4)(base 的 `service: approve precheck: %w` 文案会消失或移位);既有 `TestApproveSameWorkIDConcurrent` 断言的错误集合会变 |
| **`plan` 值传递导致 ⑤ 的 `finalXxxKey` 静默丢失** | §3.1 已点名:`copyApprovalObjects` 必须收 `*approvePlan`。`TestApproveOptionalFieldsRoundTrip` 与 api 层发布链测试会抓到(key 为空 → 作品无 bundle) | | **`plan` 值传递导致 ⑤ 的 `finalXxxKey` 静默丢失** | §3.1 已点名:`copyApprovalObjects` 必须收 `*approvePlan`。`TestApproveOptionalFieldsRoundTrip` 与 api 层发布链测试会抓到(key 为空 → 作品无 bundle) |
| **② 的 `owner` 零值被误当成"未加载"** | §3.2 已写明 `if sub.Kind == NewWork` 条件必须保留;`namespace_test.go:132 TestNewVersionMetadataChangeOwnership` 会抓到 OwnerID 错误 | | **② 的 `owner` 零值被误当成"未加载"** | §3.2 已写明 `if sub.Kind == NewWork` 条件必须保留;`namespace_test.go:132 TestNewVersionMetadataChangeOwnership` 会抓到 OwnerID 错误 |
| **③ 的 `bundleUpload = &up` 取地址语义被改成存值** | §3.2 已点名;改了会让 `plan.bundleUpload != nil` 恒真或恒假 → ⑤⑥⑦ 全链路错,api 层发布链测试必红 | | **③ 的 `bundleUpload = &up` 取地址语义被改成存值** | §3.2 已点名;改了会让 `plan.bundleUpload != nil` 恒真或恒假 → ⑤⑥⑦ 全链路错,api 层发布链测试必红 |
@@ -56,7 +56,7 @@ const targets = computed<RuntimeTarget[]>(() => resolveRuntimeTargets(props.game
| ❌ `theme: useTheme().theme.value` | **是**(读 `ref` 的 getter → computed 建立依赖) | 用户在游戏页切主题 → `targets` 重算 → **iframe `src` 变化 → 正在运行的游戏被重载**(存档/进度丢失) | | ❌ `theme: useTheme().theme.value` | **是**(读 `ref` 的 getter → computed 建立依赖) | 用户在游戏页切主题 → `targets` 重算 → **iframe `src` 变化 → 正在运行的游戏被重载**(存档/进度丢失) |
| ✅ `theme: effectiveTheme()` | **否**(`effectiveTheme()` 内部读 `localStorage` 与 `matchMedia`,都不是响应式源) | computed 不依赖主题;iframe 不重载。代价:切主题后已打开的加载屏保持旧主题,直到下次导航 | | ✅ `theme: effectiveTheme()` | **否**(`effectiveTheme()` 内部读 `localStorage` 与 `matchMedia`,都不是响应式源) | computed 不依赖主题;iframe 不重载。代价:切主题后已打开的加载屏保持旧主题,直到下次导航 |
→ **裁定用 `effectiveTheme()`**(`useTheme.ts:39` 已导出)。"游戏运行中不重载" 比 "加载屏实时跟随主题切换" 重要得多:加载屏只在**启动瞬间**可见,而重载会毁掉正在进行的游戏。**这条必须有测试钉住**(§5-T1 腿 4),否则下一个贡献者"顺手改成响应式"就会引入静默的游戏重载缺陷。 → **裁定用 `effectiveTheme()`**(`useTheme.ts:39` 已导出)。"游戏运行中不重载" 比 "加载屏实时跟随主题切换" 重要得多:加载屏只在**启动瞬间**可见,而重载会毁掉正在进行的游戏。**这条必须有测试钉住**(§5-T1 **腿 5**;⚠️ RE-PIN 2026-10-03:原写「腿 4」,而腿 4 是 IIFE/var、注入是**腿 5**——spec 内部自相矛盾,§5-T1 表自身与 §5 mutation (d)、§7 风险表都写腿 5),否则下一个贡献者"顺手改成响应式"就会引入静默的游戏重载缺陷。
**色值映射(实测;`bootstrap/index.html` 里 8 个硬编码 hex 的令牌归属)**: **色值映射(实测;`bootstrap/index.html` 里 8 个硬编码 hex 的令牌归属)**:
@@ -106,7 +106,7 @@ const targets = computed<RuntimeTarget[]>(() => resolveRuntimeTargets(props.game
``` ```
**缺口**:`ink on surface` **未钉**,而它是**所有未钉配对里使用面最大的**: **缺口**:`ink on surface` **未钉**,而它是**所有未钉配对里使用面最大的**:
- `bg-surface` 全仓 **62 处**(`app/` + `runtime/` 的 `.vue`) - `bg-surface` **`.vue` 模板内 62 处**(`app/` + `runtime/`);⚠️ 整个 `src` 是 **66 处**(另 4 处在 `.test.ts`:BaseInput/BaseTabs/BaseTextarea/FileInput 各 1)→ 原写「全仓 62 处」与 66 冲突(RE-PIN 2026-10-03)。**不影响结论**:Tailwind 只从 `.vue` 模板生成 utility,`.test.ts` 里的字面量属 §8-8 的另一回事
- 其中只有 **4 处**显式写 `text-ink-soft`(已钉)、**2 处**的 `text-paper` 属三元表达式**另一支**(P15 账本 §1 第 19 次错误:把互斥分支当同元素共现,是假阳性) - 其中只有 **4 处**显式写 `text-ink-soft`(已钉)、**2 处**的 `text-paper` 属三元表达式**另一支**(P15 账本 §1 第 19 次错误:把互斥分支当同元素共现,是假阳性)
- **58 处靠继承**:全局文字色源是 `src/index.html:34` `<body class="bg-paper text-ink font-sans antialiased">` → 继承到 **`ink`** - **58 处靠继承**:全局文字色源是 `src/index.html:34` `<body class="bg-paper text-ink font-sans antialiased">` → 继承到 **`ink`**
- 实测对比度:亮 **18.4225** / 暗 **14.8468** → **必然通过 4.5** - 实测对比度:亮 **18.4225** / 暗 **14.8468** → **必然通过 4.5**
@@ -124,7 +124,7 @@ const targets = computed<RuntimeTarget[]>(() => resolveRuntimeTargets(props.game
**零使用**(实测):全仓 `bg-info` / `text-info` / `border-info` / `ring-info` **零命中**;`color-info` 在整个 `src/` + `e2e/` 里**只有那两行定义**(命中数 = 2,即定义本身)。`main.css` 内 `info` 字样也只出现在那两行。 **零使用**(实测):全仓 `bg-info` / `text-info` / `border-info` / `ring-info` **零命中**;`color-info` 在整个 `src/` + `e2e/` 里**只有那两行定义**(命中数 = 2,即定义本身)。`main.css` 内 `info` 字样也只出现在那两行。
**矛盾点**:它在 `REQUIRED_KEYS`(`contrast.test.ts:75-88`,**12 项**,`info` 在**第 8 位**)里 → `it(\`${zh} 12 令牌齐备(spec §3.1(b))\`)`(`:119`)会**在删除时变红**。**守卫在强制一个零使用的令牌存在。** **矛盾点**:它在 `REQUIRED_KEYS`(`contrast.test.ts:75-88`,**12 项**,`info` 在**第 9 位**(第 8 是 `highlight`;⚠️ RE-PIN 2026-10-03:原写第 8 位))里 → `it(`${zh} 12 令牌齐备(spec §3.1(b))`)`(`:119`)会**在删除时变红**。**守卫在强制一个零使用的令牌存在。**
**连带面(实测清点,5 个文件,不是"顺手删两行")**: **连带面(实测清点,5 个文件,不是"顺手删两行")**:
@@ -151,7 +151,7 @@ const targets = computed<RuntimeTarget[]>(() => resolveRuntimeTargets(props.game
| # | 决策点 | 裁定 | 依据 | | # | 决策点 | 裁定 | 依据 |
|---|---|---|---| |---|---|---|---|
| **D-A** | bootstrap 主题来源 | **hash 参数 `theme` 优先 → `prefers-color-scheme` → `light`**(三级,比主应用少 `stored` 一级) | §1.1:`localStorage` 按源隔离读不到;hash 通道已存在(`main.ts:4`);`prefers-color-scheme` 不受源隔离。**判定序必须与主应用 `useTheme.effectiveTheme()` 的后两级逐字一致**,否则同一用户在主应用与游戏加载屏看到不同主题 | | **D-A** | bootstrap 主题来源 | **hash 参数 `theme` 优先 → `prefers-color-scheme` → `light`**(三级,比主应用少 `stored` 一级) | §1.1:`localStorage` 按源隔离读不到;hash 通道已存在(`main.ts:4`);`prefers-color-scheme` 不受源隔离。**判定序必须与主应用 `useTheme.effectiveTheme()` 的后两级逐字一致**,否则同一用户在主应用与游戏加载屏看到不同主题 |
| **D-B** | 父侧注入方式 | **`effectiveTheme()` 非响应式快照**,**不得**用 `useTheme().theme.value` | §1.1 的架构陷阱:响应式会让主题切换重算 `targets` → iframe `src` 变 → **运行中的游戏被重载**。必须有测试钉住(§5-T1 腿 4) | | **D-B** | 父侧注入方式 | **`effectiveTheme()` 非响应式快照**,**不得**用 `useTheme().theme.value` | §1.1 的架构陷阱:响应式会让主题切换重算 `targets` → iframe `src` 变 → **运行中的游戏被重载**。必须有测试钉住(§5-T1 **腿 5**;RE-PIN:原写腿 4) |
| **D-C** | 死令牌清理的历史条目处理 | **不改写 P13 的 CHANGELOG 条目与 spec 原文**,改为:新条目(0.27.0)说明变更 + P13 spec 加 **RE-PIN 标注块** | CHANGELOG 是发布历史,改写会让"当时交付了什么"失真。P14 已建立正确手法:保留历史决策原文 + 紧随带 ⚠️ RE-PIN 标记的增订块(全分支终审判定这是"正确的 re-pin 手法"而非自相矛盾) | | **D-C** | 死令牌清理的历史条目处理 | **不改写 P13 的 CHANGELOG 条目与 spec 原文**,改为:新条目(0.27.0)说明变更 + P13 spec 加 **RE-PIN 标注块** | CHANGELOG 是发布历史,改写会让"当时交付了什么"失真。P14 已建立正确手法:保留历史决策原文 + 紧随带 ⚠️ RE-PIN 标记的增订块(全分支终审判定这是"正确的 re-pin 手法"而非自相矛盾) |
| **D-D** | 新守卫的形态 | **新增 `app/lib/bootstrapTheme.test.ts`**(源码级、node 环境、读文件文本),**不扩展 `noHardcodedColor.test.ts`** | §1.4:`noHardcodedColor` 的 `candidates()` 只收 `.vue`(`if (file.endsWith('.vue'))`),且 `maskNonTemplate()` 把 `<style>` 块整体等长空白掉 → **它从设计上无法覆盖 `bootstrap/index.html`**(HTML 文件 + 颜色全在 `<style>` 里)。扩扫描根不会有任何效果 | | **D-D** | 新守卫的形态 | **新增 `app/lib/bootstrapTheme.test.ts`**(源码级、node 环境、读文件文本),**不扩展 `noHardcodedColor.test.ts`** | §1.4:`noHardcodedColor` 的 `candidates()` 只收 `.vue`(`if (file.endsWith('.vue'))`),且 `maskNonTemplate()` 把 `<style>` 块整体等长空白掉 → **它从设计上无法覆盖 `bootstrap/index.html`**(HTML 文件 + 颜色全在 `<style>` 里)。扩扫描根不会有任何效果 |
| **D-E** | inline `style` 的两处违规 | **删掉 inline 的 `color` 声明**(保留 `font-size`),让样式表的达标值生效 | §1.1 实测:inline 是冗余重复且对比度更低(2.2594 / 2.4776 vs 样式表 6.3586 / 4.8700)。删 inline 即修复,**同时消除"两处定义同一元素颜色"的分歧源** | | **D-E** | inline `style` 的两处违规 | **删掉 inline 的 `color` 声明**(保留 `font-size`),让样式表的达标值生效 | §1.1 实测:inline 是冗余重复且对比度更低(2.2594 / 2.4776 vs 样式表 6.3586 / 4.8700)。删 inline 即修复,**同时消除"两处定义同一元素颜色"的分歧源** |
@@ -173,7 +173,7 @@ const targets = computed<RuntimeTarget[]>(() => resolveRuntimeTargets(props.game
### §1.4 `noHardcodedColor` 为何无法覆盖 bootstrap(D-D 的依据,逐字取证) ### §1.4 `noHardcodedColor` 为何无法覆盖 bootstrap(D-D 的依据,逐字取证)
```js ```js
// app/lib/noHardcodedColor.test.ts:23-45 // app/lib/noHardcodedColor.test.ts:23-56(⚠️ RE-PIN 2026-10-03:原标 :23-45,但下方引文一直引到 :56 的 maskNonTemplate 结束——而 maskNonTemplate 恰是 D-D 论证的**关键一半**「<style> 块整体空白掉」,旧标注把最有说服力的部分排除在外。另:引文块内的 ← 旁注是**控制者批注**,不在原文里)
const SRC_ROOT = fileURLToPath(new URL('../..', import.meta.url)) const SRC_ROOT = fileURLToPath(new URL('../..', import.meta.url))
// 终审 N4:扫描根必须含 `runtime/`。…实测 runtime/ 当前零硬编码 hex,扩根不会立刻红。 // 终审 N4:扫描根必须含 `runtime/`。…实测 runtime/ 当前零硬编码 hex,扩根不会立刻红。
const SCAN_ROOTS = [join(SRC_ROOT, 'app'), join(SRC_ROOT, 'runtime')] const SCAN_ROOTS = [join(SRC_ROOT, 'app'), join(SRC_ROOT, 'runtime')]
@@ -208,8 +208,8 @@ function maskNonTemplate(content: string): string {
```html ```html
<meta name="color-scheme" content="light dark" /> <meta name="color-scheme" content="light dark" />
<meta name="theme-color" content="#F7F2E7" /> <meta name="theme-color" content="#F7F2E7" />
<!-- P15-B D-A/D-G:加载屏 pre-paint 主题脚本。必须在 <style> 与 <body> 之前,否则暗色用户每次启动游戏都闪白。 <!-- P15-B D-A/D-G:加载屏 pre-paint 主题脚本,须在样式与文档体之前执行,否则暗色用户每次启动游戏都闪白。
判定序比主应用少 stored 一级(游戏跑在独立子域,localStorage 按源隔离读不到主域的 crearte.theme.v1): 判定序比主应用少 stored 一级(游戏跑在独立子域,浏览器存储按源隔离,读不到主域的 crearte.theme.v1):
hash theme → prefers-color-scheme → light。后两级与 useTheme.effectiveTheme() 逐字一致, hash theme → prefers-color-scheme → light。后两级与 useTheme.effectiveTheme() 逐字一致,
一致性由 app/lib/bootstrapTheme.test.ts 钉住(改一处必须改另一处)。 --> 一致性由 app/lib/bootstrapTheme.test.ts 钉住(改一处必须改另一处)。 -->
<script> <script>
@@ -232,9 +232,11 @@ function maskNonTemplate(content: string): string {
})() })()
</script> </script>
``` ```
> ⚠️ **RE-PIN 2026-10-03(本段是控制者批注,不属于上面可复制的代码块)**:上面注释的措辞**刻意避开** `<style`、`<body`、`localStorage` 三个字面量。实现者与审查者**双双实测**:原措辞「必须在 `<style>` 与 `<body>` 之前」会让**腿 1 对正确实现假红**(注释自身在脚本之前 → `indexOf('<style')` 先命中注释;审查者复现 styleOpen=145 < scriptPos=811),而注释里的存储键字样会让 §5 指标「bootstrap 内该项 = 0」**字面不达标**。**对照**:主应用 `src/index.html` 的注释刻意写「必须在 **CSS 与 Vue** 之前」正是为了避开这两个字面量,所以 `themeBootstrap.test.ts` 的 naive indexOf 才不误红 —— 原措辞违反了这一既有范式。**照抄上面代码块时不要把这批注抄进去。**
⚠️ **白名单校验必须与主应用同款**(`=== 'light' || === 'dark'`),不得放宽成"任意 truthy"——`themeBootstrap.test.ts` 腿 3 正是为此存在(P13 finding #2 / N2:垃圾 `stored=neon` 必须被忽略)。 ⚠️ **白名单校验必须与主应用同款**(`=== 'light' || === 'dark'`),不得放宽成"任意 truthy"——`themeBootstrap.test.ts` 腿 3 正是为此存在(P13 finding #2 / N2:垃圾 `stored=neon` 必须被忽略)。
**(b) `<style>` 块**:加暗色覆盖块(D-F),值取 §1.1 的暗色调色板;`color-scheme: dark` 随块声明一次(同 `main.css:57` 的手法与理由)。**亮色值逐字不变**。 **(b) `<style>` 块**:加暗色覆盖块(D-F),值取 §1.1 的暗色调色板;`color-scheme: dark` 随块声明一次(同 `main.css:57` 的手法与理由)。**亮色色值逐字不变、渲染结果等价**(⚠️ RE-PIN 2026-10-03:裁定**接受**偏离 4——亮色值改由具名 `--bs-*` 变量供给、暗色块覆盖同名变量。理由三条:① 审查者独立写 CSS 解析器验证亮色态 **15 条颜色相关声明零差异**(含 4 条简写内嵌 `var()` 的 border/box-shadow)② 手法与主应用 `main.css` 同构(`@theme` 定义令牌 + `html[data-theme="dark"]` 覆盖),符合 D-F ③ 严格守「文本形态逐字不变」则暗色块须重写 **12 条规则**,同步维护面从 6 个值涨到 12 条规则)。
**(c) 两处 inline `style`**(D-E):`:25` 删 `color:#a3a3a3`、`:27` 删 `color:#f87171`,各保留 `font-size`。删后由 `<style>` 块的 `#status`(`#5c584d`) 与 `#error-message`(`#c03a1b`) 生效 → 对比度 6.3586 / 4.8700 达标。 **(c) 两处 inline `style`**(D-E):`:25` 删 `color:#a3a3a3`、`:27` 删 `color:#f87171`,各保留 `font-size`。删后由 `<style>` 块的 `#status`(`#5c584d`) 与 `#error-message`(`#c03a1b`) 生效 → 对比度 6.3586 / 4.8700 达标。
@@ -244,7 +246,7 @@ export function resolveRuntimeTargets(game: Game, opts: {
baseDomain: string; protocol: string; config?: ReturnType<typeof runtimeConfig>; theme?: 'light' | 'dark' baseDomain: string; protocol: string; config?: ReturnType<typeof runtimeConfig>; theme?: 'light' | 'dark'
}): RuntimeTarget[] { }): RuntimeTarget[] {
``` ```
⚠️ **必须可选**:`adapters.test.ts` 有 4 处调用不带 `theme`,且 `useGameFrame.test.ts` 也 import 了 `RuntimeTarget` 类型 → 加必填键会破既有测试(违反"既有测试修改数 = 0")。 ⚠️ **必须可选**:`adapters.test.ts` 有 **11 处**调用不带 `theme`(8 处裸 `opts` + 3 处 `{...opts, config}`;⚠️ RE-PIN 2026-10-03:原写「4 处」,实测 `resolveRuntimeTargets(` 出现 **11** 次、含 `theme:` **0** 次、按 `test(` 而非 `it(` 组织 → 没有任何口径得出 4),且 `useGameFrame.test.ts` 也 import 了 `RuntimeTarget` 类型 → 加必填键会破既有测试(违反"既有测试修改数 = 0")。
**(e) `GameHost.vue:27-31`**:注入非响应式快照(D-B): **(e) `GameHost.vue:27-31`**:注入非响应式快照(D-B):
```js ```js
@@ -314,8 +316,8 @@ const targets = computed<RuntimeTarget[]>(() => resolveRuntimeTargets(props.game
| 4 | 脚本是 **IIFE 且只用 `var`**(无 `let`/`const`/箭头函数/`import`) | D-G 的 CSP 前形态 | 把 `var` 改成 `const` | | 4 | 脚本是 **IIFE 且只用 `var`**(无 `let`/`const`/箭头函数/`import`) | D-G 的 CSP 前形态 | 把 `var` 改成 `const` |
| 5 | **`GameHost.vue` 注入的是 `effectiveTheme()` 而非 `useTheme().theme.value`** | 🔴 **D-B 的架构陷阱**(响应式会让主题切换重载运行中的游戏) | 改成 `theme.value` → 必须红 | | 5 | **`GameHost.vue` 注入的是 `effectiveTheme()` 而非 `useTheme().theme.value`** | 🔴 **D-B 的架构陷阱**(响应式会让主题切换重载运行中的游戏) | 改成 `theme.value` → 必须红 |
| 6 | bootstrap 暗色块的 hex 值与 `main.css` 的 `html[data-theme="dark"]` 调色板**逐值一致**(`paper`/`surface`/`ink`/`ink-soft`/`accent`/`accent-ink` 六个) | D-E 方案否决理由:bootstrap 自包含 hex,故须钉"两处调色板不分叉" | 把 bootstrap 暗色 `--paper` 改成别的值 | | 6 | bootstrap 暗色块的 hex 值与 `main.css` 的 `html[data-theme="dark"]` 调色板**逐值一致**(`paper`/`surface`/`ink`/`ink-soft`/`accent`/`accent-ink` 六个) | D-E 方案否决理由:bootstrap 自包含 hex,故须钉"两处调色板不分叉" | 把 bootstrap 暗色 `--paper` 改成别的值 |
| 7 | bootstrap 亮色值**未被改动**(钉 §1.1 表的六个亮色 hex 仍在) | 防"顺手把亮色也改了"(本批只加暗色) | 改一个亮色值 | | 7 | bootstrap **亮色调色板的值**未被改动 —— ⚠️ **须从 `:root` 块提取 `--bs-*` 再逐值比对(与腿 6 对称),不得用全文子串搜索**(RE-PIN 2026-10-03:审查者 F-4 实测 `#f7f2e7` 在文件里出现 **3 次**(`:root` 的 `--bs-paper`、暗色块的 `--bs-ink`、脚本的 theme-color meta)→ 全文搜索**必然命中**;把 `:root` 亮色 paper 改成 `#eeeeee` 时 `RED=[]`,且**四个守卫合跑 38 passed / 0 failed 全绿** = 本腿的守卫目的「防顺手把亮色也改了」**无断言覆盖**,是 P14「spec D-J 守卫目的无断言覆盖」的同族形态。改为按 scope 提取后**同时消除「暗色块 `--bs-ink` 与亮色 `--bs-paper` 同值」造成的耦合**,即 P13 `LEGACY_KEYS` 钉桩里「scrim 两主题同值 → 后写覆盖不可观测,故排除」的同一类推理) | 防"顺手把亮色也改了"(本批只加暗色) | 改 `:root` 的亮色 paper |
| 8 | `bootstrap/index.html` 内**不存在 inline `style` 里的 `color:`**(D-E 的回归钉桩) | 防 inline 违规复发(本批刚删两处 2.2594 / 2.4776) | 加回 `style="color:#a3a3a3"` | | 8 | `bootstrap/index.html` 内**不存在 inline `style` 里的 `color:`**(D-E 的回归钉桩)—— ⚠️ **正则须同时覆盖单引号与双引号属性形态**(RE-PIN 2026-10-03:审查者 F-5 实测只匹配 `style="` 时,把 inline style 改**单引号**并塞回违规色 `#a3a3a3` → `RED=[]`,而 HTML 单引号属性完全合法(`prettier`/手写都可能产出)→ **mutation (f) 想钉的性质被引号形态绕过**。与 P13「`bg-[#…]` 手写反斜杠转义对 minified 假阴性」、控制者「带引号 grep 对 minified 假阴性」**同族:模式只覆盖了自己见过的那种写法**。修法 `/style\s*=\s*(?:"[^"]*\|'[^'*])[^>]*?\bcolor\s*:/i`) | 防 inline 违规复发(本批刚删两处 2.2594 / 2.4776) | 加回 `style="color:#a3a3a3"` **或单引号形态** |
| 9 | **前提检查**:扫描到的文件数 ≥ 3 且 bootstrap html 长度 > 500 | 防"读 0 文件也报 0 违规"的假信心(P13 审查者 M5/M6、P14 终审者 R0 的同族教训) | 把路径改成不存在的文件 | | 9 | **前提检查**:扫描到的文件数 ≥ 3 且 bootstrap html 长度 > 500 | 防"读 0 文件也报 0 违规"的假信心(P13 审查者 M5/M6、P14 终审者 R0 的同族教训) | 把路径改成不存在的文件 |
| 10 | `AA_PAIRS` 含 `['ink','surface']` 且 `REQUIRED_KEYS` **不含** `info`、长度为 **11** | 改动②③ 的回归钉桩 | 删掉补钉的那一对;或把 `info` 加回 | | 10 | `AA_PAIRS` 含 `['ink','surface']` 且 `REQUIRED_KEYS` **不含** `info`、长度为 **11** | 改动②③ 的回归钉桩 | 删掉补钉的那一对;或把 `info` 加回 |
@@ -325,6 +327,7 @@ const targets = computed<RuntimeTarget[]>(() => resolveRuntimeTargets(props.game
- 腿 2 用 `indexOf` 比较位置时,**若某个 `indexOf` 返回 -1(未找到),`-1 < 任何正数` 会让顺序断言恒真** → 必须先断言三者都 `> -1`(`themeBootstrap.test.ts:42-44` 正是这么写的,照抄这个手法) - 腿 2 用 `indexOf` 比较位置时,**若某个 `indexOf` 返回 -1(未找到),`-1 < 任何正数` 会让顺序断言恒真** → 必须先断言三者都 `> -1`(`themeBootstrap.test.ts:42-44` 正是这么写的,照抄这个手法)
- 腿 6 的"逐值一致"**不得写成 `max(a,b) ≥ k` 形态**(P13 的恒真高发区:互补两侧有非平凡下界);应写成**字符串相等** - 腿 6 的"逐值一致"**不得写成 `max(a,b) ≥ k` 形态**(P13 的恒真高发区:互补两侧有非平凡下界);应写成**字符串相等**
- 腿 10 的长度断言须是 `toBe(11)` 而非 `toBeGreaterThanOrEqual(…)`(后者在删令牌后仍绿 = 无牙) - 腿 10 的长度断言须是 `toBe(11)` 而非 `toBeGreaterThanOrEqual(…)`(后者在删令牌后仍绿 = 无牙)
- 🔴 **(⚠️ RE-PIN 2026-10-03 补:原三条遗漏的第四类恒真形态)循环变量集合自身可被清空 → `for` 执行零次 = 恒真空绿**。审查者 **H4** 实测:清空 `DARK_TOKENS` 后腿 6 的 `for` 零次执行、`RED=[]`,**而退化是真的**——这是 P13 审查者 **M5**(扫描根改 e2e → 扫 0 个 `.vue`)/ **M6**(`HEX_RE` 改恒不匹配)的**精确同族**。**修法:每条以集合驱动的腿都须加 `expect(LIST.length).toBe(N)`**(腿 6 加 `DARK_TOKENS.length === 6`、腿 7 加 `LIGHT_HEX.length === 6`,与腿 10 的 `toBe(11)` 同款手法)
### T2 —— 改动②③(守卫腿 10 由红转绿) ### T2 —— 改动②③(守卫腿 10 由红转绿)
@@ -337,15 +340,15 @@ const targets = computed<RuntimeTarget[]>(() => resolveRuntimeTargets(props.game
按 §3.1 执行。完成后: 按 §3.1 执行。完成后:
- `npx vitest run` 全绿 - `npx vitest run` 全绿
- `npm run typecheck`(`vue-tsc --noEmit`)零错 - `npm run typecheck`(`vue-tsc --noEmit`)零错
- `adapters.test.ts` 的既有 4 处调用**零修改**仍绿(验证 `theme` 是可选键) - `adapters.test.ts` 的既有 **11 处**调用**零修改**仍绿(验证 `theme` 是可选键)
### T4 —— e2e 与构建产物验证 ### T4 —— e2e 与构建产物验证
1. `npm run build`(含 `vue-tsc --noEmit` + `vite build` + `build-runtime.mjs`)exit 0 1. `npm run build`(含 `vue-tsc --noEmit` + `vite build` + `build-runtime.mjs`)exit 0
⚠️ **`npm run build` 会类型检查 `e2e/*.spec.ts`**(P13 spike 6 曾因此 `BUILD_EXIT=2`) ⚠️ **`npm run build` 会类型检查 `e2e/*.spec.ts`**(P13 spike 6 曾因此 `BUILD_EXIT=2`)
2. `npm run e2e` 主套件 **102+1skip** 基线不回退(P13 交付值;`dark.spec.ts` 10 腿须全绿) 2. `npm run e2e` 主套件 **102+1skip** 基线不回退(P13 交付值;`dark.spec.ts` 10 腿须全绿)
3. **新增 e2e 腿**(`dark.spec.ts` 或新文件):暗色 + 虚拟游戏页 → 断言 iframe 的 `src` hash 含 `theme=dark`。**若 e2e fixture 无虚拟游戏可跑,改为 vitest 腿**(挂 `GameHost` 用 `@vue/test-utils`,断言 `targets[0].url` 含 `theme=dark`)——由实现者按 fixture 实际情况定,但**必须有一腿覆盖 hash 注入**,否则改动①的父侧无守卫。 3. **新增 e2e 腿**(⚠️ RE-PIN 2026-10-03:**偏离 5 已被裁定驳回**,本项按原意执行,不接受「fixture 不支持所以不测」):(a) **端到端腿**——暗色 + 虚拟游戏页,用 **`context.route`(不是 `page.route`)** + inert sw.js 冻住加载屏,断言 **`snap.url === 父侧 iframe src`**(**因果链闭合**:子文档读到的就是父侧算出的那个 url,不是测试手搓的 hash)、`attr=dark`、`bg=rgb(23,20,15)`、h1 是加载屏、`theme-color` meta = `#17140f`、**+8s 后 `path` 仍是 `/__bootstrap`**;另加亮色格(`stored=light` 压过系统暗色)。🔴 **根因纠正**:实现者四种拦截手法**全用 `page.route`**,审查者 PROBE-6A 实测**三个 route 命中数全为 0**(SW 注册请求、SW 发起的请求、被 SW `respondWith` 合成的导航都**不经 page 级路由**)→ 「挂起从未生效,真 SW 照常安装 → `runtime:ready` → `location.replace('/')`」被误归因为「加载屏本质瞬态」。改 `context.route` 后 PROBE-7A `sw.js hits = 1`、子文档稳定停在 `/__bootstrap`;PROBE-7B **T+0/T+10s/T+25s 三次全 `attr=dark`**,T+25s 因 Playwright 自身 `timeout: 45_000` 中断而**不是被顶掉**。(b) **行为腿**——游戏页切主题 → iframe `src` **不变**(同一 inert-SW 手法下可确定性断言):这是 D-B 那个 critical 性质的**唯一行为级防线**(腿 5 改回全文断言后仍有残留局限,见 §5-T1 腿 5)。(c) **反方向格**——子侧 `[系统暗色] × [hash=light]` → 期望 **`light`**(6 行):审查者实测 **H1**(白名单提取成变量 + prefers 优先)与 **H3**(拆中间变量的嵌套三元)都让腿 2 全绿,而**缺陷是真的**(显式选亮色的暗色系统用户看到暗色加载屏,违反 D-A「hash 优先」);且 **H1 同时逃过源码守卫(10 passed)与全部 5 条 e2e 腿(5 passed)**。缺失的正是这一格,它是 H1/H3 的**唯一鉴别格**。对照:主应用 `dark.spec.ts:169` **有**这条反方向腿(「stored 压过 system(反方向)」),bootstrap 侧缺
4. **产物验证**(P13 教训:源码级守卫不等于产物正确):`dist/` 里 bootstrap 入口的 HTML **含暗色块**、**不含** `#a3a3a3` 与 `#f87171`(已删的 inline 违规值);主应用 CSS 里 `--color-info` **零命中**、`var(--color-*)` 计数与 P13 交付值(75)比对并解释差异(删一个令牌应使计数变化,须说明)。 4. **产物验证**(P13 教训:源码级守卫不等于产物正确):⚠️ **必须在 e2e 之后重跑生产 `npm run build` 之上做**——`npm run e2e` 内部跑 `build:e2e`(`playwright.config.ts:12` 的 `webServer.command`)会**覆盖 `dist/`**,并用 `grep -c -F 'localhost:4173' src/dist/bootstrap/index.html` = **0** 证明量的是生产构建而非 e2e 残留(⚠️ RE-PIN 2026-10-03:控制者当日正是量了 e2e 残留,导致它的 7069 与实现者的 7056 本就不该相等而被当成矛盾;实现者报告 §2 已明写这个顺序陷阱)。判据:`dist/bootstrap/index.html` **含暗色块**、**不含** `#a3a3a3` 与 `#f87171`;主应用 CSS 里 `--color-info` **零命中**、两个色值(`2b62cc`/`7aa7f0`)**零命中**;**定义侧**令牌数 **12→11**(源码 `@theme` 与 `html[data-theme="dark"]` 各 11、产物暗色块 11);**usage 侧** `var(--color-*)` 计数与 P13 交付值 **75 保持不变**——`info` 是**零 usage 的死令牌**(7 种 utility 后缀 `bg-`/`text-`/`border-`/`ring-`/`fill-`/`stroke-`/`outline-` 全部零命中),删它**必然**不改变 usage 计数,这是预期行为而非缺陷。🔴 **判据方向**:若 usage 计数**发生变化**,才说明存在对 `info` 的引用,与 §1.3 的「零使用」取证前提矛盾,须停下来重新取证。**定义侧与 usage 侧是两个口径,不得混用**(⚠️ RE-PIN:原判据「删一个令牌应使计数变化」方向写反了,由实现者发现、审查者独立复现成立)。⚠️ **产物 grep 用 `grep -F`**,不手写转义、不用带引号模式(minifier 会去属性引号:产物是 `data-theme=dark` 而非 `data-theme="dark"`;CSS 类名是**转义**形态 `.bg-\[\#…\]`)。⚠️ **报数字必须同时报口径**:字节用 `wc -c`(**不要用 `len(str)` 标注成 B**,那是字符数,差值精确等于 UTF-8 多字节贡献)、计数**同时给 raw 与屏蔽注释后两个数**并声明大小写敏感性、行数用 `wc -l`
⚠️ **产物 grep 用 `grep -F` 不手写转义**(P13 教训:`.backdrop\:bg-scrim` 手写反斜杠转义对 minified 产物假阴性;AGENTS.md 已立不变量) ⚠️ **产物 grep 用 `grep -F` 不手写转义**(P13 教训:`.backdrop\:bg-scrim` 手写反斜杠转义对 minified 产物假阴性;AGENTS.md 已立不变量)
### mutation 自查(实现者必做并附输出,每条测"预期红 + 其余绿 + 恢复证明") ### mutation 自查(实现者必做并附输出,每条测"预期红 + 其余绿 + 恢复证明")
@@ -394,14 +397,14 @@ const targets = computed<RuntimeTarget[]>(() => resolveRuntimeTargets(props.game
| 风险 | 缓解 | | 风险 | 缓解 |
|---|---| |---|---|
| **`theme` 键写成必填 → 破既有测试** | §3.1(d) 明令可选;`adapters.test.ts` 4 处调用零修改仍绿是验收项 | | **`theme` 键写成必填 → 破既有测试** | §3.1(d) 明令可选;`adapters.test.ts` **11 处**调用零修改仍绿是验收项 |
| **🔴 注入改成响应式 → 主题切换重载运行中的游戏** | D-B + §3.1(e) 的 ⚠️ 注释 + **T1 腿 5 与 mutation (d) 专门钉这条**。这是本批最容易"顺手改错"且后果最严重的一处(用户存档丢失),且**症状只在真实使用中显现**(切主题时游戏重启),单元测试不会自然暴露 | | **🔴 注入改成响应式 → 主题切换重载运行中的游戏** | D-B + §3.1(e) 的 ⚠️ 注释 + **T1 腿 5 与 mutation (d) 专门钉这条** + **§5-T4.3(b) 的行为腿**(RE-PIN 2026-10-03)。⚠️ **腿 5 只钉字面形态,不够**:审查者 F-1(critical)实测 **G1**(调用点字面量与交付版**逐字相同**、依赖在 computed 体内别处建立)与 **G2**(import 别名 + 本地同名响应式包装)**双双绕过**(10 腿全绿、`vue-tsc` exit 0),而用 `effectScope` + `computed` 求值计数**证明缺陷语义上真实**(基准 `evals=1`/url 不变;G1/G2 均 `evals=2`、url 从 `…theme=light` 翻成 `…theme=dark` → **iframe `src` 变化 → 运行中的游戏被重载、存档丢失**)。**修法(一行 ×2)**:腿 5 的两条负向断言从 `args`(调用点内)改回 `gameHostMasked`(已屏蔽注释的**全文**)——审查者逐字验证 `maskSourceComments()` **本身已解决**「陷阱注释必然写出被禁字面量」的冲突(屏蔽后 `useTheme()` 与 `theme.value` 都从文件消失、`effectiveTheme` 真实代码保留、陷阱注释 28–33 行屏蔽后全空)→ **缩小断言范围不必要,第二道防线反而制造了缺口**(与 P14 终审「在第一轮修复自身里找到洞」完全同构)。已验五种组合:交付形态+G1/G2 = 绿(绕过);改法+G1/G2 = **红(抓到)**;改法+合法树 = **10 腿全绿(不误红)**。⚠️ **残留局限须写进守卫注释、不得声称「唯一手段」**(纪律 #4):改后仍抓不住「在 computed 之外的**模块级作用域**读 ref 再传值」,但该形态**无害**(快照在模块加载时求值一次、无依赖 → 不会重载);真正的解药是行为腿 |
| **判定序与主应用分叉 → 同一用户两处不同主题** | T1 腿 2 钉 bootstrap 内部顺序;**另需一条腿比对 `useTheme.effectiveTheme()` 的后两级顺序**(`themeBootstrap.test.ts` 腿 2 的同款手法:用 `indexOf` 比较,且先断言三者都 `> -1`) | | **判定序与主应用分叉 → 同一用户两处不同主题** | T1 腿 2 钉 bootstrap 内部顺序;**另需一条腿比对 `useTheme.effectiveTheme()` 的后两级顺序**(`themeBootstrap.test.ts` 腿 2 的同款手法:用 `indexOf` 比较,且先断言三者都 `> -1`) |
| **bootstrap 与主应用调色板分叉**(bootstrap 自包含 hex) | T1 腿 6 逐值比对六个暗色 hex + 腿 7 钉亮色值不变。**这是 D-E 方案(引令牌)被否后的必要补偿** | | **bootstrap 与主应用调色板分叉**(bootstrap 自包含 hex) | T1 腿 6 逐值比对六个暗色 hex + 腿 7 钉亮色值不变。**这是 D-E 方案(引令牌)被否后的必要补偿** |
| **新守卫写成恒真** | §5-T1 的 ⚠️ 三条(`indexOf` 返 -1 时顺序断言恒真、`max(a,b) ≥ k` 形态、`toBeGreaterThanOrEqual` 无牙)+ mutation (i) 腿 9 前提检查 + (a)–(h) 逐条验牙 | | **新守卫写成恒真** | §5-T1 的 ⚠️ 三条(`indexOf` 返 -1 时顺序断言恒真、`max(a,b) ≥ k` 形态、`toBeGreaterThanOrEqual` 无牙)+ mutation (i) 腿 9 前提检查 + (a)–(h) 逐条验牙 |
| **inline 违规复发** | T1 腿 8 + mutation (f)。**本批发现的两处是 pre-existing(P13 之前就存在),修完须有钉桩**,否则下一次改 bootstrap 又会加回来 | | **inline 违规复发** | T1 腿 8 + mutation (f)。**本批发现的两处是 pre-existing(P13 之前就存在),修完须有钉桩**,否则下一次改 bootstrap 又会加回来 |
| **产物与源码不一致**(源码级守卫绿但产物错) | §5-T4.4 产物验证。P13 教训:`APPLY.toString()` + `new Function` 序列化注入验证构建期烘焙是**无效手法**;必须验真实 `dist/` | | **产物与源码不一致**(源码级守卫绿但产物错) | §5-T4.4 产物验证。P13 教训:`APPLY.toString()` + `new Function` 序列化注入验证构建期烘焙是**无效手法**;必须验真实 `dist/` |
| **e2e fixture 无虚拟游戏 → hash 注入无覆盖** | §5-T4.3 给了退路(改 vitest 腿挂 `GameHost`),但**必须有一腿覆盖**,不接受"fixture 不支持所以不测" | | **e2e fixture 无虚拟游戏 → hash 注入无覆盖** | ⚠️ RE-PIN 2026-10-03:**该风险的前提被证伪**——审查者实测加载屏**可**确定性冻结(`context.route` + inert sw.js,稳定 ≥25s),并已写出跑绿 2 条端到端腿(8.9s + 0.6s)。原「退路」(改 vitest 腿挂 `GameHost`)**删除**;§5-T4.3 的「必须有一腿覆盖」按原意执行。**快照存档不能替代断言**(快照是一次性观测、不会自己变红)——与 P13 教训同一枚硬币的两面:那边是「产物验证不能替代源码守卫」,这边是「**存档观测不能替代 CI 断言**」 |
--- ---
@@ -419,3 +422,6 @@ const targets = computed<RuntimeTarget[]>(() => resolveRuntimeTargets(props.game
10. **`restore()` 只回滚 mutation 触及的文件,绝不用 `git checkout -- .`**;每轮恢复后核验守卫编辑仍在,否则 FATAL 终止。 10. **`restore()` 只回滚 mutation 触及的文件,绝不用 `git checkout -- .`**;每轮恢复后核验守卫编辑仍在,否则 FATAL 终止。
11. **`|| echo "(zero)"` 这类兜底文本在 glob 失败时会伪装成"测量结果为零"** → 任何"零命中"结论都要先证明扫描范围非空(T1 腿 9 就是这条的机械化)。 11. **`|| echo "(zero)"` 这类兜底文本在 glob 失败时会伪装成"测量结果为零"** → 任何"零命中"结论都要先证明扫描范围非空(T1 腿 9 就是这条的机械化)。
12. **测试被迫修改 = 设计失败的信号**,除非 spec 明列例外(本批唯一例外:`contrast.test.ts` 的 "12→11 令牌" 文案)。若发现必须改其它测试,**停下来报告**。 12. **测试被迫修改 = 设计失败的信号**,除非 spec 明列例外(本批唯一例外:`contrast.test.ts` 的 "12→11 令牌" 文案)。若发现必须改其它测试,**停下来报告**。
13. 🔴 **(RE-PIN 2026-10-03,审查者 F-2)手法的适用范围没验证就当已穷尽**:`page.route` **拦不到** SW 注册请求、SW 发起的请求、以及被 SW `respondWith` 合成的导航 → 冻住 SW 驱动的加载屏须用 **`context.route`**。**用错层级会让「挂起」静默失效,并把结果误归因为「被测对象本质瞬态」**——实现者四种手法全用 `page.route`(实测命中 0),据此得出「加载屏无法冻结、端到端腿必然 flaky」并撤掉了腿,而归因是错的。与第 6 条(带引号 grep 对 minified 假阴性)**同族**。**换一层再试一次,再下结论。**
14. **(RE-PIN 2026-10-03,审查者 F-9)报数字必须同时报口径**:字节用 `wc -c`(**不要用 `len(str)` 标注成 B**——那是字符数,差值精确等于 UTF-8 多字节贡献:CSS 12、html 532);计数**同时给 raw 与屏蔽注释后两个数**并声明大小写敏感性(控制者用 raw、实现者用屏蔽注释+大小写不敏感 → 同一份产物两份互斥数字、第三方无法判定谁错);行数用 `wc -l`(不要 `split('\n')`);**产物验证须在 e2e 之后重跑生产 build**(`npm run e2e` 内部跑 `build:e2e` 会覆盖 `dist/`)。**没有口径的数字无法交叉核对,等于没报。**
15. **一次性验证脚本一律落盘存证**(控制者的 CSS 解析器没落盘 → 结论虽被审查者独立复现,但**实现不可审计**)。