# 指示書BK — 指示書BJ の点検で出た指摘を直す（2026-11-05）

**担当**: Cursor。監督（Claude Code）がレビューする。
**前提**: develop の `53f46e0` 以降（指示書BJ の実装 `d99725d`〜`53f46e0` が入った版）。
**着手時の先端**: `git log --oneline -1` の ID を書く: `53f46e0`。`53f46e0` と違っても、その間のコミットが資料（`docs/`・`.claude/`）だけなら、本書の行番号はそのまま使える（確かめ方: `git diff --stat 53f46e0..HEAD -- . ':!docs' ':!.claude'` が何も出さない）。
★行番号はすべて `53f46e0` 時点の実物で監督が確かめた値★。ずれたら書いてある文言で探す。
★作業の前に読むこと★: `docs/handoff/指示書BJ_制御盤のログの点検で出た指摘を直す_20261105.md` の §4 表R（R45〜R48）・表T（T-b）・表E（E6）。

---

## 0. ユーザーの指示（原文）と回答

- 「確認して」（指示書BJ の実装の確認）→ 監督の点検（tsc・lint・全テスト3回・fixtures の画面・査読2観点）で、★本物の制御盤で困る不具合は無い★が、テストが見張りになっていない箇所と、上限の値の食い違いが見つかった
- ファイル ID の長さの上限について、監督が選択肢を示した: A＝D-Watch を 8192 にして制御盤とそろえる／B＝1024 のまま、制御盤側で長いものを外す
- ユーザーの回答: ★「Aで、指示書BKを書いてください」★（2026-11-05）
- 本書を書いた後、監督が「制御盤は応答を 64KB までしか読まない」ことを報告し、選択肢を示した: A＝D-Watch でエクスポートの ID の合計を約48KB までに制限し、超えたら 422「選んだファイルが多すぎます」／B＝制御盤側の読み取りの上限を上げる（別リポジトリ・本番機への反映が要る）／C＝何もしない（記録だけ）
- ユーザーの回答: ★「Aで、指示書BKに書き足してください」★（2026-11-05）→ K7

## 1. 何を直すか

| # | 重さ | いま（監督・査読役が確かめた事実） | ★直した後★ |
|---|---|---|---|
| K1 | 中 | R45・R46・R46b（`key` の不正4行）のテストは、★J5 の検査を消しても緑のまま★。`lib/ds-logs/__tests__/validate.test.ts:403-406` は `types[0]` の key だけを壊し、items の `type` は `"app"` のまま。`app/api/ds-agent/__tests__/routes.test.ts:245-248` は `types` を丸ごと差し替え、`VALID_LIST.items[0].type` は `"app"` のまま。どちらも「items の種類が types に無い」（`itemsの形式が不正です`）で 400 になる。正しい key（例 `"VALIDKEY"`）でも同じ形なら 400（査読役が tsx で確認）。`expectResultInvalid()`（`validate.test.ts:77-82`）は status と code しか見ない | 壊した key を ★items の `type` にも使う★。さらに ★メッセージ `"typesの形式が不正です"` まで検査する★ |
| K2 | 中 | 「ディスクに書かない」の見張りが ★`import("node:fs")`・`require("fs")`・`"node:fs/promises"` を見逃す★。T-b のスパイ（`end-to-end.test.ts:354-387`）は `fs` の既定オブジェクトのプロパティだけに効き、名前付き import や動的 import の書き込みは捕まえない（査読役が試して確認: 実際にファイルが書かれて緑）。静的な検査（`:389-425`）の禁止の正規表現（`:418`）は `from "node:fs"` の形だけで、`from "node:fs/promises"`・`import("node:fs")`・`require(…)` に当たらない。★指示書BJ の G3 に書いた壊し方（`await import("node:fs")`）は、そもそも赤にならない壊し方だった（監督の誤り）★ | 静的な検査で ★fs / os のモジュール名の文字列そのもの★と `require(` を禁じる。T-b のスパイは補助として残し、説明を正す |
| K3 | 低 | `parseAgentComplete()` が `total_bytes: 0` を 400 にしている。これは ★指示書BJ §2-3（`DsAgentComplete.total_bytes` も `minimum: 1`）と D-179 の BJ の記録の項3（`docs/decisions.md:13066`）のとおり★で、実装は正しい。★テストだけが無い★（監督は点検の報告で一度「BJ に無い変更」と誤って書いた — BJ §2-2 だけを見ていた） | 挙動はこのまま。★テストを1行足す★ |
| K4 | 低 | E6（`lib/ds-logs/__tests__/modal-text.test.ts:57-67`）の期待値を `formatFileSize(EXPORT_MAX_BYTES)` で作っている。表示の関数や上限が変わっても赤にならない | 期待値を ★文字列 `"選んだファイルが大きすぎます（1回 100 MB まで）"` で固定★ |
| K5 | 低 | R43′ の「その後の 404」（`routes.test.ts:256`）が status しか見ない。★ブラウザの `GET …/logs` が 502・`details.agent_code = ["result_invalid"]` になることを、ルートを通して見るテストが無い★（relay の Promise の値までしか見ていない） | 404 のコード `not_found` も見る。`end-to-end.test.ts` に「一覧 → 形の崩れた結果 → ブラウザは 502」を1本足す |
| K6 | 中（値の変更） | D-Watch の `FILE_ID_MAX_LENGTH` は 1024（`lib/ds-logs/validate.ts:39`）。★本物の制御盤が受け取れる ID の上限は 8192 文字★（`C:\Users\daich\dev\dwatch-log-agent\agent.py:64` `FILE_ID_MAX_LEN = 8192` を、受け取った ID を読む `decode_file_id()`（:367）が使う）。★作る側（`encode_file_id()`（:353-359）・それを呼ぶ `list_files()`（:518））には上限が無く★、長いものを一覧から外す処理も無い。ID は「番号:相対パス」の UTF-8 を base64url にしたもの。相対パスが約766バイトを超えるファイルが1つあると、★一覧全体が 400 → 画面は「制御盤の結果の形が不正です」でどのログも見られない★ | ★ユーザー回答 A★: `FILE_ID_MAX_LENGTH` を ★8192★ にする（一覧とエクスポートの本体の両方。同じ定数） |
| K7 | 中（上限の追加） | ★制御盤は D-Watch の応答を 64KB までしか読まない★（`C:\Users\daich\dev\dwatch-log-agent\agent.py:62` `RESPONSE_READ_MAX_BYTES = 64 * 1024`・`:820` `resp.read(RESPONSE_READ_MAX_BYTES)`）。エクスポートの指示は poll の応答に選んだ `file_ids` を全部入れて届く（`lib/ds-agent/relay.ts:703`）。★ID の合計が約64KB を超えると JSON が途中で切れ★、制御盤は「poll returned a body that is not JSON」（`agent.py:1028-1033`）でやり過ごし、★ブラウザは 50秒後に 504「制御盤から応答がありません」★（原因が分からない）。1024 のときからある（1024文字なら約64件・8192文字なら約8件で超える）。今の実際の ID（30〜40文字）なら500件でも約20KB で起きない | ★ユーザー回答 A★: ブラウザの `POST …/logs/exports` で、★`file_ids` を JSON にした大きさ（UTF-8 のバイト数）が `EXPORT_FILE_IDS_MAX_BYTES`（48KB ＝ 49,152）を超えたら 422★。画面は「選んだファイルが多すぎます。数を減らしてください」。★制御盤（`agent.py`）は直さない★ |

## 2. 直し方

### K1 — `key` の不正のテストを効かせる

- `validate.test.ts:403-406` の4行（R45・R46・R46b×2）: 本体を ★壊した key `bad` を `types[0].key` に入れ、items のうち `type: "app"` のものを全部 `type: bad` にした形★ で作る（補助関数を1つ足してよい。例 `withTypeKey(bad)`）。R47（:407。`file_id` の1025文字）は K6 で値が変わる（下）
- 期待: `expectResultInvalid()` に ★メッセージの引数（省略可）を足し★、この4行は `"typesの形式が不正です"` を検査する（`lib/ds-logs/validate.ts:284` の文言）。★この4行は R47（:407。メッセージは `itemsの形式が不正です`）と同じ `it.each` に入っているので、別の `it.each` に分ける★（または期待メッセージの列を足す）
- `routes.test.ts:245-248` の4行も同じ: `types: [{ key: bad, label: "x" }]` と ★`items: [{ ...VALID_LIST.items[0], type: bad }]`★。応答の本文の `error.message` が `"typesの形式が不正です"` であることも見る。★この4行は W2〜W7・R47 と同じ `it.each`（:231-257）に入っていて、期待メッセージが行ごとに違うので、別の `it.each` に分ける★。★`errorCode()`（:98-100）は `res.json()` で本文を読み切るので、あとから `error.message` を読めない★ — 本文を1回だけ読んで code と message を両方返す補助関数（例 `errorBody()`）を足して使う
- ★G3（壊して赤）★: `lib/ds-logs/validate.ts:236-238` の `isLogTypeKey()` を一時的に `typeof value === "string"` だけにする → ★この8行（validate 4・routes 4）が赤★になることを確かめて戻す。★戻す前の赤の出力（テスト名と件数）を報告に貼る★

### K2 — 「ディスクに書かない」の見張りを効かせる

- `end-to-end.test.ts:418` の `banned` を次にする（`tmpdir`・`mkdtemp` は残す）:
  ```ts
  const banned = [
    /["'](?:node:)?(?:fs|os)(?:\/promises)?["']/, // import・export・動的 import・require のどれで書いても、モジュール名の文字列で当たる
    /\brequire\s*\(/,
    /\bimport\s*\(/, // 動的 import はモジュール名によらず禁じる（テンプレート文字列の import(`node:fs`) も塞ぐ）
    /\btmpdir\b/,
    /\bmkdtemp\b/,
  ];
  ```
  ★今の対象ファイルにこの3つの新しい正規表現に当たる記述は無い★（`import(` も0件。`requireAgent(`・`"@/lib/ds-agent/require-agent"` には当たらない）（監督が `grep -rnE "[\"'](node:)?(fs|os)(/promises)?[\"']"` で確認。0件）。★コメントの中でもモジュール名を引用符で囲んで書かない★（書くと赤になる。書くならバッククォートで）。裏返しで、1つ目の正規表現はバッククォートのテンプレート文字列（`` import(`node:fs`) ``）には当たらない — ★それは3つ目の `import(` の禁止で塞ぐ★
- テスト名（:389）を「relay / ds-logs / agent ルート / logs ルート / agent-request に fs・os のモジュール名と require が無い」に直す
- T-b（:354 の `describe`）の冒頭に1行コメントを足す: 「スパイは `fs` の既定オブジェクト経由の書き込みしか捕まえない。名前付き import・動的 import・require は下の静的な検査で捕まえる」
- ★G3（壊して赤）★を3通り試し、★3通りとも静的な検査のテストが赤★になることを確かめて戻す（出力を報告に貼る）:
  1. `lib/ds-agent/relay.ts` の先頭に `import { writeFileSync } from "node:fs";`
  2. `pushChunk()` の中に `await import("node:fs/promises");`
  3. `pushChunk()` の中に `require("fs");`（型エラーになるなら `// @ts-expect-error` を付けてよい。試すだけ）
  ★試した変更はコミットしない★

### K3 — complete の `total_bytes: 0`

- `validate.test.ts` の `parseAgentComplete()` の不正の表（:457 の `describe` の中、:463 の `it.each`）に ★`["total_bytes が0（指示書BK K3）", JSON.stringify({ total_bytes: 0, sha256: HEX64 })]`★ を足す（他の行と同じ形。complete の本体の項目は `validate.ts:329` 以降で確かめる）
- 挙動（400 `result_invalid`・状態は変えない）は変えない。prepared が 0 を拒むので、正しい流れでは complete に 0 は来ない

### K4 — E6 を固定

- `modal-text.test.ts:64-66` の期待値を `"選んだファイルが大きすぎます（1回 100 MB まで）"` にする。`formatFileSize`・`EXPORT_MAX_BYTES` の import が使われなくなったら消す（lint）

### K5 — 404 のコードと、ルートを通した 502

- `routes.test.ts:256`: status 404 に加えて `errorCode(…)` が `"not_found"`
- `end-to-end.test.ts` に1本足す（既存の補助関数 `agentPoll()`・`browserLogs()`・`agentPostResult()`（:138・:116・:142）を使う）:
  - 制御盤が待つ → ブラウザが一覧を求める → 制御盤が `/result` に `{ types: [], items: "x" }` のような形の崩れた本体を送る → ★制御盤は 400 `result_invalid`★、★ブラウザの応答は 502・`error.code` が `agent_error`・`error.details.agent_code` が `["result_invalid"]`★
  - ★G3★: `app/api/ds-agent/commands/[command_id]/result/route.ts` の `rejectListResult()` の呼び出しを一時的に消す → ブラウザ側の応答が返らず、★vitest の既定の時間切れ（5秒）で赤★になることを確かめて戻す（e2e の relay は偽の時計で、`at()` を呼ばない限りタイマーは発火しないので、504 にはならない。制御盤側の 400 は緑のまま）

### K6 — ファイル ID の上限を 8192 に（ユーザー回答 A）

- `lib/ds-logs/validate.ts:39` の `FILE_ID_MAX_LENGTH` を ★8192★ にし、コメントに理由を書く（「制御盤の常駐プログラムが受け取れる上限 `FILE_ID_MAX_LEN`（`agent.py:64`。`decode_file_id()` が使う）とそろえる。指示書BK K6」）。一覧（:248）とエクスポートの本体（:172・:176）は同じ定数なので両方 8192 になる
- テストの値を直す（★1024 / 1025 を直書きしている行を全部★。`grep -rn "1024\|1025" lib/ds-logs/__tests__ app/api/ds-agent/__tests__ "app/api/ds-devices"` で探す。`4 * 1024 * 1024` のような大きさの式は対象外）:
  - R47（`validate.test.ts:407`・`routes.test.ts:249`）: 8193 文字は不正。「1024文字はよい」（`validate.test.ts:421-423`）は ★8192 文字はよい★ に
  - エクスポートの本体（V7）: `validate.test.ts:17`（表の説明の「1025文字」）・`:183`（テスト名の「1024文字」）・`:191`（`"x".repeat(1024)`）・`:200`（`"1025文字"` の行）を 8192 / 8193 に（`:183-192` のテストは本体を2つ作る。500件の本体は短い ID（`id-${i}`）で、長い ID は1件だけの別の本体なので、8192 にしても遅くならない）
- `d-watch-api.yaml`（G1。★`grep -n "1024" d-watch-api.yaml` で全部★）: `:1911` の「各 1〜1024 文字」・`:3667` の `DsLogFile.file_id` の `maxLength`・`:3710` の `DsLogExportRequest.file_ids.items` の `maxLength` を 8192 に。★大きさ（`1024 * 1024` 系の説明）は変えない★
- ★本体の大きさの確認★（監督が計算済み。変えなくてよいが、記録に書く）: エクスポートの本体は最大 `EXPORT_MAX_FILES`（500。`lib/config/ds-agent.ts:69`）× 8192 文字 ≒ 4MB で、proxy が本体を読む上限の 10MB（指示書AY 4-1）より小さい。★本番の nginx の `client_max_body_size`（TeamWorks の設定。既定は 1MB）を超えうる★が、実際の ID は短い（`agent.py` の ID は「番号:相対パス」の base64url。今のログのフォルダ名なら40〜80文字程度）ので、500件選んでも 40KB 程度。★長い ID を大量に選んだときだけ nginx が 413 を返しうる★ — 実害は小さいので記録に残すだけ
- 一覧の本体の上限（`LIST_RESULT_MAX_BYTES` 4MB）は変えない
- ★もう1つの上限（監督が確認。K6 では直さない・K7 で直す）★: 制御盤は D-Watch の応答を ★64KB までしか読まない★（`agent.py:62` `RESPONSE_READ_MAX_BYTES = 64 * 1024`・`:820`）。エクスポートの指示は poll の応答に選んだ `file_ids` を全部入れて届く（`lib/ds-agent/relay.ts:703`）ので、★ID の合計が約64KB を超えると JSON が途中で切れ★、制御盤は「poll returned a body that is not JSON」（`agent.py:1028-1033`）でやり過ごし、ブラウザは 50秒後に 504「制御盤から応答がありません」になる。★1024 のときからある問題★（1024文字の ID なら約64件、8192文字なら約8件で超える）。今の実際の ID（「番号:相対パス」の base64url。30〜40文字程度）なら500件でも約20KB で収まる。★手当ては K7★（ユーザー回答 A。D-Watch の POST で ID の合計を制限して 422 にする）

### K7 — エクスポートで選んだ ID の合計の上限（ユーザー回答 A）

- ★なぜ 48KB か★: poll の応答は `{"command_id":"<36文字>","kind":"export","params":{"file_ids":[…],"max_bytes":104857600,"chunk_bytes":4194304}}`（`relay.ts:700-704`。`NextResponse.json` は `JSON.stringify` そのまま）。`file_ids` の外側は約140バイト（監督が `JSON.stringify` で測って 136〜138）なので、48KB なら 64KB まで約16KB の余裕がある。★`file_ids` の部分を JSON のまま数えるので、引用符・カンマ・`\uXXXX` の逃がし・UTF-8 の多バイトも含めて、実際に届くバイト数と一致する★
- ★帰結（監督の計算）★: 8192文字の ID なら5件まで（6件で 49,171 バイト）。平均 95文字以下の ID なら、先に500件の上限が効く（49,152 ÷ 500 − 3 ≒ 95）。今の実際の ID（30〜40文字）では500件でも約21,500 バイトで、★今の使い方では一度も効かない★
- `lib/config/ds-agent.ts` の `EXPORT_MAX_FILES`（:69）の後ろに足す:
  ```ts
  /**
   * 1回のエクスポートで選ぶ `file_ids` を JSON にした大きさ（UTF-8 のバイト数）の上限（指示書BK K7）。
   * ★制御盤は D-Watch の応答を 64KB までしか読まない★（`agent.py` の `RESPONSE_READ_MAX_BYTES`）。
   * エクスポートの指示は poll の応答に `file_ids` を全部入れて届くので、外側の項目（約140バイト）の分を残して 48KB にする。
   * ★64KB に近づけない★（超えると指示が途中で切れ、ブラウザは 50秒待って 504 になる）
   */
  export const EXPORT_FILE_IDS_MAX_BYTES = 48 * 1024;
  ```
- `lib/ds-logs/validate.ts` の `parseExportRequest()`（:164-178）: 今の3つの検査（件数・各 ID の長さ・重複）を全部通った後に、★`new TextEncoder().encode(JSON.stringify(fileIds)).length > EXPORT_FILE_IDS_MAX_BYTES` なら 422★:
  - `fail(422, "validation_failed", EXPORT_FILE_IDS_TOO_LARGE_MESSAGE, { file_ids: [`ファイル ID の合計が ${EXPORT_FILE_IDS_MAX_BYTES} バイト（JSON・UTF-8）以内になるように選んでください`] })`
  - `export const EXPORT_FILE_IDS_TOO_LARGE_MESSAGE = "選んだファイルが多すぎます。数を減らしてください";`（validate.ts に置く。画面の文言の関数と共有する）
  - ★`fileIds.join("")` の長さや文字数で数えない★（罠7）。`Buffer` でなく `TextEncoder`（どちらでも動くが、画面側からも import されるファイルなので Node の型に寄せない）
  - 関数の説明のコメント（:160-163）に「★ID の合計の上限（K7）★」を一言足す
- `lib/ds-logs/modal-text.ts` の `apiErrorMessage()`: `switch (e.code)` に ★`case "validation_failed":`★ を足し、`e.serverMessage === EXPORT_FILE_IDS_TOO_LARGE_MESSAGE` なら `t(EXPORT_FILE_IDS_TOO_LARGE_MESSAGE)`、それ以外は今の `default` と同じ（`e.serverMessage ?? t("データの取得に失敗しました")`）。★他の 422 の出方は変えない★
- `lib/i18n.ts`: `"選んだファイルが大きすぎます（1回 {max} まで）"`（:598）の後ろに `"選んだファイルが多すぎます。数を減らしてください": "Too many files selected. Please select fewer files"` を足す
- `d-watch-api.yaml`（★先に直す★。G1）: `POST /ds-devices/{serial_number}/logs/exports` の 422 の説明（:1910-1912）に「`file_ids` を JSON にした大きさ（UTF-8）が 49152 バイトを超える → message「選んだファイルが多すぎます。数を減らしてください」（制御盤が応答を 64KB までしか読まないため。指示書BK K7）」を足す。`DsLogExportRequest.file_ids`（:3705-3710）に `description` で同じ条件を書く（JSON Schema では表せないので説明だけ）
- ★画面（`components/`）は直さない★。選択中に先回りしてボタンを止めることはしない（今の ID では起きないので、サーバーの 422 と文言だけにする。記録の項6 に却下として書く）
- テスト（★期待値表★。K7-1〜K7-7）:

  | # | どこ | 入力 | 期待 |
  |---|---|---|---|
  | K7-1 | `validate.test.ts`（V7 の近く） | `file_ids` の JSON が ★ちょうど 49,152 バイト★ | 通る |
  | K7-2 | 同上 | ★49,153 バイト★ | 422・message `選んだファイルが多すぎます。数を減らしてください`・`details.file_ids` が1件 |
  | K7-3 | 同上 | `["あ".repeat(8192), "い".repeat(8192)]`（文字数では 16,391 だが UTF-8 では 49,159 バイト） | 422（★バイトで数えている★ことの見張り） |
  | K7-4 | `app/api/ds-devices/[serial_number]/logs/__tests__/route.test.ts`（V7 の近く :256） | K7-2 の本体 | 422・本文が `{ error: { code: "validation_failed", message: 上の文, details: { file_ids: [1件] } } }`・★`exists` が呼ばれない★（V8 の行と同じ見方） |
  | K7-5 | `app/api/ds-agent/__tests__/end-to-end.test.ts` | 制御盤が待つ → ブラウザが K7-1 の本体（ちょうど上限）でエクスポート → 制御盤の poll の応答 | ★応答の本文（`await res.text()` の UTF-8 バイト数）が 64 × 1024 以下★、かつ `JSON.parse` でき、`params.file_ids` が送った配列と同じ。後片付けは既存のテスト（:448 の `agentPostFail()` など）と同じ作法 |
  | K7-6 | `lib/ds-logs/__tests__/modal-text.test.ts`（表E） | 422・`validation_failed`・serverMessage が上の文 | `"選んだファイルが多すぎます。数を減らしてください"`（t は恒等で可）。★英語の辞書で `"Too many files selected. Please select fewer files"` になること★も1行 |
  | K7-7 | 同上 | 422・`validation_failed`・serverMessage が `file_idsの形式が不正です` | `"file_idsの形式が不正です"`（今のまま） |

  - K7-1・K7-2・K7-5 の本体は ★`EXPORT_FILE_IDS_MAX_BYTES` から作る補助関数★で作る（数字の直書きにしない。G3 の (b) が赤になるため）。例 `fileIdsOfJsonBytes(n)`: 1件の JSON は「ID の長さ＋3」（引用符2つとカンマ。配列の括弧で＋1）なので、`k = Math.ceil((n - 1) / (FILE_ID_MAX_LENGTH + 3))` 件にし、先頭 k−1 件は `FILE_ID_MAX_LENGTH` 文字（★先頭に番号を付けて重複させない★。例 `` `${i}-`.padEnd(FILE_ID_MAX_LENGTH, "x") ``。`"a"`〜`"z"` の1文字の繰り返しは26件で尽きるので使わない）、最後の1件は `n − 1 − (k − 1) × (FILE_ID_MAX_LENGTH + 3) − 3` 文字。49,152 なら 6件（8192 × 5 ＋ 8173）。★補助関数の結果を `TextEncoder` で測って n と一致することもテストの中で確かめる★。置き場所は `lib/ds-logs/__tests__/`（例 `file-ids-of-bytes.ts`。`.test.ts` にしない）で、e2e からも import する
  - ★K6 を先に入れてから K7★（`FILE_ID_MAX_LENGTH` が 8192 でないと、49,152 バイトを作る件数が変わる。補助関数は定数から計算するので値は直書きしない）
- ★G3（壊して赤）★:
  - (a) `parseExportRequest()` の K7 の検査を一時的に消す → ★K7-2・K7-3・K7-4 が赤★
  - (b) `EXPORT_FILE_IDS_MAX_BYTES` を一時的に `64 * 1024` にする → ★K7-5 が赤★（上限ちょうどの本体に外側の約140バイトが足されて 64KB を超える）
  - (c) `modal-text.ts` の `case "validation_failed":` を消す → ★K7-6 の英語の行が赤★
  - 戻す前の赤の出力を報告に貼る

## 3. 段階（この順で。段階ごとにコミット）

0. 着手前: 冒頭の「着手時の先端」を書く。`git status --short` で他のセッションの未コミットの変更（`components/features/errors/ErrorContentPicker.tsx`・`lib/errors/error-content-label.ts` とそのテスト・指示書AY・指示書BJ・本書）を確かめ、★触らない・コミットに含めない★。`npx vitest run` の件数（前）を控える（`53f46e0` で 170ファイル・4,306件）
1. K1（テストだけ）＋G3 の赤の確認
2. K2（テストだけ）＋G3 の3通りの赤の確認
3. K3・K4・K5（テストだけ）＋K5 の G3
4. K6（★`d-watch-api.yaml` を先に直してから★ `validate.ts`・テスト。CLAUDE.md §6-1）
4b. K7（★`d-watch-api.yaml` を先に★ → `lib/config/ds-agent.ts`・`validate.ts`・`modal-text.ts`・`lib/i18n.ts`・テスト）＋G3 の (a)〜(c)
5. 記録（§4）
6. tsc・lint・vitest 全件（★3回★）の件数と、段階ごとのコミット、G3 の赤の出力（★K1・K2×3・K3・K4・K5・K6・K7×3★）を報告（★日本語★）。K3 は `parseAgentComplete()` の `total_bytes` の下限を外す → 足した行が赤。K4 は `formatFileSize()` の MB の表示を一時的に変える → E6 が赤。K6 は定数を 1024 に戻す → 「8192 文字はよい」の行が赤

★画面の確認は要らない★（画面の動きは変わらない。K6 で長い ID が通るようになるだけで、fixtures の ID は短い。K7 の 422 は fixtures の短い ID では画面から起こせないので、文言は表E（K7-6）のテストで見る）。

## 4. 決定の記録（段階5 で `docs/decisions.md` の D-179 に足す。★新しい D 番号は取らない★）

足す場所: 指示書BJ の記録の項8「★実装の記録★」（`docs/decisions.md:13072`）の後ろ・`---` と D-180 の見出し（:13076）の前。★BJ の記録の項1〜8 は書き換えない★（項8 の「本書と違う判断: なし」も消さず、下の項で補う）。

```markdown
- ★指示書BJ の点検の直し（2026-11-05。指示書BK）★:
  1. ★制御盤のファイル ID の上限を 1024 → 8192 にした★（ユーザー回答 2026-11-05「A」）。制御盤の常駐プログラムが受け取れる上限（`agent.py` の `FILE_ID_MAX_LEN`。作る側には上限が無い。8192 を超えるには相対パスが約6.1KB 要るので、Linux のパスの上限（4096）からまず起きない — 推測）とそろえる。1024 のままだと、長いパス（約766バイト超）のファイルが1つあるだけで一覧全体が「制御盤の結果の形が不正です」になる
     - 却下: B（1024 のまま、制御盤側で長いものを一覧から外す）— 別リポジトリの改修が要り、外したファイルは画面から取り出せない
     - 帰結: エクスポートの本体は最大 500件 × 8192文字 ≒ 4MB（proxy の 10MB より小さい）。本番の nginx の `client_max_body_size` は TeamWorks の設定で、ブラウザの POST（`/api/ds-devices/…/exports`）は既定（1MB）のままと思われる（第13回で 5MB を頼んだのは `/api/ds-agent/` 配下だけ）ので、長い ID を大量に選んだときだけ 413 になりうる
     - ★先に効く上限★: 制御盤は poll の応答を 64KB までしか読まない（`agent.py` の `RESPONSE_READ_MAX_BYTES`）。エクスポートの指示に入る `file_ids` の合計が約64KB を超えると指示が壊れ、ブラウザは 50秒後に 504。1024 のときからある問題（1024文字なら約64件・8192文字なら約8件で超える）。今の ID（30〜40文字）なら500件でも約20KB。★項6 で手当てした★
     - BJ の記録の項3 の「`file_id` は1〜1024文字」を本項で改める
  2. complete の `total_bytes: 0` を 400 にすること（BJ §2-3 の YAML・本記録の BJ の項3 のとおり）にテストが無かったので足した。★BJ §2-3 の括弧書き（「0 は来ても必ず失敗する」）が想定した『照合の不一致でエクスポートを即失敗』ではなく、『400・状態は変えない』（エクスポートは待ちの時間切れまで残る）になっている★。正しい流れでは来ないので、このままとする
  3. テストの見張りを効かせた: `key` の不正（R45〜R46b）は items の種類も同じ key にしてメッセージまで見る／「ディスクに書かない」は静的な検査でモジュール名の文字列と `require(` を禁じる（★指示書BJ の G3 に監督が書いた壊し方 `await import("node:fs")` はスパイでも旧い静的な検査でも捕まらない壊し方だった★）／E6 は「100 MB」を固定／R43′ の 404 は `not_found` まで見る／ブラウザの 502（`result_invalid`）をルートを通して見る
  4. ★実装の記録★: コミット／テスト件数（前 170ファイル・4,306件 → 後）／G3 の赤の確認（K1・K2×3・K3・K4・K5・K6・K7×3）／本書と違う判断
  5. ★指示書BJ の画面確認の補い（監督。2026-11-05。当方の fixtures 由来）★: BJ の記録の項8 に無かった次を監督が確かめた — J10 の「種類を戻すと `types` なしで1回」・Esc で閉じて「ログ」ボタンにフォーカスが戻る・D-Sec ログの表に横スクロールが出ない（1440×900・JP）・エクスポートの `fetch` 確認（201 → 200・SHA-256 一致・2回目 404・`X-Accel-Buffering: no`・`Content-Encoding` なし）。★未確認★: ダーク/ライト・トークンを表示したときのコピーのボタンの折り返し・DS-001 の未接続・Tab の閉じ込め・背景のクリック・100MB の警告（いずれも BJ で触っていない部分か、BJ 前の AY の点検で確認済み）
  6. ★エクスポートで選ぶ `file_ids` の合計に上限を足した★（ユーザー回答 2026-11-05「A」）: `file_ids` を JSON にした大きさ（UTF-8）が `EXPORT_FILE_IDS_MAX_BYTES`（48KB）を超えたら 422「選んだファイルが多すぎます。数を減らしてください」。制御盤は D-Watch の応答を 64KB までしか読まず（`agent.py` の `RESPONSE_READ_MAX_BYTES`）、エクスポートの指示は poll の応答に `file_ids` を全部入れて届くため。超えると指示が途中で切れ、ブラウザは 50秒待って原因の分からない 504 になっていた（1024 のときからあった）
     - 却下: B（制御盤の読み取りの上限を上げる）— 別リポジトリの改修と本番機への反映が要る。C（何もしない）— 今の ID（30〜40文字）では起きないが、起きたときに原因が画面から分からない
     - 却下: 画面で選択中に先回りしてボタンを止める — 今の ID では500件でも約21,500バイトで効かないので、サーバーの 422 と文言だけにした
     - 帰結: 8192文字の ID なら1回5件まで。平均95文字以下の ID なら先に500件の上限が効く。★制御盤の上限（64KB）を変えるときは、この値も見直す★
```

## 5. 既知の罠

1. ★K1 で items の `type` を変え忘れると、テストは緑のまま★（今と同じ）。メッセージまで見るので、変え忘れたら `itemsの形式が不正です` で赤になる — それで気づける
2. ★K2 の新しい正規表現は、コメントの中の `"node:fs"`（引用符つき）にも当たる★。対象のソースのコメントにモジュール名を書くならバッククォートで
3. ★K2 の G3 で `require("fs")` を試すとき、ESM の実行で `require is not defined` になっても、静的な検査はその前にファイルの文字列で赤になる★。実行時の失敗と取り違えない（静的な検査のテスト名が赤であることを見る）
4. ★K6 で `1024` を機械的に全部置き換えない★ — `4 * 1024 * 1024`（4MB）・`100 * 1024 * 1024`（100MB）・`formatFileSize` の `1024` は大きさの計算
5. ★K6 で `agent.py` を直さない★（別リポジトリ。読むだけ）
6. ★K5 の e2e で偽の時計を進めて 504 を作らない★。形の崩れた結果を送った時点でブラウザの応答が 502 で返ることを見る（R43′ と同じ順序。待ちは `waitUntilConnected()` の作法）
7. ★K7 は文字数でなくバイト数で、しかも JSON にした形で数える★。`fileIds.join("").length` や各 ID の `length` の和では、引用符・カンマ（1件3バイト）と多バイト文字（`あ` は3バイト）を数え落とし、上限ちょうどの本体で 64KB を超えうる（K7-3 が見張る）
8. ★K7 の 422 は今の3つの検査の後に置く★。先に置くと、501件や重複の本体が「多すぎます」の文言になり、V7 の既存の行の期待（`file_idsの形式が不正です`）と食い違う
9. ★K7 で `agent.py` の `RESPONSE_READ_MAX_BYTES` を変えない★（別リポジトリ。読むだけ）。K7-5 のテストの 64KB は、テストの中の定数として `agent.py:62` を引いて書く

## 6. 共通ルール

- G1〜G4（指示書AZ §2）: YAML の影響を受ける箇所を全部直す（K6 の `1024` は `grep` で全部）／古いコメントを残さない（`validate.ts:38` の `FILE_ID_MAX_LENGTH` の説明、`end-to-end.test.ts:389` のテスト名）／★足した・直したテストは壊して赤を確かめ、その出力を報告に貼る★／記録にコミット・件数の前後・本書と違う判断を書く
- `git add -A` 禁止。ファイルパスを明示して段階ごとにコミット。PR とマージは daichi のレビューの後
- 触らない: `components/`（今回は画面を変えない）・`docs/handoff/指示書AY_*`・`docs/handoff/指示書BJ_*`・`docs/handoff/設計_制御盤ログの取り出し_*`・`components/features/errors/ErrorContentPicker.tsx`・`lib/errors/error-content-label.ts` とそのテスト・`prisma/`・`CLAUDE.md`・`C:\Users\daich\dev\dwatch-log-agent\`（読むだけ）
- DB に書き込まない。:3000 を起動・停止しない。:3100 は監督が fixtures で動かしていることがある — ★今回は画面の確認が無いので起動しない★
