Architecture
OpenITI — Ingestion automatique
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#
| Forme | Exemple |
|---|---|
| Work URI | 0852IbnHajarCasqalani.TacrifAhlTaqdis |
| Version URI | 0852IbnHajarCasqalani.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.com | idem |
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é#
- Source Resolver — normalisation pure (regex URI, parsing URL).
OpenITISourceErrorsur saisie non reconnue. - Metadata Discovery —
listDirdu dossier de l'œuvre, lecture du YML de l'œuvre (titre AR, genres — les gabaritssrc@keyword/NODATAsont filtrés), résolution du commit SHA. - Version Discovery — une version par YML de version présent ; les fichiers texte orphelins deviennent des versions sans YML (WARNING).
- Document Retrieval — téléchargement du fichier texte (
.mARkdown→.completed→.inProgress→ sans extension). - Validation OpenITI —
######OpenITI#,#META#Header#End#, présence d'unités, ratio d'inconnus →issues(INFO/WARNING/BLOCKING). - Staging —
openiti.imports+stg_*. Leraw_textintégral est conservé dansstg_documents. - 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.
- Canonisation (A8) —
openiti.canonicalize_import(uuid): upsert par URI, jamais d'écrasement dudigital_sourced'une version existante,on conflict (document_id, entry_order). - Extraction de domaine (A11) — extracteur sélectionné par
work_uri/work_type; réécrit uniquement sa propre sortie (extraction_method).
4. Correspondance DB#
| Étape | Tables écrites |
|---|---|
| Import | openiti.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) |
| Staging | openiti.stg_work_metadata (+ work_type), stg_version_metadata, stg_documents, stg_entries, stg_entry_pages, stg_issues |
| Canonique | openiti.works, openiti.versions, openiti.documents, openiti.entries |
| Extraction | openiti.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_yml | yml.ts : readYml, ymlGet, ymlSplitMulti |
helper.uri (split author/book/version, siècle) | uri.ts |
helper.funcs.read_header / read_text | markdown.ts : splitHeader |
get_page_numbers / get_page_number | markdown.ts : getPageMarkers |
get_sections / get_section_title | markdown.ts : getSections |
normalize_ara_light | markdown.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
COMPLETEDportant 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#
| Cas | Comportement |
|---|---|
| URI/URL non reconnue | OpenITISourceError (route 422) |
| Dossier œuvre absent | GitHubError NOT_FOUND + message explicite |
| Limite de débit GitHub | GitHubError RATE_LIMITED (poser GITHUB_TOKEN) |
| Plusieurs versions, aucune choisie | VERSION_SELECTION_REQUIRED (WARNING), pas d'import |
| Fichier texte introuvable | errors[], import bloqué |
| Erreur en canonisation | transaction annulée, imports → FAILED + stg_issues BLOCKING (hors tx) |
| Réimport identique | ALREADY_IMPORTED |
| Réimport autre témoin d'une version canonisée | VERSION_CANONICAL_CONFLICT (BLOCKING) |
10. Workflow administration#
/fr/admin → bouton OpenITI → /fr/admin/openiti/import :
- coller URI / URL → Analyser
- lire le préflight (auteur, work URI, titre, dépôt, ref/commit, versions, métadonnées, pagination, TOC, SHA, statut doublon, warnings/errors)
- choisir la version (radio) si plusieurs
- Importer → progression → IMPORT COMPLETED (entrées, pages, sections, unités, stats canoniques, liens)
- 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#
- Obtenir l'URI OpenITI ou l'URL GitHub du dossier.
Analyser, vérifier le préflight, choisir la version,Importer.- Rien à coder si le pipeline générique suffit.
12. Ajouter un extracteur de domaine#
- Créer une classe implémentant
DomainExtractordanssrc/server/openiti/extractors/extractors.ts. supports(workUri, workType)+extract(ctx)→DomainFact[] | DomainRelation[](chaque fait porteentryId,surfaceText,confidence).- L'ajouter au tableau
EXTRACTORS(l'ordre = priorité). runDomainExtractiongère l'idempotence (efface la sortie deextractor.id).- Ne jamais écrire dans
public.narrators— produire des assertions/faits sourcés.
13. Sécurité (mission §36)#
- Le schéma
openitin'est pas exposé via la Data API Supabase (aucune entréedb.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 viaDATABASE_URL(rôlepostgres), jamaisservice_rolecôté client. GITHUB_TOKENfacultatif, 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.