発端
TokiQRの音声QRコードは動いていた。録音して、エンコードして、QRコードに変換して、再生する。一連の流れは安定していた。次に必要だったのは、それを国立国会図書館(NDL)に納本するパイプラインだった。
納本自体は以前から行っていた。しかし運用を続けるうちに、当初の設計では対処できない制約が次々と顔を出し始めた。顧客ごとにリポジトリを切る方式、PDFをバイナリのままgitに入れる構造、クライアント側で完結するビルド。どれも開発時には合理的だった判断が、運用フェーズでは足かせになった。
2日間で3つのリポジトリにまたがる約60本のプルリクエストを出した。やったことの記録を残す。
制約が見えた順番
GitHubの帯域制限
最初にぶつかったのは、GitHub APIの帯域制限だった。顧客ごとにニュースレター用のリポジトリを作成し、そこにPDFを格納する設計だったが、ファイルサイズが1MBを超えるとAPIのレート制限に引っかかる。1件2件なら問題ないが、複数のニュースレターを連続で納本するとすぐに枯渇する。
PDFに埋め込んだQRが読めない
ニュースレターPDFには音声データを格納したQRコードが埋め込まれている。納本後に復元するためだ。ところが、PDFからQRを読み取ろうとすると、安定して読み取れないことがわかった。
原因を掘ると、制約の連鎖が見えた。jsPDFが生成するPDFはファイルサイズが大きく、gitのバイナリ差分が効かないため更新のたびにリポジトリが膨張する。それだけではない。QRコードのスキャン解像度が足りなかった。Version 40のQRコードを800pxでレンダリングすると1モジュールあたり約2.7ピクセル。jsQRライブラリが安定して検出するには最低3〜4ピクセル必要で、検出率が実用レベルに達しなかった。
方式別に検証した。800px、1200px、1500pxと解像度を上げて検出率を測定し、OpenCVによる検出も試した。OpenCVはVersion 40で失敗。jsQRは1500pxまで上げれば1モジュール約5.1ピクセルになり安定した。さらにPDFのリンクアノテーションからURLを直接抽出する方式を発見し、QRのスキャンを経由せずに100%の精度で読み取れることがわかった。
当時はアノテーション抽出を第一手段、jsQR 1500pxスキャンをフォールバックとする二段構えを採用した。この方式自体はPDFからZIPへの転換に伴い役割を終えるが、ここで得た「QRの生成と読み取りの精度は仕組みで担保しなければならない」という教訓は残った。のちにサーバー側のビルドパイプラインに、生成したPDFのQRを実際にスキャンして照合する自動検証を組み込むことになる。次に待っていたのは、バルクモードでQRに詰め込むURLがVersion 40の容量を超えるという問題だった。制約が一つ解けるたびに、次の制約が顔を出した。
Auto-mergeの競合
TokiQRのリポジトリにはauto-mergeワークフローを設定してある。CIが通ればすぐにマージされる。これは便利だが、1つのブランチに追加のコミットをpushしようとすると、pushが届く前にマージが完了してしまうことがあった。変更が宙に浮く。
退くか、通すか
PDFのファイルサイズが膨らみ、GitHub Actionsが失敗し、QRが読み取れない。制約が重なるのを目の当たりにした時、頭をよぎったのは撤退の選択肢だった。
バルクモードをやめた方がいいのかもしれない。納本を一般の利用者に開放するのは無理があったのかもしれない。アカウントを分けて、顧客ごとに仕組みをコピーしていく方法に切り替えた方がいいのかもしれない。あるいはニュースレター本誌にレジェンド枠を設けて、半年に一人だけ選んで載せる形にすれば、パイプラインの負荷は劇的に減る。どれも一瞬は合理的に見えた。
しかしどの選択肢も、機能か価値のどちらかを削ることになる。バルクモードをやめれば長尺の音声を納本できない。一般開放をやめれば1000年保存サービスの意味が狭まる。顧客ごとのコピーは運用コストが線形に増え続ける。レジェンド枠は希少性を演出できるが、「誰でも自分の声を残せる」というサービスの根幹を手放すことになる。制約の前で機能を諦めるのではなく、機能と価値を維持したまま制約を迂回する経路を見つけると決めた。やめることを考えたからこそ、逆に退路が断たれた。各撤退案で何が失われるかを言語化した結果、残る道は前しかなかった。
突破口
newsletter-masterへの集約
顧客ごとにリポジトリを作る方式をやめた。newsletter-masterという単一のリポジトリに、全シリーズのZIPとPDFを集約する構造に移行した。GitHub Actionsでビルドし、GitHub Pagesで配信する。シリーズ一覧ページと個別ページはPythonスクリプトで自動生成される。
これによりAPIの帯域問題は解消された。ZIPファイルをpushするだけでワークフローがPDFを生成し、インデックスページを更新し、GitHub Pagesにデプロイする。一連のプロセスがサーバー側で完結する。
PDFからZIPへの転換
PDFを中間フォーマットとして使うのをやめ、ZIPを軸にした。音声データ、manifest.json、メタデータを1つのZIPに固めて、それがパイプラインの一次ソースになる。PDFはZIPからの派生物としてサーバー側で生成する。
この転換で3つのことが同時に起きた。PDFの生成をサーバー側に移したことで、jsPDFとフォントファイル(2.7MB)をクライアントから除去でき、生成されるPDFのサイズも大幅に縮小された。ZIPはそのまま再生用のデータソースになるため、play.htmlに?zip=パラメータを追加するだけでニュースレターの再生が可能になった。そしてZIPが一次ソースになったことで、gitリポジトリの肥大化も緩和された。
キューによる非同期化
NDLへの提出は動いていた。1人が1件ずつ提出する限りは。しかし「これを100人が同時に実行したらどうなるか」と問うた瞬間、制約が炙り出された。提出のたびにリアルタイムでGitHub APIを叩いてPDF生成とgit pushを行う方式では、APIのレート制限(認証済みで5000リクエスト/時)に到達する。利用者が増えた時点で詰まる。動いているように見えるものと、スケールするものは違う。
ZIPをqueue/ディレクトリに置き、5分間隔のタイマーで処理する方式に変えた。提出が失敗しても再試行される。利用者は結果を待たずに次の操作に移れる。リアルタイム性を手放すことで、同時実行の制約から解放された。
1ブランチ1コミットの規律
Auto-mergeの競合問題は、開発段階から敷いていた規律で防いだ。1つのブランチには1つのコミットだけを入れる。追加の変更は前のPRがマージされたことを確認してから、新しいブランチで行う。auto-mergeの速度に追加pushが間に合わないという問題は、コードを書き始めた時点で踏んでいる。手順が増えたように見えるが、変更が宙に浮くリスクがゼロになった。
60本のPRが教えてくれたこと
2日間で出した約60本のプルリクエストの内訳を振り返ると、パターンが見える。
- qr(20本):NDLパイプライン連携、ZIP基盤構築、バルクQR修正、UI改善
- newsletter-master(25本):リポジトリの新規立ち上げからUI構築、ワークフロー整備まで
- lp(18本):パイプライン移行、キュー非同期化、ニュースレターページ刷新
この中で新機能と呼べるものは少ない。ほとんどは既存の仕組みの「配管の引き直し」だった。データの流れる経路を変え、詰まりやすい箇所を迂回し、太いパイプを細いパイプに分けた。利用者から見える変化はわずかだが、裏側の水流はまったく変わった。
制約は設計の敵ではなく、設計の入力だった。
開発と運用の境界で起きること
開発フェーズでは「動くものをつくる」ことが目的になる。運用フェーズでは「動き続けるようにする」ことが目的になる。この違いは、コードの書き方ではなくパイプラインの設計に現れる。
リポジトリは開発段階から3つに分かれていた。クライアント(qr)、ビジネスロジック(lp)、サーバーサイドのビルド(newsletter-master)。構造は同じでも、運用に入ると各リポジトリの役割が鮮明になる。それぞれが独立してデプロイされ、それぞれが独自のペースで変更される。開発中は「分けてある」だけだったものが、運用では「分かれている必要がある」ものになった。
帯域制限がリポジトリの集約を求め、バイナリ肥大がフォーマットの転換を求め、依存の重さがクライアント・サーバー分離を求めた。開発時の分離が正しかったのは偶然ではない。制約が後から追認した。
検証で回路を閉じる
パイプラインは動いていた。ZIPからPDFを生成し、GitHub Pagesにデプロイする。しかし一つ欠けていたものがあった。生成されたPDFのQRコードが、本当に紙面から読み取れるかどうかを確認する仕組みがなかった。
TokiQRの1000年保存は、QRコードが読み取れなければ意味がゼロになる。PDFを生成する側が「正しく埋め込んだはず」と信じるだけでは足りない。実際にPDFをレンダリングし、QRコードをスキャンし、元のURLと一致するかを確かめる必要がある。
verify-qr.pyを追加した。PDFの各ページを300dpiで画像化し、pyzbarでQRコードを検出し、ZIPのmanifest.jsonに記録されたURLリストと照合する。1つでも欠落すればビルド失敗としてデプロイを阻止する。既存の6件のPDFに対して実行し、全件PASSを確認した。
もう一つ足りないものがあった。検証が失敗してデプロイが止まったとき、それに気づく手段がなかった。成功通知は件数が増えるとノイズになる。失敗だけを通知すればいい。ビルドが失敗したときにGitHub Issueを自動作成するステップを追加した。成功時は静かに、異常時だけ知らせる。
運用フェーズの問題の切り分け方
今回の改修を振り返ると、問題の発見から改善までに繰り返されたパターンがある。
まず「動いている」ものに対して「本当に?」と問う。1人が1件ずつ提出する限りは動いていた。しかし100人が同時に実行したらどうなるか。PDFを生成したが、そのQRは本当に読み取れるか。ビルドが止まったとき、それに気づけるか。動いている状態を疑うことが起点になる。
次に、制約の所在を特定する。問題はどこで起きているのか。GitHub APIのレート制限なのか、QRの解像度なのか、通知の不在なのか。「うまくいかない」で止めず、パイプラインのどの接合部が詰まっているかを切り分ける。
そして、制約に合わせてパイプラインを再設計する。リアルタイム処理をキューに変える。クライアント側のPDF解析をサーバー側の自動検証に置き換える。成功通知を削り、失敗通知だけを残す。制約を消すのではなく、制約が効かない経路を見つける。
最後に、改善を仕組みに組み込む。属人的な確認ではなく、パイプラインの一部として自動で走る検証にする。人が覚えていなくても回る状態にして初めて、改善が定着する。
既存のフレームワークが補助線になる
このサイクルを自力で回すのは難しくない。しかし既存のフレームワークを知っていると、各段階で見落としが減る。今回の改修に隣接する概念をいくつか挙げる。
DFD(データフロー図)は「制約の所在を特定する」段階を助ける。ZIP → PDF生成 → QR検証 → デプロイという流れを図に起こせば、どの接合部が詰まっているかが視覚的にわかる。今回はGitHub APIとの接点がボトルネックだった。データの流れを描くという基本動作が、制約の切り分けを加速する。
TOC(制約理論)は「再設計」の段階で効く。ゴールドラットの理論は「系全体のスループットはもっとも遅い工程で決まる」と教える。すべてを最適化するのではなく、ボトルネック1つを特定してそこだけを解消する。APIレート制限という1つの制約をキューで迂回したことで、パイプライン全体が通るようになった。
パイプ&フィルタはアーキテクチャパターンとして、今回の設計そのものだ。各段階が独立した処理単位で、入力を受け取り、変換し、次に渡す。verify-qr.pyという新しいフィルタをPDF生成の後に差し込めたのは、パイプラインが疎結合だったからだ。
PDCA(計画・実行・確認・改善)は「仕組みへの定着」の段階と重なる。ただし今回の教訓は、Checkを人ではなくパイプラインにやらせるということだった。確認を自動化しなければ、サイクルはいずれ止まる。
フレームワークはそれ自体が答えを出すわけではない。しかし「次に何を考えるべきか」という補助線を引いてくれる。制約に直面したとき、手持ちの概念が多いほど、経路の候補が早く見える。
各レイヤーの選択肢
フレームワークが補助線を引いたら、次はレイヤーごとに選択肢を並べる。今回の改修で実際に検討した代替案と、選んだ理由を残す。
データフォーマット層 — PDF単体 / ZIP+PDF / ZIP単体。PDFはgitでバイナリ差分が効かずリポジトリが膨張する。ZIP+PDFはPDFをZIPからの派生物にすることでクライアント依存を断ち、サイズも縮小できた。ZIPを一次ソースとし、PDFはサーバー側で生成する方式を採用。
リポジトリ構造層 — 顧客ごとにリポジトリ / 全シリーズ集約。顧客単位はAPIレート制限に弱く、リポジトリ数の線形増加が運用負荷になる。newsletter-masterへの集約でAPIコストを定数化した。
提出タイミング層 — リアルタイム同期 / バッチキュー / イベント駆動。リアルタイム同期はAPIレート制限で詰まる。イベント駆動はCloudflareの永続キュー等の追加インフラが要る。ファイルベースのキュー+5分タイマーは追加依存ゼロで実現でき、GitHub Actionsのcronで完結した。
QR検証層 — クライアント側jsQR / サーバー側OpenCV / サーバー側pyzbar。jsQRはブラウザ内で動くが解像度依存が大きい。OpenCVはVersion 40で失敗した。pyzbarはCベースのZBarラッパーで、300dpiレンダリングとの組み合わせで安定した検出率を出した。
通知層 — 成功・失敗両方通知 / 失敗のみ通知 / 通知なし。件数が増えると成功通知はノイズになる。GitHub Issue自動作成による失敗のみ通知を選択。ログへの直リンクを含めて即座に原因調査に入れる。
選択肢を並べる作業自体が、設計判断の言語化になる。「なぜこれを選んだか」を明示できれば、将来の自分や他者が制約の変化に応じて選び直せる。
DFD、TOC、パイプ&フィルタ、PDCA。これらの概念は、今回のパイプラインに固有のものではない。自分が一から作った仕組みでも、初めて触れる他人のシステムでも、同じように効く。データの流れを描き、ボトルネックを特定し、疎結合な接合部を見つけ、検証を自動化する。この思考の型をあらかじめ持っていれば、どんな環境に放り込まれても、明晰に、そして大胆に解決策を導いていくことができる。
AIにできることと、残る課題
今回の改修はAIとの協働で進めた。verify-qr.pyのコード生成、GitHub Actionsワークフローの修正、エッセイの日英同時記述。定型的なパターンの適用や、既知のフレームワークに基づくコードの実装は、AIが速い。
多くの人がAIを使うとき、一発で完成品を出す方向に向かう。プロンプトを工夫して、一回のやりとりで最終版を得ようとする。今回の改修で行ったのはそれとは根本的に違う。AIを使って高速にドラフトを出し、自分の目で制約を見つけ、構造ごと引き直す。このサイクルを何度も回した。AIの出力は素材であり、それを自分の問いで彫刻する。そしてその問いは、実際のプロダクト運用から得た制約に裏打ちされている。
一方で、この試作の過程でAIは外部依存性を増やす方向を繰り返し提案してきた。Google Driveに格納すれば処理がしやすい、スプレッドシート上で管理すればコーディングが楽になる。どれも一見正しい。しかし1000年保存を前提とするサービスで外部サービスへの依存を増やすことは、永続性の設計思想と矛盾する。慎重に精査し、軌道修正した。AIは目の前の課題を効率的に解く方法を提案するが、その提案がプロダクトの根幹にある設計思想と整合するかどうかまでは問わない。
しかし「100人が同時に実行したらどうなるか」という問いはAIからは出てこなかった。「成功通知は件数が増えるとノイズになる」という判断も、運用を想像した人間の感覚から生まれた。フレームワークを適用するのはAIにもできる。だが、どのフレームワークを、どの局面で、何に対して適用するかを決めるのは、問いを立てる側の仕事だ。
AIがどれだけ進化しても、この構造は変わらないと考えている。解く力はいくらでも増幅できる。しかし「何を問うか」は増幅の対象にならない。制約を炙り出す問いの質が、解決策の質を決める。フレームワークを知っていることの価値は、AIに正しい問いを渡せるようになることにある。
AIをツールとして使いこなす人は増えた。しかし「AIとの協働において、自分の側が担うべき役割は何か」を明確に言語化し、実践として見せられている人はほとんどいない。問いの不在に気づかないまま、AIの出力を完成品として受け入れてしまう。このパイプライン改修記がそのまま実証になっているのは、そういう意味でもある。
この構造は個人開発でもエンタープライズでも変わらない。むしろ規模が大きくなるほど、問いの不在がもたらす損害は深刻になる。AIが提案の生成を加速したことで、知見の裏付けがないまま魅力的に見える施策が次々と導入されやすくなった。蓋を開けると一貫性がなく、現場の実態に合わず、伝統的に維持されてきた基盤が揺らいでサービスが長期間止まる——そうした事態はもはや珍しくない。AIが生成したコードをアーキテクチャ上の熟慮なく組み込んだ結果、セキュリティホールが生まれるのも同じ構造だ。部分的には正しく見えても、全体の文脈を問わなければ穴になる。そして規模が大きくなるほど、個人情報の漏洩やサイバー攻撃がもたらす影響も桁違いになる。速く作れることと、正しく問えることは別の能力だ。
念のため補足しておくと、これはエンタープライズの組織力や安全性を否定するものではない。大規模な組織には、個人では実現できないリソースの厚み、冗長性、ガバナンスの蓄積がある。ここで指摘しているのは、その強みを活かすためにこそ「何を問うか」が不可欠だということだ。私自身、個人で取り組む場面もあれば、パートナーシップを通じてエンタープライズと協業する場面もある。規模に関わらず、問いの質が設計の質を決めるという構造は同じだ。
この改修は、ここで収束した。コードに抜本的な手を加える局面は終わり、仕組みは運用に移行している。将来どこで何が起きうるかは、マイグレーションロードマップとして別途整理済みだ。今コードに手を入れないのは、見落としているからではない。変化点を特定した上で「今は動かさない」と判断しているからだ。運用の完成とは、手を加え続けることではなく、手を加えるべき時と場所を見極めて待てる状態を指す。
AIが解く力を無限に増幅した時代において、時間を費やすこと自体はもはや価値を生まない。一行の問いが構造を変えることもあれば、100時間の作業が何も変えないこともある。問いを磨き、テコ入れし、待てる状態を作り、それを増やしていく。この改修記が示しているのは、そういう仕事のかたちだ。
そしてトキストレージというプロダクトそのものが、この構造の体現になっている。QRコードが紙に印刷された瞬間、サーバーもサブスクリプションも電力も不要になる。デコーダーはわずか3MB、データはQR紙面のURLに分散し、サーバー側は件数に依存しない。顧客が一人増えるたびに、もうひとつの「待てる状態」が生まれる。事業のバーンレートはゼロに設計されている。パイプラインも、プロダクトも、事業構造も——すべてが「待てる状態」として成立している。
個人開発からエンタープライズまで。永続性の設計から、システム化・運用の確立まで。
AIでは見切れない視点を、対話を通じて提供します。
制約が教えてくれたのは「何を諦めるか」ではなく「どこに通せば流れるか」だった。