型付き辺: 出 7 / 入 1
ADR-0087: telemetry の gate_timings を構造化カラムに正規化
- 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 で導入した TelemetryRecord の gate_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:174でJSON.stringify(gateTimings)(gateTimings は Record<string, number>) - 読出:
/audit/runs/:sessionIdでJSON.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)
- per-gate 集計 (平均所要時間、p95 等) のクエリが
json_extract()で記述複雑、可読性低下。 - ダッシュボード (将来 Cloudflare Analytics Engine or Grafana 連携) で per-gate 時系列をプロットする際、JSON フィールドだとパース層が必要 (※ 将来の副次的メリットであり、現時点では存在しない外部システムへの対応である点に留意)。
- ゲート構成変更 (新ゲート追加等) 時に 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.0 | 4 | 3 | 2 |
#operable (Must) | ×2.0 | 5 | 3 | 2 |
#flexible (High) | ×1.0 | 2 | 4 | 5 |
#efficient (Medium) | ×0.5 | 4 | 2 | 3 |
#reliable (High) | ×1.0 | 3 | 3 | 5 |
| 加重和 (正規化) | 0.736 | 0.591 | 0.609 | |
| K.O. 通過 (Must ≥3) | ✓ | ✓ | ❌ (operable=2) |
加重和は K.O. 通過候補のタイブレーク用 (Suhr 1999 CBA criterion 準拠)。案 A が K.O. 通過候補中で最高スコア。
検討した代替案 (Alternatives Considered)
案 A【採用】: 専用カラム化
telemetry_records に triage_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 が複雑なまま、ダッシュボード連携時のパース層も必要。
#operableK.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 本番事故時の原状回復手順
- 即時: 新カラム書込を停止 (worker rollback to previous version)。既存 gate_timings JSON カラムは残してあるので読出は継続可能
- データ整合: 新カラムのデータは破棄可能 (gate_timings JSON が正本のため、データ損失なし)
- schema 復元:
ALTER TABLE telemetry_records DROP COLUMN xxx_msを 10 カラム分実行 (SQLite 3.35+ で対応、本番 D1 で動作確認済) - 検証:
SELECT json_extract(gate_timings, '$.scoring') FROM telemetry_records LIMIT 1;で旧クエリが動くこと確認 - 所要時間: 全体で 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: (要追記)
- 外部資料:
- SQLite 3.35+ DROP COLUMN 仕様: https://www.sqlite.org/lang_altertable.html
- Cloudflare D1 ドキュメント (アトミック性に関する記述): (要追記)