Pablan, as it stands
Self-hosted knowledge management for SMEs: a split-screen Markdown editor whose sections an LLM refines while you write, and RAG question answering over the documents that result. FastAPI + Postgres/pgvector on the back, SvelteKit on the front, everything OpenAI-compatible and self-hostable. Squashed into a single commit; the development history stays local. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CA43ZJda8Rbp2hKXNy8f6b
This commit is contained in:
+508
@@ -0,0 +1,508 @@
|
||||
# Roadmap
|
||||
|
||||
The feature backlog for Pablan, organized by feature area rather than by
|
||||
delivery date. It doubles as a **rebuild manual**: follow the done epics top to
|
||||
bottom and you arrive at the system as it stands today; the open epics are the
|
||||
road ahead.
|
||||
|
||||
**How to read it.** Each epic is a feature area with a one-line intent and a
|
||||
checklist of stories — `[x]` shipped, `[ ]` not yet. Epics are ordered as a
|
||||
build sequence (foundation upward). File pointers name where a capability lives,
|
||||
so a rebuilder can find the pattern to follow. Implementation learnings that
|
||||
survive across features live in [notes.md](notes.md); the as-is contract lives
|
||||
in [architecture.md](architecture.md), [data-model.md](data-model.md) and
|
||||
[api-protocol.md](api-protocol.md).
|
||||
|
||||
---
|
||||
|
||||
## 1. Platform foundation *(done)*
|
||||
|
||||
The backend skeleton every other feature stands on.
|
||||
|
||||
- [x] Config via `pydantic-settings`, all `PABLAN_*` env vars; per-role LLM
|
||||
config (base_url / api_key / model for chat, utility, embedding).
|
||||
- [x] Async SQLAlchemy 2.0 (typed) + Alembic migrations on PostgreSQL + pgvector;
|
||||
UUID primary keys and a shared timestamp mixin (`models/base.py`).
|
||||
- [x] The table set — see [data-model.md](data-model.md) and
|
||||
[diagrams/data-model.svg](diagrams/data-model.svg).
|
||||
- [x] pytest harness against a real Postgres (docker); retrieval logic gets
|
||||
SQL-level tests.
|
||||
- [x] Seed script with a production guard; a shared German fixture corpus
|
||||
(`tests/fixtures/corpus/` + `golden_queries.yaml`) that seeds and tests draw
|
||||
from.
|
||||
|
||||
## 2. Authentication & sessions *(done)*
|
||||
|
||||
Server-side sessions, no JWT, and an auth boundary that cannot leak one user's
|
||||
state to the next.
|
||||
|
||||
- [x] argon2 password hashing; `auth_sessions` table; httpOnly `pablan_session`
|
||||
cookie (14-day TTL); `{detail, code}` error shape.
|
||||
- [x] login / logout / me; enumeration-resistant login.
|
||||
- [x] Session revocation (`auth/sessions.py::revoke_user_sessions`) — a password
|
||||
change logs out every other session.
|
||||
- [x] **Full-page auth boundary**: login and logout are document navigations,
|
||||
never client `goto`, so module-singleton client state (conversation list,
|
||||
chat state, resolved locale) is guaranteed dead at the boundary and cannot
|
||||
leak the previous user's data.
|
||||
|
||||
## 3. Frontend foundation *(done)*
|
||||
|
||||
The SvelteKit shell, the design system, the typed API contract, and i18n.
|
||||
|
||||
- [x] Semantic design tokens as CSS variables mapped into Tailwind; the palette
|
||||
is swappable by editing tokens only; a WCAG-AA contrast gate
|
||||
(`frontend/scripts/contrast-check.py`) runs in `make lint`.
|
||||
- [x] A small styled component set on Bits UI (Button, Input, Select, Dialog,
|
||||
Card, Badge, Tabs, Tooltip, Popover, FormField) — no component-library deps.
|
||||
- [x] Typed API client: FastAPI OpenAPI → openapi-typescript → openapi-fetch;
|
||||
`make types` regenerates it after any backend API change.
|
||||
- [x] `(app)` route group + `hooks.server.ts` session validation (absolute
|
||||
backend URL, since a relative `event.fetch` never hits the vite proxy).
|
||||
- [x] The sanitizing Markdown renderer (`components/Markdown.svelte`, DOMPurify) —
|
||||
all model and document output renders only through it.
|
||||
- [x] Paraglide i18n: **de** source, **en** written in the same change; two lint
|
||||
rules (missing `en`, em/en dashes) in `check-messages.py`; the backend never
|
||||
renders UI strings — it returns `{detail, code}` and the frontend translates.
|
||||
- [x] Theme per device (localStorage, applied pre-paint by a boot script) and
|
||||
language per account (`users.locale`).
|
||||
|
||||
## 4. LLM gateway & background jobs *(done)*
|
||||
|
||||
One place that talks to models, and a Postgres-only work queue.
|
||||
|
||||
- [x] `llm/client.py` is the ONLY code that calls LLM endpoints: `chat_stream`,
|
||||
`chat_json` (structured output via a Pydantic `response_format`), `embed`;
|
||||
roles chat / utility / embedding, each independently configured; `extra_body`
|
||||
passthrough (e.g. `enable_thinking:false`).
|
||||
- [x] In-app LLM config: bootstrap each field from env, then the DB, with
|
||||
per-field provenance ("from .env" / "changed here"); `llm/test` pings the
|
||||
three roles; model discovery is server-side.
|
||||
- [x] Postgres job queue (`jobs` table, `FOR UPDATE SKIP LOCKED`, backoff
|
||||
30s·2^n over 5 attempts); the handler runs inside the open claim transaction;
|
||||
exactly one uvicorn worker (queue and metrics are per-process).
|
||||
- [x] Scheduled retention cleanup (reschedules itself); an in-process metrics
|
||||
registry.
|
||||
- [x] Content-safe logging: metadata only, never prompts / responses / user text;
|
||||
`LLMError` raised `from None`; an openai-SDK DEBUG-log guard
|
||||
(`PABLAN_DEBUG_LOG_PROMPTS`, never in production).
|
||||
- [x] **A failing endpoint says why.** `LLMError.code` classifies every failure
|
||||
once (`llm_unreachable` / `llm_busy` / `llm_misconfigured` / `llm_failed`);
|
||||
every surface reports it unchanged and the frontend phrases it
|
||||
(`lib/api/errors.ts`). The conversations router catches `LLMError` centrally,
|
||||
so a failure during retrieval (which embeds before the model is called) ends
|
||||
the turn with the reason instead of a truncated stream. A busy endpoint
|
||||
queues rather than refuses, so the chat also says so after 12s of silence.
|
||||
|
||||
## 5. Documents & the knowledge model *(done)*
|
||||
|
||||
Markdown documents with a permissioned lifecycle; the single source of truth.
|
||||
|
||||
- [x] Documents stored as Markdown (`content_md`); chunks and embeddings are
|
||||
disposable derivatives, always re-indexable from the document.
|
||||
- [x] Lifecycle draft → published → archived; `visibility` (public /
|
||||
department / restricted).
|
||||
- [x] Publishing is the author's own action (`POST /documents/{id}/publish`),
|
||||
plus archive/republish. Whether the CONTENT is trusted is a review
|
||||
request instead, never a status (epic 16).
|
||||
- [x] The **single permission filter** (`rag/permissions.py`) shared by the API
|
||||
and retrieval, evaluated live against the table (never on stale chunk meta):
|
||||
`readable` adds a user's own drafts and the documents someone asked this
|
||||
user to check; `searchable` is published-only, so a draft never reaches
|
||||
another user or an LLM prompt.
|
||||
- [x] ZIP export of the readable knowledge base (Markdown + YAML frontmatter),
|
||||
permission-filtered, help pages excluded, stdlib `zipfile`/`io` only.
|
||||
- [x] Built-in help pages (`help/*.md`) imported into `documents` on start
|
||||
(`is_builtin`, read-only, not deletable).
|
||||
- [x] Admin CRUD guardrails (no self-delete / self-demote, 409 `self_modification`);
|
||||
GDPR surfaces (delete-own-conversation, the retention job).
|
||||
|
||||
## 6. RAG retrieval *(done)*
|
||||
|
||||
Permission-filtered hybrid search, German-first.
|
||||
|
||||
- [x] Heading-aware chunking (the active-section boundary is shared with the
|
||||
editor); index / reindex jobs; bge-m3 multilingual embeddings
|
||||
(`EMBEDDING_DIM=1024`).
|
||||
- [x] Hybrid search: a permission CTE → pgvector top-20 + Postgres `german` FTS
|
||||
top-20 → Reciprocal Rank Fusion (k=60), all in one SQL statement.
|
||||
- [x] **Permission filter before the LLM**: there is no search without a user;
|
||||
an unauthorized chunk is structurally impossible to retrieve.
|
||||
- [x] Similarity path (`similar_chunks` / `similar_documents`): pure cosine,
|
||||
permission-filtered, threshold applied in Python after `ORDER BY … LIMIT`
|
||||
(a WHERE predicate fights the HNSW index); calibrated constants
|
||||
(no-answer ≥ 0.45, capture-context 0.45).
|
||||
- [x] Eval suite (`make eval`): recall@5 baseline against the fixture corpus and
|
||||
the no-answer threshold.
|
||||
- [x] **The knowledge base stays searchable without any model.** `text_search()`
|
||||
is the full-text half alone (the `german` tsvector GIN index, no embedding
|
||||
call), same permission CTE; query mode uses it when the embedding endpoint
|
||||
is gone, and when no model answers at all the turn ends as a plain hit list
|
||||
the user opens themselves, labelled as a search without a model.
|
||||
|
||||
## 7. Query chat *(done)*
|
||||
|
||||
Ask the knowledge base a question, get a streamed, cited answer.
|
||||
|
||||
- [x] The `Mode` protocol + registry: a mode yields `ModeEvent`s and the router
|
||||
converts them to SSE; Query is the only core mode (EE registers insight).
|
||||
- [x] Conversations + messages; the SSE streaming turn; a stop button (a client
|
||||
abort persists the partial message via `asyncio.shield`).
|
||||
- [x] Citations snapshotted into `messages.meta` (chunks are disposable) + the
|
||||
source side panel; one badge per document, not per chunk.
|
||||
- [x] No-answer handling lives in retrieval (the ≥0.45 threshold), not the prompt;
|
||||
a no-answer offers a "capture this knowledge" hand-off into the editor.
|
||||
- [x] Topic-summary retrieval fallback: on a low-confidence hit with prior
|
||||
context, re-search on an LLM topic summary of the conversation.
|
||||
- [x] A prompt-cache-friendly shape: a stable system prompt + history, with the
|
||||
volatile excerpts + question last.
|
||||
|
||||
## 8. Writing-first capture *(done)*
|
||||
|
||||
The centerpiece: employees write knowledge; the model matures the section at the
|
||||
cursor. (This replaced an earlier dialogue-driven approach — see
|
||||
[notes.md](notes.md) History.)
|
||||
|
||||
- [x] A CodeMirror 6 split editor at `/documents/{id}/edit` (also the edit surface
|
||||
for any existing document); the caret is visible (`drawSelection` + accent
|
||||
caret color).
|
||||
- [x] Section refinement `POST /documents/{id}/refine` (SSE): the server owns the
|
||||
active-section boundary (shared with the chunker), sends a FIM-style natural-
|
||||
language prompt, and streams a matured version of only that section into the
|
||||
right pane; `enable_thinking:false` keeps it ~1s; only `delta.content` is read.
|
||||
- [x] **Retrieval-aware grounding**: before refining, the server searches the
|
||||
permission-filtered knowledge base for related published material the author
|
||||
may read (`_grounding` via `similar_chunks`, the current doc and help pages
|
||||
excluded) and passes it to the prompt as a reference, not a fact source.
|
||||
- [x] A staleness guard and a post-accept cooldown on "Übernehmen"; a suggestion
|
||||
computed against a since-edited region is discarded rather than applied.
|
||||
- [x] A pre-save diff: `@codemirror/merge` `unifiedMergeView` (VSCode-style).
|
||||
**Save** sits bottom right where the writing ends, shows what changed, and
|
||||
asks again — save, cancel, or (for a draft) save and publish.
|
||||
- [x] Authoring templates (`AuthoringTemplate`): a Markdown skeleton + persona +
|
||||
per-section hints — declarative config, not a Mode. A shipped catalog plus a
|
||||
**form template builder** (`POST /templates/build`; the skeleton is derived
|
||||
from the section headings), with raw YAML behind an advanced toggle.
|
||||
- [x] Capture from a chat: carries the conversation as background context
|
||||
(`meta.context`) and shows a matching-documents picker (topic summary →
|
||||
`similar_documents`) to extend an existing document instead.
|
||||
- [x] A reward flow after publishing (a check animation → View / Have it
|
||||
checked), a title suggestion in the save dialog, and a streaming token
|
||||
fade-in.
|
||||
|
||||
## 9. Insight & growth *(open)*
|
||||
|
||||
Aggregate signals about the knowledge base — never per-user data.
|
||||
|
||||
- [ ] A growth surface, in a form that earns its place. The first version (a
|
||||
`/dashboard` page with bar charts off `GET /documents/dashboard`) was
|
||||
removed again: numbers nobody acts on are noise, and the charts said
|
||||
nothing the document list does not. Rebuild only around a question someone
|
||||
actually asks.
|
||||
- [x] `GET /documents/stats` — company-wide counts, read by the landing page's
|
||||
first-run guide.
|
||||
- [x] The document detail page shows created / last-changed timestamps.
|
||||
|
||||
## 10. Product content & docs *(done)*
|
||||
|
||||
The content that ships with the product, and the docs that describe it.
|
||||
|
||||
- [x] In-product help (`help/*.md`, German) imported as read-only documents,
|
||||
kept current in the same change as the workflow it documents.
|
||||
- [x] The shipped template catalog: blueprints on disk, inert until an admin adds
|
||||
one; an added template becomes the customer's, fully editable.
|
||||
- [x] Claude-maintained developer docs (`docs/`), hand-authored SVG diagrams
|
||||
(`docs/diagrams/`, theme-aware, no toolchain), this roadmap, and
|
||||
[notes.md](notes.md).
|
||||
|
||||
---
|
||||
|
||||
## 11. Change history & review audit *(done)*
|
||||
|
||||
A git-like record of who changed what, when, and who checked it — including
|
||||
**who confirmed the content**, which used to be stored nowhere.
|
||||
|
||||
- [x] A `document_events` table: `document_id`, `actor_id`, `action` (created /
|
||||
edited / published / archived / visibility_changed /
|
||||
review_requested / review_resolved),
|
||||
`created_at`, and — for content-bearing events — a snapshot of `content_md`,
|
||||
title, visibility and meta. Snapshots the Markdown (the source of truth), never
|
||||
chunks; `actor_id` is SET NULL and events CASCADE with the document.
|
||||
- [x] An event is recorded at every transition: the content branch of
|
||||
`update_document`, and create / publish / archive plus asking and
|
||||
answering a review in `api/documents/` (helper
|
||||
`authoring/history.py::record_event`). `review_resolved` captures **who**
|
||||
confirmed the content as the event actor.
|
||||
- [x] `GET /api/documents/{id}/history` → the events newest-first (actor name,
|
||||
action, timestamp, `has_snapshot`), plus
|
||||
`GET /api/documents/{id}/versions/{event_id}` to fetch a past version's content
|
||||
for viewing or diffing. Both behind the document's own read gate.
|
||||
- [x] A history timeline on the document detail page
|
||||
(`lib/documents/DocumentHistory.svelte`) and a version diff reusing the
|
||||
`unifiedMergeView` (shared theme extracted to `lib/documents/editorTheme.ts`),
|
||||
with restore of a previous version.
|
||||
- [x] **An entry shows its OWN change.** A snapshot is written after its event, so
|
||||
diffing it against the live document made every row show the next row's edit
|
||||
(the newest edit showed nothing at all). `GET /versions/{event_id}` now also
|
||||
returns `previous_content_md`, the closest earlier snapshot, and the dialog
|
||||
diffs the pair.
|
||||
- [x] The review queue is discoverable from the landing page, deep-linking to
|
||||
`/documents?review=1` (see epic 16 for the review model itself).
|
||||
|
||||
## 12. People & profiles *(done)*
|
||||
|
||||
A directory colleagues can browse, and ONE way to describe a person: a document
|
||||
like any other.
|
||||
|
||||
- [x] A member-visible directory: `GET /api/people` (+ `/api/people/{id}`) →
|
||||
colleagues' `{id, name, role, department}` for any authenticated user (no
|
||||
password/email leakage; mirrors the permission-scoped shape of
|
||||
`ReviewerCandidate`), with a directory page (`/people`) and a person page
|
||||
(`/people/{id}`) wired into the nav.
|
||||
- [x] **The profile page is the document about you.** `GET /api/account/document`
|
||||
answers whether the caller wrote one (from the person blueprint) and what to
|
||||
start it from otherwise; the page opens it for editing or offers to create
|
||||
it. The blueprint id lives in the backend, so the frontend knows no ids.
|
||||
- [x] **The self-written bio was removed again.** A short profile blurb and a
|
||||
document about yourself are the same thing said twice, and only one of them
|
||||
is findable by the search, versioned and approvable. The `users.bio` column,
|
||||
both endpoints, the editor and its two prompts are gone.
|
||||
|
||||
## 13. Access control & sharing *(done)*
|
||||
|
||||
Let a document reach more than one department, and stop anyone from accidentally
|
||||
locking themselves out of it.
|
||||
|
||||
- [x] **Multi-department documents.** `documents.department_id` stays the owning
|
||||
department; extra departments are shared through the existing `doc_permissions`
|
||||
join (the `searchable_documents_filter` `EXISTS doc_permission` branch already
|
||||
unions them in — this was management, not permission logic).
|
||||
`PUT /api/documents/{id}/departments` replaces the set; a detail-page multi-select
|
||||
(`lib/documents/DepartmentSharing.svelte`) manages it; and several departments are
|
||||
reflected in `shared_departments`, the `?department=` filter and the export
|
||||
frontmatter.
|
||||
- [x] **Self-lockout guard.** Before committing a visibility or grant change, the
|
||||
*proposed* state is evaluated against the read rules (`_guard_self_lockout`, a
|
||||
Python mirror of `readable_documents_filter`). An author keeps access as author,
|
||||
so this only bites an admin editing a document they do not own: 409
|
||||
`self_lockout_warning` unless resent with `confirm_lockout` (surfaced as an
|
||||
inline "save anyway" in the UI).
|
||||
- [x] Close the adjacent admin lockout vectors. `delete_department` now refuses to
|
||||
silently delete a department that still has members, owned documents or grants
|
||||
(409 `department_in_use` unless `?confirm=true`) — its `doc_permissions` grants
|
||||
CASCADE away invisibly otherwise. The `delete_user` / `update_user`
|
||||
department-nulling is deliberate orphaning (documents and users survive
|
||||
authorless/departmentless, `notes.md`), not a silent lockout, and is documented
|
||||
in `data-model.md`.
|
||||
|
||||
## 14. Prompt & context transparency *(done)*
|
||||
|
||||
Give admins control over the model's instructions, and make what the model is
|
||||
working from visible to the user.
|
||||
|
||||
- [x] **Admin-editable system prompts.** Every system prompt (query + no-sources,
|
||||
refinement persona/rules, grounding framing, topic summary, title) is
|
||||
editable on the admin UI, stored in `prompt_settings` and applied without a
|
||||
restart — mirroring the per-role LLM-settings pattern (`app/prompts/overrides.py`
|
||||
+ `api/admin/`, code defaults in `app/prompts/defaults.py`), with a
|
||||
reset-to-default per prompt (`lib/admin/PromptSettings.svelte`).
|
||||
- [x] **Always-inspectable working context.** A "?" inspector in the chat
|
||||
(`lib/chat/ContextInspector.svelte`) shows every retrieved passage, marking
|
||||
which grounded the answer (`used`) versus which were too weak — so a no-answer
|
||||
is explained, not silent. The editor shows the grounding references a refinement
|
||||
drew from, sent as a `grounding` SSE frame.
|
||||
|
||||
## 16. Publish and review *(done)*
|
||||
|
||||
Separating "where does this document stand" from "is what it says right" — see
|
||||
[decisions.md](decisions.md) D24.
|
||||
|
||||
- [x] Statuses are draft / published / archived; `pending_approval` is gone and
|
||||
publishing is the author's own one-click action (from the save dialog, the
|
||||
document page, or the landing page).
|
||||
- [x] A `review_requests` table: one question, addressed to one colleague, open
|
||||
until answered (`resolved_at` + `resolved_by_id`). Orthogonal to the status —
|
||||
it can sit on a draft or on a document published months ago.
|
||||
- [x] Being asked grants read AND edit until the answer
|
||||
(`readable_documents_filter`, `can_edit`), so a reviewer fixes a wrong number
|
||||
instead of filing a second question. Owner-only decisions (delete, sharing,
|
||||
handing out a request) stay with the author or an admin.
|
||||
- [x] An open question marks the document everywhere: list card, detail page, the
|
||||
document panel, and the sources under a chat answer (`review_pending` on
|
||||
every search hit, snapshotted with the citation).
|
||||
- [x] The landing page surfaces open work: your unpublished drafts (with publish)
|
||||
and the documents waiting for your check (`lib/documents/OpenWork.svelte`).
|
||||
- [x] Follow-through from use: `AccessReason.review` names the access a request
|
||||
grants, the document page thanks a reviewer instead of 404ing when their
|
||||
answer ends it, visibility moved out of the editor to the document page
|
||||
next to department sharing (both owner-only, like publishing), and the
|
||||
title is edited in the save dialog with a ✨ suggestion on demand.
|
||||
- [x] Seed data is built through the real process — per-section edit history,
|
||||
publish events, drafts and review requests — so every history and diff
|
||||
surface has something true to show.
|
||||
|
||||
## 17. Interface pass *(done)*
|
||||
|
||||
One question asked of every screen: does the important thing catch the eye, and
|
||||
is everything else still findable? See `architecture.md` "One thing per screen".
|
||||
|
||||
- [x] Ranked actions: one labelled primary button per screen, the rest in a
|
||||
labelled overflow menu (`lib/components/Menu.svelte`) instead of a row of
|
||||
bare icons.
|
||||
- [x] Badges mark exceptions only — an open question, an overdue check, a
|
||||
draft, an archived or built-in document. "Published / public / yours" is
|
||||
the normal case and says nothing on every card.
|
||||
- [x] The document page is the document: one quiet metadata line above the
|
||||
text, visibility and sharing behind the chip that states them
|
||||
(`AccessPopover`), history collapsed to the recent entries, and the
|
||||
leading `# Title` dropped where it repeats the title.
|
||||
- [x] The document list is a search field and results; filters live behind one
|
||||
toggle and stay visible as removable chips while they are on.
|
||||
- [x] `/admin` became four tabs; `/people` groups by department and filters;
|
||||
the profile lists what you wrote; the landing page focuses its input.
|
||||
|
||||
## 18. A template set people actually use *(done)*
|
||||
|
||||
- [x] The catalog is four starter blueprints and three additions, replacing
|
||||
eight that read like a taxonomy: `notiz` (no skeleton at all), `prozess`,
|
||||
`stoerung`, `person`, plus `anlage`, `entscheidung`, `projekt-debrief`.
|
||||
Headings are the questions a colleague asks ("Wann das gilt / Schritt für
|
||||
Schritt / Wenn es klemmt"), not shapes of a document ("Worum es geht /
|
||||
Details").
|
||||
- [x] `onboarding-basis` became `person`, written by the person themselves and
|
||||
public — the profile page starts it. `offboarding`, `kundentermin` and
|
||||
`lieferantenwissen` are gone.
|
||||
- [x] The picker is a list, not a grid: a handful of choices read top to
|
||||
bottom, where the description that decides between them is legible.
|
||||
|
||||
## 19. Decided *(open)*
|
||||
|
||||
Decisions taken on 2026-08-27. The three removals landed on 2026-08-30 (D25);
|
||||
what is left needs either a screen size or an endpoint to work against.
|
||||
|
||||
- [x] **Remove the verification workflow.** `documents.verified_until`, the
|
||||
`document_review_days` setting, `POST /{id}/reverify`, the `reverified`
|
||||
event and every "Verifizierung fällig" surface are gone. It marked
|
||||
documents and reminded nobody; a review request is the mechanism that
|
||||
actually asks a person something.
|
||||
- [x] **Remove tags.** `meta.tags`, the `?tag=` filter, the chunk-meta copy,
|
||||
`metadata.tags` in the template schema and the fixture frontmatter are
|
||||
gone; they had been write-only since M8.
|
||||
- [x] **Remove `source_type` / the upload half.** Nothing can be uploaded, and
|
||||
`upload` was the value nobody wrote. The idea comes back with an import
|
||||
(below), which is when a provenance column earns its place again.
|
||||
- [ ] **Mobile.** Desktop is the main target, but the app has to be usable on a
|
||||
phone: sidebar, the chat split view, the CodeMirror editor and the new
|
||||
overflow menus are unverified below `lg`.
|
||||
- [ ] **Import, as the counterpart to the export.** A self-hosted system has to
|
||||
be able to read its own ZIP back in (Markdown + YAML frontmatter):
|
||||
restore after a move, and the first honest answer to "we already have
|
||||
documentation somewhere else".
|
||||
- [x] **Carry the chat a capture started from.** A draft started out of a chat
|
||||
keeps its subject as `meta.context`; extending an EXISTING document out of
|
||||
the same chat now does too (`PATCH /api/documents/{id} {conversation_id}`),
|
||||
which was the one path that dropped it.
|
||||
- [ ] **Show the search query the model actually wrote** — the topic-summary
|
||||
retry rewrites the question on the low-confidence path, and the context
|
||||
inspector still shows only what came back, not what was asked.
|
||||
|
||||
## Polish & known issues *(open)*
|
||||
|
||||
Smaller fixes and rough edges to pick up opportunistically.
|
||||
|
||||
- [x] **Refinement suggestions appear inline at the section you are editing**, as
|
||||
a CodeMirror block widget right below it, instead of in a disconnected
|
||||
right-hand pane (`WritingEditor.svelte`).
|
||||
- [x] **The editor no longer loses work or leaves clutter.** A `beforeNavigate`
|
||||
guard saves an edited-but-unsaved draft on the way out (no autosave, so the
|
||||
review-before-save diff is preserved) and discards an abandoned, never-filled
|
||||
template draft so empty drafts do not pile up. The document list also has its
|
||||
own "capture" entry, not only the landing page.
|
||||
|
||||
- [x] The chat input **stays focused after the first message is sent**: the
|
||||
`/chat` → `/chat/[id]` navigation swaps the page component, so `ChatView`
|
||||
re-focuses the composer in `afterNavigate`.
|
||||
- [x] **Destructive confirmations use the app's modal**, not the browser's
|
||||
`confirm()`: `lib/components/ConfirmDialog.svelte` backs delete-user,
|
||||
delete-department, delete-template, delete-document and delete-conversation.
|
||||
- [x] **The streaming reply no longer snaps.** The assistant turn renders as
|
||||
sanitized Markdown LIVE as it streams (with a blinking cursor) instead of
|
||||
fading in plain text and re-rendering to Markdown on completion.
|
||||
- [x] **Model LaTeX/math renders as math.** The sanitizing renderer runs
|
||||
`marked-katex-extension` + KaTeX before DOMPurify (`$lib/markdown.ts`), with
|
||||
KaTeX styles/fonts bundled locally (no CDN) — the local model emits real
|
||||
`$...$` / `$$...$$` LaTeX, now typeset.
|
||||
- [x] **The classic-search fallback answers a whole question.** Its terms are
|
||||
ORed rather than ANDed (`_any_term_tsquery`), so a typed-out sentence still
|
||||
finds something when no model is reachable; ranking stays `ts_rank_cd`. The
|
||||
hybrid path keeps the AND, where `fts_match` has to mean "the words are
|
||||
really in there". Open: the fallback still needs chunks, so a document
|
||||
published while the embedding endpoint was down is not findable at all
|
||||
(`notes.md`).
|
||||
- [~] **Retrieval quality on statements.** Investigated: the hybrid path (query +
|
||||
search) retrieves the same top document for a declarative statement as for the
|
||||
question, because the German full-text component catches the keywords
|
||||
regardless of phrasing — verified and now guarded by declarative-statement
|
||||
cases in the golden query eval. The pure-cosine similarity path (capture
|
||||
picker / grounding) is more phrasing-sensitive by design (it needs a
|
||||
comparable distance for its threshold, `notes.md`); switching it to hybrid is
|
||||
left open.
|
||||
|
||||
## 15. Hardening & release readiness *(open)*
|
||||
|
||||
Get to "a stranger can deploy from the README alone", with the EE boundary in
|
||||
place and every quality gate green.
|
||||
|
||||
- [ ] EE boundary: `app/ee_hooks.py` (optional `pablan_ee` import + `register`),
|
||||
`lib/ee/registry.ts` (empty frontend slot registry + Vite alias), and an
|
||||
import-linter contract (core never imports `ee/`) wired into `make lint`.
|
||||
- [ ] Minimal login throttling: in-process, per-user+IP backoff on failed logins
|
||||
(no new dependency, no Redis).
|
||||
- [ ] A Prometheus text-format exporter at `/metrics` for the metrics registry,
|
||||
documented for customer IT.
|
||||
- [x] An endpoint concurrency gate (`llm/gate.py`): one semaphore per base_url in
|
||||
front of `chat_stream` / `chat_json` / `embed`, a bounded wait and a bounded
|
||||
queue, both refusals reported as `llm_busy`, wait time and queue depth
|
||||
metered, and a `queued` chat phase so a waiting turn says so.
|
||||
- [ ] SDK-retry visibility: meter actual HTTP attempts, or document the tradeoff
|
||||
(SDK-internal retries are invisible to our metrics) in the ops docs.
|
||||
- [ ] Customer ops docs: backup/restore (pg_dump, rehearsed once), the upgrade
|
||||
path (`alembic upgrade`), the `/metrics` protection note, and the parked
|
||||
`idle_in_transaction_session_timeout` note for managed Postgres.
|
||||
- [ ] The full e2e suite green: login, chat streaming, the writing editor, document
|
||||
approval, permission boundaries.
|
||||
- [ ] **First cloud-model eval validation of every prompt shipped so far** — the
|
||||
writing-first refinement, grounding, topic-summary and query prompts have
|
||||
only run against a local Gemma-class model. Add a `PABLAN_EVAL_CLOUD_*` role to
|
||||
`.env.example`; the eval picks it up when set and skips it otherwise. Budget
|
||||
time to fix prompt behaviour a 26B model tolerated.
|
||||
- [ ] Two anti-scaffolding assertions in the refinement eval: across the eval
|
||||
set the replies must not share one opening prefix, and at most a third may
|
||||
share their first word. A model that scaffolds every answer the same way
|
||||
reads as a form letter, and nothing catches that today.
|
||||
- [ ] Clean-clone verification: quickstart (dev) and customer deploy (compose +
|
||||
Caddy) from the README on a fresh checkout; a final pass so every `docs/*.md`
|
||||
matches the implementation.
|
||||
|
||||
---
|
||||
|
||||
## Non-goals
|
||||
|
||||
Deliberately out of scope; revisit only with a concrete need.
|
||||
|
||||
- **EE insights implementation** — only extension-point stubs in the core.
|
||||
- **OIDC / Entra ID SSO.**
|
||||
- **Cross-department knowledge discovery** as a distinct feature (the capture-time
|
||||
"matching documents" panel is a deliberate precursor; this is different from epic
|
||||
13, which shares one document across departments).
|
||||
- **CI pipeline setup.**
|
||||
- **File uploads / attachments** — Markdown is the source of truth; binary storage,
|
||||
previews and scanning add complexity without serving the capture → RAG loop.
|
||||
- **Comments / in-tool collaboration** — knowledge flows through conversations with
|
||||
Pablan, not discussion threads inside the tool.
|
||||
- **Real-time / concurrent editing** — no use case; SSE streaming is all the
|
||||
liveness the product needs.
|
||||
- **Email notifications** — needs SMTP and deliverability support on customer
|
||||
infrastructure; a candidate for EE later.
|
||||
|
||||
_(Document version history was previously a non-goal; it is now planned as epic 11.)_
|
||||
Reference in New Issue
Block a user