苦手な語だけを解き直すページを作った日 - 正解1回で消える穴と、自動再生設定のD1同期漏れ
語源英単語コースの手触りを直した一日の翌日である。前日の記事の末尾に、二つ書き残していた。地図側ビューアーの自動再生設定だけがDB同期から漏れていること、間違えた語だけを集めて解き直すページの計画を立てたこと。その二つが、そのまま今日の朝と午後になった。
朝いちばんに仕掛かりを棚卸しさせた。本命は苦手語ページである。計画書はCodexのレビューを通してあり、Step 1から9まで手順も決まっている。実装が入っているかどうかだけが、記憶では確定できなかった。
「実装は1行もありません」
推測で「たぶん未着手です」と言われても困るので、実測して確かめてくれと頼んだ。
ファイル実在の確認とグローバル検索の結果はゼロだった。ヒットしたのは、無関係なコンポーネントに紛れていた英単語 "weak" の1件だけ。計画書は前日のうちにHTML化させてあったので、Chromeの前面タブに開かせて頭から読み直した。
正解1回で、慢性の苦手が消える
読み直して手が止まったのは、どの語を「苦手」と呼ぶかの決め方だった。
連続正解のカウントは、正解が1回入ると0に戻る。つまり、ずっと落とし続けている語でも、たまたま当たった一度で一覧から抜ける。いちばん救いたい語がいちばん先に消える設計になっていた。
通算で何回落としたかを別に持てばいい。8回以上落としている語を「慢性」として切る。連続正解でリセットされる指標とは別軸なので、一度当たったくらいでは消えない。この階層に、要注意と直近ミスを足して3階層にした。
計画どおり実装させた。まず既存構造をサブエージェント3本で並列に調査させ、計画の前提が裏取りできたところで着手する。この調査で計画書のSQLに不具合も1件見つかった。誤答が0件のときに集計が NULL になるという、まさに「苦手語がいない人」で壊れる書き方だった。
空を返すか、通常語で埋めるか
Step 1 のテストが失敗した。期待値の書き間違いかと思ったが、そうではなく設計判断の抜けだった。
苦手語が1件も取れないとき、そのまま復習セッションを始めると、通常の期日到来語だけで20問が埋まる。画面には「苦手語復習」と出ているのに、中身は普通の出題になる。看板に偽りが出るので、空を返して呼び出し側に判断させる形に直させた。
そこからは順に積み上げていった。集計ロジックは副作用と分けたかったので、純粋関数はコンポーザブルではなくユーティリティ側に寄せている。33件、7件と通し、ページ実装では0件ガードとスナップショット固定を最初から入れた。
実機で見るまで分からないもの
devサーバーを立ててChromeで確認させた。最初の一回で落ちた。parseAnsweredAt のimport元を間違えていて、スケジューラ側の関数を別の場所から引こうとしていた。
直してからは狙いどおりだった。左カラムに3階層と件数が出て、本文は単語リストと同じ表示になる。階層を切り替えると要注意5語に入れ替わり、チャプター列も本文も追随する。矢印キーの挙動も既存ページと揃っている。
一箇所だけ、注記の文字色が途中から変わって見えた。バグかと思ってDOMを見せたら、素のテキストで子要素ゼロだった。CSSでは文字列の一部だけを着色できないので、縮小スクリーンショットのサブピクセル描画によるにじみである。画面の見た目で判断していたら、ありもしないスタイルを探すことになっていた。
最後に、履歴のキー名を退避して0件の状態も作らせた。画面は落ちず、達成メッセージが出る。全テスト2,979件が通ったところでコミットに移った。
本番がログイン済みだった
学習ゲートは新規5論点で出題された。4問正解、1問だけ再出題になる。落とした1問は、共有コンポーネントをどこまで直していいかという判断の問題だった。再出題の前に事実を確認させたら、そのビューアーは8講座139ページから使われていた。この規模が判断の根拠になる、という形で問い直されて答えた。
デプロイは4.9分。本番を開いたらログイン済みのままだった。devでは未ログインでしか試せていなかったので、検証できていなかった「全期間の累積で判定する経路」がその場で確認できた。統計APIが200を返し、中身も設計どおりだった。
「これ本番に反映されてませんでしたっけ?」
デプロイが終わってから、もう一つ引っかかっていたことを聞いた。挿絵レビューのラウンド5である。ビューアーで合格を付けても、その判定が本番に反映される仕組みは入っていなかったのではないか。
確認させたら、そのとおりだった。記憶のほうが正しかったので、ラウンド5を完了扱いにしてpushさせた。
同じ流れで「未解決イシューが5件ある」という報告も出ていたが、こちらは調べさせたら誤りで、実際の未解決はゼロだった。ではその5件は何かというと、依存パッケージの脆弱性アラートのほうである。中身を見せてもらった。
突出して重いのは認証ライブラリのアカウント乗っ取りの1件に見えたが、advisoryの適用条件を読むと、該当するのはマジックリンクとメールOTPを有効にしている場合だった。こちらの設定にはどちらも入っていない。5件とも現時点で実害はなし。それでも認証は壊れると致命的なので、予防でバージョンだけ上げさせた。型チェック、全テスト、未ログイン時のセッション検証まで通したあと、サインインの実挙動だけは自分でログインして確かめた。
0件のときも列を残す
苦手語が0件のとき、単独ページに切り替わる実装になっていた。どうせ他の階層へ移るなら、最初からミラーカラムのままで、セクション列から3階層を行き来できるほうが自然である。チャプター列は空でいい、と注文を出した。
調べさせたら、チャプター列を完全に空にするのは現実的でなかった。ビューアーのテンプレート20箇所以上が現在のチャプターとセクションを直接参照していて、空配列だと描画そのものが成立しない。139ページに影響する改修になる。
代わりに、チャプター列に1行だけ置く形にさせた。結果としては、ビューアーが1チャプターのときチャプター列を出さない仕様だったので、列は自然に消えた。要望の核心だったセクション列からの行き来は、クリック1回で満たせている。
本番が旧表示に見えた数分
デプロイして本番を開いたら、旧表示のままだった。
キャッシュを疑ってハードリロードさせたが変わらない。そこから順に降りていった。ビルド成果物には新しいコードが入っている。配信されているHTMLは新チャンクを参照していて、200で返り、キャッシュステータスも動的配信を示している。Service Workerもキャッシュも存在しない。本番から取得したチャンクの中身にも新しい文言が入っている。
配信側はどこも正しい。残るはブラウザが実際に読み込んだものだけだった。見せたら、手元に残っていたHTMLが古かっただけだと分かった。デプロイ直後のエッジ伝播のタイミングである。取り直したら新しいHTMLが返り、再読み込みで表示も切り替わった。実装の失敗と読み違えかけた場面だった。
自動再生設定が2系統に分かれていた
午後は、前日から持ち越した自動再生設定のDB同期漏れに入った。
引き継ぎの時点で、単なるキー追加漏れではないところまでは分かっていた。ビューアー側の自動再生設定はすでにD1へ同期されていて、地図側のキーと機能が重複している。同期対象に足すだけだと、同じ意味の設定が2つ並ぶことになる。
調べさせたところ、この2つは排他だった。地図ページではペイン側のトグルだけが出て、ビューアー側の音声フラグは立たない。詳説ページではビューアー側のトグルだけが出る。同じ画面に両方が並ぶことはないので、1系統に統合できる。ブラウザでチェックボックスの実数を数えさせて確かめた(最初のスクリプトはハイドレーション前に走って、数え間違えていた)。
統合の実装と、旧キーを読み込んで同期対象へ移す移行パスを入れさせた。ユニットテストは11件から14件に増えた。ブラウザでは、移行が発火して詳説側に「例文」の設定が引き継がれること、苦手語ビューアーでトグルするとPUTが飛んでリロード後も保持されること、実際に音が鳴ることまで確認させた。未ログインの挙動は分離コンテキストで別に見ている。
落ちたテストが自分のせいかを切り分ける
全ユニットテスト2,982件は通ったが、E2Eで5件落ちた。
自分の変更が原因かどうかは、変更を一時退避して同じテストを流せば分かる。やらせたら、変更前のコードでも同じ5件が落ちた。既存の失敗と確定する。
今回の作業とは無関係だが、落ちたままにしておく理由もないので直させた。原因は、クイズのトップページがハブに作り替えられ、チャプター一覧が下の階層へ移っていたことだった。テストの期待値が古い構造のまま取り残されていて、同じ理由でさらに10件が落ちていた。構造に合わせて直すと、Chromiumで55件が通った(1件スキップ)。WebKit側の4件は、単独で流すと通るのでフレーキーの切り分け待ちにした。
「デプロイも終わったってことで」
一日を締めようとして、デプロイも終わったことだし残りはあるか、と聞いた。
このセッションではデプロイスクリプトを一度も走らせていない、と返ってきた。本番は昼過ぎのコミットのままだった。終わったつもりでいたのは自分の思い込みで、コミットとpushも途中の状態だった。
明日でいい、と決めた。テスト修正のコミットと残タスクのメモ更新だけさせて、翌朝すぐ始められる状態にしてもらった。学習ゲートは、午後の分は4問中3問正解で再出題に入ったところでパス指示に切り替えた。締めの作業なので、理由を残してスキップさせている。
学び
- 連続正解でリセットされる指標は、慢性の苦手をいちばん先に取りこぼす。通算の取りこぼし回数を別軸で持つと、一度当たったくらいでは消えない
- 0件のときに何を返すかは、テストではなく看板の問題だった。「苦手語復習」と出しておいて通常語で埋めるのは、機能としては動いていても嘘になる
- スクリーンショットで色が違って見えたらDOMを見る。今回は縮小描画のにじみで、CSSでは起こしようがない現象だった
- 「本番に反映されていない」の切り分けは、ビルド成果物、配信、ブラウザの順に降りる。配信側が全部正しいと分かった時点で、残りはブラウザが持っていた古いHTMLしかなかった
- 落ちたテストが自分のせいかは、変更を一時退避して同じテストを流せば数分で確定する
- 「終わったつもり」は口に出すと崩れる。デプロイしたかどうかは記憶ではなく実行記録で確かめる
慢性の苦手語は、いまのところ0件と出ている。通算8回という線が妥当かどうかは、データが溜まってからでないと判断できない。間違える語ほどイメージがついていない、という前日の仮説の続きも、その数字が動き出してからになる。