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
package/Dockerfile ADDED
@@ -0,0 +1,15 @@
1
+ FROM node:22-slim AS builder
2
+ WORKDIR /app
3
+ COPY package.json yarn.lock ./
4
+ RUN yarn install --frozen-lockfile
5
+ COPY . .
6
+ RUN yarn build
7
+
8
+ FROM node:22-slim AS runtime
9
+ WORKDIR /app
10
+ COPY --from=builder /app/dist ./dist
11
+ COPY --from=builder /app/node_modules ./node_modules
12
+ COPY --from=builder /app/docs ./docs
13
+ COPY --from=builder /app/package.json ./
14
+ ENV NODE_ENV=production
15
+ CMD ["node", "dist/main.js"]
package/PRD.md ADDED
@@ -0,0 +1,119 @@
1
+ ---
2
+ title: "[PRD] Bot Slack NL→SQL — efarmz-slackbot-data"
3
+ type: prd
4
+ status: approved
5
+ ---
6
+
7
+ # PRD: Bot Slack — Interrogation en langage naturel sur les bases analytiques efarmz
8
+
9
+ ## Executive Summary
10
+
11
+ Service Node.js/TypeScript standalone qui écoute les mentions et DM Slack, traduit les questions en langage naturel vers des requêtes SQL via un LLM (Claude Haiku 4.5) avec boucle agentique, exécute les requêtes en read-only sur les bases PostgreSQL analytiques efarmz, et renvoie les résultats formatés (texte / table monospace / CSV) en threading Slack. Architecture DDD hexagonale, sécurité SQL en 4 couches, déployé sur Clever Cloud via Docker.
12
+
13
+ ## Problem Statement
14
+
15
+ L'équipe efarmz a besoin d'interroger ses bases analytiques PostgreSQL (commandes, clients, produits, Intercom…) sans écrire de SQL manuellement. Aujourd'hui, obtenir une statistique ad hoc nécessite de solliciter un développeur ou d'accéder directement à la base, ce qui est lent, bloquant et risqué. Un bot Slack permettrait à n'importe quel membre de l'équipe de poser une question en français et d'obtenir un résultat immédiat, sans exposition directe aux données ni risque de mutation accidentelle.
16
+
17
+ ## User Stories
18
+
19
+ ### Primary User Story
20
+
21
+ En tant que membre de l'équipe efarmz, je veux poser une question en langage naturel dans Slack (mention ou DM) et recevoir la réponse tirée de la base de données en quelques secondes, sans écrire de SQL.
22
+
23
+ ### Additional User Stories
24
+
25
+ - En tant qu'analyste, je veux recevoir un tableau monospace ou un fichier CSV pour les résultats larges, afin de pouvoir les exploiter directement.
26
+ - En tant que développeur, je veux ajouter `--sql` à ma question pour voir la requête SQL générée et vérifier sa pertinence.
27
+ - En tant que développeur, je veux ajouter `--debug` pour obtenir le plan d'exécution EXPLAIN et diagnostiquer les performances.
28
+ - En tant qu'utilisateur, je veux que le bot se souvienne du contexte du thread pour poser des questions de suivi sans répéter le contexte.
29
+ - En tant qu'administrateur, je veux recevoir des logs de chaque requête (question, SQL, durée, coût token) dans un channel admin dédié.
30
+
31
+ ## Functional Requirements
32
+
33
+ - Le bot répond aux `app_mention` et aux DM (`message.im`)
34
+ - Le message est parsé : strip de la mention `<@U…>`, extraction des flags `--sql`, `--debug`, `--no-context`
35
+ - La boucle agentique traduit la question en SQL via Claude Haiku 4.5, exécute via `run_sql` tool, corrige automatiquement sur erreur SQL (max 5 tours)
36
+ - Le SQL est validé avant exécution : SELECT/WITH uniquement, keywords mutables refusés, LIMIT forcé à 100 lignes max
37
+ - Le format de réponse est déterministe : scalaire (1×1), table monospace (≤15 lignes), table tronquée + CSV uploadé (>15 lignes)
38
+ - L'historique de conversation est maintenu par thread (LRU mémoire, TTL 1h, max 200 threads, max 8 messages)
39
+ - Le flag `--no-context` ignore l'historique du thread courant
40
+ - Le flag `--debug` ajoute le SQL + résultat EXPLAIN au message
41
+ - Le bot log chaque requête dans le channel admin (question, sqls[], rowsCount, durationMs, tokens)
42
+ - Le boot échoue si une variable d'environnement requise est manquante (fail-fast via zod)
43
+ - La doc des schémas PostgreSQL est chargée depuis `docs/schemas/*.md` au démarrage
44
+
45
+ ## Non-Functional Requirements
46
+
47
+ - **Performance** : ack Slack < 3s (200 OK immédiat, traitement async) ; timeout SQL = 30s ; réponse totale < 60s
48
+ - **Sécurité** : utilisateur PostgreSQL read-only dédié + RegexSqlValidator + statement_timeout + SqlQuery VO (constructeur privé) — 4 couches indépendantes
49
+ - **Fiabilité** : idempotence par `event_id` (cache 5 min) pour absorber les retries Slack
50
+ - **Coûts** : prompt caching (`cache_control: ephemeral`) sur la doc schémas — économie ~80% tokens sur rafales ; modèle Haiku = quelques centimes/question
51
+ - **Maintenabilité** : architecture DDD hexagonale stricte, dependency rule non-négociable, ports fins (ISP), tests unitaires sur domain + validators + formatters
52
+ - **Extensibilité** : interface `LLMProvider` découplée — ajouter OpenAI = 1 fichier, 0 modif ailleurs
53
+ - **Déploiement** : Dockerisé, déployable sur Clever Cloud sans friction
54
+
55
+ ## Success Metrics
56
+
57
+ ### Primary Metrics
58
+
59
+ - Taux de réponse correcte (SQL valide + résultat pertinent) : > 85% des questions
60
+ - Temps de réponse médian (de la question à la réponse Slack) : < 15s
61
+ - Zéro mutation de données en production (garantie structurelle, pas de monitoring)
62
+
63
+ ### Secondary Metrics
64
+
65
+ - Coût moyen par question : < 0,05€ (Haiku 4.5 + caching)
66
+ - Taux de correction automatique SQL (boucle agentique) : mesuré via logs admin
67
+ - Adoption : > 5 utilisateurs actifs dans les 2 premières semaines
68
+
69
+ ## Technical Considerations
70
+
71
+ Architecture hexagonale en 3 couches strictes (`domain` ← `application` ← `infrastructure` ← `main`) avec DI manuelle via `Container`. La règle de dépendance est non-négociable : `domain` n'importe aucun SDK tiers. La boucle agentique (`AgentLoop`) est indépendante du provider LLM et du tool concret. Le prompt caching Anthropic (`cache_control: ephemeral`, TTL 5 min) est appliqué sur le bloc system contenant la doc schémas.
72
+
73
+ Voir `.plans/efarmz-slackbot-data-712fe7.md` pour l'architecture complète, la structure des fichiers, les variables d'environnement et le SQL d'init du rôle read-only.
74
+
75
+ ## Implementation Phases
76
+
77
+ ### Phase 1 — Bootstrap & Domain (F1 + F2)
78
+
79
+ Setup du projet (tooling, config, Docker) et implémentation de la couche domain pure : entités, value objects, erreurs typées, ports (interfaces). Tests unitaires domain.
80
+
81
+ ### Phase 2 — Application & Infrastructure (F3 + F4)
82
+
83
+ Use cases (`ParseQuestion`, `AnswerQuestion`), boucle agentique (`AgentLoop`), formatters de réponse. Puis adapters infra : `RegexSqlValidator`, `PostgresSqlExecutor`, `InMemoryConversationRepository`, `AnthropicLLMProvider`, handlers Slack, `PinoLogger`.
84
+
85
+ ### Phase 3 — Config, Déploiement & Documentation (F5 + F6)
86
+
87
+ Composition root (`Container`), entry point (`main.ts`), doc des schémas PostgreSQL efarmz, `Dockerfile`, déploiement Clever Cloud, README.
88
+
89
+ ## Risk Assessment
90
+
91
+ ### Technical Risks
92
+
93
+ - **Qualité SQL générée** : Haiku peut générer des JOINs incorrects sur des schémas complexes → mitigation : doc schémas détaillée + boucle de correction automatique (max 5 tours)
94
+ - **Timeout LLM** : API Anthropic down → message Slack "Service IA temporairement indisponible" + log admin
95
+ - **Retries Slack** : event redelivery peut déclencher des doublons → mitigation : déduplication par `event_id` (cache 5 min)
96
+ - **Explosion tokens** : historique de conversation trop long → mitigation : résumé condensé (pas les `tool_result` bruts), cap à 8 messages, TTL 1h
97
+
98
+ ### Business Risks
99
+
100
+ - **PII dans les résultats** : les tables peuvent contenir emails/noms → mitigation : workspace Slack privé uniquement, accepté
101
+ - **Questions hors-scope** : le LLM peut tenter des requêtes sur des tables sans documentation → mitigation : doc schémas complète obligatoire avant déploiement
102
+
103
+ ## Dependencies
104
+
105
+ ### Internal Dependencies
106
+
107
+ - Doc schémas PostgreSQL efarmz (`docs/schemas/efarmz_db.md`, etc.) — fournie par l'utilisateur
108
+ - Credentials PostgreSQL (rôle `slackbot_readonly` à créer manuellement)
109
+ - Slack App configurée avec les bons scopes et l'URL de webhook
110
+
111
+ ### External Dependencies
112
+
113
+ - `@slack/bolt` — HTTP receiver
114
+ - `@anthropic-ai/sdk` — Claude Haiku 4.5, tool_use, prompt caching
115
+ - `pg` — PostgreSQL pool
116
+ - `zod` — validation env vars + schemas
117
+ - `pino` — structured logging
118
+ - `vitest` — tests unitaires
119
+ - Clever Cloud — hébergement Docker
File without changes
@@ -0,0 +1,89 @@
1
+ # Guidelines — Mise à jour des docs schémas
2
+
3
+ Directives pour créer le skill `update-schema-doc` qui modernise les fiches schéma PostgreSQL.
4
+
5
+ ## Entrées attendues
6
+
7
+ L'utilisateur fournit en langage libre un des cas suivants :
8
+
9
+ | Cas | Exemple | Cible dans le doc |
10
+ |---|---|---|
11
+ | Nouvelle table | "Ajoute la table `efarmz_db.coupons` avec colonnes id, code, discount, expires_at" | `## Tables / ### {domaine}` |
12
+ | Nouvelles colonnes | "Sur `art`, ajoute `weight_kg numeric` et `origin_country varchar`" | Lignes dans le tableau de la table |
13
+ | Suppression colonne | "Retire `psecotax` de `art`, on ne l'utilise plus" | Tableau de la table |
14
+ | Nouvelle règle métier | "Une commande est *cadeau* si `eorderd.artid = 'GIFT'` et `totalpaid = 0`" | `## Règles métier / ### {thème}` |
15
+ | Nouveau pattern SQL | "Voici le pattern pour calculer le délai entre commande et livraison : …" | `## Patterns SQL` (avec ancre) |
16
+ | Nouvelle gotcha PG | "Si tu fais `SUM(qty)` sur `eorderd` sans `GROUP BY w_orderid`, ça double-compte les meals" | `## Conventions globales` |
17
+ | Mise à jour règle existante | "Le seuil panier picking passe de 40 à 50" | Bloc existant + bump `last_updated` |
18
+ | Nouvel alias | "On utilise aussi `cuid` pour `c_uuid` dans certains exports" | `## Glossaire` |
19
+
20
+ ## Algorithme
21
+
22
+ 1. **Lire** `docs/schemas/{schema}.md` en entier.
23
+ 2. **Classifier** l'input → un (ou plusieurs) des cas du tableau ci-dessus. Si ambigu, demander.
24
+ 3. **Détecter doublon** :
25
+ - Pour une règle / pattern : grep sur le nom et sur le SQL canonique avant d'ajouter. Si existe → proposer un *update* au lieu d'un *ajout*.
26
+ - Pour une colonne : si déjà présente, demander si on remplace la description ou non.
27
+ 4. **Choisir l'emplacement** :
28
+ - Table → mapper au domaine (Commandes / Catalogue / Abonnements / Clients / Référentiel). Si aucun ne colle, demander avant de créer une nouvelle sous-section.
29
+ - Règle métier → mapper à un thème existant (Filtres / Validité / Calculs / Frais). Sinon créer un thème.
30
+ - Pattern → bas de la section, avec ancre `#pattern-{slug}` cohérente.
31
+ 5. **Appliquer l'édition** via `Edit` (string replace ciblé). Jamais `Write` complet — préserve le cache Anthropic et minimise le diff.
32
+ 6. **Bumper le frontmatter** : `last_updated: {today}`.
33
+ 7. **Vérifier l'invariant de format** :
34
+ - Backticks autour de tout nom SQL.
35
+ - Tableaux avec exactement les colonnes `| Colonne | Type | Rôle | Description |`.
36
+ - `⚠️` pour pièges, pas d'autres emojis.
37
+ - Pas de numérotation `## 1`, `## 2` — titres seuls.
38
+ 8. **Retourner** un résumé : section touchée, lignes ajoutées/modifiées, alertes éventuelles (doublon détecté, ambiguïté résolue).
39
+
40
+ ## Règles d'or (non-négociables)
41
+
42
+ - **DRY** : une règle métier vit à **un** seul endroit. Si un pattern SQL la matérialise, il **référence** la règle (`-- voir Règles métier / Validité abonnement`), il ne la duplique pas.
43
+ - **Pas de renumérotation** : titres sans numéros. Réorganiser ne casse pas les ancres.
44
+ - **Insertion en fin de sous-section** par défaut — préserve le cache Anthropic.
45
+ - **Refuser le contournement de sécurité** : si l'utilisateur demande d'ajouter une règle qui implique une mutation SQL (`UPDATE`, `DELETE`…), c'est hors-scope du bot read-only. Refuser et expliquer.
46
+ - **Demander avant de créer une nouvelle sous-section de domaine** ou un nouveau thème de règle métier — la taxonomie doit rester stable.
47
+ - **Ne jamais éditer le schéma sans validation** : si l'utilisateur dit "ajoute la colonne X" sans préciser le type, demander le type (et idéalement, vérifier en DB, ou demander à l'utilisateur de la fournir).
48
+
49
+ ## Convention de tableau de colonnes
50
+
51
+ ```markdown
52
+ | Colonne | Type | Rôle | Description |
53
+ |---|---|---|---|
54
+ | `id` | `uuid` | PK | — |
55
+ | `user_id` | `uuid` | FK → `users.id` | — |
56
+ | `status` | `int` | — | `1` = actif, `2` = pausé |
57
+ ```
58
+
59
+ - **Rôle** : `PK`, `FK → table.col`, `unique`, ou `—`.
60
+ - **Description** : sémantique métier seule. Pas de "FK vers X" dans description (c'est dans Rôle).
61
+ - **Type** : type SQL réel (`varchar(255)`, `numeric(10,2)`, `timestamp`, `int`, etc.).
62
+
63
+ ## Convention de pattern SQL
64
+
65
+ ````markdown
66
+ ### {Nom court} {#pattern-{slug}}
67
+ **Quand** : {1 phrase, cas d'usage}
68
+ **Dépend de** : {refs aux règles métier ou autres patterns, optionnel}
69
+
70
+ ```sql
71
+ -- code
72
+ ```
73
+
74
+ ⚠️ {piège éventuel, optionnel}
75
+ ````
76
+
77
+ ## Frontmatter du schéma
78
+
79
+ Chaque fichier `docs/schemas/{schema}.md` doit commencer par :
80
+
81
+ ```yaml
82
+ ---
83
+ schema: {nom du schéma}
84
+ sgbd: postgresql
85
+ last_updated: YYYY-MM-DD
86
+ ---
87
+ ```
88
+
89
+ Le `last_updated` est bumé à chaque édition via le skill.