spec-first-copilot 0.5.0-beta.1 → 0.5.0-beta.3

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.
Files changed (26) hide show
  1. package/package.json +1 -1
  2. package/templates/.github/adapters/confluence.md +273 -0
  3. package/templates/.github/adapters/errors.md +234 -0
  4. package/templates/.github/adapters/filesystem.md +353 -0
  5. package/templates/.github/adapters/interface.md +301 -0
  6. package/templates/.github/adapters/naming.md +241 -0
  7. package/templates/.github/adapters/registry.md +244 -0
  8. package/templates/.github/agents/backend-coder.md +215 -215
  9. package/templates/.github/agents/db-coder.md +165 -165
  10. package/templates/.github/agents/frontend-coder.md +222 -222
  11. package/templates/.github/agents/infra-coder.md +341 -341
  12. package/templates/.github/agents/reviewer.md +99 -99
  13. package/templates/.github/agents/security-reviewer.md +153 -153
  14. package/templates/.github/copilot-instructions.md +219 -218
  15. package/templates/.github/instructions/docs.instructions.md +123 -123
  16. package/templates/.github/instructions/sensitive-files.instructions.md +32 -32
  17. package/templates/.github/skills/sf-design/SKILL.md +209 -209
  18. package/templates/.github/skills/sf-dev/SKILL.md +354 -354
  19. package/templates/.github/skills/sf-feature/SKILL.md +130 -130
  20. package/templates/.github/skills/sf-plan/SKILL.md +180 -180
  21. package/templates/.github/skills/sf-setup-projeto/SKILL.md +123 -123
  22. package/templates/.github/templates/feature/PRD.template.md +256 -256
  23. package/templates/.github/templates/feature/Progresso.template.md +136 -136
  24. package/templates/.github/templates/specs/tasks.template.md +61 -61
  25. package/templates/sfw.config.yml.example +131 -0
  26. /package/templates/{docs/specs → specs}/.gitkeep +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spec-first-copilot",
3
- "version": "0.5.0-beta.1",
3
+ "version": "0.5.0-beta.3",
4
4
  "description": "Spec-first workflow kit for GitHub Copilot — AI-driven development with specs, not guesswork",
5
5
  "bin": {
6
6
  "spec-first-copilot": "bin/cli.js"
@@ -0,0 +1,273 @@
1
+ # ConfluenceAdapter — Runbook
2
+
3
+ > Implementação do `SourceAdapter` para Atlassian Confluence via MCP
4
+ > `sooperset/mcp-atlassian` (validado end-to-end em 2026-04-11 —
5
+ > ver `planodetarefas.md §9.9` e napkin "Hard findings do teste E2E").
6
+ >
7
+ > Este arquivo é o **runbook que o agent segue** quando uma skill precisa
8
+ > de uma operação de Confluence. A interface conceitual está em
9
+ > `.github/adapters/interface.md`.
10
+
11
+ ---
12
+
13
+ ## Meta
14
+
15
+ | Campo | Valor |
16
+ |-------|-------|
17
+ | `name` | `confluence` |
18
+ | MCP provider | `sooperset/mcp-atlassian` via uvx |
19
+ | Credenciais | `.mcp.json` gitignored (hardcoded, `${VAR}` **não funciona**) |
20
+ | Transporte | MCP — tools prefixadas `mcp__atlassian__confluence_*` |
21
+ | Conflict detection | **Client-side obrigatório** (MCP não aceita `expectedVersion`) |
22
+ | Suporta attachments? | ✅ leitura + escrita |
23
+ | Suporta busca? | Fora da interface (skills podem chamar `confluence_search` direto se precisar) |
24
+
25
+ ---
26
+
27
+ ## Config
28
+
29
+ Passada via `sfw.config.yml > input.config` ou `output.targets[].config`.
30
+
31
+ | Campo | Obrig. | Tipo | Descrição |
32
+ |-------|:---:|------|-----------|
33
+ | `space_key` | ✅ | string | Ex: `"ST"`. Usado em todas as operações que exigem space. |
34
+ | `parent_page_id` | ✅ | string | ID numérico (como string) da página raiz. Ex: `"360668"`. |
35
+ | `recursive` | — | boolean | Default `true`. Se `false`, `listChildren` não desce. |
36
+ | `include_attachments` | — | boolean | Default `true`. Se `false`, `fetchAttachments` retorna `[]`. |
37
+
38
+ ### `validateConfig`
39
+
40
+ Lança `ValidationError` se:
41
+ - `space_key` ausente, vazio ou não-string
42
+ - `parent_page_id` ausente, vazio ou não parseável como inteiro
43
+ - `recursive` presente mas não-boolean
44
+ - `include_attachments` presente mas não-boolean
45
+
46
+ Não valida que `space_key` existe no Confluence — isso acontece no primeiro
47
+ `listChildren` e vira `AuthError` ou `NotFoundError`.
48
+
49
+ ---
50
+
51
+ ## Mapeamento da interface → MCP
52
+
53
+ ### `listChildren(containerId) → Item[]`
54
+
55
+ **MCP**: `mcp__atlassian__confluence_get_page_children`
56
+
57
+ **Passos**:
58
+ 1. Chamar tool com `parent_id: containerId`, `limit: 100`, `expand: "version"`
59
+ 2. Para cada filho retornado, montar `Item`:
60
+ - `id` = `page.id` (string)
61
+ - `title` = `page.title`
62
+ - `version` = `String(page.version.number)`
63
+ - `type` = `"page"` (Confluence só tem páginas — folders são páginas vazias convencionadas)
64
+ - `hasChildren` = `page.children_count > 0` (se vier na resposta; senão `true` por default — custo de 1 chamada extra é baixo)
65
+ 3. Se retorno paginado (cursor), iterar até esgotar
66
+ 4. Retornar array
67
+
68
+ **Erros**:
69
+ - `404` → `NotFoundError { kind: "container", itemId: containerId }`
70
+ - `401/403` → `AuthError`
71
+ - `5xx`/timeout → `TransportError { retryable: true }`
72
+
73
+ ---
74
+
75
+ ### `fetchContent(itemId) → { content, metadata }`
76
+
77
+ **MCP**: `mcp__atlassian__confluence_get_page` com `convert_to_markdown: true`
78
+
79
+ **Passos**:
80
+ 1. Chamar tool com `page_id: itemId`, `convert_to_markdown: true`, `include_metadata: true`
81
+ 2. Extrair `content` do campo markdown retornado
82
+ 3. Montar `metadata`:
83
+ - `version` = `page.version.number` (inteiro)
84
+ - `space_key`, `parent_id`, `created_at`, `updated_at`, `author`
85
+ - `labels` = array das labels da página (usado pelo caller pra checar `sfw-approved`)
86
+ 4. Retornar `{ content, metadata }`
87
+
88
+ **Notas importantes**:
89
+ - Markdown roundtrip **normaliza** (`#` vira `===` no H1, `-` vira `*` em listas).
90
+ `version.number` é a fonte de verdade pra drift, **não** diff de bytes.
91
+ - `labels` vêm via `confluence_get_labels` **separadamente** se não vierem
92
+ embutidos na resposta do `get_page`. Chamar só se o caller pediu (lazy).
93
+
94
+ **Erros**: mesmas regras do `listChildren`.
95
+
96
+ ---
97
+
98
+ ### `fetchAttachments(itemId) → Attachment[]`
99
+
100
+ **MCP**:
101
+ - `mcp__atlassian__confluence_get_attachments` (lista)
102
+ - `mcp__atlassian__confluence_download_attachment` (bytes, lazy)
103
+
104
+ **Passos**:
105
+ 1. Se `config.include_attachments === false`, retornar `[]` sem chamar MCP.
106
+ 2. Chamar `get_attachments` com `page_id: itemId`
107
+ 3. Para cada item retornado, montar `Attachment`:
108
+ - `filename` = `attachment.title`
109
+ - `size` = `attachment.size` em bytes
110
+ - `mimeType` = `attachment.mime_type`
111
+ - `download()` = closure que chama `confluence_download_attachment` com
112
+ `page_id: itemId, filename: attachment.title` e retorna `Buffer`
113
+ 4. Retornar array
114
+
115
+ **Lazy**: `download()` só dispara quando o caller chama. Listar attachments é
116
+ barato, baixar PDFs pode não ser.
117
+
118
+ ---
119
+
120
+ ### `getVersion(itemId) → string`
121
+
122
+ **MCP**: `mcp__atlassian__confluence_get_page` com `convert_to_markdown: false`
123
+
124
+ **Passos**:
125
+ 1. Chamar tool com `page_id: itemId`, `include_metadata: true`, `convert_to_markdown: false`
126
+ (evita o custo de converter body quando só queremos version)
127
+ 2. Retornar `String(page.version.number)`
128
+
129
+ **Nota**: se o MCP oferecer um `confluence_get_page_history` mais barato no
130
+ futuro, trocar. Hoje `get_page` é o caminho mais direto. Custo: ~1 request HTTP.
131
+
132
+ ---
133
+
134
+ ### `create(parentId, title, content) → { itemId, version }`
135
+
136
+ **MCP**: `mcp__atlassian__confluence_create_page`
137
+
138
+ **Passos**:
139
+ 1. Chamar tool com:
140
+ - `space_key: config.space_key`
141
+ - `parent_id: parentId`
142
+ - `title: title` (já vem formatado pelo naming engine — ex: `"app_barbearia - TRD"`)
143
+ - `content: content` (markdown — MCP converte pra storage format)
144
+ - `content_format: "markdown"`
145
+ 2. Retornar:
146
+ - `itemId` = `response.page.id`
147
+ - `version` = `String(response.page.version.number)` (tipicamente `"1"`)
148
+
149
+ **Erros específicos**:
150
+ - `BadRequestException: A page already exists with the same TITLE in this space` →
151
+ `ConflictError { kind: "duplicate_title", itemId: null }`
152
+ (naming templating com `{scope}` no título deveria impedir isso — se bateu,
153
+ dois scopes têm o mesmo nome, o user precisa renomear no Input)
154
+ - `parent_id` inválido → `NotFoundError { kind: "container", itemId: parentId }`
155
+
156
+ ---
157
+
158
+ ### `update(itemId, content, expectedVersion) → { version, ok, conflict? }`
159
+
160
+ **MCP**: `mcp__atlassian__confluence_update_page` (+ `get_page` pra version check)
161
+
162
+ > **Atenção crítica**: o MCP sooperset **não** aceita `expectedVersion` como
163
+ > parâmetro. Isso significa **zero optimistic locking server-side**. Toda a
164
+ > conflict detection é client-side — responsabilidade deste adapter.
165
+
166
+ **Passos**:
167
+ 1. Chamar `getVersion(itemId)` → `currentVersion`
168
+ - Se o item sumiu → `NotFoundError`
169
+ 2. Comparar `currentVersion !== expectedVersion`:
170
+ - **Se diverge** → retornar `{ version: currentVersion, ok: false, conflict: true }`
171
+ (sem chamar `update_page` — não sobrescreve drift)
172
+ 3. Se bate, chamar `confluence_update_page` com:
173
+ - `page_id: itemId`
174
+ - `title: <mesmo título atual>` (pegar do `get_page` anterior se não vier em cache)
175
+ - `content: content`
176
+ - `content_format: "markdown"`
177
+ - `version_comment: "sfw: auto-publish"` (opcional mas recomendado pra histórico Confluence)
178
+ 4. Retornar `{ version: String(response.page.version.number), ok: true }`
179
+
180
+ **Race window**: há uma pequena janela entre o `getVersion` e o `update_page`
181
+ onde outra pessoa pode ter editado. MVP aceita — o caso é raro e o próximo
182
+ `getVersion`/`update` no ciclo detecta. Se virar dor, documentar como
183
+ limitação conhecida.
184
+
185
+ **Erros específicos**:
186
+ - `version_drift` detectado no passo 2 → retorno `{ ok: false, conflict: true }`
187
+ (não lança erro — é caminho normal do contrato)
188
+ - Qualquer outro erro → embrulhar conforme `errors.md`
189
+
190
+ ---
191
+
192
+ ### `attachFile?(itemId, filename, content, mimeType)`
193
+
194
+ **MCP**: `mcp__atlassian__confluence_upload_attachment` (ou `upload_attachments` batch)
195
+
196
+ **Passos**:
197
+ 1. Chamar tool com `page_id: itemId`, `filename`, `content` (bytes), `mime_type`
198
+ 2. Não retorna valor — sucesso é ausência de erro
199
+
200
+ **Implementação opcional no MVP**: ativar se/quando o `/publish` precisar
201
+ anexar diagramas, planilhas, etc. Por padrão, não chamado.
202
+
203
+ ---
204
+
205
+ ## Constraints descobertos (aprendizado do teste E2E)
206
+
207
+ 1. **Title uniqueness por SPACE inteiro** — não por parent.
208
+ **Premissa**: 1 space = 1 projeto. Multi-projeto no mesmo space causa
209
+ colisão se dois scopes tiverem o mesmo nome.
210
+ **Mitigação**: user nomeia items no Input com nomes únicos; naming
211
+ templating com `{scope}` no `output_artifact` garante unicidade no Output.
212
+
213
+ 2. **Markdown roundtrip é lossy** — `#` vira `===`, `-` vira `*`, emojis
214
+ podem normalizar. **Consequência**: nunca usar diff de bytes pra detectar
215
+ drift. `version.number` é a única fonte de verdade.
216
+
217
+ 3. **`.mcp.json` com `${VAR}` não funciona** no startup do Claude Code.
218
+ Credenciais precisam estar hardcoded no arquivo (gitignored). Mudanças
219
+ exigem reiniciar Claude Code — servers MCP só sobem no startup.
220
+
221
+ 4. **`page_id` é chave estável** — renomeação preserva ID, history, children.
222
+ Skills persistem ID em logs, não título.
223
+
224
+ 5. **`recursive: true` no `get_page_children`** não é suportado pelo MCP
225
+ atual — adapter precisa fazer recursão manual (loop + stack) quando
226
+ `config.recursive === true`.
227
+
228
+ 6. **Paginação**: `get_page_children` retorna `limit` default 25. Caller
229
+ deve iterar via `start` ou cursor pra esgotar filhos — não assumir
230
+ que primeira página é tudo.
231
+
232
+ ---
233
+
234
+ ## Checklist de pré-requisitos (documentar no bootstrap do kit)
235
+
236
+ Antes de usar este adapter, usuário precisa:
237
+
238
+ - [ ] `uvx` instalado (ou `pipx`/`pip`)
239
+ - [ ] `.mcp.json` na raiz do projeto com entrada pro `sooperset/mcp-atlassian`
240
+ + credenciais hardcoded (NUNCA commitar — está no `.gitignore` do kit)
241
+ - [ ] Token Atlassian com scope de leitura + escrita no space alvo
242
+ - [ ] `space_key` descoberto (ex: via `confluence_search` ou URL do Confluence)
243
+ - [ ] Página raiz do projeto criada (manualmente ou via `/new-project`)
244
+ - [ ] Claude Code reiniciado após qualquer mudança no `.mcp.json`
245
+
246
+ Bootstrap do kit (§9.9.P0.3) automatiza isso.
247
+
248
+ ---
249
+
250
+ ## Matriz de erros Confluence → tipos do SFW
251
+
252
+ | Situação Confluence | Tipo lançado | Detalhes |
253
+ |---|---|---|
254
+ | 401 / token inválido | `AuthError` | `hint`: "verifique token em .mcp.json" |
255
+ | 403 / sem permissão | `AuthError` | `hint`: "usuário sem acesso ao space {space_key}" |
256
+ | 404 page_id | `NotFoundError` | `kind: "item"` |
257
+ | 404 parent_id | `NotFoundError` | `kind: "container"` |
258
+ | `BadRequestException` title duplicado | `ConflictError` | `kind: "duplicate_title"` |
259
+ | 5xx Confluence down | `TransportError` | `retryable: true` |
260
+ | MCP server não responde | `TransportError` | `retryable: true`, `hint`: "reinicie Claude Code" |
261
+ | Timeout | `TransportError` | `retryable: true` |
262
+ | Attachment muito grande | `TransportError` | `retryable: false`, `hint`: tamanho máx Confluence |
263
+
264
+ ---
265
+
266
+ ## Referências
267
+
268
+ - Interface: `.github/adapters/interface.md`
269
+ - Erros: `.github/adapters/errors.md`
270
+ - Naming: `.github/adapters/naming.md`
271
+ - Registry: `.github/adapters/registry.md`
272
+ - Validação E2E: `planodetarefas.md §9.9` + napkin "Estado do Confluence MCP"
273
+ - MCP provider: [sooperset/mcp-atlassian](https://github.com/sooperset/mcp-atlassian)
@@ -0,0 +1,234 @@
1
+ # SourceAdapter — Contrato de Erros
2
+
3
+ > Classes de erro tipadas que qualquer adapter **deve** lançar em vez de
4
+ > `throw new Error(...)`. As skills (`/load`, `/publish`, `/new-project`,
5
+ > `/extract`, `/design`, `/plan`) fazem `catch` tipado e decidem o que fazer.
6
+ >
7
+ > Arquivo é referência conceitual, não runtime. A implementação real vive no
8
+ > adapter específico (`.github/adapters/confluence.md` etc.).
9
+
10
+ ---
11
+
12
+ ## Por que erros tipados
13
+
14
+ 1. **Skills reagem diferente por categoria.** Um `ConflictError` é recuperável
15
+ (pede ao usuário), um `AuthError` é fatal (para tudo e orienta setup).
16
+ Genérico obriga a parsear mensagem — frágil.
17
+ 2. **Logs ficam úteis.** `.ai/load-log.md` e `.ai/publish-log.md` registram
18
+ o tipo do erro, não a mensagem livre. Re-execução pode filtrar por tipo.
19
+ 3. **Testes ficam honestos.** FilesystemAdapter (mock natural) lança os mesmos
20
+ tipos — skills testadas contra ele ganham cobertura real.
21
+
22
+ ---
23
+
24
+ ## Hierarquia
25
+
26
+ ```
27
+ SourceAdapterError (base — nunca lançado direto)
28
+ ├── ValidationError (config do sfw.config.yml inválido)
29
+ ├── AuthError (credenciais inválidas ou ausentes)
30
+ ├── NotFoundError (itemId / containerId inexistente)
31
+ ├── ConflictError (version drift, título duplicado)
32
+ ├── TransportError (rede, MCP down, timeout)
33
+ └── NotImplementedError (método opcional não implementado)
34
+ ```
35
+
36
+ Todos os erros **devem** carregar:
37
+ - `name` — nome da classe (ex: `"ConflictError"`)
38
+ - `adapter` — nome do adapter que lançou (ex: `"confluence"`)
39
+ - `operation` — método que falhou (ex: `"update"`)
40
+ - `message` — descrição humana
41
+ - `cause` opcional — erro original (MCP, fs, fetch) pra debug
42
+
43
+ ---
44
+
45
+ ## Catálogo completo
46
+
47
+ ### `ValidationError`
48
+
49
+ **Quando**: `validateConfig(config)` detecta campo ausente, tipo errado, ou
50
+ combinação inválida (ex: `mode: auto` sem `approval_mechanism` quando o target
51
+ requer aprovação).
52
+
53
+ **Campos extras**:
54
+ - `field` — caminho do campo inválido (ex: `"output.targets[0].publishes"`)
55
+ - `expected` — o que era esperado
56
+ - `got` — o que veio
57
+
58
+ **Como o caller reage**: para imediatamente. Nunca é recuperável em runtime —
59
+ o usuário precisa arrumar o `sfw.config.yml` e re-rodar.
60
+
61
+ **Exemplo**:
62
+ ```
63
+ ValidationError em confluence.validateConfig:
64
+ field: input.config.space_key
65
+ expected: string não-vazio
66
+ got: undefined
67
+ message: space_key obrigatório pro ConfluenceAdapter
68
+ ```
69
+
70
+ ---
71
+
72
+ ### `AuthError`
73
+
74
+ **Quando**: backend rejeita credenciais (401, 403, token expirado, MCP retorna
75
+ auth failure). Também lançado se credenciais nem foram encontradas (`.mcp.json`
76
+ ausente, env var não setada).
77
+
78
+ **Campos extras**:
79
+ - `hint` opcional — pista acionável (ex: `"verifique CONFLUENCE_TOKEN em .mcp.json"`)
80
+
81
+ **Como o caller reage**: para imediatamente. Mostra `hint` pro usuário. Nunca
82
+ tenta retry — credenciais não se consertam sozinhas.
83
+
84
+ **Importante**: adapters nunca devem vazar o token no `message` ou `cause`.
85
+ Se o erro original tem o token no URL, o adapter faz sanitize antes de embrulhar.
86
+
87
+ ---
88
+
89
+ ### `NotFoundError`
90
+
91
+ **Quando**: operação aponta pra um `itemId` / `containerId` que não existe ou
92
+ foi deletado. Inclui: `fetchContent`, `getVersion`, `update`, `listChildren`,
93
+ `fetchAttachments` em um ID inválido.
94
+
95
+ **Campos extras**:
96
+ - `itemId` — o ID que não foi encontrado
97
+ - `kind` — `"container"` | `"item"` | `"attachment"`
98
+
99
+ **Como o caller reage**:
100
+ - `/load`: pode ser scope removido no backend. Loga e pula (não fatal).
101
+ - `/publish`: se título existia no publish-log mas sumiu do backend → o caller
102
+ DEVE chamar `create` em vez de `update` (fallback implícito).
103
+ - `/new-project`: se parent root não existe → fatal (config errada).
104
+
105
+ ---
106
+
107
+ ### `ConflictError`
108
+
109
+ **Quando**:
110
+ - `create` encontra título duplicado no parent (ou no space, no Confluence).
111
+ - `update` detecta `expectedVersion != version atual` (drift — alguém editou
112
+ entre o último `getVersion` e o `update`).
113
+
114
+ **Campos extras**:
115
+ - `expectedVersion` — o que o caller achava que estava lá
116
+ - `actualVersion` — o que o backend tem de fato
117
+ - `itemId` — item que conflitou
118
+ - `kind` — `"duplicate_title"` | `"version_drift"`
119
+
120
+ **Como o caller reage**:
121
+ - `duplicate_title` (no `create`): normalmente fatal em MVP — skills assumem
122
+ naming unique via template com `{scope}` no título. Se bateu, dois
123
+ scopes têm o mesmo nome — o user precisa renomear no Input.
124
+ - `version_drift` (no `update`): **recuperável**. Fluxo:
125
+ 1. `/publish` para a operação atual
126
+ 2. Baixa content remoto via `fetchContent`
127
+ 3. Gera diff local-vs-remoto
128
+ 4. Pergunta ao usuário: **sobrescrever, abortar, ou fazer merge manual**
129
+ 5. Se sobrescrever → novo `update` com `expectedVersion` atualizada
130
+
131
+ **Nunca retry automático** — drift é sinal de que um humano tocou o artefato
132
+ (aprovação manual, edição pós-review). Requer decisão consciente.
133
+
134
+ ---
135
+
136
+ ### `TransportError`
137
+
138
+ **Quando**: rede caiu, MCP morreu, timeout, 5xx do backend.
139
+
140
+ **Campos extras**:
141
+ - `retryable` — `true` se faz sentido tentar de novo (5xx, timeout)
142
+ - `statusCode` opcional — se o backend deu HTTP
143
+
144
+ **Como o caller reage**:
145
+ - `retryable=true`: retry com backoff (proposta: 3 tentativas, 1s / 3s / 10s).
146
+ Depois disso, escala pro usuário.
147
+ - `retryable=false`: escala imediato.
148
+
149
+ **Importante**: distinguir `TransportError` de `AuthError` é crítico — 401 **não**
150
+ é transport, é auth. Adapter precisa classificar antes de embrulhar.
151
+
152
+ ---
153
+
154
+ ### `NotImplementedError`
155
+
156
+ **Quando**: skill chama método opcional (`attachFile`) num adapter que não
157
+ implementa.
158
+
159
+ **Como o caller reage**: skill **deve** ter fallback ou skipar feature. Nunca
160
+ é fatal — é esperado em MVP (FilesystemAdapter sem attachments na v0, por ex).
161
+
162
+ Caller típico:
163
+ ```
164
+ try {
165
+ await adapter.attachFile(id, name, buf, mime);
166
+ } catch (e) {
167
+ if (e instanceof NotImplementedError) {
168
+ log("adapter ${adapter.name} não suporta attachments, pulando");
169
+ return;
170
+ }
171
+ throw e;
172
+ }
173
+ ```
174
+
175
+ ---
176
+
177
+ ## Quem lança o quê (matriz)
178
+
179
+ | Método | ValidationError | AuthError | NotFoundError | ConflictError | TransportError | NotImplementedError |
180
+ |--------|:---:|:---:|:---:|:---:|:---:|:---:|
181
+ | `validateConfig` | ✅ | — | — | — | — | — |
182
+ | `listChildren` | — | ✅ | ✅ | — | ✅ | — |
183
+ | `fetchContent` | — | ✅ | ✅ | — | ✅ | — |
184
+ | `fetchAttachments` | — | ✅ | ✅ | — | ✅ | — |
185
+ | `getVersion` | — | ✅ | ✅ | — | ✅ | — |
186
+ | `create` | — | ✅ | ✅¹ | ✅² | ✅ | — |
187
+ | `update` | — | ✅ | ✅ | ✅³ | ✅ | — |
188
+ | `attachFile` | — | ✅ | ✅ | — | ✅ | ✅⁴ |
189
+
190
+ **Notas**:
191
+ - ¹ `NotFoundError` no `create` quando o `parentId` não existe.
192
+ - ² `ConflictError` no `create` = `kind: "duplicate_title"`.
193
+ - ³ `ConflictError` no `update` = `kind: "version_drift"`.
194
+ - ⁴ `NotImplementedError` apenas em adapters que não suportam attachments.
195
+
196
+ ---
197
+
198
+ ## Padrão de implementação (pseudo-código)
199
+
200
+ Todo adapter embrulha erros nativos nesses tipos. Exemplo genérico:
201
+
202
+ ```typescript
203
+ async update(itemId, content, expectedVersion) {
204
+ try {
205
+ const current = await this.getVersion(itemId);
206
+ if (current !== expectedVersion) {
207
+ throw new ConflictError({
208
+ adapter: this.name,
209
+ operation: "update",
210
+ kind: "version_drift",
211
+ itemId,
212
+ expectedVersion,
213
+ actualVersion: current,
214
+ });
215
+ }
216
+ const result = await this.backendUpdate(itemId, content);
217
+ return { version: result.version, ok: true };
218
+ } catch (e) {
219
+ if (e instanceof ConflictError) throw e;
220
+ if (isNotFound(e)) throw new NotFoundError({ adapter: this.name, operation: "update", itemId, kind: "item", cause: e });
221
+ if (isAuth(e)) throw new AuthError( { adapter: this.name, operation: "update", cause: e });
222
+ if (isTransport(e)) throw new TransportError({ adapter: this.name, operation: "update", retryable: e.status >= 500, cause: e });
223
+ throw e; // unknown — propaga cru
224
+ }
225
+ }
226
+ ```
227
+
228
+ ---
229
+
230
+ ## Referências
231
+
232
+ - Interface: `.github/adapters/interface.md`
233
+ - Registry: `.github/adapters/registry.md`
234
+ - Naming: `.github/adapters/naming.md`