記憶なんて当てにならないどこまで配ったか、どこまで配るべきかで悩んだ話
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を手元に持っていなければ、動かしても素通りしていた。この種の漏れは、実行した瞬間ではなく、実行しなかった経路で待っている。
道具を人に合わせるとき、合わせる先の人は、いつも自分とは限らない。明日には、自分すら「触った場所を覚えている人」ではなくなっている。