Architecture OCR/IDP incrémental -- Révise Mieux¶
Document d'architecture technique pour le pipeline d'ingestion incrémental de contenu scolaire. Dernière mise à jour : 2026-04-03.
1. Executive Summary¶
Révise Mieux ingère des photos de cahiers de collégiens pour en faire des assets de révision structurés. Le problème central n'est pas l'OCR lui-même -- c'est l'ingestion incrémentale dans le temps : un élève ajoute des pages jour après jour, re-photographie des pages floues, et complète son cours sur des semaines. Le système doit intégrer chaque batch sans régénérer le chapitre entier, tout en maintenant la cohérence des Notions, Items et Mastery existants.
Ce document définit : (1) un format pivot canonique entre OCR et LLM, (2) le workflow d'ingestion incrémentale en 10 étapes, (3) les règles de merge avec seuils concrets, (4) l'impact sur les Mastery existants, (5) l'intégration avec le bounded context Validation (HITL), et (6) les signatures Go et migrations SQL nécessaires.
Modèles retenus : Gemini 2.5 Flash (OCR, $0.001/page) + mistral-small ou Qwen3.5-397B (IDP, $0.00006-0.00011/item). Pipeline local viable via Qwen3-VL-32B + Gemma 3 27B ($0, ~2 min/chapitre).
2. Cadrage du problème¶
2.1 OCR éducatif vs. extraction documentaire générique¶
L'OCR/IDP éducatif diffère de l'extraction de factures ou de formulaires sur plusieurs axes fondamentaux :
| Dimension | IDP générique (factures) | IDP éducatif (cahiers) |
|---|---|---|
| Structure | Champs fixes, positions connues | Structure libre, variable selon le prof |
| Contenu | Données factuelles (montants, dates) | Connaissances sémantiques (définitions, procédures, concepts) |
| Visuels | Logos, signatures (non pertinents) | Schémas porteurs de sens (cellule, circuit, carte) |
| Manuscrit | Rare (signatures) | Dominant (notes élève, corrections prof) |
| Temporalité | Document unique, ponctuel | Cours construit sur des semaines, incrémental |
| Qualité cible | Extraction exacte de champs | Compréhension sémantique pour générer des exercices |
| Conséquence erreur | Montant incorrect (corrigible) | Élève qui apprend une erreur (pédagogiquement dangereux) |
2.2 Ce qui rend l'ingestion incrémentale difficile¶
- Uploads partiels : 3 pages lundi, 2 mercredi, 1 vendredi -- le cours est construit dans le temps.
- Pages dans le désordre : l'élève photographie la page 5 avant la page 2.
- Contenu dupliqué : re-photographie d'une page floue, ou même notion vue dans deux parties du cours.
- Contenu contradictoire : corrections du prof qui contredisent la version initiale.
- Incertitude OCR variable : confiance 0.95 sur du texte imprimé, 0.4 sur du manuscrit raturé.
- Schémas porteurs de sens : un schéma de cellule sans son texte OCR perd 50% de sa valeur pédagogique.
- Traçabilité obligatoire : chaque Item doit pouvoir pointer vers sa page source (Z2-AC13).
- Stabilité des identifiants : les Notion IDs et Item IDs ne doivent pas changer quand de nouvelles pages arrivent.
2.3 Risques d'une architecture mal conçue¶
- Fragmentation progressive : le cours se décompose en sous-ensembles incohérents au fil des uploads.
- Doublons Mastery : un Item dupliqué produit deux Mastery indépendants pour la même connaissance.
- Régression silencieuse : un re-processing complet change les Item IDs, ce qui reset tous les Mastery.
- Coût exponentiel : re-traiter tout le chapitre à chaque ajout de page (O(n) au lieu de O(delta)).
- Dérive pédagogique : des Items contradictoires coexistent, l'élève étudie du contenu faux.
3. Architecture recommandée¶
3.1 Architecture 2 étages avec format pivot¶
graph TD
subgraph "Étage 1 : Extraction (vision)"
A[Photo page] -->|S3 upload| B[Gemini 2.5 Flash / Qwen3-VL-32B]
B -->|Markdown structuré| C[PivotDocument OCR layer]
end
subgraph "Étage 2 : Structuration (texte)"
C -->|Blocs texte + descriptions visuelles| D[mistral-small / Qwen3.5-397B]
D -->|Items + Notions| E[PivotDocument pedagogical layer]
end
subgraph "Étage 3 : Merge incrémental"
E -->|Nouveaux items| F{Merge Engine}
G[Existing PivotDocument] -->|État actuel| F
F -->|append/enrich/replace/flag| H[Updated PivotDocument]
F -->|conflicts| I[ValidationTask HITL]
end
subgraph "Downstream"
H -->|Domain events| J[Mastery creation/update]
H -->|Cache invalidation| K[Question lazy generation]
I -->|HITL resolution| H
end
3.2 Couches immutables vs mutables¶
Le format pivot sépare trois couches avec des règles de mutabilité distinctes :
| Couche | Mutabilité | Contenu | Justification |
|---|---|---|---|
| Observations brutes | Immutable | OCR text, bounding boxes, confiance, timestamps | Traçabilité. On ne modifie jamais ce que l'OCR a vu. |
| Structure pédagogique | Mutable enrichissable | Notions, Items, hiérarchie de sections | Enrichie quand de nouvelles pages arrivent. Les IDs sont stables. |
| Outputs dérivés | Régénérable | Questions, fiches, quiz, cache Redis | Invalidés et régénérés à la demande (lazy generation). |
Principe : les observations brutes (table blocks) ne sont jamais modifiées après création. Elles servent de "ground truth" pour la traçabilité. La structure pédagogique (tables items, notions) évolue par merge incrémental. Les outputs dérivés (table questions, cache Redis) sont invalidés et régénérés quand la structure change.
4. Design du format pivot canonique¶
4.1 Principes¶
- JSON -- machine-friendly, inspectable, compatible avec tous les LLM.
- Enveloppe par Chapter -- un PivotDocument = un Chapter complet à un instant t.
- Versionné -- chaque mutation incrémente
pivot_version. - Traçable -- chaque unité extraite pointe vers ses
source_refs(Page ID + bbox). - Confiance explicite --
confidencesur chaque bloc et item, utilisée pour les décisions de merge. - Compatible context windows -- le pivot d'un chapitre de 10 pages tient dans ~8K tokens (compatible Gemma 3 27B, 128K context).
4.2 Structure du PivotDocument¶
Le PivotDocument n'est pas une table SQL séparée. C'est le modèle conceptuel qui décrit comment les tables existantes (chapters, chapter_revisions, pages, blocks, items, notions) s'articulent pour former une vue canonique du cours. Le JSON Schema (voir pivot-format-schema.json) décrit cette vue pour le debugging, les tests, et l'export.
Les tables SQL existantes restent la source de vérité. Le PivotDocument est une projection assemblée à la demande.
4.3 Sections du pivot¶
PivotDocument
├── metadata (chapter_id, subject, class_level, title, pivot_version, updated_at)
├── capture_sessions[] (immutable)
│ ├── revision_id, captured_at
│ └── pages[]
│ ├── page_id, page_order, photo_url, ocr_status, ocr_confidence
│ └── blocks[] (immutable observations)
│ ├── block_id, block_type, ocr_text, confidence, bbox
│ └── visual_metadata? (labels, table_structure, caption)
├── notions[] (mutable, stable IDs)
│ ├── notion_id, name, concept_tags[], sort_order
│ └── items[]
│ ├── item_id, item_type, term, keywords[], steps[], confidence
│ ├── source_refs[] (page_id + block_ids → traçabilité)
│ ├── fidelity_score, validation_required, archived
│ └── merge_status (original | enriched | replaced | conflict)
└── merge_log[] (audit trail)
├── timestamp, action (append | enrich | replace | archive_dup | flag_conflict)
├── source_item_id, target_item_id?
└── reason
5. JSON Schema et exemples¶
Voir les fichiers compagnons :
- pivot-format-schema.json -- JSON Schema complet
- pivot-format-example-photosynthese.json -- Exemple concret sur "La photosynthèse" avec 3 sessions de capture
6. Workflow d'ingestion incrémentale¶
6.1 Diagramme de séquence¶
sequenceDiagram
participant E as Élève
participant API as Backend API
participant S3 as Object Storage
participant OCR as Gemini Flash / Qwen3-VL
participant IDP as mistral-small / Qwen3.5
participant Merge as Merge Engine
participant DB as PostgreSQL
participant Redis as Redis Cache
participant HITL as Validation BC
E->>API: POST /chapters/{id}/pages (3 nouvelles photos)
API->>S3: Upload photos
API->>DB: Insert pages (status=UPLOADING)
API-->>E: 202 Accepted (SSE stream started)
loop Pour chaque nouvelle page
API->>OCR: ProcessPage(image_url)
OCR-->>API: OCRResult{blocks[], confidence}
API->>DB: Insert blocks (immutable)
API->>DB: Update page status → PROCESSED
API-->>E: SSE: page_processed {page_id, block_count}
end
API->>IDP: StructureBlocks(new_blocks, existing_context)
IDP-->>API: StructurationResult{items[], notions[]}
API->>DB: Load existing items + notions for chapter
API->>Merge: MergeIncremental(existing, new)
Merge-->>API: MergeResult{appended, enriched, replaced, conflicts, duplicates}
alt Conflits détectés
API->>HITL: CreateValidationTasks(conflicts)
API->>DB: Flag conflicting items
end
API->>DB: Save merged items + notions
API->>Redis: Invalidate question cache (impacted item_ids only)
API->>DB: Publish domain events (ItemsGenerated, NotionMerged, etc.)
API-->>E: SSE: merge_complete {new_items, updated_notions}
6.2 Détail des 10 étapes¶
Étape 1 : Upload et stockage (S3 + pages table)¶
- Toujours recalculé : upload S3, création entrées
pages. - Réutilisable : rien (nouvelles pages).
- Invalidé : rien encore.
Les photos sont stockées dans S3 avec la clé chapters/{chapter_id}/revisions/{revision_id}/pages/{page_order}.webp. Les métadonnées sont insérées dans la table pages avec ocr_status = UPLOADING.
Étape 2 : Rattachement au Chapter existant¶
Les nouvelles pages sont rattachées à la Revision courante du Chapter (Z2-AC14 : pas de nouvelle Revision pour un ajout incrémental). L'page_order continue depuis le dernier numéro existant.
- Réutilisé : le Chapter et la Revision existants.
- Invalidé : rien.
Étape 3 : OCR / analyse de layout (VLM page entière)¶
Chaque page est envoyée au VLM (Gemini 2.5 Flash en API, Qwen3-VL-32B en local) qui produit du Markdown structuré avec descriptions des visuels.
- Toujours recalculé : OCR sur les nouvelles pages uniquement (Z2-AC14).
- Réutilisé : les résultats OCR des pages existantes (jamais re-OCRisées).
- Invalidé : rien encore.
Étape 4 : Extraction de blocs (table blocks)¶
Les blocs OCR sont insérés dans la table blocks. Ils sont immutables après insertion.
- Toujours recalculé : parsing du Markdown OCR en blocs structurés.
- Réutilisé : blocs des pages existantes.
- Invalidé : rien.
Étape 5 : Structuration pédagogique (LLM IDP)¶
Le LLM de structuration (mistral-small / Qwen3.5-397B) reçoit les blocs des nouvelles pages plus un résumé du contexte existant (noms des Notions existantes, termes des Items existants) pour assurer la cohérence.
Prompt IDP enrichi (mode incrémental) :
- System: instructions d'extraction + schéma JSON
- Context: "Ce chapitre contient déjà les notions : [liste]. Items existants : [termes]."
- Input: blocs OCR des nouvelles pages
- Output: nouveaux items + rattachement aux notions existantes ou nouvelles
- Toujours recalculé : structuration des nouveaux blocs.
- Réutilisé : items et notions existants (fournis en contexte, pas re-générés).
- Invalidé : rien directement.
Étape 6 : Matching avec les structures existantes¶
Le Merge Engine compare les nouveaux items avec les existants en utilisant :
1. CanonicalKey(term, item_type, pack_id) -- matching exact (identité.go existant).
2. Similarité cosinus sur les keywords (Jaccard > 0.7) -- matching sémantique léger.
3. Confiance relative -- pour décider append vs replace.
Voir la section 7 (Règles de merge) pour les seuils détaillés.
Étape 7 : Merge / append / split / gestion de conflits¶
Le résultat du merge produit 6 types d'actions possibles (voir merge-decision-table.md).
- Toujours recalculé : la comparaison nouveaux vs existants.
- Réutilisé : les Items existants non impactés restent inchangés.
- Invalidé : cache Redis des questions pour les items modifiés/enrichis.
Étape 8 : Review basée sur la confiance (HITL)¶
Les items avec merge_status = conflict ou confidence < 0.5 génèrent des ValidationTask dans le bounded context Validation.
source = 'coherence_check'pour les contradictions (Z3-AC11).source = 'fidelity_check'pour les items de faible fidélité (Z3-AC10).
Étape 9 : Invalidation ciblée du cache¶
Seules les questions liées aux items impactés sont invalidées (Z3-AC07 : invalidation ciblée, pas globale).
Redis keys invalidated:
questions:item:{item_id} -- pour chaque item modifié/enrichi/remplacé
Les questions des items non impactés restent en cache (TTL 24h inchangé).
Étape 10 : Publication des domain events¶
| Event | Condition | Consommateurs |
|---|---|---|
ItemsGenerated |
Nouveaux items créés (append) | Mastery BC (crée Mastery UNKNOWN) |
ItemsEnriched |
Items existants enrichis | Cache invalidation |
ItemArchived |
Doublon détecté et archivé | Mastery BC (transfert) |
ConflictDetected |
Contradiction entre items | Validation BC (crée ValidationTask) |
NotionMerged |
Nouvelle notion fusionnée avec existante | UI refresh |
BatchProcessed |
Fin du traitement incrémental | SSE notification élève |
7. Règles de merge / résolution de conflits¶
Voir merge-decision-table.md pour le tableau complet avec seuils.
7.1 Résumé des règles¶
| Situation | Action | Seuil | Automatique ? |
|---|---|---|---|
| Nouveau concept | Append | Aucun match (CanonicalKey miss + Jaccard < 0.5) | Oui |
| Même terme, détails supplémentaires | Enrich | CanonicalKey match + new keywords not in existing | Oui |
| Même terme, meilleure confiance | Replace | CanonicalKey match + new.confidence > old.confidence + 0.15 |
Oui |
| Même terme, définitions contradictoires | Flag conflict | CanonicalKey match + fidelity check contradiction | Non (HITL) |
| Même terme + même keywords | Mark duplicate | CanonicalKey match + Jaccard(keywords) > 0.85 | Oui (archive lower confidence) |
| Terme proche mais pas identique | Create alternative | Jaccard(keywords) 0.5-0.85 + terms differ | Non (HITL si confidence < 0.7) |
8. Continuité du cours dans le temps¶
8.1 Heuristiques de classification des nouvelles pages¶
Quand de nouvelles pages arrivent, le système doit déterminer leur nature :
| Classification | Heuristique | Action |
|---|---|---|
| Enrichissement du même cours | Même chapter_id (explicite par l'élève via Z5-AC11) |
Merge incrémental |
| Re-photographie | Similarité OCR > 0.8 avec une page existante du même chapitre | Proposer remplacement si meilleure confiance |
| Fiche de correction | Détection de mots-clés : "correction", "exercice", "corrigé" + contexte temporal post-cours | Tag is_correction = true, items type PROCEDURE |
| Doublon inter-session | Même CanonicalKey que des items existants | Merge automatique (archive le doublon) |
| Enrichissement tardif | Upload > 7 jours après le dernier, même chapitre | Merge incrémental + notification "Chapitre mis à jour" |
8.2 Points de validation humaine¶
Le système demande confirmation à l'élève quand : 1. Une page ressemble fortement à une page existante (similarité > 0.8) : "Cette page ressemble à la page 3 existante. Remplacer ou ajouter ?" 2. Le contenu semble appartenir à un autre chapitre (LLM detect subject mismatch) : "Ce contenu parle de [sujet]. L'ajouter à [chapitre actuel] ou créer un nouveau chapitre ?" 3. Plus de 30 jours entre le dernier upload et le nouveau : "Ce chapitre n'a pas été mis à jour depuis [N] jours. Ajouter des pages ou créer un nouveau chapitre ?"
8.3 Stabilité des Notion IDs¶
Les Notion IDs sont persistants. Quand de nouvelles pages arrivent : - Le LLM IDP reçoit les noms des Notions existantes en contexte. - S'il produit un item qui correspond à une Notion existante, il rattache l'item à cette Notion (par nom). - Si l'item relève d'un concept nouveau, une nouvelle Notion est créée. - Les Notion IDs ne changent jamais. Les noms de Notions peuvent être enrichis (ex: "Photosynthèse" -> "Photosynthèse et respiration cellulaire") si le contenu ajouté justifie un élargissement.
9. Impact sur les Mastery¶
9.1 Tableau de décision¶
| Événement | Impact Mastery | Domain event | Justification |
|---|---|---|---|
| Nouvel Item (append) | Mastery créé à UNKNOWN | ItemsGenerated |
Nouveau contenu = nouvelle compétence à acquérir |
| Item enrichi (enrich) | Mastery inchangé | ItemsEnriched |
L'item reste le même, il est juste plus complet. Le Mastery reflète la maîtrise du concept, pas du détail. |
| Item remplacé (replace) | Mastery transféré de l'ancien vers le nouveau | ItemReplaced |
L'ancien item est archivé. Le Mastery est conservé car le concept est le même, seule la formulation change. |
| Item archivé (doublon) | Mastery du doublon transféré vers l'item survivant (Z3-AC11) | ItemArchived |
Le Mastery le plus avancé (state + consecutive_successes) est conservé. |
| Item splitté | Mastery de l'item source copié vers les deux items résultants, rétrogradé de 1 niveau | ItemSplit |
Le split crée de l'incertitude : l'élève maîtrisait le concept global mais pas nécessairement les deux sous-concepts. |
| Conflit détecté | Mastery gelé (pas de transition) tant que non résolu | ConflictDetected |
Un item sous investigation ne doit pas progresser (Z3-AC12). |
9.2 Transfert de Mastery¶
// TransferMastery transfers mastery state from a source item to a target item.
// If both have masteries, the more advanced state is kept.
// Used when archiving duplicates (Z3-AC11) or replacing items.
func (s *MasteryService) TransferMastery(ctx context.Context, sourceItemID, targetItemID uuid.UUID) error
Règles de transfert :
- Si seul le source a un Mastery -> transfert direct.
- Si les deux ont un Mastery -> on conserve le plus avancé (SOLID > OK > FRAGILE > UNKNOWN).
- À état égal, on conserve celui avec le plus de consecutive_successes.
- Le Mastery source est soft-deleted (conservé pour audit).
10. Workflow de review humaine (HITL)¶
10.1 Intégration avec ValidationTask¶
Le pipeline incrémental crée des ValidationTask dans les cas suivants :
| Cas | source |
priority |
Résolution |
|---|---|---|---|
| Contradiction entre items (Z3-AC11) | coherence_check |
HIGH | Élève/parent choisit la bonne version |
| Fidelity score < 0.5 (Z3-AC10) | fidelity_check |
MEDIUM | Confirmer/corriger l'item |
| Re-photographie ambiguë | merge_conflict |
LOW | Confirmer remplacement ou ajout |
| Merge incertain (Jaccard 0.5-0.7) | merge_conflict |
LOW | Confirmer fusion ou séparation |
10.2 Seuils de confiance pour HITL¶
confidence >= 0.85 → Pas de HITL (Z3-AC09)
confidence 0.50-0.84 → Item utilisable, gabarits simples uniquement (Z3-AC01)
confidence < 0.50 → HITL obligatoire, item en attente
OCR confidence < 0.30 → Page marquée BLURRY (Z2-AC03), suggestion re-photographie
10.3 File de validation¶
La file respecte la contrainte Z2-AC09 (max 8 items) : - Les items les plus impactants (proches d'un exam, état FRAGILE) sont priorisés. - Les items de merge incrémental ont une priorité inférieure aux items de premier pipeline. - Le parent en mode actif reçoit max 3 items/semaine (Z3-AC08).
11. Impact sur la génération downstream¶
11.1 Invalidation ciblée¶
Quand le merge incrémental modifie des items :
| Action merge | Questions impactées | Invalidation |
|---|---|---|
| Append (nouvel item) | Aucune existante | Pas d'invalidation. Nouvelles questions générées lazy. |
| Enrich (item enrichi) | Questions de cet item | Invalidation questions:item:{id}. Régénération lazy. |
| Replace | Questions de l'ancien item | Invalidation. L'ancien item est archivé, nouvelles questions sur le nouveau. |
| Archive dup | Questions du doublon archivé | Invalidation. Questions du survivant inchangées. |
| Conflict | Questions des deux items | Invalidation. Gabarits restreints (Z3-AC01) en attendant résolution. |
11.2 Articulation avec le bounded context Session¶
Le Session BC consomme les items via le port chapter.Repository.FindItemsByChapter(). Les items archivés sont filtrés (WHERE NOT archived). Les items en conflit sont restreints aux gabarits simples. Le Session BC n'a pas besoin de connaître le format pivot -- il opère sur les entités Item et Notion existantes.
12. Signatures Go des méthodes domaine¶
12.1 Nouveaux domain events¶
// backend/internal/domain/event/events.go
// ItemsEnriched is emitted when existing items are enriched with new content.
type ItemsEnriched struct {
BaseEvent
ChapterID uuid.UUID
ItemIDs []uuid.UUID
}
func (e ItemsEnriched) EventName() string { return "items.enriched" }
// ItemReplaced is emitted when an item is replaced by a better extraction.
type ItemReplaced struct {
BaseEvent
ChapterID uuid.UUID
OldItemID uuid.UUID
NewItemID uuid.UUID
Reason string // "higher_confidence", "re_photograph"
}
func (e ItemReplaced) EventName() string { return "item.replaced" }
// ItemArchived is emitted when a duplicate item is archived.
type ItemArchived struct {
BaseEvent
ChapterID uuid.UUID
ArchivedID uuid.UUID
SurvivorID uuid.UUID
}
func (e ItemArchived) EventName() string { return "item.archived" }
// ConflictDetected is emitted when contradictory items are found.
type ConflictDetected struct {
BaseEvent
ChapterID uuid.UUID
ItemIDs []uuid.UUID
Reason string // "contradiction", "ambiguous_merge"
}
func (e ConflictDetected) EventName() string { return "conflict.detected" }
// NotionMerged is emitted when a new notion is merged into an existing one.
type NotionMerged struct {
BaseEvent
ChapterID uuid.UUID
SourceID uuid.UUID
TargetID uuid.UUID
}
func (e NotionMerged) EventName() string { return "notion.merged" }
// BatchProcessed is emitted when an incremental batch finishes processing.
type BatchProcessed struct {
BaseEvent
ChapterID uuid.UUID
RevisionID uuid.UUID
PageIDs []uuid.UUID
NewItems int
Enriched int
Conflicts int
}
func (e BatchProcessed) EventName() string { return "batch.processed" }
12.2 Merge Engine (domain service)¶
// backend/internal/domain/chapter/merge.go
// MergeAction represents the type of merge operation performed.
type MergeAction string
const (
MergeAppend MergeAction = "APPEND"
MergeEnrich MergeAction = "ENRICH"
MergeReplace MergeAction = "REPLACE"
MergeFlagConflict MergeAction = "FLAG_CONFLICT"
MergeArchiveDup MergeAction = "ARCHIVE_DUP"
MergeCreateAlt MergeAction = "CREATE_ALT"
)
// MergeDecision represents a single merge decision for one item.
type MergeDecision struct {
Action MergeAction
NewItem *StructuredItem
ExistingItem *Item // nil for APPEND
Confidence float32
Reason string
}
// MergeResult holds the complete result of an incremental merge operation.
type MergeResult struct {
Appended []*Item
Enriched []*Item
Replaced []ItemReplacement
Archived []ItemArchival
Conflicts []ItemConflict
}
// ItemReplacement records a replacement decision.
type ItemReplacement struct {
OldItem *Item
NewItem *Item
Reason string
}
// ItemArchival records a duplicate archival.
type ItemArchival struct {
Archived *Item
Survivor *Item
}
// ItemConflict records a detected conflict requiring HITL.
type ItemConflict struct {
Items []*Item
Reason string // "contradiction", "ambiguous_merge"
}
// MergeEngine performs incremental merge of new items into existing chapter content.
// It is a pure domain service with no infrastructure dependencies.
type MergeEngine struct{}
// NewMergeEngine creates a new MergeEngine.
func NewMergeEngine() *MergeEngine {
return &MergeEngine{}
}
// MergeIncremental compares new structured items against existing items
// and produces merge decisions.
// existing: current items in the chapter (non-archived).
// incoming: newly extracted items from the IDP stage.
// packID: optional pack identifier for canonical key computation.
func (m *MergeEngine) MergeIncremental(
existing []*Item,
incoming []StructuredItem,
packID *string,
) *MergeResult
12.3 Enhanced LLMService port¶
// backend/internal/domain/chapter/ports.go (extended)
// LLMService is the port for LLM-based structuration.
type LLMService interface {
// StructureBlocks takes OCR text blocks and produces structured items and notions.
StructureBlocks(ctx context.Context, subject string, blocks []OCRBlock) (*StructurationResult, error)
// StructureBlocksIncremental takes OCR text blocks and existing context
// to produce structured items aligned with existing notions.
StructureBlocksIncremental(
ctx context.Context,
subject string,
newBlocks []OCRBlock,
existingNotions []string, // names of existing notions
existingTerms []string, // terms of existing items
) (*StructurationResult, error)
}
// CoherenceChecker is the port for detecting contradictions between items (Z3-AC11).
type CoherenceChecker interface {
// CheckCoherence compares new items against existing items for contradictions.
CheckCoherence(
ctx context.Context,
existingItems []*Item,
newItems []*Item,
) ([]CoherenceIssue, error)
}
// CoherenceIssue represents a detected coherence problem.
type CoherenceIssue struct {
Type string // "contradiction", "duplicate"
ItemIDs []uuid.UUID
Detail string
Severity float32 // 0.0-1.0
}
12.4 Application service orchestration¶
// backend/internal/app/pipeline_service.go (extended)
// ProcessIncrementalBatch orchestrates the incremental pipeline for new pages.
// It is the main entry point for Z2-AC14.
func (s *PipelineService) ProcessIncrementalBatch(
ctx context.Context,
chapterID uuid.UUID,
pageIDs []uuid.UUID,
) (*event.BatchProcessed, error)
// The method:
// 1. Loads existing items and notions for the chapter
// 2. Runs OCR on new pages (ProcessPage per page)
// 3. Calls StructureBlocksIncremental with existing context
// 4. Runs MergeEngine.MergeIncremental
// 5. Runs CoherenceChecker.CheckCoherence (new vs existing only, Z2-AC14)
// 6. Runs FidelityChecker.CheckFidelity on new items
// 7. Creates ValidationTasks for conflicts and low-fidelity items
// 8. Saves merged results
// 9. Invalidates Redis cache for impacted items
// 10. Publishes domain events
13. Migrations SQL¶
13.1 Nouvelles colonnes¶
-- Migration: 003_incremental_merge.sql
-- Add merge tracking to items
ALTER TABLE items ADD COLUMN merge_status TEXT NOT NULL DEFAULT 'original'
CHECK (merge_status IN ('original', 'enriched', 'replaced', 'conflict', 'alternative'));
ALTER TABLE items ADD COLUMN replaced_by_id UUID REFERENCES items(id);
ALTER TABLE items ADD COLUMN source_block_ids UUID[] NOT NULL DEFAULT '{}';
-- Add similarity tracking for deduplication
ALTER TABLE items ADD COLUMN canonical_key TEXT;
CREATE INDEX idx_items_canonical_key ON items(canonical_key) WHERE NOT archived;
-- Track which blocks contributed to which items (many-to-many)
CREATE TABLE item_source_blocks (
item_id UUID NOT NULL REFERENCES items(id) ON DELETE CASCADE,
block_id UUID NOT NULL REFERENCES blocks(id) ON DELETE CASCADE,
PRIMARY KEY (item_id, block_id)
);
CREATE INDEX idx_item_source_blocks_block ON item_source_blocks(block_id);
-- Merge audit log
CREATE TABLE merge_log (
id UUID PRIMARY KEY,
chapter_id UUID NOT NULL REFERENCES chapters(id) ON DELETE CASCADE,
action TEXT NOT NULL CHECK (action IN ('append', 'enrich', 'replace', 'archive_dup', 'flag_conflict', 'create_alt')),
source_item_id UUID REFERENCES items(id),
target_item_id UUID REFERENCES items(id),
reason TEXT NOT NULL,
confidence REAL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_merge_log_chapter ON merge_log(chapter_id, created_at);
-- Page similarity tracking for re-photograph detection
ALTER TABLE pages ADD COLUMN text_fingerprint TEXT; -- hash of OCR text for quick similarity check
CREATE INDEX idx_pages_fingerprint ON pages(text_fingerprint) WHERE text_fingerprint IS NOT NULL;
-- Add processing_incremental status to revision_status enum
-- Note: PostgreSQL ALTER TYPE ADD VALUE is not transactional, must be separate
ALTER TYPE revision_status ADD VALUE IF NOT EXISTS 'PROCESSING_INCREMENTAL';
13.2 Backfill canonical keys¶
-- Migration: 004_backfill_canonical_keys.sql
-- Run once after deploying merge support.
-- Canonical keys are computed in Go and written back.
-- This migration adds a trigger placeholder; actual backfill is done by the Go migration job.
-- No-op SQL migration. Backfill is performed by:
-- go run cmd/migrate/backfill_canonical_keys.go
14. Risques et mitigations¶
| # | Risque | Impact | Probabilité | Mitigation |
|---|---|---|---|---|
| 1 | Merge incorrect : deux Items différents fusionnés car CanonicalKey identique | Perte de contenu, Mastery incohérent | Moyenne | Le merge est réversible (merge_log). Items archivés soft-delete. Fallback HITL. |
| 2 | Conflit non détecté : contradiction subtile non repérée par le LLM | Élève étudie du contenu faux | Faible (fidelity check catch) | Double filet : fidelity check (Z3-AC10) + détection anomalie par taux d'échec (Z3-AC13). |
| 3 | Context window overflow : chapitre très long (30+ pages) dépasse le contexte du LLM local | IDP tronqué, items manquants | Faible (Lot 0 = 4 chapitres pilotes) | Chunking par Notion. Résumé condensé du contexte existant (noms + termes, pas le texte complet). |
| 4 | Latence merge : le merge engine + coherence check ajoutent de la latence | UX dégradée lors de l'ajout de pages | Moyenne | Coherence check en async (batch). SSE progressive. Merge engine est CPU-bound, pas I/O. |
| 5 | Drift OCR : Gemini Flash change de version, qualité OCR fluctue | Items mal extraits | Moyenne | LLM versioning (Z2-AC13). Monitoring fidelity score. Alertes drift. |
| 6 | Fragmentation Notion : les Notions se multiplient au fil des uploads | Cours illisible, trop de Notions | Moyenne | Cap à 7 Notions/chapitre (Z7-AC15). Fusion automatique des Notions < 2 items. |
15. Stratégie d'implémentation : MVP vs évolution future¶
Phase MVP (Lot 0)¶
Scope : Pipeline linéaire, pas de merge sophistiqué. Re-processing complet si ajout de pages.
| Composant | Implémentation |
|---|---|
| OCR | Gemini 2.5 Flash (API) ou Qwen3-VL-32B (local via Ollama) |
| IDP | mistral-small (API) ou Gemma 3 27B (local) |
| Merge | Aucun. Ajout de pages = re-run pipeline complet sur toutes les pages du chapitre. Items regénérés from scratch. |
| Dedup | CanonicalKey uniquement (matching exact sur term + type + pack). Archive automatique des doublons. |
| HITL | Fidelity check (Z3-AC10) + coherence basique (Z3-AC11, même terme contradictoire). |
| Mastery impact | Re-génération = nouveaux Item IDs = Mastery reset. Acceptable pour 4 chapitres pilotes. |
| Format pivot | Pas de PivotDocument formel. Les tables SQL existantes suffisent. |
Coût : re-processing de 10 pages = ~$0.02 (API) ou $0 (local). Acceptable pour le Lot 0.
Phase V2 : Incrémental basique¶
| Composant | Implémentation |
|---|---|
| Merge | MergeEngine avec les 6 actions (append/enrich/replace/conflict/dup/alt) |
| IDP | StructureBlocksIncremental avec contexte existant |
| Dedup | CanonicalKey + Jaccard keywords (seuils documentés dans merge-decision-table.md) |
| Coherence | LLM coherence check (cross-page, nouveaux vs existants uniquement) |
| Mastery | Transfert de Mastery sur archive/replace. Gel sur conflit. |
| Merge log | Table merge_log pour audit et réversibilité |
| Format pivot | PivotDocument comme projection (pas de table dédiée) |
Phase V3 : Knowledge graph éducatif¶
| Composant | Implémentation |
|---|---|
| Graphe de concepts | Relations typées entre Notions (prérequis, composition, analogie) |
| Résolution automatique | LLM agent pour résoudre les conflits mineurs sans HITL |
| Mémoire de cours | Historique complet des mutations avec undo/redo |
| Cross-chapter | Liens entre Items de chapitres différents de la même matière |
| Embeddings | Vecteurs sémantiques pour la recherche de similarité (pgvector) |
16. Recommandation finale¶
Pour le Lot 0, implémenter le pipeline linéaire simple (re-processing complet). Le coût est négligeable sur 4 chapitres pilotes et cela évite la complexité du merge.
Dès la V2, implémenter le MergeEngine avec les règles de merge documentées ici. C'est le point d'inflexion où l'expérience utilisateur change : l'élève peut ajouter des pages sans perdre sa progression.
Le format pivot JSON Schema doit être défini dès maintenant (il l'est dans ce document) car il contraint le contrat entre OCR et IDP. Même si le MVP ne fait pas de merge, le format pivot assure que les données sont structurées correctement pour le merge futur.
Priorités d'implémentation :
1. CanonicalKey (déjà implémenté dans identity.go)
2. MergeEngine (domaine pur, testable unitairement)
3. StructureBlocksIncremental (extension du port LLMService)
4. Migration SQL 003_incremental_merge.sql
5. ProcessIncrementalBatch (orchestration dans app service)
6. Domain events (ItemsEnriched, ItemReplaced, ItemArchived, ConflictDetected)
7. Intégration HITL (ValidationTask pour conflits de merge)