---
suivi: 1470
date: 2026-09-07
sujet: Diagnostic — demande de pièces aux suiveurs (tâches ops)
chantier: juridique / tâches opérationnelles
type: diagnostic
statut: poussé
hash: 8f4ffe3c
fichiers:
  - docs/suivi/SUIVI_1470_diag_demande_pieces_suiveurs.md
---

# SUIVI #1470 — PHASE 1 DIAGNOSTIC — demande de pièces aux suiveurs

## PROMPT ENVOYÉ

Diagnostic uniquement (aucun code applicatif) : cartographier le socle
Task / TaskSyncService / suiveurs juridiques pour une future feature
« une tâche par pièce demandée, échéance, assignée au suiveur, Mes tâches
+ digest Teams ». Modèle de référence #1457/#1458 (`completer_saisie_facture`).
Ne pas inventer de chiffres ; fournir les requêtes pour Robin.

## BASE GIT

```
git log --oneline -1 origin/main
c4b4bb7a chore(suivi): hash fiche #1469
```

(Attendu prompt : `2a241a43` #1468 — main a avancé via #1469 Historique
Chronologique. Travail sur main à jour ; fiche #1469 lue pour D1.)

## FICHES LUES

- **#1457** — diag tâche valideur amont : socle Task, assignee sans pivot,
  échéance = type.delai_heures, clé idempotence, fiche_url à étendre,
  rail fermé, digest ouvert.
- **#1458** — implémentation `completer_saisie_facture` : taskable
  `DepotFacture`, delai 72 h, clôture envoi validation, rail `SAISIE`,
  `fiche_url` via `depot_uuid`.
- **#1451** — suiveurs annuaire M365, `demande_suiveurs`,
  `peutGererSuiveurs` brut, `M365CommercialUserResolver`,
  `NotifierAjoutSuiveurDemandeJuridique`.
- **#1469** — Historique Chronologique (événements + commentaires + PJ
  enrichies) ; pas de type « demande pièce » ; relevé futur tâches.
- **#1461** — strip `"` dans `FactureValidateurNv1AssignmentService::resolveUser`
  (consommé par CompleterSaisie).
- **#1446** — `DeclencheFoncierAlertes` terminate (piste relance sans crontab).
- **#1394** — TacheSuivi (ACL créateur) : **ne pas utiliser** pour ce besoin.

## A. LES SUIVEURS

### A1 — Stockage

Confirmé migration `database/migrations/2026_07_03_100000_create_juridique_recolte_tables.php`
L71–89 :

- colonnes `demande_juridique_id`, `user_id` (+ `id`, timestamps) ;
- UNIQUE `ds_demande_user_unique` sur `(demande_juridique_id, user_id)` ;
- FK cascadeOnDelete vers `demandes_juridiques` et `users`.

Relation modèle : `DemandeJuridique::suiveurs()` → `belongsToMany` via
`demande_suiveurs` (`app/Models/DemandeJuridique.php` ~L123–127).

### A2 — Compte Graph jamais connecté + digest

`M365CommercialUserResolver::resolveToUser`
(`app/Services/M365CommercialUserResolver.php` L45–84) :

- email M365 vide → `null` (pas de user) ;
- **création** : `email` = email Graph (L77–84) ;
- **update** existant : ne rafraîchit **pas** `email` (forceFill azure_id,
  name, preapproved_at, email_verified_at seulement — L60–68).

Digest Teams (`app/Console/Commands/TachesDigestCommand.php` L32–41, L54–87) :
sélectionne `User` avec `email` non vide, envoie via
`TeamsNotificationService::notifierUtilisateur($email, …)`.

**Verdict :** un suiveur créé à la volée a un `users.id` + `users.email` →
peut porter `assignee_id` et recevoir le digest **si** l’email Graph était
présent à la création. Première connexion ERP non requise pour le digest
(filtre email, pas last_login). Risque : compte préexistant matché sur
`azure_id` avec email ERP vide/périmé → digest skip (log).

### A3 — Qui gère les suiveurs

`DemandeJuridiqueSuiveurResolver::peutGererSuiveurs`
(`app/Support/Juridique/DemandeJuridiqueSuiveurResolver.php` L13–25) :
`m365GroupNames()` brut ∩ `{GS_JURIDIQUE, GS_DSI, GS_ADMIN_ERP}`.

- Ajout/retrait **post-création** : gate `peutGererSuiveurs` (Request store
  suiveur ; controller destroy ~L426).
- **Demandeur seul** : ne peut **pas** gérer les suiveurs après dépôt.
- Au **dépôt** : le créateur peut passer des suiveurs dans le formulaire
  store (options disponibles à la création).

**Question ouverte (arbitrage) :** le demandeur peut-il être destinataire
d’une demande de pièce sans être dans le pivot suiveurs ? Aujourd’hui
demandeur ≠ suiveur automatiquement (sauf s’il est aussi attaché).

### A4 — Destinataire hors suiveurs

Aucune contrainte code « piece requester ∈ suiveurs » (feature absente).
Attach actuel = resolve User + `suiveurs()->attach($id)`.

**Arbitrage :** autoriser un non-suiveur ? l’ajouter auto comme suiveur ?

---

## B. LE SOCLE TÂCHES (pattern #1458 confirmé)

### B1 — Acquis confirmés (écarts notés)

| Acquis | Source | Verdict |
|---|---|---|
| Modèle `Task`, table `tasks` | `app/Models/Task.php` ; migration `2026_06_12_210100_create_tasks_table.php` L11 | OK |
| Type via `task_types.code` + `TaskType::CODE_*` | `app/Models/TaskType.php` L14–38 | OK |
| Assignee colonnes, pas pivot | migration tasks L19–21 ; `reassign` forceFill L287–291 | OK |
| Échéance = now + `delai_heures` à la **création** seulement | `TaskSyncService` L123–124, L294–300 ; update existant ne touche pas `echeance_at` | OK |
| `closeTask` → realise/annule + auto_closed + closed_reason | L205–277 | OK |
| Fermée jamais ressuscitée (realise/annule) | L98–102 | OK ; nuance : `SignalementTaskSync` peut réouvrir (hors juridique) |
| Clé `typeCode:morphClass:id` + updateOrCreate | L303–305, L132–134 | OK |
| Ouvertes = nouveau + en_cours | `Task::scopeOuvertes` L208–210 | OK |
| `/mes-taches` visible assignee_id sans whitelist type | `Task::visiblePourUtilisateur` ~L230 | OK |
| `fiche_url` | `MesTachesController` L319–325 | **Écart #1457** : aujourd’hui 3 branches — `facture_id`, `bug_report_id`, **`depot_uuid`** (#1458). Toujours **pas** de branche juridique → clic mort sans extension |
| Rail `tacheSuiviLabels.js` L226–234 | liste fermée + fallback `OPÉR.` ; `completer_saisie_facture` → `SAISIE` | OK |
| Digest `queryTasksDigest` | `MesTachesResumeService` L272–276 — toutes Task ouvertes par assignee_id | OK |
| Badge `operationnelles` | resume L36–42 → AppShell | OK |
| Drift #1461 | `FactureValidateurNv1AssignmentService::resolveUser` strip `"` ; utilisé CompleterSaisie | OK |

### B2 — Taskable polymorphe

`upsertFromEvent(string $typeCode, Model $taskable, ?User $assignee, …)`
(`TaskSyncService` L77–83) : **aucune** contrainte de classe sur `$taskable`.
Écrit `getMorphClass()` / `getKey()` (L117–118).

**Oui, `DemandeJuridique` passerait techniquement.** Callers actuels :
`WorkflowValidation`, `DepotFacture`, `Facture`, `BugReport`,
`ConditionSuspensive`, `PromesseFonciere`, `DossierFoncier`, `OffreFonciere`.
Aucun caller DemandeJuridique aujourd’hui.

Si objet dédié (C1-ii) : le taskable = cet objet, pas la demande.

### B3 — Échéance explicite par tâche (DIFFÉRENCE STRUCTURELLE)

**Colonne :** `tasks.echeance_at` + `echeance_source`
(migration `2026_06_12_210100` L25–26).

**Constante :** `Task::ECHEANCE_SOURCE_MANUEL = 'manuel'` (`Task.php` L55) —
**déclarée, jamais écrite** ailleurs dans `app/` (grep = définition seule).

**Flux ops actuel :** toujours `echeance_source = type` (L120) ;
`echeance_at` calculée **uniquement** si `!$existing` via
`delai_heures` (L123–124). Aucun paramètre d’échéance dans la signature
`upsertFromEvent`.

**TacheSuivi** a `date_echeance` saisie (`TacheSuivi.php` L62) — **autre
mécanisme**, hors périmètre (leçon #1457/#1394).

**Verdict (pas d’arrêt dur) :** le **schéma** accepte une échéance par tâche.
Le **API TaskSyncService** ne l’expose pas. Extension **additive** possible
(ex. param optionnel `?CarbonInterface $echeanceAt = null` → si fourni,
`echeance_source = manuel` ; sinon comportement actuel inchangé) **sans
impacter les types existants**. Alternative : `forceFill` post-upsert sur
la Task créée (même effet, hors moteur).

**Impact types existants si extension mal faite :** recalcul à chaque upsert
écraserait les échéances — à éviter (garder « set only on create »).

### B4 — Modification d’échéance après création

Aucun chemin applicatif trouvé (pas de TaskController ; upsert existant
ignore `echeance_at`). **Personne** via Mes tâches / TaskSync aujourd’hui.
Fillable Eloquent permettrait un write direct — à créer pour un « report ».

### B5 — Retard / relance / digest

- **UI retard :** `scopeEnRetard` / `estEnRetard` (`Task.php` L189–202) ;
  prop `en_retard` MesTachesController L339 ; filtre `en_retard` ; groupe
  « En retard » (`mesTachesGrouping.js`).
- **Relance automatique Task :** **aucune** (pas de job « relancer » Task).
- **Digest 9h :** distingue retards / urgents (`MesTachesResumeService`
  classifier ~L119–129, L346–353 : `echeance < today` → retards ;
  sinon ≤ 3 jours ouvrés → urgents). Pas de Teams DM de relance dédié.

Contexte projet (NV1 qui traînent) : l’échéance **affiche** le retard dans
Mes tâches + digest ; **sans** relance active dédiée.

### B6 — Types à jour (`TaskType::CODE_*` + seeds delai_heures)

| code | delai_heures (seed) |
|---|---|
| `validation_facture_nv1` | 48 |
| `validation_facture_nv2` | 48 |
| `virement_a_executer` | 48 |
| `signalement_dsi` | null |
| `completer_saisie_facture` | 72 |
| `arbitrage_cs_fonciere` | 48 |
| `promesse_expiration_fonciere` | 48 |
| `dossier_veille_revue_fonciere` | 48 |
| `offre_expiration_fonciere` | 48 |
| `dossier_sans_echange_fonciere` | 72 |

Aucun code « demande_piece » / juridique pièce (à confirmer par G3).

---

## C. MODÉLISATION

### C1 — Deux options

**(i) Task seule, taskable = DemandeJuridique**

- Léger, pas de table métier.
- **Problème clé :** `idempotencyKey = type:DemandeJuridique:{id}`
  (`TaskSyncService` L303–305) → **une seule** tâche ouverte par type et
  demande. Deux pièces (« titre » + « plan ») = collision / écrasement.
- Clé qu’il faudrait : enrichir la clé (ex. suffixe UUID pièce demandée) =
  **changement invasif** de `idempotencyKey` / contrat TaskSync, ou
  contourner via `closeOpenTaskRecord` + création hors upsert (hors pattern
  #1458).

**(ii) Objet dédié + Task en miroir**

- Table proposée : `demande_juridique_pieces_demandees` (nom à arbitrer) :
  `demande_juridique_id`, `assignee_user_id`, `libelle_piece`,
  `echeance_at`, `statut`, `piece_jointe_id` nullable (rattachement),
  timestamps ; soft delete éventuel.
- Task : type nouveau, taskable = **cette ligne**, titre = libelle,
  assignee = suiveur, `echeance_at` copiée (manuel).
- Idempotence naturelle : `type:DemandeJuridiquePieceDemandee:{id}`.
- Liste fiche indépendante des Task ; chronologie / bloc dédié facilités.

**Avis argumenté :** **(ii)** colle mieux au socle #1458 (1 événement métier
→ 1 taskable → 1 clé) et à l’échéance choisie par la juriste. (i) force soit
une refonte de l’idempotence, soit un contournement fragile.

### C2 — Clôture

Hypothèse « dépôt PJ clôture auto » : **non fiable** aujourd’hui — aucune
FK pièce ↔ « pièce demandée ». Options :

| Option | Coût |
|---|---|
| Suiveur clôture manuellement (bouton « Fournir » / réaliser) | UI + droit ; pas de preuve fichier |
| Juriste valide réception | Workflow 2 étapes ; fiable métier |
| Rattachement explicite à l’upload (`piece_jointe_id` sur l’objet demande) | Formulaire upload lié ; clôture auto possible comme #1458 envoi |

**Avis :** démarrer avec clôture manuelle suiveur **ou** validation juriste ;
rattachement upload = lot 2. Sans arbitrage, **ne pas** clôturer sur tout
upload de PJ (faux positifs).

### C3 — Événements de cycle de vie (alignement #1458)

| Événement | Proposition |
|---|---|
| Suiveur retiré | `reassign(null)` ou `annule` reason dédiée — arbitrage |
| Demande → Terminé / Annulé | close toutes pièces ouvertes (`CAUSE_RESOLUE` / reason dédiée) |
| Demande soft-deleted | close / cascade objet dédié + Task |

### C4 — Multiplicités

- Plusieurs pièces → un même suiveur : **oui** (N lignes / N tâches) —
  cœur de l’option A Robin.
- Même libellé → plusieurs suiveurs : **oui** si N lignes (une par
  destinataire) ; sinon ambigu.

---

## D. AFFICHAGE

### D1 — Chronologie #1469

`DemandeJuridiqueChronologieBuilder` : natures `evenement` |
`commentaire` | `piece_jointe` (spécialisation `piece_ajoutee` seulement).
**Aucun hook** « demande de pièce ».

**Arbitrage :** journaliser à l’ouverture ? à la clôture ? les deux ? dans
la chronologie et/ou bloc dédié ?

### D2 — Bloc « Pièces demandées »

Colonne droite Show.vue aujourd’hui : Pièces jointes → Commentaires
(formulaire) → Historique Chronologique (`max-h-[60vh] overflow-y-auto`).
Emplacement possible : **entre** Commentaires et Chronologie, ou sous
Pièces jointes. Pas `overflow-x-hidden` (#1271/#1444).

### D3 — Mes tâches

- Rail : ajouter entrée dans `libelleRailOperationnel`
  (`tacheSuiviLabels.js` L226–234), ex. code
  `demande_piece_juridique` → libellé court `PIÈCE` (à arbitrer).
- `fiche_url` : étendre `serializeTaskRow` L319–325 avec
  `demande_juridique_id` (ou uuid) dans `erp_context` →
  `route('juridique.demandes.show', $id)` =
  `GET /juridique/demandes/{demandeJuridique}`
  (`routes/web.php` ~L1322, name `juridique.demandes.show`).

### D4 — Droit suiveur ouvrir la fiche — **OUI**

`DemandeJuridiqueParticipantResolver::peutAcceder` L44–50 : juriste **ou**
`estVisiblePourUtilisateur` (demandeur **ou** suiveur pivot).

Controller `show` L438–442 : `abort_unless(peutAcceder)`.

ListQuery non-juriste L36–40 : demandeur OU suiveur.

Preuve test : `DemandeJuridiquePieceVisibiliteTest` —
`test_suiveur_voit_et_telecharge_les_pieces_internes` (assertOk) ;
outsider show `assertForbidden`.

**Le clic Mes tâches → fiche ne doit pas 403 pour un suiveur assigné.**

### D5 — KPI #1468

Bandeau liste (`DemandeJuridiqueKpiService`) : ouverts / délais / types.
**Pas** de compteur « pièces demandées ». **Arbitrage** d’extension.

---

## E. NOTIFICATIONS

### E1 — Création

Canaux existants :

| Canal | Mécanisme | Notes |
|---|---|---|
| Cloche | `alertes` / AlerteService | synchrone web |
| Teams DM | `TeamsNotificationService::notifierUtilisateur` | webhook Power Automate ; **HTML** côté digest (`MesTachesResumeService`), message suiveur actuel = texte/`\n` (`NotifierAjoutSuiveurDemandeJuridique`) |
| Digest 9h | `taches:digest` via `DeclencheDigestQuotidien::terminate` | Task ouvertes assignee ; **entre auto** si Task créée |

Réseau : pas d’appel Graph/Teams depuis CLI OVH — web/PHP-FPM ;
`QUEUE_CONNECTION=sync`.

**Arbitrage :** DM immédiat à la création + digest (recommandation technique
possible mais listée en arbitrages).

### E2 — Relance échéance

Pas de crontab SSH. Pistes : middleware `terminate` type
`DeclencheFoncierAlertes` / `DeclencheDigestQuotidien`
(`bootstrap/app.php`). **Aucun** relance Task existante à cloner telle
quelle — à concevoir (ex. J-1 / J+0 DM si ouverte et `echeance_at`).

### E3 — Juriste notifiée à la fourniture

Pas de pattern exact « piece demandée fournie ». Proche : notifs
changement statut / ajout suiveur. **Arbitrage.**

---

## F. RISQUES ET IMPACTS

### F1 — Fichiers probables (implémentation future) + zones communes

| Fichier | Zones / collision |
|---|---|
| `DemandeJuridiqueController` | #1455 mime, #1468 KPI liste (autre méthode), show |
| `Juridique/Show.vue` | #1456 Dialog, #1469 chronologie — **série** |
| Nouveau model/service + migration | — |
| `TaskType` + seed migration | — |
| `TaskSyncService` | si échéance optionnelle |
| `MesTachesController` | fiche_url |
| `tacheSuiviLabels.js` | rail + **rebuild** |
| `DemandeJuridiqueChronologieBuilder` | si journalisation |
| Journal mises à jour | impact user |

### F2 — Découpage proposé

1. **Socle données + type Task** : migration objet dédié + seed type +
   sync create/close + tests Eloquent (sans UI riche).
2. **UI fiche** : formulaire juriste + liste pièces demandées + clôture.
3. **Mes tâches** : rail + fiche_url + reload Show.
4. **Notifs / relance** (optionnel séparé) : DM création + middleware
   relance.

### F3 — Doublon / idempotence

Avec (ii) : clé `demande_piece_juridique:{id}` — un upsert par ligne.
Avec (i) : collision — **à refuser**.

### F4 — Migration

Oui (type seed `updateOrCreate` + table dédiée si ii). Rappels : pas
`->after()` dans `Schema::create` ; `only_full_group_by` ; UNIQUE nullable
MySQL ; drop index vs FK.

### F5 — Droits / comptes test Robin

- Lire `m365GroupNames()` brut — pas `hasAnyM365Group` / Gate.
- **Juriste demande :** Christelle (id 19) ou Manon (id 29) — GS_JURIDIQUE.
- **Suiveur reçoit :** collab non-juriste suiveur de la demande (ex. Benoit
  JOUVAUD si peuplé) — vérifier peuplement pivot.
- **Tiers :** user hors demande / hors JURIDIQUE → show 403, pas de Task.
- Ne pas tester refus avec seul GS_ADMIN_ERP (faussement positif
  `hasM365Group`).

### F6 — Peuplement à prouver

Avant prod : G1 (suiveurs + email), G2 (types/volumes), G3 (absence code),
comptes juriste/suiveur réels. Mécanisme TaskType seed = `updateOrCreate`
comme #1458.

---

## G. REQUÊTES DE MESURE (à exécuter par Robin — ne pas inventer de chiffres)

```sql
-- G1. Suiveurs par demande + email + déjà connecté ?
SELECT
  dj.id AS demande_id,
  dj.objet,
  dj.statut,
  u.id AS user_id,
  u.name,
  u.email,
  u.azure_id,
  u.preapproved_at,
  CASE
    WHEN u.microsoft_token IS NOT NULL AND u.microsoft_token != '' THEN 1
    ELSE 0
  END AS a_token_microsoft,
  ds.created_at AS suiveur_depuis
FROM demande_suiveurs ds
JOIN demandes_juridiques dj ON dj.id = ds.demande_juridique_id AND dj.deleted_at IS NULL
JOIN users u ON u.id = ds.user_id
ORDER BY dj.id, u.name;

-- G2. Types de tâches + delai_heures + volume ouvertes
SELECT
  tt.id,
  tt.code,
  tt.libelle,
  tt.actif,
  tt.delai_heures,
  COUNT(t.id) AS nb_taches_ouvertes
FROM task_types tt
LEFT JOIN tasks t
  ON t.task_type_id = tt.id
  AND t.statut IN ('nouveau', 'en_cours')
GROUP BY tt.id, tt.code, tt.libelle, tt.actif, tt.delai_heures
ORDER BY tt.code;

-- G3. Aucun type « demande de pièce » déjà présent
SELECT id, code, libelle, actif, delai_heures
FROM task_types
WHERE code LIKE '%piece%'
   OR code LIKE '%juridique%'
   OR libelle LIKE '%pièce%'
   OR libelle LIKE '%piece%';

-- G4. Demandes juridiques ouvertes (périmètre potentiel)
SELECT statut, COUNT(*) AS nb
FROM demandes_juridiques
WHERE deleted_at IS NULL
  AND statut IN ('a_qualifier', 'en_cours', 'en_attente')
GROUP BY statut
ORDER BY statut;
```

(Note G1 : si la colonne de « dernière connexion » réelle diffère chez vous —
ex. absence `microsoft_token` utile — adapter ; l’email reste le critère
digest.)

---

## PROPOSITION D’IMPLÉMENTATION (non codée)

1. **Modélisation (ii)** : table `demande_juridique_pieces_demandees` +
   modèle Eloquent ; Task type `demande_piece_juridique` (libellé à
   arbitrer) ; `delai_heures` null ou défaut documentaire — échéance
   **saisie** écrite dans `tasks.echeance_at` (`echeance_source=manuel`).
2. **TaskSync** : upsert standard (taskable = ligne) ; extension
   optionnelle échéance **ou** forceFill post-create ; close reasons
   dédiées (`piece_fournie`, `demande_cloturee`, …).
3. **Droits création** : juriste via `m365GroupNames()` brut ∩
   GS_JURIDIQUE (+ ADMIN/DSI si aligné suiveurs) — **pas** élargir DG
   hors lot droits dédié (#1468 limite).
4. **Destinataire** : user_id suiveur (ou arbitrage hors-suiveur).
5. **UI Show** : bloc « Pièces demandées » + formulaire ; chronologie =
   selon arbitrage D1.
6. **Mes tâches** : rail + `fiche_url` → `juridique.demandes.show`.
7. **Clôture V1** : manuelle (suiveur ou juriste) ; rattachement PJ = V2.
8. **Notifs V1** : digest auto ; DM création optionnel (E1).
9. Tests Eloquent réels (#1440) ; refus hors ADMIN_ERP.

---

## ARBITRAGES ROBIN (A/B/C — sans recommandation imposée)

1. **Modélisation** : (A) Task seule sur DemandeJuridique + clé enrichie ;
   (B) objet dédié + Task miroir ; (C) autre.
2. **Échéance** : (A) étendre `upsertFromEvent` param optionnel ;
   (B) forceFill après create ; (C) réutiliser seulement `delai_heures`
   (contredit le besoin « choisie par juriste »).
3. **Destinataire hors suiveurs** : (A) interdit ; (B) autorisé sans
   attach ; (C) autorisé + attach auto suiveur.
4. **Demandeur destinataire** sans être suiveur : (A) oui ; (B) non ;
   (C) oui s’il est demandeur.
5. **Clôture** : (A) manuelle suiveur ; (B) validation juriste ;
   (C) rattachement PJ à l’upload + auto-close.
6. **Chronologie #1469** : (A) à l’ouverture ; (B) à la clôture ;
   (C) les deux ; (D) bloc dédié seulement.
7. **Notif création** : (A) cloche seule ; (B) cloche + Teams DM ;
   (C) digest seulement.
8. **Relance échéance** : (A) non V1 ; (B) digest suffit ; (C) DM J-1/J+0
   via middleware terminate.
9. **Notif juriste à la fourniture** : (A) oui ; (B) non V1.
10. **KPI #1468** : (A) ignorer ; (B) compteur « pièces en attente ».
11. **Suiveur retiré** : (A) annuler Task ; (B) laisser ouverte ;
    (C) réassigner.
12. **Libellé rail** Mes tâches : proposer 1–2 lettres courtes (ex. PIÈCE).

---

## CLAUSE D’ARRÊT — APPLICATION

| Critère | Verdict |
|---|---|
| Échéance explicite absente du schéma + impact types | **Non bloquant** : colonne `echeance_at` existe ; API à étendre de façon additive |
| Contrainte taskable incompatible | **Non** : `Model` générique |
| Suiveur non-juriste 403 sur fiche | **Non** : peutAcceder = OUI |
| Clôture auto sans rattachement | **Point ouvert** : pas d’auto-close fiable sans arbitrage C2 — ne bloque pas une V1 à clôture manuelle |

Diagnostic **poursuivable** vers implémentation après arbitrages listés.

## LEÇON

Ne pas confondre « colonne `echeance_at` existe » et « TaskSync accepte une
échéance saisie » — mesurer le flux L123–124. Ne pas poser N Tasks sur le
même taskable DemandeJuridique sans casser l’idempotence #1458. Suiveur =
lecteur légitime de la fiche (D4) : la feature est utilisable côté droits
lecture.
