• #会計基準
  • #JSON
  • #条文データ
  • #ASBJ
  • #diff
  • #OCR
  • #バッチ処理
開発eurekapu-nuxt4メモ

CF計算書実務指針42条文の原文忠実化

前日に会計基準のソース収集とURL検証を済ませた流れで、今日は cf-jitsumu-shishin.json の中身そのものを原文に合わせる作業に入った。前文・本文・附則あわせて42条文。スプレッドシートで目視差分を取るような規模ではなく、比較スクリプトを書いて差異を機械的に洗い出してから手で当てる方針にした。それでもノイズの除去で半日溶けた。

出発点: OCR由来の差異と原文の差異を分離する

JSONの元データはPDFをOCRしたもので、原文(asb-j.jp)と比べると、語の入れ替わりのような実質的な差異と、数字周辺の半角スペースや改行位置のような表記ノイズが混じっている。前者だけ拾って後者は無視したい。

最初に書いた比較スクリプトは、DBダンプとJSONを provision 単位で読み込んで、差分を行ごとに出力するだけの素朴なものだった。動かしたら42条文中ほぼ全件で「差異あり」と出てきて画面が真っ赤になった。中身を読んだら、99%が「123 」と「123」のような半角スペース1個の違いだった。

ノイズを潰す: 比較関数の正規化

差異判定の前に両者を同じ基準に揃える必要がある。比較関数で以下を正規化してから diff を取るように直した。

const normalize = (s: string) =>
  s
    .replace(/\s+/g, "")        // 空白類を全削除
    .replace(/[((]/g, "(")     // 全角・半角丸括弧を統一
    .replace(/[))]/g, ")")
    .normalize("NFKC");          // 数字・記号の全半角を統一

正規化後に差異が残ったものだけを「真の差異候補」として抽出する方針にしたら、42条文のうち候補は20条文程度まで絞れた。これでようやく目視確認の射程に入った。

ただし正規化を強くしすぎると、原文側の意図的な空白(例: 数値と単位の間のスペース)まで潰してしまう。最初は迷ったが、比較段階では潰し、置換段階では原文の表記をそのまま使う、と役割を分けた。比較関数と置換関数を別物として扱うことで、ノイズ判定と書き戻しのルールを独立に決められる。

次に詰まったのは: アンカー検出のバグ

原文の特定箇所をJSON側で置換するために、置換対象テキストの前後数十文字をアンカーとして使う仕組みを書いた。ところが「1-2.」項の中にある「2.」という列挙記号が、別の独立した「2.」項のアンカーに誤マッチして、書き換える場所を間違える事故が起きた。

候補生成のロジックも甘く、provision の境界をまたいで前後文脈を取ってしまうケースがあった。アンカーは provision の本文内に閉じる、かつ前後5文字以上のユニーク部分文字列を必須にする、という制約に直して落ち着いた。

// アンカーは provision 本文内に閉じる + 前後5文字以上ユニーク
const findAnchor = (body: string, target: string) => {
  const idx = body.indexOf(target);
  if (idx < 0) return null;
  const prefix = body.slice(Math.max(0, idx - 8), idx);
  const suffix = body.slice(idx + target.length, idx + target.length + 8);
  return { prefix, target, suffix };
};

バッチ1: 和暦→西暦と provision 4の保守適用

最初のバッチは様子見で軽めに。和暦表記(昭和○年)が原文では西暦になっていたのを揃えた。provision 4 には複数の引用先間違い候補が出ていたが、自動置換で踏み抜くと条文番号がずれる。「第○項」のような相互参照を機械的に置換すると、参照元と参照先がずれて意味が壊れる典型ケース。確実に正しい2件だけ手で当てて残りは保留にした。

このバッチを通したあとに pnpm build を回し、フロントエンドの引用コンポーネントが正しく描画されることを確認した。和暦→西暦の置換は表示側にも影響するので、ビルド時点で見ておかないと積み残しが膨らむ。

バッチ2〜3: 6条文ずつ進める

バッチ2で provision 6, 7, 8, 9 の4条文。バッチ3で 12, 13, 14, 16, 17, 18, 20 の7条文を処理し、5件の修正が通った。バッチを区切ったのは、各バッチの後に pnpm build を走らせて、フロントエンド側の引用コンポーネントが正しく描画されるか確認するため。一気に全件適用して壊れたら、どの修正が原因か切り分けに時間がかかる。

ビルドが通るたびに「あと残り○条文」とコメントを残した。途中で帰ってきても続きから進められる。

バッチ4〜6: 残り provision 21〜50

後半は機械作業に近かった。差異候補を出して、アンカーを確認して、置換して、ビルドを回す。同じループを20回ほど繰り返した。原文には「,」と「、」の混在のように、ASBJ側でも統一されていない揺れがあって、これは原文に合わせる方針で割り切った。「JSON側で統一する」を選ぶと、原文と照合する次の作業者が同じ判断を再度迫られる。原文に揃える方が長期的に楽。

附則の条文は本文と書式が違っていて、アンカー検出の制約に引っかかった。「附則」のセクション内では provision 番号の付け方が変わるため、境界判定を一度緩める必要があった。スクリプト側に附則用の例外フラグを足して凌いだ。

最終的に42基準すべてでビルド検証OK。standards.jsonは前日のうちに asb-j.jp ベースに切り替え済みだったので、cf-jitsumu-shishin.json 側を揃えたことで両者の整合が取れた。

後始末: バックアップ削除と2コミット分割

作業中、置換ミスから戻すために provision 単位のバックアップJSONを6個作っていた。ビルドが通った時点で全部削除した。残しておくと次の差分検出で誤検知の元になる。

コミットは2つに分けた。

  • 1つ目: cf-jitsumu-shishin.json の42条文修正(CF基準作業の本体)
  • 2つ目: 比較スクリプト周辺で派生したユーティリティ設定(CF基準とは独立した変更)

混ぜると「なぜこのスクリプト変更がCF基準コミットに混ざっているのか」を後から読み解くのに時間がかかる。git logで意図が読めるように切り分けた。

学んだこと

  • OCR由来の差異と原文の差異は、最初に正規化関数で潰さないとノイズに埋もれる。半角スペース1個の違いを20回見せられると判断力が削られる
  • 比較関数と置換関数は別物として扱う。比較ではノイズを潰し、置換では原文の表記をそのまま使う。役割を分けるとルール設計が独立してシンプルになる
  • アンカー検出は provision の境界に閉じる制約をつけないと誤マッチする。「1-2.」の中の「2.」が独立した「2.」項に誤って当たる事故は、最初に1件踏んだら2度と起こさないように設計で潰す
  • 42条文を一気に当てるのではなく6バッチに分けて、各バッチ後にビルド検証を入れる。失敗時の切り分けコストが段違いに下がる
  • バックアップJSONは作業中だけ残し、ビルド通過と同時に削除する。残すと次の差分検出のノイズになる
  • 表記揺れは「原文に合わせる」を原則にする。JSON側で統一する判断は次の作業者にも同じ判断を強いる

明日以降のメモ

  • cf-jitsumu-shishin.json と standards.json の不整合検出スクリプトを定期実行できるようにする(現状は手動)
  • アンカー検出ロジックを別ファイルに切り出して、他の条文JSONでも使い回せるようにする
  • provision 4 で保留した置換候補(和暦→西暦以外)を再評価する