Files
crearte-monorepo/docs/plans/2026-10-03-p15a-approve-phases.md
T
XingfenD 82f0a6d360 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.
2026-10-03 13:28:33 +08:00

124 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# P15-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 立刻不一致)