ADR 0003: 記事分類 type を必須にして分類漏れを fail-closed で検出する

背景

トップページは記事を「記事 / メモ / 情報」の3タブに振り分けている。この分類を過去に2回、一括で付け直したが、いずれも取りこぼしを残した。

回 対象 基準 取りこぼす理由
1回目 (#105) 直近3ヶ月の68件 LLM 判定 範囲が3ヶ月に限定され、それ以前が対象外のまま残った
2回目 (#113) 全件 地の文200字以下 文字数は「記事らしさ」の代理指標。設定・コード主体のメモは地の文が伸びて素通りする

例として _posts/2018-04-17-sublime3.md(エディタの導入手順と設定ファイルの羅列)は地の文380字・コードブロック3個のため200字基準を超え、記事タブに残っていた。内容は完全に手順メモである。

根本原因は分類の既定値が「記事」だったことにある。type を書き忘れた記事は静かに記事タブへ入るため、漏れが漏れとして観測できない。一括分類を何度やっても取りこぼしが累積した。

作業中、同じ性質の fail-open が他に2箇所見つかった。

決定

type(article / memo / info)を全記事の必須 frontmatter キーにし、未指定・値域外・空値を検出したら失敗させる。判定基準は docs/post-classification.md に成文化する。

漏れを止める層は4つ。

層 実体 何を止めるか
値 type が全記事必須 未指定が既定で記事タブへ落ちること
ローカル検査 tools/check-post の Rule 10 / 10b 執筆時点での未指定・値域外・空値
CI validate-posts.yml の Validate post classification step _posts/** を触る PR での未指定
再判定 scripts/classify-posts.py 基準変更時に全件を測り直せない状態

付随する決定:

捨てた案

変えてよい前提 / 壊すと危ない前提

追記 (2026-09-06, #139)

決定当時の _layouts/home.html は post.type == 'memo' or post.categories.first == 'memo' の OR でメモを判定していた。本 ADR は「振り分けロジックは変更しない」としたが、#130 で全記事に type を付けた結果 categories.first == 'memo' に該当する記事は0件になり、この条件は分類の第2の入口としてだけ残っていた。

入口が2つあると categories: [memo, ...] を書いた type: article の記事がメモタブへ入り、CI が検査している type と実際の振り分けが食い違う。#139 で categories 系統を落とし、_layouts/home.html と tools/check-post の isMemo をどちらも type == 'memo' 1本に揃えた。

これは本 ADR の「未指定を許容に戻さない」「分類の入口を type に集約する」という趣旨を強める変更であり、決定そのものは覆っていない。

追記 (2026-09-06, #138)

「背景」で fail-open として挙げた2箇所のうち後者 —— .github/workflows/validate-posts.yml が ./scripts/validate-posts.sh || true で記事フォーマット検査を呼んでおり、検査がエラーを報告しても CI が緑のままだった件 —— を #138 で塞いだ。全614件で ERROR 0件・WARN 0件であることを確認した上で || true を外し、ERROR が1件でもあれば job が落ちる。ERROR はファイル名形式・front matter 欠落・title 欠落/空・title 内の未エスケープなダブルクォートの4種で、最後のものは Jekyll のビルド自体を壊す。

これに伴い「壊すと危ない前提」の「分類検査を validate-posts.sh 側へ統合すること」は、|| true を理由としては成立しなくなった。ただし2つの検査を別 step のまま保つ判断は変えていない。フォーマット検査が落ちても分類の結果を同じ run で読めるほうが、分類漏れとフォーマット破壊を切り分けやすいためである(レポート出力・分類検査・artifact の各 step は if: $ で独立に走る)。

追記 (2026-09-20, #168)

いいねした RSS 記事から週次でアイデアを自動生成して投稿するパイプラインの新設に伴い、type の値域に第4の値として idea を追加した(値域: article / memo / info / idea)。これは本 ADR の「変えてよい前提」として明記されていた「type の値域に第4の値を足すこと」に則った拡張である。

値域を広げた理由

機械生成されるアイデア記事は、既存の article(論考がある)、memo(手順・記録)、info(AI自動収集情報)のいずれの性質とも合致しない。このブログでは type の値域外を fail-closed に検出して CI を落とす設計としているため、新たな性質を持つ自動生成記事を正常に受け入れるには、3実装(Go / Python / Bash)の validTypes と docs/post-classification.md を揃えて値域を拡張する必要があった。

機械生成の記事が人の記事と混ざらない担保(独立タブ)

新設されるアイデア記事は週あたり最大25件(月100件超)という高頻度で投入される計画である。もし type: idea を独立したタブに振り分けなければ、Jekyll のレイアウト分岐(_layouts/home.html)の else 句に落ちてデフォルトの「記事」タブに混入し、人間が執筆した論考記事が短期間で押し流されてしまう。

これを防ぐ担保として以下を実施した:

  1. トップページの独立タブ化: _layouts/home.html で grok-generated(情報)および memo(メモ)の判定に加えて post.type == 'idea' を独立した「アイデア」タブ(idea_posts)へ振り分ける分岐を追加した。
  2. 既存記事の振り分け不変性の維持: idea の判定分岐は既存の grok-generated タグ判定よりも後に配置し、既存 617 記事のタブ振り分け(記事 119 / メモ 459 / 情報 19、未公開・非表示除外前ベースライン: human 123 / memo 475 / grok 19)が 1 件たりとも変動しないことを検証した。
  3. 人間執筆記事の保護: 人間が書いた記事は idea にしない運用基準を成文化し、執筆時の検査ツール(tools/check-post / scripts/check-post.sh)のエラーメッセージにも「idea(機械生成アイデア)」と明記して、人間による誤用を防いでいる。