レポートには4行が出力されますが、設定ファイルには limit: 6 とあります。その理由を調べるよう頼まれました。このリポジトリを見るのは初めてです。CLI、設定ローダー、いくつかのサービス、テストのフォルダーがあります。全部読めばいつかは理由が分かるでしょう。でも、行数を決める一つの値を追うほうがずっと早く分かります。

問いを絞る

「レポートモジュールを理解する」には終わりがありません。具体的に答えられる問いには、終わりがあります。

設定は limit: 6、環境変数は REPORT_LIMIT=4 です。レポートには4行が出力されます。どちらが優先されたのでしょうか。また、REPORT_LIMIT が空文字列ならどうなるでしょうか。

実行したコマンド、ディレクトリ、コミットを記録します。また、入力に4件を超えるレコードがあることも確かめてください。元データが4行しかなければ、4行返されたという事実から上限については何も分かりません。

手元の手掛かりから読み始めるファイルを探す

最初に役立つファイルは、上限を選ぶ関数ではなく、起動処理、フォーマッター、テストかもしれません。再現できる手掛かりから目的の関数にたどり着き、経路のどの接続を実際に確かめたかを記録しましょう。

レポートの入口を調べる実習をダウンロードし、展開して report-entry-lab に移動します。この小さなプロジェクトは本チュートリアルのために作られました。後述するのと同じ優先順位の規則に、実際のコマンド、ディスパッチテーブル、ローカルのフィクスチャローダーを加えています。Node.js 22以降、npm、ripgrep(rg)が必要です。依存関係のインストールやモデルのアカウントは不要です。以下のシェルコマンドはmacOSとLinuxで動作します。

REPORT_LIMIT=4 npm run --silent report
Report rows: 4
Aster
Birch
Clover
Dahlia

設定フィクスチャには limit: 6、データフィクスチャには8行が含まれます。最初の行はヘッダーで、その後に4行のデータが続きます。これで、不一致が短い入力ファイルによるものではないと分かります。プログラムは同梱のローカルフィクスチャだけを読み、ターミナルに出力します。テストはローカルのNodeプロセスを起動しますが、ネットワークリクエストやファイルへの書き込みは行いません。

手元の手掛かり 読み始める場所 さらに確かめること
挙動を再現するコマンド そのコマンドが呼ぶパッケージスクリプトや実行ファイル どの引数でどのハンドラーが選ばれるか
特徴的な出力やエラーの文字列 その文字列を組み立てるコード 一致箇所が生成元、フィクスチャ、アサーションのどれかと、生成元を呼ぶ場所
名前が分かっているテスト そのimportとテスト内の呼び出し 実際のコマンドを起動するか、依存先を差し替えるか
設定キーや環境変数 その値の読み取りと変換 どの呼び出し元から到達し、どのローダーが設定を供給するか

同じ実習をYenpoで読む

「この4行はどの入口から出力されたのか」を調べるには、ダウンロードした report-entry-lab を使います。読み終えたら、起動コマンド、選ばれたハンドラー、入力ファイル、出力の呼び出しを短く記録します。

  1. 展開した report-entry-lab をYenpoで開きます。package.json があるルートを選んでください。Command-P(Go to File)で package.json、続いて src/cli.mjs と src/commands.mjs を開きます。起動スクリプトと、report に登録されたハンドラーを確認します。
  2. サイドバーの Search All Files でRegexをオフにし、Report rows: を検索します。src/format-report.mjs とテストの一致箇所を読み比べます。一方は文字列を生成し、もう一方は期待値を検証しています。このフォーマッターをコマンドの一部だと判断する前に、src/report.mjs 内のimportと呼び出しを追います。
  3. src/report.mjs、src/limits.mjs、関連するテストをタブで開いておきます。各段階を裏付けるファイルと行を記録し、以下のTerminalでの確認を使って、関数を直接呼ぶテストと実際のCLIを通る実行を区別します。

ファイル閲覧とテキスト検索に、モデル接続やJavaScript言語サーバーは不要です。この流れを試すには、Mac版Yenpoを入手して同じ展開先フォルダーを開いてください。コードを読むためのヘルプでは、二つの検索機能を説明しています。ZIPにはソースファイルはありますがGit履歴はないため、この実習ではコミットを固定した比較ではなく、ファイル自体を読みます。

ターミナルのコマンドが分かっている場合

フォルダー名から入口を推測する前に、スクリプトを確認します。

npm pkg get scripts.report
rg -n 'process.argv|commands\[command\]' src/cli.mjs
rg -n 'report:|runReport|report-config.json' src/commands.mjs

最初のコマンドは "node src/cli.mjs report" を表示します。cli.mjs では、process.argv.slice(2) により report がコマンド名となり、残りの引数が選ばれたハンドラーに渡されます。commands[command] は検索処理なので、起動元を見つけただけでは半分です。commands.mjs を開きます。report プロパティは report-config.json と rows.json のローダーを渡して runReport を呼びます。隣の help プロパティは代わりにヘルプを返します。

次に、同じ起動元を使い、一つの条件だけを変えます。

REPORT_LIMIT=4 npm run --silent report -- --limit 2

Report rows: 2 に続いて Aster と Birch が出力されます。npmは二重ハイフン区切りの後の引数をスクリプトに渡します。ディスパッチの検索処理と登録されたハンドラーを読んで経路を確定し、出力の変化から、このコマンドが上限を扱う処理に届くことを確認します。出力だけでは、途中のすべての呼び出しを特定できません。

出力文字列しか分からない場合

rg -n -F 'Report rows:' src tests
rg -n 'renderReport|runReport' src

最初の検索では format-report.mjs とテストのアサーションが見つかります。フォーマッターはヘッダーを組み立て、テストは期待する文字列を比較しています。どちらの一致箇所も、自動的にコマンドの入口になるわけではありません。report.mjs で renderReport のimportと、選んだ名前を渡す呼び出しの両方を確かめます。次に runReport を commands.mjs の report 登録までさかのぼり、選ばれたハンドラーから cli.mjs の標準出力への書き込みまで追います。

これで、先ほどのコマンドで確かめる経路が得られます。実際の出力が複数の部品から組み立てられているなら、変化しにくい断片を検索し、その周囲の式の組み立て方を調べます。共有フォーマッターに複数の呼び出し元があるなら、起動引数や対象を絞ったテストで一つに絞るまでは候補として残してください。すべてを同じ実行経路にまとめないでください。

テスト名が分かっている場合

rg -n -F 'environment wins over config' tests
node --test --test-name-pattern='environment wins over config' tests/report.test.mjs

一致したテストは runReport を直接importし、独自の環境、設定ローダー、行ローダーを渡しています。この関数内で4が6より優先されることは確かめていますが、cli.mjs の起動や設定ファイルの読み取りは行いません。実際のコマンドがどのファイルを読むかは分かりません。

importを追って挙動を見つけたら、実際の commands.report の結び付けを確認します。次の別のテストは、その不足していた境界を越えます。

node --test --test-name-pattern='CLI uses REPORT_LIMIT' tests/cli.test.mjs

このテストは REPORT_LIMIT=4 を指定した子プロセスで実際のCLIを起動し、終了ステータスと標準出力を確認します。選択した二つのテストはどちらも成功します。記録には違いを残しましょう。一方は依存先を自ら選び、もう一方は起動処理とファイル接続部分を実行します。関数を直接呼ぶテストが成功しても、後者の代わりにはなりません。

設定名が分かっている場合

rg -n -F 'REPORT_LIMIT' src tests
rg -n 'resolveLimit|loadConfig|report-config.json' src

ソースでの読み取りは limits.mjs にあり、ほかの一致箇所はテスト入力を用意しています。report.mjs の resolveLimit 呼び出しを追い、commands.report が渡す loadConfig 関数を確認します。その中の new URL(..., import.meta.url) は、モジュールを基準に同梱フィクスチャを選びます。ファイル名が一致するだけでは、このハンドラーが使うことの証明にはなりません。

これで、以下で値を調べるための出発点がそろいました。help や無関係なテストはいったん脇に置きます。コマンド、選ばれたハンドラー、実際の入力ファイル、出力箇所は確認できています。実際のコマンドが別のチェックアウト、ラッパー、環境の供給元を使っているなら、検索範囲を広げる前にその違いを解消してください。そうでなければ、次の問いは値の優先順位であり、リポジトリの残りすべてではありません。

選ばれたハンドラーを追う

実習の report.mjs は、引数の解析、設定の読み込み、上限の選択、行の読み込みを行い、renderReport でヘッダーを付けます。CLIは返された文字列を書き出します。以下の初期の単独例ではデータ行だけを扱うため、コマンドの接続部分を除いて値を調べられます。どちらの版でもローダーは引数なので、ファイルを読まずにテストで順序を観察できます。

export async function runReport({ args, env, loadConfig, loadRows }) {
  const { values } = parseArgs({
    args,
    options: { limit: { type: 'string' } },
    strict: true,
  });
  const config = await loadConfig();
  const limit = resolveLimit(values, env, config);
  const rows = await loadRows();
  return rows
    .slice(0, limit)
    .map((row) => row.name)
    .join('\n');
}

ヘルパーを一つも開かないうちから、この関数で処理順が分かります。引数を解析し、設定を読み、上限を選び、行を読み、出力します。見落としやすい点もあります。resolveLimit() が実行されるのは loadConfig() が成功した場合だけです。これは後で重要になります。

実際のコードベースではimportを追ってください。loadConfig という関数は、一つのJSONファイルを読むかもしれませんし、複数をマージしたり、リモートサービスを呼んだりするかもしれません。名前だけでは分かりません。

上限はどのように選ばれるか

export function resolveLimit(cli, env, config) {
  const raw = cli.limit ?? env.REPORT_LIMIT ?? config.limit ?? 10;
  const limit = Number(raw);
  if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100) {
    throw new Error('limit must be an integer from 1 to 100');
  }
  return limit;
}

ここでは三つのことをしています。値の供給元を選び、数値に変換し、範囲を確認します。記録ではそれぞれを分けてください。「環境変数が設定を上書きする」とだけ書くと、不正な環境変数なら設定ファイルへ戻ると考えがちです。このコードはそうしません。

CLI --limit REPORT_LIMIT 設定の limit 結果
2 4 6 コマンドラインからの 2
未指定 4 6 環境からの 4
未指定 未指定 6 ファイルからの 6
未指定 未指定 未指定 既定値 10
未指定 空文字列 6 検証エラー
0 4 6 検証エラー

?? が飛ばすのは null と undefined だけです。空文字列も値として扱われるため、REPORT_LIMIT='' が選ばれます。Number('') は 0 となり、範囲の確認で例外が発生します。その後、関数が設定ファイルの値を参照することはありません。

?? を || に変えれば空文字列は飛ばされますが、0 も「未設定」として扱われます。これは整理ではなく、規則の変更です。誰かが明示的に設定した不正な値はエラーにするべきか、それとも黙って代替値を使うべきか。まず望む挙動を決めてください。

値が使われる場所まで追う

上限が正しくても、出力が正しいとは限りません。limit を rows.slice(0, limit) まで追い、さらに rows の供給元を確認します。

ここでは loadRows() が全行を返し、ハンドラーが先頭の数行を残します。データの供給元では並べ替え、絞り込み、ページ分割をしていません。したがって、この上限が制限するのは出力する行数であり、読み込む量やメモリーに保持する量ではありません。

不具合報告が違えば、ここが重要になります。「--limit 2 でもレポートがメモリー不足になる」という報告なら、優先順位の規則を調べるのは的外れです。loadRows の中に入り、上限をデータの供給元まで渡す必要があるか確かめます。同じハンドラーでも、問いが変われば次に読むファイルは変わります。

意図的に壊してみる

今度は、壊れた設定ファイルと --limit 2 を組み合わせます。コマンドラインが最優先なら、それでもコマンドは動くはずでしょうか。

動きません。上限を選ぶ前に設定を読み込むため、loadConfig() が例外を投げると、それ以降は実行されません。優先順位はどの値を使うかを決めます。その選択に至る前に何が成功している必要があるかは、別の問題です。

逆のケースもテストする価値があります。設定は読み込めたものの、選ばれた上限が不正な場合です。そのときは行を読まないはずです。読み取り回数を数えて確かめます。

let reads = 0;
await assert.rejects(
  runReport({
    args: [],
    env: { REPORT_LIMIT: '' },
    loadConfig: async () => ({ limit: 6 }),
    loadRows: async () => {
      reads++;
      return [];
    },
  }),
  /integer/,
);
assert.equal(reads, 0);

例外が投げられたことだけを検証すると、先に全行を読んでから上限を拒否する実装も成功してしまいます。順序を固定するのは reads === 0 です。

例を実行する

configuration-flow.mjsをダウンロードし、Node.js 22以降で実行します。依存関係はありません。

node --test configuration-flow.mjs

元の単一ファイル版も引き続きダウンロードできます。六つのテストは、優先順位、空の値と不正な値、出力、不正な入力後に行を読まないこと、設定が壊れているケースを扱います。これらは関数を単独で検証するものです。自分のプロジェクトでは、実際のCLIが環境をどう読み込み、エラーをどう表示するかも確認する必要があります。

新しい入口の実習では、コマンドの接続部分も対象になります。そのフォルダーから実行してください。

node --test tests/*.test.mjs
REPORT_LIMIT='' npm run --silent report

九つのテストはすべて成功します。二つ目のコマンドは意図的にステータス 1 で終了し、標準エラーに limit must be an integer from 1 to 100 を表示して、標準出力には何も出しません。ユニットテストでは行の読み取りも数えるため、空の値のケースで前述の順序を確認できます。ZIPにはSHA-256マニフェストが含まれ、アーカイブと展開後のテストの両方が確認されています。

分かったことを記録する

短いメモで十分です。次回はコードを読むよりずっと早く理解できます。

runReport
  parseArgs → loadConfig → resolveLimit → loadRows → slice → output

limit: command line > environment > config > 10
invalid selected value: error; no fallback; rows are not loaded
config read failure: stops even when --limit is present
scope: limit bounds output, not the row loader's work

実際のコードベースでは、各手順の横にファイルと行を加え、まだ調べていないことを列挙します。loadConfig が読むファイル、親プロセスが REPORT_LIMIT を設定するかどうか、CLIがエラーをどう表示するか、などです。4行になる理由を説明でき、未解決の問いを挙げられたら、いったん止めてかまいません。プロジェクトの残りすべてを理解する必要はありません。

別の保守担当者に引き継ぎを確認してもらう

読解メモは、会話の履歴がなくても別の人が再現できると、さらに役立ちます。poster-deskの引き継ぎZIPと短い引き継ぎ概要を試してください。この小さなオフラインプロジェクトと説明は教材として作られたもので、実際にAIが生成したアプリを保存したものでも、実際の引き継ぎでもありません。

受け手の課題は、固定された一つのソーススナップショットに対して、四つの約束を確認することです。ローカルのカードを新しいプロセスから読めること、完全に同じ内容を順番に再試行すると既存レコードを使うこと、受領記録がフィクスチャのプレビューにすぎないこと、デプロイの置き換えをまたぐ保存の継続性は不明なことです。「実装済み」が示すのはテストされた条件だけです。順次再試行では並行実行の安全性は確立できず、モックは何も送信せず、パッケージにはデプロイ先ストレージの証拠がありません。

概要に従い、RECEIVER.md を展開したパッケージの外にコピーします。そのコピーにコマンド、結果、ソースの位置、未解決の問いを記入してください。納品されたテンプレートはそのまま残し、元のパッケージをもう一度検証します。

ZIP SHA-256
c39d19b210c05f4667cd74db4934459a079408d14c5e7678c24cd087e1df5185

Yenpoでは、Go to Fileとワークスペース検索を使って、ハンドラー、値の解決処理、テストを並べて開いておけます。AI Readerに質問するときは、一つの値に絞ってください。「REPORT_LIMIT を、読み取る場所から使われる場所まで行参照付きで追ってください」。プロジェクト全体の要約をもう一つ受け取るのではなく、一段ずつ確認できる経路が得られます。

手元の証拠に応じて、次に読む対象を選びましょう。