26 KiB
Assistant V2.1 ObsiGate — Phase 0 : Socle sûr (purge, mémoire, politiques, exécution fiable)
| Champ | Valeur |
|---|---|
| Phase | 0 sur 5 — fondation de toute la solution |
| Document parent | ASSISTANT-V2.1.md — en particulier §2 (P1–P16), §5, §9, §10, §15, §16, §19 |
| Version | 1.0 — 9 octobre 2026 |
| Auteur | Spark, pour Bruno |
| Statut | Prêt à implémenter |
| Tranches | #191 (purge Ollama), #192 (mémoire socle amendée), #193 (miroir markdown), #213 (politiques & taint), #206 (exécution fiable), amorce de #212 (harnais golden tasks) |
| Effort estimé | 15–19 jours (dont 1–2 j d'amorce #212 ; le solde de #212 est continu, phases 1–5) |
| Prérequis | ObsiGate actuel : boucle agent backend/agent/loop.py, registre backend/tools/registry.py, scheduler #170, notifications #168, Web Push #67, recherche #70 — tels que décrits dans l'inventaire V2.1 §4 |
| Dépendances | Aucune (première phase) |
| Débloque | Phase 1 (tâches et planification s'appuient sur les runs et la mémoire), et toutes les écritures externes ultérieures (#213/#206 sont le prérequis de sécurité des phases 3–4) |
Résultat visible à la fin de cette phase. Le conteneur Ollama a disparu. L'assistant possède une mémoire long terme : il enregistre des faits (avec historique bi-temporel), des entités (personnes, projets, lieux) et des procédures, les montre dans
99_Assistant/Profil.md— éditable à la main, les éditions étant reprises comme des faits de confiance maximale — et les injecte dans chaque conversation via un compileur de contexte dont chaque section cite son origine. Surtout, tout ce que l'assistant exécute devient un run persistant : plan affiché avant les écritures, approbation par run, étapes journalisées avec preuves de post-condition, reprise sans doublon après interruption, annulation quand l'inverse existe. Et un moteur de politiques déterministe décide ce qui est autorisé, approuvé ou refusé — jamais le modèle.
Table des matières
- Objectif et principe directeur
- Périmètre
- État d'entrée — ce qui existe
- Modèle de données
- Tickets de travail
- API livrée par la phase
- Comportements détaillés
- Tests et golden tasks
- Critères d'acceptation de la phase
- Risques spécifiques et vigilance
- Ordre d'exécution
- Définition de « terminé »
1. Objectif et principe directeur
Le principe : on ne donne pas plus de pouvoir à l'assistant avant d'avoir rendu son pouvoir actuel sûr, traçable et réversible. La V2 livrait la mémoire puis les connecteurs ; la V2.1 intercale, dès la phase 0, les deux mécanismes qui rendent toutes les phases suivantes délégables : le moteur de politiques (#213 — qui a le droit de faire quoi, avec quelles données, venues d'où) et l'exécution fiable (#206 — plan, preuves, reprise, annulation).
Corollaires qui guident chaque ticket :
- La mémoire est un index du vault, pas un silo. Faits confirmés → miroir markdown ; la base ne détient en propre que les états de travail (
pending, historique de runs). - Un run est l'unité de tout. Conversation, tâche planifiée (phase 2), automatisation (phase 2), MCP : à la fin de la phase, toute exécution d'outils par un agent produit une ligne
agent_runset sesagent_steps. Les journaux épars d'aujourd'hui convergent là. - La politique est déterministe. Risque du registre × niveau d'autonomie × provenance × sensibilité × réversibilité → décision. Aucun appel LLM dans le chemin d'autorisation, jamais.
- Pas de succès sans preuve. Un run n'est
succeededque si ses post-conditions vérifiables sont vraies (§7.3).
2. Périmètre
2.1 Inclus
- #191 : retrait du service/volume Ollama des trois compose, défauts provider nettoyés, notes de migration (repris tel quel de la V2 §9).
- #192 :
backend/memory/— store SQLite amendé (faits bi-temporels, sensibilité, provenance, procédures), entités et relations, recall et compileur de contexte (§9.4 du parent), outilsmemory.*,entity_resolve,procedure_get, API mémoire, cartes de validation des faitspending(forme simple — la file unifiée arrive en phase 1, #203). - #193 :
mirror.py, dossier99_Assistant/,Profil.mdà balises gérées avec re-parse des éditions humaines,Journal.mddes consolidations,Personnes.mden coquille (le peuplement riche est en phase 4, #211). - #213 : provenance/taint sur tous les contenus entrant dans un contexte ou un run, moteur de politiques
backend/agent/policies.pyévalué avant chaque appel d'outil, règles du §15.2 du parent, journal des refus, tests d'injection. - #206 : plans d'exécution,
agent_runs/agent_stepsdansdata/assistant.sqlite3, idempotence et reprise, post-conditions déclarées dans le registre (verify), inverses (inverse), niveaux d'autonomie 0–3 et délégations bornées datées, API runs + progression SSE. - #212 (amorce) : harnais de golden tasks (
tests/golden/, fixtures de vault, connecteurs simulés) et les scénarios G3, G6, G7 de la phase.
2.2 Exclu (phases ultérieures)
- La consolidation nocturne (#194) : la phase 0 livre le store, le recall et la création de faits en conversation ; l'extraction automatique nocturne arrive en phase 2, quand
agent_prompt(#200) existe. - La vue Aujourd'hui, la file d'approbations unifiée et le centre de contrôle complet (#203, phase 1) : la phase 0 livre les cartes mémoire simples et une liste de runs basique dans les Configurations.
- Les tâches (#202, phase 1) et tout connecteur (#195+, phase 3).
- La politique « contenu connecteur » est livrée comme mécanisme (provenances
connector:<id>reconnues) ; son premier usage réel est en phase 3.
3. État d'entrée — ce qui existe
| Élément | État | Rôle en phase 0 |
|---|---|---|
| Boucle agent + tool calling natif | ✅ backend/agent/loop.py, backend/ai_chat.py |
Moteur des runs ; étendu, pas remplacé (P2) |
Registre @tool (risque/scope/confirmation/audit/rate-limit) |
✅ backend/tools/registry.py |
Reçoit les champs verify et la provenance ; la politique s'y branche |
| Historique de conversation persistant | ✅ #95, backend/ai_history.py |
Mémoire épisodique brute (source future de #194) |
| Recherche TF-IDF + sémantique RRF | ✅ #70 | Recall : fusion faits + recherche |
| Cache SQLite (patron) | ✅ backend/tools/webcache.py |
Modèle des stores SQLite de la phase |
| Audit + redaction des secrets | ✅ backend/audit.py, secret_redactor.py |
Journal des runs et des refus ; args toujours redactés |
| Providers LLM multi-fournisseurs | ✅ backend/ai.py |
Routage par type de job : amorce posée (§16.3 du parent), table complète en phases suivantes |
| Ollama comme conteneur | ✅ présent | Retiré par #191 ; le provider ollama reste une option de configuration (desktop local) |
4. Modèle de données
Repris du parent : facts amendée (§9.2 — entity_id, valid_from, valid_to, recorded_at, sensitivity, provenance_trust, kind étendu à procedure), entities et relations (§9.3), consolidation_runs (créée dès cette phase, alimentée en phase 2), agent_runs et agent_steps (§10.2), auxquelles s'ajoute la table des délégations :
CREATE TABLE delegations (
id TEXT PRIMARY KEY, user_id TEXT NOT NULL,
domain TEXT NOT NULL, -- 'vault'|'tasks'|'email'|'calendar'|…
level INTEGER NOT NULL, -- niveau accordé (0–3, §10.4 du parent)
scope_json TEXT, -- périmètre : chemins, connecteur, budgets
granted_at TEXT NOT NULL, review_at TEXT NOT NULL, -- défaut +90 j
status TEXT NOT NULL DEFAULT 'active' -- active|expired|revoked
);
Stores : data/memory.sqlite3 (faits, entités, relations, runs de consolidation) et data/assistant.sqlite3 (runs, steps, délégations ; recevra inbox_items, commitments, approvals en phases 1 et 4 — le fichier est créé ici, les tables arrivent avec leurs tranches).
Règles transverses : toute table porte user_id ; aucun secret ni contenu brut de connecteur dans ces bases ; les args d'étapes sont stockés redactés ; les stores sont reconstructibles ou journalisés (jamais de contenu de Bruno irremplaçable hors du vault — P4).
5. Tickets de travail
Convention : chaque ticket est indépendamment testable ; « Fichiers » donne les points d'ancrage d'après la V2.1 — à confirmer contre le code réel au démarrage du ticket.
Purge Ollama — #191
P0-T01 — Retrait du conteneur Ollama.
Fichiers : docker-compose.yml, docker-compose.test-linux.yml, docker-compose.test-win.yml, docs d'installation.
Travail : supprimer service, volume ollama_data, variables OLLAMA_BASE_URL/OLLAMA_MODEL et mentions du port 11434 ; procédure de suppression du volume documentée dans le CHANGELOG.
Acceptation : docker compose config ne connaît plus Ollama ; l'instance démarre et l'agent fonctionne sur deepseek/openrouter/gemini.
P0-T02 — Défauts provider nettoyés.
Fichiers : backend/ai.py, model_capabilities.py, tests d'isolation.
Travail : conserver le provider ollama comme option explicite (desktop), retirer tout défaut pointant vers le réseau interne ; le mode par défaut de la mémoire fonctionne sans embeddings (TF-IDF + RRF + faits).
Acceptation : aucun test ni défaut ne référence http://ollama:11434 ; la recherche sémantique se dégrade proprement sans sentence-transformers.
Mémoire socle — #192
P0-T03 — Store mémoire : faits amendés, entités, relations.
Fichiers : nouveau backend/memory/store.py (patron webcache.py), création/ouverture de data/memory.sqlite3.
Travail : DDL du §4 ; CRUD faits avec clôture bi-temporelle (supersede = valid_to, jamais d'écrasement) ; planchers de confiance V2 (< 0,7 → pending) ; dossiers sensibles configurables → pending + sensitivity='sensitive' ; renforcement last_seen_at et decay par kind (les procedure et decision ne décroissent pas).
Acceptation : G7 passe (changement de dentiste : ancien fait clos, nouveau actif, requête historique datée correctement répondue) ; aucun fait sensitive créé directement en confirmed.
P0-T04 — Entités et résolution de noms.
Fichiers : backend/memory/entities.py.
Travail : CRUD entités (person/place/project/org/topic, alias) ; resolve(name) → candidates scorées ; ambiguïté = pas de fusion (renvoyer les candidates, la décision est une carte, phase 2 pour la consolidation) ; relations bi-temporelles.
Acceptation : deux « Marie » distinctes coexistent sans fusion ; une relation fermée n'apparaît plus dans le recall actuel mais reste interrogeable à date.
P0-T05 — Recall et compileur de contexte.
Fichiers : backend/memory/recall.py, backend/memory/compiler.py.
Travail : score explicite et journalisé (RRF × confiance × fraîcheur × boost entité, parent §9.4) ; assemblage du bloc <assistant-context> à budget adaptatif (défaut 2 500 tokens) avec sections profil/procédures, entités du sujet, faits liés, engagements (vide cette phase), dernier briefing (absent cette phase — section omise proprement) ; chaque section cite son origine ; le bloc est visible dans les étapes du panneau avec le détail du score au survol.
Acceptation : un fait sous le seuil n'entre pas dans le bloc ; le même bloc est reproduit à l'identique pour la même requête et le même état (déterminisme de structure) ; aucun contenu sensitivity='sensitive' dans un contexte destiné à un outil externe.
P0-T06 — Outils mémoire et API.
Fichiers : backend/tools/memory.py, backend/routers/memory.py.
Travail : les 5 outils V2 (memory_recall, memory_save, memory_update, memory_forget, profile_get) + entity_resolve (READ) et procedure_get (READ) ; memory_save passe par la politique (approbation selon niveau, défaut 2) ; endpoints CRUD faits (liste/filtre statut, validation pending, édition, oubli) et export JSON complet du domaine mémoire.
Acceptation : un fait proposé en chat apparaît en carte, validé il est au miroir (T08) et au recall suivant ; memory_forget le retire du recall et du miroir, avec trace d'audit.
Miroir markdown — #193
P0-T07 — Miroir Profil.md à balises gérées.
Fichiers : backend/memory/mirror.py.
Travail : création de 99_Assistant/ dans le vault mémoire (OBSIGATE_MEMORY_VAULT, mode multi-utilisateur du §5.3 du parent) ; rendu des sections gérées (préférences, procédures, objectifs connus) ; hash de section stocké en base ; écritures idempotentes (rendu deux fois = fichier identique).
Acceptation : supprimer un fait validé régénère le miroir sans toucher au hors-balises ; deux rendus consécutifs produisent un diff vide.
P0-T08 — Re-parse des éditions humaines.
Travail : avant chaque écriture, comparer le hash stocké au contenu courant ; une section modifiée à la main est re-parsée en faits source='user', confiance 1.0, confirmed, puis régénérée ; une suppression dans le miroir = fait rejected en base ; le hors-balises n'est jamais touché.
Acceptation : éditer Profil.md à la main (ajout + suppression) puis déclencher un rendu donne des faits conformes aux éditions, sans perte du texte libre hors balises.
P0-T09 — Personnes.md (coquille) et Journal.md.
Travail : Personnes.md rendu depuis les entités personnes (nom, type de relation, dernière interaction connue) — en lecture structurée simple cette phase ; Journal.md alimenté par les événements mémoire (faits ajoutés/validés/rejetés, rendus du miroir) en lecture seule.
Acceptation : une entité personne créée en T04 apparaît au miroir ; le journal trace chaque validation de carte.
Politiques & taint — #213
P0-T10 — Provenance de bout en bout.
Fichiers : backend/agent/loop.py, backend/tools/registry.py, backend/ai_chat.py.
Travail : chaque contenu entrant dans un contexte ou un résultat d'outil reçoit une provenance (user, vault, connector:<id>, web, mcp:<serveur>, automation) portée dans les messages et les steps ; les résultats d'outils sont sanitisés (redaction existante) et étiquetés avant retour au modèle ; les prompts système distinguent explicitement blocs « instructions » et blocs « données ».
Acceptation : un test d'intégration montre la provenance présente sur chaque résultat d'outil d'un run ; aucun résultat n'est injecté sans étiquette.
P0-T11 — Moteur de politiques.
Fichiers : nouveau backend/agent/policies.py ; branchement dans le registre (évaluation avant chaque appel d'outil).
Travail : décision déterministe autorisé | approbation_run | approbation_renforcée | refusé = f(risque registre, niveau d'autonomie du domaine, provenance, sensibilité des données, réversibilité) ; règles du parent §15.2 (une instruction de contenu non-user ne crée/lève aucune approbation ; action externe justifiée uniquement par un contenu non-user → refus + carte ; données private|sensitive interdites vers web/connecteurs en écriture/MCP externe) ; journal des refus avec motif, lisible dans l'audit.
Acceptation : G3 passe (courriel piégé simulé → refus, carte à Bruno, zéro appel d'envoi) ; la décision est couverte par des tests unitaires exhaustifs sur la matrice des facteurs (aucun LLM dans le chemin).
P0-T12 — Registre étendu : verify, inverses, sensibilité des paramètres.
Fichiers : backend/tools/registry.py, outils existants (annotations).
Travail : champs optionnels du décorateur @tool : verify (post-condition vérifiable), inverse (constructeur d'inverse ou None explicite), data_classes (classes de données acceptées en entrée) ; annoter les outils vault et documents existants ; un outil d'écriture sans verify déclaré est listé dans un rapport de couverture (pas bloquant cette phase, bloquant pour les outils externes des phases 3+).
Acceptation : le rapport de couverture existe et est revu ; create_file/edit_file déclarent leur post-condition et leur inverse (backup).
Exécution fiable — #206
P0-T13 — Stores de runs.
Fichiers : backend/agent/runs.py, data/assistant.sqlite3.
Travail : DDL agent_runs/agent_steps du parent §10.2 ; cycle de vie complet des statuts ; écriture des steps au fil de l'exécution par la boucle existante (toute exécution d'outils crée ou rattache un run, origin renseignée) ; plafonds : itérations et budget tokens existants + durée murale et nombre d'écritures (OBSIGATE_RUN_MAX_WALL_SECONDS, OBSIGATE_RUN_MAX_WRITES).
Acceptation : tuer le processus pendant un run laisse un run running orphelin détectable au redémarrage (marqué paused reprenable, jamais succeeded).
P0-T14 — Plans d'exécution et approbation par run.
Travail : pour toute demande ≥ 2 écritures ou 1 outil DANGEROUS, génération d'un plan (étapes : outil, args résumés, effet attendu, post-condition, inverse) persisté dans plan_json ; affichage dans le panneau avant la première écriture ; validation en un clic = approbation du run entier (pas par étape) ; un plan refusé n'exécute rien et le run passe cancelled avec motif.
Acceptation : une demande multi-écritures ne produit aucun effet avant validation ; le plan affiché correspond exactement aux steps exécutés ensuite.
P0-T15 — Idempotence et reprise.
Travail : idempotency_key par step d'écriture (hash run+seq+args) respectée par les outils de création (vérification d'existence par clé en métadonnée) ; reprise d'un run interrompu à la première step non done, re-vérification des post-conditions incertaines, steps WRITE incertaines → awaiting_approval plutôt que rejeu aveugle.
Acceptation : G6 passe (run tué à mi-parcours, repris : aucun doublon créé, état final vérifié).
P0-T16 — Vérification et annulation.
Travail : exécution des verify après chaque step d'écriture ; statut final succeeded seulement si toutes les post-conditions vérifiables sont vraies, sinon partially_succeeded détaillé ; « Annuler ce run » exécute les inverses sûrs en ordre décroissant comme un run lié ; absence d'inverse affichée comme telle (« non annulable »).
Acceptation : un outil dont la post-condition échoue (fixture) produit partially_succeeded, jamais succeeded ; l'annulation d'un run de création de fichiers restaure l'état initial via les backups.
P0-T17 — Niveaux d'autonomie et délégations.
Fichiers : réglages Configurations, delegations (§4).
Travail : niveaux 0–3 par domaine (défauts : vault = 2, tout domaine externe = 0/1 selon existence) ; création de délégation bornée (périmètre + budget + review_at +90 j) depuis une approbation (« toujours autoriser ce périmètre ») ou les Configurations ; expiration automatique → retour au niveau inférieur ; aucun niveau ne couvre paiements, envois externes sans relecture, suppressions définitives, changements de sécurité (codé en politique, pas en réglage).
Acceptation : une délégation expirée cesse de s'appliquer sans intervention ; tenter de déléguer un envoi externe est refusé par l'UI et par l'API.
P0-T18 — API runs et progression.
Fichiers : backend/routers/runs.py.
Travail : GET /api/runs (filtres statut/origine), GET /api/runs/{id} (plan, steps, preuves), actions pause/resume/cancel/undo ; progression en SSE sur le flux existant ; liste de runs basique dans les Configurations (le centre de contrôle complet est en phase 1).
Acceptation : un run en cours est observable en direct ; pause puis reprise depuis l'API aboutit au même résultat qu'une exécution continue.
Évaluation — amorce #212
P0-T19 — Harnais golden tasks.
Fichiers : tests/golden/ (fixtures de vault, connecteurs simulés, harnais d'exécution headless).
Travail : infrastructure de scénarios rejouables en CI (état initial → demande → assertions sur les stores, le vault et les runs) ; livrer G3 (injection), G6 (reprise), G7 (bi-temporalité) ; gabarit documenté pour les phases suivantes (chaque phase ajoute les siens).
Acceptation : les trois scénarios tournent en CI et échouent si le comportement régresse (démontré par mutation volontaire en développement).
Finitions
P0-T20 — i18n, docs, guides.
Travail : clés FR/EN pour tout libellé nouveau (ai.step.* mémoire/runs/politiques, cartes, réglages d'autonomie) ; fiches docs/features/ pour #191, #192/#193, #213, #206 ; guide utilisateur « Mémoire et contrôle de l'assistant » ; CHANGELOG + ROADMAP.
Acceptation : DoD du §12 vérifiée point par point.
6. API livrée par la phase
| Route | Rôle | Ticket |
|---|---|---|
GET/POST/PATCH /api/memory/facts |
CRUD faits, validation des pending |
P0-T06 |
GET /api/memory/entities · /api/memory/export |
Entités ; export du domaine mémoire | P0-T04/T06 |
GET /api/runs · GET /api/runs/{id} |
Liste et détail (plan, steps, preuves) | P0-T18 |
| `POST /api/runs/{id}/pause | resume | cancel |
GET/POST/DELETE /api/delegations |
Délégations bornées par domaine | P0-T17 |
Toutes suivent l'authentification et les permissions par vault existantes ; les nouveaux outils @tool sont exposés au MCP automatiquement (scopes IN_APP+MCP, propose/apply déjà gérés).
7. Comportements détaillés
7.1 Qui peut faire quoi
La mémoire, les runs et les délégations sont par utilisateur (user_id partout, parent §5.3). Un administrateur ne voit pas la mémoire d'un autre utilisateur ; l'audit montre les refus de politique sans le contenu sensible qui les a motivés.
7.2 Fait pending : durée de vie
Un fait pending reste en carte jusqu'à action (valider/éditer/oublier) — pas d'expiration silencieuse. Au-delà de 30 jours, il est regroupé dans un rappel hebdomadaire unique (pas une notification par fait ; le budget d'interruption P15 s'applique dès cette phase aux cartes).
7.3 Post-conditions : la règle du succès
verify absent ≠ succès présumé : le step est marqué verified = NULL et le run ne peut prétendre qu'à succeeded si aucun step d'écriture n'est simultanément done et non vérifié alors qu'un verify existait. Le rapport de couverture (P0-T12) rend la dette visible au lieu de la cacher.
7.4 Reprise après crash du conteneur
Au démarrage, tout run running sans heartbeat récent passe paused avec le motif interrupted ; la reprise est manuelle (API ou panneau) cette phase — la reprise automatique des runs origin='scheduled' arrive avec #200 en phase 2.
8. Tests et golden tasks
- Unitaires : store mémoire (clôture bi-temporelle, decay, planchers), résolution d'entités (ambiguïté), moteur de politiques (matrice complète des facteurs), idempotence (rejouer une step ne crée pas de doublon), miroir (idempotence du rendu, re-parse).
- Intégration : run complet avec plan → approbation → exécution → vérification ; kill en cours de run → reprise ; édition manuelle de
Profil.md→ faitssource='user'. - Golden tasks de la phase : G3 (injection), G6 (reprise), G7 (bi-temporalité) — définitions au parent §16.1.
- Non-régression : l'assistant actuel (sans mémoire activée) se comporte à l'identique ;
OBSIGATE_MEMORY_ENABLED=falseéteint la phase proprement.
9. Critères d'acceptation de la phase
- Le conteneur Ollama n'existe plus et l'assistant fonctionne sur les providers API.
- Un fait dit en conversation, validé par carte, est dans
Profil.md, au recall de la conversation suivante, et son remplacement ultérieur conserve l'historique interrogeable à date. - Éditer
Profil.mdà la main modifie la mémoire (sourceuser, confiance 1.0) ; rien hors balises n'est jamais écrasé. - Un run multi-écritures affiche son plan, n'exécute rien avant approbation, prouve chaque post-condition, et un run tué reprend sans doublon.
- Le scénario d'injection G3 est refusé par la politique, journalisé, et présenté à Bruno en carte.
- Les golden tasks G3/G6/G7 sont verts en CI.
10. Risques spécifiques et vigilance
| Risque | Vigilance |
|---|---|
| Le compileur de contexte consomme le budget du modèle en silence | Le bloc est plafonné, son détail est visible (P0-T05) ; la métrique « tokens de contexte mémoire / conversation » est journalisée dès cette phase (alimente #212) |
| Approbation par run trop grossière (tout ou rien) | Le plan affiche chaque step ; un step peut être exclu du plan avant validation (case par step) — l'approbation reste unique, le périmètre est ajustable |
| Le miroir et une édition collaborative (Yjs) se marchent dessus | Le miroir écrit via le même chemin de sauvegarde que l'éditeur (backup préalable) et compare les hashs avant/après (règle durcie en phase 2 pour la consolidation) |
| Dérive du périmètre : vouloir les tâches dès la phase 0 | Les tâches sont la phase 1 ; la phase 0 ne livre que les fondations qu'elles consommeront (runs, politique, mémoire) |
11. Ordre d'exécution
P0-T01 → T02 (#191, indépendant, à faire en premier)
P0-T03 → T04 → T05 → T06 (#192)
P0-T07 → T08 → T09 (#193, après T03/T06)
P0-T10 → T11 → T12 (#213, T10 tôt : la provenance sert les runs)
P0-T13 → T14 → T15 → T16 → T17 → T18 (#206, après T11)
P0-T19 (harnais tôt, scénarios au fil des tickets) → P0-T20 (clôture)
12. Définition de « terminé »
DoD V2 §13 (tests unitaires + non-régression, JSDOM si UI, ruff/mypy 0 erreur, i18n FR/EN, CHANGELOG, ROADMAP, fiche par tranche, guide si exposé, CI verte, commit conventionnel référençant l'ID) plus les ajouts V2.1 (parent §21) : golden tasks du périmètre vertes ; toute écriture déclare sa post-condition et son inverse ou son absence ; toute source de contenu déclare sa provenance.