[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6CST3A4QFXCFKFH3A6ENBMS":3,"skill-same:01M6CST3A4QFXCFKFH3A6ENBMS":139},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":28,"publishedAt":50,"linkOnly":51,"markdownContent":52,"markdownBody":53,"frontMatter":54,"refName":98,"commitSha":99,"skillDirectory":100,"forksCount":14,"repositoryHtmlUrl":101,"repositoryHomepageUrl":6,"ownerHtmlUrl":102,"ownerBlogUrl":6,"ownerDescription":103,"updatedAt":104,"install":105,"resources":118},"01M6CST3A4QFXCFKFH3A6ENBMS","volontariapp-db-migration",null,"Playbook opérationnel pas-à-pas pour concevoir, tester et propager une migration TypeORM sans coupure de service (Zero-Downtime Expand\u002FContract, partition common\u002Fdomain, yarn meta sync-migrations).","Volontariapp\u002Fmeta","Volontariapp","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F263673138?v=4","01M6CSQNQ3J4SKRWA512JQF5K7","VolontariApp","01M6CSQNV8T2M4PR5A22DKHY5T",0,"MIT","https:\u002F\u002Fgithub.com\u002FVolontariapp\u002Fmeta\u002Fblob\u002Fmain\u002F.agents\u002Fskills\u002Fvolontariapp-db-migration\u002FSKILL.md",[18,23],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000010","后端与接口","SCENARIO","使用场景",{"categoryNo":24,"categoryName":25,"taxonomyType":26,"taxonomyTypeDesc":27},"01JBSEEDCAT000000000000037","律师与法务","PROFESSION","适用职业",[29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49],"migration","db migration","db","volontariapp db migration","npm-packages","2026","src","scripts","de","cli","packages","local","expand","partition","ms","postgresql","common","code","database","ci","Meta","2026-11-10 10:12:30",false,"---\nname: volontariapp-db-migration\ndescription: \"Playbook opérationnel pas-à-pas pour concevoir, tester et propager une migration TypeORM sans coupure de service (Zero-Downtime Expand\u002FContract, partition common\u002Fdomain, yarn meta sync-migrations).\"\ntype: Playbook\ntitle: Migrations PostgreSQL et Synchronisation Multi-Services\ntags: [db-migration, postgresql, typeorm, zero-downtime, expand-contract, playbook]\nstatus: stable\npaths:\n  - \"ms-*\u002Fsrc\u002Fmigrations\u002F**\"\n  - \"npm-packages\u002Fpackages\u002Fdatabase\u002Fsrc\u002F**\"\n  - scripts\u002Fcli\u002Fcommands\u002Fsync-migrations.mjs\nmesh_keys:\n  - migrations\n  - sync-migrations\n  - typeorm\ngenerated:\n  by: claude-code\u002Fagent\n  at: \"2026-10-12T11:56:30Z\"\nverified:\n  - by: claude-code\u002Fagent\n    at: \"2026-11-06T14:04:00Z\"\n    digest: a6bfa42cee9dd9ef\n  - by: claude-code\u002Fagent\n    at: \"2026-11-07T15:34:34Z\"\n    digest: 65868a898ae28556\n  - by: claude-code\u002Fagent\n    at: \"2026-11-07T15:49:20Z\"\n    digest: 65868a898ae28556\n  - by: claude-code\u002Fagent\n    at: \"2026-11-07T15:55:37Z\"\n    digest: 65868a898ae28556\n  - by: claude-code\u002Fagent\n    at: \"2026-11-08T20:59:18Z\"\n    digest: 65868a898ae28556\nsources:\n  - id: sync-mig\n    kind: local_file\n    resource: scripts\u002Fcli\u002Fcommands\u002Fsync-migrations.mjs\n    title: sync-migrations.mjs\n  - id: compose\n    kind: local_file\n    resource: ci-tools\u002Fdocker-compose.yml\n    title: ci-tools\u002Fdocker-compose.yml\n  - id: data-source\n    kind: local_file\n    resource: ms-user\u002Fsrc\u002Fconfig\u002Fdata-source.ts\n    title: ms-user\u002Fsrc\u002Fconfig\u002Fdata-source.ts\n---\n\n# Migrations PostgreSQL et Synchronisation Multi-Services\n\n## Quand utiliser ce playbook\n\nUtiliser ce playbook pour toute évolution de schéma relationnel PostgreSQL dans un microservice (`ms-user`, `ms-post`, `ms-event`, `ms-social`, `ms-storage`, `ms-ranking`) :\n- Ajout, renommage ou modification d'une table, colonne ou index métier.\n- Évolution des tables transactionnelles partagées (`jobs_outbox`, `event_queue`, `job_audit`).\n- Modification des triggers SQL de notification outbox.\n- Synchronisation des migrations vers les packages de test de domaine (`npm-packages\u002Fpackages\u002Fdomain-*`).\n\n---\n\n## Règles d'or de conception\n\n1. **Zero-Downtime obligatoire (Pattern Expand \u002F Contract)** :\n   - Tout ajout de colonne non-nulle doit d'abord être déployé avec une valeur par défaut (`DEFAULT 'value'`) ou être nullable (`NULL`).\n   - Interdiction formelle de renommer ou supprimer (`DROP COLUMN`) une colonne active dans la même release : le code existant doit d'abord cesser de la lire\u002Fécrire (Phase Contract ultérieure).\n2. **Bidirectionnalité stricte** :\n   - Chaque méthode `up()` DOIT avoir son pendant inverse exact et idempotente dans `down()`.\n3. **Isolation stricte par service** :\n   - Chaque microservice dispose de sa base PostgreSQL dédiée (ports 5432-5436 en local)[^compose]. Aucune jointure inter-bases n'est permise.\n4. **Partitionnement rigoureux `common\u002F` vs `domain\u002F`** :\n   - `src\u002Fmigrations\u002Fcommon\u002F` : Infrastructure technique partagée (Outbox, Audit, Triggers).\n   - `src\u002Fmigrations\u002Fdomain\u002F` : Schémas et tables purement métier propres au microservice.\n\n---\n\n## Procédure pas-à-pas\n\n### Étape 1 : Déterminer la portée de la migration\n\nIdentifier le répertoire cible dans le microservice :\n- **Migration Commune** (`ms-*\u002Fsrc\u002Fmigrations\u002Fcommon\u002F`) :\n  - Concerne l'Outbox transactionnelle (`jobs_outbox`, `event_queue`, `job_audit`)[^data-source] ou un trigger SQL de dispatch.\n  - **Script de propagation** : `scripts\u002Fpush-common-migration.sh \u003Cfichier> \"\u003Ctitre du commit>\" [--dry-run]` copie la migration dans les neuf emplacements ci-dessous (worktrees neufs sur `origin\u002Fmain`, md5 contrôlé, commit avec `Co-Authored-By`, push sur `main`). Il s'utilise après publication de `npm-packages` et levée du verrou du STOP (cas de `1794166000000-AddOutboxPollingIndexes`, 2026-11-08, neuf dépôts). Pour suivre la CI ensuite, `gh run list --commit` exige le SHA complet : un SHA court ne renvoie rien.\n  - Doit être créée dans un microservice de référence (ex. `ms-user`), puis **copiée à l'identique** (même nom, même contenu, vérifier le md5) dans `src\u002Fmigrations\u002Fcommon\u002F` de chaque service qui a **sa propre base** : `ms-user`, `ms-social`, `ms-post`, `ms-event`, `ms-storage`, `ms-ranking` **et `ws-service`** (base `ms_ws`, oublié lors de la tâche 2.9 : une colonne ajoutée à `OutboxModel` fait échouer ses écritures tant que la migration manque). `post-processors-runner\u002Fpost-processor-event` et `workers-runners\u002Fworker-event` gardent aussi une copie et utilisent en production la base `ms_event` de `ms-event` (migration déjà appliquée), **mais leur CI reconstruit une base neuve depuis leurs propres migrations** (`global-setup` de `worker-event` : drop du schéma puis `runMigrations`) : sans la copie, `column EventQueueModel.traceparent does not exist` (constaté sur workers-runners#16). Copier aussi la nouvelle migration commune dans ces deux dossiers. Pour reproduire la CI en local sans toucher à sa base : `DB_NAME=\u003Cbase jetable> yarn test:integration`. `yarn meta sync-migrations` ne fait PAS cette propagation inter-services[^sync-mig]. Pour repérer les copies : chercher le nom d'une migration commune existante (`1781000000000-AddCorrelationIdToEventQueue.ts`) sur tout le workspace, hors `deploy\u002F`. Ces trois dossiers ont dérivé (migrations plus anciennes que celles de `ms-*`) : ne copier que la nouvelle migration. Le md5 peut différer sans que le SQL change : la copie de `1781400000000-AddFinishedAndCancelledToEventStateEnum.ts` dans `worker-event` (`workers-runners#50`, dont le message de commit annonce « byte-identically ») a un md5 différent de celle de `ms-event` parce que ses lignes sont recoupées, alors que les requêtes `up`\u002F`down` sont les mêmes (cause probable : le formatage du repo cible, non vérifiée). Si le md5 diffère, comparer les requêtes avec `diff` plutôt que conclure à une dérive. `1792200000000-AddScoringVersionToEvent.ts` est, lui, identique au md5.\n- **Migration Domaine** (`ms-*\u002Fsrc\u002Fmigrations\u002Fdomain\u002F`) :\n  - Concerne les tables propres au métier (ex. `users`, `badges` dans `ms-user` ; `posts` dans `ms-post`).\n  - Reste cantonnée au répertoire `domain\u002F` du microservice concerné.\n\n---\n\n### Étape 2 : Écrire la migration TypeORM\n\nCréer un fichier nommé avec le timestamp Unix actuel sous la forme `\u003Ctimestamp>-\u003CNomExplicite>.ts` :\n\n```typescript\nimport type { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class AddFeatureColumn1792000000000 implements MigrationInterface {\n  name = 'AddFeatureColumn1792000000000';\n\n  public async up(queryRunner: QueryRunner): Promise\u003Cvoid> {\n    \u002F\u002F Pattern Expand : colonne avec DEFAULT pour ne pas bloquer les écritures en prod\n    await queryRunner.query(\n      `ALTER TABLE \"users\" ADD COLUMN \"feature_flag\" boolean NOT NULL DEFAULT false`,\n    );\n    \u002F\u002F Pas de CONCURRENTLY ici : voir le piège juste après ce bloc\n    await queryRunner.query(\n      `CREATE INDEX \"idx_users_feature_flag\" ON \"users\" (\"feature_flag\")`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise\u003Cvoid> {\n    await queryRunner.query(`DROP INDEX \"idx_users_feature_flag\"`);\n    await queryRunner.query(`ALTER TABLE \"users\" DROP COLUMN \"feature_flag\"`);\n  }\n}\n```\n\n**Piège : `CREATE INDEX CONCURRENTLY` et `transaction = false` sont refusés.** `CONCURRENTLY` ne s'exécute pas dans une transaction, mais une migration qui déclare `transaction = false` fait lever `ForbiddenTransactionModeOverrideError` quand la source de données exécute toutes les migrations en une transaction, ce qui est le mode par défaut de TypeORM (`migrationsTransactionMode: 'all'`, lu dans `typeorm\u002Fmigration\u002FMigrationExecutor.js`). Dans ce cas aucun service ne démarre (les migrations tournent dans le conteneur d'init `run-migrations`). Écrire `CREATE INDEX IF NOT EXISTS` dans la transaction : le verrou en écriture dure le temps de la construction, quelques millisecondes sur une table petite. Sur une grosse table, purger ou réduire d'abord, ou changer le mode de transaction des sources de données avant la migration (non fait à ce jour).\n\n---\n\n### Étape 3 : Déclarer les entités et vérifier `data-source.ts`\n\n1. Si la migration introduit une nouvelle entité ou un nouveau champ, mettre à jour l'entité TypeORM correspondante dans le package de domaine (`@volontariapp\u002Fdomain-*`) ou `@volontariapp\u002Fdatabase`.\n2. Vérifier que `src\u002Fconfig\u002Fdata-source.ts` référence bien l'entité dans son tableau `entities`[^data-source] :\n\n```typescript\n\u002F\u002F ms-user\u002Fsrc\u002Fconfig\u002Fdata-source.ts\nexport const AppDataSource = new DataSource({\n  type: 'postgres',\n  \u002F* ... *\u002F\n  entities: [\n    EventQueueModel,\n    JobsOutboxModel,\n    JobAuditModel,\n    UserModel,\n    NewFeatureModel, \u002F\u002F \u003C- Entité déclarée\n  ],\n  synchronize: false,\n  migrations: [\n    join(dirname(fileURLToPath(import.meta.url)), '..', 'migrations', '**', '*.{ts,js}'),\n  ],\n});\n```\n\n3. Si un `post-processor-*` ou un `worker-*` lit ou écrit la nouvelle table via le `DataSource` de la base de `ms-\u003Cd>` (ex. `post-processor-user` sur `ms_user`), ajouter aussi l'entité à la liste `entities` de son `infrastructure.module.ts` : sans cela, TypeORM lève « No metadata for X was found » au premier traitement, et les tests unitaires qui mockent le `DataSource` ne le voient pas. Exemple : `impact_score_credits` (`ImpactScoreCreditModel`, migration `1792100000000-CreateImpactScoreCredits` dans `ms-user`).\n4. **Monter `domain-\u003Cd>` dans un `worker-*` casse sa CI si ses copies de migrations ont dérivé.** Seuls les packages qui ont un `global-setup` de test d'intégration (`worker-event`) rebuild une base neuve depuis `src\u002Fmigrations\u002F` : le `domain-\u003Cd>` récent y mappe des colonnes que la copie n'a pas. Un package sans `global-setup` n'est pas concerné : `post-processor-event` avait pourtant la même dérive (2 migrations de domaine et 4 communes en retard sur `ms-event`) mais n'exécute jamais ses migrations (aucun `migrationsRun` ni `runMigrations` dans `src`, 3 tests unitaires avec `DataSource` mocké). On l'a résorbée par hygiène, sans effet sur la CI. Avant d'affirmer qu'une dérive casse quelque chose, chercher ce qui exécute réellement les migrations du package. Constaté sur `workers-runners#19` : `domain-event@3.12.1` mappe `cover_file_id`, absente de `worker-event` → `column \"cover_file_id\" of relation \"events\" does not exist`, 24 tests rouges. Correctif : copier la migration manquante à l'identique (`diff \u003C(ls ms-event\u002Fsrc\u002Fmigrations\u002Fdomain) \u003C(ls worker-event\u002Fsrc\u002Fmigrations\u002Fdomain)`, puis `md5`). Reproduire en local sans toucher aux données : `global-setup` fait `DROP SCHEMA public CASCADE` sur `DB_NAME`, dont la valeur par défaut est `ms_event`. Créer une base jetable (`CREATE DATABASE worker_event_ci_check`) et lancer `DB_NAME=worker_event_ci_check REDIS_PORT=\u003Cport> yarn test:integration` avec un Redis jetable (`docker run redis:7-alpine redis-server --requirepass password`) : `REDIS_DB_INDEX` ne suffit pas, les jobs BullMQ ne sont alors jamais traités (`handlerSpy` appelé 0 fois).\n\n---\n\n### Étape 4 : Propager les migrations avec `yarn meta sync-migrations` (si applicable)\n\nPour toute modification dans `src\u002Fmigrations\u002F` (commune ou domaine pour les tests) :\n1. Lancer la commande à la racine de `meta`[^sync-mig] (toujours non interactive) :\n   ```bash\n   yarn meta sync-migrations --dry-run                    # Prévisualiser (+ ajouté, ~ modifié, - supprimé), sans rien écrire\n   yarn meta sync-migrations                              # Tout synchroniser\n   yarn meta sync-migrations --packages storage,user      # Microservices précis (avec ou sans préfixe ms-)\n   yarn meta sync-migrations --pr                         # Branche, changeset, build, test, PR dans npm-packages, puis arrêt (règle STOP) ; refuse si npm-packages a des modifications locales, et incompatible avec --dry-run\n   ```\n2. Microservices gérés : tous les `ms-*` de `repos.json` avec `migrations: true` et un package `domain-*` présent (`user`, `social`, `post`, `event`, `storage`, `ranking`). Pour `ranking` (état vérifié le 2026-10-28 avant la CLI) : son dossier `domain-ranking\u002Fsrc\u002Ftest\u002Fmigrations\u002Fdomain\u002F` contient pour l'instant la migration `CreateRankingTables` écrite à la main, copiée à l'identique dans `ms-ranking\u002Fsrc\u002Fmigrations\u002Fdomain\u002F` (vérifié le 2026-10-28 : fichiers identiques, 10 tables `ranking_*` : `ranking_score`, `ranking_post_score`, `ranking_run`, `ranking_request`, `ranking_like_log`, `ranking_event_feedback`, `ranking_event_wish`, `ranking_event_mention`, `ranking_user_location`, `ranking_rate_bucket`). Les 16 migrations communes de `ms-ranking` sont identiques à celles de `ms-user`.\n3. La commande synchronise par un miroir équivalent à `rsync -ac --delete` (comparaison par contenu) les fichiers vers `npm-packages\u002Fpackages\u002Fdomain-\u003Cservice>\u002Fsrc\u002Ftest\u002Fmigrations` (copie `ms-*` vers `domain-*`, jamais entre services) afin de garantir que les tests du domaine s'exécutent sur un schéma strictement identique.\n4. **Piège : dérive.** Ces dossiers `test\u002Fmigrations` ne sont pas à jour (au 2026-10-13 : migrations absentes ou renommées, `--delete` supprimerait des fichiers dans `domain-post`, `domain-event`, `domain-storage`). Vérifier avec `yarn meta sync-migrations --dry-run` avant. Pour une migration commune isolée, copier uniquement le nouveau fichier plutôt que de resynchroniser le dossier entier.\n5. **Copie `workers`** : `npm-packages\u002Fpackages\u002Fworkers\u002Fsrc\u002Ftest\u002Fmigrations\u002F` contient aussi les migrations communes (outbox, `job_audit`, triggers), hors de `yarn meta sync-migrations`. Y ajouter toute nouvelle migration commune, sinon les tests de `workers` tournent sur un schéma obsolète (la PR traceparent l'a réalignée le 2026-10-13).\n6. **STOP rule** : `domain-*\u002Fsrc\u002Ftest\u002Fmigrations` et `database` sont sous `npm-packages`. Faire d'abord toutes les éditions `ms-*`, puis `npm-packages` en dernier : le hook `stop-rule-guard.sh` refuse les éditions `ms-*` après la première édition `npm-packages`.\n\n7. **Quand `npm-packages` est déjà modifié (le modèle et la migration vont ensemble).** Le hook refuse alors l'édition de `ms-*`. Écrire la migration **d'abord** dans `npm-packages\u002Fpackages\u002Fdomain-\u003Cd>\u002Fsrc\u002Ftest\u002Fmigrations\u002Fdomain\u002F`, dans la PR `npm-packages`, puis, une fois les packages publiés et le verrou levé, la **copier à l'identique** dans `ms-*\u002Fsrc\u002Fmigrations\u002Fdomain\u002F` (comparer le `md5`). Ne pas lancer `yarn meta sync-migrations` pour cela : il synchronise tout le dossier et le piège de dérive du point 4 s'applique. Exemple : `AddBadgePointsAndProgressDedup1793970000000` (`npm-packages#299`, puis `ms-user#88`). Ajouter aussi les nouvelles entités à `entities` de `data-source.ts` et du `database.module.ts` du microservice.\n\n---\n\n### Étape 5 : Exécution et validation locale\n\n1. Démarrer l'infrastructure locale PostgreSQL via Docker Compose[^compose] :\n   ```bash\n   docker compose -f ci-tools\u002Fdocker-compose.yml up -d postgres-user\n   ```\n2. Exécuter la migration en environnement local :\n   ```bash\n   cd ms-user\n   NODE_ENV=local yarn typeorm migration:run -d src\u002Fconfig\u002Fdata-source.ts\n   ```\n3. Valider impérativement le rollback (`down`) :\n   ```bash\n   NODE_ENV=local yarn typeorm migration:revert -d src\u002Fconfig\u002Fdata-source.ts\n   ```\n4. Réappliquer la migration :\n   ```bash\n   NODE_ENV=local yarn typeorm migration:run -d src\u002Fconfig\u002Fdata-source.ts\n   ```\n5. Lancer la suite de tests du microservice :\n   ```bash\n   yarn test\n   ```\n\n**Valider sur une base jetable plutôt que sur le volume du compose.** `docker compose up postgres-user` réutilise le volume persistant `postgres_user_data`, et le `data-source` de test de `domain-*` fait un `dropDatabase()`. Pour le cycle `run`, `revert`, `run` (et pour les tests d'intégration `yarn test:integration` de `domain-\u003Cd>`), lancer un Postgres sans volume sur le port 5432, après avoir vérifié qu'il est libre :\n\n```bash\nlsof -nP -iTCP:5432 -sTCP:LISTEN   # doit ne rien afficher\ndocker run --rm -d --name pg-migration-check -p 5432:5432 \\\n  -e POSTGRES_USER=user -e POSTGRES_PASSWORD=password -e POSTGRES_DB=ms_user postgres:16-alpine\n# ... migration:run, migration:revert, migration:run ...\ndocker stop pg-migration-check\n```\n\nContrôler aussi les données posées par la migration (`UPDATE` de remplissage) en insérant une ligne de test avant le second `run`.\n\n---\n\n## Tables d'outbox : index et purge (depuis `@volontariapp\u002Fdatabase` 3.13.0)\n\n- `event_queue` et `jobs_outbox` portent l'index `(status, created_at, id)` (migration commune `1794166000000-AddOutboxPollingIndexes`, déclaré aussi par `@Index` sur `EventQueueModel` et `JobsOutboxModel`). Il sert la requête de sondage des runners (`WHERE status = 'PENDING' ORDER BY created_at, id LIMIT n FOR UPDATE SKIP LOCKED`) et la purge. Sans lui, chaque sondage lisait toute la table : en novembre 2026, 125 à 245 Mo\u002Fs de lectures disque continues sur `ms-ranking-db` ont fait caler la VM entière plusieurs secondes de suite.\n- `OutboxRunner` purge les lignes `COMPLETED` plus vieilles que la rétention (défaut : 1 jour, une passe par heure, lots de 5 000, sur sa propre boucle), et `job_audit` à côté de `jobs_outbox`. Réglage : `OutboxRunnerConfig.purge` (`enabled`, `retentionMs`, `intervalMs`, `batchSize`), tous optionnels. Les lignes `PENDING`, `PROCESSING` et `FAILED` ne sont jamais supprimées. Conséquence : l'historique des lignes terminées ne dépasse pas un jour (voir `volontariapp-trace-async-flow`).\n- `job_audit` n'a pas d'index `(status, created_at)` : sa purge trie les lignes `COMPLETED` (environ 200 000 à ce jour) à chaque passe, une lecture ponctuelle de l'ordre de 100 Mo observée à la première passe. À indexer si elle gêne.\n- Pour une nouvelle requête de sondage ou de purge sur une table qui grossit, vérifier le plan (`EXPLAIN`) : un `Seq Scan` sur une table d'outbox est le signal à traiter.\n\n---\n\n## Checklist de validation\n\n- [ ] La migration applique le pattern **Expand \u002F Contract** (aucun `DROP COLUMN` ni renommage brutal direct).\n- [ ] La méthode `down()` est strictement l'inverse de la méthode `up()`.\n- [ ] Les nouvelles entités sont exportées et enregistrées dans `entities` de `src\u002Fconfig\u002Fdata-source.ts`[^data-source].\n- [ ] Si une migration commune a été touchée, `yarn meta sync-migrations` a été exécuté[^sync-mig].\n- [ ] Le cycle `migration:run` $\\rightarrow$ `migration:revert` $\\rightarrow$ `migration:run` s'exécute sans erreur sur la base locale[^compose].\n- [ ] `yarn test` passe avec succès sur le microservice.\n\n---\n\n## Références\n\n[^sync-mig]: scripts\u002Fcli\u002Fcommands\u002Fsync-migrations.mjs\n[^compose]: ci-tools\u002Fdocker-compose.yml\n[^data-source]: ms-user\u002Fsrc\u002Fconfig\u002Fdata-source.ts\n","# Migrations PostgreSQL et Synchronisation Multi-Services\n\n## Quand utiliser ce playbook\n\nUtiliser ce playbook pour toute évolution de schéma relationnel PostgreSQL dans un microservice (`ms-user`, `ms-post`, `ms-event`, `ms-social`, `ms-storage`, `ms-ranking`) :\n- Ajout, renommage ou modification d'une table, colonne ou index métier.\n- Évolution des tables transactionnelles partagées (`jobs_outbox`, `event_queue`, `job_audit`).\n- Modification des triggers SQL de notification outbox.\n- Synchronisation des migrations vers les packages de test de domaine (`npm-packages\u002Fpackages\u002Fdomain-*`).\n\n---\n\n## Règles d'or de conception\n\n1. **Zero-Downtime obligatoire (Pattern Expand \u002F Contract)** :\n   - Tout ajout de colonne non-nulle doit d'abord être déployé avec une valeur par défaut (`DEFAULT 'value'`) ou être nullable (`NULL`).\n   - Interdiction formelle de renommer ou supprimer (`DROP COLUMN`) une colonne active dans la même release : le code existant doit d'abord cesser de la lire\u002Fécrire (Phase Contract ultérieure).\n2. **Bidirectionnalité stricte** :\n   - Chaque méthode `up()` DOIT avoir son pendant inverse exact et idempotente dans `down()`.\n3. **Isolation stricte par service** :\n   - Chaque microservice dispose de sa base PostgreSQL dédiée (ports 5432-5436 en local)[^compose]. Aucune jointure inter-bases n'est permise.\n4. **Partitionnement rigoureux `common\u002F` vs `domain\u002F`** :\n   - `src\u002Fmigrations\u002Fcommon\u002F` : Infrastructure technique partagée (Outbox, Audit, Triggers).\n   - `src\u002Fmigrations\u002Fdomain\u002F` : Schémas et tables purement métier propres au microservice.\n\n---\n\n## Procédure pas-à-pas\n\n### Étape 1 : Déterminer la portée de la migration\n\nIdentifier le répertoire cible dans le microservice :\n- **Migration Commune** (`ms-*\u002Fsrc\u002Fmigrations\u002Fcommon\u002F`) :\n  - Concerne l'Outbox transactionnelle (`jobs_outbox`, `event_queue`, `job_audit`)[^data-source] ou un trigger SQL de dispatch.\n  - **Script de propagation** : `scripts\u002Fpush-common-migration.sh \u003Cfichier> \"\u003Ctitre du commit>\" [--dry-run]` copie la migration dans les neuf emplacements ci-dessous (worktrees neufs sur `origin\u002Fmain`, md5 contrôlé, commit avec `Co-Authored-By`, push sur `main`). Il s'utilise après publication de `npm-packages` et levée du verrou du STOP (cas de `1794166000000-AddOutboxPollingIndexes`, 2026-11-08, neuf dépôts). Pour suivre la CI ensuite, `gh run list --commit` exige le SHA complet : un SHA court ne renvoie rien.\n  - Doit être créée dans un microservice de référence (ex. `ms-user`), puis **copiée à l'identique** (même nom, même contenu, vérifier le md5) dans `src\u002Fmigrations\u002Fcommon\u002F` de chaque service qui a **sa propre base** : `ms-user`, `ms-social`, `ms-post`, `ms-event`, `ms-storage`, `ms-ranking` **et `ws-service`** (base `ms_ws`, oublié lors de la tâche 2.9 : une colonne ajoutée à `OutboxModel` fait échouer ses écritures tant que la migration manque). `post-processors-runner\u002Fpost-processor-event` et `workers-runners\u002Fworker-event` gardent aussi une copie et utilisent en production la base `ms_event` de `ms-event` (migration déjà appliquée), **mais leur CI reconstruit une base neuve depuis leurs propres migrations** (`global-setup` de `worker-event` : drop du schéma puis `runMigrations`) : sans la copie, `column EventQueueModel.traceparent does not exist` (constaté sur workers-runners#16). Copier aussi la nouvelle migration commune dans ces deux dossiers. Pour reproduire la CI en local sans toucher à sa base : `DB_NAME=\u003Cbase jetable> yarn test:integration`. `yarn meta sync-migrations` ne fait PAS cette propagation inter-services[^sync-mig]. Pour repérer les copies : chercher le nom d'une migration commune existante (`1781000000000-AddCorrelationIdToEventQueue.ts`) sur tout le workspace, hors `deploy\u002F`. Ces trois dossiers ont dérivé (migrations plus anciennes que celles de `ms-*`) : ne copier que la nouvelle migration. Le md5 peut différer sans que le SQL change : la copie de `1781400000000-AddFinishedAndCancelledToEventStateEnum.ts` dans `worker-event` (`workers-runners#50`, dont le message de commit annonce « byte-identically ») a un md5 différent de celle de `ms-event` parce que ses lignes sont recoupées, alors que les requêtes `up`\u002F`down` sont les mêmes (cause probable : le formatage du repo cible, non vérifiée). Si le md5 diffère, comparer les requêtes avec `diff` plutôt que conclure à une dérive. `1792200000000-AddScoringVersionToEvent.ts` est, lui, identique au md5.\n- **Migration Domaine** (`ms-*\u002Fsrc\u002Fmigrations\u002Fdomain\u002F`) :\n  - Concerne les tables propres au métier (ex. `users`, `badges` dans `ms-user` ; `posts` dans `ms-post`).\n  - Reste cantonnée au répertoire `domain\u002F` du microservice concerné.\n\n---\n\n### Étape 2 : Écrire la migration TypeORM\n\nCréer un fichier nommé avec le timestamp Unix actuel sous la forme `\u003Ctimestamp>-\u003CNomExplicite>.ts` :\n\n```typescript\nimport type { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class AddFeatureColumn1792000000000 implements MigrationInterface {\n  name = 'AddFeatureColumn1792000000000';\n\n  public async up(queryRunner: QueryRunner): Promise\u003Cvoid> {\n    \u002F\u002F Pattern Expand : colonne avec DEFAULT pour ne pas bloquer les écritures en prod\n    await queryRunner.query(\n      `ALTER TABLE \"users\" ADD COLUMN \"feature_flag\" boolean NOT NULL DEFAULT false`,\n    );\n    \u002F\u002F Pas de CONCURRENTLY ici : voir le piège juste après ce bloc\n    await queryRunner.query(\n      `CREATE INDEX \"idx_users_feature_flag\" ON \"users\" (\"feature_flag\")`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise\u003Cvoid> {\n    await queryRunner.query(`DROP INDEX \"idx_users_feature_flag\"`);\n    await queryRunner.query(`ALTER TABLE \"users\" DROP COLUMN \"feature_flag\"`);\n  }\n}\n```\n\n**Piège : `CREATE INDEX CONCURRENTLY` et `transaction = false` sont refusés.** `CONCURRENTLY` ne s'exécute pas dans une transaction, mais une migration qui déclare `transaction = false` fait lever `ForbiddenTransactionModeOverrideError` quand la source de données exécute toutes les migrations en une transaction, ce qui est le mode par défaut de TypeORM (`migrationsTransactionMode: 'all'`, lu dans `typeorm\u002Fmigration\u002FMigrationExecutor.js`). Dans ce cas aucun service ne démarre (les migrations tournent dans le conteneur d'init `run-migrations`). Écrire `CREATE INDEX IF NOT EXISTS` dans la transaction : le verrou en écriture dure le temps de la construction, quelques millisecondes sur une table petite. Sur une grosse table, purger ou réduire d'abord, ou changer le mode de transaction des sources de données avant la migration (non fait à ce jour).\n\n---\n\n### Étape 3 : Déclarer les entités et vérifier `data-source.ts`\n\n1. Si la migration introduit une nouvelle entité ou un nouveau champ, mettre à jour l'entité TypeORM correspondante dans le package de domaine (`@volontariapp\u002Fdomain-*`) ou `@volontariapp\u002Fdatabase`.\n2. Vérifier que `src\u002Fconfig\u002Fdata-source.ts` référence bien l'entité dans son tableau `entities`[^data-source] :\n\n```typescript\n\u002F\u002F ms-user\u002Fsrc\u002Fconfig\u002Fdata-source.ts\nexport const AppDataSource = new DataSource({\n  type: 'postgres',\n  \u002F* ... *\u002F\n  entities: [\n    EventQueueModel,\n    JobsOutboxModel,\n    JobAuditModel,\n    UserModel,\n    NewFeatureModel, \u002F\u002F \u003C- Entité déclarée\n  ],\n  synchronize: false,\n  migrations: [\n    join(dirname(fileURLToPath(import.meta.url)), '..', 'migrations', '**', '*.{ts,js}'),\n  ],\n});\n```\n\n3. Si un `post-processor-*` ou un `worker-*` lit ou écrit la nouvelle table via le `DataSource` de la base de `ms-\u003Cd>` (ex. `post-processor-user` sur `ms_user`), ajouter aussi l'entité à la liste `entities` de son `infrastructure.module.ts` : sans cela, TypeORM lève « No metadata for X was found » au premier traitement, et les tests unitaires qui mockent le `DataSource` ne le voient pas. Exemple : `impact_score_credits` (`ImpactScoreCreditModel`, migration `1792100000000-CreateImpactScoreCredits` dans `ms-user`).\n4. **Monter `domain-\u003Cd>` dans un `worker-*` casse sa CI si ses copies de migrations ont dérivé.** Seuls les packages qui ont un `global-setup` de test d'intégration (`worker-event`) rebuild une base neuve depuis `src\u002Fmigrations\u002F` : le `domain-\u003Cd>` récent y mappe des colonnes que la copie n'a pas. Un package sans `global-setup` n'est pas concerné : `post-processor-event` avait pourtant la même dérive (2 migrations de domaine et 4 communes en retard sur `ms-event`) mais n'exécute jamais ses migrations (aucun `migrationsRun` ni `runMigrations` dans `src`, 3 tests unitaires avec `DataSource` mocké). On l'a résorbée par hygiène, sans effet sur la CI. Avant d'affirmer qu'une dérive casse quelque chose, chercher ce qui exécute réellement les migrations du package. Constaté sur `workers-runners#19` : `domain-event@3.12.1` mappe `cover_file_id`, absente de `worker-event` → `column \"cover_file_id\" of relation \"events\" does not exist`, 24 tests rouges. Correctif : copier la migration manquante à l'identique (`diff \u003C(ls ms-event\u002Fsrc\u002Fmigrations\u002Fdomain) \u003C(ls worker-event\u002Fsrc\u002Fmigrations\u002Fdomain)`, puis `md5`). Reproduire en local sans toucher aux données : `global-setup` fait `DROP SCHEMA public CASCADE` sur `DB_NAME`, dont la valeur par défaut est `ms_event`. Créer une base jetable (`CREATE DATABASE worker_event_ci_check`) et lancer `DB_NAME=worker_event_ci_check REDIS_PORT=\u003Cport> yarn test:integration` avec un Redis jetable (`docker run redis:7-alpine redis-server --requirepass password`) : `REDIS_DB_INDEX` ne suffit pas, les jobs BullMQ ne sont alors jamais traités (`handlerSpy` appelé 0 fois).\n\n---\n\n### Étape 4 : Propager les migrations avec `yarn meta sync-migrations` (si applicable)\n\nPour toute modification dans `src\u002Fmigrations\u002F` (commune ou domaine pour les tests) :\n1. Lancer la commande à la racine de `meta`[^sync-mig] (toujours non interactive) :\n   ```bash\n   yarn meta sync-migrations --dry-run                    # Prévisualiser (+ ajouté, ~ modifié, - supprimé), sans rien écrire\n   yarn meta sync-migrations                              # Tout synchroniser\n   yarn meta sync-migrations --packages storage,user      # Microservices précis (avec ou sans préfixe ms-)\n   yarn meta sync-migrations --pr                         # Branche, changeset, build, test, PR dans npm-packages, puis arrêt (règle STOP) ; refuse si npm-packages a des modifications locales, et incompatible avec --dry-run\n   ```\n2. Microservices gérés : tous les `ms-*` de `repos.json` avec `migrations: true` et un package `domain-*` présent (`user`, `social`, `post`, `event`, `storage`, `ranking`). Pour `ranking` (état vérifié le 2026-10-28 avant la CLI) : son dossier `domain-ranking\u002Fsrc\u002Ftest\u002Fmigrations\u002Fdomain\u002F` contient pour l'instant la migration `CreateRankingTables` écrite à la main, copiée à l'identique dans `ms-ranking\u002Fsrc\u002Fmigrations\u002Fdomain\u002F` (vérifié le 2026-10-28 : fichiers identiques, 10 tables `ranking_*` : `ranking_score`, `ranking_post_score`, `ranking_run`, `ranking_request`, `ranking_like_log`, `ranking_event_feedback`, `ranking_event_wish`, `ranking_event_mention`, `ranking_user_location`, `ranking_rate_bucket`). Les 16 migrations communes de `ms-ranking` sont identiques à celles de `ms-user`.\n3. La commande synchronise par un miroir équivalent à `rsync -ac --delete` (comparaison par contenu) les fichiers vers `npm-packages\u002Fpackages\u002Fdomain-\u003Cservice>\u002Fsrc\u002Ftest\u002Fmigrations` (copie `ms-*` vers `domain-*`, jamais entre services) afin de garantir que les tests du domaine s'exécutent sur un schéma strictement identique.\n4. **Piège : dérive.** Ces dossiers `test\u002Fmigrations` ne sont pas à jour (au 2026-10-13 : migrations absentes ou renommées, `--delete` supprimerait des fichiers dans `domain-post`, `domain-event`, `domain-storage`). Vérifier avec `yarn meta sync-migrations --dry-run` avant. Pour une migration commune isolée, copier uniquement le nouveau fichier plutôt que de resynchroniser le dossier entier.\n5. **Copie `workers`** : `npm-packages\u002Fpackages\u002Fworkers\u002Fsrc\u002Ftest\u002Fmigrations\u002F` contient aussi les migrations communes (outbox, `job_audit`, triggers), hors de `yarn meta sync-migrations`. Y ajouter toute nouvelle migration commune, sinon les tests de `workers` tournent sur un schéma obsolète (la PR traceparent l'a réalignée le 2026-10-13).\n6. **STOP rule** : `domain-*\u002Fsrc\u002Ftest\u002Fmigrations` et `database` sont sous `npm-packages`. Faire d'abord toutes les éditions `ms-*`, puis `npm-packages` en dernier : le hook `stop-rule-guard.sh` refuse les éditions `ms-*` après la première édition `npm-packages`.\n\n7. **Quand `npm-packages` est déjà modifié (le modèle et la migration vont ensemble).** Le hook refuse alors l'édition de `ms-*`. Écrire la migration **d'abord** dans `npm-packages\u002Fpackages\u002Fdomain-\u003Cd>\u002Fsrc\u002Ftest\u002Fmigrations\u002Fdomain\u002F`, dans la PR `npm-packages`, puis, une fois les packages publiés et le verrou levé, la **copier à l'identique** dans `ms-*\u002Fsrc\u002Fmigrations\u002Fdomain\u002F` (comparer le `md5`). Ne pas lancer `yarn meta sync-migrations` pour cela : il synchronise tout le dossier et le piège de dérive du point 4 s'applique. Exemple : `AddBadgePointsAndProgressDedup1793970000000` (`npm-packages#299`, puis `ms-user#88`). Ajouter aussi les nouvelles entités à `entities` de `data-source.ts` et du `database.module.ts` du microservice.\n\n---\n\n### Étape 5 : Exécution et validation locale\n\n1. Démarrer l'infrastructure locale PostgreSQL via Docker Compose[^compose] :\n   ```bash\n   docker compose -f ci-tools\u002Fdocker-compose.yml up -d postgres-user\n   ```\n2. Exécuter la migration en environnement local :\n   ```bash\n   cd ms-user\n   NODE_ENV=local yarn typeorm migration:run -d src\u002Fconfig\u002Fdata-source.ts\n   ```\n3. Valider impérativement le rollback (`down`) :\n   ```bash\n   NODE_ENV=local yarn typeorm migration:revert -d src\u002Fconfig\u002Fdata-source.ts\n   ```\n4. Réappliquer la migration :\n   ```bash\n   NODE_ENV=local yarn typeorm migration:run -d src\u002Fconfig\u002Fdata-source.ts\n   ```\n5. Lancer la suite de tests du microservice :\n   ```bash\n   yarn test\n   ```\n\n**Valider sur une base jetable plutôt que sur le volume du compose.** `docker compose up postgres-user` réutilise le volume persistant `postgres_user_data`, et le `data-source` de test de `domain-*` fait un `dropDatabase()`. Pour le cycle `run`, `revert`, `run` (et pour les tests d'intégration `yarn test:integration` de `domain-\u003Cd>`), lancer un Postgres sans volume sur le port 5432, après avoir vérifié qu'il est libre :\n\n```bash\nlsof -nP -iTCP:5432 -sTCP:LISTEN   # doit ne rien afficher\ndocker run --rm -d --name pg-migration-check -p 5432:5432 \\\n  -e POSTGRES_USER=user -e POSTGRES_PASSWORD=password -e POSTGRES_DB=ms_user postgres:16-alpine\n# ... migration:run, migration:revert, migration:run ...\ndocker stop pg-migration-check\n```\n\nContrôler aussi les données posées par la migration (`UPDATE` de remplissage) en insérant une ligne de test avant le second `run`.\n\n---\n\n## Tables d'outbox : index et purge (depuis `@volontariapp\u002Fdatabase` 3.13.0)\n\n- `event_queue` et `jobs_outbox` portent l'index `(status, created_at, id)` (migration commune `1794166000000-AddOutboxPollingIndexes`, déclaré aussi par `@Index` sur `EventQueueModel` et `JobsOutboxModel`). Il sert la requête de sondage des runners (`WHERE status = 'PENDING' ORDER BY created_at, id LIMIT n FOR UPDATE SKIP LOCKED`) et la purge. Sans lui, chaque sondage lisait toute la table : en novembre 2026, 125 à 245 Mo\u002Fs de lectures disque continues sur `ms-ranking-db` ont fait caler la VM entière plusieurs secondes de suite.\n- `OutboxRunner` purge les lignes `COMPLETED` plus vieilles que la rétention (défaut : 1 jour, une passe par heure, lots de 5 000, sur sa propre boucle), et `job_audit` à côté de `jobs_outbox`. Réglage : `OutboxRunnerConfig.purge` (`enabled`, `retentionMs`, `intervalMs`, `batchSize`), tous optionnels. Les lignes `PENDING`, `PROCESSING` et `FAILED` ne sont jamais supprimées. Conséquence : l'historique des lignes terminées ne dépasse pas un jour (voir `volontariapp-trace-async-flow`).\n- `job_audit` n'a pas d'index `(status, created_at)` : sa purge trie les lignes `COMPLETED` (environ 200 000 à ce jour) à chaque passe, une lecture ponctuelle de l'ordre de 100 Mo observée à la première passe. À indexer si elle gêne.\n- Pour une nouvelle requête de sondage ou de purge sur une table qui grossit, vérifier le plan (`EXPLAIN`) : un `Seq Scan` sur une table d'outbox est le signal à traiter.\n\n---\n\n## Checklist de validation\n\n- [ ] La migration applique le pattern **Expand \u002F Contract** (aucun `DROP COLUMN` ni renommage brutal direct).\n- [ ] La méthode `down()` est strictement l'inverse de la méthode `up()`.\n- [ ] Les nouvelles entités sont exportées et enregistrées dans `entities` de `src\u002Fconfig\u002Fdata-source.ts`[^data-source].\n- [ ] Si une migration commune a été touchée, `yarn meta sync-migrations` a été exécuté[^sync-mig].\n- [ ] Le cycle `migration:run` $\\rightarrow$ `migration:revert` $\\rightarrow$ `migration:run` s'exécute sans erreur sur la base locale[^compose].\n- [ ] `yarn test` passe avec succès sur le microservice.\n\n---\n\n## Références\n\n[^sync-mig]: scripts\u002Fcli\u002Fcommands\u002Fsync-migrations.mjs\n[^compose]: ci-tools\u002Fdocker-compose.yml\n[^data-source]: ms-user\u002Fsrc\u002Fconfig\u002Fdata-source.ts\n",{"name":5,"tags":55,"type":61,"paths":62,"title":66,"status":67,"sources":68,"verified":79,"generated":93,"mesh_keys":95,"description":7},[56,44,57,58,59,60],"db-migration","typeorm","zero-downtime","expand-contract","playbook","Playbook",[63,64,65],"ms-*\u002Fsrc\u002Fmigrations\u002F**","npm-packages\u002Fpackages\u002Fdatabase\u002Fsrc\u002F**","scripts\u002Fcli\u002Fcommands\u002Fsync-migrations.mjs","Migrations PostgreSQL et Synchronisation Multi-Services","stable",[69,73,76],{"id":70,"kind":71,"title":72,"resource":65},"sync-mig","local_file","sync-migrations.mjs",{"id":74,"kind":71,"title":75,"resource":75},"compose","ci-tools\u002Fdocker-compose.yml",{"id":77,"kind":71,"title":78,"resource":78},"data-source","ms-user\u002Fsrc\u002Fconfig\u002Fdata-source.ts",[80,84,87,89,91],{"at":81,"by":82,"digest":83},"2026-11-06T14:04:00Z","claude-code\u002Fagent","a6bfa42cee9dd9ef",{"at":85,"by":82,"digest":86},"2026-11-07T15:34:34Z","65868a898ae28556",{"at":88,"by":82,"digest":86},"2026-11-07T15:49:20Z",{"at":90,"by":82,"digest":86},"2026-11-07T15:55:37Z",{"at":92,"by":82,"digest":86},"2026-11-08T20:59:18Z",{"at":94,"by":82},"2026-10-12T11:56:30Z",[96,97,57],"migrations","sync-migrations","main","6b8f03b57018fa66da4666071dc17c1577ff45e8",".agents\u002Fskills\u002Fvolontariapp-db-migration","https:\u002F\u002Fgithub.com\u002FVolontariapp\u002Fmeta","https:\u002F\u002Fgithub.com\u002FVolontariapp","Volontariapp — Application Web\u002FMobile scalabelle construite sur une architecture microservices événementielle (Event-Driven Architecture), implémentant le Trans","2026-11-10 23:40:22",{"source":8,"skillName":5,"commands":106,"prompt":116,"zipAvailable":117},[107,110,113],{"tool":108,"command":109},"npx","npx skills add Volontariapp\u002Fmeta --skill volontariapp-db-migration",{"tool":111,"command":112},"bunx","bunx skills add Volontariapp\u002Fmeta --skill volontariapp-db-migration",{"tool":114,"command":115},"pnpm","pnpm dlx skills add Volontariapp\u002Fmeta --skill volontariapp-db-migration","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FVolontariapp\u002Fmeta\u002Fblob\u002Fmain\u002F.agents\u002Fskills\u002Fvolontariapp-db-migration\u002FSKILL.md",true,[119,125,129,133],{"path":120,"resourceType":121,"resourceTypeDesc":122,"sizeBytes":123,"sourceHtmlUrl":124},"index.md","REFERENCE","参考文档",799,"https:\u002F\u002Fgithub.com\u002FVolontariapp\u002Fmeta\u002Fblob\u002Fmain\u002F.agents\u002Fskills\u002Fvolontariapp-db-migration\u002Findex.md",{"path":126,"resourceType":121,"resourceTypeDesc":122,"sizeBytes":127,"sourceHtmlUrl":128},"log.md",6475,"https:\u002F\u002Fgithub.com\u002FVolontariapp\u002Fmeta\u002Fblob\u002Fmain\u002F.agents\u002Fskills\u002Fvolontariapp-db-migration\u002Flog.md",{"path":130,"resourceType":121,"resourceTypeDesc":122,"sizeBytes":131,"sourceHtmlUrl":132},"references\u002Flessons.md",4464,"https:\u002F\u002Fgithub.com\u002FVolontariapp\u002Fmeta\u002Fblob\u002Fmain\u002F.agents\u002Fskills\u002Fvolontariapp-db-migration\u002Freferences\u002Flessons.md",{"path":134,"resourceType":135,"resourceTypeDesc":136,"sizeBytes":137,"sourceHtmlUrl":138},"scripts\u002Fpush-common-migration.sh","SCRIPT","脚本",2487,"https:\u002F\u002Fgithub.com\u002FVolontariapp\u002Fmeta\u002Fblob\u002Fmain\u002F.agents\u002Fskills\u002Fvolontariapp-db-migration\u002Fscripts\u002Fpush-common-migration.sh",[140,161,184,207,232,252],{"skillNo":141,"name":142,"nameZh":6,"description":143,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":144,"categories":145,"keywords":146,"publishedAt":50},"01M6CSW3Y5ZHVBF5BX7S5BAVS0","volontariapp-user-account-deletion","Suppression de compte en deux temps (PENDING_DELETION): requestDeletionWithUserDeleted, finalizeDeletion, fan-out de user.deleted vers les domaines et gather ws. A utiliser pour tout changement de domain-user touchant la suppression, le statut ou le cycle de vie d'un user.","https:\u002F\u002Fgithub.com\u002FVolontariapp\u002Fmeta\u002Fblob\u002Fmain\u002F.agents\u002Fskills\u002Fvolontariapp-user-account-deletion\u002FSKILL.md",[],[147,148,149,150,151,152,153,154,37,46,155,156,157,158,35,159,160,33,39],"volontariapp user account deletion","user","account deletion","deletion","cycle","gather","sonnet","pending","claude-code","reference","npm","user-account-deletion","en","domain",{"skillNo":162,"name":163,"nameZh":6,"description":164,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":165,"categories":166,"keywords":168,"publishedAt":50},"01M6CSVC1ZW30DDFZ8VMEYAAK6","volontariapp-proto-contract-evolution","Modifier un contrat Protobuf de proto-registry (champ, message, enum, RPC) sans casser la compatibilité wire, puis suivre la cascade proto-registry, PR automatique dans npm-packages, publication, consommateurs. À utiliser avant toute édition d'un .proto.","https:\u002F\u002Fgithub.com\u002FVolontariapp\u002Fmeta\u002Fblob\u002Fmain\u002F.agents\u002Fskills\u002Fvolontariapp-proto-contract-evolution\u002FSKILL.md",[167],{"categoryNo":24,"categoryName":25,"taxonomyType":26,"taxonomyTypeDesc":27},[169,170,171,172,46,173,174,39,175,48,176,177,178,157,179,180,181,182,60,183],"evolution","volontariapp proto contract evolution","grpc","lint","workflows","analyze","rpc","pr","protobuf","message","registry","sans","sync","yaml","github",{"skillNo":185,"name":186,"nameZh":6,"description":187,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":188,"categories":189,"keywords":193,"publishedAt":50},"01M6CSV99ET2Z9W4W4DAK4CMQP","volontariapp-meta-scripts","Utiliser et faire evoluer la CLI Node de meta (setup, doctor, migrate, certs, repos, bump) et le manifeste repos.json, source unique des repos et unites du workspace.","https:\u002F\u002Fgithub.com\u002FVolontariapp\u002Fmeta\u002Fblob\u002Fmain\u002F.agents\u002Fskills\u002Fvolontariapp-meta-scripts\u002FSKILL.md",[190],{"categoryNo":191,"categoryName":192,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发",[49,194,195,46,196,155,197,198,199,200,201,202,203,38,204,205,206],"volontariapp meta scripts","repos","migrate","setup","meta-scripts","json","node","workspace","shell","bump","doctor","draft","install",{"skillNo":208,"name":209,"nameZh":6,"description":210,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":211,"categories":212,"keywords":216,"publishedAt":50},"01M6CSTPHNTGE0WF5CPE8W7ZYQ","volontariapp-implement-async-event-flow","Guide pas-à-pas pour concevoir et implémenter un flux asynchrone complet (messaging -> outbox -> post-processors -> ws-service -> client).","https:\u002F\u002Fgithub.com\u002FVolontariapp\u002Fmeta\u002Fblob\u002Fmain\u002F.agents\u002Fskills\u002Fvolontariapp-implement-async-event-flow\u002FSKILL.md",[213],{"categoryNo":214,"categoryName":215,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000009","前端开发",[217,218,219,220,221,222,35,223,224,179,225,226,33,227,228,39,229,230,46,231],"async","implement","volontariapp implement async event flow","flow","event","messaging","mapping","flux","streams","flows","shared","outbox","processor","architecture","runner",{"skillNo":233,"name":234,"nameZh":6,"description":235,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":236,"categories":237,"keywords":241,"publishedAt":50},"01M6CSRQHS0T9955M8CTA5BYJQ","research","Investigate a question against high-trust primary sources and capture the findings as a Markdown file in the repo. Use when the user wants a topic researched, docs or API facts gathered, or reading legwork delegated to a background agent.","https:\u002F\u002Fgithub.com\u002FVolontariapp\u002Fmeta\u002Fblob\u002Fmain\u002F.agents\u002Fplugins\u002Fmatt-pocock\u002Fskills\u002Fresearch\u002FSKILL.md",[238],{"categoryNo":239,"categoryName":240,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000021","资料检索与研究",[234,242,148,243,244,245,246,247,248,249,250,251],"api","file","repo","markdown","docs","capture","use","question","trust","investigate",{"skillNo":253,"name":254,"nameZh":6,"description":255,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":256,"categories":257,"keywords":261,"publishedAt":50},"01M6CSRMZ82M1YK96Z4SW1STTD","prototype","Build a throwaway prototype to answer a design question. Use when the user wants to sanity-check whether a state model or logic feels right, or explore what a UI should look like.","https:\u002F\u002Fgithub.com\u002FVolontariapp\u002Fmeta\u002Fblob\u002Fmain\u002F.agents\u002Fplugins\u002Fmatt-pocock\u002Fskills\u002Fprototype\u002FSKILL.md",[258],{"categoryNo":259,"categoryName":260,"taxonomyType":26,"taxonomyTypeDesc":27},"01JBSEEDCAT000000000000032","设计师",[254,262,148,263,248,264,249,265,266,267,268,269],"check","ui","look","design","explore","model","build","state"]