Brings the P13 batch into this wrapper's version control: spec and plan enter the repo, ROADMAP gains the P13 row and its document-index entry, and the four stale 'C dark mode' backlog mentions carried since P9/P10/P9-B/P12 are marked cleared in place — following the P12 precedent of annotating the historical row rather than rewriting it, since those entries were true when written. Wrapper CHANGELOG 0.3.6 records the crearte-only dark-mode delivery (983e7c4, merge-base e59a171), the parallel server micro-batch delivered during the survey phase while the implementer owned crearte exclusively (dbf7fe5, 0.17.2: a real uploads.go error-fallthrough defect with zero prior coverage, plus a decision-record comment on the bundle-key route after grep overturned the initial suspicion of a hole), and three lessons: - max(a,b) >= k is a vacuous-assertion hot zone: when the two sides are complementary the max has a non-trivial lower bound (here sqrt(16.50) = 4.0621), so any threshold below it can never fail. The controller's own first correction to the scrim guard shipped exactly that, and the same one-directional verification recurred in the alpha fallback. Fixing a guard now requires proving both no false-red on legal values and no false-green under mutation. - Tailwind v4 scans every source file including test files, so a class-name literal in a test comment burns a dead utility into the artifact. - A universal claim needs a universal grep: 'accent is the only background use' was false because both the spike-0 grep and the new guard covered app/ while runtime/ sits beside it. That blind spot cost a false spec fact and hid a real pre-existing WCAG violation (paper on accent = 3.2590 in light, since P9-B). ROADMAP's P13 row cites merge commit 983e7c4 per the P12 convention, with the bookkeeping commits named separately so the reference cannot be mistaken for them.
14 KiB
14 KiB
P13 实现计划:暗色模式
- spec:
docs/specs/2026-10-03-p13-dark-mode-design.md(权威依据,冲突时以 spec 为准) - 证据归档:
crearte/.superpowers/sdd-p13/FINDINGS-survey.md(7 轮 spike 实测) - 分支:
feat/p13-dark-mode(已建,从 creartee59a171起) - 范围:仅
crearte。crearte-server与crearte-deploy零改动(spec §4 边界)。 - 版本:crearte 0.26.0 / wrapper 0.3.6(记账由控制者做,实现者禁动 ROADMAP、CHANGELOG、
docs/)。
任务切分与波次
全部任务都在 crearte 单一工作树与 git index 上 → 按 sdd-parallel-dispatch §1「一仓一写者」,交给一个子代理串行完成,不拆并行(拆了就是四路争用同一个 .git/index.lock,P12 已验证这条纪律)。
TDD 顺序:守卫先行。T1 写守卫并看它红(RED 输出必须复现 spec 引用的实测数字),再 T2 让它绿。P12 的经验:这个顺序使 RED 输出本身成为「守卫有牙」的证明,免去事后 revert 自证那一轮。
| 序 | 任务 | 文件 | 完成判据 |
|---|---|---|---|
| T1 | 守卫重构为双主题(RED 先行) | app/lib/contrast.test.ts |
按块解析(亮/暗各自 Map);两块各断言 12 令牌齐备;双主题各断言 spec §1.3/§7 的 12 条真实配对(阈值:正文 4.5 / wordmark 大字号 3.0 / 焦点环非文本 3.0);scrim 分离按主题钉实际分离侧(D-F):亮色钉填充侧 paper vs scrim ≥3(17.60)、暗色钉边框侧 ink vs scrim ≥3(17.60)——不得写成 max(两侧) ≥3,两侧互补使其理论下界 = √16.50 = 4.0621 > 3 → 数学恒真、永不可能失败(审查 finding #1,控制者暂力重算证实;实证:亮 scrim 改纯白得填充侧 1.1165 但 max()=18.42 → 仍绿)。也不得只钉 ink vs scrim 于两主题(亮色仅 1.07 → 误红);含「旧单 Map 解析器对含暗色块的 CSS 会得出错误结果」的反面钉桩(防后人简化回去)。此时暗色块尚不存在 → 必须 RED,红输出须显示「暗色 12 令牌缺失」 |
| T2 | 令牌层落地 | app/styles/main.css |
@theme 新增 --color-skeleton: #efe9da / --color-scrim: #0d0b08;五个 --shadow-hard* 内联 hex → var(--color-ink) / var(--color-accent)(D-D);追加 html[data-theme="dark"] { … } 全量 12 令牌 + color-scheme: dark(D-B/D-C,必须 html[data-theme=dark] 特异性 (0,1,1),禁裸属性选择器、禁 !important、禁 @apply 绕过);::selection/strong/.wordmark-label 零改动(D-A 核心收益)。T1 由红转绿 |
| T3 | 三处 usage 迁移 + 硬编码色守卫 | ResultMeta.vue:52、StatePanel.vue ×7、FilterDrawer.vue:32、新守卫文件 |
① bg-accent text-ink → bg-accent-ink text-paper(唯一把 accent 当背景的地方,迁移后 accent 纯非文本);② bg-[#EFE9DA] ×7 → bg-skeleton;③ backdrop:bg-ink/60 → backdrop:bg-scrim/80(D-E)。新增源码级守卫扫描 app/**/*.vue 禁止 bg-[#/text-[#/border-[# 硬编码 hex,RED 先行(迁移前应报 7 处违规),且必须沿用 P12 tableOverflow.test.ts 的屏蔽范式(<script>/<style>/HTML 注释/<textarea>/<title> → 等长空白),否则注释里的示例会误报(P12 FE-1 假绿同源教训) |
| T4 | 主题机制 | app/composables/useTheme.ts(新)、index.html、app/App.vue、app/components/AppHeader.vue、useTheme.test.ts(新)、AppHeader.test.ts(扩展) |
按 spec §3.5–§3.8 逐字落地。关键约束:开关必须 h-8 w-8(32px;spike 3 实测 28px 在最坏格 margin 仅 5px);容器行 gap-6→gap-2 sm:gap-6、nav gap-4→gap-2 sm:gap-4(候选 B,最坏格 margin=16);不得改 <summary> 的 max-w-[6rem] min-w-0 与两 span 结构(P12 F3 钉桩);不得改 logo 文字/字号;index.html 内联 pre-paint 脚本的判定优先级须与 useTheme.ts 逐字一致(stored → prefers-color-scheme → light);useTheme 若做模块级单例必须提供 __resetTheme()(P9-B D-I 跨测试污染教训) |
| T5 | e2e | e2e/dark.spec.ts(新) |
spec §5-T5 的 7 条最低覆盖:默认跟随系统(且在 Vue 挂载前就已设置 = 验 D-K 无 FOUC)、开关切换(data-theme + theme-color meta + localStorage 三者同步)、reload 持久化、计算值实证(body rgb(23,20,15)/rgb(247,242,231),且某 .shadow-hard 的 box-shadow 含 rgb(247,242,231) = spike 6 传导在真产物上复验)、遮罩不泛白、开关 toHaveAccessibleName 含当前态、暗色下 @320/@375 登录态 ascii60 零横向溢出 |
全量回归(实现者交付前必跑,set -o pipefail,禁 --if-present)
| 腿 | 命令 | 基线(P12 合并态,不得回退) |
|---|---|---|
| vitest | npm run test |
635 passed / 74 files |
| typecheck | npm run typecheck |
exit 0 |
| build | npm run build |
exit 0(含 vue-tsc --noEmit) |
| build:runtime | npm run build:runtime |
exit 0 |
| 主 e2e | npm run e2e |
92 passed + 1 skipped(含 P12 的 12 条 responsive 腿) |
| noauth e2e | npm run e2e:noauth |
4 passed |
新增测试后数字应上升;任何既有腿下降必须解释。P12 的 12 条 responsive 腿与 AppHeader.test.ts 的 F3 形态钉桩属不得回退项——若 gap 变更导致某腿数值变化,不得改断言迁就,先查明是否真溢出。
构建产物核验(T2 后必做,grep dist CSS)
var(--color-*)引用数 ≥ 64(不得下降)- 内联
#141414从 11 降到 ≤ 3,且逐处说明剩余来源- ⚠️ 测试/注释里禁写已迁移掉的 Tailwind 类名字面量(实现者 T2 期发现,spec 与 FINDINGS 均未预料)。Tailwind v4 的自动内容探测扫描全部源文件,含
.test.ts:FilterDrawer.test.ts里一句not.toContain('backdrop:bg-ink/60')断言(及其注释)让 Tailwind 把这个类名当候选、重新生成 utility 及#14141499fallback 烧回产物,使该计数停在 3 而非 2。修法:用字符串拼接构造字面量('backdrop:bg-i' + 'nk/60')。与 P12 屏蔽范式同源,方向相反——那里防「守卫扫到注释里的 table」,这里栽在「Tailwind 扫到测试里的死 utility」。任何「某 utility 不得再出现」的回退钉桩都必须拼接。
- ⚠️ 测试/注释里禁写已迁移掉的 Tailwind 类名字面量(实现者 T2 期发现,spec 与 FINDINGS 均未预料)。Tailwind v4 的自动内容探测扫描全部源文件,含
- 暗色块出现在产物中且未被 Tailwind 层吞掉
- ⚠️ grep 模式必须容忍 minifier 去引号(实现者发现、控制者独立复现 2026-10-03):本 plan 与 spec 原先写
grep -c 'html\[data-theme="dark"\]'(带双引号),但 lightningcss 会去掉属性选择器里非必需的引号,产物实际是html[data-theme=dark]→ 带引号的字面模式返回 0,会被误判为「暗色块被吞掉」。正确:grep -c 'html\[data-theme="\?dark"\?\]' "$CSS"或去引号版,须 ≥ 1。源码里仍须写带引号的html[data-theme="dark"](CSS 源码选择器)——错的只是产物核验的 grep。
- ⚠️ grep 模式必须容忍 minifier 去引号(实现者发现、控制者独立复现 2026-10-03):本 plan 与 spec 原先写
.shadow-hard的--tw-shadow含var(--color-ink)(spike 6 传导前提)
验收口径
实现者报告须含:六腿实跑输出摘录(数字,不是「应该没问题」)、构建产物四项 grep 结果、T1/T3 的 RED 输出原文(须复现 spec 引用的实测数字,如暗色 ink on highlight = 1.46)、mutation 自查输出(见下)、throwaway 清理证明(porcelain=0)。
mutation 自查(实现者必做并附输出)
- 亮色守卫仍被守(D-H 的核心风险 = 重构把亮色守卫静默弄丢):把亮色
--color-success临时改成低对比值 → 亮色腿必须红;改回 → 绿。 - 暗色守卫有牙:把暗色
--color-highlight临时改成#f5c518(亮黄)→ink on highlight暗色 = 1.46 红。 - 硬编码色守卫有牙:临时在某
.vue加bg-[#123456]→ 守卫红;删除 → 绿。 - P12 成果未回退:
npx vitest run app/lib/tableOverflow.test.ts app/lib/tableScope.test.ts仍绿;responsive.spec.ts12 腿仍绿。
每次 mutation 后 git checkout -- 恢复并核对 git diff 为空。
纪律(延续 P9-B / P11 / P12 教训,spec §8 全文适用)
- 不推断 CSS/Vue/构建行为,只实测。 本批已有两次栽在推断上:spike 5 用运行时注入验证构建期烘焙(失败:令牌已变而
box-shadow仍rgb(20,20,20))、spike 2 第一版用APPLY.toString()+new Function序列化注入(ReferenceError: APPLY is not defined)。 - 守卫先写、先看它红;红输出须复现 spec 数字,否则守卫可能无牙。
- 守卫自身要受同等审视(P12 FE-1:注释掉包裹层守卫仍绿 = 假信心)。
- P12/P9-B 成果不得回退:
max-w-[6rem]、五表格包裹层relative overflow-x-auto pr-1 pb-1、ResultMeta.vue:22的relative min-w-0、AccountView的min-w-0 wrap-anywhere、skip-link 首子位置、useToast的announcedId单调语义、scope="col"计数 13/6/5。 - throwaway 跑完即删;注意
npm run build含vue-tsc --noEmit会检查e2e/*.spec.ts类型(spike 6 曾因此BUILD_EXIT=2)——spike 期间可用npm run build:e2e绕开,但交付前npm run build必须真过。 - 禁动:
docs/(ROADMAP、CHANGELOG、spec、plan)、.superpowers/(只写自己的报告)、crearte-server/、crearte-deploy/、playwright.config.ts、.gitignore。 - 发现 spec 有误就纠正 spec 并在报告里说明,不要迁就实现(P12 实现者纠正了 768px 归因,控制者用 spike 17 独立定案其成立并 re-pin 五处下游)。
- commit 信息含任务号与决策号(
P13-T<n>/D-<X>);禁git add -A(.superpowers/、src/test-results/不得入库)。
挂账处置(随本批记账,由控制者执行)
- 清偿:ROADMAP P9-B 行尾「C 暗色模式」→ 标注已由 P13 清偿。
- 新增挂账:
- 第三态「恢复跟随系统」(spec §4:header 无像素容纳带标签控件;三态循环在 32px 无标签按钮上不可发现)
- CSP 设计批(内联 pre-paint 脚本届时需
nonce或外置 —— spec §3.6 已留注释提示) - OG 社交卡片、播放计数、TLS+HSTS、D4 nav 逐字换行设计决策、备份恢复演练实证(owner 手动尾巴)
- 上传面一处低危 fallthrough(
uploads.go:30-36:ParseMultipartForm返回非MaxBytesError时不 return,落到 switch 给出误导性的 400「kind must be bundle or cover」而非真实原因)—— 本轮证据扫描发现,属 server 侧打磨候选 --color-info为死令牌(bg-info/text-info实测均 0 实例,仅 1 处 token 定义)—— 清理或启用,属代码优雅候选- bundle-key 端点的公开信任边界无决策记录 + 一条误导性死管道(server 侧,零行为变更的微批候选)。P13 勘查期的只读审计发现,非缺陷,是文档缺口:
- 事实:
router.go:104的engine.GET(RouteBundleKey, limiter.Middleware(), deps.BundleKey.Get)是路由表里唯一没有RequireUser的数据端点。 - 定性为有意设计的证据(共 7 处钉桩,非顺带覆盖):server 侧 6 处显式无鉴权断言 ——
router_test.go:62(用完全不带Authorization头的httptest.NewRequest断言 200 +Cache-Control: no-store)、router_test.go:85(无头 → 第 61 次 429 +Retry-After,即限流是边界而非鉴权)、admin_test.go:121(doRequest(..., "", "")token 为空 → 200)、integration_test.go:128(同 → 200)、integration_test.go:153(同 → 410)、admin_works_test.go:120(同 → 200 后吊销变 410);前端侧e2e/full-loop.spec.ts:112-118从全新无 token 浏览器上下文request.get断言 410。若该路由挂了RequireUser,这些请求会先返 401、永远到不了吊销/限流分支 —— 故「无鉴权可取钥」是被多点钉死的既有行为。 - 支撑链:MinIO 桶
mc anonymous set download(deploy README:26/41,密文公开可下载)→content.go:202经PublicURL(ObjectKey)把 bundle URL 交给公开的 game detail 端点 →noauth.spec.ts(4 条)证明站点支持全匿名模式。即密文与 CEK 双双公开,已发布作品对任何人可下载——这与产品定位(公开托管社区)一致。 - 真实信任边界 = 60/min 限流 + 管理员吊销(410),不是鉴权。加密在此的作用是可吊销的交付机制(
bundle.UnwrapCEK+row.RevokedAt),不是访问控制。 - 误导性死管道:
runtime/sw/keyfetch.ts:6有token?: string,runtime/sw/index.ts:82一路透传,keyfetch.test.ts:28-33断言 Authorization 头透传,cors.go:31的 ACAH 也允许Authorization——但服务端从不要求、从不校验它。 - 风险:将来有人「修好」缺失的
RequireUser→ 匿名访客将无法游玩任何作品(站点必须支持登出态游玩)。上述 7 处钉桩会以 401≠200/410/429 立即抓住,故静默破坏风险实际很低。真正的缺口只剩一个:router.go:102-105处没有任何注释说明这个端点为何故意不挂RequireUser,而它是路由表里唯一没有鉴权的数据端点——视觉上极像遗漏。 - 处置(P11「大声留痕」哲学的同款应用,零行为变更):仅需① 在
router.go该处加决策记录注释,写明「有意公开:匿名访客必须能取钥游玩;信任边界是 60/min 限流 + 管理员吊销(410),不是鉴权;密文本身亦经 MinIO 匿名 download 公开,加密在此是可吊销的交付机制而非访问控制;加RequireUser会破坏登出态游玩并被router_test.go:62/integration_test.go:153/full-loop.spec.ts抓住」。 ② 补反面钉桩测试—— 撤销此建议:router_test.go:62已经就是那条测试(不带 Authorization 头断言 200)。我先前写「e2e 顺带覆盖」是未核实就下的判断,实际有 6 处 server 侧显式钉桩。教训:登记挂账前必须先 grep 既有测试,否则会开出「补一条已存在的测试」这种伪工作。- ③
keyfetch.ts:6的token?: string管道待查清后再定(属 crearte,P13 实现者正独占该仓,本批不动)。
- 事实: