Newer
Older
client_dwatch / docs / handoff / 指示書BK_指示書BJの点検の直し_20261105.md

指示書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 は残す):
    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)の後ろに足す:
    /**
     * 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. 段階(この順で。段階ごとにコミット)

  1. 着手前: 冒頭の「着手時の先端」を書く。git status --short で他のセッションの未コミットの変更(components/features/errors/ErrorContentPicker.tsx・lib/errors/error-content-label.ts とそのテスト・指示書AY・指示書BJ・本書)を確かめ、★触らない・コミットに含めない★。npx vitest run の件数(前)を控える(53f46e0 で 170ファイル・4,306件)
  2. K1(テストだけ)+G3 の赤の確認
  3. K2(テストだけ)+G3 の3通りの赤の確認
  4. K3・K4・K5(テストだけ)+K5 の G3
  5. 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)
  6. 記録(§4)
  7. 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 の「本書と違う判断: なし」も消さず、下の項で補う)。

- ★指示書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 で動かしていることがある — ★今回は画面の確認が無いので起動しない★