Skip to content

feat: law_summary.py に条文を根拠として渡す(#16)+ 4法令の概要を埋める(#14) - #19

Merged
wharfe merged 7 commits into
mainfrom
feat/law-summary-evidence
Sep 9, 2026
Merged

wharfe merged 7 commits into
mainfrom
feat/law-summary-evidence

Conversation

@wharfe

@wharfe wharfe commented Sep 9, 2026

Copy link
Copy Markdown
Owner

何を直すか

3本ある生成系(annotate.py / explainer.py / law_summary.py)のうち、law_summary.py だけがモデルに法令のテキストを渡していなかった。渡していたのは法令名・番号・分類・改正回数の4つだけで、答えの全ての語がモデルの記憶から出ていた(#16)。検証器は2025年に廃止された刑名を1件弾くだけで、誤ったスコープや別の廃止済み制度は素通りする。

あわせて、概要ブロックが無かった4法令(民法・道路交通法・労働基準法・著作権法 — いずれも検索表示の中心にある大物)を埋めた(#14)。

設計

モデルに渡す根拠は 目次(編章節の見出し)+ 各条の見出し + 第一条の全文。全文は渡さない(民法は226,000字)。条見出しは民法で1,090件・15,467字あり、その法律が何を扱うかの一覧として最も密度が高い。第一条は目的・適用範囲の規定なので scope の直接の根拠になる — 刑法第一条は「日本国内において罪を犯したすべての者に適用する」で、それ自体が答えである。

検証は2層。公開データに CI で常時かけられる層(形+廃止刑名)と、生成時にしかかけられない層(キーワード全語が根拠に実在すること)を分けた。data/raw は gitignore なので CI にスナップショットが存在せず、根拠照合ができないため。散文の語レベル照合はしない(「事業者」「日常生活」で偽陽性が出る)。

根拠は生成と同じ呼び出しの中で取得する。 ディスク上のファイルから「この条文は現行か」を判定する試みは Gate3 で2度失敗した。原因は毎回同じで、ディスク上のどこにも「いつ取得したか」が記録されていないこと — asof は「いつ時点の法令か」であって「いつ取得したか」ではなく、実データで 129AC0000000089_2026-04-01.json は 2026-03-26 に作られていた。取ったその場で使えば、鮮度を推測する必要がない。

Gate3 で見つかった、この変更なしでは起きていた事故

12法令中10法令が、施行済みの最新改正より古い条文を根拠にしていた。 data/raw にあるのは誰かが diff のために取った日付であって「今」ではない。刑法にいたっては全スナップショットが 2025-06-01 の拘禁刑統合より前で、根拠に「懲役」「禁錮」が3回ずつ現れる一方、プロンプトはその語の使用を禁じていた。根拠に忠実なモデルは検証で弾かれ、無視するモデルは記憶から書く — この検証器が存在する理由そのものの法令で、その構造ができていた。

検証

  • uv run pytest -q → 108 passed(87 → 108)
  • npm run lint → 0 errors / npm run build → 成功、12法令すべてに概要ブロックが出力
  • 新しいガードはすべて変異検算済み(1つずつ壊して赤を確認し、復元して緑)
  • 概要の新旧比較(人による目視1回): 全12法令を並べて確認済み

Gate3

3ラウンド+完成差分への再レビュー1回。critical は各ラウンド 1 / 1 / 3 / 2 で、ラウンド1〜3は根が同一(渡す条文が現行か)だったため、CLAUDE.md の規律に従って実装を止めてユーザーに上げ、判定する仕組みを足すのをやめて設計を変えた。レビュアーは Claude 構造化レビュー・intent-alignment(fresh subagent)・codex(異族)。

最終確認で出た critical 2件(不正な API 応答が既存スナップショットを破壊する/第一条の本文が空でも生成が続く)は修正し、変異検算で閉じた。この最後の修正自体は外部レビューを受けていない(規律上、修正は周回ではなく検算で閉じる)。

Closes #14
Closes #16

🤖 Generated with Claude Code

https://claude.ai/code/session_01VXy5NU1LCXmEcZfew4ezSu

wharfe and others added 7 commits September 8, 2026 23:38
law_summary.py が extract_text / walk_tags を必要としたため、diff.py から
純粋な移動として切り出す。llm.py が生まれたのと同じ理由 — 3本の生成系が
それぞれヘルパのコピーを持ったことが、そもそも互いに drift した原因だった。

振る舞いは変えていない(既存 87 件が緑のまま)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VXy5NU1LCXmEcZfew4ezSu
law_summary.py は3本の生成系で唯一、モデルに法令のテキストを一切渡して
いなかった。法令名・番号・分類・改正回数だけを渡し、答えの全ての語が
モデルの記憶から出ていた。検証器は既知の1事故(廃止刑名)しか止めない。

根拠として渡すもの: 目次(編章節の見出し)+ 各条の見出し + 第一条の全文。
全文は渡さない(民法は 226,000 字で、どんな価格でもプロンプトに入らない)。
条見出しは民法で 1,250 件・17,823 字あり、その法律が何を扱うかの一覧その
ものとして最も密度が高い。第一条は目的・適用範囲の規定なので scope の直接の
根拠になる — 刑法第一条は「日本国内において罪を犯したすべての者に適用する」
であり、それ自体が答えである。

検証器を2層に分ける:
- validate_summary_shape: 形と廃止刑名。公開済みデータにも常時かけられる
  (data/raw は gitignore なので CI にスナップショットが存在しない)
- validate_summary(summary, evidence): 上記に加え、キーワード全語が条文に
  実在すること。散文は語で照合できない(「事業者」「日常生活」で偽陽性が
  出る)が、キーワードは名詞なので機械判定できる。存在しない制度名は
  まずここに出る。evidence は必須引数 — 省略可能にすると、根拠を失った
  呼び出し側が黙って元の記憶頼みの挙動に戻る。

証拠が空ならモデルを呼ばずに raise する(annotate.py と同じ契約)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VXy5NU1LCXmEcZfew4ezSu
民法・道路交通法・労働基準法・著作権法の /law ページには概要ブロックが
無かった(law_summary.py が一度も走っていなかった)。いずれも表示回数の
中心にある大物で、民法は直近2週間の検索表示の大半を集めている。

既存8件も根拠なしで書かれたものなので同時に作り直した。刑法の適用範囲は
「一定の場合には国外で犯罪を行った日本国民など」— これは第三条(国民の
国外犯)の話で、第一条とは別の条文だった。新しい文は第一条二項どおり
「日本国外にある日本船舶・日本航空機内において罪を犯した者」となっている。

道路交通法は生成が一度検証で止まっている(モデルが「駐車及び停車」と
書いたが、実際の章題は「停車及び駐車」)。何も保存せず exit 2 で終わり、
再実行で通った。検証器が想定どおり働いた実例。

test_shipped_data の summary チェックを「無ければスキップ」から
「無ければ失敗」に変えた。次に法令を追加したとき同じ穴が黙って再発しない。

Closes #14
Closes #16

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VXy5NU1LCXmEcZfew4ezSu
Gate3 の3層(Claude 構造化レビュー / intent-alignment / codex)が独立に
同じ根を指した。「根拠を渡す」と「"現行の"根拠を渡す」は別の要件で、
前者しか実装していなかった。

data/raw に入っているのは誰かが diff のために取った asof であって「今」では
ない。実測すると 12法令中10法令が、施行済みの最新改正より古い条文を根拠に
していた。刑法は全スナップショットが 2025-06-01 の拘禁刑統合より前で、根拠に
「懲役」「禁錮」が3回ずつ現れる一方、プロンプトはその語の使用を禁じていた —
根拠に忠実なモデルは検証で弾かれ、無視するモデルは記憶から書く。この検証器が
存在する理由そのものの法令で、その構造ができていた。

- main() は最新スナップショットが最終施行改正より古いとき exit 3(モデルを
  呼ばない)。fetch すべきコマンドを出力する
- load_evidence は未来日付のスナップショットを使わない(パイプラインは diff の
  ために施行前の日付を日常的に取得する)。最新候補が壊れていたら古い版へ
  フォールバックせず raise する — 成功に見えて旧法を現行法として公開するため
- 空根拠ガードが機能していなかった。空文字の見出しノードが1つあるだけでリストは
  truthy になり、「## 目次」というラベルだけを根拠としてモデルを呼んでいた。
  組み立て後の本文で判定する
- e-Gov の XML は TOC 要素で目次を重複して持っており、本文側と両方を集めていた
  (民法で364行中197行が重複)。SKIP_SUBTREES に TOC を追加
- articles[0] を検証なしに「第一条」と呼んでいた。Num を確認し、違えば実際の
  番号でラベルする
- 1文字のキーワードを弾く。「刑」は「刑罰」の中にマッチするため根拠照合を
  すり抜ける。刑法が実際にそれを出荷しかけた
- 全12法令の現行条文を取得し直して再生成

テストは変異検算済み(各ガードを1つずつ壊して赤を確認)。空根拠ガードのみ
二重防御のため、両方を外したときに赤になる。

CLAUDE.md の AI 生成の節は law_summary.py を「根拠なしの例外」と書いたままだった
(#16 が閉じたので事実と逆)。現行の2層検証と「根拠の鮮度」の要件に更新。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VXy5NU1LCXmEcZfew4ezSu
ラウンド1で入れた鮮度ガードは fail-open だった。最新スナップショットを
<law_id>_revisions.json の最終施行改正と比べていたが、その改正一覧自体の鮮度は
誰も保証していない。timeline.py は既存ファイルを無期限に再利用するので、条文と
改正一覧が揃って古い通常ケースでは used >= latest が成立して素通りする。欠損・
不正形式・空リストはいずれも None になり、これも素通りだった。

同じ根で2ラウンド連続の指摘なので、この機械が何を買っているのかを問い直した。
答えはほとんど何も、だったので比較ごと捨てる。当日取得の条文だけを現行と認めれば
信用すべき2つ目のファイルが無くなる。fetch.py は数秒で、概要の生成は稀にしか
走らない。既存の12法令は取得時点で現行だったため再生成はしていない。

- main() は最新スナップショットが当日でなければ exit 3(fetch コマンドを提示)
- 「今日」は Asia/Tokyo。日本の施行日を UTC ランナーで判定すると、日本時間の
  深夜0時から9時が前日扱いになり、その日施行の改正を落とす
- latest_enforced_revision を削除
- ファイル名は YYYY-MM-DD の厳密一致のみ候補にする。<law_id>_2025-01-01_copy.json
  は正規ファイルより後にソートされ、日付でない文字列のまま比較されていた
- 空根拠ガードの残穴: ArticleTitle だけの Article は extract_text が「第一条」を
  返し、ラベルだけで検査を通っていた。Sentence の存在を構造的に確認する
- 第一条が無い法令では、その条を scope の根拠と断定しないよう根拠に注記する。
  ラベルを直しただけでは、定義規定を適用範囲として読ませる問題が残っていた
- 1文字キーワード拒否は grounding ではなく出力品質のヒューリスティックである旨を
  明記(「の罪」のような部分文字列は依然通る)
- テスト fixture が実データの構造とずれていた(Paragraph に Sentence が無い)

変異検算済み: 当日必須 / ファイル名厳密判定 / 構造的実質判定 / 第一条の注記 を
それぞれ単独で壊して赤を確認し、復元して緑。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VXy5NU1LCXmEcZfew4ezSu
ディスク上のファイルから「この条文は現行か」を判定する試みが2ラウンド続けて
critical を出した。原因は毎回同じで、ディスク上のどこにも「いつ取得したか」が
記録されていないこと。

- 改正一覧との比較は、鮮度を誰も保証しない2つ目のファイルを信用していた
- 最新 asof が当日かの判定は、ファイル名しか見ていない。asof は「いつ時点の
  法令か」であって「いつ取得したか」ではない。実データで
  129AC0000000089_2026-04-01.json は 2026-03-26 に作られており、4月1日には
  「当日のもの」として通っていた

同じ根で3ラウンド目なので、判定する仕組みを足すのをやめ、問い自体を消した。
生成と同じ呼び出しの中で fetch する。取得直後の条文を使うので、鮮度を推測する
必要がない。spec で決めた「fetch は law_summary から呼ばない」という責務分離を
捨てる判断(ユーザー裁定済み)— その分離こそが穴の出どころだった。

- fetch_evidence() が e-Gov から当日の条文を取得し、data/raw に保存して根拠を
  組み立てる。取得失敗は raise(古いファイルへのフォールバックはしない。それが
  この問題の原因そのもの)
- load_evidence / newest_snapshot / snapshot_date / SNAPSHOT_RE を削除
- モデルの応答中に JST の日付が変わったら保存しない。生成は日付をまたぐ長さが
  ありうる
- 空の <Sentence/> だけを持つ条文を根拠と認めない(切り詰められた API 応答の形。
  タグの存在だけを見ていたため「第一条」というラベルが本文として通っていた)
- テストが実際に API を叩いていた。fetch_law_data をモックする

変異検算済み: API応答の検査 / 空Sentenceの排除 / 日付またぎの保存拒否 /
取得結果の保存 をそれぞれ単独で壊して赤、復元して緑。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VXy5NU1LCXmEcZfew4ezSu
完成差分への再レビューで、fetch を生成に組み込んだこと自体が生んだ穴が2つ出た。

1. data/raw への書き込みが応答の検証より先だった。truthy だが中身の無い
   law_full_text が返ると、保存してから build_evidence が失敗する。data/raw は
   diff パイプラインと共有しており diff.py がこれを読むので、概要生成の失敗が
   別パイプラインの入力データの破壊になっていた。検証を先に済ませ、一時ファイル
   から os.replace で原子的に置き換える。

2. 第一条の本文が空でも、章見出しや条見出しが1つ残っていれば根拠が非空になり
   生成が続いていた。プロンプトは「根拠に第一条を含む」と説明したままなので、
   適用範囲の根拠が無いまま scope を書かせることになる。条が存在するのに非空の
   Sentence が無い場合は、見出しの有無によらず失敗させる。第一条そのものが無い
   法令(条が0件)とは別の状況で、安全に続行できるのは後者だけ。

モジュール docstring が削除済みの「改正一覧との比較」を説明したままだったのも
修正(CLAUDE.md は既に現行の設計に一致している)。

変異検算済み: 空条文で失敗させる / 検証してから書く順序 を単独で壊して赤、
復元して緑。テストはレビュアーが示した経路をそのまま写した。

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

vercel Bot commented Sep 9, 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 9, 2026 1:14am UTC

This branch was successfully deployed

1 active deployment
Preview — 6c4f6f22 Deployed Sep 9, 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

1 participant