This repository has been archived on 2026-10-08. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
recoll-modern/docs/DECISIONS.md
T
Lucas WintherandClaude Opus 5 def811bab4 docs: close the six open design questions for spec 001
Records the outcome of the 001 brainstorm and the Recoll spike that backed it.

Decisions: Python + FastAPI managed with uv; the product split into four specs
built in order; collections managed at runtime through the API with SQLite as
the store of record; indexing as an in-process job queue; search scope as an
explicit required parameter; results carrying metadata and snippets; real
authentication in 001 with user administration deferred to 004.

The load-bearing one is collection membership. A collection cannot be a
directory, because files sharing a directory must be able to belong to
different collections, so every document is indexed once into a single Recoll
index and membership is a user.collection extended attribute mapped to a
queryable field. Verified against Recoll 1.43.0 in a throwaway container: the
tag must be an opaque single-token ID rather than the collection name, because
Recoll tokenises the field value and matches case-insensitively, so a tag of
cook-books is also matched by collection:cook and collection:books. Names as
tags would let collections silently bleed into each other. Opaque IDs also make
renaming free, since the index is never touched.

Two operational consequences are recorded in docs/TODO.md: documents must live
on a volume with extended-attribute support, checked loudly at startup, and a
plain incremental recollindex does not notice xattr-only changes, so retagging
must go through recollindex -i on the affected file.

uv 0.12.16 was installed on this host as part of the stack decision; AGENTS.md
is corrected in the same commit, since it previously listed uv as absent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 16:57:54 +02:00

167 lines
11 KiB
Markdown

# Decisions
Append-only, newest first. Never edit or delete an entry — supersede it with a new one that links back.
One entry per decision a future maintainer would otherwise have to reverse-engineer.
## 2026-09-18 — Search results carry metadata and snippets, not full text
**Status:** accepted
**Context:** Recoll can return metadata, keyword-in-context snippets, extracted plain text and the
original file. The response shape drives both the web UI and the MCP tool.
**Decision:** a search hit carries title, author, path, MIME type, size, date, score, its collections and
Recoll's snippets. Extracted text and file download are separate endpoints.
**Alternatives:** metadata only (a results list without context is markedly worse, and an MCP client
would fetch every candidate to judge it, costing more overall); metadata plus full extracted text
(responses become enormous for book-length PDFs and blow an agent's context window).
**Consequences:** one search call is enough for an agent to judge relevance. The UI needs a second call
to open a document, which is the right trade.
---
## 2026-09-18 — Authentication in 001, user administration in 004
**Status:** accepted
**Context:** the service needs real auth, but roles, user CRUD and per-collection permissions are a
subsystem in their own right, and `PRODUCT.md` currently describes a single technical operator.
**Decision:** 001 ships genuine authentication — argon2 password hashing, server-side sessions, an admin
account seeded from the environment at first boot, every endpoint behind an auth dependency. User
administration and permissions are deferred to spec 004.
**Alternatives:** no auth in the service, trusting a reverse proxy (a misconfigured proxy or an exposed
Docker port would leave the destructive endpoints naked); the full identity system in 001 (a large
addition to an already-grown spec, building for users who do not exist yet).
**Consequences:** 001 is safe to expose behind a proxy. 004 must extend the auth layer without
reshaping endpoints, so the auth dependency is designed as a seam from the start.
---
## 2026-09-18 — Search scope is an explicit, required parameter
**Status:** accepted
**Context:** `docs/ARCHITECTURE.md` proposed the invariant that a query never silently spans
collections. A Recoll query with no collection clause returns every document, including untagged ones.
**Decision:** every search names one or more collections; omitting the parameter is an error, not an
implicit "all". Multiple collections are OR-ed into one Xapian query so ranking stays coherent and each
hit reports which collections it belongs to.
**Alternatives:** single-collection-only searches (the UI and MCP would be built around a model they
later outgrow); a searchable-by-default flag (reintroduces the implicit scope the invariant forbids);
per-collection fan-out and application-side merging (scores from separate databases are not comparable,
so the merged order would be strictly worse than letting Xapian rank).
**Consequences:** the API layer enforces the invariant, because Recoll will not. Every search path needs
a test proving an unscoped request is rejected.
---
## 2026-09-18 — Indexing runs as an in-process job queue
**Status:** accepted
**Context:** collections are created at runtime, so something must index a collection the moment it is
created, and report progress back to the UI and to MCP clients.
**Decision:** creating a collection, changing membership, or calling the reindex endpoint enqueues a job.
A background worker in the same FastAPI process runs `recollindex` and records status, progress and
errors in SQLite. One container process, no broker.
**Alternatives:** `recollindex -m` real-time monitoring (one supervised process per collection, inotify
watch limits, and a new collection still needs a first full index from somewhere — revisit as an opt-in
mode later); cron-scheduled full runs (new collections sit empty until the next tick, and there is no
way to force a refresh).
**Consequences:** jobs die with the process, so the queue must recover interrupted runs at startup.
Job state is queryable, so "indexing, 40%" is available to every consumer.
---
## 2026-09-18 — Collections are runtime-managed through the API
**Status:** accepted
**Context:** collections could be declared in a mounted config file or managed at runtime.
**Decision:** collections are created, edited and deleted through the HTTP API, with SQLite as the store
of record. The service creates and maintains the index state on demand.
**Alternatives:** a declarative mounted `collections.yaml` (reproducible and git-friendly, but no
management UI without adding a write path later); one collection per subdirectory by convention (forces
the on-disk layout to match the collection layout, which the per-file membership requirement rules out).
**Consequences:** 001 grows a persistence layer, validation and migrations. `AGENTS.md` makes deleting
or rebuilding an index an ask-first operation, so the delete path needs an explicit guard rather than a
bare `DELETE`.
---
## 2026-09-18 — One index; collection membership is an opaque per-file tag
**Status:** accepted
**Context:** a collection cannot be "a directory", because files sharing a directory must be able to
belong to different collections. Verified in a throwaway Recoll 1.43.0 container — details and the
exact config in `docs/ARCHITECTURE.md` under "Verified Recoll mechanics".
**Decision:** index every document once into a single Recoll index. Membership is a `user.collection`
extended attribute mapped to a queryable Recoll field. The tag is an **opaque single-token ID**; the
human-readable name lives only in SQLite.
**Alternatives:** one index per collection with rule-based selection (a file in two collections is
stored and indexed twice and can be returned twice, and hand-picking files in a shared directory is
awkward); an explicit file list driving indexing (does not map onto Recoll's topdirs-walking model).
**Why opaque IDs:** Recoll tokenises the field value and matches case-insensitively, so a tag of
`cook-books` is also matched by `collection:cook` and `collection:books`, and `TechBooks` by
`collection:techbooks`. Names-as-tags would let collections silently bleed into each other — exactly
what the no-silent-spanning invariant forbids. Opaque IDs tested clean, with no partial matches.
**Consequences:** collection names need no character restrictions and renaming never touches the index.
Documents must sit on a volume supporting extended attributes, checked at startup — without it every
scoped search silently returns nothing. Retagging costs `recollindex -i <file>`, not a rebuild, because
a plain incremental run does not notice xattr-only changes.
---
## 2026-09-18 — Stack: Python + FastAPI, managed with uv
**Status:** accepted
**Context:** the stack was unchosen and is an ask-first decision. Recoll ships first-class Python
bindings; every other language must drive `recollq` as a subprocess and parse its output.
**Decision:** Python + FastAPI for the engine and HTTP API, with `uv` as the package manager. `uv`
0.12.16 was installed on this host as part of this decision, and `AGENTS.md` updated in the same commit.
**Alternatives:** TypeScript on Node/Bun or C# on ASP.NET Core (one language across UI, API and MCP, but
both make a fragile output parser load-bearing); a split Python engine behind a TypeScript API (two
runtimes and an extra hop inside one container).
**Consequences:** structured results with no parsing layer, and OpenAPI for free, which specs 002 and 003
can generate clients from. The web UI will likely be a second language; that boundary is the HTTP API.
---
## 2026-09-18 — The product is four specs, built in order
**Status:** accepted
**Context:** "a web UI and an MCP server over Recoll" is four subsystems. One spec covering all of them
would be too large to plan or review, and `PRODUCT.md` is still an uninterviewed stub, so any UI spec
written now would design against fiction.
**Decision:** 001 search engine + API, 002 web UI, 003 MCP server, 004 multi-user management. Each gets
its own spec, plan and review cycle. The table lives in `docs/ARCHITECTURE.md`.
**Alternatives:** one thin vertical slice through all four (would lock in a stack and a UI before
`PRODUCT.md` is real); engine plus MCP together (defensible, but it widens 001, which has already grown).
**Consequences:** 002 is blocked on `/impeccable init`. 001's HTTP API is the contract 002 and 003 are
both built against, so it is the piece that must be got right.
---
## 2026-09-18 — Override the superpowers artefact paths
**Status:** accepted
**Context:** superpowers 6.3.0 writes specs to `docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md`
and plans to `docs/superpowers/plans/YYYY-MM-DD-<feature>.md` (verified in the installed skill files).
That nests a second docs tree and dates artefacts that we want numbered and paired.
**Decision:** specs -> `docs/specs/NNN-slug.md`, plans -> `docs/plans/NNN-slug.md`, plan number matching
its spec number. Recorded in `AGENTS.md` so every session picks it up.
**Alternatives:** accept the defaults (two docs trees, no spec/plan pairing); symlink (fragile on Windows
and in containers).
**Consequences:** the override must be restated if `AGENTS.md` is ever trimmed; a superpowers upgrade
that changes its defaults needs this entry re-checked.
---
## 2026-09-18 — Adopt AGENTS.md as single source of truth
**Status:** accepted
**Context:** The repository was empty — no commits, no stack, no agent context. Agents had nothing
durable to read and would re-explore from scratch every session.
**Decision:** `AGENTS.md` is canonical; `CLAUDE.md` and `GEMINI.md` are thin pointers with no rules of
their own. Enforceable things live in `.claude/settings.json` and `.gitignore` rather than in prose.
**Alternatives:** per-tool rule files (drift and synchronisation debt); no rules file (measurably more
steps and tokens per task).
**Consequences:** rule changes happen in one file; the pointer files must stay thin; `AGENTS.md` is an
executable-context and prompt-injection surface, so changes to it get reviewed like CI config.
---
## 2026-09-18 — No DESIGN.md until the first UI exists
**Status:** accepted
**Context:** The `impeccable` skill reads `PRODUCT.md` and `DESIGN.md`. `PRODUCT.md` could be written
from the stated product intent; `DESIGN.md` could not — there is no UI, no tokens, no visual world.
**Decision:** write `PRODUCT.md` now; leave `DESIGN.md` absent. The first UI build produces it
(`/impeccable document` derives it from shipped code).
**Alternatives:** a placeholder `DESIGN.md` — rejected, it would assert a visual system that does not
exist and the skill would design against fiction.
**Consequences:** the first UI task must end by writing `DESIGN.md`, not skip it.