| 項目 | 内容 | ||
|---|---|---|---|
| 実行環境 | ★Cursor 内の Claude が実装します★(.claude/commands/d-watch.md の役割分担表を参照) |
||
| 実行状態 | ★★完了(群1〜群3 の全段階。2026-09-13)★★ — ★決定番号 D-121〜D-127★。★詳細は文書末尾の「実行状態」表★。 ★★残した積み残しが2つあります★★: ★段階3-1 ①(同一エリア同日2回清掃の二重計上。★仕様判断が要るため見送り★)/ ⑤( docs/handoff/analysis/code-analysis-pure.md の古い転記。★別エージェント作業中のため見送り★)★。★完了時の実測: tsc = 0 / lint = 0 / npm test = 50ファイル / 1200テスト 全パス(着手前 1192 → 段階3-1 ② で8件増)★ |
||
| 位置づけ | ★ユーザー指示(2026-09-13)★ ①「★『上位10件に戻す』を押せるとわかるように他と同じようにリンク化★」/②「★他にSQLインジェクションなどのセキュリティ面も強化★」に対応する1本です。③ として監督の照合で挙がった小さな積み残しを付けてあります | ||
| 実行順 | ★★群1 → 群2 → 群3★★(★群どうしは独立なので入れ替えても壊れませんが、この順が一番短く区切れます★) | ||
| 対象 | ★本書に全部書いてあります★(他の指示書を参照しません) | ||
| ★着手前確認★ | ★★なし★★ — ★①②はユーザー指示(2026-09-13)で確定、③は監督判断★。★★ユーザーの回答を待たずに群1 から実行してよい★★。 ★ただし「やらない」という判断をしてよい段階が2つあります★(★段階2-4 のCSV★ / ★段階2-7 のセキュリティヘッダ★)。★どちらも「やらない」で構いませんが、その場合は理由を docs/decisions.md に書くこと★ |
||
| 取得する決定番号 | ★★**着手直前に `grep -nE "^### D-(0\ | 1)[0-9][0-9]" docs/decisions.md \ | tail -3で最終番号を実測すること**★★。★★^### D-0だけでは3桁台(D-100 以降)を拾えない★★。★2026-09-07 にD-068` の二重採番が実際に起きています★。★★本書には見込み番号を書きません★★。★群ごとに1つ取るのが自然です(群1 はモックから意図的に外れる記録、群2 は禁止事項の機械化、群3 は積み残し)★ |
| 段階数 | ★群1★ = 段階1-0〜1-1(2段階。コミット1つ)/ ★群2★ = 段階2-0〜2-8(9段階。コミット最大6つ)/ ★群3★ = 段階3-0〜3-2(3段階。コミット最大2つ) | ||
| 着手前の基準 | ★★着手直前に自分で npm test / npx tsc --noEmit / npm run lint を実行し、その実測値を基準にすること★★。★作成時点の実測(2026-09-13。★★鵜呑みにしない★★): npm test = 48ファイル / 1137テスト(全パス) / tsc = 0 / lint = 0 / db.cleanLogs() = 538件 / db.prechecks() = 58件★ |
||
| 作成 | 2026-09-13(作成時点の決定事項最終番号 D-120(docs/decisions.md:7762)/ HEAD = 041a809 / ブランチ = develop。★着手時に実測すること★) |
実行後はこの表と、文書末尾の「実行状態」表を更新してください。
★この指示書を読んでいるあなたへ★ —
.claude/commands/d-watch.mdの「実装は Cursor 側」は 2つの環境の役割分担です。Cursor の中で動いている Claude は実装担当です。 ★調査や改訂ではなく実装を行ってください。★ ★★途中で止めてよい指示書です★★ — ★各群の終わりに「★ここで止めてもよい★」と明記してあります★。
★★作成時点の実測では、アプリコードに実際に危険な箇所は見つかっていません★★。
| 調べたこと | 実測結果(2026-09-13) |
|---|---|
生SQL($queryRaw / $executeRaw / *Unsafe / Prisma.raw) |
★アプリコードに1件も無い★(lib/generated/** = Prisma の生成物にある型宣言のみ) |
orderBy のカラム名 |
★固定リテラル1箇所だけ★(lib/db/index.ts:673 の orderBy: { reported_at: "desc" })。★sort / order というクエリパラメータはリポジトリ全体に存在しない★ |
take / skip にユーザー入力 |
★渡している箇所は無い★。Prisma への take は lib/db/index.ts:674 の take: 1 のみ |
| エラーレスポンスへの内部情報 | ★err.message / スタックトレース / 接続文字列 / テーブル名を本文に載せている箇所は1つも無い★。全 catch が固定文言+details: null |
| DBへの書き込み | ★アプリコードに1件も無い★(scripts/seed-local-db.ts はローカル検証用シードで、アプリの書き込みではない) |
| リポジトリ内の認証情報 | ★実在の値は無い★(pw / s3cret / root@localhost / example.com 等のダミーのみ) |
★★だから本書の群2 は「穴を塞ぐ」作業ではなく、「いま安全であることを、人の注意力に頼らず機械で固定する」作業です★★。
★★無理に脆弱性を作り出さないこと★★(ユーザー指示 2026-09-13)。 ★「直すところが無い」は失敗ではありません★。★段階2-0 の棚卸しで「無い」と確認できたら、 それをそのまま記録して次へ進んでください★。 ★★「何か直さないと成果に見えない」と考えて、動いているコードを触らないこと★★。
★★アプリコードではなく、DB接続の経路です★★。→ ★詳細は段階2-8(申し送り)★。
have_ssl=DISABLED / require_secure_transport=OFF / 参照用アカウント@%)npm audit が mariadb コネクタ(@prisma/adapter-mariadb の依存)に ★「ssl: true を指定してもMitMに平文パスワードが漏れる」★ と ★「機密情報の平文送信」★ を報告している。★★No fix available(上げても直らない)★★lib/db/prisma.ts:38 は new PrismaMariaDb(databaseUrl) で、★TLSの指定が無い★★この2つが重なると、DB接続の資格情報が経路上で保護されていない状態になります★。 ★★これは DFA のアプリコードでは直せません(TeamWorks とインフラ側の話)★★。 ★段階2-8 で「申し送り」として文書に落とし、コードは触らないこと★。
| # | 決めたこと | どこ |
|---|---|---|
| 1 | ★collapseBtn は color だけを var(--pr) に変える★(★クラスを消したり、別モジュールから composes したりしない★) |
段階1-1 |
| 2 | ★生SQL禁止は ESLint ルールで機械化する★(★grep を手順書に書くだけにしない★) | 段階2-1 |
| 3 | ★settings/advice の PATCH に長さ上限を入れる★(★lib/config/ の定数として外出し★) |
段階2-3 |
| 4 | ★トークンの定数時間比較はすでに実装済みなので何もしない★ | 段階2-5 |
docs/handoff/指示書AC_絞り込みをSQLに降ろす.md と★前後どちらでもよい★lib/db/index.ts と lib/*/query.ts を 大きく書き換えます。本書の段階2-1(ESLint ルール)と段階2-2(パスパラメータ)が 同じ時間帯に同じ木を触ると、どちらの検証結果も信用できなくなります★docs/handoff.md の先頭)は、★本書の成果を追記した後★に 直すのが自然です(直後にまた古くなるため)★npx tsc --noEmit / npm run lint / npm test の3つが通ること★★ (★群の中の各段階でも通すこと。段階割りはそうなるように組んであります★)git add -A 禁止★★(CLAUDE.md §7。★過去に別作業の途中ファイルを巻き込む事故あり★)— ★★必ずファイルパスを明示して git add すること★★docs/decisions.md をコミットするときは git diff docs/decisions.md を先に見ること★★ — ★未コミットの追記が同居していると巻き込みます★(★指示書W で実際に起きた★)docs/handoff/ 配下の他ファイルに触らないこと★★ — ★★別のエージェントが docs/handoff/指示書AC_絞り込みをSQLに降ろす.md を作成中です★★。 ★本書が触ってよいのは docs/handoff.md(群3-1)と本書自身だけ★git add -A の使用★★ — ★ファイルパスを明示する★(CLAUDE.md §7)robot_error_reports / control_error_histories の status / comment / responded_at / resolved_at ★以外★への DB 書き込み★★(D-042)。 ★★本書のどの群も、アプリコードから DB に書き込みません★★lib/config/ から読む★(CLAUDE.md §7)。 ★段階2-3 の長さ上限を数値で直書きしないこと★lib/fixtures/ の既存の値を変更すること★★ — ★本書はどの群も fixtures の値を変えません★var(--pr) だけ。 これは★同じフッタの隣のボタンが既に使っている色★です(モック d-watch-mock.jsx:818)★npm audit fix --force を使うこと★★ — ★prisma@6 へのダウングレードを含む破壊的変更が走ります★docs/ にも commit message にも残さないこと★README.md / docs/code-map.md / docs/handoff/管理画面_設計検討_20260910.md / ★.claude/worktrees/**★ / ★docs/handoff/ 配下の本書以外のファイル(docs/handoff.md は除く)★process.exit(0) を書くこと★★(★Prisma の接続が残ってハングした事故あり★)SELECT のみ★★CLAUDE.md §7)— ★止めて確認を求める★★施設別パフォーマンス表のフッタにある「上位10件に戻す」が、★押せるように見えない★★。 ★同じフッタの「全N施設を表示 →」はリンク色(var(--pr))なのに、 「上位10件に戻す」だけ地味な文字色(var(--tx-m))で、ただのラベルに見えます★。 ★★これを隣のボタンと同じ見た目に揃えます★★。
★CLAUDE.md §5 は「モック(d-watch-mock.jsx)が画面レイアウト・文言・遷移の正」と定めています★。 ★★そして現状の実装は、モックに忠実です★★:
d-watch-mock.jsx:818 ... style={{ fontSize: 12, color: c.pr, ... }}>全{RANK.length}施設を表示 →
d-watch-mock.jsx:823 ... style={{ fontSize: 12, color: c.txM, ... }}>上位10件に戻す
★★つまり「実装ミス」ではありません★★。★モックがそう描いていたので、そのとおり移植してあります★。
★★今回はユーザー指示(2026-09-13)でモックより利用者の判断を優先します★★。 ★指示書AA(D-120)が「モックに無いUIを例外として新規に作った」のと同じ扱いです★。 ★★docs/decisions.md に「モックから意図的に外れた」と理由つきで記録すること★★ — ★記録しないと、次にモックと実装を突き合わせた人が「バグだ」と言って元に戻します★。
★★行番号を信じないこと★★。★次を自分で実測して控えること★:
grep -n "expandBtn\|collapseBtn" components/features/reports/FacilityPerformanceTable.tsx grep -n "^\.expandBtn\|^\.collapseBtn" components/features/reports/FacilityPerformanceTable.module.css grep -n "^\.linkBtn\|^\.linkBtnMuted" components/features/reports/ReportsScreen.module.css grep -n "上位10件に戻す" lib/i18n.ts
★作成時点の実測(2026-09-13。★鵜呑みにしない★)★:
| 場所 | 内容 |
|---|---|
components/features/reports/FacilityPerformanceTable.tsx:251 |
「全N施設を表示 →」= styles.expandBtn |
components/features/reports/FacilityPerformanceTable.tsx:260 |
「上位10件に戻す」= styles.collapseBtn |
components/features/reports/FacilityPerformanceTable.module.css:107 |
.expandBtn → color: var(--pr) |
components/features/reports/FacilityPerformanceTable.module.css:115 |
.collapseBtn → color: var(--tx-m) |
components/features/reports/ReportsScreen.module.css:210 |
.linkBtn → ★.expandBtn と完全に同一の4宣言★ |
components/features/reports/ReportsScreen.module.css:218 |
.linkBtnMuted → ★.collapseBtn と完全に同一。★どこからも使われていない(デッドCSS)★★ |
lib/i18n.ts:88 |
上位10件に戻す: "Back to top 10" ★既に登録済み★ |
★★.expandBtn と .collapseBtn の差は color の1宣言だけです★★:
.expandBtn { font-size: 12px; color: var(--pr); background: none; border: none; cursor: pointer; }
.collapseBtn{ font-size: 12px; color: var(--tx-m); background: none; border: none; cursor: pointer; }
★cursor: pointer は両方に既にあります★。★足りないのは色だけです★。
collapseBtn の色を揃える★(1コミット)components/features/reports/FacilityPerformanceTable.module.css の .collapseBtn:
.collapseBtn {
font-size: 12px;
color: var(--pr); /* ← ここだけ。var(--tx-m) から変える */
background: none;
border: none;
cursor: pointer;
}
★コメントを添えること★(★次の人がモックと突き合わせて戻さないように★):
/* ★モックは c.txM(地味な色)だが、押せると分からないためリンク色に揃えた★ (ユーザー指示 2026-09-13 / D-xxx)。★隣の .expandBtn と同じ見た目にする★ */
★D-xxx は着手時に実測した番号に置き換えること★。
| ❌ やらない | なぜ |
|---|---|
★.collapseBtn を消して TSX で styles.expandBtn を使い回す★ |
★クラス名は「どちらのボタンか」を表しています★。消すと module.css:100 のコメント(/* フッタ(全N施設を表示 → / 上位10件に戻す)... */)と食い違う |
★composes: linkBtn from "./ReportsScreen.module.css" を書く★ |
★表コンポーネントが画面コンポーネントの CSS に依存します★。★責務の境界(FacilityPerformanceTable.tsx:22-29 のコメント)を壊す★ |
★ReportsScreen.module.css / ReportsScreen.tsx を触る★ |
★ユーザー指示で明示的に禁止★(「ReportsScreen.tsx の他の部分に差分を出さないこと」) |
★text-decoration: underline を足す★ |
★隣の .expandBtn に下線はありません★。★「既存の作法に合わせる」=下線なし・色だけ★ |
| ★文言を変える★ | ★t("上位10件に戻す") のまま★。★lib/i18n.ts:88 に対訳も既にある★ |
★:hover を新設する★ |
★.expandBtn に :hover はありません★。★片方だけ足すと非対称になります★ |
★.linkBtnMuted(デッドCSS)を消す★ |
★本書の範囲外★。★ReportsScreen.module.css に差分を出さない★ |
npx tsc --noEmit / npm run lint / npm test が通ること★ (★CSS だけの変更なので、テスト件数は48ファイル / 1137件から1件も増減しないはずです★。 ★増減したら手が滑っています★)npm run dev → レポート画面(/reports)→ 施設別パフォーマンス表の「全N施設を表示 →」を押す → フッタが「上位10件に戻す」に変わる★
--pr は app/globals.css:7(ダーク #3b82f6)と :27(ライト #2563eb)の ★両方に定義があります★ので、新しい配色を発明せずに両テーマで成立します)Back to top 10 になる。★器を壊さない★)★表を10件以下に絞ると「全N施設を表示 →」自体が出ません★ (
FacilityPerformanceTable.tsx:163のshowExpand = !showAll && totalCount > items.length)。 ★確認するときは施設が11件以上出る期間・絞り込みを選ぶこと★。
.collapseBtn の color が var(--pr) になっている(★変更は CSS の1宣言のみ★)git diff --stat が FacilityPerformanceTable.module.css の1ファイルだけ★tsc 0 / lint 0 / npm test が着手前と同じ件数で全パスdocs/decisions.md に ★「モックから意図的に外れた」と理由つき★ で記録★★ここで止めてもよい★★ — ★ユーザー指示①はこれで完結しています★。
★★「人が気をつける」で終わらせないこと★★(ユーザー指示 2026-09-13)。 ★禁止事項を文書に書き足すだけなら、次の実装者は読まずに書きます★。 ★★npm run lint で落ちる形にすること★★ — ★CI(.github/workflows/ci.yml:29)が npm run lint を走らせているので、ESLint ルールにすれば自動的に PR で検知されます★。
★★自分で実測すること★★。★下の表は作成時点(2026-09-13)の実測値で、★鵜呑みにしないこと★★。
# 生SQL(★lib/generated は Prisma の生成物なので除外して数える★)
grep -rn 'queryRaw\|executeRaw\|Unsafe\|Prisma\.raw' --include=*.ts --include=*.tsx app lib components scripts \
| grep -v '^lib/generated/'
# orderBy と sort/order パラメータ
grep -rn 'orderBy' --include=*.ts app lib | grep -v '^lib/generated/'
grep -rn '"sort"\|"order"' --include=*.ts --include=*.tsx app lib components
# API ルートの一覧と searchParams の扱い
find app/api -name route.ts | sort
grep -rln 'searchParams' app/api
# リポジトリ内の認証情報(★ヒットは全部ダミーのはず★)
git grep -nIE "mysql://[^U]|password[[:space:]]*[:=][[:space:]]*[\"'][^\"']{4,}" -- ':!node_modules' ':!docs'
| 調査項目 | 結果 |
|---|---|
| 生SQL | ★0件★(lib/generated/prisma/internal/*.ts の型宣言のみ。★あれは Prisma のメソッド定義で、呼び出しではない★) |
orderBy |
★1箇所のみ★ lib/db/index.ts:673 orderBy: { reported_at: "desc" }(★固定リテラル★) |
sort / order クエリパラメータ |
★リポジトリ全体に存在しない★。並べ替えは全部 JS 側の固定コンパレータ(sortDsAlerts 等) |
where にユーザー入力 |
★無い★。where は deleted_at: null 等の固定条件のみ。絞り込みは全部 JS 側(filterXxx) |
Prisma への take / skip |
★ユーザー入力からは渡していない★。take: 1(lib/db/index.ts:674)のみ |
per_page / limit のクランプ |
★7つの parseXxxQuery() すべてが Math.min(PAGINATION.maxPerPage, Math.max(1, toInt(...)))★。★maxPerPage = 200(lib/config/index.ts:177-193)★ |
| エラー本文への内部情報 | ★0件★。全 catch が固定文言+details: null。例外は logApiError() でログにだけ出る |
| DB書き込み | ★アプリコードに0件★ |
| リポジトリ内の認証情報 | ★実在の値0件★(pw / s3cret / abc123 / root@localhost / db.example.internal 等のダミーのみ) |
| 守り | 実装 | 状態 |
|---|---|---|
| トークンの定数時間比較 | lib/auth/token.ts:26-44(node:crypto の timingSafeEqual。★長さ違いも同じ時間で false★) |
★実装済み。何もしない★ |
X-Forwarded-For の★末尾★を見る |
lib/auth/client-ip.ts:37-51(parts[parts.length - hops]。★先頭は使わない★。D-077 項5) |
★実装済み★ |
| 足りない段数を先頭で埋めない | lib/auth/client-ip.ts:46(parts.length < hops なら null) |
★実装済み★ |
ALLOW_DIRECT_ACCESS の fail-closed |
lib/config/allowed-ips.ts:56(=== "true"。★!== "false" にしない★。D-101) |
★実装済み★ |
cookie の httpOnly / secure / sameSite |
lib/auth/cookie.ts:29-34 |
★実装済み★ |
| ログの秘匿化 | lib/logger/redact.ts(PII 13種+認証情報9種。★キー名で判定、大文字小文字無視★) |
★実装済み★ |
| ログイン失敗時に入力値を出さない | app/api/auth/login/route.ts:46-51 |
★実装済み★ |
| シードが本番DBに書かないガード | lib/db/local-guard.ts:23-31(★dwatch_local 以外なら例外★) |
★実装済み★ |
エラーに DATABASE_URL の値を出さない |
lib/config/env-check.ts / lib/db/local-guard.ts(★「値はこのエラーには出しません」★) |
★実装済み★ |
★★この表の行を「念のため」書き直さないこと★★ — ★どれも理由つきで確定しています(D-072 / D-077 / D-079 / D-101)★。
★CLAUDE.md §3 は「なるべく MySQL 互換の SQL を使う」「生SQL を書くときは特に注意する」と 書いていますが、★禁止とは書いていません★★。 ★本番は MariaDB 10.11 / ローカル検証は MySQL 8.4 なので、★生SQL はそもそも 「ローカルで通っても本番で通らない」リスクがあります★。 ★★加えて *Unsafe 系と文字列連結は SQL インジェクションの入口そのものです★★。
★grep を検証手順に書くだけでは、次の実装者が読まなければ素通りします★。 ★★npm run lint が CI(.github/workflows/ci.yml:29)で走っているので、ESLint ルールにすれば PR で自動的に落ちます★★。
eslint.config.mjs に ★no-restricted-syntax★ のブロックを1つ追加する。
// ★生SQL禁止(指示書AE 段階2-1 / D-xxx)★
// SQLインジェクションの入口を構造的に塞ぐ。Prisma のクエリビルダ経由に統一する。
// ★lib/generated/** は先頭の ignores で除外済み★ —
// あそこには Prisma が生成した $queryRaw の「型宣言」があり、呼び出しではない。
// ★どうしても生SQLが必要になったら、このルールを消すのではなく
// タグ付きテンプレート(`prisma.$queryRaw`...`)だけを個別に eslint-disable し、
// 理由を docs/decisions.md に書くこと。★文字列連結と *Unsafe は例外を作らない★
{
files: ["app/**", "lib/**", "components/**", "scripts/**", "proxy.ts"],
rules: {
"no-restricted-syntax": [
"error",
{
selector:
"MemberExpression[property.name=/^\\$(query|execute)Raw(Unsafe)?$/]",
message:
"生SQLは使わないこと(CLAUDE.md §3 / 指示書AE 段階2-1)。Prisma のクエリビルダを使う。",
},
{
selector: "MemberExpression[object.name='Prisma'][property.name='raw']",
message:
"Prisma.raw は使わないこと(指示書AE 段階2-1)。Prisma のクエリビルダを使う。",
},
],
},
},
★D-xxx は着手時に実測した番号に置き換えること★。
★★書いただけで満足しないこと★★。★セレクタのタイポは「常に何も検知しない」形で失敗します★。
lib/ 配下のファイルへ次の1行を足す:
const _tmp = () => prisma.$queryRawUnsafe("SELECT 1");npm run lint を走らせ、★その行でエラーになること★を目で確認する$queryRaw(Unsafe なし)と Prisma.raw でも同様に確認する★git diff が eslint.config.mjs だけになっていることを確認する★npm run lint が ★0★ に戻ることを確認する★CLAUDE.md は本書では触りません★(★本書の担当外★)。★代わりに docs/decisions.md の 決定エントリに、次の3つを禁止として書くこと★:
$queryRawUnsafe / $executeRawUnsafe(★例外なし★)orderBy のカラム名をユーザー入力から組み立てること(★許可リストのみ★)$queryRaw / $executeRaw の★タグ付きテンプレート★は、★どうしても必要な場合に限り★ 個別 eslint-disable +理由の記録で許す。★★その箇所を TeamWorks に知らせること★★ (CLAUDE.md §3 の「MariaDB 特有の DML を使いたい場合は TeamWorks に知らせる」)eslint.config.mjs にルールが入っているnpm run lint が 0npx tsc --noEmit 0 / npm test が着手前と同じ件数で全パスeslint.config.mjs のみ★)| ルート | 現状 |
|---|---|
app/api/clean-logs/[id]/advice/route.ts:33-37 |
Number.parseInt(id, 10) → Number.isNaN のみ 422 |
app/api/clean-logs/[id]/areas/route.ts:47-51 |
★同一実装★ |
app/api/ds-devices/[serial_number]/note/route.ts:48 |
★形式・長さとも未検証★ |
★★Number.parseInt は前方一致なので、次が黙って通ります★★:
| 入力 | parseInt の結果 |
|---|---|
-5 |
-5(★負数★) |
12abc |
12(★ゴミが付いていても通る★) |
1.9 |
1(★非整数が切り捨てられる★) |
1e9 |
1(★指数表記が意図と違う値になる★) |
★★この値は DB クエリにもファイルパスにも渡りません★★。 ★用途は logs.find((l) => l.id === logId) の等値比較だけで、一致しなければ 404 です★ (clean-logs/[id]/advice/route.ts:48-51 / [id]/areas/route.ts:70)。 ★serial_number も devices.some(...) の実在チェックを通らなければ 404 で、 保存先のパスは lib/settings/ds-device-notes.ts:53-55 で固定、 マップは Object.create(null) です★。
★★だからこれは「穴を塞ぐ」ではなく「入口を厳しくして、将来 DB に降ろしたときに崩れないようにする」作業です★★。 ★★指示書AC が絞り込みを SQL に降ろすので、そのとき id が Prisma の where に入ります★★。 ★その前に入口を締めておくのが本段階の意味です★。
★共通の小さな純粋関数を1つ作り、3ルートから呼ぶ★。★置き場所は lib/api/ が自然です★ (既に lib/api/query-result.ts の fail() がある)。
^\d+$ の完全一致で判定する★(★parseInt の前方一致に頼らない★)。 ★lib/reports/query.ts:88-92 の toIntStrict(^-?\d+$)が★既にある前例★なので、 それに倣うこと。★新しい流儀を発明しない★Number.isSafeInteger を超える値を弾く★。 ★上限値は lib/config/ の定数として外出しすること★(CLAUDE.md §7。★直書き禁止★)0 を弾く★(★ID は 1 以上★)validation_failed★(★既存ルートと同じ形★)。 ★★エラー文言に入力値をエコーしないこと★★serial_number は★長さ上限★と★文字種★を決めて検証する★。 ★★実データの形式が不明なら、長さ上限だけにして文字種は絞らないこと★★ (CLAUDE.md §7「仕様が不明な箇所を推測で実装しない」)。 ★長さ上限は lib/config/ の定数に★★lib/ 配下の該当する __tests__/ に純粋関数のテストを追加する★。★期待値表を先に作ること★:
| 入力 | 期待 |
|---|---|
"1" |
通る(1) |
"538" |
通る(538) |
"0" |
★422★ |
"-5" |
★422★ |
"1.9" |
★422★ |
"12abc" |
★422★ |
"1e9" |
★422★ |
"" |
★422★ |
"9007199254740993"(安全整数超) |
★422★ |
全角数字 "1" |
★422★ |
lib/config/ の定数★(★直書きしていない★)tsc 0 / lint 0 / npm test 全パス(★テストは増える★)settings/advice の PATCH に長さ上限を入れる★★(1コミット)★app/api/settings/advice/route.ts:76-81 の検証は★型チェックだけ★です★:
| フィールド | 現状の検証 | 足りないもの |
|---|---|---|
rule(ruleId) |
★DEFAULT_ADVICE_RULES[ruleId] の存在チェック★ |
★★十分(許可リスト方式)★★ |
title |
typeof === "string" |
★長さ上限なし★ |
text |
typeof === "string" |
★長さ上限なし★ |
actions |
Array.isArray +要素の typeof === "string" |
★配列長の上限なし・要素長の上限なし★ |
★★これはすぐ隣の実装と非対称です★★:
app/api/ds-devices/[serial_number]/note/route.ts:69-76 は ★DEVICE_NOTE_MAX_LENGTH(= 200、lib/config/index.ts:236)で上限をかけています★lib/config/index.ts:230-235 に書いてあります★:
上限が無いと1件の備考でストアのファイル全体が肥大し、 ★全件読み込み→書き戻しという保存方式ごと遅くなる★ため。
settings/advice はまったく同じ保存方式です★★(lib/settings/advice.ts:126-139 が JSON ストアを全件読み→書き戻し)。★同じ理由が同じだけ当てはまるのに、上限だけ無い★★さらに、ここに入れた値は GET /settings/advice と /clean-logs/[id]/advice(app/api/clean-logs/[id]/advice/route.ts:85-96)で ★そのまま画面に返ります★★。
★lib/config/index.ts に定数を足し、DEVICE_NOTE_MAX_LENGTH と同じ書き方で理由を書く★。
★★値は次を推奨します(★実行者が変えてよい。変えたら理由を書くこと★)★★ — ★lib/config/advice-rules.ts の DEFAULT_ADVICE_RULES の実物から逆算した数字です★:
| 定数 | 推奨値 | 根拠(★着手時に実物を見て確かめること★) |
|---|---|---|
ADVICE_TITLE_MAX_LENGTH |
100 |
★既定値は「出発失敗(R1)」等で15文字前後★。余裕を見て100 |
ADVICE_TEXT_MAX_LENGTH |
400 |
★既定値は60文字前後★。R2 の「現場の通路状況+…」が最長 |
ADVICE_ACTION_MAX_LENGTH |
100 |
★既定値は15文字前後★ |
ADVICE_ACTIONS_MAX_ITEMS |
10 |
★既定値は3件★ |
★超過したら 422 validation_failed★。★details には上限値だけを入れ、入力値をエコーしないこと★ (★ds-devices/[serial_number]/note/route.ts:74 が DEVICE_NOTE_MAX_LENGTH を details に 入れている前例に倣う★)。
actions の中身を許可リストで縛ること★ — ★自由入力のメモです★DEFAULT_ADVICE_RULES)を書き換えること★lib/config/ へ★)ds-device-notes 側の DEVICE_NOTE_MAX_LENGTH(= 200)を変えること★★既存の settings/advice のテストに境界値を足す★:
| 入力 | 期待 |
|---|---|
title = 上限ちょうど |
通る |
title = 上限+1 |
★422★ |
text = 上限+1 |
★422★ |
actions = 上限件数ちょうど |
通る |
actions = 上限件数+1 |
★422★ |
actions の1要素が要素長上限+1 |
★422★ |
lib/config/index.ts にあり、★DEVICE_NOTE_MAX_LENGTH と同じ密度で理由が書いてある★app/api/settings/advice/route.ts が4定数すべてで検証しているdetails に入力値をエコーしていない★tsc 0 / lint 0 / npm test 全パスapp/api/clean-logs/export/route.ts:18-23 の csvCell():
function csvCell(value: string): string {
if (/[",\n\r]/.test(value)) {
return `"${value.replaceAll('"', '""')}"`;
}
return value;
}
★" , 改行は正しくエスケープしています★。 ★★ただし、先頭が = + - @ の値は表計算ソフトが数式として解釈します★★ (いわゆる CSV インジェクション)。 ★出力される値の出所は DB 由来の施設名・機種名・シリアルで、 これらを書いているのは★TeamWorks のデータ収集処理★です(CLAUDE.md §2)。 ★★つまり DFA 側でその内容を保証できません★★。
★★これは「やらない」で構いません★★。理由:
CLAUDE.md §1)で、CSV を開くのは運用担当だけ' を付ける等)は★出力される CSV の中身が変わる★ため、 ★受け取る側の手順に影響します★★★どちらを選んでもよいので、選んだ理由を docs/decisions.md に書くこと★★。
csvCell() の中だけで完結させる。★★' を前置する方式にし、 条件(先頭が = + - @ のとき)と理由をコメントに書く★★。 ★テストを必ず足すこと★(★=SUM(1) → '=SUM(1)、正常な施設名 → そのまま★)。 ★既存の "/,/改行のエスケープを壊さないこと★csvCell() の上に「★検討したうえで P1 では対策しない★」とコメントを1つ残すこと★。 ★次に見た人が同じ調査を最初からやり直さずに済みます★★app/api/clean-logs/export/route.ts:79-81 が、parseCleanLogQuery を通した後に ★生の searchParams をもう一度読んで★ Content-Disposition のファイル名に入れています★:
const from = searchParams.get("date_from") ?? "";
const to = searchParams.get("date_to") ?? "";
const range = from && to ? `_${from}_${to}` : "";
★★いまは安全です★★ — ★parseCleanLogQuery が先に ^\d{4}-\d{2}-\d{2}$ (lib/clean-logs/query.ts:28)で弾いているので、ヘッダに制御文字は入りません★。 ★★ただし「検証した場所」と「使う場所」が別の関数に分かれた暗黙の依存です★★。
★★直さなくてよいですが、コメントを1行足すこと★★:
// ★生値を使ってよいのは、上の parseCleanLogQuery が ^\d{4}-\d{2}-\d{2}$ で
// 既に弾いているから★。★この検証を緩めるならここも直すこと★
★parsed.query 側に正規化済みの値があるなら、そちらを使うほうが素直です★。 ★★どちらにするかは実行者判断★★。
docs/decisions.md に書いた★Content-Disposition の暗黙依存にコメントを足した★tsc 0 / lint 0 / npm test 全パスdocs/deploy.md に集約する★★(1コミット)★段階2-0 の表のとおり、認証・IP制限まわりはすべて実装済みです★。 ★足りないのは「本番へ出す直前に何を確かめるか」が1箇所にまとまっていないことです★。
★docs/deploy.md には既に材料があります★(★着手時に実測すること★):
| 節 | 内容 |
|---|---|
docs/deploy.md:57 |
★ALLOW_DIRECT_ACCESS は本番で設定しないこと★ |
docs/deploy.md:74 |
★X-Forwarded-For に「追記」すること(D-077 項5)★ |
docs/deploy.md:88 |
★アプリのポートに nginx 以外が到達できないようにすること★ |
docs/deploy.md:125 |
★許可IPリストの変更にはデプロイが必要★ |
★チェックボックス形式で、★それぞれに「どう確かめるか」のコマンドか観点★を添えること★。
| # | 確認項目 | 確かめ方 |
|---|---|---|
| 1 | ★AUTH_TOKEN が changeme のままでない★ |
★npm run check:env★(lib/config/env-check.ts:56-70 が★エラーで落とします★) |
| 2 | ★ALLOW_DIRECT_ACCESS が本番で未設定★ |
★npm run check:env★(検査2。★本番判定でエラー★) |
| 3 | ★USE_FIXTURES=false★ |
★npm run check:env★(検査3。★未設定だと true 扱いで、実DBに繋がらないまま動きます★) |
| 4 | ★ALLOW_ALL_WHEN_EMPTY = false かつ ALLOWED_IPS が埋まっている★ |
★lib/config/allowed-ips.ts を目視★。★★環境変数ではないのでデプロイが必要★★ |
| 5 | ★TRUSTED_PROXY_HOPS が実構成と合っている★ |
★docs/deploy.md:170 の判定表★。★2段構成になったら 2 にする★ |
| 6 | ★nginx が XFF を「追記」している(上書きでない)★ | ★docs/deploy.md:74 の手順★ |
| 7 | ★リポジトリに認証情報が入っていない★ | ★段階2-0 の git grep コマンド★ |
| 8 | ★npm audit の結果を確認した★ |
★段階2-6★ |
★項目4 と 5 は★コードに書いてあるので .env.local を直しても変わりません★ (docs/deploy.md:125-139 / .env.production.example の末尾)。 ★★デプロイ担当がここを見落とすと、ALLOW_ALL_WHEN_EMPTY = true のまま本番へ出ます★★。 ★チェックリストに入れる意味はここにあります★。
★docs/deploy.md の同じ節に、★過去に実在のDBユーザー名がコミットされた★ことを書くこと★:
c832891(fix: テストから実在のDBユーザー名を除去する)で除去済み★git grep して出ない」=「漏れていない」ではありません★docs/deploy.md にセキュリティ確認の節がある(★8項目+確かめ方★)c832891 の事故と「履歴には残る」ことが書いてある★docs/deploy.md のみ★)npm audit を検証手順に入れ、結果をそのまま記録する★★(1コミット)★★この段階でやるのは「走らせて、出た結果をそのまま書き残す」ことだけです★★。
npm audit fix --force 禁止★(★prisma@6.19.3 へのダウングレードが走ります★)npm audit fix(--force なし)も、★この段階では実行しないこと★★ — ★package-lock.json が動くと、段階2-1〜2-3 の差分と混ざります★8 vulnerabilities (1 moderate, 7 high)
| パッケージ | 経路 | 内容 | 修正 |
|---|---|---|---|
★mariadb 3.4.0-3.4.5★ |
★@prisma/adapter-mariadb(直接依存)★ |
★ssl: true でも MitM に平文パスワードが漏れる★ / ★機密情報の平文送信★ / ★特定クライアント文字セット(big5 / gbk / sjis / cp932 / gb18030)下での SQL インジェクション★ |
★★No fix available★★ |
mysql2 ≤3.23.0 |
prisma 経由 |
認証プラグインのダウングレードで平文資格情報が漏れる / 圧縮プロトコルの解凍爆弾 | --force のみ(★使わない★) |
deepmerge-ts <8.0.0 |
@prisma/config → prisma |
再帰オブジェクトでスタック枯渇 | --force のみ(★使わない★) |
js-yaml 4.0.0-4.3.1 |
— | maxTotalMergeKeys が CPU 使用を制限しない |
npm audit fix で可 |
sharp <0.35.4 |
— | libheif 由来 | npm audit fix で可 |
mariadb の SQL インジェクション勧告について(★確かめること★)★★★★これは「アプリコードに生SQLが無い」では防げない層の話です★★ — ★コネクタがパラメータをエスケープする処理の問題だからです★。
★★ただし、影響を受けるのは特定のクライアント文字セット(big5 / gbk / sjis / cp932 / gb18030)の場合だけです★★。
★作成時点の実測では lib/db/prisma.ts:38 は new PrismaMariaDb(databaseUrl) で、 DATABASE_URL(.env.example / .env.production.example)にも文字セット指定はありません★。 ★★つまり既定(utf8mb4)で、該当しないと読めます★★。
★★着手時に自分で確かめること★★:
grep -n "charset\|collation" .env.example .env.production.example lib/db/prisma.ts
★★DATABASE_URL に charset= / collation= が入っていないこと★★を確認し、 ★その事実を記録すること★。 ★★もし該当する文字セットが指定されていたら、そこで止めてユーザーに報告すること★★ (★勝手に変えない★)。
npm audit を走らせ、★出力をそのまま★ docs/deploy.md のセキュリティ節(段階2-5 で作った節)に ★日付つきで貼る★mariadb の「No fix available」を段階2-8 の申し送りに繋げること★★package-lock.json を変更しないこと★(★git diff --stat で確認★)npm audit の結果が日付つきで記録されているpackage-lock.json に差分が無い★npm audit fix を実行していない★docs/deploy.md のみ★)next.config.ts は serverExternalPackages: ["pino"] だけで、★headers() の設定はありません★。 ★つまり X-Content-Type-Options / Referrer-Policy / X-Frame-Options / CSP のいずれも出ていません★。
★★「やらない」で構いません★★。理由:
docs/deploy.md:72 に既に「nginx 側の前提」の節があります★★★推奨: アプリには入れず、docs/deploy.md の「nginx 側の前提」に申し送りとして書く★★。
X-Content-Type-Options: nosniff / Referrer-Policy: same-origin / X-Frame-Options: DENY の3つだけにする★。 ★入れたら npm run dev で全画面を開いて壊れていないことを確認すること★docs/deploy.md の nginx 節に「★アプリ側では付けていない。nginx で付けること★」と書く★docs/decisions.md に書いた★tsc 0 / lint 0 / npm test 全パス★★DFA のアプリコードに手を入れても解決しない問題です★★。 ★★文書に落として、TeamWorks とインフラ側に渡すこと★★。
★docs/handoff.md に追記する★(★★docs/handoff/ 配下の他の新規ファイルを作らないこと★★ — ★別エージェントが指示書AC を作成中です★)。
| # | 事実 | 依頼したいこと |
|---|---|---|
| 1 | ★テスト用DBが公開IPに平文で開いている★(★監督実測★: have_ssl=DISABLED / require_secure_transport=OFF / 参照用アカウント@%) |
★接続元の制限(参照用アカウント@% を★固定IP★に)★ / ★TLS の有効化★ / ★パスワードの再発行★ |
| 2 | ★npm audit が mariadb コネクタに「ssl: true でも MitM に平文パスワードが漏れる」を報告。★No fix available★★ |
★★TLS を有効にしても、このコネクタでは資格情報が保護されない可能性がある★★。★経路そのものを閉じる(固定IP / 内部ネットワーク)ことを優先してほしい★ |
| 3 | ★過去に実在のDBユーザー名がコミットされ c832891 で除去。★履歴には残っている★★ |
★★本番の資格情報は、履歴に入った値と別のものを発行すること★★ |
| 4 | ★本番はさくらインターネットのデータベースアプライアンス★(CLAUDE.md §3 / D-100) |
★★初めから閉じた構成になる見込みだが、明示の確認は未取得★★(D-100)。★確認したい★ |
★★「ステージングに実データが入る前に決めること」★★ — ★これが期限です★。 ★実データが入った後では、平文経路を通った資格情報を前提に運用が始まってしまいます★。
CLAUDE.md §9.3。★★docs/ はリポジトリに入ります★★)docs/handoff.md に申し送りが追記されている(★4項目+期限★)★★ここで止めてもよい★★ — ★ユーザー指示②はこれで完結しています★。
docs/handoff.md の先頭を直す(★実害あり。必ずやること★)★★(1コミット)docs/handoff.md:6:
- ★**最新の追記: 2026-09-11(追記36)**★ — **下の「追記36」が最新です。**
★★実際の最終追記は「追記40」です★★(docs/handoff.md:3363 ## 2026-09-11(追記40))。
★★この文書は、自分自身で警告を書いています★★:
docs/handoff.md:3 **この文書は日付ごとの追記が下へ積み上がる形式です。必ず最後まで読んでください。** docs/handoff.md:4 先頭だけ読むと最初の追記時点の状態で止まります。
★★警告しておきながら、その先頭行が4件ぶん古い★★。 ★次に読む人は「追記36 が最新」と信じて止まり、★追記37〜40 の成果(指示書AB の4群 = D-117〜D-120)を まるごと見落とします★★。
docs/handoff.md:6 を★実測した最終番号★に直す★ (★★着手時にもう一度数えること★★。★群2-8 で自分が追記していれば、その番号が最新です★)
grep -n "^## .*(追記[0-9]*)" docs/handoff.md | tail -3
docs/handoff.md:817 にも古い記述があります★★:
**最初に読むのは ... 追記7(この節)まで。**★これも stale です★。★直すか、せめて「★この節は古い★」と1行足すこと★
★★同じことが5回起きています(追記36 → 40 で4件放置)★★。 ★docs/handoff.md の先頭に、★「追記したら必ずこの行も直すこと」★と1行書き足すこと★。
:817 の古い記述に手当てをした★docs/handoff.md のみ★)★★どれもいま実害はありません★★。★時間があれば拾う、無ければ飛ばしてよい★。 ★★飛ばす場合は、その旨を作業メモに残すこと★★。
lib/clean-logs/areas.ts:110 buildAreaCompletions()rows.map(...) のみで section_id の一意化なし。 ★分母・分子の積み上げ(:125-131)も全行を無条件に加算★:
for (const r of rows) {
cleanArea += r.clean_area;
taskArea += r.task_area;
}selectAreaRowsForLog()(:55-62)も facility_id と date の一致だけ★CLAUDE.md §7)。 ★★推測で実装しないこと★★buildPageItems() の page に非整数・NaN のガードが無い★lib/pagination/page-items.ts:93-98totalPages には Number.isFinite ガードがあるが、 ★page は clamp() を通るだけ★。★Math.min(max, Math.max(min, NaN)) は NaN を素通りさせます★components/ui/Pagination.tsx:67 の1箇所だけで、 6画面すべて useState(1) の整数 state。★URL クエリから page を読む経路は存在しません★★totalPages と同じ形で Number.isFinite(page) + Math.trunc() を足し、 ★テストに NaN / 2.5 / Infinity を追加する★not_implemented の残骸★lib/api/__tests__/client.test.ts:98 test("501 の定型メッセージも serverMessage で取れる(エリア別完了率)", ...) ★同種がもう1本: lib/logger/__tests__/api.test.ts:81, 94★lib/types/index.ts:28 の ErrorCode に | "not_implemented"app/api/clean-logs/[id]/areas/route.ts:13 にその旨のコメントあり)d-watch-api.yaml には not_implemented も 501 も出てきません★★ (★実測済み。error.code は type: string のまま enum 無し★)。 ★★つまり型から外しても API 契約と乖離しません★★。むしろ CLAUDE.md §9.2 (「型は YAML のスキーマ名と揃える」)に近づきますserverMessage の取り出しは今も有効な検証です★)+ ★ErrorCode から "not_implemented" を外す★docs/handoff/PR説明_develop_to_main_骨子.md:212 / docs/handoff/analysis/code-analysis-impure.md:3195 ★★ただし docs/handoff/ 配下は別エージェントが作業中です。★触るなら git status を先に見ること★★scripts/seed-local-db.ts のコメントの件数がずれている★scripts/seed-local-db.ts:506
* ★分布は `lib/fixtures/prechecks.ts` の8件に倣う(未解消3 / 対応中2 / 解消3)★。
lib/fixtures/prechecks.ts:38 の配列は ★9件★ (id = "1","2","3","9","4","5","6","7","8")。★分布は 未解消4 / 対応中2 / 解消3★id: "9"(prechecks.ts:94、境界値検証用)が反映されていないERROR_RESOLUTION_ROTATION(:508-517)自体は8要素で自己整合しています★★。 ★★ずれているのは説明文のほうなので、コメントだけ直すこと★★。 ★★配列を触らないこと★★(★禁止事項5★)scripts/seed-local-db.ts:268 /** 完了行の何件ごとに 2/3/4 を当てるか(240行なら各5件ずつになる) */ — ABNORMAL_CLEAN_ERROR_EVERY = 17(:269)なので 240行なら該当14行、 ★3値ローテーションで 5/5/4★。★「各5件ずつ」にならない★。 ★★ただし「完了行=240」という前提自体が未検証です。直す前に実測すること★★code-analysis-pure.md の古い転記★docs/handoff/analysis/code-analysis-pure.md の ★4箇所★ (★監督の照合では「3箇所」とされていましたが、実測は4箇所です★):| 行 | 内容 |
|---|---|
| 2977 | 算出基準はTeamWorks確認事項の回答待ちのため、暫定で稼働率ベース。(コード引用ブロック) |
| 3071 | 同文(「63〜67行目のコメント(引用)」の再掲) |
| 3075 | **★「TeamWorks確認事項の回答待ち」= 仕様が未確定である★**。 |
| 3195 | 加えて 65行目「算出基準はTeamWorks確認事項の回答待ち…」 |
lib/judge/kpi.ts:70 これは DFA 側の UI 判断であって、TeamWorks 待ちではない、 kpi.ts:72 (2026-09-11 に全文検索で確認。ヒット0)。以前ここに「回答待ち」と書かれていたのは誤りで、 ★docs/handoff.md:3143(追記37)にも同旨★docs/handoff/analysis/ 配下です★。 ★★別エージェントが docs/handoff/ で作業中なので、触る前に git status を見ること★★。 ★競合しそうならこの項目は飛ばすこと★tsc 0 / lint 0 / npm test 全パスdocs/decisions.md に決定エントリを書く★(★着手時に実測した番号★)
docs/handoff.md に追記する★(★段階3-0 の先頭行も一緒に直すこと★)docs/decisions.md に記録(★git diff docs/decisions.md を先に見た★)docs/handoff.md に追記(★先頭の「最新の追記」も直っている★)★★ここで止めてもよい★★ — ★本書の全群が完了です★。
★★全段階完了(2026-09-13)★★。★ブランチ develop。git push はしていません★。
| 群 | 段階 | 状態 | 決定番号 | コミット |
|---|---|---|---|---|
| 群1 | 1-0 実測 | ★完了★(読むだけ) | — | — |
| 群1 | 1-1 collapseBtn の色 | ★完了★ | ★D-121★ | 25172dc |
| 群2 | 2-0 棚卸し | ★完了★(★危険な箇所は0件★。読むだけ) | — | — |
| 群2 | 2-1 生SQL禁止を ESLint 化 | ★完了★ | ★D-122★ | f90ed25 |
| 群2 | 2-2 パスパラメータ検証 | ★完了★ | ★D-123★ | 76f31e6 |
| 群2 | 2-3 advice の長さ上限 | ★完了★ | ★D-124★ | 25ede81 |
| 群2 | 2-4 CSV(任意) | ★完了(★「やる」を選択★)★ | ★D-125★ | 5d9cc7b |
| 群2 | 2-5 本番前チェックリスト | ★完了★(docs/deploy.md 7節) |
— | e75d319 |
| 群2 | 2-6 npm audit の記録 | ★完了★ | — | 6fc2255 |
| 群2 | 2-7 セキュリティヘッダ(任意) | ★完了(★「アプリに入れない」を選択★)★ | ★D-126★ | 03f7fce |
| 群2 | 2-8 申し送り | ★完了★(docs/handoff.md 追記41) |
— | a592053 |
| 群3 | 3-0 handoff.md の先頭 | ★完了★(追記36 → ★追記41★ に修正。★再発防止の注意書きも追加★) | — | 4c5beeb |
| 群3 | 3-1 軽微な積み残し(任意) | ★完了(★②③④を拾い、★①⑤は見送り★)★ | ★D-127★ | b20790e |
| 群3 | 3-2 記録 | ★完了★(D-127 + docs/handoff.md 追記42 + 本表) |
★D-127★ | (3-2 のコミット) |
| # | 内容 | 判断 | 理由 |
|---|---|---|---|
| ① | ★同一エリアが同日2回清掃されると二重計上★ | ★★見送り★★ | ★どう集約するか(最後の1回 / 合算 / 両方)が仕様判断★。★CLAUDE.md §7 のとおり推測で埋めず、確認を求める★ |
| ② | ★buildPageItems() の page に NaN ガードが無い★ |
★拾った★ | ★normalizePage() に寄せ、pageRange() にも同じガードを適用(同じ clamp() を通っていて同じ穴があった)★。★テスト8件追加(1192 → 1200)★ |
| ③ | ★501 / not_implemented の残骸★ |
★拾った(★コードのみ★)★ | ★ErrorCode から外し、501 前提のテスト2本は消さずに 503 / 404 へ置き換えた★。★docs/handoff/ 配下の文書は別エージェント作業中のため触っていない★ |
| ④ | ★シードのコメントの件数のずれ★ | ★拾った★ | ★実測 9件(未解消4 / 対応中2 / 解消3)に修正。★コメントのみ★。ローテーション表と fixtures は触っていない★ |
| ⑤ | ★code-analysis-pure.md の古い転記★ |
★★見送り★★ | ★docs/handoff/analysis/ は★別エージェントが作業中★。指示書の「競合しそうなら飛ばす」に従った★ |
| 項目 | 結果 |
|---|---|
npx tsc --noEmit |
★0★ |
npm run lint |
★0★ |
npm test |
★50ファイル / 1200テスト 全パス★(着手前 1192 → 段階3-1 ② で★8件増★) |
★★触っていないファイル★★: README.md / docs/code-map.md / docs/handoff/管理画面_設計検討_20260910.md(★いずれも他作業の未コミット変更。着手時に git status で確認★)。