ISNAD Scientific Platform

Documentation Center
V1 · Référence officielle

Quality Engine

Quality Engine — Dependency Graph (V1)

9 min de lectureMis à jour le 29 أكتوبر 2026
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 pour scripts/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_graph n'est nécessaire.

1. Entités participant au Quality Engine#

1.1 Entités source (lues par le moteur, jamais écrites par lui)#

Clé grapheTable(s)Champs matériels (entrent dans le hash canonique)
import_batchimport_batches, datasets, data_sourcesracine de provenance (id, checksum)
hadithhadithstext_ar (matn), hadith_number, chapter_id
chainhadith_chainshadith_id, rdf_structure_status
chain_nodehadith_chain_nodeschain_id, position, source_text_ar (surface isnād), narrator_id
narratornarrators (+ narrator_identities, narrator_appellations, narrator_nisbas, narrator_locations, narrator_kinships)full_name_ar, full_name_fr, kunya_*, affiliation_*, generation_rank, années
narrator_transmissionnarrator_transmissionsteacher_id, student_id, statut
name_formnarrator_name_forms, transliteration_aliases, transliteration_lexiconnormalized_form, form_type, is_primary
crosswalknarrator_crosswalk, external_narrators, narrator_assertions, narrator_reconciliation_decisionsstatus, ruwat_narrator_id, canonical_status
commentary_documentcommentary_documents, commentary_fath_manifestexternal_document_id, source_text (dérivé), anomalies
commentary_passagecommentary_passages, commentary_passage_labelsdocument_id, passage_kind, start_offset, end_offset
commentary_targetcommentary_targets, commentary_target_hadithstarget_type, hadith_id, position
commentary_alignmentcommentary_alignmentstarget_id, passage_id, alignment_status, method, verification_status
validated_rulecommentary_validated_rules (vue commentary_current_validated_rules)rule_type, scope, condition, conclusion, validated_status
human_feedbacknarrator_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éTableRôle
quality_engine_statequality_engine_statemémo incrémental (1 ligne par entity_type × entity_id × engine)
quality_decisionquality_decisionstrace d'audit append‑only (puits : rien n'en dépend)
processing_runprocessing_runsen‑tête d'exécution (réutilisée telle quelle)

1.3 Moteurs (modules Quality Engine — colonne « moteur responsable »)#

MoteurOrigine (16A.1)entity_type traité(s)État 16D.1
normalizationnormalize_ar_query(), normalize_translit(), normalizeNominalArabichadith, narrator, name_formà adapter
isnad-parserisnadMatching.ts, fath-segmentation.mjschain_nodeà adapter
narrator-resolversearch_narrator_candidates*, entityResolution.ts, contextual-resolver-core.mjschain_nodeà adapter
search-normalizerentity_resolution_v3_retrieval.sql, searchResultMerge.ts, ranking bonushadith, narratorà adapter
fath-matcherfath-evidence.mjs, fath-matching.mjs, fath-contextual-propagation.mjscommentary_alignmentimplémenté (pilote 16A.3/16A.4)
data-quality-scannercompute_data_quality_metrics(), compute_structural_quality_metrics, narrator_data_conflicts* (agrégat)à adapter
orchestratorscripts/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 deInvalide (→ re‑scan)Moteur(s) responsable(s)
import_batch— (racine)hadith, narrator, chain, chain_node, commentary_document de ce datasetorchestrator
hadithimport_batch, chapterhadith@normalization, hadith@search-normalizer, chain_node@isnad-parser, commentary_target@fath-matcher, commentary_alignment@fath-matchernormalization, search-normalizer, data-quality-scanner
chainhadithchain_node (appartenance), chain fiabilitéisnad-parser, data-quality-scanner
chain_nodechain, hadith (surface), name_formchain_node@narrator-resolver, narrator_transmission, chain fiabilité, narrator hadith_countisnad-parser, narrator-resolver
narratorimport_batch, crosswalk/assertion/reconciliation, human_feedbackname_form, narrator@search-normalizer, chain_node@narrator-resolver (bucket), fiabilité chaînes concernéesnormalization, narrator-resolver, data-quality-scanner
narrator_transmissionpaires chain_node, narratorchain_node@narrator-resolver (ChainContext)narrator-resolver
name_form / crosswalknarrator, transliteration_standard, human_feedback, lots lexiquenarrator@search-normalizer, chain_node@narrator-resolvernormalization, search-normalizer
commentary_documentcommentary_edition, ingestion Fatḥ, commentary_fath_manifestcommentary_passage, commentary_alignment@fath-matcher (tout le document)fath-matcher (segmentation)
commentary_passagecommentary_document, version segmentationcommentary_alignment@fath-matcher (ancre + voisins même document)fath-matcher
commentary_targethadith (existence, numéro)commentary_alignment@fath-matcherfath-matcher
commentary_alignmentcommentary_passage (+ tous ceux du document), commentary_target→hadith, hadith, version moteur, validated_rule (scope)commentary_alignment@fath-matcher (frères même document), affichagefath-matcher
validated_rulevalidation humaine, observations de runscommentary_alignment@fath-matcher dans scopefath-matcher (câblage futur)
human_feedback (APPROVED)humainname_form, chain_node@narrator-resolver (mentions similaires)narrator-resolver
quality_decision(écrite par tout moteur)— puits—
data_quality_snapshottoutes les feuilles— puitsdata-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_feedback passe APPROVED → chemin E3 (via name_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-matcher re‑évalue l'alignement et ses frères, mais la décision reste REVIEW_REQUIRED/NO_CHANGE si 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#

  1. 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.
  2. 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é).
  3. Frontières de propagation : document (Fatḥ contextuel), chain (isnād), bucket de forme normalisée (candidats resolver). Jamais « invalider tout ».
  4. Épingle humaine : entité HUMAN_VERIFIED / feedback APPROVED → le moteur re‑score mais la cascade AUTO s'arrête (jamais d'override silencieux).
  5. Bump de version moteur : borné à un seul entity_type, jamais inter‑moteurs.
  6. 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 apparentOùRupture
narrator → chain_node → assertion → narratorle resolver lit les noms du narrateur ; les assertions viennent de la surface isnād des nœudsLe 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 documentla classification contextuelle de A dépend de la présence/statut de BA 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 → observationsla connaissance dérive des runsfath-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#

  1. Hash canonique (16A.3) — projection restreinte aux champs décisionnels ; édition cosmétique = zéro travail.
  2. Gate engine_version — re‑run seulement si la logique a changé.
  3. Court‑circuit « décision stable » — pas de cascade si décision + sortie inchangées.
  4. Scoping de propagation — frontières document / chain / bucket ; jamais « invalider tout ».
  5. Batch + advisory lock + sentinelle processing_runs RUNNING (16A.3) — un seul run par moteur, pas de doublon concurrent.
  6. Ordre de dépendance — moteurs amont→aval, une couche par invocation : un import ne provoque pas de ré‑entrance dans une couche.
  7. Upsert idempotent — les rejeux sont gratuits (démontré 16A.4 : run #2 = 1000 skips en ~26 ms).
  8. Superset toujours valide — un runEngine complet 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 : par entity_type × entity_id × engine, elle porte input_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 »). Le runEngine(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) sans onlyEntityIds reste toujours valide (superset). Le filtre est une optimisation.
  • Aucune écriture hors quality_engine_state / quality_decisions / processing_runs (policy DRY_RUN_ONLY en 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_dirty uniquement si l'option 7.2(b) est retenue après mesure.

V1 — 16D.1. Design gelé. Prochaine étape : 16D.2 (implémentation du graphe).