[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6CSSPHB0YERQR4776W23FV6":3,"skill-same:01M6CSSPHB0YERQR4776W23FV6":184},{"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":141,"commitSha":142,"skillDirectory":143,"forksCount":14,"repositoryHtmlUrl":144,"repositoryHomepageUrl":6,"ownerHtmlUrl":145,"ownerBlogUrl":6,"ownerDescription":146,"updatedAt":147,"install":148,"resources":161},"01M6CSSPHB0YERQR4776W23FV6","volontariapp-api-gateway",null,"Playbook opérationnel pas-à-pas pour exposer ou modifier une route REST Edge sur api-gateway : DTO Swagger, contrôleur REST, guards, token interne propagé en métadonnée gRPC, support HTTP 206 et tests.","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-api-gateway\u002FSKILL.md",[18,23],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000010","后端与接口","SCENARIO","使用场景",{"categoryNo":24,"categoryName":25,"taxonomyType":26,"taxonomyTypeDesc":27},"01JBSEEDCAT000000000000030","开发工程师","PROFESSION","适用职业",[29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49],"volontariapp api gateway","api","api gateway","gateway","types","dto","packages","architecture","apps","errors","middleware","npm-packages","decorators","app","references","controller","route","common","code","auth","modules","2026-11-10 10:12:30",false,"---\nname: volontariapp-api-gateway\ndescription: \"Playbook opérationnel pas-à-pas pour exposer ou modifier une route REST Edge sur api-gateway : DTO Swagger, contrôleur REST, guards, token interne propagé en métadonnée gRPC, support HTTP 206 et tests.\"\ntype: Playbook\ntitle: Exposer ou Modifier une Route REST Edge\ntags: [api-gateway, auth, grpc, rest, swagger, playbook]\nstatus: stable\npaths:\n  - \"api-gateway\u002Fsrc\u002F**\"\nmesh_keys:\n  - api-gateway\n  - gatewaycontroller\n  - accesstokenguard\n  - rolesguard\n  - grpcinternalinterceptor\n  - internalmetadata\n  - iscurrentuseroradminguard\n  - ws-proxy\ngenerated:\n  by: claude-code\u002Fclaude-opus-5-5\n  at: \"2026-10-06T08:54:04Z\"\nverified:\n  - by: claude-code\u002Fagent\n    at: \"2026-11-03T20:40:37Z\"\n    digest: be5ca2862e35d8ad\n  - by: claude-code\u002Fagent\n    at: \"2026-11-04T17:55:25Z\"\n    digest: 36e80228c7f25540\n  - by: claude-code\u002Fagent\n    at: \"2026-11-08T12:02:21Z\"\n    digest: 6e8237e4a9c0e774\n  - by: claude-code\u002Fagent\n    at: \"2026-11-08T12:18:23Z\"\n    digest: 438a549648dfd54b\n  - by: claude-code\u002Fagent\n    at: \"2026-11-08T22:50:47Z\"\n    digest: 438a549648dfd54b\nsources:\n  - id: gw-claude\n    resource: api-gateway\u002Fsrc\u002Fmodules\n    title: \"Modules de api-gateway (carte : references\u002Froutes.md)\"\n  - id: decorator\n    resource: api-gateway\u002Fsrc\u002Fcommon\u002Fdecorators\u002Fgateway-controller.decorator.ts\n    title: GatewayController\n  - id: app-module\n    resource: api-gateway\u002Fsrc\u002Fapp.module.ts\n    title: \"AppModule (GrpcInternalInterceptor, HelperModule)\"\n  - id: grpc-module\n    resource: api-gateway\u002Fsrc\u002Fgrpc\u002Fgrpc-client.module.ts\n    title: GrpcClientModule\n  - id: helper\n    resource: api-gateway\u002Fsrc\u002Fmodules\u002Fhelper\u002Fcontrollers\u002Ftoken-helper.controller.ts\n    title: TokenHelperController\n  - id: seed\n    resource: api-gateway\u002Fsrc\u002Fmodules\u002Fsystem\u002Fcontrollers\u002Fsystem-seed.controller.ts\n    title: SystemSeedController\n  - id: deploy-gw\n    resource: deploy\u002Fapps\u002Fbase\u002Fapi-gateway\u002Fdeployment.yaml\n    title: Déploiement api-gateway (NODE_ENV)\n  - id: errors-nest\n    resource: npm-packages\u002Fpackages\u002Ferrors-nest\u002Fsrc\u002Ferrors\u002Fcommon.errors.ts\n    title: FALLBACK_ACTIVATED (PartialContentError 206)\n  - id: ws-proxy\n    resource: api-gateway\u002Fsrc\u002Fmodules\u002Fws-proxy\u002Fws-proxy.middleware.ts\n    title: WsProxyMiddleware\n  - id: pagination\n    resource: api-gateway\u002Fsrc\u002Fcommon\u002Fdto\u002Frequest\u002Fpagination.request.dto.ts\n    title: \"Module de pagination commun (constantes, DTO, decorateurs, ecretage)\"\n  - id: check-arch\n    resource: api-gateway\u002Fscripts\u002Fcheck-dto-architecture.ts\n    title: yarn check-arch (regles des DTO)\n  - id: web-requests\n    resource: npm-packages\u002Fpackages\u002Fcontracts\u002Fsrc\u002Fgateway\u002Fuser\u002Frequest\u002Fleaderboard.request.ts\n    title: Types web de requete du classement\n---\n\n# Playbook : Exposer ou Modifier une Route REST Edge (API Gateway)\n\nDans l'écosystème Volontariapp, `api-gateway` est l'unique point d'entrée HTTP exposé publiquement (préfixe global `\u002Fapi\u002Fv1`)[^gw-claude]. \n\nL'API Gateway **ne contient aucune logique métier** : son rôle exclusif consiste à authentifier l'utilisateur, valider le payload REST, relayer la requête au microservice cible via gRPC en y injectant un token interne signé (`x-internal-token`), et traduire la réponse ou les erreurs en HTTP.\n\nLa référence des modules, de la topologie réseau et des risques de sécurité est consignée dans [Sécurité & Risques API Gateway](\u002Fvolontariapp-api-gateway\u002Freferences\u002Fsecurity-and-risks.md).\n\n---\n\n## Trigger\n\nCe playbook doit être exécuté dès qu'une tâche nécessite :\n- L'ajout d'une nouvelle route REST publique ou authentifiée (GET, POST, PUT, PATCH, DELETE).\n- L'exposition d'un nouveau paramètre de requête ou champ de réponse vers l'application mobile `nativapp`.\n- L'ajustement des règles d'authentification ou des permissions de rôle sur un endpoint existant.\n\n---\n\n## Étape 0 : Le type Web du contrat doit exister (sinon, `npm-packages` d'abord)\n\n`yarn check-arch` impose deux choses[^check-arch] :\n- un DTO de **réponse** implémente une interface « Web » de `@volontariapp\u002Fcontracts` : même forme que le message gRPC, mais avec des `Date` à la place des `Timestamp`, dans `npm-packages\u002Fpackages\u002Fcontracts\u002Fsrc\u002Fgateway\u002F\u003Cdomaine>\u002Fresponse\u002F*.ts` ;\n- un DTO de **requête** implémente lui aussi une interface de `@volontariapp\u002Fcontracts`, dans `...\u002Fgateway\u002F\u003Cdomaine>\u002Frequest\u002F*.ts`, de la forme `extends Partial\u003CXxxQuery>`. Quand le REST expose une valeur différente du gRPC (une période en texte `'global' | 'week' | 'month'` là où le gRPC porte un enum), le type Web de requête porte la valeur REST et le DTO la convertit dans `toQuery()`[^web-requests].\n\nCes interfaces sont **écrites à la main** (seuls les messages gRPC sont générés). Si l'un des deux types manque, il faut une PR `npm-packages` (ajout seul, changeset minor), sa publication, puis le déverrouillage (règle du STOP) avant d'écrire le DTO. **Poser le type de requête et le type de réponse dans la même PR** : le classement les a livrés en deux fois (`contracts` 4.11.0 puis 4.12.0), ce qui a coûté un cycle de publication de plus. Modèle de réponse : `BadgeProgressWeb extends Omit\u003CBadgeProgress, 'awardedAt'> { awardedAt?: Date }` et `GetMyBadgeProgressWebResponse` (`npm-packages#300`).\n\n## Étape 1 : Spécification du DTO Swagger & Validation\n\nEmplacement : `src\u002Fmodules\u002F\u003Cdomaine>\u002Fdto\u002Frequest\u002F\u003Cnom>.request.dto.ts`.\n\nChaque DTO de requête doit :\n1. Documenter les champs pour OpenAPI\u002FSwagger avec `@ApiProperty()` ou `@ApiPropertyOptional()`.\n2. Appliquer des décorateurs `class-validator` stricts.\n3. Implémenter la méthode adaptatrice `toCommand(): CommandType` ou `toQuery(): QueryType` pour convertir le format REST en contrat gRPC.\n\n```typescript\nimport { ApiProperty } from '@nestjs\u002Fswagger';\nimport { IsString, IsNotEmpty, IsUUID, IsOptional } from 'class-validator';\nimport type { CreatePostCommand } from '@volontariapp\u002Fcontracts-nest';\n\nexport class CreatePostRequestDTO {\n  @ApiProperty({ description: 'Titre de la publication', example: 'Mission Maraude Paris' })\n  @IsString()\n  @IsNotEmpty()\n  title!: string;\n\n  @ApiProperty({ description: 'Contenu textuel', example: 'Besoin de 5 bénévoles ce samedi' })\n  @IsString()\n  @IsNotEmpty()\n  content!: string;\n\n  @ApiPropertyOptional({ description: 'Identifiant du fichier image déjà confirmé' })\n  @IsUUID()\n  @IsOptional()\n  coverFileId?: string;\n\n  toCommand(): CreatePostCommand {\n    return {\n      title: this.title,\n      content: this.content,\n      coverFileId: this.coverFileId,\n    };\n  }\n}\n```\n\n---\n\n## Étape 2 : Contrôleur REST NestJS & Règles de Sécurité\n\nEmplacement : `src\u002Fmodules\u002F\u003Cdomaine>\u002Fcontrollers\u002Fcommands\u002F` (écriture) ou `queries\u002F` (lecture).\n\n```typescript\nimport { Controller, Post, Body, Req, HttpCode, HttpStatus } from '@nestjs\u002Fcommon';\nimport { ApiTags, ApiOperation, ApiResponse } from '@nestjs\u002Fswagger';\nimport { Request } from 'express';\nimport { firstValueFrom } from 'rxjs';\nimport { GatewayController } from '..\u002F..\u002F..\u002F..\u002Fcommon\u002Fdecorators\u002Fgateway-controller.decorator.js';\nimport { CurrentUser, type AuthUser } from '@volontariapp\u002Fauth';\nimport { BasePostGrpcController } from '..\u002Fbase-post-grpc.controller.js';\nimport { CreatePostRequestDTO } from '..\u002F..\u002Fdto\u002Frequest\u002Fcreate-post.request.dto.js';\nimport { PostResponseDTO } from '..\u002F..\u002Fdto\u002Fresponse\u002Fpost.response.dto.js';\n\n@GatewayController('Posts')\nexport class PostCommandController extends BasePostGrpcController {\n\n  @Post()\n  @HttpCode(HttpStatus.CREATED)\n  @ApiOperation({ summary: 'Créer une nouvelle publication' })\n  @ApiResponse({ status: 201, type: PostResponseDTO })\n  async createPost(\n    @Body() dto: CreatePostRequestDTO,\n    @CurrentUser() user: AuthUser,\n    @Req() req: Request,\n  ): Promise\u003CPostResponseDTO> {\n    const command = dto.toCommand();\n    \n    \u002F\u002F Appel gRPC avec injection obligatoire de req['internalMetadata']\n    const result = await firstValueFrom(\n      this.postCommandService.createPost(command, req['internalMetadata']),\n    );\n\n    return PostResponseDTO.fromGrpc(result);\n  }\n}\n```\n\n### Règles de Sécurité Inviolables :\n- **Administration** : Pour toute route d'administration, utiliser impérativement `@GatewayController('Tag', { admin: true })`[^decorator]. Sans `{ admin: true }`, le garde `RolesGuard` n'est **pas monté**, rendant `@Roles(UserRoles.ADMIN)` totalement inopérant[^seed].\n- **Routes Publiques** : Ajouter `@Public()` uniquement pour les endpoints ouverts sans authentification (`\u002Flogin`, `\u002Frefresh`)[^helper].\n- **Identité du demandeur** : Pour les routes de profil ou d'action personnelle (`\u002Fme`), extraire l'utilisateur exclusivement via `@CurrentUser()`[^app-module]. Ne **jamais** accepter de `userId` dans le body ou les paramètres d'URL pour le demandeur connecté.\n- **Ressources scopées par utilisateur** : Utiliser `IsCurrentUserOrAdminGuard` pour les routes ciblant `:userId`.\n\n---\n\n**Conflit de routes.** Un chemin sous un préfixe qui porte déjà un `GET :id` (`badges\u002F:id`, `users\u002F:id`) peut être capturé par lui. Le placer dans un contrôleur dédié monté sur un préfixe sans ambiguïté (`@Controller('users\u002Fme\u002Fbadges')` pour `GET \u002Fusers\u002Fme\u002Fbadges\u002Fprogress`) et écrire un test d'intégration de routage (`Test.createTestingModule` avec les contrôleurs voisins, dans l'ordre du module) qui vérifie que la route atteint le bon contrôleur.\n\n## Étape 3 : Câblage du Client gRPC\n\n1. Le contrôleur hérite du contrôleur de base du domaine (`Base\u003CDomaine>GrpcController`)[^grpc-module].\n2. Lors de `onModuleInit`, le contrôleur résout le client typé gRPC via `client.getService\u003CXServiceClient>(X_SERVICE_NAME)`.\n3. Si un nouveau package de microservice doit être raccordé (ex. `ms-storage`), l'enregistrer dans `GrpcClientModule` via `getGrpcOptions(GRPC_MICROSERVICES.X, url)`.\n\n---\n\n## Étape 4 : Propagation Obligatoire du Token Interne\n\nTout microservice en aval exige un `x-internal-token` signé pour autoriser la requête. \n\n- L'intercepteur global `GrpcInternalInterceptor` génère et attache automatiquement un objet `grpc.Metadata` sur la requête Express : `req['internalMetadata']`[^app-module].\n- **Règle absolue :** Vous devez obligatoirement passer `req['internalMetadata']` comme **second argument** de chaque appel gRPC :\n  ```typescript\n  await firstValueFrom(this.service.someRpc(command, req['internalMetadata']));\n  ```\n- Omettre cet argument provoque un rejet immédiat du microservice avec le statut `UNAUTHENTICATED`.\n\n---\n\n## Étape 5 : Mapping des Réponses et Gestion du HTTP 206\n\n1. **Mapping d'Erreurs :** Les erreurs `BaseApiError` renvoyées par les microservices portent `statusCode` et `grpcCode`. L'API Gateway les retranscrit avec le statut HTTP d'origine.\n2. **Support HTTP 206 `FALLBACK_ACTIVATED` :**\n   - Lorsqu'un microservice subit une défaillance de dépendance non bloquante, il déclenche un job de fallback et retourne `FALLBACK_ACTIVATED`[^errors-nest].\n   - L'API Gateway doit renvoyer ce résultat avec le code HTTP **206 (Partial Content)** :\n   ```typescript\n   @ApiResponse({ status: 206, description: 'Traitement asynchrone différé (fallback activé)' })\n   ```\n   - Le client mobile `nativapp` sait qu'il doit écouter la notification de finalisation via WebSocket.\n\n---\n\n## Étape 6 : Tests & Validation de l'Endpoint\n\n1. **Tests unitaires de Contrôleur :**\n   - Mocker le client gRPC (ex: `MockUserClientGrpc`).\n   - Mocker l'objet `Request` avec `req['internalMetadata'] = new Metadata()`.\n   - Vérifier que le DTO appelle bien `toCommand()` et que le retour est conforme.\n2. **Validation Swagger :**\n   - Démarrer l'API Gateway ou inspecter la spec générée : vérifier que le tag, les statuts 200\u002F201\u002F206\u002F400\u002F401\u002F403 et les schémas DTO sont intégralement documentés.\n\n---\n\n## Pagination des routes de liste\n\nToute route de liste utilise le module commun `src\u002Fcommon\u002Fpagination` : ne jamais redeclarer `page`, `limit` ni un plafond dans un DTO[^pagination].\n\n- **Constantes** : `DEFAULT_PAGE = 1` (base 1), `DEFAULT_LIMIT = 10`, `MAX_LIMIT = 100`, `LEGACY_MAX_LIMIT = 500`.\n- **DTO imbrique `pagination[page|limit]`** : `PaginationRequestDTO` (plafond 100), `LegacyPaginationRequestDTO` (plafond 500), ou `createPaginationRequestDTO(maxLimit)` pour un plafond propre. Le champ du DTO de requete se declare avec `@NestedPagination(LegacyPaginationRequestDTO)` ou son pendant standard.\n- **DTO de query plate `?page=&limit=`** : `FlatPaginationQueryDTO` (plafond 100) ou `createFlatPaginationQueryDTO(maxLimit)`.\n- **Decorateurs** : `@PaginationPage()` (entier >= 1, sinon 400) et `@PaginationLimit(maxLimit = MAX_LIMIT)`.\n- **Ecretage** : `clampPaginationLimit(limit, maxLimit)` ramene une limite trop grande au plafond (`Math.min`). Une limite superieure au plafond est ecretee, jamais rejetee ; une limite invalide (non entiere, \u003C 1) reste un 400.\n- **Exception legacy (500)** : seulement `GET \u002Fapi\u002Fv1\u002Fsocial\u002Flikes` (`GetMyLikesRequestDTO`) et `GET \u002Fapi\u002Fv1\u002Fusers` admin (`ListUsersRequestDTO`), car les anciens builds de nativapp appellent avec `limit=500`. Ne pas etendre cette exception a une autre route.\n- **Lecture par lot** : `POST \u002Fapi\u002Fv1\u002Fsocial\u002Flikes-summary` (`GetPostsLikeSummaryRequestDTO`) prend 1 a 100 `postIds` uniques (`ArrayMinSize(1)`, `ArrayMaxSize(MAX_LIKE_SUMMARY_POST_IDS)`, `ArrayUnique`) et repond 200 : c'est une lecture exposee en POST pour eviter une URL de 100 identifiants. ms-social reste l'autorite sur le plafond.\n- **Lire tout un ensemble d'ids chez un ms (appel interne, pas une route)** : `fetchAllPages(fetchPage)` de `src\u002Fcommon\u002Fpagination` (api-gateway#90), jamais un seul appel avec `limit: 1000`. Il lit par pages de `INTERNAL_PAGE_SIZE` (= `MAX_LIMIT`, 100) et s'arrete sur page vide, page courte, `totalPages` atteint ou `INTERNAL_MAX_PAGES` (100 pages, soit 10 000 ids au plus : les ids deja lus sont alors renvoyes sans erreur). Une erreur de `fetchPage` est propagee telle quelle. Utilise pour les ids d'evenements recommandes par ms-social et pour les ensembles de blocage du ranking.\n\n[^pagination]: Module de pagination commun (constantes, DTO, decorateurs, ecretage)\n\n---\n\n## Checklist d'Assurance Qualité & Critères d'Arrêt\n\nAvant de déclarer la tâche terminée :\n- [ ] Le DTO de requête implémente les décorateurs Swagger et `class-validator`.\n- [ ] Le DTO possède sa méthode adaptatrice `toCommand()` ou `toQuery()`.\n- [ ] `@GatewayController(tag, { admin: true })` est posé sur toute route d'administration.\n- [ ] `req['internalMetadata']` est passé en second argument de **chaque** appel gRPC.\n- [ ] Aucun `userId` n'est extrait du payload sur les routes « me ».\n- [ ] Les réponses asynchrones avec fallback documentent le statut HTTP 206 (`FALLBACK_ACTIVATED`).\n- [ ] `yarn test` passe avec succès sur `api-gateway`.\n- [ ] `yarn check-arch` passe (un fichier par DTO, requete avec `implements` + `@volontariapp\u002Fcontracts` + `toCommand()`\u002F`toQuery()`, DTO imbriques dans `dto\u002Fcommon\u002F`)[^check-arch].\n- [ ] `yarn tsc` et `yarn lint` ne produisent aucune erreur.\n\n[^gw-claude]: Modules de api-gateway (carte : references\u002Froutes.md)\n[^decorator]: GatewayController\n[^app-module]: AppModule (GrpcInternalInterceptor, HelperModule)\n[^grpc-module]: GrpcClientModule\n[^helper]: TokenHelperController\n[^seed]: SystemSeedController\n[^deploy-gw]: Déploiement api-gateway (NODE_ENV)\n[^errors-nest]: FALLBACK_ACTIVATED (PartialContentError 206)\n[^ws-proxy]: WsProxyMiddleware\n[^check-arch]: yarn check-arch (regles des DTO)\n[^web-requests]: Types web de requete du classement\n","# Playbook : Exposer ou Modifier une Route REST Edge (API Gateway)\n\nDans l'écosystème Volontariapp, `api-gateway` est l'unique point d'entrée HTTP exposé publiquement (préfixe global `\u002Fapi\u002Fv1`)[^gw-claude]. \n\nL'API Gateway **ne contient aucune logique métier** : son rôle exclusif consiste à authentifier l'utilisateur, valider le payload REST, relayer la requête au microservice cible via gRPC en y injectant un token interne signé (`x-internal-token`), et traduire la réponse ou les erreurs en HTTP.\n\nLa référence des modules, de la topologie réseau et des risques de sécurité est consignée dans [Sécurité & Risques API Gateway](\u002Fvolontariapp-api-gateway\u002Freferences\u002Fsecurity-and-risks.md).\n\n---\n\n## Trigger\n\nCe playbook doit être exécuté dès qu'une tâche nécessite :\n- L'ajout d'une nouvelle route REST publique ou authentifiée (GET, POST, PUT, PATCH, DELETE).\n- L'exposition d'un nouveau paramètre de requête ou champ de réponse vers l'application mobile `nativapp`.\n- L'ajustement des règles d'authentification ou des permissions de rôle sur un endpoint existant.\n\n---\n\n## Étape 0 : Le type Web du contrat doit exister (sinon, `npm-packages` d'abord)\n\n`yarn check-arch` impose deux choses[^check-arch] :\n- un DTO de **réponse** implémente une interface « Web » de `@volontariapp\u002Fcontracts` : même forme que le message gRPC, mais avec des `Date` à la place des `Timestamp`, dans `npm-packages\u002Fpackages\u002Fcontracts\u002Fsrc\u002Fgateway\u002F\u003Cdomaine>\u002Fresponse\u002F*.ts` ;\n- un DTO de **requête** implémente lui aussi une interface de `@volontariapp\u002Fcontracts`, dans `...\u002Fgateway\u002F\u003Cdomaine>\u002Frequest\u002F*.ts`, de la forme `extends Partial\u003CXxxQuery>`. Quand le REST expose une valeur différente du gRPC (une période en texte `'global' | 'week' | 'month'` là où le gRPC porte un enum), le type Web de requête porte la valeur REST et le DTO la convertit dans `toQuery()`[^web-requests].\n\nCes interfaces sont **écrites à la main** (seuls les messages gRPC sont générés). Si l'un des deux types manque, il faut une PR `npm-packages` (ajout seul, changeset minor), sa publication, puis le déverrouillage (règle du STOP) avant d'écrire le DTO. **Poser le type de requête et le type de réponse dans la même PR** : le classement les a livrés en deux fois (`contracts` 4.11.0 puis 4.12.0), ce qui a coûté un cycle de publication de plus. Modèle de réponse : `BadgeProgressWeb extends Omit\u003CBadgeProgress, 'awardedAt'> { awardedAt?: Date }` et `GetMyBadgeProgressWebResponse` (`npm-packages#300`).\n\n## Étape 1 : Spécification du DTO Swagger & Validation\n\nEmplacement : `src\u002Fmodules\u002F\u003Cdomaine>\u002Fdto\u002Frequest\u002F\u003Cnom>.request.dto.ts`.\n\nChaque DTO de requête doit :\n1. Documenter les champs pour OpenAPI\u002FSwagger avec `@ApiProperty()` ou `@ApiPropertyOptional()`.\n2. Appliquer des décorateurs `class-validator` stricts.\n3. Implémenter la méthode adaptatrice `toCommand(): CommandType` ou `toQuery(): QueryType` pour convertir le format REST en contrat gRPC.\n\n```typescript\nimport { ApiProperty } from '@nestjs\u002Fswagger';\nimport { IsString, IsNotEmpty, IsUUID, IsOptional } from 'class-validator';\nimport type { CreatePostCommand } from '@volontariapp\u002Fcontracts-nest';\n\nexport class CreatePostRequestDTO {\n  @ApiProperty({ description: 'Titre de la publication', example: 'Mission Maraude Paris' })\n  @IsString()\n  @IsNotEmpty()\n  title!: string;\n\n  @ApiProperty({ description: 'Contenu textuel', example: 'Besoin de 5 bénévoles ce samedi' })\n  @IsString()\n  @IsNotEmpty()\n  content!: string;\n\n  @ApiPropertyOptional({ description: 'Identifiant du fichier image déjà confirmé' })\n  @IsUUID()\n  @IsOptional()\n  coverFileId?: string;\n\n  toCommand(): CreatePostCommand {\n    return {\n      title: this.title,\n      content: this.content,\n      coverFileId: this.coverFileId,\n    };\n  }\n}\n```\n\n---\n\n## Étape 2 : Contrôleur REST NestJS & Règles de Sécurité\n\nEmplacement : `src\u002Fmodules\u002F\u003Cdomaine>\u002Fcontrollers\u002Fcommands\u002F` (écriture) ou `queries\u002F` (lecture).\n\n```typescript\nimport { Controller, Post, Body, Req, HttpCode, HttpStatus } from '@nestjs\u002Fcommon';\nimport { ApiTags, ApiOperation, ApiResponse } from '@nestjs\u002Fswagger';\nimport { Request } from 'express';\nimport { firstValueFrom } from 'rxjs';\nimport { GatewayController } from '..\u002F..\u002F..\u002F..\u002Fcommon\u002Fdecorators\u002Fgateway-controller.decorator.js';\nimport { CurrentUser, type AuthUser } from '@volontariapp\u002Fauth';\nimport { BasePostGrpcController } from '..\u002Fbase-post-grpc.controller.js';\nimport { CreatePostRequestDTO } from '..\u002F..\u002Fdto\u002Frequest\u002Fcreate-post.request.dto.js';\nimport { PostResponseDTO } from '..\u002F..\u002Fdto\u002Fresponse\u002Fpost.response.dto.js';\n\n@GatewayController('Posts')\nexport class PostCommandController extends BasePostGrpcController {\n\n  @Post()\n  @HttpCode(HttpStatus.CREATED)\n  @ApiOperation({ summary: 'Créer une nouvelle publication' })\n  @ApiResponse({ status: 201, type: PostResponseDTO })\n  async createPost(\n    @Body() dto: CreatePostRequestDTO,\n    @CurrentUser() user: AuthUser,\n    @Req() req: Request,\n  ): Promise\u003CPostResponseDTO> {\n    const command = dto.toCommand();\n    \n    \u002F\u002F Appel gRPC avec injection obligatoire de req['internalMetadata']\n    const result = await firstValueFrom(\n      this.postCommandService.createPost(command, req['internalMetadata']),\n    );\n\n    return PostResponseDTO.fromGrpc(result);\n  }\n}\n```\n\n### Règles de Sécurité Inviolables :\n- **Administration** : Pour toute route d'administration, utiliser impérativement `@GatewayController('Tag', { admin: true })`[^decorator]. Sans `{ admin: true }`, le garde `RolesGuard` n'est **pas monté**, rendant `@Roles(UserRoles.ADMIN)` totalement inopérant[^seed].\n- **Routes Publiques** : Ajouter `@Public()` uniquement pour les endpoints ouverts sans authentification (`\u002Flogin`, `\u002Frefresh`)[^helper].\n- **Identité du demandeur** : Pour les routes de profil ou d'action personnelle (`\u002Fme`), extraire l'utilisateur exclusivement via `@CurrentUser()`[^app-module]. Ne **jamais** accepter de `userId` dans le body ou les paramètres d'URL pour le demandeur connecté.\n- **Ressources scopées par utilisateur** : Utiliser `IsCurrentUserOrAdminGuard` pour les routes ciblant `:userId`.\n\n---\n\n**Conflit de routes.** Un chemin sous un préfixe qui porte déjà un `GET :id` (`badges\u002F:id`, `users\u002F:id`) peut être capturé par lui. Le placer dans un contrôleur dédié monté sur un préfixe sans ambiguïté (`@Controller('users\u002Fme\u002Fbadges')` pour `GET \u002Fusers\u002Fme\u002Fbadges\u002Fprogress`) et écrire un test d'intégration de routage (`Test.createTestingModule` avec les contrôleurs voisins, dans l'ordre du module) qui vérifie que la route atteint le bon contrôleur.\n\n## Étape 3 : Câblage du Client gRPC\n\n1. Le contrôleur hérite du contrôleur de base du domaine (`Base\u003CDomaine>GrpcController`)[^grpc-module].\n2. Lors de `onModuleInit`, le contrôleur résout le client typé gRPC via `client.getService\u003CXServiceClient>(X_SERVICE_NAME)`.\n3. Si un nouveau package de microservice doit être raccordé (ex. `ms-storage`), l'enregistrer dans `GrpcClientModule` via `getGrpcOptions(GRPC_MICROSERVICES.X, url)`.\n\n---\n\n## Étape 4 : Propagation Obligatoire du Token Interne\n\nTout microservice en aval exige un `x-internal-token` signé pour autoriser la requête. \n\n- L'intercepteur global `GrpcInternalInterceptor` génère et attache automatiquement un objet `grpc.Metadata` sur la requête Express : `req['internalMetadata']`[^app-module].\n- **Règle absolue :** Vous devez obligatoirement passer `req['internalMetadata']` comme **second argument** de chaque appel gRPC :\n  ```typescript\n  await firstValueFrom(this.service.someRpc(command, req['internalMetadata']));\n  ```\n- Omettre cet argument provoque un rejet immédiat du microservice avec le statut `UNAUTHENTICATED`.\n\n---\n\n## Étape 5 : Mapping des Réponses et Gestion du HTTP 206\n\n1. **Mapping d'Erreurs :** Les erreurs `BaseApiError` renvoyées par les microservices portent `statusCode` et `grpcCode`. L'API Gateway les retranscrit avec le statut HTTP d'origine.\n2. **Support HTTP 206 `FALLBACK_ACTIVATED` :**\n   - Lorsqu'un microservice subit une défaillance de dépendance non bloquante, il déclenche un job de fallback et retourne `FALLBACK_ACTIVATED`[^errors-nest].\n   - L'API Gateway doit renvoyer ce résultat avec le code HTTP **206 (Partial Content)** :\n   ```typescript\n   @ApiResponse({ status: 206, description: 'Traitement asynchrone différé (fallback activé)' })\n   ```\n   - Le client mobile `nativapp` sait qu'il doit écouter la notification de finalisation via WebSocket.\n\n---\n\n## Étape 6 : Tests & Validation de l'Endpoint\n\n1. **Tests unitaires de Contrôleur :**\n   - Mocker le client gRPC (ex: `MockUserClientGrpc`).\n   - Mocker l'objet `Request` avec `req['internalMetadata'] = new Metadata()`.\n   - Vérifier que le DTO appelle bien `toCommand()` et que le retour est conforme.\n2. **Validation Swagger :**\n   - Démarrer l'API Gateway ou inspecter la spec générée : vérifier que le tag, les statuts 200\u002F201\u002F206\u002F400\u002F401\u002F403 et les schémas DTO sont intégralement documentés.\n\n---\n\n## Pagination des routes de liste\n\nToute route de liste utilise le module commun `src\u002Fcommon\u002Fpagination` : ne jamais redeclarer `page`, `limit` ni un plafond dans un DTO[^pagination].\n\n- **Constantes** : `DEFAULT_PAGE = 1` (base 1), `DEFAULT_LIMIT = 10`, `MAX_LIMIT = 100`, `LEGACY_MAX_LIMIT = 500`.\n- **DTO imbrique `pagination[page|limit]`** : `PaginationRequestDTO` (plafond 100), `LegacyPaginationRequestDTO` (plafond 500), ou `createPaginationRequestDTO(maxLimit)` pour un plafond propre. Le champ du DTO de requete se declare avec `@NestedPagination(LegacyPaginationRequestDTO)` ou son pendant standard.\n- **DTO de query plate `?page=&limit=`** : `FlatPaginationQueryDTO` (plafond 100) ou `createFlatPaginationQueryDTO(maxLimit)`.\n- **Decorateurs** : `@PaginationPage()` (entier >= 1, sinon 400) et `@PaginationLimit(maxLimit = MAX_LIMIT)`.\n- **Ecretage** : `clampPaginationLimit(limit, maxLimit)` ramene une limite trop grande au plafond (`Math.min`). Une limite superieure au plafond est ecretee, jamais rejetee ; une limite invalide (non entiere, \u003C 1) reste un 400.\n- **Exception legacy (500)** : seulement `GET \u002Fapi\u002Fv1\u002Fsocial\u002Flikes` (`GetMyLikesRequestDTO`) et `GET \u002Fapi\u002Fv1\u002Fusers` admin (`ListUsersRequestDTO`), car les anciens builds de nativapp appellent avec `limit=500`. Ne pas etendre cette exception a une autre route.\n- **Lecture par lot** : `POST \u002Fapi\u002Fv1\u002Fsocial\u002Flikes-summary` (`GetPostsLikeSummaryRequestDTO`) prend 1 a 100 `postIds` uniques (`ArrayMinSize(1)`, `ArrayMaxSize(MAX_LIKE_SUMMARY_POST_IDS)`, `ArrayUnique`) et repond 200 : c'est une lecture exposee en POST pour eviter une URL de 100 identifiants. ms-social reste l'autorite sur le plafond.\n- **Lire tout un ensemble d'ids chez un ms (appel interne, pas une route)** : `fetchAllPages(fetchPage)` de `src\u002Fcommon\u002Fpagination` (api-gateway#90), jamais un seul appel avec `limit: 1000`. Il lit par pages de `INTERNAL_PAGE_SIZE` (= `MAX_LIMIT`, 100) et s'arrete sur page vide, page courte, `totalPages` atteint ou `INTERNAL_MAX_PAGES` (100 pages, soit 10 000 ids au plus : les ids deja lus sont alors renvoyes sans erreur). Une erreur de `fetchPage` est propagee telle quelle. Utilise pour les ids d'evenements recommandes par ms-social et pour les ensembles de blocage du ranking.\n\n[^pagination]: Module de pagination commun (constantes, DTO, decorateurs, ecretage)\n\n---\n\n## Checklist d'Assurance Qualité & Critères d'Arrêt\n\nAvant de déclarer la tâche terminée :\n- [ ] Le DTO de requête implémente les décorateurs Swagger et `class-validator`.\n- [ ] Le DTO possède sa méthode adaptatrice `toCommand()` ou `toQuery()`.\n- [ ] `@GatewayController(tag, { admin: true })` est posé sur toute route d'administration.\n- [ ] `req['internalMetadata']` est passé en second argument de **chaque** appel gRPC.\n- [ ] Aucun `userId` n'est extrait du payload sur les routes « me ».\n- [ ] Les réponses asynchrones avec fallback documentent le statut HTTP 206 (`FALLBACK_ACTIVATED`).\n- [ ] `yarn test` passe avec succès sur `api-gateway`.\n- [ ] `yarn check-arch` passe (un fichier par DTO, requete avec `implements` + `@volontariapp\u002Fcontracts` + `toCommand()`\u002F`toQuery()`, DTO imbriques dans `dto\u002Fcommon\u002F`)[^check-arch].\n- [ ] `yarn tsc` et `yarn lint` ne produisent aucune erreur.\n\n[^gw-claude]: Modules de api-gateway (carte : references\u002Froutes.md)\n[^decorator]: GatewayController\n[^app-module]: AppModule (GrpcInternalInterceptor, HelperModule)\n[^grpc-module]: GrpcClientModule\n[^helper]: TokenHelperController\n[^seed]: SystemSeedController\n[^deploy-gw]: Déploiement api-gateway (NODE_ENV)\n[^errors-nest]: FALLBACK_ACTIVATED (PartialContentError 206)\n[^ws-proxy]: WsProxyMiddleware\n[^check-arch]: yarn check-arch (regles des DTO)\n[^web-requests]: Types web de requete du classement\n",{"name":5,"tags":55,"type":61,"paths":62,"title":64,"status":65,"sources":66,"verified":115,"generated":131,"mesh_keys":134,"description":7},[56,48,57,58,59,60],"api-gateway","grpc","rest","swagger","playbook","Playbook",[63],"api-gateway\u002Fsrc\u002F**","Exposer ou Modifier une Route REST Edge","stable",[67,71,75,79,83,87,91,95,99,103,107,111],{"id":68,"title":69,"resource":70},"gw-claude","Modules de api-gateway (carte : references\u002Froutes.md)","api-gateway\u002Fsrc\u002Fmodules",{"id":72,"title":73,"resource":74},"decorator","GatewayController","api-gateway\u002Fsrc\u002Fcommon\u002Fdecorators\u002Fgateway-controller.decorator.ts",{"id":76,"title":77,"resource":78},"app-module","AppModule (GrpcInternalInterceptor, HelperModule)","api-gateway\u002Fsrc\u002Fapp.module.ts",{"id":80,"title":81,"resource":82},"grpc-module","GrpcClientModule","api-gateway\u002Fsrc\u002Fgrpc\u002Fgrpc-client.module.ts",{"id":84,"title":85,"resource":86},"helper","TokenHelperController","api-gateway\u002Fsrc\u002Fmodules\u002Fhelper\u002Fcontrollers\u002Ftoken-helper.controller.ts",{"id":88,"title":89,"resource":90},"seed","SystemSeedController","api-gateway\u002Fsrc\u002Fmodules\u002Fsystem\u002Fcontrollers\u002Fsystem-seed.controller.ts",{"id":92,"title":93,"resource":94},"deploy-gw","Déploiement api-gateway (NODE_ENV)","deploy\u002Fapps\u002Fbase\u002Fapi-gateway\u002Fdeployment.yaml",{"id":96,"title":97,"resource":98},"errors-nest","FALLBACK_ACTIVATED (PartialContentError 206)","npm-packages\u002Fpackages\u002Ferrors-nest\u002Fsrc\u002Ferrors\u002Fcommon.errors.ts",{"id":100,"title":101,"resource":102},"ws-proxy","WsProxyMiddleware","api-gateway\u002Fsrc\u002Fmodules\u002Fws-proxy\u002Fws-proxy.middleware.ts",{"id":104,"title":105,"resource":106},"pagination","Module de pagination commun (constantes, DTO, decorateurs, ecretage)","api-gateway\u002Fsrc\u002Fcommon\u002Fdto\u002Frequest\u002Fpagination.request.dto.ts",{"id":108,"title":109,"resource":110},"check-arch","yarn check-arch (regles des DTO)","api-gateway\u002Fscripts\u002Fcheck-dto-architecture.ts",{"id":112,"title":113,"resource":114},"web-requests","Types web de requete du classement","npm-packages\u002Fpackages\u002Fcontracts\u002Fsrc\u002Fgateway\u002Fuser\u002Frequest\u002Fleaderboard.request.ts",[116,120,123,126,129],{"at":117,"by":118,"digest":119},"2026-11-03T20:40:37Z","claude-code\u002Fagent","be5ca2862e35d8ad",{"at":121,"by":118,"digest":122},"2026-11-04T17:55:25Z","36e80228c7f25540",{"at":124,"by":118,"digest":125},"2026-11-08T12:02:21Z","6e8237e4a9c0e774",{"at":127,"by":118,"digest":128},"2026-11-08T12:18:23Z","438a549648dfd54b",{"at":130,"by":118,"digest":128},"2026-11-08T22:50:47Z",{"at":132,"by":133},"2026-10-06T08:54:04Z","claude-code\u002Fclaude-opus-5-5",[56,135,136,137,138,139,140,100],"gatewaycontroller","accesstokenguard","rolesguard","grpcinternalinterceptor","internalmetadata","iscurrentuseroradminguard","main","6b8f03b57018fa66da4666071dc17c1577ff45e8",".agents\u002Fskills\u002Fvolontariapp-api-gateway","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":149,"prompt":159,"zipAvailable":160},[150,153,156],{"tool":151,"command":152},"npx","npx skills add Volontariapp\u002Fmeta --skill volontariapp-api-gateway",{"tool":154,"command":155},"bunx","bunx skills add Volontariapp\u002Fmeta --skill volontariapp-api-gateway",{"tool":157,"command":158},"pnpm","pnpm dlx skills add Volontariapp\u002Fmeta --skill volontariapp-api-gateway","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FVolontariapp\u002Fmeta\u002Fblob\u002Fmain\u002F.agents\u002Fskills\u002Fvolontariapp-api-gateway\u002FSKILL.md",true,[162,168,172,176,180],{"path":163,"resourceType":164,"resourceTypeDesc":165,"sizeBytes":166,"sourceHtmlUrl":167},"index.md","REFERENCE","参考文档",1030,"https:\u002F\u002Fgithub.com\u002FVolontariapp\u002Fmeta\u002Fblob\u002Fmain\u002F.agents\u002Fskills\u002Fvolontariapp-api-gateway\u002Findex.md",{"path":169,"resourceType":164,"resourceTypeDesc":165,"sizeBytes":170,"sourceHtmlUrl":171},"log.md",7647,"https:\u002F\u002Fgithub.com\u002FVolontariapp\u002Fmeta\u002Fblob\u002Fmain\u002F.agents\u002Fskills\u002Fvolontariapp-api-gateway\u002Flog.md",{"path":173,"resourceType":164,"resourceTypeDesc":165,"sizeBytes":174,"sourceHtmlUrl":175},"references\u002Flessons.md",21307,"https:\u002F\u002Fgithub.com\u002FVolontariapp\u002Fmeta\u002Fblob\u002Fmain\u002F.agents\u002Fskills\u002Fvolontariapp-api-gateway\u002Freferences\u002Flessons.md",{"path":177,"resourceType":164,"resourceTypeDesc":165,"sizeBytes":178,"sourceHtmlUrl":179},"references\u002Froutes.md",5159,"https:\u002F\u002Fgithub.com\u002FVolontariapp\u002Fmeta\u002Fblob\u002Fmain\u002F.agents\u002Fskills\u002Fvolontariapp-api-gateway\u002Freferences\u002Froutes.md",{"path":181,"resourceType":164,"resourceTypeDesc":165,"sizeBytes":182,"sourceHtmlUrl":183},"references\u002Fsecurity-and-risks.md",4640,"https:\u002F\u002Fgithub.com\u002FVolontariapp\u002Fmeta\u002Fblob\u002Fmain\u002F.agents\u002Fskills\u002Fvolontariapp-api-gateway\u002Freferences\u002Fsecurity-and-risks.md",[185,208,233,258,282,301],{"skillNo":186,"name":187,"nameZh":6,"description":188,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":189,"categories":190,"keywords":191,"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",[],[192,193,194,195,196,197,198,199,200,47,201,202,203,204,205,206,207,40,35],"volontariapp user account deletion","user","account deletion","deletion","cycle","gather","sonnet","pending","de","claude-code","reference","npm","user-account-deletion","src","en","domain",{"skillNo":209,"name":210,"nameZh":6,"description":211,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":212,"categories":213,"keywords":217,"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",[214],{"categoryNo":215,"categoryName":216,"taxonomyType":26,"taxonomyTypeDesc":27},"01JBSEEDCAT000000000000037","律师与法务",[218,219,57,220,47,221,222,35,223,224,225,226,227,203,228,229,230,231,60,232],"evolution","volontariapp proto contract evolution","lint","workflows","analyze","rpc","ci","pr","protobuf","message","registry","sans","sync","yaml","github",{"skillNo":234,"name":235,"nameZh":6,"description":236,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":237,"categories":238,"keywords":242,"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",[239],{"categoryNo":240,"categoryName":241,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发",[243,244,245,47,246,201,247,248,249,250,251,252,253,254,255,256,257],"Meta","volontariapp meta scripts","repos","migrate","setup","meta-scripts","json","node","workspace","shell","bump","cli","doctor","draft","install",{"skillNo":259,"name":260,"nameZh":6,"description":261,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":262,"categories":263,"keywords":267,"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",[264],{"categoryNo":265,"categoryName":266,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000009","前端开发",[268,269,270,271,272,273,205,274,275,228,276,277,40,278,279,35,280,36,47,281],"async","implement","volontariapp implement async event flow","flow","event","messaging","mapping","flux","streams","flows","shared","outbox","processor","runner",{"skillNo":283,"name":284,"nameZh":6,"description":285,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":286,"categories":287,"keywords":291,"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",[288],{"categoryNo":289,"categoryName":290,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000021","资料检索与研究",[284,30,193,292,293,294,295,296,297,298,299,300],"file","repo","markdown","docs","capture","use","question","trust","investigate",{"skillNo":302,"name":303,"nameZh":6,"description":304,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":305,"categories":306,"keywords":310,"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",[307],{"categoryNo":308,"categoryName":309,"taxonomyType":26,"taxonomyTypeDesc":27},"01JBSEEDCAT000000000000032","设计师",[303,311,193,312,297,313,298,314,315,316,317,318],"check","ui","look","design","explore","model","build","state"]