Architecture Decision Records¶
This section holds Architecture Decision Records (ADRs) — settled decisions with named rejected alternatives and the reasoning behind them, extracted out of the Software Requirements Specification (SRS) chapters that depend on them. The SRS states what PriorisMCP does; an ADR states why one particular way of doing it was chosen over the others considered, so a reader (human or agent) working from the SRS alone never has to read through a discarded alternative just to find the requirement.
Numbering is assigned sequentially at extraction time, in SRS document order (chapter 1 onward) — not by when the underlying decision was originally made. A number, once assigned, is never reused or reordered: a later-reversed decision gets a new ADR whose status is superseded-by, with the superseded ADR's own status updated to point forward, rather than editing history in place.
| ADR | Title | Status | Summary |
|---|---|---|---|
| 00001 | PDF parsing backend: liteparse over docling |
accepted | Avoids a PyTorch dependency for v1's PDF-to-Markdown conversion. |
| 00002 | HTML/JATS parsing pipeline: html-to-markdown + vendored XSLT |
accepted | Table-fidelity HTML conversion, reused for JATS via XSLT, with two purpose-built fixes over a third-party MathML library. |
| 00003 | DOI routing to an unsupported provider fails, rather than scraping | accepted | A DOI landing outside arXiv/Europe PMC is a hard unsupported_provider error, not a scrape attempt. |
| 00004 | SearchIndex as a separate interface from StorageBackend |
accepted | Full-text search doesn't share StorageBackend's medium-specific infrastructure, so it doesn't share its interface either. |
| 00005 | Local-filesystem source identity via content hashing | accepted | No external authority exists to pin a "current version" of caller-sent bytes, so SHA-256 of the content substitutes for resolve_identifier. |
| 00006 | Notes anchoring: document-level, not span-level | accepted | Span-level note anchors were rejected on four independent grounds; notes anchor to the document as a whole. |
| 00007 | Notes export as a resource, not a tool | accepted | Export reuses the tool/resource split already in the codebase and avoids the server writing to an arbitrary host path. |
| 00008 | Storage keys are hashed, not built from the raw identifier | accepted | The document-hash is sha256(provider, canonical identifier), sidestepping path-traversal/escaping risk entirely rather than mitigating it case by case. |
| 00009 | Catalogue as SQLite, not append-only JSONL-plus-replay | accepted | A unique constraint plus INSERT ... ON CONFLICT gives atomic concurrent writes across multiple local writer processes natively. |
| 00010 | FTS5 as a plain virtual table, not an external-content table | accepted | The source of truth is a separate markdown file on disk, which SQLite's content='' external-content mechanism can't reference. |
| 00011 | One global search index, not one per document | accepted | BM25's IDF term is only meaningful across multiple documents; a per-document index buys no ranking benefit for real file-proliferation cost. |
| 00012 | Separate SQLite files, not one shared database | accepted | Isolates the disposable search.sqlite3 cache from durable catalogue.sqlite/manifest.sqlite, and buys per-document locking, for a measured ~7.3KB/document floor. |
| 00013 | notes-search.sqlite3 is deliberately minimal, not denormalized like search.sqlite3 |
accepted | Notes never had the "one index per document" problem search.sqlite3's denormalization solves, so it doesn't need the same columns. |
| 00014 | Notes export is one file per note, not one file per document | accepted | Matches the data model 1:1 and follows the Obsidian atomic-note convention over long files with internal anchors. |
| 00015 | Discovery scope is OpenAlex search.semantic, not a general multi-aggregator expansion |
accepted | Fanning out to Semantic Scholar/PubMed/CORE was a false generalisation — none of the three replicate embedding-based search.semantic. |
| 00016 | research_discovery is a new tool, not a mode on research_search_fetched |
accepted | External not-yet-fetched candidates are structurally incompatible with research_search_fetched's local-corpus result shape. |
| 00017 | Auto-fetch of OA PDF URLs is rejected, for now | accepted | Arbitrary OA PDF URLs are an SSRF-shaped trust surface, unlike arXiv/Europe PMC's sanctioned APIs; manual upload already covers the gap. |
| 00018 | Crossref and PubMed are excluded from discovery | accepted | Crossref has no search.semantic analog; PubMed is redundant with the existing Europe PMC provider. |
| 00019 | Semantic Scholar's similarity capability — placement, not adoption | accepted | If adopted, its Recommendations API belongs beside research_discovery, not a locally-scoped citation/concept-linking mechanism — the boundary is corpus scope, not algorithm shape. |
| 00020 | Corpus topology — two corpus-wide VectorSearchBackend instances |
accepted | Not one unified type-tagged table (the use case is directional, not merged-ranked), and not per-document (would regress cross-document notes search). |
| 00021 | Vector search engine: sqlite-vec (asg017) |
accepted | Chosen over DuckDB+vss, LanceDB, ChromaDB, Qdrant Edge, and sqlite-vector — license, footprint, and fit with this project's embedded-SQLite pattern and scale. |
| 00022 | SearchIndex/VectorSearchBackend stay separate interfaces |
accepted | Coupling them behind one engine would force any future hosted swap to support every coupled function at once. |
| 00023 | Default EmbeddingBackend is fastembed, not sentence-transformers |
accepted | Avoids the PyTorch/transformers stack; 1536-dimension "target" rejected as an OpenAI-specific artifact, not a real convergence point. |
| 00024 | Composition: a mode parameter, no server-side result fusion |
accepted | Rejects both one opaque "smart search" call and an earlier draft's server-side RRF fusion in hybrid mode. |
| 00025 | index_status travels with every search response, not a separate status tool |
accepted | A separate status tool would reintroduce the exact ambiguity — empty results with no explanation — it's meant to prevent. |
| 00026 | Chunk identity is a UUID minted per parse pass, not heading text or position | accepted | Heading text isn't unique within a document; character offset drifts on incidental re-parse differences. |
| 00027 | Per-provider, domain-prefixed tools, not one generic tool parameterised by provider | accepted | A generic provider-parameterised tool can't express per-provider identifier/format validation cleanly as one JSON schema. |
| 00028 | Local-filesystem access is via caller-sent content, not a server-side path | accepted | An earlier path-based design assumed client and server share a filesystem, which streamable-http/http transport breaks. |
| 00029 | JATS transform concurrency is bounded by a dedicated gate, not anyio's to_thread.run_sync(limiter=...) |
accepted | anyio's capacity limiter releases on caller cancellation, not worker completion, so it doesn't bound abandoned-but-still-running transforms. |
| 00030 | Corpus-wide vector-index reconciliation runs automatically at server startup, not as a tool | accepted | A blocking model-mismatch reconnect gates readiness; unbounded corpus re-embedding runs backgrounded via lifespan, never as a caller-invoked tool. |
| 00031 | Rebuild progress exposes succeeded/failed counts, not an in-process retry policy | accepted | A failed re-embed left remaining stuck forever with no evidence of what happened; a restart, not in-process retry, remains the recovery path. |