24 KiB
24 KiB
Ticket Executor - Learnings
Session 2026-03-17 (26 tickets)
T-001 — Secrets .env
- Pattern: Replace secrets with
change_me_in_env_localplaceholder, move real values to.env.local - Gotcha:
.env.localmust contain ALL overridden secrets
T-002 — Security API Gitea
- Pattern: Ajouter
security: "is_granted('ROLE_USER')"sur les opérations ApiResource - Learning: Vérifier d'abord les ressources déjà sécurisées pour ne pas dupliquer
T-003 — SVG Upload
- Pattern: Double protection - bloquer à l'upload (retirer du MIME allowlist) + defense-in-depth (Content-Disposition: attachment au download)
- Learning: Toujours vérifier upload ET download controllers
T-004 — MCP create-task / Repos numérotation
- Gotcha critique: PostgreSQL n'autorise PAS
FOR UPDATEavec des fonctions d'agrégation (MAX) - Fix: Utiliser
pg_advisory_xact_lock()au lieu deFOR UPDATEpour les queries avec agrégation - Pattern: Offset les lock keys (+1000000) pour éviter collisions entre Task et ClientTicket
T-005 — Filter ROLE_CLIENT projects
- Pattern: Créer une Doctrine Extension (
QueryCollectionExtensionInterface+QueryItemExtensionInterface) pour filtrer par relation - Learning: Symfony autoconfigure enregistre l'extension automatiquement
T-006 — Block client doc upload
- Pattern: Vérifier le rôle dans le Processor AVANT de résoudre l'IRI de la tâche
- Learning: Le portail client envoie un
clientTicketIRI (pas detaskIRI), donc le check surtaskIrinon-vide suffit
T-007 — MCP role checks
- Pattern: Injecter
Securitydans chaque Tool, vérifier au début de__invoke() - Learning: 22 tools à modifier - bien séparer ROLE_ADMIN (users/clients) vs ROLE_USER (le reste)
T-009 — Password hashing
- Pattern: Champ
plainPasswordnon-persisté, writable uniquement, hashé dans le Processor - Learning: Modifier aussi le frontend (DTO + composant) quand on renomme un champ API
T-010 — Rate limiting
- Gotcha:
login_throttlingnécessitesymfony/rate-limiterinstallé, pas juste dans composer.json - Learning: Toujours vérifier que les packages sont installés, pas juste déclarés
T-012 — Harmoniser repos numérotation
- Pattern: Aligner les contrats (retourner le max, pas le next) et mettre le +1 côté appelant
- Learning: Vérifier TOUS les appelants d'une méthode renommée
T-015 — useAvatarService
- Learning: Quand on migre vers
useApi(), ajouter la détection FormData pour ne pas écraser le Content-Type multipart
T-020 — i18n
- Pattern: Ajouter
useI18n()dans le setup script avant de pouvoir utilisert()dans le JS - Learning: Les templates peuvent utiliser
$t()directement sans import
T-022 — Retirer twig-bundle
- Pattern: Retirer de composer.json + bundles.php + supprimer config YAML + templates
- Learning: API Platform ne requiert PAS twig, c'est juste suggéré pour Swagger UI
Session 2026-06-19 (LST-56 / 0.1 — Socle back modular monolith)
Contexte
- Ticket exécuté via plan TDD dédié (
docs/superpowers/plans/2026-06-19-lst-56-socle-back.md) délégué à un sous-agent (contexte isolé), pilotage MCP/chrono/vérif depuis la session principale. - 4 tâches, 14 nouveaux tests (110 total, 216 assertions, vert), 4 commits (un par tâche).
Patterns
- Strangler 100 % additif : nouveau noyau
src/Shared/(Domain/Contract, Domain/Module, Domain/Sidebar, Domain/Trait, Application, Infrastructure/{ApiPlatform,Doctrine,Security,Database}) sans toucher au métier —make testreste vert sans migration. - Endpoints DTO purs : logique métier dans classes pures testées unitairement (
ModuleRegistry,SidebarFilter), exposées par Providers API Platform minces (ModulesProvider/SidebarProvider) sur des Resources DTO. - resolve_target_entities : contrat
Shared\Domain\Contract\UserInterfacemappé surApp\Entity\User(sera re-pointé versModule\Core\Useren 1.1). Inert tant qu'aucune entité n'utilise le trait.
Gotchas
- API Platform 4 découvre les Resources sous
src/Shared/...sans configmapping.paths— le 404 anticipé dans le plan ne s'est pas produit, aucun ajout dansapi_platform.yamlnécessaire. - Hook pre-commit php-cs-fixer normalise le style du code fourni dans le plan :
\DateTimeImmutable→DateTimeImmutableimporté, FQN→use,static::createClient()→self::. Pur style, tests inchangés. Ne pas lutter contre. config/reference.php: fichier auto-généré qui apparaît modifié dansgit status— ne jamais le committer.
Time tracking
- Le sous-agent a stoppé lui-même le timer d'implémentation (id 1005, 35 min) — garder le time-tracking sur la session principale pour rester maître du chrono si un sous-agent a accès aux tools MCP lesstime.
Session 2026-06-19 (LST-62 / 0.2 — Socle front : shell + auto-détection layers Nuxt)
Contexte
- Plan TDD dédié (
docs/superpowers/plans/2026-06-19-lst-62-socle-front.md), 7 tasks. Exécution en 3 sous-agents (Task 1 back ; Tasks 2-4 fondations front ; Tasks 5-7 middlewares/layout/i18n), pilotage chrono/MCP/vérif sur la session principale. - 7 commits + 1 commit doc de correction du plan. Back : 115 tests verts (110 + 5 nouveaux cas gate rôle).
Patterns
- Gate de rôle additif dans la sidebar : clé
rolesoptionnelle sur section/item dansconfig/sidebar.php;SidebarFilter::filter($sections, $activeModuleIds, $activeRoles = [])masque sans polluerdisabledRoutes(réservé au filtrage par module).SidebarProviderinjecteSymfony\Bundle\SecurityBundle\Securityet passearray_values($user->getRoles()). ROLE_ADMIN seulement (pas le RBAC fin, qui viendra en 1.1/1.2). - Layout front aligné Starseed (vérifié dans le code Starseed) :
srcDir: '.',dir.layouts/middleware → app/, code transverse auto-importé sousshared/{composables,stores,utils}viaimports.dirsEXPLICITE, scanreaddirSync('modules/')→extends+ dossiersmodules/*/composablesajoutés dynamiquement àimports.dirs.useApi/auth/uidéplacés pargit mv(historique préservé) ;timer.ts/mail.tsrestent dansstores/(métier non migré). - Singletons module-level :
useSidebar/useModulesportent leur état enrefau niveau module ; reset explicite au logout depuisauth.global.ts(l'approche Starseed via callbackonAuthSessionCleared()est une alternative non retenue ici).
Gotchas
nuxt typecheckn'est PAS un gate vert sur ce stack : le baseline Lesstime est rouge (~230 ligneserror TS) et la RÉFÉRENCE Starseed (même Nuxt 4.3.1, même layout) ship en prod avec 325 erreurs. Classes structurelles tolérées :Cannot find name 'ref'/'useApi'/'useRoute'/'navigateTo'/'defineStore'…dansshared/(Nuxt 4 typeshared/sous untsconfig.shared.jsonisolé sans les globals d'auto-import, alors queimports.dirsles expose au RUNTIME — vérifié dans.nuxt/imports.d.ts), erreursnuxt.config.ts(node:fs/process/__dirname, pas de@types/node, compilé au runtime par Nuxt),useApi.ts'Property url'. Le vrai gate = zéroCannot find module '~/shared/…'(= vrai import cassé) + auto-imports présents dans.nuxt/imports.d.ts+ smoke runtime. Un sous-agent consciencieux s'est arrêté à tort sur ces erreurs ("bloqueur irréductible") → toujours vérifier le gate contre la réf Starseed avant de conclure à un blocage.- Vérif backend live > typecheck front : le gate de rôle a été prouvé via curl réel (
/api/login_check→ cookie BEARER →GET /api/sidebar) :alice(ROLE_USER) n'a que la section générale,admin(ROLE_ADMIN) a Administration, non-auth = 401. Plus fiable que le typecheck sur ce stack. - i18n
fr.json: une clé racinesidebarpréexistait (avec unmyTasksorphelin) → fusionner les sous-namespaces plutôt que dupliquer la clé racine (JSON invalide sinon).
Statut / time tracking
- Ticket laissé en "En attente de validation" (4), pas "Terminé" : smoke visuel front (dev server + navigateur) et sign-off du délta cosmétique d'ordre de sidebar (décision 3 du plan) relèvent du PO. Implémentation + AC API validés.
- Time-tracking 100 % sur la session principale cette fois (consigne des sous-agents : ne jamais toucher aux outils
mcp__lesstime__*) — respecté.
Session 2026-06-19 (LST-63 / 1.1 — Module Core : identité User/Auth/JWT + Notifications + layer front)
Contexte
- Plan TDD dédié (
docs/superpowers/plans/2026-06-19-lst-63-module-core.md, 7 tasks / 6 phases A→F). Exécution : Phases A/B (1 sous-agent combiné), C (1 sous-agent), D (1 sous-agent), E + F faites en direct par la session principale (tâches courtes). Pilotage chrono/MCP/vérif + re-vérif login après chaque phase touchant l'auth sur la session principale. - 5 commits impl (
6ca91cbA,f8fc4d6+d70925bB,0b4874eC,f1a9b42D,a98ea3dE,117c2ffF) + plan8865bf5. Tests : 110→120 verts. Timer impl 1012 = 43 min.
Patterns
- Move d'entité « strangler » sans migration :
git mvsrc/Entity/User.php→src/Module/Core/Domain/Entity/User.php(table + colonnes + backticks VERBATIM) ; mapping DoctrineCoreajouté (dirsrc/Module/Core/Domain/Entity, prefixApp\Module\Core\Domain\Entity) à côté deApp;resolve_target_entities: UserInterface → Core\User.migrations:diffreste vide (hors dérive préexistantemessenger_messages) → AUCUNE migration. Idem Notification en Phase D. - Alias temporaire pour découpler le move des relations : Phase B pose un
class_alias(App\Entity\User::class → Core\User)(fichier_compat_user_alias.phpenautoload.files, exclu de l'autowiringApp\:viaexcludeservices.yaml +notPathphp-cs-fixer). Permet de relier d'abord les 8 relations d'entités au CONTRATUserInterface::class(resolver propre) ; l'alias n'est qu'un pont de type-hint PHP. Phase C retire l'alias EN DERNIER, seulement quandgrep App\Entity\Userest vide. - Règle contrat-vs-concret pour migrer les consommateurs (Phase C, ~50 fichiers) : type-hint
App\Shared\Domain\Contract\UserInterfacesi le fichier n'appelle que les méthodes de lecture du contrat / instanceof / type DQL ; FQCN concretApp\Module\Core\Domain\Entity\Usersi besoin de getters HR,apiToken,avatarFileName, setters,new User(). Les deux éliminentApp\Entity\User. Collision de nom avecSymfony\...\UserInterface→ aliaser enSharedUserInterface. - Notifier (Phase D) :
NotifierInterface(Shared) = API publique inter-modules ; implNotifier(Core) persiste + flush.TaskNotificationListenerappellenotify()UNIQUEMENT enpostFlush(jamaisonFlush— le flush interne y est dangereux). Comportement identique conservé. - Layer front d'un module (Phase F) :
frontend/modules/core/nuxt.config.ts(export default defineNuxtConfig({})) +git mvdes pages d'identité sousmodules/core/pages/. Les imports~/...(alias srcDir) survivent au déplacement ; seuls les imports relatifs/par chemin casseraient. Les URLs (/login,/profile) restent identiques (fusion auto despages/de layers).
Gotchas
admin.vue= shell admin MULTI-domaines (onglets clients/workflows/efforts/gitea/zimbra/mail/absences + 1 ongletAdminUserTab) : NE PAS le déplacer entier dans Core (il porterait les admins d'autres modules pas encore extraits). Conformément au plan, en cas de doute on déplace seulement login + profile, on documente. La décomposition deadmin.vueviendra avec les modules respectifs.- Vérifier la résolution des routes d'un layer Nuxt en SPA :
ssr:false→ le dev server renvoie 200 pour N'IMPORTE QUEL chemin (shell SPA, routing client) — uncurl /login= 200 ne prouve RIEN (testé :/route-bidon-xyz= 200 aussi).nuxt preparene génère pas le manifeste de routes. Preuve déterministe =npx nuxt buildpuisgrep 'name:"login"\|name:"profile"' .output/server/chunks/build/client.precomputed.mjs(+ chunk CSSprofile.*.cssgénéré). Ne pas perturber un dev server déjà lancé (configextends/imports.dirsfigée au démarrage avant création du layer) → lancer un dev frais sur un port libre pour smoke. - Aligner le contrat sur la réalité de l'entité, pas l'inverse :
User::getUsername()est?string(passtring) et la méthode réelle estgetIsEmployee(): bool(pasisEmployee()). Le plan écrivaitisEmployee()— le contrat existant était déjà correct, aucun changement. Toujours lire l'entité avant de figer une signature de contrat. - Tests fonctionnels qui persistent réellement (pas de rollback transactionnel ici) : un
NotifierTestqui crée une notif échoue au 2e run (2 != 1) → rendre les données uniques (uniqid()sur le titre) pour l'idempotence.
Session 2026-06-19 (LST-57 / 1.2 — RBAC fin : portage Starseed)
Contexte
- Plan TDD dédié (
docs/superpowers/plans/2026-06-19-lst-57-rbac-fin.md, 7 phases A→G). Source de vérité = implémentation RBAC de Starseed (le brief attaché au ticket était inaccessible en local — fichier non synchronisé sur le stockage ; cartographié via un agent Explore sur/home/matthieu/dev_malio/Starseed). 1 sous-agent par phase, pilotage chrono/MCP/vérif/push sur la session principale. - 7 commits impl (A
ffed224, Bac662e7, C5060fb6, D48c67a5, E1a9eba9, F544d4cf, G511353c) + planfdc7257. Tests 131→147 verts. Timer impl 1014.
Décision d'architecture majeure (actée, à valider PO)
- RBAC additif,
ROLE_ADMIN= bypass, PAS de colonneis_admin— divergence assumée vs Starseed (qui a supprimé la colonne JSONrolesau profit deis_admin). Lesstime garderolesJSON +getRoles()(login/JWT/MCP/sidebar #62 reposent dessus) ; lePermissionVoterbypass siin_array('ROLE_ADMIN', $user->getRoles()). Réécrire l'auth aurait été une régression à haut risque pour zéro bénéfice AC. Migration future versis_adminpossible.
Patterns
- RBAC = Role + Permission (M2M) + relations User :
Role(code snake_case immuable, label, description, isSystem, ManyToMany permissions EAGER),Permission(codemodule.resource.actionunique, label, module, orphan),UserreçoitrbacRoles(tableuser_role) +directPermissions(tableuser_permission),getEffectivePermissions()= union triée dédupliquée. Migration 100% additive (5 CREATE TABLE, zéro DROP/ALTER suruser). - Permissions déclaratives par module :
ModuleInterface::permissions(): list<array{code,label}>, agrégées parModuleRegistry::permissions($activeClasses)(injectemodule=id(), valide le préfixe).app:sync-permissionsupsert (revive orphan / updateMetadata / create) + markOrphan des absentes.app:seed-rbacseede les rôles système (admin/user, isSystem) — sans matrice métier tant qu'aucune permission métier n'existe (les modules 2.x ajouteront leurs permissions + rôles). - Voter pur + bypass applicatif :
PermissionVoter(regex/^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/poursupports, donc abstient surROLE_*/IS_AUTHENTICATED_*). Le bypass admin de la sidebar est dansSidebarProvider(si ROLE_ADMIN → injecte le catalogue completModuleRegistry::permissions()), pas dansSidebarFilterqui reste un filtre pur (permissionSatisfied()). Le seed n'attachant aucune permission, sans ce bypass l'admin ne verrait rien. - Front :
usePermissions()(can/canAny/canAll/isAdmin) dansmodules/core/composables/(auto-importé) ; typeUserDataenrichi deeffectivePermissions; ongletAdminRoleTab+RoleDrawerdansfrontend/components/admin/(le scancomponentsNuxt ne couvre que~/components, PAS les layersmodules/*→ les composants vont danscomponents/, le composable/services dansmodules/core/).
Gotchas
Symfony\Component\Serializer\Annotation\GroupsN'EXISTE PLUS en Symfony 8 — seulAttribute\Groupsexiste. Un importAnnotation\Groupsrend tous les#[Groups]no-op silencieux (sérialisation cassée, POST en 400 car le constructeur n'est pas alimenté). Bug latent introduit en Phase A, révélé seulement par les tests fonctionnels de Phase D (TDD). Toujours utiliserAttribute\Groups. Vérifier la cohérence sur TOUTES les entités.isSystemexposé sous la clésystem: PropertyInfo strippe le préfixeis. Mettre#[Groups]+#[SerializedName('isSystem')]sur le getter pour conserverisSystemcôté API.options: ['comment' => ...]sur les colonnes des entités : sans le mappingoptions.comment, lesCOMMENT ON COLUMNde la migration créent une dérivemigrations:diffperpétuelle (Doctrine veut les remettre à''). Aligner le mapping entité sur le COMMENT de la migration.make db-resetdétruitlesstime_test(docker compose down -vsupprime le volume) — les tests tournent sur la base suffixée_test. Après un db-reset, recréer la base de test :doctrine:database:create --env=test --if-not-exists+migrations:migrate -n --env=test+fixtures:load -n --env=test. Ne jamais lancermake db-resetdepuis un sous-agent de phase.- Signature
Voter::voteOnAttribute: la version Symfony installée imposevoteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool(4e param). Sans lui : « Declaration must be compatible » fatal.
MR / Git
- MR empilées sur Gitea (
tea pr create --base <branche-précédente>) reflètent la chaîne de dépendances (#56→develop, #62→#56, #63→#62, #57→#63) avec des diffs propres ; Gitea re-cible la base à chaque merge.tea prn'a pas d'edit→ pour sortir une MR du brouillon (retraitWIP:), PATCH API Gitea/repos/{o}/{r}/pulls/{n}avec le token de~/.config/tea/config.yml. - WIP en cours : pousser la branche d'un ticket en cours + ouvrir la MR en brouillon (titre
WIP:) sauvegarde le travail sans signaler « prêt à merger » ; re-pousser à chaque phase. Le push ne lock pas l'index → aucune contention avec un sous-agent qui committe en parallèle.
Meta-learnings
- Parallélisation: Les tickets touchant des fichiers indépendants peuvent tourner en parallèle sans problème
- Commits concurrents: NE PAS lancer deux sous-agents qui committent sur le même repo en parallèle (collision
.git/index.lock) — séquencer. - Gate de vérif fourni par le plan: si un plan fixe un seuil (ex "typecheck 0 erreur"), le confronter à la réalité du projet/réf AVANT de bloquer dessus ; corriger le plan si le seuil est faux.
- MCP status: Toujours mettre "En cours" AVANT de commencer, "Terminé" APRÈS validation
- PostgreSQL gotchas: Tester les queries SQL avec agrégation + locking sur PostgreSQL, pas MySQL
- Agents: Les agents simples (1-3 fichiers) terminent en ~30s, les complexes (22 fichiers) en ~8min
Session 2026-06-19 (LST-61 / 1.3 — Audit log : #[Auditable], audit_log, AuditListener, resource)
Contexte
- Plan TDD dédié (
docs/superpowers/plans/2026-06-19-lst-61-audit-log.md, Tasks A→F). Exécution : 1 sous-agent par task (A, B, C, D, E) en séquence, vérif + smoke par la session principale entre chaque ; Task F (validation finale + correctif front + learnings + push + statut) en direct. - Infra portée VERBATIM depuis Starseed (réf canonique
/home/matthieu/dev_malio/Starseed) :AuditListenerbyte-identique (diff -qOK), + 6 fichiers API (DTO/paginator/providers/resources) copiés tels quels — namespacesApp\Module\Core\...etApp\Shared\Domain\Attribute\...DÉJÀ alignés entre les deux projets, zéro adaptation. - 6 commits impl (
934cf08A,d8553f0B,8c3699aC,90b8ca1D,e7af415E,9b26b43fix front) + planfda03bd. Tests : 147→157 verts. Branchefeat/lst-61-audit-logempilée surfeat/lst-57-rbac-fin.
Patterns
- Audit en 4 couches additives : (1) marquage déclaratif
#[Auditable](TARGET_CLASS) /#[AuditIgnore](TARGET_PROPERTY) danssrc/Shared/Domain/Attribute/(Shared, pas Core → aucun module n'a de dépendance circulaire) ; (2) captureAuditListenerDoctrine suronFlush(litUnitOfWork: insertions/updates/deletions +getScheduledCollectionUpdates/Deletionspour le M2M) puispostFlush(écrit, swap-and-clear anti-réentrance) ; (3) écritureAuditLogWritersur connexion DBAL dédiéeaudit(hors transaction ORM → survit aux rollbacks) ; (4) lectureAuditLogProviderDBAL (pas d'entité ORM) +DbalPaginator implements PaginatorInterface(API Platform génèrehydra:viewseul). - Connexion DBAL dédiée +
schema_filter: restructurerdoctrine.yamlde connexion unique →connections: {default, audit}(même DSN),default_connection: default,schema_filter: '~^(?!audit_log$).+~'surdefault(la table n'a PAS d'entité → exclue demigrations:diff/schema:validate). Le blocormreste INCHANGÉ (l'EM par défaut se lie àdefault_connection). Enwhen@test, propagerdbname_suffixaux DEUX connexions (sinonauditécrit en base dev pendant que l'ORM écrit en test). - Table append-only hors ORM : créée par migration manuelle (squelette via
doctrine:migrations:generatepuis contenu écrit à la main — JAMAISmigrations:diff, qui ne voit pas la table).id uuidnatif PG,changes JSONB,performed_at TIMESTAMP(6) WITH TIME ZONE. UUID v7 (writer, tri monotone) / v4 (requestId par requête HTTP).entity_typeau formatmodule.Entity(regexApp\Module\<module>\...\<Entity>→core.User). - Marquage scope = entités migrées :
#[Auditable]posé sur User/Role/Permission (Core) uniquement ;#[AuditIgnore]surUser.passwordETUser.apiToken(Lesstime n'a pas deplainPassword). Défense en profondeur :AuditLogWriter::SENSITIVE_KEYSstrippe aussipassword/plainPassword/apiToken/token/secret. Les entités métier legacy (src/Entity/*) seront marquées à leur migration en modules (2.x).
Gotchas
- Tests fonctionnels Lesstime SANS rollback transactionnel (pas de DAMADoctrineTestBundle) : les entités persistées survivent d'un run à l'autre → violation d'unicité
username. Convention projet :uniqid()OU nettoyage explicite ensetUp()(DELETE FROM "user" WHERE username LIKE 'audit\_%'). Les données d'audit de test se seedent directement viadoctrine.dbal.audit_connection(DELETE + inserts UUID v7) pour du déterministe. migrations:diffgénère un fichier jetable même quand on ne veut que vérifier : toujours supprimer leVersion<ts>.phpnon suivi créé après un diff de contrôle (git ls-files --others migrations/). Une dérive préexistantemessenger_messages(DROP) pollue le diff — sans rapport, ne pas committer./audit-log-entity-types= ressource item unique, pas une collection :GetAPI Platform avecuriTemplatefixe sans{id}→ renvoie{ entityTypes: string[] }(PAS d'enveloppe hydramember). Le service front ne doit PAS passer parextractHydraMembersici (bug livré par le sous-agent E, corrigé en9b26b43)./audit-logsen revanche est bien une collection paginée hydra.- Login en curl =
/login_check(POST), pas/api/login; le JWT json_login est capricieux en curl pur (405/cookie). La preuve d'auth faisant autorité reste le test fonctionnel (clientloginUser()), pas un smoke curl.
Time-tracking / orchestration
- Interdire explicitement aux sous-agents de toucher au MCP lesstime (timer + statut ticket) : un sous-agent a spontanément créé/stoppé une time entry (1016) alors que le chrono est piloté par la session principale. Ajouter la consigne « NE TOUCHE PAS au time-tracking » dans chaque prompt de sous-agent. Pas de conflit ici (il avait stoppé l'actif avant), mais découpage involontaire.