/** * Document service — registers ingested documents and emits the §4 events. * * The ingest pipeline (`src/source/pdf/ingest.ts`) is a pure function over * bytes — it does not touch the engine. The app composition root calls * `ingestPdf` then hands the result to `documentService.register()`, which * is where the engine takes over: persist into the repos, emit * `DocumentImported` + `DocumentRepresentationGenerated`. */ import type { Document, DocumentRepresentation } from "@shared/document"; import type { DocumentId, RepresentationId } from "@shared/ids"; import type { EventBus } from "../events"; import type { DocumentRepository, RepresentationRepository } from "../repos"; export interface DocumentService { register(input: { readonly document: Document; readonly representation: DocumentRepresentation; }): { readonly document: Document; readonly representation: DocumentRepresentation }; get(id: DocumentId): Document | null; list(): readonly Document[]; getRepresentation(id: RepresentationId): DocumentRepresentation | null; listRepresentations(documentId: DocumentId): readonly DocumentRepresentation[]; } export function createDocumentService( documents: DocumentRepository, representations: RepresentationRepository, bus: EventBus, ): DocumentService { return { register({ document, representation }) { const storedDocument = documents.create(document); const storedRepresentation = representations.create(representation); bus.emit({ type: "DocumentImported", documentId: storedDocument.id, document: storedDocument, }); bus.emit({ type: "DocumentRepresentationGenerated", documentId: storedDocument.id, representationId: storedRepresentation.id, representation: storedRepresentation, }); return { document: storedDocument, representation: storedRepresentation }; }, get(id) { return documents.get(id); }, list() { return documents.list(); }, getRepresentation(id) { return representations.get(id); }, listRepresentations(documentId) { return representations.listByDocument(documentId); }, }; }