ganbatte-os 0.2.19 → 0.2.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.gos/README.md ADDED
@@ -0,0 +1,67 @@
1
+ # ganbatte-os
2
+
3
+ Framework operacional para workflows design-to-code, gestão de squads e sincronização de sprints com ClickUp. Orquestra agents, skills e squads ao longo do ciclo de desenvolvimento conectando Figma, ClickUp e IDEs de IA.
4
+
5
+ Este diretório (`.gos/`) é o **core** do framework. Tratar como read-only em projetos que consomem — mudanças aqui afetam todos os workspaces instalados.
6
+
7
+ ## Estrutura
8
+
9
+ | Diretório | Conteúdo |
10
+ |---|---|
11
+ | `agents/` | Definições de agents (ganbatte-os-master, architect, dev, sm, po, devops, squad-creator, ux-design-expert) |
12
+ | `skills/` | Skills executáveis (design-to-code, humanizer, slack-review, clickup, sprint-planner, react-doctor, etc.) |
13
+ | `scripts/` | Scripts Node zero-dep (hooks/, tools/, integrations/) |
14
+ | `libraries/` | Referências e catálogos (ai-writing-patterns, design tokens, style guides) |
15
+ | `rules/` | Regras de comportamento por contexto (frontend, backend, design) |
16
+ | `prompts/` | Prompts reusáveis |
17
+ | `playbooks/` | Workflows multi-step documentados |
18
+ | `squads/` | Templates de squad (design-delivery, design-squad, git-operations) |
19
+ | `integrations/` | Adaptadores por IDE/ferramenta (claude, cursor, codex, opencode, antigravity, gemini, kilo-code) |
20
+ | `manifests/` | Registros de instalação e runtime |
21
+ | `templates/` | Templates de projeto e artefato |
22
+ | `docs/` | **Documentação canônica dos pipelines** |
23
+
24
+ ## Scripts principais
25
+
26
+ Todos invocáveis via `npm run` a partir da raiz do repo (`e:\Github\Ganbatte\package.json`):
27
+
28
+ ```bash
29
+ npm run gos:doctor # Health check — valida estrutura, skills, agents
30
+ npm run gos:init # Inicializa framework em workspace novo
31
+ npm run gos:sync # Sincroniza registry e adaptadores IDE
32
+ npm run gos:deploy-storybook # Build + deploy Storybook no Vercel
33
+ ```
34
+
35
+ ## Pipelines documentados
36
+
37
+ | Pipeline | Doc |
38
+ |---|---|
39
+ | Notificações Slack (commit → fila → aprovação → envio) | [`docs/slack-notifications.md`](docs/slack-notifications.md) |
40
+ | Instalação do framework | [`docs/gos_installation_guide.md`](docs/gos_installation_guide.md) |
41
+ | Mapa de toolchain (IDEs, APIs, MCPs) | [`docs/toolchain-map.md`](docs/toolchain-map.md) |
42
+ | Compatibilidade entre IDEs | [`docs/ide-compatibility.md`](docs/ide-compatibility.md) |
43
+ | Curadoria de agents e skills | [`docs/curation.md`](docs/curation.md) |
44
+ | Distribuição pública | [`docs/plan-distribuicao-publica.md`](docs/plan-distribuicao-publica.md) |
45
+
46
+ ## Convenções
47
+
48
+ - **Git**: não inicializar repositório na raiz (`e:\Github\Ganbatte`). Git existe só dentro de cada package em `packages/`.
49
+ - **Branch**: commit + push direto na branch (sem PR). `dev → beta` auto-merge via GitHub Actions.
50
+ - **SSH**: usar o alias configurado em `.gos-local/ssh-identity.json`, nunca `git@github.com` direto.
51
+ - **Texto pt-BR**: sanitização determinística nos hooks (`text-sanitize.js`); `/humanizer` skill para textos longos.
52
+ - **Notificações**: hooks enfileiram em `.gos/slack-queue/` — nada vai ao Slack sem `/slack-review` aprovar.
53
+ - **Plan mode**: tarefas com >3 passos seguem protocolo RESEARCH → PLAN → APPROVE → EXECUTE.
54
+
55
+ ## Tiers de orquestração
56
+
57
+ 1. **ganbatte-os-master** — orquestrador master (routing, skills, squads, workflows)
58
+ 2. **workflow-select / composer / model-router** — scoring de workflow, sequenciamento de skill, roteamento de modelo
59
+ 3. **agent-teams** — paralelização multi-agent
60
+
61
+ ## Links rápidos
62
+
63
+ - CLI de fila Slack: [`scripts/tools/slack-queue.js`](scripts/tools/slack-queue.js)
64
+ - Sanitizador de texto: [`scripts/tools/text-sanitize.js`](scripts/tools/text-sanitize.js)
65
+ - Catálogo de padrões IA: [`libraries/content/ai-writing-patterns.md`](libraries/content/ai-writing-patterns.md)
66
+ - Hook pre-commit (validação TypeScript): [`scripts/hooks/pre-commit-validate.js`](scripts/hooks/pre-commit-validate.js)
67
+ - Hook post-commit (sync registry + Slack queue): [`../scripts/hooks/post-commit-sync.js`](../scripts/hooks/post-commit-sync.js)
@@ -0,0 +1,219 @@
1
+ # Slack Notifications — Pipeline com aprovação
2
+
3
+ Workflow de notificações Slack que disparam a partir de commits com `T-NNN` no message. Tudo passa por uma fila de aprovação — nada é enviado sem revisão humana.
4
+
5
+ ## Overview
6
+
7
+ ```
8
+ git commit -m "fix: ... Refs T-NNN"
9
+
10
+
11
+ .git/hooks/post-commit (shim bash, define GANBATTE_ROOT)
12
+
13
+
14
+ scripts/hooks/post-commit-sync.js
15
+ │ 1. extrai T-NNN do message
16
+ │ 2. lê registry.taskDetails + taskStatus
17
+ │ 3. constrói payload via slack-notify.buildTaskDonePayload
18
+ │ (sanitiza texto com text-sanitize.js)
19
+ │ 4. chama slack-notify.enqueueDraft
20
+
21
+ .gos/slack-queue/<timestamp>-<taskId>-<hash>.json (status: pending)
22
+
23
+ │ [commit termina — nada foi enviado]
24
+
25
+
26
+ /slack-review (Douglas roda quando quiser)
27
+ │ lista drafts, mostra preview, AskUserQuestion
28
+ │ ações: approve / edit / reject / skip
29
+
30
+ node slack-queue.js approve <id>
31
+ │ POST webhook Slack
32
+
33
+ Slack #cspo-tech ← notificação enriquecida e revisada
34
+
35
+
36
+ .gos/slack-queue/sent/<id>.json (histórico)
37
+ ```
38
+
39
+ ## Anatomia do payload
40
+
41
+ Campos extraídos de `data/sprints/registry.json`:
42
+
43
+ | Fonte | Campo | Uso no payload |
44
+ |---|---|---|
45
+ | `taskDetails[T-NNN].name` | nome da task | linha 2 do payload |
46
+ | `taskDetails[T-NNN].assignee` | responsável | linha meta (primeiro nome) |
47
+ | `taskDetails[T-NNN].sprint` | sprint | linha meta |
48
+ | `taskDetails[T-NNN].ac[]` | array de ACs | rodapé — "N ACs" |
49
+ | `taskStatus[T-NNN].points` | story points | linha meta |
50
+ | `taskStatus[T-NNN].dueDate` | prazo | linha meta (MM/DD) |
51
+ | `taskMap[T-NNN]` | ID ClickUp | link ClickUp no rodapé |
52
+
53
+ Formato renderizado:
54
+
55
+ ```
56
+ :white_check_mark: T-110 concluída — S07 · Douglas · 8 pts · due 04/17
57
+ Resolver drift TypeScript + instalar pre-commit hook
58
+ > Commit: 54d8fe6 — fix: drift resolvido
59
+ > 7 ACs · por Douglas Oliveira · <ClickUp>
60
+ ```
61
+
62
+ ## Pipeline de sanitização
63
+
64
+ `text-sanitize.js` aplica, em ordem, sobre qualquer texto que entra em payload:
65
+
66
+ 1. **Protege code fences** (```` ``` ````) e inline code (`` ` ``).
67
+ 2. **P17 emoji-bullets**: remove emojis (✅ ❌ ⚠ 🚀 ⭐ 🎯 💡 🔧 🎉 🔥 👍 👌) no início de linhas.
68
+ 3. **P14 em-dash entre palavras**: `word—word` → `word-word` (mantém travessão legítimo com espaços).
69
+ 4. **Acentos pt-BR**: dicionário determinístico — `concluida → concluída`, `atualizacao → atualização`, ~80 termos.
70
+ 5. **P07 vocabulário IA**: `aprimorar → melhorar`, `fomentar → estimular`, `crucial → importante`, ~25 termos com preservação de flexão (gerúndio, plural) e case.
71
+ 6. **Restaura code spans** intactos.
72
+
73
+ Estender dicionários: editar `AI_VOCAB` e `MISSING_ACCENTS` em `.gos/scripts/tools/text-sanitize.js`.
74
+
75
+ ## Fila de aprovação
76
+
77
+ **Localização**: `.gos/slack-queue/` (gitignored)
78
+
79
+ **Estados**:
80
+
81
+ | Estado | Diretório | Remoção |
82
+ |---|---|---|
83
+ | `pending` | `.gos/slack-queue/*.json` | manual (approve/reject) |
84
+ | `sent` | `.gos/slack-queue/sent/*.json` | manual, após confirmação |
85
+ | `rejected` | `.gos/slack-queue/rejected/*.json` | auditoria, manter histórico |
86
+
87
+ **TTL**: sem expiração automática. Limpar manualmente `sent/` e `rejected/` a cada sprint via `rm -rf .gos/slack-queue/{sent,rejected}`.
88
+
89
+ **Estrutura do draft**:
90
+
91
+ ```json
92
+ {
93
+ "id": "20260417T143100-T110-54d8fe6",
94
+ "createdAt": "2026-04-17T14:31:00Z",
95
+ "taskId": "T-110",
96
+ "commit": "54d8fe6",
97
+ "channel": "default",
98
+ "author": "Douglas Oliveira",
99
+ "payload": { "text": "..." },
100
+ "status": "pending"
101
+ }
102
+ ```
103
+
104
+ ## Comandos
105
+
106
+ ### slack-queue.js
107
+
108
+ ```bash
109
+ # Listar pendentes
110
+ node .gos/scripts/tools/slack-queue.js list
111
+ node .gos/scripts/tools/slack-queue.js list --json
112
+
113
+ # Mostrar um draft
114
+ node .gos/scripts/tools/slack-queue.js show <id>
115
+
116
+ # Editar inline (abre $EDITOR / VISUAL / notepad no Windows)
117
+ node .gos/scripts/tools/slack-queue.js edit <id>
118
+
119
+ # Aprovar e enviar
120
+ node .gos/scripts/tools/slack-queue.js approve <id>
121
+
122
+ # Rejeitar
123
+ node .gos/scripts/tools/slack-queue.js reject <id> --reason "duplicate"
124
+
125
+ # Aprovar todos pendentes (CI, depois de revisão visual)
126
+ node .gos/scripts/tools/slack-queue.js flush
127
+ ```
128
+
129
+ ### slack-notify.js
130
+
131
+ ```bash
132
+ # Enfileirar manualmente (default)
133
+ node .gos/scripts/tools/slack-notify.js task-done \
134
+ --task T-110 --commit 54d8fe6 --author "Douglas Oliveira" \
135
+ --registry data/sprints/registry.json
136
+
137
+ # Envio imediato sem fila (bypass revisão — usar só em CI)
138
+ node .gos/scripts/tools/slack-notify.js task-done \
139
+ --task T-110 --commit 54d8fe6 --author "Douglas Oliveira" \
140
+ --registry data/sprints/registry.json --send-now
141
+
142
+ # Dry-run (monta payload sem enviar nem enfileirar)
143
+ node .gos/scripts/tools/slack-notify.js task-done \
144
+ --task T-110 --commit 54d8fe6 --author "Douglas Oliveira" \
145
+ --registry data/sprints/registry.json --send-now --dry-run
146
+ ```
147
+
148
+ ### text-sanitize.js
149
+
150
+ ```bash
151
+ # CLI
152
+ node .gos/scripts/tools/text-sanitize.js --text "Task concluida aprimorando"
153
+ node .gos/scripts/tools/text-sanitize.js --file path/to/content.md --json
154
+ echo "texto sujo" | node .gos/scripts/tools/text-sanitize.js --stdin
155
+
156
+ # Como módulo
157
+ const { sanitize } = require('./.gos/scripts/tools/text-sanitize.js')
158
+ const { text, changes } = sanitize(input)
159
+ ```
160
+
161
+ ## Skill `/slack-review`
162
+
163
+ Ver `.gos/skills/slack-review/SKILL.md`. Wrapper interativo do `slack-queue.js list|show|edit|approve|reject` com preview renderizado e `AskUserQuestion` para cada ação.
164
+
165
+ ## Como pular aprovação
166
+
167
+ **CI / automação** onde revisão humana não faz sentido:
168
+
169
+ ```bash
170
+ node .gos/scripts/tools/slack-notify.js <cmd> --send-now
171
+ ```
172
+
173
+ **Flush programado** (ex: cron de fim de dia):
174
+
175
+ ```bash
176
+ node .gos/scripts/tools/slack-queue.js flush
177
+ ```
178
+
179
+ Usar com cuidado — fila existe justamente para evitar envio acidental.
180
+
181
+ ## Troubleshooting
182
+
183
+ ### Commit rodou mas fila não tem drafts
184
+
185
+ - Verificar que o commit message contém `T-NNN` (regex `\bT-\d{3}\b`).
186
+ - Confirmar hook instalado: `ls packages/fractus/.git/hooks/post-commit`.
187
+ - Confirmar hook aponta para `$GANBATTE_ROOT/scripts/hooks/post-commit-sync.js` e exporta `GANBATTE_ROOT`.
188
+ - Rodar manual: `cd packages/fractus && GANBATTE_ROOT=$(git rev-parse --show-superproject-working-tree || pwd) node ../../scripts/hooks/post-commit-sync.js`.
189
+
190
+ ### Approve falha com "SLACK_WEBHOOK_URL not set"
191
+
192
+ - Arquivo `.env` na raiz (`e:\Github\Ganbatte\.env`) contém `SLACK_WEBHOOK_URL=...`?
193
+ - `slack-queue.js` carrega `.env` automaticamente via `loadRootEnv()` — se variável está no `.env` mas não é lida, conferir formatação (sem aspas extras, `KEY=VALUE` puro).
194
+
195
+ ### Draft stuck em pending após approve
196
+
197
+ - Abrir o JSON em `.gos/slack-queue/sent/<id>.json` — se existe, o envio OK mas `unlink` do original falhou (permission issue Windows). Remover manualmente.
198
+ - Se não existe, o POST webhook falhou — ver output do comando (status HTTP + body).
199
+
200
+ ### Edição travada
201
+
202
+ - `edit` abre `$VISUAL` → `$EDITOR` → `notepad` (Windows) / `vi` (Unix). Se o editor não fechar, o comando fica bloqueado.
203
+ - Alternativa: editar o JSON direto em `.gos/slack-queue/<id>.json` e rodar approve em seguida.
204
+
205
+ ### Texto sanitizado incorretamente
206
+
207
+ - Ver `changes[]` do sanitize: `node .gos/scripts/tools/text-sanitize.js --text "..." --json`.
208
+ - Se uma substituição está errada, remover do dicionário em `text-sanitize.js:AI_VOCAB` / `MISSING_ACCENTS`.
209
+ - Code spans (`` ` `` e ```` ``` ````) são preservados — se precisar isolar um termo, envolver em backticks.
210
+
211
+ ## Referências
212
+
213
+ - [`text-sanitize.js`](../scripts/tools/text-sanitize.js) — sanitizador
214
+ - [`slack-queue.js`](../scripts/tools/slack-queue.js) — fila
215
+ - [`slack-notify.js`](../scripts/tools/slack-notify.js) — payload + envio
216
+ - [`post-commit-sync.js`](../../scripts/hooks/post-commit-sync.js) — hook
217
+ - [`ai-writing-patterns.md`](../libraries/content/ai-writing-patterns.md) — catálogo base
218
+ - [`humanizer`](../skills/humanizer/SKILL.md) — skill de humanização criativa
219
+ - [`slack-review`](../skills/slack-review/SKILL.md) — skill de aprovação interativa
@@ -0,0 +1,405 @@
1
+ # T-084 — API Diagnóstico do Negócio (link único por projeto)
2
+
3
+ **Sprint:** S07 · **Due:** 2026-04-19 · **Prioridade:** URGENT · **Pts:** 8
4
+ **ClickUp:** https://app.clickup.com/t/86agn0211
5
+ **Assignee:** Douglas Oliveira
6
+
7
+ ---
8
+
9
+ ## 🚫 [BLOCKED-PRD 2026-04-17] — Redesign obrigatório
10
+
11
+ **Status:** BLOQUEADA. Não executar como está.
12
+
13
+ **Motivo:** O PRD vigente ([prd.md](../../../packages/fractus/docs/prd.md) §Decisões bloqueantes) invalida a premissa central desta task. A coleta de respostas passou a ser **por destinatário (link individual por Negócio pré-cadastrado pelo gestor)** — **não existe mais link único por projeto**.
14
+
15
+ Além disso:
16
+ - Status "selecionado" foi removido — novo enum: `pré-selecionado → cadastrado → ativo → desistente/concluinte` (ver M2 em [impacto-tasks-clickup.md](../../../packages/fractus/docs/regras-de-negocio/impacto-tasks-clickup.md)).
17
+ - Transição automática `pré-selecionado → cadastrado` ao responder diagnóstico.
18
+ - Nomenclatura: `Programa → Projeto` (já no plano), `Patrocinador → Financiador` (M4).
19
+
20
+ **Fluxo novo:** gestor pré-cadastra Negócio no dashboard → sistema emite link individual para o líder → líder responde → Negócio vira `cadastrado` + Participante-líder auto-criado. Sem fluxo público com link coletivo.
21
+
22
+ **Ações pendentes antes de desbloquear:**
23
+ 1. Reescrever contexto, contratos Zod e RPC para lookup por `token_diagnostico` do Negócio (não mais por `link_diagnostico` do Projeto).
24
+ 2. Trocar `status = 'selecionado'` por `status = 'cadastrado'` em toda a RPC.
25
+ 3. Remover endpoint de criação pública de Negócio — o Negócio já existe pré-cadastrado.
26
+ 4. Atualizar ACs (AC2, AC4) e exemplos.
27
+
28
+ **Referências:**
29
+ - [packages/fractus/docs/prd.md](../../../packages/fractus/docs/prd.md) — §Decisões bloqueantes, §Tipos de status
30
+ - [packages/fractus/docs/regras-de-negocio/impacto-tasks-clickup.md](../../../packages/fractus/docs/regras-de-negocio/impacto-tasks-clickup.md) — M1, M2, M3, M4
31
+
32
+ ---
33
+
34
+ ## ⚠️ Pré-requisito bloqueante
35
+
36
+ **T-082 DEVE estar fechada antes deste plano.** A cadeia depende da FK `participantes.negocio_id NOT NULL + ON DELETE CASCADE`. Hoje (2026-04-17) a migration `20260412000000_prd_alignment.sql` deixou a coluna **nullable** com `ON DELETE SET NULL`.
37
+
38
+ **Se T-082 não estiver complete:** NÃO começar este plano. Avisar Douglas para fechar T-082 primeiro (é task do próprio Douglas, em andamento).
39
+
40
+ ## Contexto
41
+
42
+ Fluxo de diagnóstico do negócio (lead inicial do CRM) — primeiro contato formal:
43
+
44
+ 1. Gestor configura um projeto e obtém `link_diagnostico` (UUID/slug único por projeto).
45
+ 2. Link é compartilhado com leads (via email marketing, site, QR).
46
+ 3. Lead acessa `/diagnostico/negocio/[linkId]` → preenche formulário → submete.
47
+ 4. Backend:
48
+ - Cria registro em `negocios` (nome do negócio, líder, telefone).
49
+ - Cria N participantes pré-selecionados (líder + equipe).
50
+ - Marca `negocios.status = 'selecionado'` + `diagnostico_respondido = true`.
51
+ - Aplica rate limit por IP.
52
+ - Retorna link individual para cada participante completar diagnóstico (T-085).
53
+
54
+ **Regras PRD (literais):**
55
+ - BR-PRT-007: Participante pertence a exatamente 1 negócio (`negocio_id` FK obrigatória).
56
+ - BR-NEG-005: Transição `pre_selecionado → selecionado` automática ao responder diagnóstico.
57
+ - BR-DIG-003: Validação de e-mail — apenas e-mails únicos no payload.
58
+ - BR-PRJ-XXX: `link_diagnostico` único por projeto.
59
+
60
+ **Decisão arquitetural:** Next 16 App Router + Server Actions (não API routes). Repo não tem `src/app/api/` ativo. Lookup público via rota `app/diagnostico/negocio/[linkId]/page.tsx` que chama a action.
61
+
62
+ ## Arquivos afetados
63
+
64
+ | Arquivo | Ação |
65
+ |---|---|
66
+ | `packages/fractus/src/lib/validations/deal.ts` | [EDITAR] schema novo (lider_email, projeto_id, diagnostico_respondido) |
67
+ | `packages/fractus/src/lib/validations/participant.ts` | [EDITAR] `negocio_id` obrigatório, status enum |
68
+ | `packages/fractus/src/lib/validations/diagnostico.ts` | [NOVO] `submitNegocioSchema` |
69
+ | `packages/fractus/src/app/actions/diagnostico/submit-negocio.ts` | [NOVO] server action |
70
+ | `packages/fractus/src/app/actions/diagnostico/submit-negocio.test.ts` | [NOVO] testes de integração |
71
+ | `packages/fractus/src/app/diagnostico/negocio/[linkId]/page.tsx` | [NOVO] página pública |
72
+ | `packages/fractus/src/app/diagnostico/negocio/[linkId]/form-client.tsx` | [NOVO] componente cliente |
73
+ | `packages/fractus/supabase/migrations/2026041X_link_diagnostico_unique.sql` | [NOVO se necessário] UNIQUE em `projetos.link_diagnostico` |
74
+
75
+ ## Passo a passo
76
+
77
+ ### 0. Validar pré-requisito
78
+
79
+ ```bash
80
+ cd packages/fractus
81
+ psql -c "\d participantes" | grep negocio_id
82
+ # Esperado: negocio_id uuid NOT NULL, FK CASCADE
83
+ # Se aparecer "NULL" ou "SET NULL" → T-082 não está fechada. PARAR.
84
+ ```
85
+
86
+ ### 1. Atualizar schemas Zod
87
+
88
+ `src/lib/validations/deal.ts` (substituir CRM legado):
89
+
90
+ ```ts
91
+ import { z } from 'zod'
92
+
93
+ export const negocioSchema = z.object({
94
+ nome: z.string().min(2, "Nome do negócio obrigatório"),
95
+ lider_nome: z.string().min(2),
96
+ lider_email: z.string().email(),
97
+ lider_telefone: z.string().regex(/^\+?[\d\s()-]{10,}$/, "Telefone inválido"),
98
+ projeto_id: z.string().uuid(),
99
+ })
100
+
101
+ export type NegocioInput = z.infer<typeof negocioSchema>
102
+ ```
103
+
104
+ `src/lib/validations/participant.ts`:
105
+
106
+ ```ts
107
+ import { z } from 'zod'
108
+
109
+ export const statusParticipanteEnum = z.enum([
110
+ 'pre_selecionado', 'selecionado', 'ativo',
111
+ 'desistente', 'concluinte', 'inativado',
112
+ ])
113
+
114
+ export const participanteSchema = z.object({
115
+ nome: z.string().min(2),
116
+ email: z.string().email(),
117
+ telefone: z.string().optional(),
118
+ negocio_id: z.string().uuid(), // FK OBRIGATÓRIA (BR-PRT-007)
119
+ status: statusParticipanteEnum.default('pre_selecionado'),
120
+ })
121
+
122
+ export type ParticipanteInput = z.infer<typeof participanteSchema>
123
+ ```
124
+
125
+ `src/lib/validations/diagnostico.ts` [NOVO]:
126
+
127
+ ```ts
128
+ import { z } from 'zod'
129
+
130
+ export const submitNegocioSchema = z.object({
131
+ linkId: z.string().min(1),
132
+ negocio: z.object({
133
+ nome: z.string().min(2),
134
+ lider_nome: z.string().min(2),
135
+ lider_email: z.string().email(),
136
+ lider_telefone: z.string().min(10),
137
+ }),
138
+ participantes: z.array(z.object({
139
+ nome: z.string().min(2),
140
+ email: z.string().email(),
141
+ telefone: z.string().optional(),
142
+ })).min(1, "Ao menos o líder como participante"),
143
+ }).refine((data) => {
144
+ const emails = data.participantes.map(p => p.email.toLowerCase())
145
+ return new Set(emails).size === emails.length
146
+ }, { message: "E-mails duplicados não são permitidos", path: ["participantes"] })
147
+
148
+ export type SubmitNegocioInput = z.infer<typeof submitNegocioSchema>
149
+ ```
150
+
151
+ ### 2. Server Action
152
+
153
+ `src/app/actions/diagnostico/submit-negocio.ts` [NOVO]:
154
+
155
+ ```ts
156
+ 'use server'
157
+
158
+ import { headers } from 'next/headers'
159
+ import { revalidatePath } from 'next/cache'
160
+ import { createClient } from '@/lib/supabase/server'
161
+ import { checkRateLimit } from '@/lib/rate-limit'
162
+ import { submitNegocioSchema } from '@/lib/validations/diagnostico'
163
+
164
+ type ActionResult =
165
+ | { ok: true; negocioId: string; participantTokens: string[] }
166
+ | { ok: false; code: 'VALIDATION' | 'RATE_LIMIT' | 'NOT_FOUND' | 'DUPLICATE' | 'ERROR'; message: string; fieldErrors?: Record<string, string[]> }
167
+
168
+ export async function submitDiagnosticoNegocio(input: unknown): Promise<ActionResult> {
169
+ // 1. Rate limit por IP
170
+ const hdrs = await headers()
171
+ const ip = hdrs.get('x-forwarded-for')?.split(',')[0]?.trim() ?? 'unknown'
172
+ const rl = checkRateLimit(ip)
173
+ if (!rl.allowed) {
174
+ return { ok: false, code: 'RATE_LIMIT', message: `Tente novamente em ${rl.retryAfterSeconds}s` }
175
+ }
176
+
177
+ // 2. Validação Zod
178
+ const parsed = submitNegocioSchema.safeParse(input)
179
+ if (!parsed.success) {
180
+ return { ok: false, code: 'VALIDATION', message: 'Dados inválidos', fieldErrors: parsed.error.flatten().fieldErrors }
181
+ }
182
+
183
+ const supabase = await createClient()
184
+ const { linkId, negocio, participantes } = parsed.data
185
+
186
+ // 3. Lookup projeto pelo link único
187
+ const { data: projeto, error: projErr } = await supabase
188
+ .from('projetos')
189
+ .select('id, ativo')
190
+ .eq('link_diagnostico', linkId)
191
+ .single()
192
+
193
+ if (projErr || !projeto) {
194
+ return { ok: false, code: 'NOT_FOUND', message: 'Link inválido ou expirado' }
195
+ }
196
+
197
+ // 4. RPC transacional (recomendado) OU insert sequencial
198
+ const { data, error } = await supabase.rpc('submit_diagnostico_negocio', {
199
+ p_projeto_id: projeto.id,
200
+ p_negocio: negocio,
201
+ p_participantes: participantes,
202
+ })
203
+
204
+ if (error) {
205
+ // Trata violação de UNIQUE em email
206
+ if (error.code === '23505' && error.message.includes('email')) {
207
+ return { ok: false, code: 'DUPLICATE', message: 'E-mail já cadastrado' }
208
+ }
209
+ return { ok: false, code: 'ERROR', message: error.message }
210
+ }
211
+
212
+ // 5. Revalidate dashboard
213
+ revalidatePath(`/dashboard/projetos/${projeto.id}`)
214
+
215
+ return { ok: true, negocioId: data.negocio_id, participantTokens: data.participant_tokens }
216
+ }
217
+ ```
218
+
219
+ ### 3. RPC em Postgres (atômica)
220
+
221
+ Criar migration `supabase/migrations/2026041X_rpc_submit_diagnostico_negocio.sql`:
222
+
223
+ ```sql
224
+ CREATE OR REPLACE FUNCTION submit_diagnostico_negocio(
225
+ p_projeto_id uuid,
226
+ p_negocio jsonb,
227
+ p_participantes jsonb
228
+ ) RETURNS jsonb
229
+ LANGUAGE plpgsql SECURITY DEFINER AS $$
230
+ DECLARE
231
+ v_negocio_id uuid;
232
+ v_tokens text[] := ARRAY[]::text[];
233
+ v_p jsonb;
234
+ v_token text;
235
+ BEGIN
236
+ INSERT INTO negocios (nome, lider_nome, lider_email, lider_telefone, projeto_id, status, diagnostico_respondido)
237
+ VALUES (
238
+ p_negocio->>'nome',
239
+ p_negocio->>'lider_nome',
240
+ p_negocio->>'lider_email',
241
+ p_negocio->>'lider_telefone',
242
+ p_projeto_id,
243
+ 'selecionado',
244
+ true
245
+ )
246
+ RETURNING id INTO v_negocio_id;
247
+
248
+ FOR v_p IN SELECT * FROM jsonb_array_elements(p_participantes) LOOP
249
+ v_token := encode(gen_random_bytes(16), 'hex');
250
+ INSERT INTO participantes (nome, email, telefone, negocio_id, status, token_diagnostico)
251
+ VALUES (
252
+ v_p->>'nome',
253
+ v_p->>'email',
254
+ v_p->>'telefone',
255
+ v_negocio_id,
256
+ 'pre_selecionado',
257
+ v_token
258
+ );
259
+ v_tokens := array_append(v_tokens, v_token);
260
+ END LOOP;
261
+
262
+ RETURN jsonb_build_object('negocio_id', v_negocio_id, 'participant_tokens', v_tokens);
263
+ END;
264
+ $$;
265
+ ```
266
+
267
+ > **Troubleshooting:** se `token_diagnostico` não existir em `participantes`, adicionar via migration: `ALTER TABLE participantes ADD COLUMN token_diagnostico text UNIQUE;`
268
+
269
+ ### 4. Garantir UNIQUE em `link_diagnostico` (AC4)
270
+
271
+ ```sql
272
+ -- Se não existir:
273
+ ALTER TABLE projetos ADD COLUMN IF NOT EXISTS link_diagnostico text UNIQUE;
274
+ UPDATE projetos SET link_diagnostico = encode(gen_random_bytes(8), 'hex') WHERE link_diagnostico IS NULL;
275
+ ```
276
+
277
+ ### 5. Página pública
278
+
279
+ `src/app/diagnostico/negocio/[linkId]/page.tsx`:
280
+
281
+ ```tsx
282
+ import { notFound } from 'next/navigation'
283
+ import { createClient } from '@/lib/supabase/server'
284
+ import { FormClient } from './form-client'
285
+
286
+ export default async function Page({ params }: { params: Promise<{ linkId: string }> }) {
287
+ const { linkId } = await params
288
+ const supabase = await createClient()
289
+ const { data: projeto } = await supabase
290
+ .from('projetos')
291
+ .select('id, nome, ativo')
292
+ .eq('link_diagnostico', linkId)
293
+ .single()
294
+
295
+ if (!projeto || !projeto.ativo) notFound()
296
+
297
+ return <FormClient linkId={linkId} projetoNome={projeto.nome} />
298
+ }
299
+ ```
300
+
301
+ `form-client.tsx` — formulário RHF + Zod que chama `submitDiagnosticoNegocio`.
302
+
303
+ ### 6. Testes
304
+
305
+ `submit-negocio.test.ts` com Vitest: mockar Supabase client, validar:
306
+ - payload válido → `ok: true`
307
+ - email duplicado → `ok: false, code: 'DUPLICATE'`
308
+ - link inexistente → `ok: false, code: 'NOT_FOUND'`
309
+ - rate limit 61ª request → `ok: false, code: 'RATE_LIMIT'`
310
+
311
+ ## Critérios de aceite (literais)
312
+
313
+ - **AC1:** Participantes criados automaticamente a partir do payload.
314
+ - **AC2:** Negócio muda para `selecionado` (e `diagnostico_respondido = true`).
315
+ - **AC3:** E-mails duplicados rejeitados (return `DUPLICATE`).
316
+ - **AC4:** Link único por projeto (`UNIQUE` constraint em `projetos.link_diagnostico`).
317
+
318
+ ## Verificação end-to-end
319
+
320
+ - [ ] `pnpm typecheck` exit 0
321
+ - [ ] `pnpm lint` OK
322
+ - [ ] `pnpm test src/app/actions/diagnostico` verde
323
+ - [ ] `curl POST` com payload válido → 200 `{ok:true}`
324
+ - [ ] `curl POST` com email duplicado → `{ok:false, code:'DUPLICATE'}`
325
+ - [ ] `curl POST` com linkId inválido → `{ok:false, code:'NOT_FOUND'}`
326
+ - [ ] Após submit, verificar no Supabase: negócio `selecionado`, N participantes `pre_selecionado`, tokens únicos
327
+ - [ ] `pnpm dev` + navegar `/diagnostico/negocio/<linkId>` → formulário carrega
328
+
329
+ ## Entrega ao Douglas (**NÃO commitar, NÃO dar push**)
330
+
331
+ > ⚠️ Dev NÃO commita. Douglas valida e commita.
332
+
333
+ Fluxo do dev:
334
+ 1. Working tree sujo + migrations novas **não aplicadas em produção** (só local).
335
+ 2. Colar output de `git status`, `git diff --stat`, resultado dos curls.
336
+ 3. Avisar Douglas.
337
+
338
+ ### Mensagem de commit sugerida
339
+
340
+ ```
341
+ feat(diagnostico): API submit diagnóstico negócio (link único por projeto)
342
+
343
+ Refs T-084
344
+
345
+ - AC1: participantes auto-criados via RPC submit_diagnostico_negocio
346
+ - AC2: negócio → selecionado + diagnostico_respondido=true
347
+ - AC3: emails duplicados rejeitados (Zod + UNIQUE constraint)
348
+ - AC4: projetos.link_diagnostico UNIQUE
349
+ - Rate limit 60/60s por IP
350
+ - Testes de integração com mock Supabase
351
+ ```
352
+
353
+ ## Rollback
354
+
355
+ ```bash
356
+ git restore packages/fractus/src/lib/validations/
357
+ rm -rf packages/fractus/src/app/actions/diagnostico
358
+ rm -rf packages/fractus/src/app/diagnostico
359
+ rm packages/fractus/supabase/migrations/2026041X_*.sql
360
+ # Reverter RPC no banco:
361
+ psql -c "DROP FUNCTION IF EXISTS submit_diagnostico_negocio;"
362
+ ```
363
+
364
+ ## Dependências e bloqueios
365
+
366
+ - **Upstream bloqueante:** T-082 (FK NOT NULL + CASCADE) — SE NÃO FECHADA, NÃO EXECUTAR.
367
+ - **Upstream:** T-110 (tsc exit 0) — validação Zod exige tipos corretos.
368
+ - **Downstream:** T-085 (usa token gerado aqui), T-086.
369
+ - **Risco 1:** RPC requer extension `pgcrypto` para `gen_random_bytes` — já vem no Supabase.
370
+ - **Risco 2:** se `projetos.link_diagnostico` já existe com valor duplicado em dados de teste → UPDATE antes do ADD CONSTRAINT UNIQUE.
371
+
372
+ ## Checklist de entrega
373
+
374
+ - [ ] T-082 confirmada fechada (psql check)
375
+ - [ ] AC1-AC4 validados
376
+ - [ ] `pnpm typecheck` + `pnpm lint` + `pnpm build` OK
377
+ - [ ] Testes unitários verdes
378
+ - [ ] Curl tests dos 4 cenários
379
+ - [ ] `git status` + `git diff --stat` colados
380
+ - [ ] **NÃO fiz commit nem push**
381
+ - [ ] Douglas notificado
382
+
383
+ ## Checklist do Douglas
384
+
385
+ - [ ] Revisar diff
386
+ - [ ] Aplicar migrations em staging
387
+ - [ ] Smoke test manual com link real
388
+ - [ ] Commit+push
389
+ - [ ] ClickUp T-084 → `complete`
390
+
391
+ ## Evidência de entrega
392
+
393
+ ```
394
+ # git status + diff --stat
395
+ (output)
396
+
397
+ # pnpm test (última linha)
398
+ (output)
399
+
400
+ # curl válido (status + body)
401
+ (output)
402
+
403
+ # curl duplicado (status + body)
404
+ (output)
405
+ ```