Files
crearte-monorepo/docs/CHANGELOG.md
T
XingfenD dd5d0fabf9 docs: book P15-A as delivered (wrapper 0.3.8, ROADMAP rows + third-wave table)
P15-A merged to crearte-server master as e70e99b (0.19.0, merge-base fe810dd,
eight commits on the branch). Both review rounds came back with notes and both
caught the controller's own verification method rather than the refactor: the
gofmt gate in the shared four-gate recipe was exit-code blind, and identifier
counting cannot prove behaviour is unchanged (whole files rather than function
bodies, a hand-picked list read as a universal claim, and no visibility into
control flow at all).

The branch review's most valuable contribution was an evidence layer all four
earlier runs had silently skipped: the five real-database guards the spec names
were SKIPPED everywhere because nobody set TEST_DATABASE_URL, and the ~11s
internal/api timing reported as "including real-database integration" was the
mock path. Running postgres against both trees gave base 194 PASS versus HEAD
199 PASS with identical state multisets - the first runtime proof of unchanged
behaviour on the production database path.

Five of its notes concerned controller artefacts, not code. Four were spec
staleness introduced during re-pin, every one from writing executable details
from memory instead of grepping them out of the code; the fifth was the
fake-rigour form of the vacuous-assertion defect this project keeps finding - a
verification script printing nineteen [OK] lines with no checking logic behind
them while citing an output file that was never archived. Both are now fixed in
37a0a8d and 9061c39, and the lesson is a spec discipline: executable details in
a spec must be grepped from the code entity and pasted, never hand-written.

ROADMAP also gains a correction of my own omission: P15-A and P15-B were
registered in the document index but never added to the third-wave status
table, which still ended at P14.

P15-B is deliberately excluded from this entry. Its branch review returned
"needs fixing before merge" - the second time a branch review has blocked a
batch after P11 - on a critical gap in the guard layer: maskSourceComments()
compares a two-character slice against the four-character '<!--', so its HTML
comment branch is dead code. A second fix-forward is in flight; P15-B books as
0.3.9 when it merges.
2026-10-04 02:42:03 +08:00

189 lines
95 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.
# Changelog / 更新日志
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.
本文档参考了 Keep a Changelog 的思路,也可以根据团队习惯调整。
## [0.3.8] - 2026-10-04
### Done / 完成
- P15-A splits `Approve` in crearte-server (0.19.0, merge `e70e99b`, merge-base `fe810dd`, 8 commits on the branch): a 169-line function body becomes a 24-line orchestrator plus eight named helpers, and the batch's only acceptance criterion — zero behaviour change — is evidenced at statement level (of 162 statement lines, 149 verbatim, 13 changed with a registered reason each, 0 missing, 0 order violations) rather than by counting identifiers. Two new guard files land: a function-length guard whose threshold of 100 rests on a survey of all 78 functions in the package (`Approve` at 169 was the only one above it, the next largest being `deleteAccountLocked` at 83), and a phase-independence guard pinning that the optimistic precheck and the pessimistic in-transaction recheck stay separate — the property the length guard structurally cannot see.
- P15-A 在 crearte-server 拆分 `Approve`(0.19.0,merge `e70e99b`,merge-base `fe810dd`,分支 8 个 commit):169 行的函数体变成 24 行编排体加八个具名 helper,而本批唯一验收判据——零行为变更——用语句级证据支撑(162 条语句行中 149 逐字保留、13 条变更且每条有登记理由、0 缺失、0 乱序),不是靠数标识符。落地两个新守卫文件:函数长度守卫(阈值 100 的依据是全包 78 个函数的普查——169 行的 `Approve` 是唯一超标者,次大的 `deleteAccountLocked` 是 83 行),以及钉住「乐观预检与事务内悲观重检保持分离」这个阶段独立性守卫,而那正是长度守卫在结构上看不见的性质。
- **The review ladder again produced the batch's main result rather than the refactor: both rounds caught the controller's own verification method.** Task-level review (PASS with notes, 12 findings from 18 self-designed mutations) proved two things. First, the gofmt gate in the shared four-gate recipe was exit-code blind — `gofmt -l .` *lists* unformatted files and still exits 0 — so the reported "gofmt clean" evidenced nothing beyond the absence of parse errors; controller and implementer used the same recipe, which is why one blind spot was computed twice and caught by neither. The recipe is now `test -z "$(gofmt -l .)"` with the raw output retained. Second, "ten identifiers count the same" cannot prove behaviour is unchanged: it counted whole files rather than function bodies, its hand-picked list carried no completeness argument yet read as a universal claim, and counting cannot see control flow at all — the reviewer's counterexample moves `GetByIDForUpdate` from phase six into phase four, leaving every count and every error string identical while destroying the TOCTOU layering, and all three shipped guards stayed green on it.
- **审查阶梯再次把本批的主要产出放在了重构之外:两轮都抓到了控制者自己的核实方法。** 任务级审查(PASS with notes,18 轮自设 mutation 挖出 12 条 findings)证明了两件事。第一,共用四门配方里的 gofmt 门是退出码盲的——`gofmt -l .` **列出**未格式化文件而仍然 exit 0——所以报出的「gofmt 干净」除了「没有解析错误」之外什么也证明不了;控制者与实现者用的是同一个配方,这正是同一盲点被复算两次而谁都没发现的原因。配方现在改为 `test -z "$(gofmt -l .)"` 并保留原始输出。第二,「十个标识符计数相同」证明不了行为未变:它数的是整文件而非函数体,手挑的清单没有完备性论证却被读成全称声明,而计数根本看不见控制流——审查者的反例把 `GetByIDForUpdate` 从阶段六搬进阶段四,每个计数与每条错误文案都不变,而 TOCTOU 分层被摧毁,三条既有守卫在它面前全绿。
- Branch-level review returned **APPROVE with notes**: the merged branch code has zero defects, and all eleven of its findings were adjudicated before merge (five fixed, three ledgered with measured reasons, two fixed as spec/evidence corrections, one accepted with reason). Its most valuable contribution was an evidence layer all four earlier runs had silently skipped — the five real-database guards the spec names, two of them concurrency/TOCTOU tests, were SKIPPED everywhere because nobody set `TEST_DATABASE_URL`, and the ~11s `internal/api` timing that had been reported as "including real-database integration" was the mock path. Running `postgres:16-alpine` against both trees gave base 194 PASS / 0 FAIL versus HEAD 199 PASS / 0 FAIL (+5 being exactly this batch's new guards) with identical state multisets: the first runtime proof of unchanged behaviour on the production database path, where everything before it had been static.
- 全分支终审返回 **APPRO with notes**:被合并的分支代码零缺陷,11 条 findings 在合并前逐条裁定(5 条已修、3 条带实测理由挂账、2 条作为 spec/证据更正已修、1 条 accepted with reason)。它最有价值的贡献是补上了此前四轮跑批都静默跳过的一层证据——spec 点名的 5 个真库守卫(其中两个是并发/TOCTOU 测试)在每一轮里都 SKIP,因为没人设 `TEST_DATABASE_URL`;而那个被报成「含真库集成」的 `internal/api` ~11s 其实是 mock 路径耗时。用 `postgres:16-alpine` 对两棵树各跑全量得 base 194 PASS / 0 FAIL vs HEAD 199 PASS / 0 FAIL(+5 恰为本批新增守卫),状态多重集完全一致:这是「行为未变」在生产数据库路径上的第一份运行时证明,此前所有证据都是静态的。
- Five of its notes were about the controller's own artefacts rather than the code. Four were spec staleness introduced during re-pin, all from writing executable details from memory instead of grepping them out of the code: `applyApprovalInTx`'s parameter order in three places, two migration ranges the re-pin claimed to have fixed but had not, a difference-table header still carrying pre-split line numbers, and a needle line number that was pre-fix plus an arithmetic error (126–200 is 75 lines, not 77 — 77 belongs to the 125–201 range and was carried over). The fifth was the fake-rigour form of the vacuous-assertion defect this project keeps finding: the controller's verification script printed nineteen `[OK]` lines with **no checking logic behind them** while citing an output file that was never archived, so its "0 FAIL" was true but not self-supporting. It was rebuilt as 106 real checks read from code and spec entities, with the cited output archived beside it. The lesson is now a spec discipline: executable details in a spec — regexes, signatures, literals, line ranges — must be grepped from the code entity and pasted, never hand-written.
- 它的 5 条 note 针对的是控制者自己的工件而非代码。4 条是 re-pin 期间引入的 spec staleness,根因都是凭记忆写可执行细节而不是从代码里 grep 出来:`applyApprovalInTx` 的形参顺序 3 处、两处 re-pin 声称已修而实际未修的搬迁区间、一处仍带着拆分前行号的差异表表头、以及一个 pre-fix 的 needle 行号加一处算术错(126–200 是 75 行不是 77 行——77 属于 125–201 那个区间,被顺手带了过来)。第 5 条是本项目反复发现的「空断言」缺陷的假严谨形态:控制者的复验脚本打印了十九条 `[OK]` 而**背后没有任何校验逻辑**,同时引用了一个从未归档的输出文件,所以那句「0 FAIL」结论虽真却不自足。它已重建为 106 条从代码与 spec 实体读出的真校验,被引用的输出也归档在旁边。教训现在写成 spec 纪律:spec 里的可执行细节——正则、签名、字面量、行号区间——必须从代码实体 grep 出来粘贴,不得手写。
- One finding is ledgered rather than fixed: P14's assertion five, which scans for method declarations on `ContentService`, is bypassed by a **type-alias receiver** (`type X = ContentService` plus a method on `*X`). The shadowing genuinely takes effect through the composition root while a regex identical to the shipped one reports zero hits, with all ten guards plus vet and build green. It is pre-existing, fixing it means editing a file this batch is forbidden to touch, and HEAD has zero such aliases so the hole is not currently exploited. It also overturns that file's claim to be the "only mechanical means" of detecting shadowed promoted methods — the third universal claim overturned by measurement in three batches, after P14's "only means" and the `architecture_test.go` completeness claim.
- 一条 finding 挂账而非在此修:P14 的断言五(扫描 `ContentService` 上的方法声明)被**类型别名接收者**绕过(`type X = ContentService` 加上 `*X` 上的方法)。遮蔽确实经组合根生效,而与既有断言逐字相同的正则报零命中,十条守卫加 vet 与 build 全绿。它是既有问题,修它要动本批禁触的文件,且 HEAD 上此类别名为 0 处故该洞当前未被利用。它还推翻了那个文件自称是侦测遮蔽提升方法的「唯一机械手段」——这是三批之内第三条被实测推翻的全称声明,前两条是 P14 的「唯一手段」与 `architecture_test.go` 的完备性声称。
- Spec work landed in three commits: `02188eb` (24 corrections across both specs and plans after task-level adjudication), `37a0a8d` (the four staleness notes), `9061c39` (the two advisories — the four-gate line now states that its timings are the mock path and lists all 11 `TEST_DATABASE_URL`-gated tests by name, and the archived differ script carries a note explaining that its 6 ORDER VIOLATIONS are a greedy `pop(0)` artefact while the prose conclusion of 0 is correct). Ledger entries added: six Go functions still over 60 lines; the threshold-100 blind spot restated as "any merge form at or under 100 lines" (merging phases four and six measures 91 lines and does not redden); the mirror blind spot where inlining one phase leaves every guard green; escape via any non-`func` form of arbitrary length (a 125-line package-level closure passes all four gates and all three length guards); three guard constants lacking self-attestation; the gofmt gate's exit-code blindness, which affects every batch's recipe; the type-alias bypass; a DB-enabled comparison added to the pre-merge checklist; and three guard-comment clarifications deferred with the minors above.
- spec 工作落在三个 commit:`02188eb`(任务级裁定后对两份 spec 与 plan 共 24 处更正)、`37a0a8d`(4 条 staleness note)、`9061c39`(2 条 advisory——四门那一行现在写明其耗时是 mock 路径并按名列出全部 11 个受 `TEST_DATABASE_URL` 门控的测试;归档的 differ 脚本加注说明它输出的 6 个 ORDER VIOLATION 是贪心 `pop(0)` 的假象、而散文结论 0 是对的)。挂账新增:6 个仍超过 60 行的 Go 函数;阈值 100 的盲区重述为「≤100 行的任何合并形态」(合并阶段四与阶段六实测 91 行、不红);内联一个阶段时所有守卫全绿的镜像盲区;任意长度的非 `func` 形态逃逸(125 行的包级闭包通过全部四门与三条长度守卫);3 个守卫常量缺自证钉;gofmt 门的退出码盲——它影响**每个**批次的配方;类型别名绕过;合并前清单新增 DB-enabled 对照;以及随上述 minor 一并延后的 3 处守卫注释澄清。
- **P15-B is not part of this entry.** Its branch review returned 🔴 **needs fixing before merge** (the second time a branch review has blocked a batch, after P11): the product code is correct and all four gates are green, but the guard layer has a critical gap — `maskSourceComments()` compares a 2-character slice against the 4-character `'<!--'`, so its HTML-comment branch is dead code and never executes. Five measured consequences follow (two false greens where a fake palette block hides in an HTML comment, three false reds where merely rewording a comment reddens up to four legs while the e2e suite stays 9/9 green), and it overturns two claims already committed to the spec and the adjudication: that the guard "does not depend on the implementer's wording discipline" and that the fix for the earlier critical finding was safe because comment masking had neutralised the trap comment — true for that file, but only because the trap comment happened to use `//`. A second fix-forward is in flight; P15-B will be booked as 0.3.9 when it merges.
- **P15-B 不在本条目内。** 它的全分支终审返回 🔴 **需修复后合并**(这是继 P11 之后第二次由终审拦下一批):产品代码正确、四门全绿,但守卫层有一个 critical 缺口——`maskSourceComments()` 用 2 字符切片去和 4 字符的 `'<!--'` 比较,于是它的 HTML 注释分支是死代码、永不执行。由此有五处实测后果(两处假绿:假调色板块藏在 HTML 注释里即可劫持;三处假红:仅仅改一句注释措辞就会红至四条腿,而 e2e 套件 9/9 全绿),并推翻了两处已入库的声称:spec 与裁定里写的「本守卫不依赖实现侧的措辞自律」,以及上一轮 critical finding 的修法之所以安全是「因为注释屏蔽已中和陷阱注释」——这句对那个文件是真的,但成立原因只是陷阱注释恰好用的是 `//`。第二轮 fix-forward 正在进行;P15-B 合并时记为 0.3.9。
## [0.3.7] - 2026-10-03
### Done / 完成
- P14 splits the `ContentService` god object in crearte-server (0.18.0, merge `fe810dd`, merge-base `dbf7fe5`): an 856-line file carrying five unrelated responsibility lines becomes five sub-service files plus an 82-line composition root, and the five handlers each narrow from 22 visible methods to a consumer-defined interface of 4/5/2/5/6 with zero over-declaration and zero orphaned methods. `cmd/catalog.go` stops paying the god object's tax (dependency slots 4 → 2, the `nil` bundle placeholder gone). Pure structural refactor with no behaviour change — 25 methods moved byte-for-byte, `NewContentService`'s four-parameter signature unchanged character for character, `cmd/serve.go` untouched, and not one existing `*_test.go` modified.
- P14 拆分 crearte-server 的 `ContentService` god object(0.18.0,merge `fe810dd`,merge-base `dbf7fe5`):一个承载五条互不相干职责线的 856 行文件,变成五个子 service 文件加一个 82 行的组合根;五个 handler 各自从 22 个可见方法收窄到 4/5/2/5/6 个消费方定义的接口,零过声明、零孤儿方法。`cmd/catalog.go` 不再为 god object 交税(依赖槽 4 → 2、`nil` bundle 占位消失)。纯结构重构、零行为变更——25 个方法逐字节迁移、`NewContentService` 的四参数签名逐字不变、`cmd/serve.go` 未动、无任何既有 `*_test.go` 被修改。
- The review ladder ran two rounds and **both caught the controller's own errors**, which is the batch's main result rather than the split itself. The task review (PASS with notes, 7 findings from 10 self-designed mutations) found that the guard purpose the spec states — "prevent methods migrating back to the composition root" — was covered by **no assertion at all**: adding a method to the root left all three assertions PASS, `go build`/`go vet` clean, and the full 11-package suite green. It also found that the spec's own positive control had a bypass: mutation (a) turns red because it *removes* the method from the sub-service, not because anything detects the extra root method, so rewriting it as a copy would have passed green. The branch review (APPROVE with notes, 6 findings from 16 mutation/spike rounds, all run on a HEAD-exported copy proven byte-identical by 140 blob hashes) then found a hole **in the first round's own fix**: the source-scan pattern required a *named* receiver, but Go permits omitting an unused one and a shadowing body is exactly the case that often needs none — `func (*ContentService) CoverURL(k string) string` left build, vet and all seven assertions green while the shadow was effective. One token fixed it. Thirteen notes were adjudicated before merge: four fix-forward commits (`e195be0`, `e04694b`) and the rest accepted-with-reason, spec-wording corrections, or backlog.
- 审查阶梯跑了两轮,**两轮都抓到了控制者自己的错误**——这才是本批的主要产出,而非拆分本身。任务级审查(PASS with notes,10 条自设 mutation 挖出 7 条 findings)发现 spec 自述的守卫目的「防方法迁回组合根」**没有任何断言覆盖**:在组合根上加方法会让三条断言全 PASS、`go build`/`go vet` 全绿、全量 11 包测试也全绿。它还发现 spec 自己的 positive control 有绕行路径:mutation (a) 之所以红,是因为它把方法从子 service **拿走**了,而不是因为任何断言侦测到组合根多出方法,故把它改写成「复制」就会误绿。全分支终审(APPROVE with notes,16 轮 mutation/spike 挖出 6 条 findings,全部跑在以 140 个 blob hash 证明与 HEAD 逐字节一致的导出副本上)接着在**第一轮的修复自身**里找到洞:源码扫描的正则要求接收者**有名**,但 Go 允许省略不用的接收者名,而遮蔽体恰好常不需要它——`func (*ContentService) CoverURL(k string) string` 会让 build、vet 与七条断言全绿,而遮蔽确实生效。一个 token 就修好了。13 条 note 在合并前逐条裁定:4 条 fix-forward(`e195be0`、`e04694b`),其余为 accepted-with-reason、spec 措辞纠正或挂账。
- Two fixes a reviewer proposed were rejected on measurement, and the branch review confirmed both rejections on the real repo: asserting `NumMethod() == 0` on the value type is unsatisfiable because embedding `*T` puts its methods in both the value and pointer method sets — the legal tree already reports 22, making it a vacuously *false* assertion, the mirror image of the vacuously *true* one P13 shipped; and comparing `Method.Func` identity cannot detect shadowing because promoted methods are forwarding wrappers that already differ on the legal tree (`9683808 != 9511104` unshadowed, `9515680 != 9511104` shadowed). Two lessons went into the spec: a vacuously false guard is as worthless as a vacuously true one and is *more* likely to pass review by looking strict; and a claim that some check is "the only way" must enumerate the forms it covers (named/unnamed receiver, value/pointer, exported/unexported) or it becomes the next reviewer's counterexample — the review overturned two such claims I had already committed.
- 审查者提议的两条修法经实测被否决,全分支终审在真仓复核确认两条驳回都正确:断言值类型的 `NumMethod() == 0` 不可满足,因为嵌入 `*T` 会使其方法进入值类型与指针类型两者的方法集——合法树上已经报 22,故那是恒**假**断言,正是 P13 交付的恒**真**断言的镜像形态;而比较 `Method.Func` 同一性无法侦测遮蔽,因为提升方法本身是 forwarding wrapper,合法树上两者已经不同(未遮蔽 `9683808 != 9511104`、遮蔽 `9515680 != 9511104`)。两条教训写入 spec:恒假守卫与恒真守卫同样无价值,且**更容易因看起来严格而通过审查**;声称某个检查是「唯一手段」时必须枚举它覆盖的形态(有名/匿名接收者、值/指针、导出/未导出),否则它会成为下一个审查者的反例——本轮审查推翻了我两处已入库的这类声明。
- Spec re-pinned twice (`11fa1e2` after the task review, `33c2d8d` after the branch review) and ROADMAP updated: D-J amended from three assertions to seven, the mutation list extended to a–h with (a)'s bypass annotated, line counts pinned to the `wc -l` convention, D-D recording that `ErrSubmissionConflict` is also used at `account.go:136` outside the two lines that share it, and the risk table gaining two Route C properties — the root has no named fields so `s.objects` is an ambiguous selector rejected at compile time (a barrier earlier than any guard), and `go vet` stays silent on a root method shadowing a promoted one. The third wave table gains the P14 row and the doc index marks it executed against the merge commit; backlog gains `Approve` at 169 lines (`moderation.go:47-215`, seven phases already mapped), `memory_content.go`'s 814-line test double, handler error-mapping dedup, the `now`-field audit for `AccountService`/`CleanupService`, assertion 6 pinning field names rather than types, the lexical scan's deliberate over-strictness on block comments, and orphan private helpers. It also logs a dark-mode gap found while surveying the next batch: P13's token work covered only the `src/index.html` entry and missed the second Vite entry `bootstrap` (`vite.config.ts:21`), whose loading screen hardcodes light values — so dark-theme users see a white flash on every virtual game launch.
- spec 两次 re-pin(任务级审查后 `11fa1e2`、全分支终审后 `33c2d8d`)并更新 ROADMAP:D-J 从三类断言增订为七类、mutation 清单扩到 a–h 并注解 (a) 的绕行路径、行数口径钉为 `wc -l`、D-D 补记 `ErrSubmissionConflict` 还被两线之外的 `account.go:136` 使用、风险表新增两条 Route C 性质——组合根零具名字段故 `s.objects` 是编译期即拒绝的 ambiguous selector(比任何守卫都更早的屏障),以及 `go vet` 对组合根方法遮蔽提升方法保持沉默。第三波表新增 P14 行、文档索引按 merge commit 标记已执行;挂账新增 169 行的 `Approve`(`moderation.go:47-215`,七阶段已测绘)、`memory_content.go` 的 814 行测试替身、handler 错误映射去重、`AccountService`/`CleanupService` 的 `now` 字段复核、断言 6 只钉字段名不钉类型、词法扫描对块注释的刻意过严、孤儿私有方法。另登记一条在为下一批取证时发现的暗色缺口:P13 的令牌化只覆盖了 `src/index.html` 入口,漏了第二个 Vite 入口 `bootstrap`(`vite.config.ts:21`),其加载屏硬编码亮色值——故暗色用户每启动一个虚拟游戏都会看到一次白闪。
## [0.3.6] - 2026-10-03
### Done / 完成
- **P13 dark mode delivered** (crearte-only batch): crearte `983e7c4` (0.26.0, merge-base `e59a171`); zero server/deploy changes. A two-state theme (light ↔ dark) site-wide — first visit follows `prefers-color-scheme`, an explicit toggle persists to `localStorage['crearte.theme.v1']` and overrides the system preference thereafter (no third "follow-system" state, deliberately deferred: spike 3 measured that the header has no pixel room at 320px worst-case for a labelled control). Each theme carries 12 design tokens; the dark palette overrides the light `@theme` block wholesale under `html[data-theme="dark"]` (specificity (0,1,1), no `!important`, no `@apply`) — **approach B**, which needed only 3 usage migrations versus the 32+ that approach A (flip every token in place) would have demanded, and which leaves `::selection`, markdown `strong`, and `.wordmark-label` untouched. A pre-paint inline `<script>` in `index.html` (IIFE, `var` only, before any CSS or the Vue module) sets `data-theme` so dark-preference users never see a white flash, with stored→system→light precedence byte-for-byte identical to `useTheme.effectiveTheme()`. Dispatch followed one-writer-per-repo: T1–T5 all touch crearte's single worktree, so they went to **one** implementer subagent; the guard task was TDD-first (write the guard, watch it go RED reproducing the spec's numbers verbatim, then implement to GREEN). **Review ladder**: task-level review **PASS with notes** (5 findings from 10 self-designed mutations), whole-branch final review **APPROVE with notes** (zero critical, 2 important, 5 minor, 3 advisory). All ten notes adjudicated before merge: four fixed forward in `2433ec7`/`481574c`/`6ec6170`, four more in `bb2cb42`, five were spec-document corrections, one pre-existing defect deferred to the a11y polish batch, two accepted with reason. Gates: crearte vitest **688/79** (baseline 635/74), vue-tsc clean, build + build:runtime clean, main e2e **102 passed + 1 skipped** (baseline 92+1skip; P12's 12 `responsive.spec.ts` legs untouched and green), noauth 4 — re-run independently by the controller and both reviewers. Artifact `main-C7966Gm-.css`: `var(--color-*)` = 75, inline `#141414` = 2, dark block present, dead utilities zero.
- **P13 暗色模式交付**(仅 crearte 的批次):crearte `983e7c4`(0.26.0,merge-base `e59a171`);server/deploy 零改动。两态主题(light ↔ dark)全站落地——首次访问跟随 `prefers-color-scheme`,显式点击开关后持久化到 `localStorage['crearte.theme.v1']` 并从此压过系统偏好(**无第三态**「恢复跟随系统」,有意延后:spike 3 实测 header 在 @320px 最坏格无像素容纳带标签控件)。每主题 12 个设计令牌;暗色调色板在 `html[data-theme="dark"]`(特异性 (0,1,1),无 `!important`/`@apply`)下整体覆盖亮色 `@theme` 块 = **方案 B**,故只需 3 处 usage 迁移(方案 A 逐令牌就地翻转需 32+ 处),且 `::selection`、markdown `strong`、`.wordmark-label` 零改动。`index.html` 一段 pre-paint 内联 `<script>`(IIFE、仅 `var`、在任何 CSS 与 Vue module 之前)设 `data-theme`,故暗色偏好用户不闪白,判定优先级 stored→system→light 与 `useTheme.effectiveTheme()` 逐字一致。派发遵循一仓一写者:T1–T5 同动 crearte 单一工作树,故交**一个**实现者子代理;守卫任务 TDD 先行(先写守卫、看它变红并逐字复现 spec 数字、再实现到绿)。**审查阶梯**:任务级审查 **PASS with notes**(10 条自设 mutation 挖出 5 条 findings)、全分支终审 **APPROVE with notes**(零关键 / 2 重要 / 5 次要 / 3 建议)。10 条 note 合并前逐条裁定:4 条在 `2433ec7`/`481574c`/`6ec6170` fix-forward、另 4 条在 `bb2cb42`、5 条是 spec 文档纠正、1 条 pre-existing 缺陷延后到可访问性打磨批、2 条 accepted-with-reason。验收:crearte vitest **688/79**(基线 635/74)、vue-tsc 零错、build + build:runtime 零错、主 e2e **102 passed + 1 skipped**(基线 92+1skip;P12 的 12 条 `responsive.spec.ts` 腿未动全绿)、noauth 4——控制者与两位审查者各自独立复跑。产物 `main-C7966Gm-.css`:`var(--color-*)` = 75、内联 `#141414` = 2、暗色块在位、死 utility 归零。
- **Parallel server micro-batch** delivered during the P13 survey phase (the implementer owned crearte exclusively, so crearte-server was free to the controller): crearte-server `dbf7fe5` (0.17.2). A read-only audit found a real defect in `uploads.go` — when `ParseMultipartForm` failed with anything other than `MaxBytesError`, the handler fell through to the kind switch and returned a misleading 400 "kind must be bundle or cover", and **no test covered it** (all five existing upload tests send valid multipart). Test-first RED, fix, then mutation-verified (revert → red, restore → green, `porcelain=0`). The second audit item — the bundle-key route being public — was initially suspected as a hole but **overturned by grep**: six existing pins already assert it, so only a decision-record comment was added (zero behaviour change). Gates: gofmt/vet clean, `go test ./...` 11 packages ok, `ls-remote` MATCH, branch deleted.
- **并行的 server 微批**在 P13 勘查期交付(实现者独占 crearte,故 crearte-server 对控制者自由):crearte-server `dbf7fe5`(0.17.2)。只读审计发现 `uploads.go` 一个真缺陷——`ParseMultipartForm` 以非 `MaxBytesError` 失败时,handler 落到 kind switch 并返回误导性的 400「kind must be bundle or cover」,且**零测试覆盖**(五个既有上传测试全发合法 multipart)。测试先行 RED、修复、再 mutation 验证(回退→红、恢复→绿、`porcelain=0`)。第二项审计发现——bundle-key 路由公开——一度被疑为漏洞,但**被 grep 推翻**:六处既有钉桩早已断言它,故只加了决策记录注释(零行为变更)。验收:gofmt/vet 净、`go test ./...` 11 包 ok、`ls-remote` MATCH、分支已删。
- Lessons recorded. **`max(a,b) ≥ k` is a vacuous-assertion hot zone, and a guard fix must be verified in both directions.** The P13 scrim-separation guard went through two reversals: the original spec text (`ink vs scrim ≥ 3` in both themes) would false-red the light theme (light `ink vs scrim` measures 1.07 — two darks that don't separate), and the controller's first correction, `max(fill, border) ≥ 3`, is **mathematically vacuous** — because the two sides are complementary, `max(cr(paper,c), cr(ink,c)) ≥ √cr(paper,ink) = 4.0621 > 3` for *any* scrim colour (brute-forced over 50653 samples), so whitening the light scrim — exactly the wash-out the token exists to prevent — left the fill side at 1.1165 while `max()` read 18.42 and the guard stayed green. Task review caught it. The first correction had verified only the false-red direction and shipped a never-red assertion; the same mistake recurred in the same batch when an alpha fallback was checked only against current Chromium output, not the CSS Color 4 forms it claims to support (percentage and exponential alpha parsed as 80 and 1). **Corollary, now in the spec: fix a guard by proving both no false-red on legal values and no false-green under mutation.** (2) **Tailwind v4 scans every source file including `.test.ts`/`.spec.ts`** — a class-name literal in a test comment is treated as a candidate and burns a dead utility into the artifact. The implementer hit this and fixed one instance by string concatenation; the controller reintroduced it eight minutes later in its own adjudication commit (hash `C7966Gm-`→`CV7wAyTH`, `var(--color-*)` 75→76), caught only by rebuilding. Now spec §8-5b. (3) **A universal claim needs a universal grep.** The spec's "accent is the only place using it as a background / becomes a pure non-text token after migration" was false: `runtime/host/GameHost.vue` has three live `bg-accent` references, and both the spike-0 grep and the `noHardcodedColor` guard covered only `app/` while `runtime/` sits *beside* it. That blind spot produced two layers of damage — a false spec fact and a real uncovered WCAG violation (`paper on accent` = 3.2590 < 4.5 in light, pre-existing since P9-B and unchanged by P13). The guard's scan root now includes `runtime/`, the spec claim is corrected, and the violation is logged to the a11y polish batch rather than silently dropped.
- 教训入账。**`max(a,b) ≥ k` 是恒真断言高发区,而修守卫必须两个方向都验。** P13 的 scrim 分离守卫经历两次反转:spec 原文(`ink vs scrim ≥ 3` 两主题)会让亮色腿误红(亮色 `ink vs scrim` 实测 1.07——两个深色互不分离),而控制者的第一次纠正 `max(填充侧, 边框侧) ≥ 3` **数学恒真**——因两侧互补,对*任意* scrim 色都有 `max(cr(paper,c), cr(ink,c)) ≥ √cr(paper,ink) = 4.0621 > 3`(50653 采样暴力验证),故把亮色 scrim 改纯白(正是该令牌要防的泛白)时填充侧得 1.1165、而 `max()` 读作 18.42、守卫仍绿。任务级审查抓到。第一次纠正**只验了误红方向**就交付了一个永不断红的断言;同一错误在同批重演——alpha 兜底分支只对着当前 Chromium 输出验,没验它声称支持的 CSS Color 4 形态(百分比与指数 alpha 被解析成 80 和 1)。**推论,已写进 spec:修守卫必须同时证明对合法值不误红、且在 mutation 下不误绿。**(2)**Tailwind v4 扫描全部源文件含 `.test.ts`/`.spec.ts`**——测试注释里的类名字面量会被当候选、把死 utility 烧进产物。实现者撞到并用字符串拼接修了一处;控制者八分钟后在自己的裁定 commit 里重新引入(哈希 `C7966Gm-`→`CV7wAyTH`、`var(--color-*)` 75→76),只靠重建才抓出。现为 spec §8-5b。(3)**全称声明需要全称 grep 支撑。** spec 那句「accent 是唯一把它当背景用的地方 / 迁移后成纯非文本令牌」为假:`runtime/host/GameHost.vue` 有 3 处 `bg-accent` 活引用,而 spike-0 的 grep 与 `noHardcodedColor` 守卫都只覆盖 `app/`,`runtime/` 却与 `app` **同级**。该盲区造成两层损害——一条假的 spec 事实 + 一条真实且无守卫覆盖的 WCAG 违规(亮色 `paper on accent` = 3.2590 < 4.5,自 P9-B 就存在、P13 未使其变差)。守卫扫描根现已含 `runtime/`、spec 声明已纠正、违规本身登记进可访问性打磨批而非静默丢弃。
## [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
### Done / 完成
- P11 server-hardening batch delivered across all three product repos: crearte-server `9da39b6` (0.17.0, merge-base `d627e12`), crearte-deploy `740958a` (0.7.0, `12f7c10`), crearte `d25c497` (0.24.1, `338acbf`). Five tasks each through implement → task-level review → adjudication (all PASS, zero critical/important; nine minor notes parked to the polish batch), plus a whole-branch final review on the strongest model. **Shipped**: rate-limiter hard cap (`maxEntries` fail-closed — when the table is full it runs one expiry sweep and, still full, refuses NEW keys with 429+Retry-After without inserting; existing keys keep byte-identical semantics, so `len(hits) ≤ maxEntries` holds inductively; evict-oldest was rejected because an attacker could flush legitimate users' counters); `LOG_LEVEL`/`LOG_FORMAT` lower-cased and trimmed at the config boundary (closes the P8-deferred strict-vs-lenient divergence without touching `internal/observability`); one-time startup `slog.Warn` when `TrustedProxies` is empty (loud trace instead of silent degradation — every client behind a reverse proxy sharing one rate-limit bucket); two negative regression pins for the compose-SNAT gateway trap; deploy `TRUSTED_PROXIES` **empty default** plus a dedicated README section; nginx `X-Real-IP` on both `/api/` locations and `nosniff`/`Referrer-Policy strict-origin-when-cross-origin` with `always` at nine insertion points (2 server blocks + 7 self-headed locations, because nginx `add_header` inheritance is all-or-nothing). CSP and HSTS deliberately out of scope (CSP needs its own design batch — game subdomains load user works via iframe+SW; HSTS waits for the TLS batch — deployment is plain `listen 80`). **The batch's defining event — endpoint verification caught a design-level error and the spec was re-pinned (D-A → D-A′)**: the original design trusted the whole compose subnet `172.28.0.0/24`, on the stated premise that the subnet is a closed boundary containing only this project's web containers. Live-stack measurement falsified the premise twice over: docker SNATs published host ports, so nginx's source is always the bridge gateway `172.28.0.1` — which is INSIDE that subnet; gin therefore walks past the trusted gateway and adopts whatever `X-Forwarded-For` prefix the client pre-seeded (measured on the running prod stack: a client sending `X-Forwarded-For: 8.8.8.8` made the server log `ip=8.8.8.8`, i.e. **anyone could pick their own rate-limit bucket — the "fix" was a bypass**); and real browsers (no XFF) collapse to the gateway regardless of any `TRUSTED_PROXIES` value, because SNAT destroys the real client IP before nginx ever sees it — defect A is unfixable in the compose-local rehearsal form. A six-case trust-matrix spike (`spike_snat`) then pinned the correct semantics: subnet-trust → spoof adopted; trust-nginx-only or trust-nothing → spoof blocked; real-prod shape (nginx directly seeing real clients, trusting nginx) → real IP recovered and spoofed prefixes ignored. Re-pinned design: **empty trust list by default** (XFF ignored entirely, no bypass, honest D-C warning about the single bucket — acceptable for a single-user local rehearsal), subnet pin reverted (its only purpose was feeding TRUSTED_PROXIES; keeping it would only add a pool-overlap failure mode; verified live — the stack came up on docker's dynamic `172.19.0.0/16` and every assertion held), README rewritten with the topology, the trap, the real-prod configuration duty and a spoof-probe verification recipe. The controller's own mid-wave regression had tested the WRONG path (two client-supplied XFF values "counting independently" — that IS the spoof path); the corrected verification uses the real-browser path plus the spoof probe as a negative test. Final review verdict 「需修复后合并」 — N1 (the WARN hint literally recommended the subnet-trust configuration that D-A′ had just classified as a bypass; reworded to "the reverse proxy's own IP/CIDR — never a range that also contains clients or the docker bridge gateway"), N2 (five spec locations still described the reverted subnet pin; corrected), N3/N4/N5 (spike count, stale D-A headings, test count +9→+12) all closed pre-merge; N6 (a validate.yml machine guard asserting the empty default — no automated test can catch a compose-file regression, since the server pins construct Deps directly) parked to the polish batch per the reviewer's own "not blocking" ruling. Acceptance: server gofmt/vet/build clean, `go test ./...` 11 packages ok, `-race` 12 packages ok (Debian golang:1.24 + CGO_ENABLED=1 — alpine lacks gcc and its apk CDN is unreachable from this host), integration leg against real db-test all ok with skip-guard 0; crearte vitest **626** / vue-tsc clean / build OK / main e2e **80+1skip** / noauth 4; deploy four-profile `config -q`, empty default + override passthrough, named-volume set byte-identical to baseline; post-fix live prod-stack regression five-for-five (warning present; spoofed XFF resolves to the nginx container IP, never `8.8.8.8`; single-bucket 429 at the login cap; security headers intact on a 404; bare `down`, all volumes preserved). The final reviewer independently re-ran six legs, matching the controller's numbers item-for-item. Lessons recorded: an isolated spike proves component semantics, not that your configuration is correct in the real topology — security/network fixes require a live-stack end-to-end pass over the REAL traffic path before merge; and an acceptance test that exercises the wrong path is more dangerous than no test.
- P11 服务端安全硬化批交付,横跨三个产品仓:crearte-server `9da39b6`(0.17.0,merge-base `d627e12`)、crearte-deploy `740958a`(0.7.0,`12f7c10`)、crearte `d25c497`(0.24.1,`338acbf`)。五任务各经实现→任务级审查→裁定(全 PASS,零关键零重要;9 条次要转打磨批),另加强模型全分支终审。**交付内容**:限流表硬上界(`maxEntries` 失败关闭——表满时先跑一次过期清理、仍满则对新面孔 429+Retry-After 且不插入;既有键语义逐字不变,故 `len(hits) ≤ maxEntries` 归纳成立;「逐最旧」被否因为攻击者可用新 IP 冲刷正常用户的计数器);`LOG_LEVEL`/`LOG_FORMAT` 在配置入口小写化与 trim(闭合 P8 挂账的严格/宽容分歧,`internal/observability` 零触碰);`TrustedProxies` 为空时的一次性启动 `slog.Warn`(大声留痕而非静默降级——反代后所有客户端共享一个限流桶);两个 compose-SNAT 网关陷阱反面钉桩;deploy `TRUSTED_PROXIES` **空默认** + README 专节;nginx 两个 `/api/` 块的 `X-Real-IP` 与九个落点的 `nosniff`/`Referrer-Policy strict-origin-when-cross-origin`(均带 `always`;2 个 server 块 + 7 个自带头 location,因 nginx `add_header` 继承全有或全无)。CSP 与 HSTS 有意不做(CSP 需独立设计批——游玩子域经 iframe+SW 加载用户作品;HSTS 待 TLS 批——当前部署是纯 `listen 80`)。**本批定义性事件——端到端验收抓出设计级错误、spec re-pin(D-A→D-A′)**:原设计信任整个 compose 子网 `172.28.0.0/24`,前提是「子网是只含本项目 web 容器的封闭边界」。真实栈实测两度证伪该前提:docker 对发布端口做 SNAT,nginx 看到的源恒为网桥网关 `172.28.0.1`——而它**就在该子网内**;gin 于是跳过这个可信网关、采纳客户端预置的 `X-Forwarded-For` 前缀(运行中的 prod 栈实测:客户端发 `X-Forwarded-For: 8.8.8.8`,服务端日志 `ip=8.8.8.8`,即**任何人可自选限流桶——「修复」本身是个绕过**);且真实浏览器(无 XFF)无论 `TRUSTED_PROXIES` 配什么都塌缩到网关,因为 SNAT 在 nginx 看到之前就销毁了真实客户端 IP——缺陷 A 在 compose 本机演练形态下不可修。六用例信任矩阵 spike(`spike_snat`)随后钉死正确语义:信任子网→伪造被采纳;只信任 nginx 或信任空→伪造被拦截;真实 prod 形态(nginx 直接见真实客户端、信任 nginx)→真实 IP 恢复且伪造前缀被忽略。re-pin 后设计:**默认空信任列表**(XFF 整体忽略、无绕过、D-C 告警如实提示单桶——单用户本机演练可接受)、subnet 固定回退(其唯一目的是喂 TRUSTED_PROXIES;保留只会新增网段冲突失败模式;实栈验证——栈起在 docker 动态分配的 `172.19.0.0/16` 上全部断言成立)、README 重写(拓扑、陷阱、真实 prod 的配置责任、伪造探测验证法)。控制者自己波次中的回归曾测错路径(两个客户端自带 XFF「各自独立计数」——那正是伪造路径);修正后的验证走真实浏览器路径 + 伪造探测作反面测试。终审裁定「需修复后合并」——N1(WARN hint 竟字面推荐 D-A′ 刚判定为绕过的 subnet 信任配置;改写为「反代自身的 IP/网段——绝非同时包含客户端或 docker 网桥网关的范围」)、N2(spec 五处仍描述已回退的 subnet 固定;已改)、N3/N4/N5(spike 份数、D-A 旧标题、测试计数 +9→+12)均合并前闭合;N6(validate.yml 加空默认机器守卫——server 钉桩直接构造 Deps、不读 compose 文件,故无任何自动化测试能抓住 compose 文件回归)依终审者本人「不阻塞」判定转打磨批。验收:server gofmt/vet/build 净、`go test ./...` 11 包 ok、`-race` 12 包 ok(Debian golang:1.24 + CGO_ENABLED=1——alpine 无 gcc 且其 apk CDN 从本宿主不可达)、真库 db-test 集成腿全 ok 且 skip 守卫 0;crearte vitest **626** / vue-tsc 零错 / build OK / 主 e2e **80+1skip** / noauth 4;deploy 四 profile `config -q`、空默认+覆盖跟随、命名卷集合与基线逐字相同;修正后 prod 真实栈回归五项全中(告警在位;伪造 XFF 解析为 nginx 容器 IP、绝非 `8.8.8.8`;login 上限处单桶 429;404 上安全头完好;裸 `down`、卷全留存)。终审者独立复跑六腿,与控制者数字逐条一致。教训入账:隔离 spike 证明的是组件语义、不是你的配置在真实拓扑下正确——安全/网络类修复合并前必须走真实流量路径的实栈端到端验证;测错路径的验收比不测更危险。
## [0.3.3] - 2026-10-01
### Done / 完成
- P9-B UX batch-2 (accessibility & keyboard-efficiency pack) delivered: crearte `338acbf` (0.24.0, merge-base `ee4edf7`) — five tasks closing seven measured a11y gaps, T1-T4 dispatched to subagents and T5 written by the controller, each through implement → task-level review → adjudication (four reviews, all PASS), plus a whole-branch final review on the strongest available model cross-checking spec §1-§6 in full, the cross-task integration seams, and mutation-testing three guards. **Contrast to WCAG AA**: `--color-success` deepened `#2fa46a` → `#1f7a4d` (paper-on 2.83 → 4.76; the success toast is 12px bold, not large text) and the error toast switched `bg-accent` → `bg-accent-ink` (3.64 → 4.87, reusing an existing token rather than adding a colour). **Toast announcer restructured** (closing P10 final-review item T1(d), an interactive control living inside a live region): visual and announcement layers split the way Radix Toast / Headless UI do it — one persistent `sr-only p[role=status][aria-live=polite]` whose text a watch drives, no announcement semantics on the visible toasts, so the dismiss button is a sibling of the live region rather than a descendant. **Skip link** as the root div's first child before `<AppHeader />`, `sr-only` until focused then `focus:not-sr-only` with `focus:z-[80]` above the toast layer. **Focus management after SPA navigation**: `<main id="main" tabindex="-1">` plus an exported `focusMain()` called from the router's `afterEach` only when `to.path !== from.path` — query-only changes (P10's clickable tags, `/games?tag=x`) deliberately do not steal focus, `preventScroll` avoids double-scrolling against `scrollBehavior`, and a missing `#main` no-ops via the optional chain. **Table column headers**: all 24 `<th>` elements across the three admin views get `scope="col"`, and the two previously empty 操作 headers gain `<span class="sr-only">操作</span>` (they were unnamed columns to a screen reader). **Rating accessible names**: each star button gets `aria-label="评 N 星"` with its glyph `aria-hidden`, and the existing group span becomes `role="group"` with a dynamic label — deliberately not `radiogroup`/`aria-checked` (the control supports click-current-star-to-unrate, which radio cannot express) and not per-star `aria-pressed` (lighting is a continuous run, so pressed would announce four stars as all-pressed). **Two source-level guards** following the existing `no-gradient.test.ts` pattern: `lib/contrast.test.ts` parses `@theme` tokens out of `main.css` and asserts WCAG 2.1 ratios for eleven pairs (ten ≥4.5 plus the focus ring ≥3 per 1.4.11), `lib/tableScope.test.ts` scans every `app/**/*.vue` asserting each `<th` opening tag carries `scope=` (word-boundary match so the five `<thead>` occurrences are not flagged; opening-tag slice taken across newlines). **New e2e** `a11y.spec.ts`: real keyboard Tab path, admin `columnheader` names, star accessible names via `toHaveAccessibleName`. Zero server/deploy changes. Acceptance (five legs, green on merge tip, independently re-run six-for-six by the final reviewer): vitest **626** (baseline 601, +25) / vue-tsc clean / build + build-runtime OK / main e2e **80+1skip** (baseline 77+1skip) / noauth 4. Final verdict APPROVE with notes — zero critical, zero important. Two spec-layer corrections landed mid-wave: ① the T1 implementer caught a real defect in the spec's initial D-I design (a `role=status` mounted *together with* its text is not reliably announced by several screen-reader/browser combinations, trading button noise for total silence) → re-pinned to D-I′, fixed by the controller in `f01baae`, which also pinned the saturation trap (the watch must key on the last entry's **id**, not the array length — at `MAX_VISIBLE=3` a push evicts and appends in one assignment so the length stays 3 while a fourth message still has to be announced). ② Two quantity slips in the controller's own briefs (contrast 3.16 vs the real 2.83 for paper; "19 `<th>`" was a line count, the element count is 24) were both corrected by implementers deferring to the spec and to measurement; spec §1 was amended to match. Lesson recorded: quantities quoted in dispatch briefs must be verified at element level, never inferred from `grep -c` or from an adjacent number in the spec. Four items parked in the polish batch (re-check a weak router-test assertion on vue-router upgrade; surface unrate semantics in the accessible name; the announcer re-reads an older message when the newest toast is manually dismissed; spec wording already fixed). C (dark mode), OG social cards, play count, mobile header and later batches remain parked pending owner go-ahead.
- P9-B UX 第二批(可访问与键盘效率包)交付:crearte `338acbf`(0.24.0,merge-base `ee4edf7`)——五任务闭合七处实测可访问缺口,T1-T4 派发子代理、T5 由控制者本人写,每任务经实现→任务级审查→裁定(四次审查全 PASS),另加最强可用模型的全分支终审,对 spec §1-§6 全量对照、跨任务集成缝隙、并对三处守卫做 mutation 式抽查。**色彩对比达 WCAG AA**:`--color-success` 由 `#2fa46a` 加深为 `#1f7a4d`(paper-on 2.83 → 4.76;成功 toast 是 12px 粗体,不属大字号),错误 toast 由 `bg-accent` 改为 `bg-accent-ink`(3.64 → 4.87,复用既有令牌而非新增颜色)。**toast 播报区重构**(收口 P10 终审转办项 T1(d):交互控件位于实时区内):按 Radix Toast / Headless UI 的做法把视觉层与播报层解耦——一个常驻的 `sr-only p[role=status][aria-live=polite]` 由 watch 驱动其文字,可见 toast 不带任何播报语义,于是关闭按钮成为实时区的兄弟而非后代。**跳转链接**作根 div 首子、在 `<AppHeader />` 之前,未聚焦时 `sr-only`、聚焦后 `focus:not-sr-only` 且 `focus:z-[80]` 压过 toast 层。**SPA 导航后焦点管理**:`<main id="main" tabindex="-1">` + 导出的 `focusMain()`,仅当 `to.path !== from.path` 时由路由 `afterEach` 调用——只改 query(P10 的可点标签 `/games?tag=x`)有意不抢焦点,`preventScroll` 避免与 `scrollBehavior` 双重滚动,`#main` 缺失时由可选链静默 no-op。**表格列头**:三个管理视图全部 24 个 `<th>` 元素加 `scope="col"`,两个此前为空的「操作」表头补上 `<span class="sr-only">操作</span>`(它们在读屏里曾是无名列)。**评分可访问名**:每颗星 `aria-label="评 N 星"` 且字形 `aria-hidden`,既有分组 span 成为 `role="group"` 并带动态标签——有意不用 `radiogroup`/`aria-checked`(本控件支持点当前分撤评,radio 无法表达取消选中),也不用逐星 `aria-pressed`(点亮是连续区间,pressed 会把四分播报成四颗星都按下)。**两个源码级守卫**沿用既有 `no-gradient.test.ts` 范式:`lib/contrast.test.ts` 从 `main.css` 解析 `@theme` 令牌并对十一个配对断言 WCAG 2.1 比值(十个 ≥4.5 加焦点环 ≥3,依 1.4.11),`lib/tableScope.test.ts` 扫描全部 `app/**/*.vue` 断言每个 `<th` 起始标签都带 `scope=`(词界匹配故五处 `<thead>` 不被误判;起始标签片段跨换行截取)。**新增 e2e** `a11y.spec.ts`:真实键盘 Tab 路径、管理端 `columnheader` 可访问名、经 `toHaveAccessibleName` 验证的星标可访问名。零后端/部署改动。验收(五腿,合并 tip 全绿,终审者六腿独立实跑逐条一致):vitest **626**(基线 601,+25)/ vue-tsc 零错 / build+build-runtime OK / 主 e2e **80+1skip**(基线 77+1skip)/ noauth 4。终审裁定 APPROVE with notes——零关键、零重要。波次内落两处 spec 层修正:① T1 实现者抓出 spec 初稿 D-I 的真缺陷(与文字**同时创建**的 `role=status` 在多个读屏+浏览器组合下不被可靠播报,等于用按钮噪音换来彻底静默)→ re-pin 为 D-I′,控制者于 `f01baae` 直修,并顺带钉死饱和态陷阱(watch 必须以末尾条目的 **id** 而非数组长度为源——`MAX_VISIBLE=3` 时 push 会「逐最旧+append」一次赋值完成,长度停在 3 而第四条消息仍需播报)。② 控制者自己简报里的两处数量笔误(对比度 3.16 与 paper 的真实值 2.83、「19 个 `<th>`」是行计数而元素实为 24)均由实现者以 spec 与实测为准纠正,spec §1 已同步修正。教训已入账:派发简报里引用的数量必须元素级核实,不得由 `grep -c` 或 spec 中相邻数字推断。四条转入打磨批(vue-router 升级时复看一处弱断言、撤评语义进可访问名、手动关掉最新 toast 时播报区会重念一条旧消息、spec 口径已修)。C(暗色模式)、OG 社交卡片、播放计数、移动端页头及后续批仍挂账待 owner 启动。
## [0.3.2] - 2026-10-01
### Done / 完成
- P10 UX batch-1 (daily-interaction pack) delivered: crearte `ee4edf7` (0.23.0, merge-base `26cd625`) — six user-facing capabilities across six tasks, each through implement → task-level review → adjudication, plus a whole-branch final review cross-checking spec §3/§4/§5 in full. **Toast system** (`composables/useToast.ts` module singleton: success/info 3s, error 5s, max 3 visible oldest-dropped, silent quota/private-mode failures; `ToastHost.vue` mounted in `App.vue`, bottom-right `role=status`/`aria-live=polite`, per-kind hard-shadow styling, TransitionGroup folded into the global reduced-motion branch). **Dirty-form leave guard** on `/submit/new` and `/submit/:id` (`onBeforeRouteLeave` confirm + `beforeunload`; deep-watch of form/kind/both upload ids; edit-mode prefill suppressed in the same window as the existing AAD watcher, manual prefill deliberately counts as an edit; save-success disarms before its `router.push`). **Clickable tags** (game-page + game-card tags → `RouterLink` to `/games?tag=`, card tags keep `relative z-10` above the stretched link, `+N` fold badge stays a span). **Copy-link share** (game detail page writes the canonical in-site URL — `location.origin + router.resolve(...)`, never `location.href` — to clipboard, confirmed via toast, single try/catch also absorbing a missing `navigator.clipboard`). **Recently played** (`lib/recent.ts` local `crearte.recent.v1`: dedup-to-front, cap 12, corrupt JSON reads empty; `GameHost` records on frame phase `ready` via a sync-flush watch so a hosted-target restart that folds booting→ready in one tick still re-bubbles; external works never recorded; landing page renders a 继续游玩 strip between hero and featured, resolving ids against the live catalog, capping at 6, and rendering nothing at all when history is empty so the landing DOM is unchanged for first-time visitors). **Header dropdown** now closes on outside-click and Escape (returning focus to the summary toggle), preserving the existing route-change auto-close. Zero server/deploy changes. Acceptance (five legs, green on merge tip): vitest 601 (baseline 551) / vue-tsc clean / build + build-runtime OK / main e2e 77+1skip (baseline 72+1skip) / noauth 4. Four review pins landed on the branch: `fc0270e` (AppHeader unmount cleanup asserted via removeEventListener spy), `dbf4bf0` (hosted restart re-records via sync-flush phase watch; spec D-H re-pinned), `bb9bb35` (e2e acceptance fixes — the spec's own test plan had picked external-runtime fixtures 2048/a-dark-room that never reach GameHost ready; switched to abs-paths/storage, plus a latent optional-fields pre-navigation race pinned by h1 level), `836b2fb` (AppHeader route-change collapse regression, a spec §5 listed item). Final review verdict 「需修复后合并」 — both closing conditions met before merge. B (keyboard/skip-link/mobile header), C (dark mode), OG social cards, play-count and later batches parked pending owner go-ahead.
- P10 UX 第一批(日常交互包)交付:crearte `ee4edf7`(0.23.0,merge-base `26cd625`)——六任务落地六个用户可感能力,每任务经实现→任务级审查→裁定,另加全分支终审对 spec §3/§4/§5 全量对照。**Toast 体系**(`composables/useToast.ts` 模块单例:success/info 3s、error 5s、同屏上限 3 条超限移最旧、配额/隐私模式写失败静默;`ToastHost.vue` 挂入 `App.vue`,右下角 `role=status`/`aria-live=polite`、按 kind 配色硬阴影、TransitionGroup 折入全局 reduced-motion 分支)。**投稿表单脏数据离开守卫**(`/submit/new` 与 `/submit/:id`:`onBeforeRouteLeave` confirm + `beforeunload`;深度观察 form/kind/两个 upload id;编辑回填与既有 AAD watcher 同窗抑制、手动 prefill 有意计为修改;save 成功先解除守卫再 `router.push`)。**标签可点**(作品页+作品卡 tags → `/games?tag=` 的 `RouterLink`,卡内标签保留 `relative z-10` 浮于拉伸链接之上,`+N` 折叠角标维持 span)。**复制链接分享**(作品详情页写入规范站内 URL——`location.origin + router.resolve(...)`,绝不用 `location.href`——经 toast 确认,单一 try/catch 同时兜住 `navigator.clipboard` 缺失)。**最近玩过**(`lib/recent.ts` 本地 `crearte.recent.v1`:去重前插、上限 12、损坏 JSON 读回空;`GameHost` 在 frame phase `ready` 时经 sync-flush watch 记录,hosted 目标重开同 tick 折叠的 booting→ready 仍会重新冒泡;external 作品不记;落地页在 hero 与精选之间渲染「继续游玩」条带,id 对实时目录解析、上限 6、无记录时整段不渲染使首访者落地页 DOM 零变化)。**页头下拉**现支持外点与 Esc 关闭(焦点回 summary),保留既有路由变化自动收起。零后端/部署改动。验收(五腿,合并 tip 全绿):vitest 601(基线 551)/ vue-tsc 零错 / build+build-runtime OK / 主 e2e 77+1skip(基线 72+1skip)/ noauth 4。分支上落四处审查补钉:`fc0270e`(AppHeader 卸载清理经 removeEventListener spy 钉桩)、`dbf4bf0`(hosted 重开经 sync-flush phase watch 再记;spec D-H re-pin)、`bb9bb35`(e2e 验收修正——spec 测试计划自身误选 external-runtime 夹具 2048/a-dark-room 永达不到 GameHost ready,换 abs-paths/storage,另钉住 optional-fields 一处潜伏的导航前竞态)、`836b2fb`(AppHeader 路由变化收起回归,spec §5 明列项)。终审裁定「需修复后合并」——两个闭合条件均在合并前满足。B(键盘/skip-link/移动端页头)、C(暗色模式)、OG 社交卡片、播放计数及后续批挂账待 owner 启动。
## [0.3.0] - 2026-10-01
### Done / 完成
- Roadmap closing wave delivered — P8 batch-2 (③ account deletion + ④ static fallback catalog) and P3 reland (backup + observability), per owner's third directive 「实现 roadmap 所有待办事项」. Sub-repo commits: crearte-server `12b8ffb` (0.15.0 — migration 0011 tombstone column; AccountService deletion kernel: password re-check, works delisted, drafts removed with best-effort object delete, pending submissions auto-rejected, sessions invalidated via token_version, email/username rewritten to `deleted-<id>@deleted.invalid`/`gone-<id8hex>` so the original address is immediately reusable, sole-admin self-deletion refused 409 `last_admin` shared with the P7 demote guard; `DELETE /api/auth/account` (204/400/401/409/410, `Cache-Control: no-store`); CLI `user delete <email>` passwordless operator path; CLI `catalog export --out DIR [--pretty]` emitting the exact StaticContentRepository contract — `index.json` schemaVersion 2 + `games/<user>__<slug>.json`, virtual entries carry the bundle object, external entries omit it, cover/bundle.url empty when `STORAGE_S3_*` unset; `user set-role` gets `--by-id`) and crearte `7d5f338` (0.21.0 — AccountView fourth 「危险区:注销账号」 section: password + acknowledgement checkbox gate, busy lock, success → invalidate session → home, failure → alert row; `auth/client.ts` `deleteAccount` with the code-branching `toUserMessage`; AdminUsersView now localizes validation/not_found/internal (P7 copy tail pinned by tests, `7a54152`) and the 409 last_admin echo stays on the admin side — the account view deliberately shows the generic internal copy because the auth client normalizes unknown codes to `internal` and drops the backend message; review note about `AuthApiError` vs `UserManagementApiError` shape resolved in-test). crearte-deploy `12f7c10` (0.6.0) and crearte-server `d627e12` (0.16.0) reland P3 byte-for-byte via cherry-picks `7fcfe0e`/`6ffd1ee`/`93a3c70` whose originals had drifted off master during the rollback window: prod backup tooling (`scripts/backup.sh` pg_dump + mc-mirror with retention and manifest, `restore-drill.sh` against `db-test` only, `docs/BACKUP-RUNBOOK.md`, CI dry-run leg, compose `LOG_FORMAT`/`LOG_LEVEL` passthrough) and observability (stdlib `log` → `slog` mechanical migration across 30 sites incl. the P7/P8b2-new-surface files, per-request `http_request` middleware wired outside `gin.Recovery` so panic paths log status=500, hand-rolled `/metrics` exposition with http/go/pgpool families, `/healthz`+`/metrics` double-skipped, single-instance rate-limit tradeoff documented). P3M merge commit `cecfae1` reconciled the observability branch with the P8b2 surface (CHANGELOG top order 0.16.0→0.15.0, router/serve both-side wiring). Acceptance: server container four-green + fresh real-DB `-p 1 -count=1` (skip-guard grep 0, deletion-chain + observability suites PASS) on every leg; independent reviews PASS (P3S reviewer verified byte-level identity against `6ffd1ee`/`93a3c70`; frontend review returned PASS with two minor notes, pinned before merge as `7a54152`; server-side LOG_LEVEL case-sensitivity divergence between `config.applyLogging` and `observability.newLogger` deferred as a documented nit); branch-build curl smoke 14/14 PASS (wrong-pw 401 `invalid_credentials`, missing-pw 400, sole-admin 409 `last_admin` after verifying token rotation on role change, correct-pw 204 + `no-store`, old token 401, tombstone row fields, original email re-register 201, CLI `user delete` output shape, double-delete rejected, export schemaVersion 2); live-stack metrics smoke on the merged tree (request/duration counters with `nomatch` bucket, pgpool + go families, HELP/TYPE headers, both skips, slog JSON request lines). FE vitest 512 / vue-tsc clean / main e2e 71+1skip / noauth 4. Remaining manual tail for the owner: schedule `backup.sh` (cron or the runbook's systemd timer) and run `restore-drill.sh` against the real prod stack once it is up — this host has no prod stack running, so the drill leg ran against throwaway containers only. Roadmap: P0–P8 all closed; P8 remainder is ④'s render-side consumption (on demand) and nothing else is 待启动.
- 路线图收口波次交付——P8 第二批(③账号注销 + ④静态兜底目录导出)与 P3 重落(备份 + 可观测),依 owner 第三条指令「实现 roadmap 所有待办事项」。子仓提交:crearte-server `12b8ffb`(0.15.0——迁移 0011 墓碑列;AccountService 注销内核:密码复核、名下作品全部下架、草稿删除含对象 best-effort、pending 投稿自动驳回、token_version 递增废全部会话、email/username 改写为 `deleted-<id>@deleted.invalid`/`gone-<id8hex>` 使原邮箱即刻可复用、唯一 admin 自拒 409 `last_admin`(与 P7 降级护栏同源);`DELETE /api/auth/account`(204/400/401/409/410,`Cache-Control: no-store`);CLI `user delete <email>` 免密码运维通道;CLI `catalog export --out DIR [--pretty]` 按 StaticContentRepository 契约出 `index.json`(schemaVersion 2)+ `games/<user>__<slug>.json`,virtual 带 bundle、external 省略、未配 `STORAGE_S3_*` 时 cover/bundle.url 置空仍可浏览;`user set-role` 加 `--by-id`)与 crearte `7d5f338`(0.21.0——AccountView 第四段「危险区:注销账号」:密码 + 知晓勾选双闸门、busy 锁、成功即清会话回首页、失败出 alert 行;`auth/client.ts` 加 `deleteAccount` 并分支 `toUserMessage` 文案;AdminUsersView 补齐 validation/not_found/internal 中文化(P7 文案尾,`7a54152` 钉桩),409 原文回显留在管理侧——账号页有意展示通用 internal 文案,因 auth client 将未知码归一并丢弃后端原文;审查提出的错误类型形状问题已在测试内钉清)。crearte-deploy `12f7c10`(0.6.0)与 crearte-server `d627e12`(0.16.0)以 cherry-pick `7fcfe0e`/`6ffd1ee`/`93a3c70` 字节级重放 P3——原提交在回滚窗口脱离 master:prod 备份件(`scripts/backup.sh` pg_dump + mc-mirror + 保留策略 + manifest、`restore-drill.sh` 只连 `db-test`、`docs/BACKUP-RUNBOOK.md`、CI dry-run 腿、compose 透传 `LOG_FORMAT`/`LOG_LEVEL`)与可观测(stdlib `log` 机械迁 `slog` 共 30 处含 P7/P8b2 新面、每请求 `http_request` 中间件挂在 `gin.Recovery` 外层使 panic 路径记 status=500、手写 `/metrics` 出 http/go/pgpool 三族、`/healthz` 与 `/metrics` 双跳过、限流单实例权衡入档)。P3M 合流提交 `cecfae1` 把可观测分支与 P8b2 面调和(CHANGELOG 顶序 0.16.0→0.15.0、router/serve 双侧接线保留)。验收:每腿容器四连绿 + 全新真库 `-p 1 -count=1`(skip 守卫 grep 0,注销链与 observability 套件 PASS);独立审查 PASS(P3S 审查员逐文件对 `6ffd1ee`/`93a3c70` 核到字节级一致;前端审查 PASS with notes——两条次要已合主前钉桩 `7a54152`;server 侧 `config.applyLogging` 与 `observability.newLogger` 的 LOG_LEVEL 大小写策略不一致按次要 deferred);分支真产物 curl 冒烟 14/14 PASS(错密码 401 `invalid_credentials`、缺密码 400、角色变更后先验 token 轮换再证唯一 admin 409 `last_admin`、对密码 204 + `no-store`、旧 token 401、墓碑行字段、原邮箱重注册 201、CLI `user delete` 输出形状、二次注销被拒、导出 schemaVersion 2);合主树实栈 metrics 冒烟(请求/耗时计数含 `nomatch` 桶、pgpool 与 go 族、HELP/TYPE 头、双跳过、slog JSON 请求行)。FE vitest 512 / vue-tsc 零错 / 主 e2e 71+1skip / noauth 4。留给 owner 的手动尾巴:按 runbook 给 `backup.sh` 排定时(cron 或 systemd timer),并在真 prod 栈起来后跑一次 `restore-drill.sh`——本机无 prod 栈在跑,演练腿只对一次性容器做过。路线图:P0–P8 全部闭合;P8 仅剩④的 render 端消费(按需),无待启动项。
## [0.3.1] - 2026-10-01
### Done / 完成
- P9 UX batch-1 (browser feedback pack) delivered: crearte `26cd625` (0.22.0 — two-phase router-level `document.title`: `afterEach` static baseline over the full 19-name route table + refinement watches on exactly four data pages (game/catalog/docs/author), home deliberately kept as the bare `crearte 创艺` string so the existing e2e pin never moved; game-page `meta description` with 120-codepoint clipping, DEFAULT fallback and per-navigation reset back to default (review ISSUE-1 patched pre-merge, pinned by a router test); `StatePanel` skeletons honest at last — card variant 3→6 (≥1.5 rows at each 2/3/4-column breakpoint), new `lines` variant on the six non-grid consumers (eight instances incl. AdminView ×3), index.html gains the default description meta). Zero server/deploy changes. Acceptance: vitest 551 (baseline 512 + 39 new) / vue-tsc clean / build OK / main e2e 72+1skip incl. a new title leg / noauth 4. Independent review PASS with notes — ISSUE-1 (leave-game description not restored, spec D-C sub-clause) and ISSUE-2 (spec §1 said "seven non-grid pages", CreatorCenterView does not consume StatePanel — count is six; wording fixed in spec and changelog) both resolved before merge. B (keyboard/skip-link/mobile header) and C (dark mode) batches parked pending owner go-ahead.
- P9 UX 第一批(浏览器反馈包)交付:crearte `26cd625`(0.22.0——两段式路由级 `document.title`:`afterEach` 按 19 个路由名全表设静态基线 + 恰好四个数据页(作品/目录/文档/作者)watch 精化;home 有意保持裸 `crearte 创艺` 整串,既有 e2e 钉桩一字未动;作品页 `meta description` 截 120 码点、默认回落、每次导航复位(审查 ISSUE-1 合主前补钉并以 router 测试钉桩);`StatePanel` 骨架终诚实——cards 变体 3→6(2/3/4 列断点各≥1.5 行)、新增 `lines` 变体给六个非网格消费页(八实例含 AdminView×3);index.html 补默认 description)。server/deploy 零改动。验收:vitest 551(基线 512+新增 39)/ vue-tsc 零错 / build OK / 主 e2e 72+1skip(含新增标题腿)/ noauth 4。独立审查 PASS with notes——ISSUE-1(离开作品页 description 未恢复默认,spec D-C 子项)与 ISSUE-2(spec §1 写「7 个非网格页」,实际 CreatorCenterView 不消费 StatePanel,应为六个;spec 与 CHANGELOG 措辞已修正)均合主前处置。B(键盘/skip-link/移动端页头)与 C(暗色模式)两批挂账待 owner 发令。
## [0.2.9] - 2026-10-01
### Chores / 杂务
- crearte-deploy `150927e` (0.5.2): `.gitignore` gains `backups/` so P3 backup-script snapshots (e.g. `backups/prod/20260930T095727Z/`) stop polluting `git status`; the on-disk snapshot is untouched.
- crearte-deploy `150927e`(0.5.2):`.gitignore` 新增 `backups/`,P3 备份脚本的本机快照(如 `backups/prod/20260930T095727Z/`)不再污染 `git status`;盘上既有快照原样保留。
## [0.2.8] - 2026-10-01
### Done / 完成
- P8 batch-1 (triage + copy + feature-switch UI) delivered: crearte-server `921443a` (0.14.0 — `games.Detail` 400 messages split into `user: invalid format` / `slug: invalid format` with user precedence pinned by assertion (status/error codes unchanged); four admin work routes (unpublish/republish/features/revoke) validate :user/:slug at handler entry via shared `validWorkParams`, malformed ids now 400 `invalid work id` instead of a misleading 404 (`adminWorkID` untouched); new gated concurrency test `TestApproveSameWorkIDConcurrent` — two same-work_id new_work approvals, exactly one winner, loser gets ErrWorkIDTaken/ErrSubmissionConflict, one row; CHANGELOG header de-templatized) and crearte `70ba10c` (0.20.0 — CSP editing UI opened to the full seven-key set: inlineStyle/wasm/coop/fullscreen/gamepad added to EditableFeatures/EMPTY_FEATURES/FEATURE_ITEMS/collectFeatures/featuresToForm/hasAnyFeature with Chinese label+hint per switch (risk + scenario spelled out); all three surfaces stay driven by the single FEATURE_ITEMS list — submission form, AdminView expanded row, AdminSubmissionView read-only; payload always carries seven keys preserving the missing-key-keeps-stored-value semantics; features/SubmitFormView/AdminSubmissionView tests rewritten and added; CHANGELOG header de-templatized). Zero deploy changes, zero migrations, zero new deps; runtime flag consumers untouched. Acceptance: server container four-green + fresh-empty-DB -count=1 integration (race test ran, skip-guard grep 0) + `-race` leg PASS (two concurrency tests, no data races — needed the Debian golang:1.24 image, alpine lacks gcc for cgo); curl smoke on a live branch build: detail 400s distinguished, both-bad → user wins, all four admin routes 400 on malformed slug/user, legal-but-missing id still 404; FE vitest 499 / typecheck / admin-flow 7 / main suite 70+1skip (first main-suite decrypt failure was a stale serve-runtime process left by a dead subagent — killed it, rerun green). Dual independent review: crearte PASS (notes: gamepad hint leans on its label for the “what is loosened” clause, accepted); server PASS with notes (only note — user-precedence not pinned by test — fixed as `ee5b960` before merge). Lesson landed: reviewers briefed with an explicit “spec §1–§3 full-text, not just the task brief” clause (P6 D-D gap). Task-2 implementer died mid-run; its uncommitted worktree was verified against spec, pinned as `54b1f82` with a full-message commit rather than redispatched. Roadmap: P8 second batch (③ account deletion awaiting product call, ④ static fallback dir on-demand) remains.
- P8 第一批(triage 小项 + 文案 + 权限开关 UI)交付:crearte-server `921443a`(0.14.0——`games.Detail` 400 文案拆为 `user: invalid format` / `slug: invalid format`,双非法 user 优先已用断言钉桩(状态码/错误码不变);四条 admin 作品路由(unpublish/republish/features/revoke)handler 入口经共享 `validWorkParams` 校验 :user/:slug,非法 id 改返 400 `invalid work id` 不再误导成 404(`adminWorkID` 未动);新增门控并发测试 `TestApproveSameWorkIDConcurrent`——同 work_id 两份 new_work 并发审批,恰一条成功、败者 ErrWorkIDTaken/ErrSubmissionConflict、库中恰一行;CHANGELOG 头部去模板腔)与 crearte `70ba10c`(0.20.0——CSP 编辑 UI 开放七键全集:inlineStyle/wasm/coop/fullscreen/gamepad 入 EditableFeatures/EMPTY_FEATURES/FEATURE_ITEMS/collectFeatures/featuresToForm/hasAnyFeature,每开关中文 label+hint 写清风险与适用场景;三处界面仍由单一 FEATURE_ITEMS 驱动——投稿表单、AdminView 展开行、AdminSubmissionView 只读;载荷恒含七键,保住「缺键→保留原值」语义;三套测试改写/新增;CHANGELOG 头部去模板腔)。deploy 零改动、零迁移、零新依赖,运行时 flag 消费端未碰。验收:server 容器四连绿 + 空库 -count=1 集成全绿(竞态测试实跑,skip 守卫 grep 0)+ `-race` 腿 PASS(两并发测试零数据竞争;alpine 无 gcc 需换 Debian golang:1.24 镜像);分支真产物 curl 冒烟全中:detail 400 可区分、双非法 user 赢、admin 四路非法 slug/user 全 400、合法格式不存在仍 404;FE vitest 499 / typecheck / admin-flow 7 / 主套件 70+1skip(首轮 decrypt 一腿失败系夭折子代理残留的旧 serve-runtime 进程污染环境,杀进程复跑绿)。双路独立审查:crearte PASS(注:gamepad hint 的「放宽了什么」靠 label 承载,可接受);server PASS with notes——唯一 note(user 优先缺钉桩)已以 `ee5b960` 在合主前补齐。教训落地:审查员任务书显式要求「对照 spec §1–§3 全文而非仅任务书」(P6 D-D 漏项的根治)。Task2 实现者中途夭折——其未提交工作树经逐条对 spec 核验后以完整提交信息钉成 `54b1f82`,未盲重跑。路线图:P8 第二批(③账号注销待产品决策、④静态兜底目录按需)仍待启动。
## [0.2.7] - 2026-09-30
### Done / 完成
- P6 hosted submission delivered as plan A (owner call: author-hosted https URL, embedded play; no file hosting): crearte-server `3e11446` (0.13.0 — D-B runtime immutable on metadata_change, D-C hosted works cannot carry a bundle, unit + fresh-empty-DB integration incl. real-HTTP draft→submit→approve→hosted_url/play_origin/fallback readback and public payload runtime/hostedUrl; zero migrations, zero new deps, Approve mapping untouched — the feared hosted_url write gap was a false alarm, PayloadToWork at import.go:39 already covers it) and crearte `fee5f3b` (0.19.0 — content-layer WorkRuntime three modes with hostedUrl/fallback aligned to server json names, submit-form third radio 自托管内嵌 w/ https + fallback=external⇒url mirror validation, prefill refusal removed and hosted fields backfilled, new_version stays virtual-only; playback side zero changes per spec; vitest 492 / typecheck / admin-flow 6 / main suite 69 green; e2e hosted draft-payload flow added). Dual independent review PASS with notes; six-leg host acceptance green (incl. branch-backend full-loop 1 passed; first admin-flow 1-passed reading was stack-contention false alarm, rerun 6 passed RC=0). T5 CSP probe: deploy templates carry no main-site frame-src (the two `frame-src 'none'` hits belong to runtime-subdomain pages only) — zero deploy changes on record. Known gaps: D-D (admin hostedUrl display row) delivered same day as crearte 0.19.1 `9bd9372` — maintainer-direct follow-up patch (Task 2 brief had omitted the row; reviews passed against the brief rather than full spec §3 — lesson: review against the spec). Plain-text row, deliberately no anchor, plus fallback label row; four legs rerun green (vitest 492 / typecheck / admin-flow 7 / main suite 70). Remaining: new_version hosted rejection is server-side only (FE no hard check, spec-accepted). See spec D-A…D-F.
- P6 hosted 作品投稿以方案 A 交付(owner 拍板:作者自部署、投稿只存 https URL、站内沙箱 iframe 播;不做文件托管):crearte-server `3e11446`(0.13.0——D-B metadata_change 禁改 runtime、D-C hosted 禁带 bundle;单测 + 空库集成含真 HTTP 草稿→提交→过审→hosted_url/play_origin/fallback 读回与公开载荷 runtime/hostedUrl;零迁移零新依赖,Approve 落库未动——此前担心的 hosted_url 落库缺口系误报,PayloadToWork 本就覆盖)与 crearte `fee5f3b`(0.19.0——内容层三档 + hostedUrl/fallback 字段与 server json 名逐字对齐;表单第三 radio「自托管内嵌」+ https 校验 + fallback 默认 external 镜像校验;预填拒绝分支删除并回填;new_version 仍 virtual-only;播放端零改动;vitest 492 / typecheck / admin-flow 6 / 主套件 69 绿;e2e 新增 hosted 草稿载荷流)。双路独立审查 PASS with notes;六腿本机验收全绿(含分支后端真栈 full-loop 1 passed;admin-flow 首轮 1 passed 为栈腿争用假警报,复跑 6 passed RC=0 坐实)。T5:deploy 模板无主站 frame-src 下发(两处 'none' 属运行时子域页),零改动记录。遗留:D-D(审核面 hostedUrl 展示行)当日补交 crearte 0.19.1 `9bd9372`——控制者直改小补丁(Task 2 任务书漏列该条;审查按任务书对照未扯出——教训:审查依据应为 spec §3 全量)。纯文本无锚点行+降级中文标签行,四腿复跑全绿(vitest 492 / typecheck / admin-flow 7 / 主套件 70)。仅剩:new_version 拒 hosted 仅 server 拦(前端无硬校验,spec 已接受)。选型见 spec D-A…D-F。
## [0.2.6] - 2026-09-30
### Done / 完成
- P7 admin console delivered: crearte-server `ca24805` (0.12.0 — audit_log table via migration 0010, audit middleware on a consolidated admin route group (owner call: no more per-route RequireAdmin), gin FullPath templates recorded for every admin request incl. GETs and failed attempts while unauthorized 401/403 stays unrecorded; user list / PATCH role / audit query endpoints; last-admin demotion guard 409 enforced in the shared service so CLI and API both obey; token invalidation on role change verified end-to-end) and crearte `900f13a` (0.18.0 — /admin/users with search/pagination/demote-confirm dual-point copy incl. 409 echo, /admin/audit with localized time, zh action labels w/ fallback, status coloring, route filter; apiRepo admin trio with AdminApiError; e2e admin-flow extended w/ self-recording semantics; vitest 488 / main suite 68 green). Host acceptance: six legs all green incl. fresh-empty-DB -count=1 integration (10 ok, zero FAIL/skip) and real-stack full-loop 1 passed ×2 against the branch backend. See spec §2 decisions D-A…D-D.
- P7 管理后台增强交付:crearte-server `ca24805`(0.12.0——迁移 0010 audit_log;审计中间件挂收敛后的 admin 路由组(owner 定稿:不再逐条手挂),admin 面全请求入史含 GET 与失败尝试(FullPath 模板),未遂 401/403 不入史;用户列表/PATCH 角色/审计查询三端点;唯一 admin 降级 409 护栏落在共享 service,CLI/API 双覆盖;角色变更废 token 端到端验证)与 crearte `900f13a`(0.18.0——/admin/users(搜索/分页/降级确认双要点文案含 409 原文回显)+ /admin/audit(本地化时间/中文动作标签+未知回退/状态着色/route 过滤);apiRepo 管理面三方法+AdminApiError;e2e admin-flow 扩两流含自记录语义;vitest 488 / 主套件 68 绿)。本机验收六腿全绿:容器四连、空库 -count=1 集成 10 ok 零 FAIL 零 skip、护栏矩阵(非唯一降级 OK / 唯一降级 CLI 拒 + API 409)、废 token 401、审计流水含失败尝试、分支后端真栈 full-loop 1 passed ×2。选型见 spec §2 D-A…D-D。
## [0.2.5] - 2026-09-30
### Docs / 文档
- Creator center `/creator` delivered (maintainer-direct need, plan 2026-09-30-creator-center.md executed): pure-frontend crearte 0.17.0 — new route + homepage-style view (hero, auth-state-aware work-data placeholder card, tutorial placeholder card linking to the submission guide) and a third header nav tab; merged & pushed as crearte `e2fab8a` after TDD e2e (4 new default cases + 1 appended noauth case) and a 6-lens review; server/deploy untouched by design.
- 创作者中心 `/creator` 交付(维护者直接需求,计划 2026-09-30-creator-center.md 已执行):纯前端 crearte 0.17.0——新路由 + 首页样式视图(hero、按登录态分流的作品数据占位卡、链向现有投稿指南的教程占位卡)与页头第三个导航 tab;TDD e2e(默认配置新增 4 用例 + noauth 追加 1 断言)与 6 透镜审查后以 crearte `e2fab8a` 合并推送;后端/部署仓按设计零改动。
## [0.2.4] - 2026-09-30
### Docs / 文档
- Roadmap P2 delivered: GitHub Actions baseline in all three repos — crearte-server `validate.yml` (check + empty-DB Postgres integration with a silent-skip guard + real-stack e2e-stack hosting the cross-repo checkout pair), crearte `validate.yml` (push:master trigger) plus `e2e-stack.sh` GO/REQUIRED env knobs, crearte-deploy `validate.yml` (compose config across four profiles with a negative empty-password guard test). First real-stack run uncovered a serve-runtime defect: the runtime triple (/__bootstrap, /sw.js, /agent.js) was gated behind the fixtures subdomain table, so live-registered games could never install the SW — fixed with a position-only move (crearte `5e0fb2e`). Merged & pushed 2026-09-30: server `7b97108` / crearte `711b6de` / deploy `522545d`; Actions enablement and branch protection are owner-manual tails (spec §2.4).
- 路线图 P2 交付:三仓 GitHub Actions 基线——crearte-server `validate.yml`(check + 空库 Postgres 集成层带静默 skip 守卫 + e2e-stack 真栈 job 宿主双仓 checkout)、crearte `validate.yml`(push:master 触发)及 `e2e-stack.sh` 的 GO/REQUIRED 环境变量开关、crearte-deploy `validate.yml`(四 profile compose config 正路 + 空密码反路守卫断言)。真栈首跑拓出 serve-runtime 缺陷:运行时三件套(/__bootstrap、/sw.js、/agent.js)被卡在夹具子域表门槛之后,活体注册的作品永远装不上 SW——已以纯位置搬移修复(crearte `5e0fb2e`)。2026-09-30 合并推送:server `7b97108` / crearte `711b6de` / deploy `522545d`;Actions 启用确认与分支保护为 owner 手动尾巴(spec §2.4)。
## [0.2.3] - 2026-09-30
### Docs / 文档
- Roadmap P5 delivered: favorites & 5-star ratings went full-stack — server `favorites`/`ratings` tables + `ReactionRepository` with reaction-watermark ETag invalidation and user-space endpoints (crearte-server `e3da246`), and frontend hot sort (Bayesian prior mean + favorite log weight), game-page reaction widget, card badges, account my-reactions section (crearte `eb5fbed`), merged & pushed 2026-09-30 after a 9-step live prod-stack smoke; crearte-deploy untouched by design (scope revision recorded in spec).
- 路线图 P5 交付:收藏与五星评分全栈落地——服务端 `favorites`/`ratings` 新表 + `ReactionRepository`(反应水位驱动 ETag 失效)与用户态端点(crearte-server `e3da246`);前端热门排序(贝叶斯先验均分 + 收藏对数权重)、详情页反应组件、卡片徽标、账号页我的反应(crearte `eb5fbed`),2026-09-30 经 prod 演练栈 9 步冒烟后合并推送;crearte-deploy 按设计零改动(范围修订已记入 spec)。
## [0.2.2] - 2026-09-29
### Docs / 文档
- Roadmap P1 delivered: prod write side went live in the compose drill — minio-prod + api-prod storage/games env + templated nginx wildcard block (crearte `63bb96d` / crearte-deploy `4d53d58`), POSTGRES_PASSWORD default retired, TLS deferred by design with a real-server checklist in README.
- 路线图 P1 交付:prod 写侧在本机演练栈完整启用——minio-prod、api-prod 存储/游玩域名环境、nginx 通配块模板化(crearte `63bb96d` / crearte-deploy `4d53d58`);POSTGRES_PASSWORD 默认值退役;TLS 按设计缓做,真机清单已入 README。
## [0.2.1] - 2026-09-29
### Fixed / 修复
- `clone_all.sh` pins `master` for `crearte` and `crearte-server` instead of the stale `feat/submission-preview` work branch — fresh clones and updates land on the integration branch that carries the merged roadmap work. (Note: `update_repo` switches clean checkouts to the pinned branch; dirty ones are skipped.)
- `clone_all.sh` 中 `crearte` 与 `crearte-server` 的引导分支由过时的 `feat/submission-preview` 工作分支改为 `master` —— 全新克隆与更新落在承载路线图已合并工作的集成分支上。(注意:`update_repo` 会把干净的 checkout 切到钉子分支,脏工作树自动跳过切换。)
## [0.2.0] - 2026-09-29
### Changed / 变更
- Roadmap delivered: P0 (known-defect cleanup — CORS `If-None-Match` fix in crearte-server `a8ea755`, compose key-store drift cleanup in crearte-deploy `27044be`) and P4 (author page `/users/:user`, crearte `f12cbf1`) are merged to their repos' master and pushed; roadmap statuses updated.
- 路线图交付:P0(已知缺陷清理——crearte-server `a8ea755` 的 CORS `If-None-Match` 修复、crearte-deploy `27044be` 的 compose 密钥库遗留清理)与 P4(作者主页 `/users/:user`,crearte `f12cbf1`)均已合并至各仓 master 并推送;路线图状态已更新。
## [0.1.0] - 2026-09-29
### Added / 新增
- Added `docs/ROADMAP.md`: the cross-repo roadmap decomposed into sub-projects P0–P8 (ops foundation → product value → governance), with ordering rationale and a doc index for future specs and plans.
- 新增 `docs/ROADMAP.md`:跨仓库路线图,分解为 P0–P8 子项目(上线底座 → 产品价值 → 治理长尾),含排序理由与后续 spec/计划的文档索引。
### Changed / 变更
- Doc convention: all specs, implementation plans and cross-repo coordination docs now live in the monorepo `docs/` instead of per-repo `docs/superpowers/`; this supersedes the crearte-deploy 0.3.0 "canonical home" convention. Commits in the monorepo wrapper may go directly to `master` (user-approved); the three inner repos keep their own branch rules.
- 文档约定:所有 spec、实现计划与跨仓库协作文档统一收归 monorepo `docs/`,不再散落各内层仓库的 `docs/superpowers/`;此约定取代 crearte-deploy 0.3.0 的「canonical home」约定。monorepo wrapper 可直接提交 `master`(维护者确认),内层三仓仍遵循各自分支规范。