Skip to content

feat: 条文ページ(条文1本=1ページ)を新設する - #27

Merged
wharfe merged 19 commits into
mainfrom
feat/article-pages
Sep 23, 2026
Merged

wharfe merged 19 commits into
mainfrom
feat/article-pages

Conversation

@wharfe

@wharfe wharfe commented Sep 23, 2026

Copy link
Copy Markdown
Owner

要は何

/diff はサイト最大の表示回数を集めながらクリック 0 だった(#4)。検索意図が「条文そのものを読む」なので、diff を見せても噛み合っていない。そこで 条文 1 本 = 1 ページを新設し、「現在の条文・いつどう変わったか・平易な解説・関連条文」を 1 画面にまとめた。現在 11 法令 163 ページ。

いちばん重要な設計判断 — 版が合わないときは解説を出さない

既存の解説(annotation)は過去のある版の条文について書かれた LLM の散文。それを今日の本文の隣に置くと、現行法についての主張になる。そこで版整合ゲートを置いた(spec §4):

  • 今日の本文が diff の paragraphs_after と一致し、かつ廃止された刑名(懲役・禁錮 → 2025-06-01 に拘禁刑へ統合)を使っていないときだけ、解説を出す
  • 合わないときはページは出すが解説を出さない。現在 163 ページ中 36 ページがこの状態。これは設計どおりの動作

根拠は必ず現在のものである必要があるので、articles.py は data/raw を読まず、生成と同じ実行で e-Gov から取得する。ディスク上のファイルは「いつ取得したか」を記録していないため、鮮度をディスクから判定する試みは過去 2 回失敗している(asof は「問い合わせた時点」であって「取得した時刻」ではない)。

レビュー

Gate 1(design-debate)・Gate 2(grok による敵対レビュー)は実施済み。実装は計画 10 タスクをタスクごとに fresh subagent + タスク単位レビューで回し、最後に Gate 3(/code-gate)を通した。

Gate 3: critical 0(周回上限 3 のうち 2 周で到達)

Layer 実行者 結果
1 証拠 受け入れコマンド実走 PASS
2 Claude 構造化 読み取り専用 4 件(high 2)
3 仕様ドリフト fresh subagent critical 0、PARTIAL 1 件
4 異族 codex(完走マーカーあり) Critical 2・Important 3

直した中身 — 根は 1 つ、面が 3 つ

3 件とも「情報を落とした表現どうしを突き合わせる/本文ではなく改正の type で判断する」という同じ形だった。

  1. 履歴カードの日付ラベル(Critical。codex と Claude が独立に発見)— 民法733条のページが「2024年4月1日改正直後の条文についての説明: 女性が離婚などで…再婚禁止期間…」と表示していた。その日付の直後の本文は「削除」の一語。e-Gov が廃止を「本文を『削除』に置換する modified」として記録するため、c.type で判定すると外れる。本文ベースの summary_basis を Python 側で決めるようにした(刑法178・民法746 も同じ状態だった)
  2. 廃止刑名のガードが履歴カードに効いていなかった(high)— 著作権法119条のページが、上段で「『懲役』から『拘禁刑』に変更されました」と言いながら下段で「改正直後の条文は懲役を定める」と矛盾していた。ただし道路交通法の 2 件はその日付では真(懲役の廃止は 2025-06-01)なので、判定軸は「カードが指している側の本文」にした
  3. [表] が版整合ゲートを素通りする(Critical、今日は該当 0 件)— diff.format_paragraph が表を [表] の固定文字列に潰すので、表だけが改正された条文は「一致」と判定されうる。fail-closed で封鎖し、出荷バイトが fetched_at 以外 1 バイトも動かないことを実証した

加えて、誤リンクの経路(複数条文を指す参照が先頭 1 条へリンク。接続語 8 種で遮断)と、空集合に対して緑になる出荷検査(11 ファイル中 10 個消えても全テストが通っていた。diff から導出した集合の一致検査に置き換え)と、未検証の説明文 132 件(第三の条文についての断定を、根拠のあるものだけに)を潰した。

受け入れ(すべて実走・再生成後の実測)

uv run pytest                                  291 passed
uv run python scripts/articles.py --all        exit 0(11 法令・163 条文)
uv run pytest tests/test_shipped_data.py       109 passed
npx tsc --noEmit / npm run lint / npm run build  すべて成功(207 ページ)
  • 生成 HTML 163 = 出荷 JSON の条文数 163 = sitemap の条文 URL 163(コードに 163 というリテラルは無い)
  • 条文リンク 365 → 365(喪失 0)/未検証の説明文 132 → 0
  • 再生成しても本文は 1 バイトも変わらず、差分は fetched_at のみ

見送ったもの

Gate 3 で検出したが今回入れなかったもの。すべて実測で「今日の効果 0 件」を確認済み。

公開処理の跨法令アトミック性(法令ごとには一貫し、再実行で回復する)と、出荷検査が自分の asof を「今日」として使う循環(実体は #23)は、判断のうえ据え置いた。

Gate 3: 実施((B) 区分)。

🤖 Generated with Claude Code

https://claude.ai/code/session_01JS8qpHech2qvH1vrsYQDe5

wharfe and others added 19 commits September 22, 2026 20:46
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERWJU1SjTef52BVw4Te1L6
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERWJU1SjTef52BVw4Te1L6
texts_match([], []) が True を返していたのを修正し、fail-closed にする。
_normalise の全体 strip が行頭の全角スペース(条文の構造の一部)まで
削っていたバグも合わせて修正し、num の食い違いに関するコメントを
実装に即した内容に直す。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERWJU1SjTef52BVw4Te1L6
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERWJU1SjTef52BVw4Te1L6
build_alias_table は同じ label を持つページが2つ以上あれば別名表から
落とす(削除条文のラベルは吸収先レンジの見出しを借りるため衝突しうる)。
validate_articles は former.paragraphs の非空と slug と article_slug(num)
の一致を追加でチェックし、range 形の article_num でも例外を投げない。
cross references のログは attempted/unresolved の分数にして、全滅した
法令と健全な法令を区別できるようにした。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERWJU1SjTef52BVw4Te1L6
validate_articles の slug 一致チェックで article_slug(num) が
ValueError を投げた場合、従来はチェックをスキップしていた
(黙示のフォールバック、グローバル制約違反)。range 形や不正な形の
article_num は collect_changes が弾くはずで、ここに到達すること自体が
報告すべき異常なので、専用のエラーメッセージを追加する。
validate_articles は引き続きリストを返すのみで例外は投げない。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERWJU1SjTef52BVw4Te1L6
不明フラグの黙殺による全件削除(--dry-run が articles/*.json を消す)、
存在しない法令IDの指定を成功扱いにする問題、shipped diff が0件の
full run が既存ファイルを全削除する問題を修正。post-loop の日付変更
番兵(2法令構成)のテストも追加。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERWJU1SjTef52BVw4Te1L6
scripts/articles.py --all を実行し、11法令ぶんの条文ページJSONを生成
(労働基準法は附則のみの変更のためファイル無し)。合計163条文中、
版不一致で current_summary が落ちたのは33件(設計時実測は162中30と近い値)。

法令別(総数/落ちた件数):
  129AC0000000089 (民法):       38 / 2
  140AC0000000045 (刑法):       11 / 3
  325AC0000000201:              14 / 0
  335AC0000000105:              14 / 3
  345AC0000000048:              18 / 3
  403AC0000000076:              20 / 10
  412AC0000000061:              12 / 2
  413AC0000000137:              23 / 3
  415AC0000000057:               1 / 0
  416AC0000000123:               6 / 1
  425AC0000000027:               6 / 6

tests/test_shipped_data.py に出荷済み articles/*.json を検査するテストを追加
(validate_articles・source記録・版一致の再検算・削除条文のtombstone・
summary無しページのrelated_articles空・ページ集合が出荷diffsから導出される
ことの確認)。空globで落ちることを確認してから生成し、全97件緑を確認済み。
uv run pytest 全体も249件緑。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERWJU1SjTef52BVw4Te1L6
e-Gov は一部の廃止を「本文を『削除』の一語に差し替える modified」として
記録する。すると diff の paragraphs_after も今日の本文も ["削除"] になり、
texts_match が削除同士を比べて True を返し、summary_is_safe も引っかからない。
resolve_current は range ノードに畳まれた場合しか墓標と認めなかったため、
民法733・民法746・刑法178 の3ページが status: present のまま、
「現在の条文」という見出しの下に廃止前の説明を現在形で載せ、
ライブリンクまで付けて出荷されていた。

判定を改正の type ではなく今日の本文に置く:

- resolve_current: 本文が削除だけなら新 status "deleted"
  (range に畳まれた merged_deleted とは区別する)。判定は既存の
  _body_is_only_deleted を再利用する
- build_law_articles: 墓標なら gate の結果によらず current_summary を None に。
  related_articles は同じ条件から種を取るので自動で空になる
- former: type == "deleted" ではなく「墓標かつ paragraphs_before 非空」で作る
- validate_articles: 新 status を受理し、former.paragraphs を両方の墓標 status で
  必須化する。墓標なのに current_summary を持つ形も明示的に弾く

あわせて、参照の context(リンク先条文を説明する1行)を、リンク先ページ自身が
current_summary を落としたときに外す。リンクは正しいので残し、説明文だけ消す。
著作権法121条が122条の2を「秘密保持命令違反の処罰」と説明していたが、
今日の122条の2は帳簿の規定で、122条の2 自身のページは正しく説明を落としていた。

test_shipped_data.py の墓標テストは changes[0]["type"] を鍵にしていたため、
壊れていた3ページを一度も見ていなかった。鍵をページが墓標であること自体に
付け替え、context の新規則も出荷バイトに対して張る。

再生成: articles 163(変わらず)/ summaries dropped 33 -> 36 / laws 11(変わらず)、
context 除去 48 件、リンク減は墓標3ページ分の 10 件のみ。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERWJU1SjTef52BVw4Te1L6
HANDOFF.md を Task 9(フロントエンド)再開用に書き直す。current.status が
present / deleted / merged_deleted の3値になった点を筆頭に、計画のコードを
そのまま書くと壊れる箇所を注意に挙げた。

CLAUDE.md: Development Commands に articles.py を追加し、Data Pipeline Flow に
articles.py の位置(出荷済み diff を読み戻して今日の本文と突き合わせる)を書く。

.gitignore: data/articles/ が SDD ワークスペースのブロックに紛れていたので、
他の再生成可能データと同じ「# Data (regenerable)」へ移す。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERWJU1SjTef52BVw4Te1L6
163本の条文ページは、導線が無ければクローラーの届かない孤児のまま
出荷されるところだった。/law/<lawId> に章立ての条文一覧を足し、
/diff の各本則カードに条文ページへのリンクと戻り先の anchor(id)を
足し、sitemap.xml に条文 URL を並べた。

id はカード外側、Link は既存の展開ボタンの外に置いている
(ボタンの中にリンクを入れるとクリックが展開と遷移の両方に掛かる)。
労働基準法など articles/ を持たない法令は getArticleLawIds().includes()
で分岐し、既存の2本の附則のみ diff ビルドを壊さないようにした。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JS8qpHech2qvH1vrsYQDe5
どれも「改正の type で決めていた」「欠落の多い表現どうしを突き合わせていた」
の同じ根。ページを除外するのではなく scripts/articles.py で直す。

F1 履歴カードの時点ラベルを本文で決める(summary_basis)。e-Gov は一部の削除を
   「本文を『削除』に置き換える modified」として記録するので type では決まらない。
   民法733/746・刑法178 が廃止規定の説明を「改正直後の条文についての説明」と
   名乗っていた。判定は Python 側で新フィールドにし、TSX は読むだけにする。
F2 履歴カードの plain_summary にも廃止刑名の検査を通す。検査の相手はそのカードが
   日付で指している本文(summary_basis の側)。著作権法119 は「懲役→拘禁刑」と
   書いた一行下で「懲役を定める」と言っていた。道交法の2件は 2024-11-01 時点で
   真なので残る。
F3 diff.py の `[表]` は情報を落とすので、版整合ゲートが両辺の一致を「同じ」と
   読めてしまう。マーカーがあれば fail-closed(定数 LOSSY_TABLE_MARKER)。
   diff.py は触らない。今日のデータでは 0 バイトの変化(再生成して照合済み)。
F4 複数の条を名指す参照はリンクしない(MULTI_ARTICLE_CONNECTIVES)。
   「第七条から第七条の四まで」が第七条へのリンクになりうる。失うリンクは 0。
F5 出荷 articles ファイルの「集合」を diff から導いて検査する。件数下限だけでは
   10 件消えても緑のままだった。
F6 リンクの説明文は「対象にページがあり、そのページが版整合ゲートを通った」
   ときだけ残す。ページの無い対象は一度も照合されていない。説明文 132 行が
   消え、リンクは 0 件も失わない。

449 → 317 contexts / 365 links 据え置き / 163 ページ据え置き。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JS8qpHech2qvH1vrsYQDe5
Gate 3 周回2。どちらも周回1で自分が書いたコードの中の指摘。

R2-1 MULTI_ARTICLE_CONNECTIVES に「若しくは」「・」「乃至」が無く、F4 の失敗形が
     まだ届いていた(第七条若しくは第八条 → 第七条へのリンクになる、実測)。
     法令用語は「及び は 並びに の内側」「若しくは は 又は の内側」と対になるので、
     片方だけ持つ表は規則の半分。残る2つは起草の慣行ではなく LLM 出力に出る形
     (中黒の列挙・から の旧表記 乃至)。コメントの根拠もその通りに書き直す。
     テストは接続語1つにつき1ケース+「定数に足してケースを忘れる」側の番人も追加。
     今日のデータへの影響は 0(該当 ref 1件は他法令参照で既に除外済み)。
     再生成しても fetched_at 以外1バイトも動かない。リンク 365 → 365。

R2-2 article-history.tsx が summary_basis === "before" で分岐しており、値が
     欠けたり想定外だと「改正直後」に倒れていた。それは F1 で直した欠陥そのもの。
     「改正直後」のほうを積極的に要求する向きへ反転する。「改正直前」は弱いほうの
     主張(この改正より前の本文の説明、という誰にでも当てはまる言い方)で、
     「改正直後」は置き換わった本文の説明だと言い切る。Python 側は
     validate_articles が SUMMARY_BASES 外を拒むので、これは二重の関門ではなく
     倒れる向きの話。描画は不変(164改正 = 直前5 / 直後159)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JS8qpHech2qvH1vrsYQDe5
@vercel

vercel Bot commented Sep 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
lex-diff Ready Ready Preview Sep 23, 2026 1:34am UTC

@wharfe
wharfe merged commit c7e6821 into main Sep 23, 2026
5 checks passed
@wharfe
wharfe deleted the feat/article-pages branch September 23, 2026 01:39
wharfe added a commit that referenced this pull request Sep 23, 2026
articles.py が既存の annotation を「今日の本文」の隣に置く以上、版整合ゲートは
この repo のハルシネーション対策の一部になった。Gate 3 で実際に出荷されかけた
3 つの誤りは、どれも「情報を落とした表現どうしを突き合わせる/本文ではなく改正の
type で判断する」という 1 つの形だったので、その形ごと残す。次に触る人が
TOMBSTONE_STATUSES や LOSSY_TABLE_MARKER を素の文字列比較に戻さないため。

HANDOFF.md は条文ページ(#4)の作業のもので、PR #27 のマージで完結した。
古い引き継ぎは次のセッションを誤った現在地から始めさせるので消す。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JS8qpHech2qvH1vrsYQDe5

This branch was successfully deployed

1 active deployment
Preview — 81f3ed3c Deployed Sep 23, 2026 by vercel[bot]
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