ISNAD Scientific Platform

Documentation Center
V1 · Référence officielle

Architecture

OpenITI — Ingestion automatique

5 min de lectureMis à jour le 29 octobre 2026
Sur cette page

Documentation de référence du chantier « OpenITI Auto Ingestion ». Voir aussi : planning/audits/openiti-auto-ingestion-baseline-audit.md (audit A1) et planning/reports/openiti-auto-ingestion/A*.md (journal des micro-tâches).

1. Architecture#

Saisie admin (URI OpenITI | URL GitHub dossier/YML | Version URI)
        │  resolveSource()                          src/server/openiti/source-resolver.ts
        ▼
NormalizedSource ─ authorUri / workUri / versionUri? / repo(OpenITI/NNNN AH, ref, path)
        │  resolveMetadata()                        src/server/openiti/metadata-resolver.ts
        ▼  (GitHub : contents API + raw + commit SHA — src/server/openiti/github.ts)
ResolvedMetadata ─ work (title_ar, genres, related), versions[] (YML), commitSha
        │  prepareImport()  → télécharge 1 fois le document retenu   src/server/openiti/prepare.ts
        ▼  analyzeDocument()  (mARkdown : header/body, pages, sections, ### $)  markdown.ts / analyze.ts
DocumentAnalysis ─ sha256, entries[], sections[], compteurs, pagination
        │
        ├─► runPreflight()   dry-run + détection doublon               preflight.ts
        │       └── PreflightReport (source, provenance, work, versions, document, alreadyImported, issues)
        │
        └─► runImport()      STAGING → CANONISATION (1 transaction)     import-runner.ts
                ├── openiti.imports (RUNNING → COMPLETED/FAILED)
                ├── openiti.stg_work_metadata / stg_version_metadata / stg_documents
                ├── openiti.stg_entries (bulk unnest) + stg_entry_pages + stg_issues
                ├── openiti.canonicalize_import(import_id)  → works / versions / documents / entries
                └── runDomainExtraction(workUri)  (best-effort, hors tx)  extractors/run.ts

Aucun LLM sur ce chemin. Toutes les étapes techniques sont déterministes.

2. Entrées supportées#

FormeExemple
Work URI0852IbnHajarCasqalani.TacrifAhlTaqdis
Version URI0852IbnHajarCasqalani.TacrifAhlTaqdis.Shia003340BK1-ara1
URL GitHub (dossier)https://github.com/OpenITI/0875AH/tree/master/data/0852IbnHajarCasqalani/0852IbnHajarCasqalani.TacrifAhlTaqdis
URL GitHub (fichier YML/texte).../blob/<ref>/.../<version>.yml · .../<version>.mARkdown
raw.githubusercontent.comidem

Le dépôt per-25-AH est déduit de l'année de décès (ceil(y/25)*25 → 0875AH), ou repris tel quel depuis l'URL GitHub fournie.

3. Pipeline détaillé#

  1. Source Resolver — normalisation pure (regex URI, parsing URL). OpenITISourceError sur saisie non reconnue.
  2. Metadata Discovery — listDir du dossier de l'œuvre, lecture du YML de l'œuvre (titre AR, genres — les gabarits src@keyword/NODATA sont filtrés), résolution du commit SHA.
  3. Version Discovery — une version par YML de version présent ; les fichiers texte orphelins deviennent des versions sans YML (WARNING).
  4. Document Retrieval — téléchargement du fichier texte (.mARkdown → .completed → .inProgress → sans extension).
  5. Validation OpenITI — ######OpenITI#, #META#Header#End#, présence d'unités, ratio d'inconnus → issues (INFO/WARNING/BLOCKING).
  6. Staging — openiti.imports + stg_*. Le raw_text intégral est conservé dans stg_documents.
  7. Parsing mARkdown (A7) — segmentation par marqueurs OpenITI :
    • ### $ / ### $ N → BIOGRAPHICAL_ENTRY (ref = N)
    • ### $$$ → CROSS_REFERENCE
    • ### |, ### ||, ### ||| → SECTION_HEADER (niveau = nombre de |)
    • autre → UNKNOWN (review_status = NEEDS_REVIEW)
    • offsets absolus, content_sha256, héritage de page depuis le marqueur précédent.
  8. Canonisation (A8) — openiti.canonicalize_import(uuid) : upsert par URI, jamais d'écrasement du digital_source d'une version existante, on conflict (document_id, entry_order).
  9. Extraction de domaine (A11) — extracteur sélectionné par work_uri / work_type ; réécrit uniquement sa propre sortie (extraction_method).

4. Correspondance DB#

ÉtapeTables écrites
Importopeniti.imports (+ colonnes provenance : work_uri, version_uri, author_uri, commit_sha, source_url, document_sha256, started_at, completed_at, error_text, created_by, preflight, stats)
Stagingopeniti.stg_work_metadata (+ work_type), stg_version_metadata, stg_documents, stg_entries, stg_entry_pages, stg_issues
Canoniqueopeniti.works, openiti.versions, openiti.documents, openiti.entries
Extractionopeniti.entry_facts, openiti.entry_relations

Jamais écrit : public.narrators, chaînes de hadith, public.narrator_* (crosswalk/assertions préparés plus tard, hors de ce chantier).

5. Outillage OpenITI réutilisé / réimplémenté#

Le package Python openiti n'est pas intégré au runtime (déploiement Next.js sans Python ; besoin restreint ; déterminisme/testabilité). Primitives réimplémentées fidèlement en TypeScript, derrière une frontière permettant de brancher un worker Python ultérieurement (mission §38) :

OpenITIÉquivalent ISNAD
helper.yml.readYML / fix_broken_ymlyml.ts : readYml, ymlGet, ymlSplitMulti
helper.uri (split author/book/version, siècle)uri.ts
helper.funcs.read_header / read_textmarkdown.ts : splitHeader
get_page_numbers / get_page_numbermarkdown.ts : getPageMarkers
get_sections / get_section_titlemarkdown.ts : getSections
normalize_ara_lightmarkdown.ts : normalizeAraLight
TOC (openiti_toc)non exploité — enrichissement facultatif (mission §9)

6. Idempotence#

Clé : work_uri + version_uri + document_sha256.

  • Document déjà rattaché à la version en canonique → ALREADY_IMPORTED.
  • Import COMPLETED portant ce SHA → ALREADY_IMPORTED.
  • Version canonisée avec un autre témoin (SHA différent) → VERSION_CANONICAL_CONFLICT (BLOCKING) : protège Taqrīb/Tahdhīb.

7. Reproductibilité (mission §10)#

Chaque import conserve : provider, repository, repository_ref, commit_sha (résolu via l'API GitHub), work_uri, version_uri, source_url, document_sha256, manifest_sha256 (sha256(version:doc_sha:importer:parser)), parser_version (openiti-importer/1.0.0), imported_at.

8. Versions d'outil (mission §41)#

src/server/openiti/parser-version.ts : openiti-importer/1.0.0, openiti-mARkdown-parser/1.0.0, openiti-preflight/1.0.0, openiti-domain-extractor/1.0.0.

9. Modes d'échec#

CasComportement
URI/URL non reconnueOpenITISourceError (route 422)
Dossier œuvre absentGitHubError NOT_FOUND + message explicite
Limite de débit GitHubGitHubError RATE_LIMITED (poser GITHUB_TOKEN)
Plusieurs versions, aucune choisieVERSION_SELECTION_REQUIRED (WARNING), pas d'import
Fichier texte introuvableerrors[], import bloqué
Erreur en canonisationtransaction annulée, imports → FAILED + stg_issues BLOCKING (hors tx)
Réimport identiqueALREADY_IMPORTED
Réimport autre témoin d'une version canoniséeVERSION_CANONICAL_CONFLICT (BLOCKING)

10. Workflow administration#

/fr/admin → bouton OpenITI → /fr/admin/openiti/import :

  1. coller URI / URL → Analyser
  2. lire le préflight (auteur, work URI, titre, dépôt, ref/commit, versions, métadonnées, pagination, TOC, SHA, statut doublon, warnings/errors)
  3. choisir la version (radio) si plusieurs
  4. Importer → progression → IMPORT COMPLETED (entrées, pages, sections, unités, stats canoniques, liens)
  5. onglets Œuvres / Imports (+ détail + anomalies) / Anomalies (filtres severity/status)

Routes serveur : /api/admin/openiti/{preflight,import,imports,imports/[id],works,issues} (runtime=nodejs, garde requireAdmin).

11. Ajouter une autre œuvre#

  1. Obtenir l'URI OpenITI ou l'URL GitHub du dossier.
  2. Analyser, vérifier le préflight, choisir la version, Importer.
  3. Rien à coder si le pipeline générique suffit.

12. Ajouter un extracteur de domaine#

  1. Créer une classe implémentant DomainExtractor dans src/server/openiti/extractors/extractors.ts.
  2. supports(workUri, workType) + extract(ctx) → DomainFact[] | DomainRelation[] (chaque fait porte entryId, surfaceText, confidence).
  3. L'ajouter au tableau EXTRACTORS (l'ordre = priorité).
  4. runDomainExtraction gère l'idempotence (efface la sortie de extractor.id).
  5. Ne jamais écrire dans public.narrators — produire des assertions/faits sourcés.

13. Sécurité (mission §36)#

  • Le schéma openiti n'est pas exposé via la Data API Supabase (aucune entrée db.schemas). La UI passe exclusivement par les routes serveur.
  • RLS laissée désactivée sur openiti.* (état constaté à l'audit) — non modifiée par ce chantier. Documenté ; si une exposition API directe devenait nécessaire, activer RLS au minimum requis.
  • Les routes /api/admin/openiti/* revalident le jeton Supabase + is_admin. Écritures via DATABASE_URL (rôle postgres), jamais service_role côté client.
  • GITHUB_TOKEN facultatif, serveur uniquement.

14. Ce qui reste hors de ce chantier#

Tradition Resolver, Fatḥ al-Bārī, ouvrages P1/P2/P3, scoring global des chaînes, crosswalk public.narrator_* automatique, IA d'extraction ambiguë, exploitation openiti_toc, import asynchrone durable, extracteurs P0 4→6.