diff --git a/docs/push-pipeline-fix-plan.md b/docs/push-pipeline-fix-plan.md index e1a4f993..049c6b74 100644 --- a/docs/push-pipeline-fix-plan.md +++ b/docs/push-pipeline-fix-plan.md @@ -496,6 +496,59 @@ T1 を最優先とする理由: 以降の全 PR の dogfood push が速くなり stop_quality step から build を削除する。 推奨は (a)。ADR-043 (fail-closed) に整合するのは実体化の方向。 - **リスク**: 低。(a) の場合 tsconfig.json の有無・内容を確認。 +- **実施結果 (2026-07-17, 実装済み / PR #285)**: + - **方針は (a) 実体化 (ユーザー承認済み)**。(b) の前提「TS 資産が実質サンプルのみ」は + 調査の結果**成り立たない**: `scripts/deploy-hooks.ts` (190 行) は `pnpm deploy:hooks` の + 実運用ツールで JSON parse と settings 生成を行い、`scripts/e2e.ts` は `pnpm test:e2e` の + 入口。サンプルは `src/sample.ts` (linter hook の意図的違反 fixture) のみで、 + 型チェックの価値は実在する。 + - **実装** (Rust runtime の変更なし = exe 再ビルド不要): + - `typescript` `^7.0.2` + `@types/node` `^26.1.1` を devDependencies に追加。 + - `tsconfig.json` を新規作成 (strict / target es2022 / module esnext / + moduleResolution bundler / types node)。`module: nodenext` を採らないのは + package.json に `"type": "module"` が無く .ts が CJS 判定 → `deploy-hooks.ts` の + `import.meta.url` が TS1470 になるため。実行系が `tsx` (extensionless import 可) で + あることに合わせて bundler 解決にした。 + - build script: `npx tsc --noEmit --pretty || true` → `npx --no-install tsc --noEmit --pretty`。 + `--no-install` は既存の `npx --no-install markdownlint-cli2` (lint:md) と同じ規約。 + - **既存 ts の型エラーは 0 だった** — 方針欄の「型エラーなら先に修正」は空振り。 + strict + 4 ファイル (deploy-hooks / e2e / logger / sample) で `tsc` は clean。 + ゲート実行コストは **0.6s** (TypeScript 7 の native 実装)。 + - **型チェック範囲はテストを除外 (ユーザー承認済み)**: `src/__tests__/` を exclude。 + 含めると `vitest` が devDependencies に無い (`pnpm test` は `npx vitest run` = 実行時 + 自動 DL) ため TS2307 で落ちる。vitest の devDep 化は「npx 自動 DL の非決定性」という + T3 と別問題なので §6 backlog 12 に分離した (§2 原則 4: 1 PR 1 変更)。 + - **fail-closed の実測 (ADR-043 整合、seal test の設計根拠)**: `|| true` 除去後は + 劣化経路がすべて loud に落ちる。 + + | 劣化経路 | 結果 | + |---|---| + | 型エラー (`const x: number = "s"` を注入) | exit 1 | + | typescript が devDeps から消える (npx が npm の `tsc` stub を掴む) | exit 1 | + | tsconfig の include が何にもマッチしない | TS18003 / exit 2 | + + よって**ゲートが黙って green に戻る経路は 2 つだけ**に絞られる: + (1) exit code の握りつぶしを build script に足し直す、(2) build script を tsc 以外に + 差し替える。回帰テストはこの 2 経路の封鎖に専念し、「tsc が型エラーを検出すること」 + 自体は TypeScript の責務として対象外にした。 + - **回帰テスト**: `src/hooks-stop-quality/tests/t3_build_gate_seal.rs` 4 本 + (33 → 37 passed)。配置理由は seal 対象の `package.json` が push / Stop 両ゲート共有の + repo root artifact で単独 owner crate を持たないため — build step を実行する側で、 + かつゲート健全性の回帰テスト (`t7_cwd_independence.rs`) が既にある本 crate に置いた。 + **incident 状態で実際に落ちることを確認済み (推測ではない)**: build script を + `|| true` 付きに戻し typescript を devDeps から削除すると、4 本中 2 本 + (`build_script_does_not_swallow_exit_code` / `typescript_is_pinned_as_dev_dependency`) + が逐語のメッセージで FAIL する。false-green guard として `scripts.build` 欠落時は + panic させている (script が消えたのに「握りつぶしは無い」で silent-pass しないため)。 + - **⚠ 遭遇した障害: 本リポの guard が tsconfig.json の新規作成をブロックする**。 + `hooks-pre-tool-validate` の `PROTECTED_CONFIG_FILES` (`protected_files.rs`) は + `tsconfig.json` をハードコードしており Write/Edit が exit 2 で拒否される。guard の + 想定は「**リンター設定を弱めてエラーを消す**」行為の抑止 (メッセージも「設定を変更 + するのではなくコード側を修正してください」) だが、T3 の「**ゲートを有効化するための + 新規作成**」と区別できない。config 側の除外機構も無い (`extra_protected_files` は + 追加専用)。guard 自身のメッセージが指示する「ユーザーに確認を取る」を実施し、 + 承認を得た上で Bash 経由で作成した。恒久対処は §6 backlog 13 に分離。 + - **実施順**: 計画の推奨順どおり T7 の次に実施。 ## 5. 改善タスク詳細 @@ -852,6 +905,28 @@ T1 を最優先とする理由: 以降の全 PR の dogfood push が速くなり 方針案: `drain_pipe_*` は行単位で読むので「UTF-8 として不正な行のみ CP932 で再デコード」の フォールバックが素直 (正常な UTF-8 出力 = cargo/pnpm は不変)。`encoding_rs` 依存の追加要否を 判断すること。規模 S。 +12. **`vitest` を devDependency に固定する** (T3 の型チェック範囲決定から分離、2026-07-17。 + **ユーザー承認済みの分離**)。`pnpm test` = `npx vitest run` だが vitest は + devDependencies に無く、npx が実行時に自動 DL する = **バージョン非固定**。 + 同型は `npx oxlint` (lint) / `npx tsx` (test:e2e) にもある。品質ゲートの中身が + 実行時に取ってくる版で変わるのは ADR-017 (takt バージョン固定と検証環境の維持) の + 問題意識と同型。T3 との接点: vitest が devDep に無いため `src/__tests__/` を + `tsconfig.json` の型チェック対象に含められない (TS2307)。devDep 化すれば + exclude を外してテストも型チェックできる。`markdownlint-cli2` は既に devDep + + `npx --no-install` になっており、その規約に揃える形。規模 XS〜S (版固定で + 既存テストが落ちないかの確認を含む)。 +13. **保護ファイル guard に「新規作成」の抜け道が無い** (T3 の実施中に遭遇、2026-07-17)。 + `hooks-pre-tool-validate` の `PROTECTED_CONFIG_FILES` (`src/hooks-pre-tool-validate/ + src/protected_files.rs`) は `tsconfig.json` 等をハードコードで拒否する。guard の意図は + 「**リンター設定を弱めてエラーを消す**」抑止だが、**存在しない設定を新規作成して + ゲートを有効化する**行為 (= T3 でやったこと、guard の意図と真逆) も同じく拒否される。 + `extra_protected_files` は追加専用で除外機構が無いため、config では回避できない。 + 現状の逃げ道は guard のメッセージが指示する「ユーザー承認 → Bash 経由で作成」だが、 + **Bash 経由なら誰でも素通りできる**ことの裏返しでもあり、guard の実効性としても + 検討の価値がある (Write/Edit だけ塞いで Bash が空いている非対称)。 + 方針案: (a) ファイル不在 = 新規作成のときは block せず警告に落とす、 + (b) config に `unprotected_files` 除外リストを足す、(c) 現状維持 (承認フローで足りる)。 + ADR-042 (ルール vs 仕組み化の境界基準) の観点で判断すること。規模 S。 ## 7. スコープ外 (本計画では実施しない) @@ -889,4 +964,5 @@ T1 を最優先とする理由: 以降の全 PR の dogfood push が速くなり | T5 | 実装済 (PR #282) | 2026-07-17 | push 拒否検知を 40 行 truncate 済み出力から**全量出力**に切替。`lib-subprocess` に `run_cmd_shell_unlimited` を追加 (`drain_pipe_unlimited` は pipe 単体、`_capped_reporting` は cap が残るためどちらも判定用には不足だった) し、3 variant の共通骨格を `run_cmd_shell_with` に集約。境界判定は ADR-044 §「後続の variant 追加」に記録。表示は成功時のみ `cap_for_log` で 40 行 + 超過明示に絞り、**失敗経路は全量表示** (診断情報を落とさない)。**副産物**: 唯一の呼び出し元が消えた `runner::run_stage_cmd` を削除 — dead code 除去に加え「capped 経路で control flow 判定する」罠の構造的排除。`MAX_LINES` は表示用として残置し doc に判定禁止を明記。**厳格化は不採用 (ユーザー承認済み)**: `contains` 誤爆の厳格化はリスクが非対称 (誤検知は出力表示で気付けるが、検知漏れは「リモート未反映のまま exit 0」= 本タスクが防ぐ事故そのもの) で ADR-043 (fail-closed) に反するため見送り、§6 backlog 8 を却下記録に変更。**回帰テスト**: `mod t5_truncated_refusal_detection` 6 本 + lib-subprocess 4 本。`run_push_cmd` を capped に戻すと 3 本が fail することを確認済み (回帰テストが素通りしないことの実証)。**サンドボックス実機で before/after 比較**: 拒否行を 41 行目に置いた fake push command で、before = `[push] 成功` + **exit 0 (silent failure 再現)** / after = 拒否検知 + exit 3。成功経路 (50 行) は 40 行 + `... (10 lines truncated)` 表示で exit 0 を維持。**発見 (本タスク外)**: `cli-pr-monitor` の `push_to_remote` は拒否検知が無く同型の穴 → §6 backlog 9 に追加 (1 PR 1 変更のため別 PR)。**post-PR 修正 (CodeRabbit Minor 1 件 + pre-push 非ブロッキング警告 2 件を採用)**: (a) `run_cmd_shell_capped` の doc「Err 経路で child を kill しない」は **pre-existing の stale 記述** (`kill_and_join_err` 導入 = PR #208 以降、実際は kill + reap + reader thread join している) だったため、child lifecycle の記述を 3 variant 共通の骨格 `run_cmd_shell_with` に集約し variant 側は参照のみにした。(b) `cap_for_log` の truncate 書式重複を `lib_subprocess::truncation_notice` として切り出し (実装は streaming vs materialize で共有できないが書式片は共有できる、という指摘は妥当)。(c) T5 行 / §4 の「本 PR」を PR #282 に backfill (T4 行が放置され本 PR で backfill する羽目になった負債を繰り返さないため)。**実施順**: 計画の推奨順どおり T4 の次に実施 | | T4 | 実装済 (PR #281) | 2026-07-17 | `push-runner-config.toml` の `refute_enabled = false → true` で dogfood 開始 (変更は方針どおり 1 行、templates は OFF 据え置き)。**dogfood 開始日を同 PR で固定**: ADR-039 bounded lifetime の起点が無いと 2 週間の期限が判定不能になるため、開始 2026-07-17 → **判定期限 2026-07-31** を ADR-047 (ステータス行 / Config opt-in / Bounded lifetime の 3 箇所) + config コメントに明記した。採否判定自体は本計画と独立に ADR-047 で進行する (§8 完了条件 4. の引き継ぎ先)。**初回 dogfood push で切替を実証** (PR #281 自身の push): 起動ログ `takt (pre-push-review-refute)` + takt の `ワークフロー 'pre-push-review-refute' を起動` → 完走を確認。合計 151s (pre_checks 1.3s / quality_gate 49.7s / diff 0.1s / takt 97.8s / push 2.2s)。**verify は予告どおり未発火**: reviewers 2 本とも APPROVE で `all("approved") → COMPLETE` に抜けたため `any("needs_fix") → verify` に入らず、verify 実動の観測は次の findings 発生 run に持ち越し (完了条件は「有効化が効いていることの確認」までで読む)。**副産物: 計測手順の誤りを発見・修正**。ADR-047 §dogfood 計測項目の `.takt/runs/*-pre-push-review-refute/trace.md` は **1 件もマッチしない** — run ディレクトリ名は workflow 名でなく task 名から作られ (`runSlug` = `-pre-push-review`)、refute run でも `20260716-182505-pre-push-review` になる (timestamp も UTC で JST の日付と 1 日ずれ得る)。放置すると 2026-07-31 の採否判定で run 0 件 →「データなし」誤読の恐れがあったため、ADR-047 と §5 T4 実施結果を `meta.json` の `piece` フィールド基準 (`grep -l '"piece": "pre-push-review-refute"' .takt/runs/*/meta.json`) に修正した。設計時の計測手順が実運用開始まで未検証だった例。有効化前の静的確認 (refute workflow / facet 群の存在、`resolve_takt_workflow` の unit test 4 本、`cli-pr-monitor` への波及なし、Rust 変更ゼロのため exe 再ビルド不要) は §5 T4 実施結果に記載。**実施順**: T5-T7 を飛ばして T8 の次に実施 (T4 は他タスクと独立の XS で、依存なし) | | T6 | 実装済 (PR #283) | 2026-07-17 | `run_diff_cmd` を `Command::output()` (無限待ち) から spawn + `drain_pipe_unlimited` × 2 + `wait_with_timeout_safe` に載せ替え、timeout 時は `DiffResult::Error` = exit 5 で中断 (fail-closed / ADR-043)。**timeout 値 60s + `[diff] timeout` で上書き可 (ユーザー承認済み)**: 方針が「30s に合わせるが 60s でも可」と両論併記だったため確認した。60s の根拠は diff が snapshot + 大 diff 書き出しを伴い `jj bookmark list` (30s) より重いこと、および timeout の目的がハング検知であって latency 制限ではなく誤 timeout のコスト (pipeline 全体が exit 5) が高いこと。config 化は `[push] timeout` と同形の escape hatch。**T5 の `run_cmd_shell_unlimited` は使えない**: `run_cmd_shell_*` は全 variant が stdout と stderr を結合するが、diff の stdout は reviewers が読むレビュー対象そのものとしてファイルに書かれるため、jj の stderr 警告 (並列 workspace 時の `Concurrent modification detected` = **まさに本タスクが想定する状況**) が混入する。分離を維持し、同型の `bookmark_check::run_jj_bookmark_list` とは direct args で signature 非互換のため共通化しない (ADR-044 層 1 に判定を追記)。**⚠ 初版実装の欠陥を回帰テストが検出した (本タスク最大の学び)**: 「timeout 後に reader thread を join する」初版は timeout 1s に対し制御が戻るまで **9.6s** 掛かった。`cmd /c` の child は cmd.exe で**孫 (実際の jj) は kill 対象外**、孫が pipe を保持するため EOF が来ず join がブロックする = timeout が意味を成さない (T6 が直すハングの再生産)。失敗経路では join せず detach する形に修正。**教訓**: timeout の回帰テストは Err の内容だけでなく**経過時間を assert する** (しないと素通りする)。**回帰テスト**: `mod t6_diff_timeout` 7 本 + config 2 本 (206 → 215 passed)。cli-push-runner のテスト全体が 9.66s → 1.55s に短縮 = timeout が効いている証跡。「stderr を diff に混ぜない」契約も seal (`run_cmd_shell_*` に載せ替えると落ちる)。**サンドボックス実機で before/after 比較**: `[diff] command` を `ping -t` (永久応答 = 返らない jj diff の代役) にし、`@-` から build した修正前 exe と比較。before は **diff stage の所要時間が外側 kill に追随** (25s→24.4s / 10s→9.4s) = 内部に上限が無く放置すれば無限待ち・診断なし。after は 3.0s で exit 5 + 「jj lock 競合を疑え」の診断。実 `jj diff` (既定 60s) が誤 timeout しないことも確認。before の run 後に `ping.exe` が残存し、孫が kill を生き延びる実機裏付けも取れた。**発見 (本タスク外)**: `lib-subprocess` の `run_cmd_shell_*` 3 variant が**同じ穴**を持ち timeout が wall-clock を縛れない (実測 9.23s)。影響は quality_gate `step_timeout` / push `timeout` / cli-merge-pipeline → §6 backlog 10 に追加 (1 PR 1 変更のため別 PR)。**実施順**: 計画の推奨順どおり T5 の次に実施 | +| T3 | 実装済 (PR #285) | 2026-07-17 | **方針 (a) 実体化を採用 (ユーザー承認済み)**。`typescript` + `@types/node` を devDependencies に追加、`tsconfig.json` を新規作成、build script を `npx tsc --noEmit --pretty \|\| true` → `npx --no-install tsc --noEmit --pretty` に変更 (`--no-install` は既存 `lint:md` の規約に追随)。**(b) の前提は成り立たなかった**: 「TS 資産が実質サンプルのみ」ではなく `scripts/deploy-hooks.ts` (190 行) は `pnpm deploy:hooks` の実運用ツール、`scripts/e2e.ts` は `pnpm test:e2e` の入口で、型チェックの価値が実在する。**既存 ts の型エラーは 0** (方針欄の「型エラーなら先に修正」は空振り)、ゲートコストは 0.6s (TypeScript 7 native)。**fail-closed を実測** (ADR-043): 型エラー → exit 1 / typescript 欠落 (npx が npm の `tsc` stub を掴む) → exit 1 / tsconfig の include 空マッチ → TS18003 exit 2。よって黙って green に戻る経路は「握りつぶしの再追加」「tsc 以外への差し替え」の 2 つだけに絞られ、**回帰テストはその 2 経路の封鎖に専念**した (tsc が型エラーを検出すること自体は TypeScript の責務)。**回帰テスト**: `tests/t3_build_gate_seal.rs` 4 本 (33 → 37 passed)。incident 状態 (build script に `\|\| true` を戻す + typescript 削除) で 2 本が実際に FAIL することを確認済み (推測ではない)。`scripts.build` 欠落時は panic させる false-green guard 付き。**型チェック範囲はテスト除外 (ユーザー承認済み)** — vitest が devDep でないため TS2307。npx 自動 DL の非決定性は T3 と別問題なので §6 backlog 12 に分離。**⚠ 本リポの guard に阻まれた**: `hooks-pre-tool-validate` が `tsconfig.json` をハードコードで保護しており Write が exit 2 で拒否される。guard の意図は「リンター設定を弱めてエラーを消す」抑止で、T3 の「ゲートを有効化するための新規作成」と区別できず、config 除外機構も無い (`extra_protected_files` は追加専用)。guard 自身のメッセージが指示する承認フローを踏んで Bash 経由で作成 → 恒久対処は §6 backlog 13 に分離。Rust runtime の変更なし = exe 再ビルド不要。**実施順**: 計画の推奨順どおり T7 の次に実施 | | T7 | 実装済 (PR #284) | 2026-07-17 | `hooks-stop-quality` の `main` 冒頭で cwd をプロジェクトルートへ正規化 (`normalize_cwd_to_project_root`)。**ルート導出は (b) exe パス**: 方針が両論併記だったため実測し、VSCode 拡張環境で **`CLAUDE_PROJECT_DIR` が空** = ADR-005 (2026-03-17) の不安定性が現在も再現することを確認して (a) を却下。既存規約 (順位 287 / ADR-010、`config_path()` / `pipeline_lock::exe_claude_dir()` / `lib_telemetry::exe_dir()`) と同形。判断根拠は**本計画が削除予定のため ADR-005 に追記**して恒久化した。**⚠ 方針の前提が 1 つ誤っていた (ユーザー承認のうえ逸脱)**: リスク欄は「正規化が takt subsession 判定に影響しないか確認が必要」「判定ロジックは元 cwd を使う形が安全」としていたが、`takt_subsession_active` は **cwd 依存で既に壊れていた** (cwd = `.takt/runs` だと `.takt/runs/.takt/runs` を探して空振り → active run 未検出 → ADR-004 § takt subsession skip が効かず edit: false の subsession に「直せ」を返す = PR #221 の事故が再発しうる)。元 cwd 維持は「安全」ではなく既知不具合の温存のため、**両症状に効く main 冒頭 1 回の正規化**を採用。回帰テストで修正前に実際に失敗することを確認済み (推測ではない)。**実装**: `project_root_from_exe` は ADR-010 の配置 (`/.claude/.exe`) を満たすときのみ `Some` を返し、`target/debug/` 等では正規化を skip (cwd 書き換えは全 step の実行位置を変えるため、推測でルート扱いせず従来挙動に倒す)。ルート特定不能・`set_current_dir` 失敗は警告のみで継続 = fail-open (`pipeline_is_running` と同じ線引き、ADR-043: Stop 時点は助言層で本物のゲートは push pipeline 側)。**lib-subprocess は無変更** — プロセス単位の正規化で `cmd /c` の子が継承するため、共有 `run_cmd_shell_*` への cwd 引数追加 (variant 増殖) を回避できた。**ファイル分割 (T7 に付随、T1 と同型)**: `main.rs` が 712 行 → 追記で 804 行となり 800 行上限に触れたため takt 判定を `takt_subsession.rs` へ切り出し (main 532 / takt_subsession 290)。**T7 が直している file-length gate に T7 自身が引っ掛かった** = gate が機能していることの副次的実証。**回帰テスト**: `tests/t7_cwd_independence.rs` E2E 5 本 + unit 2 本 (26 → 33 passed)。**exe を `/.claude/` に staging して spawn** するのが要点 — `target/debug/` の exe を直接起動すると exe-relative のルート導出を素通りして実配置を検証しない。`normalize_cwd_to_project_root()` の呼び出しを外すと **bad 2 本がちょうど失敗し good 3 本は通る**ことを確認済み (failure は incident の逐語再現)。good 側に「実失敗する step は cwd に依らず block する」= 正規化がゲートを骨抜きにする最悪の退行ガードを含む。**実機検証 (before/after、本リポジトリの実 config)**: cwd = `.takt/runs` で before = block + `**file-length** failed:` + 文字化け / after = 出力なし (通過)。root cwd と深い cwd も通過。**方針の記述も実機で裏付け**: before で失敗したのは `file-length` step のみで pnpm 系 5 step + `cargo clippy` は通っていた (pnpm/cargo は設定ファイルを上方探索する) = **ルート相対パスを書いた step だけが壊れる**非対称が症状をまだらにし発見を遅らせていた。**CP932 デコードフォールバック (方針 2) は §6 backlog 11 へ分離 (ユーザー承認済み)**: 影響先が共有 lib (push-runner / merge-pipeline) のため §2 原則 4 に従う。cwd 修正で incident の文字化けは消えるが、exe 欠落時 (ADR-005 Negative の既知事象) 等で経路自体は残るため却下ではなく backlog。**発見 (本タスク外)**: T7 は cwd drift silent 故障の 3 例目で、順位 281 (Tier 1、lint rule) / 順位 287 (Tier 3、convention 明文化) が先行 todo 化済み。ただし T7 は「config 解決」でなく「**step 実行の cwd**」の別カテゴリのため既存 lint rule 案では捕捉できず、281 着手時に検出対象拡大を検討する価値がある (本計画スコープ外 = todo13.md 管理)。**post-PR 修正 (CodeRabbit Major 1 件を採用)**: `run_hook` が `wait_with_timeout_safe` の戻り値を捨てており、hook が非 0 exit でも stdout が空なら `block_reason == None` をすり抜ける = **`None` を期待する 3 本 (bad 2 + good 1) が false green** になる穴。「block されないこと」を期待する回帰テストは hook が黙って死ぬと合格してしまうため、指摘は妥当。`cli-pr-monitor` の takt auto-fix が exit code assert を追加したが**指摘の「失敗時は stderr を出す」部分が未達**だったため補正した — メッセージに stdout を渡しており、かつ `stderr.join()` より前に呼ばれるため構造上 stderr を出せなかった。本 hook の診断は `eprintln!` = stderr にしか出ず、指摘が想定する「stdout が空の失敗」では stderr だけが手掛かりになる。**guard が空振りでないことを実証**: staged exe を `where.exe` (非 0 exit・stdout 空) に差し替えると 5 本すべてが `exit code Some(2)` で失敗する (導入前なら `None` 期待の 3 本は素通りしていた)。**実施順**: 計画の推奨順どおり T6 の次に実施 | diff --git a/package.json b/package.json index afdf6482..adca1aea 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "lint:docs": ".\\.claude\\cli-docs-lint.exe", "test": "npx vitest run", "test:e2e": "npx tsx scripts/e2e.ts", - "build": "npx tsc --noEmit --pretty || true", + "build": "npx --no-install tsc --noEmit --pretty", "build:hooks-pre-tool-validate": "cargo build --release -p hooks-pre-tool-validate && cp target/release/hooks-pre-tool-validate.exe .claude/hooks-pre-tool-validate.exe", "build:hooks-post-tool-linter": "cargo build --release -p hooks-post-tool-linter && cp target/release/hooks-post-tool-linter.exe .claude/hooks-post-tool-linter.exe", "build:hooks-post-tool-comment-lint-rust": "cargo build --release -p hooks-post-tool-comment-lint-rust && cp target/release/hooks-post-tool-comment-lint-rust.exe .claude/hooks-post-tool-comment-lint-rust.exe", @@ -40,7 +40,9 @@ "py-test:e2e": "echo py-test:e2e placeholder && python -c \"print('E2E smoke test passed')\"" }, "devDependencies": { + "@types/node": "^26.1.1", "markdownlint-cli2": "^0.22.1", - "takt": "0.35.3" + "takt": "0.35.3", + "typescript": "^7.0.2" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0697aec9..c5a37468 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,12 +8,18 @@ importers: .: devDependencies: + '@types/node': + specifier: ^26.1.1 + version: 26.1.1 markdownlint-cli2: specifier: ^0.22.1 version: 0.22.1 takt: specifier: 0.35.3 version: 0.35.3 + typescript: + specifier: ^7.0.2 + version: 7.0.2 packages: @@ -68,56 +74,66 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-win32-arm64@0.34.5': resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} @@ -226,9 +242,132 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@26.1.1': + resolution: {integrity: sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + '@typescript/typescript-aix-ppc64@7.0.2': + resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [aix] + + '@typescript/typescript-darwin-arm64@7.0.2': + resolution: {integrity: sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [darwin] + + '@typescript/typescript-darwin-x64@7.0.2': + resolution: {integrity: sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [darwin] + + '@typescript/typescript-freebsd-arm64@7.0.2': + resolution: {integrity: sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [freebsd] + + '@typescript/typescript-freebsd-x64@7.0.2': + resolution: {integrity: sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [freebsd] + + '@typescript/typescript-linux-arm64@7.0.2': + resolution: {integrity: sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [linux] + + '@typescript/typescript-linux-arm@7.0.2': + resolution: {integrity: sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==} + engines: {node: '>=16.20.0'} + cpu: [arm] + os: [linux] + + '@typescript/typescript-linux-loong64@7.0.2': + resolution: {integrity: sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==} + engines: {node: '>=16.20.0'} + cpu: [loong64] + os: [linux] + + '@typescript/typescript-linux-mips64el@7.0.2': + resolution: {integrity: sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==} + engines: {node: '>=16.20.0'} + cpu: [mips64el] + os: [linux] + + '@typescript/typescript-linux-ppc64@7.0.2': + resolution: {integrity: sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [linux] + + '@typescript/typescript-linux-riscv64@7.0.2': + resolution: {integrity: sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==} + engines: {node: '>=16.20.0'} + cpu: [riscv64] + os: [linux] + + '@typescript/typescript-linux-s390x@7.0.2': + resolution: {integrity: sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==} + engines: {node: '>=16.20.0'} + cpu: [s390x] + os: [linux] + + '@typescript/typescript-linux-x64@7.0.2': + resolution: {integrity: sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [linux] + + '@typescript/typescript-netbsd-arm64@7.0.2': + resolution: {integrity: sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [netbsd] + + '@typescript/typescript-netbsd-x64@7.0.2': + resolution: {integrity: sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [netbsd] + + '@typescript/typescript-openbsd-arm64@7.0.2': + resolution: {integrity: sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [openbsd] + + '@typescript/typescript-openbsd-x64@7.0.2': + resolution: {integrity: sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [openbsd] + + '@typescript/typescript-sunos-x64@7.0.2': + resolution: {integrity: sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [sunos] + + '@typescript/typescript-win32-arm64@7.0.2': + resolution: {integrity: sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [win32] + + '@typescript/typescript-win32-x64@7.0.2': + resolution: {integrity: sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [win32] + accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} @@ -989,9 +1128,17 @@ packages: resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} engines: {node: '>= 0.6'} + typescript@7.0.2: + resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==} + engines: {node: '>=16.20.0'} + hasBin: true + uc.micro@2.1.0: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + undici-types@8.3.0: + resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} + unicorn-magic@0.4.0: resolution: {integrity: sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==} engines: {node: '>=20'} @@ -1232,8 +1379,72 @@ snapshots: '@types/ms@2.1.0': {} + '@types/node@26.1.1': + dependencies: + undici-types: 8.3.0 + '@types/unist@2.0.11': {} + '@typescript/typescript-aix-ppc64@7.0.2': + optional: true + + '@typescript/typescript-darwin-arm64@7.0.2': + optional: true + + '@typescript/typescript-darwin-x64@7.0.2': + optional: true + + '@typescript/typescript-freebsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-freebsd-x64@7.0.2': + optional: true + + '@typescript/typescript-linux-arm64@7.0.2': + optional: true + + '@typescript/typescript-linux-arm@7.0.2': + optional: true + + '@typescript/typescript-linux-loong64@7.0.2': + optional: true + + '@typescript/typescript-linux-mips64el@7.0.2': + optional: true + + '@typescript/typescript-linux-ppc64@7.0.2': + optional: true + + '@typescript/typescript-linux-riscv64@7.0.2': + optional: true + + '@typescript/typescript-linux-s390x@7.0.2': + optional: true + + '@typescript/typescript-linux-x64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-sunos-x64@7.0.2': + optional: true + + '@typescript/typescript-win32-arm64@7.0.2': + optional: true + + '@typescript/typescript-win32-x64@7.0.2': + optional: true + accepts@2.0.0: dependencies: mime-types: 3.0.2 @@ -2115,8 +2326,33 @@ snapshots: media-typer: 1.1.0 mime-types: 3.0.2 + typescript@7.0.2: + optionalDependencies: + '@typescript/typescript-aix-ppc64': 7.0.2 + '@typescript/typescript-darwin-arm64': 7.0.2 + '@typescript/typescript-darwin-x64': 7.0.2 + '@typescript/typescript-freebsd-arm64': 7.0.2 + '@typescript/typescript-freebsd-x64': 7.0.2 + '@typescript/typescript-linux-arm': 7.0.2 + '@typescript/typescript-linux-arm64': 7.0.2 + '@typescript/typescript-linux-loong64': 7.0.2 + '@typescript/typescript-linux-mips64el': 7.0.2 + '@typescript/typescript-linux-ppc64': 7.0.2 + '@typescript/typescript-linux-riscv64': 7.0.2 + '@typescript/typescript-linux-s390x': 7.0.2 + '@typescript/typescript-linux-x64': 7.0.2 + '@typescript/typescript-netbsd-arm64': 7.0.2 + '@typescript/typescript-netbsd-x64': 7.0.2 + '@typescript/typescript-openbsd-arm64': 7.0.2 + '@typescript/typescript-openbsd-x64': 7.0.2 + '@typescript/typescript-sunos-x64': 7.0.2 + '@typescript/typescript-win32-arm64': 7.0.2 + '@typescript/typescript-win32-x64': 7.0.2 + uc.micro@2.1.0: {} + undici-types@8.3.0: {} + unicorn-magic@0.4.0: {} unpipe@1.0.0: {} diff --git a/src/hooks-stop-quality/tests/t3_build_gate_seal.rs b/src/hooks-stop-quality/tests/t3_build_gate_seal.rs new file mode 100644 index 00000000..193b02d0 --- /dev/null +++ b/src/hooks-stop-quality/tests/t3_build_gate_seal.rs @@ -0,0 +1,107 @@ +//! T3 incident 回帰テスト: `pnpm build` 品質ゲートの形骸化 (ADR-049 の流儀)。 +//! +//! **由来 incident** (2026-07-16 調査で判明 / `docs/push-pipeline-fix-plan.md` §4 T3): +//! `package.json` の build script が `npx tsc --noEmit --pretty || true` だった。 +//! typescript が devDependencies に無いため `npx tsc` は npm 上の同名 stub package +//! (`tsc`、"This is not the tsc command you are looking for" を出すだけ) を掴んで +//! 常に exit 1 になり、それを `|| true` が握りつぶして exit 0 を返していた。 +//! つまり型チェックは一度も機能しておらず、push の quality_gate +//! (`push-runner-config.toml` の build group) と Stop 品質ゲート +//! (`.claude/hooks-config.toml` の build step) は時間だけ消費する見せかけゲートだった。 +//! +//! **なぜ構成 (configuration) を seal するのか**: T3 実施時に劣化経路を実測したところ、 +//! `|| true` 除去後は型エラー → exit 1 / typescript 欠落 → exit 1 / +//! tsconfig の include 空マッチ → TS18003 exit 2 と、いずれも **fail-closed** で +//! 落ちる (ADR-043)。ゲートが黙って green に戻る経路は「exit code の握りつぶしを +//! build script に足し直す」「build script を tsc 以外のものに差し替える」の 2 つだけに +//! 絞られる。よって本テストはその 2 経路を封じることに専念する。 +//! `tsc` が型エラーを実際に検出すること自体は TypeScript 側の責務なので対象外。 +//! +//! **配置理由**: seal 対象の `package.json` は push / Stop 両ゲートが共有する repo root の +//! artifact で、単独の owner crate を持たない。本 crate は build step を実行する側であり、 +//! ゲート健全性の回帰テスト (`t7_cwd_independence.rs`) が既に同居しているためここに置く。 + +use serde_json::Value; +use std::path::PathBuf; + +fn repo_root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..").join("..") +} + +/// repo root の `package.json` を parse して返す。 +fn package_json() -> Value { + let path = repo_root().join("package.json"); + let content = std::fs::read_to_string(&path) + .unwrap_or_else(|e| panic!("failed to read {}: {e}", path.display())); + serde_json::from_str(&content) + .unwrap_or_else(|e| panic!("failed to parse {}: {e}", path.display())) +} + +/// `scripts.build` の中身。欠落は panic させる (false-green guard: script が消えたのに +/// 「握りつぶしが無い」で silent-pass すると seal の意味が無くなる)。 +fn build_script() -> String { + let pkg = package_json(); + pkg["scripts"]["build"] + .as_str() + .unwrap_or_else(|| { + panic!( + "package.json の scripts.build が存在しない — \ + build gate の seal テストが silent-pass する。scripts: {:#?}", + pkg["scripts"] + ) + }) + .to_string() +} + +/// exit code を握りつぶす shell suffix。incident 当時の `|| true` に限らず同義形も封じる。 +const EXIT_CODE_SWALLOW_PATTERNS: &[&str] = &["|| true", "|| exit 0", "; exit 0", "|| :"]; + +#[test] +fn build_script_does_not_swallow_exit_code() { + let script = build_script(); + let found: Vec<&str> = EXIT_CODE_SWALLOW_PATTERNS + .iter() + .copied() + .filter(|p| script.contains(p)) + .collect(); + assert!( + found.is_empty(), + "package.json の scripts.build が exit code を握りつぶしている: {found:?}\n\ + build script: {script:?}\n\ + これは T3 incident (型チェックが一度も機能していない見せかけゲート) の再発。\ + 型エラーで落ちない build step は push / Stop 両ゲートの時間を捨てるだけになる。" + ); +} + +#[test] +fn build_script_invokes_tsc_type_check() { + let script = build_script(); + assert!( + script.contains("tsc") && script.contains("--noEmit"), + "package.json の scripts.build が tsc の型チェックを起動していない: {script:?}\n\ + 握りつぶしが無くても、中身が型チェック以外に差し替わればゲートは形骸化する。" + ); +} + +#[test] +fn typescript_is_pinned_as_dev_dependency() { + let pkg = package_json(); + let ts = pkg["devDependencies"]["typescript"].as_str(); + assert!( + ts.is_some(), + "typescript が devDependencies に無い — `npx tsc` が npm 上の stub package を掴み、\ + T3 incident と同じく型チェックが起動しなくなる。devDependencies: {:#?}", + pkg["devDependencies"] + ); +} + +#[test] +fn tsconfig_exists_for_build_script() { + let path = repo_root().join("tsconfig.json"); + assert!( + path.exists(), + "tsconfig.json が存在しない ({}) — scripts.build の `tsc --noEmit` は\ + 型チェック対象を tsconfig.json から解決するため、消えるとゲートが成立しない。", + path.display() + ); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..b5cde07c --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "es2022", + "lib": ["es2022"], + "module": "esnext", + "moduleResolution": "bundler", + "types": ["node"], + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["scripts/**/*.ts", "src/**/*.ts"], + "exclude": ["node_modules", "src/__tests__"] +}