この日にやったこと

/coding-principles ページに4つの大改修を一気に入れた。

  1. Bad/Good を横並びに(縦に積み上がっていた比較を左右並びに)
  2. コンテンツ領域の max-width 撤去(横幅を解放してコードの折り返しを減らす)
  3. 前提条件フローチャート(「何を実装しようとしているのか」を Bad/Good の間に挟む)
  4. ベン図による抽象化セクション(Bad=影を削る/Good=光を直接定義する、をド・モルガンの法則で整理)
  5. 全62トピックに初級・中級・上級のレベル付け(Codex レビューを噛ませて配分を調整、星3つで表示)

地味な配置調整から始まり、最終的には「コーディング原則の整理軸そのものをベン図と集合論で抽象化する」ところまで広がった日になった。

1. Bad/Good を縦から横に並べ替える

最初は単純な見た目の不満から始まった。Bad と Good のコード例が縦に上下で並んでいて、目を上下に走らせないと差分が掴めない。

CodeGoodBadWrapper.vue を 2列の grid に変えて、768px 以下では従来通り縦積みに戻すレスポンシブにした。横長のコードがはみ出さないように min-width: 0 も忘れず入れる。

.examples-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1rem;
}
.example { min-width: 0; }

@media (max-width: 768px) {
  .examples-row { grid-template-columns: 1fr; }
}

この時点で「あ、Bad は条件を3段ネストしてるのに Good は早期 return で1段で済んでる」が一目で見える。縦並びだと感じなかった差が、横並びにした瞬間に視線で勝手に差し引きされる。

2. コンテンツ領域の max-width を外す

横並びにしたら今度は「コードが折り返されすぎ」になった。原因は TopicDetail.vue に置いてあった max-width: 880px で、いわゆる本文の可読幅制限が Bad/Good の左右並びに対しては狭すぎた。

max-width: 880pxnone に切り替えて、親コンテナの幅いっぱいまで解放した。これで Bad/Good のコードがそれぞれ十分な横幅を持ち、不要な改行が消えた。

3. 前提条件フローチャート:コーディング以前に「何を実装しているか」を見せる

ここから一気に話が広がる。

ユーザーから「2-1 の確定申告の電子送信判定で、何を実装しようとしているかの前提条件チャートを Bad/Good の間に入れて」という要望が来た。理由が刺さった。

コーディング原則の話だが、何を実装しているか頭に入っていないとコーディングが進まない。

たしかにそうで、canSelfTransmit() の Bad/Good を眺める前に「そもそもどんな条件を満たしたら自己送信OKなのか」が読み手の頭にないと、ネストの深さや早期 return の意義を判断できない。

最初は縦並びチャートで作って却下された

PreconditionChart.vue を新規に作って、データ駆動で条件を縦に並べる仕様で実装した。dev サーバーで見せて「これでどうですか」と出したら、即座に却下されて方針修正が入った。

いや、フローチャート(左→右に Yes/No で分岐)が欲しかったんですよ。

縦並びは「条件のリスト」であって「フロー」ではない。横方向にステップが流れて、各ステップで Yes なら右、No なら下の失敗ボックスに落ちる、という構造に作り直した。

装飾を削ぎ落として地味な情報帯にする

横方向フローに作り直したら今度は「装飾が強すぎて Bad/Good より目立つ」状態になった。フローチャート自体が主役を奪ってしまっては本末転倒なので、影・グラデーション・色付き背景を全部抜いて、枠線だけのプレーンなボックスにした。

[ ステップ1: 確定 ] → Yes → [ ステップ2: マイナンバーカード保有 ] → Yes → [ 自己送信OK ]
                                       ↓ No
                                [ 自己送信不可 ]

主役はあくまで Bad/Good のコード本体で、フローチャートは「これから読むコードはこの判定をやっている」という前提情報のスペース、という位置取りに落ちた。

4. ベン図による抽象化:光ではなく影を削るという発想

ここがこの日の一番の発見だった。フローチャートを見ながらユーザーが投げてきた問いがこれ。

このセルフトランスミット関数を実行する条件、直線で進むときの条件分岐を示すのではなく、「影について条件分岐を書いて、残りが全部用」っていう整理。これって光と影、陰陽みたいなもので、抽象化できるはず。ベン図で説明できる気がする。

腕を組んで一度天井を見上げてから「なるほど、これはド・モルガンの法則そのものだ」と返事を打った。

  • Bad: 「自己送信不可な影」を一つずつ列挙して、全部を削り取った残りを「自己送信OK」とする
  • Good: 「自己送信OKな光」を直接、ポジティブな条件として定義する

論理的にはまったく同じ集合を指しているのに、書き方の向きが反対になっている。!(A ∧ B ∧ C) ≡ ¬A ∨ ¬B ∨ ¬C のド・モルガンを、コードの書き方の選択として読み替える視点。

TopicAbstraction.vue でベン図と集合論メモを描く

TopicAbstraction.vue という抽象化セクション用のコンポーネントを新しく切って、SVG でベン図を描き、集合論メモ(ド・モルガンの法則)と比較表をその下に置いた。比較表はこの4軸。

観点Bad(影を削る)Good(光を直接定義する)
書き方の向き失敗条件を列挙して残りを成功とみなす成功条件を一つの式で定義する
名前の付き方否定形(!isInvalid肯定形(canSelfTransmit
主処理の位置ネストの一番奥に追いやられる関数の主軸に来る
条件追加のコスト影の if が一個増える光の式が一節伸びる

最後に「型の絞り込み」「バリデーション」「フィルタ処理」「権限チェック」の4分野で同じ構造(Bad=影/Good=光)が現れる例を並べて、トピック1-2の話を一般則まで持ち上げた。

dev サーバーで描画を確認しに行ったら、ベン図の重なり領域・集合論メモ・比較表・一般化例・締めの文まで一画面に揃って、抽象度の階段が3段(具体コード→ベン図→一般則)に伸びた構造になっていた。

学び

「光を直接定義する vs 影を削って残りを光にする」は、コーディング原則のかなり広い範囲で再利用できる軸だと気づいた。早期 return、ガード節、型ナローイング、フィルタ条件、権限チェックなど、別々に教わるテクニックの根っこに同じ集合論の構造が走っている。

「形容詞で言えば『光寄り』『影寄り』、動詞で言えば『削る』『定義する』」というラベル付けをしておけば、別の章を読むときも「これは光側の話か影側の話か」と即座に位置付けられる。

5. 1-3 に Good パターンを追加

ここで脱線。/coding-principles/1/3 を確認したらコンパリゾン1(保有銘柄クラス)と2(生焼けオブジェクト)に Good 例が抜けていた。Bad だけ並んでいる状態は「で、どう書けばいいの?」という疑問を残してしまうので、両方に Good を足した。横並びレイアウトが入ったあとなので、Bad と Good が左右に並んで一目で対比できる状態に整った。

6. 全62トピックに初級・中級・上級のレベル付け

午後の本丸。

グッドとバッドの左カラムと2列目のカラムに、レベルを記入してほしい。初歩的・中級・上級の3段階で。

判定軸の例も明確だった。

意味のまとまりでメソッドを分けるとか、目的ごとに変数を分けるとかは初歩的。変数の意味がなくなるから名前を変える、というのは直感的に理解できる。初期の段階で「これ変じゃない?」って気づくじゃないですか。

つまり「知識・経験がなくても直感的に違和感を持てるか」で初級/中級/上級を分ける、という軸。

memo にまとめてから Codex(GPT-5)でレビュー

全60件と思っていたが、実装中に数え直したら全62件あった。全部をいきなり実コードに反映する前に、memo/2026-06-13/coding-principles-level-assignment.md にレベル案と根拠を一覧表で書き出して、Codex(gpt-5)に投げてレビューしてもらった。

返ってきたのは8項目の指摘。本文(コード例の主題)と照らした結果、いくつかは「直感的な命名問題ではなく、設計知識を要求する話なので中級に上げるべき」「実は初学者でも気づくレベルなので初級に下げるべき」といった微調整。

最終配分は ★☆☆ 初級 20件 / ★★☆ 中級 28件 / ★★★ 上級 14件。中級が一番厚いのは、コーディング原則の本丸が「設計の文法を学んで初めて違和感を持てる」中級帯にあるという事実をきれいに反映していて、配分を見ているだけでも面白い。

実装:types → 各 chapter ファイル → LevelStars → 表示3箇所

実装順序はこれ。

  1. types.tsPrincipleLevel = 'beginner' | 'intermediate' | 'advanced' を追加
  2. chapter01.tschapter14.ts の各トピックに level:name: の直後に挿入
  3. 星3つを塗り分ける LevelStars.vue を新規作成
  4. chapters.ts に章レベル算出ユーティリティ(章内トピックの最頻レベル)を追加
  5. Miller Columns 一覧、モバイル一覧(index.vue)、トピック詳細(TopicDetail.vue)の3箇所に星を組み込む

LevelStars.vue の核心はシンプルで、beginner=1, intermediate=2, advanced=3 の数だけ ★ を塗りつぶす。

<script setup lang="ts">
const filledCount = computed(() => {
  if (props.level === 'beginner') return 1
  if (props.level === 'intermediate') return 2
  return 3
})
</script>

<template>
  <span class="level-stars" :aria-label="`難易度: ${label}`">
    <span v-for="i in 3" :key="i" class="star" :class="{ filled: i <= filledCount }"></span>
  </span>
</template>

色は塗りつぶしが #f59e0b(アンバー)、未塗りが #d1d5db(グレー)。アクセシビリティ用に aria-label で「難易度: 初級」等を喋らせる。

検証

dev サーバー(ポート3002)に立ててから SSR の出力を grep で確認したら、level-stars クラスが 92件(モバイル+デスクトップ両方)、filled 星が 171個出ていた。62トピック × 3星 × 表示2箇所だと数式上 372 になるはずだが、星の塗りつぶし数だけで 171 出ていれば配分は妥当に効いているとわかる。

Vitest は2件失敗していたが、いずれも今回の変更と無関係な既存の import.meta.dev 検出系と timeout 系の問題。dev サーバーのログにも警告なし(D1警告は無関係の既存もの)。

4本立ての全体像

朝の「Bad/Good 縦並び問題」というニッチな違和感から始まった作業が、終わってみると次の4つに広がっていた。

  • 見た目の再配置: Bad/Good 横並び+幅解放
  • 前提情報の追加: 何を実装しているかのフローチャート
  • 抽象化の追加: ベン図によるド・モルガン的整理
  • メタ情報の追加: 初級・中級・上級のレベル付け

最初の2つは「読み手の物理的な視線」を整える話、後ろの2つは「読み手の認知の階段」を整える話。同じページに対して、低層と高層の両方に手を入れた1日になった。

特にベン図による抽象化は、コーディング原則という本来「個別のテクニック集」になりがちな分野に、集合論で串を刺せた感覚があって、明日以降ほかのトピックを書くときの軸として使えそう。

残タスク

  • コミット(現時点ではすべて modified のまま)
  • 他のトピック(特に早期 return 系、フィルタ系)にも TopicAbstraction の光/影軸を適用できるか検討
  • 章ごとのレベル配分が偏っていないかバランスチェック(現状は最頻レベルで章レベルを出しているだけ)