@ds9-solutions/mcp 0.1.0 → 0.3.0

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 (70) hide show
  1. package/LICENSE +43 -0
  2. package/README.md +53 -0
  3. package/content/agents/code-reviewer.agent.md +52 -22
  4. package/content/agents/db-analyser.agent.md +121 -0
  5. package/content/agents/security-reviewer.agent.md +119 -37
  6. package/content/agents/test-writer.agent.md +84 -35
  7. package/content/skills/dart/SKILL.md +139 -0
  8. package/content/skills/dart/examples/entity.dart +37 -0
  9. package/content/skills/dart/examples/error_handling.dart +36 -0
  10. package/content/skills/dart/examples/order_test.dart +34 -0
  11. package/content/skills/dart/examples/repository.dart +57 -0
  12. package/content/skills/dart/references/checklist.md +12 -0
  13. package/content/skills/dart/references/documentation.md +26 -0
  14. package/content/skills/dart/references/naming.md +27 -0
  15. package/content/skills/dart-frog/SKILL.md +208 -0
  16. package/content/skills/dart-frog/examples/auth_middleware.dart +50 -0
  17. package/content/skills/dart-frog/examples/controller.dart +75 -0
  18. package/content/skills/dart-frog/examples/dto.dart +41 -0
  19. package/content/skills/dart-frog/examples/error_response.dart +35 -0
  20. package/content/skills/dart-frog/examples/exceptions.dart +60 -0
  21. package/content/skills/dart-frog/examples/middleware_composition.dart +34 -0
  22. package/content/skills/dart-frog/examples/model.dart +20 -0
  23. package/content/skills/dart-frog/examples/port.dart +17 -0
  24. package/content/skills/dart-frog/examples/repository.dart +56 -0
  25. package/content/skills/dart-frog/examples/route_collection.dart +25 -0
  26. package/content/skills/dart-frog/examples/route_item.dart +24 -0
  27. package/content/skills/dart-frog/examples/route_test.dart +33 -0
  28. package/content/skills/dart-frog/examples/service.dart +51 -0
  29. package/content/skills/dart-frog/examples/service_test.dart +55 -0
  30. package/content/skills/dart-frog/references/checklist.md +31 -0
  31. package/content/skills/dart-frog/references/error-handling.md +48 -0
  32. package/content/skills/dart-frog/references/fpdart-taskeither.md +53 -0
  33. package/content/skills/dart-frog/references/layers.md +43 -0
  34. package/content/skills/dart-frog/references/middlewares-di.md +45 -0
  35. package/content/skills/dart-frog/references/project-structure.md +67 -0
  36. package/content/skills/dart-frog/references/routing.md +53 -0
  37. package/content/skills/dart-frog/references/testing.md +24 -0
  38. package/content/skills/docker/SKILL.md +3 -3
  39. package/content/skills/driftorm/SKILL.md +249 -0
  40. package/content/skills/flutter/SKILL.md +419 -0
  41. package/content/skills/hex-pattern-dart/SKILL.md +83 -0
  42. package/content/skills/hex-pattern-dart/examples/adapter-in.md +24 -0
  43. package/content/skills/hex-pattern-dart/examples/adapter-out.md +20 -0
  44. package/content/skills/hex-pattern-dart/examples/errors.md +25 -0
  45. package/content/skills/hex-pattern-dart/examples/port.md +11 -0
  46. package/content/skills/hex-pattern-dart/examples/service.md +20 -0
  47. package/content/skills/hex-pattern-dart/examples/structure-tree.md +28 -0
  48. package/content/skills/hex-pattern-dart/references/anti-patterns.md +16 -0
  49. package/content/skills/hex-pattern-dart/references/checklist.md +34 -0
  50. package/content/skills/hex-pattern-dart/references/dependency-injection.md +32 -0
  51. package/content/skills/hex-pattern-dart/references/error-layers.md +27 -0
  52. package/content/skills/hex-pattern-dart/references/layers-and-dependency-rule.md +29 -0
  53. package/content/skills/hex-pattern-dart/references/persistence.md +23 -0
  54. package/content/skills/hex-pattern-dart/references/ports-and-adapters.md +33 -0
  55. package/content/skills/hex-pattern-dart/references/testing.md +30 -0
  56. package/content/skills/mysql/SKILL.md +3 -3
  57. package/content/skills/typescript/SKILL.md +4 -4
  58. package/dist/src/content.js +10 -2
  59. package/dist/src/init.js +1 -0
  60. package/dist/src/search.js +20 -12
  61. package/dist/src/server.js +60 -4
  62. package/dist/src/skill-files.js +125 -0
  63. package/package.json +2 -2
  64. package/content/agents/db-specialist.agent.md +0 -78
  65. package/content/agents/nestjs-api-designer.agent.md +0 -61
  66. package/content/skills/hexagonal/SKILL.md +0 -156
  67. package/content/skills/jest-testing/SKILL.md +0 -88
  68. package/content/skills/nestjs/SKILL.md +0 -100
  69. package/content/skills/typeorm/SKILL.md +0 -119
  70. package/dist/test/mcp.test.js +0 -112
@@ -9,12 +9,14 @@
9
9
  import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
10
10
  import { z } from "zod";
11
11
  import { rankByMetadata, searchBodies } from "./search.js";
12
+ import { isSafeRelativePath, MAX_PATH_LENGTH, SKILL_ID } from "./skill-files.js";
12
13
  // Enviado ao host no handshake. Hosts sem suporte nativo a skills/agentes dependem disto (e do
13
14
  // bloco no AGENTS.md) para saber que devem consultar o servidor antes de implementar.
14
15
  const INSTRUCTIONS = [
15
16
  "Servidor de diretrizes oficiais da DS9 (skills por tecnologia e subagentes especializados).",
16
17
  "Antes de implementar, revisar ou testar código, chame `recommend_for_task` com a descrição da tarefa",
17
18
  "e depois `get_skill` / `get_agent` para ler as diretrizes recomendadas e segui-las.",
19
+ "Se a skill indicar arquivos complementares, use `list_skill_files` e `get_skill_file` para ler exemplos e referências sob demanda.",
18
20
  ].join(" ");
19
21
  // Monta a resposta padrão de uma tool. `isError` sinaliza ao LLM que a chamada falhou, mas sem
20
22
  // derrubar o protocolo (erro de uso != erro do servidor).
@@ -31,8 +33,21 @@ export function createServer(entries, version = "0.0.0") {
31
33
  // Helpers de consulta sobre as entradas.
32
34
  const byKind = (kind) => entries.filter((e) => e.kind === kind);
33
35
  const find = (kind, id) => entries.find((e) => e.kind === kind && e.id === id);
34
- const render = (e) => `# ${e.kind === "skill" ? "Skill" : "Agente"}: ${e.id}\n\n> ${e.description}\n\n${e.body}`;
35
- const listing = (items) => items.map((e) => `- ${e.id}: ${e.description}`).join("\n") || "(nenhum resultado)";
36
+ const render = (e) => `# ${e.kind === "skill" ? "Skill" : "Agente"}: ${e.id}\n\n> ${e.description}\n\n${e.body}` +
37
+ (e.files.length
38
+ ? `\n\n---\nArquivos complementares (${e.files.length}): use \`list_skill_files\` e \`get_skill_file\` com skill="${e.id}" para exemplos e referências sob demanda.`
39
+ : "");
40
+ const listing = (items) => items
41
+ .map((e) => `- ${e.id}: ${e.description}${e.files.length ? ` (+${e.files.length} arquivos)` : ""}`)
42
+ .join("\n") || "(nenhum resultado)";
43
+ // Erros de skill/arquivo nunca citam caminhos do disco: só ids e caminhos do índice.
44
+ const skillOrError = (skill) => {
45
+ const entry = find("skill", skill);
46
+ return entry
47
+ ? { entry }
48
+ : { error: text(`skill "${skill}" não encontrada. Disponíveis:\n${listing(byKind("skill"))}`, true) };
49
+ };
50
+ const fileList = (e) => e.files.map((f) => `- ${f.path} (${f.size} bytes)${f.description ? `: ${f.description}` : ""}`).join("\n");
36
51
  // Skills e agentes têm exatamente a mesma API (list/get/prompt/resource). Em vez de duplicar o
37
52
  // código, registramos os dois em um laço; só mudam os nomes e os rótulos.
38
53
  for (const [kind, plural, label] of [
@@ -63,7 +78,7 @@ export function createServer(entries, version = "0.0.0") {
63
78
  }
64
79
  return text(render(entry));
65
80
  });
66
- // Um prompt por entrada (ex.: `skill-nestjs`). Fica estático porque o catálogo é conhecido na
81
+ // Um prompt por entrada (ex.: `skill-hex-pattern-dart`). Fica estático porque o catálogo é conhecido na
67
82
  // inicialização do servidor.
68
83
  for (const entry of byKind(kind)) {
69
84
  server.registerPrompt(`${kind}-${entry.id}`, { description: entry.description }, () => ({
@@ -91,6 +106,45 @@ export function createServer(entries, version = "0.0.0") {
91
106
  return { contents: [{ uri: uri.href, mimeType: "text/markdown", text: render(entry) }] };
92
107
  });
93
108
  }
109
+ // list_skill_files / get_skill_file — carga sob demanda dos arquivos complementares de uma skill.
110
+ // Só leem o índice em memória montado no carregamento; o cliente nunca informa caminho de disco.
111
+ const skillSchema = z
112
+ .string()
113
+ .regex(SKILL_ID, "id de skill inválido")
114
+ .describe("Id da skill (veja list_skills)");
115
+ server.registerTool("list_skill_files", {
116
+ description: "Lista os arquivos complementares (exemplos, referências) de uma skill DS9, com tamanho e descrição.",
117
+ inputSchema: { skill: skillSchema },
118
+ annotations: { readOnlyHint: true },
119
+ }, async ({ skill }) => {
120
+ const { entry, error } = skillOrError(skill);
121
+ if (!entry)
122
+ return error;
123
+ return text(entry.files.length
124
+ ? `Arquivos da skill "${skill}" (leia com get_skill_file):\n${fileList(entry)}`
125
+ : `A skill "${skill}" não possui arquivos complementares.`);
126
+ });
127
+ server.registerTool("get_skill_file", {
128
+ description: "Retorna o conteúdo de um arquivo complementar de uma skill DS9. Use o caminho exato de list_skill_files.",
129
+ inputSchema: {
130
+ skill: skillSchema,
131
+ path: z
132
+ .string()
133
+ .max(MAX_PATH_LENGTH)
134
+ .describe("Caminho relativo listado por list_skill_files, ex.: examples/entity.dart"),
135
+ },
136
+ annotations: { readOnlyHint: true },
137
+ }, async ({ skill, path }) => {
138
+ const { entry, error } = skillOrError(skill);
139
+ if (!entry)
140
+ return error;
141
+ // Busca exata no índice: caminhos fora dele (inclusive `..`, absolutos etc.) simplesmente não existem.
142
+ const file = isSafeRelativePath(path) ? entry.files.find((f) => f.path === path) : undefined;
143
+ if (!file) {
144
+ return text(`Arquivo não encontrado na skill "${skill}". Arquivos válidos:\n${fileList(entry) || "(nenhum)"}`, true);
145
+ }
146
+ return text(`# ${skill}/${file.path}\n\n${file.text}`);
147
+ });
94
148
  // search_guidelines — para dúvidas pontuais ("como nomear migrations?") sem ler arquivos inteiros.
95
149
  server.registerTool("search_guidelines", {
96
150
  description: "Busca um termo no conteúdo de todas as skills e agentes DS9 e devolve as linhas mais relevantes.",
@@ -99,7 +153,9 @@ export function createServer(entries, version = "0.0.0") {
99
153
  }, async ({ query, limit }) => {
100
154
  const hits = searchBodies(entries, query, limit ?? 8);
101
155
  return text(hits.length
102
- ? hits.map((h) => `[${h.kind}:${h.id}:L${h.line}] ${h.text}`).join("\n")
156
+ ? hits
157
+ .map((h) => `[${h.kind}:${h.id}${h.file ? `:${h.file}` : ""}:L${h.line}] ${h.text}`)
158
+ .join("\n")
103
159
  : "Nenhum trecho encontrado.");
104
160
  });
105
161
  // recommend_for_task — é a "ativação automática" possível via MCP: compensa a falta de roteamento
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Arquivos complementares de uma skill (`skills/<id>/examples/`, `references/` etc.).
3
+ *
4
+ * Segurança — por que é uma allowlist e não uma sanitização de caminho:
5
+ * - tudo é indexado UMA vez, no carregamento, pelo próprio servidor;
6
+ * - `get_skill_file` só encontra caminhos que estão nesse índice (comparação exata de string);
7
+ * - o texto vem da memória: nenhuma chamada de tool toca o disco nem monta caminho com entrada do
8
+ * cliente. Não há como escapar da pasta da skill porque o cliente nunca fornece um caminho de
9
+ * arquivo, apenas uma chave de busca.
10
+ * A varredura ainda aplica limites e ignora symlinks, ocultos e extensões fora da lista.
11
+ */
12
+ import { lstatSync, readdirSync, readFileSync, realpathSync } from "node:fs";
13
+ import { join, sep } from "node:path";
14
+ export const ALLOWED_EXTENSIONS = new Set([
15
+ ".md",
16
+ ".dart",
17
+ ".yaml",
18
+ ".yml",
19
+ ".json",
20
+ ".txt",
21
+ ]);
22
+ export const MAX_FILE_BYTES = 100 * 1024;
23
+ export const MAX_FILES_PER_SKILL = 200;
24
+ export const MAX_DEPTH = 4;
25
+ export const MAX_PATH_LENGTH = 200;
26
+ // SKILL.md é servido por `get_skill`; README.md não é copiado pelo `sync-content`, então ignorá-lo
27
+ // aqui mantém o comportamento igual em desenvolvimento e em produção.
28
+ const IGNORED_NAMES = new Set(["skill.md", "readme.md", "node_modules"]);
29
+ /** Id de skill válido: é o nome da pasta, em kebab-case. */
30
+ export const SKILL_ID = /^[a-z0-9][a-z0-9-]{0,63}$/;
31
+ // eslint-disable-next-line no-control-regex
32
+ const CONTROL_CHARS = /[\u0000-\u001f\u007f]/;
33
+ /**
34
+ * Valida o formato do caminho informado pelo cliente. É uma barreira extra de entrada; a proteção
35
+ * real é a busca exata no índice. Rejeita tudo que não seja um caminho relativo simples com `/`.
36
+ */
37
+ export function isSafeRelativePath(path) {
38
+ if (path.length === 0 || path.length > MAX_PATH_LENGTH)
39
+ return false;
40
+ if (CONTROL_CHARS.test(path))
41
+ return false;
42
+ if (path.includes("\\") || path.includes(":") || path.includes("%"))
43
+ return false;
44
+ if (path.startsWith("/"))
45
+ return false;
46
+ return path.split("/").every((seg) => seg !== "" && seg !== "." && seg !== "..");
47
+ }
48
+ const UTF8 = new TextDecoder("utf-8", { fatal: true });
49
+ /** Primeira linha útil do arquivo: título Markdown ou comentário `///`/`//`. */
50
+ function describe(text) {
51
+ for (const raw of text.split(/\r?\n/)) {
52
+ const line = raw.trim();
53
+ if (!line)
54
+ continue;
55
+ const cleaned = line.replace(/^(#+|\/\/\/?|\*|>)\s*/, "").trim();
56
+ if (cleaned && cleaned !== "---")
57
+ return cleaned.slice(0, 160);
58
+ }
59
+ return "";
60
+ }
61
+ /**
62
+ * Varre a pasta de uma skill e devolve os arquivos complementares permitidos, em ordem estável.
63
+ * Problemas (arquivo grande, extensão proibida, symlink etc.) viram avisos e o arquivo é ignorado:
64
+ * um item ruim nunca derruba o carregamento das demais skills.
65
+ */
66
+ export function scanSkillFiles(skillDir, warnings) {
67
+ const files = [];
68
+ const rootReal = realpathSync(skillDir);
69
+ const walk = (dir, prefix, depth) => {
70
+ for (const name of readdirSync(dir).sort()) {
71
+ const rel = prefix ? `${prefix}/${name}` : name;
72
+ const abs = join(dir, name);
73
+ if (name.startsWith(".") || IGNORED_NAMES.has(name.toLowerCase()))
74
+ continue;
75
+ const stat = lstatSync(abs);
76
+ if (stat.isSymbolicLink()) {
77
+ warnings.push(`"${rel}" ignorado: link simbólico`);
78
+ continue;
79
+ }
80
+ if (stat.isDirectory()) {
81
+ if (depth >= MAX_DEPTH)
82
+ warnings.push(`"${rel}" ignorado: profundidade acima de ${MAX_DEPTH}`);
83
+ else
84
+ walk(abs, rel, depth + 1);
85
+ continue;
86
+ }
87
+ if (!stat.isFile())
88
+ continue;
89
+ const ext = name.includes(".") ? name.slice(name.lastIndexOf(".")).toLowerCase() : "";
90
+ if (!ALLOWED_EXTENSIONS.has(ext)) {
91
+ warnings.push(`"${rel}" ignorado: extensão não permitida`);
92
+ continue;
93
+ }
94
+ if (stat.size > MAX_FILE_BYTES) {
95
+ warnings.push(`"${rel}" ignorado: maior que ${MAX_FILE_BYTES} bytes`);
96
+ continue;
97
+ }
98
+ if (files.length >= MAX_FILES_PER_SKILL) {
99
+ warnings.push(`"${rel}" ignorado: limite de ${MAX_FILES_PER_SKILL} arquivos por skill`);
100
+ continue;
101
+ }
102
+ if (!rel.split("/").every((s) => isSafeRelativePath(s))) {
103
+ warnings.push(`"${rel}" ignorado: nome de arquivo inválido`);
104
+ continue;
105
+ }
106
+ // Confinamento: o arquivo real precisa estar dentro da pasta da skill.
107
+ const real = realpathSync(abs);
108
+ if (!real.startsWith(rootReal + sep)) {
109
+ warnings.push(`"${rel}" ignorado: fora da pasta da skill`);
110
+ continue;
111
+ }
112
+ let text;
113
+ try {
114
+ text = UTF8.decode(readFileSync(real));
115
+ }
116
+ catch {
117
+ warnings.push(`"${rel}" ignorado: não é texto UTF-8 válido`);
118
+ continue;
119
+ }
120
+ files.push({ path: rel, size: stat.size, description: describe(text), text });
121
+ }
122
+ };
123
+ walk(skillDir, "", 1);
124
+ return files;
125
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ds9-solutions/mcp",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Servidor MCP da DS9: expõe skills e subagentes oficiais para qualquer IDE/LLM com suporte a MCP",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -8,7 +8,7 @@
8
8
  "ds9-mcp": "dist/src/index.js"
9
9
  },
10
10
  "files": [
11
- "dist",
11
+ "dist/src",
12
12
  "content"
13
13
  ],
14
14
  "engines": {
@@ -1,78 +0,0 @@
1
- ---
2
- name: DB Specialist
3
- description: Identifica o banco de dados e o ORM usados no projeto, analisa models/entities, migrations, índices e o impacto de performance das queries contra o schema. Somente leitura — devolve um relatório com as descobertas e recomendações, nunca aplica alterações. Use quando pedirem auditoria de banco de dados, revisão de schema/migrations, análise de performance de query, ou "o que esse projeto usa de banco".
4
- tools: ["read", "search"]
5
- disable-model-invocation: false
6
- user-invocable: true
7
- is_background: true
8
- ---
9
-
10
- # DB Specialist — DS9
11
-
12
- Você é um especialista em persistência: somente leitura, produz um relatório técnico sobre o banco de dados e o ORM do projeto analisado.
13
-
14
- ## 1. Identificar o stack de dados
15
-
16
- - Banco de dados: procure em `package.json` (drivers como `mysql2`, `pg`, `mongodb`), `docker-compose.yml`, variáveis de ambiente (`DB_HOST`, `DATABASE_URL`) e arquivos de configuração (`ormconfig`, `data-source.ts`).
17
- - ORM/query builder: procure dependências (`typeorm`, `prisma`, `sequelize`, `knex`, `mongoose`) e a estrutura de pastas (`*.entity.ts`, `schema.prisma`, `migrations/`).
18
- - Se o projeto usa MySQL + TypeORM (padrão DS9), aplique as skills `mysql` e `typeorm` como critério de avaliação. Se usar outro stack, avalie pelos princípios gerais (tipos corretos, índices, migrations versionadas, queries parametrizadas) e sinalize a divergência do padrão DS9.
19
-
20
- ## 2. Analisar models/entities
21
-
22
- - Tipos de coluna coerentes com o domínio (dinheiro em `decimal`, não `float`; datas em tipo de data, não `varchar`).
23
- - Chaves primárias e estratégia de ID consistente entre entidades.
24
- - Relações (`@ManyToOne`, `@OneToMany`, etc.) com FK explícita, e cascades/eager loading avaliados quanto a risco (ver skill `typeorm`).
25
- - Soft delete, auditoria (`created_at`/`updated_at`) presentes onde o domínio exige.
26
-
27
- ## 3. Analisar migrations
28
-
29
- - Toda alteração de schema tem migration versionada (nenhuma evidência de alteração manual direta)?
30
- - Migrations têm `up`/`down` reversível? Alguma migration já aplicada parece ter sido editada depois (sinal: histórico de git mostrando mudança em migration antiga)?
31
- - Alterações potencialmente perigosas em produção (`DROP COLUMN`, `ALTER` bloqueante em tabela grande) sem cuidado explícito documentado.
32
-
33
- ## 4. Analisar índices
34
-
35
- - FKs têm índice correspondente?
36
- - Índices compostos existentes fazem sentido para os padrões de consulta observados no código (repositórios/queries)?
37
- - Índices redundantes ou ausentes em colunas usadas com frequência em `WHERE`/`ORDER BY`/`JOIN`.
38
- - Existe índice único garantindo invariantes de negócio no banco (não só na aplicação)?
39
-
40
- ## 5. Analisar queries e impacto de performance
41
-
42
- - `SELECT *` em código de produção; queries sem `LIMIT`/paginação em listagens.
43
- - Risco de N+1 (loop fazendo uma query por item, ou relation `eager` carregando grafos grandes).
44
- - Queries com filtro em coluna sem índice correspondente (risco de full table scan).
45
- - SQL concatenado com valor de usuário (risco de SQL Injection) em vez de query parametrizada/QueryBuilder.
46
- - Transações ausentes em operações que alteram múltiplas tabelas com invariantes relacionadas.
47
-
48
- ## Formato do relatório
49
-
50
- ```text
51
- ## Stack de dados
52
- - Banco: ...
53
- - ORM: ...
54
- - Observações de configuração (synchronize, migrationsRun, etc.): ...
55
-
56
- ## Achados — Models/Entities
57
- [severidade] local — descrição — recomendação
58
-
59
- ## Achados — Migrations
60
- [severidade] local — descrição — recomendação
61
-
62
- ## Achados — Índices
63
- [severidade] local — descrição — recomendação
64
-
65
- ## Achados — Queries/Performance
66
- [severidade] local — descrição — recomendação
67
-
68
- ## Resumo
69
- Total por severidade e prioridade sugerida de correção.
70
- ```
71
-
72
- Severidade: 🔴 crítica (risco de perda de dados/corrupção/indisponibilidade) | 🟠 alta (performance ruim em produção, falta de integridade) | 🟡 média (dívida técnica) | ⚪ baixa (observação/sugestão).
73
-
74
- ## Não fazer
75
-
76
- - Não editar entities, migrations ou qualquer arquivo — este agente é exclusivamente de leitura e relatório.
77
- - Não executar migrations nem comandos que alterem o banco.
78
- - Não recomendar mudança de stack (trocar de ORM/banco) a menos que explicitamente perguntado; o foco é avaliar o que já existe.
@@ -1,61 +0,0 @@
1
- ---
2
- name: NestJS API Designer
3
- description: Analisa, revisa e cria contratos de endpoints REST em projetos NestJS da DS9 — rotas, DTOs de entrada/saída, códigos de status HTTP, versionamento de API e documentação OpenAPI/Swagger. Use ao criar um novo endpoint, revisar o contrato de uma API existente, ou quando pedirem "desenhar endpoint", "contrato de API", "DTO" ou "versionamento".
4
- tools: ["read", "search", "edit"]
5
- disable-model-invocation: false
6
- user-invocable: true
7
- is_background: true
8
- ---
9
-
10
- # NestJS API Designer — DS9
11
-
12
- Projeta e revisa contratos de API REST em NestJS, seguindo as skills `nestjs` e `hexagonal` (controllers são adapters de entrada, sem regra de negócio) e `typescript` (tipagem estrita).
13
-
14
- ## O que analisar/produzir
15
-
16
- ### 1. Rotas
17
-
18
- - Recurso no plural, hierarquia clara (`/orders`, `/orders/:id`, `/orders/:id/items`).
19
- - Verbo HTTP correto para a ação: `GET` (leitura, idempotente), `POST` (criação ou ação não idempotente), `PUT` (substituição completa), `PATCH` (atualização parcial), `DELETE` (remoção).
20
- - Nomes de rota não repetem o nome do recurso já presente no controller (`@Controller('orders')` + `@Get(':id')`, não `@Get('orders/:id')`).
21
-
22
- ### 2. DTOs
23
-
24
- - Um DTO de entrada por operação (`CreateOrderDto`, `UpdateOrderDto` — nunca reaproveitar o mesmo DTO para create e update se os campos obrigatórios diferem).
25
- - Um DTO de saída dedicado (`OrderResponseDto`), nunca a entidade de domínio/persistência exposta diretamente (ver skill `hexagonal`).
26
- - Toda propriedade do DTO de entrada tem decorator de validação (`class-validator`) compatível com a regra de negócio real (ex.: `@IsPositive()` para valores monetários, `@IsEmail()` para e-mail, `@IsEnum()` para status fechado).
27
- - DTOs aninhados usam `@ValidateNested()` + `@Type(() => SubDto)`.
28
- - Documente cada propriedade com `@ApiProperty({ description, example })` (ou `@ApiPropertyOptional` quando opcional).
29
-
30
- ### 3. Códigos de status
31
-
32
- - `201 Created` em `POST` que cria recurso (com `@HttpCode(HttpStatus.CREATED)` se o Nest não aplicar por padrão).
33
- - `200 OK` para `GET`/`PATCH`/`PUT` bem-sucedidos com corpo de resposta.
34
- - `204 No Content` para `DELETE` bem-sucedido sem corpo.
35
- - `400 Bad Request` para falha de validação (já automático via `ValidationPipe`), `404 Not Found` para recurso inexistente, `409 Conflict` para violação de invariante única (ex.: e-mail duplicado), `422 Unprocessable Entity` apenas se o projeto já adotar essa distinção de `400`.
36
- - Documente cada status possível com `@ApiResponse({ status, description, type })`.
37
-
38
- ### 4. Versionamento
39
-
40
- - Toda API nova decide explicitamente a estratégia de versionamento do projeto (padrão recomendado: URI versioning do Nest — `app.enableVersioning({ type: VersioningType.URI, defaultVersion: '1' })`, rotas expostas como `/v1/orders`).
41
- - Breaking changes em um contrato existente exigem nova versão (`/v2/...`), nunca alteração destrutiva silenciosa na versão corrente.
42
- - Se o projeto já usa outra estratégia (header, media type), siga a convenção existente em vez de introduzir uma segunda.
43
-
44
- ### 5. OpenAPI/Swagger
45
-
46
- - Todo controller tem `@ApiTags('orders')`; toda rota tem `@ApiOperation({ summary })`.
47
- - Gere/atualize a configuração do `SwaggerModule` se o projeto ainda não a tiver, mantendo o documento sincronizado com os DTOs reais (não escrever spec OpenAPI manual divergente do código).
48
-
49
- ## Processo ao revisar um contrato existente
50
-
51
- 1. Ler o controller, DTOs de entrada/saída e o use case associado.
52
- 2. Checar os 5 pontos acima e listar divergências.
53
- 3. Propor o contrato corrigido (rota, verbo, status, DTO, versão) antes de editar.
54
- 4. Aplicar a edição apenas nos arquivos de contrato (controller, DTOs, decorators Swagger) — nunca na lógica de negócio do use case/domínio.
55
-
56
- ## Não fazer
57
-
58
- - Não expor entidade de domínio/persistência como corpo de resposta.
59
- - Não reutilizar um DTO de criação para atualização parcial sem revisar `required`/`optional` de cada campo.
60
- - Não introduzir uma segunda estratégia de versionamento no mesmo projeto.
61
- - Não alterar regra de negócio do use case — este agente cuida apenas do contrato HTTP (ver skill `hexagonal` para a fronteira adapter/use case).
@@ -1,156 +0,0 @@
1
- ---
2
- name: hexagonal
3
- description: Diretrizes de arquitetura hexagonal (ports & adapters) aplicadas a projetos NestJS da DS9. Use ao criar, revisar ou refatorar módulos, definir camadas (domain/application/infrastructure), portas (interfaces) e adapters (controllers, repositórios, clients externos), ou quando o usuário pedir "arquitetura hexagonal", "ports and adapters" ou organização de um novo módulo/feature.
4
- ---
5
-
6
- # Arquitetura Hexagonal (Ports & Adapters) em NestJS
7
-
8
- Padrão arquitetural oficial da DS9 para backends NestJS. Objetivo: isolar as regras de negócio (domínio) de frameworks, banco de dados e protocolos de entrada/saída, tornando o core testável e substituível.
9
-
10
- ## Camadas e responsabilidades
11
-
12
- ```text
13
- src/
14
- └── <contexto>/ # bounded context / módulo de negócio
15
- ├── domain/ # núcleo — sem dependência de NestJS, TypeORM, HTTP, etc.
16
- │ ├── entities/ # entidades e value objects (regras e invariantes)
17
- │ ├── ports/ # interfaces (contratos) que o domínio precisa
18
- │ │ ├── in/ # casos de uso expostos (ex.: CreateOrderUseCase)
19
- │ │ └── out/ # dependências externas (ex.: OrderRepositoryPort)
20
- │ └── errors/ # exceções de domínio
21
- ├── application/ # orquestração dos casos de uso (services)
22
- │ └── use-cases/ # implementa as portas "in", orquestra portas "out"
23
- └── infrastructure/ # adapters concretos — tudo que é "detalhe"
24
- ├── adapters/
25
- │ ├── in/ # entrada: controllers REST, consumers de fila, resolvers GraphQL
26
- │ └── out/ # saída: repositórios TypeORM, HTTP clients, gateways de pagamento
27
- └── <contexto>.module.ts # module NestJS que faz o "wiring" via injeção de dependência
28
- ```
29
-
30
- ### Regra de dependência (inversão obrigatória)
31
-
32
- - `domain/` **não importa nada** de `infrastructure/`, `application/`, `@nestjs/*`, `typeorm`, `express`, etc. Só TypeScript puro.
33
- - `application/` depende apenas de `domain/` (entidades e ports).
34
- - `infrastructure/` depende de `domain/` e `application/`, nunca o contrário.
35
- - Sentido das setas: `infrastructure` → `application` → `domain`. Nunca inverter.
36
-
37
- ## Ports (interfaces)
38
-
39
- - Toda porta é uma `interface` TypeScript definida em `domain/ports/`.
40
- - Ports de saída (`out`) representam o que o domínio **precisa** do mundo externo (persistência, mensageria, serviços externos). Nomeie como `<Algo>Port` (ex.: `OrderRepositoryPort`, `PaymentGatewayPort`).
41
- - Ports de entrada (`in`) representam casos de uso que o domínio **oferece**. Nomeie como `<Ação><Entidade>UseCase` (ex.: `CreateOrderUseCase`).
42
- - Use `Symbol`/token de injeção dedicado para cada porta (evita colisão e permite múltiplas implementações):
43
-
44
- ```typescript
45
- // domain/ports/out/order-repository.port.ts
46
- export interface OrderRepositoryPort {
47
- save(order: Order): Promise<void>;
48
- findById(id: string): Promise<Order | null>;
49
- }
50
- export const ORDER_REPOSITORY_PORT = Symbol('ORDER_REPOSITORY_PORT');
51
- ```
52
-
53
- ## Adapters
54
-
55
- - **Adapter de entrada (`in`)**: controller HTTP, consumer de fila, cron job, resolver GraphQL. Responsabilidade única: traduzir o protocolo externo (DTO/request) para a chamada do caso de uso, e o retorno do caso de uso para a resposta do protocolo. Nunca contém regra de negócio.
56
- - **Adapter de saída (`out`)**: implementação concreta de uma port `out` (ex.: `TypeOrmOrderRepository implements OrderRepositoryPort`, usando as diretrizes da skill `typeorm`). Fica em `infrastructure/adapters/out/`.
57
- - DTOs de entrada/saída (`class-validator`/`class-transformer`) vivem no adapter de entrada, nunca no domínio.
58
-
59
- ```typescript
60
- // infrastructure/adapters/out/typeorm-order.repository.ts
61
- @Injectable()
62
- export class TypeOrmOrderRepository implements OrderRepositoryPort {
63
- constructor(@InjectRepository(OrderEntity) private readonly repo: Repository<OrderEntity>) {}
64
-
65
- async save(order: Order): Promise<void> {
66
- await this.repo.save(OrderMapper.toPersistence(order));
67
- }
68
-
69
- async findById(id: string): Promise<Order | null> {
70
- const row = await this.repo.findOneBy({ id });
71
- return row ? OrderMapper.toDomain(row) : null;
72
- }
73
- }
74
- ```
75
-
76
- ## Wiring (module NestJS)
77
-
78
- O module conecta a porta ao adapter concreto via `useClass`/token:
79
-
80
- ```typescript
81
- @Module({
82
- controllers: [OrderController],
83
- providers: [
84
- CreateOrderUseCase,
85
- { provide: ORDER_REPOSITORY_PORT, useClass: TypeOrmOrderRepository },
86
- ],
87
- })
88
- export class OrderModule {}
89
- ```
90
-
91
- Casos de uso recebem a porta por injeção, nunca a implementação concreta:
92
-
93
- ```typescript
94
- @Injectable()
95
- export class CreateOrderUseCase {
96
- constructor(@Inject(ORDER_REPOSITORY_PORT) private readonly orders: OrderRepositoryPort) {}
97
- }
98
- ```
99
-
100
- ## Transações cross-aggregate (Unit of Work)
101
-
102
- Quando um caso de uso precisa alterar **mais de um agregado atrás de ports diferentes** na mesma operação atômica (ex.: criar pedido + baixar estoque), não force esse acoplamento dentro de um único repositório concreto. Use uma port dedicada de coordenação transacional:
103
-
104
- ```typescript
105
- // domain/ports/out/unit-of-work.port.ts
106
- export interface UnitOfWorkPort {
107
- run<T>(work: () => Promise<T>): Promise<T>;
108
- }
109
- export const UNIT_OF_WORK_PORT = Symbol('UNIT_OF_WORK_PORT');
110
- ```
111
-
112
- - O use case injeta `UnitOfWorkPort` além das ports dos agregados envolvidos, e executa as chamadas das ports **dentro** do `run()`:
113
-
114
- ```typescript
115
- @Injectable()
116
- export class CreateOrderUseCase {
117
- constructor(
118
- @Inject(UNIT_OF_WORK_PORT) private readonly uow: UnitOfWorkPort,
119
- @Inject(ORDER_REPOSITORY_PORT) private readonly orders: OrderRepositoryPort,
120
- @Inject(STOCK_REPOSITORY_PORT) private readonly stock: StockRepositoryPort,
121
- ) {}
122
-
123
- async execute(cmd: CreateOrderCommand): Promise<Order> {
124
- return this.uow.run(async () => {
125
- const order = Order.create(cmd);
126
- await this.stock.decrement(cmd.productId, cmd.quantity);
127
- await this.orders.save(order);
128
- return order;
129
- });
130
- }
131
- }
132
- ```
133
-
134
- - O `UnitOfWorkPort` é a **única** peça com permissão de abrir uma transação técnica compartilhada; sua implementação concreta fica em `infrastructure/adapters/out/` (ver skill `typeorm` para a implementação com `DataSource.transaction()`).
135
- - Não implemente coordenação cross-aggregate escrevendo em duas entidades de tabelas diferentes dentro do repositório de um único agregado (ex.: `OrderRepository` não deve tocar `StockEntity`) — isso quebra a regra de "repositório só implementa a port do seu próprio agregado".
136
-
137
- ## Mappers
138
-
139
- - Toda tradução entre entidade de domínio e modelo de persistência (`OrderEntity` do TypeORM) passa por um `Mapper` dedicado (`toDomain` / `toPersistence`) em `infrastructure/adapters/out/mappers/`.
140
- - O domínio nunca conhece decorators de ORM (`@Entity`, `@Column`).
141
-
142
- ## Checklist ao criar/revisar um módulo
143
-
144
- 1. O domínio compila sem `@nestjs/*` e sem `typeorm` nos imports?
145
- 2. Toda dependência externa do domínio está atrás de uma `port`?
146
- 3. Regras de negócio e invariantes estão nas entidades/use-cases, não nos controllers?
147
- 4. Existe um teste unitário do use-case usando um fake/mock da port (sem subir banco/HTTP)?
148
- 5. O adapter de entrada só faz tradução de protocolo (sem `if` de regra de negócio)?
149
- 6. Erros de domínio são classes próprias (`OrderNotFoundError`), traduzidas para HTTP só no adapter de entrada (`ExceptionFilter`)?
150
-
151
- ## Não fazer
152
-
153
- - Não injete `Repository<Entity>` do TypeORM diretamente em use-cases ou entidades de domínio.
154
- - Não coloque decorators de validação HTTP (`class-validator`) em entidades de domínio.
155
- - Não exponha entidades de persistência (`*Entity`) fora de `infrastructure/`.
156
- - Não crie "god services" que misturam orquestração de use-case com acesso direto a dados.
@@ -1,88 +0,0 @@
1
- ---
2
- name: jest-testing
3
- description: Diretrizes de testes com Jest para projetos NestJS/TypeScript da DS9 — testes unitários de use cases/services, testes de integração de controllers, mocks de ports, cobertura e convenções de nomenclatura. Use ao escrever/revisar testes, criar mocks/fakes, configurar `jest.config`, ou quando o usuário pedir "teste unitário", "teste de integração" ou "cobertura de testes".
4
- ---
5
-
6
- # Jest — Diretrizes de Teste DS9
7
-
8
- Jest é o test runner padrão para projetos TypeScript/NestJS da DS9. Esta skill cobre convenções de teste; a organização em camadas segue a skill `hexagonal` e as convenções de framework seguem a skill `nestjs`.
9
-
10
- ## Estrutura e nomenclatura
11
-
12
- - Arquivo de teste ao lado do arquivo testado: `order.use-case.ts` → `order.use-case.spec.ts` (unitário) ou `*.e2e-spec.ts` (ponta a ponta, pasta `test/`).
13
- - `describe` nomeia a unidade testada (`describe('CreateOrderUseCase', ...)`); `it`/`test` descreve o comportamento esperado em linguagem natural (`it('deve lançar OrderNotFoundError quando o pedido não existe', ...)`), nunca o nome do método (`it('deve testar execute', ...)` é proibido).
14
- - Um `describe` por método/comportamento público quando a classe tiver múltiplos cenários relevantes.
15
-
16
- ## Pirâmide de testes no projeto
17
-
18
- | Camada | O que testar | Ferramenta | Sobe banco/HTTP real? |
19
- |---|---|---|---|
20
- | Domínio (entities, value objects) | Invariantes e regras de negócio puras | Jest puro | Não |
21
- | Use cases / application | Orquestração, chamando ports mockadas | Jest + mocks das `ports` | Não |
22
- | Adapters de saída (repositórios TypeORM) | Mapeamento correto entidade ↔ persistência | Jest + banco de teste (ou testcontainers, ver skill `docker`) | Sim (banco de teste isolado) |
23
- | Controllers / adapters de entrada | Contrato HTTP: status, shape, validação | Jest + `supertest` (`*.e2e-spec.ts`) | Sim (app NestJS em memória, banco de teste) |
24
-
25
- A maior parte dos testes deve estar nas duas primeiras camadas (rápidos, sem I/O); testes de integração/e2e cobrem os caminhos críticos, não todo caso de borda.
26
-
27
- ## Mocks de Ports (hexagonal)
28
-
29
- - Use cases são testados mockando as `ports` (interfaces), nunca subindo banco/HTTP real. Prefira um *fake* em memória simples a um mock excessivamente configurado quando a port tem poucos métodos:
30
-
31
- ```typescript
32
- class InMemoryOrderRepository implements OrderRepositoryPort {
33
- private readonly rows = new Map<string, Order>();
34
- async save(order: Order): Promise<void> { this.rows.set(order.id, order); }
35
- async findById(id: string): Promise<Order | null> { return this.rows.get(id) ?? null; }
36
- }
37
-
38
- describe('CreateOrderUseCase', () => {
39
- it('deve salvar o pedido com status PENDING', async () => {
40
- const repo = new InMemoryOrderRepository();
41
- const useCase = new CreateOrderUseCase(repo);
42
-
43
- const order = await useCase.execute({ customerId: 'c1', items: [...] });
44
-
45
- expect(order.status).toBe('PENDING');
46
- });
47
- });
48
- ```
49
-
50
- - Para mocks com `jest.fn()`, tipar sempre contra a interface da port (`jest.Mocked<OrderRepositoryPort>`), nunca `any`.
51
- - Não mocke o que você não é dono (bibliotecas externas/SDKs) diretamente nos testes de domínio — isole-as atrás de uma port e mocke a port.
52
-
53
- ## Testes de controller (NestJS)
54
-
55
- - Monte o módulo real via `Test.createTestingModule({ imports: [OrderModule] })`, sobrescrevendo apenas as dependências externas (`overrideProvider(ORDER_REPOSITORY_PORT)`), para validar o wiring de DI de verdade.
56
- - Use `supertest` contra a instância HTTP da aplicação Nest (`app.getHttpServer()`), verificando status code, shape do body e erros de validação (400) com payload inválido.
57
-
58
- ## Assertions
59
-
60
- - Prefira matchers específicos (`toEqual`, `toMatchObject`, `toThrow(OrderNotFoundError)`) a `toBeTruthy()`/`toBeDefined()` genéricos quando o valor exato importa.
61
- - Para erros de domínio, sempre assertar a **classe/tipo** do erro (`expect(fn).rejects.toThrow(OrderNotFoundError)`), não só a mensagem em texto.
62
- - Nunca deixe um teste sem nenhuma asserção (`it` vazio/`TODO`) marcado como passando; use `it.todo('...')` ou `.skip` explicitamente se o teste ainda não foi implementado.
63
-
64
- ## Isolamento e determinismo
65
-
66
- - Cada teste deve poder rodar isoladamente e em qualquer ordem — sem estado global compartilhado entre `it`s (use `beforeEach` para recriar fakes/mocks).
67
- - Nunca dependa de `Date.now()`/hora real sem controlar o clock (`jest.useFakeTimers()`); nunca dependa de ordem de `Promise`/timing sem `await` explícito.
68
- - Testes de integração usam um banco de dados de teste próprio (schema isolado por execução), nunca o banco de dev/homolog compartilhado. Se o projeto usar testcontainers para subir esse banco, siga a skill `docker` para a imagem/configuração do container.
69
-
70
- ## Cobertura
71
-
72
- - Cobertura mínima definida pelo projeto é validada no CI (`jest --coverage`); regras de negócio no domínio/use-cases devem ter cobertura próxima de 100% dos branches (if/else, early-return de erro).
73
- - Cobertura alta não substitui qualidade das asserções — não escreva teste só para "bater número" sem validar comportamento real.
74
-
75
- ## Checklist ao escrever/revisar testes
76
-
77
- 1. O teste de use case mocka a port (sem banco/HTTP real)?
78
- 2. Nomes de `describe`/`it` descrevem comportamento, não implementação?
79
- 3. Erros de domínio são testados por tipo/classe, não só mensagem?
80
- 4. O teste é determinístico (sem dependência de tempo real/ordem)?
81
- 5. Testes de controller cobrem pelo menos um caso de validação inválida (400) além do caminho feliz?
82
-
83
- ## Não fazer
84
-
85
- - Não usar `any` em mocks de ports.
86
- - Não deixar `it.skip`/`xit` sem um comentário explicando o motivo e um acompanhamento (issue/TODO).
87
- - Não testar detalhes de implementação privada (métodos privados) — teste pelo comportamento público.
88
- - Não compartilhar estado mutável entre testes sem reset em `beforeEach`/`afterEach`.