uncommit .agent

This commit is contained in:
2026-08-24 10:59:45 -04:00
parent bc0158f858
commit 076358db94
10 changed files with 0 additions and 874 deletions
-61
View File
@@ -1,61 +0,0 @@
# Story: Chat RAG Answer (happy path)
**Phase:** `03_story_chat_rag.md` · **E2E:** `tests/e2e/test_chat_rag.py`
## Narrative
As **a user** (friend, colleague, future me), I want to ask Brain a question
about Reese's setup and get a grounded, chippy answer that points me at the
exact documentation — so I can actually *do* the thing.
- **Given** the knowledge base is imported and I type "How is my Kubernetes
cluster set up?"
- **When** Brain embeds the question, retrieves the top chunks by cosine
similarity, maps them to their parent documents, and feeds the **full
document text** to `turbo`
- **Then** I see a streamed, upbeat answer that cites the source
(`Homelab/kubernetes.md`), grounded in the doc's specifics (Talos,
Cilium, the node list) — and never in anything the docs don't say.
## Acceptance criteria
1. `POST /api/chat` streams SSE: `delta` events then a final `done` event
carrying `{deflected, sources[], suggestions[]}` (PLAN §4).
2. Retrieval: top-4 chunks (`BOR_TOP_K_CHUNKS`), cosine via pgvector
`<=>`, score = 1 − distance.
3. Context assembly: top-2 **distinct documents** by best-chunk score, full
content, capped at `BOR_MAX_CONTEXT_CHARS` with truncation marker.
4. System prompt = locked persona + HONESTY GATE rules (PLAN §6), with
`<relevance>HIGH</relevance>` and `<documents>…</documents>`.
5. The answer arrives **streamed** (multiple deltas), rendered live.
6. Source chips (mono, `source/path`) render under the answer bubble.
7. Per-turn log line emitted (PLAN §9) and a `query_log` row inserted
(`deflected=false`, top_score, sources, latency).
8. LLM/embedding failure → JSON/SSE error the UI turns into the error banner
(no hang, no stale button).
## UI Visualization & Structure
- Chat column centered at 46rem (PLAN §7.1); user bubble right (brand
indigo, white text ≥4.5:1), Brain bubble left (white, ink text, avatar 🧠).
- While generating: typing indicator → live-appended text (see
loading-feedback story for the full state machine — this story only needs
"deltas render as they arrive and the button is busy throughout").
- **Source chips:** pill, `font-family: mono`, `bg --brand-soft`,
`color --brand-ink` (6.3:1), `max-width` + ellipsis; each shows
`Homelab/kubernetes.md`. `aria-label` when truncated.
- Bubble content is safe-rendered markdown (escape-first local renderer —
`<script>` in an LLM answer must NOT execute).
- On mobile the bubbles expand to ~92% width; chips wrap.
## Playwright Mapping Rule
**Test Scenario → `tests/e2e/test_chat_rag.py`** (mock LLM, seeded KB):
1. `test_on_topic_question_streams_grounded_answer` — type "How is my
Kubernetes cluster set up?", submit; assert: answer bubble appears with
streamed content (mock's answer references the question), a `.source-chip`
containing `kubernetes.md` is present, send button returns to enabled
"Send".
2. `test_chat_logs_query` — after the turn, `GET /api/health` is still ok AND
(via a test-only detail: query the DB directly) a `query_log` row exists
with `deflected=false` and sources including `kubernetes.md`.
3. `test_sse_stream_shape` — raw `httpx` streaming request to `/api/chat`:
assert multiple `data:` delta events precede a `done` event with
`deflected: false` and a non-empty `sources` list.
-57
View File
@@ -1,57 +0,0 @@
# Story: Honest Deflection
**Phase:** `04_story_honest_deflection.md` · **E2E:** `tests/e2e/test_honest_deflection.py`
## Narrative
As **a user**, when I ask something Brain genuinely has no notes about, I
want it to **admit it plainly** and still be helpful — so I never walk away
with a confident-sounding hallucination.
- **Given** the knowledge base is about homelab/infra topics
- **When** I ask "How do I bake sourdough bread?"
- **Then** retrieval's best similarity is below `BOR_RELEVANCE_THRESHOLD`,
Brain switches to deflection mode, opens with a variant of
**"I haven't done anything like that"**, stays chippy, and offers 2–3
alternative questions about things it *does* know (from the weak hits).
## Acceptance criteria
1. Gate: `max(1 − cosine_distance) < BOR_RELEVANCE_THRESHOLD` ⇒
`<relevance>LOW</relevance>` + `DEFLECT_MODE` system prompt (weak-hit
**titles only**, no full docs).
2. The LLM is still called (voice stays chippy); the prompt forces the
honesty phrasing + alternative suggestions (PLAN §6).
3. `done` event carries `deflected: true` and `suggestions[]` (2–3 strings).
4. `query_log` row has `deflected=true` + the weak `top_score`.
5. UI: the deflected bubble is visually distinct (amber border/background),
and "Maybe try:" chips render below it; clicking a chip asks that
question (delegated to the suggestion-chips story for chip behavior;
here only rendering).
6. Threshold is env-tunable; lowering it to ~0 makes every question an
"answer" (documented in README troubleshooting).
7. Unit tests cover the gate boundary (score == threshold → answer mode;
just below → deflect) using a fake retriever — no LLM needed.
## UI Visualization & Structure
- Deflected brain bubble: `background: var(--accent-bg) #fff7e8`,
`border: 1px solid var(--accent-line) #f59e0b`, text stays `var(--ink)`
(or accent-ink for emphasis ≥4.5:1) — clearly "different" from a normal
answer without being alarm-red (it's honesty, not an error).
- Below the bubble: `Maybe try:` label (visually hidden for SR, `aria-label`
on the chip group) + 2–3 `.suggestion-chip` pills (same chip component as
onboarding: ≥44px height, brand-soft bg, brand-ink text).
- Bubble may include the model's alternative list in text too; chips are the
one-click affordance.
- Contrast audit: `#92400e` on `#fff7e8` ≈ 8.7:1 ✓; chip text on chip bg ≥6:1 ✓.
## Playwright Mapping Rule
**Test Scenario → `tests/e2e/test_honest_deflection.py`** (mock LLM, seeded KB):
1. `test_off_topic_question_deflects_honestly` — ask "How do I bake
sourdough bread?"; assert the answer bubble is `.is-deflected`, its text
matches /haven't done anything like that/i, and ≥2 "Maybe try:" chips
render below it.
2. `test_deflection_suggestions_are_clickable` — click the first deflection
chip; assert the input is populated/focus behavior per chip contract and a
new user bubble is created.
3. `test_threshold_gate_unit_boundary` is a **unit** test (not Playwright):
retriever returns score 0.30 → HIGH; 0.2999 → LOW (mocked components).
-62
View File
@@ -1,62 +0,0 @@
# Story: Import Documents
**Phase:** `02_story_import_documents.md` · **E2E:** `tests/e2e/test_import_documents.py`
## Narrative
As **Reese** (the owner), I want to point the importer at one or more
directories of markdown files and have them chunked, embedded, and stored in
Postgres — so that Brain's answers always reflect my *current* documentation.
- **Given** the `~/Homelab` and `~/Deployments` trees (or any `--source` dirs)
- **When** I run `uv run python -m scripts.import_docs`
- **Then** every `*.md` file (after the exclusion list) is present in the
`documents` table with its full content, a sha256 hash, and chunk rows with
768-dim embeddings; unchanged files are skipped on re-runs; and the
Sources page in the browser shows the indexed documents.
## Acceptance criteria
1. `scripts/import_docs.py` accepts repeatable `--source PATH` (default
`~/Homelab` `~/Deployments`), `--prune`, and `--limit N` (debug).
2. Only `*.md` files are imported; excluded dirs: `.venv`, `node_modules`,
`.git`, `__pycache__`, `.pytest_cache`, `dist`, `build` (PLAN A9).
3. Delta detection by sha256 on `(source, path)`: unchanged → skipped
(no re-embedding); changed → re-chunked + re-embedded, old chunks
replaced atomically.
4. Embeddings are batched (`BOR_EMBED_BATCH_SIZE`) against `aipi /v1/embeddings`
(`embed`); a dimension mismatch fails loudly with an actionable message.
5. Rich per-file logging (`added|updated|unchanged|pruned`) + summary.
6. `GET /api/docs` returns the document list; the Sources page renders it
(stat cards + table) or the designed empty state when none exist.
7. The whole flow works against the **mock LLM** in E2E (deterministic),
and against real aipi for manual runs.
## UI Visualization & Structure
- **Sources page (`/sources.html`), desktop:** header row (h1 + sub), then
stat cards in `repeat(auto-fit, minmax(170px,1fr))` (documents / chunks /
last indexed), then a **full-width table** inside a scroll wrapper
(min-width 640px → horizontal scroll, never a squeezed hairline list).
Columns: Source · Path (mono, ellipsized w/ `title`) · Title · Chunks ·
Indexed. Uses ≥85% of the 72rem container width.
- **Empty state (no docs):** centered card with 📂, "Nothing indexed yet",
and the exact import command in a `<code>` pill. No dead links, no
placeholder tables.
- **Accessibility:** `<caption class="visually-hidden">` on the table,
`scope="col"` on headers, `role="region"` + `tabindex="0"` on the scroll
wrapper (keyboard scrollable), stat values have visible labels.
- **Mobile:** stat cards stack (auto-fit), table scrolls horizontally,
no content below the fold is unreachable.
## Playwright Mapping Rule
**Test Scenario → `tests/e2e/test_import_documents.py`** (one isolated
Playwright suite for this story):
1. *Seeding:* run the import function in-process against
`tests/fixtures/docs/` (mock embeddings, temp DB state) — a fixture, not
the test's subject.
2. `test_sources_page_lists_indexed_docs` — goto `/sources.html`, assert stat
cards show the fixture counts and the table rows include
`homelab/kubernetes.md`, `homelab/backups.md`, `deployments/new-service.md`.
3. `test_sources_table_layout` — table wrapper width ≥80% of container;
`caption` present; on a 375px viewport the wrapper scrolls horizontally.
4. `test_empty_state_when_no_docs` (fresh/truncated DB) — empty state visible
with the import command; table hidden.
-61
View File
@@ -1,61 +0,0 @@
# Story: Loading Feedback & Progress
**Phase:** `06_story_loading_feedback.md` · **E2E:** `tests/e2e/test_loading_feedback.py`
## Narrative
As **a user**, local LLM answers can take 10–30+ seconds. I want to *always*
know Brain is working — a clear "thinking" state, live progress as tokens
arrive, and a definitive end — so I never stare at a stale Send button
wondering if it's stuck.
- **Given** I submit a question
- **When** the answer is in flight (pre-token, streaming, or erroring)
- **Then** the UI shows an unambiguous in-progress state, transitions
cleanly to done/error, and the send button is never left in a zombie state.
## Acceptance criteria
1. **Pre-token:** typing-indicator bubble (3 animated dots, `role="status"`,
`aria-label="Brain of Reese is thinking"`) + send button disabled with
spinner and label "Thinking…".
2. **Streaming:** first delta replaces the typing indicator; text appends
live; button stays busy until `done`.
3. **Done:** button re-enabled, label "Send", input focused back.
4. **Error paths:** (a) LLM/DB error → red banner `role="alert"` with retry
hint, button re-enabled; (b) **120s client timeout** → same error state
(guard against a hung stream); (c) page reload mid-stream loses the
stream but the composer is usable again (state is turn-local).
5. **Slow-model E2E:** the mock LLM's 3s warm-up (message containing
"pretend to think slowly") must show the typing indicator for ≥2s before
any text appears.
6. Server side: per-turn log includes `embed_ms` / total `total_ms` (PLAN
§9) so "slow" is diagnosable.
7. `prefers-reduced-motion`: dots/spinner still visible (slower/static) —
feedback is never removed, only calmed.
## UI Visualization & Structure
- State machine (single source of truth in `app.js`):
`idle → thinking → streaming → done | error → idle`.
- Typing indicator: 8px dots, `--ink-soft`, staggered 1.2s bounce; inside a
normal brain bubble (same geometry as answers) so the layout doesn't jump.
- Send button busy style: `background: #a5b4fc` (disabled contrast still
fine — it's a disabled state), 16px spinner (2.5px ring, white top
arc), label swap "Send" ↔ "Thinking…".
- Error banner: `--err-bg/--err-ink/--err-line`, top of chat shell,
`role="alert"`, includes the actionable hint ("Try again — if this
persists, check the LLM is reachable").
- Elapsed-time hint: after 10s still pre-token, the typing bubble's aria
label becomes "…still thinking (12s)" — SR users are never left guessing.
## Playwright Mapping Rule
**Test Scenario → `tests/e2e/test_loading_feedback.py`** (mock LLM):
1. `test_typing_indicator_during_slow_think` — ask "pretend to think slowly
then tell me about kubernetes"; assert `#typing-indicator` visible within
500ms of submit, still visible at ~2s, gone by the time the answer text
is present.
2. `test_button_state_machine` — during the in-flight turn: `#send-btn`
disabled + label "Thinking…"; after done: enabled + "Send".
3. `test_streaming_appends_live` — capture bubble text at two timestamps
during the stream; second length > first (progress is visible).
4. `test_error_banner_on_llm_down` (fixture stops the mock) — submit;
assert `role=alert` banner visible and button re-enabled within timeout.
-34
View File
@@ -1,34 +0,0 @@
# Story: Long Answers (No Truncation)
**Phase:** `11_long_answers.md` · **E2E:** `tests/e2e/test_long_answers.py`
## Narrative
As **a user**, I want Brain to be able to answer at full length (up to
32 768 output tokens) so complex questions ("walk me through the whole
setup", "list every service and its config") get a **complete** answer
instead of one that stops mid-sentence.
- **Given** any question that deserves a long answer
- **When** Brain streams its reply
- **Then** the reply runs to its natural end — the model is allowed up to
32 768 output tokens, not a hard 700-token cap.
## Acceptance criteria
1. `LLMClient.chat_stream` sends `max_tokens` from settings
(`BOR_MAX_OUTPUT_TOKENS`, default **32 768**) — the hard-coded 700 is
gone.
2. A genuinely long streamed answer (several thousand words) arrives
**complete** in the browser — final line intact (E2E).
3. Setting is overridable via env; unit-tested.
4. Unit + integration green, `app/` coverage >90%, story E2E green in
isolation, one `--no-gpg-sign` commit.
## Playwright Mapping Rule
**Test Scenario → `tests/e2e/test_long_answers.py`** (mock LLM, seeded KB):
1. `test_long_answer_streams_to_completion` — question with the
"write a long answer" trigger → mock emits a ~4 000-word deterministic
answer and **honors `max_tokens`** (word-based) → the browser shows the
final line of the answer; under the old 700 cap the tail is missing.
2. `test_normal_answer_unaffected` — a normal question still streams a
complete, short answer.
-65
View File
@@ -1,65 +0,0 @@
# Story: Responsive, Polished, Accessible UI
**Phase:** `07_story_responsive_polish.md` · **E2E:** `tests/e2e/test_responsive_polish.py`
## Narrative
As **a user on any device** — phone at the coffee shop, laptop at the
desk — I want the chat to be comfortable to read and drive: no pinched
layout, no tiny tap targets, no contrast failures, no wasted whitespace —
so asking Brain feels effortless everywhere.
- **Given** any viewport from 360px to 1600px+
- **When** I use the chat and the Sources page
- **Then** the layout follows the PLAN §7 standards (containers, chat
column, full-width table), all interactive elements are reachable by
keyboard, and every color pair meets WCAG 2.1 AA.
## Acceptance criteria
1. **Layout:** container 72rem centered with side padding; chat column
capped at 46rem centered; Sources table uses full container width with
horizontal scroll below 640px (never a squeezed single hairline column).
2. **Mobile (375px):** header condenses, composer reachable above the home
indicator (`safe-area-inset-bottom`), chips scroll horizontally, bubbles
≤92% width, no horizontal page overflow (document `scrollWidth ==
clientWidth`).
3. **A11y sweep:** landmarks present on both pages (`header/nav/main/
footer`); skip link works (focus `#main`); all inputs have labels
(visible or programmatically associated); all icon-only buttons have
`aria-label`; `:focus-visible` outline on every control (Tab through).
4. **Contrast:** automated check of the key pairs (ink/surface,
ink-soft/surface, white/brand, chip-ink/chip-bg, deflection pairs) ≥4.5:1
(test computes from computed styles; PLAN §7.2 table is the baseline).
5. **No-CDN re-verification** on both pages (no `http(s)://` src/href
except same-origin `/…`).
6. **Reduced motion:** with `prefers-reduced-motion`, typing dots and
spinner do not animate (computed `animation: none` or duration ≥2s).
7. Long words/paths (e.g. a 60-char file path) wrap or ellipsize without
breaking the bubble (overflow-wrap anywhere).
## UI Visualization & Structure
- This phase is the **visual audit + fix pass**: it does not add features,
it enforces PLAN §7 end-to-end on chat + sources.
- Desktop 1440px screenshot pass: header 64px, chat centered with balanced
margins, sources table edge-to-edge within the container.
- Tablet 768px: chat column uses most of the width (≤46rem cap), no
mid-column dead zones; stat cards 3-across.
- Phone 375px: one-column flow, 44px+ targets, thumb-zone composer.
- Any deviation found → fix in `frontend/assets/styles.css` (tokens first),
re-verify with the E2E below.
## Playwright Mapping Rule
**Test Scenario → `tests/e2e/test_responsive_polish.py`**:
1. `test_no_horizontal_overflow_at_viewports` — for 360/375/768/1280/1600:
`document.documentElement.scrollWidth <= clientWidth` on both pages.
2. `test_chat_column_capped_and_centered` — at 1600px, `.chat-shell`
width ≤ 46rem (736px) + 2% and horizontally centered (±2%).
3. `test_sources_table_full_width` — at 1280px, `.table-wrap` width ≥ 80%
of `.container` width.
4. `test_a11y_landmarks_and_labels` — both pages: landmarks present,
skip link target `#main` focusable, `#message-input` has an associated
label, no `<img>`/icon buttons without accessible name.
5. `test_contrast_pairs_pass_aa` — computed-color contrast assertions for
the PLAN §7.2 pairs (helper computes WCAG relative luminance).
6. `test_reduced_motion_respected` — emulate `reducedMotion: 'reduce'`;
typing dots have no running animation (or ≥2s duration).
-58
View File
@@ -1,58 +0,0 @@
# Story: Suggestion Chips
**Phase:** `05_story_suggestion_chips.md` · **E2E:** `tests/e2e/test_suggestion_chips.py`
## Narrative
As **a user who opens the chat for the first time** (or after a deflection),
I want a few **concrete example questions** right in front of me — so I
immediately understand what Brain is good at and can start with zero
friction.
- **Given** I land on the chat page
- **When** the app is healthy
- **Then** I see 3–4 suggestion chips drawn from `GET /api/suggestions`
(defaults in settings, tuned to the real Homelab topics), and clicking one
fills the composer and submits it.
## Acceptance criteria
1. `GET /api/suggestions` returns the configured list (settings-driven,
overridable via `BOR_SUGGESTIONS` JSON env).
2. Chips render in the empty state as `<button class="suggestion-chip">`
(real buttons, not links/divs) with `role="list"` container +
`role="listitem"` items; `aria-label="Suggested questions"` on the group.
3. Click behavior: fills `#message-input`, focuses it, **and submits**
(one tap → answer). Keyboard: Tab to chip, Enter activates.
4. After the first user message the empty state (and its chips) is replaced
by the conversation; chips re-appear only on deflection (see
honest-deflection story).
5. If `/api/suggestions` fails, the chat still works (progressive
enhancement — no chips, no error spam).
6. Mobile: chips become a horizontally scrollable single row
(no wrapping into the composer's territory).
## UI Visualization & Structure
- Chips: pill (`border-radius: 999px`), `bg --brand-soft`, `text --brand-ink`
(≥6:1), 1px `--line` border, **min-height 44px**, comfortable
`padding 0.55rem 1rem`; hover deepens bg; `:active` scales 0.98.
- Desktop: `flex-wrap: wrap`, centered under the empty-state subcopy, gap 0.5rem.
- Mobile (≤640px): `flex-wrap: nowrap; overflow-x: auto` single row,
`scrollbar-width: thin`, chips `flex: 0 0 auto` (thumb-friendly, no
accidental double-tap on wrapped lines).
- Default suggestion copy (tune to real docs in this phase):
1. "How is my Kubernetes cluster set up?"
2. "What's my backup strategy?"
3. "How do I deploy a new service?"
4. "What's currently running in the homelab?"
## Playwright Mapping Rule
**Test Scenario → `tests/e2e/test_suggestion_chips.py`**:
1. `test_onboarding_chips_render` — goto `/`, assert ≥3 `.suggestion-chip`
visible inside `#suggestions` (role=list) with non-empty text.
2. `test_chip_click_submits` — click the first chip; assert a user bubble
with the chip's exact text appears and the brain reply (mock) follows.
3. `test_chips_keyboard_accessible` — Tab from the page start reaches the
first chip; Enter submits it.
4. `test_chips_mobile_row` — at 375px viewport, the chip row is
horizontally scrollable (`scrollWidth > clientWidth` or single-line
height check) and no chip is cut vertically.