L'instantané SQL du corpus
_source-oldproject/ n'entre ni dans une image Docker, ni dans un runner de CI. Sans autre
mécanisme, un environnement déployé n'aurait donc ni technologie, ni message type, ni gabarit :
une base au schéma complet et vide de tout catalogue. L'instantané SQL comble ce trou. Il vit dans
packages/database/prisma/seed/corpus/, il est versionné en clair, et c'est lui que sèment la
CI et les trois environnements.
Une commande le régénère :
pnpm db:snapshot [chemin-du-corpus]
Le chemin est optionnel : à défaut, _source-oldproject à la racine du dépôt. Les arguments
suivants sont transmis tels quels à l'import — pnpm db:snapshot ../ailleurs --overwrite-templates.
Pourquoi photographier plutôt que porter
Le corpus est hors dépôt : il porte les clés d'intake de production des 65 communities SEKOIA,
et le .gitignore comme le .dockerignore l'écartent. Son sondage, ensuite, lance un interpréteur
Python — l'import n'y lit pas des fichiers, il appelle réellement les
générateurs. Ni le dossier ni Python n'ont leur place dans une
image de production.
Le SQL produit ici ne demande rien de tout cela : il se rejoue avec une connexion PostgreSQL, et c'est tout.
La source de vérité reste le corpus. export-corpus.ts lit la base après que l'import l'a
écrite, si bien qu'aucune règle d'assemblage, de réécriture ou d'identification de pool n'est
réimplémentée à cet endroit. C'est ce qui empêche les deux chemins de diverger : il n'y a qu'un
seul lecteur du corpus, et ce n'est pas l'exportateur.
Les trois gestes de la commande
| # | Geste | Pourquoi |
|---|---|---|
| 1 | Build de @sekoiacraft/database | Les seeds l'importent par son nom, donc via exports, donc depuis dist/ |
| 2 | prisma/seed/import-source.ts <corpus> | Écrit le corpus en base — c'est l'import ordinaire, celui de pnpm db:seed |
| 3 | prisma/seed/export-corpus.ts | Relit la base et l'écrit en SQL, un fichier par technologie |
L'ordre est délibéré : elle importe, puis photographie. Exporter sans réimporter figerait ce que la base contient au moment du geste — c'est-à-dire, un jour ou l'autre, un corpus périmé pris pour le corpus courant.
Elle exige le corpus, là où pnpm db:seed s'en passe
C'est la différence qui compte entre les deux commandes.
pnpm db:seed choisit : le corpus quand il est présent, l'instantané
sinon. pnpm db:snapshot refuse : sans corpus, elle sortirait en code 1 plutôt que de recopier
l'instantané existant en se présentant comme une régénération.
Le corpus, python3, et un DATABASE_URL joignable. La commande a besoin d'une base de
travail : elle y importe, puis la relit. Contrairement à db:push et db:reset, elle ne vérifie
pas que la cible est locale — l'import n'écrit que par upsert et ne drope rien —, mais la cible
est annoncée avant de partir. Lisez-la.
Ce qu'elle produit
Le répertoire est reconstruit à chaque passage : une technologie retirée du corpus doit voir son fichier disparaître, faute de quoi le chargement la recréerait.
| Fichier | Contenu |
|---|---|
_shared.sql | Les pools qu'aucune technologie ne porte en propre et les six populations de démonstration. Chargé en premier : des messages types de plusieurs technologies s'y rattachent |
<technologie>.sql | Une technologie, ses jeux de données, ses messages types et leurs gabarits |
manifest.json | L'ordre de chargement, les comptes que le chargement doit produire, et l'empreinte des règles qui ont façonné les gabarits |
Aujourd'hui 30 fichiers, environ 12 Mo, du plus gros (sekoia-agent.sql, 4,6 Mo) au plus petit
(office-365-message-trace.sql, 20 ko). Chaque fichier s'ouvre sur un en-tête qui dit son volume et rappelle
qu'il est généré — à ne pas éditer à la main.
Les lignes sont écrites par point de code, jamais dans l'ordre de l'ORDER BY du serveur :
celui-ci suit la collation, et PGlite (C) et un PostgreSQL en en_US.UTF-8 — qui ignore les tirets
— rangent windows.sql et bind.sql différemment. Le même corpus donne donc les mêmes fichiers,
quelle que soit la base intermédiaire.
Ce que le manifeste annonce :
| Comptes du manifeste | Valeur |
|---|---|
| Technologies | 28 |
| Jeux de données | 398 |
| Enregistrements | 10 878 |
| Messages types | 474 |
| Gabarits | 474 |
Le SQL est rejouable : chaque INSERT porte son ON CONFLICT … DO UPDATE. Recharger une base
déjà chargée la met à jour au lieu d'échouer. Les identifiants sont des gen_random_uuid(), jamais
des UUID figés, et un jeu de données est retrouvé par son slug — un identifiant figé se
périmerait au premier réimport.
D'où l'écart avec les chiffres de la page Importer le corpus, qui décrivent
le corpus lui-même. export-corpus.ts lit ce qui est en base au moment du geste : les messages
types du corpus, moins celui qui en est
écarté, plus les quatre gestes d'administration Netskope que
seed-netskope-admin.ts ajoute, plus les trois catalogues que le dépôt possède en propre
(web-destinations, mail-client-process, script-process) et qu'aucun pool du corpus ne
fournit.
L'ordre de chargement est calculé, pas alphabétique
Un message type peut se rattacher au pool d'une autre technologie — deux le font aujourd'hui,
chez SentinelOne Cloud Funnel vers un pool de Github Audit Logs. Le fichier qui crée le pool doit
donc passer avant celui qui s'y rattache, et c'est pourquoi sentinelone-cloud-funnel.sql ferme la
marche là où l'alphabet le placerait au milieu.
L'ordre est un tri topologique sur une seule sorte d'arête — « un message type de A se rattache
à un pool de B, donc B avant A » —, départagé alphabétiquement pour qu'un corpus inchangé rende
toujours le même ordre. Il est écrit dans manifest.json, et le chargeur s'y tient.
Le mode de panne que cela évite est muet, et c'est ce qui justifie un garde-fou plutôt qu'une
règle : un rattachement non résolu ne fait pas échouer le chargement, il laisse le gabarit rendre
avec sample = {} — donc du JSON invalide, dont l'intake n'extrait plus aucun champ et pas une
smart description ne se déclenche. tests/corpus-snapshot.spec.ts tient l'invariant : aucun jeu de
données n'est cité avant d'avoir été créé.
Ce que le SQL ne porte pas, délibérément
| Absent | Pourquoi |
|---|---|
EventTypeAction (la couverture) | Déduite en code par deriveActionCoverage(), qui tourne sur les deux chemins |
EventType.scope / scopeOrigin | Écrits par la même passe, pour la même raison |
| Les parents des pools de premier plan | Posés par completePoolParents(), également sur les deux chemins |
createdAt | La colonne a un défaut ; l'émettre ferait bouger le diff de tous les fichiers à chaque export |
Les figer reviendrait à empêcher une correction du code de prendre effet sur une base semée depuis
l'instantané — exactement la convergence que la colonne origin d'EventTypeAction existe pour
garantir. Le SQL porte le corpus ; ce qui s'en déduit reste déduit.
Aucun DELETE … NOT IN (…) de convergence. Il serait plus strict que l'import lui-même, dont
le contrat écrit est « crée et met à jour, jamais ne retire ». Un tel DELETE emporterait un jeu
de données créé depuis l'écran Datasets ou un message type ajouté à la main, les
catalogues étant éditables. Contrepartie assumée, la même que côté corpus : un message type retiré
du corpus subsiste sur une base déjà semée.
Qui charge l'instantané
pnpm db:seed, dès que_source-oldproject/est absent. La bascule est automatique et annoncée : « Corpus absent de … — chargement de l'instantané SQL ». Le rapport final est alors différent de celui de l'import : la plupart de ses compteurs mesurent l'assemblage, travail que l'instantané a déjà figé, et les afficher à zéro ferait lire « aucun gabarit réécrit » là où il faut lire « la question ne se pose pas ici ».- Le conteneur
migratedes quatre fichiers compose (local,dev,preprod,prod). Il pose le schéma, puis lance le mêmeimport-source.ts— qui se replie, le corpus étant hors image — avant les autres semis. Les backends attendent qu'il ait réussi. - Un poste sans Python, ou fraîchement cloné, par la même bascule.
Le chargement passe par pg et non par Prisma : $executeRawUnsafe n'accepte qu'une instruction à
la fois, là où un fichier en porte des milliers, et le protocole simple de PostgreSQL avale un lot
entier d'un coup. Il se fait en une transaction — un fichier qui échouerait à mi-parcours
laisserait sinon une base avec la moitié des technologies. Avant de valider, les comptes observés
sont confrontés à ceux du manifeste : un instantané régénéré sans que son manifeste le soit
partirait autrement incomplet, en silence.
Quand la rejouer
Après un rafraîchissement de _source-oldproject/, évidemment. Mais aussi — et c'est le cas qu'on
oublie — après toute modification des règles d'assemblage ou de réécriture des gabarits
(template-rewrites.ts, template-assembly.ts). Ces règles sont cuites dans le SQL : sur le
chemin de repli, elles ne sont pas rejouées. Une correction non suivie d'un pnpm db:snapshot
laisse donc la CI et les environnements déployés sur l'ancien rendu.
C'est pourquoi un test le relève désormais. manifest.json porte templateDigest, le SHA-256 des
cinq modules qui façonnent le texte d'un gabarit — template-rewrites.ts, template-assembly.ts,
excluded-event-types.ts, source-registry.ts et extract-source-generators.py — et
snapshot-digest.spec.ts le recalcule. Retoucher l'un d'eux, commentaire compris, fait rougir la CI
jusqu'à la régénération. Le code d'import des pools et des populations en est exclu à dessein : il
voisine avec des passes qui tournent sur les deux chemins, et un garde-fou qui sonne pour rien est un
garde-fou qu'on cesse d'écouter.
pnpm db:snapshot # importe le corpus, puis régénère prisma/seed/corpus/
git add packages/database/prisma/seed/corpus/
L'instantané est versionné en clair, un fichier par technologie, précisément pour que le diff se relise : il montre quels messages types et quels gabarits ont changé. Relisez-le avant de committer — c'est ce que sèmeront la CI et les environnements déployés.
Quand ça coince
| Symptôme | Cause | Geste |
|---|---|---|
| « Corpus source introuvable » | _source-oldproject/ absent | Le déposer à la racine, ou passer son chemin en argument |
DATABASE_URL manquante | Pas de .env à la racine | cp .env.example .env |
python3: command not found | Le sondage exécute la source du corpus | Installer Python 3 |
| « La base ne porte aucune technologie : rien à photographier » | L'import a échoué, ou la base a été vidée entre les deux gestes | Relancer la commande entière, jamais l'export seul |
| « Cycle de rattachement entre technologies » | Deux technologies se rattachent mutuellement à leurs pools | Le corpus est insoluble en l'état : le cycle se casse à la source |
| Au chargement : « L'instantané n'a pas produit ce que son manifeste annonce » | Fichiers et manifeste désaccordés | Régénérer l'instantané entier, jamais un fichier seul |
snapshot-digest.spec.ts rouge : « …ont changé sans pnpm db:snapshot » | Une règle de gabarit a été modifiée sans régénérer l'instantané | pnpm db:snapshot, relire le diff, committer corpus/ avec elle |