docs: book P15-B as delivered (wrapper 0.3.9, ROADMAP ledger section)

P15-B merged to crearte master as 7f91fa3 (0.27.0, merge-base f823195, thirteen
commits across two fix-forward rounds). P15 is now fully landed.

Three things this entry records that a routine booking would omit.

The whole-branch review blocked the batch - the second time it has done so after
P11 - and its critical finding was in the guard layer rather than the product:
maskSourceComments() compared a two-character slice against the four-character
'<!--', so its HTML-comment branch never ran, producing two false greens and
three false reds and making two already-committed claims false. Product code
needed no change; one added line and one changed comparison closed it.

The second round was executed by the controller rather than the dispatched
subagent, which ran 1h25s and failed with no output, leaving nothing to salvage.
The controller produced one false green of its own on the way - a mutation round
whose anchor failed to match was scored as meeting an expectation that happened
to be an empty list, the same defect the reviewer had caught in itself.

The e2e suite carries a pre-existing flaky leg at roughly one run in three
(core.spec.ts's worker leg: helpers.ts reads an async-postMessage attribute with
a bare getAttribute and no retry). Every file involved has zero diff against the
batch base and all four legs this batch added were green in all three review
runs. Recorded so the next red CI run is not attributed to this release.

ROADMAP also gains a standing ledger section, which the roadmap never had: the
cross-batch items accumulated by P15's two review rounds and the branch review,
each with its measured basis rather than an aspirational note. It includes the
revival of feat/submission-preview - the inline play-test preview for the submit
form and the review page, which the user asked about and which turns out to be
delivered work sitting unmerged in two paired remote branches (crearte a3cb5e3,
crearte-server 6b072d6, 27 tests, zero residue on master), together with the two
hard collisions that make reviving it a real batch rather than a rebase.
This commit is contained in:
2026-10-04 04:14:09 +08:00
parent 51bed55704
commit d70991b0ad
2 changed files with 50 additions and 2 deletions
+25
View File
@@ -6,6 +6,31 @@ 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.9] - 2026-10-04
### Done / 完成
- P15-B lands in crearte (0.27.0, merge `7f91fa3`, merge-base `f823195`, 13 commits): the game loading screen now follows the theme, closing the last white-flash surface — a pre-paint script in `<head>` reads hash, then `prefers-color-scheme`, then light (one level fewer than the main app, because games run on a separate subdomain where browser storage is origin-isolated), and the parent passes the theme as an optional ninth hash key while `GameHost` injects a non-reactive `effectiveTheme()` snapshot so switching theme cannot reload a game in progress. Also pinned `AA_PAIRS` with `['ink','surface']` (62 `.vue` sites, 58 of them inheriting `text-ink` from `body`, contrast 18.4225 light / 14.8468 dark) and deleted the dead `--color-info` token (`REQUIRED_KEYS` 12→11, `LEGACY_KEYS` 9 untouched, usage-side `var(--color-*)` unchanged at 75 — a movement there would have meant something referenced it, contradicting the dead-token premise).
- P15-B 在 crearte 落地(0.27.0,merge `7f91fa3`,merge-base `f823195`,13 个 commit):游戏加载屏现在跟随主题,消除了最后一处白闪面——`<head>` 内的 pre-paint 脚本按 hash → `prefers-color-scheme` → light 判定(比主应用少一级,因为游戏跑在独立子域、浏览器存储按源隔离),父侧把主题作为可选的第九个 hash 键传下去,而 `GameHost` 注入的是非响应式的 `effectiveTheme()` 快照,故切主题不会重载正在进行的游戏。同时给 `AA_PAIRS` 补钉 `['ink','surface']`(62 处 `.vue`,其中 58 处靠 `body` 继承 `text-ink`,对比度亮色 18.4225 / 暗色 14.8468),并删掉死令牌 `--color-info`(`REQUIRED_KEYS` 12→11、`LEGACY_KEYS` 9 不动、usage 侧 `var(--color-*)` 保持 75——它若变了反倒说明有东西在引用该令牌,与死令牌前提矛盾)。
- **The whole-branch review blocked this batch — the second time it has done so, after P11 — and the critical finding was in the guard layer, not the product.** All four gates were green, every boundary held, and the product code needed no change at all. What failed was the mechanism the batch exists to deliver: `maskSourceComments()` took a 2-character slice and compared it against the 4-character `'<!--'`, so its HTML-comment branch never ran while the `//` and `/* */` branches worked. Measured on the shipped guard: two false greens (a fake `:root` palette block hidden in an HTML comment let a real light-theme fork and a real dark-theme WCAG violation at 2.5519 pass; the same block in a CSS comment reddened leg 7) and three false reds (an explanatory example in a comment reddened leg 8; **merely rewording a comment to mention `<style>` and `<body>` reddened legs 1–4 while the e2e suite stayed 9/9 green, so what reddened was the guard, not the implementation**). One added line plus one changed comparison closes all of it.
- **全分支终审拦下了这批——继 P11 之后第二次——而 critical 发现在守卫层、不在产品里。** 四门全绿、每条边界都守住、产品代码一行都不用改。出问题的是这批存在的目的本身:`maskSourceComments()` 取 2 字符切片去和 4 字符的 `'<!--'` 比较,于是它的 HTML 注释分支从未执行,而 `//` 与 `/* */` 两个分支正常。在交付守卫上的实测:两处假绿(藏在 HTML 注释里的假 `:root` 调色板块让一次真实的亮色分叉与一次对比度 2.5519 的真实暗色 WCAG 违规通过;同款块放 CSS 注释里则让腿 7 变红)、三处假红(注释里一句说明性示例让腿 8 变红;**仅仅把注释措辞改成提到 `<style>` 与 `<body>` 就让腿 1–4 变红,而 e2e 套件 9/9 全绿——所以红的是守卫,不是实现**)。一行新增加一处比较对象改动就闭合了全部。
- It also overturned two claims already committed to the spec and the adjudication: that the guard "does not depend on the implementer's wording discipline", and that an earlier critical fix was safe because comment masking had neutralised a trap comment. The second was true of that file, but only because the trap comment happened to use `//` — the fix's *direction* was right and it did catch the variants it was written for, while the *argument* for its safety rested on a generalisation that held for one carrier. That is the same shape P14's branch review found (the hole in the first round's own fix), now recurring. Two more universal/causal claims fell: "the only discriminating grid" (a real decision-order defect reddens two legs, and the phrase survived re-pin in four places — one of them written by the same child that had declared it would not use the word "only"), and a causal claim about what `snap.url === src` pins (renaming the parent's hash key `theme`→`t` leaves it green because both sides change together; it pins that the child stayed at the url the parent computed, which legs 1/2 catch for key names).
- 它还推翻了两处已写进 spec 与裁定的声称:「本守卫不依赖实现侧的措辞自律」,以及上一轮某个 critical 修法之所以安全是「因为注释屏蔽已中和了陷阱注释」。第二句对那个文件是真的,但成立原因只是陷阱注释恰好用的是 `//`——修法的**方向**是对的、也确实抓到了它要抓的那些变体,而它的**安全论证**依赖一个只对一种载体成立的泛化。这与 P14 全分支终审发现的形态相同(洞在第一轮修复自身里),现在再次复现。另有两处全称/因果声称倒下:「唯一鉴别格」(一次真实的判定序缺陷会让两条腿变红,而这个措辞在 re-pin 后仍残留在四处——其中一处出自那个声称自己不会用「唯一」这个词的子代理),以及关于 `snap.url === src` 钉什么的因果声称(把父侧 hash 键名 `theme` 改成 `t` 时它仍绿,因为两侧同步变化;它钉的是子文档停在父侧算出的那个 url,而键名一致性由腿 1/2 覆盖)。
- **The second round was executed by the controller, and that deviation is recorded rather than smoothed over.** The dispatched child ran 1h25s and failed with no output; a full wreckage inventory found nothing to salvage — no edits landed (HEAD unchanged, worktree clean, commit count still eleven), no report, no new evidence files, no working copy, no listening ports, no orphan stacks, and its transcript showed it never left preflight. With the review having supplied a literal diff already measured at 700 passed / 0 failed, the remaining work was mechanical, so the controller applied it and did the verification itself: four gates, eleven mutation rounds including the G series the reviewer had not run under the fix (all ten matching expectation, with a printed self-check that nine rounds actually mutated), a runtime probe inlining the shipped helper verbatim into a `.ts` file, and a 75-check acceptance harness. The controller produced one false green on the way — a round whose mutation anchor failed to match was scored as "meets expectation" because that expectation happened to be an empty list — the same defect the reviewer had caught in itself, fixed by scoring any errored round as a failure.
- **第二轮由控制者本人执行,这个偏离被如实记录而不是抹平。** 派出的子代理跑了 1h25s 后 failed 且零输出;完整的 wreckage 盘点发现无物可救——没有任何编辑落地(HEAD 未变、工作树干净、commit 数仍是 11)、没有报告、没有新证据文件、没有工作副本、没有监听端口、没有残留栈,而它的会话记录显示它从未离开 preflight 阶段。由于终审已给出逐字 diff 并实测过它 700 通过 / 0 失败,剩余工作是机械的,故控制者直接应用并自己做验证:四门、十一轮 mutation(含终审在修法下没跑过的 G 系列;十轮全部符合期望,并打印「九轮确实造成了变异」作为自证)、一个把交付守卫函数逐字内联进 `.ts` 的运行时探针、以及一个 75 条断言的验收 harness。控制者途中也产出过一次假绿——某轮的 mutation 锚点没匹配上,却因为它的期望恰好是空列表而被算成「符合期望」——正是终审在自己身上抓到过的同一缺陷;修法是把任何 error 轮一律判失败。
- ⚠️ **The e2e suite carries a pre-existing flaky leg at a 1-in-3 rate, and this batch is not its cause.** Across four runs (three by the branch review, one by the controller) `e2e/core.spec.ts:14 › worker 与 importScripts 可加载` failed once; the controller's own run was 111 green and **all four legs this batch added were green in all three review runs**. Root cause predates the batch: `helpers.ts`'s `frameDataset()` reads an attribute that a worker's asynchronous `postMessage` sets, using a bare `getAttribute` with no auto-retry, while `openGame()` only waits for the synchronous `data-ready`. Every file involved has zero diff against the batch base. `helpers.ts` is on the do-not-touch list, so the fix is ledgered: switch to `expect(locator).toHaveAttribute(...)`, which retries. **Written down so the next red CI run is not misattributed to P15-B.**
- ⚠️ **e2e 套件有一条既有 flaky 腿,概率约 1/3,而本批不是它的成因。** 在四次跑批里(终审三次、控制者一次)`e2e/core.spec.ts:14 › worker 与 importScripts 可加载` 失败过一次;控制者自己那次是 111 全绿,且**本批新增的四条腿在终审的三轮里全部绿**。根因早于本批:`helpers.ts` 的 `frameDataset()` 用**裸 `getAttribute`(无自动重试)**去读一个由 worker 异步 `postMessage` 落地的属性,而 `openGame()` 只等同步的 `data-ready`。涉及的每个文件对批次 base 的 diff 都是 0 行。`helpers.ts` 在禁触清单上,故修法挂账:改用自带重试的 `expect(locator).toHaveAttribute(...)`。**写下来是为了下次 CI 变红时不被误归因给 P15-B。**
- Spec and adjudication work landed in `51bed55` (eleven corrections closing the review's document-side findings: the leg-8 cell now describes the implemented attribute-extraction approach and deliberately carries **no regex literal**, because escaping an alternation bar inside a markdown table cell turns it into a literal bar in the regex — the controller's re-pinned version failed 4 of 7 attribute forms, worse than the 2 of 7 in the ruling it was copied from, and it had checked pipe counts per row without checking that the escaped code was still the code it came from; plus leg 9's un-re-pinned threshold, two uniqueness claims, the boundary table's missing e2e file, a count without its tree stated, D-G's imprecise "aligns verbatim", and two claims limited to their actual scope) and two new disciplines: every evidence file a report cites must be `ls`-checked before delivery (this caught the fixer's own fabricated evidence and the controller's fake-rigour artifact, both times by checking whether the file existed rather than whether the conclusion was right — and both conclusions happened to be right), and executable details in a spec must be grepped from the code entity or recomputed, never hand-written or transcribed unverified (three instances this batch, one of them a contrast figure quoted twice by two documents and recomputed by nobody: 3.0269 cited, 2.5519 actual, which no grey background produces).
- spec 与裁定的修正落在 `51bed55`(十一处,闭合终审的文档侧 findings):腿 8 那一格现在描述已实现的属性值提取法,并**刻意不含正则字面量**——因为在 markdown 表格单元格里转义竖线会让它在正则中变成字面量竖线,控制者 re-pin 的那版在 7 种属性形态里失配 4 种,比它所抄的那份裁定的 2/7 更差,而控制者检查了每行竖线数与表头相同、却没检查转义后的代码是否还是原来那段代码;另有腿 9 从未 re-pin 的阈值、两处唯一性声称、边界表漏列的 e2e 文件、一个没说明取哪棵树的计数、D-G 不精确的「逐字对齐」,以及两处被限定到实际适用范围的声称。并新增两条纪律:报告引用的每一份证据文件,交付前须逐个 `ls` 核在盘上(这条两次生效——抓到 fixer 自己的假取证与控制者的假严谨工件,两次都是靠核「文件在不在」而非「结论对不对」,而两个结论恰好都是对的);spec 里的可执行细节必须从代码实体 grep 出来或自己复算,不得手写或直接转录未经核实的数字(本批三例,其中一例是被两份文档各引用一次而无人复算的对比度:引用 3.0269、实为 2.5519,没有任何灰底能得出被引用的那个值)。
- Ledger entries added: the `frameDataset` retry fix above; leg 3 and leg 5 are both two-sided-imprecise literal pins and should be made semantic together (leg 5 misses five type-legal harmful forms that the behaviour leg catches — bracket access, `unref`, an unconditional read inside `adapters.ts` which it does not scan, provide/inject, and rewriting the url after the call site keeps its literal — while false-reddening two harmless ones); two `expect.poll` timeouts of 20s should drop to 10s so the end-to-end leg's worst path fits the 45s budget; the service worker's self-healing reinstall path drops the `theme` key (`RuntimeMeta` has no such field; 8 hash keys rebuilt versus the adapter's 9); three dead utility rules in the bundle whose values are ellipsis characters rather than hex, originating from placeholder literals inside a comment in the very guard that forbids hardcoded colours; the masker's `/*` branch can swallow string literals containing `/*` (no scanned file currently has that form); a second inline script now needs a CSP nonce, which belongs to the deployment repo's pending reverse-proxy work; and the `GameHost` colour clash as a design decision.
- 挂账新增:上述 `frameDataset` 的重试修法;腿 3 与腿 5 都是双向都不精确的字面量钉桩,应当一并语义化(腿 5 漏掉五类类型合法的有害形态、由行为腿兜住——方括号取值、`unref`、`adapters.ts` 内的无条件求值(它不扫该文件)、provide/inject、以及在调用点保留字面量之后改写 url——同时误红两类无害形态);两个 20s 的 `expect.poll` 超时应降到 10s,好让端到端腿的最坏路径落进 45s 预算;service worker 的自愈重装路径丢掉 `theme` 键(`RuntimeMeta` 没有该字段;重建 8 个 hash 键而适配器是 9 个);产物里三条死 utility 规则,其值是省略号字符而非 hex,来源是那个禁止硬编码颜色的守卫自己注释里的占位符字面量;屏蔽器的 `/*` 分支会吃掉含 `/*` 的字符串字面量(当前被扫的四个文件都无此形态);现在第二处内联脚本需要 CSP nonce,它属于部署仓待办的反向代理工作;以及作为设计决策的 `GameHost` 撞色。
## [0.3.8] - 2026-10-04
### Done / 完成
+25 -2
View File
File diff suppressed because one or more lines are too long