Archived
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>
167 lines
11 KiB
Markdown
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.
|