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:
@@ -56,7 +56,7 @@ const targets = computed<RuntimeTarget[]>(() => resolveRuntimeTargets(props.game
|
||||
| ❌ `theme: useTheme().theme.value` | **是**(读 `ref` 的 getter → computed 建立依赖) | 用户在游戏页切主题 → `targets` 重算 → **iframe `src` 变化 → 正在运行的游戏被重载**(存档/进度丢失) |
|
||||
| ✅ `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 的令牌归属)**:
|
||||
|
||||
@@ -106,7 +106,7 @@ const targets = computed<RuntimeTarget[]>(() => resolveRuntimeTargets(props.game
|
||||
```
|
||||
|
||||
**缺口**:`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 次错误:把互斥分支当同元素共现,是假阳性)
|
||||
- **58 处靠继承**:全局文字色源是 `src/index.html:34` `<body class="bg-paper text-ink font-sans antialiased">` → 继承到 **`ink`**
|
||||
- 实测对比度:亮 **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` 字样也只出现在那两行。
|
||||
|
||||
**矛盾点**:它在 `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 个文件,不是"顺手删两行")**:
|
||||
|
||||
@@ -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-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-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 即修复,**同时消除"两处定义同一元素颜色"的分歧源** |
|
||||
@@ -173,7 +173,7 @@ const targets = computed<RuntimeTarget[]>(() => resolveRuntimeTargets(props.game
|
||||
### §1.4 `noHardcodedColor` 为何无法覆盖 bootstrap(D-D 的依据,逐字取证)
|
||||
|
||||
```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))
|
||||
// 终审 N4:扫描根必须含 `runtime/`。…实测 runtime/ 当前零硬编码 hex,扩根不会立刻红。
|
||||
const SCAN_ROOTS = [join(SRC_ROOT, 'app'), join(SRC_ROOT, 'runtime')]
|
||||
@@ -208,8 +208,8 @@ function maskNonTemplate(content: string): string {
|
||||
```html
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<meta name="theme-color" content="#F7F2E7" />
|
||||
<!-- P15-B D-A/D-G:加载屏 pre-paint 主题脚本。必须在 <style> 与 <body> 之前,否则暗色用户每次启动游戏都闪白。
|
||||
判定序比主应用少 stored 一级(游戏跑在独立子域,localStorage 按源隔离读不到主域的 crearte.theme.v1):
|
||||
<!-- P15-B D-A/D-G:加载屏 pre-paint 主题脚本,须在样式与文档体之前执行,否则暗色用户每次启动游戏都闪白。
|
||||
判定序比主应用少 stored 一级(游戏跑在独立子域,浏览器存储按源隔离,读不到主域的 crearte.theme.v1):
|
||||
hash theme → prefers-color-scheme → light。后两级与 useTheme.effectiveTheme() 逐字一致,
|
||||
一致性由 app/lib/bootstrapTheme.test.ts 钉住(改一处必须改另一处)。 -->
|
||||
<script>
|
||||
@@ -232,9 +232,11 @@ function maskNonTemplate(content: string): string {
|
||||
})()
|
||||
</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` 必须被忽略)。
|
||||
|
||||
**(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 达标。
|
||||
|
||||
@@ -244,7 +246,7 @@ export function resolveRuntimeTargets(game: Game, opts: {
|
||||
baseDomain: string; protocol: string; config?: ReturnType<typeof runtimeConfig>; theme?: 'light' | 'dark'
|
||||
}): 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):
|
||||
```js
|
||||
@@ -314,8 +316,8 @@ const targets = computed<RuntimeTarget[]>(() => resolveRuntimeTargets(props.game
|
||||
| 4 | 脚本是 **IIFE 且只用 `var`**(无 `let`/`const`/箭头函数/`import`) | D-G 的 CSP 前形态 | 把 `var` 改成 `const` |
|
||||
| 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` 改成别的值 |
|
||||
| 7 | bootstrap 亮色值**未被改动**(钉 §1.1 表的六个亮色 hex 仍在) | 防"顺手把亮色也改了"(本批只加暗色) | 改一个亮色值 |
|
||||
| 8 | `bootstrap/index.html` 内**不存在 inline `style` 里的 `color:`**(D-E 的回归钉桩) | 防 inline 违规复发(本批刚删两处 2.2594 / 2.4776) | 加回 `style="color:#a3a3a3"` |
|
||||
| 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 的回归钉桩)—— ⚠️ **正则须同时覆盖单引号与双引号属性形态**(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 的同族教训) | 把路径改成不存在的文件 |
|
||||
| 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` 正是这么写的,照抄这个手法)
|
||||
- 腿 6 的"逐值一致"**不得写成 `max(a,b) ≥ k` 形态**(P13 的恒真高发区:互补两侧有非平凡下界);应写成**字符串相等**
|
||||
- 腿 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 由红转绿)
|
||||
|
||||
@@ -337,15 +340,15 @@ const targets = computed<RuntimeTarget[]>(() => resolveRuntimeTargets(props.game
|
||||
按 §3.1 执行。完成后:
|
||||
- `npx vitest run` 全绿
|
||||
- `npm run typecheck`(`vue-tsc --noEmit`)零错
|
||||
- `adapters.test.ts` 的既有 4 处调用**零修改**仍绿(验证 `theme` 是可选键)
|
||||
- `adapters.test.ts` 的既有 **11 处**调用**零修改**仍绿(验证 `theme` 是可选键)
|
||||
|
||||
### T4 —— e2e 与构建产物验证
|
||||
|
||||
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`)
|
||||
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 注入**,否则改动①的父侧无守卫。
|
||||
4. **产物验证**(P13 教训:源码级守卫不等于产物正确):`dist/` 里 bootstrap 入口的 HTML **含暗色块**、**不含** `#a3a3a3` 与 `#f87171`(已删的 inline 违规值);主应用 CSS 里 `--color-info` **零命中**、`var(--color-*)` 计数与 P13 交付值(75)比对并解释差异(删一个令牌应使计数变化,须说明)。
|
||||
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 教训:源码级守卫不等于产物正确):⚠️ **必须在 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 已立不变量)
|
||||
|
||||
### mutation 自查(实现者必做并附输出,每条测"预期红 + 其余绿 + 恢复证明")
|
||||
@@ -394,14 +397,14 @@ const targets = computed<RuntimeTarget[]>(() => resolveRuntimeTargets(props.game
|
||||
|
||||
| 风险 | 缓解 |
|
||||
|---|---|
|
||||
| **`theme` 键写成必填 → 破既有测试** | §3.1(d) 明令可选;`adapters.test.ts` 4 处调用零修改仍绿是验收项 |
|
||||
| **🔴 注入改成响应式 → 主题切换重载运行中的游戏** | D-B + §3.1(e) 的 ⚠️ 注释 + **T1 腿 5 与 mutation (d) 专门钉这条**。这是本批最容易"顺手改错"且后果最严重的一处(用户存档丢失),且**症状只在真实使用中显现**(切主题时游戏重启),单元测试不会自然暴露 |
|
||||
| **`theme` 键写成必填 → 破既有测试** | §3.1(d) 明令可选;`adapters.test.ts` **11 处**调用零修改仍绿是验收项 |
|
||||
| **🔴 注入改成响应式 → 主题切换重载运行中的游戏** | 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`) |
|
||||
| **bootstrap 与主应用调色板分叉**(bootstrap 自包含 hex) | T1 腿 6 逐值比对六个暗色 hex + 腿 7 钉亮色值不变。**这是 D-E 方案(引令牌)被否后的必要补偿** |
|
||||
| **新守卫写成恒真** | §5-T1 的 ⚠️ 三条(`indexOf` 返 -1 时顺序断言恒真、`max(a,b) ≥ k` 形态、`toBeGreaterThanOrEqual` 无牙)+ mutation (i) 腿 9 前提检查 + (a)–(h) 逐条验牙 |
|
||||
| **inline 违规复发** | T1 腿 8 + mutation (f)。**本批发现的两处是 pre-existing(P13 之前就存在),修完须有钉桩**,否则下一次改 bootstrap 又会加回来 |
|
||||
| **产物与源码不一致**(源码级守卫绿但产物错) | §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 终止。
|
||||
11. **`|| echo "(zero)"` 这类兜底文本在 glob 失败时会伪装成"测量结果为零"** → 任何"零命中"结论都要先证明扫描范围非空(T1 腿 9 就是这条的机械化)。
|
||||
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 解析器没落盘 → 结论虽被审查者独立复现,但**实现不可审计**)。
|
||||
|
||||
Reference in New Issue
Block a user