Files
awoooi/apps/api/migrations/adr090_asset_inventory_foundation.sql
OG T 5ae82d1d1f
Some checks failed
CD Pipeline / build-and-deploy (push) Has been cancelled
feat(db): ADR-090 L4 AIOps 地基 — 資產盤點 × 7 項自動化覆蓋矩陣永久化 DB
2026-04-18 下午(台北時區)—— ogt + Claude Opus 4.7 (1M)

MoWoooWorkDown 假警報 RCA 暴露三重結構性失守:
- 110/188 主機 load 18/16 × 13 天 / cadvisor 288% / K3s 120/121 無監控
- Prometheus 僅 35 targets / 58 rules(覆蓋不到三成)
- HostHighCpuLoad 量錯維度(CPU idle vs load_avg)

統帥戰略指令:
- 全景資產 × 七大自動化 × 永久化 DB
- AI 四分工(OpenClaw × NemoTron × Hermes × Claude LLM)
- 所有自動化操作歷程必進 DB,不靠 MD(MD 會漂移)

本 commit 交付:

1. SQL migration (apps/api/migrations/adr090_asset_inventory_foundation.sql)
   - 11 張表 + 33 indexes + 20 CHECK + 3 UNIQUE + 16 FK
   - pgcrypto extension dependency
   - 完整 idempotent(CREATE IF NOT EXISTS + single transaction)
   - 已 apply 進 awoooi_prod(188 PG),驗證通過

2. ADR-090 (docs/adr/ADR-090-monitoring-blindspot-governance.md)
   - 決策紀錄 + 7 引擎對映 + 4 替代方案否決

3. 主戰略文件 (docs/superpowers/specs/2026-04-18-blindspot-governance-capacity-l4.md)
   - §0-§14: 背景 / 根因 / Schema DDL / 4 層防禦 / 7 Phase 實施 /
     HARD_RULES / AI 分工矩陣 / 驗收指標 / 技術債 / 回滾 / 接手協議

4. MASTER §8 Living Changelog 追加 Phase 7 啟動條目

11 張表:
  asset_inventory / asset_discovery_run / asset_coverage_snapshot /
  asset_relationship / alert_rule_catalog / asset_change_event /
  asset_compliance_snapshot / host_capacity_snapshot /
  capacity_violation_event / automation_operation_log /
  ai_collaboration_trace

首筆 bootstrap 記錄已 seed 進 asset_discovery_run
(run_id=6760c5bf-57e5-4a40-b82d-31b794464652)

相關 Memory (未 commit,存於 ~/.claude/...):
  - project_blindspot_governance.md (跨 session 指針)
  - feedback_monitor_self_monitoring.md (監控工具必須被監控)
  - feedback_secrets_leak_incidents_2026-04-18.md (憑證外洩三防線)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 13:18:46 +08:00

608 lines
28 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- ADR-090: 監控盲區治理 + 資產盤點 × 7 項自動化覆蓋矩陣永久化 DB
-- 建立時間: 2026-04-18 下午 (台北時區)
-- 建立者: ogt + Claude Opus 4.7 (1M context)(亞太)
--
-- 上游:
-- - 主戰略: docs/superpowers/specs/2026-04-18-blindspot-governance-capacity-l4.md §5.2
-- - ADR: docs/adr/ADR-090-monitoring-blindspot-governance.md
-- - MEMORY: project_blindspot_governance.md
--
-- 設計說明:
-- 本檔建立 11 張表作為 AWOOOI L4 AIOps 的資產盤點 + 自動化覆蓋 + AI 協作稽核地基。
-- 目標: 把治理從 Markdown 搬進 PostgreSQL讓 AI 四分工 (OpenClaw × NemoTron ×
-- Hermes × Claude LLM) 在結構化資料上做決策,且每次動作必留 trail。
--
-- 對應七大自動化引擎:
-- E1 自動監控 / E2 自動告警 / E3 自動建規則 / E4 自動匹配
-- E5 自動 Playbook / E6 自動修復 / E7 自動 KM
--
-- 執行順序:
-- Step 0: pgcrypto extension (gen_random_uuid 需要)
-- Step 1: asset_inventory — 全景資產主表
-- Step 2: asset_discovery_run — 每次盤點 header
-- Step 3: asset_coverage_snapshot — 資產 × 7 自動化覆蓋矩陣
-- Step 4: asset_relationship — 資產依賴圖 (爆炸半徑)
-- Step 5: alert_rule_catalog — 告警規則本身即資產
-- Step 6: asset_change_event — 資產變化追蹤
-- Step 7: asset_compliance_snapshot — SSL/CVE/secret/backup 合規
-- Step 8: host_capacity_snapshot — 主機容量快照 (NemoTron 每日 02:00 寫)
-- Step 9: capacity_violation_event — 配額違規
-- Step 10: automation_operation_log — 所有 AI 自動化動作稽核主表 🔴
-- Step 11: ai_collaboration_trace — 多 Agent 協作逐步 (辯證歷程)
-- Step 12: 驗收查詢 (comment-only)
--
-- Idempotent 鐵律:
-- - CREATE TABLE IF NOT EXISTS
-- - CREATE INDEX IF NOT EXISTS
-- - CHECK constraint 寫在 CREATE TABLE 內,依賴 IF NOT EXISTS 保護
-- - 本檔可重複執行安全 (rerun 不會破壞既有資料)
--
-- 回滾:
-- DROP TABLE IF EXISTS ai_collaboration_trace, automation_operation_log,
-- capacity_violation_event, host_capacity_snapshot, asset_compliance_snapshot,
-- asset_change_event, alert_rule_catalog, asset_relationship,
-- asset_coverage_snapshot, asset_discovery_run, asset_inventory CASCADE;
--
-- ============================================================================
-- Step 0: pgcrypto extension (gen_random_uuid)
-- ============================================================================
CREATE EXTENSION IF NOT EXISTS pgcrypto;
-- ============================================================================
-- Step 1: asset_inventory — 全景資產主表
-- 用途: 主機 / 容器 / K8s workload / DB / 網站 / API / 套件 / 日誌 / KM / 前端 /
-- 後端 / 容器 / Gitea / CI-CD 全部無例外
-- 主寫者: scanner (asset_discovery) + NemoTron (capacity 欄位)
-- ============================================================================
CREATE TABLE IF NOT EXISTS asset_inventory (
asset_id BIGSERIAL PRIMARY KEY,
asset_key TEXT NOT NULL UNIQUE,
asset_type TEXT NOT NULL,
parent_asset_id BIGINT REFERENCES asset_inventory(asset_id),
environment TEXT NOT NULL DEFAULT 'prod',
host TEXT,
namespace TEXT,
name TEXT NOT NULL,
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
tags TEXT[] NOT NULL DEFAULT '{}',
owner_team TEXT,
criticality TEXT,
data_classification TEXT,
external BOOLEAN NOT NULL DEFAULT false,
lifecycle_state TEXT NOT NULL DEFAULT 'active',
source_repo TEXT,
source_commit_sha TEXT,
-- 容量欄位 (Layer 4 AI 巡檢用)
cpu_avg_7d NUMERIC(5,2),
mem_avg_7d NUMERIC(5,2),
capacity_headroom NUMERIC(5,2),
resource_limits JSONB,
resource_requests JSONB,
quota_violation_count INT NOT NULL DEFAULT 0,
sla_target JSONB,
cost_monthly_usd NUMERIC(10,2),
-- 生命週期時間戳
first_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
decommissioned_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT asset_inventory_criticality_valid
CHECK (criticality IS NULL OR criticality IN ('P0','P1','P2','P3')),
CONSTRAINT asset_inventory_data_class_valid
CHECK (data_classification IS NULL OR data_classification IN
('public','internal','sensitive','secret')),
CONSTRAINT asset_inventory_lifecycle_valid
CHECK (lifecycle_state IN
('planned','provisioning','active','degraded','deprecated','decommissioned')),
CONSTRAINT asset_inventory_type_valid
CHECK (asset_type IN (
'host','container','k8s_workload','k8s_resource','database','table',
'website','api_endpoint','package','log_stream','km_entry',
'frontend','backend','ci_pipeline','gitea_repo','monitoring_target',
'secret','volume','network','certificate','scheduled_job',
'message_queue','cache','dashboard','ai_agent','llm_model',
'third_party_service','backup_target'
))
);
COMMENT ON TABLE asset_inventory IS
'ADR-090: 全景資產主表。每一個主機/容器/K8s workload/DB/網站/API/套件/...都有一筆,跨 run 沿用同 asset_id。';
CREATE INDEX IF NOT EXISTS idx_asset_inventory_type_host
ON asset_inventory(asset_type, host);
CREATE INDEX IF NOT EXISTS idx_asset_inventory_env_lifecycle
ON asset_inventory(environment, lifecycle_state);
CREATE INDEX IF NOT EXISTS idx_asset_inventory_metadata_gin
ON asset_inventory USING GIN (metadata);
CREATE INDEX IF NOT EXISTS idx_asset_inventory_tags_gin
ON asset_inventory USING GIN (tags);
CREATE INDEX IF NOT EXISTS idx_asset_inventory_active_last_seen
ON asset_inventory(last_seen_at DESC)
WHERE lifecycle_state = 'active';
-- 註: partial index 只索引 active 資產,按最近出現時間排序
-- ============================================================================
-- Step 2: asset_discovery_run — 每次盤點 header
-- 用途: 記錄每次全景掃描的起止時間、掃描範圍、掃到什麼、新增/消失多少
-- 觸發: cron (每日) / ai (proactive_inspector) / human (手動) / incident
-- ============================================================================
CREATE TABLE IF NOT EXISTS asset_discovery_run (
run_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
triggered_by TEXT NOT NULL,
scope TEXT[] NOT NULL,
scan_depth TEXT NOT NULL DEFAULT 'shallow',
host_filter TEXT[],
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
ended_at TIMESTAMPTZ,
status TEXT NOT NULL,
total_assets INT,
new_assets INT NOT NULL DEFAULT 0,
modified_assets INT NOT NULL DEFAULT 0,
disappeared_assets INT NOT NULL DEFAULT 0,
tools_used JSONB,
duration_ms INT,
error TEXT,
summary JSONB,
CONSTRAINT asset_discovery_run_status_valid
CHECK (status IN ('running','success','partial','failed','aborted')),
CONSTRAINT asset_discovery_run_scan_depth_valid
CHECK (scan_depth IN ('shallow','deep','full'))
);
COMMENT ON TABLE asset_discovery_run IS
'ADR-090: 每次資產盤點的 header。run_id 作為下游 snapshot/event/change 的關聯主鍵。';
CREATE INDEX IF NOT EXISTS idx_asset_discovery_run_started
ON asset_discovery_run(started_at DESC);
CREATE INDEX IF NOT EXISTS idx_asset_discovery_run_status
ON asset_discovery_run(status) WHERE status IN ('running','failed','partial');
-- ============================================================================
-- Step 3: asset_coverage_snapshot — 資產 × 7 項自動化 覆蓋矩陣
-- 用途: 每個資產在 7 個自動化維度上的覆蓋狀態 (green/yellow/red)
-- 鐵律: 每次 discovery_run 為每個 asset 寫 7 筆 (7 dimensions)
-- ============================================================================
CREATE TABLE IF NOT EXISTS asset_coverage_snapshot (
snapshot_id BIGSERIAL PRIMARY KEY,
run_id UUID NOT NULL REFERENCES asset_discovery_run(run_id) ON DELETE CASCADE,
asset_id BIGINT NOT NULL REFERENCES asset_inventory(asset_id),
dimension TEXT NOT NULL,
coverage_status TEXT NOT NULL,
evidence JSONB NOT NULL DEFAULT '{}'::jsonb,
gap_reason TEXT,
recommended_action TEXT,
confidence NUMERIC(3,2),
detected_by TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT asset_coverage_snapshot_dimension_valid
CHECK (dimension IN (
'auto_monitoring','auto_alerting','auto_rule_creation',
'auto_rule_matching','auto_playbook','auto_remediation','auto_km_creation'
)),
CONSTRAINT asset_coverage_snapshot_status_valid
CHECK (coverage_status IN ('green','yellow','red','unknown')),
CONSTRAINT asset_coverage_snapshot_unique
UNIQUE (run_id, asset_id, dimension)
);
COMMENT ON TABLE asset_coverage_snapshot IS
'ADR-090: 計分卡。查 red COUNT 即覆蓋率 SLO。evidence 欄位串 playbook_id/km_entry_id/rule_name。';
CREATE INDEX IF NOT EXISTS idx_asset_coverage_snapshot_asset_dim
ON asset_coverage_snapshot(asset_id, dimension);
CREATE INDEX IF NOT EXISTS idx_asset_coverage_snapshot_red_yellow
ON asset_coverage_snapshot(coverage_status)
WHERE coverage_status IN ('red','yellow');
CREATE INDEX IF NOT EXISTS idx_asset_coverage_snapshot_run
ON asset_coverage_snapshot(run_id);
-- ============================================================================
-- Step 4: asset_relationship — 資產依賴圖 (爆炸半徑必需)
-- 用途: 記錄資產之間的 depends_on / calls / stores_data_in / backs_up_to 關係
-- AI 用途: OpenClaw 計算 blast_radius 時查這張表
-- ============================================================================
CREATE TABLE IF NOT EXISTS asset_relationship (
relationship_id BIGSERIAL PRIMARY KEY,
from_asset_id BIGINT NOT NULL REFERENCES asset_inventory(asset_id),
to_asset_id BIGINT NOT NULL REFERENCES asset_inventory(asset_id),
relationship_type TEXT NOT NULL,
strength NUMERIC(3,2),
metadata JSONB,
first_detected_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_verified_at TIMESTAMPTZ,
is_active BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT asset_relationship_type_valid
CHECK (relationship_type IN (
'depends_on','calls','stores_data_in','backs_up_to',
'routes_to','authenticates_via','monitors','alerts_to','logs_to'
)),
CONSTRAINT asset_relationship_strength_valid
CHECK (strength IS NULL OR (strength >= 0 AND strength <= 1)),
CONSTRAINT asset_relationship_unique
UNIQUE (from_asset_id, to_asset_id, relationship_type),
CONSTRAINT asset_relationship_no_self_loop
CHECK (from_asset_id <> to_asset_id)
);
COMMENT ON TABLE asset_relationship IS
'ADR-090: 資產依賴圖。AI 計算爆炸半徑必讀。edge 而非 tree,支援多重關係。';
CREATE INDEX IF NOT EXISTS idx_asset_relationship_from
ON asset_relationship(from_asset_id) WHERE is_active;
CREATE INDEX IF NOT EXISTS idx_asset_relationship_to
ON asset_relationship(to_asset_id) WHERE is_active;
CREATE INDEX IF NOT EXISTS idx_asset_relationship_type
ON asset_relationship(relationship_type);
-- ============================================================================
-- Step 5: alert_rule_catalog — 告警規則本身即資產
-- 用途: 把 alert_rules.yaml 升級為 DB-driven;記錄誰創的 / 何時 / 效能 / 生死
-- AI 用途: Hermes 做 noise_rate 分析 / 提建議 retire 低品質規則
-- ============================================================================
CREATE TABLE IF NOT EXISTS alert_rule_catalog (
rule_id BIGSERIAL PRIMARY KEY,
rule_name TEXT NOT NULL UNIQUE,
source TEXT NOT NULL,
expr TEXT NOT NULL,
duration_seconds INT,
severity TEXT,
labels JSONB,
annotations JSONB,
linked_asset_ids BIGINT[],
created_by_agent TEXT,
-- 規則品質追蹤
true_positive_count INT NOT NULL DEFAULT 0,
false_positive_count INT NOT NULL DEFAULT 0,
noise_rate NUMERIC(5,2),
last_fired_at TIMESTAMPTZ,
-- 信心與演化
confidence NUMERIC(3,2),
review_status TEXT,
superseded_by_rule_id BIGINT REFERENCES alert_rule_catalog(rule_id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT alert_rule_catalog_source_valid
CHECK (source IN ('yaml_hardcoded','ai_generated','human_written','playbook_derived')),
CONSTRAINT alert_rule_catalog_review_valid
CHECK (review_status IS NULL OR review_status IN
('draft','approved','deprecated','retired'))
);
COMMENT ON TABLE alert_rule_catalog IS
'ADR-090: 告警規則即一等資產。支援規則演化 (ai_generated) 與替代鏈 (superseded_by)。';
CREATE INDEX IF NOT EXISTS idx_alert_rule_catalog_source
ON alert_rule_catalog(source);
CREATE INDEX IF NOT EXISTS idx_alert_rule_catalog_assets_gin
ON alert_rule_catalog USING GIN (linked_asset_ids);
CREATE INDEX IF NOT EXISTS idx_alert_rule_catalog_review
ON alert_rule_catalog(review_status) WHERE review_status IS NOT NULL;
-- ============================================================================
-- Step 6: asset_change_event — 資產變化追蹤 (diff between runs)
-- 用途: 兩次 discovery_run 之間的 delta。新增/消失/修改/覆蓋率變化
-- ============================================================================
CREATE TABLE IF NOT EXISTS asset_change_event (
event_id BIGSERIAL PRIMARY KEY,
run_id UUID NOT NULL REFERENCES asset_discovery_run(run_id),
asset_id BIGINT REFERENCES asset_inventory(asset_id),
change_type TEXT NOT NULL,
before_state JSONB,
after_state JSONB,
diff JSONB,
detected_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
ai_analysis TEXT,
CONSTRAINT asset_change_event_type_valid
CHECK (change_type IN (
'asset_added','asset_removed','asset_modified',
'coverage_improved','coverage_degraded',
'criticality_changed','owner_changed','lifecycle_changed'
))
);
COMMENT ON TABLE asset_change_event IS
'ADR-090: 資產變化追蹤。兩次掃描的 diff 明確落地,LLM 可加 ai_analysis 解讀。';
CREATE INDEX IF NOT EXISTS idx_asset_change_event_run
ON asset_change_event(run_id);
CREATE INDEX IF NOT EXISTS idx_asset_change_event_asset_time
ON asset_change_event(asset_id, detected_at DESC);
-- ============================================================================
-- Step 7: asset_compliance_snapshot — 合規狀態 (SSL/CVE/secret/backup)
-- 用途: 與 coverage 不同軸的合規追蹤。SSL cert 到期 / CVE 掃描 / secret 輪替
-- ============================================================================
CREATE TABLE IF NOT EXISTS asset_compliance_snapshot (
snapshot_id BIGSERIAL PRIMARY KEY,
run_id UUID REFERENCES asset_discovery_run(run_id),
asset_id BIGINT NOT NULL REFERENCES asset_inventory(asset_id),
dimension TEXT NOT NULL,
status TEXT NOT NULL,
expires_at TIMESTAMPTZ,
detail JSONB,
remediation_deadline TIMESTAMPTZ,
detected_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT asset_compliance_snapshot_dimension_valid
CHECK (dimension IN (
'ssl_cert_valid','cve_scan','secret_rotated','backup_tested',
'audit_log_enabled','access_reviewed','encryption_at_rest'
)),
CONSTRAINT asset_compliance_snapshot_status_valid
CHECK (status IN ('compliant','warning','violation','unknown'))
);
COMMENT ON TABLE asset_compliance_snapshot IS
'ADR-090: 合規狀態快照。與 coverage 不同軸,SSL/CVE/secret/backup 專用。';
CREATE INDEX IF NOT EXISTS idx_asset_compliance_snapshot_asset_dim
ON asset_compliance_snapshot(asset_id, dimension);
CREATE INDEX IF NOT EXISTS idx_asset_compliance_snapshot_expiring
ON asset_compliance_snapshot(expires_at)
WHERE expires_at IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_asset_compliance_snapshot_violations
ON asset_compliance_snapshot(status)
WHERE status IN ('warning','violation');
-- ============================================================================
-- Step 8: host_capacity_snapshot — 主機容量快照
-- 用途: NemoTron 每日 02:00 台北 自主容量巡檢寫入
-- Layer 4 核心表。hermes 做預測,openclaw 產建議,全寫這張
-- ============================================================================
CREATE TABLE IF NOT EXISTS host_capacity_snapshot (
snapshot_id BIGSERIAL PRIMARY KEY,
host TEXT NOT NULL,
captured_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
load1 NUMERIC(6,2),
load5 NUMERIC(6,2),
load15 NUMERIC(6,2),
cpu_used_pct NUMERIC(5,2),
cpu_iowait_pct NUMERIC(5,2),
mem_used_pct NUMERIC(5,2),
swap_used_pct NUMERIC(5,2),
disk_used_pct JSONB,
container_count INT,
k8s_pod_count INT,
top_cpu_offenders JSONB,
top_mem_offenders JSONB,
headroom_pct NUMERIC(5,2),
ai_verdict TEXT,
ai_reasoning TEXT,
recommended_actions JSONB,
written_by_agent TEXT NOT NULL,
CONSTRAINT host_capacity_snapshot_verdict_valid
CHECK (ai_verdict IS NULL OR ai_verdict IN ('safe','warning','critical','unknown'))
);
COMMENT ON TABLE host_capacity_snapshot IS
'ADR-090: NemoTron 每日主機容量巡檢結果。Layer 4 AI 自主治理核心表。';
CREATE INDEX IF NOT EXISTS idx_host_capacity_snapshot_host_time
ON host_capacity_snapshot(host, captured_at DESC);
CREATE INDEX IF NOT EXISTS idx_host_capacity_snapshot_critical
ON host_capacity_snapshot(ai_verdict)
WHERE ai_verdict IN ('warning','critical');
-- ============================================================================
-- Step 9: capacity_violation_event — 配額違規事件
-- 用途: 記錄任何「缺 limit」「超 request」「主機飽和」的違規
-- ============================================================================
CREATE TABLE IF NOT EXISTS capacity_violation_event (
event_id BIGSERIAL PRIMARY KEY,
asset_id BIGINT REFERENCES asset_inventory(asset_id),
host TEXT,
violation_type TEXT NOT NULL,
threshold NUMERIC(10,2),
actual_value NUMERIC(10,2),
detected_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
auto_action TEXT,
auto_action_op_id UUID,
human_override TEXT,
resolved_at TIMESTAMPTZ,
CONSTRAINT capacity_violation_event_type_valid
CHECK (violation_type IN (
'no_limit_set','over_request','over_limit','host_saturation',
'over_sla_budget','unauthorized_new_deploy'
))
);
COMMENT ON TABLE capacity_violation_event IS
'ADR-090: 配額違規稽核。每次 AI 偵測到資產無 limit/主機飽和/未授權部署 都寫一筆。';
CREATE INDEX IF NOT EXISTS idx_capacity_violation_event_asset_time
ON capacity_violation_event(asset_id, detected_at DESC);
CREATE INDEX IF NOT EXISTS idx_capacity_violation_event_unresolved
ON capacity_violation_event(detected_at DESC)
WHERE resolved_at IS NULL;
-- ============================================================================
-- Step 10: automation_operation_log — 所有 AI 自動化動作稽核主表 🔴
-- 鐵律: 每一個 AI 自動化動作都必須寫一筆。缺筆 = 治理失效
-- ============================================================================
CREATE TABLE IF NOT EXISTS automation_operation_log (
op_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
operation_type TEXT NOT NULL,
asset_id BIGINT REFERENCES asset_inventory(asset_id),
incident_id BIGINT,
run_id UUID REFERENCES asset_discovery_run(run_id),
actor TEXT NOT NULL,
input JSONB NOT NULL DEFAULT '{}'::jsonb,
output JSONB NOT NULL DEFAULT '{}'::jsonb,
dry_run_result JSONB,
status TEXT NOT NULL,
error TEXT,
duration_ms INT,
tokens_in INT,
tokens_out INT,
cost_usd NUMERIC(10,6),
budget_bucket TEXT,
parent_op_id UUID REFERENCES automation_operation_log(op_id),
retry_count INT NOT NULL DEFAULT 0,
retry_of_op_id UUID REFERENCES automation_operation_log(op_id),
stderr_feed_back TEXT,
tags TEXT[],
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT automation_operation_log_type_valid
CHECK (operation_type IN (
'monitor_configured','monitor_removed',
'alert_fired','alert_suppressed','alert_routed',
'rule_created','rule_updated','rule_matched','rule_rejected','rule_deprecated',
'playbook_generated','playbook_updated','playbook_executed',
'remediation_executed','remediation_verified','remediation_rolled_back',
'self_correction_attempted',
'km_created','km_updated','km_linked',
'asset_discovered','coverage_recalculated',
'capacity_recommendation','quota_enforced'
)),
CONSTRAINT automation_operation_log_status_valid
CHECK (status IN ('pending','success','failed','dry_run','rolled_back'))
);
COMMENT ON TABLE automation_operation_log IS
'ADR-090: 所有 AI 自動化動作稽核主表。retry_of_op_id + stderr_feed_back 支援引擎 4 閉環。';
CREATE INDEX IF NOT EXISTS idx_automation_operation_log_type_time
ON automation_operation_log(operation_type, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_automation_operation_log_asset_time
ON automation_operation_log(asset_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_automation_operation_log_incident
ON automation_operation_log(incident_id)
WHERE incident_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_automation_operation_log_actor_time
ON automation_operation_log(actor, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_automation_operation_log_retry
ON automation_operation_log(retry_of_op_id)
WHERE retry_of_op_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_automation_operation_log_tags_gin
ON automation_operation_log USING GIN (tags);
-- ============================================================================
-- Step 11: ai_collaboration_trace — 多 Agent 協作逐步 (LLM × OpenClaw × NemoTron × Hermes)
-- 用途: 每個 automation_operation_log 背後的 N 步 AI 決策過程
-- 最寶貴的語料: challenged_by + accepted 支援 RLHF fine-tune
-- ============================================================================
CREATE TABLE IF NOT EXISTS ai_collaboration_trace (
trace_id BIGSERIAL PRIMARY KEY,
op_id UUID NOT NULL REFERENCES automation_operation_log(op_id) ON DELETE CASCADE,
step_order INT NOT NULL,
agent TEXT NOT NULL,
model TEXT,
system_prompt_version TEXT,
prompt TEXT,
response JSONB,
confidence NUMERIC(3,2),
challenged_by TEXT[],
accepted BOOLEAN,
tokens_in INT,
tokens_out INT,
duration_ms INT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT ai_collaboration_trace_unique_step
UNIQUE (op_id, step_order)
);
COMMENT ON TABLE ai_collaboration_trace IS
'ADR-090: AI 多 Agent 協作逐步紀錄。challenged_by + accepted = RLHF 訓練語料金礦。';
CREATE INDEX IF NOT EXISTS idx_ai_collaboration_trace_op
ON ai_collaboration_trace(op_id, step_order);
CREATE INDEX IF NOT EXISTS idx_ai_collaboration_trace_agent_time
ON ai_collaboration_trace(agent, created_at DESC);
-- ============================================================================
-- Step 12: 驗收查詢 (執行後手動跑,驗證 11 張表都到位)
-- ============================================================================
-- SELECT table_name
-- FROM information_schema.tables
-- WHERE table_schema = 'public'
-- AND table_name IN (
-- 'asset_inventory',
-- 'asset_discovery_run',
-- 'asset_coverage_snapshot',
-- 'asset_relationship',
-- 'alert_rule_catalog',
-- 'asset_change_event',
-- 'asset_compliance_snapshot',
-- 'host_capacity_snapshot',
-- 'capacity_violation_event',
-- 'automation_operation_log',
-- 'ai_collaboration_trace'
-- )
-- ORDER BY table_name;
-- -- 預期: 11 筆
-- SELECT table_name, COUNT(*) AS column_count
-- FROM information_schema.columns
-- WHERE table_schema = 'public'
-- AND table_name LIKE 'asset_%' OR table_name IN
-- ('alert_rule_catalog','host_capacity_snapshot','capacity_violation_event',
-- 'automation_operation_log','ai_collaboration_trace')
-- GROUP BY table_name
-- ORDER BY table_name;
-- SELECT conname, conrelid::regclass AS table_name
-- FROM pg_constraint
-- WHERE conrelid IN (
-- 'asset_inventory'::regclass,
-- 'asset_discovery_run'::regclass,
-- 'asset_coverage_snapshot'::regclass,
-- 'asset_relationship'::regclass,
-- 'alert_rule_catalog'::regclass,
-- 'asset_change_event'::regclass,
-- 'asset_compliance_snapshot'::regclass,
-- 'host_capacity_snapshot'::regclass,
-- 'capacity_violation_event'::regclass,
-- 'automation_operation_log'::regclass,
-- 'ai_collaboration_trace'::regclass
-- ) AND contype = 'c' -- CHECK constraints only
-- ORDER BY table_name, conname;
-- ============================================================================
-- END OF MIGRATION adr090_asset_inventory_foundation.sql
-- 預計新增物件: 11 tables + 33 indexes + 20 CHECK constraints + 3 UNIQUE + 16 FK references
-- 依賴: pgcrypto extension (for gen_random_uuid)
-- 影響資料: 無 (純 DDL, 不動現有表)
-- 回滾: 見檔案頭部
-- ============================================================================