Quality Engine
Quality Engine — Dependency Graph (V1)
Sur cette page
Sprint 16D.1 — architecture uniquement. Aucune implémentation, aucune migration, aucun changement Supabase, aucune modification du Quality Engine. Ce document est la référence pour l'implémentation 16D.2.
- Base : Quality Engine core 16A.3 (
scripts/quality-engine/), gate réel 16A.4 (1000 entités Fatḥ, dry-run). - Statut :
DESIGN— sert de contrat pourscripts/quality-engine/graph.mjs(déclaration statique à créer en 16D.2). - Portée : décrit quelle entité doit être re‑scannée quand quoi change,
et pourquoi aucune table
dependency_graphn'est nécessaire.
1. Entités participant au Quality Engine#
1.1 Entités source (lues par le moteur, jamais écrites par lui)#
| Clé graphe | Table(s) | Champs matériels (entrent dans le hash canonique) |
|---|---|---|
import_batch | import_batches, datasets, data_sources | racine de provenance (id, checksum) |
hadith | hadiths | text_ar (matn), hadith_number, chapter_id |
chain | hadith_chains | hadith_id, rdf_structure_status |
chain_node | hadith_chain_nodes | chain_id, position, source_text_ar (surface isnād), narrator_id |
narrator | narrators (+ narrator_identities, narrator_appellations, narrator_nisbas, narrator_locations, narrator_kinships) | full_name_ar, full_name_fr, kunya_*, affiliation_*, generation_rank, années |
narrator_transmission | narrator_transmissions | teacher_id, student_id, statut |
name_form | narrator_name_forms, transliteration_aliases, transliteration_lexicon | normalized_form, form_type, is_primary |
crosswalk | narrator_crosswalk, external_narrators, narrator_assertions, narrator_reconciliation_decisions | status, ruwat_narrator_id, canonical_status |
commentary_document | commentary_documents, commentary_fath_manifest | external_document_id, source_text (dérivé), anomalies |
commentary_passage | commentary_passages, commentary_passage_labels | document_id, passage_kind, start_offset, end_offset |
commentary_target | commentary_targets, commentary_target_hadiths | target_type, hadith_id, position |
commentary_alignment | commentary_alignments | target_id, passage_id, alignment_status, method, verification_status |
validated_rule | commentary_validated_rules (vue commentary_current_validated_rules) | rule_type, scope, condition, conclusion, validated_status |
human_feedback | narrator_resolution_feedback (statut APPROVED) | mention_text_original, proposed_narrator_id |
1.2 Entités moteur (écrites par le Quality Engine — jamais des nœuds de propagation)#
| Clé | Table | Rôle |
|---|---|---|
quality_engine_state | quality_engine_state | mémo incrémental (1 ligne par entity_type × entity_id × engine) |
quality_decision | quality_decisions | trace d'audit append‑only (puits : rien n'en dépend) |
processing_run | processing_runs | en‑tête d'exécution (réutilisée telle quelle) |
1.3 Moteurs (modules Quality Engine — colonne « moteur responsable »)#
| Moteur | Origine (16A.1) | entity_type traité(s) | État 16D.1 |
|---|---|---|---|
normalization | normalize_ar_query(), normalize_translit(), normalizeNominalArabic | hadith, narrator, name_form | à adapter |
isnad-parser | isnadMatching.ts, fath-segmentation.mjs | chain_node | à adapter |
narrator-resolver | search_narrator_candidates*, entityResolution.ts, contextual-resolver-core.mjs | chain_node | à adapter |
search-normalizer | entity_resolution_v3_retrieval.sql, searchResultMerge.ts, ranking bonus | hadith, narrator | à adapter |
fath-matcher | fath-evidence.mjs, fath-matching.mjs, fath-contextual-propagation.mjs | commentary_alignment | implémenté (pilote 16A.3/16A.4) |
data-quality-scanner | compute_data_quality_metrics(), compute_structural_quality_metrics, narrator_data_conflicts | * (agrégat) | à adapter |
orchestrator | scripts/quality-engine/orchestrator.mjs | — (pilote scan→hash→decide→audit) | implémenté |
2. Graphe des dépendances#
Notation : A --(engine)--> B = « quand A change matériellement, B doit
être re‑scannée par engine ».
Vue d'ensemble (détail exact en §2.1 et texte ci-dessous) :
Détail brut (référence textuelle exhaustive du graphe ci-dessus) :
import_batch
├─(orchestrator)─> hadith
├─(orchestrator)─> narrator
├─(orchestrator)─> chain / chain_node
└─(orchestrator)─> commentary_document
hadith
├─(normalization)──────> hadith (search_vector, normalized matn)
├─(search-normalizer)──> hadith (ranking, arabic search columns)
├─(isnad-parser)───────> chain_node (surface isnād dérivée du matn/document)
├─(fath-matcher)───────> commentary_target (rapprochement par numéro)
├─(fath-matcher)───────> commentary_alignment (évidence E1/E2/E5 : numéro + similarité matn)
└─(data-quality-scanner)> hadith
chain ──(isnad-parser)──> chain_node (appartenance, position)
chain_node
├─(isnad-parser)───────> chain_node (segmentation de la surface)
├─(narrator-resolver)──> chain_node (identité depuis les candidats)
├─(narrator-resolver)──> narrator_transmission (arêtes maître/élève)
├─(data-quality-scanner)> chain (fiabilité, maillon critique)
└─(search-normalizer)──> narrator (bonus hadith_count)
narrator
├─(normalization)──────> name_form (reconstruction des formes normalisées)
├─(search-normalizer)──> narrator (canonical display, ranking)
├─(narrator-resolver)──> chain_node (bucket de forme normalisée impacté)
└─(data-quality-scanner)> narrator (profil qualité, conflits)
narrator_transmission ──(narrator-resolver)──> chain_node (ChainContext maître/élève)
name_form / crosswalk
├─(search-normalizer)──> narrator
└─(narrator-resolver)──> chain_node (jeu de candidats)
commentary_document ──(fath-matcher)──> commentary_passage (segmentation)
commentary_passage
└─(fath-matcher)──> commentary_alignment (ancre + voisins du MÊME document)
commentary_target / commentary_target_hadiths
└─(fath-matcher)──> commentary_alignment
commentary_alignment
├─(fath-matcher)──> commentary_alignment (voisins MÊME document : cible concurrente, frontière)
└─(fath-matcher)──> [affichage commentaire du hadith] (puits)
validated_rule ──(fath-matcher)──> commentary_alignment (uniquement dans `scope`)
human_feedback
├─(narrator-resolver)──> name_form
└─(narrator-resolver)──> chain_node (mention corrigée + mentions similaires)
(toutes les feuilles) ──(data-quality-scanner)──> data_quality_snapshot (puits)
2.1 Pour chaque entité : dépend de / invalide / moteur responsable#
| Entité | Dépend de | Invalide (→ re‑scan) | Moteur(s) responsable(s) |
|---|---|---|---|
import_batch | — (racine) | hadith, narrator, chain, chain_node, commentary_document de ce dataset | orchestrator |
hadith | import_batch, chapter | hadith@normalization, hadith@search-normalizer, chain_node@isnad-parser, commentary_target@fath-matcher, commentary_alignment@fath-matcher | normalization, search-normalizer, data-quality-scanner |
chain | hadith | chain_node (appartenance), chain fiabilité | isnad-parser, data-quality-scanner |
chain_node | chain, hadith (surface), name_form | chain_node@narrator-resolver, narrator_transmission, chain fiabilité, narrator hadith_count | isnad-parser, narrator-resolver |
narrator | import_batch, crosswalk/assertion/reconciliation, human_feedback | name_form, narrator@search-normalizer, chain_node@narrator-resolver (bucket), fiabilité chaînes concernées | normalization, narrator-resolver, data-quality-scanner |
narrator_transmission | paires chain_node, narrator | chain_node@narrator-resolver (ChainContext) | narrator-resolver |
name_form / crosswalk | narrator, transliteration_standard, human_feedback, lots lexique | narrator@search-normalizer, chain_node@narrator-resolver | normalization, search-normalizer |
commentary_document | commentary_edition, ingestion Fatḥ, commentary_fath_manifest | commentary_passage, commentary_alignment@fath-matcher (tout le document) | fath-matcher (segmentation) |
commentary_passage | commentary_document, version segmentation | commentary_alignment@fath-matcher (ancre + voisins même document) | fath-matcher |
commentary_target | hadith (existence, numéro) | commentary_alignment@fath-matcher | fath-matcher |
commentary_alignment | commentary_passage (+ tous ceux du document), commentary_target→hadith, hadith, version moteur, validated_rule (scope) | commentary_alignment@fath-matcher (frères même document), affichage | fath-matcher |
validated_rule | validation humaine, observations de runs | commentary_alignment@fath-matcher dans scope | fath-matcher (câblage futur) |
human_feedback (APPROVED) | humain | name_form, chain_node@narrator-resolver (mentions similaires) | narrator-resolver |
quality_decision | (écrite par tout moteur) | — puits | — |
data_quality_snapshot | toutes les feuilles | — puits | data-quality-scanner |
3. Événements et propagation exacte#
Chaque événement se traduit par : (entity_type, entity_id, change_kind) →
lookup statique dans le graphe → runEngine(engine, { onlyEntityIds }) par
couche, dans l'ordre amont→aval.
E1 — Import corpus (import_batch créé)#
import_batch
→ orchestrator marque dirty tous les entity_types produits par ce dataset
→ normalization(hadith, narrator)
→ isnad-parser(chain_node)
→ narrator-resolver(chain_node) [+ narrator_transmission]
→ search-normalizer(hadith, narrator) [bonus hadith_count]
→ fath-matcher(commentary_document → passage → alignment)
→ data-quality-scanner(*) → data_quality_snapshot
Arrêt : hash inchangé pour une entité ré‑insérée à l'identique (import idempotent) → aucune couche aval déclenchée pour elle.
E2 — Modification hadith (matn / numéro)#
hadith
→ normalization(hadith) [si matn dans la projection canonique]
→ search-normalizer(hadith)
→ isnad-parser(chain_node) [si surface isnād dérivée du matn]
→ narrator-resolver(chain_node)
→ reliability(chain)
→ commentary_target(numéro) → fath-matcher(commentary_alignment) [E1/E2/E5]
→ data-quality-scanner(hadith)
Chaîne canonique : Hadith → Normalization → (Parser → ChainNodes → Resolver) → Search → Fatḥ → QualityScanner.
Arrêt : édition d'un champ hors projection canonique (ex. glose FR) →
hadith@normalization hash inchangé → zéro propagation.
E3 — Modification narrateur (nom / tabaqa / année)#
narrator
→ normalization(narrator) → reconstruction name_form
→ search-normalizer(narrator) [canonical display, ranking]
→ narrator-resolver(chain_node) [SEULEMENT les nœuds dont la mention
normalisée tombe dans le même bucket de forme — pas tous]
→ reliability(chain) pour les chaînes contenant ces nœuds
→ data-quality-scanner(narrator)
Arrêt : changement hors projection (ajout d'une localisation secondaire) →
narrator@normalization hash inchangé → stop.
E4 — Nouvelle version moteur (engine_version bump)#
seul l'entity_type de CE moteur est ré‑évalué
(isDirty() compare engine_version — déjà implémenté en 16A.3)
AUCUNE traversée de graphe, AUCUNE invalidation croisée
Arrêt : immédiat, borné à |entités de cet entity_type|. Les autres moteurs
gardent leur engine_version → leur état reste valide.
E5 — Correction humaine#
narrator_resolution_feedbackpasseAPPROVED→ chemin E3 (vianame_form), plus : l'entité corrigée devient épinglée. Le moteur continue de la re‑scorer mais ne peut jamais AUTO‑appliquer contre le verdict humain (cf. §4 « épingle humaine »).commentary_alignment.verification_status=HUMAN_VERIFIED/REJECTED→fath-matcherre‑évalue l'alignement et ses frères, mais la décision resteREVIEW_REQUIRED/NO_CHANGEsi elle diverge de l'épingle. Arrêt : l'épingle stoppe toute cascade AUTO ; l'audit enregistre la divergence sans agir.
E6 — Nouveau commentaire#
commentary_document (nouveau)
→ fath-segmentation → commentary_passage (nouveaux)
→ fath-matcher(commentary_alignment nouveau)
→ fath-matcher(TOUS les alignements frères du MÊME document)
[la propagation contextuelle dépend de la liste ordonnée complète
des passages → frontière + cible concurrente recalculées]
→ affichage commentaire du hadith
Arrêt : les alignements d'AUTRES documents ne bougent pas (frontière de propagation = document). Hash gate sur chaque frère.
E7 — Nouvelle règle (validated_rule courante)#
validated_rule (INFERENCE ou VALIDATED_RULE)
→ fath-matcher(commentary_alignment) pour tous les alignements dans `scope`
Arrêt : hors scope → intact. Une règle OBSERVATION (sans conclusion
actionnable) → aucune invalidation.
4. Conditions d'arrêt de propagation#
- Hash gate (16A.3) :
isDirty(prev, inputHash, engineVersion)faux → stop, aucune entité aval enfilée. La projection canonique ne contient que les champs susceptibles de changer une décision → les modifications cosmétiques ne propagent jamais. - Décision stable : nouvelle décision == précédente ET hash d'évidence inchangé → l'état est ré‑écrit mais aucune cascade aval (les entrées des dépendants n'ont pas matériellement changé).
- Frontières de propagation :
document(Fatḥ contextuel),chain(isnād),bucket de forme normalisée(candidats resolver). Jamais « invalider tout ». - Épingle humaine : entité
HUMAN_VERIFIED/ feedbackAPPROVED→ le moteur re‑score mais la cascade AUTO s'arrête (jamais d'override silencieux). - Bump de version moteur : borné à un seul
entity_type, jamais inter‑moteurs. - Budget par run : l'orchestrateur traite une couche moteur par invocation ; la convergence multi‑couches demande N invocations mais chacune est bornée (batch + advisory lock 16A.3).
5. Cycles#
| Cycle apparent | Où | Rupture |
|---|---|---|
narrator → chain_node → assertion → narrator | le resolver lit les noms du narrateur ; les assertions viennent de la surface isnād des nœuds | Le resolver ne mute jamais narrators : il écrit quality_decision + candidats crosswalk. narrator dépend de narrator_reconciliation_decision (run RECONCILIATION / humain), pas de chain_node@narrator-resolver. Arête à sens unique pour la propagation automatique. |
commentary_alignment ↔ frères même document | la classification contextuelle de A dépend de la présence/statut de B | A dépend de la ligne commentary_alignment de B (alignment_status), pas de commentary_alignment@fath-matcher (l'état QE de B). Pas de cycle état→état. Ordre déterministe : traiter toutes les ancres, 2ᵉ passe seulement si une ligne frère a changé. |
validated_rule ← observations de run → rule → décisions → observations | la connaissance dérive des runs | fath-rule-validation.mjs (filterIndependentEvidence) interdit qu'une règle cite sa propre sortie ; OBSERVATION/INFERENCE ne s'auto‑promeuvent jamais ; la création de règle est humaine. Arête run → rule non automatique. |
Conclusion : aucun cycle état→état automatique. Tout cycle apparent traverse une porte append‑only + humain/réconciliation qui ne fait pas partie de la propagation automatique.
6. Protections contre les recalculs inutiles#
- Hash canonique (16A.3) — projection restreinte aux champs décisionnels ; édition cosmétique = zéro travail.
- Gate
engine_version— re‑run seulement si la logique a changé. - Court‑circuit « décision stable » — pas de cascade si décision + sortie inchangées.
- Scoping de propagation — frontières document / chain / bucket ; jamais « invalider tout ».
- Batch + advisory lock + sentinelle
processing_runsRUNNING (16A.3) — un seul run par moteur, pas de doublon concurrent. - Ordre de dépendance — moteurs amont→aval, une couche par invocation : un import ne provoque pas de ré‑entrance dans une couche.
- Upsert idempotent — les rejeux sont gratuits (démontré 16A.4 : run #2 = 1000 skips en ~26 ms).
- Superset toujours valide — un
runEnginecomplet sans filtre est toujours correct ; le filtre d'événement n'est qu'une optimisation de perf, jamais une condition de correction.
7. Faut‑il une table dependency_graph ? → NON#
7.1 Pourquoi quality_engine_state suffit#
- Le graphe est petit, statique, déterministe : ≈ 15
entity_type, ≈ 7 moteurs. Il ne change qu'avec une release de code. Il doit vivre en code comme constante déclarée (scripts/quality-engine/graph.mjs, à créer en 16D.2) — versionné avec le moteur, revu en PR, testable. - Une table = migration + seeding + risque de dérive vis‑à‑vis du code
(exactement le problème signalé en 16A.1 avec les 3 copies divergentes de
matchTier). Source unique de vérité = code. - La partie dynamique — « qu'est‑ce qui est dirty maintenant » — est déjà
quality_engine_state: parentity_type × entity_id × engine, elle porteinput_hash,engine_version,last_run_id,last_decision. La détection dirty =LEFT JOIN quality_engine_state+ comparaison de hash. Aucun état par‑arête n'est nécessaire : la propagation est recalculée depuis le graphe statique + les hash courants à chaque scan, pas stockée. - Puits non‑nœuds :
quality_decision(audit),data_quality_snapshot, caches d'affichage — rien n'en dépend, donc rien à modéliser en table.
7.2 Ce que 16D.2 ajoute réellement (hors périmètre 16D.1)#
Seule brique manquante : traduire un événement en ensemble d'entity_id
aval à re‑scanner quand ce n'est pas dérivable par une requête triviale.
Deux options (à trancher en 16D.2, pas ici) :
- (a) préféré — sans état :
GRAPH.resolveTargets(db, engine, srcType, srcId)résout l'ensemble par requête au moment du scan (ex. « nœuds dont la forme normalisée est dans le même bucket »). LerunEngine(engine, { onlyEntityIds })réutilise l'orchestrateur 16A.3 inchangé ; l'événement ne fait que rétrécir le lot (perf), il n'est pas une dépendance de correction. - (b) repli mesuré uniquement : une table étroite
quality_engine_dirty(entity_type, entity_id, reason, enqueued_at)— seulement si (a) se révèle trop lent sur le corpus réel.
Recommandation 16D.1 : garder quality_engine_state comme unique table
d'état ; ajouter scripts/quality-engine/graph.mjs (déclaration statique) ;
n'envisager (b) qu'après mesure.
8. Algorithme de propagation (contrat pour 16D.2)#
on event(entity_type, entity_id, change_kind):
affected := GRAPH.downstream(entity_type, change_kind) # lookup statique
for (engine, target_type) in affected: # ordre amont→aval
targets := GRAPH.resolveTargets(db, engine, entity_type, entity_id)
runEngine(db, engine, { onlyEntityIds: targets, ... }) # orchestrateur 16A.3
Invariants :
runEngine(engine)sansonlyEntityIdsreste toujours valide (superset). Le filtre est une optimisation.- Aucune écriture hors
quality_engine_state/quality_decisions/processing_runs(policyDRY_RUN_ONLYen V1). - La convergence multi‑couches = ré‑invocation de l'orchestrateur, jamais une boucle interne non bornée.
9. Fichiers attendus en 16D.2 (référence, non créés ici)#
scripts/quality-engine/graph.mjs— déclaration statique du graphe (downstream(),resolveTargets()), miroir de ce document.scripts/quality-engine/events.mjs— normalisation des 7 événements §3.scripts/validation/test-quality-engine-graph.mjs— parité graphe ↔ ce document, détection de cycle état→état (doit rester vide).- Éventuelle migration
quality_engine_dirtyuniquement si l'option 7.2(b) est retenue après mesure.
V1 — 16D.1. Design gelé. Prochaine étape : 16D.2 (implémentation du graphe).