ENG-001.4 — Territory Bounded Context
1. Métadonnées de mission
| Epic | EPIC-001 — Restructuration de DMV Core |
| Mission | ENG-001.4 |
| Type | Spécification (documentaire uniquement) |
| Priorité | P0 |
| Dépendances | ENG-001.2 — Contrats inter-contextes (PR-001, mergé) ; ENG-001.3 — Actor → Monetization (PR-002, mergé) |
| Statut | Spécification produite — aucune implémentation |
| Code applicatif | Aucun changement (mission documentaire) |
| Numérotation | ENG-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ément | Rôle observé | Owner cible | Conforme | Action future probable |
|---|---|---|---|---|
Models/Commune.php | Modèle Eloquent communes, docblock indique explicitement « lecture seule — pas d'écriture via Laravel » | Territory | Non — 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.php | Modè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).php | Référentiels externes, lus et écrits uniquement par Territory | Territory | Oui | Aucune |
Controllers/TerritoryController.php | Endpoints publics en lecture (/communes, /communes/{id}/elus|collectes|infos|defibrillateurs, /defibrillateurs) | Territory (adaptateur HTTP) | Oui — délègue intégralement à TerritoryService/CommuneDefibrillateurService/DefibrillateurCatalogueService | Aucune |
Contracts/TerritoryReader.php | Contrat public introduit par ENG-001.2, une seule méthode (getCommune) | Territory | Oui, mais incomplet pour les besoins actuels (voir 11.D) | Étendre si un arbitrage le confirme |
Services/TerritoryService.php | Implémente TerritoryReader. Toutes les opérations documentées comme lecture seule | Territory | Partiel — 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 publique | Remplacer la lecture directe par ActorReader (lecture seule, faible risque) |
Services/CommuneMairieDataRefreshService.php | Orchestration 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_snapshots | Mixte — orchestration Territory légitime, écritures acteurs/municipales illégitimes | Non — voir §6 | Cœur du chantier de cette spécification (§12) |
Services/CommuneMairieResolverService.php | Résolution automatique heuristique de l'acteur mairie d'une commune (2 passes), écrit communes.mairie_actor_id, lit acteurs en DB::table() direct | Territory pour l'écriture (table propre) ; lecture croisée à clarifier | Partiel | Remplacer la lecture par ActorReader |
Services/CommuneMairieDetectService.php | Recherche de candidats mairie dans SIRENE pour l'UI d'administration, lecture seule (aucune écriture) | Territory | Oui pour l'écriture (aucune) ; lecture croisée acteurs à clarifier | Remplacer la lecture par ActorReader |
Services/SireneApiClient.php, SireneKindDetector.php | Adaptateur externe (appel HTTP SIRENE) et classification pure (aucune donnée DMV) | Territory (adaptateur d'intégration) | Oui | Aucune |
Services/CommuneDefibrillateurService.php, DefibrillateurCatalogueService.php, GeodaeDefibrillateurSourceService.php | Référentiel défibrillateurs, lecture/écriture strictement internes à Territory | Territory | Oui | Aucune |
app/Console/Commands/RefreshMairieSireneCommand.php | Commande Artisan (communes:refresh-mairie-sirene) appelant CommuneMairieDataRefreshService::refresh() en boucle sur les communes liées | Territory (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.php | Commande 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 | Écriture | Table | Owner cible |
|---|---|---|---|
MairieWriteService::updateCommune() (MairieWriteService.php:216-229) | DB::table('communes')->update() sur description, image_url uniquement | communes | À arbitrer (11.B) |
MairieWriteService::createElu/updateElu/deleteElu() (MairieWriteService.php:233-269) | CRUD DB::table() brut, sans passer par le modèle Territory\Models\CommuneElu | commune_elus | Municipal Management (cible) |
MairieWriteService::createCollecte/updateCollecte/deleteCollecte() (MairieWriteService.php:273-309) | Idem | commune_collectes | Municipal Management (cible) |
MairieWriteService::createInfo/updateInfo/deleteInfo() (MairieWriteService.php:313-351) | Idem | commune_infos | Municipal Management (cible) |
MairieWriteService::getCommuneIdForActeur() (MairieWriteService.php:364) | Lecture directe DB::table('acteurs') | acteurs | Lecture 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 | Écriture | Table | Constat |
|---|---|---|---|
AdminCommuneService::createCommune/updateCommune/setMairie/unsetMairie() (AdminCommuneService.php:32,84,99,124) | DB::table('communes') | communes | updateCommune() 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_infos | Chemin d'écriture entièrement séparé de MairieWriteService et de CommuneMairieDataRefreshService |
AdminCommuneInfoController::storeElu/updateElu/destroyElu() (AdminCommuneInfoController.php:234-304) | CRUD complet | commune_elus | Idem |
AdminCommuneInfoController::storeCollecte/updateCollecte/destroyCollecte() (AdminCommuneInfoController.php:308-380) | CRUD complet | commune_collectes | Idem — 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 complet | commune_info_sections | Table 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émentation | Fichier | Direction résolue | Mécanisme |
|---|---|---|---|
ProfileService::resolveManagedCommunes() | Identity/Services/ProfileService.php:128 et suivantes | utilisateur → communes gérées | Jointure users_roles (scope explicite), fallback profiles.commune_id |
CommuneMairieResolverService::resolve() | Territory/Services/CommuneMairieResolverService.php:43-127 | commune → acteur mairie (détection automatique) | Heuristique 2 passes sur acteurs (kind puis nom), écrit communes.mairie_actor_id |
EnsureCommuneManager | Mairie/Middleware/EnsureCommuneManager.php:77-84 | commune → acteur mairie (autorisation) | Lecture directe communes.mairie_actor_id puis vérification acteurs.kind |
EnsureMairieAccess::resolveActeurId() | Mairie/Middleware/EnsureMairieAccess.php:78-95 | ressource (alerte/service/publication) → acteur mairie | Ré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
| # | Violation | Fichier:ligne (vérifié) | Gravité |
|---|---|---|---|
| V1 | Territory écrit acteurs.kind, acteurs.categorie_id | CommuneMairieDataRefreshService.php:88-90 (refresh) et :148-152 (confirm) | P0 — écriture croisée directe |
| V2 | Territory écrit acteurs.numero_siret | CommuneMairieDataRefreshService.php:158 (confirm) | P0 — écriture croisée directe |
| V3 | Territory é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 |
| V4 | Territory écrit commune_infos (données Municipal Management) | CommuneMairieDataRefreshService.php:312-333 (upsertInfo(), appelée par refreshInfos()) | P0 — mauvais owner |
| V5 | Territory 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:117 | P1 — lecture croisée, contrat ActorReader existant mais incomplet pour ces besoins |
| V6 | Mairie écrit communes.description/image_url | MairieWriteService.php:216-229 | P0 — ownership non tranché (11.B) |
| V7 | Mairie écrit commune_elus/commune_collectes/commune_infos en SQL brut, en parallèle de deux autres chemins d'écriture | MairieWriteService.php:233-351 | P0 — déjà connu, hors périmètre de correction directe (Mairie n'est pas cette mission) |
| V8 | Admin écrit communes avec un périmètre de champs plus large que Mairie | AdminCommuneService.php:32,84,99,124 | P1 — à documenter, correction non exigée par cette PR |
| V9 | Admin possède un chemin d'écriture complet et autonome sur commune_elus/commune_collectes/commune_infos/commune_info_sections | AdminCommuneInfoController.php:22-380 | P1 — à documenter (découverte, §5.3), correction non exigée par cette PR |
| V10 | Résolution commune ↔ acteur mairie dupliquée quatre fois | §5.4 | P1 — ambiguïté architecturale, pas une écriture croisée au sens strict |
7. Ownership des données
| Donnée / table / champ | Owner actuel | Owner cible | Statut | Décision requise |
|---|---|---|---|---|
communes (identité, code INSEE, codes postaux, géographie, slug, active, deleted_at) | Territory (lecture), écritures dispersées (Admin, Mairie, Territory) | Territory | Non conforme (écritures dispersées, pas un problème d'ownership de la donnée elle-même) | Non |
commune_sirene_snapshots | Territory | Territory | Conforme | Non |
commune_defibrillateurs, defibrillateurs_catalogue (+ Sync) | Territory | Territory | Conforme | Non |
commune_elus | Fragmenté : Mairie, Admin, Territory (refresh()/confirm()) | Municipal Management | Non conforme | Non sur l'ownership (déjà tranché par RFC-001) — Oui sur le calendrier de migration (hors périmètre de cette PR) |
commune_collectes | Fragmenté : Mairie, Admin | Municipal Management | Non conforme | Idem |
commune_infos | Fragmenté : Mairie, Admin, Territory (refresh()/confirm()) | Municipal Management | Non conforme | Idem |
commune_info_sections (découverte §5.3) | Admin exclusivement | À arbitrer | Non classé par la mission initiale | Oui — nouveau point, voir 11.C |
acteurs.kind, acteurs.categorie_id, acteurs.numero_siret (écrits depuis Territory) | Territory (à tort) | Actor | Non conforme | Non sur l'ownership — Oui sur le mécanisme de correction (11.F) |
communes.mairie_actor_id (la colonne) | Territory | Territory | Conforme (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é architecturale | Oui, voir 11.A |
communes.description, communes.image_url | Territory (colonne), écrit par Mairie et Admin | Non tranché | Déjà signalé bloquant pour TerritoryWriter par ENG-001.2 | Oui, 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 initiale | Oui, 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 contratActorWriter::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) parActorReader— 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_sectionsvers 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.
- Territory reste propriétaire de la résolution (statu quo partiel). Avantages : la colonne
communes.mairie_actor_idvit déjà danscommunes; 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à deActorReaderactuel. - 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. - 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.
- Ces champs restent la propriété de Territory, avec un futur
TerritoryWriterexplicite. Avantages : aucune migration, périmètre inchangé. Inconvénients : risque de duplication permanente avec les champs équivalents deacteurs(adresse, téléphone, email, site_web — déjà utilisés parTerritoryService::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). - 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. - 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.
- Rattacher
commune_info_sectionsà Municipal Management au même titre quecommune_infos, dont elle est la taxonomie. Avantages : cohérent, une seule migration. Inconvénients : aucun aujourd'hui. - 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 decommune_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 defindById(),findBySiret(),findBySiren(), retournant?ActeurDTO. Signatures effectivement implémentées, voir ENG-001-4-implementation-report.md §3, §10. Les deux lectures fondées surcommune_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.
- Étendre
ActorReaderavec 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). - 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.
- Événement métier
CommuneSireneRefreshed, consommé par Actor et par le futur Municipal Management (aligné sur doc 30 Phase 5.1/5.2,ADR-014principe 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. - 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'écrirecommune_elus/commune_infosen direct tant que Municipal Management n'existe pas — exactement la position déjà actée parENG-001.2Option 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. - 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-014principe 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 queENG-001.2a 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 deupdateActeur(). Implémentée et testée (aucune ligneactor_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.
- 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. - 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.
- 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. - Corriger V5 côté lecture (Territory →
acteurs) : remplacer les lectures directes parActorReader, selon l'extension validée en 11.D. - Garde-fou architectural : ajouter un test (voir §14) qui échoue si une nouvelle écriture
directe vers
acteurs,commune_elus,commune_infosoucommune_collectesapparaît dansapi/app/Modules/Territory, en dehors decommuneset des tables déjà confirmées Territory. - Ne pas toucher à V3/V4 (Territory → données municipales) tant que Municipal Management
n'existe pas — reporté à
ENG-001.5par construction (11.E, option 2). - Ne pas toucher à V6/V7 (Mairie) ni V8/V9 (Admin) — documentés, correction hors périmètre.
- Documenter V10 (résolution dupliquée) sans consolider — reporté selon 11.A.
13. Risques
| Risque | Probabilité | Impact | Mitigation proposée |
|---|---|---|---|
Effet de bord Rewards lors de la réutilisation d'ActorWriter::updateActeur() (11.F) | Faible mais non nul | Moyen — événement de récompense inattendu, pas de perte de données | Test dédié (§14) ; option 2 de 11.F si le risque est jugé inacceptable |
Extension mal dimensionnée d'ActorReader (11.D) | Faible | Faible — contrat de lecture, pas de risque de donnée | Validation 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 validation | Se 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ôt | Moyen | Confirmé 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 PR | Arbitrage explicite avant toute mission touchant cette table |
14. Tests attendus
À couvrir par la future PR, sous réserve des arbitrages :
- 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. - Conservation des données territoriales :
communes.mairie_actor_idetcommune_sirene_snapshotsinchangés dans leur mécanique d'écriture (Territory reste propriétaire). - Absence d'écriture Actor depuis Territory : après correction, aucun test ne doit trouver de
DB::table('acteurs')->update()ni->insert()sousapi/app/Modules/Territory(test architectural, cf. §12.3). - 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). - Gestion d'une commune sans acteur mairie :
refresh()/confirm()retournent l'erreur appropriée (mairie_not_found,no_siret) sans écriture partielle. - Gestion d'un acteur mairie existant avec SIRET : chemin nominal complet, y compris la mise à
jour de
kind/categorie_idviaActorWriter::updateActeur(). - Absence de régression sur les données municipales : les valeurs de
commune_elus/commune_infosavant/après la bascule des écritures Actor (V1/V2) doivent être strictement identiques — V3/V4 ne changent pas de mécanisme dans cette PR. - Résolution des contrats utilisés :
ActorWriteretActorReader(étendu selon 11.D) résolus correctement par le conteneur Laravel depuisTerritory. - 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. - Test spécifique à 11.F, si l'option 1 y est retenue : le rafraîchissement SIRENE
automatisé (
refresh()) ne déclenche aucun événementRewardEngineServiceniActorXpService::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
TerritoryControlleret endpoints admin deAdminCommuneControllerstrictement 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 --checketnpm 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 :
| Point | Nature | Bloque l'implémentation de |
|---|---|---|
| 11.A — ownership de la résolution commune ↔ acteur mairie | Ownership, Bounded Context | Toute consolidation de V10 |
11.B — ownership de description/image_url/site_web/email_contact/telephone | Ownership, migration potentielle | Tout TerritoryWriter |
11.C — ownership de commune_info_sections | Ownership (donnée non anticipée) | Toute classification future de cette table |
11.D — extension d'ActorReader | Contrat public | Correction complète de V5 |
| 11.E — architecture cible du rafraîchissement SIRENE | Architecture, Process Manager potentiel | Le périmètre exact de V1-V4 dans la future PR |
| 11.F — mécanisme de correction de V1/V2 | Contrat public, risque comportemental | Le 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
- ENG-001-1 — Rapport d'audit DMV Core §3, §4, §6, §8, §10 (PR-004)
- ENG-001-2 — Contrats inter-contextes
- ENG-001-2 — Rapport d'implémentation
- ENG-001-3 — Découplage Actor → Monetization
- EPIC-001 — Restructuration de DMV Core
- ADR-014 — DMV Core : Bounded Contexts et Monolithe Modulaire
- Cartographie des Bounded Contexts réels de DMV Core
- Roadmap de migration DMV Core
- RFC-005 — Territory Refactoring
- RFC-001 — Extraction du Bounded Context Municipal Management