docs: P12 narrow-viewport batch delivered (crearte 0.25.0 / server 0.17.1 / deploy 0.7.1)
Register the P12 spec and plan in the ROADMAP doc index, add the P12 batch row, and update two backlog lines that this batch's measurements settled: - The 'mobile header / hamburger menu' backlog entry carried since P9-B is falsified and removed: nav content width is only 74-158px and measures zero horizontal overflow at 320/360/375/412/768/1280px. Building it would have shipped a hamburger menu nobody needs. - Nav links wrapping per-character at phone widths stays parked pending a design decision, not a CSS tweak: it is cosmetic only (no overflow, no clipping, no lost function), whitespace-nowrap costs +11px at 320px, and all seven gap-reduction variants measured fail at 320px + long name because logo 77px + nowrap nav 138-158px + truncated name 96px do not fit. It competes for the same pixels as the header fix. - The P11 polish backlog is partially retired: three of P9-B's four items plus P11-N5/N6 are closed by this batch; the remaining nine P11 minor notes are documentation-only with no actionable change. Merged and pushed: crearte e59a171 (0.25.0), crearte-server d56c593 (0.17.1), crearte-deploy 529a988 (0.7.1). Three task-level reviews plus a whole-branch final review verdict APPROVE with notes, zero critical and zero important; all notes adjudicated before merge. The final reviewer independently reproduced three mutations rather than accepting the controller's claims.
This commit is contained in:
@@ -6,6 +6,16 @@ All notable changes to this repository should be documented in this file.
|
|||||||
The format loosely follows Keep a Changelog and can be adapted to the team's habits.
|
The format loosely follows Keep a Changelog and can be adapted to the team's habits.
|
||||||
本文档参考了 Keep a Changelog 的思路,也可以根据团队习惯调整。
|
本文档参考了 Keep a Changelog 的思路,也可以根据团队习惯调整。
|
||||||
|
|
||||||
|
## [0.3.5] - 2026-10-02
|
||||||
|
|
||||||
|
### Done / 完成
|
||||||
|
|
||||||
|
- P12 narrow-viewport batch delivered across all three product repos: crearte `e59a171` (0.25.0, merge-base `d25c497`), crearte-server `d56c593` (0.17.1, `9da39b6`), crearte-deploy `529a988` (0.7.1, `740958a`). Zero new features, zero backend behaviour changes. Dispatch followed the one-writer-per-repo rule (`sdd-parallel-dispatch` §1): T1/T2/T3/T5 all touch crearte's single worktree and git index, so they went to **one** subagent rather than four racing the same `.git/index.lock`; the controller wrote T6 (server) and T7 (deploy) itself. The guard task was **pulled forward to be the first step** rather than a second wave — write the guards, watch them go RED reproducing the spec's measured numbers verbatim (`+380px`, `+325px`, `+3px`, five missing wrappers), then implement until GREEN. That ordering removed the throwaway verification scripts the survey phase needed and made the RED output itself the proof the guards have teeth, instead of requiring an after-the-fact revert to demonstrate it. **Four measured defects fixed** (header long-name overflow hitting every route at a *legal* 60-char name; account-page nickname escape; catalog sort-select `shrink-0`; five admin tables with no scroll wrapper — reproducing with short data) **plus one found on the way** (the user dropdown escaping the viewport, closed by the header fix). Two machine guards added: `e2e/responsive.spec.ts` (12 tests, joins the CI main leg with zero config change) and `app/lib/tableOverflow.test.ts` (source-level, per-table parent check). Three P9-B polish items landed (P9B-1/2/4). Three task-level reviews (frontend PASS with 8 notes, server PASS, deploy PASS with 3 notes plus one spec erratum) and a whole-branch final review verdict **APPROVE with notes — zero critical, zero important**; every note adjudicated before merge, three fixed forward (`20215e7`, `fa39d27`). **The final reviewer independently reproduced three mutations rather than accepting the controller's or the reviewer's claims**: reverting the header fix turns 6 e2e legs red; commenting out a table wrapper turns the source guard red; echoing the raw env value turns both server assertions red. **Two parked items decided by measurement, not taste**: the ROADMAP's "mobile header / hamburger menu" backlog entry is **falsified and deleted** (nav content is 74–158px wide with zero horizontal overflow at 320/360/375/412/768/1280px), and nav's per-character link wrapping is **parked pending a design decision** (cosmetic only — no overflow, no clipping, no lost function; `whitespace-nowrap` costs +11px at 320px and all seven gap-reduction variants fail at 320px + long name because logo 77px + nowrap nav 138–158px + truncated name 96px physically do not fit). Acceptance: crearte vitest **635**/74 files (baseline 626/73) / vue-tsc clean / build + build-runtime OK / main e2e **92+1skip** (baseline 80+1skip) / noauth 4 — re-run independently by the controller twice and by both reviewers, four-way zero deviation; server gofmt/vet clean, 11 packages ok; deploy four-profile `config -q` green with the new guard verified in three states.
|
||||||
|
- P12 窄屏批交付,横跨三个产品仓:crearte `e59a171`(0.25.0,merge-base `d25c497`)、crearte-server `d56c593`(0.17.1,`9da39b6`)、crearte-deploy `529a988`(0.7.1,`740958a`)。零新功能、零后端行为变更。派发遵循「一仓一写者」(`sdd-parallel-dispatch` §1):T1/T2/T3/T5 都动 crearte 的单一工作树与 git index,故交给**一个**子代理而非四路争用同一个 `.git/index.lock`;T6(server)与 T7(deploy)由控制者本人写。守卫任务被**提前为第一步**而非第二波——先写守卫、看它变红并逐字复现 spec 的实测数字(`+380px`、`+325px`、`+3px`、五处缺失包裹层),再实现到绿。这个顺序消除了勘查期所需的 throwaway 验证脚本,并使 RED 输出本身成为「守卫有牙」的证明,而不必事后再回退一次来自证。**四个实测缺陷已修**(页头长名溢出波及全站每个路由,而 60 字名是**合法上限**;账号页昵称逃逸;目录排序 select 的 `shrink-0`;五个 admin 表格无滚动包裹层——短数据也复现)**外加途中发现的一个**(用户下拉菜单逃逸视口,由页头修复一并闭合)。新增两道机器守卫:`e2e/responsive.spec.ts`(12 测试,零配置改动进 CI 主腿)与 `app/lib/tableOverflow.test.ts`(源码级,逐表格父元素判定)。三项 P9-B 打磨落地(P9B-1/2/4)。三份任务级审查(前端 PASS with 8 notes、server PASS、deploy PASS with 3 notes 加一条 spec 勘误)与一次全分支终审,裁定 **APPROVE with notes——零关键零重要**;全部 notes 在合并前裁定完毕,三处 fix-forward(`20215e7`、`fa39d27`)。**终审独立复现了三处 mutation,而非采信控制者或审查者的声称**:回退页头修复 → 6 腿 e2e 变红;注释掉表格包裹层 → 源码守卫变红;错误文案回显原始 env → server 两条断言同时变红。**两个挂账项由实测定夺、非口味**:ROADMAP 的「移动端页头 / 汉堡菜单」挂账**已证伪并删除**(nav 内容宽 74–158px,320/360/375/412/768/1280px 实测零横向溢出),nav 链接逐字换行**park 待设计决策**(纯外观——无溢出、无裁剪、无功能损失;`whitespace-nowrap` 在 320px 代价 +11px,且七种 gap 收缩变体在 320px + 长名下全部失败,因 logo 77px + nowrap nav 138–158px + 截断名 96px 物理上放不下)。验收:crearte vitest **635**/74 文件(基线 626/73)/ vue-tsc 零错 / build + build-runtime OK / 主 e2e **92+1skip**(基线 80+1skip)/ noauth 4——控制者独立复跑两次、两位审查者各复跑一次,四方零偏差;server gofmt/vet 净、11 包 ok;deploy 四 profile `config -q` 全绿且新守卫经三态验证。
|
||||||
|
|
||||||
|
- Lessons recorded. **A parked backlog item must be falsified or confirmed before being implemented** — "mobile header" had been carried in the ROADMAP since P9-B, and measurement showed the nav fits every viewport; implementing it would have shipped a hamburger menu nobody needed. **When every candidate fix fails at the same residue, the diagnosis is wrong, not the fix** — eight one-line remedies all stopped at exactly +32px (including `overflow: hidden`), which is what forced the ablation method (element-by-element `display: none` plus re-measure) that immediately found the culprit: P9-B's own 1px `sr-only` span, `position: absolute` with no `top`/`left`, escaping a `static` scroll wrapper because such a wrapper is not a containing block. Border-box scanning could not see it (the offender scan reported `trueOffenders=0` while the document still overflowed, because its "has a scrollable ancestor" filter classified the escapee as legitimate). **Runtime style injection cannot validate a template-level fix**: Vue compiles `{{ user.display_name }} ▾` into one text node, so three variants that tried to keep the caret outside the truncating span all measured `caret=HIDDEN` — the template had to split it explicitly, and only then could the flex shape be measured. **Source-level mutation testing can read a stale `dist/`** under `reuseExistingServer: !CI`, producing a false GREEN; the attribution that mattered (which of two fixes owns the 768px symptom) was settled by runtime-injection ablation instead, which needs no rebuild. **Guards get the same scrutiny as the code they guard**: review found the new source-level parser would stay green if someone commented out a wrapper — the same false-confidence class as the dead CIDR assertion caught in the deploy guard, both fixed before merge. **The controller itself made two verification errors this batch, both caught by reverse-testing its own work**: an assertion written as `"[0-9]` could never match compose's bare-scalar rendering (a dead assertion = false confidence), and a comment claimed a SIGPIPE/pipefail hazard that measurement did not reproduce (removed rather than left as unfounded lore).
|
||||||
|
- 教训入账。**挂账项在实现前必须先证伪或证实**——「移动端页头」自 P9-B 起就挂在 ROADMAP 上,而实测表明 nav 在所有视口都放得下;实现它等于交付一个没人需要的汉堡菜单。**当每个候选修法都失败在同一残差上,错的是诊断而非修法**——八种一行修法全停在恰好 +32px(含 `overflow: hidden`),这才逼出烧蚀法(逐元素 `display: none` 加重测),并立即找到元凶:P9-B 自己那个 1px 的 `sr-only` span,`position: absolute` 而无 `top`/`left`,因 `static` 滚动包裹层不构成包含块而逃出裁剪。边框盒扫描看不见它(offender 扫描在文档仍溢出时报 `trueOffenders=0`,因为它的「有可滚动祖先」过滤把这个逃逸者归为合法)。**运行时样式注入无法验证模板级修法**:Vue 把 `{{ user.display_name }} ▾` 编译成单个文本节点,故三个试图把 caret 留在截断 span 之外的变体全部实测 `caret=HIDDEN`——必须在模板里显式拆开,之后才谈得上测量 flex 形态。**源码级 mutation 测试可能读到 stale `dist/`**(`reuseExistingServer: !CI` 下)而产生假 GREEN;真正要紧的那次归因(两个修复中谁拥有 768px 症状)改用运行时注入消融定案,它不需要重建。**守卫自身要接受与被守卫代码同等的审视**:审查发现新的源码级解析器在有人注释掉包裹层时会保持绿——与 deploy 守卫里抓到的死 CIDR 断言属同一类假信心,两者都在合并前修掉。**控制者本人本批也犯了两个验证错误,均由反向测试自己的工作抓出**:一条断言写成 `"[0-9]` 而永不匹配 compose 的裸标量渲染(死断言 = 假信心);一句注释声称存在 SIGPIPE/pipefail 危害,而实测未复现(已删除,而非留作无据传闻)。
|
||||||
|
|
||||||
## [0.3.4] - 2026-10-02
|
## [0.3.4] - 2026-10-02
|
||||||
|
|
||||||
### Done / 完成
|
### Done / 完成
|
||||||
|
|||||||
+4
-2
@@ -36,8 +36,9 @@
|
|||||||
| P8 | 长尾打包 | 权限开关 UI 全量(`inlineStyle/wasm/coop/fullscreen/gamepad`)、后端 triage 小项(`games.Detail` 400 细分、admin 路由 slug 校验、approve 同名竞态测试)、账号注销、静态兜底目录、CHANGELOG 模板文案 | **第一批完成**(②⑤①,2026-10-01 合并推送:server `921443a` 0.14.0 / crearte `70ba10c` 0.20.0。容器四连+空库 `-count=1` 集成全绿(新竞态测试含,skip 守卫 0)+`-race` 竞态腿 PASS;curl 冒烟全中:detail 400 细分 user:/slug: 且双非法 user 优先、admin 四路非法 400 `invalid work id`、合法格式不存在仍 404;FE vitest 499/typecheck/admin-flow 7/主套件 70+1skip;双审 PASS(server PASS with notes—唯一 note 双非法优先级缺钉桩,已以 `ee5b960` 测试断言补上);deploy 零改动。**第二批完成(③账号注销+④静态兜底目录)**(2026-10-01 合并推送:server `12b8ffb` 0.15.0(迁移 0011 墓碑化删行、DELETE /api/auth/account 对密码注销、唯一 admin 409、user delete CLI、catalog export 出 schemaVersion=2 目录)+ crearte `7d5f338` 0.21.0(/account 危险区+deleteAccount 客户端+AdminUsersView 三码中文化钉桩)+。真库注销链集成+分支真产物 curl 冒烟 14 项全 PASS(墓碑行/邮箱复用/409/旧 token 作废/CLI delete/export schema);FE vitest 512/typecheck/主套件 71+1skip/noauth 4,双审 PASS with notes(前端 2 条次要已钉桩 `7a54152`,server 裁决项:LOG_LEVEL 大小写不一致—deferred)。余 ④的 render 消费端接入按需(feed 已交付)**,CHANGELOG 头部去模板腔两仓已随第一批完成、wrapper 头部不在 spec 范围 |
|
| P8 | 长尾打包 | 权限开关 UI 全量(`inlineStyle/wasm/coop/fullscreen/gamepad`)、后端 triage 小项(`games.Detail` 400 细分、admin 路由 slug 校验、approve 同名竞态测试)、账号注销、静态兜底目录、CHANGELOG 模板文案 | **第一批完成**(②⑤①,2026-10-01 合并推送:server `921443a` 0.14.0 / crearte `70ba10c` 0.20.0。容器四连+空库 `-count=1` 集成全绿(新竞态测试含,skip 守卫 0)+`-race` 竞态腿 PASS;curl 冒烟全中:detail 400 细分 user:/slug: 且双非法 user 优先、admin 四路非法 400 `invalid work id`、合法格式不存在仍 404;FE vitest 499/typecheck/admin-flow 7/主套件 70+1skip;双审 PASS(server PASS with notes—唯一 note 双非法优先级缺钉桩,已以 `ee5b960` 测试断言补上);deploy 零改动。**第二批完成(③账号注销+④静态兜底目录)**(2026-10-01 合并推送:server `12b8ffb` 0.15.0(迁移 0011 墓碑化删行、DELETE /api/auth/account 对密码注销、唯一 admin 409、user delete CLI、catalog export 出 schemaVersion=2 目录)+ crearte `7d5f338` 0.21.0(/account 危险区+deleteAccount 客户端+AdminUsersView 三码中文化钉桩)+。真库注销链集成+分支真产物 curl 冒烟 14 项全 PASS(墓碑行/邮箱复用/409/旧 token 作废/CLI delete/export schema);FE vitest 512/typecheck/主套件 71+1skip/noauth 4,双审 PASS with notes(前端 2 条次要已钉桩 `7a54152`,server 裁决项:LOG_LEVEL 大小写不一致—deferred)。余 ④的 render 消费端接入按需(feed 已交付)**,CHANGELOG 头部去模板腔两仓已随第一批完成、wrapper 头部不在 spec 范围 |
|
||||||
| P9 | UX 增强(第一批:浏览器反馈包) | 路由级 `document.title` 两段式(router `afterEach` 按 19 个 name 全表设静态基线 + 四个数据页 watch 数据后精化)+ 作品页 `meta description`(截 120 码点、默认回落)+ `StatePanel` 骨架对齐(cards 3→6 张、新增 `lines` 变体六页八实例)。零后端/部署改动 | **第一批完成**(2026-10-01 合并推送:crearte `26cd625` 0.22.0(`a196603`+`3f1de38`+审查补钉 `e9e4c56`)。五腿全绿:vitest **551**(基线 512+新增 39)/ vue-tsc 零错 / build OK / 主套件 72+1skip(含新增标题 e2e 腿,`landing.spec.ts` 首条兼容钉一字未动)/ noauth 4。独立审查 PASS with notes:ISSUE-1(离开作品页 description 未回落默认,spec D-C 子项)与 ISSUE-2(「七页」实为六页文案)均已在合主前补钉+补测。B 效率可访问 / C 暗色模式两批挂账待启动) |
|
| P9 | UX 增强(第一批:浏览器反馈包) | 路由级 `document.title` 两段式(router `afterEach` 按 19 个 name 全表设静态基线 + 四个数据页 watch 数据后精化)+ 作品页 `meta description`(截 120 码点、默认回落)+ `StatePanel` 骨架对齐(cards 3→6 张、新增 `lines` 变体六页八实例)。零后端/部署改动 | **第一批完成**(2026-10-01 合并推送:crearte `26cd625` 0.22.0(`a196603`+`3f1de38`+审查补钉 `e9e4c56`)。五腿全绿:vitest **551**(基线 512+新增 39)/ vue-tsc 零错 / build OK / 主套件 72+1skip(含新增标题 e2e 腿,`landing.spec.ts` 首条兼容钉一字未动)/ noauth 4。独立审查 PASS with notes:ISSUE-1(离开作品页 description 未回落默认,spec D-C 子项)与 ISSUE-2(「七页」实为六页文案)均已在合主前补钉+补测。B 效率可访问 / C 暗色模式两批挂账待启动) |
|
||||||
| P10 | UX 增强(第一批:日常交互包) | toast 体系(useToast 单例 store + ToastHost 挂 App.vue)+ 投稿表单脏数据离开守卫(onBeforeRouteLeave confirm + beforeunload)+ 作品标签可点(GameView/GameCard tags → /games?tag= RouterLink)+ 作品页复制链接分享(clipboard + toast)+ 最近玩过条带(recent.ts localStorage + GameHost ready 记录 + LandingView 条带)+ 页头下拉外点/Esc 关闭。零后端/部署改动 | **第一批完成**(2026-10-01 合并推送:crearte `ee4edf7` 0.23.0,merge-base `26cd625`。六任务各经实现→任务级审查→裁定,另加全分支终审(spec §3/§4/§5 全量对照)。五腿全绿:vitest **601**(基线 551+新增 50)/ vue-tsc 零错 / build OK / 主套件 **77+1skip**(基线 72+1skip)/ noauth 4。审查补钉四处:`fc0270e`(AppHeader 卸载清理 spy)、`dbf4bf0`(hosted 重开 sync-flush 再记,spec D-H re-pin)、`bb9bb35`(e2e 夹具修正——spec 测试计划自身误选 external-runtime 夹具 2048/a-dark-room,换 abs-paths/storage)、`836b2fb`(AppHeader 路由收起回归,spec §5 明列项)。终审裁定「需修复后合并」两条件均已闭合。B 效率可访问 / C 暗色模式 / OG 卡片 / 播放计数等后续批挂账待启动) |
|
| P10 | UX 增强(第一批:日常交互包) | toast 体系(useToast 单例 store + ToastHost 挂 App.vue)+ 投稿表单脏数据离开守卫(onBeforeRouteLeave confirm + beforeunload)+ 作品标签可点(GameView/GameCard tags → /games?tag= RouterLink)+ 作品页复制链接分享(clipboard + toast)+ 最近玩过条带(recent.ts localStorage + GameHost ready 记录 + LandingView 条带)+ 页头下拉外点/Esc 关闭。零后端/部署改动 | **第一批完成**(2026-10-01 合并推送:crearte `ee4edf7` 0.23.0,merge-base `26cd625`。六任务各经实现→任务级审查→裁定,另加全分支终审(spec §3/§4/§5 全量对照)。五腿全绿:vitest **601**(基线 551+新增 50)/ vue-tsc 零错 / build OK / 主套件 **77+1skip**(基线 72+1skip)/ noauth 4。审查补钉四处:`fc0270e`(AppHeader 卸载清理 spy)、`dbf4bf0`(hosted 重开 sync-flush 再记,spec D-H re-pin)、`bb9bb35`(e2e 夹具修正——spec 测试计划自身误选 external-runtime 夹具 2048/a-dark-room,换 abs-paths/storage)、`836b2fb`(AppHeader 路由收起回归,spec §5 明列项)。终审裁定「需修复后合并」两条件均已闭合。B 效率可访问 / C 暗色模式 / OG 卡片 / 播放计数等后续批挂账待启动) |
|
||||||
| P9-B | UX 增强(第二批:可访问与键盘效率包) | WCAG AA 色彩对比达标(`--color-success` 加深 + error toast 换 `bg-accent-ink`)+ toast 播报区重构(常驻 sr-only 播报层与视觉层解耦,收口 P10 终审转办项 T1(d)「实时区嵌交互按钮」)+ skip-link(`App.vue` 根首子,`focus:z-[80]`)+ SPA 导航后焦点管理(`focusMain()`,仅路径变化触发、`preventScroll`)+ 管理端表格 24 个 `<th>` 补 `scope="col"`(两处空表头补 sr-only「操作」)+ 五星评分可访问名(逐星「评 N 星」+ 分组 `role="group"` 动态标签 + 字形 `aria-hidden`)。另加两个源码级守卫(`contrast.test.ts` 十一配对对比度、`tableScope.test.ts` 全仓 `<th` 必带 scope)与 `e2e/a11y.spec.ts`。零后端/部署改动 | **第二批完成**(2026-10-01 合并推送:crearte `338acbf` 0.24.0,merge-base `ee4edf7`。五任务(T1-T4 派发 + T5 控制者本人)各经实现→任务级审查→裁定,四次任务级审查全 PASS,另加全分支终审(spec §1-§6 全量对照 + 跨任务集成缝隙 + mutation 抽查)。五腿全绿:vitest **626**(基线 601+新增 25)/ vue-tsc 零错 / build+build-runtime OK / 主套件 **80+1skip**(基线 77+1skip)/ noauth 4。终审独立实跑六腿与控制者数字逐条一致,裁定 APPROVE with notes(零关键/零重要)。波次内两次 spec 层修正:① T1 实现者抓出 spec 初稿 D-I 缺陷(per-toast `role=status` 与文字同时创建,部分读屏不播报首条)→ re-pin D-I′ 为常驻播报区,控制者直修 `f01baae` 并附带钉死饱和态 watch 陷阱(源必须是末尾 id 而非长度,`MAX_VISIBLE=3` 时 3→3 不触发);② 控制者简报两处数量笔误(对比度 3.16 应为 2.83、「19 个 th」是行计数、元素实为 24)均由实现者以 spec/实测为准纠正,spec §1 已同步。打磨批挂账四条(router 升级复看弱断言、撤评语义进可访问名、手动关最新 toast 的播报重念、spec 口径已修)。C 暗色模式 / OG 卡片 / 播放计数 / 移动端页头等后续批挂账待启动) |
|
| P9-B | UX 增强(第二批:可访问与键盘效率包) | WCAG AA 色彩对比达标(`--color-success` 加深 + error toast 换 `bg-accent-ink`)+ toast 播报区重构(常驻 sr-only 播报层与视觉层解耦,收口 P10 终审转办项 T1(d)「实时区嵌交互按钮」)+ skip-link(`App.vue` 根首子,`focus:z-[80]`)+ SPA 导航后焦点管理(`focusMain()`,仅路径变化触发、`preventScroll`)+ 管理端表格 24 个 `<th>` 补 `scope="col"`(两处空表头补 sr-only「操作」)+ 五星评分可访问名(逐星「评 N 星」+ 分组 `role="group"` 动态标签 + 字形 `aria-hidden`)。另加两个源码级守卫(`contrast.test.ts` 十一配对对比度、`tableScope.test.ts` 全仓 `<th` 必带 scope)与 `e2e/a11y.spec.ts`。零后端/部署改动 | **第二批完成**(2026-10-01 合并推送:crearte `338acbf` 0.24.0,merge-base `ee4edf7`。五任务(T1-T4 派发 + T5 控制者本人)各经实现→任务级审查→裁定,四次任务级审查全 PASS,另加全分支终审(spec §1-§6 全量对照 + 跨任务集成缝隙 + mutation 抽查)。五腿全绿:vitest **626**(基线 601+新增 25)/ vue-tsc 零错 / build+build-runtime OK / 主套件 **80+1skip**(基线 77+1skip)/ noauth 4。终审独立实跑六腿与控制者数字逐条一致,裁定 APPROVE with notes(零关键/零重要)。波次内两次 spec 层修正:① T1 实现者抓出 spec 初稿 D-I 缺陷(per-toast `role=status` 与文字同时创建,部分读屏不播报首条)→ re-pin D-I′ 为常驻播报区,控制者直修 `f01baae` 并附带钉死饱和态 watch 陷阱(源必须是末尾 id 而非长度,`MAX_VISIBLE=3` 时 3→3 不触发);② 控制者简报两处数量笔误(对比度 3.16 应为 2.83、「19 个 th」是行计数、元素实为 24)均由实现者以 spec/实测为准纠正,spec §1 已同步。打磨批挂账四条(router 升级复看弱断言、撤评语义进可访问名、手动关最新 toast 的播报重念、spec 口径已修)。C 暗色模式 / OG 卡片 / 播放计数等后续批挂账待启动(「移动端页头」已于 P12 实测证伪删除:nav 内容宽仅 74–158px,320/360/375/412/768/1280px 零横向溢出)) |
|
||||||
| P11 | 服务端安全硬化 | 限流表硬上界(`maxEntries` 失败关闭:满时先过期清理、仍满则对新面孔 429 且不插入,既有键语义逐字不变;闭合缺陷 B「同窗口 map 无界增长」)+ `LOG_LEVEL`/`LOG_FORMAT` 入口层小写化与 trim(闭合 P8 deferred 缺陷 C)+ `TrustedProxies` 空时启动 `slog.Warn`(D-C 大声留痕)+ 两个 compose-SNAT 网关陷阱反面钉桩 + deploy `TRUSTED_PROXIES` **空默认**与 README 专节(真实 prod 按实际反代 IP/网段配)+ nginx `X-Real-IP` 与九处 `nosniff`/`Referrer-Policy`(`always`)。CSP 与 HSTS 有意不做(前者需独立设计批,后者待 TLS 批) | **完成**(2026-10-02 合并推送:server `9da39b6` 0.17.0(merge-base `d627e12`)/ deploy `740958a` 0.7.0(`12f7c10`)/ crearte `d25c497` 0.24.1(`338acbf`)。五任务各经实现→任务级审查→裁定全 PASS(零关键零重要,9 条次要转打磨批),另加全分支终审(qwen3.8-max)裁定「需修复后合并」——N1(WARN hint 竟建议 subnet 信任,即 D-A′ 判定的绕过配置)/N2(spec 五处 subnet 线未随 re-pin 更新)/N3-N5 均已闭合,N6(validate.yml 加 `TRUSTED_PROXIES` 空默认机器守卫)转打磨批。**本批最重要事件:端到端验收抓出设计级错误并 re-pin D-A→D-A′**——原 D-A 信任整个 compose 子网,但真实 prod 栈实测:docker 对发布端口做 SNAT,nginx 看到的源恒为网桥网关且网关也在子网内,gin 跳过可信网关后**采纳客户端预置 XFF**(`X-Forwarded-For: 8.8.8.8` → `ip=8.8.8.8`,限流可自选桶绕过);而真实浏览器(无 XFF)在 compose 下恒塔缩到网关——缺陷 A 对合法流量根本修不了(SNAT 固有)。spike_snat 六用例信任矩阵钉死后改为:空信任默认(XFF 整体忽略、无绕过、D-C 告警如实提示单桶)+ subnet 固定回退 + README 重写真实 prod 配法 + server 两个反面钉桩。修正后 prod 真实栈回归五项全中(告警在位、伪造 XFF 解析为 nginx 容器 IP 绝非 8.8.8.8、单桶 429 正常、404 上安全头完好、卷全留存)。验收全链:server gofmt/vet/build 净 + test 11 包 ok + `-race` 12 包 ok(Debian 镜像 CGO=1)+ 真库 db-test 集成 ok(skip 守卫 0);crearte vitest **626**/typecheck 零错/build OK/主 e2e **80+1skip**/noauth 4;终审独立复跑六腿与控制者数字逐条一致。教训入账:隔离 spike 证明组件语义、不证明配置在真实拓扑下正确——安全/网络类修复合并前必须走真实流量路径的端到端验证;验收测错路径(用伪造 XFF 当回归)比不测更危险。CSP / TLS+HSTS / 打磨批(P9-B 四条 + P11 九条次要 + N6)挂账待启动) |
|
| P11 | 服务端安全硬化 | 限流表硬上界(`maxEntries` 失败关闭:满时先过期清理、仍满则对新面孔 429 且不插入,既有键语义逐字不变;闭合缺陷 B「同窗口 map 无界增长」)+ `LOG_LEVEL`/`LOG_FORMAT` 入口层小写化与 trim(闭合 P8 deferred 缺陷 C)+ `TrustedProxies` 空时启动 `slog.Warn`(D-C 大声留痕)+ 两个 compose-SNAT 网关陷阱反面钉桩 + deploy `TRUSTED_PROXIES` **空默认**与 README 专节(真实 prod 按实际反代 IP/网段配)+ nginx `X-Real-IP` 与九处 `nosniff`/`Referrer-Policy`(`always`)。CSP 与 HSTS 有意不做(前者需独立设计批,后者待 TLS 批) | **完成**(2026-10-02 合并推送:server `9da39b6` 0.17.0(merge-base `d627e12`)/ deploy `740958a` 0.7.0(`12f7c10`)/ crearte `d25c497` 0.24.1(`338acbf`)。五任务各经实现→任务级审查→裁定全 PASS(零关键零重要,9 条次要转打磨批),另加全分支终审(qwen3.8-max)裁定「需修复后合并」——N1(WARN hint 竟建议 subnet 信任,即 D-A′ 判定的绕过配置)/N2(spec 五处 subnet 线未随 re-pin 更新)/N3-N5 均已闭合,N6(validate.yml 加 `TRUSTED_PROXIES` 空默认机器守卫)转打磨批。**本批最重要事件:端到端验收抓出设计级错误并 re-pin D-A→D-A′**——原 D-A 信任整个 compose 子网,但真实 prod 栈实测:docker 对发布端口做 SNAT,nginx 看到的源恒为网桥网关且网关也在子网内,gin 跳过可信网关后**采纳客户端预置 XFF**(`X-Forwarded-For: 8.8.8.8` → `ip=8.8.8.8`,限流可自选桶绕过);而真实浏览器(无 XFF)在 compose 下恒塔缩到网关——缺陷 A 对合法流量根本修不了(SNAT 固有)。spike_snat 六用例信任矩阵钉死后改为:空信任默认(XFF 整体忽略、无绕过、D-C 告警如实提示单桶)+ subnet 固定回退 + README 重写真实 prod 配法 + server 两个反面钉桩。修正后 prod 真实栈回归五项全中(告警在位、伪造 XFF 解析为 nginx 容器 IP 绝非 8.8.8.8、单桶 429 正常、404 上安全头完好、卷全留存)。验收全链:server gofmt/vet/build 净 + test 11 包 ok + `-race` 12 包 ok(Debian 镜像 CGO=1)+ 真库 db-test 集成 ok(skip 守卫 0);crearte vitest **626**/typecheck 零错/build OK/主 e2e **80+1skip**/noauth 4;终审独立复跑六腿与控制者数字逐条一致。教训入账:隔离 spike 证明组件语义、不证明配置在真实拓扑下正确——安全/网络类修复合并前必须走真实流量路径的端到端验证;验收测错路径(用伪造 XFF 当回归)比不测更危险。CSP / TLS+HSTS 挂账待启动(打磨批已于 P12 部分清偿:P9-B 四条中三条(路由弱断言 / 撤评语义 / 播报重念)与 P11-N5/N6 已闭合;P9-B 第四条「spec 口径」在 P9-B 波次内已修;P11 其余九条次要为纯留档、无可动手改动) |
|
||||||
|
| P12 | 窄屏溢出修复与打磨批 | 四个实测窄屏溢出缺陷 + 途中发现的一个:**缺口 A** 页头长 `display_name`(60 字为**合法上限**,非敌意构造)撑破全站每个路由(320px **+380px** / 375px **+325px**);**缺口 B** 账号页昵称 dd 逃逸(`ddRight=592`);**缺口 C** 目录排序 select 的 `shrink-0`(`/games` 320px **+3px**,短名短数据也复现);**缺口 D** 五个 admin 表格零 overflow 包裹层(`/admin/users` 320px **+76px**,**短数据也复现**);**缺口 E** 用户下拉菜单逃逸视口(`menu right=485`,由页头修复一并闭合)。另加两层机器守卫(`e2e/responsive.spec.ts` 12 测试零配置改动进 CI 主腿 + `app/lib/tableOverflow.test.ts` 源码级逐表格父元素判定)与三项打磨(P9B-1 路由弱断言 / P9B-2 撤评语义进可访问名 / P9B-4 播报重念)。零新功能、零后端行为变更 | **完成**(2026-10-02 合并推送:crearte `e59a171` 0.25.0(merge-base `d25c497`)/ server `d56c593` 0.17.1(`9da39b6`)/ deploy `529a988` 0.7.1(`740958a`)。派发遵循一仓一写者:T1/T2/T3/T5 同动 crearte 单一工作树,故交**一个**子代理而非四路争用 `.git/index.lock`;T6(server)/T7(deploy)由控制者本人写。**守卫任务提前为 TDD 第一步**而非第二波:先写守卫看它变红并逐字复现 spec 实测数字(+380/+325/+3px/五处缺失包裹层),再实现到绿——这个顺序使 RED 输出本身成为「有牙」证明,免去事后 revert 自证。任务级审查三份(前端 PASS with 8 notes、server PASS、deploy PASS with 3 notes + 1 条 spec 勘误)+ 全分支终审裁定 **APPROVE with notes(零关键零重要)**,全部 notes 合并前逐条裁定(FE-1/2/6 fix-forward `20215e7`、SD-N1 `fa39d27`、N-F1/2/4 改 spec/FINDINGS/注释,余为 accepted-with-reason)。**终审独立复现三处 mutation**(不采信控制者与审查者结果):回退 T1 → 6 腿 e2e 变红;注释掉表格包裹层 → 源码守卫变红;错误文案回显原始 env → server 两条断言同时变红。**两个挂账项由实测定夺**:①「移动端页头 / 汉堡菜单」**证伪删除**——nav 内容宽仅 74–158px,320/360/375/412/768/1280px 实测零溢出;② nav 链接逐字换行(375px = iPhone 12/13/14 同样如此)**park 待设计决策**——纯外观(无溢出无裁剪无功能损失),`whitespace-nowrap` 在 320px 引入 +11px,七种 gap 收缩组合在「320px + 长名」下全灭(+19~+75px),因 logo(77px)+nowrap nav(138–158px)+截断名(96px) 物理放不下、与页头修复争同一份像素。**诊断关键**:八种一行修法全停在残差 **+32px**(含 `overflow:hidden`)→ 改用烧蚀法定位到 P9-B 自己那个 1px `sr-only` span(`absolute` 无 `top/left`,`static` 包裹层不构成包含块故逃出滚动裁剪)→ 包裹层必须 `relative`。**归因冲突用运行时注入 2×2 消融定案**:768px `/account` 的 +40px 实由缺口 A(页头)驱动而非 dd——回退 dd @768 仍 `over=+0`、回退页头 → `over=+50`;dd 固有右缘恒 592px 故牙口在 <592px 视口。两个修复独立必要、缺一不可,但不是同一视口的同一症状;spec 已 re-pin 五处下游。验收:crearte vitest **635**/74(基线 626/73)/ vue-tsc 零错 / build+build-runtime OK / 主 e2e **92+1skip**(基线 80+1skip)/ noauth 4——控制者复跑两次 + 任务级审查者与终审者各复跑一次,四方零偏差;server gofmt/vet 净 11 包 ok;deploy 四 profile `config -q` 绿 + 守卫三态验证(正向绿 / 注入 CIDR 红 / 删注入行红)。教训入账:① 挂账项实现前必须先证伪或证实;② 所有候选修法失败在同一残差上说明**诊断错了**(换烧蚀法比再加候选有效);③ 运行时样式注入无法验证模板级修法(Vue 把插值编译成单文本节点);④ **源码级 mutation 可能读到 stale `dist/`**(`reuseExistingServer: !CI`)而产生假 GREEN,关键归因须改用运行时注入;⑤ **守卫自身要受与被守卫代码同等的审视**——审查发现注释掉包裹层会使新守卫假绿,与 deploy 守卫里那个恒不匹配的死 CIDR 断言同属假信心类,两者均合并前修掉。CSP / TLS+HSTS / UX C 暗色模式 / OG 卡片 / 播放计数 / D4 nav 换行设计决策 挂账待启动) |
|
||||||
|
|
||||||
## 排序理由
|
## 排序理由
|
||||||
|
|
||||||
@@ -68,3 +69,4 @@
|
|||||||
| P10 UX 第一批(日常交互包) | `docs/specs/2026-10-01-p10-ux-daily-interaction-design.md` | `docs/plans/2026-10-01-p10-ux-daily-interaction.md`(第一批已执行,2026-10-01:crearte 0.23.0) |
|
| P10 UX 第一批(日常交互包) | `docs/specs/2026-10-01-p10-ux-daily-interaction-design.md` | `docs/plans/2026-10-01-p10-ux-daily-interaction.md`(第一批已执行,2026-10-01:crearte 0.23.0) |
|
||||||
| P9-B UX 第二批(可访问与键盘效率包) | `docs/specs/2026-10-01-p9b-a11y-keyboard-design.md` | `docs/plans/2026-10-01-p9b-a11y-keyboard.md`(第二批已执行,2026-10-01:crearte 0.24.0) |
|
| P9-B UX 第二批(可访问与键盘效率包) | `docs/specs/2026-10-01-p9b-a11y-keyboard-design.md` | `docs/plans/2026-10-01-p9b-a11y-keyboard.md`(第二批已执行,2026-10-01:crearte 0.24.0) |
|
||||||
| P11 服务端安全硬化 | `docs/specs/2026-10-02-p11-server-hardening-design.md` | `docs/plans/2026-10-02-p11-server-hardening.md`(已执行,2026-10-02:server 0.17.0 / deploy 0.7.0 / crearte 0.24.1) |
|
| P11 服务端安全硬化 | `docs/specs/2026-10-02-p11-server-hardening-design.md` | `docs/plans/2026-10-02-p11-server-hardening.md`(已执行,2026-10-02:server 0.17.0 / deploy 0.7.0 / crearte 0.24.1) |
|
||||||
|
| P12 窄屏溢出修复与打磨批 | `docs/specs/2026-10-02-p12-narrow-viewport-overflow-design.md` | `docs/plans/2026-10-02-p12-narrow-viewport-overflow.md`(已执行,2026-10-02:crearte 0.25.0 / server 0.17.1 / deploy 0.7.1) |
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# P12 窄屏溢出修复与打磨批 — 实现计划
|
||||||
|
|
||||||
|
- spec:`docs/specs/2026-10-02-p12-narrow-viewport-overflow-design.md`(权威,含全部实测数字与决策理由)
|
||||||
|
- 证据:`crearte/.superpowers/sdd-p12/FINDINGS-survey.md`(16 轮 spike 实测归档)
|
||||||
|
- 分支名:三仓统一 `fix/p12-narrow-viewport-overflow`
|
||||||
|
- 基线:crearte `d25c497`(0.24.1)/ crearte-server `9da39b6`(0.17.0)/ crearte-deploy `740958a`(0.7.0)
|
||||||
|
|
||||||
|
## 任务切分与波次
|
||||||
|
|
||||||
|
**派发结构(依 `sdd-parallel-dispatch` 技能 §1「一仓一写者」)**:T1/T2/T3/T5 全在 crearte **同仓同分支**,共享 git index → 四路并行会争用 `.git/index.lock`,故**合并为单个子代理**(四任务文件互不重叠,单写者无内部冲突)。三仓三路并行:crearte 子代理 + 控制者本人做 T6(server)/T7(deploy)——沿用 P11-T5「小而机械、控制者已元素级核实前提」的控制者自做范式(T6 前提已实测:`applyLogging` 确实回显归一化值、`strings` 已 import;T7 插入点已定位:`validate.yml` 第 19-29 行 step 之后)。
|
||||||
|
|
||||||
|
**T4 从「波次 2」提前为 crearte 子代理的第一步(RED→GREEN)**:守卫断言的正是修复后状态,先写守卫 → 跑出 RED(复现 spec §1 的 +380/+325/+76/+21/+3 实测数字)→ 再实现 T1/T2/T3 直到 GREEN。好处:① 消除 throwaway 验证脚本(不必像勘查期那样写完再删);② **RED 输出本身就是「守卫有牙」的证明**,无需事后再临时 revert 一处修复来自证;③ 实现者拿到真实反馈而非推断。T5 打磨三项不涉溢出,排在 GREEN 之后。
|
||||||
|
|
||||||
|
### crearte 子代理(单写者,TDD 顺序)
|
||||||
|
|
||||||
|
| 任务 | 仓 | 文件 | spec | 要点 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| T1 页头用户名截断 | crearte | `app/components/AppHeader.vue`、`app/components/AppHeader.test.ts` | §3.1 / D-A / D-G | summary 改 flex 三件 + `max-w-[6rem]` + `:title`;`▾` 独立 span 带 `aria-hidden`;**保留 `ref="summaryRef"`**(Esc 回焦依赖) |
|
||||||
|
| T2 五表格滚动包裹 | crearte | `app/views/AdminUsersView.vue`、`AdminView.vue`、`AdminAuditView.vue` | §3.2 / D-B / D-C / D-D | 包裹层 `relative overflow-x-auto pr-1 pb-1`;**三处 `v-else` 上移到包裹层**;表格内部(`scope`/sr-only/`data-testid`)一字不动 |
|
||||||
|
| T3 目录排序 + 账号昵称 | crearte | `app/components/ResultMeta.vue`、`app/views/AccountView.vue` | §3.3–3.4 / D-E / D-F | ResultMeta `:22` `shrink-0`→`min-w-0`(同排另两个 `shrink-0` **不动**);AccountView `:190` 加 `min-w-0 wrap-anywhere` |
|
||||||
|
| T5 打磨三项 | crearte | `app/router/index.test.ts`、`app/components/GameReactions.vue` + 其 test、`app/components/ToastHost.vue` + 其 test | §3.5(a)(b)(c) | P9B-1 直测 `focusMain()`;P9B-2 动态 `aria-label`(仅 `rated && i===score` 分支改写);P9B-4 `announcedId` 单调比较 + 新单测 |
|
||||||
|
| T6 配置错误消息钉桩 | crearte-server | `src/internal/config/config_test.go` | §3.5(d) | `LOG_LEVEL=" BOGUS "` 断言 err 含 `bogus` 且不含原样 ` BOGUS `;先核 `strings` import 与 `applyLogging` 现有文案 |
|
||||||
|
| T7 CI 空默认守卫 | crearte-deploy | `.github/workflows/validate.yml` | §3.5(e) | 新 step 三态断言(空默认 / 不得为 CIDR / 覆盖仍生效);**本地实跑 run 块含反向验证** |
|
||||||
|
|
||||||
|
### 控制者本人(与 crearte 子代理并行,各自独占一仓)
|
||||||
|
|
||||||
|
| 任务 | 仓 | 文件 | spec | 要点 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| T6 配置错误消息钉桩 | crearte-server | `src/internal/config/config_test.go` | §3.5(d) | 前提已核实:`config.go` `applyLogging` 错误文案为 `fmt.Errorf("config: LOG_LEVEL: invalid value %q", cfg.LogLevel)`,`cfg.LogLevel` 已是归一化后的值 → **回显成立,纯补测试**;`strings` 已在 test import 中 |
|
||||||
|
| T7 CI 空默认守卫 | crearte-deploy | `.github/workflows/validate.yml` | §3.5(e) | 插入点已定位:`compose` job 内第 19 行 step 之后、第 30 行 step 之前;**本地实跑 run 块三态**(当前通过 / 注入 CIDR 应失败 / 覆盖 `10.0.0.0/8` 应通过) |
|
||||||
|
|
||||||
|
(T4 两层机器守卫已并入 crearte 子代理的第一步,见上。)
|
||||||
|
|
||||||
|
## 验收口径(合并前必跑,全量)
|
||||||
|
|
||||||
|
- **crearte**:`npm run test`(基线 626 + 新增)、`npm run typecheck`、`npm run build`、`npm run build:runtime`、`npm run e2e`(基线 80+1skip + 新增 responsive)、`npm run e2e:noauth`(基线 4)。脚本名以 `package.json` 为准,**禁用 `--if-present`**(P11 假绿事件)。
|
||||||
|
- **crearte-server**:容器化 Go 1.24(`GOPROXY=https://goproxy.cn,direct`、`GOSUMDB=sum.golang.google.cn`、命名卷 `crearte_gomod`/`crearte_gocache`):`gofmt -l .` 空 / `go vet ./...` 0 / `go build ./...` 0 / `go test ./... -count=1` 11 包 ok。
|
||||||
|
- **crearte-deploy**:四 profile `config -q`;卷名集合与基线逐字相同;T7 守卫脚本本地三态实跑。
|
||||||
|
- 退出码:管道后一律 `set -o pipefail` 或不管道(P11 三度踩坑)。
|
||||||
|
|
||||||
|
## 纪律(延续 P9-B / P11 教训)
|
||||||
|
|
||||||
|
1. 派发简报引用的**每个数量**由控制者预先元素级 grep 核准(P9-B「19 个 th」教训)。
|
||||||
|
2. 简报里对既有代码/环境行为的**事实陈述**同样要核实(P11 四次被抓)。
|
||||||
|
3. 任务级审查 → 控制者裁定 → 波次末全分支终审(最强模型),终审须独立复跑验收腿并对账。
|
||||||
|
4. 审查依据是 **spec §3 全量**,不是任务书(P6 D-D 教训:任务书漏列项审查扯不出)。
|
||||||
|
5. 内仓禁 master 直提;合并用 `--no-ff`;push 不用管道且推后 `ls-remote` 非空对账。
|
||||||
|
6. **本批不做的**:D4(nav 逐字换行,park 待设计决策)、汉堡菜单(已证伪)、CSP/HSTS/TLS(独立批)、后端校验规则(60 字是既有合法契约)。
|
||||||
|
|
||||||
|
## 挂账处置(随本批记账)
|
||||||
|
|
||||||
|
- ROADMAP / 账本中「移动端页头 / 汉堡菜单」→ 标注**已证伪删除**(nav 内容宽 74–158px,320–1280px 零溢出)。
|
||||||
|
- D4(nav 链接逐字换行)→ 标注 **park**:外观级、无溢出无功能损失,修法在 320px 与 D1a 争空间,需设计决策(更短名上限 / 汉堡菜单 / nav 缩写)。
|
||||||
|
- P9B-3(spec「19 个 th」口径)→ 已在 P9-B 波次内修完,不属本批。
|
||||||
|
- P11 其余次要 notes(T1 按需回收、T2 注释语言、T2 共享片段唯一性、T3 `" warn "` 未断言 LogFormat、T4 README 窗口口径、T5 过程性陈述)→ **纯留档,无可动手改动**,不入本批。
|
||||||
@@ -0,0 +1,361 @@
|
|||||||
|
# P12 窄屏溢出修复与打磨批 — 设计文档
|
||||||
|
|
||||||
|
- 日期:2026-10-02
|
||||||
|
- 涉及仓库:`crearte`(主)+ `crearte-server`(测试)+ `crearte-deploy`(CI)+ wrapper(记账)
|
||||||
|
- 前置:P11(`docs/specs/2026-10-02-p11-server-hardening-design.md`,已交付 server 0.17.0 / deploy 0.7.0 / crearte 0.24.1)
|
||||||
|
- 证据:`crearte/.superpowers/sdd-p12/FINDINGS-survey.md`(16 轮 throwaway spike 的实测输出固化,spike 文件已删、工作树净)
|
||||||
|
|
||||||
|
本批**零新功能、零后端行为变更**。做两件事:① 修四个实测可达的窄屏溢出缺陷;② 清偿 P9-B / P11 挂账的可动手打磨项,并把「本批缺陷类型」变成机器守卫(正面应用 P11 N6 的教训)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 缺口(全部实测,非推断)
|
||||||
|
|
||||||
|
度量口径统一为 `document.documentElement.scrollWidth - clientWidth`(>0 即页面横向溢出),在真实构建产物上由 Playwright 实测(`build:e2e` + `serve-runtime.mjs --port 4173`)。
|
||||||
|
|
||||||
|
### 缺口 A(D1a):合法长用户名把页头撑出视口,且波及**全站每个路由**
|
||||||
|
|
||||||
|
`AppHeader.vue:102` 的 `<summary>` 直接插值 `{{ user.display_name }} ▾`,无任何宽度约束。60 字符 **ASCII** 名在 `word-break: normal` 下不可断行,summary 索取 484px 内容宽:
|
||||||
|
|
||||||
|
| 视口 | 实测 |
|
||||||
|
|---|---|
|
||||||
|
| 320px | `doc=700/320` → **+380px**;`sumW=484 sumRight=700` |
|
||||||
|
| 375px | `doc=700/375` → **+325px** |
|
||||||
|
| 768px | `/` ok;`/account` **+40px** — spike 17 反证更正:此 +40 由**本缺口(页头)**驱动,非缺口 B(回退 dd 修复 @768 仍 `over=+0`;回退页头 @768 → 红)。原表把它记在缺口 B 名下是归因错误 |
|
||||||
|
|
||||||
|
**可达性是硬事实,不是敌意构造**:后端 `internal/handler/auth.go:25` `maxDisplayNameLen = 60`、`:76` 错误文案「display name must be 1-60 characters」;前端 `app/auth/validation.ts:4` `DISPLAY_NAME_MAX = 60`、`RegisterView.vue:90` `maxlength="60"`。即**任何注册用户填满合法上限即触发**,且因页头全站常驻,`/`、`/games`、`/docs`、`/creator`、`/login`、`/account`、`/admin/*` 无一幸免。
|
||||||
|
|
||||||
|
24 字 **CJK** 名不触发(CJK 可断行,实测 `doc=320/320 ok`)——缺陷取决于**字符可断性**而非长度本身,故修复必须针对「不可断行串」而非「截断到 N 字」。
|
||||||
|
|
||||||
|
### 缺口 B(D1b):账号页昵称 `dd` 逃逸
|
||||||
|
|
||||||
|
`AccountView.vue:190` 的 `<dd class="text-sm font-bold">` 位于 `flex flex-wrap items-baseline gap-2` 内,但 60 字 ASCII 不可断行且 flex item 默认 `min-width:auto` 拒绝收缩:320px 实测 `ddW=542 ddRight=592 ddEscapes=true`(视口 320),`ddWhiteSpace=normal ddOverflowWrap=normal`。
|
||||||
|
|
||||||
|
**牙口位置(spike 17 2×2 消融实测)**:dd 固有宽 542px、左缘 50px → 右缘恒为 592px,故它在**任何 <592px 的视口**都溢出:回退本修复 @320px → `over=+272`(`scrollWidth=592`,即 `ddRight=592`)、@375px → `over=+217`。但 @768px dd 右缘 672 < 768,**本修复在 768 非必要**——该视口 `/account` 的 +40px 是缺口 A(页头)驱动(见上表更正)。即:dd 修复的牙在 320/375,页头修复的牙在全视口;两者独立必要、缺一不可,但**不是同一视口的同一症状**。
|
||||||
|
|
||||||
|
### 缺口 C(D2):目录页排序 select 的 `shrink-0`
|
||||||
|
|
||||||
|
`ResultMeta.vue:22` 的 `<div class="relative shrink-0">` 包裹排序 `<select>`,`shrink-0` 使其拒绝收缩。实测 `/games` @320px `doc=323/320` → **+3px**,**短名短数据也复现**(与缺口 A 无关);360px 及以上无溢出。spike 7 的四候选对照精确定位到它:仅「移除该包裹层 `shrink-0` + `min-width:0`」把 +3 归零,其余三个候选无效。
|
||||||
|
|
||||||
|
### 缺口 D(D3):五个 admin 表格零 overflow 包裹层,**短数据也溢出**
|
||||||
|
|
||||||
|
静态审计:`app/**` 共 **5 个 `<table>`**(`AdminUsersView.vue:115` 6 列、`AdminView.vue:150/173/220` 共 13 列、`AdminAuditView.vue:60` 5 列),`overflow-x`/`overflow-auto` 在表格上下文中**零命中**,全部 `table-layout: auto`、父级 `overflow-x: visible`。
|
||||||
|
|
||||||
|
短名短邮箱数据实测(与 display_name 长度无关):
|
||||||
|
|
||||||
|
| 路由 | 320px | 375px | 768px+ |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/admin/users` | **+76px**(`t0 w=364 right=396`) | **+21px** | ok |
|
||||||
|
| `/admin/audit` | **+38px**(`t0 w=326 right=358`) | ok | ok |
|
||||||
|
| `/admin` | ok(`t0 w=282`) | ok | ok |
|
||||||
|
|
||||||
|
根因:表格 `auto` 布局的 min-content 宽由各列固有内容决定(邮箱等宽串、`toLocaleString('zh-CN')` 日期、角色徽章、操作按钮),`w-full` 只是 `width:100%` 的**建议**,撑不破 min-content 下限;父级无裁剪 → 直接推宽文档。
|
||||||
|
|
||||||
|
### 缺口 E(D5):长名下用户下拉菜单逃逸视口
|
||||||
|
|
||||||
|
`AppHeader.vue:103` 的菜单是 `absolute right-0 w-32`,相对 `relative` 的 `<details>` 定位。缺口 A 使 details 本身宽 484px 且右边缘在 700px,菜单随之被推出屏外:60 字名 @320px 实测 `menu {left:357, right:485, escapesRight:true}`。**修好缺口 A 即自动修好本项**(spike 15/16 全部变体实测 `escapesRight:false`),无需独立改动,但须有断言钉住。
|
||||||
|
|
||||||
|
### 非缺口(勘查证伪,避免做无用功)
|
||||||
|
|
||||||
|
- **「移动端页头 / 汉堡菜单」挂账证伪**:nav 三链接内容宽仅 **74–158px**,320/360/375/412/768/1280px **全部零横向溢出**(spike 1+4)。ROADMAP 与账本中的该项挂账应从「待启动」改为「已证伪,删除」。
|
||||||
|
- **D4(nav 链接逐字换行)park**:320px 下「作品」渲染为 `作/品`(link `h=45`、`navH=45` vs 行高 `h-14`=56px),所有手机宽度都发生(含 375px = iPhone 12/13/14)。属**外观级**:无溢出、无截断(`selfOverflow=false`)、无功能损失。修法 `whitespace-nowrap` 实测在 320px 引入 **+11px** 溢出(demand 347 > 320),七种 gap 收缩组合(C1–C6)在「320px + 长名」下**全部失败**(+19~+75px)——logo(77) + nowrap nav(138–158) + 截断名(96) 物理放不下,与缺口 A 争同一份空间。需设计决策(更短的名上限 / 汉堡菜单 / nav 缩写),**不入本批**,转独立决策项。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 决策表
|
||||||
|
|
||||||
|
| # | 主题 | 定案 | 理由(实测依据) |
|
||||||
|
|---|---|---|---|
|
||||||
|
| D-A | `<summary>` 怎么截断 | **flex 三件**:`summary` 加 `flex max-w-[6rem] min-w-0 items-center gap-1`;名字包一层 `<span class="truncate min-w-0">`;`▾` 包一层 `<span class="shrink-0" aria-hidden="true">`;`summary` 加 `:title="user.display_name"` | spike 15 证伪了「运行时把名字节点包起来、▾ 留在外面」这条路:**Vue 把 `{{ user.display_name }} ▾` 编译成单个文本节点**,任何基于 `childNodes[0]` 的拆分都会把 `▾` 一起搬进截断 span(V2–V4 实测 `caret=HIDDEN`)。必须改模板显式拆两个 span。spike 16 六变体实测:F1(flex 无 cap)在 60 字名时 320px **+390**、375px 同样 ❌(纯 shrink 无效,summary 仍索取内容宽;375px 的具体 px 当时未归档,终审 N-F2 故删去原写的「+405」——该数无出处,且不承载任何结论:归档的「F1 仍 ❌」已足够);F2(7rem)320px **+7**;**F3(6rem)320/375px × {短名, 60字ASCII, 60字CJK} 六格全 `over=+0` 且 `caret=VIS`**;F4/F5(5/4rem)也过但 summary 更窄、无额外收益。6rem 同时是 spike 15 整体截断形态的实测赢家(`sumRight=311 ≤ 320`),两种形态上限一致 → 定 6rem。`title` 承载全名(鼠标悬停可读全文),避免截断丢信息。 |
|
||||||
|
| D-B | 包裹层为什么必须 `relative` | 五个表格的包裹层统一为 `class="relative overflow-x-auto pr-1 pb-1"` | **`relative` 不是装饰,是修 bug**:P9-B 为空「操作」列头补的 `<span class="sr-only">操作</span>` 是 `position:absolute` + `margin:-1px`,而 Tailwind 的 `sr-only` **不含 `top`/`left`** → 其包含块是最近的**已定位**祖先。`position:static` 的 `overflow-x:auto` 包裹层不构成包含块,该 span 逃出滚动裁剪,**独自**把文档撑宽:烧蚀实验(逐元素 `display:none` + 重测权威度量)定位到 `d=11 span.sr-only l=351 r=352 w=1`,短数据残差 `doc=352` = 其右边缘 352;长数据 `doc=1061` = 其右边缘 1061。加 `relative` 后残差归零(spike 12 W1 实测 `over=+0`;W2「th 加 relative」同样 `over=+0`,选 W1 因改动集中在一处新增元素而非逐个 `<th>`)。这也解释了 spike 9 的 `trueOffenders=0` 矛盾:offender 扫描把「有可滚动祖先」的元素当合法溢出筛掉了,而它恰恰逃出了容器——**边框盒扫描看不见它,只有烧蚀能**。 |
|
||||||
|
| D-C | 包裹层要不要留 padding | 留 `pr-1 pb-1`(4px) | 五个表格都带 `shadow-hard` = `4px 4px 0 #141414`(`main.css:20`)。overflow 裁剪发生在 **padding box**,无 padding 时 `roomBottom=0`(阴影被裁),`pr-1 pb-1` 时 `roomBottom=4` 恰好容下(spike 7 Q2 / spike 12 W3 对照实测)。Tailwind v4 `p*-1` = 0.25rem = 4px,与阴影偏移逐值相等。 |
|
||||||
|
| D-D | `v-else` 怎么处置 | **`v-else` 从 `<table>` 上移到包裹层** | 三个表格是 `<p v-if="空态">` + `<table v-else>` 的**相邻兄弟配对**(`AdminUsersView.vue:114-115`、`AdminView.vue:149-150`、`AdminAuditView.vue:59-60`)。把 `<table>` 包进 `<div>` 而不迁移 `v-else`,会使 `v-else` 失去配对对象 → Vue 编译报错或空态/表格逻辑错乱。另两个表格(`AdminView.vue:173/220`)无 `v-if` 兄弟,仅包裹、不涉 `v-else`。 |
|
||||||
|
| D-E | 排序 select 怎么改 | `ResultMeta.vue:22` 的 `relative shrink-0` → `relative min-w-0` | spike 7 四候选对照中唯一把 `/games` @320px 的 +3px 归零的就是它(候选 B);同排的 `shrink-0` 计数 `<p>`(`:20`)与「筛选」按钮实测**不是**驱动元素(候选 C/D 无效)。保留 `relative`(`PhCaretDown` 靠它绝对定位)。 |
|
||||||
|
| D-F | `dd` 怎么改 | `AccountView.vue:190` 的 `text-sm font-bold` → `text-sm font-bold min-w-0 wrap-anywhere` | `min-w-0` 解除 flex item 的 `min-width:auto` 下限(否则拒绝收缩),`wrap-anywhere`(`overflow-wrap:anywhere`)让不可断行串可在任意位置折行。已查包确认 Tailwind 4.3.3 **存在** `wrap-anywhere` 工具类(`node_modules/tailwindcss/dist/lib.js` 命中),非推断。**不用** `break-all`:它对 CJK/拉丁混排断词更粗暴,且本处只需「允许在任意点折行」而非「强制逐字断」。 |
|
||||||
|
| D-G | 可访问名怎么保住 | 不改任何 `aria-*`、`scope`、`sr-only` 结构;`▾` 的 span 加 `aria-hidden="true"` | `▾` 是纯装饰字形(既有代码里它就是文本节点的一部分,从未参与语义)。加 `aria-hidden` 后 `<summary>` 的可访问名从「用户名 ▾」变为「用户名」——**语义更准**(`▾` 不是名字的一部分),且 `e2e/ux.spec.ts:26` 的 locator `header details summary` 与断言(`toBeVisible` / 点击 / `details[open]` 计数)不依赖可访问名文本,实测不破。`e2e/ux.spec.ts:25` 的注释「details summary 的可访问名即用户名」在改动后**反而更准确**(原先含 `▾`),同批更新该注释措辞。 |
|
||||||
|
| D-H | 机器守卫怎么做 | 两层:① **e2e 视口守卫** `e2e/responsive.spec.ts`(进 CI 主腿);② **源码级守卫** `app/lib/tableOverflow.test.ts`(沿用 `tableScope.test.ts` 范式) | P11 终审 N6 的核心观察:**文档层防御密集但无机器守卫**,改回坏配置不会被任何测试抓住。本批把这个教训正面应用——D1/D2/D3 全部是「人眼看构建产物才会发现」的缺陷,正是 e2e 视口断言的用武之地。`playwright.config.ts` 的 `testIgnore: /noauth\.spec\.ts/` 意味着新增 `responsive.spec.ts` **自动进 CI `e2e` job**(`validate.yml:50` 跑 `npm run e2e`),无需改 CI 配置。源码级守卫按「每个 `<table>` 的**父元素**是否带 `overflow-x-auto`」判定,**不能**用全局 `overflow-x-auto` 计数 1:1 断言——`DocSidebar.vue:25` 已有一处用在 `<nav>` 上(实测基线 = 1 而非 0),全局计数会写错。 |
|
||||||
|
| D-I | 打磨项取舍 | 纳入 P9B-1/P9B-2/P9B-4、P11-N5/N6;**不纳入** P9B-3(已修)、P11 其余次要 notes(纯留档,无可动手改动) | 逐条判据见 §3.5。关键:P9B-2 与 P9B-4 都改既有断言覆盖的行为,须先证明**既有断言不破**再动手——P9B-2 的 `a11y.spec.ts:61` 在未评分态(`rated=false`)下断言 `评 3 星`,动态标签只在「已评且 i === score」时改写,故该断言路径不变;P9B-4 的六处 `toast-announce` 断言均不涉及「手动关掉最新一条」,新语义是**追加**而非改写。 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 实现
|
||||||
|
|
||||||
|
### 3.1 `crearte/src/app/components/AppHeader.vue`(D-A / D-G / 缺口 E)
|
||||||
|
|
||||||
|
当前 `:101-102`:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<details v-else ref="detailsRef" class="relative ml-auto sm:ml-0" @toggle="menuOpen = ($event.target as HTMLDetailsElement).open">
|
||||||
|
<summary ref="summaryRef" class="list-none cursor-pointer select-none border-2 border-ink bg-surface px-2 py-1 text-xs font-bold [&::-webkit-details-marker]:hidden">{{ user.display_name }} ▾</summary>
|
||||||
|
```
|
||||||
|
|
||||||
|
改为(`<details>` 一行**不动**,仅改 `<summary>` 及其内容):
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<summary
|
||||||
|
ref="summaryRef"
|
||||||
|
:title="user.display_name"
|
||||||
|
class="flex max-w-[6rem] min-w-0 list-none cursor-pointer select-none items-center gap-1 border-2 border-ink bg-surface px-2 py-1 text-xs font-bold [&::-webkit-details-marker]:hidden"
|
||||||
|
>
|
||||||
|
<span class="min-w-0 truncate">{{ user.display_name }}</span>
|
||||||
|
<span class="shrink-0" aria-hidden="true">▾</span>
|
||||||
|
</summary>
|
||||||
|
```
|
||||||
|
|
||||||
|
`ref="summaryRef"` 必须保留(`onDocKeydown` 的 Esc 回焦依赖它,见 `:26` `summaryRef.value?.focus()`)。`truncate` = `overflow:hidden` + `text-overflow:ellipsis` + `white-space:nowrap`,配合 `min-w-0` 才能在 flex 子项上生效。
|
||||||
|
|
||||||
|
### 3.2 `crearte/src/app/views/{AdminUsersView,AdminView,AdminAuditView}.vue`(D-B / D-C / D-D)
|
||||||
|
|
||||||
|
五处统一模式。**带 `v-else` 的三处**(`AdminUsersView.vue:115`、`AdminView.vue:150`、`AdminAuditView.vue:60`)——`v-else` 上移:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- 改前 -->
|
||||||
|
<table v-else class="w-full border-2 border-ink bg-surface text-sm shadow-hard">
|
||||||
|
…
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- 改后 -->
|
||||||
|
<div v-else class="relative overflow-x-auto pr-1 pb-1">
|
||||||
|
<table class="w-full border-2 border-ink bg-surface text-sm shadow-hard">
|
||||||
|
…
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**不带 `v-else` 的两处**(`AdminView.vue:173`、`AdminView.vue:220`)——仅包裹:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<div class="relative overflow-x-auto pr-1 pb-1">
|
||||||
|
<table class="w-full border-2 border-ink bg-surface text-sm shadow-hard">
|
||||||
|
…
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
表格开闭行号(供核对,改动会移动后续行号,以内容锚点为准):`AdminUsersView.vue` 115→161;`AdminView.vue` 150→165、173→214、220→239;`AdminAuditView.vue` 60→82。表格**内部**(`<thead>`/`<tbody>`/`<th scope>`/`data-testid`)一字不动——P9-B 的 `scope="col"` 与 sr-only「操作」必须原样保留(`a11y.spec.ts:52` 断言 `headers.nth(5)` 可访问名为 `操作`)。
|
||||||
|
|
||||||
|
### 3.3 `crearte/src/app/components/ResultMeta.vue`(D-E)
|
||||||
|
|
||||||
|
`:22` 单行改动(全文件仅此一处 `relative shrink-0`,唯一匹配):
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- 改前 --> <div class="relative shrink-0">
|
||||||
|
<!-- 改后 --> <div class="relative min-w-0">
|
||||||
|
```
|
||||||
|
|
||||||
|
该文件共 **4 处** `shrink-0`(元素级核准),只改 `:22` 这一处,**其余三处不动**:`:18` 计数 `<p class="shrink-0 text-[0.9375rem] font-extrabold" aria-live="polite">`、`:20` `<PhArrowsDownUp … class="hidden shrink-0 text-ink-soft sm:block" />`、`:43`「筛选」按钮 `class="lift flex shrink-0 items-center gap-1.5 …"`。spike 7 的四候选对照实测:只有移除 select 包裹层的 `shrink-0`(候选 B)能把 `/games` @320px 的 +3px 归零;针对计数 `<p>`(候选 C)与筛选按钮(候选 D)的改动均**无效**(over 仍 +3px),故不动它们。
|
||||||
|
|
||||||
|
### 3.4 `crearte/src/app/views/AccountView.vue`(D-F)
|
||||||
|
|
||||||
|
`:190` 单行改动:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- 改前 --> <dd class="text-sm font-bold">{{ user.display_name }}</dd>
|
||||||
|
<!-- 改后 --> <dd class="min-w-0 text-sm font-bold wrap-anywhere">{{ user.display_name }}</dd>
|
||||||
|
```
|
||||||
|
|
||||||
|
同文件 `:194` 的用户名 `dd` 与其他 `dd` **不动**(用户名有 `maxlength="39"`,实测不溢出)。
|
||||||
|
|
||||||
|
### 3.5 打磨项(跨仓,各自独立)
|
||||||
|
|
||||||
|
**(a) P9B-1 — `crearte/src/app/router/index.test.ts:111`**
|
||||||
|
|
||||||
|
当前弱断言:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await expect(router.push('/docs')).resolves.not.toThrow()
|
||||||
|
```
|
||||||
|
|
||||||
|
它依赖 vue-router 5.3.1「`afterEach` 抛错不被吞」的语义,升级后若改为吞抛则**恒真**(假绿)。改为直接单测被测函数本身,绕开 router 版本语义:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// 直接验证 focusMain 在 #main 缺失时静默 no-op(不经 router,故不依赖
|
||||||
|
// vue-router「afterEach 抛错不被吞」的版本语义——那会让断言退化为恒真)
|
||||||
|
expect(() => focusMain()).not.toThrow()
|
||||||
|
```
|
||||||
|
|
||||||
|
并从 `@/router` 补 `focusMain` 到既有 import。保留其后的 `expect(document.title).toBe('文档 · crearte 创艺')`(标题职责仍经 router 验证)。
|
||||||
|
|
||||||
|
**(b) P9B-2 — `crearte/src/app/components/GameReactions.vue:105`**
|
||||||
|
|
||||||
|
当前每颗星静态 `:aria-label="`评 ${i} 星`"`,读屏用户听「评 4 星」无法预知「再点同一颗星会撤评」(该控件支持点当前分取消)。改为按状态动态:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
:aria-label="rated && i === score ? `已评 ${i} 星,点击取消评分` : `评 ${i} 星`"
|
||||||
|
```
|
||||||
|
|
||||||
|
`rated`/`score` 均为既有 ref(`:16-17`,由 `apply(view)` 从服务端 `ReactionView` 全量替换,`:44-51`)。**既有断言不破**:`e2e/a11y.spec.ts:61` 在 `seedSession(page,'user')` + 夹具无个人评分下走 `rated=false` 分支,可访问名仍是 `评 3 星`。新增单测须覆盖两态(未评 → `评 N 星`;已评 N → `已评 N 星,点击取消评分`;已评 M≠N → `评 N 星`)。**不改** `role="group"` 的 `:aria-label`(`:98`,P9-B 已定稿)与星形字形 `aria-hidden`。
|
||||||
|
|
||||||
|
**(c) P9B-4 — `crearte/src/app/components/ToastHost.vue:22-28`**
|
||||||
|
|
||||||
|
当前 watch 源是**数组末尾条目的 id**:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
watch(
|
||||||
|
() => toasts.value[toasts.value.length - 1]?.id,
|
||||||
|
(id) => {
|
||||||
|
announcement.value =
|
||||||
|
id === undefined ? '' : (toasts.value[toasts.value.length - 1]?.text ?? '')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
饱和态(`MAX_VISIBLE=3`)下 push 会「逐最旧 + append」,长度 3→3 不变而末尾 id 变,故 P9-B 特意以 id 为源(这个选择是对的,**保留**)。副作用是:**手动关掉最新一条**时末尾 id 回退到较旧条 → watch 触发 → 播报区**重念一条仍在屏上的旧消息**(自动过期不触发,因为过期的是最旧条)。修法:只在 id **单调变新**时播报,并记住已播报的最大 id:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const announcement = ref('')
|
||||||
|
// 已播报的最大 id:useToast 的 nextId 单调递增,故「比它大」即「新消息」。
|
||||||
|
// 手动关掉最新一条会让数组末尾 id 回退到较旧条,若不比较大小就会重念一条
|
||||||
|
// 仍在屏上的旧消息(P9B-打磨4)。
|
||||||
|
let announcedId = 0
|
||||||
|
watch(
|
||||||
|
() => toasts.value[toasts.value.length - 1]?.id,
|
||||||
|
(id) => {
|
||||||
|
if (id === undefined) {
|
||||||
|
// 全部消失:清空播报,但不重置 announcedId(后续新消息 id 必然更大)
|
||||||
|
announcement.value = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (id <= announcedId) return
|
||||||
|
announcedId = id
|
||||||
|
announcement.value = toasts.value[toasts.value.length - 1]?.text ?? ''
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**六处既有断言均不破**(逐一核对 `ToastHost.test.ts`):`:49-53` 常驻空播报(初始 `announcement=''`);`:61` push 后等于该消息(id 1 > 0);`:74-78` 消失后清空(`id===undefined` 分支);`:98` 饱和态播报第 4 条(id 4 > 3);`:106` 播报节点内零交互控件(结构未变)。新增单测须覆盖「三条并存 → 关掉最新 → 播报文字**不变**(不重念旧消息)」。注意 `announcedId` 是模块内闭包变量,而 `toasts` 是 `useToast` 的模块级单例 ref——测试间须用既有 `__resetToasts()`(`useToast.ts:38`)隔离;若 `announcedId` 残留导致跨测试污染,改为把它也挂到 store 侧或在 `__resetToasts` 里一并复位(实现者按实测选择,并在测试注释里写明)。
|
||||||
|
|
||||||
|
**(d) P11-N5 — `crearte-server/src/internal/config/config_test.go`**
|
||||||
|
|
||||||
|
P11 T3 加了归一化,但「错误消息回显归一化后的值」无钉桩。在 `TestApplyLogging` 的 bogus 段(`:264` 附近,`LOG_FORMAT` bogus 之后)补:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 归一化后的错误消息须回显归一化值(P11-N5):运维看到 " BOGUS " 原样
|
||||||
|
// 会以为是空格问题,回显 "bogus" 才指向真正的白名单不匹配。
|
||||||
|
t.Setenv("LOG_LEVEL", " BOGUS ")
|
||||||
|
err := applyLogging(&cfg)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("invalid LOG_LEVEL must fail after normalization")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "bogus") {
|
||||||
|
t.Errorf("error = %q, want it to echo the normalized value %q", err, "bogus")
|
||||||
|
}
|
||||||
|
if strings.Contains(err.Error(), " BOGUS ") {
|
||||||
|
t.Errorf("error = %q, must not echo the raw un-normalized value", err)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
须先 `grep -n "\"strings\"" internal/config/config_test.go` 确认 import;缺失则补。先读 `applyLogging` 现有错误文案,确认它确实回显归一化值(P11 T3 报告称是)——若实测不回显,则本项从「补测试」变为「补实现 + 测试」,须在报告里说明。
|
||||||
|
|
||||||
|
> **勘误(2026-10-02,终审 N-F1)**:上方片段的第一条断言写的是裸子串 `strings.Contains(err.Error(), "bogus")`。审查 note SD-N1 指出它有理论盲区:若实现改为回显**小写但未 trim** 的 `" bogus "`,裸子串断言与下方大小写敏感的原样形式检查**会同时放过**。落地的 `config_test.go`(server `fa39d27`)已收紧为 `%q` 渲染出的**带引号形式** `` `"bogus"` ``,一次钉住 trim 与 lower 两个性质。终审已用 mutation 独立证实两条断言对「回显原始 env」同时开火。**不要按上方片段把它弱化回裸子串。**
|
||||||
|
|
||||||
|
**(e) P11-N6 — `crearte-deploy/.github/workflows/validate.yml`**
|
||||||
|
|
||||||
|
在 `compose` job 的「compose parses under every profile」step 之后新增一个 step,把 P11 D-A′ 的**空默认**变成机器守卫(终审 N6:现无任何自动化测试能抓住 compose 文件被改回 subnet 信任):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: TRUSTED_PROXIES defaults empty (P11 D-A' guard)
|
||||||
|
env:
|
||||||
|
POSTGRES_PASSWORD: "***"
|
||||||
|
MINIO_ROOT_USER: ci
|
||||||
|
MINIO_ROOT_PASSWORD: "***"
|
||||||
|
AUTH_TOKEN_SECRET: "***"
|
||||||
|
BUNDLE_KEK_k1: ci-not-a-real-secret
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
# 空默认是安全默认:信任整个 compose 网段会把 docker 网桥网关划进信任范围,
|
||||||
|
# 使客户端预置的 X-Forwarded-For 被采纳(限流可自选桶绕过)。
|
||||||
|
# 详见 README「客户端 IP 解析与限流」。
|
||||||
|
rendered="$(docker compose --profile prod config)"
|
||||||
|
# 未设 .env 时必须解析为空字符串(config 输出形如 TRUSTED_PROXIES: "")
|
||||||
|
echo "$rendered" | grep -q 'TRUSTED_PROXIES: ""' \
|
||||||
|
|| { echo 'TRUSTED_PROXIES must default to empty — see README' >&2; exit 1; }
|
||||||
|
# 且不得出现网段信任(172.28.0.0/24 或任何 COMPOSE_SUBNET 派生)
|
||||||
|
if echo "$rendered" | grep -Eq 'TRUSTED_PROXIES: "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+'; then
|
||||||
|
echo 'TRUSTED_PROXIES must not default to a CIDR (docker SNAT makes the bridge gateway trusted)' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# 覆盖仍须生效(管线未断)
|
||||||
|
TRUSTED_PROXIES=10.0.0.0/8 docker compose --profile prod config \
|
||||||
|
| grep -q 'TRUSTED_PROXIES: "10.0.0.0/8"' \
|
||||||
|
|| { echo 'TRUSTED_PROXIES override pipeline broken' >&2; exit 1; }
|
||||||
|
```
|
||||||
|
|
||||||
|
> **勘误(2026-10-02,审查 SD-D1 定案)**:上方字面片段的断言②③模式写的是**带引号**形式(`TRUSTED_PROXIES: "[0-9]+…`、`TRUSTED_PROXIES: "10.0.0.0/8"`),但 compose 实测只对**空串**加引号,非空标量**裸渲染**(`TRUSTED_PROXIES: 10.0.0.0/8`)。照抄字面片段会同时产出:②**恒不匹配的死断言**(改回 CIDR 默认也放行 = 假信心)与③**恒红的假警**(正向 CI 必炸)。落地的 `validate.yml`(deploy `17748bf`)已改为兼容裸标量的 ` *[0-9]` / ` *10\.0\.0\.0/8` 并渲染到文件复用,三态实跑验证(正向绿 / 注入 CIDR 红 / 删注入行红)。**不要按字面片段「修回去」。**
|
||||||
|
|
||||||
|
注意 `paths:` 过滤器已含 `docker-compose.yml` 与 `.github/workflows/validate.yml`,无需改触发条件。**本地无法跑 GitHub Actions**,故本项的验证方式是:把 `run:` 块内容作为脚本在本地对 `docker compose --profile prod config` 实跑一遍(含覆盖分支与「故意改坏 → 应失败」的反向验证),并在报告里贴输出。反向验证必须做——只证明「当前通过」不证明守卫有牙(P11 mutation 抽查同理)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 边界(不做的事)
|
||||||
|
|
||||||
|
- **不动 D4**(nav 逐字换行):外观级、无溢出、修法在 320px 与缺口 A 争空间,需独立设计决策。
|
||||||
|
- **不引入汉堡菜单 / 不改 nav 结构**:勘查已证伪其必要性。
|
||||||
|
- **不动 CSP / HSTS / TLS**:P11 已明确挂账为独立批(CSP 需设计——游玩子域经 iframe+SW 加载用户作品;HSTS 待 TLS)。
|
||||||
|
- **不动后端任何行为**:`maxDisplayNameLen=60` 是既有合法契约,本批不改校验规则(改短会破坏已有账号数据)。缺口 A 是**前端渲染**未防御合法输入,修在前端。
|
||||||
|
- **不改 `sr-only` 的 Tailwind 定义或 P9-B 的 `scope="col"` 结构**:D-B 的 `relative` 是在**包裹层**上补包含块,不动 sr-only 本身(它是全仓通用工具类,改它波及面不可控)。
|
||||||
|
- **不加 `overflow-x-hidden` 到 body/html**:那会把溢出**藏起来**而非修掉,且会裁掉 `sticky` 页头与 `shadow-hard`。缺口必须真消除(实测 `scrollWidth == clientWidth`),不是视觉遮盖。
|
||||||
|
- **不动 `AdminView.vue` 的三表内部结构**(列数、`data-testid`、按钮文案):只加包裹层。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 测试计划
|
||||||
|
|
||||||
|
### T1(AppHeader,缺口 A/E)
|
||||||
|
1. 既有 `app/components/AppHeader.test.ts` 六个测试全绿(它们用 `w.get('summary')` / `w.get('details')` / `w.get('details a')`,与 summary **内部**结构无关;`:23` 的 mock `display_name: 'tester'`)。
|
||||||
|
2. 新增单测:60 字 ASCII 名下 ① `<summary>` 有 `title` 属性且值为全名;② 名字 span 带 `truncate`、`▾` span 带 `aria-hidden="true"`;③ summary 的可访问名不含 `▾`。
|
||||||
|
3. e2e:`e2e/responsive.spec.ts`(见 T4)覆盖 320/375px × 60 字名 × 6 路由零溢出 + 菜单不逃逸。
|
||||||
|
|
||||||
|
### T2(三视图五表格,缺口 D)
|
||||||
|
1. 既有 `e2e/a11y.spec.ts:50/52` 的 `columnheader` 断言全绿(`headers` 计数仍 6、`nth(5)` 可访问名仍 `操作`)——`getByRole('columnheader')` 不受包裹层影响,须实跑确认。
|
||||||
|
2. 既有 `e2e/admin-flow.spec.ts` 全绿(`queue-sub-p1` 等 `data-testid` 定位不变)。
|
||||||
|
3. 新增源码级守卫 `app/lib/tableOverflow.test.ts`:扫 `app/**/*.vue`,对每个 `<table` 起始标签,向上取**同一文件文本内**最近的开标签父元素,断言其 `class` 含 `overflow-x-auto`;违规报 `文件:行号: 行内容`。沿用 `tableScope.test.ts` 的 `walk()`/`candidates()`/跨行起始标签截取范式。**不得**用全局 `overflow-x-auto` 计数断言(`DocSidebar.vue:25` 的 `<nav>` 是合法既存用例,基线 = 1)。
|
||||||
|
4. e2e:短数据下 `/admin/users`、`/admin`、`/admin/audit` 在 320/375px 零溢出,且表格**仍可横向滚动到**(断言包裹层 `scrollWidth > clientWidth` 时内容可达——修掉溢出不能靠藏内容)。
|
||||||
|
|
||||||
|
### T3(ResultMeta + AccountView,缺口 B/C)
|
||||||
|
1. 既有 `e2e/author-page.noauth.spec.ts:12` 的 `${count} 款作品` 断言全绿(只改类名,不动文本)。
|
||||||
|
2. 既有 `app/views/CatalogView.test.ts` 全绿。
|
||||||
|
3. e2e:`/games` @320px 零溢出;`/account` 在 60 字名下 320/375/768px 零溢出。(**归因更正**:spike 17 2×2 消融实测,768px 的 +40px 由缺口 A 页头驱动、非本项 dd——回退 dd @768 仍 `over=+0`;dd 修复的牙在 320/375px,回退 dd @320 → `scrollWidth=592`。故 768 腿钉的是**页头回归**,320/375 腿才是本项 dd 的牙口。)
|
||||||
|
|
||||||
|
### T4(机器守卫,D-H)
|
||||||
|
1. 新增 `e2e/responsive.spec.ts`,**必须进 CI 主腿**(`playwright.config.ts` `testIgnore: /noauth\.spec\.ts/` 不含它):
|
||||||
|
- 视口 320 / 375;名字形态 短名(`安`)/ 60 字 ASCII / 60 字 CJK;登录态 登出 / user / admin。
|
||||||
|
- 路由 `/`、`/games`、`/docs`、`/creator`、`/login`、`/register`、`/account`、`/admin/users`、`/admin`、`/admin/audit`(后四个需 admin seed + 最小 mock,沿用 `a11y.spec.ts:26` 与 `admin-flow.spec.ts:30-56` 的端点范式;表格由 `v-else` 门控,**mock 必须返回至少一条数据**否则 `<thead>` 不渲染、守卫空跑)。
|
||||||
|
- 每格断言 `document.documentElement.scrollWidth <= clientWidth + 1`。
|
||||||
|
- 另断言:60 字名下 `header details > div` 菜单 `right <= innerWidth`(缺口 E 钉桩);`<summary>` 的 `▾` 仍**可见**(防止将来有人用「整体 truncate」把 caret 吞掉——spike 15 实测那条路 `caret=HIDDEN`)。
|
||||||
|
- `seedSession` 的 `display_name` 硬编码为 `role`(`helpers.ts:37`),长名场景需**扩展可选参数**(`seedSession(page, role, displayName?)`,默认值保持 `role` 以免动既有 21 个 spec)或在 `responsive.spec.ts` 内自带 seed——二选一,实现者定,但**不得**改既有调用点的行为。
|
||||||
|
- 组合数控制:不必跑满 3×3×10 全矩阵(会拖慢 CI)。至少覆盖 {320,375} × {60字ASCII, 短名} × {全部 10 路由} + {375} × {60字CJK} × {`/`, `/account`, `/admin/users`}。总时长目标 ≤ 90s(参考:既有主 e2e 81 测试 1.1min)。
|
||||||
|
2. 守卫**必须有牙**:实现者须在本地临时 revert 任一修复(如把 `relative` 去掉、或把 `max-w-[6rem]` 去掉),确认 `responsive.spec.ts` / `tableOverflow.test.ts` 变红,再恢复;报告里贴红/绿两次输出。这是 P11 mutation 抽查纪律的落地。
|
||||||
|
|
||||||
|
### T5(打磨项)
|
||||||
|
1. (a) `router/index.test.ts` 全绿,且新断言不经 router(版本无关)。
|
||||||
|
2. (b) `GameReactions.test.ts` 新增两态可访问名断言;既有 `e2e/a11y.spec.ts:61` 不破(实跑确认)。
|
||||||
|
3. (c) `ToastHost.test.ts` 既有六处断言全绿 + 新增「关掉最新条不重念旧消息」;`__resetToasts()` 隔离实测有效。
|
||||||
|
4. (d) server `go test ./internal/config/ -run TestApplyLogging -v` 绿;`gofmt -l .` 空、`go vet ./...` 0。
|
||||||
|
5. (e) deploy 守卫脚本本地实跑三态:当前通过 / `TRUSTED_PROXIES=172.28.0.0/24` 注入 `.env` 时**失败** / 覆盖 `10.0.0.0/8` 时通过。四 profile `config -q` 仍全绿。
|
||||||
|
|
||||||
|
### 全量回归(合并前必跑)
|
||||||
|
- crearte:`npm run test`(vitest,基线 **626** + 本批新增)、`npm run typecheck`、`npm run build`、`npm run build:runtime`、`npm run e2e`(基线 **80+1skip** + 新增 responsive)、`npm run e2e:noauth`(基线 **4**)。**脚本名先查 `package.json`**——P11 曾把 `test`/`typecheck` 写成 `test:unit`/`type-check` 配 `--if-present` 导致假绿。
|
||||||
|
- server:容器化 `gofmt -l .` / `go vet ./...` / `go build ./...` / `go test ./... -count=1`(基线 11 包 ok)。
|
||||||
|
- deploy:四 profile `config -q` + 卷名集合与基线逐字相同。
|
||||||
|
- 管道后取退出码一律 `set -o pipefail` 或不管道(P11 三度踩坑)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. CHANGELOG 与版本
|
||||||
|
|
||||||
|
- `crearte`:**0.25.0**(Fixed 段记缺口 A/B/C/D/E 四缺陷 + 下拉菜单逃逸;Added 段记两层机器守卫;Changed 段记打磨三项 P9B-1/2/4)。
|
||||||
|
- `crearte-server`:**0.17.1**(Tests 段记 P11-N5 错误消息回显钉桩;纯测试,patch 级)。
|
||||||
|
- `crearte-deploy`:**0.7.1**(CI 段记 P11-N6 `TRUSTED_PROXIES` 空默认机器守卫;纯 workflow,patch 级)。
|
||||||
|
- wrapper:**0.3.5**(Done 段记 P12 交付;ROADMAP P12 行 + 文档索引表补 spec/plan 两行;同时把「移动端页头/汉堡菜单」挂账标注为**已证伪删除**、D4 标注为 **park 待设计决策**)。
|
||||||
|
|
||||||
|
条目格式沿用 AGENTS.md:同条目英文行紧跟中文行(无空行),不同条目间空行。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 证据存档
|
||||||
|
|
||||||
|
`crearte/.superpowers/sdd-p12/`(`.gitignore` 已含 `.superpowers/`,不入库):
|
||||||
|
|
||||||
|
- `FINDINGS-survey.md` — **16 轮 spike 的实测输出固化**:四轮基线审计与「移动端页头」证伪、D1a 发现与范围扩张(三渲染点)、D3 独立性证明(短数据)、修法候选对照(含两个实验缺陷:Q1 误抓 sr-only `<h2>`、Q2 被 D1 污染)、残差归因三轮(矛盾 → 逐层探测 → **烧蚀定案 sr-only span**)、H1/H2 机制验证 + H3 24 格全绿矩阵、D4 七候选全灭 → park、D1a 精确形态定案(Vue 单文本节点导致 V2–V4 失效 → flex F3 胜出)。每个数字都有出处。
|
||||||
|
- 本 spec §1/§2 的所有实测值均引自该文件;spike 源文件跑完即删(工作树 `porcelain=0` 已核),不入库。
|
||||||
Reference in New Issue
Block a user