<a id="db-ops"></a>
# Finding·승인·알림·운영 PostgreSQL 객체

finding/evidence/retest, intercept, archive, notification, log와 side-question 객체를 전수 참조한다.

> **기준** · core `db/schema.sql`은 PostgreSQL 49개 table·최종 column 합집합 520개·명시 index 97개·function 4개·trigger 22개다. startup 보조 ledger `llm_records`와 `llm_usage`가 2개 table·31개 column·5개 index를 더해 선언 총계는 51개 table·551개 column·102개 index다. `Ensure*Table` 실패는 로그 후 계속되므로 보조 ledger 존재는 core startup 보장이 아니다. Traffic은 별도 SQLite 3개 일반 table·FTS5 virtual table 1개·index 6개다. 아래 DDL은 고정 source revision의 선언이며 실제 운영 DB 적용 상태는 관찰하지 않았다.

## 객체 색인

| 객체 | 저장소 | 목적 |
|---|---|---|
| [`settings`](#table-settings) | PostgreSQL | `settings` 도메인의 영속 상태를 저장한다. 정확한 의미는 필드 정의와 사용 모듈을 함께 본다. |
| [`task_archives`](#table-task-archives) | PostgreSQL | task archive/restore/delete background job 상태와 package metadata를 저장한다. |
| [`intercept_rules`](#table-intercept-rules) | PostgreSQL | `intercept_rules` 도메인의 영속 상태를 저장한다. 정확한 의미는 필드 정의와 사용 모듈을 함께 본다. |
| [`intercept_pending`](#table-intercept-pending) | PostgreSQL | tool 승인 요청과 결정/audit 상태를 저장한다. |
| [`findings`](#table-findings) | PostgreSQL | 조회·편집·보고용 finding row를 graph finding node와 함께 유지한다. |
| [`finding_retests`](#table-finding-retests) | PostgreSQL | `finding_retests` 도메인의 영속 상태를 저장한다. 정확한 의미는 필드 정의와 사용 모듈을 함께 본다. |
| [`traffic_evidence_snapshots`](#table-traffic-evidence-snapshots) | PostgreSQL | 캡처 exchange의 request/response metadata와 body hash를 finding용 snapshot으로 고정한다. |
| [`finding_traffic_bindings`](#table-finding-traffic-bindings) | PostgreSQL | finding과 snapshot의 순서·role·설명을 연결한다. |
| [`server_logs`](#table-server-logs) | PostgreSQL | `server_logs` 도메인의 영속 상태를 저장한다. 정확한 의미는 필드 정의와 사용 모듈을 함께 본다. |
| [`side_question_sessions`](#table-side-question-sessions) | PostgreSQL | `side_question_sessions` 도메인의 영속 상태를 저장한다. 정확한 의미는 필드 정의와 사용 모듈을 함께 본다. |
| [`side_question_requests`](#table-side-question-requests) | PostgreSQL | `side_question_requests` 도메인의 영속 상태를 저장한다. 정확한 의미는 필드 정의와 사용 모듈을 함께 본다. |
| [`asset_intercept_rules`](#table-asset-intercept-rules) | PostgreSQL | `asset_intercept_rules` 도메인의 영속 상태를 저장한다. 정확한 의미는 필드 정의와 사용 모듈을 함께 본다. |
| [`task_intercept_rules`](#table-task-intercept-rules) | PostgreSQL | `task_intercept_rules` 도메인의 영속 상태를 저장한다. 정확한 의미는 필드 정의와 사용 모듈을 함께 본다. |
| [`notification_channels`](#table-notification-channels) | PostgreSQL | `notification_channels` 도메인의 영속 상태를 저장한다. 정확한 의미는 필드 정의와 사용 모듈을 함께 본다. |
| [`notification_events`](#table-notification-events) | PostgreSQL | `notification_events` 도메인의 영속 상태를 저장한다. 정확한 의미는 필드 정의와 사용 모듈을 함께 본다. |
| [`notification_deliveries`](#table-notification-deliveries) | PostgreSQL | channel별 전달 시도, lease, retry, 오류를 저장한다. |

<a id="table-settings"></a>
## `settings`

`settings` 도메인의 영속 상태를 저장한다. 정확한 의미는 필드 정의와 사용 모듈을 함께 본다.

정의 원본은 `db/schema.sql`다. [등록 근거](evidence:schema-core)

```sql
CREATE TABLE IF NOT EXISTS settings (
    key        TEXT PRIMARY KEY,
    value      TEXT NOT NULL,
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
```

### 의미·소비·운영 경계

[`settings` 필드 의미, reader/writer, index 비용과 retention 판정](semantic-catalog.md#semantic-settings)에서 물리 DDL과 별도로 C/A/U를 확인한다. 운영 DB 적용·row 규모·query plan·role은 관찰하지 않았다.


<a id="table-task-archives"></a>
## `task_archives`

task archive/restore/delete background job 상태와 package metadata를 저장한다.

정의 원본은 `db/schema.sql`다. [등록 근거](evidence:schema-core)

```sql
CREATE TABLE IF NOT EXISTS task_archives (
    id                         BIGSERIAL PRIMARY KEY,
    task_id                    BIGINT NOT NULL UNIQUE REFERENCES tasks(id) ON DELETE CASCADE,
    state                      TEXT NOT NULL DEFAULT 'archive_queued' CHECK (state IN (
                                   'archive_queued','archiving','archive_failed','ready',
                                   'restore_queued','restoring','restore_failed',
                                   'delete_queued','deleting','delete_failed'
                               )),
    phase                      TEXT NOT NULL DEFAULT 'queued',
    progress                   INTEGER NOT NULL DEFAULT 0 CHECK (progress BETWEEN 0 AND 100),
    error                      TEXT NOT NULL DEFAULT '',
    warnings                   JSONB NOT NULL DEFAULT '[]',
    format_version             INTEGER NOT NULL DEFAULT 2,
    archive_path               TEXT NOT NULL DEFAULT '',
    sha256                     TEXT NOT NULL DEFAULT '',
    original_size              BIGINT NOT NULL DEFAULT 0,
    compressed_size            BIGINT NOT NULL DEFAULT 0,
    task_name                  TEXT NOT NULL DEFAULT '',
    task_description           TEXT NOT NULL DEFAULT '',
    task_goal                  TEXT NOT NULL DEFAULT '',
    original_status            TEXT NOT NULL DEFAULT '',
    category_id_snapshot       BIGINT,
    category_name_snapshot     TEXT NOT NULL DEFAULT '',
    source_task_ids            BIGINT[] NOT NULL DEFAULT '{}',
    remaining_timeout_seconds  BIGINT NOT NULL DEFAULT 0,
    data_counts                JSONB NOT NULL DEFAULT '{}',
    aggregate_stats            JSONB NOT NULL DEFAULT '{}',
    archived_at                TIMESTAMPTZ,
    requested_at               TIMESTAMPTZ NOT NULL DEFAULT now(),
    created_at                 TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at                 TIMESTAMPTZ NOT NULL DEFAULT now()
);
```

### 후속 변경·제약

- `ALTER TABLE task_archives ALTER COLUMN format_version SET DEFAULT 2;`

`format_version` ALTER는 이미 저장된 archive row를 2로 backfill하지 않고 이후 default INSERT에만 적용한다. [migration 경계](functions-triggers.md#schema-migrations)

### 인덱스

- `CREATE INDEX IF NOT EXISTS idx_task_archives_state ON task_archives(state, requested_at, id);`
- `CREATE INDEX IF NOT EXISTS idx_task_archives_archived ON task_archives(archived_at DESC, id DESC);`
- `CREATE INDEX IF NOT EXISTS idx_task_archives_sources ON task_archives USING GIN(source_task_ids);`

### 의미·소비·운영 경계

[`task_archives` 필드 의미, reader/writer, index 비용과 retention 판정](semantic-catalog.md#semantic-task-archives)에서 물리 DDL과 별도로 C/A/U를 확인한다. 운영 DB 적용·row 규모·query plan·role은 관찰하지 않았다.


<a id="table-intercept-rules"></a>
## `intercept_rules`

`intercept_rules` 도메인의 영속 상태를 저장한다. 정확한 의미는 필드 정의와 사용 모듈을 함께 본다.

정의 원본은 `db/schema.sql`다. [등록 근거](evidence:schema-core)

```sql
CREATE TABLE IF NOT EXISTS intercept_rules (
    id              BIGSERIAL PRIMARY KEY,
    name            TEXT NOT NULL,
    enabled         BOOLEAN NOT NULL DEFAULT true,
    priority        INTEGER NOT NULL DEFAULT 0,
    match_target    TEXT NOT NULL CHECK (match_target IN ('tool_name', 'tool_input')),
    match_type      TEXT NOT NULL CHECK (match_type IN ('string', 'regex')),
    pattern         TEXT NOT NULL,
    action          TEXT NOT NULL CHECK (action IN ('allow', 'deny', 'ask')),
    message         TEXT NOT NULL DEFAULT '',
    timeout_enabled BOOLEAN NOT NULL DEFAULT true,
    timeout_seconds INTEGER NOT NULL DEFAULT 60,
    timeout_action  TEXT    NOT NULL DEFAULT 'deny',
    created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at      TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
```

### 의미·소비·운영 경계

[`intercept_rules` 필드 의미, reader/writer, index 비용과 retention 판정](semantic-catalog.md#semantic-intercept-rules)에서 물리 DDL과 별도로 C/A/U를 확인한다. 운영 DB 적용·row 규모·query plan·role은 관찰하지 않았다.


<a id="table-intercept-pending"></a>
## `intercept_pending`

tool 승인 요청과 결정/audit 상태를 저장한다.

정의 원본은 `db/schema.sql`다. [등록 근거](evidence:schema-core)

```sql
CREATE TABLE IF NOT EXISTS intercept_pending (
    id              BIGSERIAL PRIMARY KEY,
    rule_id         BIGINT REFERENCES intercept_rules(id) ON DELETE SET NULL,
    conversation_id BIGINT REFERENCES conversations(id) ON DELETE CASCADE,
    task_id         TEXT,
    agent_name      TEXT NOT NULL DEFAULT '',
    tool_name       TEXT NOT NULL,
    tool_input      JSONB NOT NULL DEFAULT '{}',
    status          TEXT NOT NULL DEFAULT 'pending'
                        CHECK (status IN ('pending', 'allowed', 'denied', 'timeout')),
    -- 判定理由:规则命中时为规则 message;LLM 兜底判定时为模型给的简短理由(前缀 [模型])。
    reason          TEXT NOT NULL DEFAULT '',
    decided_at      TIMESTAMPTZ,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
```

### 후속 변경·제약

- `ALTER TABLE intercept_pending ADD COLUMN IF NOT EXISTS reason TEXT NOT NULL DEFAULT '';`
- `ALTER TABLE intercept_pending ADD COLUMN IF NOT EXISTS audit JSONB;`
- `ALTER TABLE intercept_pending ADD COLUMN IF NOT EXISTS decision_source TEXT NOT NULL DEFAULT '';`

Column 추가 뒤 startup은 `decision_source=''`인 row를 매번 CASE update한다. `rule_id` non-null은 `rule`, reason이 `[模型]`로 시작하면 `model`, 나머지는 `unknown`이므로 빈 값을 의도적으로 저장해도 다음 startup에 재분류된다. [migration 경계](functions-triggers.md#schema-migrations)

### 인덱스

- `CREATE INDEX IF NOT EXISTS idx_intercept_pending_status ON intercept_pending(status, created_at DESC);`
- `CREATE INDEX IF NOT EXISTS idx_intercept_pending_task ON intercept_pending(task_id, created_at DESC);`

### 의미·소비·운영 경계

[`intercept_pending` 필드 의미, reader/writer, index 비용과 retention 판정](semantic-catalog.md#semantic-intercept-pending)에서 물리 DDL과 별도로 C/A/U를 확인한다. 운영 DB 적용·row 규모·query plan·role은 관찰하지 않았다.


<a id="table-findings"></a>
## `findings`

조회·편집·보고용 finding row를 graph finding node와 함께 유지한다.

정의 원본은 `db/schema.sql`다. [등록 근거](evidence:schema-core)

```sql
CREATE TABLE IF NOT EXISTS findings (
    id          BIGSERIAL PRIMARY KEY,
    task_id     BIGINT REFERENCES tasks(id) ON DELETE SET NULL,
    node_id     BIGINT REFERENCES exploration_nodes(id) ON DELETE SET NULL,
    vulnclass   TEXT NOT NULL DEFAULT '',
    -- 漏洞名称(可读标题)；为空时前端回退展示 vulnclass。severity 取值：
    -- critical 严重 / high 高 / medium 中 / low 低（不加 CHECK，与 status 一致由 server 白名单校验）。
    name        TEXT NOT NULL DEFAULT '',
    severity    TEXT NOT NULL DEFAULT '',
    summary     TEXT NOT NULL DEFAULT '',
    evidence    TEXT NOT NULL DEFAULT '',
    worker      TEXT NOT NULL DEFAULT '',
    asset_ids   JSONB NOT NULL DEFAULT '[]',
    -- 处置状态：pending 待处理 / in_progress 处理中 / confirmed 已确认 / resolved 已处理 / fixed 已修复 /
    -- false_positive 误报 / ignored 忽略 / duplicate 重复 / risk_accepted 风险接受。
    -- 取值不加 CHECK：旧库靠下面的 ALTER 补列,CHECK 无法回填,统一由 server 侧白名单校验。
    status      TEXT NOT NULL DEFAULT 'pending',
    -- 漏洞详细报告(Markdown)；默认空,仅详情页读取/展示,不进列表接口以免 payload 膨胀。
    report      TEXT NOT NULL DEFAULT '',
    created_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);
```

### 후속 변경·제약

- `ALTER TABLE findings ADD COLUMN IF NOT EXISTS status TEXT NOT NULL DEFAULT 'pending';`
- `ALTER TABLE findings ADD COLUMN IF NOT EXISTS name TEXT NOT NULL DEFAULT '';`
- `ALTER TABLE findings ADD COLUMN IF NOT EXISTS report TEXT NOT NULL DEFAULT '';`
- `ALTER TABLE findings ADD COLUMN IF NOT EXISTS evidence_version BIGINT NOT NULL DEFAULT 0;`
- `ALTER TABLE findings ADD COLUMN IF NOT EXISTS report_evidence_version BIGINT NOT NULL DEFAULT 0;`

### 인덱스

- `CREATE INDEX IF NOT EXISTS idx_findings_task ON findings(task_id, created_at DESC);`
- `CREATE INDEX IF NOT EXISTS idx_findings_time ON findings(created_at DESC);`
- `CREATE INDEX IF NOT EXISTS idx_findings_status ON findings(status, created_at DESC);`
- `CREATE INDEX IF NOT EXISTS idx_findings_asset_ids ON findings USING GIN(asset_ids jsonb_path_ops);`

### 의미·소비·운영 경계

[`findings` 필드 의미, reader/writer, index 비용과 retention 판정](semantic-catalog.md#semantic-findings)에서 물리 DDL과 별도로 C/A/U를 확인한다. 운영 DB 적용·row 규모·query plan·role은 관찰하지 않았다.


<a id="table-finding-retests"></a>
## `finding_retests`

`finding_retests` 도메인의 영속 상태를 저장한다. 정확한 의미는 필드 정의와 사용 모듈을 함께 본다.

정의 원본은 `db/schema.sql`다. [등록 근거](evidence:schema-core)

```sql
CREATE TABLE IF NOT EXISTS finding_retests (
    id BIGSERIAL PRIMARY KEY,
    finding_id BIGINT NOT NULL REFERENCES findings(id) ON DELETE CASCADE,
    conversation_id BIGINT UNIQUE REFERENCES conversations(id) ON DELETE SET NULL,
    status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','running','completed','failed','stopped')),
    verdict TEXT NOT NULL DEFAULT '' CHECK (verdict IN ('','reproduced','fixed','inconclusive')),
    notes TEXT NOT NULL DEFAULT '',
    snapshot JSONB NOT NULL,
    summary TEXT NOT NULL DEFAULT '',
    evidence TEXT NOT NULL DEFAULT '',
    error TEXT NOT NULL DEFAULT '',
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    started_at TIMESTAMPTZ,
    finished_at TIMESTAMPTZ
);
```

### 인덱스

- `CREATE INDEX IF NOT EXISTS idx_finding_retests_history ON finding_retests(finding_id, id DESC);`
- `CREATE UNIQUE INDEX IF NOT EXISTS idx_finding_retests_active ON finding_retests(finding_id) WHERE status IN ('pending','running');`

### 의미·소비·운영 경계

[`finding_retests` 필드 의미, reader/writer, index 비용과 retention 판정](semantic-catalog.md#semantic-finding-retests)에서 물리 DDL과 별도로 C/A/U를 확인한다. 운영 DB 적용·row 규모·query plan·role은 관찰하지 않았다.


<a id="table-traffic-evidence-snapshots"></a>
## `traffic_evidence_snapshots`

캡처 exchange의 request/response metadata와 body hash를 finding용 snapshot으로 고정한다.

정의 원본은 `db/schema.sql`다. [등록 근거](evidence:schema-core)

```sql
CREATE TABLE IF NOT EXISTS traffic_evidence_snapshots (
    id TEXT PRIMARY KEY,
    source_traffic_id TEXT NOT NULL,
    captured_at BIGINT NOT NULL,
    url TEXT NOT NULL,
    method TEXT NOT NULL,
    status INTEGER NOT NULL,
    content_type TEXT NOT NULL DEFAULT '',
    req_head TEXT NOT NULL,
    resp_head TEXT NOT NULL,
    req_hash TEXT NOT NULL,
    resp_hash TEXT NOT NULL,
    req_len BIGINT NOT NULL,
    resp_len BIGINT NOT NULL,
    unreferenced_at TIMESTAMPTZ,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
```

### 의미·소비·운영 경계

[`traffic_evidence_snapshots` 필드 의미, reader/writer, index 비용과 retention 판정](semantic-catalog.md#semantic-traffic-evidence-snapshots)에서 물리 DDL과 별도로 C/A/U를 확인한다. 운영 DB 적용·row 규모·query plan·role은 관찰하지 않았다.


<a id="table-finding-traffic-bindings"></a>
## `finding_traffic_bindings`

finding과 snapshot의 순서·role·설명을 연결한다.

정의 원본은 `db/schema.sql`다. [등록 근거](evidence:schema-core)

```sql
CREATE TABLE IF NOT EXISTS finding_traffic_bindings (
    id BIGSERIAL PRIMARY KEY,
    finding_id BIGINT NOT NULL REFERENCES findings(id) ON DELETE CASCADE,
    snapshot_id TEXT NOT NULL REFERENCES traffic_evidence_snapshots(id),
    role TEXT NOT NULL DEFAULT 'supporting',
    note TEXT NOT NULL DEFAULT '',
    position INTEGER NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    UNIQUE(finding_id, snapshot_id)
);
```

### 인덱스

- `CREATE INDEX IF NOT EXISTS idx_finding_traffic_order ON finding_traffic_bindings(finding_id, position, id);`
- `CREATE INDEX IF NOT EXISTS idx_finding_traffic_snapshot ON finding_traffic_bindings(snapshot_id);`

### 의미·소비·운영 경계

[`finding_traffic_bindings` 필드 의미, reader/writer, index 비용과 retention 판정](semantic-catalog.md#semantic-finding-traffic-bindings)에서 물리 DDL과 별도로 C/A/U를 확인한다. 운영 DB 적용·row 규모·query plan·role은 관찰하지 않았다.


<a id="table-server-logs"></a>
## `server_logs`

`server_logs` 도메인의 영속 상태를 저장한다. 정확한 의미는 필드 정의와 사용 모듈을 함께 본다.

정의 원본은 `db/schema.sql`다. [등록 근거](evidence:schema-core)

```sql
CREATE TABLE IF NOT EXISTS server_logs (
    id         BIGSERIAL PRIMARY KEY,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    level      TEXT NOT NULL DEFAULT 'info',
    tag        TEXT NOT NULL DEFAULT '',
    text       TEXT NOT NULL DEFAULT ''
);
```

### 인덱스

- `CREATE INDEX IF NOT EXISTS idx_server_logs_id ON server_logs(id DESC);`

### 의미·소비·운영 경계

[`server_logs` 필드 의미, reader/writer, index 비용과 retention 판정](semantic-catalog.md#semantic-server-logs)에서 물리 DDL과 별도로 C/A/U를 확인한다. 운영 DB 적용·row 규모·query plan·role은 관찰하지 않았다.


<a id="table-side-question-sessions"></a>
## `side_question_sessions`

`side_question_sessions` 도메인의 영속 상태를 저장한다. 정확한 의미는 필드 정의와 사용 모듈을 함께 본다.

정의 원본은 `db/schema.sql`다. [등록 근거](evidence:schema-core)

```sql
CREATE TABLE IF NOT EXISTS side_question_sessions (
    session_key TEXT PRIMARY KEY,
    conversation_id BIGINT REFERENCES conversations(id) ON DELETE CASCADE,
    task_id BIGINT REFERENCES tasks(id) ON DELETE CASCADE,
    exploration_id BIGINT REFERENCES explorations(id) ON DELETE CASCADE,
    intent_id BIGINT REFERENCES exploration_nodes(id) ON DELETE CASCADE,
    run_id BIGINT NOT NULL,
    version BIGINT NOT NULL,
    snapshot JSONB NOT NULL,
    generation BIGINT NOT NULL DEFAULT 0,
    CHECK ((conversation_id IS NOT NULL AND task_id IS NULL AND exploration_id IS NULL AND intent_id IS NULL)
        OR (conversation_id IS NULL AND task_id IS NOT NULL AND exploration_id IS NOT NULL))
);
```

### 후속 변경·제약

- `ALTER TABLE side_question_sessions ADD COLUMN IF NOT EXISTS memory JSONB NOT NULL DEFAULT '{}';`

### 인덱스

- `CREATE INDEX IF NOT EXISTS idx_side_sessions_conv ON side_question_sessions(conversation_id);`
- `CREATE INDEX IF NOT EXISTS idx_side_sessions_task ON side_question_sessions(task_id);`
- `CREATE INDEX IF NOT EXISTS idx_side_sessions_exp ON side_question_sessions(exploration_id);`
- `CREATE INDEX IF NOT EXISTS idx_side_sessions_intent ON side_question_sessions(intent_id);`

### 의미·소비·운영 경계

[`side_question_sessions` 필드 의미, reader/writer, index 비용과 retention 판정](semantic-catalog.md#semantic-side-question-sessions)에서 물리 DDL과 별도로 C/A/U를 확인한다. 운영 DB 적용·row 규모·query plan·role은 관찰하지 않았다.


<a id="table-side-question-requests"></a>
## `side_question_requests`

`side_question_requests` 도메인의 영속 상태를 저장한다. 정확한 의미는 필드 정의와 사용 모듈을 함께 본다.

정의 원본은 `db/schema.sql`다. [등록 근거](evidence:schema-core)

```sql
CREATE TABLE IF NOT EXISTS side_question_requests (
    id TEXT PRIMARY KEY,
    ordinal BIGSERIAL UNIQUE,
    session_key TEXT NOT NULL REFERENCES side_question_sessions(session_key) ON DELETE CASCADE,
    generation BIGINT NOT NULL,
    client_id TEXT NOT NULL,
    question TEXT NOT NULL,
    answer TEXT NOT NULL DEFAULT '',
    status TEXT NOT NULL CHECK(status IN ('running','completed','failed','cancelled','interrupted')),
    error TEXT NOT NULL DEFAULT '',
    model JSONB NOT NULL,
    snapshot_at TIMESTAMPTZ NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    sequence BIGINT NOT NULL DEFAULT 0,
    usage JSONB NOT NULL DEFAULT '{}',
    UNIQUE(session_key,generation,client_id)
);
```

### 후속 변경·제약

- `ALTER TABLE side_question_requests ADD COLUMN IF NOT EXISTS context_info JSONB NOT NULL DEFAULT '{}';`

### 인덱스

- `CREATE UNIQUE INDEX IF NOT EXISTS idx_side_request_running ON side_question_requests(session_key) WHERE status='running';`
- `CREATE INDEX IF NOT EXISTS idx_side_requests_history ON side_question_requests(session_key,ordinal DESC);`

### 의미·소비·운영 경계

[`side_question_requests` 필드 의미, reader/writer, index 비용과 retention 판정](semantic-catalog.md#semantic-side-question-requests)에서 물리 DDL과 별도로 C/A/U를 확인한다. 운영 DB 적용·row 규모·query plan·role은 관찰하지 않았다.


<a id="table-asset-intercept-rules"></a>
## `asset_intercept_rules`

`asset_intercept_rules` 도메인의 영속 상태를 저장한다. 정확한 의미는 필드 정의와 사용 모듈을 함께 본다.

정의 원본은 `db/schema.sql`다. [등록 근거](evidence:schema-core)

```sql
CREATE TABLE IF NOT EXISTS asset_intercept_rules (
    id          BIGSERIAL PRIMARY KEY,
    enabled     BOOLEAN NOT NULL DEFAULT true,
    kind        TEXT NOT NULL CHECK (kind IN (
                    'exact_domain', 'exact_ip', 'exact_url',
                    'fuzzy_domain', 'fuzzy_ip', 'fuzzy_url',
                    'cidr')),
    pattern     TEXT NOT NULL,
    note        TEXT NOT NULL DEFAULT '',
    builtin     BOOLEAN NOT NULL DEFAULT false,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
```

### 인덱스

- `CREATE INDEX IF NOT EXISTS idx_asset_intercept_enabled ON asset_intercept_rules(enabled);`

### 의미·소비·운영 경계

[`asset_intercept_rules` 필드 의미, reader/writer, index 비용과 retention 판정](semantic-catalog.md#semantic-asset-intercept-rules)에서 물리 DDL과 별도로 C/A/U를 확인한다. 운영 DB 적용·row 규모·query plan·role은 관찰하지 않았다.


<a id="table-task-intercept-rules"></a>
## `task_intercept_rules`

`task_intercept_rules` 도메인의 영속 상태를 저장한다. 정확한 의미는 필드 정의와 사용 모듈을 함께 본다.

정의 원본은 `db/schema.sql`다. [등록 근거](evidence:schema-core)

```sql
CREATE TABLE IF NOT EXISTS task_intercept_rules (
    id          BIGSERIAL PRIMARY KEY,
    task_id     BIGINT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
    enabled     BOOLEAN NOT NULL DEFAULT true,
    action      TEXT NOT NULL DEFAULT 'block' CHECK (action IN ('block','allow')),
    kind        TEXT NOT NULL CHECK (kind IN (
                    'exact_domain', 'exact_ip', 'exact_url',
                    'fuzzy_domain', 'fuzzy_ip', 'fuzzy_url',
                    'cidr')),
    pattern     TEXT NOT NULL,
    note        TEXT NOT NULL DEFAULT '',
    created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
```

### 후속 변경·제약

- `ALTER TABLE task_intercept_rules ADD COLUMN IF NOT EXISTS action TEXT NOT NULL DEFAULT 'block';`

Fresh `CREATE`는 `action IN ('block','allow')` CHECK를 만들지만 legacy `ADD COLUMN`은 CHECK를 추가하지 않는다. upgraded DB의 action 무결성은 backend 쓰기 검증에 의존한다. [migration 경계](functions-triggers.md#schema-migrations)

### 인덱스

- `CREATE INDEX IF NOT EXISTS idx_task_intercept_task ON task_intercept_rules(task_id);`

### 의미·소비·운영 경계

[`task_intercept_rules` 필드 의미, reader/writer, index 비용과 retention 판정](semantic-catalog.md#semantic-task-intercept-rules)에서 물리 DDL과 별도로 C/A/U를 확인한다. 운영 DB 적용·row 규모·query plan·role은 관찰하지 않았다.


<a id="table-notification-channels"></a>
## `notification_channels`

`notification_channels` 도메인의 영속 상태를 저장한다. 정확한 의미는 필드 정의와 사용 모듈을 함께 본다.

정의 원본은 `db/schema.sql`다. [등록 근거](evidence:schema-core)

```sql
CREATE TABLE IF NOT EXISTS notification_channels (
    id           BIGSERIAL PRIMARY KEY,
    name         TEXT NOT NULL,
    -- dingtalk 钉钉 / feishu 飞书 / wecom 企业微信 / webhook 通用 / telegram / email
    kind         TEXT NOT NULL,
    enabled      BOOLEAN NOT NULL DEFAULT true,
    -- 凭据(明文存储，UI 掩码回显；见 server 侧 maskChannelSecrets)。六种渠道字段差异极大，
    -- 统一 JSONB + Go 侧按 kind 严格校验，避免为每渠道加一堆 NULL 列：
    --   dingtalk {webhook,secret}
    --   feishu   {webhook,secret}
    --   wecom    {webhook}
    --   webhook  {url,method,content_type,headers{},body_template}
    --   telegram {bot_token,chat_id,base_url}
    --   email    {host,port,username,password,from,to[],tls}
    config       JSONB NOT NULL DEFAULT '{}',
    -- 推送时机：realtime 命中即推 / digest 进批次按全局周期汇总成一条。
    mode         TEXT NOT NULL DEFAULT 'realtime',
    -- 过滤条件，字段全部可选(缺省=不过滤)：
    --   min_severity       ''|low|medium|high|critical
    --   task_ids/asset_ids 空数组=不限；非空则须交集非空
    --   vulnclass_include/exclude 关键词数组(大小写不敏感子串)；include 空=全收
    --   on_status_change   bool，仅 realtime 模式有意义
    filter       JSONB NOT NULL DEFAULT '{}',
    -- 每分钟投递上限；0=不限流。默认 20 对齐钉钉/企微官方硬限。
    -- 超限不丢消息，只把投递推迟到下一个 tick。
    rate_per_min INTEGER NOT NULL DEFAULT 20,
    created_at   TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at   TIMESTAMPTZ NOT NULL DEFAULT now()
);
```

### 의미·소비·운영 경계

[`notification_channels` 필드 의미, reader/writer, index 비용과 retention 판정](semantic-catalog.md#semantic-notification-channels)에서 물리 DDL과 별도로 C/A/U를 확인한다. 운영 DB 적용·row 규모·query plan·role은 관찰하지 않았다.


<a id="table-notification-events"></a>
## `notification_events`

`notification_events` 도메인의 영속 상태를 저장한다. 정확한 의미는 필드 정의와 사용 모듈을 함께 본다.

정의 원본은 `db/schema.sql`다. [등록 근거](evidence:schema-core)

```sql
CREATE TABLE IF NOT EXISTS notification_events (
    id         BIGSERIAL PRIMARY KEY,
    -- finding_created | finding_status_changed
    kind       TEXT NOT NULL,
    finding_id BIGINT NOT NULL,
    snapshot   JSONB NOT NULL,
    -- fan-out 幂等标记：dispatcher 按此列取待分派事件，处理完置 true。
    -- 用列而非删行，以便投递历史能回溯到事件。
    fanned_out BOOLEAN NOT NULL DEFAULT false,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
```

### 인덱스

- `CREATE INDEX IF NOT EXISTS idx_notification_events_pending ON notification_events(id) WHERE NOT fanned_out;`

### 의미·소비·운영 경계

[`notification_events` 필드 의미, reader/writer, index 비용과 retention 판정](semantic-catalog.md#semantic-notification-events)에서 물리 DDL과 별도로 C/A/U를 확인한다. 운영 DB 적용·row 규모·query plan·role은 관찰하지 않았다.


<a id="table-notification-deliveries"></a>
## `notification_deliveries`

channel별 전달 시도, lease, retry, 오류를 저장한다.

정의 원본은 `db/schema.sql`다. [등록 근거](evidence:schema-core)

```sql
CREATE TABLE IF NOT EXISTS notification_deliveries (
    id          BIGSERIAL PRIMARY KEY,
    event_id    BIGINT NOT NULL REFERENCES notification_events(id) ON DELETE CASCADE,
    channel_id  BIGINT NOT NULL REFERENCES notification_channels(id) ON DELETE CASCADE,
    -- pending 待发 / sent 已发 / failed 重试耗尽(可手动重发) / skipped 渠道停用或批次取消
    -- pending 待发 / sending 已被某 dispatcher 领取(租约未到期) / sent 已发 /
    -- failed 重试耗尽或永久失败(可手动重发) / skipped 渠道停用。取值不加 CHECK，
    -- 与 findings.status 同理，由 server 侧白名单校验。
    state       TEXT NOT NULL DEFAULT 'pending',
    attempts    INTEGER NOT NULL DEFAULT 0,
    -- 兼作「下次可领取时间」与「租约到期时间」：领取时把它推到未来即构成租约，
    -- 于是「租约未到期」与「未到重试时间」共用同一个条件表达，不需要额外的
    -- lease_until 列。进程崩溃留下的 sending 行会因租约到期被下一轮重新领取。
    next_attempt_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    last_error  TEXT NOT NULL DEFAULT '',
    -- digest 模式同批次共享；realtime 恒为 NULL。整批渲染成一条消息后一起置 sent。
    batch_id    BIGINT,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT now(),
    sent_at     TIMESTAMPTZ
);
```

### 인덱스

- `CREATE INDEX IF NOT EXISTS idx_notification_deliveries_due ON notification_deliveries(next_attempt_at) WHERE state='pending';`
- `CREATE INDEX IF NOT EXISTS idx_notification_deliveries_history ON notification_deliveries(id DESC);`
- `CREATE INDEX IF NOT EXISTS idx_notification_deliveries_batch ON notification_deliveries(batch_id) WHERE batch_id IS NOT NULL;`
- `CREATE INDEX IF NOT EXISTS idx_notification_deliveries_channel ON notification_deliveries(channel_id, id DESC);`

### 의미·소비·운영 경계

[`notification_deliveries` 필드 의미, reader/writer, index 비용과 retention 판정](semantic-catalog.md#semantic-notification-deliveries)에서 물리 DDL과 별도로 C/A/U를 확인한다. 운영 DB 적용·row 규모·query plan·role은 관찰하지 않았다.
