Guide pour un agent de code
Ce guide s'adresse à un agent de type Claude Code qui doit travailler avec le labo : interroger les données, appeler les fonctions AI_*, charger des fichiers, lancer dbt ou la prévision, écrire du code transposable chez le client. Il vaut dans ce dépôt comme dans un autre projet qui se sert du labo comme d'un Snowflake local. Il ne remplace pas les documents de référence : il dit quoi faire, dans quel ordre, et renvoie au détail.
Pour faire évoluer le labo lui-même (nouveau code, nouveau composant), lire d'abord methode-claude-code.md : le cycle PLAN.md, les hooks, les agents et /gate s'appliquent.
1. Règles à respecter avant toute action
| Règle | Conséquence pour l'agent |
|---|---|
.env et secrets/ ne se lisent pas |
Ne jamais faire cat .env, grep ... .env, ni afficher une variable secrète. Charger l'environnement sans le lire : uv run --env-file .env <commande>. Les noms des variables sont dans .env.example |
| Aucun secret dans le code, les logs, les commandes | Les identifiants viennent de l'environnement (LabConfig.from_env()). Ne pas les recopier dans une commande, un fichier, un message |
| LiteLLM est la seule sortie vers un LLM | Pas d'appel à Ollama, OpenAI ou autre fournisseur en direct. Passer par cortex_lab (alias cortex-*) ou par LITELLM_BASE_URL. LiteLLM 1.82.7 et 1.82.8 sont interdits |
| Données synthétiques ou publiques | Aucune donnée du client dans le labo ni dans un prompt. Les fichiers de test viennent de data/generate.py |
| Aucun tiret cadratin (U+2014) | Nulle part : code, docs, commits. Un hook refuse l'écriture |
| Prose en français, code en anglais | Commentaires de code, identifiants et messages de commit en anglais |
| Plan d'abord | Dans ce dépôt, aucun code sans PLAN.md approuvé. La ligne d'approbation n'est écrite que par Stéphane |
| Actions à confirmer | git commit, git push, docker compose down : demander d'abord. Ne pas supprimer de table, de namespace ni de fichier MinIO hors des objets créés par l'agent |
| Briques du NUC | Le labo consomme pg-central, minio-central et LiteLLM. Ne jamais monter sa propre base, son propre stockage ni sa propre gateway |
2. Vérifier que le labo répond
cd /home/stephanev/Documents/snowflake/snowflake-lab-kit
docker compose ps # polaris, trino, marquez : running (healthy)
curl -s http://127.0.0.1:18080/v1/info # Trino : "starting": false
curl -s http://127.0.0.1:15000/api/v1/namespaces # Marquez : liste JSON
Si un service manque : docker compose up -d (autorisé sans confirmation). Un 401 de Trino sur analytics ou un échec de polaris-init se traite avec le tableau de dépannage de exploitation.md, sans lire .env.
Points d'accès depuis l'hôte : Trino http://127.0.0.1:18080, Polaris http://127.0.0.1:18181 (OAuth), Marquez http://127.0.0.1:15000. Depuis le LAN : openflake.nuc, polaris.openflake.nuc, marquez.openflake.nuc.
3. Où sont les données
Un seul catalogue, nommé comme la base Snowflake : analytics. Les noms ont trois niveaux, analytics.<schéma>.<table>, identiques dans DuckDB, Trino et Snowflake. Identifiants en minuscules, jamais entre guillemets.
| Schéma | Contenu | Écrit par |
|---|---|---|
raw |
Fichiers chargés tels quels, colonnes de preuve (_source_file, _loaded_at...) |
ingest.raw_loader |
standardized |
Tables typées et nettoyées, snapshots SCD2 | dbt |
semantic |
Dimensions et faits (dim_hotel, dim_comp_set, fct_hotel_daily), seeds, modèle sémantique MetricFlow |
dbt |
intake |
Historique de chargement, registre de schémas, exceptions, file de revue, suggestions et règles de mapping | ingest, dbt (seed mapping_rules) |
forecast |
forecast (une ligne par quantile), backtest_metrics |
conteneur forecast |
shared_calendar |
Snapshots datés du calendrier (lecture seule pour lab_tech) |
ingest.shared_snapshots |
Les tables it_* et les namespaces it_<id>... sont des restes de tests d'intégration : les ignorer.
Lister l'existant (dans Trino ; lab_reader ne voit que semantic et shared_*, prendre lab_tech pour tout voir) :
SHOW SCHEMAS FROM analytics;
SHOW TABLES FROM analytics.semantic;
DESCRIBE analytics.semantic.fct_hotel_daily;
Détail des colonnes et des couches : collecte-retraitement.md. Droits : socle.md.
4. Recettes
Toutes les commandes se lancent depuis la racine du dépôt. --env-file .env fournit les identifiants sans que l'agent les voie.
Interroger en SQL avec Trino
Trino n'a pas de mot de passe : le nom d'utilisateur fixe les droits. lab_reader lit semantic et shared_* ; lab_tech lit et écrit tout analytics sauf shared_*. Prendre lab_reader pour une lecture.
import trino
con = trino.dbapi.connect(host="127.0.0.1", port=18080, user="lab_reader", catalog="analytics", schema="semantic")
cur = con.cursor()
cur.execute("SELECT * FROM analytics.semantic.fct_hotel_daily LIMIT 5")
print(cur.fetchall())
À lancer avec uv run python script.py (le client trino est dans les dépendances). Trino ne connaît pas les fonctions AI_*.
Interroger avec DuckDB, fonctions AI_* comprises
from lab.duckdb_connect import connect
con = connect() # MinIO, catalogue Polaris attaché sous "analytics", fonctions AI_* et GET enregistrées
print(con.execute("SELECT count(*) FROM analytics.raw.pms_daily").fetchall())
con.close()
À lancer avec uv run --env-file .env python script.py. Sans aucun service : connect(s3=False, polaris=False) donne une connexion hors ligne avec les seules fonctions AI_*.
Appeler une fonction Cortex en SQL
On écrit les appels comme dans Snowflake ; on lit les champs avec GET(...), pas avec la syntaxe :champ.
SELECT GET(AI_CLASSIFY('Chambre propre mais accueil lent', ['room', 'service', 'price']), 'labels') AS labels;
SELECT AI_SENTIMENT('Chambre propre mais accueil lent', ['room', 'service']) AS sentiment;
SELECT AI_COMPLETE('cortex-complete', 'Résume en une phrase : ...') AS answer;
SELECT AI_CLASSIFY('texte', ['a', 'b'], NULL, TRUE) AS detailed; -- {"value": ..., "error": ...}
Une erreur par ligne renvoie NULL, comme dans Snowflake. Pour comprendre un NULL, rejouer avec return_error_details à TRUE. AI_COMPLETE n'accepte que les alias autorisés (cortex-* et CORTEX_COMPLETE_EXTRA_ALIASES). Fonctions couvertes, signatures et écarts : cortex-lab.md et .claude/skills/cortex-compat/references/contracts.md. Une fonction absente de ces contrats ne s'invente pas : elle passe par /new-cortex-function.
Appeler une fonction Cortex en Python
from cortex_lab.compat import complete, classify_text, sentiment
print(complete("cortex-complete", "Donne trois idées de promotion hôtelière."))
print(classify_text("Je veux réserver une suite", ["booking", "complaint"]))
cortex_lab.compat bascule vers snowflake.cortex quand CORTEX_BACKEND=snowflake : le même code tourne chez le client. Style Snowpark : cortex_lab.compat.snowpark_functions.
Produire et charger des fichiers
uv run python -m data.generate landing --root /tmp/landing --as-of 2026-10-22 --history-days 7
uv run --env-file .env python -m ingest.raw_loader --date 2026-10-22 --source pms
Le dépôt des fichiers dans MinIO passe par LandingStore.put (exemple complet dans exploitation.md). Le chargeur affiche un statut par fichier (loaded, skipped_already_loaded, rejected, unclassified) et émet le lineage vers Marquez si MARQUEZ_URL est défini. Un fichier déjà chargé n'est pas rechargé (mémoire de 14 jours, comme Snowpipe). Pour essayer sans toucher à raw : --raw-schema it_scratch_<nom>.
Retraiter avec dbt
cp -n dbt/profiles.example.yml dbt/profiles.yml
uv run --env-file .env dbt build --project-dir dbt --profiles-dir dbt # cible duckdb (défaut)
uv run --env-file .env dbt build --project-dir dbt --profiles-dir dbt --target trino # même projet, autre adapter
Un seul projet dbt ; seul l'adapter change (dbt-snowflake chez le client). Ne pas écrire de SQL propre à DuckDB ou à Trino dans un modèle : si c'est inévitable, passer par une macro avec une variante par adapter.
Interroger les métriques
DBT_PROJECT_DIR=dbt DBT_PROFILES_DIR=dbt uv run --env-file .env mf query --metrics occupancy_rate --group-by metric_time --limit 5
Métriques : rooms_sold_total, room_revenue_total, rooms_available_total, occupancy_rate, adr, revpar (dbt/models/semantic/semantic_models.yml). La CLI mf n'a pas d'option --target : elle suit la cible par défaut du profil.
Prévoir
docker compose --profile forecast run --rm forecast forecast --as-of 2026-10-22
docker compose --profile forecast run --rm forecast backtest --as-of 2026-10-22 --origins 6 --step-days 14
Résultats dans analytics.forecast.forecast et analytics.forecast.backtest_metrics. Construire l'image une fois avant : docker compose --profile forecast build forecast. Détail : prevision.md.
Lire le lineage
curl -s http://127.0.0.1:15000/api/v1/namespaces/snowlab/jobs | python3 -m json.tool | head -40
5. Écrire du code transposable chez le client
- Noms à trois niveaux
analytics.<schéma>.<table>, en minuscules, sans guillemets. - Fonctions
AI_*écrites comme dans Snowflake, champs lus parGET(...). - En Python, importer depuis
cortex_lab.compat, jamais depuiscortex_lab.coredans du code destiné au client. - Types : ceux de
lab/types.py, qui donnent l'équivalent Snowflake. - Configuration par variables d'environnement, jamais en dur.
- Ce qui change chez le client et ce qui reste à confirmer : portabilite-snowflake.md.
6. Tester
uv run --env-file .env pytest -q -m "not integration" # sans service, quelques minutes
uv run --group forecast --env-file .env pytest -q -m integration # pile démarrée, une vingtaine de minutes
python3 .claude/hooks/no_em_dash.py --scan .
python3 .claude/hooks/guard_litellm.py --scan .
Les tests d'intégration écrivent dans la pile réelle (tables it_*, dates fictives après 2099) et redémarrent le conteneur polaris : ne pas les lancer pendant qu'une autre session utilise le labo. Dans ce dépôt, aucun travail n'est soumis sans /gate au vert. Détail : tests-et-gate.md.
7. Pièges connus
| Piège | Comportement attendu de l'agent |
|---|---|
| Trino accepte n'importe quel nom d'utilisateur sans mot de passe | Se déclarer lab_reader pour lire. lab_tech peut supprimer définitivement des tables (purge activée, voir exploitation.md) |
Une fonction AI_* renvoie NULL |
Rejouer avec return_error_details à TRUE avant de conclure |
La réponse d'un AI_* diffère de Snowflake |
Normal : le modèle local (qwen3:8b) n'est pas celui du client. Seuls les contrats (forme, erreurs, limites) sont identiques. Ne pas tester la qualité d'une réponse |
con.sql(...).fetchone() lève AttributeError |
DuckDB 2.0 : une relation ne garde plus de résultat. Utiliser con.execute(...).fetchone() ou con.sql(...).fetchall() |
Un DuckDB hors ligne échoue au premier connect() |
Les extensions httpfs et iceberg se téléchargent au premier usage : relancer une fois avec accès réseau |
DROP TABLE depuis DuckDB ou PyIceberg |
Les fichiers restent dans MinIO (pas de purge) |
Un nouveau schéma shared_* |
lab_reader ne le lit via Polaris qu'après ajout à READER_NAMESPACES (infra/polaris/init_catalog.py) |
| Les identifiants Polaris d'un principal sont réinitialisés | Stéphane recopie secrets/polaris_credentials.env dans .env ; l'agent ne lit ni l'un ni l'autre |
8. Bloc à reprendre dans le CLAUDE.md d'un autre projet
Pour qu'un agent d'un autre dépôt se serve du labo comme d'un Snowflake local, ajouter à son CLAUDE.md :
## Labo Snowflake-like
Ce projet s'exécute contre le labo local `/home/stephanev/Documents/snowflake/snowflake-lab-kit`
(guide : `docs/guide-agent.md` de ce dépôt).
- Catalogue `analytics`, noms `analytics.<schéma>.<table>` en minuscules, sans guillemets.
- Lecture SQL : Trino `http://127.0.0.1:18080`, utilisateur `lab_reader`, sans mot de passe.
- Fonctions Cortex : DuckDB du labo (`lab.duckdb_connect.connect()`) ou `cortex_lab.compat` en Python ;
champs lus par `GET(...)`. Aucun appel LLM hors LiteLLM.
- Identifiants : `.env` du labo, jamais lu ni affiché ; commandes lancées par `uv run --env-file .env`
depuis la racine du labo.
- Données synthétiques uniquement ; aucune donnée du client.
- Ne pas écrire dans `analytics` hors d'un schéma de travail dédié, ne rien supprimer sans confirmation.