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

コンテキスト

1.1 背景 (Background)

ADR-0082 で導入した TelemetryRecordgate_timings カラムは、各ゲートの所要時間 (ms) を JSON 文字列 {triage: 1234, socratic: 4500, ...} で保存している。SQL での per-gate 集計には json_extract() が必要で、D1 (SQLite) のインデックスが効かない。ADR-0074 で導入した月次 σ モニタリングや、将来のコスト分析 (ADR-0082) で per-gate 集計頻度が増えており、クエリの可読性と保守性の観点で見直しが必要になった。

1.2 現状 (Current State / As-Is)

  • スキーマ: gate_timings TEXT (JSON encoded、最大 ~10 ゲートのキー値)
  • 書込: pipeline_consumer.ts:174JSON.stringify(gateTimings) (gateTimings は Record<string, number>)
  • 読出: /audit/runs/:sessionIdJSON.parse() して返却。/audit/runs 一覧 API も同様
  • データボリューム (本番 D1 実測 2026-05-29): 28 レコード、平均 duration 391 秒 (6.5 分)、最大 730 秒 (12 分)
  • 既存集計クエリ実行時間: AVG(json_extract(gate_timings, '$.scoring')) FROM telemetry_records は 28 件で ~5ms (フルスキャン)
  • 将来推定: 月 15-30 audits ペースで、1 年で ~300 レコード、5 年で ~1500 レコード

1.3 課題 (Problem)

  1. per-gate 集計 (平均所要時間、p95 等) のクエリが json_extract() で記述複雑、可読性低下。
  2. ダッシュボード (将来 Cloudflare Analytics Engine or Grafana 連携) で per-gate 時系列をプロットする際、JSON フィールドだとパース層が必要 (※ 将来の副次的メリットであり、現時点では存在しない外部システムへの対応である点に留意)。
  3. ゲート構成変更 (新ゲート追加等) 時に JSON のキー命名規約が散在し、リファクタが追跡困難。

1.4 制約・要件 (Constraints & Requirements)

  • D1 スキーマ変更は ALTER TABLE ADD COLUMN (破壊なし、SQLite 3.35+ で DROP COLUMN も対応)
  • 既存 v2 レコードの後方互換: 新カラムは NULL、必要なら migration スクリプトで JSON から展開
  • ゲート数は 10 で固定的 (triage / socratic / body_generation / scoring / cross_validation / consistency / parallel_review / policy_alignment / slug / numbering) と現時点で合意
  • SQL 集計頻度: 月次 σ モニタリング (ADR-0074) + 将来のコスト分析 (ADR-0082) で日常的に必要
  • バックフィル対象は 28 レコードと極小、瞬殺 (1 SQL で完了)

1.5 目標 (Goals / To-Be)

各ゲートの所要時間 (ms) を per-gate カラムとして保存し、SQL で直接集計可能にする。既存の JSON gate_timings カラムは段階的に廃止 (v4 schema で DROP)。 Non-Goals: per-gate カラムへのインデックス付与 (集計時の WHERE 句より GROUP BY が主用途のため不要)、Analytics Engine への直接ストリーミング統合 (別 ADR で扱う)。

1.6 ステークホルダー

  • 運用者 (代表取締役): 月次 σ モニタリングのクエリが SQL 1 行で書ける
  • 将来 Jr (2026-10): ダッシュボード作成・分析時のパース層が不要

1.7 過去 ADR との関係

ADR関係
ADR-0082 (Telemetry v2)Extends — gate_timings JSON は ADR-0082 で導入、本 ADR で normalize
ADR-0074 (Gate 4 σ monitoring)Confirms — 集計クエリの単純化が直接寄与
ADR-0077 (Telemetry persistence)Confirms — schema 変更パターン (ALTER TABLE ADD COLUMN) を踏襲

Review After: 2026-08-29 (3 ヶ月後) — 集計クエリ実例と速度改善、ゲート構成変更の有無、v4 DROP COLUMN の Go/No-Go 判断を評価

決定

telemetry_records テーブルに 10 ゲート分の <gate_name>_ms INTEGER カラムを ALTER TABLE で追加し (telemetry_v3 schema_version)、pipeline_consumer.ts で per-gate カラムへ書込を行う。既存 gate_timings JSON カラムは互換性のため二重書き込みで残し、v4 schema で DROP する。28 件の既存レコードは冪等なバックフィルスクリプトで JSON から展開する。本 ADR の主たる採用根拠はパフォーマンス改善ではなく、SQL 可読性向上・開発者体験・将来のダッシュボード連携時のパース層削減である。

判断基準 (Decision Drivers)

3.1 評価軸 (Q42 9 タグから選定)

重要度ラベル係数: Must ×2.0 / High ×1.0 / Medium ×0.5

#重要度 (係数)案件特有の解釈
1#maintainable[Must] (×2.0)per-gate 集計 SQL の可読性、ゲート命名規約の一元化、新規参入者の理解容易性
2#operable[Must] (×2.0)月次 σ モニタリングクエリの記述量、ダッシュボード連携時のパース層有無
3#flexible[High] (×1.0)ゲート統廃合・追加時のスキーマ変更コスト、DDL ロックのリスク
4#efficient[Medium] (×0.5)クエリ実行時間 (※ 現データ量では差が小さく副次的)
5#reliable[High] (×1.0)二重書き込み期間中のデータ整合性、NULL 混入リスク、バックフィル冪等性

K.O. criterion: Must 軸 (#maintainable, #operable) の score < 3 は不採用。

3.2 評価軸 × 案スコア表

係数案 A (採択: 専用カラム化)案 B (別テーブル normalize)案 C (現状維持)
#maintainable (Must)×2.0432
#operable (Must)×2.0532
#flexible (High)×1.0245
#efficient (Medium)×0.5423
#reliable (High)×1.0335
加重和 (正規化)0.7360.5910.609
K.O. 通過 (Must ≥3)❌ (operable=2)

加重和は K.O. 通過候補のタイブレーク用 (Suhr 1999 CBA criterion 準拠)。案 A が K.O. 通過候補中で最高スコア。

検討した代替案 (Alternatives Considered)

案 A【採用】: 専用カラム化

telemetry_recordstriage_ms, socratic_ms, body_generation_ms, scoring_ms, cross_validation_ms, consistency_ms, parallel_review_ms, policy_alignment_ms, slug_ms, numbering_ms の 10 カラム (INTEGER) を追加。既存 gate_timings JSON カラムは互換性のため残す (telemetry_v3 schema_version)。新カラムはインデックスなし (集計時の WHERE 句より GROUP BY が主用途)。

実装ファイル / 工数 (合計 ~5.5h):

項目工数
schema.sql + migrate-v3.sql 作成0.5h
audit/types.ts に 10 カラム追加0.5h
audit/persistence.ts INSERT / SELECT / rowToRecord 更新1h
queues/pipeline_consumer.ts: gate_timings → 個別カラム書込1h
バックフィルスクリプト (28 既存レコード、JSON → 個別カラム展開)0.5h
/audit/runs API レスポンス調整0.5h
動作確認 + ローカルテスト1h
runbook (Telemetry セクション) 更新0.5h

法務確認: 不要 (内部 telemetry スキーマの構造化、新規個人情報収集ではない、外部公開なし)

案 B: 別テーブル normalize

telemetry_gate_timings (session_id, gate_name, duration_ms) を新設、JOIN で集計。柔軟性は高い。

  • 不採用理由: ゲート数が固定的 (10) であり、JOIN コスト効率が悪い。集計クエリ記述も JOIN 句が必要で可読性向上が限定的。#efficient #operable で劣後。ただしゲート構成変更が頻発する局面では再評価対象 (§7 Review After 参照)。

案 C: 現状維持 (JSON_EXTRACT 継続)

変更コストゼロ。

  • 不採用理由: per-gate 集計の SQL が複雑なまま、ダッシュボード連携時のパース層も必要。#operable K.O. 不通過 (score=2 < 3)。

影響 (Consequences)

5.1 正の影響 (Good)

  • 月次 σ モニタリング (ADR-0074) のクエリが AVG(scoring_ms) で記述可能になり、SQL 可読性が向上
  • ゲート名がスキーマで Enum 化されるため、命名規約の一元管理が可能 (Property: 横断ルールの明文化)
  • 将来 Jr (2026-10) のオンボーディング時、JSON パースを介さずに API レスポンスを理解可能
  • ダッシュボード連携 (将来) のパース層削減 (副次的メリット)

5.2 負の影響 (Bad)

  • ゲート構成変更時のスキーマ硬直化: ゲート追加・統廃合時に ALTER TABLE が必要となり、DDL 実行中のロックで Workers リクエストがタイムアウトする可能性。JSON 方式なら新キー追加だけで済んでいた。
  • 二重書き込み期間中のデータ不整合リスク: gate_timings JSON と個別カラムの片方のみ書込成功した場合、NULL 混入レコードが発生する可能性。D1 は HTTP 越しの書込でアトミック性が明示保証されていない。
  • 永久二重管理リスク: v4 DROP COLUMN の移行条件・タイミング・担当者が曖昧だと、半年〜1年放置されゲート_timings JSON が「念のため残る」状態が継続しうる。
  • パフォーマンス改善の実測根拠は薄い: 現データ量 (28 件 5ms、5 年後でも ~1500 件) では SQLite フルスキャンで数十 ms 以内に収まる見込みで、5.5h の工数は DX 改善目的である点を明示。
  • YAGNI 違反リスク: LLM API 変更等でゲート構成が変わると、10 カラム固定設計が逆負債化する可能性。

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

  • gate_timings JSON カラムは v3 期間中は併存し、v4 で DROP。NULL 混入時の AVG 集計は SQLite 仕様により NULL が無視されるため、混入レコードがあると統計が歪む可能性 (Runbook で週次ヘルスチェックを実施)。
  • API レスポンスの JSON 形状 (フラット vs ネスト) を本 ADR で「フラット (各 gate_ms をトップレベル)」と決定。変更時は API バージョニングが必要。
  • 案 B (別テーブル) は将来ゲート構成が頻繁に変わる場合の retreat option として残す。
  • ゲート名を src/audit/gates.ts の Enum/const として一元管理し、スキーマカラム名とのズレを CI テストで検出する (#flexible 補強)。

撤退条件 (Rollback Plan)

6.1 撤退判定指標

  • Review After (2026-08-29) 時点で、月次 σ モニタリングのクエリが個別カラムを使用していない (利用実績ゼロ)
  • ゲート構成変更が 1 回以上発生し、ALTER TABLE のリードタイム・DDL ロックが業務影響を出した
  • 二重書き込み期間中の NULL 混入率が 1% を超えた

6.2 本番事故時の原状回復手順

  1. 即時: 新カラム書込を停止 (worker rollback to previous version)。既存 gate_timings JSON カラムは残してあるので読出は継続可能
  2. データ整合: 新カラムのデータは破棄可能 (gate_timings JSON が正本のため、データ損失なし)
  3. schema 復元: ALTER TABLE telemetry_records DROP COLUMN xxx_ms を 10 カラム分実行 (SQLite 3.35+ で対応、本番 D1 で動作確認済)
  4. 検証: SELECT json_extract(gate_timings, '$.scoring') FROM telemetry_records LIMIT 1; で旧クエリが動くこと確認
  5. 所要時間: 全体で 30 分以内 (data backup 不要、既存 JSON が無傷)

6.3 バックフィル失敗時の原状回復

バックフィルは新カラムへの一方向書込のみ。失敗時は新カラムを NULL に UPDATE するだけ (UPDATE telemetry_records SET triage_ms=NULL, ... WHERE ...)。既存 gate_timings JSON は無変更のため原状復帰可能。バックフィルスクリプトは WHERE triage_ms IS NULL 条件で冪等化し、実行前後で SELECT count(*) WHERE triage_ms IS NULL のカウント差をアサーションで検証する。

6.4 v4 DROP COLUMN の Go/No-Go 判断基準

  • v3 スキーマでの書き込みが 100 件以上に達する
  • 新カラムの NULL 率が 0% (ヘルスチェッククエリで確認)
  • 上記 2 条件を満たした翌月の定期メンテナンスで DROP 実行
  • Review After (2026-08-29) のチェックリストに「v4 DROP COLUMN の Go/No-Go 判断」を明示的に追加

6.5 Confirmation (準拠確認 / Fitness Function)

検証項目検証手段実行頻度違反時の対応
二重書き込み整合性SELECT count(*) FROM telemetry_records WHERE gate_timings IS NOT NULL AND triage_ms IS NULL; が 0 件週次 (Runbook 手動 or scheduled worker)1 件以上検出時は即時アラート、書込ロジックの TRANSACTION 化を検証
ゲート名スキーマ整合CI テスト: src/audit/gates.ts の Enum と DB schema カラム名 (*_ms) の一致を検証PR 時 (GitHub Actions)テスト失敗で PR ブロック
バックフィル冪等性バックフィルスクリプト内蔵アサーション: 実行前後の WHERE triage_ms IS NULL カウント比較バックフィル実行時アサーション失敗時は自動 abort、ロールバック手順 (§6.3) を実施
Review After 棚卸しReview After (2026-08-29) チェックリスト: ①月次 σ クエリで個別カラム使用実績、②ゲート構成変更の有無、③NULL 率、④v4 DROP Go/No-Go 判断1 回 (2026-08-29)利用実績ゼロなら案 B または案 C への retreat を検討

参照 (References)

  • 関連 ADR: ADR-0082 (Telemetry v2 / Extends), ADR-0074 (Gate 4 σ monitoring / Confirms), ADR-0077 (Telemetry persistence / Confirms)
  • 関連 PR/Issue: (要追記)
  • 外部資料: