14. Cost Gate Node (Gate 0c)
TL;DR(このノードは何をする・専門語ゼロ): 「この決定にどれくらいの工数・お金がかかるか」を本文中に粗くでも書いてもらうための関所。書いてあれば通す。書いていなければ差し戻す。ただし「不可逆な害」が確定している起案だけはコスト未記載でも先へ通す。Light 起案は最初から免除。AI は呼ばず、本文を正規表現で見るだけの単純な処理。
実装:
drp/src/nodes/cost_gate.tsプロンプト: なし (LLM 呼出なし) テスト:drp/test/cost_gate.test.ts(純関数decideCostGate/hasCostEstimate) マイグレーション:drp/migrate-v13-cost-gate.sql(cost_gate_verdict/cost_gate_ms列) 基盤 ADR: ADR-0088 (コスト試算の義務化) / ADR-0158 (triage から独立ノードへ分離) / ADR-0157 (ABC スクリーン)
1. 役割と位置づけ
Standard 以上の起案にコスト試算の数値が 1 つ以上書かれているかを機械的に検査し、欠落していれば差し戻す。
- 解決する課題: コスト試算なしの ADR が量産され「決めた後で工数膨張」が頻発する状況の防止 (ADR-0088)。
- 設計思想: 差戻し型ゲートだが fail-open ではない。LLM を呼ばずコード合成だけで判定するため、判定の揺らぎゼロ・コスト増分ゼロ。
- 由来: ADR-0088 が当初 triage ノード内部のゲートとして稼働していたが、ADR-0157 で価値スクリーン (ABC) が triage と pregate の間に入った結果、不可逆 harm (A) はコスト未記載でも先に進めたい という要件 (#safe 穴) が生じ、ADR-0158 で abc_screen の後段へ独立ノード化して切り出した。
本ノード化により、cost gate は triage の ADR-0091 ゲートより後に位置する。cost-missing と ADR-0091 違反を同時に持つ起案は ADR-0091 が先に発火する (旧は cost-missing 優先)。両者とも pre-gate-block 差し戻しのため起案者への情報損失はなく、構造欠落を先に示すのは精度向上方向。
2. フロー図
flowchart LR
AS[abc_screen] --> CG[cost_gate]
CG -->|PASS / A_AWARE_PASS / EXEMPT_LIGHT / SKIP| P[problem_space_pregate]
CG -->|BLOCK| E[END 差戻し]
3. トリガー条件
| 条件 | 挙動 |
|---|---|
abc_screen を通過して遷移 | 実行 |
state.costGateVerdict が既に確定済 | 何もしない (再入時の冪等) |
環境変数 COST_GATE_ENABLED='false' | SKIP を返して素通し (緊急停止用) |
スキップ条件は緊急停止のみ。それ以外は無条件で評価される。
4. 入力 (State)
| State フィールド | 用途 |
|---|---|
triageMode | 'Light' なら免除。null は 'Standard' 扱い (triage 旧挙動と同則) |
context | コスト試算の有無を正規表現で検査する対象 |
abcVerdict | 'A' (不可逆 harm) ならコスト未記載でも通過させる |
costGateVerdict | 既存値があれば冪等で no-op |
外部入力:
env.COST_GATE_ENABLED—'false'で SKIP
5. 処理ロジック
1. 冪等性: state.costGateVerdict が確定済なら何もしない (return {})
2. decideCostGate({ enabled, mode, hasCost, abcVerdict }) を純関数で評価
- enabled=false → SKIP
- mode='Light' → EXEMPT_LIGHT
- hasCost=true → PASS
- hasCost=false & abcVerdict='A' → A_AWARE_PASS (#safe 穴対策)
- hasCost=false & 非 A → BLOCK
3. BLOCK のみ rejected=true / needsAdr=false / rejectionReasonCode=COST_MISSING /
triageRejectKind='pre-gate-block' を立てて差し戻し
4. A_AWARE_PASS は costGateNote にコスト試算を促す note を載せて通過
5. 各分岐で構造化ログ (cost_gate_skipped / cost_gate_a_aware_pass / cost_gate_block) を出力
コスト試算検出 (hasCostEstimate):
- 単位付き数値 (
0.5 人日/$10/5 万円/2 hrs) を 1 つでも含めば PASS - または「コスト / 工数 / 予算 / effort / cost / budget」と数値が 15 文字以内に隣接していれば PASS
- 「コスト高」「予算面」「工数懸念」など形容詞的修飾は false positive 抑制のため除外
- 2026-05-31 精度改善で距離を 40→15 文字に縮小し節境界をまたぐ誤マッチを抑制
6. LLM 設定
LLM 不要 (コード合成のみ)。判定は決定論的な純関数 decideCostGate と正規表現ベースの hasCostEstimate だけで完結する。LLM/subrequest の増分はゼロ。
7. 副作用
なし。構造化ログ (event: 'cost_gate_skipped' | 'cost_gate_a_aware_pass' | 'cost_gate_block') のみ出力。
8. 出力 (State)
PASS / EXEMPT_LIGHT / SKIP (通過のみ)
{ costGateVerdict: 'PASS' | 'EXEMPT_LIGHT' | 'SKIP' }
A_AWARE_PASS (通過 + コスト試算を促す note)
{
costGateVerdict: 'A_AWARE_PASS',
costGateNote: '[コスト試算 推奨] 不可逆 harm (A) と判定したため審査は継続します...',
}
BLOCK (差戻し)
{
costGateVerdict: 'BLOCK',
needsAdr: false, // triage 旧コストゲートと同じ telemetry 整合
rejected: true,
rejectionMsg: '[コスト試算欠落] Standard 以上の ADR 起案では...',
rejectionReasonCode: 'COST_MISSING',
triageRejectKind: 'pre-gate-block',
}
costGateNote は webhook ノードが PR 本文へ転記する。
9. 分岐 (次ノード)
.addEdge('abc_screen', 'cost_gate')
.addConditionalEdges('cost_gate', (s) => s?.rejected ? END : 'problem_space_pregate')
rejected | 次ノード |
|---|---|
true (BLOCK) | END (差戻し) |
false (PASS / A_AWARE_PASS / EXEMPT_LIGHT / SKIP) | problem_space_pregate |
10. エラー時の挙動
エラー無し (deterministic)。外部依存は環境変数 1 つだけで例外を投げる経路がない。COST_GATE_ENABLED が未定義 (undefined) なら有効扱い、'false' の文字列のみで無効化される。
11. 既知の弱点・運用注意
- A_AWARE_PASS の運用監視 (撤退条件): A 免除率を週次集計し、4 週連続 50% 超で A 免除自体を無効化する (ADR-0158 §Confirmation)。A 判定への偏りで「A だからコスト要らない」が常態化するリスクを抑える。
- コスト試算検出の粒度: 「粗くてよい・数値 1 つでよい」(ADR-0088 §決定) のため、
$0.5だけでも PASS する。質を上げる判定ではなく「書いてある/書いてない」を最低限機械化するだけ。質は Gate 4 Scoring と人間レビューに委ねる。 - 形容詞的修飾の blacklist 漏れ: 「コスト懸念があるが採用」のような数値なし表現は false 判定で意図通り。一方、「コスト 5 億円規模になり得る」のような誇張表現も PASS してしまう (悪用は想定せず、悪用検出は人間レビュー)。
- gate 順序の入替えに伴う reason_code 帰属: 旧 triage 内コストゲート時代は cost-missing が ADR-0091 違反より先に発火していた。本ノード化で順序が逆転したため telemetry の reason_code 集計でこの境目に注意。
- 緊急停止の徹底:
COST_GATE_ENABLED='false'は素通しになるため、設定変更時は SKIP 率を観測する。
12. テストケース
drp/test/cost_gate.test.ts (vitest workerd プール) で純関数 2 つを検証:
| ID | 観点 | 期待 |
|---|---|---|
| TC-01 | 環境変数 OFF | SKIP |
| TC-02 | Light モード | EXEMPT_LIGHT |
| TC-03 | コスト試算あり | PASS |
| TC-04 | コスト試算なし × abcVerdict='A' | A_AWARE_PASS |
| TC-05 | コスト試算なし × 非 A | BLOCK |
| TC-06 | hasCostEstimate の正規表現 golden (単位パターン / 隣接マッチ / 形容詞除外) | true / false が期待通り |
13. 過去の設計判断ログ
| 日時 | 変更 | 経緯 |
|---|---|---|
| ADR-0088 採択 | triage 内部に起案前ゲートとして実装 | コスト未記載 ADR の量産抑止 |
| 2026-05-31 | hasCostEstimate の距離を 40→15 文字へ縮小・形容詞修飾を blacklist | ADR-0094 Phase A テスト中に発覚した cross-section false positive 修正 |
| ADR-0157 採択 | 価値スクリーン (ABC) を導入 | A (不可逆 harm) をコスト未記載でも先に進めたい要件が浮上 |
| ADR-0158 採択 | triage から独立ノード化し abc_screen の後段へ配置 | A 免除 (A_AWARE_PASS) を追加し #safe 穴を塞ぐ。triage の挙動は逐語維持 |
14. 関連リンク
- 前ノード: 13_abc_screen.md
- 次ノード: 12_problem_space_pregate.md
- 関連 ADR:
- マイグレーション:
drp/migrate-v13-cost-gate.sql - 設定:
wrangler.toml[vars] COST_GATE_ENABLED(緊急停止)