技術ドキュメントやブログ記事に掲載されるコード抜粋やコマンド実行ログは、執筆者や AI がコピー&ペーストする過程で容易に改変され、エスケープの二重化や正しくないコードが紛れ込みます。ドキュメントの品質を保とうと人間が目視で確認したり、AI に「コードを改変するな」とプロンプトで指示したりしても、転記ミスやエッジケースの変形を完全に防ぐことはできません。

コード抜粋の一次資料照合(Code Excerpt Provenance Verification) とは、ドキュメント内のコードブロックやコマンド出力の各行が、参照元のソースファイル・git diff・実行トランスクリプトに逐語で実在するかを決定論的に検査する仕組みです。

当リポジトリの執筆パイプラインでは、過去にある記事の下書きで Perl ブロック 66 行中 47 行が一次資料に存在しない「完全な姿を書こうとして作られたコード」が見つかり、さらに出どころのない実行結果の削除や記録と食い違う記述の遡及修正(公開 9 本+未公開 1 本の計 10 本)を行った経験から、行単位の照合リンター check-output-provenance.mjs を導入・強化してきました。これは 前作の tsc 非TTY 出力検証記事 の結論部で触れた、ドキュメントの出力検証を機械化する課題意識をさらに発展させた実装編に位置づけられます。

しかし、この検査器を強化していく過程で、私たちは 3 つの技術的境界条件に直面しました。安易な置換処理では解決できない JSON 文字列のエスケープ仕様、git diff が持つ行頭記号の非対称性、そしてプログラミング言語に普遍的に現れる閉じ括弧 } の連続です。これらに対処した結果、公開済みだった CDP タイムアウト記事 において .split('\\n') や .replace(/\\d+/g, ...) のようにバックスラッシュが二重化していた事故(各 5 行)を事後検出し、実在する閉じ括弧 ); } } } の誤検出を「連続する記号行の最大塊と前後の非記号行との連結照合」によって解消しました。

さらに、この検査スクリプト自体の改修において、私たちは「人間がコードを 1 行ずつ直接レビューするのを諦める」という運用方針を選択しました。決定論的リンターの限界を補い、AI コーディングにおける人間のレビューのボトルネックを解消するための「AI によるレビュー・検収ハーネス」の実践と設計思想について共有します。

なぜ「埋め込み」や「実行テスト」ではなく「事後的な行照合」なのか:実務での選択理由

ドキュメント内のコードの正確性を担保する代表的なアプローチには、外部ファイルからコードを同期・挿入する「コード埋め込み型」と、ドキュメント内のコードを実際に実行して出力を検証する「ドキュメント実行テスト型」が存在します。

しかし、技術文書や検証記事が論拠とする一次資料には、単体の独立したプログラムファイルだけでなく、AI エージェントの実行トランスクリプト(JSONL ログ)や、開発中の変更差分(git diff)、コミット履歴、巨大なソースコードからの部分的な抜粋が多数含まれます。これらの一次資料は、外部ツールで直接埋め込むことも、単体でスクリプトとして実行することもできません。

一次資料が独立したソースコードであれば既存の埋め込みや実行テストで担保できますが、実行不可能なトランスクリプトや差分ログからの抜粋を検収するには、事後的な逐語照合が必要になります。

アプローチ対象となる主な一次資料途中抜粋・差分の扱い外部実行環境の依存主な代表例・ツール
コード埋め込み型独立したソースファイル開始・終了行やマーカーで指定不要(ファイル読み込みのみ)Sphinx literalinclude 1, MarkdownSnippets 2
ドキュメント実行テスト型完全なコード例・対話セッション単体で完結した実行可能例に限定必須(処理系・依存パッケージ)doctest 3, pytest doctest 4, rustdoc doctest 5
事後的な行単位照合(本方式)ログ・JSONL・git diff・ソース任意の連続行・途中抜粋を許容不要(静的な文字列集合照合)当リポジトリの check-output-provenance.mjs

会話ログやパッチ差分のように実行不可能な一次資料を根拠として扱うドキュメントでは、コードが正しく動くかではなく「記録された事実から勝手に改変されていないか」を判定する検査器が求められます。

Sphinx の literalinclude ディレクティブは、外部のソースファイルから行番号やマーカーを指定してドキュメントにコードを取り込む強力な仕組みです 1。また MarkdownSnippets のように、ソースコード内のマーカーと Markdown 内のブロックをビルド時に自動同期するツールも広く利用されています 2。しかし、これらは「ソースコードが正本であり、ドキュメント側が生成物である」という方向のツールです。私たちが検証したいのは、すでに執筆された記事内のコード抜粋が、手元の作業ログやコミット差分と一致しているかという「事後的な正当性」です。

一方、Python の doctest 3 や pytest の doctest 機能 4、Rust の rustdoc --test 5 などのドキュメントテストツールは、ドキュメント内のコード例を抽出して実際に実行し、期待される出力と照合します。これらは API ドキュメントの動作保証には最適ですが、実行環境のセットアップや依存関係の解決が必要であり、そもそも部分的な抜粋や途中の差分、JSONL ログを単体で実行することは不可能です。

そのため、すでに書かれたドキュメントのコードブロックに対して、指定された一次資料の行集合と逐語で一致しているかを決定論的に検査する「行単位の逐語照合」という第三のアプローチが必要になりました。具体的には、ドキュメント側のコードブロック直前に 引用元: <一次資料の相対パス> というアノテーションを記述する規約とし、リンターがこれを検知して対象ファイルをロードし、ブロック内の各行と突き合わせるパイプラインを構成しました。

境界条件との格闘:JSON文字列デコード、diff接頭辞、そして閉じ括弧の連続塊

行単位の照合リンターを実務に配備した際、私たちは 3 つの技術的境界条件に直面しました。それぞれが、文字列処理における特有の罠を持っています。

境界1: JSONL トランスクリプトの文字列値デコードと二重エスケープ事故

AI エージェントの実行ログ(JSONL トランスクリプト)からコード行を照合しようとすると、JSON の文字列エスケープが大きな壁になります。

ログファイル全体に対して安易に \n を改行文字に一括置換しようとすると、2 つの問題が生じます。第 1 に、JSON のキーや構造("text": " など)が行頭に残ってしまい、コードの 1 行目と一致しなくなります。第 2 に、コードブロック内に書かれた文字列リテラル '\\n' まで改行に化けてしまい、実在するコード行が分断されて未照合エラーを引き起こします。

RFC 8259 §7 では、JSON 文字列内でバックスラッシュ(reverse solidus)や改行文字などの制御文字をエスケープすることが義務付けられています 6。エージェントの会話ログやツール実行ログは JSON スキーマが多様で入れ子も深いため、特定のスキーマやフィールド構造に依存せず、あらゆる JSON 文字列値から出力テキスト行を漏れなく抽出・インデックス化する目的で、正規表現による文字列トークン抽出を採用しました。トランスクリプト全体を単純置換するのではなく、正規表現で JSON 文字列値のトークンのみを抽出し、JSON.parse を通して正しくアンエスケープしてから照合用の行集合(インデックス)に追加する設計です。

以下のコードは、トランスクリプト内の JSON 構文を崩さず、文字列値の中身だけを安全にデコードして照合対象の行集合へ展開する処理の実装抜粋です。

引用元: 当リポジトリの check-output-provenance.mjs

    // transcript の JSON 文字列の値だけを decode して、その中の改行で行を分ける(値の外の `"text": "` や `\\n` は崩さない)
    for (const m of text.matchAll(/"(?:[^"\\\n]|\\.)*"/g)) {
      let v;
      try { v = JSON.parse(m[0]); } catch { continue; }
      if (typeof v === 'string') addSeq(v);
    }

この処理では、text.matchAll でダブルクォートで囲まれた文字列トークンを漏れなく走査し、JSON.parse を通すことで、\" や \\、\n を言語仕様どおりに復元しています。パースに失敗した不正な構文は continue でスキップし、文字列値以外の型は除外することで、堅牢性を保っています。

この改修を適用した際、予期せぬ成果が得られました。すでに公開されていた CDP タイムアウト記事 において、コードブロック内の .split('\\n') や .replace(/\\d+/g, ...) のようにバックスラッシュが \\n や \\d と二重エスケープされていた問題(2 つの引用元付きブロックで各 5 行、計 10 行)を事後検出し、一次資料の一重バックスラッシュに合わせて正しく訂正できたのです。JavaScript において '\\n' は改行ではなくバックスラッシュと文字 n の 2 文字を表すため、コードの意味そのものが変わってしまう重大なバグでした。

境界2: unified diff の非対称性と1段限定の接頭辞除去

一次資料が git diff(unified diff)である場合、変更行には特有の接頭辞が付与されます 7。追加行の先頭には +、削除行には -、そしてコンテキスト行には空白が配置されます。

このとき、照合を成功させようとして、抜粋側(ドキュメント側)と一次資料側の双方で行頭の + や - を無条件に除去してしまうと、大きな問題が発生します。ドキュメントに書かれた正当な単項演算子 -1 や +x、あるいは Markdown のリスト記号 - item まで接頭辞と誤認されて剥がれてしまいます。その結果、本来検出して止めるべき架空のコード行が見逃され、意図しない一致を招くことになります。

そこで、接頭辞の除去は「一次資料側のみ」に適用し、ドキュメント側のコード行は一切改変しない非対称な照合方式を採用しました。

さらに、diff の追加行 + と Markdown のリスト記号 - が重なる +- item のような行に対して、正規表現で繰り返し除去(/^(?:[+\->]\s?)+/)をかけると、実在する - まで消えてしまう回帰が発生しました。そのため、一次資料側の接頭辞除去は先頭の「1 段(1 文字)のみ」(/^[+\->]\s?/)に限定する実装に倒しています。

境界3: 閉じ括弧 } のすり抜け防止と } 3連の偽陽性解消(塊連結照合)

多くのプログラミング言語では、} や ); のような記号だけの行が頻繁に現れます。

これらの記号行を 1 行単独で照合可能にしてしまうと、執筆者や AI が創作した架空のコードブロック末尾の } が、一次資料内の全く無関係な } と偶然一致して素通りしてしまいます。

そこで当初は、「記号行は単独での一致を禁止し、前後の非記号行と 2 行組を作って照合する(記号行どうしの組は不許可)」というルールを導入しました。

しかし、この制約はすぐに別の偽陽性を引き起こしました。長期シミュレーションテスト記事 に掲載された実在する Rust コードの閉じ括弧連続 ); } } } において、中間の } が前後のどちらとも非記号行との組を作れず、全ドキュメントの検証走査で未照合エラー(exit 1)になってしまったのです。

解決策として、連続する記号行の最大の並びを 1 つの「塊(run)」として捉え、「記号行の最大塊全体を、その直前または直後の非記号行と連結した文字列」が一次資料に実在するかを検査する 塊連結照合(symRuns) へと設計を変更しました。

以下のコードは、連続する記号行を最大の塊として抽出し、前後の非記号行と連結してインデックス化するロジックの実装抜粋です。

引用元: 当リポジトリの check-output-provenance.mjs

// 記号行の塊 = 連続する記号だけの行の最大の並び。塊は行ごとに分けず、`}\n}` のように1つの文字列として扱う
function symRuns(seq, fn) {
  for (let a = 0; a < seq.length; a++) {
    if (!isSym(seq[a]) || (a > 0 && isSym(seq[a - 1]))) continue;
    let b = a;
    while (b + 1 < seq.length && isSym(seq[b + 1])) b++;
    fn(seq.slice(a, b + 1).join('\n'), a > 0 ? seq[a - 1] : null, b + 1 < seq.length ? seq[b + 1] : null);
  }
}

この symRuns 関数は、記号行の開始位置 a から終了位置 b までの最大連続範囲を特定し、塊全体を \n で結合した上で、直前の非記号行 prev または直後の非記号行 next と組にしてコールバックに渡します。

以下のテストコードは、創作された記号行の連続が拒否され、実在する記号行のみが正しく通過することを保証しているテストケースの抜粋です。

引用元: 当リポジトリの check-output-provenance.test.mjs

fs.writeFileSync(`${dir}/hands-on-log.md`, 'print 1;\n}\nother\n}\n}\n');
check('本物の行 + 創作の `}` `}` は exit 1', exitOf('引用元: hands-on-log.md\n\n```perl\nprint 1;\n}\n}\n```\n').status, 1);
check('本物の行 + 実在の `}` 1個は exit 0', exitOf('引用元: hands-on-log.md\n\n```perl\nprint 1;\n}\n```\n').status, 0);

一次資料に print 1; と } の組しか存在しない場合、下書き側で勝手に } を 2 つ並べた創作ブロックは exit 1 でブロックされます。一方で、実在する } 1 個との組み合わせは exit 0 で通過します。この塊連結照合の導入により、公開記事と下書きの全 108 組において、偽陽性 0 件での通過を確認できました。

メリットの裏にあるトレードオフと適用限界

行単位の決定論的照合によってコード抜粋の改変を確実に防止できるようになりましたが、このアプローチには固有の限界とトレードオフが存在します。

第 1 に、決定論的なプログラムは融通が利きません。塊連結照合を導入したことで実在する閉じ括弧連続の偽陽性は解消されましたが、「連続する記号行の途中だけを切り取って抜粋した場合」は照合から外れるという制約が残ります。例えば、一次資料に ); } } } という 4 行の塊があるとき、説明の都合で最後の } 1 行だけを省略して抜粋すると、塊が最大長と一致しないため未照合エラーになります。プログラムによる検収は融通が利かないため、幅広く「問題ない」と柔軟に判定させることは極めて困難です。

第 2 に、予防と検収の役割分担です。AI や執筆者に対して「コードを改変するな」「完全な姿を創作するな」と否定形のプロンプト指示で予防しようとしても、指示の肥大化によるコンテキスト汚染を招くだけでなく、人間が事前に想像できた失敗モードしか防ぐことができません。

ミスをする原因の空間は無限ですが、守るべき結果の空間(記事中のコード行が一次資料に実在するか)は有限です。したがって、予防のプロンプトを際限なく増やすのではなく、守るべき性質を決定論的な検収ゲートとして機械化するアプローチが有効になります。ただし、決定論的プログラムだけで「抜粋されたコードが記事の文脈において適切か」「意図通りの説明になっているか」といった文脈の妥当性まで判断することはできません。

これはドキュメントの品質担保に限った話ではありません。プロンプトによる事前予防(性善説)の限界を、決定論的ゲートと実データ全件走査による検収ハーネス(外堀)で補うという設計思想は、AI エージェント自身にコードを書かせる開発プロセスそのものにもそのまま当てはまります。

人間がレビューするのを諦める:AI によるレビュー・検収ハーネスの設計

ここで最大の課題が浮上します。検査スクリプト check-output-provenance.mjs を開発・改修する際、コードレビューを誰が担うのかという問題です。

この検査スクリプト自体も AI エージェントが実装しており、人間が修正しても AI が修正してもバグは発生します。AI にコーディングを任せると、すぐに「人間がコードをレビューすること」自体が開発のボトルネックになります。人間が AI の書いたコードを 1 行ずつ直接レビューすることを諦めない限り、AI による開発生産性を根本から引き上げることはできません。

AI による生産性向上が目的なら、レビュー工程も AI に任せる必要があります。人間はコードそのものを読むのではなく、「何をどうレビューするか」というレビューの方法(ハーネス)を設計・レビューすることで品質をコントロールすべきです。

当リポジトリでは、同一の作業環境内で「司令塔エージェント」と「実装ワーカーエージェント」の 2 端末を連携させ、規約準拠を検証する Standards 軸と、仕様・実データ全件走査を検証する Spec 軸の並列サブエージェントによってコードレビューを自動実行するハーネスを運用しています。

実際の改修タスクにおいて、この AI レビュー・検収ハーネスは、人間がコードを 1 行も読む前に以下の境界バグを自律的に検出し、修正を完了させました。

  1. JSON デコードと diff 接頭辞除去の改修タスク: 1 往復目のレビューにおいて、Spec 軸サブエージェントが既存の下書き全 109 本を実走検査し、トランスクリプト全体の改行置換による誤検出や、下書き側への接頭辞除去適用バグを検出しました。続く 2 往復目の再レビューでは、全下書きの再走査により、+- の繰り返し除去が引き起こした回帰を検出しました。これらはすべて人間が介入することなく、AI 同士のレビュー往復によって修正されました。
  2. 統計集計スクリプトの改修タスク: 実装ワーカーが初回実装を行った後、Spec 軸のレビューサブエージェントが全 196 ファイルの実走検査を実行し、番号付きリスト(1. / 2.)の指摘が数え落とされているバグを検出しました。差し戻しを受けたワーカーが正規表現を修正し、人間が目を通すことなく検収を通過しました。

この一連の改修過程で、人間が介入したのはたった 1 回だけでした。閉じ括弧 3 連の Rust コードが落ちた際、AI が作業を自律停止し、「案a: 記号行を最大の塊として前後の非記号行と連結して照合」「案b: 3 行以上の連続のみ逐語照合」「案c: 記事側の抜粋を修正」という 3 つの設計案を提示してきた場面です。人間はコードを読むのではなく、方針として「案a」を 1 行で選択しただけでした。

AI に何をさせたいのか(目的や求める精度)が明確になっていれば、人間が直接コードレビューする必要性があるかどうかは自然に決まります。実データ全件走査や自動テストといった客観的検証ハーネスが網羅的に機能している領域においては、よほどクリティカルなコアロジックでない限り、人間による目視検収は不要であり、私たちは「人の手を介さずに正しく修正される確率をどう高めていくか」というハーネスの設計に注力すべきです。

前提が変われば結論はどう変わるか:決定論的検収と AI レビューの将来展望

では、どのような前提が変われば、この「事後的な行単位照合」や「決定論的検収+AI レビューハーネス」というアプローチの有効性は変化するのでしょうか。

第 1 に、コード生成モデルやエージェントの推論精度がどれほど向上したとしても、逐語一致や構文規則といった「白黒が明確な性質の検証」において、決定論的プログラムの優位性が揺らぐことは考えにくいでしょう。LLM の出力トークンは入力に比べてコストが高く、どれほどフロンティアモデルが進化したとしても、決定論的なスクリプトによる文字列照合のほうが圧倒的に高速かつ安価で、100% の再現性を保証できます。

第 2 に、AI による自律レビュー・検収ハーネスが成立するための最大の前提条件は、「既存コードベースのアーキテクチャとテスト環境が十分に整っていること」です。本稿で紹介した自律検収が機能したのは、明確なコーディング規約(Standards 軸)と、過去の全下書き実走という検証手段(Spec 軸)が整備されていたからです。構造化されていないバイブコーディングのようなコードベースでは、AI によるレビュー精度そのものが不安定になり、依然として人間による直接の目視レビューを手放せない可能性があります。

第 3 に、将来的にエージェントが一時的なコンテナや実行サンドボックスをオーバーヘッドなく自律構築できるようになれば、事後的な行照合の一部は、ドキュメント実行テスト(doctest 型)とのハイブリッドへ移行していく余地があります。しかし、本稿で扱った会話トランスクリプトや開発中の git diff のように「そもそも単体実行が不可能な一次記録」を論拠とする限り、事後的な行単位照合という決定論的ゲートは、ドキュメントの誠実性を支える基盤として残り続けるはずです。

よくある質問(FAQ)

Q. 既存のコード埋め込みツールを使えば、事後的な行照合スクリプトは不要になりませんか?

いいえ、不要にはなりません。Sphinx の literalinclude や MarkdownSnippets などのコード埋め込みツールは、独立して存在する単一のソースコードファイルからコードを取り込む用途には適しています。しかし、技術ドキュメントが論拠とする一次資料には、AI エージェントの実行ログ(JSONL トランスクリプト)や、開発作業中の git diff、コミット履歴といった「直接ファイルとして埋め込むことが難しい生データ」が多数存在します。そうした多種多様な一次記録からの抜粋が、記録された事実から勝手に改変されていないかを保証するには、事後的な行単位の照合スクリプトが不可欠です。

Q. unified diff の接頭辞除去において、なぜ下書き側の + や - を除去してはいけないのですか?

下書き側のコードに含まれる正当な演算子や記号を破壊してしまうためです。もし下書き側の行頭にある + や - を無条件に除去してしまうと、プログラム内の -1 や +x といった単項演算子、あるいは Markdown のリスト記号 - item まで接頭辞と誤認されて剥がされてしまいます。その結果、一次資料の実際のコードと異なる文字列として比較され、本来検出して止めるべき架空のコードが見逃され、意図しない一致を招くためです。接頭辞の除去は一次資料側だけに限定し、下書き側の字面はそのまま保持しなければなりません。

Q. 検査スクリプト自体も AI に書かせる場合、人間がコードを直接レビューしなくて本当に品質は担保できるのですか?

単純に放置するのではなく、「実データ全件走査」を含む検証ハーネスを組むことで、確度を大きく高められます。人間が数十行のコードを目視でレビューしても、特定の正規表現エスケープや記号行の境界条件を見落としがちです。一方で、AI レビュアーに既存のドキュメント全件を実走させ、回帰テストを実行させるハーネスを与えれば、エッジケースの不一致を機械的に検出し、人間が目を通す前に修正させることができます。人間はコードの字面を読むのではなく、検証ハーネスの網羅性と設計方針の選択に集中します。

まとめ

  • ドキュメント内のコード抜粋が一次資料に実在するかを担保するには、埋め込みや実行テストだけでなく、事後的な行単位の逐語照合が有効である。
  • 行単位照合の実装では、JSON 文字列値の正規デコード、一次資料側限定の 1 段 diff 接頭辞除去、および記号行の最大塊連結照合という 3 つの境界条件を押さえる必要がある。
  • 閉じ括弧 } のような記号行は単独一致を禁止し、連続する記号行の塊全体を前後の非記号行と連結して照合することで、架空コードのすり抜けと実在コードの偽陽性を両立して防げる。
  • AI コーディングにおいて人間がコードを直接レビューすることはボトルネックになりやすく、生産性を高めるには人間がコードそのものではなく「レビューの方法(ハーネス)」を設計し、検収自体も AI に委ねる方針への転換が不可欠である。

AI エージェントを活用した開発プロセスの標準化、ドキュメント品質ゲートの構築、あるいは AI レビュー・検収ハーネスの導入設計について、ぜひ コンタクトフォーム よりお気軽にご相談ください。

Footnotes

  1. reStructuredText Directives: literalinclude Sphinx Documentation. 外部プレーンテキストファイルをドキュメントに取り込む :lines: マーカー機能。 ↩ ↩2

  2. MarkdownSnippets README Simon Cropp, GitHub repository. begin-snippet: と end-snippet マーカーにより外部ソースから Markdown へコードスニペットを自動同期する仕組み。 ↩ ↩2

  3. doctest — Test interactive Python examples Python Software Foundation. Python 対話セッション風のドキュメント例を抽出し実行・照合する仕組み。 ↩ ↩2

  4. How to run doctests pytest documentation. pytest における doctest 実行、--doctest-modules、空白正規化オプション等の仕組み。 ↩ ↩2

  5. The Rustdoc Book: Documentation tests Rust Documentation. rustdoc --test によるコード例のコンパイル・実行検証の仕組み。 ↩ ↩2

  6. RFC 8259: The JavaScript Object Notation (JSON) Data Interchange Format IETF (T. Bray, Ed.), December 2017. §7 文字列エスケープ構文、§8.3 文字列比較。 ↩

  7. Git - diff-format Documentation Git Documentation. unified diff の行頭記号(+, -, 空白)の意味。 ↩