# Tech Spec — SDD CLI (TUI)

## Visão técnica

A TUI é um cockpit determinístico, sem IA embarcada e sem rede, que conduz o fluxo Spec-Driven completo pelo terminal. Ela não gera conteúdo: orquestra o estado das etapas, dispara os subcomandos `sdd` existentes como processos filhos, exibe loaders, renderiza Markdown inline, conduz checkpoints humanos e persiste em `docs/<slug>/` + `traceability-map.yaml` usando exatamente o mesmo contrato do harness.

Decisões travadas reafirmadas: (1) zero IA/HTTP — geração via subprocessos `sdd` + `$EDITOR`; (2) MVP cobre o fluxo guiado completo; (3) entrada exclusiva `sdd tui` (o `sdd` sem subcomando continua exibindo a ajuda do clap, sem alteração); (4) sem streaming de tokens — loaders cobrem operações de filesystem/subcomando e o artefato é exibido inteiro após o término.

Onde a TUI mora: novo módulo `src/tui/`, isolado do binário atual. O comando entra no enum `Command` (`src/main.rs:124`) como `Tui(TuiArgs)` e ganha um único braço novo no `match cli.command` de `run()` (`src/main.rs:444`), delegando a `tui::run(args)`. Nenhuma das funções existentes (`run_stage`, `save_artifact`, `write_stage_content`, `update_stage_state`, `infer_artifact`, hooks) é modificada — apenas reusada/invocada. Isso preserva RNF-05 (não regressão) por construção.

Estrutura proposta:

```
src/tui/
  mod.rs           # entrada tui::run(args), guarda de TTY, setup/teardown do terminal
  app.rs           # App (estado global), Screen (enum de telas), reducers/transições puras
  stage.rs         # StageState, máquina de estados das 9 etapas, regras de navegação/bloqueio
  orchestration.rs # leitura de docs/, parse do traceability-map.yaml -> modelo Orchestration
  event.rs         # mapeamento de KeyEvent -> Action (atalhos RF-10), modo de input
  ui.rs            # render puro: telas, painel de progresso, viewer markdown, checkpoint, ajuda
  markdown.rs      # parser do subconjunto Markdown -> ratatui::text::Text (RF-07)
  runner.rs        # invocação de subcomando sdd via std::process::Command + loader em thread
  loader.rs        # spinner/estado do loader (RF-05, threshold 200ms)
  editor.rs        # suspensão/restauração do terminal ao abrir $EDITOR (RF-08)
  persist.rs       # escrita atômica write-then-rename + chamada a save_artifact reusada
```

Fronteira: lógica de estado/transição/parse em `app.rs`/`stage.rs`/`orchestration.rs`/`markdown.rs` (funções puras, testáveis sem terminal); `ui.rs` só desenha a partir do estado; `runner.rs`/`editor.rs` concentram efeitos colaterais.

## Arquitetura

### Componentes e fluxo

Padrão Elm-like (Model–Update–View) sobre event loop síncrono:

1. **Setup** (`mod.rs`): detecta TTY; se ausente, degrada (RF-11) e sai com código 1, antes de qualquer escrita no terminal. Entra em raw mode + alternate screen via crossterm.
2. **Model** (`App`): estado completo (tela atual, orquestração selecionada, etapas, loader, viewer, erro).
3. **Event loop**: `crossterm::event::poll(tick)` não-bloqueante. A cada iteração: drena eventos de teclado/resize → `update(app, action)` (transição pura) → `ui::render(frame, app)`. O `poll` com timeout de ~100ms também serve de tick para animar o spinner sem thread quando não há subprocesso em curso.
4. **Execução de etapa** (`runner.rs`): ao disparar uma etapa, invoca `sdd <stage>` como processo filho. Como `std::process::Command::output()` é bloqueante, a execução roda em **thread dedicada** que envia o resultado por `std::sync::mpsc::channel`. O loop principal continua animando o spinner e processando `Ctrl+C` enquanto aguarda via `try_recv`. Ao chegar o resultado (exit code, stdout, stderr), transiciona para viewer ou painel de erro.

Decisão threads vs tokio: **não usar tokio**. O único bloqueio real é a espera de processos filhos; worker `std::thread` + `mpsc` resolve com custo mínimo de binário e zero runtime assíncrono (RNF-04). O spinner é animado pelo tick do `poll`, não por thread própria, evitando estado de terminal compartilhado entre threads. A thread worker só toca filesystem/processo, nunca o terminal.

### Loader (RF-05 / RNF-01)

Loader é "armado" quando a operação começa, mas só **desenhado** após ≥200ms (`Instant::elapsed`). Operações que terminam antes do threshold não renderizam spinner (CA-RF05 cenário 2). Frames braille (`⠋⠙⠹...`) avançam a cada tick.

### Crates

- **ratatui** `0.29` — framework mantido (sucessor de tui-rs), widgets/layout estáveis, suporta `TestBackend` para testes de render. Compatível com crossterm 0.28.
- **crossterm** `0.28` — backend de terminal multiplataforma (macOS/Linux, RNF-03): raw mode, alternate screen, eventos de teclado e **resize** (RNF-02, trata SIGWINCH internamente sem signal handler manual).

Ambas pure-Rust, sem deps de C (preserva cross-compile). Pin de versões a confirmar na task de setup após `cargo bloat` (R-02). Nenhuma outra dep nova.

### Suspender/restaurar para `$EDITOR` (RF-08 / R-07)

`editor.rs`: `disable_raw_mode()` + `LeaveAlternateScreen` → `Command::new($EDITOR ou vi).arg(path).status()` (herda stdin/stdout/stderr do terminal real, bloqueante e intencional) → ao retornar, `EnterAlternateScreen` + `enable_raw_mode()` + `terminal.clear()` e força redraw. Em erro/panic, teardown garantido por guard `Drop` (RAII).

### Fluxo de telas

```mermaid
stateDiagram-v2
    [*] --> TtyCheck
    TtyCheck --> Degrade: sem TTY/TERM=dumb
    Degrade --> [*]: exit 1
    TtyCheck --> OrchestrationList: TTY ok
    OrchestrationList --> NewOrchestration: criar
    OrchestrationList --> StageBoard: selecionar
    NewOrchestration --> StageBoard: sdd init
    StageBoard --> Running: executar etapa
    Running --> Viewer: exit 0
    Running --> ErrorPanel: exit != 0
    ErrorPanel --> Running: tentar novamente
    Viewer --> Checkpoint: etapa obrigatoria
    Viewer --> StageBoard: etapa auto-aprovavel
    Checkpoint --> Editor: editar (e)
    Editor --> Checkpoint: fecha $EDITOR
    Checkpoint --> Running: regenerar (r)
    Checkpoint --> StageBoard: aprovar (a)
    StageBoard --> Summary: apos memory
    Summary --> [*]
```

## Contratos

### Subcomando `tui`

```
sdd tui [--root <path>]
```

Sem args obrigatórios. `--root` opcional (default `.`). `TuiArgs` minimalista; sem flags de provider (NO-7).

### Invocação de subcomando filho (`runner.rs`)

- **Programa:** o próprio executável (`std::env::current_exe()`), sem depender do `sdd` no PATH.
- **Args:** `[stage, "--name", <nome>, "--root", <root>, <input...>]` como argumentos individuais ao `Command` — **nunca** via shell (ver Segurança). Mapeamento etapa→subcomando segue o dispatch real (`src/main.rs:477-490`). Criação usa `init` com a ideia.
- **stdin:** `Stdio::null()` no MVP.
- **env:** herdada integralmente.
- **Captura:** `output()` → `status.code()`, `stdout`, `stderr` (UTF-8 lossy).
- **Contrato de sucesso:** exit 0 ⇒ subcomando já gravou `docs/<slug>/<NN>-stage.md` em `draft` e atualizou o traceability-map (via `write_stage_content`, `src/main.rs:1969`). A TUI localiza o arquivo por `STAGE_FILES` (`src/main.rs:70`) para o viewer.

### Leitura/escrita do `traceability-map.yaml`

- **Leitura:** `serde_yaml` (já no projeto) do bloco `artifacts.<stage>` com `file`, `state`, `approved_at` (e `skipped_reason` em refinement). Esquema produzido por `render_index` (`src/main.rs:985`). Estado da orquestração em `orchestration.state`.
- **Escrita de aprovação:** a TUI **não** reimplementa a mutação do YAML. Ao aprovar, invoca `sdd artifact save --name <nome> --stage <stage> --file <path> --state approved --force` (`src/main.rs:1140`), que chama `update_stage_state` (`src/main.rs:1061`) — contrato idêntico ao harness (RF-09, R-03). Único ponto de escrita de estado = código já testado.
- **Skip de refinement:** `sdd artifact save --stage refinement --state skipped`.

## Dados

Modelo de estado da TUI (puro, sem efeitos):

```rust
struct App {
    screen: Screen,
    orchestrations: Vec<Orchestration>,
    selected: Option<usize>,
    active: Option<ActiveOrchestration>,
    loader: Loader,
    error: Option<ErrorView>,
    should_quit: bool,
}

enum Screen {
    OrchestrationList,
    NewOrchestration { input: String },
    StageBoard,
    Running { stage: Stage },
    Viewer { stage: Stage, content: String, scroll: u16 },
    Checkpoint { stage: Stage },
    Editor { stage: Stage },
    Summary,
    Help { return_to: Box<Screen> },
}

struct Orchestration { name: String, slug: String, path: PathBuf, state: String, current_stage: Option<Stage> }
struct ActiveOrchestration { meta: Orchestration, stages: Vec<StageState> }
struct StageState { stage: Stage, file: String, state: StageStatus, approved_at: Option<String> }
enum StageStatus { Pending, Optional, InProgress, Approved, Skipped, Unknown }
enum Stage { Idea, Prd, Techspec, Tasks, Refinement, Execution, Review, Memory }
struct Loader { armed_at: Option<Instant>, frame: usize, label: String }
struct ErrorView { stage: Stage, stderr: String }
```

`Stage` mapeia 1:1 para os pares de `STAGE_FILES` (`src/main.rs:70`). `current_stage` é a primeira etapa não-aprovada (RF-04). Sem banco: fonte de verdade é o filesystem. O modelo é reconstruído a cada entrada no StageBoard (relê o YAML — mitiga R-05).

Regras de navegação (RF-04, `stage.rs`): navegar para etapas `Approved`/anteriores é livre; etapa futura só acessível se todas as obrigatórias anteriores estiverem `Approved`/`Skipped` — caso contrário bloqueio "Aprovação do <etapa> obrigatória" (CA-RF08).

## Segurança

(skill security-privacy aplicada — risco médio por dependência externa de subprocessos)

- **Sem shell injection:** subprocessos sempre via `std::process::Command` com args vetorizados; nunca `sh -c`. A ideia em linguagem natural é passada como **um único argumento** posicional, não concatenada em comando.
- **`$EDITOR`:** lido de env e usado como programa do `Command` com o path do artefato como argumento separado — sem `sh -c`, metacaracteres não viram comando. Fallback `vi`. Path sempre dentro de `docs/<slug>/`.
- **Escrita atômica (RNF-06, CA-RF09):** hoje `write_stage_content`/`save_artifact` usam `fs::write` direto (`src/main.rs:1182`, `1996`) — **não atômico**. Proposta: helper `write_atomic(path, bytes)` (escreve em `path.tmp` no mesmo diretório → `fs::rename`) em `persist.rs` e refatorar `save_artifact`/`write_stage_content` para usá-lo. `rename` no mesmo filesystem é atômico: SIGKILL durante escrita preserva o artefato anterior. Benefício para harness e TUI.
- **Sem rede:** nenhuma dep HTTP; RNF-08 garantido por ausência de reqwest/tokio-net.
- **Não sobrescrever aprovado em erro (RN-11):** painel de erro nunca dispara escrita; aprovação só via ação explícita `a`.
- **Confinamento de path:** operações restritas a `docs/<slug>/` resolvido por `artifact_dir` (`src/main.rs:981`); slug normalizado por `slugify` (`src/main.rs:499`) evita path traversal.

## Performance

- **Abertura < 1s (RNF-01):** startup só lista `docs/` (walkdir raso) e parseia N traceability-maps pequenos. Validável por `time sdd tui`.
- **Loader ≤200ms (RF-05):** threshold por `Instant::elapsed`; tick de ~100ms no `poll`.
- **Resize (RNF-02):** crossterm emite `Event::Resize`; loop redesenha com `Layout` recalculado. Sem buffer manual.
- **Tamanho do binário (RNF-04, ≤+5MB):** medir antes/depois com `cargo bloat --release` e binário stripped; gate de tamanho na task de setup.
- **Render:** viewer converte o `.md` uma vez ao entrar na tela (não a cada frame); scroll só muda offset.

## Observabilidade

- **Reuso de `log_jsonl`** (`src/main.rs:3937`): a TUI emite `.sdd/tui-events.jsonl` (mesma mecânica do gate em `.sdd/artifact-gates.jsonl`). Eventos: `tui_open`, `orchestration_selected`, `stage_started`, `stage_finished` (exit_code, duration_ms), `checkpoint_decision` (approve/regenerate/edit/skip), `tui_error` (stderr truncado), `tui_close`. Sem PII além do nome; aplicar `runtime::redaction` se stderr puder conter segredos.
- **Modo de erro:** painel dedicado (RF-13) exibe stderr; evento `tui_error` registra contexto. Em panic, o guard RAII restaura o terminal e o panic vai para stderr já restaurado.
- **Degradação (RF-11):** ao sair por falta de TTY, mensagem clara no stderr + exit 1.

## Testes

(skill test-strategy aplicada)

- **Unit — máquina de estados (`stage.rs`):** transições e bloqueio (CA-RF08) sem terminal.
- **Unit — parse do traceability-map (`orchestration.rs`):** YAML com/sem stages, ausência → `unknown` (CA-RF02), refinement `optional`/`skipped`.
- **Unit — markdown (`markdown.rs`):** cabeçalhos, listas, código, tabelas → `Text` esperado (RF-07).
- **Unit — loader threshold (`loader.rs`):** <200ms não arma; ≥200ms arma (CA-RF05).
- **Render — ratatui `TestBackend`:** snapshot de StageBoard, Viewer, Checkpoint, ErrorPanel a partir de `App` fixo; desenha sem panic em larguras variadas (RNF-02).
- **Inferência do gate (crítico):** artefatos gerados/aprovados pela TUI continuam inferidos corretamente por `infer_artifact` (`src/main.rs:4034`) e passam no gate.
- **Não regressão (CA-RNF05):** contrato confirmando stdout/stderr/exit dos subcomandos existentes inalterados.
- **Escrita atômica (CA-RF09):** unit do `write_atomic`; arquivo `.tmp` órfão não corrompe o alvo.
- **Degradação (CA-RF11):** `tui::run` com stdout não-TTY retorna erro/exit 1 sem entrar em raw mode (trait/flag de detecção injetável).
- **Manual por terminal (RNF-03):** matriz iTerm2/Terminal.app/alacritty/kitty/tmux em macOS e Linux — zero panic, resize, ciclo `$EDITOR`.

## Riscos

Herdados do PRD (R-01..R-08) permanecem. Riscos técnicos adicionais:

| ID | Risco | Sev. | Mitigação |
|---|---|---|---|
| RT-01 | Corrupção do estado do terminal em panic/erro (raw mode não restaurado) | Alto | Guard RAII (`Drop`) sempre faz teardown; `panic` hook restaura antes de imprimir |
| RT-02 | Thread worker do subprocesso vaza/trava o loop ao sair | Médio | `mpsc` com `try_recv`; em quit, abandonar o handle (filho termina); join opcional no teardown |
| RT-03 | `infer_artifact` classificar errado `.md` tocado pela TUI | Médio | Reuso do `save_artifact`; teste de inferência dedicado |
| RT-04 | Incompatibilidade ratatui/crossterm | Médio | Pin 0.29/0.28; `cargo update` controlado; CI compila |
| RT-05 | Acréscimo de binário > 5MB (RNF-04) | Médio | `cargo bloat` + gate na task de setup; sem tokio/reqwest |
| RT-06 | `fs::write` atual não-atômico contradiz RNF-06 | Médio | `write_atomic` + refatoração de `save_artifact`/`write_stage_content` |
| RT-07 | Subprocesso `current_exe()` divergir em ambiente empacotado | Baixo | `current_exe()` resolve o binário real; fallback documentado |
| RT-08 | Markdown subset insuficiente para artefatos reais | Baixo-Médio | Cobrir cabeçalhos/listas/código/tabelas; degradar para texto cru no resto |

## Plano de implementação

1. Adicionar deps `ratatui` e `crossterm` ao `Cargo.toml`; baseline de binário com `cargo bloat`; gate ≤+5MB.
2. Criar esqueleto `src/tui/mod.rs` + submódulos vazios; declarar `mod tui;` em `main.rs`.
3. Adicionar `Command::Tui(TuiArgs)` no enum e o braço em `run()` delegando a `tui::run`, sem tocar nos demais braços; `TuiArgs { root }`.
4. Detecção de TTY e degradação (RF-11/CA-RF11) antes de qualquer render; exit 1 limpo.
5. Setup/teardown do terminal (raw mode, alternate screen) com guard RAII (RT-01).
6. `App`/`Screen`/`Stage` e event loop básico (`poll`/`update`/`render`).
7. `orchestration.rs`: scan de `docs/`, parse do traceability-map (RF-02/CA-RF02).
8. Tela OrchestrationList + atalhos base (RF-10).
9. NewOrchestration: input de texto + invocação `sdd init` (RF-03).
10. `stage.rs`: máquina de estados e navegação/bloqueio (RF-04/CA-RF08).
11. StageBoard: painel de progresso com estados.
12. `loader.rs`: spinner com threshold 200ms (RF-05/CA-RF05).
13. `runner.rs`: execução em thread + canal; captura exit/stdout/stderr (RF-06).
14. `markdown.rs`: parser do subset → `Text` (RF-07).
15. Viewer com scroll (RF-07).
16. `editor.rs`: ciclo suspender/restaurar `$EDITOR` (RF-08/R-07).
17. `write_atomic` em `persist.rs` e refatoração de `save_artifact`/`write_stage_content` (RNF-06/RT-06). **Pré-requisito do item 18; impacta funções compartilhadas com o harness — task com teste de não-regressão próprio.**
18. Checkpoint (Aprovar/Regenerar/Editar) chamando `sdd artifact save` (RF-08/RF-09/CA-RF09).
19. Refinement condicional (RF-14): prompt skip/run + `--state skipped`.
20. Painel de erro e recuperação (RF-13/CA-RF13); não-sobrescrita (RN-11).
21. Tela de ajuda contextual `?` (RF-12).
22. Tela de resumo final pós-Memory (RF-15).
23. Telemetria via `log_jsonl` em `.sdd/tui-events.jsonl`.
24. Testes unit (stage, orchestration, markdown, loader, write_atomic) + render `TestBackend` + inferência/não-regressão.
25. Validação manual da matriz de terminais (RNF-03) e medição `time sdd tui` (RNF-01).

## Rastreabilidade

| Campo | Valor |
|---|---|
| Orquestração | `criar-um-cli-poderoso-...-sdd-cli` |
| Artefato de origem | `02-prd.md` (PRD aprovado), que deriva de `01-idea.md` |
| Classificação de risco | `00-risk-classification.md` — risco: medium, fator: external_dependency |
| Código ancorado | `src/main.rs` (`run()` :442, enum `Command` :124, `run_stage` :1742, `write_stage_content` :1969, `save_artifact` :1140, `update_stage_state` :1061, `render_index` :985, `infer_artifact` :4034, `log_jsonl` :3937, `STAGE_FILES` :70, `ARTIFACT_HINTS` :84); `Cargo.toml`; `src/runtime/redaction.rs` |
| Próximo artefato | `04-tasks.md` |
