コンテンツにスキップ

決定記録

indx が下したアーキテクチャ上の決定と、記録された拒否。どちらも蒸し返さないために。

英語版が原文です。

以下の各項目は、契約・境界・拒否のいずれかを形づくった決定です。再導出されないように、そして そのひとつを覆す変更が「覆している」と目に見えるように、書き残してあります。

これらの記録は一度にまとめて遡って書かれたので、番号は決定された順序ではなくグルーピングに 従います。各記録は POLICY_VERSION と機能スナップショット ID に何をしたかを述べており、ここで 意味を持つ年代記はそれだけです — 残りは git ログが、スライスごとに 1 コミットとして保持しています。 新しい記録は末尾に次の番号で追加します。

記録が短いのは意図的です。仕組みは一度だけ、プロトコルページ か それが属するソースに書かれています。記録が担うのは、なぜその形が選ばれたのか、そして何を犠牲に するのか、だけです。両者が食い違うときはコードが正しく、記録が古いのです — そのときは古い記録を 編集するのではなく、新しい記録を追加して食い違いを述べてください。

## セクション 1 つにつき記録 1 つ。/ja/developer/decisions/#adr-0007-… で参照できます。 1 ページに収まらなくなったら、ページのディレクトリに分割します。

ADR-0001 — ルーティングラダーと CapabilityKind は閉じている

Section titled “ADR-0001 — ルーティングラダーと CapabilityKind は閉じている”

ステータス 受理 · バージョン変更なし

背景。 拡張は受理され、GET /v1/capabilities に広告され、それでいてルートに一度も到達しない、 ということが起こり得ました。CapabilityKind は 6 メンバー、_ESCALATION は 3 段、 _DEVICE_PREFERENCE は 4 デバイス、_ladder() が読むシグナルは 2 つ — そして、ディストリビューションを インストールすることでそのいずれかが増えるのかどうか、どこにも書かれていませんでした。

決定。 どれも増えません。カインドとは、価格とフォールバック位置を持つポリシー上の立場であって、 ルーティング先として増やすものではありません。ラダーこそが製品です。ディストリビューションは 最も近い既存のカインドを宣言し、そのラダー位置を継承し、デプロイメントが INDX_ROUTING_ECONOMICS で数値を訂正します。

帰結。 一次製の OCR とまるで似ていないホスト型 OCR も、OCR とまったく同じようにルーティングされます。 これがラダーの存在意義である保証です。放出されたシグナルを意味あるものにする — たとえば handwriting — のは拡張ポイントではなく、POLICY_VERSION を上げる一次製のポリシー変更です。 シグナルは形式において開かれ、意味において閉じています。policy.py の docstring、_ESCALATION_ladder、およびルーティングページに明記されています。

改訂トリガーなし。 ここにある他のすべての記録は何らかの方向に暫定的ですが、これはそうではありません。ラダーこそが製品であり、インストールによって段を増やせる indx は別の製品です。

ADR-0002 — エコノミクスはコアが所有し、カインドで引く

Section titled “ADR-0002 — エコノミクスはコアが所有し、カインドで引く”

ステータス 受理 · バージョン変更なし

背景。 CapabilityDescriptor はコスト・レイテンシ・品質を宣言しません。それらは indx_router/economics.py のカインド別テーブルから来ます。機能が自分で宣言することも十分あり得ました。

決定。 させません。数値はコア所有のまま、カインドで引き、INDX_ROUTING_ECONOMICS で デプロイメントごとに上書きできます。上書きはカインドだけでなく機能 ID でも引けるので、 誤った 1 つのインストールを、そのカインド全体の値付けを変えずに訂正できます。

帰結。 失敗は現実にあり、隠さず受け入れています。1 ページ $0.05 を課金するホスト型 OCR は CPU OCR の数値を継承し、100 分の 1 の額で maximum_cost_usd を通過し、PlanEstimates.cost_usd が それを計画の価格として呼び出し元に公表します。それでも維持します。自己申告の価格とは、機能が ルートを買うために使うものであり、請求書を払うデプロイメントだけが、それについて正直である 理由を持つからです。

改訂は、同じカインドの機能を 2 つインストールするデプロイメントが現れたとき。カインドで引く数値こそが、呼び出し元が述べられるあらゆる制約にとって 2 つの OCR ディストリビューションを区別不能にしているものです — ADR-0028 を参照。

ADR-0003 — ノミネートはパーサー唯一の入口

Section titled “ADR-0003 — ノミネートはパーサー唯一の入口”

ステータス 受理 · POLICY_VERSION 0.5.0

背景。 PARSER_ESCALATION にも、_ladder() がその前に置く段にも含まれません。つまり パーサーがルートに入る扉はただ一つ、_nominations だけで、それには signature_detection=TrueSignatureDetectoravailable=True の 3 つが必要でした。そしてその失敗は静かでした — _declared はどのメディアタイプに対してもカインドだけで manual-review に一致するので、 サードパーティが .xlsx パーサーを出荷し、受理され、そのパーサーが使われないまま文書が 人間のキューへ流れていく、ということが起こり得ました。

決定。 ノミネートが唯一の扉のままです。パーサーが正しくあり得るのは、何かが先に認識した文書に ついてだけです。フォーマットを汎用的に読む機能は、その内部がどれほど専門的でも NATIVE_EXTRACTION を宣言します。signature_detectionFalse 既定のままです。検出は依然として ソースを読むコストを伴うからです。

帰結。 行き止まりは静かではなく声を上げます。PARSER だけが宣言するメディアタイプを検出オフで 計画すると、パーサーとフラグの両方を名指しする制約を持つ unsatisfied な計画になり、エグゼキューターは unsatisfied な計画を拒否します。examples/acme-indx-capability のリーダーとパーサーの組み合わせは、 回避策ではなく文書化された形です。段としての採用は原理ではなく今日の証拠に基づいて拒否されました。 _declared はメディアタイプだけで絞るので、NATIVE_EXTRACTION の前に PARSER を置くと、 インストール済みの請求書パーサーへ PDF の全ページが流れ、出力検証が存在確認のみである以上 (ADR-0015)、非請求書に対して自信満々の ゴミを返すパーサーがそのページを保持してしまいます。

ADR-0004 — 可用性は計画時に参照しない

Section titled “ADR-0004 — 可用性は計画時に参照しない”

ステータス 受理 · 最初のルーティングポリシーから

背景。 インストール済みのどの機能も利用不可になり得ます — extra が無い、モデルが未設定、 重みが無い。計画がそれらを飛ばして、今すぐ実行できるものだけにルーティングすることもできました。

決定。 しません。計画とは、記録されたインベントリに対する作業の提案です。機能が今この瞬間に 実行できるかどうかは実行時の問いであり、503 で答えます。

帰結。 最初のスライスでは、インストール済みの全機能が available=False で、それでもルーティング 可能でした。それが計画アーティファクトと実行パスを独立に構築できた理由です。計画の可搬性もそこから 来ます — 同じ計画が extra のあるマシンでもないマシンでも有効で、その差はルーティングではなく トレースに現れます。読み取り可能メディアのゲートは依然として先に走るので、manual-review しか 持たないインストールは、すべてを人間に送るのではなく、ルーティング前に 415 を答えます。

ADR-0005 — POLICY_VERSION は決定だけでなくアーティファクトでも動く

Section titled “ADR-0005 — POLICY_VERSION は決定だけでなくアーティファクトでも動く”

ステータス 受理 · POLICY_VERSION 0.5.0 と 0.7.0

背景。 分かりやすい規則 — ルートが変わったら上げる — は狭すぎました。パーサーの行き止まり制約は、 それまで到達可能だったどの入力のルートも変えず、計画アーティファクト(ステータス、制約、plan_id)は 変えました。リージョンスコープの修正は、出荷済みのどの機能も到達できない決定関数を変えました。

決定。 POLICY_VERSION は、決定または計画アーティファクトが変わったときに動きます。 2 人の呼び出し元が 1 つのバージョンの下で異なる計画を受け取ることは決してありません。機能スナップショット ID もそれと共に動きます。policy_version がスナップショットのコンテンツハッシュの中にあるからです — registry.py が除外するのは idlimitsresolvable だけです。

帰結。 その後半は高くつく形で学ばれました。あるとき、ベンチマークマニフェストの policy_version ピンだけを上げ、capability_snapshot_id ピンを逆の思い込みでそのままにしたところ、次の実行は 10 行がドリフトして何も採点しませんでした。indx-benchmark のガードテストが今はその整合性を CI の 仕事にしています — マニフェストのピン、コミット済みレポート、POLICY_VERSION は一致していなければ ならず、コミット済みレポートは採点済みでなければなりません。稼働中のスナップショット ID は 意図的にそのチェックの外です。extra が正当にそれを動かし、正式なベンチマークのインストールは CI が持たない extra を含むからです。

ADR-0006 — エントリーポイント発見こそがレジストリ

Section titled “ADR-0006 — エントリーポイント発見こそがレジストリ”

ステータス 受理 · 最初のレジストリから

背景。 インポート時レジストリ — メタクラス、__init_subclass__、デコレーター — はプラグインを 集める定石であり、TOML や JSON のテーブルは、それらが何を提供するかを宣言する定石です。

決定。 どちらも使いません。indx.capabilities グループに対する importlib.metadata.entry_points() がレジストリのすべてであり、descriptors() が宣言のすべてです。

帰結。 インポート時レジストリは、自身を埋めるためにすべてのプロバイダーモジュールをインポート しなければなりません。それは GET /v1/capabilities が支払う安価な発見の契約を壊し、 tests/unit/test_workspace_boundaries.py の禁止インポート規則を壊し、構造的 Protocol を サードパーティが継承しなければならない名前的基底クラスに変えてしまいます。宣言ファイルは ローダーとスキーマを増やし、構築時の検証を失い、それでいて計算されるフィールド — availableunavailable_reasondevices — を表現できません。本当にデプロイメント設定であるべき 値は、すでに環境変数になっています。CapabilityRegistry を参照してください。

ADR-0007 — ディストリビューションは自分のエンジンスタックを所有し、重複はその代償

Section titled “ADR-0007 — ディストリビューションは自分のエンジンスタックを所有し、重複はその代償”

ステータス 受理 · バージョン変更なし

背景。 プラグインは兄弟プラグインをインポートできません — それが、ディストリビューションを単独で インストール可能に保ち、tests/unit/test_workspace_boundaries.py の依存グラフを正直に保つものです。 しかし indx-observer-office は Office ファイルのページを数え、 indx-capability-office-extraction はそれを読みます。ページ 3 は両者にとって同じワークシートを 意味しなければなりません。

決定。 共有される列挙は、共有パッケージに抽出するのではなく逐語的に複製します。3 つのファミリーが その形で対になっています — Office (ooxml.py)、テキスト (plaintext.py)、分類器タクソノミー (taxonomy.py) — そしてテストが、コピー同士は相手のディストリビューション名を書いた行だけが 異なることを表明します。

帰結。 同じ理由で、PDF のラスタライズとテキスト抽出を機能パッケージ間で共有することも拒否します。 そうすれば pypdfium2indx-interfaces に入り込みますし、DPI の既定値はレーンごとに正当に 異なります。代償はドリフトさせてはならない実在のコピーであり、ドリフトテストこそが、それを願望では なく規則にしているものです。plaintext.py が設定を相対インポートで参照しているのも同じ理由です — 絶対インポートは各コピーで異なるパッケージを名指すことになり、2 つは 1 つの規則でなくなります。

ADR-0008 — フォーマットを観察することと読むことは別のディストリビューション

Section titled “ADR-0008 — フォーマットを観察することと読むことは別のディストリビューション”

ステータス 受理 · スナップショット ID は動き、POLICY_VERSION は動かない

背景。 indx-observer-officeindx-capability-office-extraction は明らかに 1 パッケージに できました。PDF の対も同じです。

決定。 2 つにします。フォーマットを計画可能にすることとルーティング可能にすることは、 別々のインストールです。

帰結。 オペレーターは片方だけを持てます。それが要点です。読めないフォーマットを計画する インストールは、415 ではなく manual-review への正直なラダー降下を得ますし、観察できない フォーマットを読めるインストールは、単に一度も尋ねられません。無料の段を無料に保つのもこれです — office-extractiontext-extraction はエンジンも extra も持たないので、既定インストールは それらのファミリーについて観察したものを読めます。代償はフォーマットファミリーごとに 2 つ目の pyproject.toml と、ADR-0007 が受け入れる重複です。

ADR-0009 — 素のインストールは何も観察せず、何も解決しない

Section titled “ADR-0009 — 素のインストールは何も観察せず、何も解決しない”

ステータス 受理 · バージョン変更なし

背景。 PDF 観察、file:/http: ローディング、メディアタイプの sniff、チャンキングをコアから ディストリビューションへ移すたびに、組み込みの経路が 1 つ失われました。各ポートの隣に一次製の フォールバックを残す方が安全に見えました。

決定。 フォールバックは置きません。素の pip install indx はどのフォーマットも観察せず、 どの URI も解決せず、バイト列から何も認識せず、チャンクブロックを返しません。インラインの base64 は 何もインストールしなくても動きます。それが正直な線引きです — indx は教えられていない URI を解決せず、 直接手渡されたバイト列は常に受け取ります。

帰結。 組み込みの経路と拡張の経路が 1 本になるので、これらのポートが防ぐために作られたドリフトは 起こり得ません — 共有関数では防げませんでした。共有関数もまた、唯一の入口だったからです。コアの床が 隠していたであろう 2 つのことも、これで効いてきます。チャンカーの選択は文書単位ではなくページ単位 であり、順序には builtin を超える規則が必要でした。それが indx-chunker-pagefallback = True です。この欠落は、機能がずっと持っていたものと同じです — indx はどの indx-capability-* パッケージにも依存していません。

ADR-0010 — resolvableclassifiers はスナップショットのコンテンツハッシュの外

Section titled “ADR-0010 — resolvable と classifiers はスナップショットのコンテンツハッシュの外”

ステータス 受理 · スナップショット ID は前後で同一と実測

背景。 機能スナップショット ID はコンテンツハッシュであり、計画はそれに束縛されます。新しく インストールされたローダー、観察可能なメディアタイプ、分類器をそのハッシュの中で広告すれば、 飛行中のあらゆる計画に 409 を返すことになります。

決定。 snapshot.resolvablesnapshot.classifiers は広告され、ハッシュからは除外されます。 limits.max_input_bytes がすでに作った先例に従います。registry.pyidlimitsresolvable を名指しで除外します。

帰結。 スキームや認識可能なメディアタイプや分類器を獲得することは、次に計画できるものを広げる だけで、飛行中のどの計画が選択したものも変えません。例外は 1 つではなく 2 つ、それぞれ名前つきです — limits はデプロイメント設定、resolvable はディストリビューションをインストールすることで 加わるもの。どちらも広げるだけで、再決定はしません。実測: スナップショット ID は前後で sha256:9ac3558… のまま、ベンチマークのピンは保たれ、POLICY_VERSION も動きませんでした。 分類器はリクエストごとに有効化されるので、インストールしてもどの計画も動きません。

ADR-0011 — インストール済みが組み込みより先。重複は先勝ちで静か

Section titled “ADR-0011 — インストール済みが組み込みより先。重複は先勝ちで静か”

ステータス 受理 · バージョン変更なし

背景。 オブザーバー、ローダー、チャンカーは ID を宣言しないので、機能 ID が衝突するようには 衝突しません。2 つが 1 つのメディアタイプやスキームを主張したときの答えが必要でしたし、拡張が 出荷済みの実装より上位に立つのかどうかも同じでした。

決定。 インストールされたディストリビューションは、indx が出荷するもの — builtin = True を 設定し、安定ソートで後ろに置かれるもの — より先に並びます。インストール済み同士が同じものを 主張した場合は発見順、先勝ちで静か。indx-chunker-pagefallback = True を設定し、すべての 後ろに並びます。

帰結。 発見順は依然として 2 つの拡張の間を決め、拡張と出荷済み実装の間は決して決めません。 それが、運任せではなく規則を必要とした逆転です。builtinfallback はプロトコル外のマーカーで、 False 既定で読まれ、何によっても検証されません — サードパーティがどちらを主張しても、失うのは 自分の優先順位だけなので、ゲートを設けても得るものがありません。オブザーバーの media_types が 広告であってゲートでないのも同じ理由です。決めるのは依然として observe であり、1 つの問いに 2 つの真実の源を作るのがゲートだからです。

改訂は、ID を持たないいずれかのポートに 2 つ目の実装が現れたとき。それが ADR-0026 の述べるトリガーです。オブザーバー、sniffer、ローダーの各ケースは他より優先します。その答えは出力ではなく計画アーティファクトを変えるからです — ADR-0027

ADR-0012 — チャンキングは読み取りの後に尋ねるポート

Section titled “ADR-0012 — チャンキングは読み取りの後に尋ねるポート”

ステータス 受理 · POLICY_VERSION 不変。チャンカーはどの計画にも届かない

背景。 blocks.build は 1 ページにつき 1 チャンクを発行し、その場でこう記していました。サイズに ついて意見を持つ下流が現れたら、実際の構造で分割する、と。形は 2 つあり得ました — すでに境界を知って いる機能上のチャネルか、一様に適用される独立したポートか。

決定。 ポートです。境界とは、読み取りについてではなく検索についての意見です。エンコードごとに一度、 すべてのリーダーがページを生成した後に尋ねられるので、異なる機能が読んだページを持つ文書に対しても 1 つの実装が境界を引けます。ブロック ID はエグゼキューターが発行するままです。チャンカーは ChunkPiecePageChunks を返し、page:N/chunk:M は文書全体を見られる唯一のものが導出します。

帰結。 機能供給のチャネルは、境界の品質を各ページでたまたま勝った段に縛りつけていたでしょう。 選択はページ単位なので、PDF チャンカーは OCR が読んだスキャンページを辞退して床に渡せます。例外を 投げたチャンカーはログに残して読み飛ばされ、エンコードは失敗しません — オブザーバーの raise とは 違い、それはソースではなくチャンカーについての判定であり、切るように頼まれたテキストはすでに 存在しているからです。Chunker を参照してください。

改訂は、2 つ目の非フォールバックチャンカーが存在するか、下流がチャンク境界の大きさについて意見を述べたとき。どちらも、チャンカーの id、広告、リクエストフィールドを一度の変更で導入するトリガーです。

ADR-0013 — チャンカーが未読のページを主張できるのは画像のときだけ

Section titled “ADR-0013 — チャンカーが未読のページを主張できるのは画像のときだけ”

ステータス 受理 · POLICY_VERSION 0.8.0

背景。 chunk_map はリーダーがテキストを返したページだけを残していました。そのフィルターは 見た目以上の仕事をしていました。ラダーが諦めたページに対してチャンカーが内容を主張することも、 それが止めていたのです。レンダリングされたページをチャンクにするためにそれを外すと、そのガードも 一緒に外れました。

決定。 チャンカーは、どのリーダーも読めなかったページを主張してよい。ただし画像でだけ。テキストは 拒否されます。ChunkPiecetext または image をちょうど一方だけ持ち、どちらも空ではない — 古い規則はフィルターとしてではなくモデル上へ移りました。

帰結。 ページのテキストはリーダーの判定です。計画のラダーを降り、出力検証を通り、機能 ID と トレースイベントを伴って出てきたものです。誰も読めなかったページに対してチャンカーが主張する テキストは、そのどれも持たない未検証の読み取りを結果へ紛れ込ませます。ピクセルは何も主張しません — それはページそのものです — だから、でっち上げのテキストが落とされるところで、ピクセルは受け入れられます。 正しい理由で失敗したテストが見つけました。

ADR-0014 — 文書埋め込みは (text, image)、テキストが先

Section titled “ADR-0014 — 文書埋め込みは (text, image)、テキストが先”

ステータス 受理、拒否を覆すもの · POLICY_VERSION 0.6.0、その後 0.8.0

背景。 当初、文書埋め込みはテキストのみとされ、それが DOCUMENT_EMBEDDING_MODALITIES という 一箇所に述べられました。ルーターのフォールトとエグゼキューターの選択が、同じレーンを 2 度綴って ドリフトするのではなく、1 つの文を読むためです。レンダリングのレーンは、名指しされた 3 つのコストの ために当時は拒否されました。後にそのコストを支払う事例が見つかります。何も読めなかったページは チャンクを生まず、したがってベクトルをまったく生まず、文書ブロックとページブロックだけが残って、 検索できるものが何もありませんでした。

決定。 DOCUMENT_EMBEDDING_MODALITIES(TEXT, IMAGE) です — テキストを持つあらゆるチャンクには テキストを、リーダーが failed または unreadable と報告したページにはレンダリングされたページを 1 枚。この順序こそが「文書内の画像を埋め込んでほしい」の第 3 の読みへの拒否です。読まれたページが、 同じピクセルから抽出されたものを複製するためにレンダリング代を追加で払うことは決してありません。

帰結。 測定が先に行われ、記述ではなく test_clip_compatibility.py としてコミットされています。 ベンチマークに採用済みの 2 文書にわたり、各ページの画像ベクトルは、そのページのテキストベクトルより そのページ自身の視覚的クエリによく答え、富山県の図面ではテキストレーンは逆方向を指します。 レンダリングはエグゼキューターではなく indx-chunker-pdf のもので、それが変更を小さく保ちました — チャンキングはどの計画にも触れないのでルーターは何も新たに値付けせず、economics.DEFAULTSEMBEDDER を値付けしていたら、_nominations がそのテーブルをルーティング可能カインドの集合として 再利用しているために、埋め込み器が静かにページリーダーとしてノミネート可能になっていたでしょう。 空白ページはレンダリングされません。それは正常に読まれたページだからです。

改訂は、呼び出し元がページ画像をテキストの代わりではなく並べて欲しくなったとき。このタプルは優先順序であり、優先順序こそリクエストが述べられる類のものです。ここで拒否しているのは既定であって、能力そのものではありません。

ADR-0015 — 出力検証は一次製であり、その下限は無効で出荷される

Section titled “ADR-0015 — 出力検証は一次製であり、その下限は無効で出荷される”

ステータス 受理 · POLICY_VERSION 不変。これは実行を変え、計画を変えない

背景。 dispatch._missing は 1 つの問い — ページが返ってきたか — だけを尋ね、その場でこう記して いました。存在確認が品質ゲートのすべてである、と。フォールバックは不在か例外で発火し、悪い出力では 決して発火しませんでした。つまり PageOutput.statusreason は機能によって書かれ、誰にも 読まれていなかったのです。宣言された FAILED は回答として数えられ、ページを保持し、ラダーを止めて いました。

決定。 検証は一次製、indx_executor/validation.py に置き、宣言されたポートにはしません。チェックは 3 つ、それぞれが何をどれだけ仮定するかの順です。不在は何も仮定しない。FAILED ステータスも何も 仮定しない — 機能自身がそう言ったのだから。下限を下回る自己申告の確信度は数値を仮定する。だから INDX_VALIDATION_MIN_CONFIDENCE の既定は 0.0 で、そのチェックは無効です。

帰結。 宣言されたポートは、誰のためでもないエントリーポイントグループとプロトコルと発見経路に なっていたでしょう — 阻むべきサードパーティのバリデーターは存在せず、チェックはどの読み取りカインドでも 同じ 3 つです。UNREADABLE は意図的に拒否ではありません。それは試行ではなく内容についての判定であり、 manual-review が唯一の生成元であり、終端の段からフェイルオーバーすれば既定インストールのあらゆる スキャンページに 503 を返すことになります。下限が無効で出荷されるのは、ラベル付きコーパスに対して 閾値を測定していないからです — そして、この確信度の数値が最初に測定したのは自分自身の限界でした。 PP-OCR はベンチマーク唯一の手書きページに、同じ文書の印刷ページ 2 枚より高いスコアを与えます。 手書きを捕まえるように設定した下限は、先に印刷を拒否してしまいます。

改訂は、あるディストリビューションが自前のバリデーターを持ち込む必要が生じたとき。モジュール自身の docstring がそのトリガーを名指しし、そのときポートはこのファイルの周りに育つのではなくこれを置き換える、と述べています。ここは ADR-0026 の専用ディストリビューション規則が意図的に守られていない唯一の場所でもあり、見落としではなく例外です。

ADR-0016 — REGION 粒度は拒否する。黙って落とさない

Section titled “ADR-0016 — REGION 粒度は拒否する。黙って落とさない”

ステータス 受理 · POLICY_VERSION は 0.9.0 のまま不変

背景。 Granularity.REGIONBlockKind.REGIONBlock.bbox はいずれも公開契約であり、 blocks.buildPAGECHUNK しか判定していませんでした。granularities: ["region"] を求めた 呼び出し元は、文書ブロックだけを載せ、リージョンを載せず、理由も述べない 200 を受け取り、 「このインストールはリージョンを引かない」と「この文書にリージョンが無い」を区別できませんでした。

決定。 拒否します。UnsupportedGranularityError422 unsupported_granularity で、ソースが ロードされる前に送出されるので、何も答えられないリクエストは取得コストを払いません。メッセージは このインストールが実際に生成するものを列挙します。リージョンの生成は、リージョンがルートを変えると 測定が示すまで先送りのままです。

帰結。 先送りには理があり、静かな no-op には無かった、ということです。契約が提供するものへの リクエストには、答えるか拒否するかのどちらかであるべきです。Granularity.REGION は enum に残ります — 削除すれば、契約が今も履行するつもりのメンバーのために openapi.json と生成クライアントが動きます。 BlockKind.REGIONRegionIdRegionEvidenceExecutionActuals.gpu_regions は生成元を持たないままで、 この拒否はそれを直すものではなく、見えるようにするものです。UnsupportedMediaError ではなく EmbeddingSpaceError の兄弟です。415 は届いたバイト列についての話で、これは求められた答えの形に ついての話だからです。

ADR-0017 — 言語検出はテキストで尋ねるポートであって、オブザーバーではない

Section titled “ADR-0017 — 言語検出はテキストで尋ねるポートであって、オブザーバーではない”

ステータス 受理 · POLICY_VERSION は 0.9.0 のまま不変

背景。 language_hint は 2 番目のスライス以降ずっとルーター非公開の PreflightContext に載っていて、 一度も代入されませんでした。システムのその側では埋めようがなかったのです。プリフライトは内容を デコードしませんし、言語とは文字についての事実だからです。

決定。 Chunker から複製した新しいポート、LanguageDetector。読み取りの後に、テキストで 尋ねられ、ルーティング決定ではなく実行出力を変え、ID を名乗らず、ディスクリプターに加わらず、 どこにも広告されません。language_hint は、ついに代入されるのではなく削除されました。

帰結。 答えはブロックに届き、計画には決して届きません。だからスナップショット ID は前後で バイト単位に同一です。インポートではなくポートなのは、一次製コードがプラグインモジュールを インポートできず、lingua が lang extra の背後で自身のディストリビューションに属するからです。 それが無ければ、ブロックは languages キーをまったく持ちません。文書の答えは、各スコアが計算された テキストの長さで重み付けしたページの平均です — 単純平均では 6 語のタイトルページが章を上回ります。 generic-ocr の確信度がすでに必要としたのと同じ重み付けです。フォーマットが宣言する言語 (OOXML の dc:language、PDF カタログの /Lang)は、このポートが可能にする 2 つ目の検出器であって、 統計的な方を飛ばしてよい理由ではありません。

改訂は、宣言言語の検出器が出荷されたとき。それが 2 つ目の実装であり、解決は文書単位ではなくページ単位なので、1 つの文書が 2 つの検出器によって正当に答えられ、どこをどちらが答えたのかを述べるものが何も無い、という状態になり得ます。

ADR-0018 — 分類器はリクエストが名指ししたときだけ走る

Section titled “ADR-0018 — 分類器はリクエストが名指ししたときだけ走る”

ステータス 受理 · POLICY_VERSION は 0.9.0 のまま不変

背景。 DocumentClassifierLanguageDetector から複製されており、他のすべてを形づくる 1 点で 異なります。検出器は無料でインストールされていれば常に走り、分類器はコール代がかかる — モデルの 1 パス、トークン、箱の外へのリクエスト。

決定。 リクエストが名指ししていないものは走りません。EncodeRequest.classification が インストール済み分類器を ID で、尋ねる順に有効化し、あるファセットについて意見を持った最初の 分類器がそれを取ります。機能カインドではありません。 ADR-0001 の理由と同じで、ラベルは 何もルーティングしないからです。

帰結。 リクエストが名指しするからこそ、分類器は id を持ちます — チャンカーと検出器が持たない 唯一のもの — そしてハッシュの外の snapshot.classifiers に広告されます (ADR-0010)。 未知の ID は、存在する ID を名指しする 422 unknown_classifier で、ソース取得の前に送出されます。 レジデンシーはスキップではなく拒否です。 data_residency の下での external な分類器は、 黙って省かれるのではなく 422 になります。スキップは、呼び出し元が与えられたと信じている答えが 実は与えられていない、ということだからです。ファセットは自由に命名され、一次製コードのどこにも ファセットは列挙されていません。 DocumentClassifier を参照してください。

改訂トリガーなし — これが手本です。 分類器はすでに id、ハッシュ外の広告、そして ADR-0026 が 2 つ目の実装に義務づけると述べるリクエストフィールドを持っています。そのトリガーに到達する他のあらゆるポートは、最終的にこれと同じ形になるべきです。

ADR-0019 — 呼び出し元のメタデータは encode にあり、意図的に plan には無い

Section titled “ADR-0019 — 呼び出し元のメタデータは encode にあり、意図的に plan には無い”

ステータス 受理 · POLICY_VERSION は 0.9.0 のまま不変

背景。 呼び出し元自身のラベル — 所有者、テナント、機密区分 — は往復して文書ブロックに載る必要が あります。両方のリクエストにフィールドを置くのが、分かりやすい対称の形でした。

決定。 metadataEncodeRequest にだけ置きます。classification も同じです。

帰結。 plan_id は、それが決定されて入る計画の全フィールドのハッシュです — policy.py が意図的に そう述べており、後から追加されたフィールドが黙って外れるのではなくハッシュに加わるようにしています。 テナントラベルは何もルーティングしないので、PlanRequest に置けば、同一の 2 文書に異なる 2 計画を 与えるか、request_id の隣に 2 つ目の例外規定が必要になります。indx はラベルを運ぶだけで、何も 強制しません。その後に誰がベクトルを読めるかは、ルーターではなくインデックスの問いです。予約キー languagesclassification は、上書きではなく拒否されます。黙って置き換えられたラベルとは、 呼び出し元が travelled したと信じていて実はしていないラベルだからです。

ADR-0020 — 設定はディストリビューションごとの設定モデル

Section titled “ADR-0020 — 設定はディストリビューションごとの設定モデル”

ステータス 受理 · スナップショット ID は前後で同一と実測

背景。 手書きの INDX_* リーダーが 12 個、同じ read-parse-bound-or-raise の定型が 5 コピー、 同じ失敗に対するメッセージ形式が 3 種類 — “must be positive”、“must be at least”、“must be a number”。 サードパーティの機能は、主張できる名前空間も、何を必要とするかを述べる手段も持っていませんでした。 env_int/env_float のヘルパー対が提案された修正でした。

決定。 ヘルパーではなく宣言です。indx-interfacesIndxSettings が共有の model_config と、 フィールド位置と理由だけからすべてのメッセージを組み立てる load() を持ちます。各ディストリビューションは 自分の env_prefix を持つサブクラスを、自分の settings.py に、そこにだけ宣言します。 INDX_<VENDOR>_* がサードパーティ向けの規約であり、利用不可ディスクリプターの unavailable_reason が、機能が欠けている変数を名指しする場所です。

帰結。 ヘルパーは、pydantic-settings がすでにやっていることの 4 つ目の綴りになっていたでしょう。 indx-loader-s3 はすでにそう書かれていたので、形は発明ではなく引き上げられました。メッセージは今や 1 形式、認証情報が呼び出し元の読む 422 へ紛れ込むことはなく、「これは何で設定できるのか」は grep では なくディストリビューションごとに 1 ファイルです。os.environos.getenv は Ruff で禁止されているので、 新しいリーダーはそこから始めるほかありません。例外は 2 つ、意図的で、その場に印がついています。 INDX_LOADER_HTTP_ALLOW_PRIVATE_HOSTS は独自の真値集合を保ちます。認識できない値は例外を投げるのでは なくガードを上げたままにしなければならないからです。INDX_ROUTING_ECONOMICSPath で、その中身は 下でエントリーごとに検証され続けます。

ADR-0021 — OOXML ライブラリは使わない。コンバーター層も置かない

Section titled “ADR-0021 — OOXML ライブラリは使わない。コンバーター層も置かない”

ステータス 受理 · スナップショット ID は動き、POLICY_VERSION は動かない

背景。 Office ファイルは XML の zip です。openpyxlpython-docxpython-pptx が分かりやすい 依存であり、markitdownunstructureddocling、Tika は 3 フォーマットよりはるかに多くを読みます。

決定。 zipfilexml.etree がエンジンのすべてで、ライブラリを入れるための extra も置きません。 コンバーター層は明確に拒否します。それぞれが自前のルーティング、自前のチャンキング、しばしば自前の モデルを持ち込み、1 つ入れれば、このルーターの中にもう 1 つのルーターを置くことになるからです。

帰結。 office-extraction は他のあらゆる段がそこへ落ちてくる無料の段なので、extra の背後に置けば、 既定インストールは観察できるのに読めないフォーマットを抱えることになります。標準ライブラリの 2 つの事実が、付随的ではなく本質的になりました。xml.etree は内部エンティティを展開するので、 DTD を含むパートはパースされる前に拒否されます — ECMA-376 は OOXML パート内の DTD を禁じており、 それがこのガードを回避策ではなくフォーマット規則にしています。そして入力上限は圧縮されたソースを 縛るのに対し zip は展開後について何も約束しないので、各パートは平坦な上限に対して読まれます。 1 つの限界は支払わずに印がついています。セルは保存された値のまま出るので、日付は Excel の シリアル値です。これが生成するテキストをライブラリの出力と比較した者はまだおらず、その測定は 計画中であって完了ではありません。いつかライブラリが勝つなら、それは extra の背後か 2 つ目のディストリビューションへ入り — 素の依存としては決して入らず — 自前のページ列挙を持ち込むのではなくこちらの列挙に適合します。

ADR-0022 — メディアタイプの認識はオブザーバーのもの。内容全体に対して行う

Section titled “ADR-0022 — メディアタイプの認識はオブザーバーのもの。内容全体に対して行う”

ステータス 受理 · POLICY_VERSION は 0.8.0 のまま不変

背景。 indx_source.media_type はマジックバイトの接頭辞を 3 つ認識し、両方の呼び出し元が data[:16] を渡していたので、後から長いシグネチャを追加しても一致しようがありませんでした。 一次製の型は、嘘をつくクライアントを差し置いてバイト列から認識され、サードパーティの型は クライアントの主張かファイル名からしか認識されませんでした。後者はモジュール自身の docstring が 最も弱いシグナルと呼んでいるものです。

決定。 SourceObserver 上の sniff。ローダーではなくオブザーバーです。フォーマットをパースする のはオブザーバーであり、indx-loader-file%PDF- を宣言するのは知識の置き場所が違うからです。 先頭ではなく内容全体が渡されるので、何かと歩調を合わせるべき 2 つ目の長さは存在しません。 テーブルではなくメソッドなのは、.xlsx.docx.pptx がいずれも PK\x03\x04 で始まり、ファイル末尾の zip メンバー名だけがそれらを分けるからです。

帰結。 「signature」ではなく sniff です。SignatureDetectorsignature_detection がすでに 文書の種類を認識するためにその語を占めており、両者は 1 層違います。テキストとメールは sniff の提供を辞退しますが、それは欠落ではなく答えです — .txt.csv.tsv は区切り文字が 違うだけの同じ文字列であり、「これは UTF-8 としてデコードできるか」は、実際には正しい宣言型を 差し置いて、JSON も XML も HTML も道すがら主張してしまいます。sniff からの raise はログに残して 読み飛ばされ、これは observe からの raise とは逆です。宣言された型とファイル名がまだ下で 待っているからです。スナップショットには何も追加していません。 resolvable.observable_media_types がインストール済みオブザーバーの見るものをすでに報告しており、 観察しない型を認識する sniffer は、呼び出し元の 415422 に変えるだけだからです。

改訂は、1 つのフォーマットを 2 つの sniffer が認識したとき。これはチャンカーの各ケースではなく ADR-0027 と同列です。勝者が、計画が束縛されるメディアタイプを決めるので、誤った先勝ちは粗いチャンクではなく誤ったルートになります。

ADR-0023 — デッドラインは協調的であって、プリエンプティブではない

Section titled “ADR-0023 — デッドラインは協調的であって、プリエンプティブではない”

ステータス 受理 · バージョン変更なし

背景。 INDX_REQUEST_TIMEOUT_SECONDS とクライアント切断はどちらも、走りすぎた作業を止める必要が ありますが、すべてのリーダーの周りにスレッドかプロセスの境界を置かずに、機能をコールの途中で 中断することはできません。

決定。 走っているコールを中断するものはありません。Deadlineindx_interfaces.context に contextvar として置かれ、check_deadline() は、ソースがロードされた後の Router.plan、 エグゼキューターのラダーでの各機能試行の前、各埋め込み器コールの前で呼ばれます — リーダーの中では 決して呼ばれません。

帰結。 上限が過ぎたときすでに走っていた機能は、中断されずに完走します。これは上限を名指しする ponytail: コメントで印がついています。デッドラインを過ぎたリクエストは 504 request_timeout を 答えます。クライアント切断は反対側から同じ機構を作動させます。request.is_disconnected() を ポーリングするウォッチャー経由で、これはファサード呼び出しがスレッドプールで走り、イベントループが ポーリングのために空いているからこそ機能します。503Retry-After はありません。回復見込みを 追跡しているものが何も無いからです。

ADR-0024 — 同期ファサードは 1 つ。非同期の双子は置かない

Section titled “ADR-0024 — 同期ファサードは 1 つ。非同期の双子は置かない”

ステータス 受理 · バージョン変更なし

背景。 HTTP アダプターは非同期で、CLI はそうではありません。同期のものの隣に非同期ファサードを 置くのが通例の答えです。

決定。 ファサードは同期のままです。サーバーはスレッドプール経由でそこへ到達します。

帰結。 両方のトランスポートが共有する面が 1 つになり、そのスレッドプールこそが、クライアント切断を ポーリングするためにイベントループを空けておくもの (ADR-0023)です。代償は 飛行中のリクエストごとに 1 スレッドですが、バックボーンが同期かつステートレスで、スループットの 上限を誰も測定していないうちは、それが正しい取引です。

ADR-0025 — 選択がどの層に属するか

Section titled “ADR-0025 — 選択がどの層に属するか”

ステータス 受理 · バージョン変更なし

背景。 選択が置かれ得る場所は 3 つあり、3 つとも実際に使われていますが、その間をどう選ぶかの 規則は書かれていませんでした。結果として 16 の選択点のうち、呼び出し元が述べられるのは classification.classifier_ids とその sample のちょうど 2 つだけです。残りは発見順、リスト位置、 あるいはアルファベット順で解決され、そのいずれも並べ替える INDX_* 変数は存在しません。

決定。 選択がポリシーに属するのは、それが indx が何にいくら払う気があり、どの順で払うかで あるときです(ADR-0001)。 デプロイメント設定に属するのは、インストールごとに変わるとき — この箱が何に到達でき、何を 支払え、何をデコードできるか。リクエストフィールドに属するのは、文書ごと・呼び出し元ごとに 変わるときです。そのとき知っているのは呼び出し元だけだからです。既定で黙っているものはありません。 リクエストが選べない場所では、その理由は前二者のいずれかであり、明示されます。

帰結。 既存の制約が 2 つ形を決めており、どちらも交渉の余地がありません。plan_id は、それが 決定されて入る計画の全フィールドを意図的にハッシュします。だから何もルーティングしないフィールドは PlanRequest に載せてはならず — これは metadataclassification がすでに従っている規則です — 実際にルーティングするものは両方のリクエストに載ってハッシュに加わります。embedding_space_ids が それです。そしてリクエストが名指しできるのはスナップショットが広告しているものだけなので、広告は 後追いではなく前提条件です。classifiers が実装済みの手本で、名指しのために広告されコンテンツ ハッシュからは除外された ID を、インストール済みのものを列挙する 422 で強制します。この 2 つを 合わせると、任意の候補について、選択可能にすることが無料なのか POLICY_VERSION の引き上げを 伴うのかが決まります。

ADR-0026 — 専用のロジックは専用のディストリビューションへ。そして何が ID を義務づけるか

Section titled “ADR-0026 — 専用のロジックは専用のディストリビューションへ。そして何が ID を義務づけるか”

ステータス 受理 · バージョン変更なし

背景。 9 つのポートと 15 の機能ディストリビューションを経て、この形は保たれてきました。1 つの 関心事に 1 つのパッケージ、エントリーポイント経由で到達し、決してインポートしない。記録が述べて いなかったのは、その形がパッケージ以上の何か — 同一性、広告、呼び出し元がある実装を別の実装より 優先して求める手段 — をいつ義務づけるのか、です。

決定。 結合がそれを禁じない限り、専用のロジックは自分のディストリビューションに置きます。禁じる 場合、その例外は黙って取るのではなく記録します。プラグインは兄弟をインポートできないので、共有される 列挙は逐語的に複製されドリフトテストされます (ADR-0007)。 両方の兄弟がすでに依存している規則は、その下の indx-interfaces に属します。そしてサードパーティの 実装が存在しない継ぎ目は、1 つ現れるまで一次製のままです (ADR-0015)。 実装がちょうど 1 つのポートは、競合を順序で解決してよい。 ID、スナップショット上の広告、 リクエストフィールドを義務づけるのは2 つ目の実装であり、それを出荷する者が同じ変更の中で 3 つすべてを負います。

帰結。 これはバックログではなくトリガーです。同点になる前に同点処理を設計するのは当て推量であり、 その推量が取るであろう形は分類器からすでに分かっているので、早く作って発見できるものは何もありません。 変わるのは、2 つ目のチャンカー、2 つ目の言語検出器、1 つのメディアタイプに対する 2 つ目のオブザーバーが、 静かな先勝ちではなく義務を伴って到着するようになることです。規則を無視した場合の代償も名指しします。 ID なしで 2 つ目の実装を追加するディストリビューションは、システムの振る舞いをインストール順に依存 させます。それは誰もテストも再現もできない性質です。

ADR-0027 — 答えたすべてのコンポーネントを出力に名指しする

Section titled “ADR-0027 — 答えたすべてのコンポーネントを出力に名指しする”

ステータス 提案 · POLICY_VERSION は動かない

背景。 オブザーバー、sniffer、ローダーは ID なし、重複チェックなしの先勝ちで解決され、どの ディストリビューションが答えたのかを述べるものはどこにもありません。ページ数について食い違う 2 つのオブザーバーは、1 つの文書に対して 2 つの異なる計画を生みます。1 つのスキームを主張する 2 つのローダーは、2 つの異なる source_digest を生み得ます。チャンカーのそれとは違い、これらの 選択は計画アーティファクトと、計画が束縛されている同一性そのものを変えます。趣味の問題ではありません。

今日の帰属はコンポーネント 1 つ分の深さしかありません。TraceEventplanned_capability_id actual_capability_id を持つので、フォールバックへ落ちたルートは完全に帰属可能です。そして それが、計画側と比較できる唯一のコンポーネントであり、帰属されている唯一のコンポーネントです。 Block.provenance はページを読んだ機能を名指しするので、チャンクブロックはリーダーである native-extraction を報告し、チャンクの始まりを決めたディストリビューションである indx-chunker-pdf を決して報告しません。ローダー、sniffer、オブザーバー、言語検出器、そして ファセットごとに勝った分類器は、どのレスポンスにも現れません。

決定。 出力は、参加したすべてのコンポーネントを名指しします。帰属は決定の性質ではなく実行の 性質なので、ExecutionTrace に乗ります — events の隣、すでに「実際に何が起きたか」の面であり、 相関のために plan_id をすでに持っている場所です。各参加者の役割、ディストリビューション、 そして何について答えたかを載せる components ブロックとして。

帰結。 これは RoutePlan に乗せてはなりません。plan_id が全フィールドをハッシュするのは、 後から追加されたフィールドが黙って外れるのではなくハッシュに加わるためであり、そこに observed_by を置けば、オブザーバーをインストールしただけで飛行中のすべての計画が無効になります — ADR-0010resolvable について拒否したのと同じ 409 の嵐です。media_type が計画に載っているのは、 ルートがそれに対して決定されたオブザーバーの出力だからです。オブザーバーの同一性は診断情報で あって、同じフィールドではありません。

plan() はラッパーなしの素の RoutePlan を返すので、計画レスポンスにはこれを置く場所がありません。 計画側の帰属には PlanResult ラッパーが必要で、それは他の先送りされたものと並べて記録されている ワイヤー契約の移動です。それまでは、どのオブザーバーがソースを見たのかを答えられるのは encode だけ です。これは聞こえるより重要です。計画なしのエンコードはソースを2 回ロードして観察します。 1 回はエグゼキューターで、もう 1 回は planner.plan の中で。両側でローダーとオブザーバーを名指し することが、2 つのパスの食い違いが可視になる唯一の道です。

そしてこれが確定させる順序: 帰属は選択に先立ちます。 呼び出し元は、システムが同定しないものを 名指しできません。だから ADR-0026 のすべての選択トリガーは、これが先に着地することに依存します。

2026-09-08 に再検討。 エンコード側が出荷されました。ExecutionTrace.components がローダー、 sniffer、チャンカー、言語検出器、勝った分類器をディストリビューション名で名指しします。名前は ディスカバリーが各プロバイダーを登録したエントリーポイントから読み取り、レジストリがオブジェクトの 同一性で答えます。上の背景への訂正が 1 つ。実行は観察しません。渡された計画はすでに起きた観察から 決定されたもので、計画なしのエンコードは planner.plan の中で観察するので、どのオブザーバーが ソースを見たのかは encode にも答えられません。オブザーバーは構造上計画側の帰属です。 PlanResult ラッパーは同じ日に着地しました。plan() はハッシュされた計画を plan に、ローダー、 sniffer、オブザーバーをその隣に返し、計画なしのエンコードはプランナーのオブザーバーを自分の トレースに運ぶので、計画なしのエンコードの両パスが名指しされ、両者の食い違いは可視になります。

ADR-0028 — 同じカインドの機能 2 つは ID 順に並ぶ

Section titled “ADR-0028 — 同じカインドの機能 2 つは ID 順に並ぶ”

ステータス 受理、既知の欠点つき · バージョン変更なし

背景。 _declaredcapability.id でソートし、_assignmentcandidates[0]selected にします。つまり 2 つの OCR ディストリビューションが同じメディアタイプを宣言し、 両方が _admit を通ったとき、勝者を選ぶのはアルファベットです。acme-ocrgeneric-ocr に 勝つ理由は、品質にもコストにもレイテンシにも関係がありません。そして _reasons は両者に同じ段を 与えるので、計画には同点処理が起きたという痕跡すら残りません。

決定。 ソートは維持します。ランク付けとは別の理由でこれは効いており、signatures.py がそれを 述べています。同じインベントリを持つ 2 つのインストールは同じ順で尋ねなければならず、さもなければ 2 つのパーサーが両方一致したときに plan_id が決定的でなくなります。発見順で変わる同点処理は、 アルファベット順で変わるものより悪いのです。

帰結。 述べられていなかった帰結は、同じソートがランク付けもすることであり、アルファベットは 品質のシグナルではありません。RoutingConstraints のどのフィールドも両者を分離できません。 economics.DEFAULTSカインドで引かれるからです — 両者は同一の数値を持つので、オペレーターの INDX_ROUTING_ECONOMICS は一方を完全に排除できても、生き残った 2 つを並べ替えることはできません。 正直な出口は 2 つあります。実際に異なる機能ごとのエコノミクス — 上書きキーはすでにこれをサポート しています — か、宣言された優先順序か。どちらも作られていません。同じカインドの機能を 2 つ インストールしているデプロイメントがまだ無いからで、その不在こそが、カインドで引くテーブルが まだ痛みを生んでいない理由です。実在する逃げ道は計画の受け渡しで、これはスコープごとに機能 ID を 名指しし、エグゼキューターはそれを逐語的に尊重します。

ADR-0029 — 分類は単位と戻り値の形で 5 つのポートに分かれる

Section titled “ADR-0029 — 分類は単位と戻り値の形で 5 つのポートに分かれる”

ステータス 提案 · POLICY_VERSION は動かない。EncodeRequest のフィールドが動くので、 openapi.json と生成クライアントは動く

背景。 DocumentClassifier に固有表現抽出を担わせようとして、担えないことが分かりました。 壊れるものが同時に 3 つあり、どれ 1 つでも十分な理由になります。分類器は意図的に切り詰めた サンプルを渡されますが、切り詰めたサンプルの上での固有表現抽出は、部分的というより黙って 間違っています。LabelScore{label, confidence} であり、文字スパンを置く場所がありません。 そして答えの単位は文書ですが、エンティティはそれが見つかったページなりチャンクなりに属します。 1 つのポートを広げて全部を覆うと、どう呼ばれたかによって戻り値の意味が変わる契約ができ、それは 読み違えられる形です。

決定。 軸は 1 つではなく 2 つ。渡される単位 — 文書、ページ、チャンク — と戻り値の形 — ラベルかスパンか。これは格子であり、6 つのセルのうち 5 つを出荷します。DocumentClassifierPageClassifierChunkClassifierMapping[facet, tuple[LabelScore, ...]] を返し、 PageEntityExtractorChunkEntityExtractorMapping[label, tuple[EntitySpan, ...]] を 返します。6 つ目は意図的に空です。ページはすでに隙間なく文書を敷き詰めているので、文書全体の 抽出器は、ページのポートが無料でやることをやるために入力を内部で窓分割することになります。

各ポートは ADR-0018 をまるごと継承します — その記録の結びの一文が招いているとおりに。id、コンテンツハッシュの外での広告 (ADR-0010)、 リクエストフィールド、そしてリクエストが名指ししていないものは走らないこと。ID は 5 つすべてに またがる 1 つの名前空間なので、レジストリの重複拒否はポートを横断します。ページ分類器とチャンク 抽出器が同じ文字列を共有することはできません。

帰結。 発見は今も判別できますが、見た目とは違う場所で判別しています。@runtime_checkableisinstance は属性の存在を見るので、同じ形の分類器プロトコル 5 つは互いに区別できません。 registry._declared が安全なのはプロバイダーを見ているからで、page_classifiers()chunk_classifiers()page_entity_extractors()chunk_entity_extractors() は 4 つの異なる メソッド名です。1 つのクラスを 2 つのフックから広告してよく、それが実装を重複させずに 2 つの単位に 仕えるやり方です。

拒否は 3 つ、いずれもソース取得の前、いずれも 422 です。未知の ID — インストール済みのものを 列挙して。data_residency の下での external な実装 — ADR-0018 の理由でスキップではなく拒否。 そして CHUNK 粒度なしに名指しされたチャンク対象の実装 — これは空の結果ではなく矛盾であり、 ADR-0016 が、何も生成しない粒度を求める リクエストに与えるのと同じ形です。リクエストフィールドは ID だけでなく単位も名指しします。 グローバルな名前空間に対して冗長であり、それは意図的です。誤ったフィールドの ID は、ルックアップの 失敗ではなく正確な拒否になります。

改訂は、6 つ目のセルが求められたとき。ページでもチャンクでもない単位 — BlockKind.REGION が もし生成側を得たならそれになります — か、ラベルでもスパンでもない戻り値の形。関係、表、墨消しの箱。 空いている 5 つ目のセルには固有のトリガーがあり、それは狭いものです。文書全体のテキストだけが 支えられるエンティティ。

ADR-0030 — エンティティのスパンは、それが名指しするブロックのテキストへの文字オフセット

Section titled “ADR-0030 — エンティティのスパンは、それが名指しするブロックのテキストへの文字オフセット”

ステータス 提案 · POLICY_VERSION は動かない

背景。 スパンは受け取った側が解決できなければならず、一番自明に見えるアンカーは間違いです。 実装が渡された文字列へのオフセットは、その文字列が組み立てられたものなら何も指しません — classification.sample は切り詰めたページの選択を "\n\n" で連結しており、そこへのオフセットは どこにも存在しない文書を指します。代替案は、エンティティと並べて配るオフセット表か、 エグゼキューターがオフセットを文書座標へ書き換えるかでした。

決定。 EntitySpan は表層のテキスト、[start, end)文字範囲、そして LabelScore と 同じ自己申告の土俵に立つ信頼度を持ちます。その範囲はエンティティが名指しするブロックの text フィールドを指し、他は指しません。どのブロックカインドもすでに text を持つので、ページのスパンは ページブロックのテキストを、チャンクのスパンはチャンクブロックのテキストを、一様に指し、 クライアントはすでに受け取ったデータでどちらも解決します。

帰結。 オフセット表も、座標の書き換えも、新しいワイヤーフィールドも要りません — アンカーは レスポンスがすでに含んでいるブロック ID です。同時に、できないことも決まります。切り詰められた、 あるいは組み立てられた入力はスパンを運べません。 サンプルはどのブロックの text でもないからです。 これはサンプリング廃止 (ADR-0033) の副産物ではなく規則そのもので、 内部でサンプリングする実装も、入力ブロックの座標で答えなければなりません。バイトではなく文字で あることも中立な選択ではありません。UTF-8 の日本語では両者が 3 倍違い、text を Python や JavaScript で読むクライアントは文字で添字を取ります。

改訂は、チャンカーが書き換えたテキストを返すようになったとき — 空白の正規化、ハイフンの再結合、 柱の除去 — その結果オフセットがブロックの持つ text を指さなくなったとき。indx-chunker-pdf は 今日すでに PDFium のランを行にマージしており、そのマージが最初に壊れる場所です。

ADR-0031 — エンティティの結果は文書ブロックに載り、チャンクブロックは不活性のまま

Section titled “ADR-0031 — エンティティの結果は文書ブロックに載り、チャンクブロックは不活性のまま”

ステータス 提案 · POLICY_VERSION は動かない

背景。 チャンクごとの抽出はチャンクごとの置き場所を欲しがりますが、それがありません。 ChunkPiece は 3 フィールド — テキスト、画像、bbox — でメタデータを持たず、blocks.builddocument_metadatapage_metadata を通してチャンクのチャネルを持ちません。そしてこの不在は 偶然ではなくアサートされています。test_executor.pyチャンクはページの切り口であり、何も 継承しないという言葉のもとで、チャンクブロックの metadata == {} を固定しています。そのチャネルを 開けるのは小さな差分と大きな決定です。チャンクは最も速く増えるブロックカインドだからです。

決定。 チャネルは閉じたままにします。エンティティの結果は文書ブロックの metadata へ、 3 つ目の予約キー entities の下に、各エンティティが見つかったブロック ID で引けるように書かれます。 チャンクブロックはテキスト、bbox、埋め込み、来歴を持ち続け、それ以外は持ちません。

帰結。 エンティティは解決可能であるためにすでに自分のブロックを名指ししなければならず (ADR-0030)、 そのブロックにも格納するのは冗長です。しかもその冗長性は、チャンクが数千に及ぶ文書でチャンクごとの 辞書割り当てを払わせます。代償は、1 つのチャンクブロックだけを見る消費者にはエンティティが見えず、 文書ブロックを見に行かねばならないことです — 許容できます。集約 (ADR-0032) はどのみちそこにあり、エンティティを欲しい呼び出し元はすでにそれを読んでいるからです。entitiesRESERVED_METADATA_KEYSlanguagesclassification に加わるので、EncodeRequest.metadata でこれを渡した呼び出し元は拒否されます (ADR-0019)。

改訂は2 つ目の注釈がチャンクごとの置き場所を欲しがったとき — チャンクごとの言語、チャンク ごとの分類。注釈 1 つでは、最も速く増えるブロックカインドにメタデータチャネルを開ける対価に 足りません。2 つなら足ります。これは ADR-0026 の 2 つ目の実装トリガーを、ポートではなく格納場所に当てはめたものです。

2026-09-08 に再検討。 2 つ目の注釈はポート分割とともに来ました。チャンク分類器の回答です。 それでもチャネルは閉じたままで、理由は新しいものではなく上のものです。どちらの注釈も解決可能で あるためにすでに自分のブロックを名指しするので、文書ブロック上のブロック ID で引ける 1 つのマップ — entities の隣の chunk_classification — が両方に仕え、最も速く増えるブロックカインドに チャンクごとの辞書を置きません。チャネルを開けるトリガーは、この記録が最初に置いたものより狭く なりました。チャンクブロックを 1 つだけ単独で読み、その上に注釈を必要とする消費者です。まだ そうするものはありません。

ADR-0032 — 文書レベルのエンティティは正規化された表層形の数であって、同一性ではない

Section titled “ADR-0032 — 文書レベルのエンティティは正規化された表層形の数であって、同一性ではない”

ステータス 提案 · POLICY_VERSION は動かない

背景。「文書レベルでエンティティとその数」は、同一性の規則が先に無ければ何も意味しません。 そして候補 3 つは 1 つの考えの変種ではありません。生の表層形では「ABC株式会社」と “ABC株式会社” が別のエンティティになり、これは日本語文書がことごとく示す形で間違っています。 完全な共参照解析では「山田太郎」「山田」“Yamada Taro” が 1 つのエンティティで数 3 になり、これは 研究課題であり別の機能です。その間に正規化があります。

決定。 文書ブロックの entities(ラベル, NFKC 正規化した表層形) ごとの数を持ち、生の 表層形は捨てずに数の傍らに残します。共参照解析はスコープ外であり、外のままです。 正規化された 文字列の上の数は意図的に同一性ではなく、ここでは言及からエンティティへの解決を一切行いません。

帰結。 NFKC は標準ライブラリの 1 呼び出しであり、日本語だけでその席を勝ち取ります。全角と 半角で書かれた 1 つの社名は同じ会社であり、起きなかったマージをモデルの品質はどれだけ上げても 直せません。同時にこれはこのワークスペースで unicodedata を使う最初の場所なので、共有ヘルパー (ADR-0034) に属します。各実装に 置けば 3 つのコピーが 3 通りに正規化するからです。生の形を残すことが、その正規化を検査可能に します。マージに納得しない呼び出し元は、何がマージされたのかを見られます。そして規則そのものより 拒否を名指しすることが重要です。同一性のように見える数は、契約がそうでないと述べない限り、 消費者がまさに同一性として扱うものだからです。

改訂は、呼び出し元がどの言及が同じエンティティかを尋ねたとき。そのときの答えはこのポートを 広げることではなく、独立したポートとしてのリゾルバーです。リンカーが存在するのはまさに正規化 された文字列が同一性ではないからで、ここでそうでないふりをすれば、誤った答えは気づきにくくなる だけです。

ADR-0033 — サンプリングは契約を離れ、実装のものになる

Section titled “ADR-0033 — サンプリングは契約を離れ、実装のものになる”

ステータス 提案 · POLICY_VERSION 不変。EncodeRequest からの削除なので、openapi.json と 生成クライアントは動く

背景。 TextSample は分類器が見るものを契約の中で縛っています。ページ選択の戦略、契約として 硬い上限 2 つ、INDX_CLASSIFICATION_SAMPLE_* の 5 変数、選択を連結するエグゼキューターの関数、 そして CLI フラグ。ポートが 1 つでその実装が 3 つだった頃には筋が通っていました。分割には 耐えません。縛りはページで述べられており、チャンク対象の実装には何も意味しません。上限は文書 全体を文字どおり表現不能にするので、分類器に見せられる最大は 50 ページです。そして連結された出力は そもそもスパンを運べません (ADR-0030)。 さらに悪いことに、この形はすべての実装が同じ縛りを欲しがると仮定していますが、単語リストの走査は 縛りを要らず、トランスフォーマーはトークン窓を欲しがり、ホストされたモデルはコストの上限を 欲しがります。

決定。 サンプリングは契約を完全に離れます。TextSampleClassificationRequest.sampleMAX_SAMPLE_PAGESMAX_SAMPLE_CHARACTERS_PER_PAGEINDX_CLASSIFICATION_SAMPLE_* の 5 変数、 エグゼキューターの sample()、そして --classification-sample はすべて無くなります。実装は自分の 単位の全文を渡され、必要なら自分でサンプリングします。設定は自分の設定モデル (ADR-0020) で、依存のないヘルパーは indx-interfaces にあります (ADR-0034)。

帰結。 これは計画を覆すものであり、黙って落とすのではなくそう述べます。DocumentSampler ポート — 分類器ごとに選ぶポートとしてのサンプリング — は取り下げられ、それを提案していた 「計画中」の項目は、削除ではなくその場で書き換えられて取り下げを記録します。また ADR-0025 を狭めます。あの記録は呼び出し元が述べられる選択点を 「ちょうど 2 つ」— classification.classifier_ids とその sample — と数えていました。今は 1 つ です。あの記録は編集しません。このファイル自身の規則どおり、この記録がその数を上書きします。

代償は実在し、実装に降りかかります。ホストされた分類器が 200 ページの文書を縛りなしに渡されれば 呼び出し元の金を使うので、非自明な実装はすべて、これまで要らなかったサンプリングの既定値を 負うことになり、出荷済みの 3 つも同じ変更でそれを獲得します。見返りは、それぞれが正しくあれる ことです。トークン窓があるところにはトークン窓を、請求があるところにはコストの上限を。

改訂は、2 つの実装が十分に同じサンプリングをして、結合ではなく重複のほうが代償になったとき。 そのときヘルパーはポートになります — ADR-0026 が述べるのと同じトリガーを、機能ではなくサンプリングに当てはめたものです。

ADR-0034 — indx-interfaces は依存のない振る舞いを持ってよい

Section titled “ADR-0034 — indx-interfaces は依存のない振る舞いを持ってよい”

ステータス 提案 · バージョン変更なし

背景。 サンプリングと NFKC 正規化はどちらも、互いをインポートできない実装たちが欲しがるもので、 ワークスペースにはその状況の前例がちょうど 1 つあります。モジュールを逐語的に複製してドリフトを テストすること — taxonomy.py が分類器 3 つに複製されているとおりです (ADR-0007)。 ここに当てはめると、注釈系のあらゆるディストリビューションに 2 つの振る舞いの 4 つ目 5 つ目の コピーが置かれます。そして「モデルが何を見たか」の概念が微妙に 3 通りあることこそ、2 つの実装を 比較不能にするものです。

決定。 indx-interfaces は振る舞いを持ってよい。ただし柵をつけます。柵とは、標準ライブラリ のみ、状態なし、I/O なし、INDX_* を読まないこと。この 4 つを満たす関数は実装ではなく、下の パッケージたちがすでに共有している規則であり、それらの下に属します — ADR-0026 がすでにその例外を名指ししており、この記録はそれに輪郭を与えます。

帰結。 書かれた記述を 2 つ覆します。protocols/index.md の共有規則 *「実装は indx-interfaces の外に置き、ワークスペースの依存方向を保つ」に例外節が付き、 CLAUDE.md のこのパッケージの説明「葉の Pydantic 契約 + 構造的 Protocol」*は完全ではなくなり ます。依存方向そのものは無傷です。すべてがすでに indx-interfaces に依存しており、それは今も何にも 依存しないので、逆転は起きず、test_workspace_boundaries.py のグラフは変わりません。

取っているリスクは坂であり、柵はそれが止まる場所です。サードパーティのインポートを要するヘルパーは 実装であり、ディストリビューションに属します。状態を持つものはサービスです。環境変数を読むものは 設定モデルのものです (ADR-0020)。

改訂は、2 つ目のヘルパーが提案されたか、最初のものが標準ライブラリの外からのインポートを必要と したとき。どちらも、このパッケージがユーティリティライブラリになったということであり、そのときは 規則をもう一度伸ばすのではなく述べ直す必要があります。

ADR-0035 — 2 つの NER レーンは、両者を見分けられるものが無いまま出荷される

Section titled “ADR-0035 — 2 つの NER レーンは、両者を見分けられるものが無いまま出荷される”

ステータス 提案、既知の欠点つき · バージョン変更なし

背景。 固有表現抽出のレーンは速いものとより正確なものとして仕様化されており、このリポジトリは 2 つ目の主張を確かめられません。benchmarks/case.schema.json には期待テキストも期待フィールドも 無く、ケースが持つのはルーティングのラベルだけで出力については何も持ちません。score.py が報告 するのは route_accuracyread_acceptabilitysignature_accuracyfallback_accuracycpu_only_rate とコストで、どれも何が生成されたかではなくどの機能が走ったかについてのものです。 economics.quality はテーブルの定数です。つまり「より正確」は、ベンチマークのページがすでに 「何に対しても測られていない」と述べている actuals.quality と、まったく同じ土俵に立つ主張に なります。

決定。 それでもレーンを出荷し、その欠落を都合のよい場所ではなく主張がなされる場所で述べます。 レーンが選ばれる区別はコストと依存であり、これは観測可能です — 1 つは既定のインストールに載り、 1 つはエクストラと辞書を要し、1 つはモデルを要し、1 つはトークン課金です — そして正確さの順序は、 実測ではなく期待として述べます。

帰結。 明示することがこの記録の内容のすべてです。機能の項目に無条件の「より正確」があれば、 それはこのドキュメント一式の中で唯一、裏づけの無い主張になります。レーンは測られないままでも出荷 する価値があります。両者を選ぶデプロイメントはたいてい、何をインストールし何を払う気があるかで 選んでおり、その軸は実在するからです。ベンチマークの既知のギャップの項目は、それを待つ 2 つ目の ものを得ます — 1 つ目は Office リーダーの比較でした。

改訂はbenchmarks/case.schema.json がラベル付き部分集合について期待テキストか期待フィールドを 得たとき。それはベンチマークのページがすでに記録しているギャップであり、それが閉じた瞬間に正確さの 順序は実測になり、この記録はそれがどう測られたかについての記述になります。

2026-09-08 に再検討。 トリガーは発火しました。スキーマ 1.3 が expected_textexpected_fields を加え、10 のスコープがそれを持ち、score.pytext_recallfield_accuracy を報告します。決定は変わりません。レーンはまだ存在せず、ラベル付き文書の中に 両者の差が出るものも無いからです。レーンが出荷されたとき、その正確さの順序はこの部分集合か、 そのために加えたラベルに対して測られ、実測として述べられます。「実測ではなく期待」はそこで 終わります。

同じ 2026-09-08 のうちに再々検討。 4 つのレーンが出荷されました。patterns、vibrato、ONNX、LLM です。 ラベル付き部分集合は今も期待テキストと期待フィールドを持ち、固有表現のラベルは持たないので、 一方のレーンを他方に対して測ったものはまだ無く、順序は期待のままです。それを終わらせるのは、 expected_fields がラベル付きの固有表現を名指し、各レーンが返すものに対して採点されるケースです。

2026-09-08 に 3 度目の再検討。 スキーマ 1.4 が expected_entities を加え、6 つのスコープが 30 個を持ち、 ベンチマークはインストール済みの各レーンを同じ計画の上で単独に走らせます。再現率は ner-llm 80%(ホストされた gpt-5.4-nano、費用のかかる唯一のレーン)、patterns 50%、onnx 27%、vibrato 3%。順序は実測になりました。固定の形が勝つ帳票の上での実測であり、vibrato の数字の 大半は IPADIC が表現できない粒度によるものです。この記録の題はもう成り立たず、その警告は成り立ちます。 いま動かすものは新しいシグナルではなく幅です。

ADR-0036 — 契約がユビキタス言語を運び、テストがそれをサイトに固定する

Section titled “ADR-0036 — 契約がユビキタス言語を運び、テストがそれをサイトに固定する”

ステータス 承認済み · バージョン変更なし。openapi.json と 2 つの生成クライアントは動くが、 変わるのは説明文だけ

背景。 デモホストが /scalar で提供するリファレンスは openapi.json を描画しますが、その ファイルには散文がほとんどありませんでした。info にはタイトルとバージョンだけ、タグは無く、 どの操作の要約も 1 語で、説明を持つ操作は 1 つも無く、50 のスキーマのうち 43 が説明を持ちません でした — RoutePlanBlockEncodeRequestErrorEnvelope を含めてです。読み手はすべての ペイロードの形を見られるのに、エンドポイントが何をするのかも、語が何を意味するのかも学べません でした。散文はすでに 1 ディレクトリ隣、サイトが公開する用語集にありました。つまり欠けていたのは 執筆ではなく配布です。

決定。 テキストはその源に置き、種類ごとに何がそれを所有するかで 3 か所に分けます。導入、タグ グループ、ユビキタス言語は indx_app_server/reference.py にあり、FastAPI(description=…, openapi_tags=…) に配線されます。操作の要約と説明は app.py のルートデコレータ、それに答える コードのすぐ隣にあります。スキーマの説明は indx-interfaces の Pydantic クラスの docstring です。 FastAPI がすでにそれをスキーマへ写すので、2 つ目の書き場所は 2 つ目の忘れ場所になるからです。

reference.pyGLOSSARY は 17 の用語を保持し、その文は glossary.mdx から逐語的に写され、 2 つが食い違えば tests/unit/test_api_reference.py が落ちます。これは重複したフォーマット列挙が 結んでいるのと同じ取引です (ADR-0007)。 重複は意図的であり、検査こそがそれを安全にします。同じファイルは、説明かタグを欠く操作でも、説明を 欠くスキーマでも落ちます。次のエンドポイントやモデルが文書化されないまま出荷されることはありません。

OpenAPI ドキュメントは英語のみです。それはすべてのデプロイのすべての呼び出し側が受け取る 1 つの 成果物であり、バイリンガルの面はサイトの側です。用語集の各項目は両言語を持つページへリンクし、正典の 用語集へのリンクは /developer/glossary/ を指します。

帰結。 ドキュメントの編集が openapi.json と 2 つの生成クライアントを動かすようになったので、 散文の変更も just api::gen-clientjust docs::gen-client を通り、それを引き起こしたものと 一緒にコミットされます。それが、生成されたリファレンスの隣に手書きのものを置くのではなく、源を 1 つに することの代償です。POLICY_VERSION は触りません。説明は決定も計画成果物も変えないからです。

テストが見つけた唯一のドリフトは、符号化ではなく修正しました。正典の用語集はnominationを定義し、 サイトはsignature nominationを定義していたので、正典の項目を合わせて改名します。用語集の項目は サイト相対のパスへリンクします。サイトと API を 1 つのオリジンから提供するデモホストでは解決し、 素の indx serve では行き先を失います。リファレンスを実際に読んでいる読み手にとっては、それが正しい 取引です。

改訂は、リファレンスが info.description に収まる範囲を超えたとき。Scalar は ## の見出しを サイドバーの項目として描画し、それが用語集を辿れるものにしていますが、導入と語彙を超えるセクションを 求める説明は、サイトの側を求めています。

ADR-0037 — エンリッチメントは 3 つ目の戻り値の形、要約とタグで、分類器の 3 つの単位に乗る

Section titled “ADR-0037 — エンリッチメントは 3 つ目の戻り値の形、要約とタグで、分類器の 3 つの単位に乗る”

Status Accepted · POLICY_VERSION は動かない。EncodeRequest にフィールドが、スナップショットに リストが増えるので、openapi.json と生成クライアントは動く

Context. 分類と固有表現抽出がそれぞれのポートを得た後、機能一覧が「モデルによるエンリッチメント」と 呼んでいたものは、どちらの形でも運べない残りでした。要約は散文であって集合からの選択ではなく、タグは テキストが含まなくてもよい語であって、どのタクソノミーも列挙していません。 ADR-0029 の改訂トリガーはこのケース、 「ラベルでもスパンでもない戻り値の形」を名指ししており、それが発火しました。

Decision. 格子に 3 行目を足します。Enrichment{summary: str | None, tags: tuple[LabelScore, ...]} で、各部分は省略可能、省かれた部分は「意見なし」です。DocumentEnricherPageEnricherChunkEnricher が分類器と同じ 3 つの単位についてそれを答え、1 つのクラスを 3 つのフックすべてから宣言できます。 タグは新しい型ではなく LabelScore を再利用します。タグはファセットのない、語彙の開いたラベルであり、 隣の数値は同じ自己申告だからです。他はすべてそのまま継承します。今や 8 つのポートにまたがる 1 つの 名前空間の中の id、ハッシュの外の snapshot.enrichers、単位ごとに ID を名指しする EncodeRequest.enrichment、取得前の 3 つの拒否は invalid_enricher の下に。

両方の部分を 1 つのポートに、要約器とタグ付け器に分けません。モデルは 1 回の呼び出しで両方を答え、 両方を求めるデプロイは 1 回だけ払うべきです。マージ規則はファセットの規則のファセットを部分に置き換えた ものです。要約を持つ最初の有効化済みエンリッチャーがその単位の要約を取り、タグを持つ最初のものがタグを 取り、片方しか出せない実装はその片方を出します。

答えの置き場所ADR-0031 に従います。文書ブロックの enrichment、各ページブロックの同じキー、チャンクの答えは文書ブロックの chunk_enrichment にチャンクブロック ID をキーとして。どちらのキーも予約されています。

床はタグを付けません。 既定インストールが持つレーン indx-enrich-extractive は単位自身の語彙を 最も多く運ぶ文をそのまま文書の順に返し、tags を空のままにします。テキストが含まない語を作るにはモデルが要り、語の頻度から でっち上げれば、どの計測も裏付けない主題についての主張を公開することになるからです。 indx-enrich-llmllm エクストラと独自の INDX_ENRICH_LLM_* 接頭辞の背後で両方を出します。

Consequences. chat.py は 3 方向のドリフト検査付きコピーになりました。プレイグラウンドの抽出器と エンリッチャーは 1 つの形、単位セレクターと確認タブのボックスを共有します。トレースはエンリッチャーを、 勝ち取った部分を facets として名指しします。これはそのフィールドの名前を引き伸ばしますが意味は 引き伸ばしません。部分は、ファセットと同じく、その単位が尋ねられた 1 つの問いです。

Revisit when 散文でもラベルでもスパンでもない 4 つ目の戻り値の形が求められたとき。2 つのスパンの 間の関係、表、墨消しの箱など。あるいは要約に位置が必要になったとき。それは要約が要約でなくなる点です。

ADR-0038 — 一族のエンジンアダプターはレーンごとのコピーではなく、プラグインの下のライブラリ

Section titled “ADR-0038 — 一族のエンジンアダプターはレーンごとのコピーではなく、プラグインの下のライブラリ”

Status Accepted · バージョン変更なし。ワイヤの変更なし

Context. 3 つのディストリビューションが LiteLLM を通してチャットモデルと話します。 indx-classifier-llmindx-ner-llmindx-enrich-llm です。それぞれが同じ chat.py を、他のコピーを 名指しする 1 行を除いてバイト単位で同一に持ち、フォーマット列挙を守るドリフト検査で固定されていました。 コピーがあったのは、プラグインが兄弟をインポートしてはならないからです (ADR-0007)。 3 つ目のコピーがその代償を見えるようにしました。返答のコストの読み方の修正は 3 か所の編集であり、 3 つが一致するまでテストが失敗します。

Decision. indx-llm はプラグインの下、層構造では indx-interfaces の隣にあるワークスペースの ディストリビューションです。機能を宣言せず、スナップショットに加わらず、何にも依存しません。LiteLLM にも 依存せず、client() が各レーン自身の llm エクストラの背後で呼び出し時に解決します。3 つのレーンは これに依存し、コピーは消えました。ファーストパーティのコードはこれをインポートできません。プラグインを インポートできないのと同じ理由です。ルーターやエグゼキューターの中のエンジンアダプターは、契約の中の エンジンです。tests/unit/test_workspace_boundaries.pyLIBRARY_MODULES の下で両方の辺を固定します。

ADR-0007 はそのままです。その取引はエンジンスタックについてのものです。ディストリビューションが自分の 依存を所有するので、ある依存を望まないデプロイはそれを動かすコードを一切受け取りません。このライブラリは 依存を持たないので、求めていないものは何も受け取られず、コピーの理由は当てはまりません。オブザーバーと リーダーの対が共有するフォーマット列挙(ooxml.pyplaintext.pyrfc822.py)は当面コピーのままです。 同じ道の次の候補です。

Consequences. 返答の読み方を直す場所が 1 つになります。4 つ目の LLM レーンは 1 つのインポートです。 依存グラフは契約でもプラグインでもない葉を 1 つ得ます。それがこの記録が加える唯一の新しい区分です。

Revisit when 共有アダプターが自分自身の依存を必要としたとき。それは再びエンジンスタックであり、 ADR-0007 はそれがライブラリではなくディストリビューションのものだと言っています。

ADR-0039 — 1 つのイメージが API、すべてのエンジン、サイトを運び、認証なしで公開される

Section titled “ADR-0039 — 1 つのイメージが API、すべてのエンジン、サイトを運び、認証なしで公開される”

Status Accepted · バージョン変更なし。ワイヤの変更なし

Context. indx は AWS に、デプロイのワークフローで作られコミットでタグ付けされたイメージとして 出荷されます。そのイメージが、Cloudflare が前に立つ ALB の背後の Fargate で動きます。 問いはイメージが何を持つかでした。エクストラはディストリビューションごとなので (ADR-0007)、 イメージはどの部分集合も運べ、初回使用時に重みをダウンロードするエンジン(fastembed、2 つの ONNX レーン、vibrato の IPADIC)は、そうでなければコールドなタスクのたびにそのダウンロードを 支払います。このデプロイはドキュメントサイトでもあります。examples/app2.py はビルド済みの サイト、プレイグラウンド、本物の API を 1 つのオリジンから配信し、訪問者が到達するのはそのホストです。

Decision. 1 つの Dockerfile が 1 つのイメージを作ります。linux/amd64 のみ、s3 を除く すべてのエクストラ、そしてビルド時に取得した重みを、ランタイムがオフラインで読むキャッシュに 置きます(HF_HUB_OFFLINE)。s3 を外すのはデプロイにバケットがないからです。何も解決しない スキームのローダーは要らず、タスクロールに S3 の文もありません。amd64 のみなのは vibrato が aarch64 のホイールを公開していないからで、Apple Silicon のマシンではビルドは QEMU の下で動きます。 サイトは自分のステージでビルドされ最後にコピーされるので、ドキュメントだけの変更はすべての Python レイヤーを再利用します。scalar-fastapidev グループから自分の demo グループへ 移ります。それがイメージにテストのツール群なしでデモホストを入れさせるものです。 scripts/warm_weights.py はファイルを列挙するのではなく各エンジンを構築するので、モデル名は 各ディストリビューションのものにとどまります。ネットワークを切って走らせれば、同じスクリプトが キャッシュの完全性の証明になり、just infra::image::smoke はそれをします。

サーバーには認証がなく、この記録はそれをセキュリティグループの陰に隠さず言います。ALB は Cloudflare の公開レンジだけを受け入れ、ホスト名の Cloudflare Access が、DNS 切り替えの前に 運用者が有効にする門です。それは境界であって認証ではなく、API はその背後で認証なしのままです。

Consequences. 数ギガバイトのイメージと、重みを取得するのではなく読み込むコールドスタート。 より小さいレーンはビルド引数(INDX_NO_EXTRAS)であって 2 つ目の Dockerfile ではありません。 オリジンに到達できる誰もがすべての機能を呼べます。タスクのコスト上限が唯一のブレーキです (ADR-0040 が Bedrock のためにもう 1 つ加えます)。

Revisit when サーバーが認証を得たとき。そのとき境界は防御のすべてではなく便宜になります。 または vibrato の arm64 ホイールが現れ、Graviton がより安いタスクになったとき。

ADR-0040 — Bedrock は既定で無効であり、有効なときは予算アクションがブレーキ

Section titled “ADR-0040 — Bedrock は既定で無効であり、有効なときは予算アクションがブレーキ”

Status Accepted · バージョン変更なし。ワイヤの変更なし

Context. このデプロイは公開で、認証がありません (ADR-0039)。 5 つのレーンが LiteLLM を通して Bedrock に到達し(llm 分類器、ner-llmenrich-llmgeneric-vlmhosted-text 空間)、呼び出しはそれぞれ課金されます。ホスト名を知る誰もが請求を膨らませられます。 各レーンはすでにモデル変数が設定されたときだけ自分を宣伝し、コアスタックもすでにモデルが設定された ときだけその変数とタスクロールの Bedrock の文を置いていました。欠けていたのは、スイッチとして名付け られたスイッチ、デプロイ後の無効状態の証明、そして有効状態での支出の上限です。

Decision. enable_bedrock は Terraform のブール値で、既定は falsePROD_BEDROCK_ENABLED から 渡されます。無効なら、タスクはモデル変数を持たず、ロールは Bedrock の文を持たず、無効のままモデルを 指定するとプラン時の検証エラーです。有効なら、スタックは Bedrock サービスに対する月間の AWS Budgets コスト予算を加え、その自動アクションが実績支出の 100 パーセントでタスクロールに Deny bedrock:* ポリシーを付け、予測 80 パーセントと実績 100 パーセントでアラームトピックに通知します。ブレーキは アプリケーションではなくアカウントにあります。どのレーンにもコードが要らず、上限を忘れた後付けの レーンにも効き、プロセスが侵害されても効きます。scripts/aws/prod-capabilities-guard.sh は、 稼働中の機能スナップショットから、各 Bedrock レーンがそのモデルが設定されているときに限って存在する ことを確かめます。公開スモークはデプロイと切り替えのたびにそれを走らせます。

Consequences. AWS は予算データを 1 日に数回しか更新しないので、拒否は閾値の数時間後に届き、 その月の超過はその数時間分の呼び出しです。暴走した請求を止めるのであって、リクエストを止めるのでは ありません。拒否は運用者がアクションをリセットするまで残り、月が変わっても解除されません。 スイッチの切り替えはプランとデプロイです。リクエスト単位の制限が欲しいなら、ホスト名への Cloudflare のレート制限ルールであり、それは運用者の設定でこのリポジトリにはありません。

Revisit when サーバーが認証を得て呼び出し元ごとにレーンを制御できるようになったとき。 または、数時間の遅れでは足りないリクエスト単位の上限をレーンが必要としたとき。

ADR-0041 — 重みはモデルを名指しするファイルをキーにして公開されたイメージ

Section titled “ADR-0041 — 重みはモデルを名指しするファイルをキーにして公開されたイメージ”

Status Accepted · バージョン変更なし。ワイヤの変更なし

Context. イメージはコンテナが何もダウンロードしないよう 1.3 GB の重みを焼き込みます (ADR-0039)。 ビルドは依存レイヤーが変わるたびにそれを Hugging Face から取得していました。良い日で 34 秒、 Hugging Face が落ちている日には失敗するビルドで、CI でも本番デプロイでも同じでした。同じ 1.3 GB が すべてのプルリクエストのレイヤーとして Actions のキャッシュに乗り、環境自身のレイヤーと共有する 予算の 3 分の 1 を占めていました。

Decision. 重みはそれ自体のビルドターゲットで、3 つのキャッシュを持つ FROM scratch であり、 Hugging Face に到達する唯一のステージです。タグは内容のキー、w-<モデルを名指しする 5 つのファイルの sha256>scripts/weights_key.sh)で、各ワークフローはそのタグがないときだけ GHCR に公開し (scripts/aws/ensure-weights-image.sh)、参照を WEIGHTS_IMAGE としてビルドに渡します。 したがって Hugging Face にはすべてのワークフローとすべての実行を通してキーごとに一度だけ到達します。 重みが完全であることの証明はイメージの中へ移ります。ランタイムステージがコンテナの動く場所で ネットワークを切って scripts/warm_weights.py を再度走らせ、公開された重みがそのコードに合わない ビルドは初回使用時ではなくそこで失敗します。ローカルでは同じターゲットを Hugging Face から一度だけ ビルドし(just infra::image::weights)、ビルド引数の既定値がそれを名指しするので、GHCR に 到達できないマシンがそれを必要とすることはありません。

キーは意図的にロックファイルを含みません。依存のバンプはモデルが何であるかを変えず、バンプのたびに 1 ギガバイトを再ダウンロードすることがこの記録が取り除くコストです。キャッシュの配置を変えた エンジンはオフラインの証明に失敗し、それが再公開の合図です。

Consequences. ビルドに 2 つ目のレジストリ GHCR が加わります。ADR-0039 の「1 つのレジストリ」の 行が避けていたものですが、GitHub Actions がシークレットなしで書けるのはそこだけです。キーごとの プライベートパッケージが GHCR に溜まり、手で整理します。ビルドはもはや第三者の稼働に依存せず、 プルリクエストのレイヤーキャッシュは環境だけを運びます。

Revisit when 重みをリポジトリの現在のファイルではなくリビジョンに固定しなければならなくなったとき。 そのときはキーがリビジョンも名指しします。または、デプロイが GitHub なしでビルドすべきになったとき。

ADR-0042 — URI ソースはデプロイごとの許可リストであり、公開ホストは何も有効にしない

Section titled “ADR-0042 — URI ソースはデプロイごとの許可リストであり、公開ホストは何も有効にしない”

ステータス 承認 · バージョン変更なし、ワイヤ変更なし

背景。 デプロイ済みのホストは、どの呼び出し元に対しても file:http: の URI を計画して いた。file:///etc/os-release のプローブはタスクのディスクから読み取られ、メディアタイプで しか拒否されなかった。INDX_LOADER_FILE_ROOTS が未設定で、ローダーは設定されるまで許容的 だからである。その理由は各設定が記録している(既定拒否は設定されるのではなく丸ごと切られる)。 indx-loader-http のプライベートホストの防護は保たれていたが、Fargate ではそのレーンは 依然として、見知らぬ相手のために VPC の内側から出すリクエストである。公開ホストで URI を 必要とするものは何もない。API はバイト列をインラインで受け取り、プレイグラウンドのサンプルは アップロードと同じレーンを通れる。

決定。 indx-source の設定のフィールド INDX_URI_SCHEMES が、呼び出し元が使ってよい スキームを名指しする。未設定はインストール済みローダーが宣言するすべてのスキーム、リストは それらだけ、none は URI ソースなし、を意味する。スキームを振り分ける load_uri で一度だけ 強制し、resolvable_schemes は有効な集合を返すので、スナップショットの resolvable.schemes と 拒否されたスキームの 415 が列挙するものは同じ答えになる。無効化されたスキームは既存の unsupported_source で、メッセージが変数を名指しする。resolvable はスナップショットの ハッシュの外に留まるので、進行中の計画は無効にならない。本番は none を設定し、第二の層と して INDX_LOADER_FILE_ROOTS をサンプルのディレクトリに閉じる。後から file を有効にした デプロイでも、イメージが同梱するものにしか届かない。プレイグラウンドのサンプルは アップロードと同じくインラインで API に届き、examples/samples/ にコミットされた政府文書で ある。コーパスはイメージに入らない。機能ガードと公開スモークは空集合と file: URI の拒否を 検証し、@hardened 受け入れレーンは本番の変数で起動したサーバーに対してセキュリティの シナリオをローカルと CI で実行する。同じシナリオはホストに対する実行のために @deployed を 持つ。

帰結。 公開デプロイに何かを取得させることはできない。URL を読ませたい呼び出し元は自分の インスタンスを動かすか、運用者から許可リストを与えられる。ローカル開発、CLI、ベンチマーク、 受け入れスイートは file: を使い続ける。そこでは変数が未設定だからである。ローダー自身の 既定は変えていないので、第二の層は設定する運用者の手にかかっている。スモークが第一の層を 検証するのはそのためである。

再検討の契機 サーバーが認証を得て、許可リストがデプロイ単位ではなくテナント単位にできる とき。あるいはバケットが接続され、s3 が名前で有効化されるとき。

ADR-0043 — スライドの位置情報はチャンカーであり、工程系統図はシグネチャとパーサーである

Section titled “ADR-0043 — スライドの位置情報はチャンカーであり、工程系統図はシグネチャとパーサーである”

ステータス 承認 · バージョン変更なし、ワイヤ変更なし · 第二の切り分けは ADR-0044 が置き換え

背景。 スプリントのストレッチ項目の Denso 側は、顧客デッキの 1 スライド、ネイティブの PowerPoint 図形で描かれた工程系統図と、それをすでに決定的に読む PoC(65 ノード、103 エッジ、 全記号にラベル)です。indx にはデッキの文字を読むリーダー(office-extraction)と、#140 以降は 文字を持つ図形と画像をその位置に戻すチャンカー(indx-chunker-pptx。位置のないチャンクには 型付きの bbox_reason)がありました。どちらも図が何を意味するかは述べず、顧客ファイルは リポジトリに入れられません。

決定。 三つの切り分けです。第一に、スライドの図形が何であるかはチャンカーの仕事であり、 何を描いているかはパーサーの仕事です。indx-capability-process-chart はインボイスと同じ形、 すなわちシグネチャがラダーの前に指名する PARSER であり、汎用にルーティングされることは ありません。シグネチャはページ単位なので、process-chart-parser は認識したスライドのルート だけを先導し、デッキは office-extraction のままです。パーサーはリーダーが返したはずの テキストをそのまま返し、metadata["process_chart"] を加えます。図形自身の cNvPr id を キーとするノードとエッジのグラフで、接続はコネクタの stCxn/endCxn 参照から取り、唯一の 経験則(どのテキストがどの記号のラベルか)はそう明記します。認識は必須の 2 signal と確信度を 上げる 1 signal(コネクタで結ばれた記号、複数種類の記号、すべての記号にあるラベル)で、三つとも 図そのものの構造であり、語ではありません。PoC のタイトルのみのゲートは顧客の慣例であって形式の ものではなく、同じデッキが別のタイトルでさらに 3 枚の工程図を描いており、ゲートがそれを隠して いたからです。最初の草案はタイトルを語のリストで第三の signal に残しましたが、同じデッキが、 それはリストを書き写した元の 1 枚でしか発火しないと示しました (ADR-0045)。第二に、DrawingML の走査は ooxml.py と同様に両ディストリビューションへ逐語的に複製し、ドリフトテストが複製を固定します。 図形 25 は、それを囲むチャンカーと記号として読むパーサーにとって同じ図形でなければならず、 プラグインは兄弟をインポートできないからです。コネクタの両端は bbox ではなく Shape.line に 載せます。直線には面積がなく、validate_bbox は面積を要求するからです。第三に、顧客文書は PR 本文の証拠であってフィクスチャにはしません。合成デッキ (tests/fixtures/office-process-chart.pptx)がすべての分岐を通り、顧客デッキでの受け入れ数値は PR に記します。POLICY_VERSION は変えません。段も plan のフィールドも動いておらず、新しい available なディスクリプタがスナップショット ID を動かし、それが plan を無効にする束縛だから です。インボイスのときと同じです。

帰結。 process_chartinvoice と同じくパーサーが書くキーで、RESERVED_METADATA_KEYS の 外にあり、signature_detection を求めなかった呼び出し側には何も新しく見えません。どんな タイトルであれ記号の流れを描くスライドは指名され、ラベルのない記号があれば確信度は低く なります。プレイグラウンドはチャンクと同じ計算でノードとエッジを重ねられます。LibreOffice に よるスライド描画はデモホストの便宜にとどまり、イメージの依存関係にはなりません。SmartArt、 グラフパート、セル単位の表の位置、ページブロックの図形一覧は、ベンチマークのラベルが求める まで対象外と明記します。

再検討の契機。 二つ目の描画慣例(P&ID、組織図)が同じ走査を求めたとき。その時点で記号表は パーサーが宣言するものになり、走査は indx-llm と同様にプラグインの下のライブラリになります (ADR-0038)。

ADR-0044 — Office のパッケージ層と DrawingML の走査はプラグインの下のライブラリである

Section titled “ADR-0044 — Office のパッケージ層と DrawingML の走査はプラグインの下のライブラリである”

ステータス 承認 · バージョン変更なし、ワイヤ変更なし

背景。 ADR-0043 の後、ooxml.py は 4 つの逐語的なコピー(オブザーバー、リーダー、チャンカー、 パーサー)、drawingml.py は 2 つのコピーとなり、それぞれドリフトテストが固定していました。 プラグインは兄弟をインポートできないからです (ADR-0007)。 ADR-0038 はチャットアダプターについてすでにこの形に答えていました。依存を持たないプラグインの下の ライブラリはエンジンスタックではないので ADR-0007 の取引は及ばず、その記録は ooxml.py を次の 候補として名指ししていました。どちらのファイルも標準ライブラリのみです。

決定。 indx-ooxml は、階層上 indx-llm の隣に置かれるワークスペースのディストリビューション です。package.py が zip-of-XML の層(オープン、パートの上限、DTD の拒否、パートの列挙、 リレーションシップ)、drawingml.py が図形の走査です。機能を宣言せず、スナップショットに加わらず、 何にも依存しません。Office の 4 プラグインと DrawingML を読む 2 つがこれに依存し、6 つのコピーと ドリフトファミリーの 2 行は消えました。境界テストの LIBRARY_MODULES は両ライブラリを一つの規則の 下に持ちます。ライブラリはコントラクトも、プラグインも、他のライブラリもインポートせず、 ファーストパーティのコードはライブラリをインポートしません。コードだけをテストしていたテスト (test_drawingml.py)はコードとともに移り、オブザーバーとリーダーのテストは元の場所に残ります。 それぞれのプラグインが ValueError を自らの拒否へ翻訳する仕方をテストしているからです。 slide_text はリーダーとパーサーに残ります。リーダーの規則であり、3 行であり、パーサーの等価性 テストがそれを守るからです。

帰結。 パートの上限やリレーションシップの規則を直す場所が一つになり、5 つ目の Office リーダーは インポート一つになります。ADR-0007 のコピーされた列挙の一覧は、settings.py を伴う plaintext.pyrfc822.pytaxonomy.py に縮みます。最初のものは設定モデル (INDX_TEXT_ENCODING)を持ち、依存のないモジュールとは別の問いになります。残る二つがこの経路の 次の候補です。

再検討の契機。 ライブラリが自身の依存や設定を求めたとき。前者はエンジンスタックであり ADR-0007 が ディストリビューションへ戻し、後者は設定の所有者であり、settings.py はそのディストリビューションに 住みます。

ADR-0045 — 認識の signal は形式の構造であり、一つの文書の語彙ではない

Section titled “ADR-0045 — 認識の signal は形式の構造であり、一つの文書の語彙ではない”

ステータス 承認 · バージョン変更なし、ワイヤ変更なし

背景。 process-chart シグネチャの最初の草案は、タイトルの語のリスト(工程系統図、工程図、 工程フロー、“process flow”、“process chart”)を第三の signal として持っていました。元になった顧客 デッキは 4 枚の工程図を描いていますが、この語が発火したのはリストを書き写した元の 1 枚だけで、 構造の 2 signal が 4 枚すべてを見分けていました。元になった文書でしか発火しない signal は何も 測っていません。レビューがこれを捕まえました。リポジトリの中には捕まえるものがありませんでした。

決定。 シグネチャの signal は文書タイプの性質であり、その基準は動機となった文書以外からの 証拠です。具体的には次の通りです。

  • signal は構造(記号の慣例、コネクタの参照、形式が宣言するフィールド)であるか、文書タイプが 定義上持つ語彙です。インボイスが “invoice” や「請求書」と書くのは、それがインボイスだからで、 indx-capability-invoice の 4 つの signal はその種類です。ある著者がある文書に置いた語 (スライドのタイトル、ある部署の図の呼び名)はどちらでもなく、signal ではありません。
  • signal は出荷前に、そのタイプの独立した 2 つ以上の文書で発火することを示します。フィクスチャと もう 1 つの文書、または作成元の異なる 2 つのフィクスチャです。PR 本文が signal ごとにそれを 記します。process-chart では顧客デッキと合成デッキがその対です。
  • 手元の文書の 1 つでしか発火しない signal は、確信度を上げるだけのものへ弱めるのではなく、削除 するか置き換えます。確信度は一致した独立 signal の割合であり、一つの文書から独立していない signal はそれを水増しするからです。signal は割合として数えてよく(チャートの記号のうちラベルを 持つものの割合)、一度しか発火しない語であってはなりません。
  • デプロイごとに変えたくなるもの(工場ごとの図の呼び名)は signal ではまったくなく、title が チャートに載るように出力上のデータであり、呼び出し側がそれで絞り込みます。

帰結。 process-charttitle をデータとして保ち、第三の signal はすべての記号にあるラベル、 つまり図の構造が自身について述べることになりました。レビュアーは新しい signal に二つを問います。 それが発火するタイプ水準の理由は何か、そしてどの 2 つの文書で発火したか。この規則はレビューの規則 であり、プロトコルの規則(プロバイダープロトコルの シグネチャ検出器の節)であって、テストではありません。2 つ目の文書はたいていリポジトリに入れられ ないものだからです。

再検討の契機。 signal を測れるラベル付きコーパスができたとき。その時点で、そのコーパスでの signal の精度が 2 文書の基準に取って代わります。

契約レベルで、まだ答えが出ていないもの。これにつまずく作業の途中で発見されるのではなく、意図的に 決められるように、ここに記録します。

  • ベクトルをすべてのブロックに載せるか、今日のようにチャンクだけか。 文書ベクトルは消費者が いないので先送りです。分類がその消費者になると見込まれていましたが、実際にはテキストを消費しました (ADR-0018)。チャンクが何であるかは今や インストールされたチャンカーの答えです。出荷済みチャンカーが引く境界が正しい埋め込み窓かどうかは、 検索がまだ負っている測定です。
  • 共有された 1 つの埋め込み空間は、優先モダリティすべてを、どの品質でカバーするのか。 半分は 答えが出ています。clip-vit-b32 は今や両側でテキストと画像をカバーし、文書画像レーンを開いた 測定は 2 文書を 2 キャプションに対して比べたもので、これは判別であって検索品質ではありません。 CLIP のテキスト塔は日本語に十分弱く、同じ測定が共有空間のテキスト半分に反対する証拠になって います。ラベル付き集合に対するランク付き測定が、これに答えるものです。
  • テキストとベクトルを別々にバージョン管理できるのは、再埋め込みが再読み取りを伴わない場合だけ であり、それにはソースダイジェストをキーとするテキストキャッシュが要ります。今日ここでは何も 保存していません。
  • 永続化とルーター/エグゼキューターの分離デプロイは、同期ステートレスのバックボーンが測定された ベースラインを持つまで先送りです — ただしレシピストアとエクスポート先が、状態を欲しがる最初の 2 つなので、そこがこの決定を再び先送りするのではなく下す場所になります。

ADR-0046 — PDFium はプロセス全体で一つのロックであり、プラグインの下のライブラリに置く

Section titled “ADR-0046 — PDFium はプロセス全体で一つのロックであり、プラグインの下のライブラリに置く”

ステータス 承認 · バージョン変更なし、ワイヤ変更なし

背景。 #148 のドキュメント CI で、デモホストが NASA のエンコードと検索のライブ仕様の間で 2 度、 何も出力せずに死に、再実行では通りました。pypdfium2 自身のメタデータは PDFium がスレッドセーフで ないと述べており、サーバーはスレッドプール経由でファサードに到達します (ADR-0024)。つまり 2 つのリクエストが 同時にその中にいられます。6 つの呼び出し箇所がそれぞれ自前の with PdfDocument(...) ブロックで 直接開いていました。ネイティブリーダー、インボイスリーダー、OCR と Vision のレンダラー、PDF チャンカー、そしてデモホストのページ数と描画です。8 スレッドがページを描画する傍らで 2 スレッドが 同じファイルをエンコードすると、3 回中 3 回プロセスが落ちました。2 回はセグメンテーション違反、 1 回はヒープ破損で、その前に何も出力しません。CI の失敗と同じ形です。

決定。 indx-pdfiumindx-llmindx-ooxml の隣に置くワークスペースのディストリビューション です(ADR-0038)。 再入可能なロック一つと、それを取って pypdfium2 を呼び出し時にインポートし、文書を開き、ブロック全体で ロックを保持する document(source) コンテキストマネージャ一つです。機能を宣言せず、スナップショットに 加わらず、何にも依存しません。pypdfium2 は各レーン自身の依存のままで、DPI とピクセル上限も同様です (描画については ADR-0007 が引き続き成り立ちます)。ロックはプロセスに一つのオブジェクトでなければならず、それがディストリビューション ごとのコピーを排し、ヘルパーではなくライブラリにする理由です。ページアクセスと描画を含むすべての PDFium 呼び出しはブロックの中に、結果のエンコードは外に属します。Ruff はライブラリ以外のあらゆる場所で pypdfium2.PdfDocument を拒否する(banned-api)ので、7 つ目の呼び出し箇所が迂回することはできません。 同じ 8 スレッドの実行は、ロックを入れると 3 回中 3 回通ります。

帰結。 PDF の処理はプロセスごとに直列化されます。PDF ページを並列に読みたいデプロイはスレッドでは なくワーカーを走らせます。それは、クラッシュがすでに黙って課していた境界です。OCR エンジンは自身の 理由で自身のロックを保ちます。テキストのエンコードと PNG のエンコードはロックの外に残るので、直列化 されるのはエンジンであってリクエストではありません。依存グラフには 3 つ目のライブラリが加わり、 ライブラリの規則は変わりません。コントラクトも、プラグインも、他のライブラリもインポートしないことです。

再検討の契機。 あるレーンがリクエストをまたいで文書を開いたままにする必要が出たとき。その時点で ロックの範囲はブロックではなくセッションになります。あるいは pypdfium2 がスレッドセーフなビルドを 出荷したとき。

ADR-0047 — 行を見つけたリーダーはそう述べ、チャンカーがそれを切る

Section titled “ADR-0047 — 行を見つけたリーダーはそう述べ、チャンカーがそれを切る”

ステータス 承認 · バージョン変更なし、ワイヤ変更なし

背景。 スプリントのストレッチの MHI 側は、鉄筋の試験成績表を 90° 回転した純ラスタの 1 ページ スキャンで、16 の枠付き正解領域を持ちます。ocr extra があれば generic-ocr はこれをよく読み (827 文字、確信度 0.97、証明書番号・表題・供給者が判読可能)、ページは no_geometry の一つの チャンクとして返ってきました。リーダーが認識器の行を text に結合してその枠を捨て、 indx-chunker-pdf はテキストレイヤーのページしか切らないからです。デプロイ済みホストが受け取る すべてのスキャンのアップロードに同じ隙間がありました。ベンチマークは 1 ページの文書を受け付けない ので、このスキャンは PR 本文の証拠であってケースではありません。

決定。 PageOutputlines: tuple[TextLine, ...] を加えます。エンジンが位置を知るリーダーは、 見つけた各行を TextLine(text, bbox) として述べ、枠はすべての枠が従う一つの規則に従います。 メタデータのキーではなくポートモデルの項目です。型付きで、ワイヤには決して載らず(ページブロックは text を保ち、枠はチャンクが運ぶので、何も二度述べられません)、PageOutput は HTTP のモデルでは ないのでクライアントの再生成もありません。generic-ocr は認識器の四角形から、描画した画像に対して 正規化してこれを埋めます。indx-chunker-lines はあらゆるメディアタイプの組み込みチャンカーで、 行ごとに一つのチャンクをその枠で切り、リーダーが何も述べなかったページは辞退するので、床が 引き続きそれを取ります。二つの代案を退けました。リーダー自身がチャンクを出すこと(リーダーは読み、 チャンカーは切る。サードパーティの OCR レーンはいま一つの項目を埋めるだけで位置情報を得ます)と、 枠を得るためにチャンカーが OCR を再実行すること(最初の実行がすでに知っていたもののために、ページ ごとにエンジンをもう一度走らせること)です。リーダー自身の行を切ることは何も洗浄しません。それは ラダーを降りて出力検証を通ったリーダーの判定であり、chunk_map が保つ非対称性そのものです。

帰結。 このスキャンでは、位置のない 1 チャンクの代わりに枠付きの 119 チャンク。16 領域のうち 14 に チャンクの中心が入り、外れた 2 つは文字のない朱印です。プレイグラウンドはスキャンを PDF と同じように 描きます。no_geometry はもう OCR で読まれたページを指しません。Vision モデルのレーンは枠を返さない ので何も述べません。リーダーは行と、それとは異なるテキストを述べてよく、チャンクは行です。チャンクの テキストはチャンカーが書くものだと、プロトコルはすでに述べています。

再検討の契機。 リーダーが行より多くを知るとき(語、ブロック、読み順)。その時点で TextLine は 答えの全体ではなく、小さな木の葉になります。

ADR-0048 — DXF 図面はレイアウトをページとするネイティブのテキストレイヤーであり、ezdxf で読む

Section titled “ADR-0048 — DXF 図面はレイアウトをページとするネイティブのテキストレイヤーであり、ezdxf で読む”

ステータス 承認 · バージョン変更なし、ワイヤ変更なし

背景。 スプリント最後のストレッチ項目「Denso PoC からの CAD 収穫」は存在しないコードを名指して いました。PoC の README は CAD をスコープ外とし、そのデータセットに CAD ファイルはありません。 作業はネイティブ DXF 対応へ向け直されました。DXF ファイルはプレーンなグループコードで、すべての TEXT と MTEXT、ブロック参照の属性(表題欄はその一つ)、寸法が自身のブロックに描画した計測値、そして CAD プログラムがページとして示すレイアウトを述べています。再利用条件のある実図面は手の届く範囲に なく(土木学会のサンプルは著作権表示のみの SXF)、フィクスチャは合成で生成したものです。

決定。 DXF は Office の形(ADR-0008、ADR-0044)を取ります。indx-observer-dxfimage/vnd.dxf を計画可能にします。ページはレイアウト、すなわちモデル空間と各ペーパー空間 レイアウトをタブ順に並べたもので、形式が述べる区切りです。文字のあるレイアウトは usable、 エンティティのないものは usable かつ empty(白紙のシート。空として読まれます)、図形はあるが 文字のないものは missing で、図面を描画する段はないので、人手のレビューへの正直な崖です。 indx-capability-dxf-extraction はこのタイプのネイティブの段です。無料で即時、すべてのテキストに 位置が付くので、indx-chunker-lines(ADR-0047)が追加の作業なしに図面を枠付きチャンクへ切ります。 二つが共有するもの、どのレイアウトがページでテキストがどこにあるかは indx-dxf です。 プラグインの下のライブラリで、依存を宣言せず、コントラクトもどこでもインポートしません。ezdxf は呼び出し時にインポートし、二つのディストリビューションが ezdxf をハードな依存として宣言します。 native-extraction が PDFium について述べる理由と同じです。形式のネイティブの段は extra の背後では なく既定のインストールに属し、ディストリビューションは自分のエンジンスタックを所有します (ADR-0007)。自前のパーサーは草案を書いた上で退けました。ezdxf はバイナリファイル、コードページ、 MTEXT の書式コード、ブロック変換、寸法の描画、フォント計測に基づく範囲をすでに答えており、手書きの リーダーはそのどれも近似しかできません。観察はファイル全体をロードし、ソースの上限がそれを 制限します。図面にはそのエンティティより安い構造がないからです。フィクスチャ生成器は ezdxf が 書き込みのたびに作り直すヘッダー値を固定し、固定したハッシュシードの下で走ります。ezdxf が二つの オブジェクトを集合の反復順で並べるためで、それで再生成は差分のない操作になります。

帰結。 12 のメディアタイプが観察も読み取りもできます。図面の表題欄、部品表、注記は矩形付きの チャンクとなり、PDF ページの行と同じように検索でき、プレイグラウンドに描けます。新しい ディスクリプタでスナップショット ID が動き、ベンチマークは再ピンと再実行を行いました。寸法は <> のプレースホルダーではなく描画された値として読まれます。DWG は読みません。ezdxf は開かず、 変換器はオペレーターの道具であって依存ではありません。

再検討の契機。 フィクスチャの構造が覆わない顧客図面(ペーパー空間ビューポートの尺度、表 エンティティ、外部参照)が届いたとき。あるいは図面に対するシグネチャ(表題欄をそれとして認識する。 ADR-0045 が適用されます)を指名する価値が出たとき。

ADR-0049 — ロードマップはプロダクトの青写真を目標とし、データストアはエクスポート先である

Section titled “ADR-0049 — ロードマップはプロダクトの青写真を目標とし、データストアはエクスポート先である”

ステータス 承認 · バージョン変更なし、ワイヤ変更なし

背景。 公開ロードマップは 8 つのステップを KR1 に照らして測っていました。KR1 は、設定した 本人のレビューを受けていないノートに書かれた 4 つの目標で、ページは誰も合意していない数値に 向けて作業を並べていました。2026-09-10 にプロダクトの青写真が入りました。indx が目指す Web アプリの対話型モックで、埋め込み基盤(空間、データストア、特徴とタグ)と、その上に作る アプリケーション(プレイグラウンド、few-shot ライブラリ、エンドポイント)を、選んだデプロイ ターゲットの上で示します。青写真のデータストアはレコード、ベクトル、来歴を保持し、これは 非ゴール「indx はあなたのインデックスを所有しない」に照らして読まれます。

決定。 ロードマップは青写真を画面ごとに読みます。青写真は docs/website/public/target/ からそのまま配信し、各セクションが説明する画面へリンクします。KR1 のステップ一覧は developer/milestones へ移し、記録として残します。ベンチマークは KR1 ブロックを算出し 続けます。目標が合意されたかどうかにかかわらず、それらの数値は計測されるからです。青写真の ストアは 4 つ目の未解決の問いがすでに名指しするエクスポート先であり、顧客の環境にあります。 それによって非ゴールは、永続化の拒否ではなく場所の規則として生き残ります。空間、特徴 スキーマ、few-shot ライブラリは永続化の決定の最初の消費者なので、その決定は 3 つすべてが 見えている実装順のフェーズ 7 で一度だけ下し、どれか 1 つのために早めることはしません。

帰結。 各画面は状態を持ちます。背後に使えるものがなければ 計画中、画面が見せるものにサービスがすでに答えるなら 進行中、Web アプリで人が画面を使えるようになって初めて 完了 で、これはまだどの画面も得ていません。順序のセクションは 永続化の決定を Layer 01 のすべてより前に置きます。プレイグラウンドの機能起点の枠組みと /v1/capabilities の上のデプロイ画面は決定を必要とせず、今すぐ始められます。

改訂は 青写真の日付つき後継が public/target/ に入ったとき、または永続化の決定が下され、 ストアの場所の規則が独自の記録を必要とするとき。

ADR-0050 — リクエスト側がコンポーネントを名指しし、ラダーは決定ではなく既定である

Section titled “ADR-0050 — リクエスト側がコンポーネントを名指しし、ラダーは決定ではなく既定である”

ステータス 承認 · バージョン変更なし、ワイヤ変更なし

背景。 KR1 はルーターと計画を求めましたが、望みはその後もっと単純になりました。どの NER レーン(1 つ、または言語ごとに 1 つ)、どの OCR、どの LLM、どのエンリッチャーを使うか、 あるいは使わないかを、リクエスト側が決めることです。その大半はすでにあります。 classification.*_idsextraction.*_idsenrichment.*_idsembedding_space_ids は リクエストごとにコンポーネントを名指しし、複数の抽出器 ID はすべて走ってブロックごとに最初に ラベルへ答えたレーンが勝ちます。名指しできないものが 3 つあります。ラダーが選び 5 つの RoutingConstraints が形づくるだけのリーダー、レーンごとに 1 つのデプロイ変数である LLM モデル、そして実装順のフェーズ 6 までリスト順のチャンカーです。問われたのは、これに新しい エンドポイント、/v2/、新しい実行器が要るかでした。

決定。 選択は ADR-0026 の形だけを通じてリクエスト側に届きます。ID、スナップショットへの 広告、リクエストフィールドです。追加は 3 つで、どれも既定値が今の振る舞いを保つ加算的な ものです。RoutingConstraints への計画時のケイパビリティ許可リストとフォールバック禁止 フラグ。計画は 1 つのリーダーを名指しするか、除外を名指しして unsatisfied で返ります。実行器は すでに計画が名指ししたものしか動かさないので手つかずです。各 LLM レーンについて、リスト設定の 許可されたモデルごとに広告される 1 つの ID。既存の ID フィールドがモデルを選び、デプロイの リストが青写真の Deployment 画面が見せる許可リストになります。言語ごとの抽出レーン。 LANGUAGES_METADATA_KEY の下にすでに書かれている言語による実行器のフィルタです。/v2/ は 不要です。計画とエンコードの分離は生き残り、「リーダーを固定し、フォールバックを拒む」は候補が 1 つの計画です。新しい実行器も不要です。今も 4 つのファミリーを ID でディスパッチし、計画が 名指ししたものを動かしています。各追加は決定が変わるところで POLICY_VERSION を上げ、 コントラクトチェックを通じてクライアントを再生成します。

帰結。 ラダーはリクエスト側が何も言わないときに動くものであり、すべてを名指しした リクエストはまさにそれか、理由を述べる拒否を受け取ります。KR1 のルーティング精度は プロダクトの約束ではなくベンチマークの数値にとどまります。3 つの追加はカタログの計画中項目 として記録され、どれもストアに依存しないのでロードマップの最初のステップに置かれます。

改訂は 3 つのうち最初のものが出荷されたとき、または ID を持たないもの(言語検出器、 観測器)をリクエストが名指しする必要が出たとき。その場合はまず ID が義務づけられます。

ADR-0051 — Web アプリは frontend/ に置き、ホストが /app で配信する静的エクスポートとして出荷し、apps → screen → ux → ui の層に分ける

Section titled “ADR-0051 — Web アプリは frontend/ に置き、ホストが /app で配信する静的エクスポートとして出荷し、apps → screen → ux → ui の層に分ける”

ステータス 承認 · バージョン変更なし、ワイヤ変更なし

背景。 ADR-0049 はロードマップをプロダクト青写真を画面ごとに読むものにし、その最初の ステップは既存のリクエストの上に成り立つ純粋なフロントエンドの仕事を 2 つ挙げています。 プレイグラウンドの機能起点の見せ方と、GET /v1/capabilities で埋める読み取り専用の Deployment 画面です。リポジトリにはそれを置ける場所がありませんでした。ドキュメントサイトは Astro と Starlight で、散文とサンドボックスのアイランドには適し、シェル・ルート・状態を持つ アプリケーションには不向きです。7 月の frontend/ ワークスペースの試みは着地前に リセットされ、その教訓はメモとしてしか残りませんでした。問われたのは、アプリをどこに置くか、 どう配信するか、そして青写真の 8 画面が到着するにつれてコンポーネントの一つのディレクトリに 育ってしまうのを何が防ぐかでした。

決定。 frontend/ 配下の pnpm ワークスペースで、アプリは frontend/apps/web に置き、 後のフロントエンドが Python の packages/ ではなくその隣に並ぶようにします。Next.js の 静的エクスポートで basePath: /app とし、examples/app2.pyfrontend/apps/web/out を そこにマウントします。ドキュメントサイトの / のキャッチオールより前、1 つの本番イメージ (ADR-0039)の中でです。Node ランタイムも CORS もありません。API は設計上 CORS を持たず、 エクスポートに必要なのは 1 つのオリジンです。各画面はそれ自体がパッケージ indx-screen-<section> であり、層は固定され、test_workspace_boundaries.py が Python ワークスペースを固定するのと同じ形で frontend/scripts/check-boundaries.mjs が検査します。 web → screen-* → ux → uiindx-api-client はアプリと画面が使ってよい葉、indx-ux は 表示のみでホストに依存せず(ラベルはすべてプロパティ)、indx-ui は 1 つのプリセットから 得た shadcn のプリミティブです。TypeScript クライアント、react-query フック、zod スキーマ、 MSW ハンドラはルートの openapi.json から orval が生成してコミットされ、 just frontend::client-check がドリフトで失敗します。api::contract-check がすでに持つ 形です。ロケールは常にパスにあり、各パッケージは英語に対して型付けされた自身のメッセージを 出荷するので、日本語の文字列が欠けると型エラーになります。ライトとダークは最初の画面から どちらも一級です。

帰結。 エクスポートは静的サイトにできないことを禁じます。ミドルウェアやプロキシ、 サーバーアクション、リクエストごとのレンダリング、generateStaticParams のないルートは ありません。next dev/v1/health をローカルの API にプロキシし、開発も 同一オリジンのままです。イメージは Node ステージを 1 つ、ランタイムは静的ディレクトリを もう 1 つ得ます。デプロイ先は変わりません。just frontend::ci.github/workflows/ci-frontend.yml が、答えを変えうるパスに対するゲートです。意図的に まだ無いものと、その引き金は次のとおりです。トポロジカルな pnpm -r 実行が遅くなったときの タスクランナー、ux に画面から到達できない状態が出たときの Storybook、このリポジトリの外に 利用者が現れたときのクライアント公開、ロードマップのエンドポイントのステップでの auth パッケージ、そしてそれに伴う Node ランタイムの問いです。

改訂は 認証がリクエストを見る必要を持ったとき。それが静的エクスポートでは足りなくなる 日です。あるいは 2 つ目のフロントエンドが現れ、共有パッケージが所有者を求めるとき。

ADR-0052 — Web アプリはブラウザ内ストアの上に画面優先で育ち、各画面はそれぞれ API に置き換える

Section titled “ADR-0052 — Web アプリはブラウザ内ストアの上に画面優先で育ち、各画面はそれぞれ API に置き換える”

ステータス 承認 · バージョン変更なし、ワイヤ変更なし

背景。 ADR-0051 は Web アプリに居場所と実物の画面を 1 つ与えました。青写真には 9 つの画面が あり、問われたのは、作りながら各画面を API につなぐか、まず 9 つすべてを見るかでした。 青写真が見せるものの大半には、まだ答えるものがありません。ストアも、空間も、ライブラリも、 人が作るエンドポイントもです。つないでいれば、実装順のフェーズ 7 が留保する永続化の決定に 先んじてエンドポイントを発明するか、見た目を決めている最中に大半の画面を空のままにするか になっていました。

決定。 すべての画面はサンプルデータを描画し、ブラウザ内に保持されるストア indx-mock を 変更します。青写真自身の state を、localStorage のキー indx-app-mock-v1 の下に永続化する リデューサとして持ち、サンプルレコードはロケール非依存の ID でキー付けし、その文言は両言語で 持ちます。すべての変更は sonner の警告を出し、モックであることを告げるので、誰もデモを プロダクトと取り違えません。このパッケージは層構造 web → screen-* → indx-mock → ux → ui で画面の下に置かれ、画面同士は互いを import しないため、2 つの画面が開くダイアログも ここに置きます。例外は 1 つ。Deployment 画面は最初の画面がすでに持っていた GET /v1/capabilities の表を、青写真の内容の下に保ちます。ロケールルートは概要なので、 /app/en/ が着地点となり、デモホストのロケールごとのリダイレクトはなくなりました。

帰結。 見た目は、Python の変更も新しいエンドポイントもなしに、配信されたエクスポートの 上で反復できます。後で画面を API につなぐ作業はその画面に閉じます。useMock() の読み取りが indx-api-client の生成フックになり、モックパッケージはその画面のレコード分だけ縮みます。 このストアは永続化の設計ではなく、そうなってはなりません。永続化の決定はフェーズ 7 に とどまります。

改訂は 最初の画面が実際のエンドポイントにつながったとき、または永続化の決定が着地し、 サンプルレコードがブラウザの外に居場所を得たとき。

ADR-0053 — クラウドのターゲットは 1 つの Terraform スタックであり、レジストリ、1 つのコンテナ、1 つのポート、Cloudflare の境界を持ち、モデルのレーンはその変数

Section titled “ADR-0053 — クラウドのターゲットは 1 つの Terraform スタックであり、レジストリ、1 つのコンテナ、1 つのポート、Cloudflare の境界を持ち、モデルのレーンはその変数”

ステータス 承認 · バージョン変更なし、ワイヤ変更なし

背景。 10 月 1 日のチェックポイントは 3 つのクラウドにまたがるデプロイの筋書きを求めますが、 それを持っていたのは AWS だけでした。3 つのスタック、手動起動のワークフロー、そしてすでに 生きているホスト名のまわりに作られた境界です(ADR-0039、ADR-0040)。Azure と GCP はもう一度 それにすることもできたし、もっと小さいものにすることもできました。2 つ目の問いは、モデルの レーンがコードでいくらかかるかでした。答えはゼロです。どの LLM レーンも設定された文字列を そのまま LiteLLM に渡すので、azure/<deployment>vertex_ai/<model> はすでにある変数の 新しい値であって新しいアダプターではなく、3 つ目のクラウドに届くために packages/ は何も 変わりません。

決定。 クラウドのターゲットは手で適用する 1 つの Terraform スタックであり、レジストリ、 1 つのポートの上の 1 つのコンテナ、境界、予算のアラート、DNS レコードを持ちます。 infra/azure/terraform は Cloudflare の公開レンジを受け入れるイングレスを持つ Consumption の 環境の 1 つの Container App、infra/gcp/terraformrun.app の URL を閉じ、同じレンジを 受け入れて他所の Host を拒否する Cloud Armor のポリシーを持つグローバルロードバランサーの 背後の 1 つの Cloud Run サービスです。ゼロへのスケールが既定で、温かいデプロイのための 2 つ目の テンプレートではなく min_instances(0 か 1)が唯一のつまみです。その代償は、アイドルの後の 最初の呼び出し元が失うことが想定されるコールドスタートであり、ページがそれを明記します。 オリジンの TLS は Cloudflare の Origin CA 証明書です。境界が Cloudflare だからで、オリジンの 証明書はプロキシからしか見られず、公に信頼される必要がなく、境界が閉じるためにあるドアを 検証のチャレンジが通り抜ける必要もありません。プロバイダーの資格情報は環境から取ります。 AWS_REGION とタスクロールがすでに持つ形で、Terraform には az logingcloud auth application-default login、Azure OpenAI には Container App のシークレットとして 持つ API キー、Vertex AI には Bedrock と同じくランタイムのアイデンティティです。どのスタックの 予算も通知するだけで、ほかには何もしません。ロールを外す ADR-0040 の予算アクションは AWS だけの ものにとどまります。どちらのクラウドにもそれがないからで、AWS の外でのブレーキは運用者が スイッチを無効にすることです。どのテンプレートからも意図して外してあるもの。顧客の VNet や VPC と そのピアリング、プライベートエンドポイントや Private Service Connect、WAF のルールセット、 ID フェデレーション、そして AI サービスのリソースそのもの(Azure OpenAI のアカウント、 Vertex AI のクォータ)です。それらはクォータの申請、リージョンとデータの所在の決定、顧客ごとの セキュリティを伴う実装の仕事であり、それらを当て推量するテンプレートはどのデプロイに対しても 間違っています。

帰結。 3 つ目のターゲットの費用は 1 つのスタックと 1 つのページ(両言語)であり、Python は ありません。自動化された経路は AWS のワークフローだけのままです。Azure と GCP には運用者が走らせる plan と apply があるだけで、レビュアーの門も、ロールバックのアーティファクトも、自前のスモークも ありません。その証明はホストに対する just test::bdd::deployedjust docs::e2e-deployed です。 just infra::terraform::validateinfra/*/terraform* を glob し、.github/workflows/infra.yml がそれをプルリクエストで走らせるので、4 つ目のスタックは追加されたその日にゲートされます。 AWS のスタックは途中で、欠けていた INDX_EMBED_DIMENSION を得ました。次元のないホスト型の 埋め込み空間はそもそも宣伝されず、それも黙って行われるので、その Bedrock の埋め込み空間には 到達しえませんでした。

改訂は ターゲットを端末の前の人間以外がデプロイしなければならなくなったとき。それは クラウドごとのワークフロー、承認、ステートのロックです。あるいは顧客のネットワークや AI サービスのリソースが、テンプレートの隣ではなく中になければならなくなったとき。

ADR-0054 — Azure と GCP はそれぞれ手動起動のワークフローでデプロイし、名前によって AWS からも互いからも隔離する

Section titled “ADR-0054 — Azure と GCP はそれぞれ手動起動のワークフローでデプロイし、名前によって AWS からも互いからも隔離する”

ステータス 承認 · バージョン変更なし、ワイヤ変更なし

背景。 ADR-0053 は Azure と GCP をそれぞれ 1 つのスタックにし、端末の前の人間に任せ、 それを改訂する時を名指ししました。ターゲットを人間以外がデプロイしなければならなくなったとき、 つまりクラウドごとのワークフロー、承認、ステートのロックです。その時が最初の本当のデプロイです。 AWS のワークフローはすでにその形を持っています(ADR-0039、ADR-0040)。手動起動のみ、読み取り専用の アイデンティティからの plan、必須レビュアーが承認そのものである GitHub Environment の背後の deploy、承認されたものと一致しなければならない新しい plan、そして apply の後の公開スモークと @deployed のシナリオです。3 つのクラウドは 1 つの Cloudflare ゾーンを共有し、決して起きては ならないことが 1 つあります。あるクラウドのワークフローが別のクラウドの提供するホスト名を 取ってしまうことです。どのスタックも自分の DNS レコードを書くからです。

決定。 2 つのワークフロー、deploy-azure-prod.ymldeploy-gcp-prod.yml。それぞれ AWS の 形の写しから、スタックが持たない部分を除いたものです。cutover はありません。レコードは スタックの中にあるからです。rollback のジョブもありません。ロールバックとは、レジストリが すでに持つタグを image_tag で名指しした deploy であり、どの apply もすでにそのやり方を 知っているからです。ターゲットを選ぶ名前はすべてクラウドごとで、共有のものに戻ることは ありません。変数とシークレットは AWS の PROD_* の隣の AZURE_PROD_*GCP_PROD_*、Environment は production-azureproduction-azure-planproduction-gcpproduction-gcp-plan、そして 各クラウドのアイデンティティは自分の Environment の不変の OIDC サブジェクトだけを信頼するので、 あるワークフローのために発行されたトークンで別のワークフローが認証されることはありません。 ホスト名は indx.jp 配下の任意の名前です。ホスト名がどのクラウドで動いているかを語る必要は なく、3 つのスタックは 1 つのゾーンにレコードを書くので、各ワークフローは何かが走る前に、 ほかのクラウドのホスト名の変数と等しい名前を拒否します。deploy は運用者がその名前を 打ち込んだときにだけ走ります。3 つが共有する唯一の資格情報は Cloudflare のものです。ゾーンは 1 つのゾーンであり、 それを編集できるトークンはその中のどのホスト名に対しても編集できるからです。各ワークフローは まず自分の *_CLOUDFLARE_API_TOKEN を読み、なければ AWS のワークフローがすでに持つシークレットを 読みます。Origin CA のキーはどのみちアカウントに 1 つのキーです。アイデンティティはキーではなくフェデレーションです。Environment ごとのフェデレーション 資格情報を持つ Entra のアプリケーションと、同じサブジェクトを属性条件に持つ Workload Identity Federation のプロバイダーです。それらを作るのはページにある一度きりのコマンド列で、手で行います。 ステートのコンテナがすでに持っていた形です。plan の要約、ケイパビリティのガード、重みのイメージは クラウドに依存しない AWS のスクリプトをそのまま再利用し、新しいのは 2 つのクラウドが共有する 1 つの比較スクリプトと 1 つの公開スモークです。

帰結。 Azure や GCP へのデプロイとは、レビュアーが読んだ plan を承認し、そのクラウドの レジストリにイメージをビルドし、plan をやり直し、ずれがあれば拒否し、apply し、リビジョンを待ち、 境界を証明し(Azure ではオリジンへの直接アクセスが拒否されること、GCP ではよその Host に 403 が 返ること)、受け入れのレーンを走らせる 1 回の実行です。手で適用することは引き続き可能で、同じ スタックです。費用は 3 回目の GitHub のセットアップと、クラウドごとにもう 1 つのページの節 (両言語)です。AWS のワークフローは変わりません。そのインラインの plan の比較は共有スクリプトへ 移していません。兄弟を追加する変更で本番の経路をリファクタリングはしないからです。

改訂は 4 つ目のクラウドによってワークフローの 3 つ目の写しが間違った形になったとき。その時点で 3 つはクラウドを入力とする 1 つの再利用可能なワークフローになります。あるいはロールバックが イメージのタグでは名指しできないものを復元しなければならなくなったとき。

ADR-0055 — Origin CA のキーは去る。GCP は証明書を DNS で証明し、Azure はゾーンにスコープしたトークンで Origin CA を保つ

Section titled “ADR-0055 — Origin CA のキーは去る。GCP は証明書を DNS で証明し、Azure はゾーンにスコープしたトークンで Origin CA を保つ”

ステータス 承認 · バージョン変更なし、ワイヤ変更なし

背景。 ADR-0053 は 2 つの新しいスタックにオリジンの Cloudflare Origin CA 証明書を与え、その 呼び出しをアカウント全体の Origin CA キー(GitHub では CLOUDFLARE_API_USER_SERVICE_KEY)で 認証させました。Origin CA のエンドポイントはスコープ付きトークンが到達できない唯一の Cloudflare API だ、という根拠でです。どちらのクラウドもデプロイされる前に 3 つのことがわかりました。 Cloudflare はサービスキーによる認証を 2026-03-19 に非推奨にし、2026-09-30 に廃止します。代わりは SSL and Certificates / Edit を持つ API トークンで、1 つのゾーンにスコープできます。すべての スタックが固定する Terraform プロバイダー(cloudflare/cloudflare 4.52)は 3.32 から cloudflare_origin_ca_certificate をトークンで認証しており、その根拠はこのコードにとって最初から 偽でした。そしてリポジトリのシークレットにあるアカウント全体のキーは、どのみち間違った形でした。 スコープできず、隣の DNS トークンが 1 つのゾーンを開くところを、アカウントが持つすべてのゾーンを 開きます。

決定。 キーは両方のワークフローと両方のページから去り、2 つのクラウドは証明書で袂を分かちます。 GCP は AWS の形を取ります。Certificate Manager の DNS 認可で証明する Google 管理の証明書です。 スタックはすでに持つ DNS トークンで、プロキシしない _acme-challenge の CNAME を 1 つ書き、 Google がそれを読んで発行と更新を行い、ステートが持つべき秘密鍵は存在しません。hashicorp/tls はそのスタックから去ります。Azure は Origin CA を保ちます。その道がないからです。Container Apps が マネージド証明書を発行し更新するのは、アプリの FQDN を直接指す CNAME に対してだけで(Microsoft は 発行と更新を阻む中間の CNAME として Cloudflare を名指ししています)、しかも DigiCert がアプリに 到達できるときだけで、それはイングレスの許可リストが禁じています。したがってその 1 つの Cloudflare トークンはゾーンに DNS / EditSSL and Certificates / Edit を持ち、共有の AWS のトークンへ 落ちるのは、それが後者も持つときだけです。Azure で選ばなかったもの。DNS-01 による Let’s Encrypt は、公開の信頼を持ちトークンだけで済みますが、手動起動のワークフローでは 90 日の期限が、無人で 落ちるオリジンを意味します。手で一度発行してシークレットに置く証明書は、鍵を 2 か所に置きます。

結果。 各クラウドはゾーンにスコープした Cloudflare のシークレットを 1 つ持ちます。AWS がすでに 持っていた形です。GCP のステートには秘密が入りません。Azure のステートはまだオリジンの秘密鍵を 持ちます。環境の証明書が鍵と証明書を一組で受け取るからで、そのページはそう言い続けます。GCP の デプロイは証明書が ACTIVE になる前に返り、公開スモークの再試行がそれを待ちます。GCP の deploy アカウントは roles/certificatemanager.editor を得て、certificatemanager がスタックの有効にする API に加わります。ADR-0053 のオリジンの TLS についての一文と、Origin CA のキーはアカウントごとに 1 つだという ADR-0054 の節は、そのとき決めたことの記録として残ります。この記録がその両方に 取って代わります。

改訂は Container Apps がマネージド証明書を DNS レコードで検証するようになったとき。その時点で Azure は GCP の形を取り、Origin CA のリソースはそれを持つ最後のスタックから去ります。この記録の Azure の半分には ADR-0064 が取って代わります。そちらは代わりに、オリジンをプロキシの背後から 出しました。

ADR-0056 — Web アプリはホストのルートを取り、ドキュメントサイトは /docs/ 配下に移り、ロケールのルートはセールスページになる

Section titled “ADR-0056 — Web アプリはホストのルートを取り、ドキュメントサイトは /docs/ 配下に移り、ロケールのルートはセールスページになる”

ステータス 承認 · バージョン変更なし、ワイヤ変更なし

背景。 ADR-0051 は Web アプリを /app で、ドキュメントサイトを / で配信し、ADR-0052 は各 ロケールのルートを概要画面にしました。サイトが製品の顔で、アプリはその奥の設計図だったからです。 9月14日のレビューは、正面にドキュメントではなくセールスサイトを置き、見せるものはアプリにするよう 求め、スプリントの最初のストーリーは、優位性を一枚で語るページを本番ホストのルートに置き、ドキュメ ントはワンクリック先に置きます。障害は二つありました。ドキュメントは内部リンクをすべてサイトルート から書き、base を設定していないため、プレフィックス配下に移すには百ファイル千リンクの書き換えか、 ビルド時の処理が必要でした。そして FastAPI はすべてのデプロイで Swagger UI を /docs で配信して おり、それはドキュメントが欲しいプレフィックスそのものでした。

決定。 エクスポートはルート向けにビルドします。basePath は Next の設定から去り、 examples/app2.pyfrontend/apps/web/out を最後に / にマウントし、//en/ にリダイ レクトします。ドキュメントサイトは base: /docs でビルドしてそこにマウントします。コンテンツは 引き続きサイトルートからリンクを書き、Sätteri の HAST プラグインがビルド時に hrefsrc に プレフィックスを付け、デモホストがサイトの横で配信するルートは除きます。書き手が /docs/ を書く ことはなく、コンテンツはルートで grep できるままです。手で書いた唯一のプレフィックスはホームページ のヒーローで、Starlight はそこに触れません。create_app() はリファレンスページのパスをキーワード 引数として受け取り、デモホストはそれらを /api/docs/api/redoc に移します。indx serve は FastAPI の既定を保ち、エクスポートされる契約は変わりません。ロケールのルートはセールスページ、 ほかと同じ画面パッケージをアプリシェルなしで描画したものになり、概要画面は /overview/ に移ります。 どのクラウドの Terraform もホスト名全体を一つのオリジンに送り、パスによるルーティングをしないため、 この配置はデモホストだけのものであり、インフラの変更なしに出荷できます。選ばなかったもの: コンテンツ のリンクを一度だけ書き換えること。これは今後のすべてのリンクに /docs/ を置き、一つのプルリク エストに千行の差分を積むことになります。サイトに別のプレフィックスを使うこと。これは API リファレンス が /scalar であるホストで、/docs がスキーマの上のフォームを名指し続けることになります。

帰結。 /app/… は応答しなくなります。リポジトリの外からそこを指すリンクはありませんでした。 スモークスクリプトはリダイレクトに従い、//en/deploy//docs/ で HTML を検証し、AWS のもの はさらに正面ページの目印を検証します。プラグインは Web アプリのロケールルートへのコンテンツリンクと ドキュメントのロケールリンクを見分けられず、それはそうしたリンクが書かれる前にドキュメントプレイ グラウンドのリダイレクト(スプリント 1、ストーリー 2b)が答える問いです。ADR-0051 の /app マウント と ADR-0052 の「ロケールルートは概要画面」は当時の決定の記録として残り、本記録が両方を置き換えます。

見直しの時期 ドキュメントがコンテンツからアプリへリンクする必要が出たとき、または二つ目のホスト 配置(ドキュメントだけの静的ホスト)が現れたとき。その時点で base はビルド引数になります。

ADR-0057 — Web アプリは API が返さなかったものを何も見せない: ブラウザだけのストアは退役し、バックエンドのない画面はナビゲーションで無効化される

Section titled “ADR-0057 — Web アプリは API が返さなかったものを何も見せない: ブラウザだけのストアは退役し、バックエンドのない画面はナビゲーションで無効化される”

ステータス 承認 · バージョン変更なし、ワイヤ変更なし

背景。 ADR-0052 は、どの画面も配線される前に青写真の 9 画面を見られるように、すべての画面が ブラウザに置いたストアの上でサンプルデータを描くことを許し、見直しの契機として最初の画面が実際 のエンドポイントに配線されたときを挙げました。今は 6 つが配線されています。概要と空間の画面はケイ パビリティスナップショットが公開する埋め込み空間を描き、特徴量の画面はインストールが提供するレーン を、エンドポイントの画面は配信されている契約を、デプロイの画面はスナップショットそのものを描き、 プレイグラウンドは POST /v1/encode を実行します。ストアに残っていたのは装う部分でした。プレイ グラウンドの他の 8 機能は固定例で答え、うち 2 つは API にない形をしており、概要の件数とエンドポイ ントは作り物で、データ、ライブラリ、ガイドの画面はストア以外を読んでいませんでした。この週のスプ リントの規則は、見込み客が API の返さなかったものを何も見ないことです。

決定。 indx-mock は削除し、ブラウザストア、サンプルレコード、ダイアログ、トップバーの環境 ピルも一緒に消します。API が答える画面は答える部分だけを残し、API に届かないときはそう言い、代わり に何も見せません。API がまだ答えられない画面(データ、ライブラリ、ガイド)はパッケージもルートも持 ちません。サイドバーの項目は残り、無効化されて「まだ利用できません」のヒントを持つので、青写真は 見えたままで、そこにあるものは何も装いません。プレイグラウンドは選ぶ機能のない一回のエンコード実行 です。バックエンドのなかった機能(few-shot 検出、関係)はアプリを去り、ロードマップに計画中として 残ります。レイヤーは web → screen-* → {live, ux} → ui と一つ短くなり、画面のテストは共有ヘル パーを通さず、パッケージ自身のメッセージで画面をマウントします。選ばなかったもの: 答えのない 3 画面 のためにストアを残すこと。これは誰も使えない 3 画面のためにモックパッケージ、そのレイヤー辺、その 通知を残すことになります。それらのセクションを隠すこと。これは青写真を隠すことになります。

帰結。 ADR-0052 のストアとサンプルデータはなくなります。その記録は当時の決定として残り、本記録 がその部分を置き換えます(ロケールルートの条項は ADR-0056 がすでに置き換えています)。 frontend/scripts/check-boundaries.mjsindx-mock の辺を落とします。無効化されたセクションの 一つにバックエンドができたら、シェルのナビの DISABLED 集合から外し、再びパッケージを与えます。 永続化の決定は ADR-0052 が述べたとおり、ビルド順序の Phase 7 に留まります。

見直しの時期 無効化されたセクションの最初の一つに答えるものができたとき。その時点でそれは他と 同じく indx-live の上の画面パッケージになります。

ADR-0058 — 出力形式はそのグラフを所有するディストリビューションに置き、配信するのはデモホストである

Section titled “ADR-0058 — 出力形式はそのグラフを所有するディストリビューションに置き、配信するのはデモホストである”

状態 承認 · バージョン変更なし、ワイヤー変更なし

背景。 プロダクトのストーリーのステップ 5 は顧客固有の出力形式であり、その最初の実例が .drawio ファイルです。工程系統図パーサーはすでにスライドの記号と接続をページブロックの metadata["process_chart"] に書いており(ADR-0043)、製造の技術者はすでに diagrams.net を 開いています。欠けていたのは書き手だけでした。置き場所は 3 つありえました。同じグラフをすでに 解析している chart.ts の隣で、ブラウザが XML を書く。indx-executor が書き、あらゆる デプロイの性質にする。あるいはグラフを読むケイパビリティのディストリビューションが書く。ただし 最後の案には、どの一次コードもインポートできないパッケージにブラウザがどう届くのか、という問いが 残ります。

決定。 ファイルを書くのは indx_capability_process_chart.drawio.to_drawio() です。 Chart データクラスではなくワイヤーの辞書を受け取ります。ファイルを欲しがる呼び手は API の 向こう側にいて、to_json() がそこに置いたものしか持っていないからです。examples/app2.pyPOST /drawio で配信し、/renders/samples と同じくデモホストのものとしてタグ付けします。 プレイグラウンドのチャートタブは、すでに手元にあるグラフを POST し、返ってきたものを保存します。 契約は何も得ません。openapi.json にはエクスポートの操作も、JSON でないレスポンスも、format フィールドもありません。

この置き方から 3 つが従います。形式はその形式を理解するものの性質なので、書き手は読み手の隣に座り、 第二のチャート方言はエグゼキューターの分岐ではなくそこの第二の関数になります。ワークスペースの境界 は変わりません。examples/tests/unit/test_workspace_boundaries.py が走査する木の外にあり、 これは examples/samples.py がレンダリングのために indx_dxfindx_pdfium に手を伸ばすのに すでに使っている許可と同じものです。そしてファイルは構成上バイト安定です。modified 属性も agent 文字列も圧縮もなく、小数点以下 2 桁なので、tests/fixtures/office-process-chart.drawio をバイト単位で比較でき、書き手のずれは誰かのエディタでの驚きではなく落ちるテストになります。

選ばなかったもの。 ブラウザで XML を書くこと。差分は小さいのですが、エクスポートをデモ UI の 性質にしてしまいます。CLI の利用者、API の利用者、そして第二のフロントエンドはそれぞれ何も得られず、 形式は最後に書かれた表面が決めることになります。

選ばなかったもの。 今週 indx の契約にエクスポート操作を置くこと。ADR-0045 はすでに、レシピ ストアとエクスポート先が状態を求める最初の 2 つであり、その決定はどちらかが作られるときに下す、 先送りはもうしない、と記録しています。デモホストのルート 1 本の背後にある 1 つのファイル形式は、 その操作を形づくる根拠としては足りず、間違ったものを出荷すれば足すより引っ込めるほうが難しくなります。

帰結。 indx serve のデプロイは .drawio ファイルを書きません。グラフは書きたい人のために ワイヤーに乗っており、このリポジトリの書き手はインポート 1 つ先にありますが、レシピの作業が エンドポイントのあるべき姿を述べるまでは、エンドポイントにはなりません。

見直しの時期 第二の出力形式が現れたとき、あるいはレシピストアが着地したとき。その時点で問いは 「この書き手はどこに住むのか」ではなく「デプロイはどの形式を提供すると言うのか」になり、それが ADR-0045 が先送りした操作です。

ADR-0059 — プレイグラウンドの図は Mermaid が描き、ソースはケイパビリティが書く

Section titled “ADR-0059 — プレイグラウンドの図は Mermaid が描き、ソースはケイパビリティが書く”

状態 承認 · バージョン変更なし、ワイヤー変更なし · ADR-0058 の拡張

背景。 チャートタブは工程系統図をテキストで並べるだけでした。ノードごとに役割、ラベル、shape:N。 ADR-0058 のあとはそこにグラフの .drawio ダウンロードが付きました。diagrams.net を開かない読み手には 11 行が見えるだけで、図は頭の中で描くしかありません。ドキュメントのサンドボックスは出荷当初から同じ図を 描いています。スライドへの重ね描きと、手書きのレイアウトによるノードリンク図です。深さ優先探索による 後ろ向きの辺、弱連結成分、始点からの最長路によるレベル。そのサンドボックスはプレイグラウンドを 1 つに したときから到達不能で、スプリント 2 が最初に消すものです。放っておけばその描画がアプリに届くことは ありませんでした。

決定。 タブは Mermaid で図を描き、ソースはグラフを読んだケイパビリティが書きます。 indx_capability_process_chart.mermaid.to_mermaid()to_drawio() の隣に座る理由は ADR-0058 が 述べたとおりです。デモホストの POST /drawiodrawiomermaid を取る POST /chart/{format} になります。1 つのグラフを 2 通りに書くのは、パラメータを持つ 1 本のルートであって 2 本ではありません。 ブラウザはソースを求め、Mermaid に渡し、返ってきたものを表示します。

記号はこの移行を生き延びます。選ぶ前に確かめるべきはそこでした。Mermaid 11.3 以降はノードごとに型付き の形を取り、tricirclediam は工程系統図が描く三角形、円、ひし形そのものです。tri は頂点を -h に置きます。SVG の座標ではこれは上であり、スライドが材料を描くのと同じ向きです。組立は入れ子の 二重三角形で、Mermaid にその形はありません。元になる三角形を太い線で描きます。drawio の書き手が すでにしているのと同じ妥協です。

1 つのダウンロードメニューが単独のボタンを置き換え、項目は 4 つです。2 つはケイパビリティが書く ファイル、.drawio と Mermaid のソースで、後者は保存ではなくコピーします。レンダリングするウィキに 貼るためのものだからです。残る 2 つは画面上の図そのもの、Mermaid が返した SVG と、それをキャンバスに 通した PNG です。この 2 つはエクスポートではなくケイパビリティのものでもありません。この画面が描いた ものの写しなので、ブラウザこそが作るべき場所です。

選ばなかったもの。 サンドボックスのレイアウトをアプリへ移植すること。維持されているライブラリが 描くグラフのために、およそ 260 行のグラフアルゴリズムを二重に抱えることになり、しかも移植は Preact から React への書き直しであって移動ではありません。

選ばなかったもの。 Mermaid のソースをブラウザで組み立てること。20 行で済みますが、どちらかが変わった 瞬間にアプリの図と CLI 利用者が得るものが食い違います。ADR-0058 は .drawio について同じことを退けて おり、2 つ目の形式でその理屈が弱まることはありません。

帰結。 描画はデモホストのルートに依存するので、indx serve のデプロイでは件数とノードの一覧が見え、 絵は見えません。それが正直な姿です。グラフは描きたい人のためにワイヤーに乗っており、このリポジトリの 書き手はインポート 1 つ先にありますが、契約にエクスポートの面はまだありません。ノードの一覧は図に 置き換えられるのではなく図の下に残ります。流れから外れる画像、離れたテキスト、枠を説明するのはその 一覧であり、スクリーンリーダーにとって 1 枚の画像でしかない図の、アクセシブルな姿でもあります。

Mermaid は Web アプリのワークスペースに加わり、プレイグラウンドのルートだけが抱えるよう動的に インポートされます。jsdom では動きません。ドキュメントサイトは図を 1 つ パース するだけで 100 行の DOM シムを必要とします。そこでユニットテストはモックしてソースが渡ることを検証し、Web アプリの Playwright が実ブラウザで描いて、スライド自身のラベルが SVG に無ければ落とします。

見直しの時期 2 つ目の画面がこの図を欲しがったとき、あるいはレシピストアが着地して、問いが 「デプロイはどの形式を提供すると言うのか」になったとき。ADR-0045 が先送りし、ADR-0058 が推測を 断った操作です。

ADR-0060 — セールスページは目標の出力を、そう明示したうえで描いてよい

Section titled “ADR-0060 — セールスページは目標の出力を、そう明示したうえで描いてよい”

状態 承認 · バージョン変更なし、ワイヤー変更なし · ADR-0057 の範囲を狭める

背景。 ロケールのルートには、見出しと 6 つのステップがあり、それぞれに実際の実行のスクリーンショットが 付いていました。正確ではありましたが、indx が何をするかを説明するだけで、見せてはいませんでした。 ランディング案 H(examples/landing-claude/h-routed-by-indx.html)は、それを見せます。サンプルの 3 ページ (厚生労働省の通知の 22 ページとスキャンされた 5 ページ、そして工程系統図のスライド)がステージに並びます。 各ページはまず indx が選んだ経路を示し、ページ上のブロックにカーソルを合わせると、indx が渡すブロックが 表示されます。定義語を伴う段落全体、正規化されたセルを持つ表、図のノードとエッジです。これらは契約が 返すべきものであって、いま返しているものではありません。encode が返すのは行のチャンクで、段落・表・図の 形はそこから手で組み立てたものです。ADR-0057 は、Web アプリは API が返さなかったものを何も見せないと 定めています。

決定。 セールスページを案 H として React に移植します。独自の配色、書体、レイアウトを持ち、ライトと ダークの切り替え、英語と日本語の文言を備えます。ステージが目標の出力を描いてよいのは、次の 3 つの条件を 満たす場合です。

  • ステージ上とページのフッターで、目標の出力であると明示する。
  • ページとその本文はリポジトリのサンプルファイルであり、作った文書ではない。
  • 各ページに示す経路は、ルーターがいまそのページに実際に選ぶ経路である。

ワークスペースの画面にはすべて、引き続き ADR-0057 が適用されます。セールスページはそのひとつでは ありません。シェルなしで描画され(ADR-0056)、ホストの状態を報告するのではなく、製品の価値を示す ページだからです。

選ばなかったもの。 現在の行チャンクをステージに描くこと。正直ではありますが、このページが主張するただひとつの こと、つまりブロックが意味を保つことを見せられません。

選ばなかったもの。 新しいステージの横に 6 枚の実証スクリーンショットを残すこと。あれは別のページのためのもので、残せば話が 2 つになります。 スクリーンショット、e2e/capture.spec.tsjust frontend::capture は削除し、履歴は git に残ります。

帰結。 screen-home[data-screen="home"] に閉じた独自のスタイルシートを持ち、デモのデータは文言の 隣の demo-data.ts に置かれます。出力の形についてのページの主張はロードマップについての主張なので、 計画しているブロックの種類を変えることは、このページを変えることでもあります。

見直しの時期 encode が段落、表、図のブロックを返すようになったとき。そのときステージは同じ 3 ページの実際の実行を描き、この例外は終わります。

ADR-0061 — 本番イメージは LibreOffice を含み、デプロイ先のプレイグラウンドはスライドを描く

Section titled “ADR-0061 — 本番イメージは LibreOffice を含み、デプロイ先のプレイグラウンドはスライドを描く”

状態 承認 · バージョン変更なし、ワイヤー変更なし · ADR-0043 の一文を覆す

背景。 ADR-0043 は、LibreOffice によるスライド描画をデモホストの便宜にとどめ、イメージの依存関係には しないとしました。しかしイメージこそがデモホストです。デプロイ先のプレイグラウンドは、その中で動く examples/app2.py です。そこで PowerPoint をアップロードすると、チャンクは空白のページの上に重なり、 soffice が PATH にあるノートパソコンでは同じデッキの背後にスライドが表示されました。工程系統図の説明に 使う形式で、デプロイ先のプレイグラウンドが壊れているように見えていました。

決定。 ランタイムのステージは libreoffice-impress をインストールし、examples/samples.py は ノートパソコンと同じく PATH 上の soffice を見つけます。デッキは作成者の手元にあったフォントを指定しますが、 Linux のホストにはそれがないため、イメージは代わりになるフォントも含みます。游ゴシック、メイリオ、MS ゴシックには Noto CJK を使い、図面の描画が使っていた IPA フォント一つを置き換えます。Carlito、Caladea、Liberation は Calibri、Cambria、Arial、Times New Roman、Courier New と字幅が同じなので、行は PowerPoint と同じ位置で折り返します。それ以外は変わりません。 変換はデモホストの /renders ルートの背後にあり、一度に一つずつ、それぞれ専用のプロファイルディレクトリで 行われ、LibreOffice が変換できないデッキは画像のないページに戻ります。

却下。 DrawingML の走査からスライドを描くこと。indx_ooxml.drawingml は図形の位置を求めますが、 テーマの塗り、画像、グラデーション、テキストのレイアウトは描きません。部分的な描画では、ファイルにある スライドとは違うスライドを見せることになります。

帰結。 apt のレイヤーは約 473 MB です(linux/amd64 で計測)。API が LibreOffice を呼ぶことは今もありません。呼ぶのは デモホストのページ画像だけなので、indx serve と契約は変わらず、別のイメージからビルドしたデプロイでは スライド画像だけが失われます。

見直しの時期 イメージのサイズやコールドスタートがターゲットのコストになったとき。その時点で変換は、 デモホストが呼ぶサイドカーか描画ジョブに移ります。

ADR-0062 — ワークシートは 1 枚の画像として描き、Word 文書はセクションが 1 つのときだけ描く

Section titled “ADR-0062 — ワークシートは 1 枚の画像として描き、Word 文書はセクションが 1 つのときだけ描く”

状態 承認 · バージョン変更なし、ワイヤー変更なし · ADR-0061 を拡張する

背景。 ADR-0061 のあと、デプロイ先のプレイグラウンドはデッキのスライドを描くようになりましたが、 ワークブックと Word 文書には画像がないままでした。LibreOffice はどちらも変換できますが、そのページは 印刷ページです。indx が数えるページは形式が定める区切り(ADR-0043 のオブザーバー)で、ワークシートと Word のセクションです。ワークシートはセルに必要なだけの用紙にまたがって印刷され、Word のセクション (多くは文書全体)はレイアウトに必要なだけのページになります。印刷ページごとに画像を出すと、 2 枚目以降の画像はすべて違うページに付きます。

決定。 デモホストはワークブックを LibreOffice の SinglePageSheets で書き出し、ワークシートは どれだけ広くても PDF の 1 ページになります。セクションが 1 つの Word 文書は 1 ページとして描き、 その印刷ページの先頭 20 ページを縦に並べます。セクションが複数ある文書は描きません。LibreOffice は セクションのページがどこから始まるかを教えないためです。印刷ページの数が indx の数えるページと 合わない Office ファイルは描きません。書き出しは非表示のスライドやシートを省くので、それを含む デッキやワークブックも同じ扱いです。

却下。 indx のページの隣に、印刷ページを別の列として並べること。プレイグラウンドは indx の ページを描きます。隣に別のページ割りを置けば、その食い違いを読み手が解くことになります。

帰結。 イメージに libreoffice-calclibreoffice-writer が加わり、apt のレイヤーは約 473 MB から 573 MB になります。長い Word 文書の画像は 縦に長く(96 DPI でテキスト 20 ページなら約 21,000 px、数メガバイト)、Word のチャンクは矩形を 持たないので、画像は矩形の背景ではなくチャンク一覧の上の絵になります。

見直しの時期 Word のリーダーがテキストの位置を求めるようになったとき、またはセクションが複数ある 文書がサンプルに入ったとき。その時点でセクションの境界はファイルではなくレイアウトから求めます。

ADR-0063 — リクエスト側は許可リストとフォールバック禁止フラグでリーダーを固定する

Section titled “ADR-0063 — リクエスト側は許可リストとフォールバック禁止フラグでリーダーを固定する”

状態 承認 · POLICY_VERSION は動かない。RoutingConstraints に 2 つのフィールドが加わるため、openapi.json と生成クライアントは動く · ADR-0050 の 3 つの追加の 1 つ目

背景。 ADR-0050 の時点で、リクエストが名指しできないコンポーネントはリーダーだけでした。 選ぶのはラダーで、5 つの制約はその選択を形づくるだけでした。スプリント 1 の混在ページのサンプルは、 1 つの文書が 2 つのリーダーにルーティングされることを示しました。残った問いは、呼び出し側がどちらを使うかをどう指定するかです。

決定。 RoutingConstraints に 2 つのフィールドを加え、どちらも計画時に適用します。capability_ids は許可リストです。空ならこれまでどおりラダーが決めます。空でなければ、リストにない機能は、 デバイスや予算の制約と同じ場所で拒否されます。対象は指名されたパーサー、すべての段、manual-review で、区別はありません。許可された ID の順位はラダーが付け、許可リスト自体の順序には意味がありません。 fallbacks_allowed=false は、各ページで選ばれた候補だけを残します。1 件だけの許可リストとフォールバック 禁止を組み合わせると、ページごとに 1 つのリーダーを名指しする計画になります。許可リストのせいで何も残らないページは capability_ids: no capability is eligible for N of M pages として、スナップショットにない ID は capability_ids: <id> is not in capability snapshot <id> として返ります。どちらも 422 ではなく unsatisfied な計画で、未知の埋め込み空間と同じ扱いです。

却下。 バージョンの引き上げ。0.9.0 が受け付けたリクエストは、どれもバイト単位で同じ計画になります。 どちらかのフィールドを含むリクエストは以前は 422 でした。つまり 1 つのバージョンで 2 通りの計画が出る入力はなく、 引き上げが防ぐべき状況は起きません。引き上げればスナップショット ID が動き、どの決定も変わっていないのに ベンチマークを固定し直すことになります。ページ単位の固定も却下しました。capability_ids は文書全体に効き、 リストにあるどの ID も読めないページは、別のリーダーに回されずに拒否されます。

帰結。 実行側は変わりません。selected を実行し、次に fallbacks を実行します。残りがなければ、 読み取りの失敗でルートが尽き、ページとリーダーを名指しする既存の 503 になります。この 503 はこれまで、終端フォールバックを欠いたインストールだけを意味していました。今後は、フォールバックを禁止した 呼び出し側も意味しえます。CLI と Web アプリは、まだどちらのフィールドも公開していません。

見直しの時期 1 つの文書のページごとに別々のリーダーが必要な呼び出し側が現れたとき (許可リストがスコープ単位になる)、 または ADR-0050 の残り 2 つの追加が出荷されたとき。

ADR-0064 — Azure はマネージド証明書で自分のホスト名を直接配信し、その Cloudflare レコードは DNS のみ

Section titled “ADR-0064 — Azure はマネージド証明書で自分のホスト名を直接配信し、その Cloudflare レコードは DNS のみ”

状態 承認 · バージョン変更なし、ワイヤー変更なし

背景。 Azure のワークフローは、そのホスト名が名指しされた時点でまだ一度も起動されていませんでした。 その名前は everything.az.indx.jp で、GCP の everything.g.indx.jp と同じく、ゾーンから 2 ラベル 下です。Cloudflare の Universal 証明書が覆うのは indx.jp*.indx.jp までで、それより深い ところは覆いません。それは何かを適用する前にエッジで確かめました。サーバー名 az.indx.jp の ハンドシェイクはゾーンの証明書を受け取り、everything.az.indx.jp のハンドシェイクはアラートを 受け取ります。ADR-0053 と ADR-0055 がスタックに与えたプロキシされた形でその名前を配信するには、 ゾーンに有料機能の Advanced Certificate Manager が要り、さらにオリジンの Origin CA 証明書のために SSL and Certificates / Edit を持つ Cloudflare のトークンも要りました。運用者はその両方を 断りました。トークンは AWS のものと同じく、DNS を編集するだけで、ほかは何も編集しないべきだからです。

決定。 レコードは DNS のみにして手で作り、オリジンを公開ホストにします。運用者がアプリの FQDN を指すプロキシしない CNAME と asuid の TXT をダッシュボードで作り、スタックは DNS のプロバイダーを 何も知りません。イングレスは許可リストを持たず、証明書は Azure のマネージド証明書です。Container Apps がそれを発行し更新するのは、まさにその形に対してだけです。アプリを直接解決する CNAME と、 DigiCert が HTTP で到達できるアプリです。Origin CA 証明書、tlscloudflare のプロバイダー、 環境の証明書、カスタムドメインのリソース、IPv4 のレンジはスタックから去ります。プロバイダーに マネージド証明書のリソースがなく、レコードは Terraform のものではないからです。ワークフローの deploy は、リビジョンが healthy になった時点で az containerapp hostname bind --validation-method CNAME を走らせます。それがホスト名を加え、証明書を見つけるか発行し、以後は 何もしません。その前に 2 つのレコードをアプリの FQDN と検証 ID に対して解決し、違っていれば両方の レコードをジョブの要約に印字して失敗します。それが最初のデプロイの終わり方です。値はアプリが できて初めて存在するからです。公開スモークは、オリジンが直接のクライアントに拒否ではなく 200 を返すことを期待します。スクリプトは refused403 の隣にその 3 つ目の期待を得て、GCP の 経路は変わりません。選ばなかったもの。Advanced Certificate Manager とより広いトークンは、月額の 料金と 2 つ目の権限と引き換えに境界を保ちます。1 ラベルのホスト名はエッジの証明書を保ちますが、 それでも Origin CA のトークンが要ります。DNS / Edit だけを持つトークンは、スタックにレコードを 書かせはしますが、2 つのレコードのために運用者が持ちたくなかった資格情報です。ホスト名ごとの Full の暗号化モードの下に置く自己署名のオリジン証明書は、代わりにゾーンの設定の権限が要り、 どのスタックも所有しないダッシュボードの設定に縛られます。

帰結。 Azure に境界はありません。API は認証なしで(ADR-0039)誰からでも到達でき、DNS のみの レコードでは Cloudflare Access が使えないので、認証は、来るとすればアプリか Azure のものです。 Cloudflare の資格情報は持たず、ステートに秘密は入りません。最初のデプロイは 2 サイクルで、 1 回目がレコードを印字し、2 回目が束ねます。以後のどのデプロイも何も束ねません。 コールドスタートはプロキシではなくイングレスで失敗します。Container App の名前は <project>-app です。それまでの <project>-indx-everything は 36 文字で、Container Apps の上限の 32 文字を 超えるからで、それは最初のローカルの plan でわかりました。ワークフローはその名前と、環境と リソースグループの名前を、導き出すのではなく Terraform の出力から読みます。環境は Consumption の ワークロードプロファイルを宣言します。最初のデプロイの 400 が、Consumption だけの環境は 2 vCPU と 4 GiB で止まり、プロファイルなら 4 と 8 まで行ってなおゼロにスケールすると告げたからです。レコードはプロキシ しないまま、イングレスは開いたままでなければなりません。そうでなければ次の更新が、変更から数か月 後に失敗します。ADR-0055 の Azure の半分にはこれが取って代わります。GCP も同じ 2 ラベルの ホスト名を持ち、いまもプロキシの背後にいます。ここでは触れません。

見直しの時期 Access や WAF のために、ホスト名の前にもう一度 Cloudflare が必要になったとき。 その時点でゾーンには Advanced Certificate Manager が要り、スタックはオリジン証明書を取り戻します。 あるいは Container Apps がマネージド証明書を DNS レコードで証明するようになったとき。