# Modelo de Dados — Módulo AI

## Diagrama Entidade-Relacionamento

```
┌─────────────────────────────────────────────────────────────┐
│                     TABELAS EXISTENTES                       │
│                                                              │
│  binaries ──────┬── static_analysis ──┬── static_function   │
│  (workspace)    │                     ├── static_label       │
│                 │                     └── static_xref        │
│                 │                                            │
│                 └── jobs                                     │
└─────────────────────────────────────────────────────────────┘
                         │
                         │ FK: binary_id
                         ▼
┌─────────────────────────────────────────────────────────────┐
│                   NOVAS TABELAS (V008)                       │
│                                                              │
│  embedding_chunk ──── chat_session ──── chat_message         │
│       │                   │                                  │
│       │                   │ FK: binary_id → binaries         │
│       │                   │ FK: project_id → projects        │
│       │                   │                                  │
│       │ FK: binary_id     │                                  │
│       │     → binaries    │                                  │
│       │                   │                                  │
│       │ FK: source_id     │                                  │
│       │     → opcional    │                                  │
│       │     (static_func) │                                  │
└─────────────────────────────────────────────────────────────┘
```

## Migration V008 — DDL Completo

```sql
-- V008__ai_embedding_and_chat.sql

-- Garantir que a extensão pgvector está ativada
CREATE EXTENSION IF NOT EXISTS vector;

-- ============================================================
-- Tabela: embedding_chunk
-- Armazena chunks de texto embeddados para busca semântica
-- ============================================================
CREATE TABLE embedding_chunk (
    id UUID PRIMARY KEY,

    -- Referência ao binário (obrigatória)
    binary_id UUID NOT NULL REFERENCES binaries(id) ON DELETE CASCADE,

    -- Tipo de chunk: FUNCTION, METADATA
    chunk_type VARCHAR(30) NOT NULL,

    -- Tipo da fonte: STATIC_FUNCTION, IMPORT, STRING (extensível)
    source_type VARCHAR(30),

    -- FK opcional para a entidade fonte (ex: static_function.id)
    source_id UUID,

    -- Texto completo que foi embeddado
    content TEXT NOT NULL,

    -- Vetor de embedding (dimensão configurável, default 4096)
    -- qwen3-embedding:latest (8B) retorna 4096 dimensões
    embedding vector(4096),

    -- Metadados flexíveis em JSON:
    -- {
    --   "function_name": "sub_401000",
    --   "address": "0x401000",
    --   "callers": ["WinMain", "sub_402300"],
    --   "callees": ["CreateFileA", "WriteFile"],
    --   "label_count": 2
    -- }
    metadata JSONB,

    -- Contagem estimada de tokens (para controle de contexto)
    token_count INTEGER,

    -- Timestamp de criação
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Índice para busca por binário (filtro de escopo)
CREATE INDEX idx_embedding_chunk_binary ON embedding_chunk(binary_id);

-- Índice para lookup por fonte (ex: achar chunk de uma função específica)
CREATE INDEX idx_embedding_chunk_source ON embedding_chunk(source_type, source_id);

-- Índice de busca textual (fallback para queries específicas)
CREATE INDEX idx_embedding_chunk_content ON embedding_chunk
    USING gin (content gin_trgm_ops);

-- NOTA: Não há índice vetorial. HNSW e IVFFlat do pgvector limitados a 2000d.
-- Sequential scan é rápido para < 10K vetores de 4096d.

-- ============================================================
-- Tabela: chat_session
-- Representa uma sessão de conversa com o chat IA
-- ============================================================
CREATE TABLE chat_session (
    id UUID PRIMARY KEY,

    -- Binário em análise (escopo da conversa)
    binary_id UUID NOT NULL REFERENCES binaries(id) ON DELETE CASCADE,

    -- Projeto pai (para navegação/breadcrumb)
    project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,

    -- Título da sessão (ex: "Analisando funções de rede")
    title VARCHAR(255),

    -- Modelo de chat usado nesta sessão
    -- Ex: "gemma4:12b", "deepseek-chat", "gemma4:e4b"
    chat_model VARCHAR(100) NOT NULL,

    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_chat_session_binary ON chat_session(binary_id);

-- ============================================================
-- Tabela: chat_message
-- Mensagens individuais dentro de uma sessão
-- ============================================================
CREATE TABLE chat_message (
    id UUID PRIMARY KEY,

    -- Sessão pai
    session_id UUID NOT NULL REFERENCES chat_session(id) ON DELETE CASCADE,

    -- Papel do emissor: USER, ASSISTANT, SYSTEM
    role VARCHAR(20) NOT NULL,

    -- Conteúdo textual da mensagem
    content TEXT NOT NULL,

    -- Estimativa de tokens (para tracking de uso)
    token_count INTEGER,

    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Índice para buscar histórico em ordem cronológica
CREATE INDEX idx_chat_message_session ON chat_message(session_id, created_at);
```

## Entidades JPA

### `EmbeddingChunk`

```java
@Entity
@Table(name = "embedding_chunk")
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
public class EmbeddingChunk {

    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;

    @Column(name = "binary_id", nullable = false)
    private UUID binaryId;

    @Column(name = "chunk_type", nullable = false, length = 30)
    private String chunkType;
    // Valores: "FUNCTION", "METADATA"

    @Column(name = "source_type", length = 30)
    private String sourceType;
    // Valores: "STATIC_FUNCTION", null (para METADATA)

    @Column(name = "source_id")
    private UUID sourceId;
    // FK lógica para static_function.id (sem constraint de FK no banco
    // porque pode ser null ou apontar para tabelas diferentes no futuro)

    @Column(nullable = false, columnDefinition = "TEXT")
    private String content;

    @JdbcTypeCode(SqlTypes.VECTOR)
    @Column(name = "embedding", columnDefinition = "vector(1024)")
    private float[] embedding;

    @JdbcTypeCode(SqlTypes.JSON)
    @Column(name = "metadata", columnDefinition = "jsonb")
    private String metadata;
    // JSON string: {"function_name":"...", "address":"...", "callers":[...], ...}

    @Column(name = "token_count")
    private Integer tokenCount;

    @CreationTimestamp
    @Column(name = "created_at", nullable = false, updatable = false)
    private Instant createdAt;
}
```

**Notas sobre o mapeamento**:
- `embedding` usa `@JdbcTypeCode(SqlTypes.VECTOR)` — requer Hibernate 6.2+ (Spring Boot 4.0.6 inclui)
- `metadata` é `String` com `@JdbcTypeCode(SqlTypes.JSON)` — Hibernate converte de/para JSONB automaticamente
- `sourceId` é FK lógica (não constraint no banco) para permitir flexibilidade futura (apontar para outras tabelas além de `static_function`)

### `ChatSession`

```java
@Entity
@Table(name = "chat_session")
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
public class ChatSession {

    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;

    @Column(name = "binary_id", nullable = false)
    private UUID binaryId;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "binary_id", insertable = false, updatable = false)
    private Binary binary;

    @Column(name = "project_id", nullable = false)
    private UUID projectId;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "project_id", insertable = false, updatable = false)
    private Project project;

    @Column(length = 255)
    private String title;

    @Column(name = "chat_model", nullable = false, length = 100)
    private String chatModel;

    @OneToMany(mappedBy = "session", cascade = CascadeType.ALL, orphanRemoval = true)
    @Builder.Default
    private List<ChatMessage> messages = new ArrayList<>();

    @CreationTimestamp
    @Column(name = "created_at", nullable = false, updatable = false)
    private Instant createdAt;

    @UpdateTimestamp
    @Column(name = "updated_at", nullable = false)
    private Instant updatedAt;
}
```

### `ChatMessage`

```java
@Entity
@Table(name = "chat_message")
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
public class ChatMessage {

    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "session_id", nullable = false)
    private ChatSession session;

    @Column(nullable = false, length = 20)
    private String role;  // "USER", "ASSISTANT", "SYSTEM"

    @Column(nullable = false, columnDefinition = "TEXT")
    private String content;

    @Column(name = "token_count")
    private Integer tokenCount;

    @CreationTimestamp
    @Column(name = "created_at", nullable = false, updatable = false)
    private Instant createdAt;
}
```

## Relacionamentos

```
chat_session (1) ────────── (N) chat_message
    │                                    │
    │ FK binary_id → binaries            │ FK session_id → chat_session
    │ FK project_id → projects           │
    │                                    │
    └── cascade: ALL, orphanRemoval      └── cascade: ALL, orphanRemoval
         (deletar sessão deleta              (deletar sessão deleta
          todas as mensagens)                 todas as mensagens)

embedding_chunk (N) ────────── (1) binaries
    │
    │ FK binary_id → binaries
    │ ON DELETE CASCADE (deletar binário deleta todos os chunks)
    │
    └── source_id: FK lógica para static_function.id
         (não é constraint; usado para join manual)
```

## Índices e Performance

### Índices

| Índice | Coluna(s) | Uso |
|---|---|---|
| `idx_embedding_chunk_binary` | `binary_id` | Filtrar chunks por binário (todas as queries) |
| `idx_embedding_chunk_source` | `source_type, source_id` | Lookup por fonte (ex: achar chunk de uma função específica) |
| `idx_embedding_chunk_content` | `content` (GIN trgm) | Busca textual de fallback (`ILIKE '%term%'`) |
| `idx_chat_session_binary` | `binary_id` | Listar sessões de um binário |
| `idx_chat_message_session` | `session_id, created_at` | Buscar histórico em ordem cronológica |

> **Performance**: Sem índice vetorial (HNSW/IVFFlat limitados a 2000d). Para < 10K vetores de 4096d, sequential scan < 100ms.

## Coluna `metadata` (JSONB)

O campo `metadata` armazena informações estruturadas que não fazem parte do texto embeddado mas são úteis para filtragem e exibição:

```json
{
  "function_name": "sub_401000",
  "address": "0x401000",
  "callers": ["WinMain", "sub_402300"],
  "callees": ["CreateFileA", "WriteFile", "CloseHandle"],
  "label_count": 2,
  "assembly_length": 1247,
  "has_decompiled_code": false
}
```

Para chunks do tipo `METADATA`:

```json
{
  "filename": "malware.exe",
  "format": "PE32",
  "architecture": "x86",
  "compiler": "MSVC 19.0",
  "function_count": 342
}
```

Vantagens do JSONB sobre colunas separadas:
- Flexível: adicionar campos sem migration
- Indexável: `CREATE INDEX ... ON embedding_chunk USING gin (metadata jsonb_path_ops)` se necessário
- Permite queries como: `SELECT * FROM embedding_chunk WHERE metadata->>'function_name' = 'WinMain'`

## Políticas de Deleção

| Operação | Comportamento |
|---|---|
| Deletar binário | `ON DELETE CASCADE` → deleta todos os `embedding_chunk`, `chat_session` e `chat_message` |
| Deletar projeto | `ON DELETE CASCADE` via binário → cascata completa |
| Re-analisar binário | Hard delete de `embedding_chunk` via `deleteByBinaryId()` antes de reindexar |
| Deletar sessão | `orphanRemoval = true` → deleta todas as `chat_message` |

## Relação com o Schema Existente

A migration V008 é aditiva — não altera nenhuma tabela existente. As novas tabelas referenciam:
- `binaries(id)` — FK com CASCADE
- `projects(id)` — FK com CASCADE

O `source_id` em `embedding_chunk` referencia logicamente `static_function.id`, mas **sem constraint de FK** para:
- Permitir valores nulos (chunk METADATA não tem source)
- Permitir referências futuras a outras tabelas (ex: tabela de strings, tabela de imports)
