ganbatte-os 0.2.20 → 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 +67 -0
- package/.gos/docs/slack-notifications.md +219 -0
- package/.gos/plans/tasks/T-084-api-diagnostico-negocio.md +405 -0
- package/.gos/plans/tasks/T-085-api-diagnostico-individual.md +291 -0
- package/.gos/plans/tasks/T-086-api-ativacao-manual-batch.md +287 -0
- package/.gos/plans/tasks/T-104-hook-storybook-branch-context.md +218 -0
- package/.gos/plans/tasks/T-110-drift-typescript-pre-commit-hook.md +355 -0
- package/.gos/scripts/tools/text-sanitize.js +236 -0
- package/.gos/skills/registry.json +2 -1
- package/.gos/skills/slack-review/SKILL.md +91 -0
- package/.gos/skills/weekly-update/CHANGELOG.md +84 -0
- package/.gos/tests/hooks/storybook-branch-check.test.sh +42 -0
- package/package.json +1 -1
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
|
+
```
|