• Status: Accepted
  • Mode: Standard
  • Kruchten Type: Existence/Property
  • Scope: platform
  • Implementation Status: Done (PR #1135)
  • 起案者: [email protected]
  • 起案日時 (JST): 2026-05-29 18:06
  • 承認日時 (JST): 2026-05-29
  • Deciders: [email protected] (単独)

コンテキスト

1.1 背景

ADR-0076 で導入した Cross-Validation Gate は、各 (blind-spot finding × evaluation axis) ペアに対する verdict (undermines: true/false, reasoning) を生成し、pipeline state の crossValidationVerdicts 配列に格納する。しかし TelemetryRecord (D1 schema) には cross_validation_detail TEXT (markdown サマリ) しか保存されておらず、構造化された verdict データは DB に残らない

1.2 現状

  • pipeline_consumer.ts: cross_validation_detail: (r.crossValidationDetail as string) ?? null のみ。r.crossValidationVerdicts は破棄
  • crossValidationVerdicts の型: Array<{findingTitle, findingSeverity, mustAxis, mustScore, isMustAxis, undermines, reasoning}>
  • PR #1112 で extended (全 finding × 全 axis) になり、1 run あたり 10-40 verdict が生成される
  • markdown サマリは人間用、機械的な集計には使えない

1.3 課題

(1) 「過去 1 ヶ月で最も毀損が多い Must 軸はどれか」「critical 盲点で実際に reject に至ったケース統計」のような SQL 集計が不可能 (markdown を grep するしかない)。 (2) 将来の改善施策 (Cross-Validation 精度改善、軸別の閾値調整等) のための分析データが失われる。 (3) /audit/runs API は markdown を返すだけで、UI 側で構造化データとして再構築できない。

1.4 制約・要件

  • D1 スキーマ変更は ALTER TABLE ADD COLUMN (破壊なし)
  • 既存 v2 レコードの後方互換: 新カラムは NULL
  • verdict 配列のサイズ: 典型 10-40 件、最大 100 件 (cap 不要、JSON 文字列で 5-20KB、ただし reasoning 長文化時は超過リスクあり → §5.2 / §6 で対策)
  • /audit/runs API レスポンスで verdict 配列を JSON parse して返却 (parse 失敗時のフォールバック必須、§6.5 Confirmation 参照)

1.5 目標

crossValidationVerdicts を JSON 文字列として telemetry に保存し、SQL で軸別/severity 別の集計を可能にする。Non-Goals: 別テーブルへの normalize、Analytics Engine 等の外部分析基盤連携 (本 ADR スコープ外、必要時に別 ADR で検討)。

1.6 D1 json_each() 動作実測 (2026-05-29 検証)

Cloudflare D1 (SQLite-based) で json_each() が利用可能であることを実測:

SELECT key, value FROM telemetry_records, json_each(gate_timings)
WHERE session_id = '...' LIMIT 5;

結果: {"key": "triage", "value": 44}, {"key": "socratic", "value": 107964}, ... のように JSON フィールド展開が正常に動作。本番 telemetry-records テーブルで実行成功 (rows_read: 約 20, sql_duration_ms: 数 ms)。SQLite 3.38+ の json1 拡張が D1 にバンドル済みであり、機能・性能ともに集計用途で問題なし。

検証範囲の限界: 本実測は session_id 指定の単一行に限定。全期間・全セッションを対象とした集計クエリ (例: 過去 1 ヶ月で最も毀損が多い Must 軸) では未検証。数万行 × 40 verdict の json_each() 展開で rows_read が数百万に達し得る (Gate 1 盲点 #1 指摘)。Review After 評価時に 10,000 件規模ダミーデータでの実測を必須とする (§6 撤退条件で再評価)。

1.7 ステークホルダー

  • 運用者: Cross-Validation 精度評価のための過去データ蓄積
  • 将来 Jr: 軸ごとの cross-validation 統計を分析可能

1.8 過去 ADR との関係

ADR関係
ADR-0076 (Cross-Validation Gate)Extends — verdict をテレメトリ保存
ADR-0082 (Telemetry v2)Extends — schema 拡張パターンを踏襲
PR #1112 (全 finding × 全 axis)Extends — 拡張されたデータ量を保存対象に

Review After: 2026-08-29 (3 ヶ月後) — §6 撤退条件の定量基準で評価

決定

telemetry_records テーブルに cross_validation_verdicts TEXT カラム (JSON 配列) を ALTER TABLE ADD COLUMN で追加し、pipeline_consumer.ts で JSON.stringify(r.crossValidationVerdicts) を保存する。/audit/runs API では try-catch 付き JSON.parse でフォールバック (null) を提供する。reasoning 長文化対策として書き込み前に 50KB のバイト長閾値チェックを実装し、超過時は reasoning を truncate する。

判断基準 (Decision Drivers)

3.1 評価軸

#重要度 (係数)案件特有の解釈
1#suitableMust (×2.0)Cross-Validation 精度改善のための構造化分析データを永続化できるか
2#maintainableHigh (×1.0)スキーマ変更コスト、後方互換、影響モジュール数
3#reliableHigh (×1.0)JSON parse 失敗・書き込み失敗・マイグレーション失敗時の堅牢性
4#efficientMedium (×0.5)D1 rows_read コスト、集計クエリのスケーラビリティ
5#secureMedium (×0.5)reasoning に含まれうる機密情報の保存・露出リスク

K.O. criterion: Must 軸 (#suitable) score < 3 は不採用。

3.2 評価軸 × 案スコア表

係数案 A (採択: JSON カラム)案 B (別テーブル)案 C (現状維持)
#suitable×2.0450
#maintainable×1.0425
#reliable×1.0345
#efficient×0.5345
#secure×0.5225
加重和 (正規化)0.6900.7000.600
K.O. 通過 (Must ≥3)

案 A と案 B は加重和が拮抗するが、verdict 数 10-40 の中規模データに対し別テーブル + JOIN のオーバーヘッドは過剰投資であり、#maintainable で案 A を選好する (タイブレーク)。

検討した代替案 (Alternatives Considered)

  • 案 A【採用】: JSON カラム追加 — telemetry_records に cross_validation_verdicts TEXT (JSON 配列) カラムを追加。pipeline_consumer.ts で JSON.stringify(r.crossValidationVerdicts) を保存。/audit/runs API で JSON parse して返却。SQL 集計は json_each() でアクセス (§1.6 実測検証済)。影響: schema.sql, migrate-v3.sql, audit/types.ts, audit/persistence.ts, queues/pipeline_consumer.ts, index.ts。工数 ~1.5h。
  • 案 B: 別テーブル normalizecross_validation_verdicts (session_id, finding_title, must_axis, must_score, is_must_axis, undermines, reasoning) を新設。不採用: verdict 数が 10-40 と中程度、JSON で十分。テーブル追加 + JOIN のオーバーヘッドに見合わない。#maintainable 劣後。
  • 案 C: 現状維持 (markdown のみ) — 変更コストゼロ。不採用: 構造化データの永続的損失。将来の分析施策が不可能。#suitable K.O. 不通過。

影響 (Consequences)

5.1 正の影響 (Good)

  • SQL 集計可能化: 軸別 undermines 率、severity 別統計などが json_each() で取得可能 (§1.6 実測)
  • /audit/runs API が構造化 verdict を返却し、UI 側で再構築可能
  • Cross-Validation Gate の精度改善 (ADR-0076) のための過去データ蓄積開始
  • スキーマ変更は ALTER TABLE ADD COLUMN のみで破壊なし、既存 v2 レコードは NULL で後方互換

5.2 負の影響 (Bad)

  • 全件集計クエリでの json_each() フルスキャンによる rows_read 超過リスク (Gate 1 #1): 数万行 × 40 verdict の展開で rows_read が数百万に達し、D1 課金上限超過でクエリブロックの可能性。§1.6 の実測は単一行のみで全件未検証 → §6 で実測を撤退条件化
  • reasoning 長文化による JSON カラムサイズ肥大化 (Gate 1 #2): 100 件 × 平均 300 文字で約 30KB となり「5-20KB」見積もりを超過。LLM 出力長文化で更に悪化 → §6.5 Confirmation で 50KB 閾値の truncate フォールバックを必須実装
  • migrate-v3.sql 適用失敗の無音検知漏れ (Gate 1 #3): ALTER TABLE ADD COLUMN は冪等でなく、部分失敗時にカラム不在で INSERT が失敗、queue consumer が drop 設定だと telemetry が無音損失 → §6.5 で smoke test と DLQ アラート必須化
  • /audit/runs API での JSON.parse 失敗による 500 エラー (Gate 1 #4): NULL (v2 レコード)・空文字列・破損 JSON で例外発生時、try-catch 漏れがあれば 1 件で全レスポンスが落ちる → §6.5 で try-catch + null フォールバック + ユニットテスト必須
  • reasoning に含まれる機密情報・個人情報の無期限保存リスク (Gate 1 #6): LLM 生成 reasoning が評価対象ドキュメントの個人情報・営業秘密を間接含有しうる。telemetry_records に TTL なし、/audit/runs で外部露出。GDPR 「忘れられる権利」対応困難 → §6 で TTL ポリシーと PIA 要否を Review After 評価項目化

5.3 中立・トレードオフ (Neutral / Trade-offs)

  • デプロイ混在期間リスク (Gate 1 #5): Cloudflare Workers の Blue-Green 非保証により v2 コード × v3 スキーマの混在時間帯が存在。既存 INSERT が named-column 形式 (新カラム省略可) であることを実装時に確認し、デプロイ手順を「スキーマ適用 → コードデプロイ」順序で固定化する
  • undermines 件数の KPI 化によるメトリクスゲーム化リスク (Gate 1 #8): 集計可能化により「数値改善のための閾値調整」インセンティブが生じ、ADR-0076 の genuine 品質保証が歪曲されうる → Review After 評価に定性的事例レビューを必須化
  • 案 B (別テーブル) への将来移行は SQL レベルで可能 (json_each + INSERT INTO) であり、現時点で案 A 採用しても後戻り可能

撤退条件 (Rollback Plan)

6.1 定量的撤退条件 (Review After: 2026-08-29)

以下のいずれかを満たした場合、案 B (別テーブル) または案 C (削除) への移行を検討:

  1. rows_read 超過: 10,000 件規模ダミーデータでの軸別集計クエリで rows_read > 100 万 または sql_duration_ms > 5,000ms
  2. 保存失敗率: cross_validation_verdicts カラムの NULL 率 (v3 レコード対象) > 1%
  3. JSON サイズ超過率: 50KB 閾値を超えて truncate が発生したレコード比率 > 5%
  4. 集計クエリ未実施: 3 ヶ月間で軸別 undermines 率の集計クエリが 1 度も実行されなかった (= 価値未実証)
  5. 意思決定への寄与なし: 集計結果が閾値調整等の意思決定に 1 件も使われなかった
  6. GDPR インシデント: reasoning 経由の個人情報露出が 1 件でも発生

6.2 ロールバック手順

対象具体手順影響範囲
pipeline_consumer.tscross_validation_verdicts 書き込み行を削除 → 再デプロイ新規 telemetry は NULL になるが既存データは保持
/audit/runs APIJSON.parse 部分を削除し markdown のみ返却UI 互換 (構造化データは消える)
D1 schemaD1 が ALTER TABLE DROP COLUMN をサポートするか事前確認。未サポートなら新テーブル作成 + データ移行 + RENAME過去データの構造化部分は喪失

6.3 Review After 評価チェックリスト

  • 10,000 件ダミーデータで全件集計クエリの rows_read / sql_duration_ms 実測実施済か
  • 軸別 undermines 率の集計クエリが少なくとも 1 回実行されたか
  • その結果が閾値調整等の意思決定に使われたか
  • cross_validation_verdicts カラムの NULL 率 (v3 レコード対象) が 1% 未満か
  • JSON サイズ超過 (truncate 発生) 率が 5% 未満か
  • 実際の reject 事例の質的レビューを実施したか (メトリクスゲーム化防止)
  • reasoning の機密情報含有について PIA (プライバシー影響評価) 要否を判断したか
  • telemetry_records の TTL / 定期削除ポリシー策定要否を判断したか

6.4 Confirmation (準拠確認 / Fitness Function)

#検証手段実行頻度違反時の対応
1CI smoke test: migrate-v3.sql 適用後に PRAGMA table_info(telemetry_records)cross_validation_verdicts カラム存在確認デプロイ毎 (CI)デプロイブロック
2ユニットテスト: audit/persistence.ts に NULL (v2)・正常 JSON (v3)・破損 JSON の 3 ケースを追加し JSON.parse フォールバックを検証PR 毎 (CI)PR マージブロック
3書き込み前バイト長チェック: pipeline_consumer.ts で JSON.stringify 結果が 50KB 超過時に reasoning を truncate するロジックをユニットテストで検証PR 毎 (CI)PR マージブロック
4DLQ 監視: pipeline_consumer.ts の D1 書き込みエラーを明示的に catch し Dead Letter Queue または専用アラートに接続。NULL 率を週次でメトリクス化週次 (運用ダッシュボード)NULL 率 > 1% でアラート、原因調査
5Review After 評価: §6.3 チェックリストを 2026-08-29 に起案者または後任が実施1 回 (2026-08-29)撤退条件 6.1 該当時は案 B / 案 C への移行 ADR を起案

参照 (References)

  • 関連 ADR: ADR-0076 (Cross-Validation Gate), ADR-0082 (Telemetry v2)
  • 関連 PR/Issue: PR #1112 (全 finding × 全 axis 拡張)
  • 外部資料: Cloudflare D1 json1 拡張ドキュメント (SQLite 3.38+) (要追記: 正式 URL)