# 03 — Tech Spec: SDD Bot do Slack

worktree: SDDL-2
branch-raiz: main

**Orquestração:** SDDL-2 — SDD Bot do Slack
**Data:** 2026-06-24
**Agente:** techspec (lead) + security-reviewer + data-contract-reviewer
**Estado:** draft — aguardando aprovação humana (CP-2)
**Artifact store:** `.worktree/SDDL-2/docs/sddl-2/`
**Risco:** ALTO

---

## 1. Rastreabilidade

| Campo | Valor |
|---|---|
| Origem (PRD) | `docs/sddl-2/02-prd.md` — aprovado CP-1 |
| Ideia | `docs/sddl-2/01-idea.md` |
| Project Discovery | `docs/sddl-2/00-project-discovery.md` |
| Risk Classification | `docs/sddl-2/00-risk-classification.md` — Nível: ALTO |
| Próximo artefato | `docs/sddl-2/04-tasks.md` |
| Checkpoint | CP-2 — aprovação humana obrigatória |

**Requisitos cobertos:** RF-01..RF-24, RNF-01..RNF-09, RN-01..RN-09, REST-01..REST-07.
**Critérios de aceite endereçados:** CA-01..CA-13.

**Código real ancorado:**
- `bot/src/index.ts` — entrypoint Bolt (Socket Mode)
- `bot/src/commands/{bot-criar,card-editar,aprovado}.ts` — a refatorar
- `bot/src/providers/{jira,slack}.ts` — `JiraProvider` já tem `getLastComment`, `createCard`, `addComment`, `updateDescription`
- `bot/src/session/store.ts` — `SessionStore` (JSON; SQLite vem ao lado, não substitui imediatamente)
- `src/main.rs` — `run_orchestration` (~4394), `is_card_id`, `BotCommand` (~939), `run_bot` (~10696)
- `src/domain/orchestrator/engine.rs` — motor `tick`/`step`

---

## 2. Visão técnica geral

O bot é um daemon Node.js/TypeScript (`bot/`), executado via `pm2`, conectado ao Slack por Socket Mode. Esta feature:

1. **Substitui** o contrato de comandos atual (`/sdd bot-criar`, `/sdd card-editar`, `/aprovado`) por um único slash command `/bot` com cinco subcomandos.
2. **Introduz SQLite** (`better-sqlite3`) como fonte de verdade de ownership, estado e conteúdo de etapas.
3. **Adiciona gate `/bloqueado` no CLI Rust** (`run_orchestration`) — aborta a orquestração quando o último comentário Jira do card bate `/bloqueado` (case-insensitive + normalização Unicode).
4. **Desamarra** o bot dos comandos core (`sdd install`/`sdd update` deixam de oferecer o bot; passa a existir `sdd bot install` independente — esqueleto já existe em `BotCommand::Install`).

O CLI Rust permanece o harness determinístico. O bot orquestra efeitos (Slack, Jira, LLM, SQLite) e delega geração de artefatos a `sdd orchestrator <CARD-ID>` via `execa` (`orchestrator` é alias de `orchestration`, confirmado em `main.rs` linha ~147 `#[command(alias = "orchestrator")]`).

---

## 3. Arquitetura

```mermaid
flowchart LR
    subgraph Slack[Slack Workspace]
        U[Usuário]
        SC[/bot slash command]
    end
    subgraph Bot[Daemon Bot TS - pm2]
        BOLT[Bolt App Socket Mode]
        ROUTER[Router /bot]
        CMD[Handlers create/update/aproved/list/view]
        REPO[CardRepository SQLite]
        DB[(cards + card_stages)]
        JP[JiraProvider]
        SP[SlackProvider]
        LLM[LLM Provider]
        EXEC[execa → sdd]
    end
    subgraph CLI[CLI Rust sdd]
        ORCH[run_orchestration]
        GATE[block_gate.rs]
    end
    JIRA[(Jira REST v3)]
    LLMAPI[(Anthropic/OpenAI/Ollama)]

    U --> SC --> BOLT --> ROUTER --> CMD
    CMD --> REPO --> DB
    CMD --> JP --> JIRA
    CMD --> SP --> Slack
    CMD --> LLM --> LLMAPI
    CMD --> EXEC --> ORCH --> GATE --> JIRA
    GATE -. exit != 0 .-> EXEC
```

---

## 4. Stack

| Camada | Tecnologia | Existe? |
|---|---|---|
| Daemon | Node.js >= 20, TypeScript, `@slack/bolt` ^3.22.0, `@slack/web-api` ^7.8.0 | Sim |
| Persistência | `better-sqlite3` ^11.x | **Novo** |
| HTTP Jira | `axios` via `JiraProvider` | Sim |
| Subprocesso CLI | `execa` ^8.0.1 (ESM-only, dynamic import) | Sim |
| LLM | Anthropic / OpenAI / Ollama | Sim |
| Daemon manager | `pm2` | Sim |
| CLI determinístico | Rust (`sdd`), `clap` 4.5 | Sim |
| HTTP no Rust (gate) | `reqwest` — confirmar em `Cargo.toml` (R-4) | A confirmar |

---

## 5. Estrutura de pastas

```
bot/src/
  index.ts                        MODIFICAR: registrar /bot, remover /sdd e /aprovado
  commands/
    bot.ts                        NOVO: router ack + dispatch assíncrono
    create.ts                     NOVO (refatora bot-criar.ts)
    update.ts                     NOVO (refatora card-editar.ts)
    aproved.ts                    NOVO (refatora aprovado.ts)
    list.ts                       NOVO
    view.ts                       NOVO
    help.ts                       NOVO: help text por subcomando
  db/
    index.ts                      NOVO: abre better-sqlite3, aplica migrations
    migrations/001_init.sql       NOVO: DDL cards + card_stages
    cardRepository.ts             NOVO: CRUD + ownership + stages
  providers/
    jira.ts                       REUSO (getLastComment, createCard, addComment, updateDescription)
    slack.ts                      MODIFICAR: postEphemeral, uploadString-as-file
  channelKey.ts                   NOVO: extrai "key: PROJ" do tópico/descrição do canal

src/
  main.rs                         MODIFICAR: gate em run_orchestration; limpar amarrações bot
  domain/orchestrator/
    block_gate.rs                 NOVO: regex /bloqueado + fetch último comentário Jira
    mod.rs                        MODIFICAR: pub mod block_gate
```

---

## 6. Modelagem de dados (SQLite)

Banco em `.sdd/bot/sdd-bot.db`. Acesso serializado pela instância única do daemon (`better-sqlite3` síncrono — RNF-06). WAL habilitado.

### Tabela `cards`

| Coluna | Tipo | Constraint | Descrição |
|---|---|---|---|
| `card_id` | TEXT | PRIMARY KEY | Chave Jira (ex: `PROJ-42`) |
| `slack_user_id` | TEXT | NOT NULL | Criador — único autorizado (RN-01) |
| `slack_channel_id` | TEXT | NOT NULL, UNIQUE | Canal de origem; garante 1:1 canal↔card |
| `titulo` | TEXT | NOT NULL | Título gerado pelo LLM |
| `texto_original` | TEXT | NOT NULL | Descrição original do usuário |
| `status` | TEXT | NOT NULL DEFAULT `'em_progresso'` CHECK(`'em_progresso'`,`'aprovado'`) | Estado do card |
| `criado_em` | TEXT | NOT NULL DEFAULT `strftime(...)` | ISO-8601 UTC |
| `atualizado_em` | TEXT | NOT NULL | ISO-8601 UTC (atualizado por trigger) |
| `deleted_at` | TEXT | DEFAULT NULL | Soft-delete para auditoria pós-aprovação |

Índices: `idx_cards_user_status (slack_user_id, status)` — cobre `/bot list` (RNF-09).

### Tabela `card_stages`

| Coluna | Tipo | Constraint | Descrição |
|---|---|---|---|
| `id` | INTEGER | PRIMARY KEY AUTOINCREMENT | |
| `card_id` | TEXT | NOT NULL, FK → cards(card_id) ON DELETE CASCADE | |
| `stage` | TEXT | NOT NULL | `'risk'` \| `'idea'` \| `'prd'` |
| `conteudo` | TEXT | NOT NULL | Markdown da etapa para `/bot view` |
| `gerado_em` | TEXT | NOT NULL DEFAULT `strftime(...)` | ISO-8601 UTC |
| — | — | UNIQUE(card_id, stage) | Re-execução faz UPSERT, sem duplicatas |

Índice: `idx_stages_card (card_id)`.

### DDL completo — migration 001

```sql
-- bot/src/db/migrations/001_init.sql
PRAGMA foreign_keys = ON;
PRAGMA journal_mode = WAL;

CREATE TABLE IF NOT EXISTS cards (
  card_id          TEXT    PRIMARY KEY,
  slack_user_id    TEXT    NOT NULL,
  slack_channel_id TEXT    NOT NULL,
  titulo           TEXT    NOT NULL,
  texto_original   TEXT    NOT NULL,
  status           TEXT    NOT NULL DEFAULT 'em_progresso'
                           CHECK (status IN ('em_progresso', 'aprovado')),
  criado_em        TEXT    NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
  atualizado_em    TEXT    NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
  deleted_at       TEXT    DEFAULT NULL,
  UNIQUE (slack_channel_id)
);

CREATE TRIGGER IF NOT EXISTS cards_set_atualizado_em
AFTER UPDATE ON cards FOR EACH ROW
BEGIN
  UPDATE cards SET atualizado_em = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
  WHERE card_id = NEW.card_id;
END;

CREATE INDEX IF NOT EXISTS idx_cards_user_status ON cards (slack_user_id, status);
CREATE INDEX IF NOT EXISTS idx_cards_deleted_at  ON cards (deleted_at);

CREATE TABLE IF NOT EXISTS card_stages (
  id        INTEGER PRIMARY KEY AUTOINCREMENT,
  card_id   TEXT    NOT NULL,
  stage     TEXT    NOT NULL,
  conteudo  TEXT    NOT NULL,
  gerado_em TEXT    NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
  UNIQUE (card_id, stage),
  FOREIGN KEY (card_id) REFERENCES cards(card_id) ON DELETE CASCADE
);

CREATE INDEX IF NOT EXISTS idx_stages_card ON card_stages (card_id);

PRAGMA user_version = 1;
```

Controle de versão via `PRAGMA user_version`. Migration 2+ fora do MVP.

---

## 7. Contratos de API e interfaces TypeScript

### 7.1 Tipos de domínio

```typescript
// bot/src/types.ts (adições)

export type CardStatus = 'em_progresso' | 'aprovado'
export type StageName = 'risk' | 'idea' | 'prd'

export interface Card {
  card_id: string           // PK, formato validado [A-Z]+-[0-9]+
  slack_user_id: string     // criador
  slack_channel_id: string  // UNIQUE
  titulo: string
  texto_original: string
  status: CardStatus
  criado_em: string         // ISO-8601
  atualizado_em: string     // ISO-8601
  deleted_at: string | null
}

export interface CardStage {
  id: number
  card_id: string
  stage: StageName
  conteudo: string          // Markdown; /bot view retorna isso
  gerado_em: string
}

// Slack payloads
export interface SlackCommandPayload {
  user_id: string
  channel_id: string
  channel_name?: string
  text: string | undefined
  command: string           // '/bot'
}

export interface SlackMessagePayload {  // usado em app.message (ex: /aprovado legado)
  user: string | undefined
  channel: string | undefined
  text?: string
}

// Contrato de saída do CLI sdd orchestrator
export interface SddCliResult {
  ok: boolean
  status?: 'produced' | 'awaiting_approval' | 'error'
  stage?: string
  message?: string
  error?: { code: number; detail: string }
}

export const SDD_EXIT = {
  OK: 0,
  GATE_BLOCKED: 2,    // card bloqueado — fluxo esperado, não crash
  CONFIG_ERROR: 3,    // requer intervenção humana
  RUNTIME_ERROR: 1,   // retry possível
} as const
```

### 7.2 CardRepository

```typescript
class CardRepository {
  // Mutações
  insertCard(c: Omit<Card,'criado_em'|'atualizado_em'|'deleted_at'>): void
  updateStatus(cardId: string, status: CardStatus): void
  touchUpdate(cardId: string, textoOriginal: string): void
  upsertStage(cardId: string, stage: StageName, conteudo: string): void

  // Leitura
  getCard(cardId: string): Card | undefined
  assertOwner(cardId: string, userId: string): Card  // lança AccessDeniedError se != criador
  listByUser(userId: string): Pick<Card,'card_id'|'titulo'>[]  // status != 'aprovado', deleted_at IS NULL
  getStages(cardId: string): CardStage[]  // /bot view — sem IA (RF-15)
}
```

### 7.3 Slash command `/bot`

**Ack e dispatch:**
```typescript
app.command('/bot', async ({ command, ack, respond, client }) => {
  await ack()  // < 3s obrigatório (RNF-01)
  const [sub, ...rest] = (command.text ?? '').trim().split(/\s+/)
  void router.dispatch(sub, rest.join(' '), command, respond, client)
    .catch(err => logError(err))
})
```

**Help text** (exibido ao usuário no Slack — aprovado pelo usuário em CP-1):
```
/bot create <descrição>   — Cria um card no Jira e gera os documentos SDD até o PRD.
/bot update <id> <texto>  — Atualiza o card e refaz os documentos com o novo contexto.
/bot aproved <id>         — Aprova o card, liberando para o time de desenvolvimento.
/bot list                 — Lista seus cards ainda não aprovados.
/bot view <id>            — Exibe toda a documentação gerada para um card.
```

### 7.4 Chamada ao orquestrador via execa

```typescript
const { execa } = await import('execa')  // ESM-only

const CARD_ID_RE = /^[A-Z][A-Z0-9]+-\d+$/

async function runOrchestrator(cardId: string, worktreePath: string) {
  if (!CARD_ID_RE.test(cardId)) throw new Error(`Card ID inválido: ${cardId}`)

  const result = await execa('sdd', ['orchestrator', cardId], {
    cwd: worktreePath,
    // NÃO propagar process.env completo — apenas variáveis necessárias
    env: {
      PATH: process.env.PATH,
      HOME: process.env.HOME,
      SDD_BOT_CONTEXT: 'true',
    },
    reject: false,  // capturar exit code != 0 (gate)
    timeout: 300_000,  // 5 min (RNF-08)
  })
  return result
}
```

**Leitura de resultado:**
```typescript
if (result.exitCode !== 0) {
  const isGate = result.exitCode === SDD_EXIT.GATE_BLOCKED
                 || /bloqueado/i.test(result.stderr ?? '')
  if (isGate) { /* ephemeral de bloqueio ao usuário (RF-20) */ }
  else        { /* ephemeral de erro genérico + log */ }
}
```

### 7.5 Gate `/bloqueado` no Rust

**Localização:** `src/domain/orchestrator/block_gate.rs`, chamado em `run_orchestration` (~linha 4394) dentro do bloco `if let Some(ref cid) = card_id`, após `enforce_branch_for_card`.

```rust
// src/domain/orchestrator/block_gate.rs
use anyhow::Result;
use std::path::Path;

pub fn ensure_not_blocked(root: &Path, card_id: &str) -> Result<()> {
    let Some(jira_cfg) = load_jira_config(root) else { return Ok(()); };
    let last = fetch_last_comment(&jira_cfg, card_id)?;
    if is_blocked_marker(last.as_deref()) {
        anyhow::bail!(
            "card {card_id} está bloqueado (último comentário Jira: /bloqueado). \
             Remova o bloqueio antes de orquestrar."
        );
    }
    Ok(())
}

/// Case-sensitive, apenas trim. Exige "/bloqueado" em minúsculo (AD-5).
pub fn is_blocked_marker(text: Option<&str>) -> bool {
    let Some(t) = text else { return false };
    t.trim() == "/bloqueado"
}
```

**Pontos de inserção em `main.rs`:**
```rust
// dentro de run_orchestration, bloco if let Some(ref cid) = card_id
enforce_branch_for_card(&root, cid, true)?;
assert_prerequisite_artifacts(&root, cid)?;
crate::domain::orchestrator::block_gate::ensure_not_blocked(&root, cid)?;  // NOVO
```

`is_card_id` (já existente, ~linha 11170) garante que somente Card IDs chegam aqui — texto livre não consulta o Jira (CA-10/RF-19).

### 7.6 Jira REST calls

| Operação | Endpoint | Uso |
|---|---|---|
| Criar card | `POST /rest/api/3/issue` | RF-05 |
| Adicionar comentário | `POST /rest/api/3/issue/{id}/comment` | RF-06 (/bloqueado), RF-13 (/aprovado) |
| Atualizar descrição | `PUT /rest/api/3/issue/{id}` | RF-12 |
| Último comentário | `GET /rest/api/3/issue/{id}/comment?orderBy=-created&maxResults=1` | Gate RF-17; Rust block_gate |

Todas já implementadas em `bot/src/providers/jira.ts`.

### 7.7 channelKey.ts

```typescript
// Extrai "key: PROJ" do tópico/descrição do canal (RN-04/RF-03)
const KEY_RE = /key\s*:\s*([A-Z][A-Z0-9]+)/i

export async function extractProjectKey(client: WebClient, channelId: string): Promise<string> {
  const info = await client.conversations.info({ channel: channelId })
  const topic   = info.channel?.topic?.value ?? ''
  const purpose = info.channel?.purpose?.value ?? ''
  const m = KEY_RE.exec(topic) ?? KEY_RE.exec(purpose)
  if (!m) throw new ChannelKeyMissingError(channelId)
  return m[1].toUpperCase()
}
```

---

## 8. Decisões arquiteturais

### AD-1 — `better-sqlite3` como biblioteca SQLite
- **Alternativas:** `sql.js` (WASM, sem persistência em disco confiável), `typeorm`+`sqlite` (overengineering para 2 tabelas).
- **Decisão:** `better-sqlite3` — API síncrona, prepared statements, WAL, serializa naturalmente no processo único (RNF-06). Binário nativo exige `npm rebuild` por plataforma (R-1, mitigado pelo `sdd bot install`).

### AD-2 — Processamento assíncrono no Bolt
- **Decisão:** `await ack()` imediato + `void dispatch(...).catch(...)`. Já é o padrão de `index.ts` linha ~105. Sem fila persistente no MVP — reinício pm2 não retoma jobs em voo (documentar no runbook, R-3).

### AD-3 — UPSERT de etapas para re-execução idempotente
- `UNIQUE(card_id, stage)` + `INSERT OR REPLACE INTO card_stages` permite que `/bot update` reescreva etapas sem duplicatas.

### AD-4 — Gate no `run_orchestration` (não no `tick`/`step`)
- O engine (`engine.rs`) itera a fila de demandas e não é o entrypoint de `sdd orchestrator <id>`. O ponto correto é `run_orchestration` (~4394), pré-condição antes de qualquer geração.

### AD-5 — Case-sensitive no gate (alinhado com PRD §5.6.3)
- O comentário deve ser exatamente `/bloqueado` (minúsculo) para ativar o gate. Capitalização diferente (`/BLOQUEADO`, `/BloquEado`) não bloqueia — comportamento intencional, confirmado em CP-2.
- Apenas `trim()` antes do compare; sem normalização Unicode.

### AD-6 — Help text como ephemeral de ajuda
- `/bot` sem args ou `/bot help` retorna lista formatada de subcomandos com sintaxe e descrição (aprovado pelo usuário em CP-1).
- Subcomando inválido também cai no help.

### AD-7 — Limpeza das amarrações antigas
- **Rust `main.rs`:** remover prompt "Bot Slack opcional - instalar agora?" do fluxo de `install`/`update`; manter apenas `print_bot_install_hint` apontando para `sdd bot install`.
- **Bot TypeScript:** `index.ts` troca `app.command('/sdd')` + `app.message(/^\/aprovado$/)` por `app.command('/bot')`; arquivos de comando renomeados.
- **Não tocar:** `run_orchestration`, `resolve_input`, `extract_card_id`, demand/queue/engine.

### AD-8 — `env` restrito no execa (S-02 data-contract)
- `process.env` completo **não** é propagado para o subprocesso `sdd` — vaza tokens Slack/Jira/LLM. Propagar apenas `PATH`, `HOME` e `SDD_BOT_CONTEXT`.

---

## 9. Fluxos técnicos

### Sequence — `/bot create`

```mermaid
sequenceDiagram
    actor U as Usuário
    participant B as Bot (Bolt)
    participant K as channelKey
    participant L as LLM
    participant J as Jira REST
    participant D as SQLite
    participant E as execa → sdd
    participant G as block_gate.rs

    U->>B: /bot create <texto>
    B-->>U: ack() (< 3s)
    B->>K: extrai "key: PROJ" do canal
    alt sem key configurada
        B-->>U: ephemeral: configure "key: PROJ" no tópico (CA-02)
    else key encontrada
        B->>L: generateTitle(texto)
        L-->>B: titulo
        B->>J: POST issue → card_id
        B->>D: INSERT cards (em_progresso)
        B->>J: POST comment "/bloqueado"
        B->>E: sdd orchestrator CARD-ID
        E->>G: ensure_not_blocked(CARD-ID)
        G->>J: GET último comentário
        alt último == /bloqueado (case-insensitive)
            G-->>E: exit 2 + stderr
            E-->>B: gate ativado
            B-->>U: ephemeral: card bloqueado (CA-08)
        else liberado
            loop por etapa risk/idea/prd
                B-->>U: ephemeral "gerando <etapa>..."
                E-->>B: conteúdo da etapa
                B->>D: upsertStage(CARD-ID, etapa, conteudo)
                alt conteúdo > 3000 chars
                    B->>U: arquivo Slack (CA-11)
                else
                    B-->>U: ephemeral conteúdo
                end
            end
        end
    end
```

### State machine — Card

```mermaid
stateDiagram-v2
    [*] --> em_progresso : /bot create
    em_progresso --> em_progresso : /bot update (+/bloqueado)
    em_progresso --> aprovado : /bot aproved (+/aprovado Jira)
    aprovado --> [*]

    note right of em_progresso
        Gate /bloqueado ativo
        Aparece em /bot list
    end note
    note right of aprovado
        Comentário /aprovado no Jira
        Não aparece em /bot list
    end note
```

---

## 10. Segurança (achados incorporados do security-reviewer)

### S-01 — Gate `/bloqueado` — case-sensitive, trim apenas

O compare é exato: `text.trim() == "/bloqueado"`. Somente o comentário em minúsculo ativa o gate (AD-5, confirmado CP-2).

Implementação Rust: ver `is_blocked_marker` em §7.5.
Implementação TypeScript (validação local no bot antes de chamar o CLI):
```typescript
const isBlocked = (comment: string) => comment.trim() === '/bloqueado'
```

### S-02 — Ownership obrigatório em `update`

`CardRepository.assertOwner(cardId, userId)` **deve** ser chamado no handler `update.ts` antes de qualquer mutação. Achado: `card-editar.ts` atual não verifica ownership — corrigir na refatoração.

### S-03 — Permissões de arquivo 0600

No boot do daemon, verificar modo do `sdd-bot.config.yaml` e do arquivo SQLite:
```typescript
import { statSync } from 'node:fs'
const mode = statSync(configPath).mode & 0o777
if (mode !== 0o600) {
  logger.warn(`SEGURANÇA: ${configPath} tem permissão ${mode.toString(8)} — deveria ser 0600`)
}
```
`sdd bot install` deve criar o arquivo com `0600` explicitamente.

### S-04 — Redação de secrets em logs e mensagens Slack

```typescript
const SECRET_RE = /(xox[bp]-[\w-]+|xapp-\d-[\w-]+|Basic\s+[A-Za-z0-9+/=]+)/g
const redact = (s: string) => s.replace(SECRET_RE, '[REDACTED]')
```
Aplicar em `postError`, `postToLogs` e `extractStderr` antes de logar ou postar no Slack.

### S-05 — Validação do Card ID retornado pela API Jira

```typescript
const CARD_ID_RE = /^[A-Z][A-Z0-9]+-\d+$/
// Após createCard:
if (!CARD_ID_RE.test(cardId)) throw new Error(`Card ID inesperado da API Jira: ${cardId}`)
```
Previne path traversal e argument injection se a API retornar valor inesperado.

### S-06 — `execa` sem shell + env restrito

`execa` usa array de args (sem shell) — command injection via texto livre não é possível. Verificado no código existente (`bot-criar.ts`, `worktree.ts`, `context.ts`). **Garantir** que nenhuma chamada futura use `shell: true` (lint/grep de CI).
`env` restrito: somente `PATH`, `HOME`, `SDD_BOT_CONTEXT` (ver AD-8 / §7.4).

### S-07 — SQLite: sem dados sensíveis

Tabela `cards` **não** armazena tokens, API keys ou passwords. `card_stages` armazena conteúdo Markdown dos artefatos (não-sensível). Esta é uma invariante que deve ser documentada no `cardRepository.ts`.

---

## 11. Autenticação e autorização

- **Slack ingress (Socket Mode):** Bolt valida o transporte via App Token `xapp-`. Assinatura HMAC-SHA256 em `src/runtime/slack.rs` aplica-se ao modo HTTP — **documentar** que CA-13 (401 em payload inválido) refere-se ao runtime HTTP, não ao Socket Mode atual.
- **Ownership de card (RN-01):** `CardRepository.assertOwner` compara `slack_user_id`; aplicado em `update`, `aproved`, `view` **antes** de qualquer mutação/leitura sensível.
- **`list` filtra por userId** do executor — somente cards do próprio usuário (RN-06, CA-06).
- **Jira:** Basic auth (`user:api_token`) já em `JiraProvider`. Tokens fora do repo (REST-01), validados por `sdd bot install`.

---

## 12. Validações de entrada

| Input | Validação | Onde |
|---|---|---|
| Subcomando `/bot` | `∈ {create,update,aproved,list,view}` | `bot.ts` router |
| Card ID (update/aproved/view) | regex `^[A-Z][A-Z0-9]+-\d+$` | `bot.ts` antes do dispatch |
| Card ID retornado pela Jira API | mesma regex, logo após `createCard` | `create.ts` (S-05) |
| Texto `/bot create` | não-vazio; máx. 4000 chars | `create.ts` |
| `key:` do canal | regex `key\s*:\s*([A-Z][A-Z0-9]+)` case-insensitive | `channelKey.ts` |
| Comentário gate `/bloqueado` | trim + compare exato case-sensitive `/bloqueado` | `block_gate.rs`, `isBlocked` TS |
| Argumento `sdd orchestrator` | `is_card_id` (já existente) decide se consulta Jira | `main.rs` |

---

## 13. Tratamento de erros

- Cada handler captura erros, envia ephemeral ao usuário e loga no canal `-logs` — sem derrubar o daemon (RNF-08).
- `execa({ reject: false })` para distinguir gate (exit 2) de crash (exit 1/3).
- Falha Jira/Slack/LLM → ephemeral + log; mutações SQLite em transação.
- `AccessDeniedError` → ephemeral amigável, zero mutação (CA-04).
- Secrets redatados em todos os caminhos de log/erro (S-04).

---

## 14. Performance

- `ack()` < 3s garantido por dispatch assíncrono (AD-2, RNF-01).
- `/bot list` coberto por `idx_cards_user_status` — < 500ms até 10k cards (RNF-09).
- `/bot view` lê `card_stages` por `idx_stages_card`, sem IA (RF-15, RNF-09).
- WAL reduz contenção durante streaming de etapas.

---

## 15. Observabilidade

- Log estruturado de `card.created`, `gate.blocked`, `card.approved`, `access.denied` (RNF-07).
- CLI Rust: gate emite evento em `.sdd/events.jsonl` com `card_id` e `blocked: bool`.
- `sdd bot status` reporta config + pm2.

---

## 16. Testes (evidências para CP-4)

| Camada | O que testar |
|---|---|
| Bot unit | Mocks de `JiraProvider`, `SlackProvider`, `LLMProvider`; `CardRepository` contra `:memory:` |
| Roteamento | ack imediato; ownership negado (CA-04); help em subcomando inválido |
| channelKey | Extração presente/ausente (CA-02) |
| SQLite | Migration idempotente; UPSERT de etapa; filtro list (CA-06); view sem IA (CA-07) |
| Gate Rust | `is_blocked_marker` para `/bloqueado` (match), `/BLOQUEADO`, `/BloquEado`, texto com espaço, `/aprovado`, vazio — todos devem resultar correto |
| gate Rust CLI | `ensure_not_blocked` com mock HTTP; texto livre não consulta Jira (CA-10) |
| Regressão | `cargo test` existente verde após limpeza de amarrações |
| Segurança | Card ID malicioso da API Jira rejeitado (S-05); env restrito verificado (AD-8); 0600 em boot (S-03) |

---

## 17. Riscos técnicos

| ID | Risco | Severidade | Mitigação |
|---|---|---|---|
| R-1 | `better-sqlite3` binário nativo falha em rebuild | Alta | Node >= 20 fixo; `npm rebuild` em `sdd bot install`; documentar no runbook |
| R-2 | Remoção de amarrações quebra `sdd install`/`update` | Alta | Checklist de regressão; `sdd doctor` valida bot opcionalmente; manter `print_bot_install_hint` |
| R-3 | Reinício pm2 perde jobs em voo (sem fila) | Média | Documentar no runbook; `/bot update` re-executa; status permanece `em_progresso` |
| R-4 | Cliente HTTP Jira no Rust pode não existir no `Cargo.toml` | Média | Confirmar `reqwest`; se ausente, adicionar feature `reqwest/rustls-tls` |
| ~~R-5~~ | ~~PRD §5.6.3 divergência case~~ | — | **Resolvido CP-2:** case-sensitive confirmado; PRD §5.6.3 está correto. |
| R-6 | RNF-04/CA-13 (HMAC/401) não se aplica a Socket Mode | Baixa | Documentar superfície real; HMAC vive em `src/runtime/slack.rs` para modo HTTP futuro |

---

## 18. Plano de implementação (para detalhamento em Tasks)

| Bloco | Escopo | Owner sugerido |
|---|---|---|
| A — Persistência | `better-sqlite3`, `db/index.ts`, `001_init.sql`, `cardRepository.ts` | backend-implementer |
| B — Comandos `/bot` | `bot.ts` router, `create/update/aproved/list/view/help.ts`, `channelKey.ts`, ajuste `SlackProvider`, reescrita `index.ts` | backend-implementer |
| C — Gate Rust | `block_gate.rs` + integração em `run_orchestration` + `mod.rs` | backend-implementer |
| D — Instalador + limpeza | estender `run_bot`, remover amarrações de `install`/`update`, checagens `.gitignore`/PATH | backend-implementer |
| E — Testes + evidências | §16 completo, `cargo audit`, `gitleaks` | test-implementer |

**Ordem:** A → (B ‖ C) → D → E. B e C paralelizáveis após A.

---

## 19. Checklist de regressão (RF-24)

Antes de merge, confirmar que os seguintes comandos continuam funcionando sem alteração de comportamento:

- [ ] `sdd install <projeto>` — não pergunta sobre bot
- [ ] `sdd update` — não pergunta sobre bot
- [ ] `sdd doctor` — bot listado como componente opcional
- [ ] `sdd orchestrator <texto-livre>` — executa normalmente, sem consultar Jira
- [ ] `sdd orchestrator <CARD-ID>` com último comentário ≠ `/bloqueado` — executa normalmente
- [ ] `cargo test` completo — verde
