efarmz-slackbot-data 1.0.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 (131) hide show
  1. package/.clever.json +12 -0
  2. package/.dockerignore +13 -0
  3. package/.env.example +28 -0
  4. package/.github/workflows/deploy-production.yaml +34 -0
  5. package/.prettierrc +6 -0
  6. package/.tasks/F1-bootstrap.md +110 -0
  7. package/.tasks/F2-domain-layer.md +173 -0
  8. package/.tasks/F3-application-layer.md +166 -0
  9. package/.tasks/F4-infrastructure-layer.md +229 -0
  10. package/.tasks/F5-config-main.md +160 -0
  11. package/.tasks/F6-schemas-deployment.md +129 -0
  12. package/CLAUDE.md +163 -0
  13. package/Dockerfile +15 -0
  14. package/PRD.md +119 -0
  15. package/docs/schemas/.gitkeep +0 -0
  16. package/docs/schemas/_guidelines.md +89 -0
  17. package/docs/schemas/efarmz_db.md +759 -0
  18. package/docs/schemas/example.md +16 -0
  19. package/eslint.config.mjs +18 -0
  20. package/package.json +54 -0
  21. package/releaserc.json +15 -0
  22. package/src/.gitkeep +0 -0
  23. package/src/application/agent/.gitkeep +0 -0
  24. package/src/application/agent/AgentContext.test.ts +263 -0
  25. package/src/application/agent/AgentContext.ts +93 -0
  26. package/src/application/agent/AgentLoop.test.ts +275 -0
  27. package/src/application/agent/AgentLoop.ts +101 -0
  28. package/src/application/agent/AgentRunResult.ts +11 -0
  29. package/src/application/agent/LLMMessage.ts +16 -0
  30. package/src/application/agent/tools/RunSqlTool.ts +23 -0
  31. package/src/application/formatting/.gitkeep +0 -0
  32. package/src/application/formatting/CsvRenderer.test.ts +162 -0
  33. package/src/application/formatting/CsvRenderer.ts +34 -0
  34. package/src/application/formatting/MonospaceTableRenderer.test.ts +129 -0
  35. package/src/application/formatting/MonospaceTableRenderer.ts +58 -0
  36. package/src/application/formatting/RenderedResponse.ts +7 -0
  37. package/src/application/formatting/ResponseRenderer.test.ts +159 -0
  38. package/src/application/formatting/ResponseRenderer.ts +39 -0
  39. package/src/application/formatting/ScalarRenderer.test.ts +36 -0
  40. package/src/application/formatting/ScalarRenderer.ts +12 -0
  41. package/src/application/usecases/.gitkeep +0 -0
  42. package/src/application/usecases/AnswerQuestion.test.ts +362 -0
  43. package/src/application/usecases/AnswerQuestion.ts +69 -0
  44. package/src/application/usecases/ParseQuestion.test.ts +39 -0
  45. package/src/application/usecases/ParseQuestion.ts +9 -0
  46. package/src/config/.gitkeep +0 -0
  47. package/src/config/Container.test.ts +35 -0
  48. package/src/config/Container.ts +74 -0
  49. package/src/config/constants.ts +9 -0
  50. package/src/config/env.test.ts +103 -0
  51. package/src/config/env.ts +41 -0
  52. package/src/domain/entities/.gitkeep +0 -0
  53. package/src/domain/entities/Conversation.test.ts +69 -0
  54. package/src/domain/entities/Conversation.ts +26 -0
  55. package/src/domain/entities/ConversationMessage.test.ts +49 -0
  56. package/src/domain/entities/ConversationMessage.ts +18 -0
  57. package/src/domain/entities/index.ts +2 -0
  58. package/src/domain/errors/.gitkeep +0 -0
  59. package/src/domain/errors/AgentLoopExceededError.ts +12 -0
  60. package/src/domain/errors/DomainError.test.ts +106 -0
  61. package/src/domain/errors/DomainError.ts +11 -0
  62. package/src/domain/errors/InvalidSqlError.ts +15 -0
  63. package/src/domain/errors/LLMError.ts +15 -0
  64. package/src/domain/errors/SchemaLoadError.ts +15 -0
  65. package/src/domain/errors/SqlExecutionError.ts +15 -0
  66. package/src/domain/errors/index.ts +15 -0
  67. package/src/domain/ports/.gitkeep +0 -0
  68. package/src/domain/ports/AdminLogger.ts +16 -0
  69. package/src/domain/ports/ConversationRepository.ts +10 -0
  70. package/src/domain/ports/LLMProvider.ts +33 -0
  71. package/src/domain/ports/Logger.ts +8 -0
  72. package/src/domain/ports/SchemaCatalog.ts +5 -0
  73. package/src/domain/ports/SlackMessenger.ts +8 -0
  74. package/src/domain/ports/SqlExecutor.ts +8 -0
  75. package/src/domain/ports/SqlValidator.ts +5 -0
  76. package/src/domain/ports/index.ts +17 -0
  77. package/src/domain/value-objects/.gitkeep +0 -0
  78. package/src/domain/value-objects/LLMProviderName.ts +6 -0
  79. package/src/domain/value-objects/QueryResult.test.ts +51 -0
  80. package/src/domain/value-objects/QueryResult.ts +18 -0
  81. package/src/domain/value-objects/Question.test.ts +59 -0
  82. package/src/domain/value-objects/Question.ts +22 -0
  83. package/src/domain/value-objects/QuestionFlags.test.ts +59 -0
  84. package/src/domain/value-objects/QuestionFlags.ts +18 -0
  85. package/src/domain/value-objects/ResponseRendering.ts +7 -0
  86. package/src/domain/value-objects/SqlQuery.test.ts +40 -0
  87. package/src/domain/value-objects/SqlQuery.ts +12 -0
  88. package/src/domain/value-objects/ThreadId.test.ts +68 -0
  89. package/src/domain/value-objects/ThreadId.ts +27 -0
  90. package/src/domain/value-objects/index.ts +13 -0
  91. package/src/infrastructure/llm/.gitkeep +0 -0
  92. package/src/infrastructure/llm/AnthropicLLMProvider.test.ts +229 -0
  93. package/src/infrastructure/llm/AnthropicLLMProvider.ts +45 -0
  94. package/src/infrastructure/llm/index.ts +4 -0
  95. package/src/infrastructure/llm/mappers/AnthropicMessageMapper.test.ts +173 -0
  96. package/src/infrastructure/llm/mappers/AnthropicMessageMapper.ts +34 -0
  97. package/src/infrastructure/llm/prompts/SystemPromptBuilder.test.ts +41 -0
  98. package/src/infrastructure/llm/prompts/SystemPromptBuilder.ts +31 -0
  99. package/src/infrastructure/llm/prompts/ToolDefinitions.ts +7 -0
  100. package/src/infrastructure/logging/.gitkeep +0 -0
  101. package/src/infrastructure/logging/PinoLogger.test.ts +59 -0
  102. package/src/infrastructure/logging/PinoLogger.ts +28 -0
  103. package/src/infrastructure/logging/index.ts +1 -0
  104. package/src/infrastructure/persistence/.gitkeep +0 -0
  105. package/src/infrastructure/persistence/InMemoryConversationRepository.test.ts +325 -0
  106. package/src/infrastructure/persistence/InMemoryConversationRepository.ts +69 -0
  107. package/src/infrastructure/persistence/PostgresPoolFactory.ts +11 -0
  108. package/src/infrastructure/persistence/PostgresSqlExecutor.test.ts +130 -0
  109. package/src/infrastructure/persistence/PostgresSqlExecutor.ts +34 -0
  110. package/src/infrastructure/persistence/index.ts +3 -0
  111. package/src/infrastructure/schemas/.gitkeep +0 -0
  112. package/src/infrastructure/schemas/FileSystemSchemaCatalog.test.ts +163 -0
  113. package/src/infrastructure/schemas/FileSystemSchemaCatalog.ts +35 -0
  114. package/src/infrastructure/schemas/index.ts +4 -0
  115. package/src/infrastructure/slack/.gitkeep +0 -0
  116. package/src/infrastructure/slack/BoltSlackMessenger.test.ts +59 -0
  117. package/src/infrastructure/slack/BoltSlackMessenger.ts +36 -0
  118. package/src/infrastructure/slack/SlackAdminLogger.test.ts +54 -0
  119. package/src/infrastructure/slack/SlackAdminLogger.ts +27 -0
  120. package/src/infrastructure/slack/SlackApp.ts +9 -0
  121. package/src/infrastructure/slack/handlers/AppMentionHandler.ts +52 -0
  122. package/src/infrastructure/slack/handlers/DirectMessageHandler.ts +65 -0
  123. package/src/infrastructure/slack/index.ts +5 -0
  124. package/src/infrastructure/sql/.gitkeep +0 -0
  125. package/src/infrastructure/sql/RegexSqlValidator.test.ts +242 -0
  126. package/src/infrastructure/sql/RegexSqlValidator.ts +53 -0
  127. package/src/infrastructure/sql/index.ts +1 -0
  128. package/src/main.ts +19 -0
  129. package/tsconfig.json +23 -0
  130. package/vitest.config.ts +15 -0
  131. package/vitest.setup.ts +23 -0
@@ -0,0 +1,229 @@
1
+ ---
2
+ title: "F4 — Infrastructure Layer"
3
+ type: feature
4
+ status: todo
5
+ prd: .tasks/PRD.md
6
+ depends_on: F3
7
+ github_issue: 29
8
+ ---
9
+
10
+ # F4 — Infrastructure Layer
11
+
12
+ ## Objectif
13
+
14
+ Implémenter tous les adapters concrets qui réalisent les ports définis dans `src/domain/ports/`. Cette couche peut importer les SDKs tiers (`pg`, `@anthropic-ai/sdk`, `@slack/bolt`, `pino`) mais ne doit jamais importer `src/application/`.
15
+
16
+ ## Fichiers à créer
17
+
18
+ ### SQL (`src/infrastructure/sql/`)
19
+
20
+ **`RegexSqlValidator.ts`** — implémente `SqlValidator`
21
+
22
+ Règles de validation (dans l'ordre) :
23
+ 1. Trim + normalisation whitespace
24
+ 2. Doit commencer par `SELECT` ou `WITH` (insensitive) → sinon `InvalidSqlError`
25
+ 3. Refus des keywords mutables : `\b(INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|TRUNCATE|GRANT|REVOKE|COPY|CALL|VACUUM|MERGE)\b` (insensitive) → `InvalidSqlError`
26
+ 4. Refus de `;` sauf en toute fin de requête (optionnel) → `InvalidSqlError`
27
+ 5. Si absent : append `LIMIT {SQL_MAX_ROWS}` avant le `;` final ou en fin de chaîne
28
+
29
+ Retourne le SQL nettoyé (string), ou throw `InvalidSqlError`.
30
+
31
+ Tests exhaustifs requis (voir ci-dessous).
32
+
33
+ ### Persistence (`src/infrastructure/persistence/`)
34
+
35
+ **`PostgresPoolFactory.ts`**
36
+ - Crée un `Pool` pg depuis `DATABASE_URL`
37
+ - `idleTimeoutMillis: 30000`, `connectionTimeoutMillis: 5000`
38
+ - Export : factory function `createPool(databaseUrl: string): Pool`
39
+
40
+ **`PostgresSqlExecutor.ts`** — implémente `SqlExecutor`
41
+ ```typescript
42
+ class PostgresSqlExecutor implements SqlExecutor {
43
+ constructor(private readonly pool: Pool, private readonly timeoutMs: number) {}
44
+
45
+ async execute(query: SqlQuery): Promise<QueryResult> {
46
+ const client = await this.pool.connect();
47
+ try {
48
+ await client.query(`SET statement_timeout = ${this.timeoutMs}`);
49
+ const result = await client.query(query.raw);
50
+ return QueryResult.create(
51
+ result.fields.map(f => f.name),
52
+ result.rows,
53
+ );
54
+ } finally {
55
+ client.release();
56
+ }
57
+ }
58
+ }
59
+ ```
60
+ - Wrap les erreurs PG en `SqlExecutionError`
61
+
62
+ **`InMemoryConversationRepository.ts`** — implémente `ConversationRepository`
63
+ - Stockage : `Map<ThreadId, { conversation: Conversation; lastAccess: number }>`
64
+ - LRU : si `Map.size >= CONVERSATION_MAX_THREADS`, supprimer l'entrée avec le `lastAccess` le plus ancien
65
+ - TTL : au `get`, vérifier si `Date.now() - lastAccess > CONVERSATION_TTL_MS` → retourner `null` et purger
66
+ - `save` met à jour `lastAccess`
67
+
68
+ ### Schemas (`src/infrastructure/schemas/`)
69
+
70
+ **`FileSystemSchemaCatalog.ts`** — implémente `SchemaCatalog`
71
+ - Au constructeur : lire tous les `*.md` dans `SCHEMAS_DIR` (sync, fail-fast si erreur → `SchemaLoadError`)
72
+ - Concatène le contenu de tous les fichiers avec séparateurs
73
+ - `getDocumentation()` retourne la chaîne concaténée (mémoïsée)
74
+
75
+ ### LLM (`src/infrastructure/llm/`)
76
+
77
+ **`prompts/SystemPromptBuilder.ts`**
78
+ ```typescript
79
+ class SystemPromptBuilder {
80
+ static build(schemaDoc: string): AnthropicSystemContent {
81
+ return [
82
+ {
83
+ type: 'text',
84
+ text: SYSTEM_PROMPT_HEADER,
85
+ },
86
+ {
87
+ type: 'text',
88
+ text: schemaDoc,
89
+ cache_control: { type: 'ephemeral' },
90
+ },
91
+ ];
92
+ }
93
+ }
94
+ ```
95
+ Le `SYSTEM_PROMPT_HEADER` décrit le rôle du bot, les règles (SELECT uniquement, toujours utiliser `run_sql`), et le format de réponse attendu.
96
+
97
+ **`prompts/ToolDefinitions.ts`**
98
+ - Export de la définition JSON du tool `run_sql` (réexporte depuis `application/agent/tools/RunSqlTool.ts`)
99
+
100
+ **`mappers/AnthropicMessageMapper.ts`**
101
+ - Convertit `LLMMessage[]` (domain) → `Anthropic.MessageParam[]` (SDK)
102
+ - Convertit `Anthropic.Message` (SDK) → `LLMResponse` (domain)
103
+
104
+ **`AnthropicLLMProvider.ts`** — implémente `LLMProvider`
105
+ ```typescript
106
+ class AnthropicLLMProvider implements LLMProvider {
107
+ constructor(
108
+ private readonly client: Anthropic,
109
+ private readonly model: string,
110
+ private readonly schemaCatalog: SchemaCatalog,
111
+ ) {}
112
+
113
+ async generate(request: LLMRequest): Promise<LLMResponse> {
114
+ const response = await this.client.messages.create({
115
+ model: this.model,
116
+ max_tokens: 4096,
117
+ system: SystemPromptBuilder.build(this.schemaCatalog.getDocumentation()),
118
+ messages: AnthropicMessageMapper.toSDK(request.messages),
119
+ tools: request.tools,
120
+ });
121
+ return AnthropicMessageMapper.fromSDK(response);
122
+ }
123
+ }
124
+ ```
125
+ - Wrap les erreurs Anthropic en `LLMError`
126
+
127
+ ### Slack (`src/infrastructure/slack/`)
128
+
129
+ **`SlackApp.ts`** — Setup Bolt
130
+ - `App` avec `token` + `signingSecret` + `receiver: ExpressReceiver`
131
+ - Export : `{ app, receiver }`
132
+
133
+ **`BoltSlackMessenger.ts`** — implémente `SlackMessenger`
134
+ ```typescript
135
+ class BoltSlackMessenger implements SlackMessenger {
136
+ async post(threadId: ThreadId, text: string): Promise<void> {
137
+ await this.client.chat.postMessage({
138
+ channel: threadId.channelId,
139
+ thread_ts: threadId.threadTs,
140
+ text,
141
+ });
142
+ }
143
+
144
+ async postCsv(threadId, text, csvBuffer, filename): Promise<void> {
145
+ await this.client.chat.postMessage({ ... text ... });
146
+ await this.client.files.uploadV2({
147
+ channel_id: threadId.channelId,
148
+ thread_ts: threadId.threadTs,
149
+ filename,
150
+ file: csvBuffer,
151
+ });
152
+ }
153
+ }
154
+ ```
155
+
156
+ > Note : `ThreadId` doit encoder `channelId` + `threadTs` pour permettre le posting. Adapter la définition du VO si nécessaire.
157
+
158
+ **`SlackAdminLogger.ts`** — implémente `AdminLogger`
159
+ - Si `SLACK_ADMIN_CHANNEL_ID` absent → no-op (log warning au boot)
160
+ - Post un message formaté dans le channel admin avec : user, question, sqls[], rowCount, durationMs, tokens
161
+
162
+ **`handlers/AppMentionHandler.ts`**
163
+ ```typescript
164
+ // app.event('app_mention', async ({ event, ack, say }) => {
165
+ // ack(); // 200 OK immédiat
166
+ // const dedupKey = event.event_id;
167
+ // if (eventCache.has(dedupKey)) return;
168
+ // eventCache.set(dedupKey, true, TTL_5MIN);
169
+ // setImmediate(async () => { await answerQuestion.execute(...) });
170
+ // });
171
+ ```
172
+ - Ack immédiat
173
+ - Déduplication par `event_id` (cache Map avec TTL 5 min)
174
+ - Traitement en `setImmediate` / promise détachée
175
+ - Catch-all : log + message Slack générique d'erreur
176
+
177
+ **`handlers/DirectMessageHandler.ts`**
178
+ - Même logique que `AppMentionHandler` pour `message.im`
179
+ - Filtrer les bots (`event.bot_id` présent → ignorer)
180
+
181
+ ### Logging (`src/infrastructure/logging/`)
182
+
183
+ **`PinoLogger.ts`** — implémente `Logger`
184
+ ```typescript
185
+ class PinoLogger implements Logger {
186
+ private readonly pino: pino.Logger;
187
+ constructor(level: string) {
188
+ this.pino = pino({ level });
189
+ }
190
+ debug(msg, meta?) { this.pino.debug(meta ?? {}, msg); }
191
+ info(msg, meta?) { this.pino.info(meta ?? {}, msg); }
192
+ warn(msg, meta?) { this.pino.warn(meta ?? {}, msg); }
193
+ error(msg, meta?) { this.pino.error(meta ?? {}, msg); }
194
+ }
195
+ ```
196
+
197
+ ## Tests unitaires (`src/infrastructure/__tests__/`)
198
+
199
+ **`RegexSqlValidator.test.ts`** — tests exhaustifs
200
+ - `SELECT * FROM orders` → valide
201
+ - `WITH cte AS (...) SELECT ...` → valide
202
+ - `SELECT ...; DROP TABLE orders` → `InvalidSqlError`
203
+ - `INSERT INTO orders ...` → `InvalidSqlError`
204
+ - `UPDATE orders SET ...` → `InvalidSqlError`
205
+ - `DELETE FROM orders ...` → `InvalidSqlError`
206
+ - `DROP TABLE orders` → `InvalidSqlError`
207
+ - `CREATE TABLE ...` → `InvalidSqlError`
208
+ - `ALTER TABLE ...` → `InvalidSqlError`
209
+ - `TRUNCATE orders` → `InvalidSqlError`
210
+ - `GRANT SELECT ...` → `InvalidSqlError`
211
+ - `select * from orders where name like 'drop%'` → valide (keyword dans string)
212
+ - SQL sans LIMIT → LIMIT ajouté
213
+ - SQL avec LIMIT existant → LIMIT non-dupliqué
214
+ - SQL avec `;` au milieu → `InvalidSqlError`
215
+
216
+ **`InMemoryConversationRepository.test.ts`**
217
+ - `get` après TTL → retourne `null`
218
+ - `save` → LRU éviction si `MAX_THREADS` atteint
219
+ - `purge` → supprime l'entrée
220
+
221
+ ## Acceptance Criteria
222
+
223
+ - [ ] `yarn typecheck` passe
224
+ - [ ] `yarn test` : tests `RegexSqlValidator` et `InMemoryConversationRepository` passent
225
+ - [ ] `RegexSqlValidator` couvre tous les keywords interdits
226
+ - [ ] `PostgresSqlExecutor` set `statement_timeout` avant chaque requête
227
+ - [ ] `AnthropicLLMProvider` utilise `cache_control: { type: 'ephemeral' }` sur le bloc system
228
+ - [ ] Handlers Slack ack < 3s (traitement en `setImmediate`)
229
+ - [ ] Déduplication par `event_id` dans les deux handlers
@@ -0,0 +1,160 @@
1
+ ---
2
+ title: "F5 — Config & Composition Root"
3
+ type: feature
4
+ status: todo
5
+ prd: .tasks/PRD.md
6
+ depends_on: F4
7
+ ---
8
+
9
+ # F5 — Config & Composition Root
10
+
11
+ ## Objectif
12
+
13
+ Assembler toutes les couches via la DI manuelle (`Container`), valider les variables d'environnement au boot, et créer l'entry point `main.ts`. C'est le seul endroit où les implémentations concrètes sont instanciées et câblées ensemble.
14
+
15
+ ## Fichiers à créer
16
+
17
+ ### `src/config/env.ts`
18
+
19
+ Validation via zod au boot — fail-fast si variable manquante ou invalide.
20
+
21
+ ```typescript
22
+ import { z } from 'zod';
23
+
24
+ const envSchema = z.object({
25
+ PORT: z.coerce.number().default(3000),
26
+ NODE_ENV: z.enum([`development`, `production`, `test`]).default(`development`),
27
+ LOG_LEVEL: z.enum([`debug`, `info`, `warn`, `error`]).default(`info`),
28
+
29
+ SLACK_BOT_TOKEN: z.string().min(1),
30
+ SLACK_SIGNING_SECRET: z.string().min(1),
31
+ SLACK_ADMIN_CHANNEL_ID: z.string().optional(),
32
+
33
+ LLM_PROVIDER: z.enum([`anthropic`]).default(`anthropic`),
34
+ LLM_MODEL: z.string().default(`claude-haiku-4-5-20251001`),
35
+ ANTHROPIC_API_KEY: z.string().min(1),
36
+ AGENT_MAX_TURNS: z.coerce.number().default(5),
37
+
38
+ DATABASE_URL: z.string().url(),
39
+ SQL_TIMEOUT_MS: z.coerce.number().default(30000),
40
+ SQL_MAX_ROWS: z.coerce.number().default(100),
41
+ SQL_CSV_THRESHOLD: z.coerce.number().default(15),
42
+
43
+ CONVERSATION_MAX_MESSAGES: z.coerce.number().default(8),
44
+ CONVERSATION_TTL_MS: z.coerce.number().default(3600000),
45
+ CONVERSATION_MAX_THREADS: z.coerce.number().default(200),
46
+
47
+ SCHEMAS_DIR: z.string().default(`./docs/schemas`),
48
+ });
49
+
50
+ const parsed = envSchema.safeParse(process.env);
51
+ if (!parsed.success) {
52
+ console.error(`Invalid environment variables:`, parsed.error.format());
53
+ process.exit(1);
54
+ }
55
+
56
+ export const env = parsed.data;
57
+ ```
58
+
59
+ ### `src/config/constants.ts`
60
+
61
+ Constantes dérivées de `env` ou fixes :
62
+
63
+ ```typescript
64
+ export const SYSTEM_PROMPT_HEADER = `...`; // prompt system du bot
65
+ ```
66
+
67
+ ### `src/config/Container.ts`
68
+
69
+ Composition root — instancie et câble toutes les dépendances.
70
+
71
+ ```typescript
72
+ import { env } from './env.js';
73
+ import { PinoLogger } from '@/infrastructure/logging/PinoLogger.js';
74
+ import { FileSystemSchemaCatalog } from '@/infrastructure/schemas/FileSystemSchemaCatalog.js';
75
+ import { RegexSqlValidator } from '@/infrastructure/sql/RegexSqlValidator.js';
76
+ import { createPool } from '@/infrastructure/persistence/PostgresPoolFactory.js';
77
+ import { PostgresSqlExecutor } from '@/infrastructure/persistence/PostgresSqlExecutor.js';
78
+ import { InMemoryConversationRepository } from '@/infrastructure/persistence/InMemoryConversationRepository.js';
79
+ import { AnthropicLLMProvider } from '@/infrastructure/llm/AnthropicLLMProvider.js';
80
+ import { BoltSlackMessenger } from '@/infrastructure/slack/BoltSlackMessenger.js';
81
+ import { SlackAdminLogger } from '@/infrastructure/slack/SlackAdminLogger.js';
82
+ import { AgentLoop } from '@/application/agent/AgentLoop.js';
83
+ import { ResponseRenderer } from '@/application/formatting/ResponseRenderer.js';
84
+ import { AnswerQuestion } from '@/application/usecases/AnswerQuestion.js';
85
+ import { ParseQuestion } from '@/application/usecases/ParseQuestion.js';
86
+ import Anthropic from '@anthropic-ai/sdk';
87
+
88
+ export function createContainer() {
89
+ const logger = new PinoLogger(env.LOG_LEVEL);
90
+
91
+ const schemaCatalog = new FileSystemSchemaCatalog(env.SCHEMAS_DIR, logger);
92
+
93
+ const sqlValidator = new RegexSqlValidator(env.SQL_MAX_ROWS);
94
+ const pool = createPool(env.DATABASE_URL);
95
+ const sqlExecutor = new PostgresSqlExecutor(pool, env.SQL_TIMEOUT_MS);
96
+ const conversationRepository = new InMemoryConversationRepository(
97
+ env.CONVERSATION_TTL_MS,
98
+ env.CONVERSATION_MAX_THREADS,
99
+ env.CONVERSATION_MAX_MESSAGES,
100
+ );
101
+
102
+ const anthropicClient = new Anthropic({ apiKey: env.ANTHROPIC_API_KEY });
103
+ const llmProvider = new AnthropicLLMProvider(anthropicClient, env.LLM_MODEL, schemaCatalog);
104
+
105
+ // Slack (app + client instanciés avant pour les messengers)
106
+ const { app } = createSlackApp(env); // setup Bolt
107
+ const slackMessenger = new BoltSlackMessenger(app.client);
108
+ const adminLogger = new SlackAdminLogger(app.client, env.SLACK_ADMIN_CHANNEL_ID, logger);
109
+
110
+ const agentLoop = new AgentLoop(llmProvider, sqlValidator, sqlExecutor, schemaCatalog, env.AGENT_MAX_TURNS);
111
+ const responseRenderer = new ResponseRenderer(env.SQL_CSV_THRESHOLD);
112
+
113
+ const answerQuestion = new AnswerQuestion(
114
+ conversationRepository,
115
+ agentLoop,
116
+ responseRenderer,
117
+ slackMessenger,
118
+ adminLogger,
119
+ logger,
120
+ );
121
+ const parseQuestion = new ParseQuestion();
122
+
123
+ return { app, answerQuestion, parseQuestion, logger };
124
+ }
125
+ ```
126
+
127
+ ### `src/main.ts`
128
+
129
+ Entry point minimal — boot, setup handlers, start server.
130
+
131
+ ```typescript
132
+ import { createContainer } from '@/config/Container.js';
133
+ import { AppMentionHandler } from '@/infrastructure/slack/handlers/AppMentionHandler.js';
134
+ import { DirectMessageHandler } from '@/infrastructure/slack/handlers/DirectMessageHandler.js';
135
+ import { env } from '@/config/env.js';
136
+
137
+ async function main() {
138
+ const { app, answerQuestion, parseQuestion, logger } = createContainer();
139
+
140
+ AppMentionHandler.register(app, answerQuestion, parseQuestion, logger);
141
+ DirectMessageHandler.register(app, answerQuestion, parseQuestion, logger);
142
+
143
+ await app.start(env.PORT);
144
+ logger.info(`Bot started on port ${env.PORT}`);
145
+ }
146
+
147
+ main().catch((err) => {
148
+ console.error(`Fatal error at startup:`, err);
149
+ process.exit(1);
150
+ });
151
+ ```
152
+
153
+ ## Acceptance Criteria
154
+
155
+ - [ ] `yarn typecheck` passe
156
+ - [ ] `yarn build` produit `dist/main.js` sans erreur
157
+ - [ ] `yarn start` démarre sur le port configuré (avec env valide)
158
+ - [ ] Boot échoue immédiatement si une variable requise est manquante (exit code 1)
159
+ - [ ] `FileSystemSchemaCatalog` lève `SchemaLoadError` au boot si `SCHEMAS_DIR` invalide
160
+ - [ ] Aucune instanciation de classe concrète en dehors de `Container.ts` et `main.ts`
@@ -0,0 +1,129 @@
1
+ ---
2
+ title: "F6 — Documentation des schémas & Déploiement"
3
+ type: feature
4
+ status: todo
5
+ prd: .tasks/PRD.md
6
+ depends_on: F5
7
+ ---
8
+
9
+ # F6 — Documentation des schémas & Déploiement
10
+
11
+ ## Objectif
12
+
13
+ Rédiger la documentation des schémas PostgreSQL efarmz (chargée dans le system prompt du LLM), finaliser le Dockerfile pour Clever Cloud, et rédiger le README de setup. Aucun code de production à écrire dans cette feature.
14
+
15
+ ## Livrables
16
+
17
+ ### 1. Documentation des schémas (`docs/schemas/`)
18
+
19
+ Un fichier `.md` par schéma PostgreSQL. Format attendu par `FileSystemSchemaCatalog` :
20
+
21
+ ```
22
+ docs/schemas/
23
+ ├── efarmz_db.md # schéma principal (commandes, clients, produits, …)
24
+ ├── efarmz_intercom.md # schéma Intercom (conversations, contacts)
25
+ └── ... # autres schémas si nécessaire
26
+ ```
27
+
28
+ **Format de chaque fichier** (exemple `efarmz_db.md`) :
29
+
30
+ ```markdown
31
+ # Schéma efarmz_db
32
+
33
+ ## Table: orders
34
+ Description courte du contenu de la table.
35
+
36
+ | Colonne | Type | Description |
37
+ |----------------|-------------|--------------------------------------|
38
+ | id | uuid | Identifiant unique de la commande |
39
+ | customer_id | uuid | Référence vers customers.id |
40
+ | status | varchar | Statut : pending, confirmed, shipped |
41
+ | total_amount | numeric | Montant total TTC en euros |
42
+ | created_at | timestamptz | Date de création |
43
+
44
+ ## Table: customers
45
+ ...
46
+ ```
47
+
48
+ **Contenu fourni par l'utilisateur** — ce fichier est un placeholder vide à compléter.
49
+
50
+ > Important : plus la doc est précise (descriptions des colonnes, valeurs d'enum, relations FK, exemples de valeurs), meilleure sera la qualité du SQL généré.
51
+
52
+ ### 2. `Dockerfile` finalisé
53
+
54
+ Multi-stage, Node 22 slim, copie `docs/` dans l'image runtime :
55
+
56
+ ```dockerfile
57
+ FROM node:22-slim AS builder
58
+ WORKDIR /app
59
+ COPY package*.json ./
60
+ RUN npm ci --omit=dev
61
+ COPY tsconfig.json ./
62
+ COPY src ./src
63
+ RUN npm run build
64
+
65
+ FROM node:22-slim AS runtime
66
+ WORKDIR /app
67
+ ENV NODE_ENV=production
68
+ COPY --from=builder /app/dist ./dist
69
+ COPY --from=builder /app/node_modules ./node_modules
70
+ COPY docs ./docs
71
+ EXPOSE 3000
72
+ CMD ["node", "dist/main.js"]
73
+ ```
74
+
75
+ ### 3. Clever Cloud — configuration
76
+
77
+ Fichier `clevercloud/` ou variables dans l'interface Clever Cloud :
78
+
79
+ - **Type** : Node.js ou Docker (préférer Docker pour le contrôle de la version Node)
80
+ - **Build** : `docker build`
81
+ - **Port** : 3000 (variable `PORT`)
82
+ - **Toutes les variables d'env** depuis `.env.example` à configurer dans l'interface CC
83
+
84
+ ### 4. Rôle PostgreSQL read-only
85
+
86
+ Script SQL à exécuter manuellement sur la base de prod :
87
+
88
+ ```sql
89
+ -- À adapter pour chaque schéma accessible
90
+ CREATE USER slackbot_readonly WITH PASSWORD 'mot_de_passe_fort';
91
+ GRANT CONNECT ON DATABASE efarmz TO slackbot_readonly;
92
+ GRANT USAGE ON SCHEMA efarmz_db TO slackbot_readonly;
93
+ GRANT SELECT ON ALL TABLES IN SCHEMA efarmz_db TO slackbot_readonly;
94
+ ALTER DEFAULT PRIVILEGES IN SCHEMA efarmz_db GRANT SELECT ON TABLES TO slackbot_readonly;
95
+ -- Répéter pour efarmz_intercom, etc.
96
+ ```
97
+
98
+ ### 5. `README.md`
99
+
100
+ Sections :
101
+
102
+ 1. **Pré-requis** : Node 22, Docker, ngrok (dev local), compte Clever Cloud
103
+ 2. **Setup Slack App** :
104
+ - Créer une app sur api.slack.com
105
+ - Activer les scopes : `app_mentions:read`, `chat:write`, `chat:write.public`, `files:write`, `im:history`, `im:read`, `im:write`, `users:read`
106
+ - Activer les event subscriptions : `app_mention`, `message.im`
107
+ - URL webhook : `https://<clever-domain>/slack/events`
108
+ - Activer l'App Home → DM tab
109
+ 3. **Variables d'environnement** : référence à `.env.example`, description de chaque variable
110
+ 4. **Setup local (ngrok)** :
111
+ ```bash
112
+ cp .env.example .env
113
+ # Remplir .env
114
+ yarn dev
115
+ ngrok http 3000
116
+ # Configurer l'URL ngrok dans la Slack App
117
+ ```
118
+ 5. **Init PostgreSQL read-only** : script SQL ci-dessus
119
+ 6. **Déploiement Clever Cloud** : push Docker, config env vars
120
+ 7. **Ajout d'un nouveau schéma** : déposer un `.md` dans `docs/schemas/`, redéployer
121
+ 8. **Flags utilisateur** : `--sql`, `--debug`, `--no-context`
122
+
123
+ ## Acceptance Criteria
124
+
125
+ - [ ] Au moins un fichier `docs/schemas/*.md` présent (même placeholder)
126
+ - [ ] `docker build .` réussit depuis zéro
127
+ - [ ] `README.md` couvre setup local (ngrok) et déploiement Clever Cloud
128
+ - [ ] Script SQL init du rôle read-only documenté dans le README
129
+ - [ ] `.env.example` contient toutes les variables avec commentaires explicatifs
package/CLAUDE.md ADDED
@@ -0,0 +1,163 @@
1
+ # CLAUDE.md — efarmz-slackbot-data
2
+
3
+ Bot Slack qui traduit des questions en langage naturel vers des requêtes SQL via LLM (boucle agentique), exécute en read-only sur les bases analytiques efarmz, et renvoie le résultat formaté dans Slack.
4
+
5
+ > Plan détaillé : `.plans/efarmz-slackbot-data-712fe7.md`
6
+
7
+ ## Stack (différente du starter Next.js)
8
+
9
+ - **Runtime** : Node.js 22 + TypeScript strict (ESM, `noUncheckedIndexedAccess`)
10
+ - **Slack** : `@slack/bolt` (HTTP receiver Express, pas Socket Mode)
11
+ - **DB** : `pg` (PAS de Drizzle — service standalone, pas de migrations)
12
+ - **LLM** : `@anthropic-ai/sdk` par défaut (Haiku 4.5), derrière une interface `LLMProvider`
13
+ - **Validation** : `zod`
14
+ - **Logs** : `pino`
15
+ - **Tests** : `vitest`
16
+ - **Hosting** : Clever Cloud (Docker)
17
+
18
+ Pas de Next.js, pas de React, pas de Tailwind, pas de Drizzle, pas de Trigger.dev, pas d'auth utilisateur (le workspace Slack est la frontière de sécurité).
19
+
20
+ ## Architecture DDD / Hexagonal
21
+
22
+ Trois couches, **dépendances unidirectionnelles** : `domain` ← `application` ← `infrastructure` ← `main`.
23
+
24
+ ```
25
+ src/
26
+ ├── domain/ # PURE — aucun import externe (sauf zod pour validation)
27
+ │ ├── entities/ # Conversation, ConversationMessage, QueryResult
28
+ │ ├── value-objects/ # ThreadId, Question, QuestionFlags, SqlQuery, ...
29
+ │ ├── errors/ # DomainError + sous-classes
30
+ │ └── ports/ # Interfaces : LLMProvider, SqlExecutor, SqlValidator, ...
31
+ ├── application/ # Use cases (orchestration) — dépend de domain seulement
32
+ │ ├── usecases/ # AnswerQuestion, ParseQuestion
33
+ │ ├── agent/ # AgentLoop, RunSqlTool
34
+ │ └── formatting/ # ResponseRenderer + Scalar/Monospace/Csv renderers
35
+ ├── infrastructure/ # Adapters — implémente les ports de domain
36
+ │ ├── llm/ # AnthropicLLMProvider
37
+ │ ├── persistence/ # PostgresSqlExecutor, InMemoryConversationRepository
38
+ │ ├── sql/ # RegexSqlValidator
39
+ │ ├── schemas/ # FileSystemSchemaCatalog
40
+ │ ├── slack/ # BoltSlackMessenger, handlers
41
+ │ └── logging/ # PinoLogger
42
+ ├── config/ # env.ts (zod), Container (DI manuel), constants
43
+ └── main.ts # Entry point
44
+ ```
45
+
46
+ ### Dependency Rule (non-négociable)
47
+
48
+ - `domain/` n'importe **JAMAIS** de `application/`, `infrastructure/`, `config/` ni de SDK tiers (`@slack/bolt`, `pg`, `@anthropic-ai/sdk`).
49
+ - `application/` importe **uniquement** de `domain/`.
50
+ - `infrastructure/` peut importer `domain/` (pour implémenter ses ports) — jamais `application/`.
51
+ - `main.ts` + `config/Container.ts` sont les seuls endroits où on assemble (compose) les implémentations.
52
+
53
+ Si tu te retrouves à importer `pg` ou `@slack/bolt` dans `domain/`, c'est une erreur d'architecture — il faut un nouveau port.
54
+
55
+ ## Conventions DDD
56
+
57
+ **Value Objects** : immuables, validés à la construction via factory statique. Constructeur privé.
58
+ ```typescript
59
+ // src/domain/value-objects/SqlQuery.ts
60
+ class SqlQuery {
61
+ private constructor(public readonly raw: string) {}
62
+ static create(raw: string, validator: SqlValidator): SqlQuery {
63
+ return new SqlQuery(validator.validate(raw));
64
+ }
65
+ }
66
+ ```
67
+
68
+ **Entities** : ont une identité, encapsulent leurs invariants. Pas de DTO anémique.
69
+ ```typescript
70
+ // src/domain/entities/Conversation.ts — append plafonne à N messages
71
+ class Conversation {
72
+ append(message: ConversationMessage): Conversation { /* invariants ici */ }
73
+ }
74
+ ```
75
+
76
+ **Ports** : interfaces dans `domain/ports/`, granularité fine (ISP). Pas de port fourre-tout.
77
+ - ✅ `SlackMessenger` (post) + `AdminLogger` (logQuery) séparés
78
+ - ❌ `SlackService` qui mélange tout
79
+
80
+ **Erreurs** : sous-classes de `DomainError`, jamais `throw new Error("string")` brut.
81
+
82
+ **DI** : manuelle via `config/Container.ts`. Pas d'inversify/tsyringe.
83
+
84
+ ## Boucle agentique
85
+
86
+ `AgentLoop.run()` itère sur `tool_use` jusqu'à `end_turn` ou `AGENT_MAX_TURNS`. À chaque tour avec `run_sql` :
87
+ 1. `SqlValidator.validate(input.sql)` → si KO, renvoie `tool_result.is_error = true` au LLM
88
+ 2. `SqlExecutor.execute(query)` → succès ou erreur PG renvoyée comme `tool_result.is_error`
89
+ 3. Le LLM corrige automatiquement au tour suivant — pas de logique de retry manuelle à écrire
90
+
91
+ Si tu modifies la boucle, garde la règle : **toute erreur SQL doit être renvoyée comme `tool_result`, pas thrown**. Sinon le LLM ne peut pas corriger.
92
+
93
+ ## Sécurité SQL (4 couches, toutes obligatoires)
94
+
95
+ 1. **Utilisateur PG read-only** dédié (rôle `slackbot_readonly`) — impossible de muter même si tout le reste est bypassé
96
+ 2. **`RegexSqlValidator`** : doit commencer par `SELECT`/`WITH`, refuse les keywords mutables (INSERT/UPDATE/DELETE/DROP/CREATE/ALTER/TRUNCATE/GRANT/REVOKE/COPY/CALL/VACUUM/MERGE), refuse `;` en milieu de requête, force `LIMIT SQL_MAX_ROWS`
97
+ 3. **`SET statement_timeout = SQL_TIMEOUT_MS`** par session
98
+ 4. **`SqlQuery` VO** : constructeur privé — impossible d'exécuter un SQL non validé (vérifié au compile-time)
99
+
100
+ **Ne jamais** ajouter un chemin qui contourne le validator — tout `SqlExecutor.execute` doit recevoir un `SqlQuery`, pas une `string`.
101
+
102
+ ## Doc des schémas
103
+
104
+ `docs/schemas/*.md` — un fichier par schéma PostgreSQL (efarmz_db, efarmz_intercom, …). Chargés au boot par `FileSystemSchemaCatalog`, concaténés dans le system prompt avec `cache_control: { type: "ephemeral" }` (TTL 5 min Anthropic) pour économiser les tokens en rafale.
105
+
106
+ Quand on ajoute un schéma : déposer le `.md` dans `docs/schemas/`, redéployer. Pas de code à changer.
107
+
108
+ ## Format de réponse
109
+
110
+ `ResponseRenderer` décide :
111
+ - 0 ligne → "Aucun résultat."
112
+ - 1×1 → `ScalarRenderer` (valeur en gras)
113
+ - ≤ `SQL_CSV_THRESHOLD` lignes → `MonospaceTableRenderer` (bloc code aligné inline)
114
+ - \> `SQL_CSV_THRESHOLD` → monospace tronqué **+** CSV uploadé via `files.uploadV2`
115
+
116
+ Flags utilisateur dans le message Slack :
117
+ - `--sql` : ajoute le SQL en bloc code
118
+ - `--debug` : `--sql` + `EXPLAIN`
119
+ - `--no-context` : ignore l'historique du thread
120
+
121
+ ## Conventions de code
122
+
123
+ - **Strings** : backticks (cohérent avec règles globales)
124
+ - **Imports** : paths absolus `@/` (alias TS configuré sur `src/`), jamais `../`
125
+ - **ESM** : tout est ESM, imports avec `.js` extension dans les paths relatifs si nécessaire
126
+ - **Naming** : `PascalCase.ts` pour classes/types, `camelCase.ts` pour fonctions/helpers
127
+ - **1 fichier = 1 export default** (cohérent avec convention globale)
128
+ - **Pas de comments** sauf si le *why* est non-évident (cf. règles globales)
129
+
130
+ ## Commandes
131
+
132
+ ```bash
133
+ yarn dev # tsx watch src/main.ts
134
+ yarn build # tsc
135
+ yarn start # node dist/main.js
136
+ yarn test # vitest run
137
+ yarn test:watch # vitest
138
+ yarn lint # eslint
139
+ yarn typecheck # tsc --noEmit
140
+ ```
141
+
142
+ ## Variables d'environnement
143
+
144
+ Voir `.env.example`. Toutes validées via `config/env.ts` (zod). Le boot échoue si une variable est manquante ou invalide (fail-fast).
145
+
146
+ ## Hors-scope V1
147
+
148
+ À refuser si demandé sans discussion préalable :
149
+ - UI admin web
150
+ - Persistance DB du bot lui-même (LRU mémoire suffit pour les conversations)
151
+ - Implémentation OpenAI concrète (l'interface est prête, l'impl viendra plus tard)
152
+ - Graphiques, exports custom
153
+ - Slash commands (`/efarmz-stats ...`)
154
+ - i18n des réponses du bot
155
+ - Auth utilisateur niveau bot (workspace = frontière)
156
+
157
+ ## Points d'attention récurrents
158
+
159
+ - **Ack Slack < 3s** : les handlers doivent répondre `200 OK` immédiatement, traitement en arrière-plan
160
+ - **Idempotence** : Slack retry — dédupliquer par `event_id` (cache 5 min)
161
+ - **Prompt caching** : toujours marquer le bloc system (doc schémas) avec `cache_control: { type: "ephemeral" }`
162
+ - **PII** : les résultats peuvent contenir emails/noms — workspace privé only, accepté
163
+ - **Coûts** : surveiller via `SlackAdminLogger` (channel admin)