Files
crearte-monorepo/docs/plans/2026-10-03-p15b-bootstrap-dark.md
XingfenD 51bed55704 docs(spec): close the P15-B branch review's document-side findings
The P15-B branch review returned "needs fixing before merge" on a critical gap
in the guard layer. The code side is with a second fix-forward; this commit
closes the document side, which is the controller's.

F-FINAL-4 - the leg-8 cell carried a one-line regex that the controller wrote
into the spec during re-pin. It was worse than the version in the adjudication
it copied: 4 of 7 attribute forms fail rather than 2, because markdown escaping
turned the alternation bar into a literal bar and `'[^']*'` was mistyped as
`'[^'*]`. The controller had checked that every table row has the same number of
pipes as the header, but never checked that the escaped code was still the code
it came from. The cell now describes the implemented approach (extract every
style attribute value in all three quoting forms, strip quotes, test each for
color:) and deliberately carries no regex literal, with the reason stated: an
escaped bar inside a table cell is a literal character in the regex. Read the
code entity for the pattern.

§6-1 - leg 9 was never re-pinned: the spec still said "length > 500" while the
code says 4000 plus three structural preconditions. Corrected, with the unit
spelled out (source characters 4423, not the built artifact's 7056/7588).

§6-2 - "the only discriminating grid" survived re-pin in four places; the
review measured two legs reddening on a real decision-order defect. Narrowed to
"the only discriminating grid on the child-side direct-visit path" here and in
the plan. The two occurrences in code comments belong to the fix-forward.

§6-3/§6-4/§6-5 - the §1.2 count now states which tree it came from (base 66,
HEAD 67, the extra one being this batch's own note text quoting the figure); the
§4 boundary table lists the e2e spec it had omitted; D-G says "form aligns"
rather than "aligns verbatim", since the script bodies differ by design (the
main app has one more level, 16 token-level differences).

F-FINAL-1 - two claims already committed are corrected rather than deleted. The
§3.1(a) annotation and the §7 risk row both rested on "maskSourceComments()
neutralises the trap comment, so the guard does not depend on the implementer's
wording discipline". That is true for the file in question, but only because the
trap comment happens to use `//`: the function's HTML-comment branch compared a
2-character slice against a 4-character literal and never ran. Merely rewording
a comment reddened up to four legs while the e2e suite stayed green.

§8 gains the census case for discipline 15 (an unlanded census script is why the
48-versus-47 discrepancy is still untraceable), plus disciplines 16 and 17:
every evidence file a report cites must be ls-checked before delivery, and
executable details in a spec must be grepped from the code entity or recomputed,
never hand-written. Discipline 17 records this batch's three instances, one of
them a contrast ratio cited twice by two documents and recomputed by nobody
(3.0269; the actual value is 2.5519, which no grey background produces).

Post-write checks: 25/25 assertions, 11 table blocks with 0 column anomalies,
16 code fences paired, 25 headings with no duplicates, copyable-block cleanliness
scanned across 8 blocks.
2026-10-04 02:53:53 +08:00

134 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# P15-B 实现计划:bootstrap 加载屏暗色 + 对比度守卫补钉 + 死令牌清理
- **spec(权威)**:`docs/specs/2026-10-03-p15b-bootstrap-dark-design.md`
- **取证账本**:`crearte/.superpowers/sdd-p15/survey.md`(§2/§3/§5/§7/§8 全部实测;含"叉积当违规清单"与"三元互斥分支当同元素共现"两次自我纠错)
- **仓**:`crearte`(**仅此一个**)· **base** `f823195`(master)· 分支 `feat/p15b-bootstrap-dark-and-guard-pins`
> ⚠️ 分支前缀用 `feat/`(AGENTS.md 只允许 `{feat|fix|docs|chore}/`)。本批含用户可感知的暗色改动 → `feat/`;若实现者认为纯守卫部分占主体,**也不得改用 `refactor/`**(不在允许集内)。
> ⚠️ inner repo 红线:**不得在 `master` 上提交**。`git commit` 前先 `git branch --show-current` 确认。
- **性质**:用户可见改动(加载屏暗色)+ 守卫补强 + 死代码清理,**三者一批**(spec D-A…D-H)
- **目标版本**:crearte **0.27.0** · wrapper **0.3.8**
- **派发**:**一个**实现者子代理做完 T1→T4(所有任务同动 crearte 单一工作树与 git index,`sdd-parallel-dispatch` §1 一仓一写者)
- **并行**:P15-A 在 `crearte-server` 仓同时进行 → **不得触碰 `crearte-server/`、`crearte-deploy/`、wrapper 的任何文件**
> spec 与本计划冲突时 **spec 赢**。spec 全文(§0–§8)必读,尤其 **§1.1 的 🔴 架构陷阱(响应式注入会让运行中的游戏被重载)**、§1.3 的连带面表(5 文件)、§1.4(为何不能扩 `noHardcodedColor`)、§2.1 被否方案 A–F、§5-T1 的 10 腿清单 + ⚠️ 恒真审视三条、§8 纪律 12 条(前端 4 条加粗)。
---
## 关键路径约定(与后端不同,勿照搬 P15-A)
- **前端源码与 `package.json` 都在 `crearte/src/`,不在仓根**。所有 `npx`/`npm` 命令须 `cd crearte/src`。(P15 取证期控制者两次因 `cd` 层级写错路径而拿到假结果:`cd` 仓根却写 `app/...`(真根 `src/app/...`)致 grep 全失败输出 `bg-surface=0`;`ls ../e2e` 猜错致空输出。**任何"零命中"结论都要先证明扫描范围非空。**)
- **测试在宿主机跑 `npx vitest run`,不进容器**(P13 plan 第 58 行同款先例:`npx vitest run app/lib/tableOverflow.test.ts app/lib/tableScope.test.ts`)。`crearte/src/node_modules` 已装(232M)。AGENTS.md 的"一切在容器内"针对的是 compose 栈与应用服务,**不针对前端单测**;e2e 需要浏览器,同样在宿主机跑。
- **vitest 环境是 node**(`src/vite.config.ts` 的 `test` 段只有 `exclude`,**无 `environment` 键**)→ 源码级守卫可用 `fileURLToPath(new URL('../..', import.meta.url))` 读文件。**P13 教训:happy-dom 下该写法抛 `ERR_INVALID_URL_SCHEME`**;本批新增守卫**不得引入需要 DOM 的断言**。
---
## 任务表
| 任务 | 交付物 | 要求 | 完成判据 |
|---|---|---|---|
| **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** 项不动 |
| **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 自查 | ⚠️ **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 在**子侧直访**路径上的唯一鉴别格;端到端亮色格是第二个鉴别格(RE-PIN 2026-10-04))。产物验证**必须在 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) 对这七条**全部无牙**)+ 全部对照组 + 合法树 |
---
## 验收(控制者独立复跑,不采信自报)
### 测试与构建
```bash
cd crearte/src
npx vitest run # 基线 79 文件 / 688 测试 → 目标 80 / 688+N
npm run typecheck # vue-tsc --noEmit,零错
npm run build # 含 vue-tsc + vite build + build-runtime.mjs
npm run e2e # 基线 102+1skip(dark.spec.ts 10 腿)
```
基线(spec §0,已实测 2026-10-03 13:07):**vitest 79 文件 / 688 测试全绿,Duration 44.83s**——与 P13 交付值逐字相同(P14 未触及前端)。**e2e 102+1skip** 是 P13 交付值。
⚠️ `npm run build` 含 `vue-tsc --noEmit`,会**类型检查 `e2e/*.spec.ts`**;spike 期间若只想快速验产物可用 `npx vite build`(P13 纪律 5 的同款做法),但**交付验收必须跑完整 `npm run build`**。
### 结构指标(spec §5 表,交付时须报实测数字)
| 指标 | 基线 | 目标 |
|---|---|---|
| vitest 文件数 / 测试数 | **79 / 688** | **80 / 688+N**(N ≥ 10) |
| 既有 `*.test.ts` 修改文件数 | — | **1**(仅 `contrast.test.ts`,仅因 "12→11 令牌" 文案;**须在报告显式声明**) |
| `AA_PAIRS` 对数 | 10 | **11** |
| `REQUIRED_KEYS` 项数 | 12 | **11** |
| `LEGACY_KEYS` 项数 | 9 | **9**(不动) |
| `--color-info` 全仓命中 | 2(两处定义) | **0** |
| bootstrap 内 `localStorage` / `prefers-color-scheme` / `data-theme` 命中 | **0 / 0 / 0** | **0 / ≥1 / ≥1**(`localStorage` **仍须为 0**:源隔离,spec D-A) |
| bootstrap 内 inline `style` 的 `color:` 数 | 2 | **0** |
| `typecheck` / `build` | 0 / 0 | **0 / 0** |
| 主 e2e | 102+1skip | **≥102+1skip**(新增腿另计) |
### mutation 抽查(spec §5-T1 的 (a)–(j),控制者独立复现,不复用实现者结果)
**十条**,每条测"预期红 + 其余绿 + 恢复证明":
| # | mutation | 预期 |
|---|---|---|
| (a) | bootstrap 的 pre-paint `<script>` 移到 `<style>` 之后 | 腿 1 红 |
| (b) | 交换 hash theme 与 `prefers-color-scheme` 的判定先后 | 腿 2 红 |
| (c) | hash theme 校验放宽成 `hashTheme ? hashTheme : …` | 腿 3 红 |
| (d) | **`GameHost.vue` 改用 `useTheme().theme.value`** | 腿 5 红(**这条是 spec D-B 的牙**:响应式注入会让主题切换重载运行中的游戏) |
| (e) | 改 bootstrap 暗色的一个 hex | 腿 6 红 |
| (f) | 加回 `style="color:#a3a3a3"` | 腿 8 红 |
| (g) | 从 `AA_PAIRS` 删掉补钉的 `['ink','surface']` | 腿 10 红 |
| (h) | 把 `info` 加回 `REQUIRED_KEYS` | 腿 10 红 |
| (i) | **守卫的文件路径改成不存在的文件** | 腿 9 红(防"读 0 文件报 0 违规"的假信心) |
| (j) | **删掉 `main.css` 的暗色块**(模拟 P13 成果回退) | 腿 6 或 `contrast.test.ts` 的暗色腿红 |
⚠️ **`restore()` 只回滚 mutation 触及的文件,绝不用 `git checkout -- .`**(P14 控制者第 24 次自伤:全量回滚抹掉了未提交的守卫编辑,导致后四轮全在测没有修复的树、汇总报"绿=3")。每轮恢复后核 `git diff HEAD --exit-code -- <file>` 空 + 守卫文件腿数不变。
⚠️ **写守卫时注释里不要出现完整的 `bg-[#…]` / `text-[#…]` 字面量**(spec §8-8:**Tailwind v4 扫描全部源文件含 `.test.ts`**,注释里的类名字面量会被当候选、烧死 utility 进产物。P13 实现者用拼接修对一处,控制者八分钟后在自己裁定 commit 里重新引入,靠重建抓出)。
### 产物验证(P13 教训:源码级守卫绿 ≠ 产物正确)
- **必须验真实 `dist/`**,不得用运行时注入或 `APPLY.toString()`+`new Function` 序列化注入替代(P13 控制者勘查期两次栽在这两种无效手法上)
- 选择器/字符串 grep **用 `grep -F`**,不手写反斜杠转义(P13:`.backdrop\:bg-scrim` 手写转义对 minified 产物假阴性)
- **带引号的 grep 模式对 minified 产物也假阴性**(`grep 'bg-[#]'` 匹配不到压缩形态)
---
## 边界(**不做**,spec §4)
- **不动 `themeBootstrap.test.ts`**(主应用 pre-paint 守卫 7 腿)——若某腿因本批变红,**停下来报告**
- **不扩 `noHardcodedColor.test.ts` 的扫描根**(spec §1.4 + 被否方案 D:`candidates()` 只收 `.vue`、`maskNonTemplate()` 会空白掉 `<style>` 块 → 扩根**扫不到任何东西**,只会制造"已覆盖"的假信心)
- **不动 `useTheme.ts`**(`effectiveTheme()` 已够用;改它会波及主应用 10 腿 e2e)
- **不动 `src/index.html`**(主应用入口,P13 交付)
- **不改 P13 的 CHANGELOG 0.26.0 条目与 P13 spec 原文**(spec D-C:改写会让"当时交付了什么"失真;改为新条目说明 + P13 spec 加 ⚠️ RE-PIN 标注块,那是控制者的记账动作)
- **不修 GameHost 亮色徽标 WCAG 3.26**(spec §4 挂账 + 被否方案 F:属**设计决策**,显而易见的一行修法已实测证伪——亮色 `accent` vs `accent-ink` 仅 **1.4943**,徽标与「加载失败」状态点同屏共存,改完会把两个语义压成一个视觉信号;ROADMAP `9965fd7` 已登记该约束)
- **不做第三态"恢复跟随系统"**(P13 spike 3 已证伪:header 在 @320px 最坏格无像素容纳带标签控件)
- 不新增任何令牌(只删 `--color-info`)
- 不碰 `crearte-server`、`crearte-deploy`、wrapper 的 `docs/`(实现者红线;记账由控制者做)
---
## 记账(控制者做,实现者禁动 `docs/`)
- `crearte/docs/CHANGELOG.md` 新增 **`## [0.27.0] - 2026-10-03`**(插 `## [0.26.0]` 前)
- wrapper `docs/CHANGELOG.md` 新增 **`## [0.3.8] - 2026-10-03`**(插 `## [0.3.7]` 前),小节 `### Done / 完成`;**与 P15-A 合并为一条还是分两条按合并时序定**
- `docs/ROADMAP.md`:第三波表新增 **P15-B 行** + 文档索引表新增一行(**哈希引 merge commit**);挂账里**删掉「死令牌 `--color-info`」**(已处置)、**新增「bootstrap 内联脚本的 CSP nonce」**(与主应用同批)
- P13 spec 加 **⚠️ RE-PIN 标注块**(spec D-C:12→11 令牌),**不改写原文**
- 格式:**同条目英文行紧跟中文行、无空行**;不同条目**空一行**;小节标题双语
- **纪律**:显式 `git add <file>`,**禁 `git add -A`**;wrapper 的 `IDENTITY.md`/`SOUL.md`/`USER.md` 三个未跟踪文件**绝不 stage**
- **合并**:CHANGELOG 改动提交到**特性分支** → `git checkout master` → `git merge --no-ff` → 记 merge 哈希 → 推送(**禁管道**)→ `git ls-remote origin master` **非空**对账(`-z` 检查,防空变量假 MATCH)→ 删分支
---
## 审查阶梯(P11–P14 惯例,不可省)
1. **实现者自报** + 证据落盘(RED 逐腿标注、mutation 十条、产物验证)
2. **控制者独立核实**(全部自己跑,不采信自报数字)
3. **任务级审查者**(只读,**自设 mutation 不照抄 spec 的 (a)–(j)**,审守卫恒真/恒假、spec 自身问题、**特别是腿 5 这条架构陷阱断言是否真能防住响应式注入**)
- P13 先例:10 条自设 mutation 挖出 5 条 findings,**M1 抓到控制者自己写错两次的恒真守卫**(`max(a,b) ≥ 3` 互补下界 4.0621)
- P14 先例:抓到 **spec D-J 的守卫目的无断言覆盖** + **spec 里 positive control 的绕行路径** + 控制者行数口径错(`split` vs `wc -l`)
4. **控制者逐条裁定**(`sdd-pre-merge-review` §3:**未裁定的 note 阻塞合并**,"PASS with notes" 不等于门过了)
5. **全分支终审者**(只读;spec 全文自洽性 + **审控制者的裁定工作** + 独立复现 mutation)
- P11 先例:终审裁定过「**需修复后合并**」→ **不是橡皮章**
- P14 先例:终审在**第一轮修复自身**里找到洞(匿名接收者 `func (*ContentService) M()` 绕过源码扫描),并**推翻控制者两处已入库的全称声明**
6. **spec re-pin**(裁定后一次性批量改,**勿在审查者读 spec 期间改**=移动靶);**代码改了 spec 必须同步**(P14:正则字面量写进 spec,改代码后 spec 立刻不一致,构成"需修复后合并"的同类形态)