Aller au contenu principal

ENG-001.4 — Territory Bounded Context

1. Métadonnées de mission

EpicEPIC-001 — Restructuration de DMV Core
MissionENG-001.4
TypeSpécification (documentaire uniquement)
PrioritéP0
DépendancesENG-001.2 — Contrats inter-contextes (PR-001, mergé) ; ENG-001.3 — Actor → Monetization (PR-002, mergé)
StatutSpécification produite — aucune implémentation
Code applicatifAucun changement (mission documentaire)
NumérotationENG-001.4 acté pour Territory Bounded Context depuis le réalignement du 2026-08-01 (voir ENG-001-3-implementation-report.md §9)

Cette spécification prépare la future PR d'implémentation. Elle ne modifie aucun code applicatif et ne tranche aucune décision de Niveau 3 au sens d'IA-GOUVERNANCE.md.


2. Objectif

Préparer la mise en conformité de Territory avec son ownership cible tel que défini par ADR-014, RFC-005 et RFC-001 : Territory ne possède que le territoire au sens strict, plus aucune écriture croisée vers acteurs (Actor) ni vers les données municipales (commune_elus, commune_collectes, commune_infos, futur Municipal Management).

Cette spécification ne crée pas Municipal Management. Elle prépare le terrain, dans la continuité directe de ce qu'ENG-001.2 a déjà acté pour ce contexte (Option B — contrats reportés, aucun binding vers un module inexistant).


3. Contexte

ADR-014 fixe l'ownership par Bounded Context et interdit l'écriture croisée. RFC-005 acte Territory comme propriétaire exclusif du territoire au sens strict et lui interdit toute logique métier municipale ou organisationnelle. RFC-001 acte la création future d'un Bounded Context Municipal Management, propriétaire des élus, collectes, infos pratiques, services municipaux et alertes, référençant Territory et Actor en lecture seule sans jamais les modifier.

ENG-001-1-dmv-core-audit-report.md (§4, §6, §10 — PR-004) et 28-dmv-core-bounded-context-map.md (§3, §5, §6) documentent déjà les écritures croisées de Territory vers acteurs et vers les données municipales, ainsi que la fragmentation du métier municipal entre Mairie et Territory. 30-dmv-core-migration-roadmap.md (Phase 3, Phase 4, Phase 5) planifie la clarification produit préalable, l'extraction de Municipal Management et la refonte du rafraîchissement SIRENE comme des chantiers distincts et séquencés.

Cette mission relit le code réel pour vérifier, préciser et compléter ces constats avant de proposer un périmètre de PR incrémental.


4. Architecture cible

Reprise sans modification d'ADR-014, RFC-005 et RFC-001 :

  • Territory possède l'identité territoriale : la commune comme territoire, le code INSEE, les codes postaux, le nom officiel, le slug territorial, les coordonnées géographiques, les référentiels territoriaux (défibrillateurs, snapshots SIRENE), les relations et découpages territoriaux.
  • Actor possède l'identité organisationnelle. Une mairie est un acteur. Territory ne possède jamais cette identité et ne doit jamais écrire dans acteurs.
  • Municipal Management (futur, non extrait) possédera élus, collectes, informations pratiques, services municipaux, alertes municipales. Il référencera Territory et Actor en lecture seule.
  • Aucun contrat ni binding ne doit être créé vers Municipal Management tant qu'il n'existe pas comme module — décision déjà actée par ENG-001.2 (Option B), non remise en question ici.

5. État réel observé

Analyse du code réel de api/app/Modules/Territory, api/app/Modules/Mairie, api/app/Modules/Actor et api/app/Modules/Admin, en lecture seule, à la date de cette spécification. Les citations fichier:ligne reflètent le code actuellement sur main (vérifiées directement, pas recopiées depuis un audit antérieur — certaines lignes ont bougé depuis ENG-001-1-dmv-core-audit-report.md).

5.1 Cartographie de Territory

ÉlémentRôle observéOwner cibleConformeAction future probable
Models/Commune.phpModèle Eloquent communes, docblock indique explicitement « lecture seule — pas d'écriture via Laravel »TerritoryNon — le docblock ne reflète pas la réalité (voir 5.3)Aucune action sur le modèle lui-même ; clarifier les écritures qui le contournent
Models/CommuneElu.php, CommuneCollecte.php, CommuneInfo.phpModèles Eloquent, utilisés uniquement en lecture (TerritoryService, MairieReadService)Municipal Management (cible)Ownership de données non conforme, modèles eux-mêmes sainsÀ déplacer vers Municipal Management lors de son extraction (RFC-001 étape 2)
Models/CommuneSireneSnapshot.php, CommuneDefibrillateur(Sync).php, DefibrillateurCatalogue(Sync).phpRéférentiels externes, lus et écrits uniquement par TerritoryTerritoryOuiAucune
Controllers/TerritoryController.phpEndpoints publics en lecture (/communes, /communes/{id}/elus|collectes|infos|defibrillateurs, /defibrillateurs)Territory (adaptateur HTTP)Oui — délègue intégralement à TerritoryService/CommuneDefibrillateurService/DefibrillateurCatalogueServiceAucune
Contracts/TerritoryReader.phpContrat public introduit par ENG-001.2, une seule méthode (getCommune)TerritoryOui, mais incomplet pour les besoins actuels (voir 11.D)Étendre si un arbitrage le confirme
Services/TerritoryService.phpImplémente TerritoryReader. Toutes les opérations documentées comme lecture seuleTerritoryPartiel — getCommuneInfos() lit acteurs directement en DB::table() (TerritoryService.php:117) pour fusionner les coordonnées de contact de l'acteur mairie dans la réponse publiqueRemplacer la lecture directe par ActorReader (lecture seule, faible risque)
Services/CommuneMairieDataRefreshService.phpOrchestration du rafraîchissement SIRENE (refresh() et confirm()) : résout l'acteur mairie, appelle l'API SIRENE, écrit acteurs, communes, commune_elus, commune_infos, commune_sirene_snapshotsMixte — orchestration Territory légitime, écritures acteurs/municipales illégitimesNon — voir §6Cœur du chantier de cette spécification (§12)
Services/CommuneMairieResolverService.phpRésolution automatique heuristique de l'acteur mairie d'une commune (2 passes), écrit communes.mairie_actor_id, lit acteurs en DB::table() directTerritory pour l'écriture (table propre) ; lecture croisée à clarifierPartielRemplacer la lecture par ActorReader
Services/CommuneMairieDetectService.phpRecherche de candidats mairie dans SIRENE pour l'UI d'administration, lecture seule (aucune écriture)TerritoryOui pour l'écriture (aucune) ; lecture croisée acteurs à clarifierRemplacer la lecture par ActorReader
Services/SireneApiClient.php, SireneKindDetector.phpAdaptateur externe (appel HTTP SIRENE) et classification pure (aucune donnée DMV)Territory (adaptateur d'intégration)OuiAucune
Services/CommuneDefibrillateurService.php, DefibrillateurCatalogueService.php, GeodaeDefibrillateurSourceService.phpRéférentiel défibrillateurs, lecture/écriture strictement internes à TerritoryTerritoryOuiAucune
app/Console/Commands/RefreshMairieSireneCommand.phpCommande Artisan (communes:refresh-mairie-sirene) appelant CommuneMairieDataRefreshService::refresh() en boucle sur les communes liéesTerritory (déclencheur)Hérite du statut de refresh()Aucun scheduler trouvé dans le dépôt (routes/console.php absent) — déclenchement externe (cron serveur) non vérifiable depuis le code
app/Console/Commands/ResolveMairiesCommand.phpCommande Artisan (communes:resolve-mairies) appelant CommuneMairieResolverService::resolve()Territory (déclencheur)Hérite du statut de resolve()Idem

5.2 Cartographie de Mairie (pertinente pour Territory)

Mairie reste hors périmètre de correction de cette mission (RFC-005/RFC-001 le traitent par une extraction ultérieure), mais ses écritures sur des tables potentiellement Territory/Municipal Management sont cartographiées ici car elles conditionnent le périmètre de la future PR.

ÉlémentÉcritureTableOwner cible
MairieWriteService::updateCommune() (MairieWriteService.php:216-229)DB::table('communes')->update() sur description, image_url uniquementcommunesÀ arbitrer (11.B)
MairieWriteService::createElu/updateElu/deleteElu() (MairieWriteService.php:233-269)CRUD DB::table() brut, sans passer par le modèle Territory\Models\CommuneElucommune_elusMunicipal Management (cible)
MairieWriteService::createCollecte/updateCollecte/deleteCollecte() (MairieWriteService.php:273-309)Idemcommune_collectesMunicipal Management (cible)
MairieWriteService::createInfo/updateInfo/deleteInfo() (MairieWriteService.php:313-351)Idemcommune_infosMunicipal Management (cible)
MairieWriteService::getCommuneIdForActeur() (MairieWriteService.php:364)Lecture directe DB::table('acteurs')acteursLecture croisée à clarifier (faible priorité, hors périmètre)

5.3 Cartographie d'Admin (pertinente pour Territory) — écart avec l'audit antérieur

Constat non documenté par ENG-001-1 ni par 28-dmv-core-bounded-context-map.md : Admin ne se limite pas à modifier communes — il possède un quatrième chemin d'écriture complet et autonome sur les données municipales, en plus de Mairie et des deux entrées de Territory (refresh()/confirm()), portant le total à quatre chemins de code distincts sur les mêmes tables plutôt que les trois déjà documentés :

ÉlémentÉcritureTableConstat
AdminCommuneService::createCommune/updateCommune/setMairie/unsetMairie() (AdminCommuneService.php:32,84,99,124)DB::table('communes')communesupdateCommune() autorise description, image_url, mais aussi site_web, email_contact, telephone, nom, code_postal, code_insee, slug, active, latitude, longitude, population — un superset bien plus large que ce que Mairie autorise
AdminCommuneInfoController::storeInfo/updateInfo/destroyInfo() (AdminCommuneInfoController.php:154-230)CRUD complet DB::table()commune_infosChemin d'écriture entièrement séparé de MairieWriteService et de CommuneMairieDataRefreshService
AdminCommuneInfoController::storeElu/updateElu/destroyElu() (AdminCommuneInfoController.php:234-304)CRUD completcommune_elusIdem
AdminCommuneInfoController::storeCollecte/updateCollecte/destroyCollecte() (AdminCommuneInfoController.php:308-380)CRUD completcommune_collectesIdem — Mairie et Admin sont les deux seuls écrivains de commune_collectes ; Territory n'y écrit jamais (confirmé, absent de CommuneMairieDataRefreshService)
AdminCommuneInfoController::indexSections/storeSection/updateSection/destroySection() (AdminCommuneInfoController.php:22-96)CRUD completcommune_info_sectionsTable entièrement absente de la mission, de l'audit ENG-001.1 et de la cartographie doc 28. Catalogue de rubriques (slug, label, icone, description, ordre, active) consommé par commune_infos.section. Propriétaire exclusif aujourd'hui : Admin. Voir 11.C.

Ce constat élargit — sans le remettre en cause — le diagnostic déjà connu : « aucune situation n'est acceptable temporairement » (doc 28 §6) reste vrai, mais le nombre réel de chemins d'écriture concurrents sur commune_elus/commune_infos est de trois (Mairie, Admin, Territory via refresh()+confirm() qui partagent le même code refreshElus()/refreshInfos()) et sur commune_collectes de deux (Mairie, Admin — Territory n'y touche jamais). La table commune_info_sections constitue une découverte supplémentaire, non couverte par le périmètre initial de cette mission.

5.4 Résolution commune ↔ acteur mairie — quatre implémentations indépendantes confirmées

Le constat de la mission (« dupliquée dans plusieurs endroits : Identity, Territory, middleware Mairie ») est confirmé et précisé : il y a en réalité quatre points de résolution indépendants, dans trois modules :

ImplémentationFichierDirection résolueMécanisme
ProfileService::resolveManagedCommunes()Identity/Services/ProfileService.php:128 et suivantesutilisateur → communes géréesJointure users_roles (scope explicite), fallback profiles.commune_id
CommuneMairieResolverService::resolve()Territory/Services/CommuneMairieResolverService.php:43-127commune → acteur mairie (détection automatique)Heuristique 2 passes sur acteurs (kind puis nom), écrit communes.mairie_actor_id
EnsureCommuneManagerMairie/Middleware/EnsureCommuneManager.php:77-84commune → acteur mairie (autorisation)Lecture directe communes.mairie_actor_id puis vérification acteurs.kind
EnsureMairieAccess::resolveActeurId()Mairie/Middleware/EnsureMairieAccess.php:78-95ressource (alerte/service/publication) → acteur mairieRésolution ad hoc selon le paramètre de route

Admin lit également communes.mairie_actor_id directement à trois endroits (AdminCommuneService::getMairie/setMairie/unsetMairie, AdminCommuneInfoController::fullCommune) sans passer par un service partagé.


6. Violations concernées

#ViolationFichier:ligne (vérifié)Gravité
V1Territory écrit acteurs.kind, acteurs.categorie_idCommuneMairieDataRefreshService.php:88-90 (refresh) et :148-152 (confirm)P0 — écriture croisée directe
V2Territory écrit acteurs.numero_siretCommuneMairieDataRefreshService.php:158 (confirm)P0 — écriture croisée directe
V3Territory écrit commune_elus (données Municipal Management)CommuneMairieDataRefreshService.php:232-236,246-248,260-269 (refreshElus(), appelée par refresh() et confirm())P0 — mauvais owner
V4Territory écrit commune_infos (données Municipal Management)CommuneMairieDataRefreshService.php:312-333 (upsertInfo(), appelée par refreshInfos())P0 — mauvais owner
V5Territory lit acteurs en accès direct (hors contrat)CommuneMairieDataRefreshService.php:57-59,133,184-187, CommuneMairieResolverService.php:83-88,102-110, CommuneMairieDetectService.php:41-46,123-135, TerritoryService.php:117P1 — lecture croisée, contrat ActorReader existant mais incomplet pour ces besoins
V6Mairie écrit communes.description/image_urlMairieWriteService.php:216-229P0 — ownership non tranché (11.B)
V7Mairie écrit commune_elus/commune_collectes/commune_infos en SQL brut, en parallèle de deux autres chemins d'écritureMairieWriteService.php:233-351P0 — déjà connu, hors périmètre de correction directe (Mairie n'est pas cette mission)
V8Admin écrit communes avec un périmètre de champs plus large que MairieAdminCommuneService.php:32,84,99,124P1 — à documenter, correction non exigée par cette PR
V9Admin possède un chemin d'écriture complet et autonome sur commune_elus/commune_collectes/commune_infos/commune_info_sectionsAdminCommuneInfoController.php:22-380P1 — à documenter (découverte, §5.3), correction non exigée par cette PR
V10Résolution commune ↔ acteur mairie dupliquée quatre fois§5.4P1 — ambiguïté architecturale, pas une écriture croisée au sens strict

7. Ownership des données

Donnée / table / champOwner actuelOwner cibleStatutDécision requise
communes (identité, code INSEE, codes postaux, géographie, slug, active, deleted_at)Territory (lecture), écritures dispersées (Admin, Mairie, Territory)TerritoryNon conforme (écritures dispersées, pas un problème d'ownership de la donnée elle-même)Non
commune_sirene_snapshotsTerritoryTerritoryConformeNon
commune_defibrillateurs, defibrillateurs_catalogue (+ Sync)TerritoryTerritoryConformeNon
commune_elusFragmenté : Mairie, Admin, Territory (refresh()/confirm())Municipal ManagementNon conformeNon sur l'ownership (déjà tranché par RFC-001) — Oui sur le calendrier de migration (hors périmètre de cette PR)
commune_collectesFragmenté : Mairie, AdminMunicipal ManagementNon conformeIdem
commune_infosFragmenté : Mairie, Admin, Territory (refresh()/confirm())Municipal ManagementNon conformeIdem
commune_info_sections (découverte §5.3)Admin exclusivementÀ arbitrerNon classé par la mission initialeOui — nouveau point, voir 11.C
acteurs.kind, acteurs.categorie_id, acteurs.numero_siret (écrits depuis Territory)Territory (à tort)ActorNon conformeNon sur l'ownership — Oui sur le mécanisme de correction (11.F)
communes.mairie_actor_id (la colonne)TerritoryTerritoryConforme (table propre)Non
Relation commune ↔ acteur mairie (le service de résolution, pas la colonne)Dupliqué × 4 (Identity, Territory, Mairie, Admin)Non tranchéAmbiguïté architecturaleOui, voir 11.A
communes.description, communes.image_urlTerritory (colonne), écrit par Mairie et AdminNon tranchéDéjà signalé bloquant pour TerritoryWriter par ENG-001.2Oui, voir 11.B
communes.site_web, communes.email_contact, communes.telephone (découverte)Territory (colonne), écrit par Admin uniquement (AdminCommuneService::updateCommune)Non tranchéMême ambiguïté que description/image_url, non mentionnée par la mission initialeOui, voir 11.B (étendu)

8. Périmètre inclus

Cette spécification définit le périmètre recommandé pour la future PR d'implémentation ENG-001.4. Aucune implémentation n'est réalisée ici.

  • Supprimer les écritures directes Territory → acteurs (V1, V2) en réutilisant le contrat ActorWriter::updateActeur() déjà validé par ENG-001.2/PR-001 — sous réserve de l'arbitrage 11.F sur le risque d'effet de bord (récompenses).
  • Remplacer les lectures directes Territory → acteurs (V5) par ActorReader — sous réserve de l'arbitrage 11.D sur l'extension nécessaire du contrat.
  • Introduire un garde-fou (test architectural et/ou revue) empêchant Territory de créer de nouvelles écritures sur commune_elus/commune_infos/commune_collectes, sans déplacer les écritures existantes (V3, V4 restent non résolues tant que Municipal Management n'existe pas).
  • Documenter, sans les corriger, les dépendances Admin → Territory (V8, V9) et Mairie → Territory (V6, V7).
  • Documenter, sans les trancher, les points d'arbitrage 11.A à 11.F.

9. Hors périmètre

  • Création du module Municipal Management (RFC-001 étape 1 — mission future).
  • Déplacement physique de commune_elus, commune_collectes, commune_infos, commune_info_sections vers Municipal Management.
  • Toute migration SQL, y compris de renommage ou de déplacement de colonne.
  • Refonte de Mairie (V6, V7 restent en l'état).
  • Correction des écritures Admin (V8, V9) — documentées uniquement.
  • Tranchage de l'ownership de communes.description, image_url, site_web, email_contact, telephone.
  • Tranchage de l'ownership de commune_info_sections.
  • Tranchage de l'ownership du service de résolution commune ↔ acteur mairie.
  • Introduction d'un Process Manager ou d'événements métier pour le rafraîchissement SIRENE — sauf validation explicite de l'architecte sur l'arbitrage proposé en §12.4.
  • Toute nouvelle API publique.
  • Changement de comportement observable pour les utilisateurs finaux ou pour le backoffice.

10. Décisions déjà validées

Reprises telles quelles, non remises en question par cette spécification :

  • Territory possède le territoire au sens strict (identité territoriale, commune, code INSEE, codes postaux, nom officiel, slug territorial, coordonnées géographiques, référentiels et découpages territoriaux, données stables) — RFC-005, mission.
  • Actor possède les organisations ; une mairie est un acteur ; Territory ne doit jamais écrire dans acteurs — mission, ADR-014.
  • Municipal Management possédera élus, collectes, informations pratiques, services municipaux, alertes municipales, et n'existe pas encore comme module — RFC-001, mission, ENG-001.2.
  • Aucun contrat ni binding ne doit être créé vers Municipal Management avant son extraction réelle (Option B, actée par ENG-001.2).
  • La future PR ENG-001.4 doit rester incrémentale (pas de nouveau module, pas de migration de données, pas de changement d'API, pas de changement fonctionnel) — mission.

11. Décisions restant à arbitrer

Conformément à IA-GOUVERNANCE.md, chaque point ci-dessous est présenté avec constat, options, avantages, inconvénients, recommandation et décision attendue. Aucun n'est tranché par cette spécification.

Décision architecturale requise — 11.A : ownership du service de résolution commune ↔ acteur mairie

Constat. Quatre implémentations indépendantes résolvent cette relation dans des directions et avec des mécanismes différents (§5.4), sans qu'aucun contrat partagé n'existe. La mission demande explicitement de signaler ce point sans trancher entre Actor, Territory ou futur Municipal Management.

Options.

  1. Territory reste propriétaire de la résolution (statu quo partiel). Avantages : la colonne communes.mairie_actor_id vit déjà dans communes ; aucun déplacement de donnée. Inconvénients : Territory devrait alors exposer un contrat de résolution consommé par Identity, Mairie et Admin, ce qui étend son périmètre de lecture d'Actor bien au-delà de ActorReader actuel.
  2. Actor devient propriétaire de la résolution (« quel est mon acteur mairie pour cette commune » devient une capacité Actor). Avantages : cohérent avec « Actor possède les organisations » ; Territory reste un pur référentiel. Inconvénients : Actor devrait alors lire communes (relation inverse), création d'une dépendance Actor → Territory qui n'existe pas aujourd'hui.
  3. La résolution devient une capacité du futur Municipal Management, qui référence les deux en lecture seule (cohérent avec RFC-001 « Municipal Management référence Territory et Actor »). Avantages : aligné avec la cible long terme, évite de faire porter cette responsabilité à un Bounded Context qui n'en a pas vocation. Inconvénients : Municipal Management n'existe pas encore — ce choix reporte nécessairement toute correction de V10 après son extraction.

Recommandation. Option 3, par cohérence avec RFC-001 et pour éviter de créer un contrat de résolution dans Territory ou Actor qui devrait être défait dès l'extraction de Municipal Management. Dans l'intervalle, ne pas consolider les quatre implémentations — les documenter comme dette connue (déjà fait ici) plutôt que de les fusionner prématurément dans le mauvais contexte.

Décision attendue. Confirmation du choix d'option avant toute tentative de consolidation, même partielle.


Décision architecturale requise — 11.B : ownership de communes.description, image_url, site_web, email_contact, telephone

Constat. communes.description et communes.image_url sont déjà signalés bloquants pour la création d'un TerritoryWriter par ENG-001-2-implementation-report.md (« Aucun contrat ne doit être créé avant décision produit sur l'ownership de communes.description et communes.image_url »). L'analyse du code réel étend ce constat : AdminCommuneService::updateCommune() autorise également site_web, email_contact, telephone dans son payload — trois champs présents sur le modèle Commune (Commune.php, docblock) mais jamais mentionnés par la mission ni par les documents antérieurs. Ces cinq champs posent la même question : sont-ils une donnée propre à la commune en tant qu'entité administrative (Territory), ou un doublon du profil de l'acteur mairie (Actor) qui devrait être supprimé au profit d'une lecture croisée ?

Options.

  1. Ces champs restent la propriété de Territory, avec un futur TerritoryWriter explicite. Avantages : aucune migration, périmètre inchangé. Inconvénients : risque de duplication permanente avec les champs équivalents de acteurs (adresse, téléphone, email, site_web — déjà utilisés par TerritoryService::getCommuneInfos() pour remplacer ces mêmes données côté commune_infos, ce qui suggère que la duplication est déjà perçue comme un problème par le code existant).
  2. Ces champs sont supprimés au profit d'une lecture croisée vers l'acteur mairie (Actor reste seul propriétaire de ses coordonnées de contact). Avantages : élimine la duplication constatée en 5.1 (TerritoryService.php:117-153). Inconvénients : migration de données et changement de comportement pour les communes sans acteur mairie lié — hors périmètre de cette PR par construction.
  3. Statu quo documenté, décision différée à une mission produit dédiée. Avantages : n'engage rien, cohérent avec le périmètre incrémental demandé. Inconvénients : la violation V6 (Mairie écrit ces champs) et V8 (Admin écrit un superset plus large) restent non corrigées.

Recommandation. Option 3 pour cette PR — la question est produit, pas seulement technique (cf. duplication déjà visible dans getCommuneInfos()), et sa résolution engagerait une migration de données explicitement hors périmètre.

Décision attendue. Confirmation qu'aucun TerritoryWriter ne doit être créé pour ces cinq champs tant que cet arbitrage n'est pas rendu, y compris pour les trois champs nouvellement identifiés (site_web, email_contact, telephone).


Décision architecturale requise — 11.C : ownership de commune_info_sections

Constat. Table découverte lors de l'analyse du code réel (§5.3), absente de la mission, de ENG-001-1-dmv-core-audit-report.md et de 28-dmv-core-bounded-context-map.md. C'est un catalogue de rubriques (slug, label, icone, ordre, active) consommé par commune_infos.section, géré exclusivement par Admin (AdminCommuneInfoController), sans aucun lien avec Territory ou Mairie dans le code actuel.

Options.

  1. Rattacher commune_info_sections à Municipal Management au même titre que commune_infos, dont elle est la taxonomie. Avantages : cohérent, une seule migration. Inconvénients : aucun aujourd'hui.
  2. La classer comme donnée de configuration transverse (proche de Settings), au motif que c'est un catalogue générique plutôt qu'une donnée municipale à proprement parler. Avantages : évite de surcharger Municipal Management d'une responsabilité de configuration. Inconvénients : introduit un troisième type de propriétaire pour un ensemble de données très proche de commune_infos.

Recommandation. Option 1, par simplicité et cohérence avec la donnée qu'elle catalogue — mais ce n'est qu'une observation, pas une décision : la mission n'ayant pas anticipé cette table, elle doit être explicitement validée avant d'être intégrée à la classification cible.

Décision attendue. Confirmation de l'owner cible de commune_info_sections, à défaut de quoi cette PR la documente comme table non classée plutôt que de lui assigner un owner par défaut.


Décision architecturale requise — 11.D : extension de ActorReader — RÉSOLU

Résolu par arbitrage architecte (Arbitrage 2). ActorReader étendu de findById(), findBySiret(), findBySiren(), retournant ?ActeurDTO. Signatures effectivement implémentées, voir ENG-001-4-implementation-report.md §3, §10. Les deux lectures fondées sur commune_id (CommuneMairieResolverService) restent hors périmètre (Arbitrage 3, voir 11.A) et sont documentées comme reportées dans le rapport §11.

Constat. Le contrat ActorReader (ENG-001.2) n'expose que getActeur(string $slug), getActeurXp() et isOwner(). Les besoins de lecture identifiés dans Territory (V5) sont différents : recherche par commune_id + kind (CommuneMairieResolverService), recherche par identifiant UUID (CommuneMairieDataRefreshService::findMairieActor(), TerritoryService::getCommuneInfos()), recherche par siret/siren (CommuneMairieDetectService::findMatchingActeur()). Aucune de ces signatures n'existe aujourd'hui sur ActorReader.

Options.

  1. Étendre ActorReader avec les méthodes de lecture nécessaires (ex. getActeurById, findByCommuneAndKind, findBySiret). Avantages : cohérent avec la trajectoire de contractualisation déjà engagée. Inconvénients : modification d'un contrat public déjà validé — relève explicitement de la gouvernance (« la création d'un nouveau contrat » et, par extension, l'ajout de méthodes à un contrat existant, engagent une décision de Niveau 3).
  2. Ne pas toucher au contrat pour cette PR, et se limiter à corriger uniquement les écritures (V1-V4), en laissant les lectures croisées (V5) en l'état. Avantages : périmètre plus étroit, aucun risque de contrat mal dimensionné. Inconvénients : le point « clarifier les read paths », explicitement cité par la mission comme correctible immédiatement, resterait non traité.

Recommandation. Option 1, mais avec des signatures proposées à valider explicitement — ne pas les considérer comme actées du seul fait qu'elles apparaissent dans cette spécification.

Décision attendue. Validation (ou amendement) des signatures à ajouter à ActorReader avant implémentation.


Décision architecturale requise — 11.E : architecture cible du rafraîchissement SIRENE

Constat. CommuneMairieDataRefreshService mélange aujourd'hui, dans les deux mêmes méthodes (refresh(), confirm()) : récupération externe (déléguée à SireneApiClient), transformation (déléguée à SireneKindDetector et aux fonctions internes de normalisation), mise à jour du territoire (communes.mairie_actor_id, commune_sirene_snapshots — légitime), mise à jour de l'acteur mairie (acteurs.kind/categorie_id/numero_siret — violation V1/V2) et mise à jour de données municipales (commune_elus, commune_infos — violation V3/V4). 30-dmv-core-migration-roadmap.md Phase 5 traite ce chantier séparément et le conditionne à l'existence de Municipal Management.

Options.

  1. Événement métier CommuneSireneRefreshed, consommé par Actor et par le futur Municipal Management (aligné sur doc 30 Phase 5.1/5.2, ADR-014 principe 5). Avantages : cible finale correcte, découplage complet. Inconvénients : nécessite un mécanisme de dispatch d'événements non encore introduit dans le code (Phase 1.3 de la roadmap non réalisée) ; casserait le comportement synchrone actuel où refresh()/confirm() retournent immédiatement le résultat (élus ajoutés/mis à jour/désactivés, infos mises à jour) dans la réponse HTTP admin (AdminCommuneController::mairieConfirm/refreshMairieData) — changement fonctionnel non permis par le périmètre de cette PR.
  2. Correction incrémentale et synchrone : Territory continue d'orchestrer refresh()/confirm() de façon synchrone, délègue la partie Actor à ActorWriter::updateActeur() (contrat déjà validé), et continue temporairement d'écrire commune_elus/commune_infos en direct tant que Municipal Management n'existe pas — exactement la position déjà actée par ENG-001.2 Option B pour Municipal Management. Avantages : strictement incrémental, ne casse rien, réutilise un contrat existant, cohérent avec la décision déjà prise. Inconvénients : ne corrige que V1/V2, laisse V3/V4 ouvertes jusqu'à l'extraction de Municipal Management.
  3. Process Manager explicite nommé dès maintenant, orchestrant Territory (fetch/transform/ snapshot), Actor et un adaptateur temporaire documenté vers les données municipales (Option A d'ENG-001.2, plutôt que l'Option B retenue). Avantages : nomme la responsabilité d'orchestration conformément à ADR-014 principe 3, prépare Phase 5. Inconvénients : introduit une abstraction nouvelle (Process Manager) non présente dans le code, pour un bénéfice incertain si Municipal Management est extrait prochainement — risque de sur-ingénierie que ENG-001.2 a explicitement cherché à éviter pour Municipal Management.

Recommandation. Option 2, strictement pour cette PR : elle seule respecte à la fois le périmètre incrémental demandé, l'absence de changement fonctionnel, et la cohérence avec la décision Option B déjà actée par l'architecte en ENG-001.2. Les options 1 et 3 restent pertinentes comme cible ultérieure (Phase 5), pas comme périmètre de cette PR.

Décision attendue. Confirmation de l'option 2 comme périmètre de la future PR, ou arbitrage contraire de l'architecte.


Décision architecturale requise — 11.F : risque comportemental de la réutilisation d'ActorWriter::updateActeur() — RÉSOLU

Résolu par arbitrage architecte (Arbitrage 1, option 2 retenue). Ajout d'une méthode dédiée ActorWriter::applySireneRefresh(string $actorId, ActeurSireneDataDTO $data): void, sans les effets de bord de updateActeur(). Implémentée et testée (aucune ligne actor_activity_events, aucun appel Rewards — 4 tests dédiés), voir ENG-001-4-implementation-report.md §3, §8.

Constat. ActorWriter::updateActeur(string $acteurId, array $data): ActeurDTO, tel qu'implémenté par ActeurWriteService::updateActeur(), accepterait aujourd'hui kind, categorie_id et numero_siret sans les bloquer (ils n'apparaissent pas dans CHAMPS_INTERDITS). Réutiliser cette méthode pour corriger V1/V2 est donc techniquement possible sans étendre le contrat. Mais updateActeur() déclenche aussi, comme effet de bord, ActorXpService::award() et RewardEngineService::recordEvent() si la description de l'acteur passe de vide à renseignée, et un événement profile_completed si nom + description + commune_id sont tous renseignés après la mise à jour. Le rafraîchissement SIRENE automatisé (refresh(), déclenché par cron) n'envoie jamais description, donc le premier effet de bord ne se déclenchera pas par ce chemin — mais le second (profile_completed) pourrait se déclencher de façon inattendue si un acteur mairie remplit par ailleurs les conditions au moment précis du rafraîchissement automatisé, ce qui ne se produit jamais aujourd'hui (l'écriture actuelle est un DB::table('acteurs')->update() brut, sans aucun effet de bord).

Options.

  1. Réutiliser ActorWriter::updateActeur() tel quel, en couvrant le risque par un test dédié (« le rafraîchissement SIRENE ne déclenche aucun événement Rewards inattendu »). Avantages : zéro nouvelle surface de contrat. Inconvénients : dépend d'un test pour garantir l'absence de changement fonctionnel plutôt que d'une garantie structurelle.
  2. Ajouter une méthode dédiée, plus étroite, sans les effets de bord de mise à jour de profil (ex. ActorWriter::updateActeurFields(string $acteurId, array $fields): void, limitée aux champs techniques). Avantages : élimine le risque à la racine, signature honnête sur son usage. Inconvénients : nouvelle méthode sur un contrat public — même remarque de gouvernance que 11.D.

Recommandation. Aucune recommandation ferme : les deux options sont défendables et le choix dépend de l'appétit pour le risque plutôt que d'un critère technique tranchant. À arbitrer.

Décision attendue. Choix entre 1 et 2 avant implémentation ; si l'option 1 est retenue, le test associé (§14) devient un critère d'acceptation obligatoire, pas seulement recommandé.


12. Plan d'implémentation proposé

Sous réserve des arbitrages 11.A à 11.F. Aucune étape ci-dessous n'est engagée par cette spécification.

  1. Corriger V1/V2 (Territory → acteurs) : remplacer les trois écritures directes (CommuneMairieDataRefreshService.php:88-90,148-152,158) par des appels à ActorWriter::updateActeur(), selon l'arbitrage retenu en 11.F.
  2. Corriger V5 côté lecture (Territory → acteurs) : remplacer les lectures directes par ActorReader, selon l'extension validée en 11.D.
  3. Garde-fou architectural : ajouter un test (voir §14) qui échoue si une nouvelle écriture directe vers acteurs, commune_elus, commune_infos ou commune_collectes apparaît dans api/app/Modules/Territory, en dehors de communes et des tables déjà confirmées Territory.
  4. Ne pas toucher à V3/V4 (Territory → données municipales) tant que Municipal Management n'existe pas — reporté à ENG-001.5 par construction (11.E, option 2).
  5. Ne pas toucher à V6/V7 (Mairie) ni V8/V9 (Admin) — documentés, correction hors périmètre.
  6. Documenter V10 (résolution dupliquée) sans consolider — reporté selon 11.A.

13. Risques

RisqueProbabilitéImpactMitigation proposée
Effet de bord Rewards lors de la réutilisation d'ActorWriter::updateActeur() (11.F)Faible mais non nulMoyen — événement de récompense inattendu, pas de perte de donnéesTest dédié (§14) ; option 2 de 11.F si le risque est jugé inacceptable
Extension mal dimensionnée d'ActorReader (11.D)FaibleFaible — contrat de lecture, pas de risque de donnéeValidation explicite des signatures avant implémentation
Régression sur le rafraîchissement SIRENE en production (job non supervisé, cf. doc 30 §8)Faible si périmètre limité aux quick wins (11.E option 2)Élevé si l'option 1 ou 3 de 11.E était choisie sans validationSe limiter à l'option 2 pour cette PR ; toute extension ultérieure suit le plan de validation de la Phase 5 (doc 30)
Découverte tardive d'un cinquième chemin d'écriture non cartographié (au-delà de V3/V4/V6-V9)Faible — recherche exhaustive DB::table('commune menée sur l'ensemble du dépôtMoyenConfirmé par grep exhaustif (§5.3) ; à revérifier au moment de l'implémentation si le code a évolué entretemps
Confusion sur le statut de commune_info_sections (11.C, non anticipée par la mission)Moyenne (nouveau point)Faible à ce stade — aucune écriture prévue par cette PRArbitrage explicite avant toute mission touchant cette table

14. Tests attendus

À couvrir par la future PR, sous réserve des arbitrages :

  1. Rafraîchissement d'une commune (refresh()) : succès nominal, élus ajoutés/mis à jour/désactivés, infos mises à jour, snapshot stocké — comportement observable inchangé avant/après correction de V1/V2.
  2. Conservation des données territoriales : communes.mairie_actor_id et commune_sirene_snapshots inchangés dans leur mécanique d'écriture (Territory reste propriétaire).
  3. Absence d'écriture Actor depuis Territory : après correction, aucun test ne doit trouver de DB::table('acteurs')->update() ni ->insert() sous api/app/Modules/Territory (test architectural, cf. §12.3).
  4. Comportement idempotent du refresh : deux appels successifs de refresh() sur la même commune ne doivent pas dupliquer les élus/infos (déjà garanti par la logique de correspondance par nom normalisé — à conserver, pas à réécrire).
  5. Gestion d'une commune sans acteur mairie : refresh()/confirm() retournent l'erreur appropriée (mairie_not_found, no_siret) sans écriture partielle.
  6. Gestion d'un acteur mairie existant avec SIRET : chemin nominal complet, y compris la mise à jour de kind/categorie_id via ActorWriter::updateActeur().
  7. Absence de régression sur les données municipales : les valeurs de commune_elus/ commune_infos avant/après la bascule des écritures Actor (V1/V2) doivent être strictement identiques — V3/V4 ne changent pas de mécanisme dans cette PR.
  8. Résolution des contrats utilisés : ActorWriter et ActorReader (étendu selon 11.D) résolus correctement par le conteneur Laravel depuis Territory.
  9. Non-régression API : les quatre endpoints publics de TerritoryController (/communes, /communes/{slug}, /communes/{id}/elus\|collectes\|infos\|defibrillateurs) renvoient une réponse strictement identique avant/après.
  10. Test spécifique à 11.F, si l'option 1 y est retenue : le rafraîchissement SIRENE automatisé (refresh()) ne déclenche aucun événement RewardEngineService ni ActorXpService::award() inattendu — à faire échouer explicitement si un tel effet de bord apparaît.

Aucun cas d'usage non présent dans le code actuel n'est ajouté.


15. Critères d'acceptation

  • Territory n'écrit plus directement dans acteurs (V1, V2 corrigées).
  • Aucun comportement utilisateur visible ne change (endpoints publics de TerritoryController et endpoints admin de AdminCommuneController strictement inchangés en réponse).
  • Aucun endpoint public ne change.
  • Aucune migration de données non validée.
  • Les données territoriales continuent d'être rafraîchies (SIRENE, snapshots) exactement comme aujourd'hui.
  • Les données municipales (commune_elus, commune_infos, commune_collectes) ne sont pas perdues ni altérées par la correction de V1/V2.
  • Les 449 tests existants (suite complète api/, état au 2026-08-01) restent verts, plus les nouveaux tests du §14.
  • Aucune nouvelle dépendance inter-contexte n'est créée au-delà de ce que valident 11.D et 11.F.
  • Les divergences non tranchées (11.A, 11.B, 11.C, 11.E options 1/3) restent explicitement reportées, non contournées.

16. Définition de Done

Pour cette spécification (documentaire) :

  • Le code réel de Territory, Mairie, Actor, Admin a été lu et cartographié avec citations vérifiées.
  • L'ownership des données a été classé, y compris les données non anticipées par la mission (commune_info_sections, site_web/email_contact/telephone, quatrième chemin d'écriture Admin).
  • Le périmètre incrémental recommandé pour la future PR est défini et distingue explicitement ce qui peut être corrigé immédiatement de ce qui doit être reporté.
  • Le rafraîchissement SIRENE est décomposé par responsabilité et un arbitrage explicite est soumis pour son architecture cible.
  • Toute décision de Niveau 3 (ownership non tranché, extension de contrat, architecture SIRENE, mécanisme de correction) est présentée en section « Décision architecturale requise », non tranchée.
  • git -C dmv-docs diff --check et npm run build (liens stricts) passent.
  • Aucun code applicatif n'a été modifié. Aucun commit n'a été créé.

Pour la future PR d'implémentation (hors périmètre de cette mission, rappel) : voir §15.


17. Gouvernance des divergences

Conformément à IA-GOUVERNANCE.md, cette spécification ne tranche aucune des six décisions identifiées en §11. Chacune suit le format « Constat / Options / Avantages / Inconvénients / Recommandation / Décision attendue » exigé par la gouvernance.

Récapitulatif des points nécessitant une décision explicite de l'architecte avant implémentation :

PointNatureBloque l'implémentation de
11.A — ownership de la résolution commune ↔ acteur mairieOwnership, Bounded ContextToute consolidation de V10
11.B — ownership de description/image_url/site_web/email_contact/telephoneOwnership, migration potentielleTout TerritoryWriter
11.C — ownership de commune_info_sectionsOwnership (donnée non anticipée)Toute classification future de cette table
11.D — extension d'ActorReaderContrat publicCorrection complète de V5
11.E — architecture cible du rafraîchissement SIRENEArchitecture, Process Manager potentielLe périmètre exact de V1-V4 dans la future PR
11.F — mécanisme de correction de V1/V2Contrat public, risque comportementalLe choix d'implémentation exact de la correction Territory → Actor

Aucune de ces décisions n'a été prise seul. Cette spécification s'arrête à leur formulation, conformément au principe directeur d'IA-GOUVERNANCE.md : « une implémentation incomplète mais correctement interrompue vaut toujours mieux qu'une implémentation terminée ayant pris une mauvaise décision d'architecture ».


Références