# M7.1 — `SourceConnector` trait + registry | Field | Value | |---|---| | Phase | M7 — Source connectors | | Size | M — 1–3 days | | Status | ⬜ Not started | | Flags | — | | Spec | inlined below | | Blocks | M7.2, M7.3, M7.4, M7.5, M7.6, M7.10 | | Depends | M0.3, M3.6.1 | ## Goal Define the extensible connector interface so that adding a new document source (paperless-ngx, S3, git repo, etc.) requires implementing one trait and adding one YAML config block — no changes to the ingest pipeline, chunking, embedding, storage, or query layers. ## Facts (inlined — no spec read needed) Memory service is a **cluster-wide RAG** serving multiple agents. Knowledge lives in many places: an Obsidian vault, paperless-ngx, git repositories, S3 buckets. The connector abstraction makes all of them look the same to the ingest pipeline. **The trait has three methods.** `list_documents()` enumerates what's available without fetching content. `fetch_document()` retrieves one document's text. `health_check()` reports reachability. The sync framework (M7.6) handles everything else — change detection, tombstoning, chunking, embedding. **Configuration is YAML-driven.** Each connector instance is a block in `connectors.yaml` with `kind`, `name`, and source-specific `config`. The registry maps `kind` to a factory function that constructs the connector from its config. **Two connector families exist.** Session connectors (pi, claude) produce evidence for the gated loop (L0/L1/L2). Document connectors (obsidian, paperless, git, s3) produce reference material at Level R, bypassing the gate. The connector's `source_type()` method declares which family it belongs to. ## Steps 1. Define `SourceConnector` trait in `mem-ingest/src/connector.rs`: ```rust #[async_trait] pub trait SourceConnector: Send + Sync { fn kind(&self) -> &str; fn name(&self) -> &str; fn source_type(&self) -> SourceType; // Evidence or Reference async fn list_documents(&self) -> Result>; async fn fetch_document(&self, doc_id: &str) -> Result; async fn health_check(&self) -> Result; } ``` 2. Define supporting types: `SourceDocument`, `DocumentContent`, `SourceHealth`, `SourceType` enum (`Evidence`, `Reference`). 3. Define `ConnectorConfig` serde struct for YAML deserialization: ```yaml connectors: - kind: obsidian name: homelab-vault config: { root: /data/vault, extensions: [md, txt] } ``` 4. Implement `ConnectorRegistry` — maps `kind` string to factory function, constructs connectors from config at startup. 5. Add `connectors.yaml` loading in `mem-cli` startup path. 6. Provide a `VecConnector` test helper (in-memory documents) for testing downstream consumers without real I/O. ## Acceptance - The trait compiles and is object-safe (`Box`). - `ConnectorRegistry` can register and construct connectors by kind string. - `VecConnector` implements the trait and passes basic list/fetch assertions. - YAML config deserialization works for known and unknown kinds (unknown = skip with warning, not crash). - `source_type()` is enforced at the type level — no runtime flag confusion. ## Verify **Harness:** in-memory `VecConnector`, YAML config fixtures. **Integration test** — `tests/it_source_connector.rs`: 1. `a1_trait_is_object_safe` — construct a `Box` from `VecConnector`; call all three methods. 2. `a2_registry_constructs_by_kind` — register "vec" kind, construct from config, assert `kind()` and `name()` match. 3. `a3_list_documents_returns_all` — `VecConnector` with 3 docs, assert `list_documents()` returns 3. 4. `a4_fetch_document_by_id` — assert content matches what was registered. 5. `a5_fetch_unknown_id_errors` — assert `fetch_document("nonexistent")` returns an error, not a panic. 6. `a6_health_check_reports_count` — assert `health_check()` returns `document_count = Some(3)`. 7. `a7_yaml_config_loads` — parse a fixture `connectors.yaml` with two connector blocks; assert both are constructed. 8. `a8_unknown_kind_skipped` — config with `kind: "nonexistent"`; assert registry logs a warning and continues without the connector. 9. `a9_source_type_evidence_vs_reference` — assert session connectors return `Evidence`, document connectors return `Reference`. 10. `a10_empty_config_is_valid` — no `connectors.yaml` or empty file; registry starts with zero connectors, no crash. **Command:** `cargo test --test it_source_connector` **False pass:** - Testing only `VecConnector` and claiming the trait works. The trait is validated by M7.2–M7.5 implementing it against real sources. - Parsing YAML without verifying the constructed connector's methods work. ## Traps - Making the trait not object-safe (generic methods, `Self` in return types). Every consumer stores `Box`, so object safety is load-bearing. - Putting chunking logic inside the connector. Connectors fetch documents; the sync framework (M7.6) chunks them. Mixing concerns means every connector reimplements chunking. - Hard-coding the list of known kinds. The registry must be extensible — a `register(kind, factory_fn)` call, not a match statement. --- Background: [DESIGN.md](../DESIGN.md) — source connectors section