Skip to content

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.