# Ingestion spec: source documents → knowledge graph Turns admin-uploaded text into `topics` and `relations` using Claude. Runs entirely client-side; there is no ingestion service. - **UI:** `src/components/admin/UploadZone.jsx` (Admin → Sources tab) - **Pipeline:** `src/lib/extractionPipeline.js` - **Tool/schema:** `emit_knowledge_graph` (`src/lib/llmTools.js`) validated by `extractionResultSchema` (`src/lib/llmSchemas.js`) --- ## Upload - Accepted formats: **`.txt` and `.md`**, max **5 MB** per file. - Drag-and-drop or click-to-browse. Unsupported files are skipped with a toast. - The queue tracks each file: `pending → processing → done / failed / cancelled`. - Progress is polled every ~2s from the `sources.progress` field (`{ current, total, message }`), shown as "Chunk N/total". - **Orphan detection:** sources stuck in `processing` for >5 minutes (e.g. a closed tab) can be marked failed or deleted. --- ## Pipeline: `processSourceText(textContent, sourceName, { signal })` 1. **Create source record** with `status='processing'`. 2. **Chunk** the text (`chunkText`): target ~8000 chars per chunk with ~800 chars overlap, splitting on sentence/paragraph boundaries (hard-splitting oversized sentences). Overlap preserves cross-boundary context. 3. **Known-topics hint** (`buildKnownIdsHint`): up to the 200 most recent existing topics are listed so the model reuses existing ids instead of duplicating them. 4. **Per-chunk extraction** via `callLLM`: - tier `standard`, `maxTokens: 8192`, `timeoutMs: 180_000` - forced `toolChoice` on `emit_knowledge_graph` - rate-limited (~20 req/min, burst 2) to protect quota across many chunks - system prompt instructs: ≤15 topics per chunk; topic `type` ∈ {`concept`, `role`, `process`}; `learning_relevance` ∈ {`core`, `standard`, `peripheral`, `exclude`}; relation `type` ∈ {`related_to`, `depends_on`, `part_of`, `executed_by`} 5. **Update progress** before each chunk. 6. **Merge** (`mergeKnowledgeGraph`): topics keyed by `id` (new data updates existing, but `learning_relevance` is preserved when `relevance_locked` is true); relations de-duplicated on `(source, target, type)`. Persisted via `db.saveTopics` / `db.saveRelations`. 7. **Finalize:** `status='completed'`, or `failed` (error) / `cancelled` (abort). Aborting via the `signal` stops the run and marks the source `cancelled`. --- ## Output shape (`emit_knowledge_graph`) ```json { "topics": [ { "id", "label", "type", "description", "learning_relevance" } ], "relations":[ { "source", "target", "type" } ] } ``` `theme`, `complexity_weight`, and `difficulty` are **not** set here — they are added later by the curriculum enrichment step (see `docs/curriculum-spec.md`). --- ## Gotchas - If extraction logs a truncation (`LLMTruncatedError`, `stop_reason: max_tokens`), tighten the per-chunk topic cap before raising `max_tokens`. - A source already `completed` is not re-processed; delete it to force re-analysis. - There are no embeddings produced here — R42 retrieval is computed at query time with TF-IDF over `topics`.