docs: register P13 dark mode (crearte 0.26.0) + server micro-batch (0.17.2)
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.
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
# 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`(已建,从 crearte `e59a171` 起)
|
||||
- **范围**:仅 `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 及 `#14141499` fallback 烧回产物**,使该计数停在 **3** 而非 2。修法:用**字符串拼接**构造字面量(`'backdrop:bg-i' + 'nk/60'`)。与 P12 屏蔽范式同源,方向相反——那里防「守卫扫到注释里的 table」,这里栽在「Tailwind 扫到测试里的死 utility」。任何「某 utility 不得再出现」的回退钉桩都必须拼接。
|
||||
- 暗色块出现在产物中且未被 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。
|
||||
- `.shadow-hard` 的 `--tw-shadow` 含 `var(--color-ink)`(spike 6 传导前提)
|
||||
|
||||
---
|
||||
|
||||
## 验收口径
|
||||
|
||||
实现者报告须含:六腿实跑输出摘录(数字,不是「应该没问题」)、构建产物四项 grep 结果、T1/T3 的 **RED 输出原文**(须复现 spec 引用的实测数字,如暗色 `ink on highlight` = 1.46)、**mutation 自查输出**(见下)、throwaway 清理证明(`porcelain=0`)。
|
||||
|
||||
### mutation 自查(实现者必做并附输出)
|
||||
|
||||
1. **亮色守卫仍被守**(D-H 的核心风险 = 重构把亮色守卫静默弄丢):把亮色 `--color-success` 临时改成低对比值 → **亮色腿必须红**;改回 → 绿。
|
||||
2. **暗色守卫有牙**:把暗色 `--color-highlight` 临时改成 `#f5c518`(亮黄)→ `ink on highlight` 暗色 = **1.46 红**。
|
||||
3. **硬编码色守卫有牙**:临时在某 `.vue` 加 `bg-[#123456]` → 守卫红;删除 → 绿。
|
||||
4. **P12 成果未回退**:`npx vitest run app/lib/tableOverflow.test.ts app/lib/tableScope.test.ts` 仍绿;`responsive.spec.ts` 12 腿仍绿。
|
||||
|
||||
每次 mutation 后 `git checkout --` 恢复并核对 `git diff` 为空。
|
||||
|
||||
---
|
||||
|
||||
## 纪律(延续 P9-B / P11 / P12 教训,spec §8 全文适用)
|
||||
|
||||
1. **不推断 CSS/Vue/构建行为,只实测。** 本批已有两次栽在推断上:spike 5 用运行时注入验证**构建期烘焙**(失败:令牌已变而 `box-shadow` 仍 `rgb(20,20,20)`)、spike 2 第一版用 `APPLY.toString()` + `new Function` 序列化注入(`ReferenceError: APPLY is not defined`)。
|
||||
2. **守卫先写、先看它红**;红输出须复现 spec 数字,否则守卫可能无牙。
|
||||
3. **守卫自身要受同等审视**(P12 FE-1:注释掉包裹层守卫仍绿 = 假信心)。
|
||||
4. **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。
|
||||
5. **throwaway 跑完即删**;注意 `npm run build` 含 `vue-tsc --noEmit` 会检查 `e2e/*.spec.ts` 类型(spike 6 曾因此 `BUILD_EXIT=2`)——spike 期间可用 `npm run build:e2e` 绕开,但**交付前 `npm run build` 必须真过**。
|
||||
6. **禁动**:`docs/`(ROADMAP、CHANGELOG、spec、plan)、`.superpowers/`(只写自己的报告)、`crearte-server/`、`crearte-deploy/`、`playwright.config.ts`、`.gitignore`。
|
||||
7. **发现 spec 有误就纠正 spec 并在报告里说明**,不要迁就实现(P12 实现者纠正了 768px 归因,控制者用 spike 17 独立定案其成立并 re-pin 五处下游)。
|
||||
8. 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 实现者正独占该仓,本批不动)。
|
||||
Reference in New Issue
Block a user