NGW開発BLOG
← 記事一覧へ
·#076

記憶なんて当てにならないどこまで配ったか、どこまで配るべきかで悩んだ話

PDF共通関数は、外部ライブラリ用のディレクトリの下に置いてあった。それだけの理由で配布から落ち、呼び出す側だけが現場に届いた。関数が無いまま「対応済み」になるところだった。どこまで配ったのか、どこまで配るべきなのか。チェックひとつ付け忘れれば同じことが起きる仕組みを、憶えていなくても済む形に作り替えた一日。

配る仕組みは、また手前で壊れた

NGW は自治体ごとのサーバで動く。直したファイルを固めて配り、現場で当ててもらう。この配布と適用の仕組みは何年も問題なく回っている。

少し前に、その手前が壊れていた話を書いた。「何を配るか」を選ぶところだ。ファイルの更新日で選ぶ方式が git と噛み合わなくなり、74件のうち本当に配るべきものは6件だった。内容差分で選ぶ方式に作り替えて、決着したつもりでいた。

今日、同じツールの、さらにその奥で詰まった。何を配るかは正しく選べていた。選んだあとに、1本だけ捨てられていた。

外部ライブラリは配らない、という決まり

配布ツールには除外リストがある。ディレクトリ名を並べただけの、単純な仕組みだ。

添付ファイルの保管場所。ログ。一時ファイル。ダミーデータ。そして外部ライブラリ。この名前がパスに含まれていたら、配布対象から外す。

外部ライブラリを外すのには理由がある。PDF生成にも、表計算にも、画面部品にも、他所で作られたものを使っている。量が多い。滅多に変わらない。毎回配れば更新モジュールが膨らむだけだ。だから既定では配らない。入れ替えたときだけ「外部ライブラリも含める」のチェックを付けて配る。

この決まりは長く正しく働いてきた。

直したのは2か所だった

今日の作業は、サーバに溜まったエラーログの棚卸しから始まった。その中に、決裁書類の一括印刷が止まる件があった。

添付されたPDFにパスワード保護がかかっていると、複数のPDFを1本にまとめる処理が例外を投げる。一括印刷そのものが終わる。 保護されたPDFは画面のプレビューでは普通に開けるので、添付した本人は気づかない。決裁する側だけが踏む。

直し方は決めやすかった。まとめられないPDFは飛ばして、最後のページに「このファイルは結合できませんでした」と理由つきで並べる。全部か無しかにすると、添付1本のせいで書類そのものが印刷できなくなる。それが一番困る。

修正は自然と2か所に分かれた。

  • 一括印刷の処理 — まとめる繰り返しを例外で囲み、飛ばして続ける
  • PDF共通関数のファイル — 「なぜ結合できなかったか」を日本語に変える関数を新設

理由の判定は他の帳票からも使う。だから共通関数の側に置いた。設計としては素直だ。

zipを開いて、1本足りなかった

配布用のzipを作り、中身を一覧に出した。習慣でやっている確認だ。

今日直したのは19ファイル。zipに入っていたのは18だった。

落ちていたのは、新設した関数が入っているファイルだった。置き場所は libCreatePDF/include/PDF生成ライブラリを収めたディレクトリの、その下の階層だ。ここに入っているのは自社のコードだけで、ライブラリ本体は別の階層にある。

だが、上のディレクトリの名前は外部ライブラリのものだ。パスにその名前が含まれる。 除外リストはそれを見て、外していた。

一括印刷の処理は配られる。その中から呼ばれる関数は配られない。

「半分直っている」が一番わるい

このまま配っていたら、どうなっていたか。

保護されたPDFが添付された決裁書類を、誰かが一括印刷する。修正前なら、PDFをまとめる処理が例外を投げて止まっていた。修正後は、その例外を受け止めて飛ばそうとする。飛ばした理由を作るために関数を呼ぶ。その関数が存在しない。

Call to undefined function

止まることに変わりはない。むしろ悪い。直したつもりになっている分だけ、悪い。

未修正なら「まだ直っていない」で済む。半分だけ直っていると、報告は「対応済み」になり、現場では止まり続ける。突き合わせるまで誰も気づかない。

エラーの出方も、修正前より遠い場所になる。PDFの処理が投げた例外を追うのと、存在しない関数の名前から「なぜ配られていないのか」に辿り着くのとでは、距離が違う。

ルールは場所を書いていて、意味は持ち主だった

原因を一行で書くとこうなる。

除外ルールは どこに置いてあるか を書いていた。判定したかったのは 誰が書いたか だった。

そのディレクトリの名前は「外部ライブラリの置き場」の意味で付けていた。実際にはそこに自社のコードも同居していた。PDF共通関数がそうだ。PDF関連だからPDFのライブラリの隣に置く。置き場所としては自然で、10年以上そのままだった。

名前が二つの意味を持ってしまうと、機械は場所しか見られない。「外部のものだから配らない」と「その名前がパスに入っているから配らない」は、長いあいだ同じ結果を出していた。 ずれた瞬間に、静かに間違える。

チェックボックスは、安全装置ではなかった

ここで一度、手が止まった。

ツールには「外部ライブラリも含める」のチェックがある。付ければ配られた。つまり道具は困っていない。困ったのは、付け忘れたことだ。ならば運用で気をつければいい——と、書きかけた。

そこで指摘が入った。

私が修正した場合はわかる。だが Claude が修正したら漏れることが確定だからだ。

これは効いた。

自分で書いたコードなら、どのディレクトリを触ったか覚えている。だからチェックを付けられる。その前提は、書いた人と配る人が同じときにしか成り立たない。 誰かに直してもらったとき、AIに直してもらったとき、配る側は触った場所を知らない。知らないものはチェックできない。

これは注意力の問題ではない。構造の問題だ。 「覚えていれば防げる」仕組みは、覚えている人が配る限りにおいてのみ安全装置として働く。人が増えた瞬間、あるいは道具が増えた瞬間、ただのチェックボックスに戻る。

今回はまさにその形で漏れた。修正を書いたのは私ではない。私は、その場所が触られたことを知らなかった。

判定が、2か所にあった

直しにかかって、もう一つ出てきた。

除外の判定が2か所に書かれていた。 内容差分で選ぶ方式と、従来の更新日で選ぶ方式。片方は共通の関数を呼び、もう片方はその場でディレクトリ名を照合していた。書いてあることは同じだ。だが同じものが2か所にあると、片方だけ直したときに黙ってずれる。

今日の事故はこの構造がなくても起きた。だが同じ形の事故を、これから何度でも起こせる構造ではあった。ついでに片付けた。

直した形

四つ変えた。

1. ライブラリ配下でも、自社コードの階層は常に配る

libCreatePDF/include は自社コードだと明示的に書いた。ライブラリ本体は除外のまま。除外リストに例外を1行足した形だ。ルールに「持ち主」の情報を持たせた。

2.「外部ライブラリも含める」を「外部ライブラリだけ」に変えた

ライブラリの入れ替えは滅多にない。量は多い。通常の修正と混ぜるとzipが膨らみ、更新記録も読みにくくなる。入れ替えたときは外部ライブラリだけのzipを別に作る。混ぜない。

3. 判定を1か所に寄せた

両方の方式が同じ関数を呼ぶようにした。

4. 外部ライブラリ側の件数を数えて出す

後述。憶えていなくても気づけるようにした。

多い分には、害がない

ここで、この道具の性質を思い出した。

NGW は PHP で書かれている。コンパイルしないのでソースをそのまま配る。全団体が同じファイル構成で動く。だからすでに配ったファイルをもう一度配っても、同じ中身で上書きされるだけだ。何も起きない。

つまり、こうなっている。

起きること
多く入れる無害。上書きされるだけ
少なく入れる致命的。呼び出す側だけが届く

非対称だ。 ならば、迷ったときに取るべき方向は決まっている。範囲を狭く刻む努力に意味はない。広く取ればいい。

そうすると、答えはずいぶん簡単になる。外部ライブラリ以外の包括と、外部ライブラリだけの包括。この2本を毎回作る。 どちらを作るか考えなくていい。両方作るのだから。

それでも、憶えていないといけなかった

書いていて、まだ穴があることに気づいた。

「2本作る」は、外部ライブラリを触ったかどうかを憶えている前提に立っている。触っていない月は2本目が空になる。空になると分かっていれば作らない。作らないと決めるために、触ったかどうかを思い出す必要がある。

結局さっきと同じ形だ。チェックを付け忘れる代わりに、2本目を作り忘れる。

そこで、こういうやり取りになった。

私の記憶なんて、当てにならないのです

数えさせることにした。通常のzipを作るとき、同じ選択のまま判定だけを「外部ライブラリ側」に倒して数え直す。1件でもあれば、画面に出す。

ℹ 外部ライブラリにも 3 件の変更があります(このZIPには入りません)
  「外部ライブラリだけ」にチェックを入れて、同じ起点でもう1本作ってください
    ・lib/css/sweetalert2.css
    ・lib/fontawesomeproNgw/css/v4-shims.min.css
    ・lib/js/swal/sweetalert2.js

0件なら何も出さない。落ちるものがあるときだけ喋る。

試しに範囲を1か月に広げて数えたら、本当に3件出た。しかもその中に、こちらで手を入れたJavaScriptが混じっていた。古い書き方の呼び出しを新しいライブラリに繋ぐ、小さな追記だ。配り漏れたら、その呼び出しが全部だめになる。憶えていられる自信は、まったくない。

数字で見る

項目
今日の修正19ファイル
zipに入っていた18ファイル
落ちていた1ファイル(新設した関数の本体)
落ちた原因パスに外部ライブラリのディレクトリ名が含まれていた
気づいた方法zipの一覧と修正一覧の突き合わせ
直した箇所除外の例外1行 + 判定の一本化 + 件数の表示
1か月に広げたときの外部ライブラリ側3件(うち1件は自分で手を入れたJS)

1ファイルだ。19分の1が抜けただけで、修正は機能しない。配布物は、足りている割合では測れない。

AIとの協働作業での学び

今回の漏れは、AI を入れたから表に出た種類のものだった。

除外の仕組みは20年、正しく働いてきた。付け忘れが起きなかったのは、注意深かったからではない。書いた人間と配る人間が同じだったからだ。AI が修正を書いた瞬間、その一致が崩れた。 道具は一行も変わっていないのに、安全装置としては働かなくなっていた。

事実の確認は、AI のほうが速かった。zipの中身を並べて修正一覧と突き合わせ、1本足りないと出す。判定が2か所に分かれていることまで見つけてくる。その速さは疑いようがない。

止まったのは、その先だ。「チェックを付ければ配られます」——これは事実で、反論の余地がない。だが事実として正しいことと、この先も成り立つことは別だ。 成り立たなくなった当人が、まさに今日の相手だった。更新日で選ぶ方式が壊れたときも同じだった。壊れるのは道具ではなく前提で、前提はエラーを出さない。そして前提が壊れたことに気づけるのは、たいてい使う側だ。

気づけたのは、動かしたからでもない。zipを開いて、入っているファイルを目で数えたからだ。 保護されたPDFを手元に持っていなければ、動かしても素通りしていた。この種の漏れは、実行した瞬間ではなく、実行しなかった経路で待っている。

道具を人に合わせるとき、合わせる先の人は、いつも自分とは限らない。明日には、自分すら「触った場所を覚えている人」ではなくなっている。