Introduces the data layer for multi-store ingestion. Documents can now
live in multiple stores, each with its own Ingested/Stale/Failed state.
The doc-level lifecycle (#202) becomes the aggregate over all per-store
links, computed by a pure domain function.
Domain
- Store entity (name, slug, kind, embedder, config, is_default)
- DocumentStoreLink entity with mark_ingested / mark_stale / mark_failed
helpers
- StoreKind and DocumentStoreLinkState enums
- aggregate_lifecycle(): pure function — Failed > Stale > Ingested
> fallback (the doc's pre-link Uploaded/Parsed/Chunked state)
Persistence
- New tables 'stores' and 'document_store_links' with the right indexes
(doc_id, store_id, state) and a UNIQUE (doc, store) on the link
- Default 'opensearch' store seeded idempotently in init_db, embedder
pulled from DEFAULT_EMBEDDER (fallback bge-m3)
- SqliteStoreRepository (find_by_slug, find_by_id, get_default, …)
- SqliteDocumentStoreLinkRepository with ON CONFLICT … DO UPDATE upsert
Ports
- StoreRepository and DocumentStoreLinkRepository protocols added
Tests
- 14 new tests: seed idempotency, insert/find round-trips, UNIQUE
constraint, cascade delete with the document, every link state
round-trips, aggregation rule with all branches
Refs #203