docs: add P15-A and P15-B specs and plans, register both in ROADMAP
Two batches, one per repo, so they can run in parallel under the one-writer-per-repo rule: P15-A refactors crearte-server's Approve function, P15-B fixes a dark-mode gap in crearte's game loading screen plus two guard items. P15-A splits a 169-line function whose seven phases are mapped here with real line numbers — the base-tree mapping logged during P14 is void, since that batch moved the function into moderation.go. The spec's central constraint is that phases four and six look alike but must not be merged: four is an optimistic pre-check that runs unlocked before any object copy, six is a pessimistic re-check under a row lock after the copies have happened. They differ in four concrete ways (kind coverage, the NewVersion runtime and bundle checks, whether entities are created, and the error wrapping), so the apparent duplication is deliberate TOCTOU defence rather than removable redundancy. A measurement also overturned the survey's claim about helper shape: both an unexported method and a package-level function leave all seven P14 assertions green, because IsExported filtering and NumMethod's exported-only view put neither in scope. The choice is therefore about needing receiver fields, not about the guard. P15-B fixes a gap P13 left: its token work covered only the src/index.html entry and missed the second Vite entry, bootstrap, so the game loading screen hardcodes light values and dark-theme users see a white flash on every virtual game launch. The fix rides the hash channel bootstrap already parses rather than inventing a postMessage protocol, which would be async and so repaint light first — the very flash being removed. The spec records the trap that makes this easy to get wrong: GameHost builds targets in a computed, so injecting the reactive theme ref would make a theme switch recompute the iframe src and reload a game in progress; the non-reactive snapshot is required, and a guard leg plus a mutation exist solely to hold that line. It also records two pre-existing contrast violations found on the way, where inline style attributes override conforming stylesheet values with lower-contrast ones, and why noHardcodedColor cannot be extended to cover this file: it only collects .vue and blanks out style blocks, so widening its roots would scan nothing while looking like coverage. Every line number and quoted signature in both specs was checked against source before commit; two claims the survey had asserted from reasoning were measured instead, and one of them was wrong.
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
# P15-A 实现计划:拆分 `Approve` 169 行七阶段
|
||||
|
||||
- **spec(权威)**:`docs/specs/2026-10-03-p15a-approve-phases-design.md`
|
||||
- **取证账本**:`crearte-server/.superpowers/sdd-p15a/survey.md`(控制者派发前落盘;P15 前期取证在 `crearte/.superpowers/sdd-p15/survey.md` §8.1)
|
||||
- **仓**:`crearte-server`(**仅此一个**)· **base** `fe810dd`(0.18.0,master)· 分支 `chore/p15a-approve-phases`
|
||||
> ⚠️ 分支前缀用 `chore/`:AGENTS.md 只允许 `{feat|fix|docs|chore}/` 四种(**`refactor/` 不允许**,P14 控制者曾写错)。纯结构重构归 `chore/`。
|
||||
> ⚠️ inner repo 红线:**不得在 `master` 上提交**。`git commit` 前先 `git branch --show-current` 确认。
|
||||
- **性质**:纯结构重构,**零行为变更**(spec D-G)
|
||||
- **目标版本**:crearte-server **0.19.0** · wrapper **0.3.8**
|
||||
- **派发**:**一个**实现者子代理做完 T1→T3(所有任务同动单一工作树与 git index,`sdd-parallel-dispatch` §1 一仓一写者)
|
||||
- **并行**:P15-B 在 `crearte` 仓同时进行 → **不得触碰 `crearte/`、`crearte-deploy/`、wrapper 的任何文件**
|
||||
|
||||
> spec 与本计划冲突时 **spec 赢**。spec 全文(§0–§8)必读,尤其 **§1.2(④⑥不同形,禁止合并)**、§1.3(helper 形态实测)、§2 决策表 D-A…D-I、§3.2(逐阶段要求,含 6 处"逐字保留")、§5-T1(mutation a–h 及 (b) 的已知盲区警告)、§7 风险表、§8 纪律 10 条。
|
||||
|
||||
---
|
||||
|
||||
## 任务表
|
||||
|
||||
| 任务 | 交付物 | 要求 | 完成判据 |
|
||||
|---|---|---|---|
|
||||
| **T1** | `internal/service/function_length_test.go`(**新增文件**,不改 `architecture_test.go`) | **RED 先行**(spec §5-T1)。三条断言:① `TestNoOversizedFunction`:`service/` 内非测试 `.go` 无任何函数 >100 行(阈值依据见 spec §0 普查:当前只有 `Approve` 169 超过,次大 83)② `TestApproveIsSmall`:`Approve` <60 行 ③ `TestApproveHelpersExist`:五个 helper 名存在——**必须用源码文本匹配,不能用 reflect**(spec §1.3:未导出标识符 reflect 不可见)。断言内须含"至少扫到 N 个 `.go` 文件"的前提检查(spec §5 mutation (e) 的教训) | RED 输出**逐字存证** `.superpowers/sdd-p15a/impl-evidence/t1-red.txt`,且**红的原因是断言失败而非编译错误**(与 P14 T1 的编译红形态**不同**:本批引用的都是既有类型)。三条各自的红须可分辨 |
|
||||
| **T2** | `internal/service/moderation.go`(改) | 按 spec §3.1 骨架 + §3.2 逐阶段要求执行拆分:`loadApprovePlan`(①②③) / `precheckApprove`(④) / `copyApprovalObjects`(⑤,**收 `*approvePlan`**) / `applyApprovalInTx`(⑥,**包级函数**收 `tx repository.ContentRepository`) / `cleanupApprovalUploads`(⑦) + 包级私有 struct `approvePlan`(spec D-B 七字段)。**六处"逐字保留"**见 spec §3.2 的 ⚠️ 标注 | `go test ./... -count=1` **11 包全绿**;**既有 `*_test.go` 修改数 = 0**;`architecture_test.go` **修改行数 = 0**;P14 七断言 `-v` 全 PASS |
|
||||
| **T3** | 字节级保真证明(加做项,P14 同款) | 写脚本对 base `fe810dd` 的 `Approve` 函数体与新树的 `Approve`+5 helper 做**语句级比对**(归一化缩进与接收者前缀),产出 `verbatim` / `changed`(逐条给理由)/ `missing`(**必须 0**) 三张清单。**脚本的三个已知坑见 spec §5-T3 的 ⚠️**(字符串字面量里的 `//` 被当注释、单行 `var` 须提前终止、意外值先怀疑自己的模式) | 三份清单存证 `.superpowers/sdd-p15a/impl-evidence/t3-verbatim.txt`;`missing = 0`;`changed` 每条有理由且都落在 spec §3.2 允许的范围内 |
|
||||
|
||||
---
|
||||
|
||||
## 验收(控制者独立复跑,不采信自报)
|
||||
|
||||
### 四门(dockerized Go,AGENTS.md 硬红线)
|
||||
|
||||
```bash
|
||||
cd crearte-server && docker run --rm -v "$PWD/src:/src" -w /src \
|
||||
-v crearte_gomod:/go/pkg/mod -e GOCACHE=/gocache -v crearte_gocache:/gocache \
|
||||
-e GOPROXY=https://goproxy.cn,direct -e GOSUMDB=sum.golang.google.cn \
|
||||
golang:1.24-alpine sh -c 'gofmt -l . ; go vet ./... ; go build ./... ; go test ./... -count=1'
|
||||
```
|
||||
|
||||
基线(spec §0,已实测):`gofmt -l .` 空 · `go vet` exit 0 · `go build` exit 0 · `go test ./... -count=1` **11 包 ok**(api ~11.0s 真库集成、service ~3.7s)。
|
||||
|
||||
⚠️ **host Go 1.18 不可用**;**必须带 `GOPROXY=https://goproxy.cn,direct`**(`proxy.golang.org` 本机不可达,默认下载**挂死并杀代理**);冷构建 1–3 min → `background: true` + 耐心 poll,**不要 sleep 循环、不要因没立刻返回就断定失败**。
|
||||
⚠️ **`-count=1` 强制实跑**(禁缓存)。
|
||||
⚠️ **落盘证据用 `setsid nohup` 脱离 + 宿主侧重定向**(容器只挂 `/src` 写不了 `.superpowers/`;exec session 一断容器就被杀)。
|
||||
⚠️ **`grep -c` 输出 0 时 exit 1**,放 `&&` 链里会静默中断后续步骤(含 `git commit`)。
|
||||
⚠️ **`pkill -f <pattern>` 会匹配到自己的命令行**(P14 控制者把自己 SIGKILL)→ 用 `[p]attern` 括号技巧。
|
||||
|
||||
### 结构指标(spec §5 表,交付时须报实测数字,**一律 `wc -l` 口径**)
|
||||
|
||||
| 指标 | 基线 | 目标 |
|
||||
|---|---|---|
|
||||
| `Approve` 行数 | **169** | **< 60** |
|
||||
| `service/` 包内 >100 行的函数数 | **1** | **0** |
|
||||
| `service/` 包内最大单函数行数 | **169** | **< 100**(次大者 83,故落点应在 83–100) |
|
||||
| `moderation.go` 行数 | 298 | **不作指标**(spec D-H:helper 同文件,可能不降反升;报了即可) |
|
||||
| 既有 `*_test.go` 修改文件数 | — | **0** |
|
||||
| `architecture_test.go` 修改行数 | — | **0** |
|
||||
| `Approve` 签名 | `(ctx context.Context, adminID, submissionID, note string) error` | **逐字不变** |
|
||||
| 字节级比对 `missing` | — | **0** |
|
||||
|
||||
> ⚠️ **不要用 `len(text.split('\n'))` 数行数**:文件以换行结尾时它多算一个空段。P14 F5 就因此让控制者报出 83/299 而真值是 82/298,被任务级审查者抓到。`wc -l` 是本仓钉死的口径(P14 spec §5 RE-PIN)。
|
||||
|
||||
### mutation 抽查(spec §5-T1 的 a–h,控制者独立复现,不复用实现者结果)
|
||||
|
||||
**八条**,每条测"预期红 + 其余绿 + **非编译红** + 恢复证明":
|
||||
|
||||
| # | mutation | 预期 |
|
||||
|---|---|---|
|
||||
| (a) | helper 存在但 `Approve` 不调它、改为原地展开 | `TestApproveIsSmall` 红 |
|
||||
| (b) | 构造一个 **>100 行**的 helper(⚠️ spec §5 已警告:把⑥整体 80 行搬进 `applyApprovalInTx` **不会红**,那是阈值 100 的已知盲区,不是守卫失效) | `TestNoOversizedFunction` 红 |
|
||||
| (c) | 删掉一个 helper(内容合并进 `Approve`) | `TestApproveHelpersExist` 红 |
|
||||
| (d) | helper 改名(如 `loadApprovePlan`→`loadApproveInputs`) | `TestApproveHelpersExist` 红(**(c) 的对照**:证明钉的是名字集合而非"存在任意 5 个函数") |
|
||||
| (e) | 扫描范围改成空目录 | 红(防"扫 0 文件报 0 违规") |
|
||||
| (f) | 阈值 100 → 1000 | 红(防阈值被放宽;断言须自证阈值字面量) |
|
||||
| (g) | **给 `ModerationService` 加字段**(`clock func() time.Time`) | **P14 断言 6 `TestLineFields` 红**(跨批回归) |
|
||||
| (h) | **在 `ContentService` 上加方法** | **P14 断言 5 红**,且**须同时测有名与匿名两种接收者形态**(P14 FR-1 教训:只测有名会漏掉 `func (*ContentService) M()`) |
|
||||
|
||||
⚠️ **`restore()` 只回滚 mutation 触及的文件,绝不用 `git checkout -- .`**(P14 控制者第 24 次自伤:全量回滚抹掉了未提交的守卫编辑,导致后四轮全在测没有修复的树、汇总报"绿=3")。每轮恢复后核 `git diff HEAD --exit-code -- <file>` 空 + 守卫文件 `func Test` 计数不变。
|
||||
⚠️ **mutation 若产生编译错误,不算"守卫有牙"**(P14 控制者第 22 次错误:正则截断签名致编译红,却被报成"守卫拦住了遮蔽")。
|
||||
|
||||
### 行为不变的额外证据
|
||||
|
||||
- **既有 13 个测试函数就是行为守卫**(spec §1.6 表):`TestApproveConcurrentOnlyOneWins`(⑥行锁+重校)· `TestApproveSameWorkIDConcurrent`(④⑥的 `ErrWorkIDTaken`,断言文本含 `want ErrWorkIDTaken or ErrSubmissionConflict`)· `TestApproveMetadataChangeFeaturesSemantics`(⑥ 的 `Features` **三态语义**,用 `meta-absent` 与 `,"features":{}` 两个 payload 精确区分)· `TestApproveOptionalFieldsRoundTrip` · `TestMetadataChangeRuntimeImmutable` · `TestNewVersionMetadataChangeOwnership` · api 层 5 个 + 2 个真库发布链
|
||||
- **错误文案逐字比对**:`service: load submission: %w` / `service: resolve submitter: %w` / **`service: approve precheck: %w`(两处,只在④)** / `service: copy bundle: %w` / `service: copy cover: %w`;以及 `ErrContentNotFound`(①) vs **`ErrSubmissionConflict`(⑥ 的 `GetByIDForUpdate` 未找到)** 这个**刻意的差异**(spec §3.2 ⑥)
|
||||
- **两条 slog 逐字**:`slog.Error("approve: pending delete failed", "key", key, "error", err)`(⑦ helper 内)· `slog.Info("approve", "submission", …, "work", …, "kind", …, "admin", …)`(**留在 `Approve` 本体**,spec §3.2 ⑦)
|
||||
- **`Approve` 只有一个非测试调用点** `handler/admin.go:62`(spec §1.7)→ helper 无须导出;签名变更会打破 `moderationPort` 满足性 → `serve.go` 编译红
|
||||
|
||||
---
|
||||
|
||||
## 边界(**不做**,spec §4)
|
||||
|
||||
- **不合并④⑥的 switch**(spec §1.2 四处实质差异 + 乐观/悲观语义 + 错误文案不同;被否方案 A/B)
|
||||
- 不动 `Reject`/`Unpublish`/`Republish`/`UpdateWorkFeatures`/`ListQueue`(同文件其它方法)
|
||||
- 不动 `deleteAccountLocked`(83)/`UpdateSubmission`(76)/`ValidateWorkFields`(75)/`ImportDir`(70)/`Run`(70)/`checkSubmissionRules`(66) —— 六个 >60 行函数各自独立成批(spec §4 挂账)
|
||||
- 不新增/修改任何测试(既有测试即守卫,spec §2.1 被否方案 D)
|
||||
- **不改 `architecture_test.go`**(P14 七断言)——若某条因新 helper 变红,**停下来报告**,不自行改守卫
|
||||
- 不碰 `docs/`、`go.mod`/`go.sum`(实现者红线;记账由控制者做)
|
||||
- 不碰其它三个仓
|
||||
|
||||
---
|
||||
|
||||
## 记账(控制者做,实现者禁动 `docs/`)
|
||||
|
||||
- `crearte-server/docs/CHANGELOG.md` 新增 **`## [0.19.0] - 2026-10-03`**(插 `## [0.18.0]` 前)
|
||||
- wrapper `docs/CHANGELOG.md` 新增 **`## [0.3.8] - 2026-10-03`**(插 `## [0.3.7]` 前),小节 `### Done / 完成`;**与 P15-B 合并为一条还是分两条按合并时序定**
|
||||
- `docs/ROADMAP.md`:第三波表新增 **P15-A 行** + 文档索引表新增一行(**哈希引 merge commit**,P12/P13/P14 惯例)
|
||||
- **挂账新增**:六个 >60 行函数 · `TestNoOversizedFunction` 阈值 100 的已知盲区(80 行 helper 不被拦)
|
||||
- 格式:**同条目英文行紧跟中文行、无空行**;不同条目**空一行**;小节标题双语(`### Changed / 变更`、`### Tests / 测试`、`### Context / 背景`)
|
||||
- **纪律**:显式 `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. **控制者独立核实**(全部自己跑,不采信自报数字;P14 控制者因此抓到实现者报告可信度高、也抓到自己 6 处错)
|
||||
3. **任务级审查者**(只读,**自设 mutation 不照抄 spec 的 a–h**,审守卫恒真/恒假、跨任务缝隙、spec 自身问题)
|
||||
- P13 先例:10 条自设 mutation 挖出 5 条 findings,**M1 抓到控制者自己写错两次的恒真守卫**
|
||||
- P14 先例:抓到 **spec D-J 的守卫目的无断言覆盖** + **spec 里 positive control 的绕行路径** + 控制者的行数口径错
|
||||
4. **控制者逐条裁定**(`sdd-pre-merge-review` §3:**未裁定的 note 阻塞合并**,"PASS with notes" 不等于门过了)
|
||||
5. **全分支终审者**(只读;spec 全文自洽性 + **审控制者的裁定工作** + 独立复现 mutation)
|
||||
- P11 先例:终审裁定过「**需修复后合并**」→ **不是橡皮章**
|
||||
- P14 先例:终审在**第一轮修复自身**里找到洞(匿名接收者绕过源码扫描),并**推翻控制者两处已入库的全称声明**
|
||||
6. **spec re-pin**(裁定后一次性批量改,**勿在审查者读 spec 期间改**=移动靶);**代码改了 spec 必须同步**(P14:正则字面量写进 spec,改代码后 spec 立刻不一致)
|
||||
@@ -0,0 +1,133 @@
|
||||
# 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 自查 | 按 spec §5-T4:`npm run build` exit 0(⚠️ 它会类型检查 `e2e/*.spec.ts`,P13 spike 6 曾因此 `BUILD_EXIT=2`);`npm run e2e` **≥102+1skip** 不回退(`dark.spec.ts` 10 腿全绿);**新增一腿覆盖 hash 注入**(e2e 断言 iframe `src` hash 含 `theme=dark`;**若 fixture 无虚拟游戏可跑,改为 vitest 腿**用 `@vue/test-utils` 挂 `GameHost` 断言 `targets[0].url` 含 `theme=dark`——**必须有一腿覆盖**,不接受"fixture 不支持所以不测");**产物验证**:`dist/` 里 bootstrap HTML 含暗色块、**不含** `#a3a3a3` 与 `#f87171`,主应用 CSS 里 `--color-info` 零命中、`var(--color-*)` 计数与 P13 交付值 **75** 比对并**解释差异**;**mutation (a)–(j) 逐条**附输出 | 全部输出存证 `.superpowers/sdd-p15b/impl-evidence/`;产物 grep **用 `grep -F`**(spec §8-6:手写转义与带引号模式对 minified 产物假阴性) |
|
||||
|
||||
---
|
||||
|
||||
## 验收(控制者独立复跑,不采信自报)
|
||||
|
||||
### 测试与构建
|
||||
|
||||
```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 立刻不一致,构成"需修复后合并"的同类形态)
|
||||
Reference in New Issue
Block a user