ISNAD Scientific Platform

Documentation Center
V1 · Référence officielle

ADR

ADR-0001 — Multi-Type Acquisition & Canonical Projection Boundary

6 min de lectureMis à jour le 29 octobre 2026
Sur cette page
  • Statut : Accepté
  • Date : 2026-08-30
  • Portée : EPIC 1 (B0) et toute acquisition de source ultérieure
  • Remplace : la S1.4 « AcquisitionTransformer / BukhariHadithTransformer » de la roadmap v1.1
  • Références : planning/epics/scientific-knowledge-layer-roadmap.md §2, docs/scientific/knowledge-model.md, docs/architecture/domain-layer.md, planning/epics/b0-persistence-mapping-decision.md, planning/audits/knowledge-acquisition-contracts-audit.md

1. Contexte#

ISNAD n'est pas un simple moteur d'import de recueils de hadith. La base contiendra plusieurs familles documentaires de structure scientifique différente : recueils de hadith (Bukhārī, Muslim, Sunan, Jāmiʿ, Musnad, Muwaṭṭaʾ), ouvrages de rijāl (Taqrīb, Tahdhīb al-Kamāl, al-Jarḥ wa-l-Taʿdīl, Mīzān, Ṭabaqāt), commentaires (Fatḥ al-Bārī, Sharḥ al-Nawawī), le Coran, les ouvrages de terminologie (muṣṭalaḥ al-ḥadīth), et d'autres familles à venir.

L'état du code au moment de cette décision :

  • src/domain/knowledge-acquisition/ : 10 contrats d'acquisition matures et corpus-agnostiques (CorpusDocument, CorpusChain, CorpusNode, CorpusCommentary, CorpusEdition, CorpusProvenance, CorpusImportResult, CorpusAdapter, KnowledgeAcquisitionEngine). Audit KNOWLEDGE_ACQUISITION_CONTRACTS_AUDIT_V1.md : PASS 14 axes.
  • src/infrastructure/knowledge-acquisition/bukhari/BukhariAdapter.ts : premier CorpusAdapter, lit le corpus Bukhārī legacy (hadiths / hadith_chains / hadith_chain_nodes) et le neutralise en Corpus*. Aucune logique métier, aucune résolution.
  • KnowledgeAcquisitionEngine : interface déjà présente dans src/domain/knowledge-acquisition/.
  • Persistance KM V1 (knowledge_sources, source_editions, knowledge_entries) : déjà créée en S1.3 (supabase/migrations/20260829143612_...sql).

La roadmap v1.1 prévoyait pour S1.4 :

T1.4.2 — Implémenter BukhariHadithTransformer : CorpusDocument HADITH → hadiths + hadith_chains + hadith_chain_nodes.

Deux problèmes :

  1. Nom lié à un corpus après la frontière de neutralisation. Les contrats Corpus* existent précisément pour qu'après l'Adapter le système ne sache plus s'il traite Bukhārī ou Muslim. Une classe BukhariHadithTransformer réintroduit cette information.
  2. Couplage projection ↔ persistance. « → hadiths + hadith_chains + hadith_chain_nodes » invite à écrire du SQL dans le transformer, couplé au schéma Supabase, court-circuitant les ports de persistance du domaine.

Un troisième constat : les contrats d'acquisition sont aujourd'hui plus expressifs que les entités canoniques et que le schéma SQL (co-narration CorpusNode.mentionsRaw[] vs hadith_chain_nodes unique (chain_id, position) ; taḥwīl CorpusChain.sharedTailOfChainId / divergesAtPosition sans équivalent ; suspended / elided ; LocalizedText[] vs text_ar / text_fr). Une projection naïve serait destructrice.


2. Décision#

2.1 Doctrine (8 règles)#

  1. Source specificity ends at the Adapter — as behavior. Après l'Adapter, tout branchement conditionné par le nom du corpus (if (corpus === "bukhari")) est interdit.
  2. Source identity and provenance cross the boundary — as data. corpusId, editionId, externalRefs, l'identité du document et la provenance restent explicitement disponibles en aval : ce sont des données scientifiques, pas des dépendances architecturales.
  3. Transformation varies by scientific content type, not by book/source. Un Projector par famille scientifique (HADITH, RIJĀL, COMMENTARY, QURAN, TERMINOLOGY), jamais un transformer par livre.
  4. Projectors are pure. Aucun SQL, Supabase, pg, HTTP, filesystem, ni résolution d'identité. Entrée : contrats d'acquisition. Sortie : commandes de création.
  5. Projectors emit creation commands / aggregates, not persisted entities. Aucun identifiant SQL fabriqué ; clés de corrélation temporaires pour relier document → chaînes → nœuds.
  6. Persistence occurs exclusively through domain ports implémentés par des adapters d'infrastructure.
  7. Aggregate persistence is transactional and idempotent. Un hadith et ses chaînes forment une unité ; un réimport ne crée pas de doublon.
  8. Identity resolution, grading, reconciliation and reasoning are downstream — jamais des préoccupations d'acquisition.

2.2 Propriété des champs (invariant)#

PropriétaireExemples de champs
Acquisitiontexte source, mention brute, formule de transmission, référence, position, provenance, contentHash
Résolution (aval)narratorId, identityConfidence, resolutionStatus
Traitement scientifique (aval)grading, Assertion, Evidence, décisions de conflit, validation humaine, conclusions de reasoning

Un réimport peut mettre à jour ce que l'acquisition possède. Il ne peut jamais remettre à NULL un champ produit en aval. Si une donnée source dont dépend un enrichissement change réellement : politique future d'invalidation / review / supersession, jamais un reset aveugle.

2.3 Identité vs version#

  • Identité = « de quel objet source parle-t-on ? ». Vérifier d'abord si document.id / chain.id (déterministes, produits par l'Adapter) suffisent comme clés stables avant d'introduire un nouveau concept (sourceDocumentKey). Si un nouveau concept est nécessaire, documenter précisément pourquoi.
  • Version = contentHash : « le contenu a-t-il changé ? ». Une correction typographique dans une édition ne doit pas fabriquer un nouveau hadith scientifique.
  • Éditions : le même hadith présent dans deux éditions de Bukhārī ne devient pas deux hadiths distincts. Documenter le problème ; prendre la décision minimale nécessaire au vertical slice HADITH ; ne pas lancer une refonte universelle Work / Edition.

2.4 Politique de perte — trois issues seulement#

Chaque information d'acquisition, dans la matrice de fidélité, reçoit exactement un statut :

  • MAPPED — atterrit dans un champ canonique / une colonne.
  • PRESERVED — conservée verbatim dans le témoin d'acquisition (JSONB), rejouable ultérieurement.
  • HARD_FAIL — la projection refuse de produire l'agrégat.

Plus un drapeau scientific-critical: oui / non. Règle : critique + non MAPPED ⇒ obligatoirement PRESERVED ou HARD_FAIL, jamais un simple warning. Aucune taxonomie plus complexe en V1.

Mieux vaut refuser une projection que produire silencieusement une représentation historiquement fausse de l'isnād.

2.5 Vertical slices & généralisation#

  • Chaque famille documentaire est introduite par un vertical slice sur une source réelle.
  • Aucun framework générique, modèle universel ou abstraction commune créé uniquement pour anticiper des familles futures.
  • Une abstraction n'est généralisée qu'après démonstration par au moins deux cas réels structurellement différents.

2.6 Ce qui n'est PAS conçu maintenant#

BiographyEntryProjector, CommentaryProjector, QuranProjector, TerminologyProjector, registry universel de Projectors, UnitOfWork générique, SourceReader universel obligatoire, modèle universel Work → Edition, framework de raw witnesses, architecture pour des sources non intégrées.

2.7 Architecture cible#

SOURCE / FORMAT (DB, JSON, XML, Shamela, API…)
        │
        ▼
  Source Reader / Corpus Adapter   ── Anti-Corruption Layer (spécifique à l'œuvre)
        │
        ▼   (identité + édition + externalRefs + provenance conservées)
  Contrats d'acquisition
  CorpusDocument / CorpusChain / CorpusNode / CorpusCommentary / CorpusProvenance
  + HadithAcquisitionBundle (S1.4)
        │
        ▼
  Canonical Projector — par nature scientifique du contenu — PUR, sans DB
  (HADITH d'abord : projectHadithBundle / HadithCorpusProjector)
        │
        ▼
  Commandes canoniques : NewHadithAggregate { hadith, chains[], nodes[] }
  (aucune id SQL — clés de corrélation ; nodes: narrator_id = null, UNRESOLVED)
        │
        ▼
  Ports applicatifs : HadithAggregateWriter (transactionnel, idempotent)
        │
        ▼
  Infrastructure : SupabaseHadithAggregateWriter
        │
        ▼
  PostgreSQL / Supabase

        │
        ▼
  AVAL (jamais de l'acquisition) : Narrator Resolver · Assertions · Evidence
  · Confidence · Conflict/Reconciliation · Reasoning

2.8 NewHadithAggregate (référence indicative)#

HadithAcquisitionBundle {
  document: CorpusDocument            // kind === 'HADITH'
  ancestry: { work, book, chapter }   // CorpusDocument
  chains: CorpusChain[]               // nodes portés par CorpusChain — ne pas dupliquer
  provenance: CorpusProvenance
}

NewHadithAggregate {
  hadith   // clé de corrélation, ex. "doc:<id>"
  chains[] // clé "doc:<id>:chain:<n>"
  nodes[]  // clé "doc:<id>:chain:<n>:node:<p>" ; narrator_id = null ; resolution_status = UNRESOLVED
}

Le repository transforme les clés de corrélation en identifiants persistants.


3. Conséquences#

Positives#

  • Ajouter Muslim, Tirmidhī, un extrait Shamela, un ouvrage de rijāl… se fait par un nouvel Adapter sans toucher au cœur.
  • Remplacer Supabase par un autre stockage ne change pas le moteur d'acquisition (ports).
  • Aucune perte scientifique silencieuse ; l'isnād n'est jamais réécrit de façon historiquement fausse.
  • Un réimport de corpus ne détruit pas la résolution, la validation ou le grading produits en aval.

Coûts / risques acceptés#

  • Un HadithAcquisitionBundle + des types de commande spécifiques au chemin HADITH (pas d'entités Hadith/Chain réutilisées telles quelles — elles portent une id obligatoire).
  • Le cas Bukhārī est un aller-retour (hadith_* → Adapter → Corpus* → Projector → hadith_*) : il valide la plomberie, pas l'acquisition d'une source neuve. D'où le jalon S1.6 Independent Source Proof.
  • Certaines informations critiques (co-narration, taḥwīl, suspended/elided) devront être PRESERVED ou déclencher HARD_FAIL tant que le schéma ne les représente pas ; les migrations correspondantes sont décidées en S1.4, exécutées en S1.5.
  • Un guardrail CI est nécessaire pour rendre la règle « projectors purs » exécutable.

Neutres#

  • La roadmap est renumérotée E1–E9 ; B0 devient « Multi-Type Acquisition Foundation ».
  • KnowledgeAcquisitionEngine existant est étendu en S1.5 — aucune seconde interface.

4. Alternatives rejetées#

AlternativeRaison du rejet
BukhariHadithTransformer (+ un transformer par corpus)Réintroduit l'identité du corpus après l'ACL ; recrée des silos par livre ; dette qui explose à N œuvres.
Transformer écrivant directement dans hadiths / hadith_chains / hadith_chain_nodesCouple la projection au schéma SQL ; court-circuite les ports ; non testable sans DB.
Modèle canonique universel Book → Chapter → Hadith → Chain → Node pour toutes les famillesFaux pour RIJĀL, QURAN, COMMENTARY, TERMINOLOGY ; dette scientifique.
Port de persistance unique pour toute la plateformeNe respecte pas les frontières d'agrégat ; une écriture partielle laisse la base incohérente. Retenu : un writer par agrégat, UnitOfWork seulement si une opération métier coordonne plusieurs agrégats.
Taxonomie de perte à 5 valeurs + criticité à 3 niveauxSur-spécification : 15 cases de politique pour ~10 lignes de matrice en V1. Retenu : MAPPED / PRESERVED / HARD_FAIL + criticité binaire.
Concevoir tous les Projectors (Biography, Commentary, Quran, Terminology) dès E1Overengineering ; abstraction non validée. Retenu : HadithCorpusProjector réel ; les autres = vertical slices futurs.
Tables knowledge_documents / knowledge_chains / knowledge_nodes / knowledge_import_results / knowledge_provenanceDéjà rejetées en S1.2 (planning/epics/b0-persistence-mapping-decision.md §9). Duplication du Corpus Engine et de import_batches / processing_runs.

5. Décisions ouvertes — à trancher en S1.4 (planning/audits/b0-hadith-fidelity-matrix.md)#

  1. document.id / chain.id suffisent-ils comme identité stable, ou faut-il une clé dédiée (et pourquoi) ?
  2. Identité édition-indépendante (citation canonique) ou édition-scopée (⇒ table de concordance) ?
  3. Conversion de position : contrat 0-based ↔ hadith_chain_nodes.position >= 1.
  4. Co-narration : CorpusNode.mentionsRaw[] vs mention_ar singulier + unique (chain_id, position) → MAPPED (nouveau schéma), PRESERVED, ou HARD_FAIL ?
  5. taḥwīl (sharedTailOfChainId / divergesAtPosition) : idem.
  6. suspended / elided : resolution_status n'a pas de valeur pour « structurellement suspendu ».
  7. LocalizedText[] (n langues / n éditions) vs text_ar / text_fr figés.
  8. CorpusChain.endpoint / completeness : MAPPED ou PRESERVED ?
  9. Clé d'idempotence côté repository : source_key existant vs (knowledge_source, edition, external_ref) vs contentHash.
  10. Édition : décision minimale pour le slice HADITH — réutiliser / étendre source_editions, sans refonte Work / Edition.

Ces décisions produisent des spécifications de migration (texte) consommées par S1.5. S1.4 n'exécute aucune migration.