qmd_py.store¶
The service-layer facade: collection/context CRUD, content-addressed
ingest, document retrieval (get/multi-get/ls), and the ACL choke
points every one of those goes through.
store ¶
Service/CRUD layer - the single facade the CLI, MCP server, and any
future REST API all call through (fixes the TS reference implementation's
inconsistency #3, where the SDK's searchLex/searchVector silently
skipped behavior the CLI's search/vsearch commands did: one code path
per operation here, not two that can drift apart).
Every function that touches a specific collection's data resolves the
Collection row and calls can_access() (see auth.py) before proceeding -
mocked to always allow today, but the choke point is real from day one.
This was one 1000-line module; it is now a package split by
responsibility. The whole public API is re-exported here, so
from qmd_py.store import ... keeps working exactly as before and no
caller needed changing. Submodules are layered to avoid cycles:
_common -> (nothing in-package)
cleanup -> _common
documents-> _common
collection, context, indexing, retrieval -> the above
DEFAULT_MULTI_GET_MAX_BYTES
module-attribute
¶
The TS reference's real default - its own --help text says "10KB", but the code's actual default is 64KB (one of the plan's flagged inconsistencies to fix, not reproduce).
CollectionNotFoundError ¶
Bases: Exception
No collection of that name is visible to the caller.
Also what a non-owner gets for a collection that does exist, since
lookups prefilter on ownership in SQL before can_access() is
consulted - see auth.py.
PermissionDeniedError ¶
Bases: Exception
The collection was found, but can_access() refused the operation.
Unreachable while can_access() is mocked to always allow; the call
sites raise it so a real check needs no new error handling.
CollectionListRow
dataclass
¶
CollectionListRow(
name: str,
path: str,
pattern: str,
doc_count: int,
active_count: int,
last_modified: datetime | None,
include_by_default: bool,
)
One row of list_collections(), with its document statistics.
Attributes:
-
name(str) –Collection name, unique per owner.
-
path(str) –Filesystem path the collection indexes.
-
pattern(str) –Glob used when walking that path.
-
doc_count(int) –Active documents. Currently identical to
active_count- the distinction is reserved for when inactive (deactivated but not yet reclaimed) rows are counted separately. -
active_count(int) –Documents currently on disk and indexed.
-
last_modified(datetime | None) –Newest
modified_atamong the active documents, or None for an empty collection. -
include_by_default(bool) –Whether unscoped queries search this collection.
RemoveCollectionResult
dataclass
¶
What remove_collection() reclaimed.
Attributes:
-
deleted_docs(int) –Document rows removed, active and inactive alike.
-
cleaned_hashes(int) –Content rows dropped because no document anywhere still referenced them. Lower than
deleted_docswhenever another collection shares the same file content, since content is addressed by hash and shared across collections.
CollectionMissingContext
dataclass
¶
A collection with no context set at all.
Attributes:
-
name(str) –Collection name.
-
path(str) –Its indexed filesystem path.
-
doc_count(int) –Active documents in it - a rough measure of how much search quality is being left on the table.
ContextRow
dataclass
¶
One stored context entry, flattened for listing.
Attributes:
-
collection(str) –Owning collection's name.
-
path(str) –The prefix this context applies to;
""means the whole collection. -
context(str) –The prose itself.
ReindexResult
dataclass
¶
ReindexResult(
indexed: int,
updated: int,
unchanged: int,
removed: int,
orphaned_cleaned: int,
skipped_oversize: int = 0,
)
Summary counts from one reindex_collection() pass.
Informational only - the CLI prints them - so they are not relied on for correctness anywhere. Files skipped as unreadable or blank appear in no bucket at all, so the four document counts need not sum to the number of files on disk.
Attributes:
-
indexed(int) –Files that had no document row and got one.
-
updated(int) –Existing documents whose content or title changed. Also counts a file reappearing after deactivation, and a title-only change with an unchanged body hash.
-
unchanged(int) –Active documents whose hash and title both matched, so nothing was written.
-
removed(int) –Documents deactivated because their file is no longer on disk. Deactivated, not deleted -
marq cleanupreclaims them. -
orphaned_cleaned(int) –Content rows dropped because no document, active or inactive, still referenced them.
-
skipped_oversize(int) –Files skipped for exceeding
MAX_INDEXABLE_BYTES- the one skip reason that gets its own visible count, because silently dropping a legitimate (if huge) file from the index would otherwise look like a search bug.
CollectionStatus
dataclass
¶
CollectionStatus(
name: str,
path: str,
pattern: str,
doc_count: int,
last_updated: datetime | None,
)
Per-collection line of get_status().
Attributes:
-
name(str) –Collection name.
-
path(str) –Indexed filesystem path.
-
pattern(str) –Glob used to walk it.
-
doc_count(int) –Active documents.
-
last_updated(datetime | None) –Newest document mtime, or None when empty.
DocumentDetail
dataclass
¶
DocumentDetail(
filepath: str,
display_path: str,
title: str,
context: str | None,
hash: str,
docid: str,
collection_name: str,
modified_at: datetime,
body_length: int,
body: str,
)
One resolved document, body included.
Carries the path in three forms because different surfaces want different ones - MCP resources use the URI, CLI output the readable form, glob matching all three.
Attributes:
-
filepath(str) –Virtual URI,
marq://<collection>/<path>. -
display_path(str) –<collection>/<path>- what CLI output prints. -
title(str) –Extracted heading, or the filename stem as fallback.
-
context(str | None) –Hierarchical context for this path (global first, then each matching prefix, most general first), or None if none applies.
-
hash(str) –Full SHA-256 of the body.
-
docid(str) –First 6 chars of
hash- what#abc123lookups use. Short enough to collide in principle; resolution is deterministic. -
collection_name(str) –Owning collection.
-
modified_at(datetime) –Source file's mtime at index time, not the row's.
-
body_length(int) –len(body)in characters, not bytes. -
body(str) –Full document text.
DocumentNotFound
dataclass
¶
Returned instead of raising when a lookup finds nothing.
A miss is an ordinary outcome here - the caller is usually a person mistyping a path - so it is a value to render, not an exception.
Attributes:
-
query(str) –The lookup string as given, so the caller can echo it back.
-
similar_files(list[str]) –Up to five paths within Levenshtein distance 5, for a "did you mean" hint. Empty when the query looked like a docid, since edit distance over hex is meaningless.
FileRow
dataclass
¶
One row of list_files() - metadata only, no body.
Attributes:
-
path(str) –Path relative to the collection root.
-
title(str) –Extracted heading or filename stem.
-
modified_at(datetime) –Source file's mtime as recorded at index time.
-
size(int) –Body length in characters, computed in SQL.
GlobMatch
dataclass
¶
One glob hit, without its body.
Attributes:
-
filepath(str) –Virtual URI,
marq://<collection>/<path>. -
display_path(str) –Bare relative path, not collection-prefixed - ambiguous across collections, but kept for parity with the TS reference's own convention.
-
body_length(int) –Character count, fetched with
length()in SQL so the body itself is never loaded.
MultiGetFile
dataclass
¶
MultiGetFile(
filepath: str,
display_path: str,
title: str,
body: str,
context: str | None,
skipped: bool,
docid: str | None = None,
skip_reason: str | None = None,
)
One document from a multi_get() batch, possibly skipped.
Attributes:
-
filepath(str) –Virtual URI,
marq://<collection>/<path>. -
display_path(str) –<collection>/<path>. -
title(str) –Extracted heading or filename stem.
-
body(str) –Document text, already truncated and line-numbered per the call's arguments. Empty string when
skipped. -
context(str | None) –Hierarchical context for this path, or None.
-
skipped(bool) –True when the file exceeded
max_bytesand was not read. -
docid(str | None) –Six-char hash prefix, for a follow-up
get. -
skip_reason(str | None) –Human-readable explanation, set only when
skipped.
StatusInfo
dataclass
¶
Index summary behind marq status.
Attributes:
-
total_documents(int) –Active documents across every readable collection.
-
collections(list[CollectionStatus]) –Per-collection detail, ordered by name.
add_line_numbers ¶
Prefix each line with N:.
Parameters:
-
start_line(int, default:1) –Number for the first line, so a slice taken from partway through a document still reports true line numbers.
Returns:
-
str–The text with every line prefixed. A trailing newline yields a
-
str–numbered empty final line, since the split is unconditional.
Source code in src/qmd_py/store/_common.py
extract_title ¶
Port of the TS reference's extractTitle(): first #/## markdown
heading (skipping a generic "Notes" heading in favor of the next one,
a quirk carried over from the TS version's own note-taking-app
interop), #+TITLE:/org heading for .org files, else the filename
without its extension.
Source code in src/qmd_py/store/_common.py
hash_content ¶
SHA256 hex digest - matches the TS reference's hashContent()
exactly (content-addressable hashes must agree byte-for-byte with the
TS side for the Phase 3 parity check to mean anything).
Sync: pure CPU, no I/O to await. It was async only because the TS
original returns a Promise.
Source code in src/qmd_py/store/_common.py
utcnow ¶
Timezone-aware current time.
Returns:
-
datetime–datetime.now(UTC)- aware, not naive, since every timestamp -
datetime–column is
TIMESTAMP WITH TIME ZONEand psycopg rejects naive -
datetime–values for those.
Source code in src/qmd_py/store/_common.py
cleanup_orphaned_content
async
¶
A hash is only truly orphaned once NO document row - active or
inactive - references it: document.hash is a NOT NULL FK with no
ON DELETE CASCADE, so Postgres blocks deleting a content row an
inactive document still points to, even though search/retrieval
never reads inactive documents. Filtering this query to only active
documents (as an earlier version did) raised a FK violation the first
time an inactive document was the last remaining reference. Run
delete_inactive_documents first (see the cleanup command) to
actually reclaim content orphaned only by now-abandoned inactive
rows.
Returns:
-
int–Number of content rows deleted.
Source code in src/qmd_py/store/cleanup.py
delete_inactive_documents
async
¶
Hard-delete deactivated document rows across a user's collections.
Removes the rows only; their content is cleanup_orphaned_content's
job, and has to run after this to reclaim bodies whose last
remaining reference was one of these inactive rows.
Deliberately irreversible: once these are gone, a file that reappears is indexed as a new document rather than reactivating its old row.
Returns:
-
int–Number of document rows deleted; 0 when the user has no
-
int–accessible collections.
Source code in src/qmd_py/store/cleanup.py
delete_llm_cache
async
¶
Empty the cached-LLM-response table.
Global, not per user: the cache is keyed by prompt, so there is nothing user-scoped to preserve.
Returns:
-
int–Number of cached responses deleted.
Source code in src/qmd_py/store/cleanup.py
add_collection
async
¶
add_collection(
session: AsyncSession,
user: CurrentUser,
name: str,
path: str,
pattern: str = "**/*.md",
ignore: list[str] | None = None,
) -> Collection
Register a new collection. Does not index it.
Indexing is a separate step: call reindex_collection() afterwards
(that is what marq collection add does), so a caller can create and
populate a collection in one transaction.
Parameters:
-
name(str) –Unique per owner, not globally - two users may each have a collection called
notes. -
path(str) –Absolute filesystem path to walk. Not validated here; a path that doesn't exist simply yields no files at reindex.
-
pattern(str, default:'**/*.md') –Glob relative to
path. Brace groups are expanded ({src,docs}/**/*.{md,py}), which plainglobcannot do. -
ignore(list[str] | None, default:None) –Extra glob patterns to skip, on top of the always-excluded hidden files and
node_modules/.git/dist-style directories.
Returns:
-
Collection–The persisted row, with
idpopulated by the flush.
Raises:
-
IntegrityError–This owner already has a collection of that name (
UNIQUE(owner_user_id, name)). Raised on flush, so the caller must roll back before reusing the session.
Source code in src/qmd_py/store/collection.py
get_collection
async
¶
Fetch one collection by name, gated for read access.
Returns:
-
Collection–The ORM row, so callers can read
path/pattern/update_command -
Collection–directly.
Raises:
-
CollectionNotFoundError–No collection of that name owned by
user. -
PermissionDeniedError–can_access()refusedreadon it.
Source code in src/qmd_py/store/collection.py
list_collections
async
¶
Every collection user can read, with per-collection statistics.
Today that is all of them, since can_access() is mocked True - but
the filter is real and applied per row, not skipped.
Returns:
-
list[CollectionListRow]–Rows ordered by collection name.
Note
Runs one statistics query per collection (an N+1). Fine at the scale this runs at - a handful of collections per user - but the counts would need folding into a single grouped query before that stopped being true.
Source code in src/qmd_py/store/collection.py
remove_collection
async
¶
Delete a collection, its documents, contexts and grants.
Deletes the contexts and grants before the collection row -
collectioncontext_collection_id_fkey/
collectiongrant_collection_id_fkey have no ON DELETE CASCADE, so
Postgres blocks the collection delete while either still references it
(caught live: removing a collection with any per-path context still
set raised a raw FK violation instead of succeeding).
Returns:
-
RemoveCollectionResult–Counts of what was removed; see
RemoveCollectionResult.
Raises:
-
CollectionNotFoundError–No collection of that name owned by
user. Note this is also what a non-owner gets, because the lookup prefilters on ownership in SQL - see auth.py. -
PermissionDeniedError–can_access()refusedadminon it.
Source code in src/qmd_py/store/collection.py
rename_collection
async
¶
rename_collection(
session: AsyncSession,
user: CurrentUser,
old_name: str,
new_name: str,
) -> None
Rename a collection in place, keeping its documents and contexts.
Raises:
-
CollectionNotFoundError–No collection named
old_nameowned byuser. -
PermissionDeniedError–can_access()refusedadminon it. -
IntegrityError–new_nameis already taken by another of this owner's collections. Raised on flush, not checked up front.
Source code in src/qmd_py/store/collection.py
set_include_by_default
async
¶
set_include_by_default(
session: AsyncSession,
user: CurrentUser,
name: str,
include: bool,
) -> None
Include or exclude this collection from unscoped queries.
Backs marq collection include/exclude. Excluding removes it from the
default scope only: an explicit -c <name> still searches it.
Note
Excluding every collection leaves the default scope empty, and an
empty scope matches nothing rather than everything. That direction
was once inverted - see _filter_by_collections in
cli/commands/read.py.
Parameters:
-
include(bool) –True to put it back in the default scope, False to drop it.
Raises:
-
CollectionNotFoundError–No collection of that name owned by
user. -
PermissionDeniedError–can_access()refusedadminon it.
Source code in src/qmd_py/store/collection.py
set_update_command
async
¶
set_update_command(
session: AsyncSession,
user: CurrentUser,
name: str,
command: str | None,
) -> None
Set the shell command run before this collection is re-indexed.
Used for collections backed by something that has to be refreshed
first - a git pull, an export script - so marq update can bring
the source up to date before walking it.
Parameters:
-
command(str | None) –Shell command to run from the collection's path, or None to clear it.
Raises:
-
CollectionNotFoundError–No collection of that name owned by
user. -
PermissionDeniedError–can_access()refusedadminon it.
Source code in src/qmd_py/store/collection.py
add_context
async
¶
add_context(
session: AsyncSession,
user: CurrentUser,
collection_name: str,
path_prefix: str,
text: str,
) -> None
Attach context prose to a collection, or to a path within it.
Upserts: setting context for a prefix that already has one replaces the text rather than failing or accumulating.
Contexts nest. A document inherits every context whose prefix it
starts under, joined most-general-first, so a root context and a
journal/ context both reach journal/entry.md.
Parameters:
-
path_prefix(str) –Path within the collection, or
""for the whole collection. Matched as a path prefix, not a glob. -
text(str) –The prose to attach. Search results carry it alongside the body so an agent knows what kind of document it is reading.
Raises:
-
CollectionNotFoundError–No collection of that name owned by
user. -
PermissionDeniedError–can_access()refusedwriteon it.
Source code in src/qmd_py/store/context.py
context_check
async
¶
context_check(
session: AsyncSession, user: CurrentUser
) -> tuple[
list[CollectionMissingContext], dict[str, list[str]]
]
Port of the TS reference's getCollectionsWithoutContext +
getTopLevelPathsWithoutContext, combined into the one context check
command they jointly back (which the TS CLI never actually wired up
despite CLAUDE.md documenting it - see the qmd-py plan's list of fixed
TS inconsistencies).
Two different gaps are reported, which is why the return value is a
pair: a collection with no context at all, and a collection that has
some context but leaves whole top-level directories uncovered. A
collection with a root ("") context is never reported for the
second, since that covers everything beneath it.
Returns:
-
list[CollectionMissingContext]–A
(collections, paths)pair.collectionslists collections -
dict[str, list[str]]–with no context, sorted by name.
pathsmaps a collection name to -
tuple[list[CollectionMissingContext], dict[str, list[str]]]–its uncovered top-level directories, sorted, and omits collections
-
tuple[list[CollectionMissingContext], dict[str, list[str]]]–with nothing missing - so an empty dict means fully covered.
Note
ACL (see auth.py): owner-prefiltered in SQL rather than gated
through can_access() - same widening needed as list_contexts
when grants go live.
Runs two to three queries per collection (an N+1), which is fine for an interactive advisory command.
Source code in src/qmd_py/store/context.py
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 | |
get_global_context
async
¶
Return this user's global context.
Returns:
-
str | None–The prose, or None when unset - which is also what a user row that
-
str | None–somehow doesn't exist yields, since the caller has no use for the
-
str | None–distinction.
Source code in src/qmd_py/store/context.py
list_contexts
async
¶
Every context this user has set, across all their collections.
Returns:
-
list[ContextRow]–Rows ordered by collection name, then path prefix - so a
-
list[ContextRow]–collection's root context sorts before its sub-path ones.
Note
ACL (see auth.py): owner-prefiltered in SQL rather than gated
through can_access(), so a granted-but-not-owned collection's
contexts stay hidden until this query is widened.
Source code in src/qmd_py/store/context.py
remove_context
async
¶
remove_context(
session: AsyncSession,
user: CurrentUser,
collection_name: str,
path_prefix: str,
) -> bool
Delete the context set for one exact path prefix.
Parameters:
-
path_prefix(str) –Must match what was stored exactly - this deletes one row, it does not clear a subtree.
Returns:
-
bool–True if a row was deleted, False if there was nothing set for that
-
bool–prefix. Removing a context that isn't there is not an error.
Raises:
-
CollectionNotFoundError–No collection of that name owned by
user. -
PermissionDeniedError–can_access()refusedwriteon it.
Source code in src/qmd_py/store/context.py
set_global_context
async
¶
Set the context that applies across all of this user's collections.
The TS reference's context add /. User-scoped here, not a single
system-wide value (see User.global_context in db/models.py). It is
prepended before any per-path context, so it reads as the most general
statement about everything this user has indexed.
Parameters:
-
text(str | None) –The prose, or None to clear it.
Source code in src/qmd_py/store/context.py
deactivate_document
async
¶
Mark a document inactive, keeping the row.
Soft delete: search and retrieval ignore inactive documents, but the
row survives so a returning file can reactivate it rather than
colliding with UNIQUE(collection_id, path). marq cleanup is what
hard-deletes them.
A path with no row is a no-op, not an error.
Source code in src/qmd_py/store/documents.py
find_active_document
async
¶
Find the live document at a path, ignoring deactivated rows.
Returns:
-
Document | None–The document, or None if there is none or it has been
-
Document | None–deactivated. Use
find_document_by_path()when a deactivated row -
Document | None–still matters.
Source code in src/qmd_py/store/documents.py
find_document_by_path
async
¶
Find the document at a path whether or not it is active.
reindex_collection needs this rather than find_active_document, to
reactivate a deactivated document whose file reappears instead of
violating Document's table-wide UNIQUE(collection_id, path)
constraint by inserting a second row.
Returns:
-
Document | None–The document, active or not, or None if the path was never
-
Document | None–indexed.
Source code in src/qmd_py/store/documents.py
get_active_document_paths
async
¶
List the paths currently indexed and active in a collection.
Returns:
-
list[str]–Paths relative to the collection root, unordered.
reindex_collection -
list[str]–diffs this against what it found on disk to decide what to
-
list[str]–deactivate.
Source code in src/qmd_py/store/documents.py
insert_content
async
¶
Store a body under its content hash, if not already present.
Idempotent by design: content is addressed by hash and shared across every collection, so re-indexing an unchanged file - or the same file in a second worktree - inserts nothing and costs nothing.
Parameters:
-
hash_(str) –SHA-256 hex digest of
doc, fromhash_content(). Not recomputed or verified here. -
doc(str) –The full body text.
Source code in src/qmd_py/store/documents.py
insert_document
async
¶
insert_document(
session: AsyncSession,
collection_id: int,
path: str,
title: str,
hash_: str,
created_at: datetime,
modified_at: datetime,
) -> Document
Create a document row and build its search vector.
Assumes the content row already exists - call insert_content()
first, since document.hash is a NOT NULL foreign key.
Parameters:
-
path(str) –Path relative to the collection root.
-
hash_(str) –Content hash linking to the stored body.
-
created_at(datetime) –Usually the file's mtime rather than now, so the value survives a reindex from a fresh clone.
-
modified_at(datetime) –The file's mtime.
Returns:
-
Document–The persisted row, with
idpopulated by the flush.
Raises:
-
IntegrityError–A row already exists for this
(collection_id, path), active or not, orhash_has no content row.
Source code in src/qmd_py/store/documents.py
update_document
async
¶
update_document(
session: AsyncSession,
document: Document,
title: str,
hash_: str,
modified_at: datetime,
) -> None
Point a document at new content and rebuild its search vector.
Reactivates it (active=True) unconditionally: every caller reaches
this because a real file on disk currently maps to this
(collection_id, path), including a file that reappeared after being
deactivated - switching git branches back and forth over the same
indexed working tree does exactly that. Document has a table-wide
UNIQUE(collection_id, path) with no partial/active condition, so
there can only ever be one row per path regardless of active status;
reactivating it is the only option, inserting a second is not.
Parameters:
-
document(Document) –The row to update, already loaded in this session.
-
hash_(str) –New content hash. Its content row must exist.
-
modified_at(datetime) –The file's current mtime.
Source code in src/qmd_py/store/documents.py
reindex_collection
async
¶
Walk a collection's filesystem path, syncing document/content
to match what's on disk - backs collection add and update.
Disk is the source of truth for one direction only: a file that has
vanished deactivates its document rather than deleting it, so the row
survives to be reactivated if the file comes back (switching git
branches over an indexed working tree does exactly that). Document
has a table-wide UNIQUE(collection_id, path) with no active/inactive
carve-out, so reactivating the existing row is the only option -
inserting a second one is impossible.
Simplification vs. the TS reference: a title-only change (same content
hash) counts as updated rather than getting its own bucket.
A skipped file never aborts the run. Unreadable or non-UTF-8 files
are skipped with a WARNING; whitespace-only files with a DEBUG line
(an empty __init__.py is a normal state, not a degrade - anything
louder would break the healthy-run-is-silent contract on every
reindex of an ordinary code collection); files excluded by the
collection's pattern or ignore rules, silently. One case is counted
as well as logged: files over MAX_INDEXABLE_BYTES (see
skipped_oversize). A previously indexed file that has since crossed
the cap is treated like any other skipped file - its document is
deactivated.
Parameters:
-
name(str) –Collection name, resolved against
user's own collections.
Returns:
-
ReindexResult–Per-bucket counts; see
ReindexResult.
Raises:
-
CollectionNotFoundError–No collection of that name owned by
user. -
PermissionDeniedError–can_access()refusedwriteon it.
Note
Reads every matched file and hashes it on each run - there is no mtime shortcut. Content addressing makes that cheap in storage (an unchanged file writes nothing) but not in I/O, so the cost scales with total collection size, not with how much changed.
Source code in src/qmd_py/store/indexing.py
find_document
async
¶
find_document(
session: AsyncSession,
user: CurrentUser,
filename: str,
collection_name: str | None = None,
) -> DocumentDetail | DocumentNotFound
Resolve one document by docid, virtual path, or bare path.
Tries, in order: docid (#abc123 or bare hex, matched as a hash
prefix), exact virtual path (marq://collection/path), exact bare
path, then a suffix match against the full virtual path - so both
sample/src/foo.py and a partial src/foo.py resolve. Port of the TS
reference's findDocument.
Parameters:
-
filename(str) –Docid or path, in any of the forms above.
-
collection_name(str | None, default:None) –Restrict to one collection. None searches every collection the user can read.
Returns:
-
DocumentDetail | DocumentNotFound–A
DocumentDetailwith the body, or aDocumentNotFoundcarrying -
DocumentDetail | DocumentNotFound–near-miss suggestions. Never raises for a miss.
Note
A 6-char docid can front two documents. Which one wins is
arbitrary but stable: _active_document_refs is ordered, so
repeated lookups agree rather than depending on row order.
Source code in src/qmd_py/store/retrieval.py
get_status
async
¶
Backs status - reuses list_collections's per-collection stats
rather than recomputing them. Deliberately narrower than the TS
reference's own status command for now (no MCP-daemon liveness, no
AST-chunking availability, no embedding-completeness/vector-index-
health checks) - those need Phase 9's MCP server and Phase 5's
embedding pipeline wired into the CLI first.
Returns:
-
StatusInfo–Totals and per-collection detail; see
StatusInfo. Counts only -
StatusInfo–collections the user can read, so it is empty rather than an error
-
StatusInfo–for a user with none.
Source code in src/qmd_py/store/retrieval.py
list_files
async
¶
list_files(
session: AsyncSession,
user: CurrentUser,
collection_name: str,
path_prefix: str | None = None,
) -> list[FileRow]
List one collection's active files, optionally under a sub-path.
Backs marq ls <collection>[/path].
Parameters:
-
path_prefix(str | None, default:None) –Restrict to paths starting with this string. Matched as a literal prefix (LIKE wildcards
%/_are escaped), not as a glob, so*has no special meaning either.
Returns:
-
list[FileRow]–Rows ordered by path. Empty for a collection with no matches -
-
list[FileRow]–only an unknown collection raises.
Raises:
-
CollectionNotFoundError–No collection of that name owned by
user. -
PermissionDeniedError–can_access()refusedreadon it.
Source code in src/qmd_py/store/retrieval.py
match_files_by_glob
async
¶
Glob-match against three forms of every active, accessible
document's path (virtual marq://collection/path, bare path, and
collection/path) - matches if any form matches, same as the TS
reference's matchFilesByGlob. display_path is the bare relative
path (not collection-prefixed), matching that function's own
(slightly ambiguous across collections) convention.
Source code in src/qmd_py/store/retrieval.py
multi_get
async
¶
multi_get(
session: AsyncSession,
user: CurrentUser,
pattern: str,
max_lines: int | None = None,
max_bytes: int = DEFAULT_MULTI_GET_MAX_BYTES,
line_numbers: bool = True,
) -> list[MultiGetFile]
Fetch multiple documents by glob pattern or comma-separated list -
port of the TS reference's multiGet (src/cli/qmd.ts).
The two forms are mutually exclusive, not combinable: pattern counts
as a comma-separated list only if it holds no glob metacharacter at
all. So "a.md,b*.md" is treated as one glob containing a literal
comma and matches nothing, rather than as two patterns. That's the TS
reference's behavior, kept deliberately for parity - split such a
request into separate calls.
Oversized files are reported rather than dropped: the entry comes back
with skipped=True and a skip_reason, so a caller can tell "too
large, fetch it singly" from "no such file".
Parameters:
-
pattern(str) –Glob, or a comma-separated list of paths/docids.
-
max_lines(int | None, default:None) –Truncate each body to this many lines, appending a note about how many were omitted. None keeps the whole body.
-
max_bytes(int, default:DEFAULT_MULTI_GET_MAX_BYTES) –Skip any document longer than this rather than returning it. Compared against the character length.
-
line_numbers(bool, default:True) –Prefix each body line with
N:.
Returns:
-
list[MultiGetFile]–One entry per match, in the order the documents were scanned.
-
list[MultiGetFile]–Empty when nothing matched - a miss is not an error here.
Source code in src/qmd_py/store/retrieval.py
332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 | |