トップページは記事を「記事 / メモ / 情報」の3タブに振り分けている。この分類を過去に2回、一括で付け直したが、いずれも取りこぼしを残した。
| 回 | 対象 | 基準 | 取りこぼす理由 |
|---|---|---|---|
| 1回目 (#105) | 直近3ヶ月の68件 | LLM 判定 | 範囲が3ヶ月に限定され、それ以前が対象外のまま残った |
| 2回目 (#113) | 全件 | 地の文200字以下 | 文字数は「記事らしさ」の代理指標。設定・コード主体のメモは地の文が伸びて素通りする |
例として _posts/2018-04-17-sublime3.md(エディタの導入手順と設定ファイルの羅列)は地の文380字・コードブロック3個のため200字基準を超え、記事タブに残っていた。内容は完全に手順メモである。
根本原因は分類の既定値が「記事」だったことにある。type を書き忘れた記事は静かに記事タブへ入るため、漏れが漏れとして観測できない。一括分類を何度やっても取りこぼしが累積した。
作業中、同じ性質の fail-open が他に2箇所見つかった。
scripts/check-post.sh は Go 製バイナリ(tools/check-post)があればそちらに exec する。シェル側の Python だけに検査を足しても、実際には検査が効かない.github/workflows/validate-posts.yml は ./scripts/validate-posts.sh || true で呼んでおり、検査がエラーを報告しても CI は緑のまま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 |
基準変更時に全件を測り直せない状態 |
付随する決定:
classify-posts.py は未分類の検出(--report / --validate)と判定結果の一括適用(--apply)だけを担い、記事かメモかの判断はしない|| true を付けない。同 workflow の validate-posts.sh に相乗りさせると失敗が job に伝播しないtype の値の取り出しは3実装(Go / classify-posts.py / check-post.sh の fallback)で同一規則にする。クォート付きは閉じクォートまで、無しは空白に続く # まで_layouts/home.html)は変更しない。type は分類の明示と CI ゲートのために持つ(→ この点は #139 で改めた。下記「追記」を見よ)sublime3 のようにコードが多く地の文が伸びる型は閾値を上げないと拾えず、上げると論考のある短い記事まで巻き込む。実際、既存メモ222件のうち30件は論考があり記事へ是正した。代理指標を調整し続ける限りこの誤りは消えないpublished: false の下書きを検査対象から外す — Jekyll のビルド対象外でも分類は必要(公開時に漏れる)。実際に4件が該当し、いずれも type を付けてあるtype の値域に第4の値を足すこと(3実装の validTypes と docs/post-classification.md を同時に更新する)。判定を誰が行うか(人 / AI / 別ツール)。classify-posts.py の TSV 形式validate-posts.sh 側へ統合すること。あちらは || true で呼ばれているため、統合した時点でゲートが無効化されるclassify-posts.py に自動判定を実装すること。機械判定は代理指標に戻ることを意味し、この ADR の前提そのものを崩す決定当時の _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 に集約する」という趣旨を強める変更であり、決定そのものは覆っていない。
「背景」で 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: $ で独立に走る)。
いいねした 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 句に落ちてデフォルトの「記事」タブに混入し、人間が執筆した論考記事が短期間で押し流されてしまう。
これを防ぐ担保として以下を実施した:
_layouts/home.html で grok-generated(情報)および memo(メモ)の判定に加えて post.type == 'idea' を独立した「アイデア」タブ(idea_posts)へ振り分ける分岐を追加した。idea の判定分岐は既存の grok-generated タグ判定よりも後に配置し、既存 617 記事のタブ振り分け(記事 119 / メモ 459 / 情報 19、未公開・非表示除外前ベースライン: human 123 / memo 475 / grok 19)が 1 件たりとも変動しないことを検証した。idea にしない運用基準を成文化し、執筆時の検査ツール(tools/check-post / scripts/check-post.sh)のエラーメッセージにも「idea(機械生成アイデア)」と明記して、人間による誤用を防いでいる。