[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6CSTJGEJKB3CZZW8HHFSRGF":3,"skill-same:01M6CSTJGEJKB3CZZW8HHFSRGF":169},{"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":18,"publishedAt":40,"linkOnly":41,"markdownContent":42,"markdownBody":43,"frontMatter":44,"refName":35,"commitSha":111,"skillDirectory":112,"forksCount":14,"repositoryHtmlUrl":113,"repositoryHomepageUrl":6,"ownerHtmlUrl":114,"ownerBlogUrl":6,"ownerDescription":115,"updatedAt":116,"install":117,"resources":130},"01M6CSTJGEJKB3CZZW8HHFSRGF","volontariapp-grpc-microservice",null,"Playbook opérationnel pas-à-pas pour implémenter ou modifier un RPC dans un microservice gRPC (ms-user, ms-event, ms-post, ms-social) : contrat, DTO, contrôleur CQRS, garde interne, fallback, mappers 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-grpc-microservice\u002FSKILL.md",[],[19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39],"microservice","volontariapp grpc microservice","grpc","2026","dto","src","scripts","command","de","cli","structure","packages","controller","npm-packages","app","code","main","social","ddd","ms","modules","2026-11-10 10:12:30",false,"---\nname: volontariapp-grpc-microservice\ndescription: \"Playbook opérationnel pas-à-pas pour implémenter ou modifier un RPC dans un microservice gRPC (ms-user, ms-event, ms-post, ms-social) : contrat, DTO, contrôleur CQRS, garde interne, fallback, mappers et tests.\"\ntype: Playbook\ntitle: Implémenter ou Modifier un RPC gRPC\ntags: [microservice, grpc, nestjs, ddd, cqrs, playbook]\nstatus: stable\npaths:\n  - \"ms-user\u002Fsrc\u002F**\"\n  - \"ms-event\u002Fsrc\u002F**\"\n  - \"ms-post\u002Fsrc\u002F**\"\n  - \"ms-social\u002Fsrc\u002F**\"\n  - scripts\u002Fcli\u002Fcommands\u002Fsync-migrations.mjs\nmesh_keys:\n  - ms-user\n  - ms-event\n  - ms-post\n  - ms-social\n  - basecommandcontroller\n  - grpcvalidationpipe\n  - grpcinternalguard\n  - transformer\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-10-30T12:29:16Z\"\n    digest: 37f4a83af8a6d3d1\n  - by: claude-code\u002Fagent\n    at: \"2026-11-03T20:31:03Z\"\n    digest: 79cb461c249d3c2e\n  - by: claude-code\u002Fagent\n    at: \"2026-11-04T17:53:23Z\"\n    digest: a4e563bea3a17189\n  - by: claude-code\u002Fagent\n    at: \"2026-11-06T14:04:01Z\"\n    digest: 90966abbcd8b04bd\n  - by: claude-code\u002Fagent\n    at: \"2026-11-08T12:02:22Z\"\n    digest: e1f929df232e29f5\nsources:\n  - id: agent-md\n    resource: AGENT.md\n    title: \"AGENT.md (structure de module, DTO, contrôleurs, tests)\"\n  - id: main-user\n    resource: ms-user\u002Fsrc\u002Fmain.ts\n    title: Bootstrap hybride (gRPC + HTTP \u002Fhealth)\n  - id: app-event\n    resource: ms-event\u002Fsrc\u002Fapp.module.ts\n    title: \"AppModule ms-event (GrpcValidationPipe, APP_GUARD)\"\n  - id: base-cmd\n    resource: ms-event\u002Fsrc\u002Fmodules\u002Fevent\u002Fcontrollers\u002Fcommands\u002Fbase.command.controller.ts\n    title: BaseCommandController.withFallback\n  - id: sync-mig\n    resource: scripts\u002Fcli\u002Fcommands\u002Fsync-migrations.mjs\n    title: sync-migrations.mjs\n  - id: validation\n    resource: npm-packages\u002Fpackages\u002Fvalidation-nest\n    title: GrpcValidationPipe\n---\n\n# Playbook : Implémenter ou Modifier un RPC gRPC dans un Microservice\n\nDans l'architecture Volontariapp, un microservice `ms-*` est une coquille NestJS purement dédiée à l'exposition gRPC (port 3000) et HTTP `\u002Fhealth`[^main-user] : il valide les messages entrants, applique la garde du token interne de sécurité, et délègue l'exécution métier au package de domaine partagé `@volontariapp\u002Fdomain-\u003Cdomaine>`.\n\nLa documentation d'architecture détaillée et les conventions structurelles sont disponibles dans [Architecture Microservice](\u002Fvolontariapp-grpc-microservice\u002Freferences\u002Fmicroservice-architecture.md) et `AGENT.md`[^agent-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 méthode RPC `rpc Method(Command) returns (Response)` dans un microservice existant.\n- La modification d'un endpoint gRPC existant (nouveau champ reçu ou retourné, ajustement de validation).\n- L'ajout d'un appel gRPC sortant vers un autre microservice du mesh.\n\n---\n\n## Étape 1 : Vérification des Contrats Amont & Règle du STOP\n\nAvant d'écrire la moindre ligne de code dans `ms-*` :\n\n1. **Localiser la méthode dans le graphe gRPC :**\n   ```json\n   analyze_grpc({ \"target\": \"\u003CMethodName>\" })\n   ```\n2. **Contrôler la présence du contrat dans `@volontariapp\u002Fcontracts-nest` :**\n   - L'interface de commande\u002Frequête, l'interface de réponse et les constantes de service (`GRPC_SERVICES`, `\u003CSERVICE>_METHODS`) doivent être exportées par le package publié.\n   \n> [!CAUTION]\n> ### 🛑 RÈGLE CRITIQUE DU STOP IMMÉDIAT\n> Si le RPC, le message ou le champ n'existe pas encore dans les contrats TypeScript :\n> 1. **INTERDICTION FORMELLE** de créer des interfaces locales temporaires ou d'injecter des types arbitraires.\n> 2. Dérouler d'abord le playbook [`volontariapp-proto-contract-evolution`](\u002Fvolontariapp-proto-contract-evolution\u002FSKILL.md) dans `proto-registry\u002F`.\n> 3. Attendre la publication de `@volontariapp\u002Fcontracts-nest` par la CI.\n> 4. Mettre à jour les dépendances du microservice : `yarn up @volontariapp\u002Fcontracts-nest`.\n\n---\n\n## Étape 2 : Création des DTOs de Requête (Validation)\n\nEmplacement : `src\u002Fmodules\u002F\u003Cdomaine>\u002Fdto\u002Frequest\u002F{command,query}\u002F`.\n\nChaque DTO de commande ou requête **doit impérativement** implémenter l'interface TypeScript du contrat gRPC et définir des règles `class-validator` strictes :\n\n```typescript\nimport { IsString, IsNotEmpty, IsUUID, IsEnum, IsOptional } from 'class-validator';\nimport type { CreateEventCommand, EventType } from '@volontariapp\u002Fcontracts-nest';\n\nexport class CreateEventCommandDTO implements CreateEventCommand {\n  @IsString()\n  @IsNotEmpty()\n  title!: string;\n\n  @IsEnum(EventType)\n  type!: EventType;\n\n  @IsUUID()\n  @IsOptional()\n  coverFileId?: string;\n}\n```\n\n- **Règles de validation automatique** : `GrpcValidationPipe` est configuré en `APP_PIPE` global avec `enumMaps`[^app-event][^validation]. Il convertit les chaînes vides en `undefined`, les entiers Int64 en `number`, et les noms d'enums sous forme de string en valeurs numériques conformes Protobuf.\n- **Défense en profondeur** : Utiliser l'assertion d'assignation définie `!` pour les champs obligatoires du contrat (aucun `any`).\n- **Pagination des requêtes de liste** : ne jamais déclarer `pagination` avec le seul `@IsOptional()` (aucune valeur n'est alors validée). Motif commun : `@IsOptional() @ValidateNested() @Type(() => PaginationRequestDTO) pagination: PaginationRequestDTO | undefined;`, avec `PaginationRequestDTO` dans `src\u002Fcommon\u002Fdto\u002Fpagination.dto.ts` (`page` : `@IsInt @Min(1)`, `limit` : `@IsInt @Min(1) @Max(MAX_PAGINATION_LIMIT)`). Le plafond `MAX_PAGINATION_LIMIT` dépend du service (relevé le 2026-11-04) : 100 dans `ms-post` (#69) et `ms-event` (#83), qui l'ont abaissé de 1000 à 100, et 1000 dans `ms-user` et `ms-social` (la gateway écrête à 100 sur ses routes, et lit les ensembles d'ids par pages de 100 avec `fetchAllPages` depuis api-gateway#90, au lieu d'un appel `limit: 1000`). Ne pas supposer 1000 : lire la constante du service. Pagination absente : défaut page 1 \u002F limit 10 dans le contrôleur (`DEFAULT_PAGE`, `DEFAULT_LIMIT`), jamais une lecture de toute la table. Listes d'ids en entrée : `@IsArray @ArrayMaxSize(100)` (1000 pour `ids`\u002F`excludedIds` de `SearchEvents`, que la gateway remplit avec les événements recommandés).\n- **Lectures par lot** : pas de `Promise.all(ids.map(findById))`. Utiliser le `findByIds` du domaine (une requête, 100 ids maximum, tronqué en silence au-delà donc découper par lots de 100) puis remettre le résultat dans l'ordre des ids d'entrée, car cet ordre vient de ms-social pour les listes de profils et d'événements. Exemples : `ms-user\u002Fsrc\u002Fmodules\u002Fuser\u002Fhelpers\u002Ffind-active-users-in-order.ts`, `ms-event\u002Fsrc\u002Fmodules\u002Fevent\u002Futils\u002Fevents-by-ids.util.ts`. Côté domaine, filtrer en SQL avant `limit`\u002F`offset` (`UserService.findPage`) et toujours trier avec un départage par `id` (offset instable sinon).\n\n---\n\n## Étape 3 : Câblage Métier dans `@volontariapp\u002Fdomain-*`\n\nLes microservices `ms-*` n'ont **aucune logique métier propre** :\n\n1. Les entités TypeORM, modèles, règles de gestion et repositories résident dans le package partagé `@volontariapp\u002Fdomain-\u003Cdomaine>`.\n2. Si la nouvelle méthode RPC requiert une nouvelle règle de calcul, une transaction ou une transition d'état :\n   - Modifier d'abord `npm-packages\u002Fpackages\u002Fdomain-\u003Cdomaine>\u002F`.\n   - Appliquer le playbook [`volontariapp-shared-npm-package-change`](\u002Fvolontariapp-shared-npm-package-change\u002FSKILL.md) avec bump changeset et publication snapshot.\n   - Lancer `yarn up @volontariapp\u002Fdomain-\u003Cdomaine>` dans le microservice consommateur.\n3. Injecter le service de domaine comme provider dans `\u003CDomaine>Module`.\n4. **Cas d'une lecture qui agrège plusieurs tables avec un catalogue partagé** (progression des badges) : un service local au microservice, `src\u002Fmodules\u002F\u003Cdomaine>\u002Fservices\u002F\u003Cnom>.service.ts`, qui injecte les repositories TypeORM par `@InjectRepository` (le `DatabaseModule` les expose en global) et délègue le calcul à une fonction pure de `@volontariapp\u002Fshared`. Précédent : `BadgeProgressService` de `ms-user` (`computeBadgeProgress`). Les mocks des repositories vont dans un fichier `*.mock.ts` séparé.\n\n---\n\n## Étape 4 : Implémentation du Controller gRPC (CQRS)\n\nLes contrôleurs sont strictement scindés selon le pattern CQRS :\n- Écriture : `src\u002Fmodules\u002F\u003Cdomaine>\u002Fcontrollers\u002Fcommands\u002F\u003Centite>.command.controller.ts`\n- Lecture : `src\u002Fmodules\u002F\u003Cdomaine>\u002Fcontrollers\u002Fqueries\u002F\u003Centite>.query.controller.ts`\n\n### Contrôleur de Commande (avec Fallback éventuel) :\n```typescript\nimport { Controller } from '@nestjs\u002Fcommon';\nimport { GrpcMethod } from '@nestjs\u002Fmicroservices';\nimport { Logger } from '@volontariapp\u002Flogger';\nimport { GRPC_SERVICES, EVENT_COMMAND_METHODS } from '@volontariapp\u002Fcontracts-nest';\nimport { CurrentUser, type AuthUser } from '@volontariapp\u002Fauth';\nimport { BaseCommandController } from '.\u002Fbase.command.controller.js';\nimport { CreateEventCommandDTO } from '..\u002F..\u002Fdto\u002Frequest\u002Fcommand\u002Fcreate-event.command.dto.js';\nimport { CreateEventResponseDTO } from '..\u002F..\u002Fdto\u002Fresponse\u002Fcreate-event.response.dto.js';\nimport { EventResponseMapper } from '..\u002F..\u002Fmappers\u002Fevent-response.mapper.js';\nimport { EventDomainService } from '@volontariapp\u002Fdomain-event';\n\n@Controller()\nexport class EventCommandController extends BaseCommandController {\n  private readonly logger = new Logger({ context: EventCommandController.name });\n\n  constructor(private readonly eventService: EventDomainService) {\n    super();\n  }\n\n  @GrpcMethod(GRPC_SERVICES.EVENT_COMMAND_SERVICE, EVENT_COMMAND_METHODS.CREATE_EVENT)\n  async createEvent(\n    data: CreateEventCommandDTO,\n    @CurrentUser() user: AuthUser,\n  ): Promise\u003CCreateEventResponseDTO> {\n    this.logger.info(`gRPC CreateEvent: ${data.title} par user ${user.id}`);\n\n    \u002F\u002F Exécution avec fallback pour absorption des pannes de dépendances\n    const entity = await this.withFallback(\n      JobMessagingType.FALLBACK_CREATE_EVENT,\n      user.id,\n      data,\n      async () => this.eventService.createEvent(user.id, data),\n    );\n\n    return EventResponseMapper.toCreateResponseDTO(entity);\n  }\n}\n```\n\n- **Identification de l'acteur** : L'identité provient exclusivement du décorateur `@CurrentUser()` injecté par `GrpcInternalGuard` depuis l'`INTERNAL_TOKEN`. Ne **jamais** lire l'acteur dans le corps du message DTO[^app-event].\n- **Gestion `withFallback`** : Héritée de `BaseCommandController`[^base-cmd], cette méthode ne déclenche aucun job en cas d'erreur client (400, 404, 409, et aussi 403 dans `ms-event` ; vérifier les autres `ms-*` avant de supposer le même comportement). Sur panne système ou dépendance injoignable, elle insère un job dans `jobs_outbox` et renvoie la réponse typée `FALLBACK_ACTIVATED` (traduite en HTTP 206 par l'API Gateway).\n\n---\n\n**Pièges du contrôleur de lecture :**\n- **Constante de méthode manquante.** `BADGE_QUERY_METHODS`, `USER_QUERY_METHODS`... dans `contracts-nest\u002Fsrc\u002F\u003Cd>\u002Fgrpc-helpers.ts` sont écrites à la main : un nouveau RPC n'y figure pas automatiquement. Utiliser le littéral du `.proto` : `@GrpcMethod(BADGE_SERVICE_NAME, 'GetMyBadgeProgress')` (même approche que `'GetUsersByIds'`), ou ajouter la constante dans `npm-packages` si plusieurs consommateurs la veulent.\n- **Route « me ».** Aucun identifiant dans le message : `@UseGuards(GrpcInternalGuard)` et `@CurrentUser() user: AuthUser`, comme `getUser`. Un `userId` fourni par le client ne doit jamais être lu.\n\n## Étape 5 : Mappers & DTOs de Réponse\n\n1. Définir le DTO de réponse sous `src\u002Fmodules\u002F\u003Cdomaine>\u002Fdto\u002Fresponse\u002F\u003Centite>.response.dto.ts` en implémentant l'interface du contrat (ex. `CreateEventResponse`).\n2. Implémenter la transformation dans `src\u002Fmodules\u002F\u003Cdomaine>\u002Fmappers\u002F\u003Cdomaine>-response.mapper.ts` :\n   - Conversion des dates en chaînes ISO 8601 UTC ou timestamps conformes au `.proto`.\n   - Transformation des enums numériques et normalisation des relations.\n   - Ne jamais exposer directement l'entité TypeORM brute sur le bus gRPC.\n\n---\n\n## Étape 6 : Stratégie de Tests Obligatoires\n\nChaque nouveau RPC ou modification doit être couvert par des tests unitaires stricts :\n\n1. **Isolation des Mocks & Factories :**\n   - Mocks obligatoires dans un fichier dédié : `src\u002Fmodules\u002F\u003Cd>\u002F__tests__\u002F\u003Cnom>.mock.ts`.\n   - Données de test générées par factories dédiées : `src\u002Fmodules\u002F\u003Cd>\u002F__tests__\u002F\u003Cnom>.factory.ts`.\n   - Interdiction formelle de déclarer des objets `as unknown as Type` ou `any` inline dans les tests.\n2. **Utilisation rigoureuse de Spies :**\n   - Toujours espionner les appels de dépendances avec `jest.spyOn()`.\n   - Nettoyage systématique dans `afterEach(() => { jest.restoreAllMocks(); })`.\n3. **Cas de test obligatoires :**\n   - Cas nominal : renvoi du DTO de réponse avec mapping exact.\n   - Cas d'erreur métier : validation des exceptions typées (`@volontariapp\u002Ferrors` ou `@volontariapp\u002Ferrors-nest`).\n   - Cas de refus d'authentification : rejet si `x-internal-token` est manquant.\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 RPC et ses DTOs proviennent exclusivement de `@volontariapp\u002Fcontracts-nest` (aucun type inventé).\n- [ ] Aucun bout de logique métier n'a été inséré dans `src\u002F` (délégué à `domain-\u003Cdomaine>`).\n- [ ] Aucun cast `as unknown as Type` ni `any` n'est présent.\n- [ ] L'acteur est extrait via `@CurrentUser()`, jamais depuis le body.\n- [ ] Les mappers transforment proprement les entités en DTOs gRPC.\n- [ ] `yarn test` passe avec succès sur le microservice.\n- [ ] `yarn tsc` et `yarn lint` ne produisent aucune erreur.\n- [ ] En cas de migration de base de données associée : exécuter `yarn meta sync-migrations` si commune[^sync-mig].\n\n[^agent-md]: AGENT.md (structure de module, DTO, contrôleurs, tests)\n[^main-user]: Bootstrap hybride (gRPC + HTTP \u002Fhealth)\n[^app-event]: AppModule ms-event (GrpcValidationPipe, APP_GUARD)\n[^validation]: GrpcValidationPipe\n[^base-cmd]: BaseCommandController.withFallback\n[^sync-mig]: scripts\u002Fcli\u002Fcommands\u002Fsync-migrations.mjs\n","# Playbook : Implémenter ou Modifier un RPC gRPC dans un Microservice\n\nDans l'architecture Volontariapp, un microservice `ms-*` est une coquille NestJS purement dédiée à l'exposition gRPC (port 3000) et HTTP `\u002Fhealth`[^main-user] : il valide les messages entrants, applique la garde du token interne de sécurité, et délègue l'exécution métier au package de domaine partagé `@volontariapp\u002Fdomain-\u003Cdomaine>`.\n\nLa documentation d'architecture détaillée et les conventions structurelles sont disponibles dans [Architecture Microservice](\u002Fvolontariapp-grpc-microservice\u002Freferences\u002Fmicroservice-architecture.md) et `AGENT.md`[^agent-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 méthode RPC `rpc Method(Command) returns (Response)` dans un microservice existant.\n- La modification d'un endpoint gRPC existant (nouveau champ reçu ou retourné, ajustement de validation).\n- L'ajout d'un appel gRPC sortant vers un autre microservice du mesh.\n\n---\n\n## Étape 1 : Vérification des Contrats Amont & Règle du STOP\n\nAvant d'écrire la moindre ligne de code dans `ms-*` :\n\n1. **Localiser la méthode dans le graphe gRPC :**\n   ```json\n   analyze_grpc({ \"target\": \"\u003CMethodName>\" })\n   ```\n2. **Contrôler la présence du contrat dans `@volontariapp\u002Fcontracts-nest` :**\n   - L'interface de commande\u002Frequête, l'interface de réponse et les constantes de service (`GRPC_SERVICES`, `\u003CSERVICE>_METHODS`) doivent être exportées par le package publié.\n   \n> [!CAUTION]\n> ### 🛑 RÈGLE CRITIQUE DU STOP IMMÉDIAT\n> Si le RPC, le message ou le champ n'existe pas encore dans les contrats TypeScript :\n> 1. **INTERDICTION FORMELLE** de créer des interfaces locales temporaires ou d'injecter des types arbitraires.\n> 2. Dérouler d'abord le playbook [`volontariapp-proto-contract-evolution`](\u002Fvolontariapp-proto-contract-evolution\u002FSKILL.md) dans `proto-registry\u002F`.\n> 3. Attendre la publication de `@volontariapp\u002Fcontracts-nest` par la CI.\n> 4. Mettre à jour les dépendances du microservice : `yarn up @volontariapp\u002Fcontracts-nest`.\n\n---\n\n## Étape 2 : Création des DTOs de Requête (Validation)\n\nEmplacement : `src\u002Fmodules\u002F\u003Cdomaine>\u002Fdto\u002Frequest\u002F{command,query}\u002F`.\n\nChaque DTO de commande ou requête **doit impérativement** implémenter l'interface TypeScript du contrat gRPC et définir des règles `class-validator` strictes :\n\n```typescript\nimport { IsString, IsNotEmpty, IsUUID, IsEnum, IsOptional } from 'class-validator';\nimport type { CreateEventCommand, EventType } from '@volontariapp\u002Fcontracts-nest';\n\nexport class CreateEventCommandDTO implements CreateEventCommand {\n  @IsString()\n  @IsNotEmpty()\n  title!: string;\n\n  @IsEnum(EventType)\n  type!: EventType;\n\n  @IsUUID()\n  @IsOptional()\n  coverFileId?: string;\n}\n```\n\n- **Règles de validation automatique** : `GrpcValidationPipe` est configuré en `APP_PIPE` global avec `enumMaps`[^app-event][^validation]. Il convertit les chaînes vides en `undefined`, les entiers Int64 en `number`, et les noms d'enums sous forme de string en valeurs numériques conformes Protobuf.\n- **Défense en profondeur** : Utiliser l'assertion d'assignation définie `!` pour les champs obligatoires du contrat (aucun `any`).\n- **Pagination des requêtes de liste** : ne jamais déclarer `pagination` avec le seul `@IsOptional()` (aucune valeur n'est alors validée). Motif commun : `@IsOptional() @ValidateNested() @Type(() => PaginationRequestDTO) pagination: PaginationRequestDTO | undefined;`, avec `PaginationRequestDTO` dans `src\u002Fcommon\u002Fdto\u002Fpagination.dto.ts` (`page` : `@IsInt @Min(1)`, `limit` : `@IsInt @Min(1) @Max(MAX_PAGINATION_LIMIT)`). Le plafond `MAX_PAGINATION_LIMIT` dépend du service (relevé le 2026-11-04) : 100 dans `ms-post` (#69) et `ms-event` (#83), qui l'ont abaissé de 1000 à 100, et 1000 dans `ms-user` et `ms-social` (la gateway écrête à 100 sur ses routes, et lit les ensembles d'ids par pages de 100 avec `fetchAllPages` depuis api-gateway#90, au lieu d'un appel `limit: 1000`). Ne pas supposer 1000 : lire la constante du service. Pagination absente : défaut page 1 \u002F limit 10 dans le contrôleur (`DEFAULT_PAGE`, `DEFAULT_LIMIT`), jamais une lecture de toute la table. Listes d'ids en entrée : `@IsArray @ArrayMaxSize(100)` (1000 pour `ids`\u002F`excludedIds` de `SearchEvents`, que la gateway remplit avec les événements recommandés).\n- **Lectures par lot** : pas de `Promise.all(ids.map(findById))`. Utiliser le `findByIds` du domaine (une requête, 100 ids maximum, tronqué en silence au-delà donc découper par lots de 100) puis remettre le résultat dans l'ordre des ids d'entrée, car cet ordre vient de ms-social pour les listes de profils et d'événements. Exemples : `ms-user\u002Fsrc\u002Fmodules\u002Fuser\u002Fhelpers\u002Ffind-active-users-in-order.ts`, `ms-event\u002Fsrc\u002Fmodules\u002Fevent\u002Futils\u002Fevents-by-ids.util.ts`. Côté domaine, filtrer en SQL avant `limit`\u002F`offset` (`UserService.findPage`) et toujours trier avec un départage par `id` (offset instable sinon).\n\n---\n\n## Étape 3 : Câblage Métier dans `@volontariapp\u002Fdomain-*`\n\nLes microservices `ms-*` n'ont **aucune logique métier propre** :\n\n1. Les entités TypeORM, modèles, règles de gestion et repositories résident dans le package partagé `@volontariapp\u002Fdomain-\u003Cdomaine>`.\n2. Si la nouvelle méthode RPC requiert une nouvelle règle de calcul, une transaction ou une transition d'état :\n   - Modifier d'abord `npm-packages\u002Fpackages\u002Fdomain-\u003Cdomaine>\u002F`.\n   - Appliquer le playbook [`volontariapp-shared-npm-package-change`](\u002Fvolontariapp-shared-npm-package-change\u002FSKILL.md) avec bump changeset et publication snapshot.\n   - Lancer `yarn up @volontariapp\u002Fdomain-\u003Cdomaine>` dans le microservice consommateur.\n3. Injecter le service de domaine comme provider dans `\u003CDomaine>Module`.\n4. **Cas d'une lecture qui agrège plusieurs tables avec un catalogue partagé** (progression des badges) : un service local au microservice, `src\u002Fmodules\u002F\u003Cdomaine>\u002Fservices\u002F\u003Cnom>.service.ts`, qui injecte les repositories TypeORM par `@InjectRepository` (le `DatabaseModule` les expose en global) et délègue le calcul à une fonction pure de `@volontariapp\u002Fshared`. Précédent : `BadgeProgressService` de `ms-user` (`computeBadgeProgress`). Les mocks des repositories vont dans un fichier `*.mock.ts` séparé.\n\n---\n\n## Étape 4 : Implémentation du Controller gRPC (CQRS)\n\nLes contrôleurs sont strictement scindés selon le pattern CQRS :\n- Écriture : `src\u002Fmodules\u002F\u003Cdomaine>\u002Fcontrollers\u002Fcommands\u002F\u003Centite>.command.controller.ts`\n- Lecture : `src\u002Fmodules\u002F\u003Cdomaine>\u002Fcontrollers\u002Fqueries\u002F\u003Centite>.query.controller.ts`\n\n### Contrôleur de Commande (avec Fallback éventuel) :\n```typescript\nimport { Controller } from '@nestjs\u002Fcommon';\nimport { GrpcMethod } from '@nestjs\u002Fmicroservices';\nimport { Logger } from '@volontariapp\u002Flogger';\nimport { GRPC_SERVICES, EVENT_COMMAND_METHODS } from '@volontariapp\u002Fcontracts-nest';\nimport { CurrentUser, type AuthUser } from '@volontariapp\u002Fauth';\nimport { BaseCommandController } from '.\u002Fbase.command.controller.js';\nimport { CreateEventCommandDTO } from '..\u002F..\u002Fdto\u002Frequest\u002Fcommand\u002Fcreate-event.command.dto.js';\nimport { CreateEventResponseDTO } from '..\u002F..\u002Fdto\u002Fresponse\u002Fcreate-event.response.dto.js';\nimport { EventResponseMapper } from '..\u002F..\u002Fmappers\u002Fevent-response.mapper.js';\nimport { EventDomainService } from '@volontariapp\u002Fdomain-event';\n\n@Controller()\nexport class EventCommandController extends BaseCommandController {\n  private readonly logger = new Logger({ context: EventCommandController.name });\n\n  constructor(private readonly eventService: EventDomainService) {\n    super();\n  }\n\n  @GrpcMethod(GRPC_SERVICES.EVENT_COMMAND_SERVICE, EVENT_COMMAND_METHODS.CREATE_EVENT)\n  async createEvent(\n    data: CreateEventCommandDTO,\n    @CurrentUser() user: AuthUser,\n  ): Promise\u003CCreateEventResponseDTO> {\n    this.logger.info(`gRPC CreateEvent: ${data.title} par user ${user.id}`);\n\n    \u002F\u002F Exécution avec fallback pour absorption des pannes de dépendances\n    const entity = await this.withFallback(\n      JobMessagingType.FALLBACK_CREATE_EVENT,\n      user.id,\n      data,\n      async () => this.eventService.createEvent(user.id, data),\n    );\n\n    return EventResponseMapper.toCreateResponseDTO(entity);\n  }\n}\n```\n\n- **Identification de l'acteur** : L'identité provient exclusivement du décorateur `@CurrentUser()` injecté par `GrpcInternalGuard` depuis l'`INTERNAL_TOKEN`. Ne **jamais** lire l'acteur dans le corps du message DTO[^app-event].\n- **Gestion `withFallback`** : Héritée de `BaseCommandController`[^base-cmd], cette méthode ne déclenche aucun job en cas d'erreur client (400, 404, 409, et aussi 403 dans `ms-event` ; vérifier les autres `ms-*` avant de supposer le même comportement). Sur panne système ou dépendance injoignable, elle insère un job dans `jobs_outbox` et renvoie la réponse typée `FALLBACK_ACTIVATED` (traduite en HTTP 206 par l'API Gateway).\n\n---\n\n**Pièges du contrôleur de lecture :**\n- **Constante de méthode manquante.** `BADGE_QUERY_METHODS`, `USER_QUERY_METHODS`... dans `contracts-nest\u002Fsrc\u002F\u003Cd>\u002Fgrpc-helpers.ts` sont écrites à la main : un nouveau RPC n'y figure pas automatiquement. Utiliser le littéral du `.proto` : `@GrpcMethod(BADGE_SERVICE_NAME, 'GetMyBadgeProgress')` (même approche que `'GetUsersByIds'`), ou ajouter la constante dans `npm-packages` si plusieurs consommateurs la veulent.\n- **Route « me ».** Aucun identifiant dans le message : `@UseGuards(GrpcInternalGuard)` et `@CurrentUser() user: AuthUser`, comme `getUser`. Un `userId` fourni par le client ne doit jamais être lu.\n\n## Étape 5 : Mappers & DTOs de Réponse\n\n1. Définir le DTO de réponse sous `src\u002Fmodules\u002F\u003Cdomaine>\u002Fdto\u002Fresponse\u002F\u003Centite>.response.dto.ts` en implémentant l'interface du contrat (ex. `CreateEventResponse`).\n2. Implémenter la transformation dans `src\u002Fmodules\u002F\u003Cdomaine>\u002Fmappers\u002F\u003Cdomaine>-response.mapper.ts` :\n   - Conversion des dates en chaînes ISO 8601 UTC ou timestamps conformes au `.proto`.\n   - Transformation des enums numériques et normalisation des relations.\n   - Ne jamais exposer directement l'entité TypeORM brute sur le bus gRPC.\n\n---\n\n## Étape 6 : Stratégie de Tests Obligatoires\n\nChaque nouveau RPC ou modification doit être couvert par des tests unitaires stricts :\n\n1. **Isolation des Mocks & Factories :**\n   - Mocks obligatoires dans un fichier dédié : `src\u002Fmodules\u002F\u003Cd>\u002F__tests__\u002F\u003Cnom>.mock.ts`.\n   - Données de test générées par factories dédiées : `src\u002Fmodules\u002F\u003Cd>\u002F__tests__\u002F\u003Cnom>.factory.ts`.\n   - Interdiction formelle de déclarer des objets `as unknown as Type` ou `any` inline dans les tests.\n2. **Utilisation rigoureuse de Spies :**\n   - Toujours espionner les appels de dépendances avec `jest.spyOn()`.\n   - Nettoyage systématique dans `afterEach(() => { jest.restoreAllMocks(); })`.\n3. **Cas de test obligatoires :**\n   - Cas nominal : renvoi du DTO de réponse avec mapping exact.\n   - Cas d'erreur métier : validation des exceptions typées (`@volontariapp\u002Ferrors` ou `@volontariapp\u002Ferrors-nest`).\n   - Cas de refus d'authentification : rejet si `x-internal-token` est manquant.\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 RPC et ses DTOs proviennent exclusivement de `@volontariapp\u002Fcontracts-nest` (aucun type inventé).\n- [ ] Aucun bout de logique métier n'a été inséré dans `src\u002F` (délégué à `domain-\u003Cdomaine>`).\n- [ ] Aucun cast `as unknown as Type` ni `any` n'est présent.\n- [ ] L'acteur est extrait via `@CurrentUser()`, jamais depuis le body.\n- [ ] Les mappers transforment proprement les entités en DTOs gRPC.\n- [ ] `yarn test` passe avec succès sur le microservice.\n- [ ] `yarn tsc` et `yarn lint` ne produisent aucune erreur.\n- [ ] En cas de migration de base de données associée : exécuter `yarn meta sync-migrations` si commune[^sync-mig].\n\n[^agent-md]: AGENT.md (structure de module, DTO, contrôleurs, tests)\n[^main-user]: Bootstrap hybride (gRPC + HTTP \u002Fhealth)\n[^app-event]: AppModule ms-event (GrpcValidationPipe, APP_GUARD)\n[^validation]: GrpcValidationPipe\n[^base-cmd]: BaseCommandController.withFallback\n[^sync-mig]: scripts\u002Fcli\u002Fcommands\u002Fsync-migrations.mjs\n",{"name":5,"tags":45,"type":49,"paths":50,"title":56,"status":57,"sources":58,"verified":82,"generated":99,"mesh_keys":102,"description":7},[19,21,46,37,47,48],"nestjs","cqrs","playbook","Playbook",[51,52,53,54,55],"ms-user\u002Fsrc\u002F**","ms-event\u002Fsrc\u002F**","ms-post\u002Fsrc\u002F**","ms-social\u002Fsrc\u002F**","scripts\u002Fcli\u002Fcommands\u002Fsync-migrations.mjs","Implémenter ou Modifier un RPC gRPC","stable",[59,63,67,71,75,78],{"id":60,"title":61,"resource":62},"agent-md","AGENT.md (structure de module, DTO, contrôleurs, tests)","AGENT.md",{"id":64,"title":65,"resource":66},"main-user","Bootstrap hybride (gRPC + HTTP \u002Fhealth)","ms-user\u002Fsrc\u002Fmain.ts",{"id":68,"title":69,"resource":70},"app-event","AppModule ms-event (GrpcValidationPipe, APP_GUARD)","ms-event\u002Fsrc\u002Fapp.module.ts",{"id":72,"title":73,"resource":74},"base-cmd","BaseCommandController.withFallback","ms-event\u002Fsrc\u002Fmodules\u002Fevent\u002Fcontrollers\u002Fcommands\u002Fbase.command.controller.ts",{"id":76,"title":77,"resource":55},"sync-mig","sync-migrations.mjs",{"id":79,"title":80,"resource":81},"validation","GrpcValidationPipe","npm-packages\u002Fpackages\u002Fvalidation-nest",[83,87,90,93,96],{"at":84,"by":85,"digest":86},"2026-10-30T12:29:16Z","claude-code\u002Fagent","37f4a83af8a6d3d1",{"at":88,"by":85,"digest":89},"2026-11-03T20:31:03Z","79cb461c249d3c2e",{"at":91,"by":85,"digest":92},"2026-11-04T17:53:23Z","a4e563bea3a17189",{"at":94,"by":85,"digest":95},"2026-11-06T14:04:01Z","90966abbcd8b04bd",{"at":97,"by":85,"digest":98},"2026-11-08T12:02:22Z","e1f929df232e29f5",{"at":100,"by":101},"2026-10-06T08:54:04Z","claude-code\u002Fclaude-opus-5-5",[103,104,105,106,107,108,109,110],"ms-user","ms-event","ms-post","ms-social","basecommandcontroller","grpcvalidationpipe","grpcinternalguard","transformer","6b8f03b57018fa66da4666071dc17c1577ff45e8",".agents\u002Fskills\u002Fvolontariapp-grpc-microservice","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":118,"prompt":128,"zipAvailable":129},[119,122,125],{"tool":120,"command":121},"npx","npx skills add Volontariapp\u002Fmeta --skill volontariapp-grpc-microservice",{"tool":123,"command":124},"bunx","bunx skills add Volontariapp\u002Fmeta --skill volontariapp-grpc-microservice",{"tool":126,"command":127},"pnpm","pnpm dlx skills add Volontariapp\u002Fmeta --skill volontariapp-grpc-microservice","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FVolontariapp\u002Fmeta\u002Fblob\u002Fmain\u002F.agents\u002Fskills\u002Fvolontariapp-grpc-microservice\u002FSKILL.md",true,[131,137,141,145,149,153,157,161,165],{"path":132,"resourceType":133,"resourceTypeDesc":134,"sizeBytes":135,"sourceHtmlUrl":136},"index.md","REFERENCE","参考文档",1998,"https:\u002F\u002Fgithub.com\u002FVolontariapp\u002Fmeta\u002Fblob\u002Fmain\u002F.agents\u002Fskills\u002Fvolontariapp-grpc-microservice\u002Findex.md",{"path":138,"resourceType":133,"resourceTypeDesc":134,"sizeBytes":139,"sourceHtmlUrl":140},"log.md",9889,"https:\u002F\u002Fgithub.com\u002FVolontariapp\u002Fmeta\u002Fblob\u002Fmain\u002F.agents\u002Fskills\u002Fvolontariapp-grpc-microservice\u002Flog.md",{"path":142,"resourceType":133,"resourceTypeDesc":134,"sizeBytes":143,"sourceHtmlUrl":144},"references\u002Flessons.md",11171,"https:\u002F\u002Fgithub.com\u002FVolontariapp\u002Fmeta\u002Fblob\u002Fmain\u002F.agents\u002Fskills\u002Fvolontariapp-grpc-microservice\u002Freferences\u002Flessons.md",{"path":146,"resourceType":133,"resourceTypeDesc":134,"sizeBytes":147,"sourceHtmlUrl":148},"references\u002Fmicroservice-architecture.md",4540,"https:\u002F\u002Fgithub.com\u002FVolontariapp\u002Fmeta\u002Fblob\u002Fmain\u002F.agents\u002Fskills\u002Fvolontariapp-grpc-microservice\u002Freferences\u002Fmicroservice-architecture.md",{"path":150,"resourceType":133,"resourceTypeDesc":134,"sizeBytes":151,"sourceHtmlUrl":152},"references\u002Fms-event.md",3925,"https:\u002F\u002Fgithub.com\u002FVolontariapp\u002Fmeta\u002Fblob\u002Fmain\u002F.agents\u002Fskills\u002Fvolontariapp-grpc-microservice\u002Freferences\u002Fms-event.md",{"path":154,"resourceType":133,"resourceTypeDesc":134,"sizeBytes":155,"sourceHtmlUrl":156},"references\u002Fms-post.md",2637,"https:\u002F\u002Fgithub.com\u002FVolontariapp\u002Fmeta\u002Fblob\u002Fmain\u002F.agents\u002Fskills\u002Fvolontariapp-grpc-microservice\u002Freferences\u002Fms-post.md",{"path":158,"resourceType":133,"resourceTypeDesc":134,"sizeBytes":159,"sourceHtmlUrl":160},"references\u002Fms-social.md",2785,"https:\u002F\u002Fgithub.com\u002FVolontariapp\u002Fmeta\u002Fblob\u002Fmain\u002F.agents\u002Fskills\u002Fvolontariapp-grpc-microservice\u002Freferences\u002Fms-social.md",{"path":162,"resourceType":133,"resourceTypeDesc":134,"sizeBytes":163,"sourceHtmlUrl":164},"references\u002Fms-user.md",4344,"https:\u002F\u002Fgithub.com\u002FVolontariapp\u002Fmeta\u002Fblob\u002Fmain\u002F.agents\u002Fskills\u002Fvolontariapp-grpc-microservice\u002Freferences\u002Fms-user.md",{"path":166,"resourceType":133,"resourceTypeDesc":134,"sizeBytes":167,"sourceHtmlUrl":168},"references\u002Fservices.md",4234,"https:\u002F\u002Fgithub.com\u002FVolontariapp\u002Fmeta\u002Fblob\u002Fmain\u002F.agents\u002Fskills\u002Fvolontariapp-grpc-microservice\u002Freferences\u002Fservices.md",[170,191,218,244,269,289],{"skillNo":171,"name":172,"nameZh":6,"description":173,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":174,"categories":175,"keywords":176,"publishedAt":40},"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",[],[177,178,179,180,181,182,183,184,27,34,185,186,187,188,24,189,190,32,30],"volontariapp user account deletion","user","account deletion","deletion","cycle","gather","sonnet","pending","claude-code","reference","npm","user-account-deletion","en","domain",{"skillNo":192,"name":193,"nameZh":6,"description":194,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":195,"categories":196,"keywords":202,"publishedAt":40},"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",[197],{"categoryNo":198,"categoryName":199,"taxonomyType":200,"taxonomyTypeDesc":201},"01JBSEEDCAT000000000000037","律师与法务","PROFESSION","适用职业",[203,204,21,205,34,206,207,30,208,209,210,211,212,187,213,214,215,216,48,217],"evolution","volontariapp proto contract evolution","lint","workflows","analyze","rpc","ci","pr","protobuf","message","registry","sans","sync","yaml","github",{"skillNo":219,"name":220,"nameZh":6,"description":221,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":222,"categories":223,"keywords":229,"publishedAt":40},"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",[224],{"categoryNo":225,"categoryName":226,"taxonomyType":227,"taxonomyTypeDesc":228},"01JBSEEDCAT000000000000023","Skill与智能体开发","SCENARIO","使用场景",[230,231,232,34,233,185,234,235,236,237,238,239,240,28,241,242,243],"Meta","volontariapp meta scripts","repos","migrate","setup","meta-scripts","json","node","workspace","shell","bump","doctor","draft","install",{"skillNo":245,"name":246,"nameZh":6,"description":247,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":248,"categories":249,"keywords":253,"publishedAt":40},"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",[250],{"categoryNo":251,"categoryName":252,"taxonomyType":227,"taxonomyTypeDesc":228},"01JBSEEDCAT000000000000009","前端开发",[254,255,256,257,258,259,24,260,261,213,262,263,32,264,265,30,266,267,34,268],"async","implement","volontariapp implement async event flow","flow","event","messaging","mapping","flux","streams","flows","shared","outbox","processor","architecture","runner",{"skillNo":270,"name":271,"nameZh":6,"description":272,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":273,"categories":274,"keywords":278,"publishedAt":40},"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",[275],{"categoryNo":276,"categoryName":277,"taxonomyType":227,"taxonomyTypeDesc":228},"01JBSEEDCAT000000000000021","资料检索与研究",[271,279,178,280,281,282,283,284,285,286,287,288],"api","file","repo","markdown","docs","capture","use","question","trust","investigate",{"skillNo":290,"name":291,"nameZh":6,"description":292,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":293,"categories":294,"keywords":298,"publishedAt":40},"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",[295],{"categoryNo":296,"categoryName":297,"taxonomyType":200,"taxonomyTypeDesc":201},"01JBSEEDCAT000000000000032","设计师",[291,299,178,300,285,301,286,302,303,304,305,306],"check","ui","look","design","explore","model","build","state"]