Skip to content

0.6.0 の後始末 4 件——赤の帰属・mock の裏口・node の固定・台帳 - #94

Merged
terapyon merged 14 commits into
mainfrom
integration/after-0-6-0
Aug 29, 2026
Merged

0.6.0 の後始末 4 件——赤の帰属・mock の裏口・node の固定・台帳#94
terapyon merged 14 commits into
mainfrom
integration/after-0-6-0

Conversation

@terapyon

Copy link
Copy Markdown
Owner

計算は 1 行も変わっていません。

git diff origin/main..HEAD -- crates/ corpus/ testdata/ reference/src/ web/src/calc/
→ 空

21 ファイル / +1,601 / −216。すべてテスト・報告書・ワークフロー・台帳・node の固定です。

① 赤の帰属

heavy-corpus.ymlif: always() は「何があっても走る」ので、前提が欠けて
いても走ります。

v0.4.1 の走行 32857987219 で、道具の赤がブラウザ導入を飛ばし、36 本が
Executable doesn't exist で全滅して、1 本の真赤が埋もれました。

直したのは 2 つ。報告書の「測っていない」が 2 つの理由(土台が赤い / 測定が落
ちた)を
区別できなかった
こと、always() が前提の欠落を無視したこと。

着手時の前提が偽でした。 台帳は「再現性が赤でも本番へ出る」と書いていました
が、
落ちた段が 1 つあればジョブは赤く Deploy は skipped——門はジョブ側に既に在
りました。
**穴は「本番へ出る」ではなく「真犯人が埋もれる」**です。打つ手は変わりませんが、
理由づけは書き直しました。

CI の条件式は走行でしか確かめられません。 場面表(6 本)と、番人自身の赤確認
(裸の always() / id の打ち間違いで 4 本赤)まで。次のタグの走行で、飛ばされた段が
想定どおりかを見ます。

vi.mock の裏口(9 件 / 6 ファイル)

web/srcany 0 件ですが、vi.mock のファクトリには戻り値型が無く、TS が何も
見ていませんでした。
0.6.0 の T-3 で実害が出ています——kind を持たないスタ
ブが
9 本、型検査にも実行時にも捕まらないまま緑
でした。

注釈 1 つで塞がります(巻き上げられても型は消えるので参照できる)。
9 本を同時に壊して 9 件が別々に名指しされることを確認済み。論理は 0 行。

③ node を 1 つの版に固定(4 か所)

CI は node-version: 22(そのときの最新)で実際は v22.23.2、手元は v22.14.0
宣言が ci.yml の 1 か所にしかなく、誰も見ていませんでした。

.nvmrc / webheavyengines / setup-web の 4 か所を 22.23.2

同じ綴りなので git grep 22.23.2 で照合できます。

engines だけでは止まりませんpnpm install は警告して exit 0)。
web/.npmrcheavy/.npmrcengine-strict=true で止めます。
副作用: 22.23.2 以外では pnpm install が通らなくなります——承知のうえの判断です。

v22.23.2 は 2026-07-28 リリース、32 日経過。現行 LTS は 24.x ですが、
「揃える」と「上げる」を混ぜないため今回は 22 系に留めました。

CLAUDE.md に 2 項目——道具の版はリリースから 2 週間以上経ってから上げる(これまで口伝)、
node の版は 4 か所に在り同時に上げる。

④ 台帳(239 行 → 122 行)

日付つきの「その日の状態」の節が 5 つ溜まり、ほとんどが嘘になっていました。
書く場所を 3 つに固定し、その規約自体を同じ文書に。引用は行番号でなく段の名
前で。

実測(統合枝の木で)

cargo 395 / wasm-pack 57 / web vitest 375 / heavy vitest 183 / pytest 413 / E2E 192
check:version 0.6.0(5 箇所一致) / check:boundary / check:sw

pnpm heavy    211 passed(31 秒)
heavy:power   18 変異 / 18 が期待どおり(11 分)
heavy:ui      36 passed(11.8 分)・押下 19,849 / 打鍵 1,266 / 指摘 0

検出力は動いていません。minRate にも触れていません(先に実測した 7 値と一
致、
かつ冒頭の git diff が空)。

版数は 0.6.0 のまま。この PR ではリリースしません。

terapyon and others added 14 commits August 29, 2026 15:19
0.6.0 が出荷され、**私の枝は 1 本も残っていない**のに、台帳は「裁定待ち 3 本」と
「0.6.0 でやったこと・枝の一覧」を持ったままだった。**次にクリアされた私が読むと、
存在しない枝を探す。**

**溜まり方のほうを直した。** `0.5` `0.7` `0.8` `0.9` と**その日の状態を書いた節が
5 つ**積み上がっており、そのほとんどが嘘になっていた。**書く場所を 3 つに固定する**
——**§1 いまの状態**(上書きする。足さない)、**§2 次にやること**(終わったら消す)、
**§3 これまで**(1 版 1 行。経緯は残すが、状態としては読ませない)。**その規約自体を
§7 に書いた**ので、次に足そうとした人が気づく。

**引用を行番号から段の名前へ変えた**(`heavy-BACKLOG-red-propagation.md`)。
`heavy-corpus.yml:150-155` ではなく `Type a sample on the real keypad` の段
——**名前は編集で動かない。** この文書自身が「行番号は書いた日の座標」と警告して
いたのに、**前日に自分が足した段のせいで 1 日でずれた**(`:130-133` → 139、
`:150-155` → 156)。引用した 4 つの段名が現物に実在することは確かめた。

**当て直しの結果も同じ文書に足した**——主張 3 件はすべて生きており、腐っていたのは
座標だけ。選択肢は 3 つのままだが、**(c) には制約が 1 つ増え**(0.5.0 で入った順序ピン
3 本を緑に保つ必要がある)、**(b) には未解決の前提がある**(再現性の結果を報告書へ渡す
経路が無い。**この非対称が無いと (b) が不当に安く見える**)。

作法に 3 つ足した。**長走行は重ねない**(変異が別の走行に載る)、**数字はどの走行・
どの木のものかを先に確かめる**(2026-08-28 に 3 回踏んだ)、**翻訳を足す前に既存の
変換を探す**(2 か所が同じ仕事をすると片方だけ直した日に静かに壊れる)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
重量級の報告書は「不一致 0 件」と書くとき、**期待値そのものが生成器の
出力である**ことを前提にしている。その前提を確かめているのは
`reference/tests/test_corpus_reproducibility.py` だが、**結果が報告書へ
届く経路が無かった**——検出力は `detection-power.json`、盤面は
`heavy-ui-run.json` を通るのに、再現性は `pytest -q` を走らせるだけで
何も残さない。

そのため報告書の「測っていない」が **2 つの理由**を兼ねていた。CI では
検出力の段は再現性検査の後ろに在り `if:` を持たないので、**再現性が赤いと
飛ばされる**——回し忘れと、土台が崩れて飛ばされたのが、同じ文で出ていた。

再現性検査が信号を残し、報告書がそれを読む。状態は畳まずに 5 つ持つ
(`uiHealth` と同じ形): 確かめていない/古い/途中で落ちた/食い違って
いる/通った。**読む人の次の一手がそれぞれ違う。**

**信号には指紋を持たせた。** 検査を通したあとで期待値を書き換えると、
ディスクには「緑」と書かれた信号だけが残る——このテストが塞いだはずの穴が、
信号の側から開き直る。`corpus/generated/` の生バイト列の指紋を両側で組み、
合わなければ報告書は「緑」ではなく「**古い**」と書く。

**報告書は信号の `ok` を読まない。** 判定は部品から組み直す。要約のフラグを
信じると、書く側が壊れた日に報告書が緑を書く。

番人:
- `report.spec.ts` に 12 本(5 状態・`ok` を信じないこと・**「測っていない」の
  2 つの理由が別物になること**・指紋)
- `test_corpus_reproducibility.py` に 5 本(指紋・片方だけ走った走行が
  `ok: true` にならないこと)
- **指紋は言語をまたぐので、同じ作り物から出る同じ 16 進を両側に留めた。**
  片側だけ手順を変えると、そちら側が赤くなる

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
報告書は再現性検査が残す `heavy/reproducibility.json` を読む。段を後ろへ
動かすと、**土台の節が毎回「確かめていない」になる**——`heavy:ui` と
`heavy:power` をレポートより前に置いている理由とまったく同じ形である。
順番だけがそれを決めているので、順番を見張る。

赤確認: 再現性の段を `pnpm heavy` の後ろへ移すと
「expected 21 to be less than 19」で落ちる。ワークフローはバイト一致で戻した。

あわせて、今日踏んだ罠を 1 つ。**`uv run` も `--no-config` が要る。**
CLAUDE.md は `uv lock` / `uv sync` だけを挙げていたが、`uv run pytest` でも
依存の解決が走り、`uv.lock` に `exclude-newer` が書き込まれる。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
**`vi.mock` のファクトリが返す素のオブジェクトを、TS は一切見ていなかった。**
ファクトリに戻り値型が無いためである。0.6.0 で境界の形を変えたとき、
**9 本のスタブが古い形のまま緑だった**——`undefined === "error"` は偽なので
実行時も成功の枝に落ち、型検査も何も言わなかった。

**塞ぎ方は注釈 1 つ。** `vi.mock` は巻き上げられるので値は参照できないが、
**型は消えるので参照できる**——`initExpr: (): Promise<ExprCalc> => ...`
と書ける。抽出も再構成も要らない。

**捕まる/捕まらないの境目は mock かどうかではなく、戻り値に型注釈が
付いているか**だった。型付きヘルパー(`result(): DataScaleResult` など)に
書いたスタブは、0.6.0 でも型検査が行番号つきで全部落としている。

**部分スタブ 2 本は完全スタブになった**(`fieldOrder` の `transfer` だけ、
`operators` の空オブジェクト)。足りない method は `DataScalePanel.test.tsx`
の `stubCalc` と同じ流儀で「呼ばれたら投げる `vi.fn()`」で埋めた——
**盤面が想定外の関数を呼んだ日に、黙って `undefined` を返さずに落ちる。**

論理は変えていない。テストの中身も触っていない。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fable のレビューで 2 件が未充足だった。

**1. 残骸。** 信号は前の走行の状態を運ぶ。コーパスの指紋だけでは
**期待値が 1 バイトも動いていないのに生成器が変わった**場合を捕まえられず、
古い信号が緑のまま残る。信号に**生成器一式の指紋**を足した——選ぶのは
Python 側だけ(`generator_files()`)で、読み手は信号が名指しした名前を
数え直す。選び方が 2 か所に在ると、片方が増えた日に静かにずれる。

走行 id や git SHA ではなく指紋にしたのは、**コミットしていない編集を
SHA が捕まえないから**である。読み手は不一致を「無い」ではなく **`stale`**
として出す——畳むと「一度は通って、その後に何かが変わった」が消える。

**2. 書き手と読み手の欄一致。** 欄がずれると読み手は黙って「信号が無い」に
落ち、**仕組みが壊れたまま報告書は「確かめていない」と書き続ける。**
静かな壊れ方なので、欄の綴りを両側から同じ一覧に留めた
(`SIGNAL_FIELDS`)。検証を IO から切り離し(`parseReproducibility`)、
1 欄ずつ落として読み手が受け取らないことを見る。

**言語をまたぐ指紋の作り物を強くした。** 2 枚の ASCII では、割れうる差が
1 つも入っていなかった。いまは 4 枚:
`a` / `ab` / `b`(並べ替えが割れうる形)・`Z`(大文字が小文字より前)・
**非 ASCII の中身**(コーパスの実物も日本語のラベルを含む)。
**作り物が痩せても指紋は変わらない**ので、条件そのものを assert する
テストを別に置いた。

**(c) に持ち主を付けた。** 台帳に「この信号は (3) の門の前提である」と
書き、**(3) を名前のある次の作業として**書いた。`report.ts` にも
「これは門ではなく計器である」と、門がどこに来るかを書いた。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
**前提を 1 つ訂正してから設計した。** 台帳は「再現性が赤でも照合は
`always()` で走って緑になれる」と書いていたが、**それが真なのは段の話**で
あって、**ジョブは赤くなる**——落ちた段が 1 つでもあればジョブは失敗で、
`release.yml` の `Deploy` は走らない。v0.4.1 の走行 32857987219 で実測した:
道具の段が failure、`Deploy` は skipped、証拠のジョブも skipped。
**門はジョブの側に既に在る。**

では何が壊れていたのか。**同じ走行が答えを持っていた**——ブラウザの導入が
飛ばされ、盤面と照合が `always()` でそのまま走り、**前提が無いせいで
36 本が「`Executable doesn't exist`」で全滅した。** 真犯人(型検査の赤)が
埋もれたのはそのためである。**`always()` は「何があっても走る」なので、
前提が欠けていても走ってしまう。**

だから分ける:

- **前提を整える**(`always()`): ブラウザ・Python・依存。落ちても整える。
  **Python の用意にも `always()` を付けた**——土台の検査は TypeScript の
  道具立てと無関係なのに、v0.4.1 では道具の赤に巻き込まれて飛ばされ、
  期待値が生成器の出力かどうかを一度も見ないまま走行が終わっていた。
- **集める**(`always()`): サマリーと添付。
- **判定・測る**(`always() && 前提の名指し`): 自分の前提だけを名指しする。
  - 土台 ← 依存だけ。**道具の赤に巻き込まれない。**
  - 検出力 ← wasm・ブラウザ・**土台**。数は報告書に主張として載るので、
    期待値が生成器の出力でないなら 11 分を費やさない。**以前も同じ挙動
    だったが、それは並びが偶然そうなっていただけで条件ではなかった。**
  - 盤面・照合 ← wasm・ブラウザのみ。**土台は前提にしない**——盤面は
    期待値の素性と無関係で、照合はどのファイルが食い違ったかを教える。
    土台が赤いことは報告書が先頭近くで書く(段階 1)。

番人:
- 名前のある段はすべて `# 種別:` を宣言する(除外は頭の 3 つだけで、
  その数も固定した)
- 条件の形は `always()` か `always() && steps.X.outcome == 'success'` だけ
- **裸の `always()` を名乗れるのは「前提を整える」と「集める」だけ**
- 判定・測る段の前提の集合を 1 つずつ固定
- **「赤くなるべき日」を場面表で実際に動かす**(6 本)。push できない以上
  本当の確認は CI の走行だが、条件式の論理はここで動く。形の規則が条件を
  狭めているので、評価器が本物と食い違ったまま緑にはならない

赤確認: 裸の always() を名乗らせる → 4 本赤(構文 2・**場面 2**)。
前提を 1 つ外す → 2 本赤。種別を消す → 1 本赤。ワークツリーはバイト一致で戻した。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
「再現性が赤でも照合は緑になれる」は**段の話としては真だが走行の話としては
偽**だった。ジョブは赤くなり、`Deploy` は skipped になる(v0.4.1 の走行
32857987219 で実測)。**期待値が触られたコーパスが本番へ出ることはなかった。**

台帳にこの訂正を書き、(2)(3) が入ったことと、**CI の走行でしか確かめられない
残り 2 件**を書いた。閉じる人が現物へ当て直せるように、走行 id を残す。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
**1. 名指しした step id の実在。** 存在しない id への参照は GitHub では
黙って偽になり、**その段が永遠に飛ぶ**——名指しの規律が、名指しの外れで
裏返る。**場面表は既にこれを捕まえていた**(未知の id は決して success に
ならないので「何も落ちない日は全部走る」が落ちる。実測で 4 本赤)が、
**失敗文が原因を言わない。** 診断名のためのテストを 1 本足した。

**2. cancel の裁定。** `always()` は手で止めても走る——全部 `always()` の
ままだと、止めたはずの走行が 23 分の測定を続ける。**止めた人は理由を
知っているので、測り直す意味がない。**

- **前提を整える / 判定・測る → `!cancelled()`**(失敗のあとは走る、
  止められたら走らない)
- **集める → `always()` のまま**。止められた走行でも、どこまで進んで
  いたかは読めるほうがよい。**代償を 1 つ引き受ける**——報告書が無いので
  添付の段が赤くなる。走行の結論は `cancelled` で下流は動かないが、
  **赤い段が 1 つ見える。**

場面表に「手で止めた日」を足した。

**3. 偽の理由の掃除。** 「この信号は (3) の門の前提である」は、**門が
ジョブ側に既に在る**以上そのままでは偽である。写っていたのは 2 か所
(`report.ts` と台帳)で、両方を**赤の帰属**の話に書き直した——直しているのは
「本番へ出る」ではなく「**真犯人が埋もれる**」であり、根拠は走行 32857987219。

**あわせて、型検査を通していないコミットが 1 本あった**(段階 2)。
`noUncheckedIndexedAccess` の 2 か所。CI では落ちる状態だった。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CI は `node-version: 22` と書いていた——**「そのときの最新の 22.x」**が入るので、
走行ごとに違う node で回りうる。**手元は誰も宣言していなかった**ので、
`v22.14.0` と CI の `v22.23.2` がずれていても気づけない。

**版は `22.23.2`。** 22.x の最新で、2026-07-28 リリース(`nodejs.org/dist` で
確認)——**2 週間のクールダウンを通過している**(ユーザー方針)。24 系へ上げるのは
別の日の判断とする。

**4 か所に同じ綴りで書いた**ので `git grep 22.23.2` で照合できる:
`.nvmrc` / `web/package.json` の `engines` / `heavy/package.json` の `engines` /
`.github/actions/setup-web/action.yml` の `node-version`。**`.nvmrc` に `v` を
付けないのは、4 か所を同じ文字列にするため**(nvm はどちらでも読む)。

**`engines` は宣言であって強制ではない。** 実測すると `pnpm install` は
`WARN Unsupported engine` を出して **exit 0** で通る。**「完全固定」を機械が
守るようにするため**、`web/.npmrc` と `heavy/.npmrc` に `engine-strict=true`
を置いた(ユーザー承認)。版が違えば `ERR_PNPM_UNSUPPORTED_ENGINE` で
**exit 1** になる——実測で両方確かめてある。

**`heavy` にも足した。** 独立パッケージで、CI では `setup-web` のあとに
**自分の `pnpm install --frozen-lockfile` を別に走らせる**(`ci.yml:101`)。
`web` だけ固定すると、`heavy` は宣言の無いまま何の版でも通る。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
**「2 週間のクールダウン」はユーザーの頭の中にしか無かった**——`CLAUDE.md`・
`CONTRIBUTING.md`・`docs/` のどこにも書かれていない(grep で 0 件)。
道具の版を上げる判断は今後も繰り返し来るので、規約として置く。

併せて**`node` の版が 4 か所に在ること**と、**`engines` だけでは止まらない
こと**を書いた。後者は実測で分かった——「宣言したから守られている」と
読むと、次に版がずれた日に誰も気づかない。biome の pin と `$schema` が
別々に動いた件と同じ棚である。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@terapyon
terapyon merged commit f64bd30 into main Aug 29, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant