<a id="db-task"></a>
# 태스크·자산·탐색 PostgreSQL 객체

task, asset, scope, exploration graph와 activity의 물리 객체를 전수 참조한다.

> **기준** · 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 적용 상태는 관찰하지 않았다.

## 객체 색인

| 객체 | 저장소 | 목적 |
|---|---|---|
| [`companies`](#table-companies) | PostgreSQL | `companies` 도메인의 영속 상태를 저장한다. 정확한 의미는 필드 정의와 사용 모듈을 함께 본다. |
| [`assets`](#table-assets) | PostgreSQL | 전역 자산 graph의 정규화된 자산과 관찰 속성·task 연계를 저장한다. |
| [`company_scope`](#table-company-scope) | PostgreSQL | 회사별 domain/IP/CIDR/ICP/keyword scope를 저장한다. |
| [`explorations`](#table-explorations) | PostgreSQL | 태스크별 탐색 graph root와 round를 소유한다. |
| [`exploration_nodes`](#table-exploration-nodes) | PostgreSQL | goal/intent/fact/finding/hint/digest 노드와 상태·우선순위·payload를 저장한다. |
| [`exploration_edges`](#table-exploration-edges) | PostgreSQL | 노드 사이 rel을 저장한다. |
| [`exploration_anchors`](#table-exploration-anchors) | PostgreSQL | 탐색 노드와 asset의 anchor를 저장한다. |
| [`task_constraints`](#table-task-constraints) | PostgreSQL | `task_constraints` 도메인의 영속 상태를 저장한다. 정확한 의미는 필드 정의와 사용 모듈을 함께 본다. |
| [`activity`](#table-activity) | PostgreSQL | `activity` 도메인의 영속 상태를 저장한다. 정확한 의미는 필드 정의와 사용 모듈을 함께 본다. |
| [`main_sessions`](#table-main-sessions) | PostgreSQL | `main_sessions` 도메인의 영속 상태를 저장한다. 정확한 의미는 필드 정의와 사용 모듈을 함께 본다. |
| [`task_categories`](#table-task-categories) | PostgreSQL | `task_categories` 도메인의 영속 상태를 저장한다. 정확한 의미는 필드 정의와 사용 모듈을 함께 본다. |
| [`tasks`](#table-tasks) | PostgreSQL | 태스크의 사용자 입력, 실행 상태, timeout, queue, LLM chain, 분류와 archive 표지를 소유한다. |
| [`task_templates`](#table-task-templates) | PostgreSQL | `task_templates` 도메인의 영속 상태를 저장한다. 정확한 의미는 필드 정의와 사용 모듈을 함께 본다. |
| [`task_relations`](#table-task-relations) | PostgreSQL | `task_relations` 도메인의 영속 상태를 저장한다. 정확한 의미는 필드 정의와 사용 모듈을 함께 본다. |
| [`task_asset_links`](#table-task-asset-links) | PostgreSQL | `task_asset_links` 도메인의 영속 상태를 저장한다. 정확한 의미는 필드 정의와 사용 모듈을 함께 본다. |
| [`task_llm_profiles`](#table-task-llm-profiles) | PostgreSQL | `task_llm_profiles` 도메인의 영속 상태를 저장한다. 정확한 의미는 필드 정의와 사용 모듈을 함께 본다. |
| [`task_scope`](#table-task-scope) | PostgreSQL | task에서 사용하는 root_domain/subdomain/IP/CIDR/company scope를 저장한다. |

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

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

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

```sql
CREATE TABLE IF NOT EXISTS companies (
    id         BIGSERIAL PRIMARY KEY,
    name       TEXT NOT NULL,
    nkey       TEXT NOT NULL UNIQUE,
    logo       TEXT,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
```

### 인덱스

- `CREATE INDEX IF NOT EXISTS idx_companies_nkey ON companies(nkey);`

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

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


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

전역 자산 graph의 정규화된 자산과 관찰 속성·task 연계를 저장한다.

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

```sql
CREATE TABLE IF NOT EXISTS assets (
    id              BIGSERIAL PRIMARY KEY,
    type            TEXT NOT NULL CHECK (type IN (
                        'root_domain','ip','subdomain','app','service','endpoint'
                    )),
    company_id      BIGINT REFERENCES companies(id) ON DELETE SET NULL,
    -- explicit: caller/user selected the company; scope: derived from company_scope.
    -- Existing installations are conservatively migrated as explicit so a scope
    -- rebuild can never erase a historical manual association.
    company_source  TEXT NOT NULL DEFAULT 'explicit'
                    CHECK (company_source IN ('explicit','scope')),
    task_ids        BIGINT[] NOT NULL DEFAULT '{}',
    domain          TEXT,
    root_domain     TEXT,
    ip              TEXT,
    c_segment       CIDR,
    port            INTEGER CHECK (port BETWEEN 1 AND 65535),
    icp             TEXT,
    bound_domains   TEXT[]  NOT NULL DEFAULT '{}',
    open_ports      JSONB[] NOT NULL DEFAULT '{}',
    record_type     TEXT,
    record_value    TEXT[],
    bundle_id       TEXT,
    app_name        TEXT,
    category        TEXT,
    app_description TEXT,
    app_icp         TEXT,
    url             TEXT,
    service_type    TEXT CHECK (service_type IN ('http','other')),
    service_name    TEXT,
    favicon_mmh3    TEXT,
    status_code     INTEGER,
    content_length  BIGINT,
    page_title      TEXT,
    technologies    TEXT[]  NOT NULL DEFAULT '{}',
    auth            JSONB[] NOT NULL DEFAULT '{}',
    method          TEXT,
    params          JSONB[] NOT NULL DEFAULT '{}',
    extra           JSONB   NOT NULL DEFAULT '{}',
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
    last_seen       TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);
```

### 후속 변경·제약

- `ALTER TABLE assets ADD COLUMN IF NOT EXISTS company_source TEXT;`
- `ALTER TABLE assets ALTER COLUMN company_source SET DEFAULT 'explicit';`
- `ALTER TABLE assets ALTER COLUMN company_source SET NOT NULL;`
- `ALTER TABLE assets DROP CONSTRAINT IF EXISTS assets_company_source_check;`
- `ALTER TABLE assets ADD CONSTRAINT assets_company_source_check CHECK (company_source IN ('explicit','scope'));`

Startup은 null `company_source`를 `explicit`로 backfill한 뒤 default/not-null/CHECK를 적용한다. CHECK는 매 startup drop/recreate되어 기존 row를 재검증하며, `explicit/scope` 외 legacy 값은 startup transaction을 실패시킨다. [migration 경계](functions-triggers.md#schema-migrations)

### 인덱스

- `CREATE UNIQUE INDEX IF NOT EXISTS uq_av2_root_domain ON assets(domain) WHERE type = 'root_domain';`
- `CREATE UNIQUE INDEX IF NOT EXISTS uq_av2_ip ON assets(ip) WHERE type = 'ip';`
- `CREATE UNIQUE INDEX IF NOT EXISTS uq_av2_subdomain ON assets(domain, COALESCE(record_type,'')) WHERE type = 'subdomain';`
- `CREATE UNIQUE INDEX IF NOT EXISTS uq_av2_app_bundle ON assets(bundle_id) WHERE type = 'app' AND bundle_id IS NOT NULL;`
- `CREATE UNIQUE INDEX IF NOT EXISTS uq_av2_app_name ON assets(app_name) WHERE type = 'app' AND bundle_id IS NULL;`
- `CREATE UNIQUE INDEX IF NOT EXISTS uq_av2_service_http ON assets(url) WHERE type = 'service' AND service_type = 'http';`
- `CREATE UNIQUE INDEX IF NOT EXISTS uq_av2_service_other ON assets(COALESCE(domain,''), COALESCE(ip,''), port, service_name) WHERE type = 'service' AND service_type = 'other';`
- `CREATE UNIQUE INDEX IF NOT EXISTS uq_av2_endpoint ON assets(url, method) WHERE type = 'endpoint';`
- `CREATE INDEX IF NOT EXISTS idx_av2_company ON assets(company_id) WHERE company_id IS NOT NULL;`
- `CREATE INDEX IF NOT EXISTS idx_av2_company_type ON assets(company_id, type) WHERE company_id IS NOT NULL;`
- `CREATE INDEX IF NOT EXISTS idx_av2_task_ids ON assets USING GIN(task_ids);`
- `CREATE INDEX IF NOT EXISTS idx_av2_domain ON assets(domain) WHERE domain IS NOT NULL;`
- `CREATE INDEX IF NOT EXISTS idx_av2_root_domain ON assets(root_domain) WHERE root_domain IS NOT NULL;`
- `CREATE INDEX IF NOT EXISTS idx_av2_ip ON assets(ip) WHERE ip IS NOT NULL;`
- `CREATE INDEX IF NOT EXISTS idx_av2_c_segment ON assets USING GIST(c_segment inet_ops) WHERE c_segment IS NOT NULL;`
- `CREATE INDEX IF NOT EXISTS idx_av2_technologies ON assets USING GIN(technologies) WHERE type = 'service';`
- `CREATE INDEX IF NOT EXISTS idx_av2_bound_domains ON assets USING GIN(bound_domains) WHERE type = 'ip';`
- `CREATE INDEX IF NOT EXISTS idx_av2_open_ports ON assets USING GIN(open_ports) WHERE type = 'ip';`
- `CREATE INDEX IF NOT EXISTS idx_av2_last_seen ON assets(last_seen DESC);`
- `CREATE INDEX IF NOT EXISTS idx_av2_type_seen ON assets(type, last_seen DESC);`

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

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


<a id="table-company-scope"></a>
## `company_scope`

회사별 domain/IP/CIDR/ICP/keyword scope를 저장한다.

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

```sql
CREATE TABLE IF NOT EXISTS company_scope (
    id         BIGSERIAL PRIMARY KEY,
    company_id BIGINT NOT NULL REFERENCES companies(id) ON DELETE CASCADE,
    kind       TEXT NOT NULL CHECK (kind IN ('domain','ip','cidr','icp','keyword')),
    domain     TEXT,
    net        CIDR,
    value      TEXT,
    raw        TEXT NOT NULL,
    reason     TEXT,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    CONSTRAINT uq_sv2_domain UNIQUE (company_id, domain),
    CONSTRAINT uq_sv2_net    UNIQUE (company_id, net),
    CONSTRAINT ck_company_scope_payload CHECK (
        (kind = 'domain' AND domain IS NOT NULL AND net IS NULL AND value IS NULL)
        OR (kind IN ('ip','cidr') AND domain IS NULL AND net IS NOT NULL AND value IS NULL)
        OR (kind IN ('icp','keyword') AND domain IS NULL AND net IS NULL AND value IS NOT NULL)
    )
);
```

### 후속 변경·제약

- `ALTER TABLE company_scope ADD COLUMN IF NOT EXISTS value TEXT;`
- `ALTER TABLE company_scope DROP CONSTRAINT IF EXISTS company_scope_kind_check;`
- `ALTER TABLE company_scope ADD CONSTRAINT company_scope_kind_check CHECK (kind IN ('domain','ip','cidr','icp','keyword'));`
- `ALTER TABLE company_scope DROP CONSTRAINT IF EXISTS ck_company_scope_payload;`
- `ALTER TABLE company_scope ADD CONSTRAINT ck_company_scope_payload CHECK ( (kind = 'domain' AND domain IS NOT NULL AND net IS NULL AND value IS NULL) OR (kind IN ('ip','cidr') AND domain IS NULL AND net IS NOT NULL AND value IS NULL) OR (kind IN ('icp','keyword') AND domain IS NULL AND net IS NULL AND value IS NOT NULL) );`

Kind/payload CHECK는 매 startup drop/recreate되며 `NOT VALID`가 아니라 legacy row를 전수 재검증한다. invalid row는 schema transaction을 abort하고 ALTER lock은 concurrent writer를 block할 수 있다. [migration 경계](functions-triggers.md#schema-migrations)

### 인덱스

- `CREATE INDEX IF NOT EXISTS idx_sv2_domain ON company_scope(domain) WHERE kind = 'domain';`
- `CREATE INDEX IF NOT EXISTS idx_sv2_net ON company_scope USING GIST(net inet_ops) WHERE kind IN ('ip','cidr');`
- `CREATE UNIQUE INDEX IF NOT EXISTS uq_sv2_value ON company_scope(company_id, kind, value) WHERE kind IN ('icp','keyword');`
- `CREATE INDEX IF NOT EXISTS idx_sv2_icp ON company_scope(value) WHERE kind = 'icp';`
- `CREATE INDEX IF NOT EXISTS idx_sv2_company ON company_scope(company_id);`

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

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


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

태스크별 탐색 graph root와 round를 소유한다.

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

```sql
CREATE TABLE IF NOT EXISTS explorations (
    id          BIGSERIAL PRIMARY KEY,
    description TEXT,
    goal        TEXT NOT NULL,
    status      TEXT NOT NULL DEFAULT 'open'
                  CHECK (status IN ('open','achieved','failed')),
    created_at  TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);
```

### 후속 변경·제약

- `ALTER TABLE explorations ADD COLUMN IF NOT EXISTS round_no BIGINT NOT NULL DEFAULT 0;`

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

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


<a id="table-exploration-nodes"></a>
## `exploration_nodes`

goal/intent/fact/finding/hint/digest 노드와 상태·우선순위·payload를 저장한다.

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

```sql
CREATE TABLE IF NOT EXISTS exploration_nodes (
    id             BIGSERIAL PRIMARY KEY,
    exploration_id BIGINT NOT NULL REFERENCES explorations(id) ON DELETE CASCADE,
    kind           TEXT NOT NULL,
    payload        JSONB NOT NULL DEFAULT '{}',
    priority       INT  NOT NULL DEFAULT 0,
    state          TEXT NOT NULL DEFAULT 'open',
    origin         TEXT,
    owner          TEXT,
    blocked_reason TEXT,
    delete_reason  TEXT,
    created_at     TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at     TIMESTAMPTZ NOT NULL DEFAULT now(),
    completed_at   TIMESTAMPTZ,
    CONSTRAINT ck_node_kind CHECK (kind IN ('begin','goal','intent','fact','finding','hint','digest')),
    CONSTRAINT ck_node_state CHECK (
        (kind='begin'   AND state IN ('open')) OR
        (kind='intent'  AND state IN ('open','running','paused','done','blocked','exhausted','stopped','deleted')) OR
        (kind='goal'    AND state IN ('open','met','abandoned')) OR
        (kind='fact'    AND state IN ('confirmed','dismissed','origin')) OR
        (kind='finding' AND state IN ('confirmed','dismissed')) OR
        (kind='hint'    AND state IN ('active','consumed')) OR
        (kind='digest'  AND state IN ('active','superseded'))
    )
);
```

### 후속 변경·제약

- `ALTER TABLE exploration_nodes ADD COLUMN IF NOT EXISTS blocked_reason TEXT;`
- `ALTER TABLE exploration_nodes ADD COLUMN IF NOT EXISTS delete_reason TEXT;`
- `ALTER TABLE exploration_nodes ADD COLUMN IF NOT EXISTS content_version INT NOT NULL DEFAULT 0;`
- `ALTER TABLE exploration_nodes ADD COLUMN IF NOT EXISTS cold_since_round BIGINT;`

### 인덱스

- `CREATE INDEX IF NOT EXISTS idx_expnodes_part ON exploration_nodes(exploration_id, kind);`
- `CREATE INDEX IF NOT EXISTS idx_expnodes_frontier ON exploration_nodes(exploration_id, priority DESC) WHERE kind='intent' AND state='open';`

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

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


<a id="table-exploration-edges"></a>
## `exploration_edges`

노드 사이 rel을 저장한다.

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

```sql
CREATE TABLE IF NOT EXISTS exploration_edges (
    exploration_id BIGINT NOT NULL REFERENCES explorations(id) ON DELETE CASCADE,
    src_id         BIGINT NOT NULL REFERENCES exploration_nodes(id) ON DELETE CASCADE,
    dst_id         BIGINT NOT NULL REFERENCES exploration_nodes(id) ON DELETE CASCADE,
    rel            TEXT NOT NULL,
    created_at     TIMESTAMPTZ NOT NULL DEFAULT now(),
    PRIMARY KEY (exploration_id, src_id, rel, dst_id),
    CONSTRAINT ck_edge_noself CHECK (src_id <> dst_id),
    CONSTRAINT ck_edge_rel CHECK (rel IN ('spawns','derived_from','yields','proves','covers'))
);
```

### 인덱스

- `CREATE INDEX IF NOT EXISTS idx_expedges_src ON exploration_edges(src_id, rel);`
- `CREATE INDEX IF NOT EXISTS idx_expedges_dst ON exploration_edges(dst_id, rel);`

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

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


<a id="table-exploration-anchors"></a>
## `exploration_anchors`

탐색 노드와 asset의 anchor를 저장한다.

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

```sql
CREATE TABLE IF NOT EXISTS exploration_anchors (
    node_id   BIGINT NOT NULL REFERENCES exploration_nodes(id) ON DELETE CASCADE,
    asset_id  BIGINT NOT NULL REFERENCES assets(id) ON DELETE CASCADE,
    PRIMARY KEY (node_id, asset_id)
);
```

### 인덱스

- `CREATE INDEX IF NOT EXISTS idx_anchor_asset ON exploration_anchors(asset_id);`

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

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


<a id="table-task-constraints"></a>
## `task_constraints`

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

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

```sql
CREATE TABLE IF NOT EXISTS task_constraints (
    id             BIGSERIAL PRIMARY KEY,
    exploration_id BIGINT NOT NULL REFERENCES explorations(id) ON DELETE CASCADE,
    kind           TEXT NOT NULL CHECK (kind IN ('allow','deny')),
    text           TEXT NOT NULL,
    origin         TEXT,
    created_at     TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at     TIMESTAMPTZ NOT NULL DEFAULT now()
);
```

### 인덱스

- `CREATE INDEX IF NOT EXISTS idx_task_constraints_exp ON task_constraints(exploration_id);`

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

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


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

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

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

```sql
CREATE TABLE IF NOT EXISTS activity (
    id                 BIGSERIAL PRIMARY KEY,
    exploration_id     BIGINT NOT NULL REFERENCES explorations(id) ON DELETE CASCADE,
    node_id            BIGINT REFERENCES exploration_nodes(id) ON DELETE SET NULL,
    worker             TEXT,
    kind               TEXT,
    tool               TEXT,
    tool_use_id        TEXT,
    is_error           BOOLEAN NOT NULL DEFAULT false,
    summary            TEXT,
    detail             TEXT,
    metadata           JSONB NOT NULL DEFAULT '{}',
    input_tokens       INTEGER,
    output_tokens      INTEGER,
    cache_read_tokens  INTEGER,
    cache_write_tokens INTEGER,
    created_at         TIMESTAMPTZ NOT NULL DEFAULT now()
);
```

### 후속 변경·제약

- `ALTER TABLE activity ADD COLUMN IF NOT EXISTS metadata JSONB NOT NULL DEFAULT '{}';`
- `ALTER TABLE activity ADD COLUMN IF NOT EXISTS main_seg INTEGER;`

### 인덱스

- `CREATE INDEX IF NOT EXISTS idx_act_node ON activity(exploration_id, node_id, id);`
- `CREATE INDEX IF NOT EXISTS idx_act_since ON activity(exploration_id, id);`
- `CREATE INDEX IF NOT EXISTS idx_act_tool_call ON activity(exploration_id, tool_use_id, id) WHERE kind IN ('tool_use', 'tool_result');`
- `CREATE INDEX IF NOT EXISTS idx_act_worker ON activity(exploration_id, worker, id);`
- `CREATE INDEX IF NOT EXISTS idx_act_main_seg ON activity(exploration_id, main_seg, id) WHERE worker='mainagent';`
- `CREATE INDEX IF NOT EXISTS idx_act_result_usage ON activity(exploration_id) INCLUDE (input_tokens, output_tokens, cache_read_tokens, cache_write_tokens) WHERE kind='result';`
- `CREATE INDEX IF NOT EXISTS idx_act_latest ON activity(exploration_id, created_at DESC);`

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

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


<a id="table-main-sessions"></a>
## `main_sessions`

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

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

```sql
CREATE TABLE IF NOT EXISTS main_sessions (
    exploration_id BIGINT NOT NULL REFERENCES explorations(id) ON DELETE CASCADE,
    seq            INTEGER NOT NULL,
    created_at     TIMESTAMPTZ NOT NULL DEFAULT now(),
    PRIMARY KEY (exploration_id, seq)
);
```

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

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


<a id="table-task-categories"></a>
## `task_categories`

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

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

```sql
CREATE TABLE IF NOT EXISTS task_categories (
    id         BIGSERIAL PRIMARY KEY,
    name       TEXT NOT NULL,
    nkey       TEXT NOT NULL UNIQUE,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
```

### 인덱스

- `CREATE INDEX IF NOT EXISTS idx_task_categories_name ON task_categories(name, id);`

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

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


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

태스크의 사용자 입력, 실행 상태, timeout, queue, LLM chain, 분류와 archive 표지를 소유한다.

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

```sql
CREATE TABLE IF NOT EXISTS tasks (
    id             BIGSERIAL PRIMARY KEY,
    name           TEXT NOT NULL DEFAULT '',
    category_id    BIGINT REFERENCES task_categories(id) ON DELETE SET NULL,
    description    TEXT NOT NULL,
    goal           TEXT NOT NULL,
    exploration_id BIGINT NOT NULL UNIQUE
                     REFERENCES explorations(id) ON DELETE RESTRICT,
    status         TEXT NOT NULL DEFAULT 'created'
                     CHECK (status IN ('created','running','paused','done','failed','timeout')),
    paused         BOOLEAN NOT NULL DEFAULT false,
    queued         BOOLEAN NOT NULL DEFAULT false,
    queued_at      TIMESTAMPTZ,
    queue_mode     TEXT NOT NULL DEFAULT '',
    llm_profile_id BIGINT REFERENCES llm_profiles(id) ON DELETE SET NULL,
    active_llm_profile_id BIGINT REFERENCES llm_profiles(id) ON DELETE SET NULL,
    llm_chain_revision BIGINT NOT NULL DEFAULT 0,
    company_id     BIGINT REFERENCES companies(id) ON DELETE SET NULL,
    parent_ref     TEXT,
    timeout_seconds INTEGER NOT NULL DEFAULT 0,
    plan_heartbeat_seconds INTEGER NOT NULL DEFAULT 300,
    coverage_enabled BOOLEAN NOT NULL DEFAULT true,
    pinned_at      TIMESTAMPTZ,
    first_run_at   TIMESTAMPTZ,
    deadline_at    TIMESTAMPTZ,
    archived_at    TIMESTAMPTZ,
    deleted_at     TIMESTAMPTZ,
    completed_at   TIMESTAMPTZ,
    created_at     TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at     TIMESTAMPTZ NOT NULL DEFAULT now()
);
```

### 후속 변경·제약

- `ALTER TABLE tasks ADD COLUMN IF NOT EXISTS plan_heartbeat_seconds INTEGER NOT NULL DEFAULT 300;`
- `ALTER TABLE tasks ADD COLUMN IF NOT EXISTS queued BOOLEAN NOT NULL DEFAULT false;`
- `ALTER TABLE tasks ADD COLUMN IF NOT EXISTS coverage_enabled BOOLEAN NOT NULL DEFAULT true;`
- `ALTER TABLE tasks ADD COLUMN IF NOT EXISTS queued_at TIMESTAMPTZ;`
- `ALTER TABLE tasks ADD COLUMN IF NOT EXISTS queue_mode TEXT NOT NULL DEFAULT '';`
- `ALTER TABLE tasks ADD COLUMN IF NOT EXISTS name TEXT NOT NULL DEFAULT '';`
- `ALTER TABLE tasks ADD COLUMN IF NOT EXISTS category_id BIGINT REFERENCES task_categories(id) ON DELETE SET NULL;`
- `ALTER TABLE tasks ADD COLUMN IF NOT EXISTS pinned_at TIMESTAMPTZ;`
- `ALTER TABLE tasks ADD COLUMN IF NOT EXISTS active_llm_profile_id BIGINT REFERENCES llm_profiles(id) ON DELETE SET NULL;`
- `ALTER TABLE tasks ADD COLUMN IF NOT EXISTS llm_chain_revision BIGINT NOT NULL DEFAULT 0;`
- `ALTER TABLE tasks ADD COLUMN IF NOT EXISTS archived_at TIMESTAMPTZ;`

### 인덱스

- `CREATE INDEX IF NOT EXISTS idx_tasks_alive ON tasks(created_at DESC) WHERE deleted_at IS NULL;`
- `CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status) WHERE deleted_at IS NULL;`
- `CREATE INDEX IF NOT EXISTS idx_tasks_category ON tasks(category_id, created_at DESC) WHERE deleted_at IS NULL AND category_id IS NOT NULL;`
- `CREATE INDEX IF NOT EXISTS idx_tasks_pinned ON tasks(pinned_at DESC) WHERE deleted_at IS NULL AND pinned_at IS NOT NULL;`
- `CREATE INDEX IF NOT EXISTS idx_tasks_archived ON tasks(archived_at DESC) WHERE archived_at IS NOT NULL;`
- `CREATE INDEX IF NOT EXISTS idx_tasks_llm_profile ON tasks(llm_profile_id) WHERE llm_profile_id IS NOT NULL;`
- `CREATE INDEX IF NOT EXISTS idx_tasks_active_llm_profile ON tasks(active_llm_profile_id) WHERE active_llm_profile_id IS NOT NULL;`

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

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


<a id="table-task-templates"></a>
## `task_templates`

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

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

```sql
CREATE TABLE IF NOT EXISTS task_templates (
    id          BIGSERIAL PRIMARY KEY,
    name        TEXT NOT NULL,
    nkey        TEXT NOT NULL UNIQUE,
    description TEXT NOT NULL,
    goal        TEXT NOT NULL,
    -- 预设的任务分类；分类删除时置空（与 tasks.category_id 一致，不阻断）。
    category_id     BIGINT REFERENCES task_categories(id) ON DELETE SET NULL,
    -- 预设的任务级拦截/允许规则快照(AssetInterceptRuleInput 数组)；应用模板时灌进新任务。
    intercept_rules JSONB NOT NULL DEFAULT '[]',
    created_at  TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);
```

### 후속 변경·제약

- `ALTER TABLE task_templates ADD COLUMN IF NOT EXISTS category_id BIGINT REFERENCES task_categories(id) ON DELETE SET NULL;`
- `ALTER TABLE task_templates ADD COLUMN IF NOT EXISTS intercept_rules JSONB NOT NULL DEFAULT '[]';`

### 인덱스

- `CREATE INDEX IF NOT EXISTS idx_task_templates_updated ON task_templates(updated_at DESC, id DESC);`

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

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


<a id="table-task-relations"></a>
## `task_relations`

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

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

```sql
CREATE TABLE IF NOT EXISTS task_relations (
    task_id        BIGINT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
    source_task_id BIGINT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
    created_at     TIMESTAMPTZ NOT NULL DEFAULT now(),
    PRIMARY KEY (task_id, source_task_id),
    CONSTRAINT ck_task_relation_not_self CHECK (task_id <> source_task_id)
);
```

### 인덱스

- `CREATE INDEX IF NOT EXISTS idx_task_relations_source ON task_relations(source_task_id);`

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

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


<a id="table-task-asset-links"></a>
## `task_asset_links`

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

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

```sql
CREATE TABLE IF NOT EXISTS task_asset_links (
    task_id        BIGINT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
    asset_id       BIGINT NOT NULL REFERENCES assets(id) ON DELETE CASCADE,
    source         TEXT NOT NULL DEFAULT 'system',
    source_summary TEXT NOT NULL DEFAULT '',
    source_node_id BIGINT REFERENCES exploration_nodes(id) ON DELETE SET NULL,
    created_at     TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at     TIMESTAMPTZ NOT NULL DEFAULT now(),
    PRIMARY KEY (task_id, asset_id)
);
```

### 인덱스

- `CREATE INDEX IF NOT EXISTS idx_task_asset_links_asset ON task_asset_links(asset_id, task_id);`
- `CREATE INDEX IF NOT EXISTS idx_task_asset_links_node ON task_asset_links(source_node_id) WHERE source_node_id IS NOT NULL;`

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

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


<a id="table-task-llm-profiles"></a>
## `task_llm_profiles`

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

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

```sql
CREATE TABLE IF NOT EXISTS task_llm_profiles (
    task_id          BIGINT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
    profile_id       BIGINT NOT NULL REFERENCES llm_profiles(id) ON DELETE CASCADE,
    position         INTEGER NOT NULL CHECK (position >= 0),
    status           TEXT NOT NULL DEFAULT 'ready'
                       CHECK (status IN ('ready','quota_exhausted')),
    last_error       TEXT,
    exhausted_at     TIMESTAMPTZ,
    created_at       TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at       TIMESTAMPTZ NOT NULL DEFAULT now(),
    PRIMARY KEY (task_id, profile_id),
    UNIQUE (task_id, position)
);
```

### 인덱스

- `CREATE INDEX IF NOT EXISTS idx_task_llm_profiles_order ON task_llm_profiles(task_id, position);`
- `CREATE INDEX IF NOT EXISTS idx_task_llm_profiles_profile ON task_llm_profiles(profile_id, task_id);`

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

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


<a id="table-task-scope"></a>
## `task_scope`

task에서 사용하는 root_domain/subdomain/IP/CIDR/company scope를 저장한다.

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

```sql
CREATE TABLE IF NOT EXISTS task_scope (
    id          BIGSERIAL PRIMARY KEY,
    task_id     BIGINT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
    kind        TEXT NOT NULL CHECK (kind IN ('company','root_domain','subdomain','ip','cidr','icp','keyword')),
    company_id  BIGINT REFERENCES companies(id) ON DELETE CASCADE,  -- kind='company'
    domain      TEXT,          -- root_domain / subdomain
    net         CIDR,          -- ip / cidr
    value       TEXT,          -- icp / keyword
    source      TEXT NOT NULL DEFAULT 'auto' CHECK (source IN ('auto','agent','manual')),
    reason      TEXT,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);
```

### 후속 변경·제약

- `ALTER TABLE task_scope ADD COLUMN IF NOT EXISTS value TEXT;`
- `ALTER TABLE task_scope DROP CONSTRAINT IF EXISTS task_scope_kind_check;`
- `ALTER TABLE task_scope ADD CONSTRAINT task_scope_kind_check CHECK (kind IN ('company','root_domain','subdomain','ip','cidr','icp','keyword'));`

Startup은 old `uq_task_scope`를 drop한 뒤 `value`까지 포함한 `uq_task_scope_v2`를 만들고 kind CHECK를 drop/recreate한다. Legacy duplicate/invalid row가 있으면 index/CHECK 생성이 실패하고 schema transaction이 rollback된다. [migration 경계](functions-triggers.md#schema-migrations)

### 인덱스

- `CREATE UNIQUE INDEX IF NOT EXISTS uq_task_scope_v2 ON task_scope( task_id, kind, COALESCE(domain,''), COALESCE(net::text,''), COALESCE(company_id,0), COALESCE(value,''));`
- `CREATE INDEX IF NOT EXISTS idx_ts_domain ON task_scope(domain) WHERE kind IN ('root_domain','subdomain');`
- `CREATE INDEX IF NOT EXISTS idx_ts_net ON task_scope USING GIST(net inet_ops) WHERE kind IN ('ip','cidr');`
- `CREATE INDEX IF NOT EXISTS idx_ts_company ON task_scope(company_id) WHERE kind = 'company';`

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

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