diff --git a/.agents/phases/todo/118_summary_seed_context/00_phase.md b/.agents/phases/complete/118_summary_seed_context/00_phase.md similarity index 100% rename from .agents/phases/todo/118_summary_seed_context/00_phase.md rename to .agents/phases/complete/118_summary_seed_context/00_phase.md diff --git a/.agents/phases/todo/118_summary_seed_context/01_all_doc_summaries.md b/.agents/phases/complete/118_summary_seed_context/01_all_doc_summaries.md similarity index 100% rename from .agents/phases/todo/118_summary_seed_context/01_all_doc_summaries.md rename to .agents/phases/complete/118_summary_seed_context/01_all_doc_summaries.md diff --git a/.agents/phases/todo/118_summary_seed_context/02_suggested_tier_selection.md b/.agents/phases/complete/118_summary_seed_context/02_suggested_tier_selection.md similarity index 100% rename from .agents/phases/todo/118_summary_seed_context/02_suggested_tier_selection.md rename to .agents/phases/complete/118_summary_seed_context/02_suggested_tier_selection.md diff --git a/.agents/phases/todo/118_summary_seed_context/03_summary_document_blocks.md b/.agents/phases/complete/118_summary_seed_context/03_summary_document_blocks.md similarity index 100% rename from .agents/phases/todo/118_summary_seed_context/03_summary_document_blocks.md rename to .agents/phases/complete/118_summary_seed_context/03_summary_document_blocks.md diff --git a/.agents/phases/todo/118_summary_seed_context/04_read_suggested_docs.md b/.agents/phases/complete/118_summary_seed_context/04_read_suggested_docs.md similarity index 100% rename from .agents/phases/todo/118_summary_seed_context/04_read_suggested_docs.md rename to .agents/phases/complete/118_summary_seed_context/04_read_suggested_docs.md diff --git a/.agents/phases/todo/118_summary_seed_context/05_chat_turn_wiring.md b/.agents/phases/complete/118_summary_seed_context/05_chat_turn_wiring.md similarity index 100% rename from .agents/phases/todo/118_summary_seed_context/05_chat_turn_wiring.md rename to .agents/phases/complete/118_summary_seed_context/05_chat_turn_wiring.md diff --git a/.agents/phases/todo/118_summary_seed_context/06_e2e_summary_context.md b/.agents/phases/complete/118_summary_seed_context/06_e2e_summary_context.md similarity index 100% rename from .agents/phases/todo/118_summary_seed_context/06_e2e_summary_context.md rename to .agents/phases/complete/118_summary_seed_context/06_e2e_summary_context.md diff --git a/.agents/phases/todo/118_summary_seed_context/07_fixture_battery_gate.md b/.agents/phases/complete/118_summary_seed_context/07_fixture_battery_gate.md similarity index 100% rename from .agents/phases/todo/118_summary_seed_context/07_fixture_battery_gate.md rename to .agents/phases/complete/118_summary_seed_context/07_fixture_battery_gate.md diff --git a/.agents/reports/118_summary_seed_context/118_summary_seed_context__00_phase.a1.err b/.agents/reports/118_summary_seed_context/118_summary_seed_context__00_phase.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/118_summary_seed_context/118_summary_seed_context__00_phase.a1.md b/.agents/reports/118_summary_seed_context/118_summary_seed_context__00_phase.a1.md new file mode 100644 index 0000000..8411200 --- /dev/null +++ b/.agents/reports/118_summary_seed_context/118_summary_seed_context__00_phase.a1.md @@ -0,0 +1,9 @@ +**Phase 118 final verification pass — complete.** All criteria verified; 4 pre-existing defects found and fixed. + +- **Verified:** summary-seed wiring (`select_suggested` top-5 no-floor → summary blocks, no full text in HIGH prompt), all-doc markdown summaries + NULL backfill (`summary_backfilled`, no `sources_meta` bump), `read` adds full text with `read_docs`-only dedupe, `done.sources` = suggested+read / durable record = suggested+related+read + `suggested=N` log line (seen live in E2E), byte-locked PERSONA/LOW/TOOLS_SECTION, battery gate PASS recorded in `TOOL_CALLING_TESTING.md` §10 (turbo 2026-09-16: 1/2/4 GREEN, cond-3 reported 9/10 per A7, contract 21/21, caps 0). +- **Defects fixed (all pre-existing, none phase-118):** ① `ChatMessage` schema missing the phase-113 `related` key → `extra="forbid"` 422'd every done-time auto-save of grounded turns with a related tier, leaving `message_count=1` (root cause of `test_share_chat` 3F; browser-level instrumentation proved the PUT 422) — added the field + unit/integration pins; ② `test_theme_semantic_completion` pins stale vs phase-117 debox (border/chip removed) — re-targeted to assert border/chip *absence*; ③ `test_header_consistency` `<26`px pin red on 26.125px native date-input line — bound relaxed to `<34` (wrap-detection intent kept); ④ `test_navbar_refresh` bor.chat.v1 key set updated for `related`. +- **Test/lint/coverage:** `uv run pytest --cov=app --cov-report=term-missing` → **2506 passed, app/ 99%** (>90%); `uv run ruff check . && uv run pyright` → clean, 0 errors. +- **E2E:** new story suite in isolation → **2 passed**; full 103-suite matrix sweep (each isolated) → **all 103 green** after the fixes; `test_share_chat` 4 passed, `test_theme_semantic_completion` 8 passed, `test_header_consistency` 3 passed, `test_navbar_refresh` 7 passed. +- **Deviations:** none from LOCKED decisions. Note: orphaned diagnostic uvicorn processes briefly made E2E sessions exercise stale code — killed and re-verified; a sweep-regenerated tracked screenshot was restored. No commits made (harness commits). +- **Completion criteria:** all 7 ✅ (commit/phase-move is the harness's step). +- **Next pending phase:** none — `todo/` holds only this phase's overview pending the harness move. diff --git a/.agents/reports/118_summary_seed_context/118_summary_seed_context__00_phase.a1.validate b/.agents/reports/118_summary_seed_context/118_summary_seed_context__00_phase.a1.validate new file mode 100644 index 0000000..e200eb4 --- /dev/null +++ b/.agents/reports/118_summary_seed_context/118_summary_seed_context__00_phase.a1.validate @@ -0,0 +1,103 @@ +........................................................................ [ 2%] +........................................................................ [ 5%] +........................................................................ [ 8%] +........................................................................ [ 11%] +........................................................................ [ 14%] +........................................................................ [ 17%] +........................................................................ [ 20%] +........................................................................ [ 22%] +........................................................................ [ 25%] +........................................................................ [ 28%] +........................................................................ [ 31%] +........................................................................ [ 34%] +........................................................................ [ 37%] +........................................................................ [ 40%] +........................................................................ [ 43%] +........................................................................ [ 45%] +........................................................................ [ 48%] +........................................................................ [ 51%] +........................................................................ [ 54%] +........................................................................ [ 57%] +........................................................................ [ 60%] +........................................................................ [ 63%] +........................................................................ [ 66%] +........................................................................ [ 68%] +........................................................................ [ 71%] +........................................................................ [ 74%] +........................................................................ [ 77%] +........................................................................ [ 80%] +........................................................................ [ 83%] +........................................................................ [ 86%] +........................................................................ [ 89%] +........................................................................ [ 91%] +........................................................................ [ 94%] +........................................................................ [ 97%] +.......................................................... [100%] +=============================== warnings summary =============================== +.venv/lib/python3.13/site-packages/fastapi/testclient.py:1 + /var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead. + from starlette.testclient import TestClient as TestClient # noqa + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +================================ tests coverage ================================ +_______________ coverage: platform linux, python 3.13.13-final-0 _______________ + +Name Stmts Miss Cover +-------------------------------------------------- +app/__init__.py 1 0 100% +app/api/__init__.py 0 0 100% +app/api/auth.py 52 0 100% +app/api/chat.py 225 1 99% +app/api/chats.py 110 0 100% +app/api/config.py 13 0 100% +app/api/doc_drafts.py 99 0 100% +app/api/docs.py 156 1 99% +app/api/git_sources.py 232 0 100% +app/api/health.py 10 0 100% +app/api/steering.py 42 0 100% +app/api/suggestions.py 33 0 100% +app/api/sync.py 139 0 100% +app/api/tokens.py 40 0 100% +app/api/ui_settings.py 55 0 100% +app/config.py 224 0 100% +app/core/__init__.py 0 0 100% +app/core/auth.py 45 0 100% +app/core/caching.py 124 0 100% +app/core/debugging.py 29 2 93% +app/core/docs_push.py 39 0 100% +app/core/errors.py 5 0 100% +app/core/logging.py 13 0 100% +app/core/rate_limit.py 44 0 100% +app/core/security_headers.py 20 0 100% +app/core/theming.py 38 0 100% +app/core/tokens.py 44 0 100% +app/db.py 22 0 100% +app/main.py 66 0 100% +app/models.py 128 0 100% +app/rag/__init__.py 0 0 100% +app/rag/agent.py 317 1 99% +app/rag/archive_upload.py 134 0 100% +app/rag/chunker.py 206 4 98% +app/rag/doc_dates.py 18 0 100% +app/rag/folder_summaries.py 123 0 100% +app/rag/git_sources.py 14 0 100% +app/rag/importer.py 219 3 99% +app/rag/llm.py 244 1 99% +app/rag/overview.py 71 0 100% +app/rag/prompts.py 100 0 100% +app/rag/retriever.py 217 3 99% +app/rag/scaffolding.py 55 0 100% +app/rag/source_removal.py 41 0 100% +app/rag/sources_meta.py 16 0 100% +app/rag/suggestions.py 27 0 100% +app/rag/summarizer.py 24 0 100% +app/schemas.py 330 0 100% +-------------------------------------------------- +TOTAL 4204 16 99% +coverage gate: app/ 99% (>90%) OK +All checks passed! +0 errors, 0 warnings, 0 informations +WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414). +Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest` + +validation OK diff --git a/.agents/reports/118_summary_seed_context/118_summary_seed_context__01_all_doc_summaries.a1.err b/.agents/reports/118_summary_seed_context/118_summary_seed_context__01_all_doc_summaries.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/118_summary_seed_context/118_summary_seed_context__01_all_doc_summaries.a1.md b/.agents/reports/118_summary_seed_context/118_summary_seed_context__01_all_doc_summaries.a1.md new file mode 100644 index 0000000..f4a5275 --- /dev/null +++ b/.agents/reports/118_summary_seed_context/118_summary_seed_context__01_all_doc_summaries.a1.md @@ -0,0 +1,18 @@ +All gates green. Task 01 is complete. + +**Implemented** +- `app/rag/importer.py`: markdown exclusion removed — every new/changed A9 doc gets a `lite` summary (locked A2); UNCHANGED branch backfills NULL-summary docs via `_store_summary(backfill=True)` before the `created_at_manual` early-return/date refresh (strict `is None` — owner-set summaries untouched); `ImportSummary.summary_backfilled` counter + `summary_backfilled=%d` in the PLAN §9 log line after `summary_errors`; fail-soft rollback + `summary_errors` unchanged; no added/updated/pruned count (no `sources_meta` bump) +- Docstrings updated for the retired "non-markdown only" wording: `models.py`, `schemas.py`, `rag/llm.py`, `rag/overview.py` +- Unit tests: markdown test inverted (`test_markdown_file_gets_stored_summary`) + 5 new backfill tests (success, no-resummarize, empty-string never backfilled, manual-date, fail-soft with date still refreshing) + log-line pin +- Re-pinned 7 integration suites whose old-contract pins my change invalidated (`test_importer_e2e`, `test_import_extensions_env`, `test_import_extensionless`, `test_importer_ignore`, `test_importer_include_hidden`, `test_import_docs_overview`, `test_sync_folder_summaries`) + +**Gates** +- `uv run pytest tests/unit/test_importer.py -v --no-cov` → 35 passed +- `uv run pytest --cov=app --cov-report=term-missing` → 2471 passed, **99%** on `app/` (>90%) +- `uv run ruff check . && uv run pyright` → clean (0 errors) + +**Decisions** +- Sync-API result dict and the `scripts/import_docs.py` CLI line left unchanged (task mandates only the `log()` line + dataclass counter; the sync result is a pinned API surface) +- E2E pins of the retired behavior (`test_document_summaries.py` etc.) deliberately left for task 06 per the phase design + +**Next pending task:** `02_suggested_tier_selection.md` diff --git a/.agents/reports/118_summary_seed_context/118_summary_seed_context__01_all_doc_summaries.a1.validate b/.agents/reports/118_summary_seed_context/118_summary_seed_context__01_all_doc_summaries.a1.validate new file mode 100644 index 0000000..017a2ca --- /dev/null +++ b/.agents/reports/118_summary_seed_context/118_summary_seed_context__01_all_doc_summaries.a1.validate @@ -0,0 +1,103 @@ +........................................................................ [ 2%] +........................................................................ [ 5%] +........................................................................ [ 8%] +........................................................................ [ 11%] +........................................................................ [ 14%] +........................................................................ [ 17%] +........................................................................ [ 20%] +........................................................................ [ 23%] +........................................................................ [ 26%] +........................................................................ [ 29%] +........................................................................ [ 32%] +........................................................................ [ 34%] +........................................................................ [ 37%] +........................................................................ [ 40%] +........................................................................ [ 43%] +........................................................................ [ 46%] +........................................................................ [ 49%] +........................................................................ [ 52%] +........................................................................ [ 55%] +........................................................................ [ 58%] +........................................................................ [ 61%] +........................................................................ [ 64%] +........................................................................ [ 67%] +........................................................................ [ 69%] +........................................................................ [ 72%] +........................................................................ [ 75%] +........................................................................ [ 78%] +........................................................................ [ 81%] +........................................................................ [ 84%] +........................................................................ [ 87%] +........................................................................ [ 90%] +........................................................................ [ 93%] +........................................................................ [ 96%] +........................................................................ [ 99%] +....................... [100%] +=============================== warnings summary =============================== +.venv/lib/python3.13/site-packages/fastapi/testclient.py:1 + /var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead. + from starlette.testclient import TestClient as TestClient # noqa + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +================================ tests coverage ================================ +_______________ coverage: platform linux, python 3.13.13-final-0 _______________ + +Name Stmts Miss Cover +-------------------------------------------------- +app/__init__.py 1 0 100% +app/api/__init__.py 0 0 100% +app/api/auth.py 52 0 100% +app/api/chat.py 224 1 99% +app/api/chats.py 110 0 100% +app/api/config.py 13 0 100% +app/api/doc_drafts.py 99 0 100% +app/api/docs.py 156 1 99% +app/api/git_sources.py 232 0 100% +app/api/health.py 10 0 100% +app/api/steering.py 42 0 100% +app/api/suggestions.py 33 0 100% +app/api/sync.py 139 0 100% +app/api/tokens.py 40 0 100% +app/api/ui_settings.py 55 0 100% +app/config.py 210 0 100% +app/core/__init__.py 0 0 100% +app/core/auth.py 45 0 100% +app/core/caching.py 124 0 100% +app/core/debugging.py 29 2 93% +app/core/docs_push.py 39 0 100% +app/core/errors.py 5 0 100% +app/core/logging.py 13 0 100% +app/core/rate_limit.py 44 0 100% +app/core/security_headers.py 20 0 100% +app/core/theming.py 38 0 100% +app/core/tokens.py 44 0 100% +app/db.py 22 0 100% +app/main.py 66 0 100% +app/models.py 128 0 100% +app/rag/__init__.py 0 0 100% +app/rag/agent.py 317 1 99% +app/rag/archive_upload.py 134 0 100% +app/rag/chunker.py 206 4 98% +app/rag/doc_dates.py 18 0 100% +app/rag/folder_summaries.py 123 0 100% +app/rag/git_sources.py 14 0 100% +app/rag/importer.py 219 3 99% +app/rag/llm.py 244 1 99% +app/rag/overview.py 71 0 100% +app/rag/prompts.py 88 0 100% +app/rag/retriever.py 192 3 98% +app/rag/scaffolding.py 55 0 100% +app/rag/source_removal.py 41 0 100% +app/rag/sources_meta.py 16 0 100% +app/rag/suggestions.py 27 0 100% +app/rag/summarizer.py 24 0 100% +app/schemas.py 329 0 100% +-------------------------------------------------- +TOTAL 4151 16 99% +coverage gate: app/ 99% (>90%) OK +All checks passed! +0 errors, 0 warnings, 0 informations +WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414). +Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest` + +validation OK diff --git a/.agents/reports/118_summary_seed_context/118_summary_seed_context__02_suggested_tier_selection.a1.err b/.agents/reports/118_summary_seed_context/118_summary_seed_context__02_suggested_tier_selection.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/118_summary_seed_context/118_summary_seed_context__02_suggested_tier_selection.a1.md b/.agents/reports/118_summary_seed_context/118_summary_seed_context__02_suggested_tier_selection.a1.md new file mode 100644 index 0000000..4031b3c --- /dev/null +++ b/.agents/reports/118_summary_seed_context/118_summary_seed_context__02_suggested_tier_selection.a1.md @@ -0,0 +1,16 @@ +Task 02 complete — all gates green. + +**Implemented** +- `app/rag/retriever.py`: pure `select_suggested(chunks, n=None)` — top-N distinct parent docs in the same stable score-descending walk as `select_documents_tiered` (rank fixed by first-seen chunk, dedupe by `document.id`), **no cosine floor** (A3), `n` defaults to `get_settings().suggested_docs`; rows carry full byte-identical content (A6). +- `app/config.py`: `suggested_docs: int = 5` (`BOR_SUGGESTED_DOCS`, validator `>= 1`, `agent_max_rounds` pattern) + `.env.example` entry in the RAG-tuning block. +- `tests/unit/test_config.py`: default 5 + env override; 0/−2 rejected, 1 accepted. +- `tests/unit/test_retriever.py`: first-seen-chunk rank order, dedupe, cap (explicit `n` + live settings default via monkeypatched `get_settings`), no-floor contrast pin vs `select_documents_tiered`'s floored cited tier on the same input, deterministic tie-break inherited from fused order, empty + full-content pins. + +**Results** +- `uv run pytest tests/unit/test_retriever.py tests/unit/test_config.py -v` → 104 passed +- `uv run pytest --cov=app --cov-report=term-missing` → 2480 passed, **99%** total (`app/config.py` 100%; `retriever.py` missing lines are pre-existing SQL paths) +- `uv run ruff check .` → All checks passed; `uv run pyright` → 0 errors + +**Decisions** — early-exit cap in the walk (safe: no floor/related tier to compute); settings-default test pins the live value, not a frozen constant. No defects found in prior work; no git operations (left for the harness). + +**Next pending task:** `.agents/phases/todo/118_summary_seed_context/03_summary_document_blocks.md` diff --git a/.agents/reports/118_summary_seed_context/118_summary_seed_context__02_suggested_tier_selection.a1.validate b/.agents/reports/118_summary_seed_context/118_summary_seed_context__02_suggested_tier_selection.a1.validate new file mode 100644 index 0000000..ba97a47 --- /dev/null +++ b/.agents/reports/118_summary_seed_context/118_summary_seed_context__02_suggested_tier_selection.a1.validate @@ -0,0 +1,103 @@ +........................................................................ [ 2%] +........................................................................ [ 5%] +........................................................................ [ 8%] +........................................................................ [ 11%] +........................................................................ [ 14%] +........................................................................ [ 17%] +........................................................................ [ 20%] +........................................................................ [ 23%] +........................................................................ [ 26%] +........................................................................ [ 29%] +........................................................................ [ 31%] +........................................................................ [ 34%] +........................................................................ [ 37%] +........................................................................ [ 40%] +........................................................................ [ 43%] +........................................................................ [ 46%] +........................................................................ [ 49%] +........................................................................ [ 52%] +........................................................................ [ 55%] +........................................................................ [ 58%] +........................................................................ [ 60%] +........................................................................ [ 63%] +........................................................................ [ 66%] +........................................................................ [ 69%] +........................................................................ [ 72%] +........................................................................ [ 75%] +........................................................................ [ 78%] +........................................................................ [ 81%] +........................................................................ [ 84%] +........................................................................ [ 87%] +........................................................................ [ 90%] +........................................................................ [ 92%] +........................................................................ [ 95%] +........................................................................ [ 98%] +................................ [100%] +=============================== warnings summary =============================== +.venv/lib/python3.13/site-packages/fastapi/testclient.py:1 + /var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead. + from starlette.testclient import TestClient as TestClient # noqa + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +================================ tests coverage ================================ +_______________ coverage: platform linux, python 3.13.13-final-0 _______________ + +Name Stmts Miss Cover +-------------------------------------------------- +app/__init__.py 1 0 100% +app/api/__init__.py 0 0 100% +app/api/auth.py 52 0 100% +app/api/chat.py 224 1 99% +app/api/chats.py 110 0 100% +app/api/config.py 13 0 100% +app/api/doc_drafts.py 99 0 100% +app/api/docs.py 156 1 99% +app/api/git_sources.py 232 0 100% +app/api/health.py 10 0 100% +app/api/steering.py 42 0 100% +app/api/suggestions.py 33 0 100% +app/api/sync.py 139 0 100% +app/api/tokens.py 40 0 100% +app/api/ui_settings.py 55 0 100% +app/config.py 217 0 100% +app/core/__init__.py 0 0 100% +app/core/auth.py 45 0 100% +app/core/caching.py 124 0 100% +app/core/debugging.py 29 2 93% +app/core/docs_push.py 39 0 100% +app/core/errors.py 5 0 100% +app/core/logging.py 13 0 100% +app/core/rate_limit.py 44 0 100% +app/core/security_headers.py 20 0 100% +app/core/theming.py 38 0 100% +app/core/tokens.py 44 0 100% +app/db.py 22 0 100% +app/main.py 66 0 100% +app/models.py 128 0 100% +app/rag/__init__.py 0 0 100% +app/rag/agent.py 317 1 99% +app/rag/archive_upload.py 134 0 100% +app/rag/chunker.py 206 4 98% +app/rag/doc_dates.py 18 0 100% +app/rag/folder_summaries.py 123 0 100% +app/rag/git_sources.py 14 0 100% +app/rag/importer.py 219 3 99% +app/rag/llm.py 244 1 99% +app/rag/overview.py 71 0 100% +app/rag/prompts.py 88 0 100% +app/rag/retriever.py 205 3 99% +app/rag/scaffolding.py 55 0 100% +app/rag/source_removal.py 41 0 100% +app/rag/sources_meta.py 16 0 100% +app/rag/suggestions.py 27 0 100% +app/rag/summarizer.py 24 0 100% +app/schemas.py 329 0 100% +-------------------------------------------------- +TOTAL 4171 16 99% +coverage gate: app/ 99% (>90%) OK +All checks passed! +0 errors, 0 warnings, 0 informations +WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414). +Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest` + +validation OK diff --git a/.agents/reports/118_summary_seed_context/118_summary_seed_context__03_summary_document_blocks.a1.err b/.agents/reports/118_summary_seed_context/118_summary_seed_context__03_summary_document_blocks.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/118_summary_seed_context/118_summary_seed_context__03_summary_document_blocks.a1.md b/.agents/reports/118_summary_seed_context/118_summary_seed_context__03_summary_document_blocks.a1.md new file mode 100644 index 0000000..fc434a1 --- /dev/null +++ b/.agents/reports/118_summary_seed_context/118_summary_seed_context__03_summary_document_blocks.a1.md @@ -0,0 +1,19 @@ +All gates verified. Task complete. + +**Task 03 — "start here" suggestion blocks in the HIGH prompt** + +Implemented: +- `app/rag/prompts.py`: `build_high_prompt` now seeds each `` block with the doc's **summary** (markup `source/path/title/date` byte-stable); new pinned `SUGGEST_INTRO` start-here line before the first block; `_document_body()` A5 fallback — NULL/whitespace summary → first `suggestion_preview_chars` chars + `TRUNCATION_MARKER` (marker only when content was actually cut; settings read on fallback path only) +- `app/config.py` + `.env.example`: `suggestion_preview_chars` (default 400, env `BOR_SUGGESTION_PREVIEW_CHARS`, validator `> 0`) +- `app/rag/retriever.py`: module docstring + `TRUNCATION_MARKER` comment + `select_documents`/`select_documents_tiered` docstrings re-pointed to the A6 re-revised contract (functions untouched) +- Tests: summary-seed sentinels (5-doc pin, zero full-content), preview-fallback matrix (None/whitespace/short/exact-cap/cap-override/no-settings-read), `date`-attribute pin on summary bodies, `SUGGEST_INTRO` sha+prefix lock, LOW-prompt build sha pin (byte-identical); updated the one retired full-text pin in `tests/integration/test_chat_api.py` to pin the summary contract + +Gates: +- `uv run pytest tests/unit/test_prompts.py tests/unit/test_prompts_dates.py tests/unit/test_prompt_lock.py -v --no-cov` → 59 passed +- `uv run pytest` (unit+integration) → exit 0, 0 failures +- `uv run pytest --cov=app --cov-report=term-missing` → TOTAL 99% (>90%) +- `uv run ruff check .` → clean; `uv run pyright` → 0 errors + +Decisions: empty `` section keeps the fallback line with **no** intro (intro rides only on present blocks); LOW/`DEFLECT_MODE`/`PERSONA`/`TOOLS_SECTION` untouched. + +Next pending task: `.agents/phases/todo/118_summary_seed_context/04_read_suggested_docs.md` diff --git a/.agents/reports/118_summary_seed_context/118_summary_seed_context__03_summary_document_blocks.a1.validate b/.agents/reports/118_summary_seed_context/118_summary_seed_context__03_summary_document_blocks.a1.validate new file mode 100644 index 0000000..1e5f7a2 --- /dev/null +++ b/.agents/reports/118_summary_seed_context/118_summary_seed_context__03_summary_document_blocks.a1.validate @@ -0,0 +1,103 @@ +........................................................................ [ 2%] +........................................................................ [ 5%] +........................................................................ [ 8%] +........................................................................ [ 11%] +........................................................................ [ 14%] +........................................................................ [ 17%] +........................................................................ [ 20%] +........................................................................ [ 23%] +........................................................................ [ 25%] +........................................................................ [ 28%] +........................................................................ [ 31%] +........................................................................ [ 34%] +........................................................................ [ 37%] +........................................................................ [ 40%] +........................................................................ [ 43%] +........................................................................ [ 46%] +........................................................................ [ 49%] +........................................................................ [ 51%] +........................................................................ [ 54%] +........................................................................ [ 57%] +........................................................................ [ 60%] +........................................................................ [ 63%] +........................................................................ [ 66%] +........................................................................ [ 69%] +........................................................................ [ 72%] +........................................................................ [ 75%] +........................................................................ [ 77%] +........................................................................ [ 80%] +........................................................................ [ 83%] +........................................................................ [ 86%] +........................................................................ [ 89%] +........................................................................ [ 92%] +........................................................................ [ 95%] +........................................................................ [ 98%] +.............................................. [100%] +=============================== warnings summary =============================== +.venv/lib/python3.13/site-packages/fastapi/testclient.py:1 + /var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead. + from starlette.testclient import TestClient as TestClient # noqa + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +================================ tests coverage ================================ +_______________ coverage: platform linux, python 3.13.13-final-0 _______________ + +Name Stmts Miss Cover +-------------------------------------------------- +app/__init__.py 1 0 100% +app/api/__init__.py 0 0 100% +app/api/auth.py 52 0 100% +app/api/chat.py 224 1 99% +app/api/chats.py 110 0 100% +app/api/config.py 13 0 100% +app/api/doc_drafts.py 99 0 100% +app/api/docs.py 156 1 99% +app/api/git_sources.py 232 0 100% +app/api/health.py 10 0 100% +app/api/steering.py 42 0 100% +app/api/suggestions.py 33 0 100% +app/api/sync.py 139 0 100% +app/api/tokens.py 40 0 100% +app/api/ui_settings.py 55 0 100% +app/config.py 224 0 100% +app/core/__init__.py 0 0 100% +app/core/auth.py 45 0 100% +app/core/caching.py 124 0 100% +app/core/debugging.py 29 2 93% +app/core/docs_push.py 39 0 100% +app/core/errors.py 5 0 100% +app/core/logging.py 13 0 100% +app/core/rate_limit.py 44 0 100% +app/core/security_headers.py 20 0 100% +app/core/theming.py 38 0 100% +app/core/tokens.py 44 0 100% +app/db.py 22 0 100% +app/main.py 66 0 100% +app/models.py 128 0 100% +app/rag/__init__.py 0 0 100% +app/rag/agent.py 317 1 99% +app/rag/archive_upload.py 134 0 100% +app/rag/chunker.py 206 4 98% +app/rag/doc_dates.py 18 0 100% +app/rag/folder_summaries.py 123 0 100% +app/rag/git_sources.py 14 0 100% +app/rag/importer.py 219 3 99% +app/rag/llm.py 244 1 99% +app/rag/overview.py 71 0 100% +app/rag/prompts.py 100 0 100% +app/rag/retriever.py 205 3 99% +app/rag/scaffolding.py 55 0 100% +app/rag/source_removal.py 41 0 100% +app/rag/sources_meta.py 16 0 100% +app/rag/suggestions.py 27 0 100% +app/rag/summarizer.py 24 0 100% +app/schemas.py 329 0 100% +-------------------------------------------------- +TOTAL 4190 16 99% +coverage gate: app/ 99% (>90%) OK +All checks passed! +0 errors, 0 warnings, 0 informations +WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414). +Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest` + +validation OK diff --git a/.agents/reports/118_summary_seed_context/118_summary_seed_context__04_read_suggested_docs.a1.err b/.agents/reports/118_summary_seed_context/118_summary_seed_context__04_read_suggested_docs.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/118_summary_seed_context/118_summary_seed_context__04_read_suggested_docs.a1.md b/.agents/reports/118_summary_seed_context/118_summary_seed_context__04_read_suggested_docs.a1.md new file mode 100644 index 0000000..63875db --- /dev/null +++ b/.agents/reports/118_summary_seed_context/118_summary_seed_context__04_read_suggested_docs.a1.md @@ -0,0 +1,17 @@ +All green. Final report: + +**Task 04 complete — `read` on a suggested doc adds its full text; copy rewritten; lock re-cut** + +- `app/rag/agent.py`: `_execute_tool` read dedupe set → `holder.read_docs` only (seeds drop out; first read of a suggested doc succeeds through the unchanged path — cap/marker/notice/date-line intact); `ALREADY_IN_CONTEXT` constant byte-identical; `AGENT_TOOLS` `read` description + `path` param rewritten (identity, bare-path, truncation-notice, one-call sentences byte-preserved); module docstring A7-clarification → A6 re-revised contract; point-3 + `run_agent` `seed_docs` docstring paragraphs rewritten (parameters kept, callers unchanged) +- `app/rag/prompts.py`: `TOOLS_SECTION` `read` clause rewritten for summary-seed mode; `ls`/`grep` clauses + discipline rules byte-identical; comment block updated +- `tests/unit/test_prompt_lock.py`: `TOOLS_SECTION_SHA256`/`LEN` (2465) re-cut, comment cites phase-118 (A6, 2026-09-15); PREFIX/SUFFIX verified to survive; `PERSONA`/LOW-body anchors untouched and passing +- `tests/unit/test_agent.py`: suggested-read success + re-read refusal (no DB lookup, counters untouched) + over-cap suggested doc truncation re-target + description pins; `tests/unit/test_prompts.py`: new phase-118 read-clause pin (new copy, byte-pinned ls/grep/discipline, old copy gone, HIGH-vs-LOW) + +**Results** +- `uv run pytest tests/unit/test_agent.py tests/unit/test_prompt_lock.py tests/unit/test_prompts.py -v` → 161 passed +- `uv run pytest --no-cov` → 2496 passed; `uv run pytest --cov=app` → 2496 passed, app coverage 99% (>90%) +- `uv run ruff check .` → clean; `uv run pyright` → 0 errors, 0 warnings + +**Decisions:** E2E mock + old pins (`mock_llm.py`, `test_ls_tree_drilldown.py`, `test_read_truncation_cap.py`) intentionally untouched — task 06's explicit scope (not in the default unit+integration suite). + +Next pending task: `.agents/phases/todo/118_summary_seed_context/05_chat_turn_wiring.md` diff --git a/.agents/reports/118_summary_seed_context/118_summary_seed_context__04_read_suggested_docs.a1.validate b/.agents/reports/118_summary_seed_context/118_summary_seed_context__04_read_suggested_docs.a1.validate new file mode 100644 index 0000000..d4472f0 --- /dev/null +++ b/.agents/reports/118_summary_seed_context/118_summary_seed_context__04_read_suggested_docs.a1.validate @@ -0,0 +1,103 @@ +........................................................................ [ 2%] +........................................................................ [ 5%] +........................................................................ [ 8%] +........................................................................ [ 11%] +........................................................................ [ 14%] +........................................................................ [ 17%] +........................................................................ [ 20%] +........................................................................ [ 23%] +........................................................................ [ 25%] +........................................................................ [ 28%] +........................................................................ [ 31%] +........................................................................ [ 34%] +........................................................................ [ 37%] +........................................................................ [ 40%] +........................................................................ [ 43%] +........................................................................ [ 46%] +........................................................................ [ 49%] +........................................................................ [ 51%] +........................................................................ [ 54%] +........................................................................ [ 57%] +........................................................................ [ 60%] +........................................................................ [ 63%] +........................................................................ [ 66%] +........................................................................ [ 69%] +........................................................................ [ 72%] +........................................................................ [ 75%] +........................................................................ [ 77%] +........................................................................ [ 80%] +........................................................................ [ 83%] +........................................................................ [ 86%] +........................................................................ [ 89%] +........................................................................ [ 92%] +........................................................................ [ 95%] +........................................................................ [ 98%] +................................................ [100%] +=============================== warnings summary =============================== +.venv/lib/python3.13/site-packages/fastapi/testclient.py:1 + /var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead. + from starlette.testclient import TestClient as TestClient # noqa + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +================================ tests coverage ================================ +_______________ coverage: platform linux, python 3.13.13-final-0 _______________ + +Name Stmts Miss Cover +-------------------------------------------------- +app/__init__.py 1 0 100% +app/api/__init__.py 0 0 100% +app/api/auth.py 52 0 100% +app/api/chat.py 224 1 99% +app/api/chats.py 110 0 100% +app/api/config.py 13 0 100% +app/api/doc_drafts.py 99 0 100% +app/api/docs.py 156 1 99% +app/api/git_sources.py 232 0 100% +app/api/health.py 10 0 100% +app/api/steering.py 42 0 100% +app/api/suggestions.py 33 0 100% +app/api/sync.py 139 0 100% +app/api/tokens.py 40 0 100% +app/api/ui_settings.py 55 0 100% +app/config.py 224 0 100% +app/core/__init__.py 0 0 100% +app/core/auth.py 45 0 100% +app/core/caching.py 124 0 100% +app/core/debugging.py 29 2 93% +app/core/docs_push.py 39 0 100% +app/core/errors.py 5 0 100% +app/core/logging.py 13 0 100% +app/core/rate_limit.py 44 0 100% +app/core/security_headers.py 20 0 100% +app/core/theming.py 38 0 100% +app/core/tokens.py 44 0 100% +app/db.py 22 0 100% +app/main.py 66 0 100% +app/models.py 128 0 100% +app/rag/__init__.py 0 0 100% +app/rag/agent.py 317 1 99% +app/rag/archive_upload.py 134 0 100% +app/rag/chunker.py 206 4 98% +app/rag/doc_dates.py 18 0 100% +app/rag/folder_summaries.py 123 0 100% +app/rag/git_sources.py 14 0 100% +app/rag/importer.py 219 3 99% +app/rag/llm.py 244 1 99% +app/rag/overview.py 71 0 100% +app/rag/prompts.py 100 0 100% +app/rag/retriever.py 205 3 99% +app/rag/scaffolding.py 55 0 100% +app/rag/source_removal.py 41 0 100% +app/rag/sources_meta.py 16 0 100% +app/rag/suggestions.py 27 0 100% +app/rag/summarizer.py 24 0 100% +app/schemas.py 329 0 100% +-------------------------------------------------- +TOTAL 4190 16 99% +coverage gate: app/ 99% (>90%) OK +All checks passed! +0 errors, 0 warnings, 0 informations +WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414). +Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest` + +validation OK diff --git a/.agents/reports/118_summary_seed_context/118_summary_seed_context__05_chat_turn_wiring.a1.err b/.agents/reports/118_summary_seed_context/118_summary_seed_context__05_chat_turn_wiring.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/118_summary_seed_context/118_summary_seed_context__05_chat_turn_wiring.a1.md b/.agents/reports/118_summary_seed_context/118_summary_seed_context__05_chat_turn_wiring.a1.md new file mode 100644 index 0000000..a314ce6 --- /dev/null +++ b/.agents/reports/118_summary_seed_context/118_summary_seed_context__05_chat_turn_wiring.a1.md @@ -0,0 +1,16 @@ +Task 05 complete. All validation gates are green. + +**Implemented (task 05 — `plan_turn` wiring):** +- `app/rag/retriever.py`: new pure `select_related(chunks, excluded_ids, cap)` — same stable score-descending walk, skips excluded ids, no floor, capped; plus "phase 118 retired the seeding role (A6)" docstring notes on dormant `select_documents`/`select_documents_tiered` +- `app/api/chat.py`: `plan_turn` now computes `select_suggested` (top-5, no floor, honors `suggested_docs`) + `select_related` (rank 6+, honors `related_max_docs`) once for BOTH branches; HIGH prompt seeds the suggested summaries; `TurnPlan.docs` → `suggested_docs` (same position); `summary_hits` redefined to the suggested set; `run_agent(seed_docs=plan.suggested_docs)`; `cited_docs` = suggested+read, `record_docs` = suggested+related+read; log line gains `suggested=%d` after `summary_hits=%d` (phase-114 tail stays last) +- `app/config.py`: retirement notes on `top_n_docs`/`source_usefulness_floor` (kept, no `app/` consumer); script ripples: `plan.suggested_docs` in `scripts/agent_realmodel_check.py` + `scripts/load_test_kb.py` +- Tests: 8 `select_related` unit tests; `test_chat_gate.py` re-pinned (7-doc fixture: 5 summaries seeded, zero full content, related = rank 6–7; deflected prompt sha256-pinned byte-identical + `build_deflect_prompt` equality; `summary_hits` redefinition; suggested/related on both branches; `suggested=N` log-slot + full field-order pin; retired-settings behavioral pin); `test_source_chip_quality.py` shapes re-pinned to no-floor tiering; `test_chat_api.py`: `done.sources` = 5 suggested (+read deduped last), `done.related` = rank 6–7 minus cited, `query_log.sources` = full 7-doc retrieval, deflection = `[]` + +**Results:** +- `uv run pytest tests/unit/test_chat_gate.py tests/integration/test_chat_api.py -v` → 94 passed +- `uv run pytest --cov=app` → 2506 passed, app/ coverage 99% (>90% gate) +- `uv run ruff check .` → clean; `uv run pyright` → 0 errors + +**Decisions:** `select_suggested(chunks, n=settings.suggested_docs)` passes the turn's settings (unit tests control the cap without env); deflected-frame "byte-identical" pinned as prompt-sha + frame-shape (related-row content is rank 6+ by design, per task item 7); one deflection wire test's scripted read re-targeted from `backups.md` (now rank 6, durably recorded) to `tables.md` (rank 11) to keep the "never read" pin distinct. + +**Next pending task:** `118_summary_seed_context/06_e2e_summary_context.md` diff --git a/.agents/reports/118_summary_seed_context/118_summary_seed_context__05_chat_turn_wiring.a1.validate b/.agents/reports/118_summary_seed_context/118_summary_seed_context__05_chat_turn_wiring.a1.validate new file mode 100644 index 0000000..4876b9b --- /dev/null +++ b/.agents/reports/118_summary_seed_context/118_summary_seed_context__05_chat_turn_wiring.a1.validate @@ -0,0 +1,103 @@ +........................................................................ [ 2%] +........................................................................ [ 5%] +........................................................................ [ 8%] +........................................................................ [ 11%] +........................................................................ [ 14%] +........................................................................ [ 17%] +........................................................................ [ 20%] +........................................................................ [ 22%] +........................................................................ [ 25%] +........................................................................ [ 28%] +........................................................................ [ 31%] +........................................................................ [ 34%] +........................................................................ [ 37%] +........................................................................ [ 40%] +........................................................................ [ 43%] +........................................................................ [ 45%] +........................................................................ [ 48%] +........................................................................ [ 51%] +........................................................................ [ 54%] +........................................................................ [ 57%] +........................................................................ [ 60%] +........................................................................ [ 63%] +........................................................................ [ 66%] +........................................................................ [ 68%] +........................................................................ [ 71%] +........................................................................ [ 74%] +........................................................................ [ 77%] +........................................................................ [ 80%] +........................................................................ [ 83%] +........................................................................ [ 86%] +........................................................................ [ 89%] +........................................................................ [ 91%] +........................................................................ [ 94%] +........................................................................ [ 97%] +.......................................................... [100%] +=============================== warnings summary =============================== +.venv/lib/python3.13/site-packages/fastapi/testclient.py:1 + /var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead. + from starlette.testclient import TestClient as TestClient # noqa + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +================================ tests coverage ================================ +_______________ coverage: platform linux, python 3.13.13-final-0 _______________ + +Name Stmts Miss Cover +-------------------------------------------------- +app/__init__.py 1 0 100% +app/api/__init__.py 0 0 100% +app/api/auth.py 52 0 100% +app/api/chat.py 225 1 99% +app/api/chats.py 110 0 100% +app/api/config.py 13 0 100% +app/api/doc_drafts.py 99 0 100% +app/api/docs.py 156 1 99% +app/api/git_sources.py 232 0 100% +app/api/health.py 10 0 100% +app/api/steering.py 42 0 100% +app/api/suggestions.py 33 0 100% +app/api/sync.py 139 0 100% +app/api/tokens.py 40 0 100% +app/api/ui_settings.py 55 0 100% +app/config.py 224 0 100% +app/core/__init__.py 0 0 100% +app/core/auth.py 45 0 100% +app/core/caching.py 124 0 100% +app/core/debugging.py 29 2 93% +app/core/docs_push.py 39 0 100% +app/core/errors.py 5 0 100% +app/core/logging.py 13 0 100% +app/core/rate_limit.py 44 0 100% +app/core/security_headers.py 20 0 100% +app/core/theming.py 38 0 100% +app/core/tokens.py 44 0 100% +app/db.py 22 0 100% +app/main.py 66 0 100% +app/models.py 128 0 100% +app/rag/__init__.py 0 0 100% +app/rag/agent.py 317 1 99% +app/rag/archive_upload.py 134 0 100% +app/rag/chunker.py 206 4 98% +app/rag/doc_dates.py 18 0 100% +app/rag/folder_summaries.py 123 0 100% +app/rag/git_sources.py 14 0 100% +app/rag/importer.py 219 3 99% +app/rag/llm.py 244 1 99% +app/rag/overview.py 71 0 100% +app/rag/prompts.py 100 0 100% +app/rag/retriever.py 217 3 99% +app/rag/scaffolding.py 55 0 100% +app/rag/source_removal.py 41 0 100% +app/rag/sources_meta.py 16 0 100% +app/rag/suggestions.py 27 0 100% +app/rag/summarizer.py 24 0 100% +app/schemas.py 329 0 100% +-------------------------------------------------- +TOTAL 4203 16 99% +coverage gate: app/ 99% (>90%) OK +All checks passed! +0 errors, 0 warnings, 0 informations +WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414). +Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest` + +validation OK diff --git a/.agents/reports/118_summary_seed_context/118_summary_seed_context__06_e2e_summary_context.a1.err b/.agents/reports/118_summary_seed_context/118_summary_seed_context__06_e2e_summary_context.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/118_summary_seed_context/118_summary_seed_context__06_e2e_summary_context.a1.md b/.agents/reports/118_summary_seed_context/118_summary_seed_context__06_e2e_summary_context.a1.md new file mode 100644 index 0000000..08556e1 --- /dev/null +++ b/.agents/reports/118_summary_seed_context/118_summary_seed_context__06_e2e_summary_context.a1.md @@ -0,0 +1,11 @@ +All gates verified and the repo is clean. Final report: + +**Task 06 (phase 118) complete — summary-seed E2E contract.** + +- NEW `tests/e2e/test_summary_seed_context.py` (mock-only, own app :8138): 7-doc markdown KB (token-controlled strength gradient) proves cases a–e — tail-echo pins the LAST suggested doc's mock-digest summary tail in the prompt (a/d, markdown included), NO tail sentinels (b, full text never seeded), scripted `SUMMARY_SEED_READ_TRIGGER` flow reads rank-1 suggested doc → verbatim read-echo lands the sentinel (c), chips = suggested+read deduped, related row = rank 6+ (e). Setup pins tier order via the app's real `retrieve`/`select_suggested`/`select_related` (tail: a,d,b,c,e+f,g; read: a,b,c,d,e+f,g). +- Deleted `test_whole_document_context.py`; re-targeted drill (read SUCCEEDS — seed-read refusal gone; mock drill quote now skips the D5 `date:` line → byte-identical content quote, fixing a pre-existing drill break), read-cap (removed the not-a-seed guard + `embed_text` import), source-chip-quality (chips = suggested top-5 no-floor; related = rank 6+), document-dates (4 chips, no related row, exact `row.sources`), document-summaries (summaries 1→2; md HAS the digest; sentinel pin inverted to absent), summary-in-viewer (md gets the panel, API summary non-null), mock docstrings (ALREADY_IN_CONTEXT → already-read case). +- Defect found + fixed: `test_document_dates` focus-trap pin broke under A2 (markdown now has a summary → `doc-summary-edit` joins the trap): Tab chain re-targeted (revert → summary-edit → wrap to `doc-modal-open`). +- Audit sweep: no test pins the retired full-text-seed/seed-read-refusal contract (comment-only updates in agent_document_tools / theme_semantic_completion / share_chat; unit pins were task 04's). +- Gates: `uv run pytest tests/e2e/test_summary_seed_context.py -v --no-cov` → 2 passed; re-targeted E2E in isolation → drill 3, read-cap 3, chip-quality 2, dates 6, doc-summaries 2, viewer 4 (all passed); tool-flow regressions (agent_document_tools, grep_regex_teaching, harness_aligned, +7) all passed; `uv run pytest --cov=app --cov-report=term-missing` → 2506 passed, app TOTAL **99%** (>90%); `uv run ruff check .` clean; `uv run pyright` → 0 errors. +- Noted, not fixed (pre-existing, out of scope): the COMBINED `pytest tests/e2e` batch is poisoned by `test_extensionless_import.py`'s module-level `os.environ["BOR_IMPORT_EXTENSIONS"]` pin (phase 102) leaking through collection into every other suite's in-process `Settings` (65F+110E "5 vs 13 docs" cascade) — the project gate is per-story isolation (AGENTS.md rule 9), which is green; a proper fix needs that suite to own a module app (separate task). +- Next pending task: none — task 06 is the last task of phase 118 (01–05 already in `complete/`). diff --git a/.agents/reports/118_summary_seed_context/118_summary_seed_context__06_e2e_summary_context.a1.validate b/.agents/reports/118_summary_seed_context/118_summary_seed_context__06_e2e_summary_context.a1.validate new file mode 100644 index 0000000..4876b9b --- /dev/null +++ b/.agents/reports/118_summary_seed_context/118_summary_seed_context__06_e2e_summary_context.a1.validate @@ -0,0 +1,103 @@ +........................................................................ [ 2%] +........................................................................ [ 5%] +........................................................................ [ 8%] +........................................................................ [ 11%] +........................................................................ [ 14%] +........................................................................ [ 17%] +........................................................................ [ 20%] +........................................................................ [ 22%] +........................................................................ [ 25%] +........................................................................ [ 28%] +........................................................................ [ 31%] +........................................................................ [ 34%] +........................................................................ [ 37%] +........................................................................ [ 40%] +........................................................................ [ 43%] +........................................................................ [ 45%] +........................................................................ [ 48%] +........................................................................ [ 51%] +........................................................................ [ 54%] +........................................................................ [ 57%] +........................................................................ [ 60%] +........................................................................ [ 63%] +........................................................................ [ 66%] +........................................................................ [ 68%] +........................................................................ [ 71%] +........................................................................ [ 74%] +........................................................................ [ 77%] +........................................................................ [ 80%] +........................................................................ [ 83%] +........................................................................ [ 86%] +........................................................................ [ 89%] +........................................................................ [ 91%] +........................................................................ [ 94%] +........................................................................ [ 97%] +.......................................................... [100%] +=============================== warnings summary =============================== +.venv/lib/python3.13/site-packages/fastapi/testclient.py:1 + /var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead. + from starlette.testclient import TestClient as TestClient # noqa + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +================================ tests coverage ================================ +_______________ coverage: platform linux, python 3.13.13-final-0 _______________ + +Name Stmts Miss Cover +-------------------------------------------------- +app/__init__.py 1 0 100% +app/api/__init__.py 0 0 100% +app/api/auth.py 52 0 100% +app/api/chat.py 225 1 99% +app/api/chats.py 110 0 100% +app/api/config.py 13 0 100% +app/api/doc_drafts.py 99 0 100% +app/api/docs.py 156 1 99% +app/api/git_sources.py 232 0 100% +app/api/health.py 10 0 100% +app/api/steering.py 42 0 100% +app/api/suggestions.py 33 0 100% +app/api/sync.py 139 0 100% +app/api/tokens.py 40 0 100% +app/api/ui_settings.py 55 0 100% +app/config.py 224 0 100% +app/core/__init__.py 0 0 100% +app/core/auth.py 45 0 100% +app/core/caching.py 124 0 100% +app/core/debugging.py 29 2 93% +app/core/docs_push.py 39 0 100% +app/core/errors.py 5 0 100% +app/core/logging.py 13 0 100% +app/core/rate_limit.py 44 0 100% +app/core/security_headers.py 20 0 100% +app/core/theming.py 38 0 100% +app/core/tokens.py 44 0 100% +app/db.py 22 0 100% +app/main.py 66 0 100% +app/models.py 128 0 100% +app/rag/__init__.py 0 0 100% +app/rag/agent.py 317 1 99% +app/rag/archive_upload.py 134 0 100% +app/rag/chunker.py 206 4 98% +app/rag/doc_dates.py 18 0 100% +app/rag/folder_summaries.py 123 0 100% +app/rag/git_sources.py 14 0 100% +app/rag/importer.py 219 3 99% +app/rag/llm.py 244 1 99% +app/rag/overview.py 71 0 100% +app/rag/prompts.py 100 0 100% +app/rag/retriever.py 217 3 99% +app/rag/scaffolding.py 55 0 100% +app/rag/source_removal.py 41 0 100% +app/rag/sources_meta.py 16 0 100% +app/rag/suggestions.py 27 0 100% +app/rag/summarizer.py 24 0 100% +app/schemas.py 329 0 100% +-------------------------------------------------- +TOTAL 4203 16 99% +coverage gate: app/ 99% (>90%) OK +All checks passed! +0 errors, 0 warnings, 0 informations +WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414). +Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest` + +validation OK diff --git a/.agents/reports/118_summary_seed_context/118_summary_seed_context__07_fixture_battery_gate.a1.err b/.agents/reports/118_summary_seed_context/118_summary_seed_context__07_fixture_battery_gate.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/118_summary_seed_context/118_summary_seed_context__07_fixture_battery_gate.a1.md b/.agents/reports/118_summary_seed_context/118_summary_seed_context__07_fixture_battery_gate.a1.md new file mode 100644 index 0000000..8b7a880 --- /dev/null +++ b/.agents/reports/118_summary_seed_context/118_summary_seed_context__07_fixture_battery_gate.a1.md @@ -0,0 +1,11 @@ +All completion criteria are met. Final report: + +**Phase 118, task 07 — fixture-battery gate: complete (PASS)** + +- Rebuilt `tests/fixtures/test_kb.dump.sql` via `scripts.load_test_kb` — all 8 markdown fixture docs now carry a `lite` summary + embedded `is_summary` chunk (8 docs, 17 chunks: 9 content + 8 summary); `summary_errors=0`, 0 failed folder-summary batches, retrieval report all 10 GROUNDED, round-trip checksum verified. +- Fixed a pre-existing defect blocking the rebuild: `load_test_kb._TABLES` omitted `documents.created_at`/`created_at_manual`, `git_sources.ignore_paths`/`include_hidden`, `folder_summaries.manually_edited` → restore re-defaulted `created_at` and the build's own round-trip check failed on `documents`. Full column sets now dumped; header reports content + summary chunk counts. +- Verdict run (`uv run python -m scripts.agent_realmodel_check --restore --mode fixture`, model `turbo`): `gate: turbo PASS turns=10 answered=10 caps=0 tool-turns=9 calls 21/21 executed (100%) contract 21/21 (100%) 2026-09-16 (wall 118.1s)` — conditions 1/2/4 GREEN, condition 3 reported (9/10, also ≥6); no copy iteration needed. +- Appended dated §10 to `TOOL_CALLING_TESTING.md` (telemetry, contract line, wall, dump rebuild note, locked-A7 semantics note, final `read` copy); prior sections untouched. +- Results: `uv run pytest --cov=app` → 2506 passed, 99% on `app/` (no suite pins the old 9-chunk dump — it is script-owned); `uv run ruff check . && uv run pyright` → clean; `uv run pytest tests/e2e/test_summary_seed_context.py -v --no-cov` → 2 passed (sanity). +- Notable: gate script's `evaluate()` left unchanged per the task file — A7 interpretation (condition 3 reported, not gated) is applied to the recorded verdict; the script exited 0 here anyway. No commits made (harness commits). +- Next pending task: none — task 07 was the last task of phase 118; the phase is complete. diff --git a/.agents/reports/118_summary_seed_context/118_summary_seed_context__07_fixture_battery_gate.a1.validate b/.agents/reports/118_summary_seed_context/118_summary_seed_context__07_fixture_battery_gate.a1.validate new file mode 100644 index 0000000..4876b9b --- /dev/null +++ b/.agents/reports/118_summary_seed_context/118_summary_seed_context__07_fixture_battery_gate.a1.validate @@ -0,0 +1,103 @@ +........................................................................ [ 2%] +........................................................................ [ 5%] +........................................................................ [ 8%] +........................................................................ [ 11%] +........................................................................ [ 14%] +........................................................................ [ 17%] +........................................................................ [ 20%] +........................................................................ [ 22%] +........................................................................ [ 25%] +........................................................................ [ 28%] +........................................................................ [ 31%] +........................................................................ [ 34%] +........................................................................ [ 37%] +........................................................................ [ 40%] +........................................................................ [ 43%] +........................................................................ [ 45%] +........................................................................ [ 48%] +........................................................................ [ 51%] +........................................................................ [ 54%] +........................................................................ [ 57%] +........................................................................ [ 60%] +........................................................................ [ 63%] +........................................................................ [ 66%] +........................................................................ [ 68%] +........................................................................ [ 71%] +........................................................................ [ 74%] +........................................................................ [ 77%] +........................................................................ [ 80%] +........................................................................ [ 83%] +........................................................................ [ 86%] +........................................................................ [ 89%] +........................................................................ [ 91%] +........................................................................ [ 94%] +........................................................................ [ 97%] +.......................................................... [100%] +=============================== warnings summary =============================== +.venv/lib/python3.13/site-packages/fastapi/testclient.py:1 + /var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead. + from starlette.testclient import TestClient as TestClient # noqa + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +================================ tests coverage ================================ +_______________ coverage: platform linux, python 3.13.13-final-0 _______________ + +Name Stmts Miss Cover +-------------------------------------------------- +app/__init__.py 1 0 100% +app/api/__init__.py 0 0 100% +app/api/auth.py 52 0 100% +app/api/chat.py 225 1 99% +app/api/chats.py 110 0 100% +app/api/config.py 13 0 100% +app/api/doc_drafts.py 99 0 100% +app/api/docs.py 156 1 99% +app/api/git_sources.py 232 0 100% +app/api/health.py 10 0 100% +app/api/steering.py 42 0 100% +app/api/suggestions.py 33 0 100% +app/api/sync.py 139 0 100% +app/api/tokens.py 40 0 100% +app/api/ui_settings.py 55 0 100% +app/config.py 224 0 100% +app/core/__init__.py 0 0 100% +app/core/auth.py 45 0 100% +app/core/caching.py 124 0 100% +app/core/debugging.py 29 2 93% +app/core/docs_push.py 39 0 100% +app/core/errors.py 5 0 100% +app/core/logging.py 13 0 100% +app/core/rate_limit.py 44 0 100% +app/core/security_headers.py 20 0 100% +app/core/theming.py 38 0 100% +app/core/tokens.py 44 0 100% +app/db.py 22 0 100% +app/main.py 66 0 100% +app/models.py 128 0 100% +app/rag/__init__.py 0 0 100% +app/rag/agent.py 317 1 99% +app/rag/archive_upload.py 134 0 100% +app/rag/chunker.py 206 4 98% +app/rag/doc_dates.py 18 0 100% +app/rag/folder_summaries.py 123 0 100% +app/rag/git_sources.py 14 0 100% +app/rag/importer.py 219 3 99% +app/rag/llm.py 244 1 99% +app/rag/overview.py 71 0 100% +app/rag/prompts.py 100 0 100% +app/rag/retriever.py 217 3 99% +app/rag/scaffolding.py 55 0 100% +app/rag/source_removal.py 41 0 100% +app/rag/sources_meta.py 16 0 100% +app/rag/suggestions.py 27 0 100% +app/rag/summarizer.py 24 0 100% +app/schemas.py 329 0 100% +-------------------------------------------------- +TOTAL 4203 16 99% +coverage gate: app/ 99% (>90%) OK +All checks passed! +0 errors, 0 warnings, 0 informations +WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414). +Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest` + +validation OK diff --git a/.env.example b/.env.example index d6ebd65..ca0a2df 100644 --- a/.env.example +++ b/.env.example @@ -38,6 +38,8 @@ BOR_RELEVANCE_THRESHOLD=0.62 # answer when best cosine >= this OR an FTS hi # BOR_LEXICAL_SUPPORT_FLOOR=0.35 # cosine floor for FTS hits to flip HIGH (A8 revised 2026-09-14); 0 <= floor <= relevance_threshold # BOR_SOURCE_USEFULNESS_FLOOR=0.35 # cosine bar for a citation slot (phase 113): a doc earns done.sources only when its best-chunk cosine clears it; 0 <= floor <= relevance_threshold; 0 = no bar # BOR_RELATED_MAX_DOCS=2 # cap on the related-doc tier in the done frame (phase 113: the UI's de-emphasized "nearby docs" row, never a citation chip); 0 = no related docs +# BOR_SUGGESTED_DOCS=5 # cap on the "start here" suggestion tier (phase 118): top-N doc summaries seeded into the grounded prompt; NO cosine floor (opt-in starting points, not citations); >= 1 +# BOR_SUGGESTION_PREVIEW_CHARS=400 # NULL-summary fallback preview in a suggestion block (phase 118): first N content chars + the truncation marker; > 0 BOR_MAX_OUTPUT_TOKENS=32768 # max answer length in tokens (answers must not be cut off) BOR_STEERING_MAX_CHARS=8000 # char budget for the (steering notes) prompt section BOR_SUMMARY_MAX_CHARS=12000 # cap on document content sent to the lite summary model (phase 30) diff --git a/TOOL_CALLING_TESTING.md b/TOOL_CALLING_TESTING.md index 07a821d..6cc240c 100644 --- a/TOOL_CALLING_TESTING.md +++ b/TOOL_CALLING_TESTING.md @@ -589,3 +589,136 @@ listing answers to reach the file level by default, the lever is the `ls` description / `TOOLS_SECTION` copy (teach that a folder's file lines appear only after drilling into it) — flagged here rather than acted on unilaterally. + +--- + +## 10. Phase 118 — summary-seed gate, 2026-09-15 (run 2026-09-16) + +Phase 118 re-worked the grounded-turn seed: the `` section +no longer carries the **full text** of the top-2 cited documents — it +carries the **summaries** of the top 5 ranked documents (the +"start here if these summaries seem right to you" tier), and a +document's full text enters the context only through the capped `read` +tool. The fixture battery had to be re-run against the real configured +chat model under the re-cut gate semantics (locked A7, owner-confirmed +2026-09-15) before the phase could complete — the phase-70/72/94 +tool-copy gate precedent. + +**Gate semantics changed (locked A7).** The phase gate is now +condition **1** (all turns answer) AND condition **2** (zero round-cap +hits) AND condition **4** (fixture-mode contract accuracy ≥ 0.90). +Condition **3** (≥6/10 turns emit ≥1 tool call) is **REPORTED, not +gated** — a summary-only answer is now the *intended* fast path, not +tool abandonment. The gate script still gates condition 3 on a full +(non-`--turns`) run and exits 1 if it is the only miss; the phase +verdict reads the four printed conditions under A7, so a run that +misses **only** condition 3 with 1/2/4 green is a **PASS** (recorded +as such, below). The full telemetry is recorded regardless. + +**Fixture dump rebuild (a legitimate re-run trigger).** The import +pipeline's summary policy changed (phase 118, task 01 — every document +gets a stored summary + one embedded `is_summary` chunk, markdown +included), so the dump was rebuilt with +`uv run python -m scripts.load_test_kb` (the real pipeline). All eight +hand-written fixture documents are markdown and each now carries a +`doc.summary` + one `is_summary` chunk: **8 documents, 17 chunks** +(9 content + 8 summary). The build verified +`summary_errors=0`, zero failed folder-summary batches, and an +all-GROUNDED retrieval report (the honesty gate is untouched by this +phase — grounded/deflected is unchanged). The dump is tracked and +committed with the phase. + +The rebuild also surfaced and fixed a latent dump bug: the +`_TABLES` column lists in `scripts/load_test_kb.py` predated three +migrations, so the snapshot omitted `documents.created_at` / +`documents.created_at_manual` (phase 106), +`git_sources.ignore_paths` / `git_sources.include_hidden`, and +`folder_summaries.manually_edited` (phase 97). Restoring such a dump +silently re-defaulted `created_at` to `now()`, so the build's own +round-trip checksum verification FAILED on `documents`. The column +lists now cover every column of each table, and the round-trip +verification passes. + +**The verdict run.** `uv run python -m scripts.agent_realmodel_check +--restore --mode fixture` — the configured chat model (`turbo` per +`.env`), the refreshed fixture KB, the real grounded path, the full +10-question battery (the verdict is always the full battery — +`--turns N` is the copy-iteration micro-loop only): + +``` +$ uv run python -m scripts.agent_realmodel_check --restore --mode fixture +restore: ok in 0.04s (8 docs, 2 sources) +turn 01 | emitted=1 executed=1 cap=no defl=no | 9.49s | List the files in this directory. +turn 02 | emitted=4 executed=4 cap=no defl=no | 15.22s | List the documents you have in the … +turn 03 | emitted=8 executed=8 cap=no defl=no | 24.16s | List every document you have indexed. +turn 04 | emitted=1 executed=1 cap=no defl=no | 12.55s | Open the document … +turn 05 | emitted=1 executed=1 cap=no defl=no | 10.45s | Read … +turn 06 | emitted=1 executed=1 cap=no defl=no | 7.99s | Open the document … +turn 07 | emitted=1 executed=1 cap=no defl=no | 6.63s | Find the exact string "rbm-8842" in … +turn 08 | emitted=0 executed=0 cap=no defl=no | 9.92s | Which document has the title "Lab … +turn 09 | emitted=1 executed=1 cap=no defl=no | 10.57s | What do you know about the qwen 3.8 … +turn 10 | emitted=3 executed=3 cap=no defl=no | 11.00s | List the files in the deployments … +gate: turbo PASS turns=10 answered=10 caps=0 tool-turns=9 calls 21/21 executed (100%) contract 21/21 (100%) 2026-09-16 (wall 118.1s) +``` + +**The four conditions (locked A7) and metrics.** + +| condition | gated under A7 | result | +|---|---|---| +| 1. all turns answer | yes | 10/10 answered — **GREEN** | +| 2. zero round-cap hits | yes | caps=0 — **GREEN** | +| 3. ≥6/10 turns emit ≥1 tool call | **no (reported)** | 9/10 tool-turns | +| 4. contract accuracy ≥ 0.90 | yes | 21/21 (100%) — **GREEN** | +| executed / emitted (reported) | no | 21/21 (100%) | + +Contract line: **contract 21/21 (100%)**. Wall time: **118.1 s**. +Model: **turbo** (the configured chat model). + +**Per-turn reading.** The three designed read turns (04/05/06) each +emitted exactly one contract-correct `read` in the combined +`source/path` form — and each **executed** (the seeded document is now +a *summary*, so `read` adds its full text rather than being +dedupe-refused: 100 % executed, not the ~58–73 % the full-text seed +produced). The discipline turns behave exactly as the new contract +wants: turn 08 (title lookup, target seeded as a summary) answered +straight from the `` context with **zero** tool calls, and +turn 09 (the qwen launch arguments) answered from the seeded summary +context. The listing turns drill the tree as under phase 94 (turn 03: +top level + both sources + the leaf folders in 8 calls). Zero cap +hits, zero contract violations, 10/10 answered. + +**Baseline comparison.** Against the phase-94 `turbo` baseline +(98.7 s wall, 100 % contract, 100 % executed on 19 calls): contract +and executed are unchanged at 100 %, caps remain 0, and the wall time +is 118.1 s (+19.7 %) — inside the ~20 % band the phase-94 gate used +as its slowdown tripwire, and within the normal endpoint-load +variance of the 97–135 s `turbo` range recorded in §3. The gate is +met on the conditions that gate (1/2/4 all GREEN) and condition 3 is +reported (9/10). + +**`read` copy — final shape (no copy iteration was needed).** +Condition 4 did not degrade and condition 2 was never hit, so no +`--turns` micro-loop iteration was required. The final copy is the +task-04 rewrite, byte-pinned in `tests/unit/test_prompt_lock.py` / +`test_agent.py`: the `read` description in `AGENT_TOOLS` +(`app/rag/agent.py`) teaches that `` shows the *summaries* +of the top-ranked documents (their full texts are NOT in the prompt +yet) and that `read` adds one of them — or any other document — by +its combined `source/path`, and must not re-read an already-read +document (that refusal's copy stays accurate: "the full text is +already in your prompt"); the `read` clause of `TOOLS_SECTION` +(`app/rag/prompts.py`) mirrors it ("The `` section holds +SUMMARIES — the full text of a suggested document is not in your +prompt until you `read` it"). `classify_call` needed **no** +re-baselining: a `read` of a suggested document is a well-formed call +targeting a resolvable document — contract-correct under both the old +and the new app semantics (its docstring's `ALREADY_IN_CONTEXT` note +already excludes dedupe re-reads from the metric). + +**Conclusion.** The phase-118 summary-seed gate is **PASS**: conditions +1, 2, and 4 GREEN (condition 3 reported at 9/10, not gated), contract +accuracy 100 %, zero cap hits, the full battery all-answered, wall +within the ~20 % band. The summary-seed behavior is proven against the +real configured chat model — full text enters the context only through +the capped `read` tool, and a summary-only answer is the intended fast +path. diff --git a/app/api/chat.py b/app/api/chat.py index 84ced78..313a378 100644 --- a/app/api/chat.py +++ b/app/api/chat.py @@ -37,13 +37,30 @@ Steering (phase 15): the owner's stored tuning notes are loaded per turn section — both the HIGH and the LOW prompt carry it. The per-turn log line records ``tuning=N`` (the number of injected notes). -Summaries (phase 30): a lite-model summary chunk's parent *is* the -source document, so a summary hit resolves to the full source document -through the unchanged chunk→document mapping (A7 revised) — context -assembly is untouched. ``TurnPlan.summary_hits`` counts the hit chunks -with ``is_summary`` whose parent document landed in the selected -top-N context, and the per-turn log line records ``summary_hits=N`` -after ``fts_hits`` (PLAN §9 line extension). +Summaries (phase 30; phase 118 redefinition): a lite-model summary +chunk's parent *is* the source document, so a summary hit resolves to +the full source document through the unchanged chunk→document mapping +(A7 revised). ``TurnPlan.summary_hits`` counts the hit chunks with +``is_summary`` whose parent document is in the SUGGESTED set (phase 118: +redefined from the phase-113 cited set — the suggestion tier is the +seeded context now), and the per-turn log line records ``summary_hits=N`` +after ``fts_hits`` (PLAN §9 line extension; phase 118 adds +``suggested=N`` after ``summary_hits=N``). + +Summary seed context (phase 118, TODO L3 — the owner directive that +re-revises A7, LOCKED A6): a grounded turn's ```` section +seeds the top-5 suggested documents' SUMMARIES (never their full +texts) — the "start here if these summaries seem right to you" starting +point — and the LLM extends its context by ``read``-ing what it needs +(the capped ``read`` tool is the ONLY full-text path). Both tiers are +computed once per turn, for BOTH branches: ``suggested_docs`` (no +floor, A3) and ``related_docs`` (rank 6+, the done frame's row). +``done.sources`` is the citation surface — suggested + agent-read, +deduped (A4: a grounded turn always shows chips); ``query_log.sources`` +and the per-turn log line keep recording the full retrieval +(suggested + related + read, LOCKED A3). The deflected branch's prompt +is byte-identical (weak-hit titles only — A8 untouched); its TurnPlan +still carries both tiers for the durable record. KB overview (phase 31): the lite-generated outline of the knowledge base (single ``kb_overview`` row) is read per turn (one indexed PK @@ -195,7 +212,13 @@ from app.rag.llm import ( ) from app.rag.overview import load_kb_overview from app.rag.prompts import build_deflect_prompt, build_high_prompt, history_to_messages -from app.rag.retriever import RetrievedChunk, retrieve, select_documents_tiered, weak_hit_titles +from app.rag.retriever import ( + RetrievedChunk, + retrieve, + select_related, + select_suggested, + weak_hit_titles, +) from app.rag.scaffolding import ScaffoldingFilter # phase 71: the streaming filter from app.rag.suggestions import derive_suggestions from app.schemas import ( @@ -258,12 +281,19 @@ class TurnPlan: fts_hits: int # lexical (OR-tsquery) candidates matched deflected: bool system_prompt: str - docs: list[Document] # cited sources (phase 113: the bar-clearing tier) - related_docs: list[Document] = field(default_factory=list) # phase 113 + # The summary-seeded suggestion tier (phase 118, A6: top-N distinct + # documents, NO floor, A3 — the HIGH prompt carries their SUMMARIES; + # the done frame's citation surface + run_agent's seed_docs, A4). + suggested_docs: list[Document] + # Rank 6+ after the suggested set (phase 118: the next ranked docs + # that are not already suggested, at most related_max_docs — the + # done frame's de-emphasized "nearby docs" row, never a citation). + related_docs: list[Document] = field(default_factory=list) suggestions: list[str] = field(default_factory=list) # "Maybe try" chips (deflected turns only) tuning_count: int = 0 # steering notes injected into the system prompt - #: Hit chunks with ``is_summary`` whose parent document made it into - #: *docs* (phase 30; per-turn log line ``summary_hits=N``). + #: Hit chunks with ``is_summary`` whose parent document is in the + #: SUGGESTED set (phase 30; redefined from the phase-113 cited set + #: in phase 118; per-turn log line ``summary_hits=N``). summary_hits: int = 0 #: Length of the stored KB overview injected as the #: ```` section (phase 31; per-turn log line @@ -281,7 +311,8 @@ def plan_turn( * **HIGH (grounded)** when ``best_cosine >= threshold`` **or** (``fts_hits > 0`` **and** ``best_cosine >= lexical_support_floor``): - HIGH prompt with the full top-N documents, no suggestions. A cosine + HIGH prompt seeded with the suggested documents' SUMMARIES (phase + 118, A6 — never their full texts), no suggestions. A cosine exactly at the threshold is an answer — the gate is strict (``< threshold``). An FTS hit alone, without vector corroboration (cosine < lexical_support_floor), stays LOW (A8 revised 2026-09-14). @@ -305,42 +336,52 @@ def plan_turn( prompts are byte-identical to the pre-phase text and ``kb_chars`` is 0. - ``summary_hits`` (phase 30) counts the hit chunks with - ``is_summary`` whose parent document is among the selected - top-N documents — both the HIGH and the LOW branch record it. + ``summary_hits`` (phase 30; phase 118 redefinition) counts the hit + chunks with ``is_summary`` whose parent document is in the + SUGGESTED set — both the HIGH and the LOW branch record it. - Phase 113 (the usefulness bar, LOCKED A2): retrieval documents are - tiered before either branch — ``docs`` (cited) are the distinct - parent documents whose best hit-chunk cosine clears - ``settings.source_usefulness_floor`` (at most ``top_n_docs`` — the - ceiling, never a quota); ``related_docs`` are the next ranked - documents (at most ``related_max_docs``) that did not earn a cited - slot. On a deflected turn the weak hits fall to ``related_docs`` - (the cited tier is usually empty — nothing below the bar earned a - citation slot); the LOW prompt itself is unchanged (weak-hit titles - only). + Phase 118 (the summary seeding, LOCKED A3/A6): retrieval documents + are tiered once, before either branch — ``suggested_docs`` are the + top-N distinct parent documents in fused rank order with NO cosine + floor (``settings.suggested_docs``, default 5 — the "start here" + seeding: the HIGH prompt carries their SUMMARIES, never their full + texts; the floor never filters, so a lexical-only hit is suggested + when it ranks) and ``related_docs`` are the next ranked documents + that are not already suggested (rank 6+ for the contiguous top-5 + suggestion set, at most ``related_max_docs`` — the done frame's + de-emphasized "nearby docs" row). On a deflected turn the weak hits + are suggested too (no floor); the LOW prompt itself is unchanged + (weak-hit titles only), and both tiers still ride the TurnPlan for + the durable record (LOCKED A3: query_log records retrieval, not + citations). + + ``settings.top_n_docs`` and ``settings.source_usefulness_floor`` are + NOT consulted here (phase 118 retired their seeding role, A6) — + they remain settings for env back-compat only. """ steering = list(notes or []) kb_text = (kb_overview or "").strip() kb_chars = len(kb_text) best_cosine = max((c.cosine for c in chunks), default=0.0) fts_hits = sum(1 for c in chunks if c.fts_hit) - docs, related_docs = select_documents_tiered( - chunks, - n=settings.top_n_docs, - floor=settings.source_usefulness_floor, - related_cap=settings.related_max_docs, + # Phase 118 (A3/A6): BOTH tiers, computed once, for BOTH branches — + # the suggested tier (top-N, NO floor) seeds the HIGH prompt and the + # agent; the related tier (rank 6+ after the suggested set) feeds + # the done frame's row and the durable record. + suggested = select_suggested(chunks, n=settings.suggested_docs) + related_docs = select_related( + chunks, {d.id for d in suggested}, settings.related_max_docs ) - selected_ids = {d.id for d in docs} - summary_hits = sum(1 for c in chunks if c.is_summary and c.document.id in selected_ids) + suggested_ids = {d.id for d in suggested} + summary_hits = sum(1 for c in chunks if c.is_summary and c.document.id in suggested_ids) lexical_supported = fts_hits > 0 and best_cosine >= settings.lexical_support_floor if best_cosine >= settings.relevance_threshold or lexical_supported: return TurnPlan( best_cosine, fts_hits, False, - build_high_prompt(docs, notes=steering, kb_overview=kb_text), - docs, + build_high_prompt(suggested, notes=steering, kb_overview=kb_text), + suggested, related_docs, [], len(steering), @@ -353,7 +394,7 @@ def plan_turn( fts_hits, True, build_deflect_prompt(titles, notes=steering, kb_overview=kb_text), - docs, + suggested, related_docs, derive_suggestions(titles, settings.suggestions), len(steering), @@ -606,7 +647,7 @@ async def chat( db_factory, # SEC-14-04: session factory, not a long-lived session system_prompt=plan.system_prompt, user_message=request.message, - seed_docs=plan.docs, + seed_docs=plan.suggested_docs, # phase 118 (A4): the suggestion tier settings=settings, holder=holder, history=hist, # phase 74: the same trimmed prior turns @@ -804,30 +845,33 @@ async def chat( # 4. Durable record + required per-turn log line (PLAN §9). # Phase 37: the agent's read documents join the # retrieval's — deduped by (source, path), order - # preserved (the read doc stays last). Phase 113: - # the retrieval now arrives in two tiers — the cited - # docs (``plan.docs``) and the related docs - # (``plan.related_docs``, the scored-but-below-the-bar - # documents). The DURABLE record keeps the full + # preserved (a read doc not already suggested stays + # last). Phase 118: the retrieval arrives in two + # tiers — the suggested docs + # (``plan.suggested_docs``, the summary-seeded + # "start here" tier, no floor, A3) and the related + # docs (``plan.related_docs``, rank 6+ after the + # suggested set). The DURABLE record keeps the full # retrieval (LOCKED A3: query_log records retrieval, # not citations — even on deflected turns, where the - # weak hits live in the related tier). Phase 112: + # weak hits are suggested). Phase 112/118: # done.sources is the CITATION surface — it carries the - # cited docs + the agent-read docs on grounded turns - # and [] on deflected ones (a deflected answer cites - # nothing; the weak hits stay in the durable record). + # suggested docs + the agent-read docs (deduped, + # LOCKED A4) on grounded turns and [] on deflected + # ones (a deflected answer cites nothing; the weak + # hits stay in the durable record). # A cancelled turn (the generator closed by the # consumer) never reaches this step — no query_log row. cited_docs: list[Document] = [] cited_seen: set[tuple[str, str]] = set() - for doc in [*plan.docs, *holder.read_docs]: + for doc in [*plan.suggested_docs, *holder.read_docs]: key = (doc.source, doc.path) if key not in cited_seen: cited_seen.add(key) cited_docs.append(doc) record_docs: list[Document] = [] seen: set[tuple[str, str]] = set() - for doc in [*plan.docs, *plan.related_docs, *holder.read_docs]: + for doc in [*plan.suggested_docs, *plan.related_docs, *holder.read_docs]: key = (doc.source, doc.path) if key not in seen: seen.add(key) @@ -852,7 +896,8 @@ async def chat( logger.exception("chat: failed to write query_log question=%r", request.message) logger.info( - "question=%r embed_ms=%d top_score=%.3f fts_hits=%d summary_hits=%d tuning=%d " + "question=%r embed_ms=%d top_score=%.3f fts_hits=%d summary_hits=%d " + "suggested=%d tuning=%d " "kb_chars=%d history_msgs=%d threshold=%.2f deflected=%s sources=%r " "thinking_chars=%d tool_calls=%d total_ms=%d retries=%d scaffold_stripped=%d", request.message, @@ -860,6 +905,7 @@ async def chat( plan.top_score, plan.fts_hits, plan.summary_hits, + len(plan.suggested_docs), # phase 118: the seeded suggestion tier size plan.tuning_count, plan.kb_chars, len(hist), @@ -879,14 +925,15 @@ async def chat( # citations). The retrieval stays durably recorded above # (query_log.sources + the log line — observability # unchanged). - # Phase 113 (A2/A4): done.related carries the related - # tier — the scored documents that did not clear the - # usefulness bar (deduped against the cited list, the - # same (source, path) pattern as cited_docs: an agent- - # read related doc is a citation, never a "nearby doc" - # — and capped by related_max_docs in the tiering). The - # UI renders it as the de-emphasized related-docs row, - # never a citation chip; old clients ignore the field. + # Phase 118 (A3/A4): done.related carries the related + # tier — the ranked documents beyond the suggested set + # (rank 6+ after the contiguous top-N suggestion, capped + # by related_max_docs in the tiering), deduped against + # the cited list, the same (source, path) pattern as + # cited_docs: an agent-read related doc is a citation, + # never a "nearby doc". The UI renders it as the + # de-emphasized related-docs row, never a citation chip; + # old clients ignore the field. cited_refs: list[SourceRef] = [] if not plan.deflected: cited_refs = [ diff --git a/app/config.py b/app/config.py index afa3b8e..762eed7 100644 --- a/app/config.py +++ b/app/config.py @@ -111,6 +111,9 @@ class Settings(BaseSettings): # --- RAG tuning --- embedding_dim: int = 768 # verified against aipi /v1 (embed model) + #: Phase 118 retired the full-text seeding role (A6); the suggested + #: tier (``select_suggested``) seeds the prompt now — kept for env + #: back-compat (no ``app/`` consumer left). top_n_docs: int = 2 # Honesty gate (A8, re-tuned 2026-08-21): the ``embed`` model's cosine # scores compress into 0.41–0.84 on the real corpus, so the old 0.30 @@ -126,6 +129,9 @@ class Settings(BaseSettings): # ``relevance_threshold`` (a floor above the threshold is a typo that # would make every FTS hit require a HIGH cosine anyway). lexical_support_floor: float = 0.35 + #: Phase 118 retired the full-text seeding role (A6); the suggested + #: tier (``select_suggested``) seeds the prompt now (no floor, A3) — + #: kept for env back-compat (no ``app/`` consumer left). #: Usefulness bar for the citation slot (phase 113, LOCKED A2): a #: retrieved document earns ``done.sources`` (the UI's citation chip) #: only when the **cosine** of its best hit chunk clears this floor — @@ -149,6 +155,26 @@ class Settings(BaseSettings): #: docs at all (the kill switch); a negative value fails startup #: loudly (the ``agent_max_rounds`` pattern). related_max_docs: int = 2 + #: Cap on the "start here" suggestion tier (phase 118, LOCKED A3 — + #: the owner directive, TODO L3): a grounded turn seeds the top-N + #: related documents into the prompt as SUMMARY blocks (opt-in + #: starting points, never citations) and the LLM extends its context + #: by reading only what it needs. NO cosine floor applies — unlike + #: the cited tier's ``source_usefulness_floor``, a lexical-only hit + #: (cosine 0.0) is a valid starting point when it ranks. Default 5; + #: tunable via ``BOR_SUGGESTED_DOCS``. A value below 1 is a typo — + #: the validator fails startup loudly (the ``agent_max_rounds`` + #: pattern). + suggested_docs: int = 5 + #: Preview cap for a suggestion block whose document summary is missing + #: (phase 118, task 03, LOCKED A5): a NULL/blank ``doc.summary`` (a + #: fail-soft import miss) falls back to the first ``suggestion_preview_chars`` + #: characters of the document content plus the shared + #: ``[…truncated…]`` marker — deterministic, no LLM call at chat time. + #: Tolerates content at or under the cap whole (no marker — nothing was + #: cut). ``0``/negative is a typo (empty preview) — the validator fails + #: startup loudly (the ``agent_max_rounds`` pattern). + suggestion_preview_chars: int = 400 #: Maximum output tokens a chat answer may use (owner instruction #: 2026-08-22: answers must run to their natural end — the old hard #: 700-token cap cut long answers off mid-sentence). @@ -387,6 +413,25 @@ class Settings(BaseSettings): raise ValueError("related_max_docs must be >= 0 (0 = no related docs)") return v + @field_validator("suggested_docs") + @classmethod + def _suggested_docs_at_least_one(cls, v: int) -> int: + """The suggestion tier always seeds at least one summary block — + ``0`` (no starting points) and negatives are typos (the + ``agent_max_rounds`` pattern, phase 118).""" + if v < 1: + raise ValueError("suggested_docs must be >= 1") + return v + + @field_validator("suggestion_preview_chars") + @classmethod + def _suggestion_preview_chars_positive(cls, v: int) -> int: + """``0``/negative would preview an empty/absent prefix — fail loud at + startup (the ``agent_max_rounds`` pattern, phase 118).""" + if v <= 0: + raise ValueError("suggestion_preview_chars must be > 0 (chars)") + return v + @field_validator("import_extensions") @classmethod def _import_extensions_known(cls, v: str) -> str: diff --git a/app/models.py b/app/models.py index 14c1ce9..aa1a4f7 100644 --- a/app/models.py +++ b/app/models.py @@ -133,10 +133,11 @@ class Document(Base): Boolean, default=False, server_default=text("false"), nullable=False ) #: Lite-model summary, phase 30. Natural-language summary of the - #: document (non-markdown A9 docs only, generated at import time by the - #: aipi ``lite`` model). NULL for markdown docs, pre-phase-30 rows, and - #: the fail-soft path where summary generation failed but the document - #: was still indexed. + #: document (every A9 doc — markdown included since phase 118, A2 — + #: generated at import time by the aipi ``lite`` model). NULL for + #: pre-phase-30 rows, the fail-soft path where summary generation + #: failed, and until the phase-118 backfill stores one on the next + #: sync. summary: Mapped[str | None] = mapped_column(Text, default=None) chunks: Mapped[list[Chunk]] = relationship( diff --git a/app/rag/agent.py b/app/rag/agent.py index e02ea79..074f4bf 100644 --- a/app/rag/agent.py +++ b/app/rag/agent.py @@ -97,12 +97,13 @@ task 04): context; ``grep`` — which searches the whole document — is the follow-up), with the truncation recorded on the holder so the loop yields a :class:`app.rag.llm.ToolResultPiece` (task 02 → the SSE - ``tool_result`` frame + UI marker). **A7 scope clarification:** the - never-truncated contract is for the retrieval ```` path - (the top-2 seed documents stay whole — "this should never happen"); - the ``read`` TOOL path is the only capped read, per the owner's - explicit request — the two paths are distinct (retrieval seeds vs. - agent-requested additions). And ``grep`` greps the + ``tool_result`` frame + UI marker). **A6 re-revised contract + (phase 118, owner directive 2026-09-15):** the retrieval + ```` path seeds SUMMARIES only — a suggested document's + full text never enters the prompt on the retrieval path; full text + enters the context ONLY through this ``read`` TOOL path, which is + the only capped read (the phase-95 cap unchanged). And ``grep`` + greps the indexed documents (or the one document a combined ``source/path`` names) for a case-insensitive fixed substring and returns up to 20 ``source/path:line: text`` match lines (owner-locked A5, phase 68), @@ -138,7 +139,9 @@ task 04): :data:`NOT_A_FOLDER`, the drill-down teaching with the argument echoed and the deepest existing ancestor's direct subfolders listed, so the model self-corrects in the next round; a document - already in context (seed or previously read) → + already READ into full-text context (phase 118: the suggested + seeds are summary blocks in the prompt, not full text — only an + already-read document is refused) → :data:`ALREADY_IN_CONTEXT` (phase 72, task 05 gate iteration: the line names the correct action — answer from the text already in the prompt, do not call read again — so a fired refusal ends @@ -301,19 +304,21 @@ AGENT_TOOLS: list[dict[str, Any]] = [ "function": { "name": "read", "description": ( - "Do not call this tool for a document already shown in " - "the section, even when the user asks you to " - "open or read it — its full text is already in your " - "prompt; answer directly from it. Use it only to add a " - "document NOT already in to your context, " - "by its combined `source/path` string. Very large " - "documents are truncated: you receive the first part " - "plus a TRUNCATED notice naming how many more characters " - "exist — the notice is authoritative, the document did " - "NOT end where it stopped. Follow it and use `grep` " - "(pattern) to locate the rest — it searches the whole " - "document. Call one tool at a time — wait for this " - "result before your next call." + "The section shows the SUMMARIES of the " + "top-ranked documents — their full texts are NOT in " + "your prompt yet. Use this tool to add one of them (or " + "any other document) to your context, by its combined " + "`source/path` string, exactly as shown in the `ls` " + "output or the blocks. Do not re-read a " + "document you have already read — its full text is " + "already in your prompt. Very large documents are " + "truncated: you receive the first part plus a TRUNCATED " + "notice naming how many more characters exist — the " + "notice is authoritative, the document did NOT end " + "where it stopped. Follow it and use `grep` (pattern) " + "to locate the rest — it searches the whole document. " + "Call one tool at a time — wait for this result before " + "your next call." ), "parameters": { "type": "object", @@ -326,10 +331,9 @@ AGENT_TOOLS: list[dict[str, Any]] = [ "shown in the `ls` output (e.g. " "'homelab/active/container_caddy/caddy.md'). " "A bare document path (without the source " - "name) will not resolve. Only pass a document " - "NOT already shown in the " - "section — it is already in your context; do " - "not re-read it." + "name) will not resolve. Do not re-read a " + "document you have already read — its full " + "text is already in your prompt." ), } }, @@ -1152,10 +1156,15 @@ def _execute_tool( arg = raw_path.strip() if isinstance(raw_path, str) else "" if not arg: return MISSING_READ_ARGS - known = {(doc.source, doc.path) for doc in (*seed_docs, *holder.read_docs)} + # Phase 118 (A6): the dedupe set is ``holder.read_docs`` ONLY — + # the ``seed_docs`` are SUMMARY blocks in the prompt, not full + # text, so a FIRST read of a suggested document adds its full + # text through the path below; only a document ALREADY READ is + # refused. + known = {(doc.source, doc.path) for doc in holder.read_docs} # The dedupe check needs no DB: the split pair of a combined - # identity that is in context is in `known` as-is (the resolve - # below would find the same document). + # identity that is already in full-text context is in `known` + # as-is (the resolve below would find the same document). if "/" in arg: src, _, p = arg.partition("/") if (src, p) in known: @@ -1326,11 +1335,14 @@ async def run_agent( the capture mechanism for new registry entries; *holder* accumulates the turn's ``scaffold_stripped`` total for the API layer's log line. - ``seed_docs`` are the documents the retrieval already put in context - (they shape the *system_prompt* the caller built); re-reading one of - them is rejected with :data:`ALREADY_IN_CONTEXT` (the phase-72 - teaching line — answer from the text already in the prompt) — the - rejection counts in nothing, but it still consumes a round. + ``seed_docs`` are the suggested documents whose SUMMARY blocks the + caller put in the *system_prompt* (phase 118: the retrieval seeds + summaries, never full texts); reading one of them ADDS its full + text to the context through the ordinary ``read`` path — only a + document ALREADY READ is rejected with :data:`ALREADY_IN_CONTEXT` + (the phase-72 teaching line — answer from the text already in the + prompt); the rejection counts in nothing, but it still consumes a + round. DB sessions (SEC-14-04): *db_factory* is a callable that returns a new :class:`sqlalchemy.orm.Session` (e.g. ``lambda: SessionLocal()``). diff --git a/app/rag/importer.py b/app/rag/importer.py index b2ddc2d..11992d0 100644 --- a/app/rag/importer.py +++ b/app/rag/importer.py @@ -10,7 +10,8 @@ the two-phase upsert: 2. embed the new chunks in batches and attach the vectors 3. commit — one transaction per file, so a failed embedding leaves the database untouched and the file is simply retried on the next run -4. non-markdown files only (phase 30): generate a ``lite``-model summary +4. every file (phase 30; phase 118, A2: markdown included — the + non-markdown-only scope is retired): generate a ``lite``-model summary and, best-effort, store it on ``documents.summary`` plus one extra embedded chunk (``is_summary``, position −1). The document row and its content chunks are already committed at this point, so a summary @@ -42,6 +43,17 @@ guard) and counted in ``summary.dates_updated`` — unless the row carries the owner's manual correction (``created_at_manual``, D1), which the sync never touches. +NULL-summary backfill (phase 118, A2): an UNCHANGED file (same +``content_hash``) whose ``documents.summary`` is still NULL — a +pre-phase-30 row, or an earlier fail-soft miss — gets the same +best-effort summary pass on every sync until it sticks. A success counts +``summary_backfilled`` (never ``summaries``) and touches nothing else: no +content re-embed, no added/updated/pruned count — so no +``sources_meta`` bump, no KB-overview/folder-summary regeneration. The +backfill runs BEFORE the ``created_at_manual`` early-return (the manual +flag protects the DATE only, D1) and the strict ``is None`` check leaves +owner-set summaries (even empty strings, phase 57) alone. + ``import_sources`` accepts an optional per-file ``progress`` callback (phase 64, task 01) reporting the file being processed right now. """ @@ -99,12 +111,18 @@ class ImportSummary: errors: int = 0 chunks: int = 0 embed_batches: int = 0 - #: Non-markdown files whose lite summary was generated + indexed - #: (phase 30). One ``is_summary`` chunk per success. + #: Files whose lite summary was generated + indexed (phase 30; phase + #: 118, A2: every A9 format, markdown included). One ``is_summary`` + #: chunk per success. summaries: int = 0 - #: Non-markdown files whose summary generation failed (best-effort — - #: the document is still indexed, without a summary). + #: Files whose summary generation failed (best-effort — the document + #: is still indexed, without a summary). summary_errors: int = 0 + #: Unchanged docs whose NULL summary was backfilled (phase 118, A2) — + #: one ``is_summary`` chunk per success; the content is untouched, so + #: a backfill NEVER counts added/updated/pruned (no + #: ``sources_meta`` bump, no overview/folder-summary regeneration). + summary_backfilled: int = 0 #: Files whose ``created_at`` was refreshed on the UNCHANGED path — #: content untouched, date re-sourced (phase 106, D4: the date may #: go OLDER; a date-only refresh NEVER counts added/updated/pruned, @@ -125,7 +143,7 @@ class ImportSummary: logger.info( "import: summary files=%d added=%d updated=%d unchanged=%d pruned=%d " "errors=%d chunks=%d embed_batches=%d summaries=%d summary_errors=%d " - "dates_updated=%d formats=%s", + "summary_backfilled=%d dates_updated=%d formats=%s", self.files, self.added, self.updated, @@ -136,6 +154,7 @@ class ImportSummary: self.embed_batches, self.summaries, self.summary_errors, + self.summary_backfilled, self.dates_updated, self.format_counts(), ) @@ -442,6 +461,20 @@ async def _index_file( if doc is not None and doc.content_hash == digest: summary.unchanged += 1 logger.info("import: unchanged source=%s path=%s", source, rel) + # Phase 118 (A2): an unchanged doc whose summary is still NULL + # (a pre-phase-30 row, or an earlier fail-soft miss) gets a + # summary-only backfill — one ``is_summary`` chunk, no content + # re-embed, and NEVER an added/updated/pruned count (so no + # ``sources_meta`` bump, no overview/folder-summary + # regeneration). Strict ``is None``: an empty-string summary is + # owner-set (phase 57) and is never overwritten. BEFORE the + # manual-date early-return: ``created_at_manual`` protects the + # DATE only (phase 106, D1), not the summary. + if doc.summary is None: + await _store_summary( + session, doc=doc, source=source, rel=rel, content=content, + llm=llm, summary=summary, backfill=True, + ) if doc.created_at_manual: # D1/D4: the owner's correction survives the sync — no # write at all (the phase-97 ``manually_edited`` precedent). @@ -536,10 +569,11 @@ async def _index_file( summary.chunks += len(chunks_text) logger.info("import: %s source=%s path=%s chunks=%d", verb, source, rel, len(chunks_text)) - # Phase 30: markdown is already natural language, so only the other A9 - # formats (txt, yaml, yml, json, py) get a ``lite``-model summary. - if full_path.suffix.lower() in (".md", ".markdown"): - return + # Phase 30; phase 118 (A2, 2026-09-15): EVERY new/changed document + # gets a ``lite``-model summary — markdown included. Phase 30's + # "markdown is already natural language" exclusion is retired: the + # summary is the retrieval seed context (the phase-118 suggestion + # blocks), not a formatting convenience. await _store_summary( session, doc=doc, source=source, rel=rel, content=content, llm=llm, summary=summary ) @@ -554,6 +588,7 @@ async def _store_summary( content: str, llm: Embedder, summary: ImportSummary, + backfill: bool = False, ) -> None: """Best-effort ``lite`` summary for one already-committed document. @@ -569,6 +604,11 @@ async def _store_summary( :class:`EmbeddingError` only rolls back the summary rows — the file stays indexed, without a summary, and the failure is counted in ``summary_errors`` (PLAN phase 30). + + ``backfill`` (phase 118, A2): the unchanged-doc NULL-summary path — + a success counts ``summary_backfilled`` instead of ``summaries`` + (the doc content is untouched, so the import's KB-change signal must + not move); the rest of the mechanics are identical. """ try: text = await generate_summary(llm, source=source, path=rel, content=content) @@ -588,8 +628,18 @@ async def _store_summary( # ``expire_on_commit=False`` — reflects the committed state. doc.chunks.append(chunk) session.commit() - summary.summaries += 1 - logger.info("import: summary source=%s path=%s chars=%d", source, rel, len(text)) + if backfill: + # Phase 118 (A2): the backfill counts itself apart from fresh + # imports — the doc content is unchanged, so ``summaries`` + # (a KB-change signal) must not move. + summary.summary_backfilled += 1 + logger.info( + "import: summary-backfill source=%s path=%s chars=%d", + source, rel, len(text), + ) + else: + summary.summaries += 1 + logger.info("import: summary source=%s path=%s chars=%d", source, rel, len(text)) except (LLMError, EmbeddingError) as e: session.rollback() summary.summary_errors += 1 diff --git a/app/rag/llm.py b/app/rag/llm.py index 3a975b5..362dde7 100644 --- a/app/rag/llm.py +++ b/app/rag/llm.py @@ -1,8 +1,9 @@ """Async OpenAI-compatible client for the self-hosted aipi endpoint (PLAN A5). Provides the embeddings surface (importer, retrieval), one-shot chat -completions (phase 30: the ``lite`` model summarizes non-markdown -documents at import time), and chat streaming (PLAN A15) for the RAG +completions (phase 30: the ``lite`` model summarizes documents at import +time — every A9 format, markdown included since phase 118), and chat +streaming (PLAN A15) for the RAG pipeline. Chat streaming yields typed :class:`StreamPiece` values (phase 17) and — when the caller passes a ``tools`` list — :class:`ToolCallPiece` values (phase 37): aipi's ``turbo`` model streams diff --git a/app/rag/overview.py b/app/rag/overview.py index cca8ba9..cb552e4 100644 --- a/app/rag/overview.py +++ b/app/rag/overview.py @@ -100,8 +100,9 @@ def build_overview_prompt( * ``user`` — one line per document, ``source — path — title — {first line of summary}``, joined with newlines. The summary field is omitted when the document has no - summary (markdown docs and the fail-soft path — no dangling - dash). The list is capped at *max_chars* (default + summary (pre-phase-30 rows, the fail-soft path, and pre-backfill + NULL rows — no dangling dash). The list is capped at *max_chars* + (default ``BOR_OVERVIEW_INPUT_MAX_CHARS``): overflow is cut exactly at the cap and the shared ``[…truncated…]`` marker is appended on its own line, so the model never sees more than the cap and the cut is diff --git a/app/rag/prompts.py b/app/rag/prompts.py index a8bbfe4..54c3355 100644 --- a/app/rag/prompts.py +++ b/app/rag/prompts.py @@ -7,7 +7,14 @@ no mandated deflection opening; the honesty gate itself is unchanged.) Two modes: -* ``HIGH`` — grounded turn: full top-document texts under ````. +* ``HIGH`` — grounded turn: the top-ranked documents' SUMMARIES under + ```` (phase 118, LOCKED A6 re-revising A7 — the "start here" + suggestion seeding: each block is the document's stored summary, never + the full content; the LLM extends its context by ``read``-ing a + document's full text through the capped ``read`` tool, the ONLY full- + text path. A NULL/blank summary — a fail-soft import miss — falls back + to a ``suggestion_preview_chars`` content preview + the shared + ``[…truncated…]`` marker; no LLM call at chat time, LOCKED A5). * ``LOW`` — deflection turn: weak-hit *titles only* plus the ``DEFLECT_MODE`` marker (the E2E mock LLM keys on that marker). @@ -124,6 +131,24 @@ _KB_INTRO = ( "(generated at import time):\n" ) +#: The ```` section's start-here intro (phase 118, task 03 — +#: the owner directive, TODO L3): the seeded blocks are the SUMMARIES of +#: the top-ranked documents for the question, opt-in starting points — +#: start here if one seems right; ``read`` the document's combined +#: ``source/path`` to add its FULL content before answering specifics +#: (the full text is NOT in the prompt until you read it); cite the +#: document(s) you used, by path. Rendered BEFORE the first block, only +#: when at least one block is present. The ```` tag and the +#: per-block ```` markup stay byte-stable around it (the E2E +#: mock's block parser and the ``read``-tool copy key off both). +SUGGEST_INTRO = ( + "The blocks below are the summaries of the top-ranked documents for " + "your question — start here if one seems right to you: call `read` " + "with that document's combined `source/path` to add its full content " + "before answering specifics (its full text is not in the prompt until " + "you read it). Cite the document(s) you used, by path." +) + #: The ```` instructions section — **HIGH prompt only** (phase 37, #: task 03; phase 70: the copy is rewritten for the harness-aligned #: ``ls`` / ``read`` / ``grep`` shapes, names/args exactly as the @@ -158,12 +183,15 @@ _KB_INTRO = ( #: the user's "open it / read it" and ``read`` seed-context documents, #: then repeated the refused call; run 2: 8/18, 44% — the repeat is #: gone, but a grep turn batched two calls per reply and the harness -#: runs only the first of a batch): the do-not-read rule names the -#: user-command scenario (a document already in the ```` -#: section: do not call ``read``, answer from the text already in the -#: prompt — an anchor on the concrete ```` markup -#: was tried and REVERTED: it primed the model to latch the seed -#: documents' paths as ``ls`` scopes, regressing the incident turn); +#: runs only the first of a batch): the ``read`` clause carries the +#: phase-118 summary-seed contract (the ```` section holds +#: SUMMARIES — a suggested document's full text is not in the prompt +#: until ``read`` adds it; do not re-read a document you have already +#: read — answer from the text already in the prompt; if the user +#: asks to open or read a suggested document, ``read`` it — that is +#: the point of the section — the phase-72 do-not-read rule and the +#: reverted ```` anchor are retired with the +#: full-text seeds); #: the one-call rule names the consequence (a batched second call is #: discarded — runs only the first); the never-repeat rule says why #: (the refusal already told you the correct form); the ``grep`` clause @@ -171,10 +199,11 @@ _KB_INTRO = ( #: live runs showed the model scoping ``grep`` with an ``ls``-style #: source name — the incident shape, but on grep). The behavioral #: contract lives in the ``AGENT_TOOLS`` descriptions as well (the most -#: local text at call time): ``read`` must not be called for a -#: ```` document at all; ``grep`` with only ``pattern`` -#: searches the whole knowledge base, and a source name is not a -#: document. +#: local text at call time): ``read`` adds a document's FULL text by +#: its combined ``source/path`` identity — the ```` +#: summaries are the starting points, not the content; ``grep`` with +#: only ``pattern`` searches the whole knowledge base, and a source +#: name is not a document. TOOLS_SECTION: str = ( "\n" "You may extend your context with three tools. `ls` lists the " @@ -189,12 +218,16 @@ TOOLS_SECTION: str = ( "`source: X | path: Y | title: Z`; to find one specific document " "without listing, use `grep`. `read` pulls in one document by its " "combined `source/path` string, exactly as shown in the `ls` " - "output — including the source name — adding its full content to " - "your context. Do not call `read` for a document already shown in " - "the section, even when the user asks you to open or " - "read it — its full text is already in your prompt; answer " - "directly from it. For `read`, a bare document path (without the " - "source name) will not resolve. Very large documents are capped: a " + "output — including the source name — or in the " + "summary blocks — adding its full content to your context. The " + " section holds SUMMARIES — the full text of a " + "suggested document is not in your prompt until you `read` it. Do " + "not re-read a document you have already read — its full text is " + "already in your prompt; answer directly from it. If the user " + "asks you to open or read a suggested document, `read` it — that " + "is the point of the section. For `read`, a bare document path " + "(without the source name) will not resolve. Very large documents " + "are capped: a " "cut read returns the first part plus a TRUNCATED notice — the " "document did not end where it stopped; use `grep` (pattern) to " "find the rest, it searches the whole document. `grep` locates an exact string " @@ -355,17 +388,42 @@ def build_kb_section(overview: str, max_chars: int | None = None) -> str: return "" +def _document_body(doc: Document) -> str: + """The body of one ```` suggestion block (phase 118, LOCKED + A6): the document's stored summary (stripped) — NEVER the full + content. + + Defensive fallback ONLY when the summary is missing (``None`` or + whitespace — a fail-soft import miss, LOCKED A5): the first + ``suggestion_preview_chars`` characters of the content plus the + shared :data:`TRUNCATION_MARKER` on its own line — the settings read + happens on this fallback path ONLY (a prompt built from + summary-bearing docs makes no settings call for the cap). Content at + or under the cap rides whole, unmarked (nothing was cut). No LLM + call at chat time — the preview is deterministic. + """ + summary = (doc.summary or "").strip() + if summary: + return summary + limit = get_settings().suggestion_preview_chars + content = doc.content + if len(content) > limit: + return content[:limit] + "\n" + TRUNCATION_MARKER + return content + + def build_high_prompt( documents: Sequence[Document], notes: Sequence[str] | None = None, kb_overview: str | None = None, ) -> str: - """Grounded turn: locked persona (+ steering, + KB overview) + full -texts of the top documents + the ```` instructions (phase 37; -phase 70: the harness-aligned ``ls`` / ``read`` / ``grep`` shapes; -phase 72: the copy states the document-identity contract — the -source-name ``ls`` scope, the combined ``source/path`` identity for -``read``/``grep`` — up front). + """Grounded turn: locked persona (+ steering, + KB overview) + the + top-ranked documents' SUMMARY blocks + the ```` instructions + (phase 37; phase 70: the harness-aligned ``ls`` / ``read`` / + ``grep`` shapes; phase 72: the document-identity contract up front; + phase 118, LOCKED A6 re-revising A7: the ```` section + seeds SUMMARIES, never full texts — full text enters the context + only through the capped ``read`` tool). Section order: ```` → ```` → ```` → ```` → ````; empty steering/overview omit their @@ -373,42 +431,42 @@ source-name ``ls`` scope, the combined ``source/path`` identity for cap — not the prompt — decides whether the tools are actually offered to the model, see :mod:`app.rag.agent`). + When at least one block is present, the section leads with the + start-here :data:`SUGGEST_INTRO` line (before the first block — the + phase-15 ``_STEERING_INTRO`` / phase-31 ``_KB_INTRO`` precedent): the + blocks are the summaries of the top-ranked documents, ``read`` adds + the full text, and the answer cites the document(s) used by path. + This is NOT the reverted phase-72 in-context reminder (that copy + taught the seed documents as already-read context; this one teaches + the summary-as-starting-point contract the A6 re-revision requires). + Each ```` block carries the identity attributes ``source`` / ``path`` / ``title`` — and, since phase 106 (D5), ``date`` (the row's ``created_at`` UTC date part, ``YYYY-MM-DD``, APPENDED after ``title`` — the only position; always present, - ``created_at`` is NOT NULL) — plus the document's full text. - - Gate-iteration note (task 05, 2026-09-03/04): an in-context reminder - LEADING this section (the document texts are already context — do - not ``read`` one the user asked to open) was tried and REVERTED: - it never flipped the seed-doc reads (15/15 across gate runs 1-5) - and correlated with the incident-turn regression (the model latched - the seed documents' paths as ``ls`` scopes — cap reached on the - "list the files in this directory" turn) whenever the copy named - the ```` blocks explicitly. + ``created_at`` is NOT NULL) — plus the document's summary + (:func:`_document_body`; the NULL/whitespace-summary preview + fallback is LOCKED A5). """ - # 2026-09-04 (controlled tool-calling fast loop): the do-not-read - # rule for seed documents lives in TOOLS_SECTION and the ``read`` - # tool descriptions (the copy levers that stuck — see the gate's - # telemetry in TOOL_CALLING_TESTING.md). A per-block instruction - # attribute at the ``source``/``path`` copy site was TRIED and - # REVERTED the same day (no improvement across runs; the block stays - # exactly the document identity + full text). # Phase 106 (D5): every document the model sees carries its # creation date — the block's ``date`` attribute (the row's # ``created_at`` UTC date part, appended after ``title`` — the # only position; always present, ``created_at`` is NOT NULL). + # Phase 118 (A6): the block body is the summary — the full content + # stays out of the prompt (the capped ``read`` tool is the only + # full-text path). blocks = [ f'\n' - f"{doc.content}\n" + f"{_document_body(doc)}\n" "" for doc in documents ] body = "\n\n".join(blocks) if blocks else ( "(no documents matched — do not invent specifics)" ) + if blocks: + body = SUGGEST_INTRO + "\n\n" + body prompt = _base("HIGH") for part in (build_kb_section(kb_overview or ""), build_steering_section(notes or [])): if part: diff --git a/app/rag/retriever.py b/app/rag/retriever.py index 573101e..b93c8c4 100644 --- a/app/rag/retriever.py +++ b/app/rag/retriever.py @@ -35,12 +35,16 @@ ranked documents (up to ``BOR_RELATED_MAX_DOCS``) become the related tier. -The product requirement (LOCKED A7, revised 2026-08-24): the LLM receives -the **entire relevant document**, not just the chunk — chunk hits map back -to their parents, dedupe, rank by best fused score, and the full text of -the top-N documents is always fed through, never truncated. If a future KB -ever makes the prompt too large for the model, the ``LLMError`` → SSE -``error`` path surfaces it loudly — no silent partial context. +The product requirement (A7, re-revised by the phase-118 owner directive, +LOCKED A6, 2026-09-15): the retrieval path seeds **summaries** — the +suggestion tier (:func:`select_suggested`, top-N distinct documents, no +cosine floor, LOCKED A3) whose summary blocks are the grounded prompt's +```` starting points. The full text of a document enters the +context ONLY through the agent's capped ``read`` tool +(:mod:`app.rag.agent`; ``BOR_READ_MAX_CHARS`` + :data:`TRUNCATION_MARKER`), +never through the retrieval seeding. A document's content itself is still +carried on its rows byte-identical — the ``read`` tool serves it whole, +un-truncated up to its cap. Deterministic tie-break for equal fused scores: ``(−fused, −cosine, document.path, chunk.position)``. @@ -61,9 +65,12 @@ from sqlalchemy.orm import Session from app.config import get_settings from app.models import Chunk, Document -#: Shared overflow marker — now used by the steering () section -#: only (phase 15; imported by ``app.rag.prompts``). The document context -#: path never truncates (A7 revised, owner permission 2026-08-24). +#: Shared overflow marker (phase 15; imported by ``app.rag.prompts``) +#: — used by the steering () section, the phase-118 NULL-summary +#: suggestion preview fallback (A5), and the capped agent ``read`` +#: result. The seeded summary blocks and the ``read``-served document +#: content never truncate silently (A6 re-revised): full text enters the +#: context only through the capped ``read`` tool. TRUNCATION_MARKER = "[…truncated…]" #: Alphanumeric tokens of a question (``to_tsquery`` input, OR-joined), @@ -574,6 +581,11 @@ def select_documents_tiered( """Tier chunk hits into the cited and the related parent documents (phase 113, LOCKED A2/A4 — the usefulness bar). + Phase 118 retired the full-text seeding role (A6); the suggested + tier (:func:`select_suggested`) seeds the prompt now — this helper + stays as a dormant public helper (env back-compat for the settings + it was calibrated by). + Distinct parent documents are ranked exactly like :func:`select_documents` (best fused score first — the same stable score-descending walk, so a document's rank position is fixed by its FIRST seen chunk) and each @@ -603,7 +615,11 @@ def select_documents_tiered( The returned rows carry the full document content, byte-identical — a matched parent document is **never truncated** (A7 revised, owner - permission 2026-08-24). + permission 2026-08-24; A6 re-revised 2026-09-15: the retrieval path + seeds SUMMARIES — the cited tier's full texts no longer ride the + grounded prompt, full text enters the context only through the + capped ``read`` tool; the rows themselves still carry the whole + content). """ top_n = n if n is not None else get_settings().top_n_docs no_bar = floor <= 0.0 @@ -642,12 +658,20 @@ def select_documents( ) -> list[Document]: """Map chunk hits to distinct parent documents, ranked by best fused score. + Phase 118 retired the full-text seeding role (A6); the suggested + tier (:func:`select_suggested`) seeds the prompt now — this helper + stays as a dormant public helper (env back-compat for the settings + it was calibrated by). + At most *n* documents are returned (default ``BOR_TOP_N_DOCS``). The returned rows carry the full document content, byte-identical — a matched parent document is **never truncated** (A7 revised, owner - permission 2026-08-24). There is deliberately no context budget: an - oversized prompt must fail loudly through the ``LLMError`` → SSE - ``error`` path, never arrive as silent partial context. + permission 2026-08-24; A6 re-revised 2026-09-15: the seeded prompt + now carries SUMMARIES — the full text reaches the context only + through the capped ``read`` tool, not through this selection). There + is deliberately no context budget: an oversized prompt must fail + loudly through the ``LLMError`` → SSE ``error`` path, never arrive + as silent partial context. Phase 113: a thin wrapper on :func:`select_documents_tiered` — the legacy "any score, top-N" behavior is the cited tier with a zero @@ -656,3 +680,79 @@ def select_documents( """ cited, _ = select_documents_tiered(chunks, n, 0.0, 0) return cited + + +def select_suggested( + chunks: Sequence[RetrievedChunk], + n: int | None = None, +) -> list[Document]: + """Top-N distinct parent documents in fused rank order — the phase-118 + "start here" suggestion tier (LOCKED A3), with NO cosine floor. + + Distinct parent documents are walked in the SAME stable score- + descending order as :func:`select_documents_tiered` (a document's rank + position is fixed by its FIRST seen chunk; dedupe by ``document.id``), + and at most *n* of them are returned (default the + ``BOR_SUGGESTED_DOCS`` setting, 5). Unlike the phase-113 cited tier, + the usefulness bar NEVER filters here: a lexical-only hit with + cosine 0.0 is suggested when it ranks. Suggestions are opt-in + starting points, not citations — the seeded prompt carries the + document's summary, and the LLM decides whether to extend its context + by reading the document's full text. + + The returned rows carry the full document content, byte-identical — + the content is what the agent's ``read`` tool serves later (never + truncated; A6 re-revises A7: full text enters the context only through + the capped ``read`` tool). + """ + top_n = n if n is not None else get_settings().suggested_docs + + order: list[Document] = [] + seen: set[uuid.UUID] = set() + for rc in sorted(chunks, key=lambda c: c.score, reverse=True): + if len(order) >= top_n: + break + doc = rc.document + if doc.id in seen: + continue + seen.add(doc.id) + order.append(doc) + return order + + +def select_related( + chunks: Sequence[RetrievedChunk], + excluded_ids: set[uuid.UUID], + cap: int, +) -> list[Document]: + """The documents ranked AFTER *excluded_ids* — the phase-118 related + tier (rank 6+ for the contiguous top-5 suggestion set), up to *cap* + (``BOR_RELATED_MAX_DOCS``). + + The SAME stable score-descending walk as + :func:`select_documents_tiered` / :func:`select_suggested` (a + document's rank position is fixed by its FIRST seen chunk; dedupe by + ``document.id``), skipping every document whose id is in + *excluded_ids* and admitting at most *cap* documents. There is NO + cosine floor: the related tier is the ranked remainder (a lexical- + only cosine 0.0 hit is included) — its job on the ``done`` frame is + visibility (the UI's de-emphasized "nearby docs" row), not + citation. With the turn wiring's exclusion — exactly the suggested + tier's document ids (LOCKED A3: a contiguous top-N, no floor) — + "excluding the suggested" is exactly "rank 6+". + + The returned rows carry the full document content, byte-identical + (the tier is metadata for the ``done`` frame and the durable + record; the prompt and ``read`` contract are untouched). + """ + out: list[Document] = [] + seen: set[uuid.UUID] = set() + for rc in sorted(chunks, key=lambda c: c.score, reverse=True): + if len(out) >= cap: + break + doc = rc.document + if doc.id in seen or doc.id in excluded_ids: + continue + seen.add(doc.id) + out.append(doc) + return out diff --git a/app/schemas.py b/app/schemas.py index adbbf95..1412f2b 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -388,9 +388,10 @@ class DocContent(BaseModel): path: str title: str format: str - #: Lite-model summary (phase 30) — non-markdown A9 docs only; None for - #: markdown documents, pre-phase-30 rows, and the fail-soft path where - #: summary generation failed but the document was still indexed. + #: Lite-model summary (phase 30) — every A9 doc, markdown included + #: since phase 118 A2; None for pre-phase-30 rows, the fail-soft path + #: where summary generation failed, and until the phase-118 backfill + #: stores one on the next sync. summary: str | None = None #: The document's creation date (phase 106, D8) — ISO-8601, verbatim #: from the row; the viewer's top meta row renders the ``Created`` @@ -742,14 +743,26 @@ class ChatMessage(BaseModel): """One conversation record in the ``bor.chat.v1`` localStorage shape (phase 14) — the stored ``messages`` payload of a saved chat (phase 50). - ``{who, text, sources?, deflected?, suggestions?, thinking?, tools?, - stopped?}`` — raw text, never HTML, so a saved chat restores - pixel-identical through the existing ``renderStoredMessage`` path. + ``{who, text, sources?, related?, deflected?, suggestions?, + thinking?, tools?, stopped?}`` — raw text, never HTML, so a saved + chat restores pixel-identical through the existing + ``renderStoredMessage`` path. ``extra="forbid"`` rejects unknown keys (a corrupted or HTML-shaped payload, e.g. a stray ````-ish extra key) at the boundary with a 422, so nothing outside this shape can poison a restored conversation. + ``related`` (phase 113, the related-doc tier the UI persists with + every grounded brain record — the restore path re-renders the + de-emphasized row from it): the same :class:`SourceRef` list shape + as ``sources``, the same cap. Its absence from this model was a + phase-113 omission — the ``extra="forbid"`` boundary 422'd every + done-time auto-save carrying the key (the A2 quiet-failure path + swallowed it), leaving grounded turns' brain messages unsaved; + pinned by ``tests/integration/test_chats_api.py`` (the full brain + record carries ``related``) and the E2E ``test_share_chat`` + auto-save count. + Phase 83 (SEC-05) bounds the anonymous write surface (``POST/PUT /api/chats`` is public — the row id is the credential, phase 55 A1): ``text`` / ``thinking`` carry :class:`HistoryTurn`'s 32 000 caps @@ -757,7 +770,8 @@ class ChatMessage(BaseModel): path, so a saved chat can never legitimately carry more), and the nested lists get length caps (``max_length``) sized to the realistic ``bor.chat.v1`` record the UI produces (``sources`` ≤ 20 — top-N docs - + agent reads; ``suggestions`` ≤ 50 chips of ≤ 200 chars; ``tools`` + + agent reads; ``related`` ≤ 20 — the related-doc tier, same ref + shape; ``suggestions`` ≤ 50 chips of ≤ 200 chars; ``tools`` ≤ 50 — one entry per tool call, the round cap is 10). Only value bounds were added — the accepted/rejected KEYS are unchanged. """ @@ -767,6 +781,7 @@ class ChatMessage(BaseModel): who: Literal["user", "brain"] text: str = Field(min_length=1, max_length=32_000) sources: list[SourceRef] | None = Field(default=None, max_length=20) + related: list[SourceRef] | None = Field(default=None, max_length=20) deflected: bool | None = None suggestions: list[_Chip] | None = Field(default=None, max_length=50) thinking: str | None = Field(default=None, max_length=32_000) diff --git a/scripts/agent_realmodel_check.py b/scripts/agent_realmodel_check.py index c722a7c..f8617d0 100644 --- a/scripts/agent_realmodel_check.py +++ b/scripts/agent_realmodel_check.py @@ -376,7 +376,7 @@ async def run_turn( lambda: SessionLocal(), system_prompt=plan.system_prompt, user_message=question, - seed_docs=plan.docs, + seed_docs=plan.suggested_docs, # phase 118: the suggestion tier settings=settings, holder=holder, ) diff --git a/scripts/load_test_kb.py b/scripts/load_test_kb.py index fcc0ef6..47dbeef 100644 --- a/scripts/load_test_kb.py +++ b/scripts/load_test_kb.py @@ -35,9 +35,11 @@ guessable by any model. This script: comparing a per-table checksum**. Re-run it only when the fixture documents, the chunker, the embedding -model, or the folder-summary prompt (its ``lite`` output is baked into -the dump) change — everyday iterations restore the dump in sub-second -time (``scripts/restore_test_kb`` / the gate's ``--restore``), never +model, the folder-summary prompt (its ``lite`` output is baked into +the dump), or the importer's summary policy (phase 118: every document +gets a stored summary + an embedded ``is_summary`` chunk) change — +everyday iterations restore the dump in sub-second time +(``scripts/restore_test_kb`` / the gate's ``--restore``), never re-embedding (see ``TOOL_CALLING_TESTING.md``). Exit codes: **0** built + verified, **1** build/verification failure, @@ -105,10 +107,12 @@ FIXTURE_KB_OVERVIEW: str = ( #: tsvector column is excluded; Postgres recomputes it). _TABLES: tuple[tuple[str, type, tuple[str, ...]], ...] = ( ("documents", Document, ("id", "source", "path", "full_path", "title", - "content", "content_hash", "indexed_at", "summary")), + "content", "content_hash", "indexed_at", + "created_at", "created_at_manual", "summary")), ("chunks", Chunk, ("id", "document_id", "position", "content", "embedding", "is_summary")), - ("git_sources", GitSource, ("id", "url", "kind", "path", "added_at")), + ("git_sources", GitSource, ("id", "url", "kind", "path", "ignore_paths", + "include_hidden", "added_at")), ("kb_overview", KbOverview, ("id", "content", "updated_at")), ("sources_meta", SourcesMeta, ("id", "version", "updated_at")), ("steering_notes", SteeringNote, ("id", "note", "created_at")), @@ -125,7 +129,8 @@ _TABLES: tuple[tuple[str, type, tuple[str, ...]], ...] = ( # step 5 of :func:`_build` (the build truncates first, so the sync # paths' table-empty first-run trigger holds). ("folder_summaries", FolderSummary, ("source", "folder_path", - "summary", "updated_at")), + "summary", "manually_edited", + "updated_at")), ) @@ -209,7 +214,7 @@ async def _retrieval_report(llm: LLMClient, battery: list[str]) -> int: vec = await llm.embed_one(question) chunks = retrieve(db, question, vec) plan = plan_turn(chunks, settings) - seed = ", ".join(f"{d.source}/{d.path}" for d in plan.docs) or "—" + seed = ", ".join(f"{d.source}/{d.path}" for d in plan.suggested_docs) or "—" if plan.deflected: deflected += 1 print( @@ -317,7 +322,8 @@ async def _build(kb_dir: Path, dump_path: Path) -> int: "-- Data-only snapshot (the schema stays alembic-managed; the", "-- generated chunks.tsv column is recomputed on restore).", f"-- Sources: {', '.join(FIXTURE_SOURCES)} " - f"({summary.added} documents, {summary.chunks} chunks).", + f"({summary.added} documents, {summary.chunks} content " + f"+ {summary.summaries + summary.summary_backfilled} summary chunks)", "-- Restore (one transaction, sub-second):", "-- uv run python -m scripts.restore_test_kb", "-- psql \"$BOR_DATABASE_URL\" --single-transaction -f " diff --git a/tests/e2e/mock_llm.py b/tests/e2e/mock_llm.py index feb397e..be89917 100644 --- a/tests/e2e/mock_llm.py +++ b/tests/e2e/mock_llm.py @@ -55,14 +55,20 @@ Implements just enough of the aipi surface: - system prompt containing ```` (phase 31, KB overview) -> the composed answer ends with `` (kb: )`` — the same echo convention for the overview's prompt injection. - - user message containing ``show the end of your notes`` (phase 24, - whole-document context) -> the answer quotes the **last 160 chars of - the ```` block** — a tail echo, byte-stable across runs, so - a sentinel placed at the *end* of a document appears in the rendered - answer iff the whole document was in the prompt. (Phase 37: the HIGH - prompt now ends with a ```` section after ````, so - the echo targets the block itself; its tail still includes the - closing tag — same sentinel semantics.) + - user message containing ``show the end of your notes`` (phase 24; + phase 118 re-targeted — the story's dedicated suite + ``tests/e2e/test_summary_seed_context.py``) -> the answer quotes + the **last 160 chars of the ```` block** — a tail echo, + byte-stable across runs. (Phase 37: the HIGH prompt now ends with + a ```` section after ````, so the echo targets + the block itself; its tail still includes the closing tag.) + Phase 118 (A6): the block carries the suggested documents' + SUMMARIES (never full texts), so the echoed tail is the LAST + suggested document's SUMMARY tail (its digest + the + ``Source: /`` pointer line) — a sentinel on a + document's *last line* appears in the rendered answer iff the + FULL content (not the summary) was in the prompt, which under the + summary-seed contract is only through a ``read`` tool result. - user message containing ``use your tools`` (phase 37, agent document tools; phase 70: the flow emits the harness-aligned names — ``ls`` / ``read`` with the combined ``source/path`` identity; phase 94: @@ -235,16 +241,19 @@ Implements just enough of the aipi surface: content>`` (the phase-37 single-read shape — the grounded- turn citation contract); * the last tool result is the agent's ALREADY_IN_CONTEXT dedupe - refusal (the read target is already a top-2 retrieval - document — with the drill fixture that is DETERMINISTIC: - the read question names the file's path, so the file - self-matches the hybrid gate and its FULL text is in the - ```` prompt): the model answers FROM THE PROMPT — - the deterministic answer ``Already in context: Read - . `` block>`` (same citation - shape as the read-result branch — the document text reached - the model either way, and the answer proves it); + refusal (phase 118: the read target is a document ALREADY + READ into full-text context earlier in the same turn — the + seeds are summaries, so a first read of any document + succeeds and only a re-read is refused): the model answers + FROM THE PROMPT — the deterministic answer ``Already in + context: Read . `` block>`` + (same citation shape as the read-result branch — the document + text reached the model either way, and the answer proves it; + the block it quotes now carries the document's SUMMARY). + No suite exercises this branch today (the drill questions + read once per turn) — it is kept for the still-real + already-read refusal; * any other last result (a top-level or folder LISTING landed): the deterministic ECHO answer ``Here's the level I listed:\n `` — the mock echoes what it received @@ -287,6 +296,38 @@ Implements just enough of the aipi surface: needs the ```` section, so deflected turns never hit it); verified 2026-09-10: no existing E2E question or fixture file contains the phrase, so every other suite is unaffected. + - user message containing ``read the suggested document`` + (``SUMMARY_SEED_READ_TRIGGER``, phase 118 task 06 — the + summary-seed context's dedicated story suite + ``tests/e2e/test_summary_seed_context.py``) **and** the system + prompt carries the ```` section -> the deterministic + SCRIPTED SUMMARY-READ flow: the question carries its own tool call + after the colon — ``read the suggested document: read source/path`` + — parsed by ``_SUMMARY_SEED_READ_CALL_RE`` from the RAW user + message (the target keeps its case), then discriminated + statelessly from the tool results (streaming only): + * request 1 (``tools`` offered, no ``tool``-role result in the + messages yet): the scripted call — ``read`` with the parsed + target (synthetic id ``call_0``); + * a ``tool``-role result is in the messages: the deterministic + ECHO — the answer carries the LAST tool result VERBATIM + (``Here's what the read returned:\n``): under the + phase-118 summary-seed contract a first ``read`` of ANY + document succeeds (the seeds are summaries, not full text), so + a read result (``"Document :…`` — header + the + phase-106 D5 ``date:`` line + the FULL content) lands in the + answer with its tail intact — the story suite's lens on the + full text the ``read`` tool delivered (a tail sentinel on the + document's last line appears in the answer iff the full content + reached the model through the read, not the seed); a refusal + (the premise broke) lands just as visibly, so the suite fails + loudly on it. The mock is the only E2E lens on the LLM's + context, so the echo is the assertion surface. + Checked BEFORE the plain ``TOOLS_TRIGGER`` flow (disjoint trigger + phrases — the phase-71/72/94 ordering convention; the trigger + needs the ```` section, so deflected turns never hit it); + no existing E2E question or fixture file contains the phrase, so + every other suite is unaffected. - user message containing ``what are the correct llama.cpp arguments`` (``GREP_TEACH_TRIGGER``, the 2026-09-05 incident — the harness prior is that grep takes a REGEX; this app's grep is a @@ -802,10 +843,12 @@ _DRILL_CALL_RE = re.compile( _NOT_A_FOLDER_MARKER = "is not a folder" #: The stable substring of the harness-owned dedupe refusal the -#: drill-down flow's ``ctx_answer`` branch keys on (a read target that -#: is already a top-2 retrieval document — the mock then answers from -#: the document's text in the ```` prompt block, exactly -#: what the refusal instructs). Keyed on a substring (not the whole +#: drill-down flow's ``ctx_answer`` branch keys on (phase 118: a read +#: target ALREADY READ into full-text context in the same turn — the +#: seeds are summaries, so a first read of any document succeeds and +#: only a re-read is refused; the mock then answers from the document's +#: summary in the ```` prompt block, exactly what the +#: refusal instructs). Keyed on a substring (not the whole #: constant) so a re-wrap of the constant cannot silently re-route the #: mock; the module-level assert below fails loudly if the substring #: ever leaves the constant (the mock must never drift from @@ -819,11 +862,13 @@ assert _ALREADY_IN_CONTEXT_MARKER in ALREADY_IN_CONTEXT, ( #: section (``app.rag.prompts.build_high_prompt``): the block is the #: document identity (``source``/``path``/``title`` attributes — plus, #: since phase 106 D5, the ``date`` attribute, the row's ``created_at`` -#: UTC date part, APPENDED after ``title``) plus the document's FULL -#: text (never truncated on the retrieval path, owner-locked A7) -#: between the tags. The ``date`` group is OPTIONAL so the mock -#: tolerates the pre- and post-phase block shapes (house rule: the -#: marker/regex lands with the prompt change). +#: UTC date part, APPENDED after ``title``) plus the document's +#: SUMMARY (phase 118, A6 — the summary-seed contract: the seeded +#: blocks are summaries, never full text; full text enters the context +#: only through the capped ``read`` tool) between the tags. The +#: ``date`` group is OPTIONAL so the mock tolerates the pre- and +#: post-phase block shapes (house rule: the marker/regex lands with +#: the prompt change). _DOCUMENT_BLOCK_RE = re.compile( r'\n(?P.*?)\n', @@ -835,10 +880,14 @@ def _document_block(system: str, source: str, path: str) -> str | None: """The stored text of one ```` block (or ``None``). The drill-down flow's ``ctx_answer`` branch: when the agent's read - of a top-2 retrieval document gets the ALREADY_IN_CONTEXT dedupe, - the document's full text is in the ```` prompt — the - mock (the model) extracts it by the block's identity attributes - and quotes it, answering from the prompt as the refusal instructs. + of a document ALREADY READ in the same turn gets the + ALREADY_IN_CONTEXT dedupe (phase 118 — the seeds are summaries, so + a first read of any document succeeds and only a re-read is + refused), the mock (the model) extracts the block's text by the + identity attributes and quotes it, answering from the prompt as + the refusal instructs. The block carries the document's SUMMARY + (phase 118 A6) — no suite exercises the branch today; it is kept + for the still-real already-read refusal. """ for block in _DOCUMENT_BLOCK_RE.finditer(system): if block.group("source") == source and block.group("path") == path: @@ -1377,16 +1426,22 @@ def _drill_flow(body: dict[str, Any]) -> tuple[str, ...] | None: last = results[-1] if last.startswith(_READ_RESULT_PREFIX): head, _, content = last.partition("\n") - # The read result is ``"Document :\n"`` — - # the head carries the server's appended ``:`` (removed here; - # a document path never legitimately ends with one). + # The read result is ``"Document :\ndate: …\n"`` + # — the head carries the server's appended ``:`` (removed here; + # a document path never legitimately ends with one); the + # phase-106 D5 ``date:`` line sits between the header and the + # document text — skipped so the quote stays pure document + # content (the suite's byte-identical pin). combined = head[len(_READ_RESULT_PREFIX):].strip().removesuffix(":") + if content.startswith("date: "): + content = content.partition("\n")[2] return ("read_answer", combined, content[:80]) if _ALREADY_IN_CONTEXT_MARKER in last and verb == "read" and "/" in target: - # The dedupe fired: the read target is already a top-2 - # retrieval document, so its FULL text is in the - # ```` prompt — answer from the prompt (the - # refusal's instruction), quoting the block's text. + # Phase 118: the dedupe fires only for a document ALREADY READ + # in the same turn (the seeds are summaries — a first read of + # a suggested document succeeds). Answer from the prompt (the + # refusal's instruction), quoting the block's text (now the + # document's summary). src, _, p = target.partition("/") content = _document_block(_system(body), src, p) if content is not None: @@ -1469,6 +1524,81 @@ def _read_cap_flow(body: dict[str, Any]) -> tuple[str, ...] | None: return ("echo", results[-1]) +# --------------------------------------------------------------------------- +# Phase 118 (task 06, the summary-seed context's dedicated story suite): +# the deterministic SCRIPTED summary read — see the module docstring +# --------------------------------------------------------------------------- + +#: A user message containing this substring (case-insensitive) — +#: combined with the ```` section in the system prompt — drives +#: the scripted SUMMARY-READ flow (the summary-seed context's story +#: suite, ``tests/e2e/test_summary_seed_context.py``): the question +#: carries its own tool call after the colon — ``read the suggested +#: document: read source/path`` — the mock emits the scripted ``read`` +#: (phase 118: the seeds are summaries, so a first read of a suggested +#: document SUCCEEDS — the full text arrives through the read), then +#: ECHOES the ENTIRE tool result into its answer (the house +#: scripted-turn lens on the LLM's context — the full content's tail +#: reaches the rendered answer iff the read delivered it). Checked +#: BEFORE the plain ``TOOLS_TRIGGER`` flow (disjoint trigger phrases — +#: the phase-71/72/94 ordering convention); no existing E2E question +#: or fixture file contains the phrase, so every other suite is +#: unaffected. +SUMMARY_SEED_READ_TRIGGER = "read the suggested document" + +#: The scripted call in the summary-read question (case-insensitive — +#: the suite's questions capitalize the trigger's first letter): the +#: verb (``read``) plus the target — a combined ``source/path``, parsed +#: from the RAW user message so the target keeps its case. The target +#: is a ``[a-z0-9_./-]`` run (case-insensitively), so the suite's +#: `` — `` flavor separator (em dash) can never bleed into it (the +#: read-cap/drill convention, ``_READ_CAP_CALL_RE`` / ``_DRILL_CALL_RE``). +_SUMMARY_SEED_READ_CALL_RE = re.compile( + r"read the suggested document:\s*read\s+(?P[a-z0-9_./-]+)", + re.I, +) + + +def _summary_seed_read_flow(body: dict[str, Any]) -> tuple[str, ...] | None: + """Classify a phase-118 scripted summary-read request (see the + module docstring). The question carries the scripted call (``read + the suggested document: read source/path``); the step is then + discriminated statelessly from the tool results, like the other + marker flows: + + * ``("call", target, "call_0")`` — ``tools`` are offered and no + ``tool``-role result is in the messages yet: the scripted + ``read`` on the parsed target (synthetic id ``call_0``). + * ``("echo", result)`` — a ``tool``-role result is in the messages: + the deterministic ECHO — the answer carries the LAST tool result + VERBATIM (``Here's what the read returned:\n``): a read + result (``"Document :…`` — header + date line + + FULL content) lands in the answer with its tail intact (the + full text the ``read`` tool delivered — the phase-118 contract: + a first read of a suggested document succeeds, the seed was a + summary); a refusal (the premise broke) lands just as visibly, + so the suite fails loudly on it. + * ``None`` — not the flow: the trigger is absent, the ```` + section is missing (deflected turns never carry it), the scripted + call is unparseable, or ``tools`` are not offered and no tool + results are in the messages yet (e.g. ``agent_max_rounds=0``). + """ + user = _user(body) + if SUMMARY_SEED_READ_TRIGGER not in user.lower(): + return None + if "" not in _system(body): + return None + match = _SUMMARY_SEED_READ_CALL_RE.search(user) + if match is None: + return None + results = _tool_results(body) + if not results: + if not body.get("tools"): + return None + return ("call", match.group("arg"), "call_0") + return ("echo", results[-1]) + + def long_answer() -> str: """~900-word deterministic walkthrough (phase 11): numbered steps plus a unique final line that must survive the stream untruncated.""" @@ -1924,14 +2054,19 @@ def compose_answer(body: dict[str, Any]) -> str: "You've got this!" ) elif END_OF_NOTES_TRIGGER in user.lower(): - # Whole-document-context story (phase 24): echo the tail of the - # document context. Byte-stable across runs — a sentinel on the - # document's last line appears in the answer iff the whole - # document was in the prompt. (The tail includes the closing - # — harmless for the E2E sentinel assertions.) - # Phase 37: the HIGH prompt now ends with the section - # after , so the echo targets the block - # itself — the sentinel semantics are unchanged. + # Phase 24 (whole-document context) — phase 118 re-targeted + # (the summary-seed context's story suite): echo the tail of + # the block. Byte-stable across runs — under the + # phase-118 summary-seed contract the block carries the + # suggested docs' SUMMARIES, so the echoed tail is the LAST + # suggested doc's summary tail (digest + Source: pointer line), + # and a sentinel on a document's last line appears in the + # answer only if the FULL content reached the model (through a + # read tool result — never the seed). (The tail includes the + # closing — harmless for the E2E sentinel + # assertions.) Phase 37: the HIGH prompt ends with the + # section after , so the echo targets the + # block itself. block = _DOCUMENTS_BLOCK_RE.search(_system(body)) tail_source = block.group(0) if block else _context(body) answer = ( @@ -2461,6 +2596,36 @@ def chat_completions(body: dict[str, Any]) -> Any: media_type="text/event-stream", headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}, ) + # Phase 118 (task 06): the deterministic SCRIPTED summary read + # (the question carries its own call — ``read the suggested + # document: read source/path``): the scripted ``read`` (phase + # 118: a first read of a suggested document succeeds — the + # seeds are summaries), then the answer that ECHOES the whole + # tool result (the full text's tail reaches the rendered answer + # iff the read delivered it — the suite's lens on the LLM's + # context). Checked BEFORE the plain TOOLS_TRIGGER flow + # (disjoint trigger phrases — the phase-71/72/94 ordering + # convention; the trigger needs the ```` section, so + # deflected turns never hit it). + seed_read = _summary_seed_read_flow(body) + if seed_read is not None: + if seed_read[0] == "call": + stream = _tool_call_stream( + "read", {"path": seed_read[1]}, seed_read[2] + ) + else: # "echo" — the last tool result verbatim (the lens) + stream = _sse_stream( + _apply_max_tokens( + f"Here's what the read returned:\n{seed_read[1]}", + body.get("max_tokens"), + ), + 0.0, + ) + return StreamingResponse( + stream, + media_type="text/event-stream", + headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}, + ) # Phase 109 (task 03, never-frozen-turn story suite): the # deterministic REPORTED-REPRO turn (delta → tool → # thinking-after-delta — TODO.md L3): the scripted first answer, diff --git a/tests/e2e/test_agent_document_tools.py b/tests/e2e/test_agent_document_tools.py index a20694d..428d8f3 100644 --- a/tests/e2e/test_agent_document_tools.py +++ b/tests/e2e/test_agent_document_tools.py @@ -44,8 +44,13 @@ reseelink.json" but does not include it): (a ``documents`` row: it is in the agent's catalog, readable, and a source-chip target) but seeded WITHOUT chunks. In a real hundreds-of-document KB the file would simply fail to rank into the - top-2 context; with a two-document corpus every chunk would rank, so - "not in context" is expressed as "no retrieval candidates". Its + suggested (summary-seeded) context; with a two-document corpus every + chunk would rank, so "not in context" is expressed as "no retrieval + candidates". (Phase 118, A6: a read of a suggested doc now succeeds + anyway — the ALREADY_IN_CONTEXT refusal fires only for a document + already READ in the turn — so the catalog-only design stands on the + mock's first-catalog-line parse, not on the retired seed-read + refusal.) Its ``(source, path)`` also sorts FIRST in the catalog (``Deployments`` < ``Homelab``) — which is exactly the line the mock parses out of the listing and reads. diff --git a/tests/e2e/test_document_dates.py b/tests/e2e/test_document_dates.py index e793804..0e48208 100644 --- a/tests/e2e/test_document_dates.py +++ b/tests/e2e/test_document_dates.py @@ -46,9 +46,14 @@ Test → contract mapping (six tests, one per contract bullet): the ``Indexed`` badge (D8 — the date at the top of the clicked document), with the source/format/indexed/chunks badges intact. 4. ``test_old_correct_beats_new_similar`` — THE OWNER SCENARIO end to - end: the real retriever + the DEFAULT recency boost (0.0007 / 365 d) - over the mock's token-overlap embeddings ranks the OLDER correct - document as the first cited source over the newer similar one. + end (phase 118, A4): the real retriever + the DEFAULT recency boost + (0.0007 / 365 d) over the mock's token-overlap embeddings ranks the + OLDER correct document first in the suggested tier — the chip row IS + the suggested tier (top-5, NO floor; this four-doc KB therefore + chips ALL four docs in rank order: retention, draft, forward, + old-doc) and the related row is absent (no rank-6+ doc exists). + The newer similar doc (the boost's intended beneficiary) stays + second — the boost never lets it outrank the one that answers. 5. ``test_date_edit_and_sync_preserves`` — the admin-only editor in the real UI: set → Save → the badge re-renders from the RESPONSE (never the optimistic input) → the API round-trips; a re-import @@ -69,9 +74,12 @@ deterministic for fixed text — measured with ``app.rag.retriever._vector_candidates`` / ``_lexical_candidates`` / ``retrieve()`` against a real Postgres + the mock: -* ``retention.md`` (correct, 2020) — rank 1 in BOTH lists (cosine - 0.6222; the lexical tsquery ``how|did|i|configure|backup|retention| - policy`` after stopword removal matches it most densely). +* ``retention.md`` (correct, 2020) — rank 1 in BOTH lists (content + chunk cosine 0.6222; the lexical tsquery ``how|did|i|configure| + backup|retention|policy`` after stopword removal matches it most + densely). Phase 118 (A2): its EMBEDDED summary is a retrieval + candidate too (best-chunk cosine 0.7133 — the seeded summary ranks, + which is the point of seeding it). * ``retention-draft.md`` (similar, now) — rank 4 in the vector list (cosine 0.1443) and rank 2 in the lexical list. Its wording was tuned for exactly this: it shares ONLY the three "backup retention @@ -85,14 +93,18 @@ deterministic for fixed text — measured with (0.1875) rank 2–3 in the vector list (unrelated content) and match the tsquery not at all. -Fused (RRF k=60) + the default boost (0.0007 · exp(−age/365d)): -retention.md 1/61+1/61 = 0.0327878 (+ ≈ 0, 6.7 half-lives old) vs -retention-draft.md 1/64+1/62 = 0.0317540 (+ the FULL zero-age 0.0007 -= 0.0324540) → the older correct doc wins by 0.000334 WITH the boost -on (it would win by 0.001034 with the boost off — the scenario holds -both ways; the boost never lets the newer similar doc outrank the one -that answers the question). ``select_documents`` (top-2) cites -retention.md first, the draft second. The boost defaults are owned by +Fused (RRF k=60) + the default boost (0.0007 · exp(−age/365d)), +probe-verified against the current candidate set (the embedded summary +chunks join the walk — phase 118 A2): retention.md 0.032523 vs +retention-draft.md 0.031498 (the full zero-age boost included) → the +older correct doc wins by ≈ 0.001025 WITH the boost on (the margin +only grew once the summary chunks ranked — the scenario holds both +ways; the boost never lets the newer similar doc outrank the one that +answers the question). The phase-118 suggested tier (top-5, NO floor) +carries all four docs in that rank order — retention.md first, the +draft second, then forward.md (0.016325) and old-doc.md (0.015874) +(no floor filters the unrelated docs); the related tier is EMPTY +(no rank-6+ doc in a four-doc KB). The boost defaults are owned by task 07 — untouched here. DB isolation: every test TRUNCATEs the KB tables (the @@ -529,11 +541,14 @@ def test_old_correct_beats_new_similar( ) -> None: """``How did I configure the backup retention policy?`` → grounded, and the FIRST cited source is the OLDER correct doc (2020) — the - NEWER similar one (now, "under review") is cited second. The real - hybrid retriever + the DEFAULT recency boost (0.0007 / 365 d) over - the mock's token-overlap embeddings (the module docstring records - the measured fused scores: 0.0327878 vs 0.0324540 — margin - 0.000334 WITH the full zero-age boost on the newer doc).""" + NEWER similar one (now, "under review") is cited second. Phase 118 + (A4): the chip row IS the suggested tier (top-5, NO floor) — this + four-doc KB chips ALL four docs in rank order — and the related row + is absent (no rank-6+ doc). The real hybrid retriever + the DEFAULT + recency boost (0.0007 / 365 d) over the mock's token-overlap + embeddings (the module docstring records the measured fused scores: + 0.032523 vs 0.031498 — margin ≈ 0.001025 WITH the full zero-age + boost on the newer doc).""" _reset_db(mock_llm, dates_tree) page.set_default_timeout(30_000) login(page, app_url, next="/") # phase 79: chat is require_user-gated @@ -548,22 +563,34 @@ def test_old_correct_beats_new_similar( # top_score 0.6222 ≥ the e2e threshold 0.30). expect(page.locator(".msg.brain.is-deflected")).to_have_count(0) - # The OLDER correct doc is the FIRST source chip; the NEWER similar - # one (the boost's intended beneficiary) is cited — but second. + # Phase 118 (A4): the chip row IS the suggested tier — top-5, NO + # floor, and this KB has exactly four docs, so ALL four are + # suggested (chipped) in fused rank order: the OLDER correct doc + # first, the NEWER similar one (the boost's intended beneficiary) + # second, then the two unrelated docs (no floor filters them — the + # LLM decides what the summaries earn). chips = page.locator(".msg.brain .source-chip") - expect(chips).to_have_count(2, timeout=30_000) + expect(chips).to_have_count(4, timeout=30_000) assert chips.nth(0).inner_text() == f"{dates_tree.name}/{RETENTION_MD}" assert chips.nth(1).inner_text() == f"{dates_tree.name}/{DRAFT_MD}" + assert chips.nth(2).inner_text() == f"{dates_tree.name}/{FORWARD_MD}" + assert chips.nth(3).inner_text() == f"{dates_tree.name}/{OLDDOC_MD}" + # No rank-6+ doc exists in this four-doc KB → the related row is + # absent (the de-emphasized row renders only when it has entries). + expect(page.locator(".msg.brain .related-docs")).to_have_count(0) - # Durable record: one row, grounded, both docs cited in rank order. + # Durable record: one row, grounded, the FULL retrieval (suggested + # tier + related + read, deduped — here: all four docs) in rank + # order (LOCKED A3). with SessionLocal() as db: row = db.scalars(select(QueryLog)).one() assert row.question == QUESTION assert row.deflected is False assert row.top_score >= 0.30 # the e2e mock-calibrated threshold assert (row.fts_hits or 0) >= 1 - assert f"{dates_tree.name}/{RETENTION_MD}" in row.sources - assert row.sources.index(RETENTION_MD) < row.sources.index(DRAFT_MD) + assert row.sources == ", ".join( + f"{dates_tree.name}/{p}" for p in (RETENTION_MD, DRAFT_MD, FORWARD_MD, OLDDOC_MD) + ), row.sources # --------------------------------------------------------------------------- @@ -728,12 +755,20 @@ def test_anonymous_gate_and_editor_a11y( " === document.querySelector('#doc-modal .doc-date-input')" ) # Keyboard traversal runs through the editor's controls (the modal - # focus trap): Tab from Save → Cancel, and Tab from Revert wraps to - # the panel's first control (the trap's edge behavior). (The CDP - # key dispatch of headless Chromium does NOT perform the native - # focus move off itself — a harness artifact, - # not a product defect: the same Tab works from every text input - # and button in the editor, pinned here through the buttons.) + # focus trap — the visible focusable order is "Full page" → Close → + # the content's controls, in DOM order): Tab from Save → Cancel, + # Tab from Revert → the summary editor's button (phase 118, A2: the + # markdown doc is summarized TOO, so the modal's summary-edit + # button is visible and joins the trap after the date editor — it + # did not exist in the focusable set when this pin was written, + # which is why the wrap below is now reached from IT), and Tab from + # the summary editor wraps to the panel's FIRST control, the + # "Full page" link (the trap's edge behavior the pin originally + # carried). (The CDP key dispatch of headless Chromium does NOT + # perform the native focus move off itself — a + # harness artifact, not a product defect: the same Tab works from + # every text input and button in the editor, pinned here through + # the buttons.) page.evaluate("() => document.querySelector('#doc-modal .doc-date-save').focus()") page.keyboard.press("Tab") assert page.evaluate( @@ -742,6 +777,12 @@ def test_anonymous_gate_and_editor_a11y( ) page.evaluate("() => document.querySelector('#doc-modal .doc-date-revert').focus()") page.keyboard.press("Tab") + assert page.evaluate( + "() => !!document.activeElement" + " && document.activeElement.classList.contains('doc-summary-edit')" + ) + page.evaluate("() => document.querySelector('#doc-modal .doc-summary-edit').focus()") + page.keyboard.press("Tab") assert page.evaluate( "() => !!document.activeElement" " && document.activeElement.classList.contains('doc-modal-open')" diff --git a/tests/e2e/test_document_summaries.py b/tests/e2e/test_document_summaries.py index 67324e0..b15bb08 100644 --- a/tests/e2e/test_document_summaries.py +++ b/tests/e2e/test_document_summaries.py @@ -1,4 +1,5 @@ -"""Phase 30 E2E (Playwright): a summary hit delivers the full source doc. +"""Phase 30 E2E (Playwright) — phase 118 re-targeted (A2/A6): a summary +hit seeds the SUMMARY into the prompt — never the full source doc. Story: ``.agents/user_stories/document-summaries.md`` Run in isolation (DB must be up: ``podman compose up -d db``): @@ -7,27 +8,33 @@ Run in isolation (DB must be up: ``podman compose up -d db``): The fixture KB is a story-dedicated directory (``tests/fixtures/summary_kb/`` — the shared ``tests/fixtures/docs/`` -stays at its 9 pinned files) with two documents: +stays at its pinned files) with two documents: -* ``quadlet/qwen-llamacpp.yaml`` — a non-markdown A9 doc. At import the - mock ``lite`` model (``SUMMARY_MODE`` marker, ``tests/e2e/mock_llm.py``) - reduces it to a deterministic 24-token digest, stored on - ``documents.summary`` and indexed as one ``is_summary`` chunk. The raw - yaml body is deliberately token-diluted, so the document's best fused - chunk is its summary chunk. The sentinel ``RESE-SUMMARY-SENTINEL-7f3a`` - sits on the document's LAST line — outside the 24-token digest, - unreachable from the summary. -* ``notes/qwen-llamacpp-notes.md`` — a markdown control doc (never - summarized) that ranks first, which puts the yaml document LAST inside - ````. +* ``quadlet/qwen-llamacpp.yaml`` — at import the mock ``lite`` model + (``SUMMARY_MODE`` marker, ``tests/e2e/mock_llm.py``) reduces it to a + deterministic 24-token digest, stored on ``documents.summary`` and + indexed as one ``is_summary`` chunk. The raw yaml body is deliberately + token-diluted, so the document's top fused chunks are the summary and + the one lexical-hitting raw chunk. The sentinel + ``RESE-SUMMARY-SENTINEL-7f3a`` sits on the document's LAST line — + outside the 24-token digest, unreachable from the summary. +* ``notes/qwen-llamacpp-notes.md`` — a markdown doc (phase 118 A2: it is + summarized TOO — the phase-30 non-markdown-only scope is retired) that + ranks first, which puts the yaml document LAST inside + ```` (a two-doc KB → both docs are suggested, the related + tier is empty). The mock LLM's tail-echo trigger (``END_OF_NOTES_TRIGGER``) makes the -answer quote the last 160 chars of the document context — the tail of -the LAST selected document. The sentinel therefore appears in the -rendered answer **iff the entire yaml source document (not the summary -digest) reached the LLM prompt** — the summary→parent-document resolution -through the unchanged chunk→document mapping (A7 revised: never -truncated), which is what this story is about. +answer quote the last 160 chars of the seeded ```` block — +under the phase-118 summary-seed contract (A6) that block carries the +suggested docs' SUMMARIES, never their full texts, so the echoed tail is +the LAST suggested doc's summary (the yaml doc's byte-stable digest tail ++ ``Source:`` pointer line). The sentinel therefore appears in the +rendered answer **only if the entire yaml source document (not the +summary) reached the LLM prompt** — under the locked contract it must be +ABSENT (full text enters the context only through the capped ``read`` +tool), which is the inverse of the retired phase-30/24 full-text pin +and what this story is about now. """ from __future__ import annotations @@ -47,7 +54,7 @@ from app.models import Document, QueryLog from app.rag.chunker import chunk_document from app.rag.importer import ImportSummary, import_sources from app.rag.llm import LLMClient -from app.rag.retriever import RetrievedChunk, retrieve +from app.rag.retriever import RetrievedChunk, retrieve, select_suggested from e2e.auth_helpers import login from tests.e2e.mock_llm import TOKEN_RE, embed_text @@ -156,17 +163,20 @@ def _chunks_by_path(chunks: Sequence[RetrievedChunk], path: str) -> list[Retriev # --- Story tests ------------------------------------------------------------- -def test_summary_hit_retrieves_full_source_document( +def test_summary_hit_seeds_the_summary_not_the_full_text( page: Page, app_url: str, mock_llm: int, db_ready: None ) -> None: - """A question whose best yaml match is the summary chunk yields an - answer grounded in the FULL yaml source document: its tail sentinel — - which the summary digest cannot contain — is echoed back, and the - source chip cites the yaml path (deflected: false).""" + """A question whose best yaml match is its summary chunk yields a + grounded answer seeded from the SUMMARY — the mock's tail echo + quotes the last suggested doc's summary tail (digest + pointer), and + the sentinel (the yaml doc's last line, outside the digest) is + ABSENT: the full source doc never reached the prompt (A6). Both + fixture docs are suggested (two-doc KB, no floor) and both chips + render (deflected: false).""" _reset_db() summary = _run_in_thread(_import_fixtures(mock_llm)) assert summary.added == 2 # yaml + md control - assert summary.summaries == 1 and summary.summary_errors == 0 + assert summary.summaries == 2 and summary.summary_errors == 0 # A2: md too assert summary.errors == 0 # Import state: exactly one embedded ``is_summary`` chunk (position @@ -182,30 +192,48 @@ def test_summary_hit_retrieves_full_source_document( assert schunks[0].embedding is not None assert yaml_doc.summary == _expected_summary(yaml_content, SOURCE, YAML_PATH) - # Retrieval state: the summary chunk is the yaml document's best fused - # chunk — the document enters the context through its summary, not - # through the diluted raw yaml chunks. + # Retrieval state (phase 118, A2/A6): the EMBEDDED summary chunk is + # a retrieval candidate (the seeded text ranks on its own), and the + # suggested tier is the two docs in rank order — md first, yaml + # LAST (so the yaml's summary is the tail of the block, + # the mock echo's target). The document enters the prompt through + # its SUMMARY block, not through the diluted raw yaml chunks. with SessionLocal() as db: chunks = retrieve(db, QUESTION, embed_text(QUESTION)) yaml_chunks = _chunks_by_path(chunks, YAML_PATH) - best_yaml = max(yaml_chunks, key=lambda c: c.score) - assert best_yaml.is_summary + assert any(c.is_summary for c in yaml_chunks) # the embedded summary ranks assert len(yaml_chunks) >= 2 # summary + at least one raw candidate + assert [d.path for d in select_suggested(chunks)] == [MD_PATH, YAML_PATH] bubble = _ask(page, app_url, QUESTION) - # The tail sentinel exists only on the document's last line and - # cannot be in the summary digest — its presence proves the entire - # source document was in the LLM prompt (summary→parent resolution). - expect(bubble).to_contain_text(SENTINEL, timeout=30_000) - expect(bubble).to_contain_text(MOCK_ANSWER_MARKER) + # Phase 118 (A6): the seed is the SUMMARY, not the full text — the + # mock's tail echo quotes the last 160 chars of the + # block, which end in the LAST suggested doc's summary: the yaml + # doc's byte-stable digest tail + pointer line. The sentinel + # (document's last line, outside the digest) is therefore ABSENT — + # the full source doc never reached the prompt (full text enters + # only through the capped read tool; the inverse of the retired + # phase-30/24 full-text pin). The bubble renders the answer as + # markdown, which collapses the summary's newline — so pin each + # LINE separately (the digest line's tail sits inside the echoed + # 160 chars; the pointer line is single-line too). + expect(bubble).to_contain_text(MOCK_ANSWER_MARKER, timeout=30_000) + yaml_summary = _expected_summary(yaml_content, SOURCE, YAML_PATH) + yaml_digest_line = yaml_summary.split("\n", 1)[0] + expect(bubble).to_contain_text(f"Source: {SOURCE}/{YAML_PATH}") + expect(bubble).to_contain_text(yaml_digest_line[-80:]) + expect(bubble).not_to_contain_text(SENTINEL) - # Grounded: the yaml source chip renders (the md doc ranks first, so - # both fixtures are cited). + # Grounded: both fixture docs are suggested (two-doc KB — no floor) + # and both chips render, in rank order (md first, yaml last). chip = page.locator(".msg.brain .source-chip", has_text=YAML_PATH) expect(chip).to_have_count(1) expect(chip.first).to_contain_text(f"{SOURCE}/{YAML_PATH}") expect(page.locator(".msg.brain .source-chip", has_text=MD_PATH)).to_have_count(1) + expect(page.locator(".msg.brain .source-chip")).to_have_count(2) + # No rank-6+ doc in a two-doc KB → the related row is absent. + expect(page.locator(".msg.brain .related-docs")).to_have_count(0) # Button recovers (never stale) and the turn was grounded, not # deflected. @@ -214,20 +242,26 @@ def test_summary_hit_retrieves_full_source_document( row = _last_query_log() assert row.question == QUESTION assert row.deflected is False - assert f"{SOURCE}/{YAML_PATH}" in row.sources - assert f"{SOURCE}/{MD_PATH}" in row.sources + # The durable record: suggested + related + read (deduped) — here + # exactly the two suggested docs, in rank order (LOCKED A3). + assert row.sources == ( + f"{SOURCE}/{MD_PATH}, {SOURCE}/{YAML_PATH}" + ), row.sources -def test_markdown_control_doc_gets_no_summary_chunk( +def test_markdown_control_doc_gets_a_summary_chunk( mock_llm: int, db_ready: None ) -> None: - """Control: in the same KB the markdown doc gets no summary at all — - its chunk count is exactly the raw chunks; the yaml doc has exactly - one ``is_summary`` row and its raw chunk count is untouched by the - summary.""" + """Phase 118 (A2): in the same KB the markdown doc gets a summary TOO + — the phase-30 non-markdown-only scope is retired (markdown docs + backfill + summarize like every other doc): the same byte-stable + digest + pointer line, exactly one ``is_summary`` chunk, and the raw + chunk count untouched. The yaml doc keeps its exactly-one summary + row with its raw chunks untouched.""" _reset_db() summary = _run_in_thread(_import_fixtures(mock_llm)) assert summary.added == 2 + assert summary.summaries == 2 # A2: the markdown doc is summarized too md_content = (FIXTURES / MD_PATH).read_text(encoding="utf-8") yaml_content = (FIXTURES / YAML_PATH).read_text(encoding="utf-8") @@ -235,11 +269,20 @@ def test_markdown_control_doc_gets_no_summary_chunk( md_doc = _doc(db, MD_PATH) yaml_doc = _doc(db, YAML_PATH) md_chunks = [c for c in md_doc.chunks if not c.is_summary] + md_summary = [c for c in md_doc.chunks if c.is_summary] yaml_raw = [c for c in yaml_doc.chunks if not c.is_summary] yaml_summary = [c for c in yaml_doc.chunks if c.is_summary] - # Markdown: never summarized (phase 30 scope — A9 non-markdown only). - assert md_doc.summary is None + # Markdown: NOW summarized (phase 118 A2) — the same byte-stable + # digest + deterministic pointer line, exactly one is_summary chunk + # (position −1, embedded), raw chunks untouched. + expected_md_summary = _expected_summary(md_content, SOURCE, MD_PATH) + assert md_doc.summary == expected_md_summary + assert len(md_summary) == 1 + assert md_summary[0].position == -1 + assert md_summary[0].embedding is not None + assert md_summary[0].content == expected_md_summary + assert expected_md_summary.endswith(f"\nSource: {SOURCE}/{MD_PATH}") assert len(md_chunks) == len( chunk_document(md_content, MD_PATH, CHUNK_TARGET, CHUNK_OVERLAP) ) diff --git a/tests/e2e/test_header_consistency.py b/tests/e2e/test_header_consistency.py index 9e620e4..af2fce7 100644 --- a/tests/e2e/test_header_consistency.py +++ b/tests/e2e/test_header_consistency.py @@ -201,7 +201,13 @@ def test_viewer_header_content_still_fits( assert _box_height(page, "#doc-title") < 30 # one line at either size # Meta row: badges + path present and visible, single line. - assert _box_height(page, ".doc-meta") < 26 + # The bound catches a WRAP (a second line would be ≥ ~44px), + # not a pixel-exact line height: the row is content-sized by + # its tallest child — the native type=date input (phase 106 + # D8), whose metrics are Chromium/font-dependent and render + # the single line at ~26px on this host (the pre-phase 26px + # pin red-lined on the 26.125px measurement, 2026-09-16). + assert _box_height(page, ".doc-meta") < 34 expect(page.locator(".doc-source-badge", has_text="docs")).to_be_visible() expect(page.locator(".format-badge", has_text="md")).to_be_visible() expect(page.locator(".doc-path", has_text="homelab/kubernetes.md")).to_be_visible() diff --git a/tests/e2e/test_ls_tree_drilldown.py b/tests/e2e/test_ls_tree_drilldown.py index 89a0260..a6e16e7 100644 --- a/tests/e2e/test_ls_tree_drilldown.py +++ b/tests/e2e/test_ls_tree_drilldown.py @@ -57,14 +57,18 @@ Test → story mapping (Playwright Mapping Rule): ``source: X | path: Y | title: Z`` file lines), and the grounded ``read(alpha/two/two-a.md)`` (the ``📄 Reading …`` line + the answer citing the document, the phase-37 assertion pattern). The read - target is DELIBERATELY a top-2 retrieval document for its question - (the question names the file's path, so the file self-matches the - hybrid gate deterministically): the agent's phase-72 dedupe returns - ``ALREADY_IN_CONTEXT`` (a refusal — counts in nothing), and the - mock answers FROM THE ```` PROMPT with the same citation - shape (``Already in context: Read . ``) — the - document text reached the model either way, and the prefix pins - that the dedupe notice itself reached it. + target is DELIBERATELY a suggested (summary-seeded) document for + its question (the question names the file's path, so the file + self-matches the hybrid gate and takes rank 1 deterministically): + phase 118 (A6) — the seed is a SUMMARY, not full text, so the read + SUCCEEDS (the phase-72 ALREADY_IN_CONTEXT dedupe fires only for a + document already READ in the turn — the retired top-2 seed-read + refusal is gone) and the full text arrives through the ``read`` + tool; the mock answers from the READ RESULT with the phase-37 + citation shape (``Read . `` — the mock skips the phase-106 D5 ``date:`` line, so the + quote is pure content, byte-identical to the retired + answer-from-prompt quote). 2. ``test_wide_folder_holds_the_fifty_line_cap`` — ``ls(alpha/wide)`` on the 51-file folder: the mock's echo carries exactly 50 file lines + the ``…and 1 more documents in this folder — use grep (pattern)…`` @@ -226,12 +230,16 @@ READ_QUESTION = f"Drill down the tree: read {READ_SP} — read the file" WIDE_QUESTION = f"Drill down the tree: ls {ALPHA}/wide — how many files does this folder hold?" NOPE_QUESTION = f"Drill down the tree: ls {ALPHA}/nope — is there such a folder?" -#: The read target is a top-2 retrieval document for its question (the -#: question names the path — the file self-matches the hybrid gate -#: deterministically), so the read gets the phase-72 ALREADY_IN_CONTEXT -#: dedupe and the mock answers from the ```` prompt with the -#: same citation shape, prefixed (the suite pins the dedupe path). -READ_ANSWER_PREFIX = f"Already in context: Read {READ_SP}." +#: The read target is a suggested (summary-seeded) document for its +#: question (the question names the path — the file self-matches the +#: hybrid gate and takes rank 1 deterministically). Phase 118 (A6): +#: the seed is a summary, not full text — a first read of a suggested +#: document SUCCEEDS, the full text arrives through the read tool, and +#: the mock answers from the READ RESULT with the phase-37 citation +#: shape (the mock skips the phase-106 D5 ``date:`` line, so the quote +#: is pure document content — byte-identical to the retired +#: answer-from-prompt quote, which the suite pins). +READ_ANSWER_PREFIX = f"Read {READ_SP}." READ_ANSWER_QUOTE = TWO_A_CONTENT[:80] # -------------------------------------------------------------------------- @@ -658,14 +666,15 @@ def test_drill_down_sources_folders_files_and_read( {"type": "tool", "name": "read", "argument": READ_SP} ], _tool_frames(frames) bubble = _last_brain(page).locator(".bubble") - # The dedupe notice reached the model (the prefix pins it — the read - # was refused as ALREADY_IN_CONTEXT because the target is a top-2 - # retrieval document)… + # Phase 118 (A6): the read SUCCEEDED — the seeds are summaries, + # not full text, so the phase-72 ALREADY_IN_CONTEXT refusal (only + # for a document already READ in the turn) did not fire — expect(bubble).to_contain_text(READ_ANSWER_PREFIX) - # …and the answer still cites the document: the mock quotes the - # FIRST 80 chars of the target's text from the ```` - # prompt (the refusal's instruction — answer from that text; the - # quote is newline-free, pinned above). + expect(bubble).not_to_contain_text("Already in context") + # — and the answer cites the document from the READ RESULT: the mock + # quotes the FIRST 80 chars of the document's own content (the full + # text arrived through the read tool; the quote is newline-free, + # pinned above). expect(bubble).to_contain_text(READ_ANSWER_QUOTE) done = next(f for f in frames if f.get("type") == "done") assert done["deflected"] is False, done diff --git a/tests/e2e/test_navbar_refresh.py b/tests/e2e/test_navbar_refresh.py index d4544bc..ee679d3 100644 --- a/tests/e2e/test_navbar_refresh.py +++ b/tests/e2e/test_navbar_refresh.py @@ -623,8 +623,11 @@ def test_api_created_chats_carry_the_bor_chat_v1_shape( assert body["message_count"] == 2 # The bor.chat.v1 record shape: who/text present; only the schema's # optional keys may accompany them (explicit nulls are preserved — - # the plain model_dump round-trips byte-identical). - allowed = {"who", "text", "sources", "deflected", "suggestions", + # the plain model_dump round-trips byte-identical). ``related`` is + # the phase-113 related-doc tier the UI persists with grounded + # brain records (accepted by the ChatMessage schema — its earlier + # absence 422'd the done-time auto-save, phase-118 verification fix). + allowed = {"who", "text", "sources", "related", "deflected", "suggestions", "thinking", "tools", "stopped"} assert all({"who", "text"} <= set(m) <= allowed for m in body["messages"]) # The History row renders it (the Open link's text IS the title). diff --git a/tests/e2e/test_read_truncation_cap.py b/tests/e2e/test_read_truncation_cap.py index 472472a..7d68c82 100644 --- a/tests/e2e/test_read_truncation_cap.py +++ b/tests/e2e/test_read_truncation_cap.py @@ -36,16 +36,14 @@ the same host) registered as a local-directory source (the ``test_local_directory_sources.py`` registration + real-Sync pattern — registration through the authenticated API, the real in-process ``POST /api/sync`` pipeline; no git anywhere), with THREE documents -whose bodies are token-controlled so the phase-72 ALREADY_IN_CONTEXT -dedupe (the read target is refused when it is a top-2 retrieval seed) -NEVER fires — each scripted read target must actually execute, not be -refused: +whose bodies are token-controlled so every scripted turn stays +GROUNDED (the hybrid gate HIGH — the mock's read-cap flow needs the +```` section): * ``anchor-2024.md`` — the retrieval ANCHOR: a digit-bearing name. Both questions name "anchor 2024", whose joined normalized token - (``anchor2024``) name-hits this document — the name-hit list LEADS - the lexical side, so the anchor is the #1 seed of BOTH turns - (grounding: the name hit's ``fts_hit`` keeps the gate HIGH, the + (``anchor2024``) name-hits this document — the name hit's + ``fts_hit`` keeps the gate HIGH for every turn (grounding: the ```` section is present, and the anchor is never a read target); * ``zz-capped.md`` — the SUBJECT: ~3 100 chars of varied rotation @@ -53,18 +51,19 @@ refused: ``chars_total`` the assertions use is ``len(CAP_DOC)`` of this very string, and ``synced_kb`` pins the stored content byte-identical to it). Its body avoids EVERY token of both questions, so on its own - (turn 1) question it has zero lexical hits and only the common-word - cosine — it is the #3 fused candidate, NOT a seed; + question it has zero lexical hits and only the common-word cosine — + it ranks below the anchor (irrelevant to the cap's contract: phase + 118, A6 — the seeds are SUMMARIES, so a first ``read`` of ANY + document, suggested or not, succeeds, and the cap applies to + everything ``read`` returns — the retired top-2 seed-read refusal + that used to constrain this design is gone); * ``aa-short.md`` — the CONTROL: ~200 chars, under the cap. -The turn-specific flavor words pick the #2 seed: turn 1's question -carries ``note, long form`` (planted in the SHORT doc's body) and turn -2's carries ``quick pass`` (planted in the CAPPED doc's body), so each -turn's NON-target document wins the second seed slot on its own -question and the target stays #3/#6. ``synced_kb`` pins this design -with the app's real hybrid retrieval (``_assert_target_not_seed`` — a -fixture-text regression that makes a target a seed fails at setup with -a clear message, not at the wire assertions). +The turn-specific flavor words (``note, long form`` planted in the +SHORT doc's body, ``quick pass`` in the CAPPED doc's) are inert +leftovers of the retired not-a-seed design — they no longer select a +seed slot (phase 118: a read of a suggested document succeeds, so the +target's seed status never changes the wire contract). Test → story mapping (Playwright Mapping Rule; the story is the owner TODO item — one Playwright file per story, A16): @@ -116,7 +115,6 @@ from e2e.conftest import ( USE_REAL_LLM, _wait_http, ) -from e2e.mock_llm import embed_text REPO = Path(__file__).resolve().parents[2] @@ -163,9 +161,10 @@ def _cap_doc() -> str: every token of BOTH questions (``read``, ``capped``, ``document``, ``capkb``, ``zz``, ``md``, ``anchor``, ``2024``, ``note``, ``long``, ``form``, ``aa``, ``short``, ``quick``, ``pass``, - ``wire``, ``save``, ``control``, ``check``) — the target must have - zero lexical hits on its own turn so the dedupe cannot refuse the - read.""" + ``wire``, ``save``, ``control``, ``check``) — the target has zero + lexical hits on its own turn, so it ranks below the anchor (phase + 118: its seed status is irrelevant — a first read of any document + succeeds; the anchor keeps the gate HIGH).""" topics = [ "vault", "mirror", "raid", "pool", "drive", "chain", "slot", "cycle", "guard", "probe", @@ -226,7 +225,8 @@ NOTICE = READ_TRUNCATION_NOTICE.format(shown=READ_CAP, total=CAP_DOC_TOTAL) # The scripted turns (the mock's ``READ_CAP_TRIGGER`` questions — each # carries its own tool call after the colon; the turn-specific flavor -# words are the #2-seed selectors, see the module docstring). +# words are inert leftovers of the retired #2-seed design — see the +# module docstring). Q_WIRE = ( f"Read the capped document: read {CAPPED_SP} — " "anchor 2024 note, long form, wire check" @@ -359,26 +359,6 @@ def _truncate_all() -> None: db.commit() -def _assert_target_not_seed(question: str, target_rel: str) -> None: - """Pin the retrieval-anchor design (see the module docstring) with - the app's REAL hybrid retrieval over the mock's embeddings - (deterministic): the scripted read target must NOT be a top-2 seed - for its own question — the phase-72 ALREADY_IN_CONTEXT dedupe would - refuse the read and the cap would never fire (the turn would echo - the refusal instead). A fixture-text regression that breaks this - fails here, at setup, with a clear message.""" - from app.rag.retriever import retrieve, select_documents - - with SessionLocal() as db: - seeds = select_documents(retrieve(db, question, embed_text(question))) - paths = [f"{d.source}/{d.path}" for d in seeds] - assert f"{SOURCE}/{target_rel}" not in paths, ( - f"the read target {SOURCE}/{target_rel} is a top-2 seed for its own " - f"question — the ALREADY_IN_CONTEXT dedupe would refuse the read and " - f"the cap would never fire (seeds: {paths})" - ) - - def _wait_sync_done_http(client: httpx.Client, timeout_s: float = 180.0) -> dict[str, Any]: """Poll the (cookie-authenticated) status endpoint until the run reaches a terminal state (the test_ls_tree_drilldown pattern, over @@ -403,8 +383,10 @@ def synced_kb(app_server: str, cap_dirs: Path) -> None: runs the REAL in-process sync (``POST /api/sync`` — walk → chunk → embed → overview → version bump), pins the stored content byte-identical to the fixture strings (the ``chars_total`` - assumption), and pins the retrieval-anchor design for all three - scripted questions (the dedupe never fires).""" + assumption). Phase 118: the retrieval-anchor design only pins the + gate's grounding (the anchor keeps every turn HIGH) — a read of a + suggested document succeeds, so the targets' seed status no longer + constrains the suite.""" _truncate_all() with httpx.Client(base_url=app_server, timeout=30.0) as client: r = client.post("/api/login", json={"password": ADMIN_PASSWORD}) @@ -436,11 +418,6 @@ def synced_kb(app_server: str, cap_dirs: Path) -> None: ) ) assert stored == expected, f"stored content drifted for {rel}" - # The retrieval-anchor design (the module docstring): every scripted - # read target stays OUT of its own question's top-2 seeds. - _assert_target_not_seed(Q_WIRE, CAPPED_REL) - _assert_target_not_seed(Q_SAVE, CAPPED_REL) - _assert_target_not_seed(Q_CONTROL, SHORT_REL) @pytest.fixture(autouse=True) @@ -681,9 +658,10 @@ def test_truncated_read_frame_order_live_marker_and_llm_notice( assert i_tool < i_result < i_delta, (i_tool, i_result, i_delta) done = next(f for f in frames if f.get("type") == "done") assert done["deflected"] is False, done - # The read document is the turn's cited source even though it was - # NOT a retrieval seed (the anchor design) — retrieval + agent-read, - # deduped (the grounded-turn record). + # The read document is the turn's cited source (retrieval + + # agent-read, deduped — the grounded-turn record; phase 118: the + # target may be a suggested (summary-seeded) document — its seed + # status never changes the cap's wire contract). assert any( s["path"] == CAPPED_REL and s["source"] == SOURCE for s in done["sources"] ), done["sources"] diff --git a/tests/e2e/test_share_chat.py b/tests/e2e/test_share_chat.py index a7133bd..7cbc944 100644 --- a/tests/e2e/test_share_chat.py +++ b/tests/e2e/test_share_chat.py @@ -346,9 +346,10 @@ def test_anonymous_shared_view( expect(think.first).not_to_have_attribute("open") # Source chips are PLAIN TEXT: the on-topic turn carries its - # source chips (top-2 docs — the hybrid retrieval), but every - # one as a : zero anywhere (a guest - # cannot open documents; the documents API is admin-only). + # source chips (the suggested docs — the hybrid retrieval's + # summary-seed tier, phase 118), but every one as a : + # zero anywhere (a guest cannot open documents; + # the documents API is admin-only). assert ( anon.locator(".msg.brain .source-chip").count() >= 1 ), "the grounded turn must carry its source chips" diff --git a/tests/e2e/test_source_chip_quality.py b/tests/e2e/test_source_chip_quality.py index 8e21645..b404821 100644 --- a/tests/e2e/test_source_chip_quality.py +++ b/tests/e2e/test_source_chip_quality.py @@ -1,35 +1,44 @@ -"""Phase 113 E2E (Playwright): the source-chip quality contract (TODO L5 + -L2c) — the usefulness bar + the de-emphasized related-docs row, as VISIBLE -chip counts. +"""Phase 113 E2E (Playwright) — phase 118 re-targeted: the citation-surface +contract (LOCKED A4) as VISIBLE chip counts — the summary-seed contract +replaced the phase-112/113 usefulness bar: the chip row is the +SUGGESTED tier (top-5 distinct docs, NO floor) + agent reads (none on +these turns), the related row is rank 6+ (capped at +``related_max_docs`` = 2), and a deflected turn still cites nothing +(done.sources = [] — its weak hits are suggested for the durable record +but are never citation chips). Run in isolation (DB must be up: ``podman compose up -d db``): uv run pytest tests/e2e/test_source_chip_quality.py -v --no-cov -Acceptance (TODO L144–146): "for a single-document question, the turn -shows one citation chip"; a weak 2nd doc renders only in the -de-emphasized related row (``.related-doc`` links, NEVER ``.source-chip``); -a deflected turn shows zero citation chips (its weak hits, if any, live in -the related row). +Acceptance (TODO L144–146, phase-118 shape): a grounded turn's chip row +is the suggested tier — the 5 docs the model was seeded with (its +"start here" set — the old "one chip per bar-clearing doc" is retired +with the full-text seeds; the LLM decides what the summaries earn); +a deflected turn shows zero citation chips (its weak hits, if any, live +in the de-emphasized related row — ``.related-doc`` links, NEVER +``.source-chip``; L2c: never render weak hits as answer citations). The fixture KB's tier shapes are deterministic under the E2E mock's -bag-of-words embeddings + the mock-calibrated bar (conftest: -``BOR_SOURCE_USEFULNESS_FLOOR=0.15``, half the 0.30 threshold — like the -lexical floor): +bag-of-words embeddings + the fused rank walk (probe-verified, pinned +here against the wire): -* **single-source question** — "What SSH aliases do I have?": - ``ssh_aliases.txt`` is the ONLY doc whose best-chunk cosine clears the - bar (0.352 ≥ 0.15; grounded at 0.352 ≥ 0.30) → the done frame carries - exactly ONE cited ref; the two below-bar docs (gitlab-compose.yaml - 0.025, uptime_probe.py 0.113) ride the related tier. This IS the - strong+weak two-tier shape on the wire (the four OBSERVED live shapes - are unit-pinned at plan level in - ``tests/unit/test_source_chip_quality.py`` — the fixture KB reproduces - the same shape live, so no docstring caveat is needed). +* **grounded question** — "What SSH aliases do I have?": best cosine + 0.352 ≥ 0.30 → grounded; the suggested tier (top-5, NO floor) is + ssh_aliases.txt > gitlab-compose.yaml > uptime_probe.py > kubernetes.md + > backups.md (fused rank — the lexical-only cosine-0.0 docs rank when + they rank: no floor filters them, LOCKED A3) and the done frame + carries EXACTLY those five cited refs; the rank-6+ remainder + (compose.container, static-dns.json) rides the related row, capped at + two. (The four OBSERVED live shapes are unit-pinned at plan level in + ``tests/unit/test_source_chip_quality.py`` — the fixture KB + reproduces the tiered shape live, so no docstring caveat is needed.) * **deflected question** — "How do I bake sourdough bread?": best cosine - 0.109 < 0.30 and zero FTS hits → honest deflection; nothing clears the - bar → zero cited refs; the weak hits (new-service.md 0.109, - ssh_aliases.txt 0.050) ride the related row. + 0.109 < 0.30 and zero FTS hits → honest deflection → zero cited refs + (done.sources = []); the weak hits ARE suggested (no floor) for the + durable record — new-service.md > ssh_aliases.txt > lan.network > + uptime_probe.py > compose.container — and the rank-6+ remainder + (backups.md, kubernetes.md) rides the related row. """ from __future__ import annotations @@ -97,12 +106,14 @@ def _ask(page: Page, message: str) -> None: def test_single_source_question_shows_exactly_one_citation_chip( page: Page, app_url: str, mock_llm: int, db_ready: None ) -> None: - """The acceptance pin: a single-document question → the done bubble - carries EXACTLY ONE ``.source-chip`` (the bar-clearing doc) and the - below-bar docs render only in the de-emphasized ``.related-docs`` row - (``.related-doc`` links — never ``.source-chip``), labeled - "Nearby docs, in case:". The durable record keeps the FULL retrieval - (LOCKED A3).""" + """The acceptance pin (phase 118, A4): a grounded question → the done + bubble carries EXACTLY the suggested tier as ``.source-chip``s (the + five docs the model was seeded with — top-5, NO floor: the old + usefulness-bar "one chip" is retired with the full-text seeds) and + the rank-6+ remainder renders only in the de-emphasized + ``.related-docs`` row (``.related-doc`` links — never + ``.source-chip``), labeled "Nearby docs, in case:". The durable + record keeps the FULL retrieval (LOCKED A3).""" _reset_db(mock_llm) page.set_default_timeout(30_000) login(page, app_url, next="/") # phase 79: chat is require_user-gated @@ -116,52 +127,64 @@ def test_single_source_question_shows_exactly_one_citation_chip( # Grounded: no deflected bubble at all. expect(page.locator(".msg.brain.is-deflected")).to_have_count(0) - # EXACTLY ONE citation chip — the bar-clearing doc (the 2nd chip of - # the pre-phase turn demoted to the row; the acceptance criterion). + # The citation surface IS the suggested tier (LOCKED A4 — top-5, + # NO floor): five chips, in fused rank order — the model was + # seeded with exactly these five docs' summaries. chip = page.locator(".msg.brain .source-chip") - expect(chip).to_have_count(1, timeout=30_000) - expect(chip.first).to_contain_text("ssh_aliases.txt") + expect(chip).to_have_count(5, timeout=30_000) + expect(chip.nth(0)).to_contain_text("ssh_aliases.txt") + expect(chip.nth(1)).to_contain_text("gitlab-compose.yaml") + expect(chip.nth(2)).to_contain_text("uptime_probe.py") + expect(chip.nth(3)).to_contain_text("kubernetes.md") + expect(chip.nth(4)).to_contain_text("backups.md") - # The below-bar docs ride the related row: a labeled, de-emphasized - # list — one .related-doc link per doc (rank order, capped at - # related_max_docs = 2), never a .source-chip. + # The rank-6+ remainder rides the related row: a labeled, + # de-emphasized list — one .related-doc link per doc (rank order, + # capped at related_max_docs = 2), never a .source-chip. row = page.locator(".msg.brain .related-docs") expect(row).to_have_count(1) expect(row.first).to_have_attribute("aria-label", "Nearby docs, in case") expect(row.first.locator(".related-docs-label")).to_have_text("Nearby docs, in case:") links = page.locator(".msg.brain .related-docs .related-doc") expect(links).to_have_count(2, timeout=30_000) - expect(links.nth(0)).to_contain_text("gitlab-compose.yaml") - expect(links.nth(1)).to_contain_text("uptime_probe.py") + expect(links.nth(0)).to_contain_text("compose.container") + expect(links.nth(1)).to_contain_text("static-dns.json") expect(page.locator(".msg.brain .related-docs .source-chip")).to_have_count(0) # The related links keep the chip's /document.html href + identity. expect(links.first).to_have_attribute( - "title", "docs/homelab/container_gitlab/gitlab-compose.yaml" + "title", "docs/homelab/quadlet/compose.container" ) - # Durable record: not deflected; the FULL retrieval (cited + related) - # is logged — query_log records retrieval, not citations (LOCKED A3). + # Durable record: not deflected; the FULL retrieval (suggested + + # related + read, deduped) is logged — query_log records retrieval, + # not citations (LOCKED A3). with SessionLocal() as db: row_log = db.scalars(select(QueryLog)).one() assert row_log.question == SINGLE_SOURCE_QUESTION assert row_log.deflected is False - for path in ( - "homelab/ssh/ssh_aliases.txt", - "homelab/container_gitlab/gitlab-compose.yaml", - "homelab/scripts/uptime_probe.py", - ): - assert path in row_log.sources + # Suggested tier + related remainder, in the logged order. + assert row_log.sources == ( + "docs/homelab/ssh/ssh_aliases.txt, " + "docs/homelab/container_gitlab/gitlab-compose.yaml, " + "docs/homelab/scripts/uptime_probe.py, " + "docs/homelab/kubernetes.md, " + "docs/homelab/backups.md, " + "docs/homelab/quadlet/compose.container, " + "docs/homelab/networking/static-dns.json" + ), row_log.sources def test_deflected_question_shows_zero_citation_chips( page: Page, app_url: str, mock_llm: int, db_ready: None ) -> None: - """The acceptance pin: a deflected turn (known-out-of-KB) → ZERO - ``.source-chip`` elements under the bubble; the weak hits live in the - related row only — its links are ``.related-doc``, never - ``.source-chip`` (L2c: "at minimum: never render them as answer - citations"). The "Maybe try" chips and the durable record are - unchanged.""" + """The acceptance pin (phase 118, A4): a deflected turn + (known-out-of-KB) → ZERO ``.source-chip`` elements under the bubble + (done.sources = [] — a deflected answer cites nothing; the weak hits + are suggested for the durable record but are scored docs, not + citations); the rank-6+ weak hits live in the related row only — + its links are ``.related-doc``, never ``.source-chip`` (L2c: "at + minimum: never render them as answer citations"). The "Maybe try" + chips are unchanged.""" _reset_db(mock_llm) page.set_default_timeout(30_000) login(page, app_url, next="/") # phase 79: chat is require_user-gated @@ -181,21 +204,35 @@ def test_deflected_question_shows_zero_citation_chips( row = page.locator(".msg.brain .related-docs") expect(row).to_have_count(1, timeout=30_000) links = page.locator(".msg.brain .related-docs .related-doc") - expect(links).to_have_count(2) # the weak hits, rank order, capped at 2 - expect(links.nth(0)).to_contain_text("new-service.md") - expect(links.nth(1)).to_contain_text("ssh_aliases.txt") + # The rank-6+ remainder of the weak hits, rank order, capped at 2 — + # the top five weak hits are suggested (no floor) for the durable + # record, but a deflected turn cites nothing, so they never render + # as chips. + expect(links).to_have_count(2) + expect(links.nth(0)).to_contain_text("backups.md") + expect(links.nth(1)).to_contain_text("kubernetes.md") # ZERO citation chips under the bubble — the weak hits are scored # docs, not citations (the phase-112/113 contract on the wire). expect(page.locator(".msg.brain .source-chip")).to_have_count(0) expect(page.locator(".msg.brain .related-docs .source-chip")).to_have_count(0) - # Durable record: deflected, weak top score — the retrieval stays - # logged for threshold tuning (LOCKED A3: observability unchanged). + # Durable record: deflected, weak top score — the FULL retrieval + # (the weak hits ARE suggested — no floor — plus the rank-6+ related + # remainder) stays logged for threshold tuning (LOCKED A3: + # observability unchanged). with SessionLocal() as db: row_log = db.scalars(select(QueryLog)).one() assert row_log.question == OFF_TOPIC assert row_log.deflected is True assert 0.0 < row_log.top_score < get_settings().relevance_threshold assert row_log.fts_hits == 0 - assert row_log.sources # the weak-hit paths, for threshold tuning + assert row_log.sources == ( + "docs/deployments/new-service.md, " + "docs/homelab/ssh/ssh_aliases.txt, " + "docs/homelab/quadlet/lan.network, " + "docs/homelab/scripts/uptime_probe.py, " + "docs/homelab/quadlet/compose.container, " + "docs/homelab/backups.md, " + "docs/homelab/kubernetes.md" + ), row_log.sources diff --git a/tests/e2e/test_summary_in_viewer.py b/tests/e2e/test_summary_in_viewer.py index b9a1ceb..7555933 100644 --- a/tests/e2e/test_summary_in_viewer.py +++ b/tests/e2e/test_summary_in_viewer.py @@ -16,9 +16,10 @@ reused here as closely as possible): ``documents.summary``. The sentinel ``RESE-SUMMARY-SENTINEL-7f3a`` sits on the document's LAST line — **outside** the 24-token digest — so it is a marker for "the original, not the summary". -* ``notes/qwen-llamacpp-notes.md`` — a markdown control doc, never - summarized (phase 30 scope): the viewer must render it exactly as - before, with no Summary panel. +* ``notes/qwen-llamacpp-notes.md`` — a markdown doc that phase 118 (A2) + summarizes TOO (the phase-30 non-markdown-only scope is retired): the + viewer must show its Summary panel exactly like the yaml's, above the + rendered markdown. This phase only surfaces the stored field: the content endpoint returns ``summary`` (task 01) and the shared ``renderDocument`` core draws the @@ -27,8 +28,8 @@ labeled ``.doc-summary`` panel above the content on BOTH surfaces (task exactly that contract. Phase 79 supersedes the phase-16 soft rule: the content endpoint is ``require_user``-gated, so the viewer surfaces and the API shape pin run under a signed-in session (the shape itself — -``summary`` for the yaml, ``null`` for the markdown control — is -unchanged). +``summary`` for BOTH fixture docs, phase 118 A2 — is what the pins +carry). """ from __future__ import annotations @@ -98,13 +99,14 @@ def _run_in_thread(coro: Any) -> Any: def _reset_db_and_import(mock_llm: int) -> ImportSummary: """Truncate the KB (and query log + steering) and re-import the - summary_kb fixtures — yaml summarized, md control not.""" + summary_kb fixtures — both docs summarized (phase 118, A2: the + markdown doc too).""" with SessionLocal() as db: db.execute(text("TRUNCATE chunks, documents, query_log, steering_notes")) db.commit() summary = _run_in_thread(_import_fixtures(mock_llm)) - assert summary.added == 2 # yaml + md control - assert summary.summaries == 1 and summary.summary_errors == 0 + assert summary.added == 2 # yaml + md + assert summary.summaries == 2 and summary.summary_errors == 0 # A2: md too assert summary.errors == 0 return summary @@ -279,19 +281,30 @@ def test_modal_shows_panel_and_full_page_agrees( # --------------------------------------------------------------------------- -def test_markdown_doc_has_no_summary_panel( +def test_markdown_doc_has_a_summary_panel( page: Page, app_url: str, mock_llm: int, db_ready: None ) -> None: - """Control doc (markdown — never summarized): NO .doc-summary - element on either surface, and the content renders exactly as - before (first child of the content container is the doc body).""" + """Phase 118 (A2): the markdown doc is summarized TOO — the SAME + labeled ``.doc-summary`` panel (the deterministic mock digest + + pointer line) renders above the rendered markdown on BOTH surfaces + (the phase-30 "markdown never summarized" control is retired with + the non-markdown-only scope).""" _reset_db_and_import(mock_llm) page.set_default_timeout(30_000) login(page, app_url, next="/") # phase 79: the viewer content is gated - # Full page: no panel, markdown column untouched. + digest_line, pointer_line = _summary_lines(SOURCE, MD_PATH) + + # Full page: the labeled panel above the markdown column (the same + # contract as the yaml — one shared renderer). page.goto(f"{app_url}/document.html?source={SOURCE}&path={MD_URL_PATH}") - expect(page.locator(".doc-summary")).to_have_count(0) + panel = page.locator(".doc-summary") + expect(panel).to_have_count(1) + expect(panel).to_be_visible() + expect(panel).to_have_attribute("aria-label", "Summary") + expect(panel.locator(".doc-summary-title")).to_have_text("Summary") + expect(panel).to_contain_text(digest_line) + expect(panel).to_contain_text(pointer_line) expect(page.locator("#doc-content .doc-md")).to_have_count(1) expect(page.locator("#doc-content")).to_contain_text( "came out of a week of" @@ -300,10 +313,10 @@ def test_markdown_doc_has_no_summary_panel( "() => [...document.querySelector('#doc-content').children]" ".map((el) => el.className)" ) - assert order == ["doc-md"], f"markdown doc gained children: {order}" + assert order == ["doc-summary", "doc-md"], f"panel not first: {order}" - # Modal: same story — no panel, .doc-md is the sole content child. - # (the session is already signed in — the form login above) + # Modal: same story — the panel above .doc-md (the session is + # already signed in — the form login above). page.goto(f"{app_url}/sources.html") # Phase 97: the re-mount lands on the tree's top level — drill to # the notes level where the row lives (the drill is the only @@ -314,7 +327,11 @@ def test_markdown_doc_has_no_summary_panel( expect(row).to_have_count(1) row.locator("td:nth-child(2) a.doc-link").click() expect(page.locator(".doc-modal")).to_be_visible() - expect(page.locator("#doc-modal .doc-summary")).to_have_count(0) + modal_panel = page.locator("#doc-modal .doc-summary") + expect(modal_panel).to_have_count(1) + expect(modal_panel).to_be_visible() + expect(modal_panel).to_contain_text(digest_line) + expect(modal_panel).to_contain_text(pointer_line) expect(page.locator("#doc-modal .doc-md")).to_have_count(1) expect(page.locator("#doc-modal-content")).to_contain_text( "came out of a week of" @@ -323,7 +340,7 @@ def test_markdown_doc_has_no_summary_panel( "() => [...document.querySelector('#doc-modal-content').children]" ".map((el) => el.className)" ) - assert order == ["doc-md"], f"markdown modal gained children: {order}" + assert order == ["doc-summary", "doc-md"], f"markdown modal order: {order}" # --------------------------------------------------------------------------- @@ -336,10 +353,9 @@ def test_content_api_summary_shape( page: Page, app_url: str, mock_llm: int, db_ready: None ) -> None: """``GET /api/documents/content`` carries ``summary`` — the string - for the summarized yaml, ``null`` for the markdown control. Phase 79 - superseded the phase-16 soft rule: the endpoint is require_user, so - the pin runs under the form-login session (the shape itself is - unchanged).""" + for BOTH fixture docs (phase 118, A2: the markdown doc too). Phase + 79 superseded the phase-16 soft rule: the endpoint is require_user, + so the pin runs under the form-login session.""" _reset_db_and_import(mock_llm) login(page, app_url, next="/") @@ -364,5 +380,10 @@ def test_content_api_summary_shape( assert resp_md.status == 200 md_body = resp_md.json() assert md_body["format"] == "md" - assert md_body["summary"] is None, "markdown docs never carry a summary" + # Phase 118 (A2): the markdown doc carries its summary too — the + # same byte-stable digest + pointer line. + md_digest_line, _ = _summary_lines(SOURCE, MD_PATH) + assert md_body["summary"] == ( + f"{md_digest_line}\nSource: {SOURCE}/{MD_PATH}" + ) assert "came out of a week of" in md_body["content"] diff --git a/tests/e2e/test_summary_seed_context.py b/tests/e2e/test_summary_seed_context.py new file mode 100644 index 0000000..f933064 --- /dev/null +++ b/tests/e2e/test_summary_seed_context.py @@ -0,0 +1,671 @@ +"""Phase 118 task 06 E2E (Playwright, mock-only): the summary-seed +context — grounded turns seed SUMMARIES, full text arrives only via +``read`` (the owner directive, TODO.md L3; the retired phase-24 +full-text seeding is gone — locked A1/A6). + +Run in isolation (DB must be up: ``podman compose up -d db``): + + uv run pytest tests/e2e/test_summary_seed_context.py -v --no-cov + +MOCK-ONLY suite: ``E2E_REAL_LLM=1`` is not supported — the story's gate +is the deterministic contract, not the real model's behavior: the mock +LLM's tail-echo trigger (``END_OF_NOTES_TRIGGER``) quotes the last 160 +chars of the seeded ```` block, and the scripted +summary-read flow (``SUMMARY_SEED_READ_TRIGGER``) echoes the whole read +tool result — both are the house scripted-turn lenses on the LLM's +context (the mock is the only E2E lens), so what reached the prompt is +assertable byte-exactly. + +KB fixture — one host temp dir (``tmp_path_factory``; the app runs on +the same host) registered as a local-directory source (the +``test_ls_tree_drilldown.py`` / ``test_read_truncation_cap.py`` +registration + real-Sync pattern — registration through the +authenticated API, the real in-process ``POST /api/sync`` pipeline; no +git anywhere), with SEVEN markdown documents (every file markdown — +the locked A2 end-to-end proof: the mock ``SUMMARY_MODE`` digest must +land on every doc, markdown included) whose bodies are +token-controlled so the hybrid gate picks the intended suggested set +deterministically (``synced_kb`` pins it with the app's REAL +retrieval — a fixture-text regression that breaks the design fails at +setup with a clear message, not at the wire assertions): + +* each doc = a neutral 24-token prelude (no question tokens — the + mock's first-24-token digest therefore shares NOTHING with either + question, so a doc's embedded summary chunk carries no strength + tokens and never outranks that doc's own content chunks) + + ``seed vault rotation notes`` repeated *i* times (the strength + gradient) + six unique filler tokens + a unique tail SENTINEL on + the document's LAST line (outside the digest — the full-text + marker); +* the measured rank order (probe, stable across re-imports) is + ``doc-a > doc-d > doc-b > doc-c > doc-e > doc-f > doc-g`` for the + tail question (×1 strength gradient — md5-collision-reordered) and a + clean ``doc-a > doc-b > doc-c > doc-d > doc-e > doc-f > doc-g`` for + the read question (×3 gradient — the collision noise cannot cross + the wider gaps); the suggested tier (LOCKED A3: top-5, NO floor) is + the first five of each, the related tier (rank 6+, ``related_max_docs`` + = 2) is ``doc-f``/``doc-g`` in both. + +Test → contract mapping (the task's cases a–e; the story is the owner +TODO item — one Playwright file per story, A16): +1. ``test_summaries_seed_the_prompt_not_the_full_text`` — cases (a) + + (b) + (d): the tail-echo question's answer quotes the last 160 + chars of the seeded ```` block, which end in the LAST + suggested document's SUMMARY — the mock's byte-stable + ``SUMMARY_MODE`` digest tail + pointer line (case a: summaries + reached the prompt; case d: the markdown doc's block carries the + digest, not a content preview — the phase-30 digest shape, and the + ``synced_kb`` pin proves every doc's stored summary IS the digest). + The inverse of the retired phase-24 pin: NO document's tail + sentinel is in the echoed context — the full content of no + suggested doc ever reached the model (case b). The grounded turn + cites the 5 suggested docs (case e's chip surface, no read yet) and + renders the de-emphasized related row (rank 6+); the durable + record carries suggested + related. +2. ``test_read_suggested_doc_adds_full_text_and_cites`` — cases (c) + + (e): the scripted flow ``read``s the rank-1 suggested doc — the + read SUCCEEDS (phase 118: the seeds are summaries, not full text — + the retired top-2 seed-read refusal is gone) and the mock's + verbatim echo of the read result lands the tail sentinel in the + answer (case c: the full text now arrives through the ``read`` + tool, not the seed); the citation chips = suggested + read, + deduped — the read doc is among the suggested, so the chip row is + exactly the 5 suggested docs, and the related row renders rank 6+ + (case e); the durable record carries suggested + related + read. +""" +from __future__ import annotations + +import json +import os +import re +import subprocess +import sys +import time +from collections.abc import Iterator +from pathlib import Path +from typing import Any + +import httpx +import pytest +from playwright.sync_api import Locator, Page, expect +from sqlalchemy import select, text + +from app.config import Settings as _Settings +from app.db import SessionLocal +from app.models import Document, QueryLog +from app.rag.retriever import TRUNCATION_MARKER, retrieve, select_related, select_suggested +from e2e.auth_helpers import login +from e2e.conftest import ( + ADMIN_PASSWORD, + SESSION_SECRET, + USE_REAL_LLM, + _wait_http, +) +from e2e.mock_llm import TOKEN_RE, embed_text + +REPO = Path(__file__).resolve().parents[2] + +# Phase 79 (task 04, full inventory): the conftest session app owns its +# port in a combined run — this module app binds its own port instead +# (a same-port second uvicorn dies on bind and would drive the wrong +# server). Env-overridable. +APP_PORT = int(os.environ.get("E2E_APP_PORT_SEEDCTX", "8138")) +APP_URL = f"http://127.0.0.1:{APP_PORT}" + +SOURCE = "seedkb" # the local directory's basename = the source name +MOCK_ANSWER_MARKER = "Deterministic mock answer for E2E" + +# -------------------------------------------------------------------------- +# Fixture documents (deterministic, token-controlled — see the module +# docstring for the design and the measured rank order) +# -------------------------------------------------------------------------- + +#: 22 neutral tokens — together with the 2-token title they ARE the +#: mock's first-24-token digest, so no summary chunk shares a token +#: with either question (the summary chunks rank below every content +#: chunk; the digest is the byte-stable assertion surface). +PRELUDE = ( + "fixture preamble block one two three four five six seven eight nine ten " + "eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen" +) +assert len(TOKEN_RE.findall(PRELUDE.lower())) == 22 + +#: The strength gradient (each token a question token — ``seed``, +#: ``vault``, ``rotation``, ``notes``) — repeated *i* times in doc *i*. +PHRASE = "seed vault rotation notes" + +#: (path, title, phrase repeats, filler token, tail sentinel) — the +#: MEASURED rank order (probe, stable across re-imports) is +#: doc-a > doc-d > doc-b > doc-c > doc-e > doc-f > doc-g: the suggested +#: tier is the first five, the related tier the last two. +DOCS: list[tuple[str, str, int, str, str]] = [ + ("doc-a.md", "Zeta Alfa", 7, "fillera", "SEEDA-TAIL-7f3a"), + ("doc-b.md", "Zeta Bravo", 6, "fillerb", "SEEDB-TAIL-8c4d"), + ("doc-c.md", "Zeta Charlie", 5, "fillerc", "SEEDC-TAIL-9d5e"), + ("doc-d.md", "Zeta Delta", 4, "fillerd", "SEEDD-TAIL-0e6f"), + ("doc-e.md", "Zeta Echo", 3, "fillere", "SEEDE-TAIL-1f7a"), + ("doc-f.md", "Zeta Foxtrot", 2, "fillerf", "SEEDF-TAIL-2a8b"), + ("doc-g.md", "Zeta Golf", 1, "fillerg", "SEEDG-TAIL-3b9c"), +] +#: The suggested tier per scripted question (the MEASURED rank order — +#: probe-verified, stable across re-imports): the tail question's ×1 +#: strength gradient is md5-collision-reordered (d before b), the read +#: question's ×3 gradient is clean. Related is the rank-6+ remainder in +#: both (the gradient never puts a strength doc below the filler docs). +SUGGESTED_TAIL = ["doc-a.md", "doc-d.md", "doc-b.md", "doc-c.md", "doc-e.md"] +SUGGESTED_READ = ["doc-a.md", "doc-b.md", "doc-c.md", "doc-d.md", "doc-e.md"] +RELATED_PATHS = ["doc-f.md", "doc-g.md"] +LAST_SUGGESTED = "doc-e.md" # the tail-echo target (case a/d) +READ_TARGET = "doc-a.md" # rank 1 — a suggested doc (case c) +SENTINELS = [sentinel for _p, _t, _i, _f, sentinel in DOCS] +READ_SENTINEL = next(s for p, _t, _i, _f, s in DOCS if p == READ_TARGET) + + +def _doc_content(title: str, i: int, filler: str, sentinel: str) -> str: + return ( + f"# {title}\n" + f"\n{PRELUDE}\n" + f"\n{' '.join([PHRASE] * i)}\n" + f"\n{' '.join([filler] * 6)}\n" + f"\n{sentinel}\n" + ) + + +def _expected_summary(content: str, path: str) -> str: + """The mock lite model's byte-stable digest + the code pointer line + (mirrors ``mock_llm.compose_answer``'s ``SUMMARY_MODE`` branch — + first 24 tokens of the document content — plus the summarizer's + deterministic ``Source:`` line; no model output is ever trusted).""" + digest = " ".join(TOKEN_RE.findall(content.lower())[:24]) + return f"This document covers {digest}.\nSource: {SOURCE}/{path}" + + +#: The turn's questions (the mock's trigger phrases — see the module +#: docstring). The tail question carries ``END_OF_NOTES_TRIGGER``; its +#: strength tokens ground the turn (best cosine ≈ 0.55 ≥ the E2E 0.30 +#: threshold). The read question carries ``SUMMARY_SEED_READ_TRIGGER`` +#: with its own scripted call after the colon (the house scripted-turn +#: convention — ``_READ_CAP_CALL_RE`` / ``_DRILL_CALL_RE`` shape), +#: ``seed vault rotation notes`` × 3 for the same grounding. +TAIL_QUESTION = "Show the end of your notes about the seed vault rotation, please." +READ_QUESTION = ( + f"Read the suggested document: read {SOURCE}/{READ_TARGET} — " + f"{PHRASE} {PHRASE} {PHRASE}" +) +assert "show the end of your notes" in TAIL_QUESTION.lower() +assert "read the suggested document" in READ_QUESTION.lower() +assert re.fullmatch(r"[a-z0-9_./-]+", f"{SOURCE}/{READ_TARGET}") + +# -------------------------------------------------------------------------- +# Fixtures +# -------------------------------------------------------------------------- + + +@pytest.fixture(scope="module") +def seed_dirs(tmp_path_factory: pytest.TempPathFactory) -> Path: + """The story's local-directory source: one host temp dir (the app + server runs on the same host, so the path is visible to it) holding + the seven token-controlled markdown documents. The directory's + basename is the source name (``kind=local``, phase 38).""" + root = tmp_path_factory.mktemp("bor_seed_ctx") / SOURCE + root.mkdir() + for path, title, i, filler, sentinel in DOCS: + (root / path).write_text( + _doc_content(title, i, filler, sentinel), encoding="utf-8" + ) + assert (root / READ_TARGET).is_file() + return root + + +@pytest.fixture(scope="module") +def app_server(mock_llm: int, seed_dirs: Path) -> Iterator[str]: + """The real app under test — per-module app (the conftest pattern, + cf. ``test_read_truncation_cap.py``): NO ``BOR_GIT_SOURCES`` (the + env fallback is git-only — the source here is a DB-registered local + directory), the mock LLM, the mock-calibrated threshold, and the + leak-guarded code defaults (the suggested/related tier settings + ride their code defaults — 5 / 2 — exactly like the production + ``.env``-free defaults). The session app is never started in this + isolated run, so no port clash.""" + env = dict(os.environ) + env.pop("DEBUGPY", None) + env["BOR_ENVIRONMENT"] = "e2e" + env["BOR_STATIC_DIR"] = str(REPO / "frontend") + env["BOR_LLM_BASE_URL"] = ( + "https://aipi.reeseapps.com/v1" + if USE_REAL_LLM + else f"http://127.0.0.1:{mock_llm}/v1" + ) + # Mock-calibrated gate (conftest pattern): the strength gradient + # keeps every scripted turn grounded (best cosine ≈ 0.55 / ≈ 0.80). + env["BOR_RELEVANCE_THRESHOLD"] = "0.30" + env["BOR_LEXICAL_SUPPORT_FLOOR"] = "0.15" + # Phase 67: instant retry waits + the code-default budget (the + # conftest leak-guard pattern). + env["BOR_LLM_RETRY_DELAY"] = "0" + env["BOR_LLM_RETRIES"] = str(_Settings.model_fields["llm_retries"].default) + env.setdefault( + "BOR_DATABASE_URL", + "postgresql+psycopg://reese:reese@localhost:5432/brain_of_reese", + ) + # Phase 16: admin auth must be set or create_app() refuses to boot. + env["BOR_ADMIN_PASSWORD"] = ADMIN_PASSWORD + env["BOR_SESSION_SECRET"] = SESSION_SECRET + # The repo's .env file carries the owner's BOR_GIT_SOURCES (the app + # reads it from cwd) — override it with an EMPTY value (the env var + # beats the .env file): the registry must hold EXACTLY the local + # directory this suite registers (a leftover env git list would + # pollute the KB the scripted turns run against). + env["BOR_GIT_SOURCES"] = "" + # Leak guards (conftest pattern): an operator's local (gitignored) + # .env cannot leak corpus-specific settings into the app under test. + env["BOR_DOCS_REPO"] = "" + env["BOR_SUGGESTIONS"] = json.dumps( + _Settings.model_fields["suggestions"].default + ) + env["BOR_INPUT_PLACEHOLDER"] = _Settings.model_fields["input_placeholder"].default + env["BOR_FOOTER_TEXT"] = _Settings.model_fields["footer_text"].default + proc = subprocess.Popen( + [sys.executable, "-m", "uvicorn", "app.main:app", + "--host", "127.0.0.1", "--port", str(APP_PORT), "--log-level", "warning"], + cwd=REPO, + env=env, + ) + try: + _wait_http(f"{APP_URL}/api/health") + yield APP_URL + finally: + proc.terminate() + try: + proc.wait(timeout=10) + except subprocess.TimeoutExpired: + proc.kill() + + +@pytest.fixture(scope="module") +def app_url(app_server: str) -> str: + return app_server + + +def _truncate_all() -> None: + """Fresh registry + KB (the E2E isolation pattern): the E2E suites + share one Postgres, so a leftover git_sources row or document would + pollute the retrieval the scripted turns run against (the strength + gradient's margins are pinned against EXACTLY these seven + documents).""" + with SessionLocal() as db: + db.execute( + text( + "TRUNCATE chunks, documents, query_log, steering_notes, " + "kb_overview, git_sources, folder_summaries" + ) + ) + db.commit() + + +def _wait_sync_done_http(client: httpx.Client, timeout_s: float = 180.0) -> dict[str, Any]: + """Poll the (cookie-authenticated) status endpoint until the run + reaches a terminal state (the test_ls_tree_drilldown pattern, over + plain httpx — this fixture has no browser page yet).""" + deadline = time.monotonic() + timeout_s + body: dict[str, Any] = {} + while time.monotonic() < deadline: + r = client.get("/api/sync/status") + assert r.status_code == 200, r.text + body = r.json() + if body["state"] in ("success", "failed"): + return body + time.sleep(0.5) + raise AssertionError(f"sync did not reach a terminal state: {body}") + + +def _assert_tiers(question: str, suggested_paths: list[str]) -> None: + """Pin the strength-gradient design with the app's REAL hybrid + retrieval over the mock's embeddings (deterministic): the suggested + tier is exactly the five strength docs in the MEASURED rank order + for THIS question (LOCKED A3 — top-5, NO floor) and the related + tier is the rank-6+ remainder (``related_max_docs`` = 2). A + fixture-text regression that breaks the design fails here, at + setup, with a clear message.""" + with SessionLocal() as db: + chunks = retrieve(db, question, embed_text(question)) + suggested = [f"{d.source}/{d.path}" for d in select_suggested(chunks)] + related = [ + f"{d.source}/{d.path}" + for d in select_related( + chunks, + {d.id for d in select_suggested(chunks)}, + _Settings.model_fields["related_max_docs"].default, + ) + ] + expected = [f"{SOURCE}/{p}" for p in suggested_paths] + assert suggested == expected, ( + f"suggested tier drifted for {question!r}: {suggested} " + f"(expected {expected})" + ) + assert related == [f"{SOURCE}/{p}" for p in RELATED_PATHS], ( + f"related tier drifted for {question!r}: {related}" + ) + + +@pytest.fixture(scope="module") +def synced_kb(app_server: str, seed_dirs: Path) -> None: + """The story's precondition: the one-source KB synced under the + deterministic mock. Registers the temp directory through the + authenticated API (the ``test_local_directory_sources.py`` + pattern), runs the REAL in-process sync (``POST /api/sync`` — walk + → chunk → embed → summaries → overview → folder summaries → + version bump), pins the stored content byte-identical to the + fixture strings, pins LOCKED A2 end-to-end (every doc — markdown + included — stores the mock's byte-stable digest + exactly one + embedded ``is_summary`` chunk), and pins the tier design for both + scripted questions.""" + _truncate_all() + with httpx.Client(base_url=app_server, timeout=30.0) as client: + r = client.post("/api/login", json={"password": ADMIN_PASSWORD}) + assert r.status_code == 204, r.text + r = client.post( + "/api/git-sources", json={"kind": "local", "path": str(seed_dirs)} + ) + assert r.status_code == 201, r.text + r = client.post("/api/sync") + assert r.status_code == 202, r.text + body = _wait_sync_done_http(client) + assert body["state"] == "success", body + detail = body["detail"] + assert detail["added"] == len(DOCS), detail + assert detail["pruned"] == 0, detail + # The import stored the fixture strings BYTE-IDENTICALLY and, for + # EVERY doc (markdown included — locked A2), the mock's byte-stable + # digest: the deterministic assertion surface of the whole suite. + with SessionLocal() as db: + for path, title, i, filler, sentinel in DOCS: + stored = db.scalar( + select(Document).where( + Document.source == SOURCE, Document.path == path + ) + ) + assert stored is not None, f"fixture doc {path} was not imported" + expected = _doc_content(title, i, filler, sentinel) + assert stored.content == expected, f"stored content drifted for {path}" + assert stored.summary == _expected_summary(expected, path), ( + f"summary is not the mock digest for {path}: {stored.summary!r}" + ) + schunks = [c for c in stored.chunks if c.is_summary] + assert len(schunks) == 1 and schunks[0].position == -1, ( + f"expected exactly one is_summary chunk for {path}" + ) + assert schunks[0].embedding is not None, ( + f"the is_summary chunk of {path} is not embedded" + ) + _assert_tiers(TAIL_QUESTION, SUGGESTED_TAIL) + _assert_tiers(READ_QUESTION, SUGGESTED_READ) + + +@pytest.fixture(autouse=True) +def _clean(db_ready: None) -> Iterator[None]: + """Per-test query_log isolation (the KB itself is module-scoped — + the scripted turns never change it, so the registry and the KB + persist across the tests of this module).""" + with SessionLocal() as db: + db.execute(text("TRUNCATE query_log")) + db.commit() + yield + with SessionLocal() as db: + db.execute(text("TRUNCATE query_log")) + db.commit() + + +# -------------------------------------------------------------------------- +# Page helpers (the test_ls_tree_drilldown / test_read_truncation_cap +# house patterns) +# -------------------------------------------------------------------------- + +#: Captures the raw SSE ``data:`` payloads of the /api/chat stream +#: (a response clone read in the background) — wire-level assertions +#: for the ``tool`` / ``done`` frames, independent of the UI rendering. +SSE_HOOK = """ +() => { + if (window.__sseInstalled) return; + window.__sseInstalled = true; + window.__sseFrames = []; + const origFetch = window.fetch; + window.fetch = async function (...args) { + const res = await origFetch.apply(this, args); + try { + const url = typeof args[0] === 'string' ? args[0] : args[0].url; + if (url.includes('/api/chat')) { + res.clone().text().then((bodyText) => { + for (const block of bodyText.split('\\n\\n')) { + const line = block.trim(); + if (line.startsWith('data: ')) { + window.__sseFrames.push(line.slice(6)); + } + } + }); + } + } catch (e) { /* non-clonable responses: ignored */ } + return res; + }; +} +""" + + +def _install_page_hooks(page: Page) -> None: + page.evaluate(SSE_HOOK) + + +def _frames(page: Page) -> list[dict]: + """The SSE frames captured since the last submit (``_submit`` + clears the buffer), once the hook's background read settles.""" + deadline = time.monotonic() + 30.0 + while True: + raw = page.evaluate("() => window.__sseFrames || []") + parsed = [json.loads(line) for line in raw if line] + if any(f.get("type") == "done" for f in parsed): + return parsed + if time.monotonic() > deadline: + raise AssertionError( + f"SSE hook captured no `done` frame (frames so far: " + f"{len(parsed)}) — hook install failed?" + ) + time.sleep(0.05) + + +def _tool_frames(frames: list[dict]) -> list[dict]: + return [f for f in frames if f.get("type") == "tool"] + + +def _submit(page: Page, question: str) -> None: + page.evaluate("window.__sseFrames = []") + page.fill("#message-input", question) + page.click("#send-btn") + # The user bubble lands synchronously with the submit handler. + expect(page.locator(".msg.user .bubble").last).to_contain_text(question) + + +def _wait_settled(page: Page) -> None: + """The turn is complete: answer text in the bubble, button recovered + (the phase-48 settle wait, the test_agent_document_tools helper).""" + expect(page.locator(".msg.brain .bubble").last).not_to_have_text("", timeout=30_000) + expect(page.locator("#send-btn")).to_be_enabled(timeout=30_000) + expect(page.locator("#send-label")).to_have_text("Send", timeout=30_000) + + +def _last_brain(page: Page) -> Locator: + return page.locator(".msg.brain").last + + +def _last_query_log() -> QueryLog: + with SessionLocal() as db: + rows = db.scalars(select(QueryLog)).all() + assert len(rows) == 1, f"expected exactly one query_log row, got {len(rows)}" + return rows[0] + + +def _assert_suggested_chips_and_related_row(page: Page, suggested_paths: list[str]) -> None: + """The citation surface of a grounded turn (LOCKED A4): the chip row + is the suggested set (+ agent reads, deduped — asserted per test) + and the de-emphasized ``related-docs`` row carries the rank-6+ + remainder (phase-113 UI, never a citation chip).""" + chips = page.locator(".msg.brain .source-chip") + expect(chips).to_have_count(len(suggested_paths)) + for path in suggested_paths: + expect( + chips.filter(has_text=path), message=f"chip for {path}" + ).to_have_count(1) + row = page.locator(".msg.brain .related-docs") + expect(row).to_have_count(1) + expect(row.first).to_have_attribute("aria-label", "Nearby docs, in case") + links = page.locator(".msg.brain .related-docs .related-doc") + expect(links).to_have_count(len(RELATED_PATHS)) + for i, path in enumerate(RELATED_PATHS): + expect(links.nth(i)).to_contain_text(f"{SOURCE}/{path}") + # The related links are never citation chips. + expect(page.locator(".msg.brain .related-docs .source-chip")).to_have_count(0) + + +# -------------------------------------------------------------------------- +# 1. Cases (a) + (b) + (d): summaries seed the prompt — no full text +# -------------------------------------------------------------------------- + + +def test_summaries_seed_the_prompt_not_the_full_text( + page: Page, app_url: str, synced_kb: None, db_ready: None +) -> None: + """The tail-echo question quotes the last 160 chars of the seeded + ```` block: they end in the LAST suggested doc's SUMMARY + (the mock's byte-stable digest tail + pointer — cases a + d: the + markdown doc's block carries the digest, not a content preview), + and NO document's tail sentinel is in the echoed context (case b: + the full text of no suggested doc reached the model — the inverse + of the retired phase-24 pin).""" + page.set_default_timeout(30_000) + login(page, app_url, next="/") + _install_page_hooks(page) + + _submit(page, TAIL_QUESTION) + _wait_settled(page) + + # No tools on this turn — the answer is the mock's direct tail echo + # of the seeded context (the summary-seed lens). + frames = _frames(page) + assert _tool_frames(frames) == [], _tool_frames(frames) + done = next(f for f in frames if f.get("type") == "done") + assert done["deflected"] is False, done + # The grounded turn cites the 5 suggested docs (LOCKED A4 — no read + # yet: the chip row IS the suggested set, in rank order). + assert [ + (s["source"], s["path"]) for s in done["sources"] + ] == [(SOURCE, p) for p in SUGGESTED_TAIL], done["sources"] + + bubble = _last_brain(page).locator(".bubble") + expect(bubble).to_contain_text(MOCK_ANSWER_MARKER, timeout=30_000) + + # Case (a) + (d): the echoed tail ends in the LAST suggested doc's + # summary — the markdown doc-e's byte-stable digest tail + pointer + # line (the digest is the mock's SUMMARY_MODE shape, pinned in + # ``synced_kb`` — a content preview would carry the prelude/phrase + # text instead, and the pointer line only ever exists on a stored + # summary). The bubble renders the answer as markdown, which + # collapses the summary's newline — so pin each LINE separately + # (the digest line's tail sits inside the echoed 160 chars; the + # pointer line is single-line too). + last_doc = next(d for d in DOCS if d[0] == LAST_SUGGESTED) + last_summary = _expected_summary(_doc_content(*last_doc[1:]), LAST_SUGGESTED) + digest_line = last_summary.split("\n", 1)[0] + expect(bubble).to_contain_text("Source: seedkb/doc-e.md") + expect(bubble).to_contain_text(digest_line[-100:]) + # The digest is NOT the raw content: the echoed tail cannot carry + # any document's tail sentinel — the inverse of the retired + # phase-24 full-text pin (case b). + for sentinel in SENTINELS: + expect(bubble).not_to_contain_text(sentinel) + expect(bubble).not_to_contain_text(TRUNCATION_MARKER) + + # Case (e)'s chip surface (no read yet): chips = the 5 suggested + # docs; the related row renders rank 6+ (the de-emphasized row). + _assert_suggested_chips_and_related_row(page, SUGGESTED_TAIL) + + # Durable record: grounded; suggested + related (LOCKED A3 — the log + # records retrieval, not citations). + row = _last_query_log() + assert row.question == TAIL_QUESTION + assert row.deflected is False + assert row.sources == ", ".join(f"{SOURCE}/{p}" for p in SUGGESTED_TAIL + RELATED_PATHS) + + +# -------------------------------------------------------------------------- +# 2. Cases (c) + (e): ``read`` adds the full text; chips = suggested + +# read (deduped); related row = rank 6+ +# -------------------------------------------------------------------------- + + +def test_read_suggested_doc_adds_full_text_and_cites( + page: Page, app_url: str, synced_kb: None, db_ready: None +) -> None: + """The scripted flow ``read``s the rank-1 suggested doc: the read + SUCCEEDS (phase 118 — the seeds are summaries, not full text; the + retired top-2 seed-read refusal is gone) and the mock's verbatim + echo of the read result lands the document's tail sentinel in the + answer (case c: the full text reached the model through the + ``read`` tool, not the seed). The citation chips = suggested + + read, deduped — the read doc is among the suggested, so the chip + row is exactly the 5 suggested docs; the related row renders rank + 6+ (case e).""" + page.set_default_timeout(30_000) + login(page, app_url, next="/") + _install_page_hooks(page) + + _submit(page, READ_QUESTION) + _wait_settled(page) + + # Exactly ONE executed tool call: the scripted read of the + # rank-1 suggested doc. + frames = _frames(page) + assert _tool_frames(frames) == [ + {"type": "tool", "name": "read", "argument": f"{SOURCE}/{READ_TARGET}"} + ], _tool_frames(frames) + done = next(f for f in frames if f.get("type") == "done") + assert done["deflected"] is False, done + # LOCKED A4: done.sources = suggested + read, deduped — the read doc + # is already among the suggested, so the citation surface is + # exactly the 5 suggested docs (in rank order). + assert [ + (s["source"], s["path"]) for s in done["sources"] + ] == [(SOURCE, p) for p in SUGGESTED_READ], done["sources"] + # done.related = the rank-6+ remainder (deduped against the cited). + assert [ + (s["source"], s["path"]) for s in done["related"] + ] == [(SOURCE, p) for p in RELATED_PATHS], done["related"] + + # The live Reading line carries the combined identity. + line = _last_brain(page).locator(".tool-call") + expect(line).to_have_count(1) + expect(line).to_contain_text(f"Reading {SOURCE}/{READ_TARGET}") + + # Case (c): the answer is the mock's verbatim echo of the READ + # RESULT — its header and the document's tail sentinel (the last + # line, outside the digest): the full text reached the model + # through the read, not the seed. + bubble = _last_brain(page).locator(".bubble") + expect(bubble).to_contain_text(f"Document {SOURCE}/{READ_TARGET}:", timeout=30_000) + expect(bubble).to_contain_text(READ_SENTINEL) + expect(bubble).not_to_contain_text(TRUNCATION_MARKER) + # The seed itself carried no full text: none of the OTHER docs' + # sentinels are in the answer either. + for path, _t, _i, _f, sentinel in DOCS: + if path != READ_TARGET: + expect(bubble).not_to_contain_text(sentinel) + + # Case (e): the UI chip row = suggested + read (deduped — the read + # doc is among the suggested, so chips = the 5 suggested docs), and + # the related row renders rank 6+. + _assert_suggested_chips_and_related_row(page, SUGGESTED_READ) + + # Durable record: grounded; suggested + related + read (deduped, + # LOCKED A3). + row = _last_query_log() + assert row.question == READ_QUESTION + assert row.deflected is False + assert row.sources == ", ".join(f"{SOURCE}/{p}" for p in SUGGESTED_READ + RELATED_PATHS) diff --git a/tests/e2e/test_theme_semantic_completion.py b/tests/e2e/test_theme_semantic_completion.py index fad6991..eec9f25 100644 --- a/tests/e2e/test_theme_semantic_completion.py +++ b/tests/e2e/test_theme_semantic_completion.py @@ -71,8 +71,11 @@ computed-style assertion): gray ok-ink on gray ok-bg, text intact. 7. ``test_tool_call_lines_gray`` — screenshot 1: a mock-LLM turn that executes the tools renders the "Listing documents" / "Reading - " lines — gray accent-ink text, the gray accent-line - left border, and the gray brand-soft path chip, all text intact. + " lines — gray accent-ink text, all text intact. + (Phase 117 deboxed the line and dechipped the path on the owner's + visual-glitch report — the accent rides the TEXT: the pins now + assert the old accent-line border and the brand-soft chip + background are GONE, not gray.) 8. ``test_reset_removes_tag_byte_identical`` — the no-op contract: the gray tag is present in the live document pre-reset; Reset to defaults removes it from the LIVE document, a fresh load serves NO @@ -790,10 +793,12 @@ def test_local_badge_gray_labeled( #: document (a chunk carrying the mock's own bag-of-words embedding) that #: grounds the turn, and one CATALOG-ONLY document (indexed, no chunks) the #: mock's single-read flow reads. The catalog-only document sorts FIRST -#: ("Checklist" < "ThemeNotes") — the mock reads the first catalog line, and -#: it must NOT be the in-context retrieval document: the agent's read tool -#: refuses documents already in the prompt (ALREADY_IN_CONTEXT), and a -#: refused single-read flow would re-loop ls/read to the round cap. +#: ("Checklist" < "ThemeNotes") — the mock reads the first catalog line. +#: (Phase 118, A6: the read target's seed status no longer matters — a +#: first read of ANY document succeeds; the ALREADY_IN_CONTEXT refusal +#: fires only for a document already READ in the same turn. The +#: catalog-only design stands because the flow reads the first catalog +#: line, which must be a real, readable document.) READ_SOURCE = "Checklist" READ_PATH = "read-me.md" READ_SP = f"{READ_SOURCE}/{READ_PATH}" @@ -929,11 +934,15 @@ def test_tool_call_lines_gray(page: Page, app_url: str, db_ready: None) -> None: expect(page.locator("#send-label")).to_have_text("Send", timeout=60_000) # Screenshot 1: the yellow "Listing documents" / "Reading" lines — - # now gray accent-ink text with the gray accent-line left border, - # the lines' text intact. Phase 94: the drill-down ls adds a THIRD - # line between them — the drill ls scoped to the first source of - # the top level (registry order: Checklist — the read target's - # source). + # now gray accent-ink TEXT, the lines' text intact. Phase 94: the + # drill-down ls adds a THIRD line between them — the drill ls scoped + # to the first source of the top level (registry order: Checklist — + # the read target's source). Phase 117 (owner visual-glitch report) + # deboxed the line and dechipped the path: the accent rides the + # text color — there is NO left border and NO chip background to + # be gray, so the pins assert their ABSENCE (a colored border or + # chip background returning would fail both the width/alpha + # assertion and the theme completeness contract). lines = page.locator(".msg.brain .tool-call") expect(lines).to_have_count(3) expect(lines.nth(0)).to_contain_text("Listing documents") @@ -942,14 +951,16 @@ def test_tool_call_lines_gray(page: Page, app_url: str, db_ready: None) -> None: expect(lines.nth(2)).to_contain_text("Reading") expect(lines.nth(2)).to_contain_text(READ_SP) _assert_gray(lines.nth(0), "color", GRAY["accent_ink"], label="ls line text") - _assert_gray( - lines.nth(0), "borderLeftColor", GRAY["accent_line"], label="ls line border" - ) + assert ( + lines.nth(0).evaluate("el => getComputedStyle(el).borderLeftWidth") == "0px" + ), "phase 117 debox: the tool line must carry no left border" _assert_gray(lines.nth(2), "color", GRAY["accent_ink"], label="read line text") - # The path chip on the Reading line: gray brand-soft background + - # gray ink (the screenshot's code chip — still gray under the ramp). + # The path `code` on the Reading line: phase 117 dechipped it — + # transparent background, gray ink (still gray under the ramp). code = lines.nth(2).locator("code") - _assert_gray(code, "backgroundColor", GRAY["brand_soft"], label="read chip bg") + assert ( + code.evaluate("el => getComputedStyle(el).backgroundColor") == "rgba(0, 0, 0, 0)" + ), "phase 117 dechip: the path must have no chip background" _assert_gray(code, "color", GRAY["ink"], label="read chip text") diff --git a/tests/e2e/test_whole_document_context.py b/tests/e2e/test_whole_document_context.py deleted file mode 100644 index e888df0..0000000 --- a/tests/e2e/test_whole_document_context.py +++ /dev/null @@ -1,316 +0,0 @@ -"""Phase 24 E2E (Playwright): a matched document reaches the LLM whole. - -Story: ``.agents/user_stories/whole-document-context.md`` -Run in isolation (DB must be up: ``podman compose up -d db``): - - uv run pytest tests/e2e/test_whole_document_context.py -v --no-cov - -The mock LLM's tail-echo trigger (``END_OF_NOTES_TRIGGER``, see -``tests/e2e/mock_llm.py``) makes the model quote the last 160 chars of -the document context. A sentinel placed on the *last line* of a document -therefore appears in the rendered answer **iff the entire document was in -the prompt** — which is what makes the no-truncation contract (A7 revised, -owner permission 2026-08-24: matched parent documents are never cut) -provable end-to-end. - -The oversized documents are seeded directly via SQLAlchemy (a -``documents`` row + 2–3 ``chunks`` rows whose embeddings are the mock's -own deterministic bag-of-words vectors, so the question's live mock -embedding genuinely overlaps — no fixture files added: -``tests/fixtures/docs/`` stays at its 13 files (phase 47), other suites -pin ``summary.added == 13``). -""" -from __future__ import annotations - -import asyncio -import hashlib -from collections.abc import Callable, Sequence -from datetime import UTC, datetime -from pathlib import Path -from threading import Thread -from typing import Any - -from playwright.sync_api import Page, expect -from sqlalchemy import select, text -from sqlalchemy.orm import Session - -from app.config import Settings -from app.db import SessionLocal -from app.models import Chunk, Document, QueryLog -from app.rag.importer import ImportSummary, import_sources -from app.rag.llm import LLMClient -from app.rag.retriever import TRUNCATION_MARKER -from e2e.auth_helpers import login -from tests.e2e.mock_llm import embed_text - -REPO = Path(__file__).resolve().parents[2] -FIXTURES = REPO / "tests" / "fixtures" / "docs" - -#: The pre-phase-24 ``BOR_MAX_CONTEXT_CHARS`` default — the budget this -#: suite proves is gone from the document path. -OLD_CONTEXT_CAP = 24_000 - -QUESTION = "Show the end of your notes about the gitlab install playbook, please." -SMALL_QUESTION = "How is my Kubernetes cluster set up?" -MOCK_ANSWER_MARKER = "Deterministic mock answer for E2E" - - -# --- Content builders (deterministic, token-controlled) ------------------- - - -def _repeated(line: str, min_chars: int) -> str: - """"line" (newline-terminated) repeated until at least min_chars chars.""" - unit = line + "\n" - return unit * max(1, -(-min_chars // len(unit))) - - -def _doc_slices(content: str, n: int) -> list[str]: - """Even slices of *content* (the last slice keeps the final line).""" - step = len(content) // n - return [content[i * step : (i + 1) * step] for i in range(n - 1)] + [ - content[(n - 1) * step :] - ] - - -def _gitlab_30k_doc(sentinel: str) -> str: - """A ~30 000-char document (past the old 24k cap): a body of repeated - "gitlab install playbook" lines — the same tokens the question carries, - so hybrid retrieval genuinely hits — whose LAST line is a unique - sentinel only a tail echo can surface.""" - body = _repeated( - "gitlab install playbook: run the gitlab install playbook on the homelab host.", - OLD_CONTEXT_CAP + 6_000, - ) - return body + sentinel + "\n" - - -def _pair_doc(strong_line: str, filler_line: str, sentinel: str) -> tuple[str, list[str]]: - """A ~16 000-char document: a ~2 000-char first chunk carrying the - question's key tokens, a ~14 000-char low-overlap remainder, and a - unique sentinel as the last line. Returns (content, chunk_texts).""" - chunk0 = _repeated(strong_line, 2_000) - chunk1 = _repeated(filler_line, 14_000) - return chunk0 + chunk1 + sentinel + "\n", [chunk0, chunk1] - - -# --- DB seeding (TRUNCATE-then-seed, cf. test_chat_rag.py) ----------------- - - -def _seed_doc( - db: Session, - source: str, - path: str, - title: str, - content: str, - chunk_texts: Sequence[str], -) -> None: - """One ``documents`` row + one ``chunks`` row per chunk text. - - Each chunk's embedding is the mock's own ``embed_text`` vector, so the - app's live mock embedding of the question genuinely overlaps. - """ - doc = Document( - source=source, - path=path, - full_path=f"/tmp/{path}", - title=title, - content=content, - content_hash=hashlib.sha256(content.encode("utf-8")).hexdigest(), - indexed_at=datetime.now(UTC), - ) - db.add(doc) - db.flush() - db.add_all( - Chunk(document_id=doc.id, position=i, content=chunk, embedding=embed_text(chunk)) - for i, chunk in enumerate(chunk_texts) - ) - - -def _reset_db(seed: Callable[[Session], None] | None = None) -> None: - """Truncate the KB (and query log), then optionally run *seed*.""" - with SessionLocal() as db: - db.execute(text("TRUNCATE chunks, documents, query_log")) - db.commit() - if seed is not None: - seed(db) - db.commit() - - -# --- Importer + thread helpers (test_chat_rag.py pattern) ------------------ - - -async def _import_fixtures(mock_port: int) -> ImportSummary: - kwargs: dict[str, Any] = { - "_env_file": None, - "llm_base_url": f"http://127.0.0.1:{mock_port}/v1", - } - settings = Settings(**kwargs) # pyright: ignore[reportCallIssue] - return await import_sources([FIXTURES], LLMClient(settings)) - - -def _run_in_thread(coro: Any) -> Any: - """Run a coroutine on a worker thread. - - Playwright's sync API keeps an asyncio loop running on the test thread, - so ``asyncio.run`` cannot be called directly from a test body. - """ - box: dict[str, Any] = {} - - def runner() -> None: - try: - box["value"] = asyncio.run(coro) - except BaseException as e: # noqa: BLE001 — re-raised on the test thread - box["error"] = e - - t = Thread(target=runner) - t.start() - t.join() - if "error" in box: - raise box["error"] - return box["value"] - - -def _ask(page: Page, app_url: str, question: str) -> Any: - """Submit *question* and wait for the streamed brain bubble.""" - page.set_default_timeout(30_000) - login(page, app_url, next="/") - page.fill("#message-input", question) - page.click("#send-btn") - bubble = page.locator(".msg.brain .bubble") - bubble.first.wait_for(state="visible", timeout=30_000) - return bubble.first - - -def _last_query_log() -> QueryLog: - with SessionLocal() as db: - rows = db.scalars(select(QueryLog)).all() - assert len(rows) == 1, f"expected exactly one query_log row, got {len(rows)}" - return rows[0] - - -# --- Story tests ------------------------------------------------------------- - - -def test_whole_document_over_old_cap_reaches_llm( - page: Page, app_url: str, mock_llm: int, db_ready: None -) -> None: - """A 30k document (past the old 24k cap) reaches the LLM whole: its - tail sentinel — the last 160 chars of the context — is echoed back.""" - sentinel = "WHOLE-DOC-TAIL-GITLAB-30K" - content = _gitlab_30k_doc(sentinel) - assert len(content) > OLD_CONTEXT_CAP # this is the point of the test - - def seed(db: Session) -> None: - _seed_doc( - db, - "Homelab", - "gitlab-30k.md", - "GitLab Install Playbook (30k)", - content, - _doc_slices(content, 3), - ) - - _reset_db(seed) - bubble = _ask(page, app_url, QUESTION) - - # The tail sentinel exists only on the document's last line — its - # presence proves the entire 30k document was in the LLM prompt. - expect(bubble).to_contain_text(sentinel, timeout=30_000) - expect(bubble).to_contain_text(MOCK_ANSWER_MARKER) - expect(bubble).not_to_contain_text(TRUNCATION_MARKER) - - # Grounded: the document's source chip renders under the bubble. - chip = page.locator(".msg.brain .source-chip", has_text="gitlab-30k.md") - expect(chip).to_have_count(1) - expect(chip.first).to_contain_text("Homelab/gitlab-30k.md") - - # Button recovers (never stale) and the turn was grounded. - expect(page.locator("#send-btn")).to_be_enabled() - expect(page.locator("#send-label")).to_have_text("Send") - row = _last_query_log() - assert row.question == QUESTION - assert row.deflected is False - assert "Homelab/gitlab-30k.md" in row.sources - - -def test_second_document_of_over_cap_pair_reaches_llm( - page: Page, app_url: str, mock_llm: int, db_ready: None -) -> None: - """Two ~16k documents (32k combined — under the old budget the - lower-ranked one was truncated in place): the SECOND document, the - last block inside , reaches the LLM whole — its sentinel is - the one the tail echo surfaces.""" - sentinel_a = "WHOLE-DOC-TAIL-PAIR-A" - sentinel_b = "WHOLE-DOC-TAIL-PAIR-B" - # Doc A's first chunk carries the question's key tokens → it ranks - # first in both candidate lists → it comes first in . - content_a, chunks_a = _pair_doc( - "gitlab install playbook: run the gitlab install playbook end to end.", - "the server room keeps a steady temperature and the racks are labelled.", - sentinel_a, - ) - # Doc B's first chunk has only a weaker overlap ("playbook", "notes") - # → it ranks second → it is the LAST block inside . - content_b, chunks_b = _pair_doc( - "playbook notes: the playbook notes track what changed and where.", - "the rack elevation drawing shows cable trays and pdu positions.", - sentinel_b, - ) - assert len(content_a) + len(content_b) > OLD_CONTEXT_CAP # 32k > 24k - - def seed(db: Session) -> None: - _seed_doc( - db, "Homelab", "gitlab-install-playbook.md", - "GitLab Install Playbook", content_a, chunks_a, - ) - _seed_doc( - db, "Homelab", "playbook-notes.md", - "Playbook Notes", content_b, chunks_b, - ) - - _reset_db(seed) - bubble = _ask(page, app_url, QUESTION) - - # The tail echo quotes doc B's sentinel (the last block's tail) — doc B - # was in the prompt whole, past the old budget. Doc A's sentinel sits - # mid-prompt, so it must NOT be in the quoted tail: that is what pins - # the rank order (A first, B last inside ). - expect(bubble).to_contain_text(sentinel_b, timeout=30_000) - expect(bubble).not_to_contain_text(sentinel_a) - expect(bubble).to_contain_text(MOCK_ANSWER_MARKER) - expect(bubble).not_to_contain_text(TRUNCATION_MARKER) - - # Both documents are cited (top-2), in rank order. - chips = page.locator(".msg.brain .source-chip") - expect(chips).to_have_count(2) - expect(page.locator(".msg.brain .source-chip", - has_text="gitlab-install-playbook.md")).to_have_count(1) - expect(page.locator(".msg.brain .source-chip", - has_text="playbook-notes.md")).to_have_count(1) - - row = _last_query_log() - assert row.question == QUESTION - assert row.deflected is False - assert "Homelab/gitlab-install-playbook.md" in row.sources - assert "Homelab/playbook-notes.md" in row.sources - - -def test_small_document_path_unchanged( - page: Page, app_url: str, mock_llm: int, db_ready: None -) -> None: - """Regression: the standard (small) fixtures still take the grounded - path, byte-identical to before — no marker, kubernetes.md cited.""" - _reset_db(None) - summary = _run_in_thread(_import_fixtures(mock_llm)) - assert summary.added == 13 # A9 formats (phase 47 added quadlet+j2) - - bubble = _ask(page, app_url, SMALL_QUESTION) - expect(bubble).to_contain_text(SMALL_QUESTION, timeout=30_000) - expect(bubble).to_contain_text(MOCK_ANSWER_MARKER) - expect(bubble).not_to_contain_text(TRUNCATION_MARKER) - - chip = page.locator(".msg.brain .source-chip", has_text="kubernetes.md") - expect(chip).to_have_count(1) - row = _last_query_log() - assert row.deflected is False - assert "docs/homelab/kubernetes.md" in row.sources diff --git a/tests/fixtures/test_kb.dump.sql b/tests/fixtures/test_kb.dump.sql index 538ab77..d68a269 100644 --- a/tests/fixtures/test_kb.dump.sql +++ b/tests/fixtures/test_kb.dump.sql @@ -1,16 +1,16 @@ -- ============================================================ -- Brain of Reese — controlled tool-calling test KB (fixture dump) --- Generated by scripts/load_test_kb.py on 2026-09-11T00:16:45-04:00 +-- Generated by scripts/load_test_kb.py on 2026-09-16T04:56:55-04:00 -- Data-only snapshot (the schema stays alembic-managed; the -- generated chunks.tsv column is recomputed on restore). --- Sources: deployments, homelab (8 documents, 9 chunks). +-- Sources: deployments, homelab (8 documents, 9 content + 8 summary chunks) -- Restore (one transaction, sub-second): -- uv run python -m scripts.restore_test_kb -- psql "$BOR_DATABASE_URL" --single-transaction -f tests/fixtures/test_kb.dump.sql -- ============================================================ TRUNCATE public.documents, public.chunks, public.git_sources, public.kb_overview, public.sources_meta, public.steering_notes, public.query_log, public.saved_chats, public.doc_drafts, public.folder_summaries; -INSERT INTO public.documents (id, source, path, full_path, title, content, content_hash, indexed_at, summary) VALUES ('a053e592-9b7d-486e-9da6-81535f6d64a8', 'deployments', 'ansible/lab-inventory.md', '/var/home/ducoterra/Projects/Personal/brain_of_reese/tests/fixtures/agent_kb/deployments/ansible/lab-inventory.md', 'Lab Ansible Inventory', '# Lab Ansible Inventory +INSERT INTO public.documents (id, source, path, full_path, title, content, content_hash, indexed_at, created_at, created_at_manual, summary) VALUES ('53d68063-3056-4f99-9a88-7812a146c877', 'deployments', 'ansible/lab-inventory.md', '/var/home/ducoterra/Projects/Personal/brain_of_reese/tests/fixtures/agent_kb/deployments/ansible/lab-inventory.md', 'Lab Ansible Inventory', '# Lab Ansible Inventory The lab deployment inventory lives in `op-build:~/lab/inventory/hosts.yml` and is managed with ansible-core 2.19.4. @@ -27,8 +27,9 @@ and is managed with ansible-core 2.19.4. - Playbooks run from `op-build` only; the shelf has no internet route. - The `lab-upgrade` play requires the `pve-upgrade` tag on the target. - These files are the single source of truth for shelf addresses. -', '8eb4df05c80ca10ec32f0f5b6c7202f4061b3f4fabfc5f1b518b8a0627ea263b', '2026-09-11 04:16:38.457148+00:00', NULL), -('f90d20cc-a0be-4ab2-89dd-0d57ac6145c4', 'deployments', 'ci/gitlab-runner.md', '/var/home/ducoterra/Projects/Personal/brain_of_reese/tests/fixtures/agent_kb/deployments/ci/gitlab-runner.md', 'GitLab Runner (lab-ci)', '# GitLab Runner (lab-ci) +', '8eb4df05c80ca10ec32f0f5b6c7202f4061b3f4fabfc5f1b518b8a0627ea263b', '2026-09-16 08:56:19.020014+00:00', '2026-09-04 15:27:12.975282+00:00', FALSE, 'The lab Ansible inventory defines a group called "lab" with three hosts: rack7-pbx1, rack7-pbx2, and minio01. Each host has an IP address and a role assigned. The inventory uses ansible-core 2.19.4 and sets variables for the ansible user and become settings. Playbooks run from op-build, and the lab-upgrade play requires the pve-upgrade tag. These files are the single source of truth for shelf addresses. +Source: deployments/ansible/lab-inventory.md'), +('bf670a2f-cdbb-4e83-9b67-e3a8645c412c', 'deployments', 'ci/gitlab-runner.md', '/var/home/ducoterra/Projects/Personal/brain_of_reese/tests/fixtures/agent_kb/deployments/ci/gitlab-runner.md', 'GitLab Runner (lab-ci)', '# GitLab Runner (lab-ci) CI for the shelf runs on a single GitLab Runner registered against `git.reeseapps.com`. @@ -40,8 +41,9 @@ CI for the shelf runs on a single GitLab Runner registered against `debian:13-slim` for lint. - Concurrency is capped at 2; the `pve-upgrade` job never runs in parallel with itself. -', '5634ed3c87d8e8e2c89f7c56339d74c075f62eb554bf98a818b04fee6f15f754', '2026-09-11 04:16:38.568872+00:00', NULL), -('f183f389-a110-4373-8640-4af205d2d026', 'deployments', 'quadlet/mimir-service.md', '/var/home/ducoterra/Projects/Personal/brain_of_reese/tests/fixtures/agent_kb/deployments/quadlet/mimir-service.md', 'BOR Vault Sync Unit', '# BOR Vault Sync Unit +', '5634ed3c87d8e8e2c89f7c56339d74c075f62eb554bf98a818b04fee6f15f754', '2026-09-16 08:56:21.504272+00:00', '2026-09-04 15:27:12.975282+00:00', FALSE, 'This document configures the GitLab Runner used for CI in the shelf project. It specifies that the runner is registered against git.reeseapps.com and uses a docker executor on rack7-pbx2. The registration token format is glrt- followed by 20 hex characters, rotated in 2026-06. Tags include lab-ci as default and pve-upgrade for a gated upgrade job. Job images are ghcr.io/reese/lab-ci:2026.7 for ansible plays and debian:13-slim for lint, with concurrency capped at 2. +Source: deployments/ci/gitlab-runner.md'), +('a5e096fa-11de-46b4-ba6a-9a46f3a620e9', 'deployments', 'quadlet/mimir-service.md', '/var/home/ducoterra/Projects/Personal/brain_of_reese/tests/fixtures/agent_kb/deployments/quadlet/mimir-service.md', 'BOR Vault Sync Unit', '# BOR Vault Sync Unit The Brain-of-Reese vault syncs from `obsidian.container` on `rack7-pbx2`. @@ -56,8 +58,9 @@ The Brain-of-Reese vault syncs from `obsidian.container` on `rack7-pbx2`. - Only `rack7-pbx2` may hold the vault — do not clone it to another node. - Image tags are date-stamped; bump by re-pushing `ghcr.io/reese/obsidian-bor` and restarting the unit. -', '703d8425ba26caf607b240abbebff4be048f828482a6c3297338120e166dc635', '2026-09-11 04:16:38.623801+00:00', NULL), -('fcf52742-dc89-41f9-aade-380d0cf16cd4', 'homelab', 'backups/restic-rack7.md', '/var/home/ducoterra/Projects/Personal/brain_of_reese/tests/fixtures/agent_kb/homelab/backups/restic-rack7.md', 'Restic Backups for Rack7', '# Restic Backups for Rack7 +', '703d8425ba26caf607b240abbebff4be048f828482a6c3297338120e166dc635', '2026-09-16 08:56:22.926145+00:00', '2026-09-04 16:14:23.302397+00:00', FALSE, 'The Brain-of-Reese vault sync unit syncs from obsidian.container on rack7-pbx2. It uses the container image ghcr.io/reese/obsidian-bor:2026.7.14 and publishes on port 8080 via 127.0.0.1:18765. The vault directory is set to /vault/rack7 and the unit restarts always. The vault is a bind mount from /data/vaults/rack7 on a Btrfs subvolume, and only rack7-pbx2 may hold it. Image tags are date-stamped, so they must be updated by re-pushing and restarting. +Source: deployments/quadlet/mimir-service.md'), +('7ded7333-b66a-4e78-b087-5d0b89b058de', 'homelab', 'backups/restic-rack7.md', '/var/home/ducoterra/Projects/Personal/brain_of_reese/tests/fixtures/agent_kb/homelab/backups/restic-rack7.md', 'Restic Backups for Rack7', '# Restic Backups for Rack7 Every config and document on the cluster is backed up nightly with restic into the minio repo `minio01:/backups/rack7`. @@ -68,8 +71,9 @@ restic into the minio repo `minio01:/backups/rack7`. - Retention: `--keep-daily 7 --keep-weekly 4 --keep-monthly 12` - Prune runs only when the repo grows more than 5 GiB. - Restores are tested quarterly; the last dry-run passed on 2026-07-01. -', '7998c1127f41ef3dc8d0786af071d6b91817d791a5b5098b10e56ada239a4e8b', '2026-09-11 04:16:38.678174+00:00', NULL), -('4b6dcf82-7c3a-46d3-8e17-f27d7b4aebf4', 'homelab', 'containers/qwen38-llamacpp.md', '/var/home/ducoterra/Projects/Personal/brain_of_reese/tests/fixtures/agent_kb/homelab/containers/qwen38-llamacpp.md', 'Qwen 3.8 on llama.cpp (pbx-node3)', '# Qwen 3.8 on llama.cpp (pbx-node3) +', '7998c1127f41ef3dc8d0786af071d6b91817d791a5b5098b10e56ada239a4e8b', '2026-09-16 08:56:28.826767+00:00', '2026-09-04 15:27:12.974513+00:00', FALSE, 'This document configures nightly backups of cluster configuration and documents using restic. The backups are stored in a minio repository at minio01:/backups/rack7. The backup process runs from machine ID rbm-8842 with a password retrieved from vault. The schedule is set to 02:17 daily, and retention keeps 7 days, 4 weeks, and 12 months of backups. Pruning of backups only occurs if the repository exceeds 5 GiB. Quarterly restore tests are performed, with the last dry-run passing on 2026-07-01. +Source: homelab/backups/restic-rack7.md'), +('aeee2625-fbda-4663-9225-0d1e1938297f', 'homelab', 'containers/qwen38-llamacpp.md', '/var/home/ducoterra/Projects/Personal/brain_of_reese/tests/fixtures/agent_kb/homelab/containers/qwen38-llamacpp.md', 'Qwen 3.8 on llama.cpp (pbx-node3)', '# Qwen 3.8 on llama.cpp (pbx-node3) The GPU box (`pbx-node3`) serves Qwen 3.8 through llama.cpp. @@ -84,8 +88,9 @@ Working launch line (verified 2026-08): `/srv/llamacpp/cache`. - First-token target is under 400 ms; sustained throughput about 28 tok/s. - Do not add `-ctk f16` — it doubled VRAM usage with no quality gain. -', 'e145b56dd4945a7ba83fabd52a84d00a1cb7ca41dcb2235aafb303b1056b8016', '2026-09-11 04:16:38.732039+00:00', NULL), -('92d409ac-611d-4dc9-bcdb-c72c2c9a8235', 'homelab', 'containers/uptime-kuma.md', '/var/home/ducoterra/Projects/Personal/brain_of_reese/tests/fixtures/agent_kb/homelab/containers/uptime-kuma.md', 'Uptime Kuma (AIO)', '# Uptime Kuma (AIO) +', 'e145b56dd4945a7ba83fabd52a84d00a1cb7ca41dcb2235aafb303b1056b8016', '2026-09-16 08:56:32.810572+00:00', '2026-09-04 15:27:12.974230+00:00', FALSE, 'The GPU box pbx-node3 serves Qwen 3.8 using llama.cpp. The working launch line uses a 30-billion parameter model with 32,768 context tokens and 99 global layers. KV cache and prompt cache are stored in q8_0 format and persisted to /srv/llamacpp/cache. First-token latency is under 400 ms with sustained throughput around 28 tok/s. It is important not to add the -ctk f16 option as it doubles VRAM usage without quality gain. +Source: homelab/containers/qwen38-llamacpp.md'), +('335f9cdf-414a-4489-9550-2b2d251c7569', 'homelab', 'containers/uptime-kuma.md', '/var/home/ducoterra/Projects/Personal/brain_of_reese/tests/fixtures/agent_kb/homelab/containers/uptime-kuma.md', 'Uptime Kuma (AIO)', '# Uptime Kuma (AIO) Uptime Kuma runs on `rack7-pbx2` as the AIO image, listening on **18443** with TLS terminated by `caddy-rack7`. @@ -96,8 +101,9 @@ Uptime Kuma runs on `rack7-pbx2` as the AIO image, listening on `/opt/kuma/backup/`. - Lab-iot monitors watch every VLAN 130 device; the shelf watchdog restarts the container after 3 missed checks. -', 'a573e8063d9c69cdb3e9823dc718cbe2ef07772deea1e0466c6cfb66611b2335', '2026-09-11 04:16:38.787448+00:00', NULL), -('f0b1915b-fd38-4811-9b44-3c880c60574e', 'homelab', 'networking/meridian-notes.md', '/var/home/ducoterra/Projects/Personal/brain_of_reese/tests/fixtures/agent_kb/homelab/networking/meridian-notes.md', 'Rack7 DNS and DHCP', '# Rack7 DNS and DHCP +', 'a573e8063d9c69cdb3e9823dc718cbe2ef07772deea1e0466c6cfb66611b2335', '2026-09-16 08:56:34.291283+00:00', '2026-09-04 15:27:12.974230+00:00', FALSE, 'Uptime Kuma runs on the rack7-pbx2 AIO image and listens on port 18443. TLS is terminated by caddy-rack7. The health endpoint is at https://uptime.rack7.local/ping. On failure, every monitor posts to the ntfy topic reese-uptime-7. History is kept for 30 days with hourly backups to /opt/kuma/backup/. Lab-iot monitors watch every VLAN 130 device, and the shelf watchdog restarts the container after three missed checks. +Source: homelab/containers/uptime-kuma.md'), +('9a3457c7-68ea-4112-bc39-ab680bffa26b', 'homelab', 'networking/meridian-notes.md', '/var/home/ducoterra/Projects/Personal/brain_of_reese/tests/fixtures/agent_kb/homelab/networking/meridian-notes.md', 'Rack7 DNS and DHCP', '# Rack7 DNS and DHCP `minio01` (10.77.42.31) serves DNS and DHCP for the whole lab shelf. @@ -107,8 +113,9 @@ Uptime Kuma runs on `rack7-pbx2` as the AIO image, listening on - Pi-hole runs on 10.77.42.53; the admin UI is on port 18553 behind caddy-rack7 and uses the blocklist `reese-abuse-v3`. - VLAN 42 has no DHCP at all — every device there gets a static address. -', 'a8e53576835de52df06ff5942cda021d5c28ab82a9de211a322beb8c23daa709', '2026-09-11 04:16:38.841260+00:00', NULL), -('9c6376c8-8b10-49d4-bbc2-72aa4d04e5e2', 'homelab', 'networking/vela-bridges.md', '/var/home/ducoterra/Projects/Personal/brain_of_reese/tests/fixtures/agent_kb/homelab/networking/vela-bridges.md', 'Rack7 Proxmox Cluster', '# Rack7 Proxmox Cluster +', 'a8e53576835de52df06ff5942cda021d5c28ab82a9de211a322beb8c23daa709', '2026-09-16 08:56:35.632905+00:00', '2026-09-04 16:14:23.302397+00:00', FALSE, 'The document configures DNS and DHCP services on `minio01` (10.77.42.31). dnsmasq answers DNS queries on port 53 using upstream servers 1.1.1.1 and 9.9.9.9. For VLAN 130 (lab-iot), the DHCP pool is 10.77.130.50–10.77.130.200 with 6-hour leases and a netmask of 255.255.255.0. Pi-hole runs on 10.77.42.53 with its admin UI accessible via port 18553 behind caddy-rack7, using the blocklist reese-abuse-v3. VLAN 42 has no DHCP, so all devices there use static addresses. +Source: homelab/networking/meridian-notes.md'), +('2d9eddb3-1a94-4248-ad69-f5b0fdfdaa58', 'homelab', 'networking/vela-bridges.md', '/var/home/ducoterra/Projects/Personal/brain_of_reese/tests/fixtures/agent_kb/homelab/networking/vela-bridges.md', 'Rack7 Proxmox Cluster', '# Rack7 Proxmox Cluster Three-node Proxmox VE cluster on the rack7 shelf: `pbx-node1`, `pbx-node2` and `pbx-node3`, all running PVE 8.3.4 (build `8.3.4-1-lab1`). @@ -126,9 +133,10 @@ and `pbx-node3`, all running PVE 8.3.4 (build `8.3.4-1-lab1`). lab-iot VLAN. - Node roles: pbx-node1 = hypervisor + gateway, pbx-node2 = AIO containers, pbx-node3 = GPU box. -', '520f7df799d0841b1dbcc636705aa5727d882dbb8c0453a4ccbf8b459df304f8', '2026-09-11 04:16:38.895972+00:00', NULL); +', '520f7df799d0841b1dbcc636705aa5727d882dbb8c0453a4ccbf8b459df304f8', '2026-09-16 08:56:38.799564+00:00', '2026-09-04 16:14:23.302397+00:00', FALSE, 'The Rack7 Proxmox cluster consists of three nodes: pbx-node1, pbx-node2, and pbx-node3, all running Proxmox VE version 8.3.4. Each node has virtual interfaces defined: vmbr0 for management (10.77.42.0/24, gateway 10.77.42.1 on pbx-node1), vmbr1 for the lab VLAN 42 with NAT-only access, and vmbr2 for lab-iot VLAN 130 with DHCP from minio01. The cluster firewall is enabled on all nodes, and VLAN 42 traffic is NAT-only. Corosync heartbeat uses vmbr0, keeping the management interface out of the lab-iot VLAN. The nodes have distinct roles: pbx-node1 acts as hypervisor and gateway, pbx-node2 hosts AIO containers, and pbx-node3 serves as a GPU box. +Source: homelab/networking/vela-bridges.md'); -INSERT INTO public.chunks (id, document_id, position, content, embedding, is_summary) VALUES ('6eefceee-8abb-4188-b6b5-68b38fadb867', 'a053e592-9b7d-486e-9da6-81535f6d64a8', 0, '# Lab Ansible Inventory +INSERT INTO public.chunks (id, document_id, position, content, embedding, is_summary) VALUES ('66a3f6fc-78eb-4a36-a363-c1a81e5eff62', '53d68063-3056-4f99-9a88-7812a146c877', 0, '# Lab Ansible Inventory The lab deployment inventory lives in `op-build:~/lab/inventory/hosts.yml` and is managed with ansible-core 2.19.4. @@ -145,7 +153,9 @@ and is managed with ansible-core 2.19.4. - Playbooks run from `op-build` only; the shelf has no internet route. - The `lab-upgrade` play requires the `pve-upgrade` tag on the target. - These files are the single source of truth for shelf addresses.', '[0.013243707,-0.0128352465,0.0019547255,-0.025034178,0.02702336,-0.01005102,0.0363859,-0.002513086,-0.0019683652,-0.0056082397,-0.04408114,-0.0054861284,0.027522376,-0.015843553,-0.0032135674,0.01509602,-0.00011724297,0.006546753,0.017614163,-0.03634901,-0.019627508,0.021843048,-0.01116331,0.024140809,-0.01296918,0.01725409,0.011422229,0.03565781,0.033322763,-0.04082196,-0.023349185,-0.01198729,-0.03362099,-0.0019399156,0.0126888985,0.017794173,0.008712221,0.001394899,-0.026989464,-0.09557105,0.017171873,0.016430007,-0.041646946,-0.012717809,0.025636157,0.005930746,-0.02121914,0.021009343,-0.014569487,-0.016641594,-0.04910908,0.0017814087,0.004953145,-0.0031862764,0.025204713,0.020313593,0.01969703,-0.051585037,0.030326033,-0.029000891,-0.02347819,-0.008223794,0.018297756,-0.01098346,-0.011048523,-0.009284077,0.010952927,-0.013212637,-0.050052542,0.0015116878,0.0079887435,-0.021479897,0.023794016,0.032033417,-0.0016191782,-0.015217598,-0.03447513,0.014565385,-0.057121683,-0.014002848,0.041706204,-0.005959589,0.0060224696,-0.0204793,0.012521412,-0.006303612,-0.044611067,-0.00467792,-0.008517854,-0.0053633754,0.020172304,0.021619186,0.0005239858,-0.080017805,0.01529577,0.0015442024,-0.011695493,-5.957887e-05,-0.022696357,-0.017713794,-0.003329005,0.0058646468,0.002935853,-0.013866694,-0.02277299,0.03398878,0.021213993,0.0023002306,0.013768723,0.0103675015,-0.0051184483,-0.03909858,-0.020735694,-0.030986298,0.014522613,0.022964546,0.009084225,0.0061787367,0.030804787,0.017979302,-0.00852432,0.028770719,-0.017288633,-0.0050971103,-0.0018098141,0.017639168,-0.011298532,-0.0403817,0.025823312,0.0056047323,0.013507997,0.018194491,0.03588094,5.5394677e-05,0.013518262,-0.008565419,-0.0010493967,-0.024363104,0.013184747,-0.03515832,0.021115607,-0.049394153,0.03870123,0.005634947,0.050738696,-0.046896733,-0.0029160536,0.0056243804,0.014751591,0.015311016,-0.0012607635,-0.020500273,0.006191942,0.008142317,0.005033765,-0.008505002,-0.00882494,0.010440774,-0.0035533654,0.0006726597,0.007936897,-0.015687428,0.06958979,0.011704834,-0.020871835,-0.01940203,-0.012850117,-0.01290134,0.012459115,-0.01286111,0.0010525442,0.021971611,-0.013106545,-0.0010058926,-0.015072058,0.023954568,0.018268595,0.021084037,0.0021309752,0.006486254,0.023034988,-0.027143888,0.031964358,-0.017557139,0.01367743,-0.029482841,0.021734253,-0.034227744,0.0036470115,-0.011511932,-0.0136344405,0.0126612,0.00082304503,0.009397778,-0.032351434,0.017433247,0.006008721,0.008718234,-0.031024123,-0.044650123,0.0131605435,-0.002495953,-0.029082693,-0.024488,0.020053474,0.030387333,-0.0061772666,-0.023270534,-0.0017449781,0.011454752,0.029030161,0.0072354465,0.013117538,0.020577988,0.002245291,-0.005234876,0.004875423,-0.011284984,0.013831045,6.666326e-05,0.031060737,0.014888739,0.012995825,0.002694933,0.031477913,0.010705367,0.009346292,-0.0061707064,-0.00992261,-0.028578456,0.0033538414,0.017268809,0.022283478,0.009084584,-0.02669227,-0.062048934,0.006088067,-0.0015534722,0.00024874858,0.024307625,0.0033969735,0.017501011,0.018406063,0.012257899,0.0024825914,0.011945767,0.025286388,-0.01336253,-0.04005653,-0.013366831,0.00016858967,-0.0150807295,-0.018939726,-0.024651818,0.032618623,0.010378422,0.00816154,0.0015352393,0.0058718417,-0.0014963901,0.008343149,0.009092777,0.0078062997,0.017059177,-0.0014545813,-0.0044085314,-0.011013153,-0.027818572,0.013625809,0.00088979315,-0.033877566,0.013769177,-0.015751397,0.030415813,0.0053004595,-0.012012374,0.042492907,-0.026129462,0.017857991,0.014757782,0.034212038,-0.028255144,-0.031153738,-0.0208306,-0.015646942,-0.03244824,0.02857202,-0.017769633,0.030526763,0.011767631,0.013512412,0.004398589,0.0098373005,-0.0018924761,0.022184541,0.016675455,0.0033274114,-0.03449235,0.010143173,-0.013732179,-0.009266764,-0.008188583,-0.011176804,0.005126446,-0.02455554,-0.0350694,0.024437597,-0.0035163108,0.005672017,-0.0059790253,-0.017222028,0.016544089,-0.020211818,-0.003794192,-0.0026761636,0.022248005,0.01903592,0.01414654,-0.015811957,-0.019412482,-0.015289699,0.011394453,0.024607353,-0.017270185,0.03723887,0.04343247,0.009117531,0.013847502,-0.010062529,7.1181e-05,0.062179066,0.09010617,-0.0022222723,0.006409135,0.0071915477,0.010895524,0.011107339,0.034558468,0.03802365,0.03720733,0.033421267,-0.023704275,0.01462052,0.021062283,-0.006043507,0.010695791,0.026139956,0.004537822,0.058068894,0.005986127,0.0155631425,-0.024892585,0.00027051507,0.003393463,0.02525237,0.026490193,-0.03162742,-0.027992412,-0.015827494,0.030073948,-0.07057165,0.00539886,-0.02057992,0.020540223,0.0068986923,0.01756063,0.028855225,-0.0008232112,-0.036383916,0.056197934,0.02053185,-0.015247768,0.006972787,-0.023284262,0.024706274,-0.010340418,0.004265057,-0.0005430383,0.007850707,0.0052151224,0.053063285,-0.0063086143,0.018810201,-0.02189751,0.0067011598,0.013246322,-0.015287204,-0.03069716,-0.00035621788,0.006282735,0.011282402,-0.019562107,0.025916709,0.00089787255,-0.027869673,0.0003884989,0.030883497,0.020914564,-0.02251685,-0.0011071025,0.016003989,-0.0043504774,-0.009861116,-0.018504212,-0.00038649884,-0.002730932,0.012599917,0.5962195,-0.009152897,-0.03760986,-0.01365894,-0.004686175,0.011187252,-0.031298935,0.01334345,0.0039640306,-0.007615152,0.0068328436,0.0055018025,-0.020915035,-0.0025845733,-0.026298124,-0.028142655,0.014340335,-0.00097223016,0.020157458,-0.019406704,-0.059332658,-0.022455517,-0.0735775,0.0280202,-0.021778332,0.008305056,0.025796121,0.04828509,0.017930826,0.037004862,-0.062024355,0.013102827,0.0058363783,0.00019557535,-0.014906133,-0.004944261,-0.0032015566,0.014936546,-0.020785434,0.011632971,-0.007209346,-0.0076893046,-0.036852855,-0.034075584,0.028216733,0.04143087,-0.05372739,-0.012303433,0.029553916,-0.01279119,0.0022284638,0.0163803,0.059877735,0.01209722,-0.010004236,0.0070340564,-0.008721748,0.007833697,-0.005983081,-0.0041162767,0.0070409537,-0.0025250516,-0.038625404,-0.019826453,-0.01898965,0.014385476,-0.012470305,-0.023436714,-0.0019519689,-0.0053672073,0.008818317,0.005658672,0.019795803,0.01574107,-0.02779403,-0.014249827,0.0064809597,-0.0058654454,-0.028157549,-0.029932516,-0.021267124,-0.006363535,0.010000626,-0.028941572,0.0048525943,-0.053766098,0.005541182,-0.005576335,0.0118172215,0.036481287,-0.012373216,0.031962298,0.0073529636,0.047022972,-0.031323563,-0.025493877,0.010040255,0.030179927,-0.00326126,-0.00088375044,-0.012069897,0.011701702,0.0067243297,-0.007839504,0.0021989287,0.044502683,-0.0069334586,0.027925314,0.014361307,0.01722135,-0.031227292,-0.0030248833,-0.032946136,-0.0026885124,-0.033750717,0.0319288,-0.02285328,-0.029958645,0.002912204,-0.0041536964,-0.023306262,-0.004009821,0.035103746,0.030880542,0.015122729,-0.006122689,-0.015245125,-0.06124895,-0.02462424,-0.027044022,0.0047263526,-0.0066784336,-0.024198364,0.008289448,-0.0043963064,0.001535028,-0.0025284812,0.030400665,-0.038280647,-0.010751735,-0.010187664,-0.015443415,0.012994212,-0.009203223,0.0127542345,-0.004142154,-0.02055053,-0.007513913,0.032094933,0.003631807,0.017246194,0.0035726673,-0.014342318,-0.018973337,-0.035233963,0.00025584994,0.0010380727,-0.04327715,-0.018499445,-0.0028698205,0.008865057,-0.0054183938,0.016315717,-0.0012779945,0.022601169,-0.041607063,0.008244511,-0.01898295,0.007667829,0.029676897,0.021520728,-0.036045387,-0.030925691,0.06362404,0.016486559,-0.015876304,0.030018562,-0.017137697,-0.0018977048,-0.0614861,-0.012598878,0.018104682,-0.026762657,-0.03586926,-0.015874155,-0.004484293,-0.012034174,-0.0034145596,-0.03244005,0.00052743277,-0.011755669,-0.023678288,0.015359879,0.029775653,-0.030895498,0.015243749,0.0069879577,-0.007775612,-0.005894554,0.00404858,0.008153662,0.029028922,-0.103886396,-0.06300147,-0.040324993,0.014738666,-0.030903505,-0.005380083,0.00479972,-0.013979445,-0.010730124,-0.0015290496,0.03202446,-0.0022236044,-0.05069758,0.01257021,-0.030463068,-0.002194062,0.0026955723,-0.038842022,-0.018324634,0.02187549,-0.03113017,-0.011220648,-0.014118834,0.0018811014,0.004431487,0.0035116111,-0.012229569,-0.005359658,-0.029586976,0.013342969,0.03960584,-0.024912437,-0.02084592,-0.024105053,-0.009539531,-0.006504481,-0.01599788,-0.0047218525,-0.0308851,-0.011813184,-0.018866016,0.005213318,0.006050005,0.0031432123,0.00031233573,0.012762122,0.0089366045,-0.038346514,-0.013111467,0.0041915667,-0.0036324474,0.07344826,0.016927762,-0.018709943,0.008533934,0.007284777,-0.033627763,-0.012821086,-0.0104801,-0.013542603,-0.0104402965,0.033605028,0.03159867,0.013711957,0.005683676,-0.012270489,0.015225894,0.021720594,0.009340693,-0.009213327,0.0071403184,0.021436322,-0.014758995,0.0038774663,0.021148756,0.045758888,0.04046954,-0.05473027,0.014617992,-0.001955312,-0.004101191,-0.019052884,0.009115258,-0.007392201,-0.009878139,0.001136668,0.048035763,-0.04436095,0.05787015,0.025376474,-0.007815715,0.046007283,0.003415971,-0.01476289,0.0027159727,0.012463498,-0.0038894706,-0.031714525,0.011157288,0.009905591,-0.016428512,0.024322804,-0.035546508,0.0264122,0.025084179,-0.026237035,0.004806986,0.036058813,-0.00505341,-0.015136581,-0.031691045,0.019558635,-0.0043123933,-0.006154057,0.008191376,-0.0025635592,0.009082912,-0.0016534218,-0.049305562,-0.014248054,-0.02073708,-0.0061675874,0.48297983,-0.027113182,-0.0016253266,0.0007709881,-0.021306982,0.0060137594,-0.0015741717,-0.010387024,-0.016571535,0.010178796,0.021282358,0.005351372,0.007892563,0.02710505,0.0050336253,0.0097162565,-0.007814699,0.013188433,-0.004998858,-0.017770506,0.0044398014,0.0023147718,-0.00083244033,-0.01972588,-0.037353918,-0.05821515,0.024033142,-0.0102176415,0.011014663,0.005453703,0.052829962,-0.023385895,-0.01782453,-0.0013423671,-0.0060053687,-0.024148583,-0.014389759]', FALSE), -('bfffafed-d351-4447-95c7-c5df5fccafa4', 'f90d20cc-a0be-4ab2-89dd-0d57ac6145c4', 0, '# GitLab Runner (lab-ci) +('55371711-50a5-458f-ba35-97e6772bba2f', '53d68063-3056-4f99-9a88-7812a146c877', -1, 'The lab Ansible inventory defines a group called "lab" with three hosts: rack7-pbx1, rack7-pbx2, and minio01. Each host has an IP address and a role assigned. The inventory uses ansible-core 2.19.4 and sets variables for the ansible user and become settings. Playbooks run from op-build, and the lab-upgrade play requires the pve-upgrade tag. These files are the single source of truth for shelf addresses. +Source: deployments/ansible/lab-inventory.md', '[0.014020604,-0.012202159,-0.0015335995,-0.008271892,0.017590167,-0.02774162,0.027370915,-0.007681813,6.876737e-05,-0.013074403,-0.052735854,-0.019707337,0.021851087,-0.028395306,-0.022780852,0.007789266,0.0024288234,0.01642925,-0.0009766057,-0.029963916,-0.009157839,0.039708123,-0.009574358,0.03479195,-0.020545963,0.020932801,0.032314934,0.030365296,0.024619922,-0.021510284,-0.038218908,-0.0058197393,-0.054899126,0.00042121214,0.012047014,0.013872403,0.017226325,0.0011800005,-0.040563397,-0.10230443,0.025553957,0.023025729,-0.015381544,-0.011662423,0.023980055,0.0008108559,-0.008477306,0.008183347,-0.012169742,-0.024798948,-0.06574565,0.0027793807,0.013891096,-0.003169073,0.02456944,0.018168192,0.03354464,-0.0555654,0.020604283,-0.049160305,-0.019955218,-0.010175589,0.028875126,-0.0083691,-0.017590465,-0.008949928,0.0062097483,-0.039926212,-0.035499997,0.0016487369,0.017245036,-0.041953873,0.032387186,0.034027282,-0.013576783,-0.025912708,-0.0342146,0.009911465,-0.06797855,-0.00074191316,0.02874178,0.016331017,0.014629578,-0.010600709,0.0048754425,-0.0013507016,-0.042703856,-0.006055406,-0.007024629,-0.016221564,0.006396859,0.032427672,0.002135777,-0.06957603,-0.0017901969,0.003521997,-0.01703424,0.022986494,-0.0115736965,-0.01981344,-0.020493176,0.00015588266,-0.0049341363,-0.023066465,-0.024667067,0.026290873,0.03227907,-0.0042090663,0.013653079,0.01553148,-0.0077328505,-0.031954158,-0.015168191,-0.032961834,0.011042366,0.017837169,0.011622123,0.005763056,0.044717792,0.002417074,-0.008605827,0.043356117,-0.027440306,0.0103174085,-0.009322791,0.015955677,-0.019750789,-0.038195033,0.033354316,-0.004898369,0.006111383,0.026044946,0.027221683,0.008344263,-0.014945869,-0.016473273,-0.0034900948,-0.03508292,0.03299565,-0.05056734,0.020312259,-0.044170987,0.036917966,0.010076796,0.04016156,-0.050861854,-0.017871419,0.0012845192,0.02303549,0.010549971,-0.0012577274,-0.013432044,0.015413666,0.017202059,-0.0048863837,-0.0066266563,-0.00033034882,0.004392827,0.0055241208,0.009401786,0.017425794,-0.0042576618,0.07066634,0.015573614,-0.02036031,-0.02281911,-0.009451383,-0.011013535,0.0047038742,-0.020384904,0.0072869523,0.010136293,-0.018949332,0.0028423774,-0.012820954,0.03851521,0.0052980003,0.038943842,0.0029781635,0.007068765,0.029158704,-0.022198785,0.048204806,-0.0094766095,-0.0019189279,-0.018892076,0.013363629,-0.03554645,-0.013962743,0.005597148,-0.0011190703,0.014129813,0.004419677,0.0056003192,-0.037160024,0.017487299,0.005284047,0.0119177215,-0.011303575,-0.04174858,0.0069446964,-0.010995072,-0.01868088,-0.042833287,0.009668713,0.035962373,-0.0075407485,-0.010821966,-0.007798259,0.011633676,0.036792707,-0.009771816,0.017519979,0.007867752,-0.0043885736,-0.004302618,-0.011559241,-0.018971156,0.013593559,-0.00056715065,0.039584797,0.024430199,0.0037896365,-0.0043520224,0.035584316,0.0041087586,0.018424766,8.129601e-05,-0.006130368,-0.030165613,0.014332316,0.0088529335,0.016891034,0.011672566,-0.03799946,-0.060059033,0.005310045,-0.0082044285,-0.01833421,0.029770436,0.02178641,0.027990522,0.01314421,0.012632816,-0.016501337,0.032886058,0.013442785,-0.024509808,-0.04411632,-0.013436572,0.00053166156,-0.0028077592,-0.027206192,-0.013702758,0.029802933,-0.011298138,0.001504803,-0.008004661,0.010413642,-0.00062900485,0.012113746,0.013668672,-0.002920744,0.022389228,-0.00341081,0.001043151,-0.012800415,-0.029694242,0.006563422,-0.0017139022,-0.017047986,0.015983932,-0.00900608,0.034698162,-0.01329956,-0.009755428,0.03344699,-0.04514519,0.029007567,0.008075164,0.03157552,-0.013658182,-0.022600904,-0.016104506,-0.010191851,-0.022537047,0.028090956,-0.016655229,0.029497396,0.014425555,0.0065882807,-0.010272128,0.01670857,0.00078021915,0.0030701251,0.007819151,-0.013862215,-0.038527876,0.0075511658,-0.019265033,-0.013067965,-0.020752246,0.005537396,0.0043411287,-0.014476368,-0.049484074,0.022894697,-0.0037859795,0.009318499,0.009810708,-0.030470222,0.0056822216,-0.011758404,-0.0060837897,-0.039604977,0.01364144,0.0169761,0.043221544,-0.020827347,-0.013459558,0.0018948977,0.007141671,0.026048329,-0.030126326,0.019422123,0.045498736,0.014693526,-0.0020303712,-0.028828902,0.013495941,0.05676043,0.11496741,-0.012346647,0.00032611468,-0.0037505024,-0.0024604546,0.031536262,0.026890669,0.04137763,0.024038728,0.026304377,-0.015260585,0.0060934285,0.024266774,-0.01194795,0.008014261,0.014515089,0.0024608923,0.051977847,0.013235692,0.017540025,-0.0110572055,-0.013222978,-0.018891744,0.02364411,0.026674148,-0.032944895,-0.0274075,-0.020173062,0.02908319,-0.05508447,0.008173796,-0.025381807,0.01286121,0.007533222,0.016193025,0.028653678,0.0139219565,-0.03389664,0.048053488,0.020768488,-0.011883624,0.0073709944,-0.028853586,-0.010463446,-0.011000913,0.0008439401,-0.0055509936,0.014625177,-0.007971303,0.05825443,-0.01228262,0.01762368,-0.03259645,0.0028606502,-0.0038339898,-0.02509269,-0.020285212,-0.00558576,0.0004424649,0.009052539,-0.019068167,0.025652828,0.0017487811,-0.027016861,-0.006979617,0.005584163,0.001023336,-0.016978024,-0.006311059,0.013905599,0.0047829566,-0.0046533626,-0.02890675,-0.014456882,0.0038846023,0.009158071,0.6000437,-0.0024758554,-0.025922589,-0.0051230574,-7.1552284e-05,0.0125652775,-0.016001094,0.0034266948,-0.0011412085,-0.00273293,0.0053494456,0.011422394,-0.012212304,0.011083309,-0.03637189,-0.031829346,0.011776877,-0.011756501,0.015685992,-0.027217021,-0.06193559,-0.02106548,-0.061457116,0.02026153,-0.0258218,0.021301042,0.009459417,0.042980485,0.013573491,0.047786582,-0.05072584,0.0045037232,-0.0074205324,-0.015482218,-0.022657529,0.0033464467,0.003929547,0.01912607,-0.018412177,0.0034777827,-0.01783451,0.016102223,-0.03713045,-0.04405914,0.039649263,0.04996823,-0.051277496,-0.023062984,0.03202705,-0.012479008,0.008661666,0.007098082,0.060774185,0.003239536,0.0018427033,0.005697484,-0.0020371233,0.002695656,-0.0193536,0.007927974,0.013674119,-0.00786025,-0.03505011,-0.016116692,-0.010335289,0.003744791,-0.016290111,-0.022604529,-0.0055753454,0.00049434474,0.0027428104,-0.0057934406,0.017891224,0.004379733,-0.028376997,-0.00040559372,0.008599802,0.0040700343,-0.027499648,-0.027385727,-0.025550067,0.0015646572,0.009614719,-0.015325897,-0.005891429,-0.057393804,0.011903145,0.002742574,0.008668679,0.028890712,-0.0005179214,0.008537138,0.0031761075,0.060240656,-0.03608996,-0.009850331,0.00821024,0.022189422,-0.012732383,0.02028538,-0.013741919,-0.008510494,0.004948642,-0.01566298,-0.010557738,0.08376936,-0.020093048,0.030341085,0.019895608,0.011501758,-0.031919695,0.0035807013,-0.033777352,-0.009939484,-0.029790243,0.025102187,-0.024066364,-0.026847677,-0.012905752,-0.0016875559,-0.02460411,-0.0012148425,0.029828273,0.032935973,0.003294256,-0.00836824,0.015474761,-0.07113757,-0.032296807,-0.02844438,-0.0071381624,-0.0019590056,-0.04048018,-0.011769984,-0.025343357,-0.0005342146,-0.005091221,0.035867233,-0.03108521,-0.0072023985,-0.017837608,-0.026142227,0.0056683077,-0.01159659,0.011491553,-0.0037944994,-0.020514276,-0.009329098,0.027409239,0.015617401,0.016855966,-0.0003543529,-0.018480714,-0.026620865,-0.027673855,-0.0047137747,-0.0065364777,-0.047949854,-0.022070615,-0.002756999,-0.0150590055,-0.010318972,0.024282428,0.0028376253,0.018535266,-0.030210137,0.014960173,-0.0038312646,0.012688662,0.024611846,0.012462357,-0.028569652,-0.02737627,0.054019514,0.024299068,0.030596882,0.026985945,-0.036750015,-0.0095095225,-0.05059486,-0.023935053,0.0020813232,-0.035059415,-0.040035035,0.0017505129,-0.0008983363,0.0022834472,0.009762918,-0.029915849,0.0049085575,-0.007268664,-0.013084336,0.013453697,0.029218419,-0.024614163,0.025224218,0.0016071108,-0.010696005,-0.0022537343,0.018821234,0.0011905411,0.02363467,-0.100738145,-0.05834604,-0.033747356,0.015373463,-0.0409644,-0.017833615,0.0076857754,-0.010251118,0.009960851,0.008601086,0.020205408,0.009445203,-0.042745363,0.0075239744,-0.03275653,-0.0043260963,0.009646031,-0.048541028,-0.02590771,0.013865483,-0.040538453,0.0029998936,-0.018149206,0.008270456,0.010738086,0.01105856,-0.011563724,0.004104679,-0.01250044,0.003223077,0.013359108,-0.031569928,-0.018017244,-0.017766282,-0.007956902,0.00035288182,-0.029525172,-0.0028692707,-0.030230409,-0.005969176,0.0069405613,0.028846128,-0.0047611585,0.015704751,0.0066011064,0.023351073,-0.0019147812,-0.042134494,-0.010176044,-0.0018103968,-0.02282191,0.08901973,0.023598513,-0.032740474,0.041448187,0.018480947,-0.009901606,-0.017664224,-0.02256298,-0.02104655,-0.005761112,0.012716175,0.030482953,0.007207993,0.0065788673,-0.031072728,0.027093114,0.011543793,0.014472166,-0.022459602,0.0076193637,0.027403282,-0.017258884,0.0055475794,0.03318813,0.029994624,0.024222843,-0.04782944,0.012190151,0.013470233,-0.005053067,-0.025748653,0.026049012,-0.027007505,-0.018044904,0.009143241,0.04603524,-0.02583452,0.05990795,0.036245573,-0.001912381,0.0532756,-0.001121591,-0.012387744,0.013089629,0.010472807,0.004390238,-0.041602913,0.0018958527,0.0054609366,-0.028368361,0.02429965,-0.03146207,0.0090588,0.023153938,-0.01270576,0.0049379673,0.029471915,-0.025392499,-0.020453565,-0.030933682,0.023035012,-0.010813631,-0.009244084,0.006247621,-0.0033719048,0.0067992467,0.0020293235,-0.040471707,-0.023970194,-0.01107124,-0.0007354404,0.4351086,-0.03099356,0.012935767,0.002208837,-0.024821552,0.013459746,-0.0005139287,0.01075241,-0.0138920965,0.01732744,0.010419957,0.0019535844,-0.0054732314,0.030534226,0.0009726351,0.013316851,-0.024632402,0.010450816,0.014002035,-0.02985218,-0.0015829793,-0.011784337,-0.0052603264,-0.02967571,-0.048318863,-0.05847023,0.022384342,-0.029054416,0.01303391,0.0047471863,0.0348233,-0.023839055,-0.02099768,0.017419523,-0.00036000853,-0.013446736,-0.01573651]', TRUE), +('866b9c4f-d387-4083-bb88-5bff00be29de', 'bf670a2f-cdbb-4e83-9b67-e3a8645c412c', 0, '# GitLab Runner (lab-ci) CI for the shelf runs on a single GitLab Runner registered against `git.reeseapps.com`. @@ -157,7 +167,28 @@ CI for the shelf runs on a single GitLab Runner registered against `debian:13-slim` for lint. - Concurrency is capped at 2; the `pve-upgrade` job never runs in parallel with itself.', '[-0.024116822,0.013582843,0.004386523,-0.039468747,0.010714522,-0.02940293,0.00062085653,0.036253355,0.0007905356,-0.042149622,-0.010932942,-0.040079787,0.027607756,-0.017769411,-0.0012000569,0.012741768,0.021396674,0.0043564094,-0.0013055144,-0.025355283,0.009088329,-0.020417389,-0.013989464,0.02165144,-0.008005497,0.019243274,0.013345685,0.0068106353,-0.013564918,-0.052589975,-0.021433057,-0.004943664,-0.03801153,0.0018660363,-0.019979395,0.0022761505,0.032552965,-0.0115053505,-0.012950076,-0.08554341,-0.0045150775,-0.00022509386,-0.012584977,-0.021409336,0.0023448118,0.0060078204,-0.046994373,0.010072697,0.0021553528,-0.0018646555,-0.039557386,-0.0014488944,0.008787829,0.020940749,-0.012491114,0.031436965,-0.004775834,-0.023784265,-0.026694743,-0.026811315,-0.0031969936,0.028058479,0.0028653515,0.009021052,-0.033479102,-0.017815948,0.022679579,0.047686335,-0.02096332,-0.004362952,0.011535343,-0.050018564,0.032637008,-0.002855468,4.602102e-05,-0.030843593,-0.022617461,0.0026673803,-0.04478168,-0.024296636,0.016531428,0.020181855,-0.017000046,-0.009045113,-0.010909765,-0.012419658,-0.044060625,0.00493711,0.011341915,-0.01309127,0.0006641895,-0.00025382204,0.0030397372,-0.053052302,-0.012423507,0.01713795,0.014637437,-0.020871148,0.008674247,0.0049016904,-0.004880794,-0.026537731,-0.006724413,-0.0039100046,-0.036311727,0.014446144,0.016475001,0.020254234,-0.003611665,0.019801605,0.019661797,0.022953292,-0.0009282561,-0.024679419,0.015475549,0.014485649,0.019377733,0.0012895243,0.00096474576,0.015161533,0.004141969,-0.005221057,-0.03668028,0.010144769,0.030615525,0.01702737,-0.05437858,-0.04211706,-0.0037851331,0.014353958,0.016203323,0.021185944,0.053818356,0.0100585,0.002961705,0.054878533,-0.05313572,0.0005841338,-0.003909398,-0.05651002,-0.0029585096,-0.0060187276,-0.0016473212,-0.008509374,0.019071873,-0.04635953,-0.027045835,-0.003749647,-0.00341005,-0.0008702617,-0.01711971,-0.03428456,0.009413125,0.00038029221,-0.0406957,0.01631861,-0.0093980245,-0.020244012,-0.031952895,0.010971265,0.004810953,0.0025394915,0.027870052,0.01853287,-0.045917917,0.0063836654,0.0009425779,-0.034054942,0.0023540575,-0.02760875,-0.004611929,-0.0008282802,-0.011708838,0.015961166,-0.013897487,0.047915746,0.012038858,0.02200958,-0.0017908633,-0.02701742,-0.020263072,-0.029770609,0.021419944,0.009495933,0.0044518136,-0.03370517,0.0050876844,-0.032826453,0.00042420372,-0.03689647,-0.010855446,0.044744257,0.025869358,0.012165939,-0.023371892,0.028597029,0.022319976,0.057358354,-0.029747041,-0.013588111,-0.013032264,-0.023503736,-0.060959935,0.02228466,0.014145311,0.024900433,-0.001708143,-0.018447302,-0.010237136,-0.015946556,0.020831052,-0.0030785536,0.04038536,0.020435654,-0.0014110317,-0.022520095,0.029113073,-0.038251244,0.028171482,0.02982935,0.02014283,0.0015813858,-0.009161687,0.014605978,0.021137128,0.014737973,-0.0006142884,0.009587725,-0.005410448,-0.018578436,-0.0039915033,0.011765063,-0.03235492,-0.0044408115,-0.025799839,-0.07523895,0.00042489005,0.0134699065,0.0033270319,0.004331067,0.023863774,0.040818363,-0.008955573,0.021866206,-0.037772518,0.017683975,0.0029602747,0.026456874,-0.028068474,0.00054297684,0.011153577,0.019826673,-0.028045254,-0.01114085,0.08636642,0.0064883954,-0.0055549894,0.022260739,-0.035033498,0.011486429,-0.001824893,0.010180993,0.0142385885,0.018409695,0.012820815,0.019024586,-0.006076182,0.008856199,0.007005139,0.001980965,-0.02051117,0.026708668,-0.020427698,-0.0028726158,-0.024111988,-0.010099944,0.033126045,-0.021200594,0.008469044,-0.026756683,0.011387502,-0.046990383,-0.03599431,-0.025596738,-0.016734155,-0.033334095,0.020245666,0.0012142167,0.029057834,0.026086109,-0.018239519,0.004188605,0.018955192,0.0018954043,-0.017912008,-0.004315132,0.010276821,-0.0004918768,0.00870519,0.00020935052,-0.030974023,0.033415034,-0.017046014,-0.01998852,-0.015504538,-0.010690331,-0.00866975,0.0051916847,0.0041865488,-0.011581849,0.0077387444,-0.01949865,-0.006847701,0.0035261721,0.0012294085,-0.007572184,0.038176216,0.017446775,-0.0111778015,-0.040084463,-0.015286507,0.002566728,0.020304082,0.015757266,0.021291269,0.045004603,0.00788068,0.031150691,-0.014897958,0.034656532,0.026969617,0.008756257,-0.031970605,0.018212654,-0.0062813074,0.017376753,0.021032576,0.04024,0.016006358,0.020420633,0.021126747,0.0046155606,0.018095518,0.016892124,-0.018886218,0.0069549107,0.031638943,0.001078911,0.04806689,-0.0038476884,0.02233359,-0.029053902,0.00647414,0.0093020825,-0.018790632,0.008803533,-0.036765456,-0.039927974,-0.0013289462,0.010331271,-0.022647886,0.024121935,0.0019764155,-0.0076370207,-0.02303141,-0.005925559,0.027104931,-0.0026038338,-0.016714126,0.050322898,0.00096380815,-0.0328902,-0.0004168509,-0.017785175,-0.017214833,-0.005997206,-0.0393216,-0.010692921,-0.013098847,-0.003761648,0.060534116,-0.0008535708,0.0117305145,-0.022003153,0.0127784815,0.012391986,0.004742131,-0.037683997,0.047139637,0.008477822,0.018390706,-0.04353765,-0.0028005517,-0.014573723,-0.023155732,-0.02024789,0.014385394,-0.010911122,-0.024676522,0.01862267,-0.006288318,0.002037177,-0.030564914,-0.026819915,0.00036598442,0.026073795,0.018819705,0.6082579,-0.03076146,-0.019254243,-0.00032471766,-0.0087986505,-0.011274565,0.009158637,0.06254641,0.023093596,-0.02842072,0.011834288,0.0077121546,-0.0031696297,-0.011469505,0.012349181,-0.033126276,0.025087358,-0.018347804,-0.0049358904,-0.00097267557,-0.072676435,-0.021054696,-0.04360654,0.0048467987,0.026226593,0.0046063815,-0.0014406224,0.020856412,0.011408706,0.055701267,-0.034958713,-0.0046967915,-0.0060248063,0.015220075,-0.024398033,-0.015909787,-0.016725216,0.018912228,-0.0019152377,-0.008403512,0.024036737,0.0012114088,0.015556748,-0.024352588,0.023466606,0.012584397,-0.0015827273,-0.015215252,0.0193538,0.028887581,0.00053882296,0.02361732,0.010345831,-0.009642387,0.011155847,-0.012833427,-0.022515038,-0.03034843,-0.013201996,-0.012981448,0.008977614,-0.0067193964,-0.04222164,-0.037844915,0.007520403,0.0061633145,-0.0034429356,-0.0074062003,-0.005778101,0.012826829,0.004585153,-0.0033465729,0.04210328,0.015616376,-0.015145456,0.040293545,0.024506524,-0.015472268,-0.008369547,-0.02565445,-0.021565529,0.031324647,0.00076559896,-0.012005482,0.003926164,-0.032529734,0.019109204,-0.029080424,-0.010937735,-0.0016042006,-0.040208135,-0.006432157,0.0011765962,0.052878164,-0.01404623,-0.0033037702,-0.0009563177,0.012319135,-0.018224334,-0.020786881,0.0058521307,0.007672871,0.005807977,-0.0029974934,0.016115382,-0.05616418,0.030537229,0.029368784,0.0047586937,0.019905247,-0.010384243,0.007051042,-0.008827569,-0.051557805,0.028857669,0.015849125,-0.0023454516,-0.02829373,-0.015052764,-0.022746792,-0.016548665,-0.030782826,0.057959914,0.045012232,0.019627927,0.016590364,0.013007738,-0.037896518,-0.011078249,-0.031482972,0.020727562,-0.026125627,0.000576315,0.004130303,0.014417994,-0.01879066,0.00500434,0.0322011,-0.039125834,0.0070535266,-0.02370904,0.010999503,0.033413537,0.016256576,-0.03044955,-0.0053935912,-0.024242504,0.00020798342,-0.021646354,0.01099937,-0.0031402665,0.0017626273,0.0048102513,0.012284462,-0.0073124515,-0.020521821,-0.0012996235,-0.008350085,-0.0038437056,-0.026358362,-0.017053569,-0.008282087,0.030537201,0.010937768,0.008246693,0.00866275,0.011908377,0.023442801,0.026611324,0.025424074,0.030008439,-0.044106,-0.014928423,0.0571431,-0.012711779,-0.016584154,-0.0045287935,-0.0022971826,-0.030279748,-0.042003736,0.009975218,0.014749385,-0.012462775,-0.0052131754,0.016805423,0.022552723,-0.010052815,0.0016899536,-0.0027734602,-0.023034649,-0.036692962,-0.0032133225,0.012313432,0.0055910056,-0.010615684,0.022262927,0.015987296,0.009635059,-0.024490345,0.018711839,-0.014715654,0.007568695,-0.05719657,-0.046245568,-0.004131033,0.03042342,-0.004074865,0.019796697,-0.013365249,-0.014446556,0.017023593,0.0032932954,-0.008381949,0.014810913,-0.012148249,-0.0030019968,-0.005564855,-0.01544994,-0.04391753,-0.037873812,-0.026438795,0.0142047135,-0.05379285,-0.0293535,0.0109109925,-0.035060443,-0.006734033,-0.03322203,0.016078046,-0.013065838,0.005103742,9.221103e-06,0.005402875,-0.0061820894,-0.037343495,0.012524797,0.009832902,-0.009529433,-0.008971119,0.033547264,-0.02994443,-0.008482362,-0.013524741,0.015089063,0.010441941,0.008710904,-0.017310515,0.016912488,-0.004576312,-0.06900509,0.006175203,0.008040931,-0.009450683,0.013769464,0.006098804,-0.023779249,0.036455184,-0.012823508,-0.017660301,-0.0042583463,0.021754405,-0.0033332643,-0.012997082,0.034163292,0.0037508735,0.014684624,0.010679393,-0.012764521,0.005695331,-0.007978649,-0.03197465,-0.018878765,-0.019604955,0.02245284,-0.03362725,-0.00434011,-0.0017746734,0.025670532,0.042606004,-0.04360027,0.00401066,-0.016906202,0.006705162,-0.049133934,0.0097822435,-0.008350082,-0.0024336234,0.015097091,-0.011603665,-0.025388477,0.046054125,0.027392337,-0.02911631,0.0067302366,0.010617388,-0.013633539,0.021514878,-0.009625878,-0.005938455,-0.000515592,0.0066952948,-0.016069094,0.0040316302,-0.014688354,-0.038111035,0.024709098,0.048691783,-0.01766583,-0.0110362815,0.03181625,-0.023211205,-0.030641198,-0.027594946,-0.009887914,0.002914938,-0.016349852,0.028198335,-0.020968823,-0.020051096,-0.0043889903,-0.034783382,-0.012574511,-0.013167687,-0.013881421,0.47548082,-0.0152872745,-0.03137056,0.020359354,-0.032009386,0.027026398,0.005965917,0.014332174,0.0085998755,-0.03438479,0.022741614,0.015041919,0.01468982,-0.0051415614,-0.012292642,-0.032745294,0.0027117417,0.0027087643,-0.008033892,-0.013110882,0.0043599326,-0.0046270015,0.012419274,-0.011490081,0.004123226,-0.053247765,0.045349568,-0.01951295,-0.0076957433,0.017521137,0.036079083,0.018929943,-0.023364812,0.0080572795,0.038413588,-0.04283349,-0.013518736]', FALSE), -('e64a231d-3fa4-4831-801b-ece57ad95cf5', 'f183f389-a110-4373-8640-4af205d2d026', 0, '# BOR Vault Sync Unit +('4789dafd-8d75-4b8f-b41b-8099cb7154ac', '2d9eddb3-1a94-4248-ad69-f5b0fdfdaa58', 0, '# Rack7 Proxmox Cluster + +Three-node Proxmox VE cluster on the rack7 shelf: `pbx-node1`, `pbx-node2` +and `pbx-node3`, all running PVE 8.3.4 (build `8.3.4-1-lab1`).', '[0.02947179,0.05273666,-0.027711319,-0.015784733,0.010825048,-0.0339169,0.013884701,0.0017440706,0.023955395,-0.011074052,-0.0558246,0.014945878,0.0070238053,-0.018894942,0.0020477513,-0.017253065,0.021797314,0.013409431,-0.012358879,-0.0484666,-0.007395613,0.040150363,-7.947853e-06,0.05840922,-0.01850724,-0.010216898,0.005258857,-0.011595587,-0.009903863,-0.0026851145,-0.038947523,0.0006657878,-0.09932908,0.03448276,-0.006496546,0.013543051,0.013305193,0.0096986,-0.03473789,-0.22742008,-0.03616923,0.020848395,0.032240774,0.017168246,0.03329197,0.018844824,0.020591637,-0.004859286,0.0007690272,-0.036261626,-0.071519606,0.011710496,-0.005465223,-0.009200342,0.02633801,0.0038075827,0.041970693,-0.028817924,0.05402746,-0.04305386,-0.013251919,0.008083975,0.019859506,0.0007425523,-0.038418334,-0.018973026,0.016984995,0.0042781285,-0.044051558,-0.007425603,0.040315293,-0.035383996,0.016750187,0.0136492755,-0.0032498366,-0.030457217,0.0016755589,-0.016239027,-0.06867313,-0.007000096,-0.008398752,0.0038284454,-0.019395102,-0.010151075,-0.009894964,-0.005152765,0.022599917,0.008691466,0.013886691,0.011534241,0.0044699153,0.004161901,-0.0006227428,-0.04032398,0.017648702,-0.0077744117,0.018969988,0.00539991,0.010947072,-0.06952604,-0.016446015,-0.0025244267,0.003480906,-0.0062981853,-0.03614026,0.014312263,0.061299138,0.0029057048,0.020618148,-0.0007610704,-0.010225831,-0.048844468,0.02096593,-0.028540688,0.008906331,-0.023473307,-0.021172596,-0.012167171,0.033200987,0.03876503,-0.00535851,0.054586865,-0.017483814,0.0029616042,0.019550066,0.036270563,-0.05806391,-0.05922733,0.052392885,-0.008011316,-0.019170986,0.024631783,0.03474906,-0.012604656,-0.0046086847,-0.0013819279,-0.009709661,-0.052062508,0.011762868,-0.06536196,0.02587166,-0.023791105,0.021167807,0.00587518,0.006524233,-0.015164304,-0.00053952954,-0.013634156,-0.012787917,0.0005394903,-0.030857332,-0.024978958,0.02308257,-0.00096889527,-0.044553015,0.042532332,-0.0071639917,0.024208156,-0.015873313,0.030168032,0.0038441864,0.017340995,0.022198075,0.03373887,-0.034321204,-0.017553518,0.0035101871,0.0060693417,0.02643902,0.03005111,-0.011138484,0.016253341,-0.013009474,-0.029500509,0.050036453,0.025161821,-0.0041664895,-0.0185678,0.004857845,-0.025409935,0.024585254,-0.010726919,0.06775745,-0.00036631845,-0.002843825,-0.018090378,-0.016639762,-0.033036947,0.015885154,0.008696714,-0.003118673,0.024340121,0.038164712,0.00232088,-0.0072651566,0.011154616,-0.016201176,0.013677319,0.018623013,0.04099256,-0.010523035,0.0030790593,-0.03451293,0.0050639464,0.012994956,0.018477205,0.022085756,-0.0049982388,-0.011140509,0.028921321,0.04971319,0.03591568,-0.009275636,-0.00042801935,-0.0051765013,-0.028840913,0.0022352664,-0.0051006055,-0.019658484,0.01296962,-0.010268156,0.012411154,0.009956161,-0.03742223,0.06730179,0.02166912,-0.00658872,0.023532014,0.0020671836,-0.027074806,0.0071047214,0.00889803,0.014374481,-0.03053439,0.0061262366,-0.10945427,0.0030007786,0.027619839,-0.00023972546,-0.002622999,0.023956494,-0.02058311,-0.0007480924,-0.012880868,0.0051815035,0.013835906,-0.002292309,0.004215785,-0.033671677,-0.0070665823,-0.0065107276,0.024095787,-0.008306433,-0.015490541,0.0084021175,0.021848237,-0.020413084,0.0159365,-0.029002994,0.017487455,0.011847598,0.025995107,-0.0451477,0.015581239,-0.008790325,0.009772083,-0.010961809,-0.013429611,0.018719675,-0.015051938,-0.04102243,0.0056124832,-0.013872004,0.033648413,0.008184171,-0.0062649967,0.0034839734,-0.0016528892,0.00040041847,0.016111424,0.028487293,0.022721548,-0.016209355,-0.035366934,-0.04437892,-0.0052462835,0.05001252,-0.0020419587,-0.0011713025,0.0039600353,-0.027367905,0.014869539,0.009023408,0.010541941,0.0013969438,-0.025515066,-0.02800142,0.029551605,0.0072172564,-0.014843135,-0.030121395,0.06942979,0.02446107,0.0040812725,-0.018107066,0.0062876954,0.010419319,-0.018091865,0.005578113,0.018851183,0.0023110546,0.018473797,0.015942076,-0.014172991,-0.019989518,-0.020644477,0.001722023,0.026922401,-0.06252025,-0.01804985,-0.0006978744,-0.0153585775,0.012346434,-0.018981677,0.012789961,0.019105062,0.039592054,-0.02221463,-0.002966364,0.03861257,0.08506798,0.016917462,-0.021648878,-0.0027141408,0.0060126437,0.008429997,0.004264167,0.016824607,0.023519265,-0.03939255,0.007140928,0.0022091954,0.009076951,0.033035196,-0.028514955,-0.023441076,-0.006397857,-0.0137089845,0.01764717,-0.0037272298,0.050958354,-0.0068051065,0.012758356,0.040014293,-0.0081170825,0.027842706,-0.032733686,-0.022652548,-0.007898523,0.02634825,-0.058481205,0.03384183,-0.045804188,0.005212702,-0.026303168,-0.005983731,0.036592137,0.0061082216,-0.032126974,0.037915602,0.005970488,-0.017598787,0.05656355,-0.015831025,0.027316537,-0.017011479,-0.022025036,0.013478694,-0.0065615214,0.01682456,0.051431425,-0.02649923,0.023716958,-0.04061072,-0.0022369958,-0.021982023,-0.031336512,-0.016288681,0.01705889,0.020724481,0.031379692,0.0017335167,0.061135367,-0.008405098,0.03181531,-0.008567795,0.037682474,-0.027704218,0.021738581,0.0008446226,-0.01867015,-0.02473881,-0.03391406,-0.037349068,0.00591665,0.030534143,0.00018393622,0.54197407,-0.0041990383,-0.012584305,0.0016728651,0.022779673,-0.026268287,0.010007982,-0.018740615,-0.021577418,-0.0105613135,0.008203874,0.03274404,-0.04295889,0.03568519,0.010836502,-0.034529332,0.023684584,0.0036330111,-0.022633921,-0.029080266,-0.055202305,-0.026085654,-0.057023715,0.0032710636,0.03267922,0.03708666,0.011950908,-0.01553815,-0.0050555957,0.029106177,-0.04080792,-0.0122996345,0.005014248,0.012663238,-0.0329788,-0.02273598,-0.004665696,0.04138589,0.0062054424,-0.0004994814,-0.04947562,-0.010197292,0.026763968,-0.034045685,0.035082825,0.01318003,-0.009237884,0.00658673,0.02627062,-0.001062633,-0.00063803076,0.014331438,-0.0054668733,-0.006188321,0.014981679,-0.029719463,0.045635793,0.03029133,-0.013203186,-0.0034704662,-0.034685567,-0.0013221087,-0.03645798,-0.0032345976,0.025256615,0.00018678512,-0.02486103,-0.0036582672,-0.01686654,0.01760221,-0.014246641,0.012963446,0.0016298617,-0.038205232,-0.019453462,-0.036377013,0.03150286,-0.0016855684,-0.003946158,-0.0105712805,0.032511126,-0.015622715,-0.016928658,-0.0128994705,0.010841793,-0.018525306,-0.0047999085,0.021090347,0.03174067,0.06497855,-0.0014641815,-0.011168603,-0.02827319,0.02489609,-0.022814896,-0.007404194,0.054635815,0.0037655672,0.009922268,0.0080024,0.026780283,0.008828925,0.006303184,-0.025911639,0.045230087,0.041039784,-0.009569589,0.05737916,-0.011171908,-0.01475545,-0.02474837,0.010145209,-0.03682598,-0.044958666,-0.031844135,0.057749864,-0.013231724,0.0035270287,0.024880195,0.011509866,-0.029095296,-0.012280461,0.029147673,0.074253686,0.010778453,-0.016200917,-0.021937096,-0.08554501,-0.023150608,-0.028629739,0.0063404706,-0.031147331,-0.024322255,0.02202066,-0.030119264,0.053692084,0.0008292566,0.04777749,-0.06673819,0.007917716,-0.020578727,-0.040911593,0.013896773,-0.0133630205,0.0076323077,0.005210799,0.011236882,-0.016090034,-0.026576383,0.019090435,0.0025150606,0.03296406,0.03545014,-0.029646493,-0.013407579,-0.0011393632,-0.010017857,-0.024524335,-0.056836,-0.0046703126,-0.014492948,-0.001005861,0.022389628,-0.020688163,-0.0061789188,0.008696485,-0.018706592,0.037709076,-0.002976811,-0.010606289,0.020075342,-0.022711951,-0.022697052,0.06923275,0.020649035,0.0044265627,-0.0043500694,-0.044649284,0.015334995,-0.061812114,-0.008923505,-0.018980091,0.02080654,0.0025927413,0.0078000976,-0.013424206,0.0125784995,0.0018344943,-0.001920672,-0.06461751,-0.022224233,0.02516888,0.013010658,-0.01308218,0.01530085,0.011270152,0.0074332836,0.016886795,-0.012713134,0.008917671,-0.023829566,0.0062506073,-0.032406617,-0.03175695,-0.021906864,0.028415568,0.020288812,0.026727146,-0.02434922,-0.07260969,0.0062702224,-0.009402054,-0.050421137,0.013144148,-0.032493614,0.016988184,-0.032835502,-0.030754615,-0.0066360286,-0.0053189234,-0.029021997,0.042490073,-0.045744892,0.009204587,0.0067456216,-0.011485968,0.029427323,-0.0036565042,-0.011513274,0.026136318,0.03521754,0.0243553,0.004180649,-0.0063142264,-0.02083049,0.009999072,-0.0120309,0.012027965,-0.010187527,-0.012243263,0.008922213,-0.054089252,0.0040299,0.013689454,0.019708049,0.0057793413,-0.019452898,-0.003996307,0.018073155,-0.036662634,-0.006110841,-0.0017720548,-0.033240512,0.02031167,0.027661694,-0.038722336,0.052094538,0.0012132737,-0.041801956,-0.0070860223,0.01029992,-0.092738256,-0.029095039,0.025867973,0.015249259,-0.012761243,0.023575576,-0.030213961,0.056971494,0.018263754,-0.00013011604,0.0058205235,0.008994598,0.031744443,0.0084665995,-0.0140361,0.012193612,0.04375884,0.03457983,-0.010244359,0.0106597245,0.005880494,-0.015955795,-0.0019070792,0.0076773195,-0.0292393,0.036681987,-0.0016058314,-0.012879363,-0.009696598,0.058409978,-0.029277379,0.0022872104,0.06402818,-0.004618462,-0.037639435,0.0042933607,-0.0051029343,0.0011560551,-0.008970006,4.1457126e-05,-0.04448202,0.00061085395,-0.01886099,-0.029591572,0.04402536,0.002781682,0.0045951693,0.0045554475,0.027485205,-0.0443752,0.013201931,-0.033992615,-0.008106896,0.007536819,-0.008946295,0.014116697,-0.01028927,-0.0029480609,-0.0016819441,-0.040662456,-0.033769798,-0.026692105,-0.013871698,0.35631037,0.0042587453,-0.01018426,0.002172908,-0.007839823,0.007772351,-0.0050968034,0.03936941,-0.001808078,0.008870187,0.028953977,-0.0030233003,0.0049478174,0.028806219,0.026526397,-0.004203426,0.041059695,-0.0002612158,-0.0015766654,-0.008619727,-0.011881172,0.012683398,-0.057956286,-0.008369453,-0.034398306,-0.030939082,-0.0133120045,-0.010479462,-0.025744833,0.022934029,0.024627363,0.0076244166,-0.024004264,-0.02386235,0.020837445,-0.014629329,-0.015540852]', FALSE), +('923f14e7-e17f-49ca-a939-70439648a66c', '2d9eddb3-1a94-4248-ad69-f5b0fdfdaa58', 1, '## Virtual interfaces + +| vmbr | purpose | config | +|-------|---------------------|----------------------------------------------------| +| vmbr0 | management | 10.77.42.0/24, gateway 10.77.42.1 on pbx-node1 | +| vmbr1 | lab (VLAN 42) | tag 42, no DHCP, NAT-only to the uplink | +| vmbr2 | lab-iot (VLAN 130) | tag 130, DHCP served by minio01 | + +- Cluster firewall is enabled on every node; VLAN 42 traffic is NAT-only. +- Corosync heartbeat rides vmbr0; keep the management iface out of the + lab-iot VLAN. +- Node roles: pbx-node1 = hypervisor + gateway, pbx-node2 = AIO + containers, pbx-node3 = GPU box.', '[0.044090904,0.03506125,-0.0065333466,-0.04037126,0.019116757,0.003163668,0.0493262,0.014093121,0.003734496,0.020801315,-0.0066810856,0.008061381,0.0037118967,-0.0010079825,-0.0055030724,-0.021629548,0.013809108,0.004031243,-3.1064726e-05,-0.010732642,-0.009457824,0.044427846,0.003076857,0.044551585,-0.025031088,0.013410618,0.018464576,0.018867383,0.0030438947,0.010819371,0.00026895394,0.0035205944,-0.018951898,0.02660877,-0.0047742017,0.020875387,0.032034937,0.0029537086,-0.038749576,-0.141726,-0.018284244,0.034727134,-0.03467948,-0.002354501,0.027228462,0.015220807,-0.01390946,0.013705793,-0.023493825,-0.015353352,-0.029932784,-0.017410003,-0.007462412,0.00048827784,-0.004396609,-0.0026624368,-0.011468333,-0.06072033,0.0003884515,0.023096116,-0.014378877,0.0026812644,0.043855947,0.010168275,-0.031240936,0.010185312,0.02508793,0.006078057,-0.036706127,-0.01355374,0.019466488,-0.030079884,-0.028377224,0.029290956,-0.00611819,-0.02161064,-0.00057731254,0.027404891,-0.058125466,-0.011658331,0.029176507,-9.562594e-05,0.024798445,-0.02054545,0.036580753,-0.0067376914,-0.045170132,0.028127838,-0.0027328113,-0.010147561,0.02122604,-0.000816791,0.0031774186,-0.041369807,-0.008546775,-0.010232325,-0.00245952,-0.012571975,0.01986701,-0.033050623,0.008783675,-0.013433669,0.0023644357,-0.0031036146,-0.008802553,0.021760663,0.029190207,0.018519143,-0.0052284715,0.04021339,-0.011470746,-0.035793506,-0.009956232,-0.022511927,0.022560177,0.022008222,-0.019496638,0.0074646245,0.005298663,0.0071605677,-0.009982926,-0.0061334693,0.001438716,0.010172516,-0.00999517,0.006955903,-0.034806542,-0.029828139,0.025580503,-0.016598567,-0.0010821517,0.037285354,0.013302285,0.03276486,0.010941496,-0.0014496665,-0.026435189,-0.024939924,0.017793335,-0.063663,0.019144228,-0.029071717,0.03456295,-0.006870705,0.0132413525,-0.017168278,-0.007332172,0.012888791,-0.011482603,-0.00930182,-0.01884676,0.025009047,0.019437019,0.0027305556,0.008520554,0.016104387,0.011633782,-0.008667489,-0.041224875,0.025756314,-0.019353433,0.004779974,0.03703703,0.012893865,-0.01419307,0.0013815933,0.022546094,0.00024580807,0.013997404,-0.0012565145,-0.0025656587,-0.002822898,-0.008361664,-0.01300203,0.03400799,0.044580318,0.028505944,-0.023362426,0.022760672,-0.030009588,0.008787465,-0.014680718,0.014007434,-0.030067744,-0.00081371004,-0.027857255,0.019108316,-0.0010473115,-0.030907597,-0.00044233399,-0.009459387,0.010412693,-0.007036881,-0.035961922,-0.015552622,3.3536977e-05,0.0031959459,0.039240286,-0.024648305,0.02358112,-0.01865805,-0.0063161994,-0.0077826977,0.0021140717,-0.0053664385,0.010107109,-0.010141633,-0.01661435,-0.008279472,-0.009963206,0.030938298,0.01519676,0.0022404017,-0.019731624,0.00204073,0.013102924,-0.0012006528,0.013815114,-0.01213702,-0.016025258,0.02336963,0.0011977082,0.0014210852,-0.026968855,0.0420813,-0.0058949445,-0.00872279,0.010575269,-0.012999297,-0.028886711,-0.003599588,-0.007189209,0.008598475,0.012477014,-0.020680102,-0.08665082,0.004118642,-0.00067548506,0.016692983,0.0029282349,0.02322444,0.010879798,0.014566984,-0.021927455,-0.00027583202,0.00867221,-0.014400229,-0.0027054641,-0.02890984,0.013887877,-0.013357678,0.02481259,-0.009131545,-0.033862583,0.028076598,-0.005851144,0.0012112289,0.0048697703,-0.006220367,-0.002589135,0.007893901,0.0069141714,-0.017070528,-0.014003856,0.0023886675,-0.012335221,0.010215234,-0.0064662504,0.011740956,-0.04351022,-0.04052899,-0.021912312,-0.0032480257,0.020687984,-0.001838177,-0.026259165,0.014604166,-0.032664057,0.020539464,0.02938064,0.036722854,-0.0052545564,0.0034121885,-0.022488954,-0.03966889,-0.041075822,-0.031341095,-0.018001242,0.00053791166,0.0004535302,-0.017717717,0.029543877,0.0033990808,-0.005764202,0.023799105,0.011210136,-0.0032564148,0.0059696725,0.0117669115,-0.030907966,-0.014580318,-0.027639048,0.013088263,-0.00034675974,0.030694852,-0.0014304216,0.016649745,-0.0098391045,-0.011057657,-0.03362159,-0.0116385,0.041187704,-0.0103438785,0.0012065201,-0.026108135,0.0047040964,0.026596854,0.0057446756,-0.010080786,0.011868897,0.009648084,0.015811378,0.0033108676,-0.04874905,0.005185516,-0.0041486844,-0.013541592,-0.008461982,0.003899214,0.0061234403,0.038994554,-0.03983773,0.0056889765,0.0119238235,0.025105584,0.0101963375,0.0041089407,0.0067941393,0.014950495,-0.067071564,-0.019546412,0.022496095,0.0026924585,0.024260228,-0.033108745,0.008387231,0.008598228,-0.021066828,0.001509026,0.037358113,0.04192134,0.0014627693,0.025173122,0.0027361535,0.006647223,0.030318966,0.0064756693,-0.027915947,-0.038243264,0.0169757,-0.007824336,0.02273127,-0.05440576,0.03319989,-0.015136893,0.027178453,0.044172093,0.0015222396,-0.034370963,0.032657318,-0.013422325,0.022078093,0.019115912,-0.013985227,0.0015931041,-0.0057930723,-0.026898222,0.019349847,-0.009798414,-0.0043504755,0.031130543,-0.011251777,-0.008496533,-0.044300333,0.03908439,0.002514718,-0.009044317,-0.013838358,-0.014396138,0.0022281068,-0.004476578,-0.0014685066,0.029579554,-0.017828463,0.01849108,-0.003655577,-0.00088823825,0.014059982,-0.023510111,-0.0034021118,0.0009526866,0.0025002668,-0.005019148,0.006044111,0.003013866,0.008758841,-0.024685752,0.5700741,0.009950002,-0.015211176,-0.007447873,-0.005263842,-0.0065710563,-0.005122209,-0.018275086,0.01789232,-0.025483105,-0.020442832,0.011846827,0.0029012135,-0.022995757,0.014683729,-0.012949135,0.034284197,0.0063805655,0.0047287266,-0.00035739737,-0.06595213,-0.012504227,-0.04821438,0.013580749,-0.025956191,0.010423866,0.018202838,0.015638089,-0.020316254,0.043307856,-0.026126364,-0.029953849,0.0028370542,0.023913076,0.01806139,9.416579e-05,-0.0069395616,0.03992493,0.017412681,-0.0005845584,-0.05378947,-0.010970127,-0.011100186,-0.00052795507,0.012359656,0.039079368,0.030102463,0.007894443,0.012490799,-0.005110037,0.014874442,0.004697175,0.036941517,0.010035651,-0.023712313,0.009793956,0.008819516,0.029654952,0.0047304397,-0.011028721,0.0019383165,-0.012806935,-0.025668932,0.0018171171,0.05497557,-0.008795507,0.0076718414,-0.03516565,0.0034056297,-0.0082482295,0.0026366794,0.0133662615,0.016605133,0.00514955,-0.015570955,-0.036215283,0.01358356,-0.009346209,0.001306039,-0.018181475,0.017647797,-0.0039659883,0.0063252295,-0.01313405,0.0098333545,-0.046464417,-0.01074037,0.0031554142,0.03237128,0.013017701,-0.030168844,-0.007001779,-0.0017932465,-0.00011093452,-0.018769823,-0.014823905,0.016038023,-0.00030474033,0.030863715,0.055082962,0.005135659,0.036433917,0.019996421,-0.03722266,0.024918107,0.07734255,-0.0069308295,-0.0102753285,0.0078096143,-0.020279888,-0.05004738,-0.02760338,-0.052640337,-0.0032523707,-0.00012376024,0.020983342,-0.00020870185,-0.018219557,0.0028694521,-0.019145867,-0.013708588,-0.018935727,0.023113573,0.051968087,0.008695195,0.034417015,0.0149253635,-0.028501837,-0.0051987334,-0.034198448,-0.025433261,-0.02325279,-0.016047955,0.038873006,-0.0035271102,0.050107647,0.0028929536,0.034484558,0.0021774843,0.0014689497,-0.0013511436,-0.023549128,0.0058980063,0.0049503893,-0.0017079932,-0.017523397,-0.025994264,-0.019941382,0.0449624,-0.02015076,0.039561246,0.012398441,0.022239031,-0.0293279,-0.007867156,-0.009659206,-0.039639473,0.007970692,-0.018975522,-0.016977645,0.02969836,0.010853339,0.010913841,0.0076690745,0.03341413,-0.025158107,-0.0053532524,-0.023127945,-0.023041882,-0.011096663,0.02691559,-0.005693111,0.0047246236,0.05062418,0.033135455,-0.015722265,0.016641028,-0.004164779,-0.0057695736,-0.042875376,-0.009625247,0.000100695965,-0.014763337,-0.030211288,-0.010564216,-0.013652455,0.011132317,0.013419962,-0.041319855,-0.03791631,-0.005186033,-0.011099004,0.026991675,0.02656854,0.009926363,0.033096883,0.009510686,-0.012382397,0.002212012,0.011482787,0.00038683513,0.03568174,-0.053974338,0.0047241375,-0.029936362,0.027437763,-0.025868537,-0.0073313825,-0.0061370414,-0.052750524,0.005100442,0.0024185327,-0.02218889,-0.016099134,-0.019387826,0.013618499,-0.007688945,-0.013884777,-0.006824745,-0.011646917,0.0045241085,-0.001321555,-0.016895257,-0.019236386,-0.004011423,-0.010890068,-0.0061298283,0.004782712,0.007263785,0.0019650941,-0.01673671,0.010394042,-0.006047905,-0.0019588424,0.012634293,-0.030272212,0.01418395,-0.0048616896,-0.02077727,0.011666427,0.009388869,-0.04955082,-0.07045036,-0.0140231745,0.038335703,0.012459456,0.005628696,-0.0062583964,0.03519652,-0.043199737,0.0025558309,-0.008547278,-0.029400075,-0.050867885,0.0072968337,0.003937473,0.04397928,0.011106733,0.015517466,-0.014517383,0.029270966,-0.039199848,-0.0025747195,0.033907607,0.028060516,0.006763508,0.0044535156,-0.010460264,0.008485904,0.02704847,0.020836417,-0.011526325,0.02055618,0.020445053,0.028514553,-0.021253938,0.029333862,0.034915805,0.035516318,-0.019490998,0.0056938846,-0.034537666,-0.0056797303,-0.01776361,0.01799546,-0.028681425,0.011988823,0.028171094,-0.0077752736,-0.008783407,0.106141016,-0.013698108,-0.039745692,0.022761982,0.03723057,-0.005964683,0.019778611,-0.034233253,0.014138663,-0.054720115,0.022315325,-0.006645327,0.0013477314,0.007597388,-0.038873773,0.03329475,0.0046822387,0.0077839787,-0.007833058,-0.0034724192,-0.04089201,-0.02763523,-0.008071155,-0.011118146,0.01041735,0.0002483303,0.018861761,-0.0010453621,0.007516462,0.02693593,-0.052501902,0.0017777162,-0.025443524,-0.04398944,0.50754225,0.015903763,0.009750523,-0.014103099,-0.022350289,0.013922063,-0.008504802,-0.012112021,-0.019376842,0.008499552,0.019322645,-0.024656232,0.0028208008,0.036444154,0.0015970512,-0.023776522,0.006157973,0.009569028,-0.00085789536,-0.019792672,-0.012860993,0.006077868,0.016975837,-0.024976235,-0.055628225,-0.055648305,0.025298888,-0.024573503,-0.013079936,0.012528608,0.06849178,-0.03990199,-0.014271789,-0.0106403325,0.0047020977,-0.032421626,-0.004716217]', FALSE), +('441737e9-8b7d-4765-9081-e85eaaae3a62', '2d9eddb3-1a94-4248-ad69-f5b0fdfdaa58', -1, 'The Rack7 Proxmox cluster consists of three nodes: pbx-node1, pbx-node2, and pbx-node3, all running Proxmox VE version 8.3.4. Each node has virtual interfaces defined: vmbr0 for management (10.77.42.0/24, gateway 10.77.42.1 on pbx-node1), vmbr1 for the lab VLAN 42 with NAT-only access, and vmbr2 for lab-iot VLAN 130 with DHCP from minio01. The cluster firewall is enabled on all nodes, and VLAN 42 traffic is NAT-only. Corosync heartbeat uses vmbr0, keeping the management interface out of the lab-iot VLAN. The nodes have distinct roles: pbx-node1 acts as hypervisor and gateway, pbx-node2 hosts AIO containers, and pbx-node3 serves as a GPU box. +Source: homelab/networking/vela-bridges.md', '[0.05479434,0.03587535,-0.034691498,-0.043385025,-0.0022553466,-0.015660316,0.036978416,0.021913866,0.014450065,0.0026973784,-0.020248067,0.0015315152,-0.0005831822,-0.0035946313,-0.0028266078,-0.00873966,0.021705324,0.013933665,0.011898244,-0.028097281,-0.0002936983,0.04305212,-0.007101456,0.06698145,-0.028953703,0.02022014,-0.0025134294,-0.014971217,-0.014806042,-0.010051762,-0.015213262,-0.0029446948,-0.036369804,0.026084319,-0.01139871,0.023026258,0.040553678,0.026108751,-0.03951198,-0.13841544,-0.023947278,0.039827015,0.0007803628,0.018675912,0.03721693,0.013550192,0.00366449,0.015636567,-0.027161738,-0.02864496,-0.015569491,-0.0054448475,-0.0056155585,-0.009568823,0.0063068336,0.0041977353,0.012896109,-0.050861206,0.015420626,0.018977443,-0.028203463,0.00947274,0.030411594,0.004751641,-0.0566451,0.0069797975,0.011921945,-0.017399961,-0.055337742,-0.012823665,0.017050559,-0.034537163,-0.015947072,0.03226904,-0.004421302,-0.013009333,-0.0031704744,0.008983381,-0.07719344,-0.011292838,0.0078867655,0.014465518,0.020562628,-0.022236338,-0.0047141407,-0.01527818,-0.032824486,0.018652482,0.0059934272,-0.002865452,0.0024997483,0.009325684,0.010754155,-0.03750209,0.0074700844,-0.011691421,0.008882812,-0.009791955,0.020410717,-0.047878835,-0.0125357155,0.0010580035,-0.016222237,0.00077045924,-0.013686561,0.023397291,0.0400945,0.00568046,0.009733026,0.023279827,-0.015807733,-0.05961808,0.0025064726,-0.0033038107,0.018104862,0.0028845207,-0.023388123,0.006315738,0.013747941,0.022707388,-0.00020331207,0.014703832,0.0040139905,0.021907022,0.005791069,0.012063804,-0.038203016,-0.04418524,0.057278875,-0.0044244337,-0.01187211,0.034030475,0.026007839,0.029761361,-0.0004600724,-0.010974267,-0.012553019,-0.033730213,0.008110861,-0.07867592,0.026271943,-0.031393714,0.024717595,-0.0061317827,0.0067691235,-0.008300874,-0.0011694233,0.0032235638,-0.026826091,-0.004862706,-0.028342277,-0.00066941493,0.034747325,0.002241876,-0.01457137,0.041657194,0.0061857114,0.0068543646,-0.018600492,0.041475028,-0.0047073364,0.0002568798,0.01896116,0.019289322,-0.02001727,-0.012116134,0.035094053,0.00023030672,0.020755302,0.020563662,0.0031696244,-0.009929034,0.0029355998,-0.027949467,0.023892527,0.065303676,0.023586664,-0.03203238,0.014986495,-0.00618593,0.027730586,-0.023818254,0.025661564,-0.010918889,-0.0031497562,-0.029664116,0.019373007,-0.005380921,0.0009816145,-0.0055744844,0.0022259129,0.015882958,0.0109645855,-0.026168177,-0.0033386208,-0.0073275412,0.021016518,0.030408835,-0.008977467,0.035772108,-0.021678811,-0.011193123,0.00073080766,-0.0010094725,-0.003563635,0.01234056,-0.014295552,-0.017035177,-0.0122192595,-0.0028324337,0.029536996,0.043021176,0.007229638,-0.026609665,-0.00044948512,0.0066667506,0.014415066,0.006327316,-0.0176506,-0.0011340284,0.015045121,0.012341276,0.0013316404,-0.0114918435,0.034266338,0.004335323,0.0012881142,0.007516767,0.0042211493,-0.020087624,-0.019196222,-0.013795215,0.013741426,0.0044852793,-0.011954612,-0.098201446,0.012104367,0.007077686,-0.0034638832,-0.004884501,0.014987517,-0.010720849,0.017187318,-0.031340837,0.0024195174,0.008710442,-0.021788621,0.0075569004,-0.03589875,0.008825349,-0.015300546,0.0054184753,-0.006991928,-0.041213546,0.013621736,0.009014192,-0.017534811,-0.016391,-0.0067576757,-0.0070439847,0.013763962,0.022380002,-0.03280923,-0.017694503,-0.0096492255,-0.010863936,-0.009909377,0.002914234,0.024687657,-0.037414867,-0.033996135,-0.0047704875,-0.0035852175,0.03146986,-0.0015298518,-0.022269864,9.158886e-05,-0.020506546,0.004176593,0.05003135,0.040812727,0.022251086,7.6639546e-05,-0.022332337,-0.045018345,-0.026578892,-0.014124725,-0.015400048,0.007556679,-0.0023926403,-0.029681437,0.0164004,0.00040799036,-0.006256043,0.0058544804,0.020892277,-0.01047068,0.018287508,0.013951185,-0.03863256,-0.01158741,0.0014743366,0.011553021,-0.006267685,0.001545558,0.013469445,0.010874004,-0.016598955,-0.0047839605,-0.005311389,-0.013391762,0.014759505,0.0060305786,-0.0049659614,-0.032724947,-0.006250397,0.015097303,0.015151196,-0.005820653,0.012786803,0.024437921,0.0019456149,0.027267173,-0.053141505,-0.0006012197,-0.01330519,0.012313677,-0.030540917,0.022822766,0.02315255,0.049552165,-0.027660199,-0.005463645,0.003690415,0.0410285,0.015755,0.0013924015,-0.0015650135,0.015601247,-0.069456965,-0.026937077,0.025602037,0.0068557286,0.033485297,-0.030820554,-0.0046675177,0.010980687,-0.026551252,0.00037937664,0.01468319,0.042245857,-0.014145497,0.01923351,0.008363787,0.0010655904,0.026555609,0.0076151947,-0.024011489,-0.029211387,0.023894714,-0.022452129,0.03728372,-0.039307594,0.019214822,-0.026948785,0.033485636,0.040653214,0.01816598,-0.03860583,0.035636246,-0.018994572,-0.0062568584,0.043940093,-0.01434005,0.01812721,-0.0014892898,-0.031962432,0.019457592,-0.009479343,-0.011854486,0.026825475,-0.024755843,-0.0029476483,-0.030737065,0.02765217,-0.0032982347,-0.011570973,-0.03054339,-0.0026828717,0.01831113,0.019349571,0.0027208654,0.04975294,-0.025288489,0.046099063,-0.008451138,-0.0024480615,0.0028845333,0.001810224,-0.011941824,-0.00044093648,0.007090945,-0.012055595,-0.014634228,0.010858933,0.005824269,-0.017001787,0.5382715,0.005141645,-0.019199846,-0.0064850063,-0.0004323948,-0.03607351,0.011938852,-0.026390644,-0.0053918264,-0.011791925,-0.0009941396,0.009020504,-0.018530479,0.014793236,0.016093075,-0.020544387,0.019027578,0.008008502,0.004523263,-0.0040653897,-0.046164975,-0.015708916,-0.03160641,0.0036876644,0.01364604,0.019933786,0.011161712,-0.009965947,-0.012958454,0.038885202,-0.04512532,-0.042309225,-0.025525073,0.0015096923,-0.0019462028,0.003494487,-0.019299334,0.03389074,0.026187798,-7.9660625e-05,-0.04838278,-0.018615928,0.016269017,-0.0046290234,0.017200349,0.044082373,0.032091424,-0.0010016656,0.0005260435,0.010595506,0.012141918,0.002756618,0.015109781,0.006573246,-0.0068041435,0.011788155,0.022772767,0.04799138,-0.0067753782,-0.007001816,-0.010581335,-0.014810684,-0.001618717,0.013219837,0.040081084,-0.021589786,-0.010873284,-0.034566183,0.0059335344,0.012326398,-0.009686228,0.011218367,0.01575232,-0.02317793,-0.031035094,-0.020810034,0.03291742,-0.0037895106,0.0035970923,-0.029982701,0.028273555,-0.011333912,0.0011911225,-0.004663137,-0.0023771839,-0.036066886,-0.015133009,0.023145024,0.03003367,0.0292153,-0.022645798,-0.016472453,-0.011983148,0.008728179,-0.02700414,-0.011167397,0.03176581,-0.004318162,0.013511757,0.034791,0.005557584,0.025530107,0.0007929979,-0.019508323,0.028772011,0.042950865,-0.008298346,0.0070955865,-0.0038420488,-0.015962843,-0.05295469,-0.027333312,-0.04083577,-0.011410235,-0.025032768,0.01506385,0.008229581,-0.0058254483,0.021018155,-0.009131329,-0.0054831365,-0.0122233555,0.037744984,0.07165152,0.010162693,0.017872415,0.023238253,-0.06662232,-0.017895926,-0.031028945,-0.0066606267,-0.040965576,-0.024004718,0.0346376,-0.00036753432,0.06876396,0.003912015,0.048821,-0.021921108,-0.0053519057,-0.022893643,-0.029745482,0.012947771,-0.013083661,-0.007628686,-0.00037766137,0.005280464,-0.020235011,0.038156494,0.02643064,0.043027524,0.0063297506,0.028055685,-0.03962299,-0.0038361347,-0.009389244,-0.034056086,-0.00079716014,-0.020991161,-0.019006168,0.0146501,-0.01438071,0.025549827,-0.0045712916,0.022418225,-0.0041558547,-0.022326909,0.01540421,-0.021699464,-0.013146452,0.02176634,-0.017037926,0.009290582,0.0360744,0.036459886,-0.007959019,0.0052746534,-0.017370593,0.010859069,-0.02549467,-0.009356314,-0.007708241,-0.00050892745,-0.007810706,0.020908106,-0.026288789,0.017658843,0.006563727,-0.037035536,-0.048480187,-0.030142287,0.0237636,0.040102515,0.0026364469,0.0210194,0.02714613,0.0029289974,0.013645787,-0.0014459122,0.007190679,-0.017109828,0.018220685,-0.04541624,-0.008005011,-0.031843748,0.045956105,0.0048621437,0.012348184,-0.017284384,-0.05755682,0.004020448,0.0022513636,-0.033664897,-0.0074682506,-0.024952492,0.005141969,-0.023712916,-0.04054682,0.0046868986,0.0022262195,0.011125945,0.007910535,-0.025630577,0.008094361,0.0014313445,-0.009003975,0.0041553476,-0.007594502,-0.003986997,0.015994493,-0.003322234,0.009629465,-0.0044905893,0.0130601935,-0.01896182,0.013584784,0.026279895,0.00076560513,-0.023381006,0.0032188816,0.013616138,-0.05419441,-0.055944033,-0.025845211,0.0588165,0.021411443,-0.007368037,-0.007724026,0.02354395,-0.053501084,0.0044004275,-0.022465708,-0.028056111,-0.053301055,0.012099373,-0.019400964,0.060497716,0.0037058007,0.005144927,-0.02085442,0.02327041,-0.056196313,-0.0063815396,0.038454685,0.04382147,-0.000794339,0.0070261173,-0.012906729,0.027753176,0.030610947,0.011079907,-0.015738266,0.00650762,0.027350038,0.02399554,-0.024144907,0.037262037,0.02037182,0.024958653,0.005381574,0.02423061,-0.032569468,-0.022687474,0.0047351792,0.027785825,-0.0075482605,0.01754196,0.025553735,-0.011067392,0.008894554,0.076705605,-0.019734925,-0.00034516826,0.029293649,0.018653033,-0.031037342,0.0043619745,-0.024999648,0.031105036,-0.036887098,0.037177302,-0.03850591,0.008442544,-0.006153054,-0.024521736,0.029417714,-0.006413334,0.01395743,0.00047228372,-0.0113806315,-0.043588426,-0.010448976,-0.015161216,-0.014732598,0.014019501,0.001172994,0.019730508,-0.003610587,0.010405098,0.022683812,-0.058567807,-0.012295552,-0.025967367,-0.047672886,0.5109485,-0.0039722705,0.0030313602,-0.0074653984,-0.022162681,0.023820464,-0.010125831,-0.0002604625,-0.022631507,0.017313348,0.019374609,-0.014507757,-0.0028736624,0.022967936,0.00916854,-0.023208765,0.04566721,-0.009497419,-0.00627524,-0.008833548,0.0016707835,0.014094395,0.0069383783,-0.025442846,-0.0398603,-0.082911715,-0.007258328,-0.017121216,-0.018624643,-0.003441023,0.04584731,-0.019193763,-0.013708014,-0.011074967,0.025553586,-0.016022382,0.0016147016]', TRUE), +('bd20608c-2ca4-4cf5-bd0c-a03fc4795725', 'bf670a2f-cdbb-4e83-9b67-e3a8645c412c', -1, 'This document configures the GitLab Runner used for CI in the shelf project. It specifies that the runner is registered against git.reeseapps.com and uses a docker executor on rack7-pbx2. The registration token format is glrt- followed by 20 hex characters, rotated in 2026-06. Tags include lab-ci as default and pve-upgrade for a gated upgrade job. Job images are ghcr.io/reese/lab-ci:2026.7 for ansible plays and debian:13-slim for lint, with concurrency capped at 2. +Source: deployments/ci/gitlab-runner.md', '[-0.008289133,0.013222049,-0.0051277783,-0.04345629,0.0020650902,-0.01760043,0.0037666913,0.033427168,-0.0038565597,-0.041955855,-0.026470937,-0.051774602,0.028182251,-0.01503871,-0.009156482,0.010424782,0.020656839,-0.0024811474,0.0007545897,-0.0054377723,0.01299258,-0.011262483,-0.0149289565,0.029735781,-0.014005446,0.014427226,0.01046701,-0.0046057743,-0.011444656,-0.05858845,-0.033746928,0.0052217864,-0.053999953,-0.006875711,-0.012547159,0.00046664895,0.031512506,-0.017637193,-0.010326546,-0.075698756,0.0055562854,0.00029286908,0.005464274,-0.02078723,0.014805166,-0.00032456953,-0.04225514,-0.003090533,-0.0007709219,0.0015245709,-0.036428835,0.011019971,0.012452574,0.02688515,-0.02333353,0.026456045,-0.013354691,-0.028755141,-0.03644052,-0.023572955,0.0025424452,0.023342833,-0.0018517975,0.009789453,-0.02877245,-0.03405512,0.020179005,0.051973257,-0.027647087,-0.0006307446,0.011018533,-0.056543708,0.02878791,-0.0074376436,0.005534127,-0.02797433,-0.027947342,-0.0012380534,-0.04637074,-0.020992916,0.023680052,0.016010553,-0.00686196,-0.02191533,-0.02651318,-0.023028405,-0.0506983,0.0045550144,0.018383263,-2.3896026e-05,0.0026173107,-0.0077929357,0.0022995495,-0.060572647,-0.0025703653,0.018741975,0.0071566068,-0.027304614,0.019320907,0.009457978,-0.0025480974,-0.027446978,0.0009675573,-0.008299259,-0.03085842,0.01913658,0.022258475,0.026591253,-0.0088157,0.010127626,0.0076003037,0.028355585,0.001343849,-0.01297405,0.008088696,0.009157969,0.022055328,0.00420234,-0.0065096267,0.017617669,-0.000998535,-0.012022712,-0.04108354,0.011204487,0.029011382,0.031194655,-0.071432196,-0.03796355,0.0012529757,0.01765612,0.017858941,0.021643888,0.05297763,0.0072919005,-0.0038620252,0.045310676,-0.056377657,0.007801098,-0.011049307,-0.056928083,-0.0016207881,-0.012020111,-0.005627441,-0.011586852,0.022846088,-0.042725462,-0.038166918,-0.00611517,0.002287402,0.01283832,-0.0031865868,-0.027069319,0.012188078,-0.00048262707,-0.045644812,0.018695142,-0.008876914,-0.017423872,-0.027574701,0.016916128,0.0033056685,0.007767223,0.014126293,0.016883584,-0.048191745,0.008058995,0.0032931585,-0.041454874,-0.0009151152,-0.03603289,-0.0060851043,-0.0049866284,-0.010875997,0.024589453,-0.0074892123,0.03179119,0.019286487,0.035071403,0.0074129514,-0.01881568,-0.018265342,-0.03073348,0.024181372,0.024783516,-0.0030609677,-0.019795584,0.0065108133,-0.021112984,-0.003081178,-0.03984362,-0.0037958634,0.04855433,0.036892436,0.003775181,-0.024173012,0.029261623,0.028795488,0.053288784,-0.028707374,-0.009488534,-0.020449445,-0.032386404,-0.056879204,0.0075921677,0.006841219,0.026217325,-0.021972181,-0.013060292,-0.017497402,-0.016531458,0.023506831,-0.006385942,0.040450852,0.013800153,-0.0124963345,-0.02677081,0.022176104,-0.038521204,0.02419196,0.02956852,0.011630679,0.0019144351,-0.0028070966,0.01732142,0.025688043,0.018256465,0.0058967737,0.018619657,-0.0024678286,-0.016443523,0.0014110536,0.012004707,-0.037594568,0.003294119,-0.018298684,-0.06928475,0.0012585942,0.018851157,0.0024847414,-0.0030235113,0.022592403,0.046188585,-0.0037906521,0.012725536,-0.04205413,0.0121743465,0.005088281,0.034324653,-0.030821942,0.0011236181,0.011482624,0.022355955,-0.03186337,-0.016750844,0.06998474,-0.0025388526,-0.0035417997,0.012631482,-0.020059982,0.020435916,-0.0046043587,0.010674877,0.02178141,0.028035922,0.010124295,0.017162561,-0.0020877148,0.018101767,0.001127695,-0.0048788036,-0.023521096,0.02955089,-0.017710729,-0.0048413803,-0.024677781,-0.007362112,0.031003784,-0.021978969,0.014043678,-0.03158835,0.013838067,-0.039655954,-0.04418056,-0.029813113,-0.02616813,-0.021951476,0.009060041,0.0018016177,0.028537558,0.03384244,-0.017158935,0.015202065,0.019428005,0.005643871,-0.02368377,0.006296716,0.008390751,0.00785907,0.008365873,-0.009234571,-0.033007912,0.030657718,-0.00169198,-0.02258245,-0.015268097,-0.010129144,-0.009979698,0.007893801,0.0021168233,-0.011787522,-0.0003036786,-0.027941367,-0.013329931,0.0008157502,0.017802734,-0.013526821,0.055426598,0.010943349,-0.023195872,-0.043734856,-0.015788136,0.00020617298,0.016238822,0.009603803,0.020691749,0.060155217,0.018126374,0.03375933,-0.020722674,0.04415012,0.007254101,0.010635118,-0.03454623,0.030490406,-0.0044090524,0.011731551,0.02072839,0.041549228,0.019481452,0.036786076,0.019651603,0.00872363,0.028511155,0.022139017,-0.012869676,0.022819655,0.02850342,0.0005377586,0.063273974,-0.0093159005,0.026025686,-0.037057023,0.009129405,0.005884464,-0.006286657,0.018469287,-0.04609837,-0.037476353,0.0032357003,0.0030196896,-0.01648655,0.029488096,0.0014095412,-0.00010491757,-0.020248607,-0.009518827,0.026377855,-0.006476553,-0.012658894,0.046269473,0.003244727,-0.035910282,0.011304747,-0.007587913,-0.024563098,-0.0030578382,-0.025409928,-0.004547088,-0.018963013,0.0015150591,0.039722923,0.0024748375,0.0068832366,-0.016752107,0.016664887,0.0076320907,0.009360136,-0.040943775,0.04835888,0.002143375,0.02873531,-0.04640802,-0.0017459607,-0.0066032563,-0.024473278,-0.009569757,0.0019909428,-0.02109115,-0.017697548,0.029213391,-0.0113416165,-0.00015910376,-0.025900777,-0.035206426,0.009953964,0.03026893,0.013958277,0.5896217,-0.03541269,-0.009281837,0.0071872543,-0.0021024924,-0.008403046,0.025597315,0.052737575,0.025554355,-0.023145117,0.01744895,0.008524963,0.010621165,-0.017344618,0.015528165,-0.04153619,0.036364682,-0.021861479,-0.012753118,0.0011471816,-0.06826844,-0.02656941,-0.03371001,3.4462886e-05,0.033976838,0.02015138,-0.00037803175,0.0034016175,0.0077511533,0.04572495,-0.034943905,-0.0064578233,0.012785735,0.031837575,-0.02626019,-0.019303123,-0.011680092,0.029619955,-0.0017011238,-0.018559547,0.035735935,0.0009054021,0.010459648,-0.02584423,0.011300573,0.02155564,-0.0012840721,-0.016226882,0.022070982,0.03725822,0.0061375923,0.013176478,-9.513202e-05,-0.0036755106,0.015198368,-0.020128787,-0.016183523,-0.0385939,-0.01570537,-0.016797278,0.01058293,-0.0089268675,-0.040287845,-0.05127835,-0.011519785,0.004604152,0.0010061435,-0.012905027,-0.008270378,0.02005566,0.0004662613,-0.0002714861,0.045717992,0.014023738,-0.019544823,0.039160106,0.027301518,-0.0046376507,-0.0151764145,-0.010368722,-0.022746237,0.02508596,-0.003456427,-0.012640069,0.0040275175,-0.031074798,0.034083545,-0.036214273,-0.016557807,-0.0051343944,-0.035272814,-0.007453147,-0.02211432,0.059787996,-0.029188696,-0.012830058,0.0035143015,0.015534042,-0.029214552,-0.022953704,-0.0019350628,0.00044374267,0.010864582,0.0012258991,0.01258976,-0.06566458,0.027871411,0.030125747,0.0041184165,0.023780182,-0.00896662,0.006771983,-0.016692676,-0.04889522,0.028094962,0.011880517,-0.0006217509,-0.03328182,-0.03582471,-0.028271396,-0.027544377,-0.031047452,0.066600695,0.039357666,0.013730317,0.0096024405,0.0008742339,-0.04324416,-0.0074109477,-0.038068593,0.025507,-0.02514127,-0.008190573,0.0048154355,0.0041079517,-0.019469302,-0.0007773982,0.03545177,-0.05023299,0.016176239,-0.023857616,0.020885602,0.03764681,0.008366462,-0.03321363,0.014664156,-0.021060899,0.0031096642,-0.037109498,0.0015070917,-0.0019859665,0.006988756,0.0017061484,0.010109499,-0.0031930702,-0.010410242,-0.0017634116,-0.0008955429,0.0057401983,-0.037751615,-0.0116220955,-0.011709752,0.023745354,0.012208449,0.021384155,0.009016361,0.012487584,0.023833051,0.016411277,0.017585615,0.021350114,-0.050811853,-0.022160934,0.06732984,-0.00586223,0.0003844984,-0.01251852,-0.0019423959,-0.02422481,-0.021659076,0.024423804,0.023152137,-0.011607179,-0.0027762386,0.021646695,0.032326486,-0.014538317,0.006333236,0.0011510613,-0.008342502,-0.035969775,0.005930408,-0.0023686457,0.0048176465,-0.013298793,0.007676296,0.009223915,0.012208061,-0.021534119,0.0039481563,-0.018644728,0.0005101525,-0.052919857,-0.05075973,-0.005944506,0.025154384,-0.012896662,0.022168623,-0.019105645,-0.01791869,0.008803016,-0.009572773,-0.009487832,0.010319746,-0.0098582795,0.0058445013,-0.008129005,-0.011532564,-0.044641614,-0.037060298,-0.04041327,0.024445942,-0.045693308,-0.028127406,0.0144544635,-0.04064216,-0.017311284,-0.026699718,0.015316168,-0.010949345,0.005719374,0.0025447202,0.0046241223,0.001922299,-0.04784496,0.0070648375,0.004997788,0.0032697192,-0.017384429,0.030234268,-0.03215398,0.001112111,0.0062815356,0.007390075,0.01129007,0.0070266807,-0.007187294,0.012740683,-0.008614429,-0.05910193,0.007110036,0.0143593205,-0.007052405,0.010321699,0.011163593,-0.033777643,0.027732346,-0.006097745,-0.008445811,-0.01126953,0.029731037,-0.013788672,-0.0033585504,0.039209634,0.008437688,0.011605825,0.013670339,-0.0112194335,0.007784889,-0.0030451126,-0.026660345,-0.019987822,-0.023917723,0.01597206,-0.025478601,0.011306708,-0.004189644,0.025703257,0.030800343,-0.036642384,-0.0015964018,-0.022414124,0.003128182,-0.051389482,0.018336298,-0.007274837,-0.0016125944,0.01065326,-0.015305316,-0.0081626,0.053035926,0.031263378,-0.025370052,0.01037535,0.014604105,-0.0155613255,0.023511473,-0.021609148,-0.012181997,-0.0110507915,0.01536832,-0.015449365,0.0059380056,-0.021661995,-0.042508226,0.021001356,0.053606,-0.016251555,-0.023927983,0.03155419,-0.03720803,-0.02693897,-0.021841982,-0.016463628,0.0015684817,-0.013417766,0.031004354,-0.024767034,-0.032031942,-0.014000859,-0.0553491,-0.0117216995,-0.009997757,-0.017526573,0.4591662,-0.023892978,-0.024167962,0.025489155,-0.03048177,0.03179007,0.007035504,0.017968744,0.01299181,-0.03894913,0.026242713,0.0237454,0.016331058,-0.011606225,-0.025917377,-0.03592183,-0.0011671712,0.008339585,-0.020126585,-0.01537461,0.0027863467,-0.007004618,-0.0017297895,-0.012200844,0.0035397792,-0.044280823,0.041598096,-0.016644487,-0.011807691,0.012882764,0.03160237,0.015217159,-0.025732866,0.008954392,0.04350705,-0.041411147,-0.0089791175]', TRUE), +('6ab307a1-7a46-4e7f-a848-a6169331c6fc', 'a5e096fa-11de-46b4-ba6a-9a46f3a620e9', 0, '# BOR Vault Sync Unit The Brain-of-Reese vault syncs from `obsidian.container` on `rack7-pbx2`. @@ -172,7 +203,9 @@ The Brain-of-Reese vault syncs from `obsidian.container` on `rack7-pbx2`. - Only `rack7-pbx2` may hold the vault — do not clone it to another node. - Image tags are date-stamped; bump by re-pushing `ghcr.io/reese/obsidian-bor` and restarting the unit.', '[-0.03491476,0.035869095,-0.01582354,-0.014913304,0.023047931,0.0040641287,0.023789382,0.013562909,0.0017575669,0.0048258016,-0.01417429,-0.008365432,0.011217217,0.03382932,0.012256219,-0.0019164067,0.015187311,-0.006443484,0.0011336063,-0.053635865,0.009237916,0.011571051,0.030657401,0.039006814,-0.015935235,0.018259618,0.0036865643,-0.012118487,0.0029672247,-0.007449511,-0.022886971,-0.004606291,-0.021222001,0.029964825,-0.01598977,0.0070107137,0.027741048,-0.0028492126,-0.08241297,-0.1417608,0.009792102,-0.010859354,-0.03323338,-0.0014661638,0.03373998,0.0038416851,-0.053533446,0.010505285,0.010886497,-0.008709819,-0.046715036,-0.0038849588,0.009949761,-0.005753343,0.0012441397,0.008868596,-0.03682936,-0.055505987,0.014378312,0.0041284286,-0.025578251,0.00713573,-0.0032559428,0.0077388682,0.0008104813,-0.030581586,0.032466806,-0.068177715,0.007285088,-0.012180802,-0.0039089452,-0.017825395,0.015365129,-0.00805161,-0.0007870331,-0.0004605712,-0.0014293618,-0.002529493,-0.005489246,-0.026496267,0.049179796,-0.0034335267,-0.023806706,-0.021579027,0.0046472456,-0.006944113,0.003547938,0.020514354,0.025287619,0.025854977,0.0011490957,0.019939594,-0.01112379,-0.034943715,-0.019842505,-0.012838516,0.015448881,-0.00620218,0.013366523,-0.037232224,-0.0046703382,0.01824641,0.011178504,-0.017197588,-0.013704094,0.03313564,0.015647413,0.01104509,-0.0067171594,-0.02021759,0.023525463,-0.03285407,-0.012680353,-0.02852033,0.02787499,-6.196879e-05,0.0054960675,-0.01637356,0.03862195,0.0417377,0.0018851402,0.018348562,-0.028157426,-0.022434404,0.004896942,0.03125376,-0.028167827,-0.026175734,0.027954767,0.006358384,0.006367786,0.016984489,0.032957263,0.025355808,-0.012507706,0.008274623,-0.01684413,-0.05940413,0.008672528,-0.03477825,0.020603681,-0.024629997,0.009412474,0.014947181,0.035822663,-0.06005317,-0.00018374424,0.0018438756,-0.029478507,-0.014245474,-0.024093123,-0.022279184,0.016383216,-0.01502709,0.007129251,0.021606432,0.005494586,0.0027172498,-0.0034462877,0.01787134,0.00047053257,0.014607162,0.04576581,-0.0043632304,-0.033313267,-0.020357693,0.01830546,-0.0052860407,0.016146543,-0.013151272,-0.012257923,0.0013569269,-0.023861503,-0.01740621,0.049803916,0.04077827,-0.0010482303,-0.0061541926,-0.0059006014,-0.03815713,-0.023245918,-0.024858383,0.03052257,0.011753797,-0.007863504,-0.034726307,-0.008400688,-0.022926077,-0.01456653,0.0067407084,0.024232756,0.0027375778,-0.0059344117,0.011247073,-0.029990338,0.017855464,0.0109511,0.05115164,0.0072480584,-0.0003062795,-0.006528544,-0.020522743,-0.036752623,0.031061467,0.012485196,-0.019710775,-0.016817026,-0.011010415,0.038690325,0.0031353275,0.049972866,0.011392703,0.00990533,0.006939394,-0.011593379,-0.038645104,0.022984905,0.00081058266,0.0044187657,-0.0018131812,0.0018593954,0.0019916233,0.012191942,0.032604717,0.062565275,0.043910276,-0.031168763,0.012807796,-0.026026959,-0.0011220289,-0.030335443,-0.010007707,0.007320958,-0.02043068,-0.02124878,-0.07352998,-0.017434547,0.037229653,0.013079997,-0.008766941,0.025550947,-0.02187038,-0.014684178,0.021304678,-0.027758067,0.018734075,-0.009583994,0.01842437,-0.033987056,-0.025148863,0.0067491517,0.013744916,-0.026198892,-0.001434952,-0.00022633,0.036939465,-0.03147357,0.024529599,-0.031058442,0.00026222316,0.010650576,0.013139437,-0.0037367512,0.024971241,-0.004753872,-0.0038758945,0.007683009,0.0014173533,0.009532024,0.02085894,-0.009302611,0.021705484,-0.013778506,0.0036563086,-0.017993152,-0.04451264,0.050999034,0.011215426,0.035474434,0.032429412,0.030460324,-0.025795631,-0.023880223,-0.030148467,-0.008933386,-0.030688606,-0.02209962,-0.016402403,0.013983203,-0.019845042,-0.014157701,0.040775757,0.0009724358,-0.010064222,-0.036574826,-0.025851998,0.022322934,0.008369289,0.021926058,-0.0005764995,-0.012018586,0.0198562,0.016225602,-0.021030318,-0.016870515,-0.044984996,-0.07310409,0.022306092,-0.015412706,-0.0105398875,0.006997247,-0.0029632817,0.001379999,-0.0054038214,-0.0061594453,-0.0039874674,0.048765134,0.008192606,-0.023259778,-0.019667588,0.0024709594,0.014753283,-0.0013687283,-0.002567522,-0.032247514,0.056194823,0.023484748,0.009422233,0.01699597,0.019818343,0.017051738,-0.02528219,-0.007012189,0.041996308,-0.013598569,0.003208542,-0.01536727,0.021425601,0.047321938,0.05487729,0.01745316,-0.0053871637,0.030941315,-0.0010349898,-0.01787658,-0.0065305955,0.014605657,-0.026412044,-0.0033485107,0.003414621,0.038437217,-0.0039284267,0.025778882,0.007315828,0.0016050945,0.037665047,-0.018974716,0.006362325,0.00805567,-0.017442137,-0.053135034,0.03726995,0.014838984,0.033377457,-0.029895764,0.0069851466,0.035346188,-0.041558076,-0.04230425,0.05348194,-0.022533108,-0.0033933253,0.032567564,0.0020994702,0.055183176,-0.0023375393,0.009626058,0.008747565,-0.016893225,-0.025735695,0.02054109,-0.006647928,0.00780003,-0.010545069,0.020434823,-0.017858805,-0.004559389,-0.04571354,0.036758926,-0.0036382321,0.008531458,0.0123026455,0.029869044,-0.01612412,-0.01212411,0.018952169,0.021056773,-0.016444402,-0.03579193,0.0027450295,-0.02168778,-0.0014023158,0.009042949,-0.019703439,0.009014894,-0.012542053,-0.003582477,0.57318133,-0.0031027657,-0.013744698,-0.034019068,0.00639313,-0.015967177,0.012343676,-0.008962466,-0.028848631,-0.028546672,-0.00045014618,0.0057781683,-0.011496155,-0.00022388811,-0.02368052,-0.019405328,0.030829953,0.0144582875,-0.0064275977,0.022805054,-0.028728545,-0.023028705,-0.05155202,-0.0036293478,-0.020149566,0.0166887,0.02884873,0.021299012,-0.018780882,0.0364361,-0.028830837,0.0066860425,0.0044301082,0.009249094,0.00039838586,0.0037026329,-0.015297888,0.0035080435,-0.009073404,-0.012039886,-0.0025210136,-0.028569546,0.0002678635,-0.015705079,0.008743415,0.040644348,-0.016816141,0.013186344,0.021340754,0.013723544,-0.016519127,0.029227776,0.01797647,-0.00647885,0.008866187,-0.02637539,-0.015101548,0.002648872,-0.006036967,0.008096322,-0.008027847,-0.008591394,-0.04459291,-0.043529388,0.015997197,-0.008370768,-0.034368902,-0.022297088,0.002527394,0.013468776,0.0076101366,0.01528596,0.019102382,-0.02749151,0.009466012,-0.005390122,0.0660544,-0.037319526,-0.012797196,-0.0011945106,0.0016030549,0.008626481,0.0074641034,0.006661069,0.042736422,0.016737409,-0.015245205,-0.010233676,0.006139119,0.047145944,-0.009454054,-0.008019231,0.0006373843,0.053716168,-0.0044701276,-0.017820418,0.036264654,0.02956405,0.00086162734,-0.016460339,-0.0046347273,0.00036732393,0.022411222,0.011528445,0.022649102,-0.018078348,-0.010379539,0.016014194,-6.465884e-05,0.010844828,-0.03306597,0.026766581,-0.045150746,-0.039773285,-0.022638274,0.004153827,-0.048574105,-0.023961393,0.024349779,-0.0012804773,0.033930607,-0.027680773,0.018665504,0.044573132,-0.012049181,0.037799064,0.019078441,-0.050386243,-0.008727531,-0.024944123,-0.007611353,-0.040783368,-0.037229132,0.009166551,0.009216431,-0.0011333114,0.06680629,0.018572992,-0.047817096,0.016538981,-0.026672367,-0.033986483,0.014523136,0.010669626,-0.023458576,0.016847022,0.00010316847,0.0028071275,-0.008591283,-0.017822465,-0.015972188,0.0046595517,0.006438011,-0.018010251,0.0028868932,-0.0033013613,0.004247601,-0.017765738,-0.008802803,-0.015839059,0.019932894,0.01970615,0.054040957,-0.030290313,-0.020429738,0.004522077,0.007726137,0.024426457,-0.022549707,-0.014129789,0.015152783,-0.02317134,-0.013029866,0.057063207,0.008516101,-0.021897566,0.0024167574,0.00664235,0.014301064,-0.04496261,-0.0004537507,0.014201957,0.00092583406,-0.023396222,0.016048005,-0.0059766453,0.0019315595,0.03244023,-0.020635746,-0.030667413,-0.021497164,-0.01001244,0.053333867,0.005531757,0.002220426,0.020501351,-0.0037081225,0.013925581,-0.027093364,0.030647254,0.0017512044,0.01218534,-0.07351095,-0.019544061,-0.03731257,0.04219197,-0.019219792,0.012973138,-0.028414922,-0.021399394,-0.016947908,0.0069144266,-0.039505754,-0.056913342,-0.022146625,0.008300099,-0.00679111,-0.014975441,0.014995372,-0.022811677,-0.03524119,0.011367382,-0.01469032,0.005239235,0.019098163,-0.027872873,0.0018983616,-0.019308453,-0.010995269,0.010991139,-0.005752357,0.003990531,0.04642447,-0.0008055107,-0.0016246375,-0.011999439,-0.024374854,0.006509696,-0.009689294,0.03192983,-0.008194086,-0.028688682,-0.008159369,-0.01663278,0.015483363,-0.00786583,0.001604683,-0.0025028028,0.044583995,-0.028584065,0.013350136,-0.01490404,-0.005565288,0.023649152,0.025876304,0.002370156,0.030393127,-0.012227227,-0.04718138,-0.01612711,0.022822697,-0.024859339,-0.011959154,0.023162702,0.03174487,-0.0033841974,0.03564113,-0.009820065,-0.035548832,0.01090328,-0.008216658,0.0060808943,0.0012314146,0.015314087,0.024478298,0.015156573,0.019020867,0.0073133414,0.03517479,-0.014739921,0.017773269,-0.05603017,-0.035431985,0.0032994282,0.009655588,-0.048770532,-0.00045394446,0.005502618,-0.009288288,-0.014752971,0.020892184,0.0012755902,-0.019935332,0.024496567,-0.00809341,-0.01500759,-0.0076748957,7.184657e-05,0.0028789213,-0.009207791,0.04956478,-0.050457936,-0.00058725406,0.017516803,-0.0091628665,0.04787639,0.018898109,0.025621148,-0.011736344,0.005751374,-0.008064948,-0.030344509,-0.007107051,0.014693758,0.0023579502,-0.03637552,-0.0009408563,-0.01474573,-0.018284312,-0.0028875344,-0.0043632598,-0.010369,0.0021809652,0.015590178,0.48350582,-0.012569357,-0.022647247,0.02064792,0.0050278096,0.022019563,0.035367757,0.03511219,0.0005456674,-0.0076610725,0.036181826,0.009352723,-0.010042959,0.026403217,-0.006695136,-0.035746977,-0.0190866,0.037352737,-0.011659422,-0.018410359,-0.01106692,0.026087696,-0.0034166353,0.0051437463,0.0062516513,-0.032287315,0.019403517,-0.008193781,-0.03824763,-0.021597223,0.030820336,0.0035690118,0.003914854,0.04397935,0.028178155,-0.00507096,-0.015304896]', FALSE), -('15509e99-fa3d-4299-919e-b89caba47ad0', 'fcf52742-dc89-41f9-aade-380d0cf16cd4', 0, '# Restic Backups for Rack7 +('4b95a01d-69c5-439b-a21d-26068dcd0e3f', 'a5e096fa-11de-46b4-ba6a-9a46f3a620e9', -1, 'The Brain-of-Reese vault sync unit syncs from obsidian.container on rack7-pbx2. It uses the container image ghcr.io/reese/obsidian-bor:2026.7.14 and publishes on port 8080 via 127.0.0.1:18765. The vault directory is set to /vault/rack7 and the unit restarts always. The vault is a bind mount from /data/vaults/rack7 on a Btrfs subvolume, and only rack7-pbx2 may hold it. Image tags are date-stamped, so they must be updated by re-pushing and restarting. +Source: deployments/quadlet/mimir-service.md', '[-0.024698632,0.010983728,-0.0061292816,-0.002380541,0.014510315,-0.0029425912,0.026285972,0.026124706,0.016408661,0.0033822635,-0.0056877974,-0.027571583,0.011562945,0.045357835,-0.01711488,-0.0012454037,0.014308353,-0.011843254,0.007722133,-0.04747971,0.013527538,0.014107089,0.010042503,0.024868123,-0.009427715,0.009603007,-0.0011097534,-0.008352663,0.0007875476,-0.01010581,-0.029085359,-0.009801754,-0.01678546,0.02586394,-0.02336179,0.0053742044,0.034320652,-0.0015637857,-0.071977854,-0.15666011,-0.00887643,-0.008162026,-0.029534759,0.002019192,0.040535625,-0.037454706,-0.049025845,0.0062663173,-0.009457499,0.004523174,-0.054791864,-0.03879326,0.005429235,0.010594155,0.005529482,0.018140988,-0.034005187,-0.060611155,0.0025062691,-0.0006810248,-0.01747623,0.014836745,0.0057265596,0.035226893,-0.011115156,-0.035391904,0.026191108,-0.09067863,0.010529909,0.0026136271,9.3688126e-05,-0.027604,0.016805727,0.010459827,-0.0013699834,0.0006645012,0.004381232,-0.008601919,-0.014604939,-0.0161924,0.039434962,-0.005667244,-0.025063979,-0.011857294,-0.0011422419,-0.009563125,-0.0070988806,0.019477464,0.021634918,0.020724192,-0.00107073,0.0024950155,-0.025851185,-0.023494136,-0.017987309,-0.011593314,0.010542329,-0.0006301704,0.024502296,-0.027011102,0.005695344,0.015490581,0.021696923,-0.023095313,-0.008119736,0.060283296,0.015054909,0.014158858,-0.0028825654,-0.022580773,0.024356723,-0.013755783,-0.008117554,-0.02060183,0.019849293,-0.01096425,0.00029544937,-0.026143517,0.0375697,0.0321507,0.0010719048,0.018915655,-0.024136944,0.0052805343,0.018008934,0.019742146,-0.03488039,-0.020458667,0.02701095,0.0024252804,0.01831667,0.004524336,0.022223672,0.032859962,-0.035114437,0.022962365,-0.0346574,-0.042622562,0.02583564,-0.025486948,0.02838941,-0.023634786,0.017373709,0.023085602,0.02149403,-0.060902443,0.0073899613,0.00049269706,-0.053822294,-0.004870747,-0.00467077,-0.006223471,0.02332593,-0.008831606,-0.010718604,0.022196896,0.0149363,-0.0015439977,-0.015165681,0.020063184,0.011470447,-0.017581306,0.03700461,-0.0032187381,-0.028836794,-0.01428426,0.016811786,-0.0071597476,0.013375166,-0.020491937,-0.013634923,0.036016233,-0.0067677028,-0.024645258,0.054434136,0.04047187,0.0199567,0.010762914,-0.012464446,-0.028199287,-0.021601947,-0.0138261,0.016790343,0.01998462,-0.02164406,-0.024843955,-0.012579056,-0.016491512,-0.015228474,0.0057566166,0.0008586335,0.010248048,0.0005076875,0.0033348096,-0.03407023,0.016248712,0.008567122,0.016113464,0.011006007,0.006431846,-0.011452585,-0.02310028,-0.03516132,0.02639218,0.012433874,0.02105139,-0.028056538,-0.008990615,0.02029044,0.008475104,0.031695742,0.017922422,0.011328621,0.0015437468,-0.015249463,-0.042263377,0.019742858,0.006817652,-0.0055807517,-0.004895368,0.002012234,0.016507743,0.006754602,0.058539446,0.058034334,0.036896124,-0.027039843,-0.0030505725,-0.023114683,-0.0037691349,-0.026018757,-0.010197348,0.007766739,-0.0019826968,-0.029962046,-0.07442973,-0.0048365383,0.02681539,-0.00096595945,-0.02347909,0.020075291,-0.02835371,-0.00786579,0.010474543,-0.009503461,0.02578945,0.0023421948,0.010484227,-0.016973378,-0.018423392,0.0004963033,-0.0099886,-0.02737227,0.019669497,-0.005257927,0.01013823,-0.026817339,0.035388235,-0.025312513,0.0036312777,0.009129244,0.023816407,-0.0014685568,0.022845604,-0.0095505025,0.0058398265,-0.0064084604,0.004382781,0.011109914,0.019300926,-0.0038884534,0.026185036,-0.017554166,-0.01026491,-0.025193911,-0.055697765,0.037226208,0.017079884,0.03563014,0.034659646,0.031483512,-0.026958615,-0.054438345,-0.028688328,-0.0023807622,-0.01864787,-0.035069633,-0.010563743,0.026522448,-0.006498235,-0.010149667,0.031616606,-0.0059725293,-0.0035258657,-0.044661753,0.0006059851,0.012668769,0.008307641,0.02928269,-0.0056083146,-0.009147959,0.008945472,0.012096356,-0.0062566027,-0.018244993,-0.05339029,-0.048999965,0.008479074,-0.0108523015,0.023084385,0.016166901,-0.024176912,-0.018750357,-0.013535472,-0.00734362,-0.02232054,0.0574791,0.007971154,-0.011869975,-0.007847874,0.02014998,0.01712854,-0.0060190954,-0.02008847,-0.018592803,0.054623384,0.006067928,-0.006863795,0.016132534,0.016389562,0.020848023,-0.016660279,-0.0065667704,0.037839174,-0.0006368565,-0.014241675,0.0031109483,0.010913128,0.034938265,0.05505348,0.024636634,-0.028872179,0.0124294115,0.00043286904,-0.00025084554,0.0055839284,0.011667169,-0.02212895,0.0074743633,0.012617464,0.045047376,-0.01519318,0.027079249,-0.013428834,-0.006508861,0.03433938,-0.016912168,0.018102383,0.006177578,-0.012308086,-0.051349502,0.04039809,0.014075444,0.037897464,-0.031334117,-0.01063949,0.02475322,-0.022157466,-0.046157766,0.020313071,-0.016291847,0.0063203154,0.015710194,-0.0041180295,0.040677603,0.0061416756,0.002891301,0.016013695,-0.025109721,-0.025290782,0.011191435,0.015572648,0.0072053806,-0.0073310933,0.017429437,-0.0037884256,-0.0011617929,-0.0596646,0.032699954,0.0015750017,0.0066873003,0.021377103,0.014659442,-0.0046870653,-0.012050726,0.0038497238,-0.003596457,-0.0067261965,-0.027449487,-0.0006843876,-0.022278572,0.0064169494,-0.005095329,-0.009385927,-0.00363358,-0.0133215785,-0.01215929,0.57209146,-0.005795392,-0.0055498173,-0.034686342,-0.001580741,-0.015094014,-0.004919903,0.0024892054,-0.020847106,-0.03422514,0.014275064,0.022403535,-0.011330044,0.0033420308,-0.033956196,-0.01896489,0.008977368,0.013739759,-0.004995123,0.031955805,-0.0120392535,-0.015557268,-0.044031095,-0.024852594,0.0033988438,0.01912984,0.037331086,0.025883667,-0.010706026,0.04907777,-0.009609584,-0.0066818525,0.004513509,0.035378154,-0.02089975,-0.011300899,-0.0032175947,0.017861895,-0.007603666,-0.015727887,-0.0040115644,-0.037109137,-0.019354071,-0.02011625,0.02151827,0.0425263,-0.0067625814,0.00091100775,0.02163213,0.020212436,-0.007955015,0.043635048,0.01185707,-0.011345487,0.013110356,-0.02015004,-0.003802961,-0.005984993,-0.0018712091,0.008638843,0.0048232535,-0.019977175,-0.033136718,-0.026265562,0.0070416033,-0.009542218,-0.011596601,-0.015223384,-0.0106770685,0.009576403,0.008465322,0.014081845,0.021365236,-0.017893638,0.022158641,0.0007355732,0.053137545,-0.02613615,-0.021630142,0.005086473,0.009552367,0.004232333,-0.010271406,0.01458039,0.033910405,0.01727951,-0.0038805215,-0.024767738,0.004120062,0.037094336,-0.0121338805,-0.0018089792,0.0049009807,0.0326177,-0.004826066,-0.0027075259,0.040862657,0.010419805,-0.017321788,-0.00067632773,0.0131451925,-0.020494282,0.033429407,0.005595467,0.03671983,-0.05453879,-0.007062267,0.0075227013,-0.014837063,-0.014979964,-0.022250392,0.014159453,-0.044705834,-0.030588655,-0.017888375,0.0127067305,-0.03439598,-0.030693246,-0.0061499653,0.0052755675,0.016771678,-0.023581196,0.017614983,0.038340818,-0.0065514864,0.052440114,0.010236683,-0.0448113,-0.0018562013,-0.018200273,0.013378915,-0.03621067,-0.045297418,0.02032595,-0.0030391093,0.003141598,0.06300697,0.01337601,-0.040198546,0.025956595,-0.010544166,-0.036688726,0.022843452,0.024751699,-0.009891603,0.01855866,-0.013931952,0.009181049,-0.0075570876,-0.022339022,-0.015923662,0.0041380837,0.003829389,-0.00810638,-0.008292907,0.017536828,-0.011796341,-0.01851985,-0.004725307,-0.017102048,0.015505616,0.013152149,0.04326659,-0.036944948,-0.00583558,0.009073849,0.0007194119,0.022761673,-0.027396126,-0.021008285,0.019082852,-0.0313961,0.0015323687,0.038383175,0.010782233,-0.018018115,0.004043392,-0.006224087,0.0015795024,-0.045919083,0.007953236,0.004374314,-0.00043658869,-0.03006724,0.0050347853,-0.025007289,0.01452231,0.052972145,-0.018675739,-0.044998735,-0.021286558,-0.03735344,0.054029167,0.004275509,-0.0015712092,0.031095065,-0.0053504664,0.0031551241,-0.016220754,0.042323597,0.007319823,0.010358578,-0.062497687,-0.024525449,-0.025003793,0.03460163,-0.008265351,0.020863628,-0.032609478,-0.018844772,-0.010010019,0.01325875,-0.030521521,-0.047748845,-0.029903216,-0.004866989,-0.02731651,-0.037068684,0.0070948005,0.0032983236,-0.032019578,0.013219348,0.0070790052,0.011014258,0.033613876,-0.020951828,0.005134959,-0.022274408,-0.0072705685,0.0011951529,-0.015130333,-0.010894298,0.030833013,-0.004783016,-0.005622442,-0.02402303,-0.02565846,0.0110405525,-0.012172469,0.009498124,0.009647436,-0.024024485,0.0017932382,-0.011943206,0.021774113,-0.011796251,-0.011662284,-0.0063176695,0.029499875,-0.017384518,0.019973008,-0.036181573,0.0035308392,0.025425658,0.024257159,-0.012885619,0.034664765,-0.01396927,-0.032898594,-0.01567901,0.020970957,-0.030453837,-0.008126599,0.037765127,0.02573341,0.0066405577,0.033183828,-0.008233075,-0.01496341,0.010208692,-0.0013475759,-0.0024238876,-0.011892037,0.0076741613,0.0014067038,0.01593311,0.024424719,0.0061573735,0.035778273,-0.026726078,0.008894296,-0.039542127,-0.03803305,-0.0025383579,0.005175802,-0.056831803,0.0004407448,-0.0008934838,-0.016230674,-0.015710926,0.04222163,-0.003262854,-0.018693153,0.024695354,-0.0041015036,-0.024322875,-0.009352382,0.004347683,-0.00036598527,-0.017031487,0.052799173,-0.0543822,0.0037749228,0.0058585415,-0.020910041,0.054166134,0.027331436,0.024877658,-0.023626389,0.017228266,-0.0068893903,-0.02429345,-0.018109951,0.018650278,-0.00859242,-0.011590333,0.004261768,-0.025242047,-0.027188025,0.00031316475,-0.02477256,-0.007945667,0.016323084,-0.0018185349,0.4936347,-0.023719419,-0.009335869,0.020186786,0.0041801925,0.03367667,0.033933233,0.026941318,0.0077975914,-0.003911629,0.025299408,0.0055502197,0.0007418088,0.03768667,-0.009144314,-0.025857054,0.00063021714,0.026097648,-0.043515563,-0.015910635,-0.009758731,0.020231556,-0.004579897,0.0011565705,0.021882666,-0.00812267,0.02563037,-0.023079691,-0.031603243,-0.029533567,0.031115325,0.0050793667,0.0026623118,0.04344396,0.024435941,-0.017983073,-0.011706546]', TRUE), +('bc2b4b28-a654-4b7d-aa01-9a8fdbb80729', '7ded7333-b66a-4e78-b087-5d0b89b058de', 0, '# Restic Backups for Rack7 Every config and document on the cluster is backed up nightly with restic into the minio repo `minio01:/backups/rack7`. @@ -183,7 +216,9 @@ restic into the minio repo `minio01:/backups/rack7`. - Retention: `--keep-daily 7 --keep-weekly 4 --keep-monthly 12` - Prune runs only when the repo grows more than 5 GiB. - Restores are tested quarterly; the last dry-run passed on 2026-07-01.', '[0.03761096,0.03845751,-0.046449885,-0.018830845,0.013782317,-0.026799997,0.028011672,-0.014759444,0.04048799,0.0125113735,-0.03270166,0.020369474,0.029716162,0.056183625,0.004086181,0.0021839845,0.013344225,-0.01195098,0.017994436,0.0040622517,0.0033874852,0.0031000057,0.008979001,0.013740418,-0.0012302804,-0.008771627,0.023737019,0.006424138,0.022654146,-0.0033294868,-0.001471167,-0.0022851627,-0.0014323967,-0.028203065,-0.029449882,0.047680467,0.004218848,-0.014611294,-0.044602387,-0.18689843,0.006125248,-0.0042640874,-0.006139907,-0.013590747,0.016424276,0.010886777,-0.038443487,-0.010576557,-0.0001617777,-0.017242054,-0.05279384,-0.018232,-0.016828304,-0.024608774,-0.00017247943,0.012613475,6.3012696e-05,-0.018185548,0.005539525,-0.021843316,-0.009800368,0.017444877,0.0024649997,0.009993986,-0.018718682,-0.024884202,0.022708906,0.020322997,-0.027833063,-0.02249481,-0.010331156,-0.0118693225,0.03266986,-0.011695564,-0.024418436,0.0010251983,-0.011319636,-0.00064347807,-0.026136942,0.026511464,0.039550077,0.006194668,-0.0046101552,0.023782557,-0.008779051,0.0001826582,0.0590264,0.021849662,0.020296859,-0.0113497395,0.0038890531,0.006403461,0.024841694,-0.034869667,-0.009757708,-0.005141348,0.0034396513,-0.015564897,0.017966133,-0.011501329,0.021277754,-0.002021882,-0.02602786,-0.0067004897,-0.023739833,0.031453557,0.010629095,0.01538254,0.030754486,-0.00034020317,0.043437373,-0.010995092,0.0104634045,-0.010023622,0.042626232,0.005970923,-0.023613768,-0.0279048,0.023494547,0.014301009,-0.023132104,0.013174213,0.01227915,0.012290398,0.0076261703,0.010518905,-0.042261988,-0.036460213,0.024107073,0.00078200153,0.04550258,0.016715905,0.01544688,0.024979662,0.024689354,0.013024123,-0.022708489,-0.030681428,0.0024540261,-0.020500494,0.01917579,-0.023505684,-0.0044378205,0.012491571,0.008246802,-0.054716535,-0.010336207,0.026989613,0.0068090567,-0.007533021,-0.01673501,-0.014486656,0.01986469,-0.0002712997,-0.020300835,0.011188979,-0.007495112,-8.997209e-05,-0.027488157,0.0072011873,0.004804083,0.004209749,0.058572426,-0.00479968,-0.038208302,-0.055804368,0.022218633,-0.01997908,0.03951621,5.8289872e-05,-0.014424578,-0.009032474,0.00073962956,-0.0034119012,0.036099695,0.01852639,0.021552198,0.0127526475,-0.026182782,-0.035445888,-0.015438639,0.0027175683,0.023037706,-0.007262907,-0.023425858,-0.030220727,0.015140303,0.01839637,-0.010851032,0.006931794,0.0036087362,0.0046170573,0.020288102,0.02002083,-0.044236816,0.0025493335,0.02237714,-0.008931042,0.0003407974,0.018352956,-0.021331111,0.01104459,-0.014687541,-0.019028705,0.009501976,0.0013785468,0.017071674,-0.011701193,-0.0035739534,-0.0071924054,-0.0039796885,0.020758366,0.021297554,0.0033917052,-0.018489916,-0.0016993035,-0.0063099177,0.009485167,-0.0005972616,0.013237664,-0.031366646,0.038725346,7.4767795e-06,0.027179282,0.0547354,0.018808955,-0.012212906,0.034384247,-0.019159215,-0.025321016,-0.017608784,0.029767074,0.030611057,-0.018098311,0.002305545,-0.03935213,-0.008776912,-0.0018306598,-0.015316228,-0.040251862,0.01285102,-0.0031037815,0.01263414,0.012208599,0.0034347405,0.0018293039,0.030281797,-0.0032900763,-0.017589536,-0.0064115785,0.01272321,-0.014466027,-0.016066836,-0.0028359646,0.008342219,0.043442994,-0.01913109,0.028180197,-0.039988346,0.012164127,-0.014547484,0.007841115,7.388102e-05,-0.0012689865,-0.018071882,-0.012062892,0.018275686,-0.0019326421,0.021694044,0.03596519,-0.047186945,0.015006069,0.0074002277,0.007906536,0.0028772077,-0.0059939907,0.05708875,0.0005403434,0.004942406,-0.01015348,0.07780932,0.034239862,-0.013970348,0.010422828,-0.0069098566,0.026320975,0.05176009,-0.03791575,0.010149191,-0.022847103,-0.018528596,0.028213969,0.022199374,-0.021671794,-0.028123785,0.0027973787,0.0062475232,0.02320845,0.0049380832,-0.0060180086,-0.006489382,0.052308332,-0.021829002,-0.02643016,0.010096845,-0.0012172251,-0.021524867,0.014934391,-0.021118557,-0.022989336,-0.00895462,0.014305431,-0.0067681195,0.0028159288,-0.013418973,-0.00047048926,0.010038304,0.02075569,-0.028722212,0.023547674,0.005346972,-0.0025025706,-0.010678364,-0.0048973276,0.021656714,0.014410446,0.008417302,-0.0115486905,-0.008412803,-0.0028947804,0.046248313,-0.015704505,-0.025723837,0.017168032,0.001019225,0.0030896533,0.0027074066,0.0021803342,0.0029365693,0.049839474,-0.003381094,-0.0031961831,0.030304339,0.04933238,-0.011017354,0.015305227,0.024338406,-0.017067926,0.020326449,0.015574955,0.016124824,-0.014013079,0.031115657,0.019007782,-0.013144482,0.0292043,-0.022218157,-0.02324806,0.015768329,0.022160605,-0.08665321,0.0052939453,-0.050235044,0.029383892,-0.00054845936,-0.0071919668,0.0055732946,0.011285889,-0.009802767,0.0043365476,-0.014954502,-0.008088233,0.022363022,0.0040325704,0.048826504,-0.009965912,0.012336393,0.0075166486,0.006612405,0.01064673,0.045952834,-0.015774496,0.009098071,-0.006717549,0.0067918757,-0.01928878,-0.025228461,-0.021348912,0.046100885,-0.031358406,0.01848588,-0.009997917,-0.014437652,0.0023690509,0.0033770115,-0.0010565926,0.030118087,-0.00967853,-0.0011700417,-0.0072166943,-0.0121249305,0.0004005595,-0.027963022,-0.01125453,0.0077636377,0.007157014,0.031584684,0.5855089,-0.011663495,-0.0024803379,-0.0115528805,-0.009733317,-0.028117035,-0.017949805,0.013395014,0.019470725,-0.017198669,-0.013404827,0.007306281,-0.02482736,-0.01712637,-0.028973598,-0.034433573,0.030809833,-0.0038871488,-0.011646894,0.021613702,-0.015741246,-0.011423277,-0.007403161,0.016648663,0.012707949,0.009709885,0.03213687,0.023952544,0.011794202,0.01467203,-0.022777429,0.018281553,0.023862937,0.028303035,-0.012100144,0.0007123921,-0.0002629568,0.015857702,-0.027246853,0.006870157,-0.038692597,-0.0014972732,-0.030566974,-0.050413456,0.016157929,0.020323312,-0.014329851,0.0040810653,0.032390717,0.0026425903,0.002546029,0.00071060454,0.03411991,-0.007807385,0.028760796,-0.021841938,0.010217062,0.0060451734,0.007598971,0.0018214614,-0.008900798,-0.026798876,-0.05486691,0.0062237177,0.027850548,0.046236355,0.011708924,-0.023480145,-0.0058089364,0.0181067,-0.015160994,-0.0084482115,0.04767011,0.0016210005,-0.004564487,0.025731487,0.01829214,-0.029410899,0.008616046,0.0029778685,-0.022747375,0.012595691,-0.008717069,-0.0027399282,0.0372084,-0.0019402337,-0.0048225424,-0.018705647,-0.020919837,0.03009349,-0.0314376,0.003784681,-0.021762853,-0.020258566,0.012773146,-0.015708165,0.03272881,-0.012274982,0.016842317,0.0009903049,0.013883126,-0.048592817,0.018047998,-0.011396761,0.014007741,0.011222103,-0.0145386085,0.038577247,0.014752438,-0.009243947,-0.020052353,0.0122023625,-0.010715282,-0.031433746,-0.029768387,0.006028392,-0.049416814,-0.027611403,0.0029618572,0.024212373,-0.006705885,-0.01079005,0.0025553394,0.00033352952,0.0038378115,0.028561989,-0.007332827,-0.029395266,-0.01914031,-0.05887388,-0.009600884,-0.0050616795,-0.032508913,-0.018510865,0.015708232,0.033725187,0.012103836,0.0232546,-0.05595556,0.029001812,0.017541206,-0.0489441,0.0010217237,0.0071330625,0.025188176,-0.0041661575,-0.017974136,0.026003418,0.007979857,-0.017892009,0.0010450962,0.018773438,0.029164443,-0.017492268,0.047877554,-0.0016484922,-0.03336197,0.0020111904,-0.04045502,-0.015797758,0.017935364,0.025911553,0.02618955,0.026777921,-0.026671449,-0.006852658,0.010420287,-0.008348874,0.0011636202,0.029316692,0.055606592,-0.012509326,0.0008563652,0.04740409,0.014411872,-0.03487884,0.021067595,-0.0126126325,0.044001736,-0.063164264,-0.027318986,0.010695179,-0.0003432193,-0.005149867,-0.024190817,-0.016059043,0.02489626,0.00994767,0.01742785,0.020036422,-0.007741041,0.0138123445,0.023263868,0.010511255,-0.025223354,0.032618422,-0.012514445,-0.008064137,-0.019283054,0.02968576,-0.042000186,0.034744922,-0.08000382,-0.023906216,-0.009709265,0.042285435,0.016569715,0.010252344,-0.014524059,-0.0071021784,0.010271416,-0.015865302,-0.04126646,-0.013233949,-0.025880793,0.017527044,-0.012520677,0.027858503,-0.0056137885,0.014634232,0.000799132,0.021729574,0.011616285,0.013146522,0.034776356,-0.041942805,0.0097889835,0.0007933133,-0.001745205,-0.0022480197,0.03805336,0.012616475,0.027529,-0.005266023,-0.021932572,-0.03740726,0.009104739,0.01594064,0.0024212725,0.001330272,-0.010808129,0.0050421227,-0.014689926,-0.012917686,0.022572238,0.010961904,-0.0074047623,-0.004245147,0.006762044,-0.03644341,-0.0074358312,-0.026903765,0.021245304,0.028217973,0.0039303284,-0.018399272,-0.03921105,-0.038483188,-0.039419714,-0.020354215,0.007132286,-0.050268583,0.008026643,0.046403915,0.022957828,-0.0032463854,0.013751087,0.0108671505,0.013702765,0.025366062,-0.032861084,0.010498066,0.013494579,-0.0032024528,0.042453922,0.010593409,0.0012229299,0.02512064,0.06790153,-0.033845257,-0.00976506,-0.018716387,-0.027007371,-0.007153931,0.021231068,-0.043418415,-0.009926794,0.01090704,-0.015320391,-0.017510762,0.014164759,-0.01283177,-0.0028165567,0.018318905,0.009508245,0.007911559,-0.006961052,0.02335703,-0.0041041384,-0.018327875,0.025319248,-0.019910257,0.008480754,-0.023945875,0.02610638,0.052361015,0.011040081,0.01621559,-0.012236439,-0.0052494104,-0.01117124,-0.0043942006,0.008391458,-0.015080645,0.007826582,-0.012133952,-0.009219443,-0.04504332,0.00516558,-0.0066840453,-0.047285356,-0.005357862,0.012288057,0.00930124,0.48115137,-0.015110162,-0.053059775,0.032381345,-0.008177271,0.027620994,0.0067838496,0.006392116,0.0010489281,-0.0051077995,-0.0013732259,0.039070234,0.0005995462,0.036348723,0.0014011143,0.011963445,0.020486841,0.043508593,-0.026771178,-0.019147022,-0.011938439,-0.00010539129,-0.035564005,-0.006321061,-0.010632165,0.0161966,-0.0016563269,-0.003962014,-0.027796913,-0.021819497,0.033436473,-0.010124782,-0.012545251,0.026379736,-0.029676761,-0.030830748,-0.003297667]', FALSE), -('61fde899-250f-412c-a82c-2d7cca3fda65', '4b6dcf82-7c3a-46d3-8e17-f27d7b4aebf4', 0, '# Qwen 3.8 on llama.cpp (pbx-node3) +('510a2f16-7ec5-401c-a43c-3f4cf3db7b24', '7ded7333-b66a-4e78-b087-5d0b89b058de', -1, 'This document configures nightly backups of cluster configuration and documents using restic. The backups are stored in a minio repository at minio01:/backups/rack7. The backup process runs from machine ID rbm-8842 with a password retrieved from vault. The schedule is set to 02:17 daily, and retention keeps 7 days, 4 weeks, and 12 months of backups. Pruning of backups only occurs if the repository exceeds 5 GiB. Quarterly restore tests are performed, with the last dry-run passing on 2026-07-01. +Source: homelab/backups/restic-rack7.md', '[0.039883267,0.0237394,-0.047055013,-0.01357414,0.021763118,-0.026152648,0.027699228,-0.005760386,0.035003588,0.018800598,-0.03542627,0.017658887,0.0078345165,0.033329885,-0.0011813623,0.01642496,0.01630808,-0.014859223,-0.0084684435,0.017898425,0.009518834,0.0037572703,0.02039516,0.013224879,-0.017258689,-0.014366709,0.030044002,-0.0011633134,0.017248828,0.020877844,-0.008410886,-0.005078634,-0.004325131,-0.041229863,-0.032030363,0.04590882,0.005227436,-0.014516612,-0.037543636,-0.18129528,0.014928699,-0.00028699596,0.004562158,0.00023118946,0.020574046,-0.01462545,-0.0440924,-0.02732851,-0.021932714,-0.0046417788,-0.039281107,-0.008647248,-0.0130224265,-0.009242488,0.018282795,0.009526479,-0.011845393,-0.012700973,0.0015152299,0.00353328,-0.0015830124,0.018383976,-0.0036847112,-0.024865946,-0.029035926,-0.0452746,0.024488417,-0.003813077,-0.023590133,-0.010631872,-0.0031545078,-0.02694852,0.04257714,-0.018935513,-0.014369247,-0.001700724,-0.020089526,0.0126579655,-0.018705543,0.010255023,0.042319942,0.009348141,-0.016663628,0.020724043,-0.018510215,-0.011140343,0.042973146,0.0012978549,0.031445686,0.0066614305,-0.010469226,0.012484262,0.024535436,-0.0071730125,-0.011039418,0.00043673973,-0.0042392663,-0.01310192,0.013017749,-5.171203e-05,0.026918694,-0.005294822,-0.021287233,-0.012235678,-0.044190325,0.022957752,0.0069480226,0.0019257934,0.017486634,-0.00029604856,0.04596113,0.008629171,0.0124683175,0.022813376,0.035007488,0.0039462997,-0.028769894,-0.027953863,0.024988454,0.021958333,-0.009017235,0.023069676,0.0034625845,0.03360983,0.0052354042,0.027046064,-0.05320343,-0.024504364,0.04225639,0.011595104,0.035008345,0.015865207,0.011838416,0.02583566,-0.0032675418,0.022431854,-0.023980571,-0.046381094,-0.006478017,-0.014807996,0.035760682,-0.020005928,0.00525668,0.0045032827,-0.0035120256,-0.018588662,-0.021023538,0.016279565,0.0018606079,0.0054344027,-0.0017039577,-0.006984731,0.0038505718,0.0019331872,-0.030037692,0.003432177,-0.01068802,0.008174902,-0.017928962,0.019779705,0.004804442,0.0044661355,0.041803233,0.002197234,-0.03256558,-0.05587877,0.01778111,-0.0261762,0.029697182,-0.020470995,-0.01691274,0.007304933,-0.0072741923,-0.003219502,0.047923774,0.02803025,-0.0030593167,0.019978756,-0.029277047,-0.035814673,-0.008745122,0.006665402,0.021672864,0.004318663,-0.01464627,-0.012371333,0.00981698,0.015315855,0.00219052,0.012765476,0.0053116456,0.017431606,0.026872559,0.015239964,-0.047193315,-0.018727193,0.016134284,-0.013079834,0.019808944,0.008688435,-0.02292199,-0.0005043046,-0.03138905,-0.029685019,-0.016584745,0.012858299,-0.0023584343,0.0010971148,-0.00083862257,0.004642264,0.011356536,0.025342373,0.035368845,0.008081806,-0.0054316926,-0.0058563487,0.023035908,0.0051874765,0.013974754,0.006805957,-0.046460837,0.034173712,0.0016165903,0.03974781,0.035508435,0.017076416,-0.0063767345,0.014568228,-0.017308343,-0.020122206,-0.020695336,0.025093265,0.04254115,-0.001769868,0.0031503853,-0.03614834,-0.023309924,-0.010883552,-0.026306694,-0.04364816,0.003386244,-0.020972379,0.009839096,-0.012947216,0.0044139894,0.0037875006,0.018120319,0.0041983407,-0.034709476,-0.0025699288,0.020573938,-0.012911097,-0.021203853,-0.018621968,0.021113923,0.040427405,-0.014590275,0.033493172,-0.048385423,0.0030879008,-0.020799866,0.0011221877,-0.0044827284,-0.01473079,-0.015677178,-0.002706719,0.00575384,0.0068580555,0.0049446775,0.036013685,-0.042024113,0.026214536,0.02225331,-0.00096659706,0.008313392,-0.0133450935,0.05428449,0.010833474,0.0057330388,-0.013457531,0.0722634,0.03492147,-0.026013952,0.019689286,0.00027989584,0.011964135,0.037235998,-0.024473008,0.008692206,0.0033363935,-0.0044145305,0.006322754,0.009186428,-0.02361724,-0.037084352,0.00011854931,-0.012548757,0.03851575,0.0015848655,-0.005173175,-0.0038694432,0.057308365,-0.020443708,-0.01842193,0.007999384,-0.013485716,-0.0306786,0.023595603,-0.011556226,-0.018560436,-0.022928514,0.021179255,-0.015596201,0.0039222213,-0.022983698,-0.014452022,0.025604667,0.01731312,0.0064477446,0.0154967625,0.00051343866,-0.0046158163,-0.0029057947,-0.0130487485,0.025939826,0.029482767,0.022025965,-0.000759044,0.0049747163,-0.0019225142,0.027936297,-0.0291021,-0.014142194,0.017273804,-0.0023095384,-0.0076354877,0.011569472,0.0017413658,0.011527086,0.05643461,0.006348374,-0.018714573,0.018875806,0.046857253,-0.011593859,0.03166819,0.035552386,-0.021979026,0.020179715,0.03279257,0.018356107,-0.014965607,0.02919843,-0.016050067,0.00028779326,0.04871657,-0.023650598,-0.018412417,-0.01329137,0.03139757,-0.107372575,0.0039923247,-0.040553946,0.026000155,-0.009510168,-0.0035633568,-0.007237141,0.0131194,-0.01603995,0.0033207077,-0.009091481,-0.019014578,0.054620616,0.012164764,0.040911872,-0.012820852,0.013188188,0.021422928,0.0064401766,0.0043549547,0.048729014,-0.015568207,0.0050969184,-0.0057557393,0.0018307111,-0.026716156,-0.03360349,-0.024005795,0.0658164,-0.02334248,0.032369647,-0.016557392,-0.0042204754,0.006220851,0.009851479,0.002714585,0.0024578406,-0.014375583,-0.0034193178,0.0042251535,-0.0015071172,0.006609014,-0.032847166,-0.038169388,0.01749506,0.009881951,0.030157052,0.5842859,-0.0023046834,0.002073966,-0.018369632,-0.0061110756,-0.04449102,-0.00085585215,-0.0018205126,0.025476754,-0.004755715,-0.006021494,0.0023610205,-0.027366659,-0.021079328,-0.020720303,-0.021218259,0.037687458,0.00018805244,-0.016083969,0.026917925,-0.011977504,-0.019819079,-0.00518821,0.0067048,0.018171757,0.017023116,0.036745798,0.007758195,0.004939763,0.004207287,-0.028556988,0.014901672,0.008828816,0.025036696,-0.012383388,-0.014481003,-0.0012680091,0.0223234,-0.03256324,0.00794806,-0.059664506,0.0049922443,-0.011905483,-0.03660583,0.024116855,0.019942028,-0.0014672062,0.0002804114,0.031186027,0.009903511,0.004831232,-0.0041654897,0.029304184,-0.003836475,0.042025898,-0.017708294,0.0073451404,-0.006809242,0.0042849523,0.013000933,-0.0058691865,-0.03162393,-0.047355175,0.03316599,0.023026546,0.019931383,0.009196648,-0.032546666,-0.0009942164,0.02069124,-0.010136466,-0.0017174287,0.048359577,-0.015692353,-0.026372591,0.014135943,0.017701896,-0.047534116,0.0055647767,0.004637105,-0.034823444,0.03226192,-0.024518311,0.0042953105,0.03393813,-0.021624058,0.0037774972,-0.025202945,-0.01173844,0.025802162,-0.021523124,-0.02205053,-0.019980706,-0.023540892,-0.0022938256,-0.011529628,0.033675864,-0.019897388,0.0080872765,0.014246417,0.0042057172,-0.045164056,0.007347003,0.0059293024,0.0030751422,0.002962453,-0.025782455,0.049910408,-0.0008913418,0.0027523807,-0.010663115,0.016526219,0.00021167393,-0.023881342,-0.01954703,0.008936239,-0.05086724,-0.023254177,-0.005494978,0.033622522,-0.021368787,-0.009653334,0.0097493855,-0.00603935,-0.0037647756,0.015647968,-0.02233759,-0.014784148,-0.012314167,-0.05929898,-0.022924129,-0.0030701417,-0.042118493,-0.023093937,-0.018158251,0.036908947,0.016458036,0.02046838,-0.041862406,0.035078943,-0.0051067476,-0.040285252,-0.012675104,0.004306222,0.025689565,-0.0065169516,0.005317306,0.011438045,0.034370147,-0.006038906,0.015501296,0.016732126,0.025351608,-0.011601884,0.048291143,0.016487889,-0.025980165,0.022179835,-0.02188667,-0.024747228,0.01663225,0.01220296,0.031216605,0.018849641,-0.016274277,-0.0004393711,0.004903546,0.0031958155,-0.0075530685,0.021513218,0.053974096,-0.039884914,-0.0010941238,0.041928444,0.023965353,-0.02115434,0.024428552,-0.0031574515,0.056266565,-0.04912857,-0.015689641,0.002971063,0.015812824,-0.0037046873,-0.024008423,-0.014622879,0.0133710075,0.0073941816,0.016265905,0.028720088,0.002785311,0.026164776,0.029951908,0.016693776,-0.018532401,0.02841729,-0.011384688,-0.031613585,0.005646303,0.021254756,-0.033839464,0.03605229,-0.085405596,-0.019673234,-0.017201977,0.04177028,0.010788672,0.027375506,-0.022735218,-0.02362753,0.0037759943,-0.012640621,-0.04253197,-0.01257204,-0.021304723,0.009868347,-0.011206114,0.018088754,-0.025161324,0.030153299,-0.022286715,0.027640162,0.022215191,0.015955126,0.033599198,-0.036793757,0.009899947,0.005035731,0.0067131245,0.0012699962,0.030301444,0.0037394075,0.023561927,-0.013574348,-0.0303958,-0.0290068,0.011284313,0.016098808,-8.8899695e-05,-0.0003779865,-0.00719074,0.0051864767,-0.0036466192,-0.022990566,0.018589094,0.0044950205,-0.004365273,-0.005466151,0.0049933563,-0.032164633,-0.003242076,-0.0013483991,0.02090338,-0.012873451,0.0075735273,-0.021559386,-0.018982528,-0.031863306,-0.028455075,-0.015721833,0.019213527,-0.075354904,0.015133945,0.034998994,0.03091031,0.016104266,0.008442916,0.008539062,0.00025241496,0.0081622405,-0.026021952,-0.0016838127,0.012418038,0.0059126243,0.054307044,0.008721159,0.01699635,-0.002388513,0.0531318,-0.028066805,0.0040138876,-0.026218994,-0.039944932,0.0014211971,0.020336898,-0.046445444,0.00708479,0.010390967,-0.017701948,0.008412935,0.02359271,-0.008100165,0.0027101089,0.018472273,0.010353387,0.0026435945,-0.0008172849,0.0035398449,-0.009001319,-0.023909837,0.030256433,-0.03258238,0.0006499124,-0.036294725,0.023298342,0.06309947,0.005449883,0.022393178,-0.018807061,-0.0024763136,-0.023604333,-0.01358149,-0.0025545373,-0.012757047,-0.004603173,0.005470008,-0.020617759,-0.03534547,0.004030544,-0.011467707,-0.03903438,-0.0070818765,0.010935642,0.0021921878,0.4706591,-0.015898053,-0.048806153,0.030517198,-0.009497328,0.028868731,0.024334671,0.0011606697,0.0017484623,-0.017719049,0.0148900105,0.0322479,0.0018698407,0.0189505,0.002597219,0.013247742,0.017401645,0.038551778,-0.026660679,-0.019844381,-0.008296647,-0.003230987,-0.015398769,-0.012543352,-0.0048139733,0.0101918345,-0.007128411,-0.0007028627,-0.034060102,-0.024691999,0.034571353,-0.005853237,0.0014528373,0.018615017,-0.014924708,-0.025866011,0.001876907]', TRUE), +('8646b320-8cbe-4af4-bda7-e26c409acfa2', 'aeee2625-fbda-4663-9225-0d1e1938297f', 0, '# Qwen 3.8 on llama.cpp (pbx-node3) The GPU box (`pbx-node3`) serves Qwen 3.8 through llama.cpp. @@ -198,7 +233,9 @@ Working launch line (verified 2026-08): `/srv/llamacpp/cache`. - First-token target is under 400 ms; sustained throughput about 28 tok/s. - Do not add `-ctk f16` — it doubled VRAM usage with no quality gain.', '[0.0048955665,0.006563279,-0.007181519,-0.00087667035,-0.0001494127,0.0041002524,0.03486239,-0.010442664,0.04654968,-0.00062298507,-0.014839416,-0.026391102,0.0042345123,0.015116379,0.022619056,0.0009777708,0.028190494,0.03359672,-7.516318e-05,0.0044173757,0.011689127,0.026692953,0.008683573,0.0054613664,0.009840982,-0.00543539,0.002404915,-0.014604245,0.046088442,0.01996778,-0.0053201355,0.016592607,0.011093611,0.021259272,-0.0100452,0.025072746,0.050448727,-0.024068335,0.0005037724,-0.123938255,0.019930616,0.016123548,-0.012261513,-0.007168551,-0.0052392534,0.026928259,-0.01830889,-0.013332525,0.01113737,-0.028172234,-0.04211392,0.038134392,0.008170393,-0.0019649295,-0.0021533538,-0.015615097,0.03715273,-0.07404866,0.026345331,0.027053162,-0.0103950715,-0.010379613,0.01142355,0.04201101,0.016158048,0.0032359394,0.0051045036,-0.08533491,-0.016216135,0.01755373,0.037980907,-0.013595591,-0.011804061,0.014737857,0.013910744,0.0017709701,-0.0014323548,-0.0099557135,-0.061918937,-0.03499627,0.017452922,0.03419971,-0.005971018,-0.012934874,-0.03589443,-0.056448344,0.040954806,0.01189212,-0.012140317,0.0029239808,0.04582019,-0.018504582,0.0010148176,0.007992842,-0.020338904,-0.00057266373,0.01419736,-0.022202691,0.024471182,-0.0047688927,-0.015945453,-0.001319592,-0.0004144271,-0.020826649,-0.02122115,-0.009065083,0.009961357,0.026947564,0.005761857,0.016008656,-0.010263227,-0.016692484,-0.009426757,-0.023408469,0.020954221,-0.006834443,0.00015946043,-0.022398654,0.00811421,-0.015216082,-0.058738913,0.0019529116,-0.008094718,-0.028743854,-0.003629762,0.01607263,-0.006443377,0.010046084,0.010847033,0.022551531,0.01890359,-0.0033641905,0.005947516,0.022039598,0.0018877899,0.011570262,-0.04431249,-0.045278054,0.027297659,-0.030513378,0.016191365,-0.0068402844,0.011648631,-0.009792368,0.011688657,-0.027694903,-0.015376687,0.0036972517,-0.06667251,0.00279258,0.014512869,-0.010571717,0.019950487,-0.00510253,0.0051548383,-0.029431343,0.009242776,0.0029232616,0.0051241084,0.036594886,0.033472266,-0.011438357,-0.013577757,0.020002194,6.408319e-05,0.006390332,-0.0047882586,0.01786309,0.02623147,-0.004530237,0.010013077,0.037040416,0.0015922404,-0.04090499,0.009565517,0.034942735,0.015371917,-0.041636758,0.015157398,0.0035107376,0.0010699562,0.0049742297,0.041617032,-0.0122791175,-0.046042874,-0.018129803,-0.025809463,-0.031154087,0.009840487,-0.013892415,0.013241036,-0.0122767305,-0.031039575,-0.013409763,-0.015825685,-0.028761823,-0.0010036855,-0.015593802,-0.033522908,0.040459964,-0.006285017,0.0052159233,-0.008955376,0.02954908,0.009136782,0.035330363,-0.011976576,0.00085887464,-0.00058417296,0.006988886,0.020804772,0.013841128,0.0065909354,-0.031734783,0.003101882,-0.004596761,0.03152351,-0.01682704,-0.02279396,-0.0036088282,-0.022577504,0.0059727123,-0.0055290004,-0.002467074,0.052295998,0.009077599,-0.010048351,0.027179865,-0.013771848,-0.034916595,-0.0028860553,-0.03409196,0.0013420157,-0.028017856,-0.0139933545,-0.11455335,0.014553773,-0.009769525,0.018900985,-0.00018000547,0.02674599,-0.0026077859,-0.014292218,0.018549707,0.007176582,-0.00524445,0.0067480793,0.020493506,-0.04315773,0.0012774172,0.00016863417,-0.037763555,0.026371205,-0.024641762,0.022864973,-0.00026404535,-0.013460384,-0.030186862,-0.0006765427,0.010698746,-0.0151727395,-0.016492585,-0.016962964,0.0050722617,0.016741289,-0.005336824,0.024588695,0.007054809,0.028952006,0.032294203,-0.06015683,0.03386316,-0.010625896,-0.008354121,-0.03576819,0.0022805019,0.03007562,0.0017865172,0.006932809,-0.03219113,-0.009874747,-0.05370649,-0.0093274005,-0.033166938,-0.023110712,-0.0106076915,0.02953865,-0.013281908,0.012887829,0.009703634,-0.0223772,0.049679916,0.014743398,0.009506544,-0.035854444,-0.012600612,0.004360275,-0.0060984665,0.020735657,0.013778773,-0.016842883,0.010328998,0.014963391,0.004014235,-0.040971603,0.012713414,0.005264425,0.014449455,0.006594772,-0.010362017,-0.008873379,0.001828986,0.010795534,0.0047058337,0.009414113,-0.025088096,0.0072225197,0.04566632,-0.0134786675,-0.01717815,-0.015313934,-0.007852127,0.03187852,0.0019300855,-0.0022634403,0.030619174,-0.00076825224,0.020829678,0.027920464,0.027656483,0.0140968,0.016732715,-0.039085284,0.021018768,0.016044175,-0.006376451,-0.02166852,0.028667368,0.037805025,-0.046582635,0.012060221,0.017108422,0.027381478,-0.0077456515,-0.013147688,-0.03350115,-0.014701538,0.028656946,0.008880647,0.025945505,0.016721597,-0.034841914,-0.002273065,0.005764383,-0.007783028,0.0013623552,0.017819587,0.021723904,0.0408222,0.0071258824,-0.010116982,0.012574956,-0.03361452,-0.00546187,-0.035441924,-0.039337095,0.026577402,-0.008789138,-0.0038227334,-0.0045391712,-0.008881999,-0.028659098,-3.4475514e-05,0.0044263904,-0.020026846,0.039916992,-0.0142340455,0.007635163,-0.010122548,-0.017784249,0.06864606,-0.009090166,-0.01620657,-0.021489706,0.014632963,0.015785113,0.014206889,-0.019216552,-0.0127429785,0.03449181,0.006859396,-0.034090742,0.007956646,-0.018511686,-0.031121837,-0.0073925373,0.020612573,-0.008365267,-0.014917866,-0.0021894078,-0.007577364,-0.024495602,-0.023563232,0.029264096,-0.009388129,0.0011837069,0.021435851,0.57144135,-0.0037046075,-0.001773113,0.021439508,-0.014951692,-0.023537952,0.007987271,0.04831866,0.013583593,-0.0124364905,0.0082983915,0.05114194,0.0010810867,0.040192854,-0.009587394,-0.012170911,-0.02550272,0.012826917,-0.04221689,-0.031700388,-0.038051162,-0.0004655864,-0.03210607,-0.0006102221,0.03724679,0.024805026,0.024591077,-0.007270306,-0.002306738,0.047509495,-0.014774238,-0.004008489,0.018793672,0.0333338,-0.0041552726,-0.018083137,-0.013136035,0.02026712,0.023770852,-0.018911786,-0.027863882,0.0063823643,-0.011584553,0.020738417,-0.0016652654,0.067888916,0.029373188,-0.007804247,0.01616861,0.01121884,-0.049425237,0.012759104,0.023202403,0.016641427,-0.015898827,-0.01252207,-0.015623935,0.008582678,-0.00021590645,-0.007648496,-0.031614356,-0.010606006,-0.03037187,-0.020407232,0.029394396,0.0069672726,0.001531273,0.014256806,0.0040911012,0.0067095845,-0.01460142,0.019386606,0.010528491,0.004631454,0.004955289,-0.02191551,-0.023333808,0.02373839,-0.028561832,-0.01531578,0.010180001,-0.00722261,-0.009954034,-0.032802477,-0.0020075992,-0.00926649,0.025394544,-0.0018348336,0.017538806,0.024107987,-0.003267733,-0.03632002,-0.0053524547,0.06220804,-0.0022804793,-0.018087465,0.0023589076,-0.040393636,-0.02822909,-0.018264702,0.0007215282,0.010310542,-0.0064700684,-0.012421958,0.011986024,0.012995388,0.010285612,-0.012728171,-0.050489485,-0.038417924,0.011247069,0.0034419291,0.010115315,-0.008901029,-0.025021082,-0.00806476,-0.003912413,0.024752563,0.004432443,0.019115897,0.0034502454,-0.058956992,0.005726109,0.06468911,0.015542277,0.045765214,0.024322657,-0.03229392,-0.0058277035,0.0029522167,0.0025867443,-0.052941095,0.004423396,-0.0023595837,-0.03322513,0.006161832,0.009435967,0.03544027,-0.0077349064,-0.01067597,0.002711163,-0.05410377,8.08606e-05,0.0153931575,0.0016065796,0.02697184,0.01550072,0.00765706,-0.029126698,-0.009670663,-0.020443369,0.005098447,-0.017370118,0.0008378353,-0.022455212,-0.010795827,-0.016161384,-0.0016622384,-0.024454175,-0.044175886,-0.039290726,0.008705461,0.06391128,0.040215548,0.02761868,0.009053639,-0.028968822,0.028128045,-0.003375794,0.003947923,0.031200934,-0.06509073,0.013665974,-0.020265613,-0.015686924,-0.043482274,-0.020814456,0.012766796,0.015871856,0.0036653874,0.032306027,0.028730683,0.029821305,-0.012606251,0.016982393,0.008684355,0.011789271,0.027443055,0.018474393,0.031362277,0.010853364,0.0044677523,0.027239934,0.018246273,-0.01210991,0.011967698,-0.010611614,-0.010974056,-0.029387115,0.01715941,0.0019640236,-0.008255181,-0.04020242,-0.0074104,-0.0150027275,0.024445185,-0.0030437366,-0.006090408,-0.017326897,-0.0039222953,-0.012711753,0.017433658,-0.045412827,0.018128024,-0.029545136,-0.010056471,0.0016170235,-0.06369609,-0.0047071553,0.015662374,0.021821082,0.028972296,0.013798789,-0.008147158,-0.026444456,-0.0051401877,0.0069444994,0.009159432,0.03477793,0.010061883,-0.026974952,0.0018418212,-0.027717479,-0.0414512,-0.039439455,-0.034816936,-0.029772568,0.015421248,-0.022874575,0.015967619,0.041933674,-0.023928823,-0.009823949,-0.03581972,0.0024727695,-0.00070218724,-0.022126721,-0.011641544,0.051030524,-0.0067454507,0.02464649,-0.031181823,0.011941172,0.01865798,0.0227618,-0.020178411,0.026008785,-0.01254727,-0.05196905,0.009817773,0.026001647,-0.024467824,-0.045146246,0.03524986,0.032573443,0.023551475,-0.005872927,0.00051757006,0.04396492,0.0011700821,-0.002264675,0.0018914959,0.005025549,6.283882e-06,0.008800582,0.025943873,0.035621837,0.012905208,0.074440226,0.0069493204,0.01927294,-0.01680089,-0.014648021,-0.020952731,-0.0075518047,0.0065574157,0.027262304,-0.0042370157,0.011130277,-0.015295326,0.09357574,-0.03247258,-0.013211409,0.032024637,-0.005070768,-0.016264796,0.003244228,-0.028404089,-0.003958001,-0.017947234,-0.00014456721,0.007935018,0.0046610995,-0.002032552,-0.011023005,0.039254826,0.041368302,0.0017380503,0.012548076,0.01972549,-0.020155054,0.016230797,-0.0627952,0.019442117,-0.0011268728,-0.024063507,0.017257866,-0.0064519406,0.00055164465,0.0019923886,-0.01294924,-0.020687189,0.00090918806,-0.004862219,0.47812584,-0.054451454,0.00702057,0.0053896084,-0.012131436,0.013340367,0.0021163258,0.022390652,0.017757623,-0.009057089,-0.044811495,-0.0037117407,-0.015191798,0.031603582,0.012217852,-0.03098824,0.018898344,-0.0020048036,-0.004285091,0.009732921,-0.0054604798,0.008219087,-0.010395033,-0.030771334,-0.020701116,-0.02800187,0.046767753,0.008777982,0.01642557,0.051535062,0.020504575,-0.0027047421,-0.0060711633,-0.024570538,0.02776279,-0.051648192,-0.024737744]', FALSE), -('7ad73e21-f953-495f-b514-4ecb803d17f5', '92d409ac-611d-4dc9-bcdb-c72c2c9a8235', 0, '# Uptime Kuma (AIO) +('ddad1f28-b66e-4b28-b082-184196b3a435', 'aeee2625-fbda-4663-9225-0d1e1938297f', -1, 'The GPU box pbx-node3 serves Qwen 3.8 using llama.cpp. The working launch line uses a 30-billion parameter model with 32,768 context tokens and 99 global layers. KV cache and prompt cache are stored in q8_0 format and persisted to /srv/llamacpp/cache. First-token latency is under 400 ms with sustained throughput around 28 tok/s. It is important not to add the -ctk f16 option as it doubles VRAM usage without quality gain. +Source: homelab/containers/qwen38-llamacpp.md', '[0.022350531,0.0029020738,-0.0031621528,-0.00755808,-0.004762228,0.009424084,0.0446587,-0.01738915,0.030924456,-0.0075244205,-0.037614215,-0.037986383,0.009822545,0.022848034,0.011100047,0.0068959673,0.011529276,0.048092067,0.0012575457,0.00086992915,0.025933333,0.025555246,0.0064920485,0.00151605,0.007132923,-0.017501978,-0.01675356,-0.039015647,0.021904763,0.031302657,-0.010996464,0.016538832,-0.018094646,0.05554437,-0.002594678,0.02320982,0.045414228,-0.006158099,-0.020221107,-0.12191887,0.023340028,0.015777247,-0.0027425224,-0.0073030987,-0.0018110905,0.020775039,-0.016520988,-0.012389816,-0.009604215,-0.026103487,-0.034711253,0.050266538,0.0026453105,0.0009897582,-0.007103881,-0.017302848,0.034701936,-0.06114776,0.021657111,0.032871407,-0.009972673,-0.009238429,0.01809874,0.009817728,0.003971012,0.010693747,0.014293113,-0.051914774,-0.006799252,0.012483197,0.036947712,-0.013948752,-0.0023719503,0.029632648,0.00081681175,0.013949221,0.0012427205,0.0052804053,-0.06368722,-0.022230575,0.0072175446,0.028709032,0.0114135975,-0.020227376,-0.031213371,-0.05514486,0.028657408,0.022749776,-0.016310254,-0.012105889,0.032028858,-0.019315455,0.009228648,0.00053543557,-0.028558152,-0.0029794206,0.009023706,-0.01583384,-0.007087002,5.7561683e-06,-0.020158943,0.005409049,-0.007590806,-0.032695223,-0.030280996,-0.012917387,0.02799745,0.034712806,0.008617215,0.012805168,-0.013840079,-0.009396646,-0.01081509,-0.014674379,0.01888144,-0.01914875,-0.010771767,-0.008621612,-0.0013497935,-0.02097653,-0.05553135,0.0021088594,-0.020945976,-0.01706968,-0.0067146616,0.02804324,-0.0024340758,0.014306581,0.021190943,0.01711074,0.029384626,0.0035477614,0.005267279,0.01630447,-0.020375587,0.02474735,-0.051293448,-0.024069699,0.0526117,-0.03110927,0.01191271,-0.038646635,0.013769423,-0.020496082,0.011473262,-0.031639233,-0.014388696,0.009234907,-0.037138008,-0.0061936,-0.0044034226,-0.021611145,0.0028240022,0.011469471,0.007416513,-0.017334811,0.030230472,-0.0068191625,0.005032843,0.029474467,0.026051616,0.0002651628,-0.0067776674,0.030863356,-0.00025359372,0.0034617104,-0.003686365,0.0343688,0.018864835,-0.02690621,0.009530178,0.019770563,0.009312975,-0.045388393,0.023013266,0.025145985,0.013492096,-0.040151585,0.013783509,0.009038441,-0.0015376766,-0.0059561394,0.023552958,-0.003733994,-0.05027763,-0.004904229,-0.011796739,-0.031470858,-0.000106582214,0.0033024496,-0.0037506183,-0.015444254,-0.031186245,-0.005739583,-0.009591199,-0.025686022,0.020580493,-0.008930419,-0.037557106,0.038413193,0.0012977951,0.0009662711,-0.00013211978,0.0062038316,0.0048678517,0.050264962,-0.024572907,-0.0111064175,0.006500986,-0.0068927784,0.046271723,0.04445573,0.03113462,-0.020850215,-0.008197917,-0.0056538475,0.0357771,-0.013639546,-0.028620046,-0.016704455,-0.010459688,0.016061597,-0.013833835,-0.016965883,0.05641704,0.036412906,-0.0007531007,0.03055788,0.016789189,-0.040709063,-0.005003772,-0.05654944,-0.0017171672,-0.025558583,-0.0035945633,-0.09067911,-0.0013679138,0.002418507,0.012770584,0.004496593,0.03438921,-0.0036952505,-0.011231623,0.020731706,-0.011696006,-0.0027912185,-0.024656672,0.0072509283,-0.033253632,0.009119464,-0.013845786,-0.05007802,0.0062614954,-0.023277,0.023473108,-0.0026487845,-0.025491973,-0.044425122,8.057167e-05,-0.00385577,-0.013940247,-0.015803872,-0.024431204,-0.0008866522,0.018112218,-0.014633304,0.027645681,0.0071010636,0.010922413,0.027476816,-0.061917923,0.04447568,-0.0089801345,-0.007836178,-0.023057263,0.018900193,0.029110506,0.0015939316,0.0021324523,-0.020946171,-0.018579222,-0.04161934,-0.009139239,-0.04383336,-0.018476283,-0.001027839,0.021824205,0.008807199,-0.002379407,0.025657117,-0.047230937,0.04710777,0.013266403,0.022774078,-0.02947133,-0.0052568167,-0.010565787,-0.0020208023,0.036505044,0.031911466,-0.0001467072,-0.00975582,0.009611313,0.00028766238,-0.013666486,0.002319249,0.010573167,0.010227316,-0.011318767,-0.0030573322,-0.03490459,0.002701816,0.007539244,0.01047166,-0.011358688,-0.029530415,-0.013303426,0.05480227,-0.002806253,0.0064588813,-0.012578134,-0.0019681447,0.03826009,-0.0032821747,-0.019568717,0.008188945,-0.0057944194,0.042998053,0.02351988,0.014864729,0.025033925,0.015150581,-0.014401918,-0.0011997682,0.0027131739,-0.0052162562,-0.03522965,0.03822389,0.03966196,-0.052080743,-0.005819528,0.020126136,0.026734354,0.013760718,-0.020641731,-0.027365247,0.0005471416,0.010573361,0.005968698,0.028634213,0.017879048,-0.03148623,-0.008961858,-0.021254458,-0.017660908,0.0078460025,0.027905261,0.027026683,0.006501709,0.017859489,-0.036767483,0.027190177,-0.032624297,-0.011950872,-0.026538644,-0.026254361,0.0006627554,-0.0049855174,-0.00701358,-0.01171866,-0.016268656,-0.02120991,0.017048536,0.0013116396,-0.004625698,0.029293386,-0.030704128,0.011803514,-0.01429012,-0.07785075,0.077648304,-0.010649755,-0.008909245,-0.008405174,0.0003363854,0.0082864445,0.026052,-0.03092753,-0.0063834195,0.027755085,0.004131926,-0.028542537,0.0014789779,-0.024205167,-0.024624612,-0.0107181035,0.014759637,-0.011390651,-0.019507421,0.0017776069,0.0013914343,-0.03789877,-0.043867316,0.02676106,0.0045777056,0.009830675,0.033784375,0.56166315,-0.00065695465,8.663707e-05,0.027848683,-0.0207989,-0.026660265,0.012862243,0.04400293,-0.0016168938,-0.016610984,-8.903981e-05,0.034866393,0.013609883,0.025921052,-0.023300834,-0.0134940175,-0.029564807,0.0117775295,-0.05179316,-0.027988195,-0.046265613,-0.0077399965,-0.025803905,-0.0056999475,0.02993904,0.015934711,0.022630118,0.0044514597,-0.010478626,0.047314666,-0.01029348,-0.0047312244,0.01601875,0.023136469,0.0032148662,-0.025104526,-0.0179759,0.019795664,0.03925551,-0.024160428,-0.059876688,0.010317512,-0.03266012,0.017343136,2.0119642e-05,0.048781894,0.036474906,-0.016032914,0.010827291,0.018380914,-0.030232593,0.0037013825,0.011229151,0.012268143,0.00088080345,-0.0086622555,-0.010786894,-0.0031067221,-0.002614952,-0.007939261,-0.035556834,-0.011802603,-0.04213622,-0.017944327,0.02888655,0.0034207383,-0.020022826,0.010058202,0.0032706612,-0.0016213548,-0.01148756,0.0110026905,0.031346172,-0.010426987,0.001980838,-0.030166771,-0.011931882,0.013312799,-0.018050956,-0.016456375,-0.0019506916,-0.004609516,-0.03149513,-0.034123063,-0.012475459,-0.00978868,0.009468898,-0.003947473,0.011136641,0.02324948,-0.002813674,-0.04859454,-0.0133017795,0.06872839,-0.01253076,-0.034557216,0.007464955,-0.03417124,-0.025393605,-0.017510591,0.013667067,0.0042673233,-0.0014312512,-0.0056785177,0.024771115,0.038557734,0.0034403377,-0.018250577,-0.052441865,-0.030785367,-0.005384268,-0.006805428,0.03073903,0.0015951735,-0.024281709,0.007271519,-0.0018345685,0.022249117,0.0018642053,0.012046571,0.01091216,-0.053964566,0.025428453,0.07925477,0.0041131773,0.050735787,0.025868753,-0.06732489,0.0065018698,-0.000319712,0.0063841976,-0.058613814,-0.0034146914,0.00042778466,-0.035203904,-0.001985698,0.006870635,0.011878905,-0.0027071,-0.02661635,-0.002581594,-0.06333794,0.01859078,0.013957103,-0.0025194248,0.030802323,0.0371451,0.01726249,-0.02670542,-0.010813004,-0.023025943,-0.007132077,-0.015274359,-0.0013689076,-0.026878651,-0.0051130545,-0.009818183,0.018139482,-0.02109197,-0.022973295,-0.037017744,0.009398206,0.057553686,-0.0046024257,0.033287983,0.012688383,-0.015465159,0.03231399,-0.024069356,0.013336451,0.028521942,-0.06562627,0.01065429,-0.02476421,0.00216325,-0.02696717,-0.022951182,0.01774428,-0.00036883782,0.0122727435,0.043451097,0.017172372,0.012321784,-0.0043899524,0.017049229,0.00074177206,0.014040629,0.051633354,-0.017246787,0.03034583,0.025336413,-0.010320837,0.03041271,0.030544916,-0.00779129,0.021966934,-0.007870213,5.939765e-05,-0.02189779,0.0050055003,0.017996203,-0.014052815,-0.08742553,0.010919318,-0.03636979,0.02787986,0.004834777,0.00068236026,-0.021666795,0.0002457757,-0.0010239828,0.007496919,-0.056052547,0.012621685,-0.04711028,0.0037794036,0.0070506386,-0.05495796,-0.016235493,0.009376987,0.013199445,0.028587764,0.016311828,0.010376365,-0.035090942,0.0014332714,0.0044833766,0.0067119133,0.016411765,0.008243299,-0.011627615,-0.016514739,-0.022089278,-0.021836957,-0.032173913,-0.023926765,-0.02973212,-0.006765224,-0.041308973,0.020360842,0.045934867,-0.022876352,0.0059844856,-0.037297126,-0.014267781,-0.016195063,-0.02149758,-0.00051819533,0.033645518,-0.01568027,0.037317973,-0.011491358,0.025634605,-0.007742767,0.009268794,-0.038674813,0.056549847,-0.012687303,-0.056379933,-0.0016463347,0.033144057,-0.040919393,-0.030534629,0.03575202,0.034257077,0.029442793,0.011304186,-0.009712952,0.04651478,-0.010809965,-0.0008189292,-0.0021175558,-0.0020635868,-0.0034275735,0.004407919,0.029630521,0.049284555,-0.002993523,0.07280203,0.013088011,0.0035748987,-0.034056406,-0.017304072,-0.01772485,0.0014997219,0.013052199,0.018899413,-0.009904378,0.020735072,-0.021882918,0.07389297,-0.022017932,0.0041326447,0.038378775,-0.0065605626,-0.018661084,0.031942,-0.024292646,0.017630152,-0.012479756,0.012157291,0.015567441,0.0022899923,-0.0027889386,-0.007663302,0.007729086,0.05932244,0.025695281,0.022068324,0.030252764,-0.02957226,-0.013037414,-0.056980778,0.009825735,-0.008300618,-0.030724257,0.011693969,-0.008980125,-0.004954225,-0.002250005,-0.017152525,-0.019549621,0.012831335,0.0045418395,0.4497982,-0.04373422,0.00016759167,-0.0016176478,-0.020471126,0.026756221,0.0009790546,0.017944707,0.025340954,0.004378023,-0.04244223,-0.005677558,-0.012207678,0.014129846,0.0070797554,-0.04618747,0.010775391,0.011647603,0.01315958,0.00029873263,-0.0043864814,-0.00055804243,0.002298617,-0.021124996,-0.024876317,-0.024889044,0.039005425,-0.0059408676,0.024206731,0.030847033,0.018887911,-0.011485597,-0.011115032,-0.023905734,0.04707956,-0.0629516,-0.024265323]', TRUE), +('c5164a0c-0197-4172-831f-959825c323d6', '335f9cdf-414a-4489-9550-2b2d251c7569', 0, '# Uptime Kuma (AIO) Uptime Kuma runs on `rack7-pbx2` as the AIO image, listening on **18443** with TLS terminated by `caddy-rack7`. @@ -209,7 +246,9 @@ Uptime Kuma runs on `rack7-pbx2` as the AIO image, listening on `/opt/kuma/backup/`. - Lab-iot monitors watch every VLAN 130 device; the shelf watchdog restarts the container after 3 missed checks.', '[0.038159218,0.018766368,-0.008253423,-0.013605577,0.027275689,-0.03881082,0.03136027,0.024545735,0.0022254444,-0.021746263,-0.038330168,0.024475353,-0.0024082318,0.010989825,0.029897645,0.017008957,-0.0034538836,0.0040506534,0.009018497,-0.051237334,0.025878752,-0.0039471523,-0.012335345,-0.023936685,-0.00020952584,0.03227632,0.014291341,-0.015075375,-0.0052616233,-0.004106364,-0.011243411,0.0005395321,-0.032830995,-0.016840471,-0.0071959007,0.02587844,0.032565247,-0.012554153,-0.0585903,-0.13028347,0.028154796,-0.0003125621,0.008201726,0.00013462148,0.0018716869,0.023011925,-0.04613856,0.012324085,-0.026596045,-0.0018444498,-0.028290417,-0.0019582724,0.011061781,-0.005081163,-0.0014394689,-0.0038169506,-0.023758896,-0.035982393,0.015444764,-0.017953694,0.0025930842,0.04690821,0.019655857,0.015997222,-0.01035222,-0.0085015455,0.009612204,0.012081764,-0.017245257,0.012259898,0.021012094,-0.028193142,0.0009625798,-0.013342246,-0.012423026,-0.017247362,0.005413176,-0.011198339,-0.025112718,-0.01717486,0.047352664,0.014006981,0.030235115,-0.03381662,0.0010321399,-0.008599289,0.0030500966,0.00958591,0.034073126,0.0138813965,0.010177249,0.015034586,-0.009406735,-0.012959303,-0.0067514917,0.024943145,0.025212131,-0.03840047,0.028081615,-0.035782777,0.014841646,0.0066556744,0.01711066,-0.00782687,-0.003944481,0.020587487,0.012070259,-0.023496177,-0.0052147857,0.007391373,0.0337459,0.0007494017,-0.014253776,-0.024298122,0.022561314,-0.0032339823,-0.024170933,-0.026598321,0.005943223,0.0070259427,0.020536521,0.053431496,-0.005016835,-0.00957662,-0.0039195227,0.0027632192,-0.040144775,-0.074567825,-0.0016065622,0.028698375,0.046559416,0.012744336,0.0065780594,0.029551797,0.047300253,0.0377461,-0.0057376083,-0.029495241,0.017846648,-0.07933009,0.018881338,-0.056741882,-0.007917166,-0.002108944,-0.0125145605,-0.035038862,-0.0022280358,0.011857434,0.019379286,0.021743948,-0.030438954,-0.016549902,0.0072811977,0.018260848,-0.030772012,0.059968855,0.01623587,-0.00956995,-0.029745804,0.058739465,-0.01780903,0.016551126,0.03483684,0.024088737,-0.007910434,-0.039591182,0.010726799,-0.013124529,0.023936164,-0.029122753,-0.014645679,0.049172733,0.022598179,-0.015064444,0.03045499,0.022768918,0.055990722,-0.0023959544,0.009487157,-0.008319769,-0.009828278,-0.011313003,0.03430367,-0.0066451104,-0.03358979,-0.01901464,-0.028350182,-0.022544382,0.013778697,0.00096817577,-0.011669359,-0.00064084574,0.022067044,-0.014250984,-0.033291034,0.013435835,-0.007895832,-0.0435121,0.050338335,-0.020124057,0.0013673076,-0.006123357,-0.030915188,-0.006217783,0.00184315,0.029571505,0.00017984724,0.011484679,-0.015428686,-0.012450903,-0.0011139292,0.07086261,0.032490503,-0.033374745,-0.01611274,-0.01817711,0.0074659516,-0.008527701,0.011954,0.014015908,0.010896786,0.0096697835,0.0010633719,-0.0078439,0.0018644283,0.02450314,-0.027504912,0.020896038,-0.014261642,-0.019902783,-0.014018837,0.02268212,0.0019078444,0.024644947,0.022649344,-0.039609555,0.0022322934,0.010866106,0.021020789,0.031339727,-0.0019409998,0.00804027,0.023475386,0.013793229,-0.0041860244,0.00068617373,0.030877337,-0.022798508,-0.046333246,-0.0006098806,0.0018179342,-0.013253388,-0.023813667,-0.023507144,0.040883727,-0.004960361,-0.03429914,0.006985133,-0.01690206,0.001079718,-0.019873507,-0.022333086,-0.01843556,0.0045994134,0.0019346426,0.022578984,0.019646727,0.018698283,0.020227343,0.006465924,-0.009357026,0.0285092,0.026439387,-0.018821731,0.01785992,-0.023379024,0.0328339,0.00906192,0.036875874,0.0122806905,0.03713214,-0.014105316,-0.021576937,-0.029937448,-0.03130318,0.015720842,0.010916684,-0.026985252,0.0049028583,-0.009101857,0.014156448,0.012237011,-0.010061466,0.027317133,0.004841282,0.0022600654,-0.0064523914,-0.0035552862,0.025742443,-0.0140306605,-0.01978169,0.040567867,-0.0069704484,-0.014565992,-0.018590657,0.001493883,-0.014525455,-0.02108338,0.0007337922,-0.035207093,0.0041469363,0.026874207,-0.007607632,-0.017545627,-0.020728907,-0.006609046,0.050973892,0.02318893,-0.011619542,-0.013409942,-0.0102318665,0.01427026,0.037187044,0.0062105893,0.006819699,0.025218781,0.02363647,0.005797882,-0.006668888,0.024162691,0.033319,-0.0676369,-0.0028128661,0.023299167,-8.887338e-05,-0.00095401943,-0.019578375,0.020993676,0.018148687,-0.009766716,0.021126654,0.016780678,-0.0004846624,0.04359566,-0.0046644607,-0.0059245317,0.0003452194,-0.005103796,0.00643287,0.0074717375,0.043332934,-0.018292936,0.013660074,0.041535508,-0.008128472,0.04040573,-0.0012612409,-0.030853568,-0.023786614,0.024411676,-0.0899016,0.008203847,-0.08212391,-0.011497224,-0.02182007,-0.00018818058,0.022151763,-0.012590545,-0.034498673,0.012822626,0.009033786,-0.017400881,0.024277946,-0.01436378,-0.008470993,-0.0031400735,-0.0047247075,0.014101902,-0.027472546,0.028913107,0.04147831,-0.008445014,-0.010610114,-0.019504977,0.035980787,-0.0035986442,-0.020342525,-0.03446169,-0.030969726,0.008832018,0.017437147,0.015452877,0.002460168,-0.016922656,0.0155213745,-0.016501343,0.0034446577,0.007888586,0.008754243,-0.005791031,-0.0035745217,0.0053625815,-0.009805715,-0.025621004,0.015334344,-0.025696596,0.032231282,0.5818506,-0.0009127479,0.008781549,-0.0404033,0.017690152,-0.03339413,0.008147407,0.0012115368,0.017358596,0.0008687974,-0.0037383474,0.015784938,-0.0138791185,0.0010421259,-0.0034051072,-0.035542812,-0.014621247,0.045631014,-0.029905302,0.019948402,-0.059740875,-0.017703611,-0.05745828,-0.011849292,0.004311079,0.024263525,0.009006841,-0.0063104033,0.0061325505,0.035345193,-0.016127631,-0.01515801,-0.016806554,0.016084135,-0.038372286,-0.006293392,-0.012006788,-0.013065379,-0.031241326,0.01876682,-0.0315777,-0.0051238528,0.047522318,-0.01842231,0.027752813,-0.015727554,0.030570626,-0.015821248,0.005933215,-0.031067876,-0.003984921,-0.01745568,0.0031388653,-0.012759245,-0.00030708004,-0.0036189852,0.021884779,-0.00013428024,-0.015181302,-0.01565875,-0.012563986,-0.02003281,-0.07091831,-0.0012740447,0.019138146,0.021306822,-0.008940225,0.0019754616,0.040073052,0.0093680555,0.0039536282,-0.008149495,0.008599966,-0.0117114,-0.0020341433,0.038233045,0.012339041,-0.016833153,0.0018601435,-0.012175559,-0.016105922,0.0091493605,0.011136175,-0.015943803,-0.002836913,-0.0075827283,0.0026337034,-0.004601471,-0.014707327,0.013346738,0.0034896184,0.005205931,-0.0068176095,0.035144124,-0.007903046,-0.015688565,0.037423838,0.024715263,-0.035532825,0.01410341,0.0029356685,-0.037915163,-0.0033772755,-0.02866371,0.023256207,-0.028989233,-0.02078311,0.019434474,-0.043115407,0.009578664,-0.035096914,-0.013211956,0.008042774,-0.031923704,-0.014153227,0.02271921,-0.014104999,0.0071672644,0.031510893,-0.015708072,-0.04625553,-0.01351024,0.03669114,0.04701573,0.018969871,0.029682387,0.03239575,-0.040427703,-0.0015362906,-0.03628466,0.019091822,-0.02329012,-0.03654893,0.019721663,0.010070151,0.0031683268,0.0022381637,0.03578615,-0.02659168,0.0074246246,-0.033442996,-0.029372027,-0.008211009,0.002408334,-0.0016395809,0.009154189,-0.007356419,-0.008628813,-0.027466912,0.03875262,-0.007274545,0.024309803,0.0063317628,-0.02322456,0.004003449,0.024640443,-0.013712703,0.0026409258,-0.01528814,-0.065920636,-0.019163916,0.016477428,0.033001892,0.003998611,0.00489135,-0.00751594,0.004881554,0.03977872,-0.0034592662,0.028938282,0.025962649,-0.041032985,-0.0043044803,-0.008900665,-0.009092332,-0.047250513,0.012029938,-0.008913916,0.034373417,-0.051826827,-0.0075965645,-0.0071530845,-0.020212775,-0.028912742,0.0026825275,-0.024025965,0.031196509,0.02883199,-0.008675958,-0.034472287,-0.0036358535,0.0044447174,0.045623288,0.01461601,-0.003237491,0.022890441,0.01849239,-0.029219927,-0.03160422,0.033338543,-0.033579364,0.033091873,-0.084296696,-0.021352416,-0.05392992,0.015070809,0.029287986,0.012988411,-0.009287086,-0.015413265,0.004192497,-0.015984682,-0.03868364,-0.0068655536,0.010168921,0.03190114,-0.0019890002,0.0036486916,-0.028452745,0.025604306,-0.034126267,0.018712895,-0.024226358,-0.014337878,-0.016651558,-0.016441636,-0.020924974,-0.002076056,0.009606195,-0.031834144,0.0020748174,-0.0024409718,0.0014975271,-0.028112877,0.00080603926,0.0073278113,-0.0017216707,0.0043397285,0.024062831,0.024707992,-0.01960756,-0.011737399,-0.06974354,0.006114015,0.041986737,-0.0027097801,-0.016997699,-0.0020113802,0.015896361,-0.026251728,-0.041235384,-0.010523438,-0.0011578123,0.019168964,-0.017656313,-0.012977178,0.07017156,-5.5585893e-05,-0.038561232,-0.023473632,0.005338477,-0.032689944,-0.023772737,0.024979025,0.014612079,-0.004116939,0.018034302,0.019188818,-0.01396384,0.034114454,-0.026111113,-0.010152364,0.009369804,0.009984667,-0.008741782,0.012626517,0.06503526,0.030034266,0.041482814,-0.0097587155,0.0006775198,-0.023332952,-0.038117766,0.017031511,0.017152391,-0.0032437397,0.0032535405,-0.013523022,0.026786799,-0.0076928586,0.09178384,-0.005431776,-0.028048282,0.03222108,0.030563857,-0.007752654,0.0041815205,0.027604029,-0.0030938408,0.0050458307,-0.012640894,0.008764827,-0.007446982,-0.006671126,-0.005744553,0.050886635,0.013522733,-0.01915811,0.014717424,0.012068527,-0.009827528,0.0042127017,-0.012827268,-0.007734152,-0.013348408,-0.016333815,0.036859572,-0.02662184,-0.0029473912,0.018192112,-0.022294471,0.009441132,0.02264232,-0.01874246,0.4630353,0.008452438,-0.024897946,0.018475221,0.0014431686,0.025222093,-0.011626082,-0.009256397,0.00564271,-0.016621042,0.00854827,0.007835509,0.018367168,0.040664464,0.016625496,-0.036606073,0.017565979,0.011455481,-0.014436409,-0.039349534,0.0048975656,0.004223717,-0.013195844,0.007417254,-0.034899957,0.004117812,-0.022522656,-0.015886012,-0.01620161,-0.012729721,0.022665009,-0.0018260349,0.019693198,-0.012087841,-0.026082406,-0.012064667,0.0025059327]', FALSE), -('ddff1afe-e6c1-4524-9cfb-2a93fc1edab5', 'f0b1915b-fd38-4811-9b44-3c880c60574e', 0, '# Rack7 DNS and DHCP +('91d4871f-f9f8-44a9-81d5-316596de1db3', '335f9cdf-414a-4489-9550-2b2d251c7569', -1, 'Uptime Kuma runs on the rack7-pbx2 AIO image and listens on port 18443. TLS is terminated by caddy-rack7. The health endpoint is at https://uptime.rack7.local/ping. On failure, every monitor posts to the ntfy topic reese-uptime-7. History is kept for 30 days with hourly backups to /opt/kuma/backup/. Lab-iot monitors watch every VLAN 130 device, and the shelf watchdog restarts the container after three missed checks. +Source: homelab/containers/uptime-kuma.md', '[0.020220868,0.011950333,-0.010784615,-0.020636687,0.02022524,-0.03414699,0.026649961,0.015740888,0.0038072104,-0.019052811,-0.02989259,0.025748013,-0.016488196,0.0051215603,0.033103403,0.020477122,-0.0060988264,0.011091633,-0.0023457364,-0.035734404,0.032463647,0.010087783,-0.00678882,-0.003838814,0.0055688582,0.037971184,0.009618698,-0.02340489,-0.020410486,-0.0038687887,-0.00075690844,-0.016907487,-0.03203231,-0.0053794724,-0.012042234,0.022194583,0.023340395,-0.0034102416,-0.055558816,-0.11904075,0.028851466,-0.0131574925,0.012603207,-0.0030208721,0.013714662,0.011129662,-0.046263363,0.017368078,-0.021420961,-0.0013641517,-0.031799182,-0.007489396,0.0043372284,-0.009660171,0.0043225004,-0.0062478767,-0.020894164,-0.040778976,0.012523634,-0.019599186,-0.0005375262,0.059253585,0.015290469,0.009728266,-0.017799817,-0.020772196,0.006117828,0.005506764,-0.015057452,0.017127527,0.026650451,-0.036473498,0.0043668956,-0.023287427,-0.02289479,-0.017197331,-0.0018580179,-0.010705887,-0.0039326786,-0.009403888,0.049497884,0.015057887,0.025041986,-0.05490742,0.002627527,-0.0069922637,0.0008011151,0.009693435,0.035631187,0.019994978,0.01116585,0.0066853575,-0.0033017888,-0.009434832,-0.015675116,0.017634986,0.021417446,-0.03553972,0.035554346,-0.03329669,0.012054306,0.014549119,0.0087895775,-0.016314004,-0.005002651,0.02810359,0.011928253,-0.027744351,0.0001962427,0.020687481,0.028753858,0.009687243,-0.01681053,-0.01737001,0.0106095625,-0.00489152,-0.027181813,-0.023677189,0.010083573,-0.0054108673,0.0210185,0.0422593,-0.0129928915,-0.009941113,-0.00077701954,0.0025499978,-0.033105407,-0.07386573,0.004992766,0.027584294,0.046748567,0.0038978145,0.00058554113,0.03233574,0.019622082,0.027727503,-0.009685457,-0.040458273,0.017311761,-0.08112775,0.011004883,-0.056528892,0.00083577406,-0.00015378081,-0.013078904,-0.03617244,-0.0028547188,0.0065686572,0.021199794,0.021715024,-0.031252626,-0.018183438,0.013436334,0.016740613,-0.023432497,0.068023585,0.012839587,-0.010170547,-0.023308195,0.06452471,0.0011235308,0.02317568,0.03845875,0.023270963,-0.0032605976,-0.04179032,0.022161491,-0.0035472952,0.03487847,-0.029504964,-0.018344946,0.056933142,0.019717785,-0.013927463,0.044262867,0.012233962,0.058313947,-0.0028695315,0.019412559,0.007984376,-0.007752166,0.003092977,0.04662951,-0.01116416,-0.033311367,-0.009259908,-0.026377283,-0.020607308,0.010079671,0.020054843,-0.007971516,-0.005675897,0.024283014,-0.0111926645,-0.031677727,0.0108012585,-0.0056341835,-0.05873832,0.046942644,-0.017173475,0.0018280327,0.0003972594,-0.021667175,0.012723884,0.004801819,0.031184569,0.013858232,0.014908587,-0.009198023,-0.017628789,-0.0039817975,0.08898859,0.037853703,-0.026957478,-0.028084308,-0.025448631,0.007641749,-0.007310828,0.020305084,0.009118113,0.021895517,0.02066927,0.0077993055,0.005221142,0.0038138086,0.03694974,-0.034826007,0.013750162,-0.010594401,-0.014360189,-0.014936086,0.015645614,0.0051389835,0.033967227,0.01944958,-0.044463385,0.0059678606,0.0058164042,0.009065466,0.02944117,-0.0007046001,0.013074325,0.015769733,-0.0036178383,-0.0145266885,0.003444104,0.015889391,-0.038877256,-0.052046154,-0.008824693,0.005661994,-0.0063534845,-0.028524145,-0.027550591,0.03034017,3.284557e-05,-0.026343692,0.0058669187,-0.012348562,-0.0024998777,-0.010457022,-0.020942861,-0.0049515995,-0.005652652,0.005322101,0.020579902,0.020095775,0.018936742,0.013837811,0.010580173,-0.0036542732,0.03644157,0.022785245,-0.0090822615,0.0157179,-0.015015259,0.025841707,0.0025294542,0.033766564,0.010358106,0.037358705,-0.009534292,-0.016019246,-0.043344922,-0.015802149,0.02025677,0.00029095082,-0.014539355,0.0059478395,-0.010852841,0.011966889,0.017691426,-0.005268035,0.026517145,-0.0037817236,0.005285485,-0.0035054516,0.0010080496,0.028808717,-0.017612407,-0.018614672,0.038448106,-0.017531652,-0.01860604,-0.0110509405,0.010319167,-0.0066294055,-0.016053552,-0.0073097884,-0.0134044,0.01175766,0.012128847,-0.0058948547,-0.020541048,-0.023569236,-0.020497162,0.052676592,0.017472213,-0.03173977,-0.007986064,-0.00991972,0.009312116,0.03754547,-0.0070674615,-0.007844117,0.033142105,0.025850454,0.016726226,-0.0050385706,0.011584351,0.029712772,-0.08871469,-0.0056387456,0.023511363,0.008282065,-0.0013207493,-0.012407833,0.025338165,0.01990365,-0.0049352394,0.018711317,0.0012232825,0.00060716027,0.05386165,-0.0070374683,-0.007785926,-0.00065501785,-0.014115945,0.0035159898,0.0059672007,0.026032368,-0.0191639,0.0052752164,0.050428983,-0.01846535,0.041542035,-0.00010977444,-0.031399015,-0.02242292,0.013957368,-0.080579236,0.02015314,-0.06412418,0.0002767364,-0.016666913,-0.0005754044,0.013122894,-0.015001012,-0.045563675,0.018075483,0.008163156,-0.014585975,0.02497144,-0.022047775,0.0015921077,-0.004311408,0.0019614666,0.016193235,-0.035947163,0.021216212,0.034494165,-0.014687108,-0.01122008,-0.017432244,0.03700784,-0.004853148,-0.01491571,-0.047412965,-0.045129787,0.014529039,0.01680046,0.016598472,0.0021182885,-0.024028499,0.02623504,-0.023336466,-0.0053651244,0.0040782504,0.009153798,-0.0076170596,0.00032125835,0.0014828482,-0.010503583,-0.019007098,0.03094075,-0.026418809,0.023355428,0.56775224,0.012384092,0.008585723,-0.039145317,0.019415261,-0.045265242,0.011404087,-0.006036446,0.007882229,-0.00035633222,-0.00020526919,0.017068224,-0.017928144,0.0005548198,0.0010655201,-0.03347161,-0.008795868,0.042002734,-0.044806797,0.0069176853,-0.040482745,-0.018170513,-0.04601679,-0.02149606,0.014632923,0.041623637,-0.002230513,0.0011436756,-0.0033660068,0.038592968,-0.005356925,-0.021041075,-0.007336198,0.012624116,-0.042107046,-0.0061308355,-0.01275285,-0.014087386,-0.024420263,0.0113257365,-0.02792117,0.0013979153,0.052174352,-0.016031973,0.045438472,-0.017619789,0.039491452,-0.021849945,0.011690253,-0.03513069,-0.0012941782,-0.017158102,-0.004734737,-0.013664196,0.005373109,-0.012552135,0.030960204,-0.012982323,-0.017364645,-0.010895432,-0.013240562,-0.02455017,-0.065369785,0.006841481,0.004223796,0.0113556795,-0.013591914,-0.0059212204,0.030632403,0.008597766,0.007854798,-0.0014227381,0.01306773,-0.018932644,-0.0090506,0.03250426,0.022805376,-0.030656178,-0.003129489,-0.00998725,-0.019681834,0.01776436,0.004771423,-0.015840674,-0.0062320684,0.0031835595,0.0073824995,-0.00664901,-0.018878885,0.0140669765,-0.0031711508,-0.007921751,-0.013521125,0.034329098,-0.008334496,-0.024117103,0.055059973,0.02580631,-0.04029495,0.012771647,-0.0002975727,-0.039150715,-0.010951253,-0.030491136,0.032486383,-0.032709435,-0.006335003,0.015377629,-0.029253287,0.0006784954,-0.034913477,-0.008331592,-0.0023082984,-0.028533882,-0.001288714,0.016580086,-0.0134793315,0.016408293,0.02613222,-0.01573661,-0.044039354,-0.011109609,0.02763118,0.046234556,0.009764928,0.022022268,0.03473513,-0.049086727,-0.0031377233,-0.028936503,0.020196404,-0.020756893,-0.038807545,0.017348785,0.00091450824,-0.00020600956,0.007643275,0.04679071,-0.012863406,0.010111092,-0.04356378,-0.022225117,-0.004368238,0.00667405,-0.0047961785,0.0045367763,0.0034770407,-0.0043097623,-0.011247123,0.035158418,0.0023820982,0.01806262,-0.005158486,-0.029290719,0.0011691318,0.029221995,-0.02046964,-0.00292139,-0.016575994,-0.058286477,-0.024387814,0.00025291333,0.023678342,-0.003842399,0.01427855,-0.005418095,-0.0021805388,0.046382245,-0.005037121,0.03375469,0.039551985,-0.032072544,-0.010807261,-0.016134521,-0.001103001,-0.047727954,0.007639517,-0.009617287,0.034178585,-0.040068313,-0.009873528,-0.017425315,-0.027537229,-0.023623634,0.0071543567,-0.033908904,0.026338236,0.03225556,-0.018581929,-0.03412982,-0.005276791,0.012063768,0.040082313,0.013183734,-0.0014904686,0.02850629,0.012601916,-0.028748179,-0.03356972,0.029260349,-0.02423639,0.027943688,-0.09331393,-0.03750376,-0.046871684,0.010807446,0.03545119,0.02226647,-0.0141449785,-0.009390037,0.015829213,-0.008454697,-0.03961678,-0.0125502385,0.021445129,0.019248355,-0.008706933,0.0048890994,-0.029086258,0.033593178,-0.03320751,0.017346341,-0.036095098,-0.0065231114,-0.018749563,-0.01786651,-0.024059936,-0.0061891777,0.0061040865,-0.027953312,-0.0018598546,-0.0111482795,0.0019304059,-0.02598405,-0.007885809,-0.0001868422,0.0020467516,0.0008128566,0.023562897,0.031781886,-0.014296844,-0.014159509,-0.06943104,0.001456242,0.04572277,0.004146854,-0.023993758,0.005912373,0.0033274244,-0.02380445,-0.044344943,-0.0173485,0.010901211,0.021165341,-0.019308561,-0.0198109,0.06403211,-0.0021098864,-0.03600255,-0.03409555,0.005384702,-0.037308272,-0.023776596,0.02936444,0.019146608,-0.008532978,0.027324699,0.01934465,-0.011321844,0.024007855,-0.024261927,-0.018016558,-0.0029696722,0.008968138,-0.0065985564,0.021590328,0.067405134,0.0369106,0.046266433,0.0021980335,0.026475463,-0.019404951,-0.049128924,0.019654837,0.007327125,0.0046750195,0.006825924,-0.009551462,0.020404108,-0.0017051165,0.083177164,-0.0048697707,-0.019748285,0.03715341,0.031989694,-0.013028412,-0.00625373,0.017715365,-0.0065385466,0.016912265,-0.00673758,0.0068090097,-0.010849402,-0.013177927,-0.003769515,0.06320433,0.010059431,-0.013958107,0.012867623,0.028791029,-0.008797006,0.009309323,-0.011531454,-0.011701388,-0.016902637,-0.019953491,0.0415364,-0.03231753,0.0026414471,0.027532304,-0.03556271,0.011195214,0.032052778,-0.01448317,0.45803645,0.004584585,-0.017458292,0.030584114,0.0097863935,0.027739614,-0.016688537,-0.005874047,6.505074e-05,-0.015891373,0.0148198735,0.010605714,0.019383268,0.047266472,0.024410404,-0.04222885,0.023334146,0.0073012724,-0.009183364,-0.04124026,0.013348922,0.008513694,-0.01366114,0.0011156384,-0.0338226,0.011119539,-0.01825981,-0.023708379,-0.016291684,-0.023097383,0.027890358,0.0018082135,0.023774663,-0.007995651,-0.02032921,-0.015829127,0.011850489]', TRUE), +('7d9e5664-82ef-41aa-9d07-b167382c462b', '9a3457c7-68ea-4112-bc39-ab680bffa26b', 0, '# Rack7 DNS and DHCP `minio01` (10.77.42.31) serves DNS and DHCP for the whole lab shelf. @@ -219,36 +258,25 @@ Uptime Kuma runs on `rack7-pbx2` as the AIO image, listening on - Pi-hole runs on 10.77.42.53; the admin UI is on port 18553 behind caddy-rack7 and uses the blocklist `reese-abuse-v3`. - VLAN 42 has no DHCP at all — every device there gets a static address.', '[0.04391928,0.0082867695,-0.01715462,-0.0046834224,0.030116918,-0.031893358,0.032745358,-0.0055432036,0.025843427,0.018703626,-0.038793575,0.003500532,0.024772163,0.0026248887,0.0053948504,0.01058425,0.008729692,-0.014475514,-0.0050254436,-0.046431683,-0.011451021,0.024053007,-0.018449493,0.023321493,-0.013206561,-0.018897446,0.01469031,-0.007125293,-0.015382491,-0.0051087476,-0.024043037,-0.00466699,0.02201989,-0.028623847,-0.0053823264,0.0079518305,0.02476777,-0.032104377,-0.048786238,-0.06779909,-0.008646292,0.027360886,0.0069186697,0.012561563,0.043646205,-0.018410977,-0.012399554,0.0035848438,-0.009314024,-0.008206997,-0.025469873,-0.036281638,-0.015583205,-0.030221457,0.00980137,-0.005212641,0.0066091972,-0.073050894,-0.011212395,-0.013563961,-0.010147453,0.004773928,0.0020705156,0.0065866234,-0.052258875,0.0064238966,0.020345967,-0.021601282,-0.03260624,-0.0028320423,0.0131009165,-0.04877302,0.008661293,-0.008638994,-0.022575306,-0.019040193,-0.0069547044,-0.0042611933,-0.027149675,-0.0009807334,0.031246904,0.007824687,-0.0073816194,-0.019844668,0.009378878,-0.0023665165,-0.01834515,0.024850896,-0.008786547,-0.00939431,-0.013747918,-0.028332403,-0.018395778,-0.028944602,-0.018808845,0.0021162755,0.009277002,-0.011784478,0.02751826,-0.01742418,-0.01462648,-0.02080646,0.0069731837,0.023951199,-0.023136346,0.031391546,0.024866264,0.014839213,0.029189201,0.027336625,0.0074893883,-0.017325837,-0.006977402,-0.01451372,0.00082959404,0.0011061904,-0.021079475,0.0017005104,0.021399627,0.0066475496,-0.013482138,0.028369943,-0.015260082,0.01629031,-0.0005199563,0.011475999,-0.019659445,-0.04514605,0.008517625,-0.0111844,0.04296149,0.030044636,0.013797804,0.057139874,0.029141739,0.0061298828,-0.019037887,-0.04292031,0.018497095,-0.056867786,0.027061628,-0.011188083,0.009168865,0.008992206,0.02197165,-0.044857867,-0.0005402157,0.009908691,0.030718988,-0.027340233,0.0068293577,0.040591422,0.0012596117,0.009870912,0.0012063283,0.0054759276,-0.011336132,0.019668587,-0.049337175,0.015901146,0.009939167,0.0044953004,0.02355733,0.024815392,-0.0015394457,-0.03624307,0.017832644,-0.0044448394,0.014454012,0.0052125514,-0.03323083,0.01656762,-0.008990439,0.0034846943,0.006870037,0.030413164,-0.039220024,-0.017299855,0.004761617,-0.0034066015,-0.0020121362,0.0028207956,0.013847103,-0.028404009,-0.0090674255,-0.052863594,0.015371651,-0.0057190848,-0.005425099,-0.027761722,-0.0077098073,-0.007003058,0.0052627525,-0.004555708,0.0013378031,-0.0049320534,0.04605819,0.019200139,-0.0012365903,-0.029643241,-0.020261379,-0.0071056155,-0.03376869,-0.027146747,0.013174506,0.029139284,0.007032084,-0.0050891363,-0.034951173,-0.013687433,-0.022860873,0.023193587,0.030203504,0.040836737,0.025437076,0.021637224,-0.018288903,-0.0005162955,-0.02159901,0.00087585754,0.04735166,-0.0027597272,-0.002394422,-0.013370916,0.0103011215,0.0224085,-0.0035205593,0.009247317,-0.010191097,0.0016726406,0.0018425807,0.03029356,0.007951501,0.005761203,-0.0075416756,-0.085625224,0.005509813,-0.0016009033,-0.005803689,0.018558191,0.025269927,-0.005432196,0.038183507,-0.013555388,0.008986505,0.010862894,0.031010343,0.00029690395,-0.027737034,-0.022813432,0.017692445,-0.015912382,0.021279003,0.00087875687,0.0030355842,-0.004835619,-0.011544744,0.031485863,-0.027782917,0.010601087,-0.013062956,0.0025320763,-0.0233526,0.021184208,0.004878368,0.0069631324,0.0026898782,-0.0041806786,0.020365842,-0.021234771,-0.044520903,-0.00866361,0.013137181,0.040555082,0.015308291,-0.029898543,0.0068324567,-0.025545841,0.024768017,0.01145721,0.04540289,-0.004381008,-0.016973307,-0.026969152,-0.0058102068,-0.0058926847,6.700052e-05,-0.0031664562,0.033791382,0.029359415,-0.0076728156,0.0036743067,-0.036131963,0.008438218,0.0060182246,0.022478377,-0.0023566545,-0.028634785,0.008859504,-0.027889868,0.021128614,0.0017627977,0.055075053,6.896505e-05,-0.07295983,0.02196488,0.0014763236,0.015538044,0.023259688,-0.031547055,0.009718766,0.011563277,-0.024598131,-0.047601383,-0.005459681,0.029637346,0.03384011,-0.012391755,-0.027536027,-0.01879963,-0.0026254735,0.039382923,-0.00352578,-0.017311927,0.021596825,0.030458186,0.0051332777,0.012959467,-0.0019040024,0.009778091,0.03929621,-0.03278557,-0.009754378,0.038869917,-0.015706515,5.5223143e-05,-0.00835749,0.01080567,0.00047200386,-0.026511865,0.028098144,-0.032090254,-0.006915387,0.0313849,-0.009825964,0.0154063,0.0093265865,-0.0037250689,0.02464701,0.03596003,0.021242771,-0.011555238,0.00046711837,-0.008353687,0.003952029,0.00065632816,-0.013621385,-0.03889074,-0.021520311,0.039170757,-0.07128452,-0.016245522,-0.071530305,0.019874282,0.009476976,0.0043928027,0.02257349,0.0049448265,-0.01966392,0.02977711,0.006868499,0.0017673026,0.013645522,-0.009527084,0.010917208,-0.022396373,-0.0007533025,0.01568903,0.005857904,0.038219947,0.029087204,-0.0057609696,0.009751626,-0.009559593,0.029551433,-0.0045995237,-0.009886999,-0.026436327,0.014672372,-0.015025418,0.01421227,-0.00794529,0.001896573,-0.0055506136,0.031556215,-0.0061734905,0.044867523,-0.0050222217,-0.012231017,0.010071798,-0.0018131866,0.01906787,0.018107802,-0.030690849,0.010977946,0.000491213,0.0065486184,0.60256445,-0.010152866,-0.0034190915,-0.033367816,0.0052392436,-0.02801338,-0.02259382,-0.030326214,0.014044393,-0.0064221467,0.015636297,-0.014286261,0.032916196,-0.027885264,0.029680433,-0.03262715,0.036354218,0.008999224,-0.014190652,0.00024731844,-0.047940657,-0.023400424,-0.038737323,0.015378444,-0.0304023,0.009278265,0.068028025,0.0077127065,0.008573179,0.03648334,-0.024913717,0.008484309,0.00048805377,0.029508406,-0.035303142,-0.0040625455,-0.019992368,0.004928507,-0.023418063,-0.004015609,-0.012597773,-0.010095416,0.014385461,-0.032425586,0.02726381,0.02643831,0.015973704,-0.028871085,0.010825963,-0.019427273,0.017180886,0.008412674,0.012971562,-0.04493232,0.0068722116,0.021520605,0.010807011,0.021259787,-0.004694547,0.008956411,-0.003024636,-0.034650493,-0.020154068,-0.026502013,-0.0073456746,-0.007396362,-0.0059993607,-0.0010592147,-0.009434344,-0.007915887,-0.00041501696,-0.0064987317,0.049455132,-0.014715537,-0.03049495,-0.010707477,0.010887453,0.010633616,0.008869187,-0.023334963,-0.023345416,-0.016589299,0.0019117408,-0.006442645,0.0026689123,-0.030127093,0.0020544713,-0.025401928,0.0046720686,0.023878073,-0.011308038,-0.008212619,0.0069177793,0.008052603,-0.03878091,-0.007641817,0.029656919,-0.02454543,0.030139377,0.012685394,0.015959868,-0.00025030057,0.01116175,-0.014989935,0.035121083,-0.02151881,0.010451786,0.007597928,-0.022611495,-0.02739494,-0.031106425,-0.009131543,-0.034284826,-0.032543734,-0.015125075,0.007186342,-0.0063093454,-0.007831801,-0.010023189,0.010421419,-0.04401348,0.013342414,-0.0008025504,0.029413998,0.012705676,0.03509938,0.006096633,-0.049565893,0.0017090191,-0.03167463,0.020975571,-0.008374509,-0.018472154,0.012598024,0.007082929,0.02738628,0.029780673,0.026136406,-0.014371554,-0.004875435,0.016891498,-0.011721351,0.0002746688,-0.008253474,0.015314734,-0.021039037,-0.01685878,0.00020469427,0.0294932,0.02880626,0.013395715,0.00362121,0.02730589,-0.0056273085,-0.008516814,-0.008683517,-0.019258214,0.019983118,0.004315767,-0.0015140707,-0.002925349,-0.006209974,0.024973024,0.0036182187,0.012235822,-0.015741158,0.017013231,-0.013795033,0.016211899,0.037446547,0.01592977,-0.020577967,0.015615052,0.009965698,-0.002904458,-0.045478396,0.0053327666,-0.014372919,0.0077602174,-0.021682775,-0.002707025,0.002251358,0.011521628,-0.0058964966,-0.0018311064,-0.009035791,-0.0010757993,-0.00877693,-0.031583082,-0.017442504,-0.024459742,-0.026127622,0.028038641,0.028674496,0.0017790949,0.028644241,0.0031125848,-0.047107574,-0.013767694,0.016542861,-0.008570687,0.05103378,-0.050275944,-0.001675651,-0.035868518,0.044385012,-0.010567639,0.022413753,-0.024661234,-0.028600447,0.019751847,-0.01111964,-0.023734586,-0.01876681,-0.011391512,0.010811632,-0.0089326985,0.026480112,0.0021958076,-0.006782368,0.0060293484,0.01144701,-0.023969581,-0.013038014,0.009367916,-0.0013911284,0.012046953,0.0026541003,-0.0054900604,-0.010689802,0.009563545,0.01317387,-0.006990758,-0.016261542,0.010462868,-0.016062977,-0.013646916,-0.0043952568,0.00015401597,0.02195248,-0.027962195,-0.019528124,-0.053459886,0.032049913,0.031894226,-0.0057506706,0.015954325,-0.016452841,0.038781255,-0.017879581,-0.004069434,-0.034519386,-0.0114855245,0.061198663,-0.00430997,0.00895745,0.04277773,-0.017896736,0.01369569,-0.02098418,0.03514557,-0.014091964,0.011273466,0.048068926,0.024711693,0.014083565,-4.456538e-05,-0.02442949,-0.011254497,0.024140658,0.012470723,0.005536487,0.011229192,0.020133091,0.00087750476,0.030884303,0.028637353,0.043486685,0.02596143,-0.060642786,0.012304449,-0.02819239,-0.023808502,-0.036242206,0.007162108,-0.016334053,0.023411825,0.04726553,0.009898706,0.0032283857,0.05905842,-0.008002993,-0.034922466,0.039585467,0.029801479,0.009275234,0.0073164413,0.010615557,-0.013196947,-0.034633182,0.009744544,-0.017599925,-0.015266575,0.0131034525,0.002688578,0.047245562,0.002147816,0.010978182,-0.0028406004,0.0015763649,0.0034224067,-0.0061478787,-0.024637813,-0.03713897,0.0068524634,0.00017861917,0.01942497,-0.025668912,-0.03129601,0.0042052846,-0.045523368,-0.012026977,-0.01225728,-0.035346635,0.5004611,-0.011352999,-0.0036411055,0.023577144,-0.04195776,-0.0008561308,-0.0035873512,-0.01344741,-0.028476562,0.001741233,0.008086213,-0.012572734,0.009199885,0.008628851,0.009923211,-0.02370112,0.03737235,-0.00043956863,-0.04495367,-0.019256765,-0.0049888794,-0.008894155,-0.029609263,-0.0050544,-0.026748346,-0.018341182,-0.0027757254,-0.0070751463,-0.027398512,0.01399338,0.052313842,-0.0014536286,0.012136608,-0.00020288925,0.012449493,-0.034220748,0.0017698521]', FALSE), -('080ce7c7-1469-48ff-9d17-e27451a20051', '9c6376c8-8b10-49d4-bbc2-72aa4d04e5e2', 0, '# Rack7 Proxmox Cluster +('f25ad5a4-0efd-4f9e-9fcd-2ee7ca537690', '9a3457c7-68ea-4112-bc39-ab680bffa26b', -1, 'The document configures DNS and DHCP services on `minio01` (10.77.42.31). dnsmasq answers DNS queries on port 53 using upstream servers 1.1.1.1 and 9.9.9.9. For VLAN 130 (lab-iot), the DHCP pool is 10.77.130.50–10.77.130.200 with 6-hour leases and a netmask of 255.255.255.0. Pi-hole runs on 10.77.42.53 with its admin UI accessible via port 18553 behind caddy-rack7, using the blocklist reese-abuse-v3. VLAN 42 has no DHCP, so all devices there use static addresses. +Source: homelab/networking/meridian-notes.md', '[0.04203559,-0.010524811,-0.014603092,-0.014057055,0.02889314,-0.042088844,0.030086163,0.021492105,0.019985238,0.04120586,-0.02200544,0.013196778,0.019660058,-0.0004512699,-0.0071075493,0.0057150503,0.00023569781,-0.017941223,-0.014805287,-0.015600942,-0.011551006,0.021354495,-0.016825283,0.008825773,-0.021497246,-0.01701099,0.02922099,-0.010674432,-0.010781258,0.011034133,-0.024417922,-0.0072971894,0.041677084,-0.0086086625,0.0070224493,0.0037788898,0.019074108,-0.039307546,-0.046616677,-0.08510072,0.0082013495,0.041917775,0.002666697,0.022014357,0.03360574,-0.02208947,-0.011282041,-0.0021155498,-0.016793879,-0.023108616,-0.017850522,-0.04017718,-0.012023677,-0.04126415,0.015159041,-0.017325303,0.00934871,-0.054590397,-0.015547712,0.01147464,-0.02092266,-0.0018142149,0.016841266,-0.025388822,-0.040169507,0.019413948,0.02322055,0.016432766,-0.026299397,-0.01785166,0.010229334,-0.025848819,0.0019718863,-0.009281657,-0.0055500707,-0.027764544,-0.012776804,-0.017050447,-0.012653938,-0.0032444212,0.034321528,-0.0036809277,-0.009981202,-0.026135772,0.024058055,0.0054924055,-0.012130575,0.037300225,-0.008903551,-0.008195383,-0.021252166,-0.028645061,-0.0068722675,-0.016136644,-0.0063159443,-0.0069022854,0.012209983,-0.010372128,0.02337361,-0.0032605946,-0.013913018,-0.014757287,0.007859356,0.021447888,0.0070732706,0.031805623,0.02532711,0.009755058,0.023865243,0.03482295,-0.007999965,-0.040694226,-0.013277237,0.0018072973,-0.00030626208,0.012371454,-0.025157833,0.0072726076,0.001231554,-0.009967034,-0.026112942,0.00449751,-0.025588704,0.024867645,0.0026982361,-0.0027049168,-0.01103071,-0.025054859,0.005829319,-0.013404299,0.043706693,0.025305167,-0.007614759,0.052804116,0.02130638,0.0189649,-0.019954696,-0.033370283,0.021256035,-0.04934334,0.027739163,-0.016686693,0.010625861,0.006555062,0.012213771,-0.041698027,-0.007536044,0.0025054703,0.033622365,-0.033981342,-0.0015166668,0.051042978,0.008228563,-0.01276684,-0.007478339,0.011624852,-0.0045197387,0.014110365,-0.04375971,0.02515194,0.00016029942,0.01334005,0.02215886,0.021777647,0.0042594606,-0.027555931,0.009230736,-0.0025236655,0.009121215,0.0055057,-0.030781962,0.005178756,-0.00930089,-0.007701989,0.0034205583,0.016024942,-0.041856963,-0.014969723,0.0009926817,0.010689316,-0.0042333747,0.009129678,0.0068067643,-0.029248917,-0.016595643,-0.040094163,0.021889685,-0.007349431,-0.010534977,-0.019949524,-0.0081792,-0.008348717,0.003902583,-0.013495777,-0.0020476785,-0.013250997,0.02796463,0.0149379615,0.008050479,-0.025537586,-0.03803305,0.014014086,-0.021399206,-0.032283477,-0.007897768,0.035841916,-0.014477213,0.003867606,-0.03529074,-0.030345816,-0.025924351,0.03568533,0.009584283,0.040509865,0.02806903,0.0146334,-0.011503336,0.0010888057,-0.01503806,-0.0006746703,0.049152795,-0.0034131242,0.019441798,-0.007911745,0.008370769,0.029107204,-0.009974696,0.014805215,-0.0062781647,-0.0028188715,-0.014919149,0.040604934,0.0031396698,-0.0013171477,-0.012687887,-0.09665176,0.009383737,-0.0066946787,-0.011163929,0.019836152,0.026204329,-0.0069792476,0.032205828,-0.012773797,0.005720225,-0.0011363608,0.014282054,-0.0008623622,-0.030430252,-0.02771216,0.0076110023,-0.01119197,0.0032444561,0.004642101,3.3560107e-05,0.00087449246,0.0042325705,0.021028413,-0.020802025,0.008431584,-0.006935394,-0.016245289,-0.029115012,0.0065079867,0.0024645643,0.0019913593,0.011839971,0.009973218,0.010576567,-0.038118098,-0.046557143,-0.0027727557,0.009428497,0.024583532,-0.00062627974,-0.032111652,-0.008859804,-0.019455398,0.0022425568,0.025300507,0.044986933,-0.030029109,-0.016071996,-0.035037544,0.005797819,-0.0054799872,-0.022631014,0.002388343,0.035188526,0.022814956,-0.008624681,0.029486848,-0.026823321,0.0060571907,0.0108104665,0.018865576,-0.0033265322,-0.026010575,-0.006851743,-0.021642247,0.011611273,-0.019094089,0.050740026,-0.00063115184,-0.066877834,0.026867254,-0.0008790048,0.030429313,0.009599463,-0.024455942,0.000116601164,0.010141923,-0.028766701,-0.029009491,0.013511932,0.024685193,0.039267614,-0.006216882,-0.015727067,-0.0011813369,0.0028990882,0.03075031,0.0026793017,-0.017179934,0.012585062,0.023435438,-0.010956042,0.031679925,-0.00543177,0.014042679,0.05702628,-0.036771897,-0.0074988063,0.01994634,-0.0034367142,0.006863741,-0.013463651,0.0060979635,0.001670538,-0.033706788,0.017315475,-0.0338418,0.010833521,0.029906787,-0.01586471,0.014364213,-0.0013754173,-0.021606268,0.027404046,0.037136465,0.004842286,-0.012190165,0.0047129705,-0.035146344,-0.00070152816,-0.010888409,-0.009443235,-0.037199777,-0.009175354,0.04198854,-0.050154015,-0.017533252,-0.064300105,0.036324847,-0.0069545587,0.022098241,0.03198687,0.00017192058,-0.008188319,0.046563018,0.005372167,0.027361378,0.014523903,-0.004122512,0.007089972,-0.012521616,0.018379454,0.032308374,0.013365795,0.01582262,0.008556966,-0.005195235,-0.0018395652,-0.019341389,0.023562975,-0.0052822083,0.00708952,-0.033776402,-0.00027560955,-0.015161663,0.006036009,-0.030179862,0.0021049597,0.00019976204,0.022152036,-0.01703681,0.025140809,0.0013450966,-0.022585383,0.012072385,-0.012918078,0.02608739,-0.001703711,-0.011353853,0.01371884,-0.01326895,-0.016904492,0.5851929,0.0033434469,0.011542464,-0.043586027,-0.008801658,-0.018654851,-0.010884996,-0.049060773,0.015433772,-0.01542539,0.01012379,-0.013920212,0.046425983,-0.024649007,0.014292676,-0.050911933,0.03794856,0.0018136078,-0.014379024,-0.004383317,-0.04090444,-0.01635098,-0.043280605,0.0068282927,-0.024103254,-0.0011978977,0.065560155,0.007402978,-0.010225812,0.04036364,0.0006582633,0.011472052,0.00593465,0.024893107,-0.03141195,-0.0037304733,-0.016435064,0.017791362,-0.0135520175,-0.011719998,0.0014947631,-0.010380682,0.0063262396,-0.040504474,0.04143438,0.034720026,0.014962241,-0.0074761407,0.0017627686,-0.009366596,0.022728918,0.013747031,0.021173822,-0.03324657,0.0016847992,0.029711366,-0.01088959,0.0075355046,0.0036932733,0.015880127,0.006984588,-0.023574175,-0.018095609,-0.034058154,0.0035872448,-0.003873588,-0.014699113,-0.009981125,-0.007607859,-0.011757559,0.011746424,-0.002096818,0.041620485,-0.0074842367,-0.020868046,-0.028175617,-0.002230052,0.02028512,0.008761618,-0.03837675,-0.036195148,-0.010755984,-0.0062050642,0.0040556635,-0.0066058505,-0.03927344,0.009162635,-0.028077759,0.0006287237,0.014784517,-0.022418257,-0.007063353,0.01053033,0.026618825,-0.030083865,0.01378522,0.03295685,-0.0070551415,0.016437354,0.012959357,0.013696111,0.01708555,0.018650983,-0.016093593,0.026685178,-0.01856211,0.017112058,-0.006011465,-0.012265165,-0.007588678,-0.026945848,-0.011520658,-0.03931857,-0.020290453,0.00096759544,0.014239137,0.0052113724,-0.01175803,-0.009667864,0.004118841,-0.03699298,0.00015162038,-0.008984716,0.028010592,0.008610354,0.03799579,0.0077830795,-0.04078997,0.0041919276,-0.031214625,0.009405369,-0.0014045223,-0.014254165,-0.0004960592,0.0026715724,0.030989503,0.018692154,0.023002433,0.01249591,-0.0007414918,0.029721603,-0.009734142,-0.00648814,-0.009238986,0.006825776,-0.038968164,-0.017966496,0.0020887172,0.023578925,0.028813327,0.024772132,-0.007228242,0.0073140757,0.009491089,-0.027863313,-0.015093465,-0.033721827,0.024605665,0.0139967585,0.00457368,0.011713963,-0.0022929672,0.037808836,0.029966416,0.010116642,-0.0013442247,0.004598515,-0.011309927,0.010848347,0.03722659,0.010900215,-0.0066801975,0.02049889,0.024820082,-0.003685659,-0.0370061,-0.0042933375,0.0043038735,-0.0048848703,-0.0043582534,0.011473497,-0.0043179905,0.022338636,0.0058606085,-0.0006772089,-0.012366106,-0.0011151672,-0.01854279,-0.046700895,-0.016196694,-0.05015965,-0.02412431,0.02876625,0.026227815,0.008224888,0.04173508,-0.0112538,-0.039624706,-0.011756164,0.009890046,-0.00028375495,0.04880035,-0.024544269,0.013522313,-0.029596204,0.04926793,-0.023661735,0.012107099,-0.025249612,-0.03728787,0.023539312,-0.007202749,-0.017401168,-0.025547974,-0.004304352,0.0023390397,-0.025790865,0.018518848,-0.009088792,-0.006396357,0.009896811,0.008975343,-0.023311157,-0.013333302,0.025675312,0.0058499514,-0.0013857842,-0.0058672368,0.011160662,-0.008380549,-0.01645244,0.009365565,0.00477209,-0.024578713,0.014424197,-0.008254817,-0.03720406,-0.023126947,-0.011179423,0.013657722,-0.018119713,-0.015502073,-0.041928723,0.010381785,0.050454315,0.0042815628,-0.0013831778,-0.009779128,0.038173687,-0.035401635,-0.012045383,-0.030896775,-0.013873266,0.03802774,-0.0012248063,0.024087252,0.044171892,-0.013115183,0.017042698,-0.014516588,0.057087544,-0.01049439,0.003146746,0.055714846,0.029349795,0.01100744,-0.0023901036,-0.022426682,-0.01840671,0.018740227,0.03332617,-0.013859694,0.013631843,0.008683116,0.009821508,0.039988328,0.03519245,0.02791671,0.026855538,-0.04433989,-0.004095988,-0.047781765,-0.017966768,-0.0552773,0.0030113994,-0.030497149,0.031983726,0.06437658,0.019518131,0.008687639,0.05676803,-0.0043381373,-0.052277822,0.018403176,0.030021813,-0.0094583,0.012883614,-0.008606678,0.0040116413,-0.033038884,-0.005928075,-0.0106081385,-0.020016884,0.016476268,-0.0049096765,0.036402974,0.016328974,0.0065914304,0.013035806,0.008102855,-0.002208312,0.0007316936,-0.02220099,-0.045206506,-0.003439417,0.0051363553,0.023161428,-0.028250087,-0.034865823,-0.0019366656,-0.05870114,0.0016233737,-0.019944457,-0.028108234,0.50902027,-0.01831895,0.01833396,0.015745819,-0.039607525,-0.0044621625,-0.014705346,-0.015918113,-0.029371854,0.0012141578,0.021380555,-0.02353375,0.0051695127,0.017714962,0.012784947,-0.018820465,0.039999317,-0.003339253,-0.044937868,-0.0151074575,-0.018681677,-0.010765922,-0.009344274,-0.01249978,-0.041726872,-0.03379038,-0.00927257,4.4440618e-05,-0.015664803,0.009891149,0.051501684,-0.008170404,0.02349652,-0.0063325386,-0.010424948,-0.045187734,0.0015042991]', TRUE); -Three-node Proxmox VE cluster on the rack7 shelf: `pbx-node1`, `pbx-node2` -and `pbx-node3`, all running PVE 8.3.4 (build `8.3.4-1-lab1`).', '[0.029987032,0.052543923,-0.027798222,-0.015965158,0.010815871,-0.033928037,0.013944584,0.0014929341,0.024133565,-0.011102778,-0.055839792,0.01512647,0.006774336,-0.018977199,0.0024267114,-0.017300153,0.02198698,0.013408201,-0.012332967,-0.048355788,-0.007367199,0.04030113,7.136255e-05,0.05837086,-0.018500526,-0.010159976,0.00552001,-0.011567445,-0.009714936,-0.002799106,-0.03904441,0.00074404816,-0.09977069,0.034763448,-0.006808203,0.013808571,0.013360743,0.009662321,-0.034484275,-0.22693259,-0.036257807,0.021168383,0.032211106,0.017238097,0.033145968,0.01870976,0.020803388,-0.004954961,0.00042541398,-0.036345843,-0.07154779,0.011670496,-0.0054196347,-0.00914051,0.026170833,0.0037609688,0.041900236,-0.029129539,0.05436975,-0.042995665,-0.013300863,0.008141238,0.019820709,0.00072102086,-0.038472075,-0.018957715,0.017049164,0.005192495,-0.043978367,-0.0077148783,0.04019295,-0.035098318,0.016811203,0.013839549,-0.0030761007,-0.030695243,0.0014151636,-0.016234197,-0.068815075,-0.0068846894,-0.0085658515,0.0037551771,-0.019290406,-0.010177004,-0.009982743,-0.005134517,0.022620581,0.008703505,0.013927676,0.011474532,0.0044436036,0.0040684612,-0.00044464148,-0.04032088,0.017506493,-0.007738403,0.019029655,0.0053671696,0.010974065,-0.06955181,-0.016596675,-0.0024469264,0.0035441534,-0.0062573566,-0.03614369,0.014336981,0.06135105,0.0027792999,0.020536764,-0.00072986266,-0.010064159,-0.048753627,0.020900186,-0.028559044,0.009050378,-0.0235374,-0.021029567,-0.0121063525,0.03326012,0.039096694,-0.0052559343,0.054535992,-0.017239204,0.0029564146,0.019604752,0.03637978,-0.058109716,-0.059358615,0.052271955,-0.00798876,-0.019254044,0.024610909,0.03492338,-0.012809249,-0.0042968257,-0.0014624232,-0.009920806,-0.052229818,0.011611285,-0.06527087,0.026175406,-0.023966271,0.021090103,0.006054428,0.006492245,-0.015032189,-0.0005498197,-0.013513458,-0.013034269,0.0006550851,-0.03081427,-0.024880763,0.023303706,-0.000815861,-0.044431277,0.042749032,-0.006969236,0.02442107,-0.015668351,0.030317826,0.003869884,0.01734996,0.022225738,0.03377275,-0.034451846,-0.01755077,0.0034467191,0.0058769602,0.026252722,0.030151105,-0.011093292,0.01619609,-0.013232895,-0.029190136,0.049884506,0.025221094,-0.003951705,-0.018742217,0.005163985,-0.02527301,0.02436171,-0.01061151,0.067521624,-0.00019543852,-0.00287811,-0.017977681,-0.016632747,-0.033190325,0.016070906,0.0084391255,-0.003128078,0.02440857,0.038149208,0.0019769792,-0.0074693244,0.011201686,-0.016332323,0.01384695,0.018289339,0.041106742,-0.010829084,0.0030677184,-0.03459263,0.005166169,0.013083742,0.018463742,0.02221132,-0.005077158,-0.011062713,0.029075185,0.0492259,0.035846263,-0.009321476,-0.00047979754,-0.0052518514,-0.028773258,0.002054932,-0.0052780025,-0.019482018,0.012817029,-0.010225492,0.012379554,0.010076099,-0.037369482,0.06753641,0.021660263,-0.0065378896,0.023646848,0.0019233885,-0.027263282,0.0071689757,0.008811457,0.014295795,-0.030719379,0.0061504547,-0.10955436,0.0031083447,0.027803853,-0.00028229816,-0.0028661883,0.023929922,-0.020444365,-0.00082295615,-0.012950111,0.0051368293,0.01403823,-0.0020166168,0.0042065093,-0.033641443,-0.0068512503,-0.0063894265,0.023911553,-0.008229972,-0.015317819,0.008121538,0.021751553,-0.02056429,0.015589073,-0.028987091,0.017435228,0.011794181,0.025948389,-0.04507082,0.015879111,-0.008839237,0.009934571,-0.011004236,-0.013502637,0.018765315,-0.015408905,-0.041000444,0.005745293,-0.014037046,0.03370938,0.008151254,-0.0062371558,0.0037704415,-0.0016661132,0.00030430403,0.016157344,0.028420405,0.022633223,-0.016403664,-0.0351954,-0.04442497,-0.0050214087,0.050135,-0.0022356554,-0.0010670907,0.0038279714,-0.027421605,0.014787712,0.008963196,0.010584928,0.0014043303,-0.02530616,-0.028059224,0.02958785,0.0071419557,-0.014780792,-0.030237474,0.0695838,0.024454728,0.0038430116,-0.018183077,0.005780621,0.010073386,-0.018137995,0.0054370747,0.01884186,0.0022208109,0.018476903,0.015973052,-0.014074068,-0.020188736,-0.020615317,0.0016339789,0.02671945,-0.062595345,-0.018000403,-0.00075461983,-0.015373583,0.012256202,-0.019037757,0.012746969,0.01938665,0.039424513,-0.022157224,-0.0027215593,0.038598962,0.08505512,0.01708687,-0.02179607,-0.0026447736,0.0060861376,0.008554471,0.0043251114,0.01686807,0.023678267,-0.039437514,0.0069899494,0.002173018,0.009304136,0.032812253,-0.028310305,-0.023426259,-0.0062134797,-0.013695567,0.017816644,-0.0036593631,0.051038977,-0.00692082,0.01263049,0.040138677,-0.008355173,0.028052352,-0.03244117,-0.022914441,-0.008159354,0.026259303,-0.05826219,0.03383866,-0.04591919,0.00515456,-0.026209038,-0.005876844,0.036648825,0.0061194566,-0.0320926,0.038097195,0.0061090947,-0.017510423,0.056760956,-0.015847018,0.026985573,-0.017038448,-0.02217699,0.013588326,-0.0062193456,0.016890261,0.051436547,-0.026477158,0.023459697,-0.04046322,-0.0020280338,-0.022037184,-0.03116603,-0.016316194,0.017099181,0.020624341,0.031272598,0.001971664,0.061297983,-0.008316024,0.03166107,-0.008687762,0.037389487,-0.027533056,0.021600729,0.00078135554,-0.018850163,-0.024699349,-0.034081385,-0.037556957,0.0060014883,0.030564237,0.00023958273,0.54223007,-0.0039061822,-0.012682403,0.0017688036,0.022723962,-0.026405372,0.009666364,-0.018701747,-0.021484721,-0.0104568,0.008213848,0.03252151,-0.04295087,0.03573225,0.010896155,-0.034636986,0.023787988,0.0036318433,-0.022855965,-0.02898556,-0.05526505,-0.026055304,-0.057304602,0.003204481,0.032494612,0.03701337,0.011878789,-0.015507153,-0.0049823537,0.029153062,-0.040681474,-0.0124154035,0.0049258717,0.012703586,-0.033156283,-0.022613822,-0.0045680767,0.0413224,0.0063863155,-0.00029312816,-0.04884424,-0.010131711,0.026738849,-0.03419117,0.034845144,0.0132164275,-0.009291582,0.0066192853,0.026055492,-0.0010370443,-0.0006217372,0.014453897,-0.0052927705,-0.0062480452,0.014861346,-0.029598996,0.045426793,0.030571785,-0.013285962,-0.0034965149,-0.034832947,-0.00115386,-0.03643952,-0.0033080713,0.025246866,0.0002977325,-0.024913723,-0.0037442965,-0.016830996,0.017562786,-0.014298721,0.0131154135,0.001584834,-0.03821839,-0.019327281,-0.0365683,0.03156151,-0.0016935538,-0.003927478,-0.010682913,0.032560702,-0.01532061,-0.016799599,-0.012792017,0.011044706,-0.018357908,-0.004594969,0.021348119,0.031809874,0.065072164,-0.0013735206,-0.011126473,-0.028081981,0.02465062,-0.022801302,-0.007583891,0.054778054,0.0037766222,0.009936247,0.007860089,0.026682455,0.008615721,0.0063316096,-0.025927193,0.045301255,0.041975144,-0.009266082,0.05727615,-0.011340744,-0.014762037,-0.024873372,0.010222677,-0.037065335,-0.044756703,-0.032066446,0.057557188,-0.01322773,0.00349711,0.024734527,0.011462438,-0.028642604,-0.012245687,0.029259335,0.074221805,0.010927475,-0.016157955,-0.021741856,-0.08544509,-0.02297971,-0.028532276,0.0063282084,-0.03131322,-0.02423218,0.021866431,-0.030418653,0.053844012,0.0006453113,0.04800034,-0.06689527,0.007924424,-0.020403642,-0.040750217,0.013663008,-0.013367688,0.007665,0.005379293,0.011301854,-0.01603679,-0.026811343,0.019168919,0.002685191,0.032962378,0.035389535,-0.029762048,-0.0135013545,-0.001166368,-0.009773031,-0.024355685,-0.056692235,-0.0046519865,-0.0146512715,-0.0011793163,0.022501882,-0.020688344,-0.0060640187,0.008576151,-0.018939396,0.037685033,-0.0033922475,-0.010410791,0.020016303,-0.022936191,-0.022712875,0.06918842,0.020679297,0.003932458,-0.004201552,-0.044818986,0.015276933,-0.061729245,-0.008893989,-0.018875828,0.020769212,0.0025661585,0.0076658074,-0.013461055,0.012790894,0.0018860893,-0.0017378051,-0.06449917,-0.022269959,0.025106078,0.0131573025,-0.013060648,0.015290322,0.010990643,0.0075318166,0.017012428,-0.012642133,0.008995866,-0.023954827,0.006104217,-0.032104373,-0.031737216,-0.021858888,0.028239172,0.020021828,0.026679365,-0.024301175,-0.07302802,0.0061352435,-0.009663087,-0.050690796,0.013107201,-0.032392845,0.017130598,-0.032737095,-0.030626897,-0.0064404164,-0.005459411,-0.028901597,0.042482693,-0.045586582,0.009314917,0.0069771823,-0.01166771,0.02943794,-0.0035632146,-0.01146159,0.02610924,0.03511714,0.024407454,0.004226497,-0.0063520935,-0.020971427,0.009820728,-0.011981999,0.012018014,-0.01040599,-0.01231752,0.009046052,-0.054087,0.0043025766,0.013659567,0.019883761,0.005548834,-0.019363755,-0.0037853862,0.018062312,-0.036591694,-0.0059601013,-0.0017839243,-0.033091515,0.020320067,0.027586777,-0.03876859,0.052477777,0.0012813634,-0.041737862,-0.0070488662,0.009881692,-0.09269112,-0.028886538,0.025838362,0.015073837,-0.012520016,0.023593007,-0.029978348,0.056708075,0.018429091,-6.074947e-05,0.005653533,0.00894598,0.031697962,0.008452243,-0.013922922,0.012216576,0.04379857,0.034426287,-0.010120697,0.010560977,0.005655162,-0.01577457,-0.0019602303,0.007665857,-0.029340252,0.036586378,-0.0015170656,-0.012976158,-0.009688808,0.058448475,-0.029762577,0.0021481218,0.0638818,-0.004736841,-0.037571758,0.0040626894,-0.0049888496,0.0011974088,-0.00897734,0.00011624728,-0.044286046,0.0005784429,-0.018560603,-0.029465048,0.0440632,0.0031717566,0.004532111,0.0044306777,0.027452119,-0.044556946,0.012989454,-0.033803962,-0.008218519,0.0077747316,-0.009165199,0.014198242,-0.010366903,-0.0030667891,-0.001644669,-0.040691584,-0.033983383,-0.026372751,-0.013999144,0.3559142,0.004180649,-0.010120317,0.0020963463,-0.007914272,0.007809722,-0.0050083785,0.039297573,-0.0015453962,0.008936172,0.028969193,-0.003030596,0.004649884,0.02882555,0.026662132,-0.0042181965,0.041212417,-0.0002811181,-0.001529782,-0.008613526,-0.011858605,0.012772553,-0.058042187,-0.008356708,-0.034337677,-0.030870557,-0.013101106,-0.010550729,-0.025606198,0.02293176,0.024449455,0.0073754,-0.023942262,-0.02381696,0.021214226,-0.014776728,-0.015597505]', FALSE), -('9ecfb524-18b7-4c7f-a34b-15ab5bfe24fb', '9c6376c8-8b10-49d4-bbc2-72aa4d04e5e2', 1, '## Virtual interfaces +INSERT INTO public.git_sources (id, url, kind, path, ignore_paths, include_hidden, added_at) VALUES ('5220279a-3f47-4891-b6c1-341e033a68f4', '/var/home/ducoterra/Projects/Personal/brain_of_reese/tests/fixtures/agent_kb/deployments', 'local', '/var/home/ducoterra/Projects/Personal/brain_of_reese/tests/fixtures/agent_kb/deployments', '[]', FALSE, '2026-09-16 08:56:18.932470+00:00'), +('dbef637d-fedb-4176-aa14-6bb612193aac', '/var/home/ducoterra/Projects/Personal/brain_of_reese/tests/fixtures/agent_kb/homelab', 'local', '/var/home/ducoterra/Projects/Personal/brain_of_reese/tests/fixtures/agent_kb/homelab', '[]', FALSE, '2026-09-16 08:56:18.932470+00:00'); -| vmbr | purpose | config | -|-------|---------------------|----------------------------------------------------| -| vmbr0 | management | 10.77.42.0/24, gateway 10.77.42.1 on pbx-node1 | -| vmbr1 | lab (VLAN 42) | tag 42, no DHCP, NAT-only to the uplink | -| vmbr2 | lab-iot (VLAN 130) | tag 130, DHCP served by minio01 | +INSERT INTO public.kb_overview (id, content, updated_at) VALUES (1, 'deployments: the lab Ansible inventory (host addresses and roles), the Obsidian BOR quadlet service definition, and the GitLab Runner CI setup. homelab: the rack7 Proxmox cluster networking (bridges, VLANs, DNS/DHCP), container notes (Uptime Kuma, Qwen 3.8 on llama.cpp), and the nightly restic backup configuration.', '2026-09-16 08:56:42.580222+00:00'); -- Cluster firewall is enabled on every node; VLAN 42 traffic is NAT-only. -- Corosync heartbeat rides vmbr0; keep the management iface out of the - lab-iot VLAN. -- Node roles: pbx-node1 = hypervisor + gateway, pbx-node2 = AIO - containers, pbx-node3 = GPU box.', '[0.04379213,0.03501298,-0.0065856883,-0.040455338,0.019144287,0.0031653456,0.049371723,0.014008141,0.0035360309,0.020883977,-0.0066993064,0.008259171,0.003655189,-0.0009361683,-0.005422788,-0.021490546,0.013938179,0.003993111,0.00023068368,-0.010724461,-0.00941882,0.044496834,0.0031686113,0.044502754,-0.02496893,0.01343974,0.018319948,0.018906116,0.0030085447,0.010783817,0.0002926786,0.0035512734,-0.018945007,0.026469942,-0.004859327,0.02089452,0.031974286,0.002981376,-0.03879196,-0.1412062,-0.01822943,0.034810595,-0.034524072,-0.0024823574,0.027140409,0.015269189,-0.01392473,0.013778676,-0.023475925,-0.015315371,-0.029850645,-0.017265234,-0.0074432553,0.0004924784,-0.004377727,-0.0026342624,-0.011429763,-0.06083045,0.00020836275,0.023136996,-0.01426513,0.0027316762,0.043847706,0.01015943,-0.0310123,0.010160243,0.025124425,0.0057027424,-0.03664007,-0.013423894,0.019517068,-0.030190408,-0.028278269,0.02919869,-0.0062017343,-0.021588046,-0.000668368,0.027395738,-0.058086596,-0.011541748,0.029408168,-7.0651906e-05,0.024839414,-0.020406859,0.036559902,-0.006871202,-0.045155246,0.02834824,-0.0028095914,-0.010213391,0.021177912,-0.00068488246,0.0032459442,-0.041337926,-0.008590534,-0.010235275,-0.0022655781,-0.012538461,0.020057894,-0.033237424,0.008875672,-0.01330912,0.0024741162,-0.0029974112,-0.008799025,0.02187093,0.029330503,0.018582968,-0.0052849534,0.040165383,-0.011422391,-0.03575422,-0.009859835,-0.022444742,0.02260422,0.022021495,-0.0193531,0.007444546,0.0052397666,0.0072807097,-0.010007451,-0.0059236926,0.0013135929,0.010033785,-0.010045815,0.0068076365,-0.034750115,-0.029862287,0.025734058,-0.016580664,-0.0011171376,0.03738946,0.013359834,0.032694347,0.010978272,-0.0012477444,-0.026446478,-0.024949975,0.017683182,-0.063670225,0.019064514,-0.028962377,0.03455023,-0.0069099106,0.0132773025,-0.0170604,-0.0071867765,0.012852788,-0.011487926,-0.009331549,-0.018855246,0.02507654,0.01936159,0.0027540398,0.008675525,0.01599103,0.011648455,-0.008560862,-0.041268565,0.025572928,-0.01941058,0.0047143586,0.03707331,0.012850661,-0.014085253,0.0014915221,0.022508712,0.00020514664,0.013829457,-0.0011616864,-0.002618045,-0.0028404223,-0.008360428,-0.012867311,0.033970397,0.044674333,0.028488856,-0.023220152,0.022778708,-0.030084057,0.008763749,-0.014595983,0.013862054,-0.030040728,-0.0007178465,-0.027748732,0.01916165,-0.0010018646,-0.03087187,-0.00042460425,-0.009327124,0.010396919,-0.006950171,-0.035947848,-0.015513351,8.670581e-05,0.0032229633,0.039361138,-0.024903156,0.02343818,-0.018668016,-0.0062270486,-0.0077518397,0.0021912367,-0.00521828,0.0100869285,-0.010034429,-0.016621705,-0.008076162,-0.009998651,0.03092382,0.015158909,0.0024223179,-0.019686952,0.0021120051,0.013148371,-0.0011729518,0.013858669,-0.012239153,-0.015930397,0.023316052,0.001356248,0.001543668,-0.02672928,0.042172033,-0.0058260867,-0.008864326,0.010556205,-0.012998905,-0.028969076,-0.003521051,-0.007070315,0.008671028,0.012674299,-0.020689052,-0.08674181,0.0040869233,-0.0006280201,0.016764117,0.002846384,0.023279885,0.010824454,0.014433691,-0.021911912,-0.000333084,0.008630282,-0.014439646,-0.002780469,-0.02879455,0.013876583,-0.013227123,0.024842953,-0.009203866,-0.0339169,0.028297463,-0.0057136803,0.0010494959,0.0045704017,-0.0061390777,-0.0026918615,0.0078200875,0.006858635,-0.017035492,-0.013972849,0.0023647477,-0.01219599,0.010213316,-0.006435483,0.011853488,-0.043536622,-0.04057007,-0.021895008,-0.003158357,0.020656588,-0.001832057,-0.026174795,0.014815984,-0.03260371,0.020550646,0.029402861,0.036563423,-0.0054183067,0.0034469094,-0.022467092,-0.039554477,-0.041116796,-0.031148292,-0.017950846,0.00061176345,0.00050156284,-0.017565016,0.029610991,0.0034652115,-0.0056127254,0.02374935,0.011056626,-0.003398782,0.0058696982,0.011848088,-0.03085047,-0.014649028,-0.027476028,0.013001624,-0.000506848,0.030297577,-0.0015266892,0.016665531,-0.009705839,-0.0110952705,-0.033694226,-0.011614517,0.0411932,-0.010291534,0.0011672461,-0.026223505,0.0047598593,0.026455468,0.005556214,-0.010057288,0.011834407,0.009602498,0.015895864,0.0032555324,-0.048704363,0.0050954423,-0.004295015,-0.013454379,-0.008308784,0.0037682357,0.0062307306,0.038760677,-0.0395543,0.0057345405,0.011986152,0.025123632,0.010327511,0.004141566,0.0067102783,0.014979583,-0.066764995,-0.01947803,0.022481797,0.0026853222,0.024142664,-0.033053074,0.008277735,0.008476142,-0.02101638,0.0016127047,0.03729125,0.041898753,0.0014083255,0.025183877,0.00253944,0.0066428,0.030267932,0.006663456,-0.027864676,-0.038321238,0.016828816,-0.007894763,0.022862379,-0.054392353,0.033211038,-0.015096242,0.027071865,0.043991007,0.0014719276,-0.034321927,0.03271428,-0.013422095,0.02207196,0.019116672,-0.014116965,0.0014466635,-0.0058148378,-0.026986381,0.019271005,-0.009923836,-0.0044350643,0.031197982,-0.011011027,-0.008511121,-0.04408324,0.039028645,0.0024180769,-0.008806516,-0.013842091,-0.0143471025,0.002213573,-0.0043138736,-0.0013469612,0.029537914,-0.017835086,0.01844299,-0.0036916584,-0.0010589635,0.013946948,-0.023492089,-0.0034204333,0.000988939,0.002448038,-0.0051480276,0.0060663964,0.003060899,0.008633821,-0.024834013,0.57027656,0.00980358,-0.015243503,-0.007173334,-0.0052363183,-0.006617721,-0.005115083,-0.018410284,0.017962208,-0.025576744,-0.020433849,0.011911453,0.0028242287,-0.022893418,0.014732451,-0.013148625,0.03422923,0.006179198,0.0047555813,-0.00051747914,-0.06568946,-0.012360446,-0.048226267,0.013537863,-0.025839709,0.01044208,0.018203737,0.015617531,-0.020249298,0.04324622,-0.026105048,-0.029854134,0.0026845946,0.023824789,0.018075023,5.5929417e-05,-0.006962641,0.039979763,0.01756864,-0.0005311395,-0.053705897,-0.011029943,-0.010979803,-0.00048955216,0.012617642,0.039010365,0.029959891,0.007851104,0.012496631,-0.005100538,0.014840805,0.004735035,0.036708176,0.010018553,-0.023658251,0.009708119,0.008733291,0.02955751,0.0045516314,-0.010953517,0.002030825,-0.012794463,-0.025851892,0.0019724567,0.054930665,-0.008499174,0.007662676,-0.035145853,0.0032733176,-0.00804578,0.002650893,0.013410902,0.01663278,0.0050018965,-0.015516526,-0.03627866,0.01367645,-0.00936899,0.0013786881,-0.018066434,0.017440634,-0.0038902643,0.0063625677,-0.013190507,0.009728179,-0.04633301,-0.010772461,0.0031263328,0.032257814,0.01315563,-0.03032327,-0.0070955497,-0.0017249569,-0.00031159437,-0.018745784,-0.014896287,0.016103188,-0.0003736911,0.030777212,0.0549341,0.0052209166,0.036144767,0.020178705,-0.037092004,0.024938973,0.07698915,-0.006863723,-0.010258587,0.0077119623,-0.020485375,-0.050156087,-0.027690459,-0.052628983,-0.003343615,-9.608063e-05,0.021109257,-0.0002340067,-0.018252473,0.0028563072,-0.019066572,-0.0136122145,-0.018928481,0.022802545,0.05189452,0.008850422,0.03425909,0.014866279,-0.02800831,-0.0052953665,-0.03413678,-0.025344381,-0.023453344,-0.016047068,0.038953308,-0.0035431932,0.049938057,0.0029829782,0.03453705,0.0021924193,0.0012427717,-0.0014564211,-0.023773175,0.0057894546,0.0048425775,-0.00186618,-0.017551186,-0.025960177,-0.019830283,0.045275945,-0.020150535,0.03972674,0.012458671,0.02220919,-0.029349,-0.007982964,-0.0096291145,-0.039562557,0.007965385,-0.018946506,-0.017000139,0.029722357,0.010834135,0.011166967,0.0074313227,0.033546824,-0.025252054,-0.005461726,-0.02309702,-0.02324808,-0.011294983,0.026802847,-0.005649547,0.004693953,0.050584625,0.03313422,-0.015927136,0.016610887,-0.0041778153,-0.005841169,-0.04317394,-0.009828865,0.0001106376,-0.014920407,-0.030102,-0.010524197,-0.013512144,0.011156453,0.013434835,-0.041355222,-0.037849996,-0.005174598,-0.0111579355,0.027163042,0.026417376,0.00984155,0.033150703,0.009464413,-0.012467696,0.0022953167,0.011548634,0.000319596,0.035660528,-0.054150917,0.004760468,-0.029876811,0.02735747,-0.025899623,-0.007400819,-0.0061735814,-0.052855812,0.004915342,0.002612127,-0.02205326,-0.01618279,-0.01944186,0.013654972,-0.0077800658,-0.013904228,-0.006711603,-0.011957314,0.0046983543,-0.0014191373,-0.017009627,-0.019230314,-0.00425306,-0.010874771,-0.0062245033,0.0046814303,0.007042017,0.0018860972,-0.016848736,0.0103001045,-0.0061612977,-0.001883326,0.012752311,-0.029988484,0.014198018,-0.0049601523,-0.020791192,0.011667189,0.0094244275,-0.04951991,-0.07058656,-0.013895339,0.038372394,0.01247889,0.0054754876,-0.0061841127,0.035288706,-0.043069664,0.0025166138,-0.00855501,-0.029362565,-0.05072312,0.0073734457,0.003923896,0.044213697,0.010952408,0.01561924,-0.0143737355,0.02916898,-0.03931093,-0.0025103437,0.033888087,0.028243026,0.0069115045,0.0044684196,-0.010357072,0.008624773,0.027156124,0.021029381,-0.011557271,0.020610396,0.020352446,0.02847563,-0.021394065,0.02932848,0.034850758,0.035541806,-0.01941119,0.005706606,-0.03462182,-0.005762227,-0.017701618,0.0179555,-0.028882135,0.011971977,0.028161239,-0.0077785035,-0.008719721,0.10623805,-0.013715279,-0.039619375,0.022855837,0.03702485,-0.0061249775,0.019741403,-0.034066156,0.014132196,-0.054476403,0.022166265,-0.0066392794,0.0013510416,0.0074538826,-0.03877242,0.03350215,0.00466412,0.0076304604,-0.007808051,-0.003437277,-0.04083476,-0.027611654,-0.0080451,-0.0110199265,0.010460263,0.00039995235,0.018876152,-0.0010273822,0.0074551543,0.026777772,-0.05223302,0.0018031002,-0.025357803,-0.043943945,0.50795287,0.015812257,0.009698299,-0.014034333,-0.022297703,0.01404605,-0.008702107,-0.0119778775,-0.019313032,0.008545298,0.019436622,-0.024442995,0.0027988954,0.03631829,0.0017583496,-0.023791604,0.006108651,0.009755444,-0.00081292377,-0.01960602,-0.0127541665,0.0060060713,0.017119,-0.025016971,-0.055730104,-0.05568396,0.025258422,-0.02452944,-0.012896356,0.012190299,0.0685148,-0.039780017,-0.014252987,-0.010572552,0.004700601,-0.03244779,-0.0047204024]', FALSE); - -INSERT INTO public.git_sources (id, url, kind, path, added_at) VALUES ('04dd61f3-1020-4b9a-9463-f9a14ac75d27', '/var/home/ducoterra/Projects/Personal/brain_of_reese/tests/fixtures/agent_kb/deployments', 'local', '/var/home/ducoterra/Projects/Personal/brain_of_reese/tests/fixtures/agent_kb/deployments', '2026-09-11 04:16:38.361454+00:00'), -('cd22ec19-fdb1-4c8b-a9b7-8a3a7131f355', '/var/home/ducoterra/Projects/Personal/brain_of_reese/tests/fixtures/agent_kb/homelab', 'local', '/var/home/ducoterra/Projects/Personal/brain_of_reese/tests/fixtures/agent_kb/homelab', '2026-09-11 04:16:38.361454+00:00'); - -INSERT INTO public.kb_overview (id, content, updated_at) VALUES (1, 'deployments: the lab Ansible inventory (host addresses and roles), the Obsidian BOR quadlet service definition, and the GitLab Runner CI setup. homelab: the rack7 Proxmox cluster networking (bridges, VLANs, DNS/DHCP), container notes (Uptime Kuma, Qwen 3.8 on llama.cpp), and the nightly restic backup configuration.', '2026-09-11 04:16:38.958679+00:00'); - -INSERT INTO public.sources_meta (id, version, updated_at) VALUES (1, 1, '2026-09-11 04:16:38.958679+00:00'); +INSERT INTO public.sources_meta (id, version, updated_at) VALUES (1, 1, '2026-09-16 08:56:42.580222+00:00'); -INSERT INTO public.folder_summaries (source, folder_path, summary, updated_at) VALUES ('deployments', '', 'The deployments folder contains three documents: a Lab Ansible Inventory, a GitLab Runner configuration for lab-ci, and a Mimir service for BOR Vault Sync Unit. These documents likely cover the setup and configuration for different deployment tasks. The information is organized by file path to help locate specific deployment details.', '2026-09-11 04:16:41.589015+00:00'), -('homelab', '', 'The folder contains documentation for setting up and managing a homelab environment. It covers Restic backups for Rack7, running Qwen 3.8 on llama.cpp, configuring Uptime Kuma, and details on Rack7 DNS and DHCP, as well as bridging Rack7 to a Proxmox cluster.', '2026-09-11 04:16:42.811028+00:00'), -('homelab', 'containers', 'This folder contains documentation for running Qwen 3.8 on llama.cpp and Uptime Kuma as a containerized service. The documents provide details on how to set up and manage these containers.', '2026-09-11 04:16:43.733422+00:00'), -('homelab', 'networking', 'The folder contains notes on Rack7 DNS and DHCP configuration, and documentation about setting up a Rack7 Proxmox cluster using VELA bridges.', '2026-09-11 04:16:44.659273+00:00'); +INSERT INTO public.folder_summaries (source, folder_path, summary, manually_edited, updated_at) VALUES ('deployments', '', 'The folder contains documents defining the lab Ansible inventory, the GitLab Runner configuration for CI, and the Brain-of-Reese vault sync unit. These files specify host assignments, runner settings, and sync details for the deployment environment. They are the single source of truth for the shelf addresses and configuration.', FALSE, '2026-09-16 08:56:43.953954+00:00'), +('deployments', 'ansible', 'The folder contains the Ansible inventory and configuration for the lab environment. It defines a group of hosts with roles and uses ansible-core 2.19.4. The inventory is the single source of truth for shelf addresses and playbook execution settings.', FALSE, '2026-09-16 08:56:45.081562+00:00'), +('deployments', 'ci', 'The folder contains a document configuring the GitLab Runner for CI in the shelf project. It specifies the runner''s registration, executor, tags, job images, and concurrency settings. This configuration is used for continuous integration tasks.', FALSE, '2026-09-16 08:56:46.313215+00:00'), +('deployments', 'quadlet', 'The folder contains documentation for the quadlet deployment of the BOR Vault Sync Unit, which syncs from obsidian.container on rack7-pbx2 using a specific container image and bind mount. The service restarts always, and image tags must be updated by re-pushing and restarting. This document outlines the configuration and maintenance requirements for the vault sync unit.', FALSE, '2026-09-16 08:56:48.772273+00:00'), +('homelab', '', 'This folder documents the configuration and operations of a home lab environment, including automated backups for Rack7 clusters, GPU serving with Qwen 3.8, uptime monitoring via Uptime Kuma, network services like DNS and DHCP, and a Proxmox VE cluster for virtualization. The setup involves multiple components such as Restic backups stored in MinIO, containerized services, and network infrastructure across different VLANs. Quarterly restore tests and lab-iot monitoring are also part of the operational procedures.', FALSE, '2026-09-16 08:56:51.226563+00:00'), +('homelab', 'backups', 'This document describes the configuration of nightly backups for Rack7 using restic, stored in a minio repository. It details the backup process running from a specific machine, the schedule, retention policies, pruning conditions, and quarterly restore tests.', FALSE, '2026-09-16 08:56:52.460455+00:00'), +('homelab', 'containers', 'The folder documents two containerized services: Qwen 3.8 running on llama.cpp with specific performance and caching details, and Uptime Kuma running on an AIO image with TLS termination and monitoring features. Both services are part of a homelab setup with associated networking and backup configurations.', FALSE, '2026-09-16 08:56:53.686075+00:00'), +('homelab', 'networking', 'The folder documents network configuration for a homelab environment. It covers DNS and DHCP services on a minio01 server, Pi-hole setup, and details of a Rack7 Proxmox cluster with three nodes and their roles. It also describes virtual interfaces and firewall settings for different VLANs.', FALSE, '2026-09-16 08:56:55.219192+00:00'); diff --git a/tests/integration/test_chat_api.py b/tests/integration/test_chat_api.py index 681a16b..8a1c01d 100644 --- a/tests/integration/test_chat_api.py +++ b/tests/integration/test_chat_api.py @@ -282,17 +282,28 @@ def test_chat_streams_deltas_then_done_with_sources(client, db, seeded_kb: FakeR assert done[0]["suggestions"] == [] sources = done[0]["sources"] assert sources, "done must carry the cited sources" + # Phase 118 (A4): the citation surface is the suggested tier (top-5, + # no floor) + the agent's reads (none on this turn) — deduped. + assert len(sources) == get_settings().suggested_docs assert sources[0]["path"] == "homelab/kubernetes.md" assert sources[0]["source"] == "docs" assert sources[0]["title"] == "Kubernetes Homelab Cluster" - # The LLM received the locked HIGH prompt with the FULL document text. + # The LLM received the locked HIGH prompt — the ```` block + # seeds the document's stored SUMMARY (phase 118, LOCKED A6: summary + # seeding re-revises the pre-phase full-text contract; the full text + # reaches the context only through the capped ``read`` tool). The + # summarizer's code-appended pointer line proves the summary block is + # present; the doc's full body is no longer seeded. (system, user) = seeded_kb.seen_messages[0][0], seeded_kb.seen_messages[0][1] assert user["content"] == QUESTION assert "HIGH" in system["content"] assert "DEFLECT_MODE" not in system["content"] assert "" in system["content"] - assert "Talos Linux" in system["content"] # full doc, not just the chunk + section = system["content"].split("", 1)[1].split("", 1)[0] + assert "Summary of" in section # the fake lite model's summary text + assert "Source: docs/homelab/kubernetes.md" in section # code-appended pointer + assert "Talos Linux" not in section # full doc no longer seeded (A6) assert "HONESTY GATE" in system["content"] @@ -382,7 +393,19 @@ def test_chat_writes_query_log_row(client, db, seeded_kb: FakeRagLLM) -> None: assert 1 <= row.chunk_hits <= total_chunks assert row.top_score > 0.0 # genuine token-overlap cosine, best hit assert row.top_score <= 1.0 - assert "docs/homelab/kubernetes.md" in row.sources + # Phase 118 (LOCKED A3): the durable record is the FULL retrieval — + # the suggested tier (ranks 1–5) + the related tier (ranks 6–7) + + # the agent's reads (none on this turn), for this question. + for path in ( + "docs/homelab/kubernetes.md", # rank 1 + "docs/homelab/templates/deploy.j2", # rank 2 + "docs/homelab/ssh/ssh_aliases.txt", # rank 3 + "docs/homelab/container_gitlab/gitlab.md", # rank 4 + "docs/deployments/new-service.md", # rank 5 + "docs/homelab/quadlet/cache.volume", # rank 6 (related) + "docs/homelab/quadlet/compose.container", # rank 7 (related) + ): + assert path in row.sources assert row.latency_ms >= 0 # Why the gate answered (A8 revised): cosine over the threshold OR a # lexical hit. The mock-calibrated threshold (0.30, see tests/conftest.py) @@ -446,10 +469,12 @@ def test_off_topic_question_deflects_honestly(client, db, seeded_kb: FakeRagLLM) def test_done_frame_carries_related_tier_on_grounded_turn( client, db, seeded_kb: FakeRagLLM ) -> None: - """Phase 113 (LOCKED A4): a grounded turn's done frame carries the - related tier — the ranked docs beyond the cited ceiling, capped at - ``related_max_docs`` (2), disjoint from the cited list. The durable - record keeps the FULL retrieval (cited + related, LOCKED A3).""" + """Phase 118 (LOCKED A3/A4): a grounded turn's done frame carries the + suggested tier in ``sources`` (top-5, no floor) and the related + tier — the ranked docs from rank 6+ after the suggested set, capped + at ``related_max_docs`` (2) — in ``related``, disjoint from the + citation surface. The durable record keeps the FULL retrieval + (suggested + related + read, LOCKED A3).""" fastapi_app.dependency_overrides[chat_api.get_llm] = lambda: seeded_kb try: _, _, frames = _stream_chat(client, QUESTION) @@ -458,49 +483,54 @@ def test_done_frame_carries_related_tier_on_grounded_turn( done = frames[-1] assert done["deflected"] is False - sources = {(s["source"], s["path"]) for s in done["sources"]} + # A4: the citation surface is the suggested tier (5, no read on this + # turn) — ranks 1–5 for the Kubernetes question. + sources = [(s["source"], s["path"]) for s in done["sources"]] + assert len(sources) == get_settings().suggested_docs + assert sources[0] == ("docs", "homelab/kubernetes.md") related = done["related"] - assert related, "the 2nd-and-lower scored docs ride the related tier" + # Rank 6–7 for the Kubernetes question (after the top-5 suggested + # set), capped at related_max_docs. + assert [(s["source"], s["path"]) for s in related] == [ + ("docs", "homelab/quadlet/cache.volume"), + ("docs", "homelab/quadlet/compose.container"), + ] assert len(related) <= get_settings().related_max_docs - # The related tier never overlaps the cited list (the dedupe is by - # (source, path) — the same pattern as the cited docs). + # The related tier never overlaps the citation surface (the dedupe is + # by (source, path) — the same pattern as the cited docs). related_keys = {(s["source"], s["path"]) for s in related} - assert sources.isdisjoint(related_keys) - # Rank order: the cited top-2 are the kubernetes doc and the template; - # the next ranked doc is the ssh aliases file. - assert related[0]["path"] == "homelab/ssh/ssh_aliases.txt" + assert set(sources).isdisjoint(related_keys) # Every ref carries the chip identity fields (the UI row reuses them). assert all(s["title"] for s in related) - # Durable record: the full retrieval (cited + related) is logged. + # Durable record: the full retrieval (suggested + related) is logged. row = db.scalars(select(QueryLog)).one() - assert "docs/homelab/ssh/ssh_aliases.txt" in row.sources + assert "docs/homelab/quadlet/cache.volume" in row.sources assert "docs/homelab/kubernetes.md" in row.sources def test_deflected_done_frame_carries_weak_hits_in_related( - client, db, seeded_kb: FakeRagLLM, monkeypatch: pytest.MonkeyPatch + client, db, seeded_kb: FakeRagLLM ) -> None: - """Phase 113: on a deflected turn nothing clears the bar — the cited - tier is empty (done.sources stays [], the phase-112 contract) and the - weak hits fall to the related tier (their visibility home). The - durable record still carries the retrieval (LOCKED A3).""" - monkeypatch.setenv("BOR_SOURCE_USEFULNESS_FLOOR", "0.20") - get_settings.cache_clear() + """Phase 118: on a deflected turn done.sources stays [] (the + phase-112 contract — a deflected answer cites nothing) and + done.related carries rank 6+ after the suggested set (capped at + ``related_max_docs``) — the weak hits' visibility home; the weak + hits themselves are the suggested tier (no floor, A3). The durable + record still carries the retrieval (LOCKED A3).""" fastapi_app.dependency_overrides[chat_api.get_llm] = lambda: seeded_kb try: _, _, frames = _stream_chat(client, OFF_TOPIC) done = frames[-1] assert done["deflected"] is True assert done["sources"] == [] # a deflected answer cites nothing - # The weak hits (the sourdough question's best mock cosines are - # ~0.11/0.04 — both below the 0.20 bar) ride the related tier, in - # rank order, capped at related_max_docs. + # Rank 6–7 for the sourdough question (after the top-5 suggested + # set), capped at related_max_docs. related = done["related"] assert len(related) <= get_settings().related_max_docs - assert [s["path"] for s in related][:2] == [ - "deployments/new-service.md", - "homelab/quadlet/lan.network", + assert [s["path"] for s in related] == [ + "homelab/backups.md", + "homelab/container_gitlab/gitlab-compose.yaml", ] assert all(s["title"] for s in related) assert done["suggestions"] # the "Maybe try" chips are unchanged @@ -510,19 +540,16 @@ def test_deflected_done_frame_carries_weak_hits_in_related( assert row.deflected is True assert row.sources # the weak-hit paths, for threshold tuning finally: - # The cache clear is LAST — an assertion that calls get_settings() - # after the clear would re-populate the lru_cache with the - # monkeypatched value and leak it into the next test. fastapi_app.dependency_overrides.clear() - get_settings.cache_clear() def test_related_doc_read_by_agent_is_cited_not_related( client, db, seeded_kb: FakeRagLLM ) -> None: - """Phase 113 × phase 37: an agent-read doc is a citation by definition - — when the agent ``read``s a doc that would otherwise ride the related - tier, it joins done.sources (deduped, last) and is EXCLUDED from + """Phase 118 × phase 37: an agent-read doc is a citation by definition + (LOCKED A4) — when the agent ``read``s a rank-6+ doc (the related + tier, "nearby docs"), it joins done.sources (deduped, last — it was + not suggested, so the read appends it) and is EXCLUDED from done.related (a "nearby doc" that was actually used must not read as nearby).""" scripted = FakeRagLLM( @@ -531,7 +558,7 @@ def test_related_doc_read_by_agent_is_cited_not_related( ToolCallPiece( id="call_1", name="read", - arguments={"path": "docs/homelab/ssh/ssh_aliases.txt"}, + arguments={"path": "docs/homelab/quadlet/cache.volume"}, ) ] ] @@ -545,12 +572,14 @@ def test_related_doc_read_by_agent_is_cited_not_related( done = frames[-1] assert done["deflected"] is False sources = [(s["source"], s["path"]) for s in done["sources"]] - assert sources[-1] == ("docs", "homelab/ssh/ssh_aliases.txt") # read ⇒ cited + # A4: suggested (5) + the read doc (last). + assert len(sources) == get_settings().suggested_docs + 1 + assert sources[-1] == ("docs", "homelab/quadlet/cache.volume") # read ⇒ cited related = [(s["source"], s["path"]) for s in done["related"]] - assert ("docs", "homelab/ssh/ssh_aliases.txt") not in related + assert ("docs", "homelab/quadlet/cache.volume") not in related assert set(sources).isdisjoint(set(related)) - # The OTHER related-tier doc (gitlab, rank 4) stays in the tier. - assert ("docs", "homelab/container_gitlab/gitlab.md") in related + # The OTHER related-tier doc (compose.container, rank 7) stays in the tier. + assert ("docs", "homelab/quadlet/compose.container") in related def test_keyword_question_grounded_by_lexical_hit_despite_weak_cosine( @@ -1341,6 +1370,11 @@ def test_deflected_turn_stays_byte_identical_without_tools( direct-``chat_stream`` output even for a fake scripted to call tools (its script is never consumed). The LLM was called once, without a ``tools`` key.""" + # The scripted read targets a doc OUTSIDE the OFF_TOPIC retrieval + # top-7 (tables.md ranks 11th — not suggested, not rank 6+ related), + # so "never read" stays distinguishable from "retrieved" in the + # durable record below (phase 118: backups.md — the pre-phase read + # target — now rides the rank-6+ related tier, durably recorded). scripted = FakeRagLLM( tool_script=[ [ToolCallPiece(id="call_1", name="ls", arguments={})], @@ -1348,7 +1382,7 @@ def test_deflected_turn_stays_byte_identical_without_tools( ToolCallPiece( id="call_2", name="read", - arguments={"path": "docs/homelab/backups.md"}, + arguments={"path": "docs/homelab/tables.md"}, ) ], [StreamPiece("content", "never used — the agent never runs")], @@ -1380,7 +1414,7 @@ def test_deflected_turn_stays_byte_identical_without_tools( if r.question == OFF_TOPIC ][-1:] assert row.deflected is True - assert "backups.md" not in row.sources + assert "tables.md" not in row.sources def test_zero_max_rounds_reproduce_pre_phase_single_request( diff --git a/tests/integration/test_chats_api.py b/tests/integration/test_chats_api.py index 7018640..f19bc89 100644 --- a/tests/integration/test_chats_api.py +++ b/tests/integration/test_chats_api.py @@ -63,6 +63,16 @@ FULL_BRAIN: dict[str, Any] = { "sources": [ {"source": "Homelab", "path": "kubernetes.md", "title": "Kubernetes Cluster"} ], + # Phase 113's related-doc tier — the UI persists it with every + # grounded brain record (the restore path re-renders the row from + # it). It must be an ACCEPTED key: the phase-113 omission (the key + # missing from ChatMessage) made the extra="forbid" boundary 422 + # every done-time auto-save carrying it, so grounded turns' brain + # messages never persisted (the A2 quiet failure swallowed the + # 422). This round-trip is the regression pin. + "related": [ + {"source": "Homelab", "path": "traefik.md", "title": "Traefik Notes"} + ], "deflected": False, "suggestions": ["What ports does Traefik expose?"], "thinking": "The kubernetes doc covers the cluster layout…", @@ -175,6 +185,7 @@ def _expect(records: list[dict[str, Any]]) -> list[dict[str, Any]]: "who": m["who"], "text": m["text"], "sources": m.get("sources"), + "related": m.get("related"), "deflected": m.get("deflected"), "suggestions": m.get("suggestions"), "thinking": m.get("thinking"), @@ -411,8 +422,8 @@ def test_create_round_trips_full_brain_record(admin_client: TestClient) -> None: ) assert r.status_code == 201 # The bor.chat.v1-shaped payload round-trips losslessly: every - # optional key (sources/deflected/suggestions/thinking/tools/ - # stopped) survives identical. + # optional key (sources/related/deflected/suggestions/thinking/ + # tools/stopped) survives identical. assert r.json()["messages"][1] == FULL_BRAIN @@ -516,9 +527,9 @@ def test_get_returns_full_payload_round_trip(admin_client: TestClient) -> None: assert body["id"] == created["id"] assert body["title"] == EXPLICIT_TITLE assert body["message_count"] == 2 - # Byte-identical payload: the brain record with sources/thinking/ - # tools/stopped (incl. the `argument: null` tool) survives the trip - # to Postgres and back. + # Byte-identical payload: the brain record with sources/related/ + # thinking/tools/stopped (incl. the `argument: null` tool) survives + # the trip to Postgres and back. assert body["messages"] == _expect([_user(FIRST_QUESTION), FULL_BRAIN]) diff --git a/tests/integration/test_import_docs_overview.py b/tests/integration/test_import_docs_overview.py index d5f453f..e70361d 100644 --- a/tests/integration/test_import_docs_overview.py +++ b/tests/integration/test_import_docs_overview.py @@ -30,8 +30,10 @@ ends with the folder-summary stats — (this fixture's 2-doc source holds exactly ONE qualifying subtree: the source root) or ``folder_summaries=skipped`` otherwise — so the line pinned here gains that token, and a KB-changing run burns exactly ONE -extra ``lite`` call (the source-root folder summary, markdown files -never get a document summary). +extra ``lite`` call beyond the phase-118 document summaries (the +source-root folder summary; markdown files get document summaries too +since phase 118, A2 — so this 2-doc markdown source burns TWO doc-summary +calls on a fresh import). """ from __future__ import annotations @@ -97,7 +99,8 @@ def _run_main( @pytest.fixture() def src(tmp_path: Path) -> Path: - """A source dir with two markdown docs (md → no summary chat calls).""" + """A source dir with two markdown docs (phase 118, A2: both get + document summaries — two extra ``chat`` calls over pre-118).""" root = tmp_path / "MyDocs" root.mkdir() (root / "alpha.md").write_text("# Alpha\n\nFirst document.\n", encoding="utf-8") @@ -164,16 +167,18 @@ def test_changed_import_writes_overview_row( "overview=updated sources_version=1 folder_summaries=1/0/0" ) assert _version(db) == 1 # phase 53: a changed import bumps exactly once - # Exactly two lite calls — the overview + the source-root folder - # summary (markdown files never get a document summary, so nothing - # else may touch ``chat``). - assert len(llm.chat_calls) == 2 - by_role = {m["role"]: m["content"] for m in llm.chat_calls[0]} + # Exactly four lite calls — the two phase-118 document summaries + # (markdown included) + the overview + the source-root folder summary + # (nothing else may touch ``chat``). + assert len(llm.chat_calls) == 4 + by_role = {m["role"]: m["content"] for m in llm.chat_calls[2]} assert "KB_OVERVIEW_MODE" in by_role["system"] - # One line per doc: source — path — title (no summary for markdown). - assert "MyDocs — alpha.md — Alpha" in by_role["user"] - assert "MyDocs — beta.md — Beta" in by_role["user"] - by_role = {m["role"]: m["content"] for m in llm.chat_calls[1]} + # One line per doc: source — path — title — first summary line + # (phase 118: the markdown docs are summarized too — the fake's + # deterministic digest for each). + assert "MyDocs — alpha.md — Alpha — Summary of #" in by_role["user"] + assert "MyDocs — beta.md — Beta — Summary of #" in by_role["user"] + by_role = {m["role"]: m["content"] for m in llm.chat_calls[3]} assert "FOLDER_SUMMARY_MODE" in by_role["system"] assert by_role["user"].splitlines()[0] == "Folder: MyDocs" # The model's outline lands in the single row. @@ -197,17 +202,19 @@ def test_unchanged_reimport_does_not_call_lite( assert out.rstrip().endswith( "overview=updated sources_version=1 folder_summaries=1/0/0" ) - assert len(llm.chat_calls) == 2 # overview + source-root folder summary + # 2 doc summaries (phase 118) + overview + source-root folder summary. + assert len(llm.chat_calls) == 4 assert _row(db) is not None - # Same hashes → no KB change → no lite call, previous outline kept. + # Same hashes → no KB change → no lite call, previous outline kept — + # and nothing to backfill (both summaries are already stored). rc, out = _run_main(monkeypatch, llm, ["--source", str(src)], capsys) assert rc == 0 assert "unchanged=2" in out assert out.rstrip().endswith( "overview=skipped sources_version=skipped folder_summaries=skipped" ) - assert len(llm.chat_calls) == 2 # no new lite call + assert len(llm.chat_calls) == 4 # no new lite call row = _row(db) assert row is not None and row.content == "Summary of MyDocs" assert _version(db) == 1 # phase 53: an unchanged re-run never bumps @@ -241,7 +248,10 @@ def test_lite_failure_is_fail_soft( assert out.rstrip().endswith( "overview=failed sources_version=2 folder_summaries=0/1/0" ) - assert len(bad.chat_calls) == 2 # the (failed) attempts were made + # The three (failed) attempts: the changed doc's summary (phase 118), + # the overview, and the folder summary — the unchanged, already- + # summarized doc burns no backfill. + assert len(bad.chat_calls) == 3 row = _row(db) assert row is not None assert row.content == previous_content # previous row untouched @@ -262,7 +272,7 @@ def test_limit_run_skips_overview( assert out.rstrip().endswith( "overview=updated sources_version=1 folder_summaries=1/0/0" ) - assert len(llm.chat_calls) == 2 + assert len(llm.chat_calls) == 4 # 2 doc summaries + overview + folder # An incomplete walk must not rewrite the outline (mirrors the # --prune-with---limit guard) — and must not advance the version. @@ -273,7 +283,9 @@ def test_limit_run_skips_overview( assert out.rstrip().endswith( "overview=skipped sources_version=skipped folder_summaries=skipped" ) - assert len(llm.chat_calls) == 2 # --limit never burns a lite call + # --limit walks only alpha.md: its changed summary is the sole new + # lite call; the overview + folder gates skip under --limit. + assert len(llm.chat_calls) == 5 row = _row(db) assert row is not None and row.content == "Summary of MyDocs" assert _version(db) == 1 # phase 53: --limit debug runs never bump @@ -323,6 +335,7 @@ def test_prune_only_run_bumps_sources_version( # Delete one file; a --prune run drops exactly it: no add/update, # but pruned=1 → the version still bumps while the overview skips. + assert len(llm.chat_calls) == 4 # 2 doc summaries + overview + folder (src / "alpha.md").unlink() rc, out = _run_main(monkeypatch, llm, ["--source", str(src), "--prune"], capsys) assert rc == 0 diff --git a/tests/integration/test_import_extensionless.py b/tests/integration/test_import_extensionless.py index 15dddc8..886a6eb 100644 --- a/tests/integration/test_import_extensionless.py +++ b/tests/integration/test_import_extensionless.py @@ -16,8 +16,8 @@ fixture of phase 56 stays pinned by its own suite): * positive — ``md,dockerfile,containerfile`` walks ``Dockerfile``, ``Containerfile`` and ``notes.md`` (the ``Makefile`` negative control stays out): rows + embedded chunks + the deterministic mock - ``SUMMARY_MODE`` digest for both build files (non-markdown → the - phase-30 ``lite`` path), ``formats == {"dockerfile": 1, + ``SUMMARY_MODE`` digest for EVERY doc (phase 118, A2: markdown + included — the phase-30 ``lite`` path), ``formats == {"dockerfile": 1, "containerfile": 1, "md": 1}`` with NO ``unknown`` key; * case — an on-disk ``DOCKERFILE`` imports under the ``dockerfile`` token, its row keeps the on-disk case; @@ -146,9 +146,9 @@ def test_name_token_files_import_end_to_end(mock_llm_port: int, db: Session) -> # D2: extensionless files count under their matched token. assert summary.formats == {"dockerfile": 1, "containerfile": 1, "md": 1} assert "unknown" not in summary.formats - # Phase 30: both build files are non-markdown → lite summaries; - # the markdown control never is. - assert (summary.summaries, summary.summary_errors) == (2, 0) + # Phase 118 (A2): EVERY doc gets a lite summary — both build + # files AND the markdown control. + assert (summary.summaries, summary.summary_errors) == (3, 0) for rel, sentinel, tokens in ( (DOCKER_REL, DOCKER_SENTINEL, DOCKER_SENTINEL_TOKENS), @@ -179,13 +179,18 @@ def test_name_token_files_import_end_to_end(mock_llm_port: int, db: Session) -> assert len(schunks) == 1 and schunks[0].position == -1 assert schunks[0].embedding is not None - # The markdown control doc imported too — but markdown never - # gets a summary (phase 30). + # The markdown control doc imported too — AND got the mock + # ``SUMMARY_MODE`` digest (phase 118, A2: markdown summarized). note = db.scalar( select(Document).where(Document.source == SOURCE, Document.path == NOTES_REL) ) assert note is not None - assert note.summary is None + assert note.summary is not None + assert note.summary.startswith("This document covers extensionless fixture notes") + assert f"Source: {SOURCE}/{NOTES_REL}" in note.summary + note_schunks = [c for c in note.chunks if c.is_summary] + assert len(note_schunks) == 1 and note_schunks[0].position == -1 + assert note_schunks[0].embedding is not None assert [c for c in note.chunks if not c.is_summary] # The negative control: no token names Makefile exactly — never @@ -243,7 +248,8 @@ def test_without_tokens_the_extensionless_files_stay_out( summary.files, summary.added, summary.unchanged, summary.updated, summary.errors ) == (1, 1, 0, 0, 0) assert summary.formats == {"md": 1} - assert summary.summaries == 0 + # Phase 118 (A2): the md-only run still summarizes the note. + assert summary.summaries == 1 for rel in (DOCKER_REL, CONTAINER_REL, "Makefile"): assert ( db.scalar( diff --git a/tests/integration/test_import_extensions_env.py b/tests/integration/test_import_extensions_env.py index 388135e..21bc7d1 100644 --- a/tests/integration/test_import_extensions_env.py +++ b/tests/integration/test_import_extensions_env.py @@ -117,8 +117,9 @@ def test_novel_extension_imports_end_to_end(mock_llm_port: int, db: Session) -> summary.files, summary.added, summary.unchanged, summary.updated, summary.errors ) == (2, 2, 0, 0, 0) assert summary.formats == {"sh": 1, "md": 1} - # The .sh file is non-markdown → exactly one lite summary (phase 30). - assert (summary.summaries, summary.summary_errors) == (1, 0) + # Phase 118 (A2): EVERY doc gets a lite summary — the novel + # ``.sh`` file AND the markdown control. + assert (summary.summaries, summary.summary_errors) == (2, 0) sh = db.scalar( select(Document).where( @@ -148,15 +149,20 @@ def test_novel_extension_imports_end_to_end(mock_llm_port: int, db: Session) -> assert len(schunks) == 1 and schunks[0].position == -1 assert schunks[0].embedding is not None - # The markdown control doc imported too — but markdown never - # gets a summary (phase 30). + # The markdown control doc imported too — AND got the mock + # ``SUMMARY_MODE`` digest (phase 118, A2: markdown summarized). note = db.scalar( select(Document).where( Document.source == SOURCE, Document.path == MD_REL ) ) assert note is not None - assert note.summary is None + assert note.summary is not None + assert note.summary.startswith("This document covers extension fixture note") + assert f"Source: {SOURCE}/{MD_REL}" in note.summary + note_schunks = [c for c in note.chunks if c.is_summary] + assert len(note_schunks) == 1 and note_schunks[0].position == -1 + assert note_schunks[0].embedding is not None assert [c for c in note.chunks if not c.is_summary] finally: _cleanup_source(db, SOURCE) @@ -175,7 +181,8 @@ def test_narrowing_to_md_still_excludes_the_novel_extension( summary.files, summary.added, summary.unchanged, summary.updated, summary.errors ) == (1, 1, 0, 0, 0) assert summary.formats == {"md": 1} - assert summary.summaries == 0 + # Phase 118 (A2): the md-only run still summarizes the note. + assert summary.summaries == 1 assert db.scalar( select(Document).where( Document.source == SOURCE, Document.path == SH_REL diff --git a/tests/integration/test_importer_e2e.py b/tests/integration/test_importer_e2e.py index e21072e..1d31518 100644 --- a/tests/integration/test_importer_e2e.py +++ b/tests/integration/test_importer_e2e.py @@ -78,8 +78,9 @@ def test_import_fixtures_end_to_end(admin_client, db) -> None: k8s = next(d for d in docs if d.path == "homelab/kubernetes.md") assert "Talos Linux" in k8s.content and k8s.content_hash - # Phase 30: the four non-markdown fixtures each gained one embedded - # ``is_summary`` chunk, so the DB holds content + summary chunks. + # Phase 30; phase 118 (A2): EVERY fixture gained one embedded + # ``is_summary`` chunk (markdown included), so the DB holds content + + # summary chunks. n_chunks = db.scalar(select(func.count()).select_from(Chunk)) assert n_chunks == summary.chunks + summary.summaries for c in db.scalars(select(Chunk)).all(): @@ -87,21 +88,15 @@ def test_import_fixtures_end_to_end(admin_client, db) -> None: assert summary.summary_errors == 0 for d in docs: - non_md = Path(d.path).suffix.lower() not in (".md", ".markdown") + # Phase 118 (A2): EVERY document — markdown included — carries a + # stored summary + exactly one embedded summary chunk (−1). schunks = [c for c in d.chunks if c.is_summary] - if non_md: - # Lite summary stored + exactly one embedded summary chunk (−1). - assert d.summary is not None, f"{d.path} should have a summary" - assert len(schunks) == 1 - assert schunks[0].position == -1 - assert schunks[0].content == d.summary - assert schunks[0].embedding is not None - else: - # Markdown docs never get a summary (phase 30 scope). - assert d.summary is None and not schunks - assert summary.summaries == sum( - 1 for d in docs if Path(d.path).suffix.lower() not in (".md", ".markdown") - ) + assert d.summary is not None, f"{d.path} should have a summary" + assert len(schunks) == 1 + assert schunks[0].position == -1 + assert schunks[0].content == d.summary + assert schunks[0].embedding is not None + assert summary.summaries == len(docs) # one per doc, no exceptions # The Sources page consumes exactly this shape. r = admin_client.get("/api/docs") # phase 16: the catalog is admin-only diff --git a/tests/integration/test_importer_ignore.py b/tests/integration/test_importer_ignore.py index 597dc09..4f2f55e 100644 --- a/tests/integration/test_importer_ignore.py +++ b/tests/integration/test_importer_ignore.py @@ -60,8 +60,9 @@ def test_ignored_files_never_indexed(db: Session, tmp_path: Path) -> None: # invisible to the pipeline. assert summary.files == 2 assert summary.errors == 0 - # The kept non-markdown file IS summarized; the ignored .txt is not. - assert summary.summaries == 1 + # Both kept files ARE summarized (phase 118, A2: markdown too); + # the ignored files are never walked, hence never summarized. + assert summary.summaries == 2 assert summary.summary_errors == 0 docs = db.scalars(select(Document)).all() @@ -78,12 +79,17 @@ def test_ignored_files_never_indexed(db: Session, tmp_path: Path) -> None: assert not any( "SECRET-CONTENT" in t or "IGNORED-TEXT-CONTENT" in t for t in texts ) - # Exactly one summary call (top.txt) — the ignored files never reached - # the lite model. - assert len(llm.chat_calls) == 1 - user = next(m["content"] for m in llm.chat_calls[0] if m["role"] == "user") - assert "TOP-TEXT-CONTENT" in user - assert "SECRET-CONTENT" not in user and "IGNORED-TEXT-CONTENT" not in user + # Exactly one summary call per kept file (keep.md + top.txt) — the + # ignored files never reached the lite model. + assert len(llm.chat_calls) == 2 + users = [ + next(m["content"] for m in msgs if m["role"] == "user") + for msgs in llm.chat_calls + ] + assert any("TOP-TEXT-CONTENT" in u for u in users) + assert any("kept body" in u for u in users) + for u in users: + assert "SECRET-CONTENT" not in u and "IGNORED-TEXT-CONTENT" not in u top = next(d for d in docs if d.path == "top.txt") assert top.summary is not None _reset(db) diff --git a/tests/integration/test_importer_include_hidden.py b/tests/integration/test_importer_include_hidden.py index fb13eb9..654eb62 100644 --- a/tests/integration/test_importer_include_hidden.py +++ b/tests/integration/test_importer_include_hidden.py @@ -62,7 +62,8 @@ def test_hidden_paths_not_indexed_by_default(db: Session, tmp_path: Path) -> Non assert summary.files == 1 assert summary.added == 1 assert summary.errors == 0 - assert summary.summaries == 0 + # Phase 118 (A2): the one visible md IS summarized. + assert summary.summaries == 1 assert summary.summary_errors == 0 docs = db.scalars(select(Document)).all() @@ -73,8 +74,9 @@ def test_hidden_paths_not_indexed_by_default(db: Session, tmp_path: Path) -> Non for t in texts: assert "HIDDEN-MD-CONTENT" not in t and "HIDDEN-YAML-VALUE" not in t assert "EXCLUDED-CONTENT" not in t - # The hidden yaml never reached the lite model. - assert not llm.chat_calls + # Only the visible md reached the lite model (phase 118, A2) — the + # hidden files were never walked. + assert len(llm.chat_calls) == 1 _reset(db) @@ -105,14 +107,14 @@ def test_hidden_paths_indexed_when_flag_on(db: Session, tmp_path: Path) -> None: assert not any(d.path == ".venv/junk.md" for d in docs) chunks = db.scalars(select(Chunk)).all() assert not any("EXCLUDED-CONTENT" in c.content for c in chunks) - # The hidden md was embedded like any visible md (no summary — the - # markdown path skips the lite model). + # The hidden md was embedded like any visible md — AND summarized + # like any other doc (phase 118, A2: markdown included). note = db.scalar(select(Document).where(Document.path == ".hidden/note.md")) - assert note is not None and note.summary is None + assert note is not None and note.summary is not None assert any("HIDDEN-MD-CONTENT" in c.content for c in chunks) - # The hidden yaml went through the FULL non-markdown path (phase 30): - # a stored summary plus one embedded is_summary chunk on top of the - # content chunks. + # The hidden yaml went through the full doc path (phase 30): a stored + # summary plus one embedded is_summary chunk on top of the content + # chunks. yaml_doc = db.scalar(select(Document).where(Document.path == ".hidden/data.yaml")) assert yaml_doc is not None and yaml_doc.summary is not None yaml_chunks = [ @@ -121,11 +123,16 @@ def test_hidden_paths_indexed_when_flag_on(db: Session, tmp_path: Path) -> None: assert any(c.is_summary for c in yaml_chunks) assert any(not c.is_summary for c in yaml_chunks) assert any("HIDDEN-YAML-VALUE" in c.content for c in yaml_chunks) - # Only the yaml reached the lite model. - assert summary.summaries == 1 - assert len(llm.chat_calls) == 1 - user = next(m["content"] for m in llm.chat_calls[0] if m["role"] == "user") - assert "HIDDEN-YAML-VALUE" in user + # EVERY doc reached the lite model (phase 118, A2: markdown too). + assert summary.summaries == 3 + assert len(llm.chat_calls) == 3 + users = [ + next(m["content"] for m in msgs if m["role"] == "user") + for msgs in llm.chat_calls + ] + assert any("HIDDEN-YAML-VALUE" in u for u in users) + assert any("HIDDEN-MD-CONTENT" in u for u in users) + assert any("visible body" in u for u in users) _reset(db) diff --git a/tests/integration/test_sync_folder_summaries.py b/tests/integration/test_sync_folder_summaries.py index b3be71f..7c05241 100644 --- a/tests/integration/test_sync_folder_summaries.py +++ b/tests/integration/test_sync_folder_summaries.py @@ -579,7 +579,8 @@ def test_limit_run_skips_folder_generation( capsys: pytest.CaptureFixture[str], ) -> None: """A ``--limit`` debug run (an incomplete walk) never generates — - no ``lite`` call at all, no rows, the line says ``skipped``.""" + no overview/folder ``lite`` call, no rows, the line says + ``skipped``.""" llm = FakeEmbedder() rc, out = _run_main(monkeypatch, llm, ["--source", str(src), "--limit", "2"], capsys) assert rc == 0 @@ -587,7 +588,10 @@ def test_limit_run_skips_folder_generation( assert out.rstrip().endswith( "overview=skipped sources_version=skipped folder_summaries=skipped" ) - assert llm.chat_calls == [] # no lite call, any mode + # Phase 118 (A2): the two walked docs still burn their document + # summaries — but the folder generation never fires (and neither + # does the overview). + assert len(llm.chat_calls) == 2 # doc summaries only, any other mode absent assert _rows(db) == {} # an incomplete walk never writes rows diff --git a/tests/unit/test_agent.py b/tests/unit/test_agent.py index 9b5112c..cd3a4cf 100644 --- a/tests/unit/test_agent.py +++ b/tests/unit/test_agent.py @@ -221,50 +221,71 @@ def test_agent_tools_names_and_parameters() -> None: "'homelab/active'). Omit it to list every source." ) read = by_name["read"]["function"] - # Tool-calling fast loop (2026-09-04, controlled fixture gate): - # the do-not-read rule is FRONT-LOADED — the controlled gate's - # telemetry showed the `lite` model obeying the user's "open it / - # read it" and reading seed-context documents the - # section already carries (every refusal of a 12-call run was - # ALREADY_IN_CONTEXT); the rule now leads the description instead - # of sitting mid-paragraph, and the tool is framed as "only for - # documents NOT already in ". Phase 95 (task 01): the - # read-truncation sentence is inserted before the one-call-at-a- - # time discipline clause (the discipline rule stays last, as in the - # other two tools) — a capped read carries the TRUNCATED notice and - # the `grep` follow-up (the pinned copy). + # Phase 118 (task 04, A6): the read description is rewritten for + # the summary-seed mode — the section shows the + # top-ranked documents' SUMMARIES (their full texts are NOT in + # the prompt yet); read adds one of them (or any other document) + # to the context; only an already-read document is refused. The + # combined-identity handoff to the `ls` output / the + # blocks stays; the truncation-notice paragraph (phase 95) and + # the one-call-at-a-time discipline clause (task 05) survive the + # rewrite byte-identical. assert read["description"] == ( - "Do not call this tool for a document already shown in " - "the section, even when the user asks you to " - "open or read it — its full text is already in your " - "prompt; answer directly from it. Use it only to add a " - "document NOT already in to your context, " - "by its combined `source/path` string. Very large " - "documents are truncated: you receive the first part " - "plus a TRUNCATED notice naming how many more characters " - "exist — the notice is authoritative, the document did " - "NOT end where it stopped. Follow it and use `grep` " - "(pattern) to locate the rest — it searches the whole " - "document. Call one tool at a time — wait for this " - "result before your next call." + "The section shows the SUMMARIES of the " + "top-ranked documents — their full texts are NOT in " + "your prompt yet. Use this tool to add one of them (or " + "any other document) to your context, by its combined " + "`source/path` string, exactly as shown in the `ls` " + "output or the blocks. Do not re-read a " + "document you have already read — its full text is " + "already in your prompt. Very large documents are " + "truncated: you receive the first part plus a TRUNCATED " + "notice naming how many more characters exist — the " + "notice is authoritative, the document did NOT end " + "where it stopped. Follow it and use `grep` (pattern) " + "to locate the rest — it searches the whole document. " + "Call one tool at a time — wait for this result before " + "your next call." ) + # The byte-preserved contracts inside the rewrite, pinned as + # substrings: the combined source/path identity, the + # truncation-notice paragraph, the one-call-at-a-time + # discipline sentence. + assert ( + "by its combined `source/path` string, exactly as shown in " + "the `ls` output or the blocks" + ) in read["description"] + assert ( + "Very large documents are truncated: you receive the first " + "part plus a TRUNCATED notice naming how many more " + "characters exist — the notice is authoritative, the " + "document did NOT end where it stopped. Follow it and use " + "`grep` (pattern) to locate the rest — it searches the " + "whole document." + ) in read["description"] + assert ( + "Call one tool at a time — wait for this result before " + "your next call." + ) in read["description"] read_params = read["parameters"] assert read_params["type"] == "object" assert read_params["required"] == ["path"] assert set(read_params["properties"]) == {"path"} assert read_params["properties"]["path"]["type"] == "string" # The combined source/path string is the canonical document identity - # (phase 70) — the description pins it with a worked example. Phase - # 72 (task 02): the bare-path contract is stated up front; task 05 - # (live gate iteration 1): the do-not-re-read clause (the dedupe - # refusal's prevention at the prompt). + # (phase 70) — the description pins it with a worked example; the + # bare-path contract is stated up front (phase 72, task 02). Phase + # 118 (task 04, A6): the trailing clause is the do-not-RE-READ + # teaching — the seeds are summary blocks, not full text, so a + # first read of a suggested document succeeds and only an + # already-read document is refused. assert read_params["properties"]["path"]["description"] == ( "The document to add to your context, as the combined " "`source/path` string exactly as shown in the `ls` output (e.g. " "'homelab/active/container_caddy/caddy.md'). A bare document " - "path (without the source name) will not resolve. Only pass a " - "document NOT already shown in the section — it is " - "already in your context; do not re-read it." + "path (without the source name) will not resolve. Do not " + "re-read a document you have already read — its full " + "text is already in your prompt." ) grep = by_name["grep"]["function"] # Task 05 (live gate iterations 2-6, refined in the 2026-09-03 @@ -1455,15 +1476,17 @@ def test_read_bare_filename_without_slash_keeps_no_db_refusal( assert llm.requests[1][1] == AGENT_TOOLS -def test_read_bare_path_of_seed_doc_gets_suggestion_then_dedupe( +def test_read_bare_path_of_suggested_doc_gets_suggestion_then_succeeds( monkeypatch: pytest.MonkeyPatch, ) -> None: - """Dedupe precedence: the in-context dedupe fires on the SPLIT pair - of the argument — the bare path of an in-context document - (``read('app/rag/importer.py')`` with ``sample/app/rag/importer.py`` - seeded) is NOT that pair, so it is not a dedupe: it gets the - suggestion line naming the combined identity, and the model's next, - correctly-formed call is then deduped as ALREADY_IN_CONTEXT.""" + """Dedupe precedence (phase 118): the in-context dedupe fires on + the SPLIT pair of a document already READ — the bare path of a + SUGGESTED (seeded) document (``read('app/rag/importer.py')`` with + ``sample/app/rag/importer.py`` seeded) is NOT that pair, so it is + not a dedupe: it gets the suggestion line naming the combined + identity, and the model's next, correctly-formed call ADDS the + suggested document's full text (A6: the seeds are summary blocks, + not full text — a first read of a suggested document succeeds).""" seed = [_doc("sample", "app/rag/importer.py", "Importer", "IMPORTER")] def _find(db: Any, source: str, path: str) -> Document | None: @@ -1482,8 +1505,9 @@ def test_read_bare_path_of_seed_doc_gets_suggestion_then_dedupe( ], [ # Round 2: the corrected call (the suggested combined - # identity) — the seed document is already in context, so it - # dedupes. + # identity) — the suggested document's full text is NOT in + # the prompt (only its summary is), so this read succeeds + # and adds the full content. ToolCallPiece( id="call_2", name="read", @@ -1493,12 +1517,18 @@ def test_read_bare_path_of_seed_doc_gets_suggestion_then_dedupe( [StreamPiece("content", "ans")], ) asyncio.run(_run(llm, holder, _settings(), seed_docs=seed)) - assert holder.read_docs == [] and holder.tool_calls == 0 # both refused + # Round 1 refused (the teaching), round 2 executed (A6). + assert holder.read_docs == [seed[0]] + assert holder.tool_calls == 1 assert llm.requests[1][0][3]["content"] == ( "No document at 'app/rag/importer.py' — " "did you mean 'sample/app/rag/importer.py'?" ) - assert llm.requests[2][0][5]["content"] == agent.ALREADY_IN_CONTEXT + assert llm.requests[2][0][5]["content"] == ( + "Document sample/app/rag/importer.py:\n" + "date: 2024-06-15\n" + "IMPORTER" + ) @pytest.mark.parametrize( @@ -1529,16 +1559,23 @@ def test_read_missing_arguments_refused( assert llm.requests[1][1] == AGENT_TOOLS -def test_reading_a_seed_doc_is_already_in_context(monkeypatch: pytest.MonkeyPatch) -> None: - """The combined identity of a seeded document: its split pair is in - the known set → ALREADY_IN_CONTEXT with no DB lookup (the dedupe - check precedes the resolve).""" +def test_reading_a_suggested_seed_doc_adds_its_full_text( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Phase 118 (A6): a FIRST read of a suggested (seeded) document + SUCCEEDS — the seed is a summary block in the prompt, not the full + text, so it falls out of the dedupe set (``holder.read_docs`` + only): the read goes through the existing path unchanged — the + full-content result (header + date line), appended to + ``holder.read_docs``, counted in ``holder.tool_calls``.""" seed = [_doc("Homelab", "kubernetes.md", "Kubernetes", "K8S-CONTENT")] - - def _boom(*_a: Any, **_k: Any) -> None: - raise AssertionError("find_document must not be called for a seeded doc") - - monkeypatch.setattr(agent, "find_document", _boom) + monkeypatch.setattr( + agent, + "find_document", + lambda db, source, path: seed[0] + if (source, path) == ("Homelab", "kubernetes.md") + else None, + ) holder = AgentHolder() llm = ScriptedLLM( [ @@ -1551,11 +1588,58 @@ def test_reading_a_seed_doc_is_already_in_context(monkeypatch: pytest.MonkeyPatc [StreamPiece("content", "ans")], ) asyncio.run(_run(llm, holder, _settings(), seed_docs=seed)) - assert holder.read_docs == [] and holder.tool_calls == 0 - assert llm.requests[1][0][3]["content"] == agent.ALREADY_IN_CONTEXT + assert holder.read_docs == [seed[0]] + assert holder.tool_calls == 1 + assert llm.requests[1][0][3]["content"] == ( + "Document Homelab/kubernetes.md:\ndate: 2024-06-15\nK8S-CONTENT" + ) + assert llm.requests[1][1] == AGENT_TOOLS + + +def test_re_reading_a_suggested_doc_is_deduped(monkeypatch: pytest.MonkeyPatch) -> None: + """Phase 118 (A6): once the suggested document's full text has been + read into context, a SECOND read of the same combined identity is + refused with the byte-identical ``ALREADY_IN_CONTEXT`` line — the + dedupe set is ``holder.read_docs`` only, so the refusal fires with + NO DB lookup (the dedupe check precedes the resolve) and the + counters stay untouched.""" + seed = [_doc("Homelab", "kubernetes.md", "Kubernetes", "K8S-CONTENT")] + lookups: list[tuple[str, str]] = [] + + def _find(db: Any, source: str, path: str) -> Document | None: + lookups.append((source, path)) + return seed[0] if (source, path) == ("Homelab", "kubernetes.md") else None + + monkeypatch.setattr(agent, "find_document", _find) + holder = AgentHolder() + llm = ScriptedLLM( + [ + ToolCallPiece( + id="call_1", + name="read", + arguments={"path": "Homelab/kubernetes.md"}, + ) + ], + [ + ToolCallPiece( + id="call_2", + name="read", + arguments={"path": "Homelab/kubernetes.md"}, + ) + ], + [StreamPiece("content", "ans")], + ) + asyncio.run(_run(llm, holder, _settings(), seed_docs=seed)) + assert holder.read_docs == [seed[0]] # appended exactly once + assert holder.tool_calls == 1 # the re-read counts nothing + assert lookups == [("Homelab", "kubernetes.md")] # the re-read deduped pre-lookup + assert llm.requests[1][0][3]["content"] == ( + "Document Homelab/kubernetes.md:\ndate: 2024-06-15\nK8S-CONTENT" + ) + assert llm.requests[2][0][5]["content"] == agent.ALREADY_IN_CONTEXT # Rejected → the tools are still offered on the next request (the # round cap is the only bound). - assert llm.requests[1][1] == AGENT_TOOLS + assert llm.requests[2][1] == AGENT_TOOLS def test_reading_an_already_read_doc_is_deduped(monkeypatch: pytest.MonkeyPatch) -> None: @@ -1718,28 +1802,47 @@ def test_run_agent_short_read_yields_no_tool_result_piece( assert isinstance(out[1], StreamPiece) -def test_read_truncation_does_not_touch_refusal_paths( +def test_read_truncation_paths_with_suggested_seed( monkeypatch: pytest.MonkeyPatch, ) -> None: - """Phase 95: the read refusal paths are untouched by the cap — a SEED - document that is over the cap is still refused with - ``ALREADY_IN_CONTEXT`` (not truncated, nothing recorded, nothing - counted), and an unknown path is still the no-document refusal (no + """Phase 118 (A6) re-target of the phase-95 pins ("seed" → + "suggested"): a SUGGESTED (seeded) document that is over the cap is + read through the ordinary cap path — cut at the cap + the shared + marker + the pinned notice, the truncation recorded on the holder, + and the call still successful (the seed is a summary, not full + text) — and an unknown path is still the no-document refusal (no content is read, so no truncation either).""" big = "B" * 5000 # far over the tiny cap below seed = _doc("S", "seed.md", "Seed", big) - monkeypatch.setattr(agent, "find_document", lambda db, source, path: None) - monkeypatch.setattr(agent, "all_documents", lambda db: []) - # (a) Reading the (over-cap) seed doc → ALREADY_IN_CONTEXT (refusal). + monkeypatch.setattr( + agent, + "find_document", + lambda db, source, path: seed + if (source, path) == ("S", "seed.md") + else None, + ) + monkeypatch.setattr(agent, "all_documents", lambda db: [seed]) + # (a) Reading the (over-cap) SUGGESTED doc → the capped read: cut at + # the cap, marker + pinned notice, holder entry, still counted. holder = AgentHolder() llm = ScriptedLLM( [ToolCallPiece(id="call_1", name="read", arguments={"path": "S/seed.md"})], [StreamPiece("content", "ans")], ) asyncio.run(_run(llm, holder, _settings(read_max_chars=100), seed_docs=[seed])) - assert llm.requests[1][0][3]["content"] == agent.ALREADY_IN_CONTEXT - assert holder.read_truncations == [] - assert holder.tool_calls == 0 and holder.read_docs == [] + assert llm.requests[1][0][3]["content"] == ( + "Document S/seed.md:\n" + "date: 2024-06-15\n" + + big[:100] + + "\n" + + TRUNCATION_MARKER + + "\n" + + READ_TRUNCATION_NOTICE.format(shown=100, total=5000) + ) + # (argument, cap, total) — the raw argument, the cap kept, the + # true length. + assert holder.read_truncations == [("S/seed.md", 100, 5000)] + assert holder.tool_calls == 1 and holder.read_docs == [seed] # (b) An unknown path → the no-document refusal (argument echoed), # even though a big doc could have truncated — no content is read. holder2 = AgentHolder() diff --git a/tests/unit/test_chat_gate.py b/tests/unit/test_chat_gate.py index 0eb47e6..b394bb3 100644 --- a/tests/unit/test_chat_gate.py +++ b/tests/unit/test_chat_gate.py @@ -8,6 +8,7 @@ session, and the LLM all faked, so the whole deflection contract """ from __future__ import annotations +import hashlib import json import uuid from collections.abc import Iterator @@ -23,6 +24,7 @@ from app.main import app as fastapi_app from app.models import Document, KbOverview, QueryLog from app.rag.agent import AGENT_TOOLS from app.rag.llm import StreamPiece +from app.rag.prompts import build_deflect_prompt from app.rag.retriever import RetrievedChunk, weak_hit_titles from app.rag.suggestions import MAX_SUGGESTIONS, derive_suggestions from tests.conftest import ADMIN_PASSWORD @@ -46,7 +48,9 @@ def _settings(threshold: float = 0.30, floor: float | None = None) -> Settings: ) -def _doc(title: str, content: str) -> Document: +def _doc( + title: str, content: str, summary: str | None = None +) -> Document: return Document( id=uuid.uuid4(), source="Homelab", @@ -54,6 +58,10 @@ def _doc(title: str, content: str) -> Document: full_path="/tmp/doc.md", title=title, content=content, + # Phase 30/118: the stored lite-model summary — the HIGH block's + # BODY (task 03). ``None`` exercises the A5 preview fallback + # (the first ``suggestion_preview_chars`` content chars). + summary=summary, content_hash="0" * 64, # Phase 106, D5: the HIGH block formats the row's created_at # UTC date part — the detached fixture carries it (the NOT NULL @@ -179,11 +187,12 @@ def test_gate_lexical_only_chunk_does_not_inflate_cosine() -> None: plan = chat_api.plan_turn(chunks, _settings(threshold=0.30)) assert plan.top_score == pytest.approx(0.55) assert plan.deflected is False # 0.55 >= 0.30 anyway - # Phase 113 (the usefulness bar): Beta's doc ranks first by fused - # score, but a lexical-only doc (cosine 0.0 by construction) cannot - # clear the bar — it lands in the RELATED tier, never the cited one. - assert plan.docs[0].title == "Alpha" - assert plan.related_docs[0].title == "Beta" + # Phase 118 (A3): the suggestion tier has NO floor — the lexical- + # only doc (cosine 0.0 by construction) is SUGGESTED when it ranks. + # Rank order is the fused score, so Beta (0.90) leads Alpha (0.50); + # with two docs nothing is left for the related tier (rank 6+). + assert [d.title for d in plan.suggested_docs] == ["Beta", "Alpha"] + assert plan.related_docs == [] # ---------- lexical support floor (A8 revised 2026-09-14) ---------- @@ -373,178 +382,225 @@ def test_gate_zero_chunks_deflects_with_fallback_chips() -> None: assert 2 <= len(plan.suggestions) <= MAX_SUGGESTIONS -# ---------- usefulness bar tiering (phase 113, LOCKED A2/A4) ---------- +# ---------- summary-seed tiering (phase 118, LOCKED A3/A4/A6) ---------- -def _bar_settings( +def _tier_settings( threshold: float = 0.62, lex_floor: float = 0.35, - source_floor: float = 0.35, + suggested_cap: int = 5, related_cap: int = 2, - top_n: int = 2, ) -> Settings: """Explicit code defaults (production calibration) — the env's mock- - calibrated floor (tests/conftest.py) is overridden per test.""" + calibrated values (tests/conftest.py) are overridden per test. + ``source_usefulness_floor`` / ``top_n_docs`` are deliberately left + at their code defaults: phase 118 retired their seeding role (A6) — + ``plan_turn`` never consults them (pinned in + ``test_plan_turn_does_not_consult_retired_seeding_settings``).""" return Settings( _env_file=None, # pyright: ignore[reportCallIssue] relevance_threshold=threshold, lexical_support_floor=lex_floor, - source_usefulness_floor=source_floor, + suggested_docs=suggested_cap, related_max_docs=related_cap, - top_n_docs=top_n, ) -def test_plan_turn_high_tiers_strong_plus_weak() -> None: - """Grounded turn: the bar-clearing doc is cited (and in the prompt), - the weak 2nd doc loses its citation slot and lands in related_docs — - the recurring incident's fix at the plan level.""" +def _seven_docs_with_summaries() -> list[Document]: + """The 7-doc fixture (rank 1–7 by fused score): every document has a + stored SUMMARY (distinct sentinel) and a distinct FULL-CONTENT + sentinel that must never reach the prompt (A6: the ``read`` tool is + the only full-text path).""" + return [ + _doc( + f"Doc {i}", + f"FULL_CONTENT_SENTINEL_{i}_SHOULD_NEVER_REACH_THE_PROMPT", + summary=f"SUMMARY_TEXT_{i}", + ) + for i in range(7) + ] + + +def test_plan_turn_high_seeds_top5_suggested_related_is_rank6plus() -> None: + """The phase-118 core pin (LOCKED A3/A6): the HIGH prompt seeds + exactly the top-5 suggested documents' SUMMARY text and NONE of + their full content; the related tier is rank 6+ (docs 6–7, capped + by ``related_max_docs``).""" + docs_in = _seven_docs_with_summaries() + chunks = [ + _chunk(d, 0.9 - 0.1 * i, cosine=0.8 - 0.05 * i) for i, d in enumerate(docs_in) + ] + plan = chat_api.plan_turn(chunks, _tier_settings()) + assert plan.deflected is False + assert [d.title for d in plan.suggested_docs] == [f"Doc {i}" for i in range(5)] + assert [d.title for d in plan.related_docs] == ["Doc 5", "Doc 6"] + # The prompt seeds the five summaries … + for i in range(5): + assert f"SUMMARY_TEXT_{i}" in plan.system_prompt + # … and NONE of the seven documents' full content (suggested OR + # related) reaches the LLM (A6). + for i in range(7): + assert f"FULL_CONTENT_SENTINEL_{i}" not in plan.system_prompt + + +def test_plan_turn_high_single_strong_doc_yields_one_suggested() -> None: + """The suggested cap is a CEILING, not a quota: one doc ⇒ one + suggested doc, an empty related tier (nothing beyond rank 1).""" + strong = _doc("Kubernetes Homelab Cluster", "STRONG_DOC_CONTENT") + plan = chat_api.plan_turn([_chunk(strong, 0.90, cosine=0.80)], _tier_settings()) + assert plan.deflected is False + assert [d.title for d in plan.suggested_docs] == ["Kubernetes Homelab Cluster"] + assert plan.related_docs == [] + # No stored summary (the fixture default) → the A5 preview fallback + # carries the short content whole (under the 400-char cap). + assert "STRONG_DOC_CONTENT" in plan.system_prompt + + +def test_plan_turn_high_suggests_strong_and_weak_no_floor() -> None: + """The recurring incident under phase 118 (A3): the weak 2nd doc no + longer loses a citation slot to a bar — the floor never filters, so + BOTH docs are suggested (rank order) and ride the citation surface + (A4); the HIGH prompt seeds both summaries (the A5 fallback carries + the short fixture content whole).""" strong = _doc("Kubernetes Homelab Cluster", "STRONG_DOC_CONTENT") weak = _doc("Backup Strategy", "WEAK_DOC_CONTENT") chunks = [_chunk(strong, 0.90, cosine=0.80), _chunk(weak, 0.80, cosine=0.20)] - plan = chat_api.plan_turn(chunks, _bar_settings()) + plan = chat_api.plan_turn(chunks, _tier_settings()) assert plan.deflected is False - assert [d.title for d in plan.docs] == ["Kubernetes Homelab Cluster"] - assert [d.title for d in plan.related_docs] == ["Backup Strategy"] - # The HIGH prompt carries the cited doc's content only. - assert "STRONG_DOC_CONTENT" in plan.system_prompt - assert "WEAK_DOC_CONTENT" not in plan.system_prompt - - -def test_plan_turn_high_single_strong_doc_yields_one_cited() -> None: - """top_n_docs is a CEILING, not a quota: one strong doc ⇒ one cited doc, - an empty related tier (LOCKED A2).""" - strong = _doc("Kubernetes Homelab Cluster", "STRONG_DOC_CONTENT") - plan = chat_api.plan_turn([_chunk(strong, 0.90, cosine=0.80)], _bar_settings()) - assert plan.deflected is False - assert [d.title for d in plan.docs] == ["Kubernetes Homelab Cluster"] + assert [d.title for d in plan.suggested_docs] == [ + "Kubernetes Homelab Cluster", + "Backup Strategy", + ] assert plan.related_docs == [] + assert "STRONG_DOC_CONTENT" in plan.system_prompt + assert "WEAK_DOC_CONTENT" in plan.system_prompt -def test_plan_turn_low_weak_hits_fall_to_related() -> None: - """Deflected turn: nothing clears the bar ⇒ the cited tier is empty - and the weak hits fall to related_docs (the done frame's home for - their visibility). The LOW prompt is unchanged (titles only).""" +def test_plan_turn_low_weak_hits_are_suggested_record() -> None: + """Deflected turn: the weak hits are SUGGESTED too (no floor, A3) — + the TurnPlan carries suggested + related for the durable record — + while the LOW prompt itself stays byte-identical (weak-hit titles + only, never content).""" a = _doc("Alpha", "ALPHA_DOC_NEVER_SENT") b = _doc("Beta", "BETA_DOC_NEVER_SENT") chunks = [_chunk(a, 0.30, cosine=0.20), _chunk(b, 0.20, cosine=0.15)] - plan = chat_api.plan_turn(chunks, _bar_settings()) + plan = chat_api.plan_turn(chunks, _tier_settings()) assert plan.deflected is True - assert plan.docs == [] # no citation slot below the bar - assert [d.title for d in plan.related_docs] == ["Alpha", "Beta"] # rank order + assert [d.title for d in plan.suggested_docs] == ["Alpha", "Beta"] # rank order + assert plan.related_docs == [] # nothing beyond rank 2 for 2 docs assert "ALPHA_DOC_NEVER_SENT" not in plan.system_prompt assert "Beta" in plan.system_prompt # weak-hit titles still carried assert plan.suggestions # chips unchanged def test_plan_turn_related_cap_zero_kills_the_related_tier() -> None: - """related_max_docs=0 is the kill switch: weak docs are scored but - neither cited nor related (the pre-phase-113 visibility, minus the - false citation — a deflected turn cites nothing).""" - a = _doc("Alpha", "AAA") - b = _doc("Beta", "BBB") - chunks = [_chunk(a, 0.30, cosine=0.20), _chunk(b, 0.20, cosine=0.15)] - plan = chat_api.plan_turn(chunks, _bar_settings(related_cap=0)) - assert plan.deflected is True - assert plan.docs == [] - assert plan.related_docs == [] - - -def test_plan_turn_floor_zero_keeps_legacy_cited_docs() -> None: - """source_usefulness_floor=0 disables the bar: plan.docs is the legacy - rank-ordered top-N (any cosine, incl. 0.0 lexical-only) and the - related tier is empty.""" - a = _doc("Alpha", "AAA") - b = _doc("Beta", "BBB") - chunks = [ - _chunk(a, 0.90, cosine=0.0, fts_hit=True), # lexical-only, rank 1 - _chunk(b, 0.80, cosine=0.10), - ] - plan = chat_api.plan_turn( - chunks, _bar_settings(source_floor=0.0, lex_floor=0.05) - ) - assert plan.deflected is False # 0.10 + the fts hit clears the 0.05 lex floor - assert [d.title for d in plan.docs] == ["Alpha", "Beta"] # legacy order - assert plan.related_docs == [] - - -def test_plan_turn_lexically_grounded_below_source_floor_has_no_cited_docs() -> None: - """The degenerate operator config (citation bar STRICTER than the - grounding bar): a turn grounded by a corroborated-lexical hit whose - cosine sits between the two floors has an EMPTY cited tier — the HIGH - prompt carries no document content (the tools remain the escape - hatch). The bar is a citation filter, not a gate input.""" - doc = _doc("Static DNS", "DNS_DOC_CONTENT") - plan = chat_api.plan_turn( - [_chunk(doc, 0.50, cosine=0.35, fts_hit=True)], - _bar_settings(threshold=0.62, lex_floor=0.30, source_floor=0.50), - ) - assert plan.deflected is False # 0.35 >= lex floor 0.30, fts fired - assert plan.docs == [] # 0.35 < source floor 0.50 — no citation slot - assert "DNS_DOC_CONTENT" not in plan.system_prompt - # The doc still SCORED — it rides the related tier (the "nearby docs" - # row), it is not invisible. - assert [d.title for d in plan.related_docs] == ["Static DNS"] - - -def test_plan_turn_related_tier_capped_in_rank_order() -> None: - """Grounded turn, four bar-clearing docs, ceiling 2: cited = the top-2 - in rank order; related = the next two (the ceiling overflow, any - cosine), capped at related_max_docs.""" - docs_in = [ - _doc(f"Doc {i}", f"DOC_CONTENT_{i}") for i in range(4) - ] + """related_max_docs=0 is the kill switch: rank-6+ docs are scored + and suggested-adjacent but neither suggested nor related — the + done frame's row stays empty.""" + docs_in = _seven_docs_with_summaries() chunks = [ _chunk(d, 0.9 - 0.1 * i, cosine=0.8 - 0.05 * i) for i, d in enumerate(docs_in) ] - plan = chat_api.plan_turn(chunks, _bar_settings(top_n=2, related_cap=2)) + plan = chat_api.plan_turn(chunks, _tier_settings(related_cap=0)) assert plan.deflected is False - assert [d.title for d in plan.docs] == ["Doc 0", "Doc 1"] - assert [d.title for d in plan.related_docs] == ["Doc 2", "Doc 3"] + assert len(plan.suggested_docs) == 5 # the suggestion tier is untouched + assert plan.related_docs == [] + # The cap restores the rank-6+ row (rank order, capped). + wide = chat_api.plan_turn(chunks, _tier_settings(related_cap=3)) + assert [d.title for d in wide.related_docs] == ["Doc 5", "Doc 6"] + + +def test_plan_turn_suggested_docs_setting_caps_the_suggested_tier() -> None: + """``suggested_docs`` (``BOR_SUGGESTED_DOCS``) is honored as the + suggestion cap: 3 here ⇒ the top-3 rank-ordered docs are suggested + and rank 4+ falls to the related tier (capped at ``related_max_docs``).""" + docs_in = _seven_docs_with_summaries() + chunks = [ + _chunk(d, 0.9 - 0.1 * i, cosine=0.8 - 0.05 * i) for i, d in enumerate(docs_in) + ] + plan = chat_api.plan_turn(chunks, _tier_settings(suggested_cap=3)) + assert [d.title for d in plan.suggested_docs] == ["Doc 0", "Doc 1", "Doc 2"] + assert [d.title for d in plan.related_docs] == ["Doc 3", "Doc 4"] # cap 2 + + +def test_plan_turn_does_not_consult_retired_seeding_settings() -> None: + """Phase 118 (A6): ``top_n_docs`` and ``source_usefulness_floor`` + lost their seeding role — ``plan_turn`` never consults them. A + degenerate config (the maximum legal bar — 0.62, above every chunk's + cosine of 0.50 — and a top-N of 1) changes nothing: the suggested + tier is still the no-floor top-5 in rank order and the related tier + is still rank 6+. + (Behavioral pin — the settings themselves stay, env back-compat.)""" + docs_in = _seven_docs_with_summaries() + chunks = [ + _chunk(docs_in[0], 0.9, cosine=0.50, fts_hit=True), + ] + [ + _chunk(d, 0.9 - 0.1 * i, cosine=0.50) + for i, d in enumerate(docs_in[1:], start=1) + ] + settings = Settings( + _env_file=None, # pyright: ignore[reportCallIssue] + relevance_threshold=0.62, + lexical_support_floor=0.35, + top_n_docs=1, # retired: the old full-text seeding ceiling + # retired: the maximum legal bar (== threshold) — above every + # cosine here (0.50), so the OLD tiering would cite nothing. + source_usefulness_floor=0.62, + related_max_docs=2, + ) + plan = chat_api.plan_turn(chunks, settings) + assert plan.deflected is False # 0.50 >= the 0.35 lex floor, fts fired + assert [d.title for d in plan.suggested_docs] == [f"Doc {i}" for i in range(5)] + assert [d.title for d in plan.related_docs] == ["Doc 5", "Doc 6"] # ---------- summary hits (phase 30: summary → full source document) ---------- -def test_summary_hit_on_selected_top_doc_counts() -> None: - """HIGH branch: the top document was hit via its summary chunk ⇒ 1. - - Context assembly is unchanged (A7 revised): the *source* document's - full content lands in the prompt, not the summary text alone. - """ +def test_summary_hit_on_suggested_doc_counts() -> None: + """HIGH branch: a suggested (rank-1) document hit via its summary + chunk ⇒ 1. Phase 118 (A6): the *summary* is what the LLM sees in + the prompt (no stored summary here → the A5 preview fallback carries + the short fixture content whole).""" a = _doc("Alpha", "ALPHA_FULL_SOURCE_CONTENT") b = _doc("Beta", "BETA_FULL_SOURCE_CONTENT") chunks = [ - _chunk(a, 0.90, is_summary=True), # top doc reached through its summary + _chunk(a, 0.90, is_summary=True), # suggested doc reached through its summary _chunk(b, 0.50), ] plan = chat_api.plan_turn(chunks, _settings(threshold=0.30)) assert plan.deflected is False assert plan.summary_hits == 1 - # The full source document is what the LLM sees (phase 24 contract). + # The A5 preview fallback (short content, under the 400-char cap) + # carries the content whole — the block body, never more. assert "ALPHA_FULL_SOURCE_CONTENT" in plan.system_prompt -def test_summary_hit_outside_top_n_selection_not_counted() -> None: - """A summary chunk on a document outside the top-N (default 2) selection - does not count — only hits that landed in the selected context do.""" - a = _doc("Alpha", "ALPHA_CONTENT") - b = _doc("Beta", "BETA_CONTENT") - c = _doc("Gamma", "GAMMA_CONTENT") +def test_summary_hits_counts_suggested_parent_only() -> None: + """Phase 118 redefinition (redefined from the phase-113 cited set): + a summary chunk counts ONLY when its parent document is in the + SUGGESTED set — a rank-1 (suggested) parent counts, a rank-6 + (related-tier-only) parent does not.""" + docs_in = [_doc(f"Doc {i}", f"CONTENT_{i}") for i in range(7)] chunks = [ - _chunk(a, 0.90), - _chunk(b, 0.80), - _chunk(c, 0.70, is_summary=True), # 3rd-ranked doc — not selected + _chunk(docs_in[0], 0.90, is_summary=True), # suggested parent — counts + _chunk(docs_in[1], 0.80), + _chunk(docs_in[2], 0.70), + _chunk(docs_in[3], 0.60), + _chunk(docs_in[4], 0.50), + _chunk(docs_in[5], 0.40, is_summary=True), # related-only parent — does not + _chunk(docs_in[6], 0.30), ] plan = chat_api.plan_turn(chunks, _settings(threshold=0.30)) assert plan.deflected is False - assert [d.title for d in plan.docs] == ["Alpha", "Beta"] - assert plan.summary_hits == 0 + assert [d.title for d in plan.suggested_docs] == [f"Doc {i}" for i in range(5)] + assert [d.title for d in plan.related_docs] == ["Doc 5", "Doc 6"] + assert plan.summary_hits == 1 -def test_low_branch_counts_summary_hit_on_selected_doc() -> None: +def test_low_branch_counts_summary_hit_on_suggested_doc() -> None: """LOW (deflected) branch records ``summary_hits`` too: the weak hit's - parent is still the selected (weak-hit) document.""" + parent is still the suggested (weak-hit) document (no floor, A3).""" a = _doc("Gamma", "GAMMA_DOC_CONTENT") b = _doc("Delta", "DELTA_DOC_CONTENT") chunks = [ @@ -683,7 +739,43 @@ def test_high_path_unaffected() -> None: assert "Reply in plain text only" not in plan.system_prompt assert "ALPHA_DOC_CONTENT" in plan.system_prompt assert "BETA_DOC_CONTENT" in plan.system_prompt - assert [d.title for d in plan.docs] == ["Kubernetes Homelab Cluster", "Backup Strategy"] + # Phase 118 (A3): both docs are suggested (no floor) — the durable- + # record input — with nothing left for the related tier (rank 6+). + assert [d.title for d in plan.suggested_docs] == [ + "Kubernetes Homelab Cluster", + "Backup Strategy", + ] + assert plan.related_docs == [] + + +# ---------- deflected branch: byte-identical (A8 gate untouched) ---------- + + +def test_low_prompt_byte_identical_to_pre_task_sha_pin() -> None: + """Phase 118 (A8 gate untouched): the deflected prompt is + BYTE-IDENTICAL to pre-task on the same chunks — it is exactly + ``build_deflect_prompt(weak_hit_titles(…))`` (the tiering feeds the + TurnPlan's durable-record fields, never the LOW prompt). The sha256 + pin makes any future LOW-body drift loud; the suggestions/chips and + the deflected flag are unchanged.""" + a = _doc("Kubernetes Homelab Cluster", "ALPHA_DOC_CONTENT") + b = _doc("Backup Strategy", "BETA_DOC_CONTENT") + chunks = [_chunk(b, 0.10), _chunk(a, 0.20)] + plan = chat_api.plan_turn(chunks, _settings()) + assert plan.deflected is True + expected = build_deflect_prompt(weak_hit_titles(chunks)) + assert plan.system_prompt == expected # byte-identical to the pre-task build + assert ( + hashlib.sha256(plan.system_prompt.encode("utf-8")).hexdigest() + == "603395e013c97be8a13837eda533c0b7bd5da4f7a0806b0ae8ad7d6c52420913" + ) + assert plan.suggestions # the "Maybe try" chips are unchanged + # Both tiers still ride the plan (the durable-record inputs, A3). + assert [d.title for d in plan.suggested_docs] == [ + "Kubernetes Homelab Cluster", + "Backup Strategy", + ] + assert plan.related_docs == [] # ---------- weak_hit_titles (fake retriever mapping) ---------- @@ -1059,3 +1151,55 @@ def test_endpoint_no_kb_row_prompt_unchanged( assert "" not in system["content"] log_lines = [r.getMessage() for r in caplog.records if "question=" in r.getMessage()] assert log_lines and "kb_chars=0" in log_lines[-1] + + +def test_endpoint_log_line_records_suggested_after_summary_hits( + client: TestClient, + gate_env: tuple[_FakeSession, _CannedLLM], + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, +) -> None: + """Phase 118 (PLAN §9 extension): the per-turn log line gains + ``suggested=N`` immediately AFTER ``summary_hits=N`` — the field + order of every existing field is untouched (the phase-114 + ``retries=N scaffold_stripped=N`` tail stays last). The value is the + seeded suggestion tier's size (``len(plan.suggested_docs)``).""" + _session, _llm = gate_env + doc = _doc("Kubernetes Homelab Cluster", "TALOS_DOC_SENT") + chunks = [_chunk(doc, 0.30), _chunk(doc, 0.20, is_summary=True)] + monkeypatch.setattr(chat_api, "retrieve", _fake_retriever(chunks)) + + with caplog.at_level("INFO", logger="app.chat"): + _ask(client, "How is my Kubernetes cluster set up?") + + log_lines = [r.getMessage() for r in caplog.records if "question=" in r.getMessage()] + assert log_lines + line = log_lines[-1] + # The new slot: suggested=N right after summary_hits=N (one suggested + # doc — the single fixture doc — and one summary hit on it). + assert "summary_hits=1 suggested=1" in line + # The full field order (the phase-114 tail stays last). + order = ( + "question=", + "embed_ms=", + "top_score=", + "fts_hits=", + "summary_hits=", + "suggested=", + "tuning=", + "kb_chars=", + "history_msgs=", + "threshold=", + "deflected=", + "sources=", + "thinking_chars=", + "tool_calls=", + "total_ms=", + "retries=", + "scaffold_stripped=", + ) + idx = -1 + for field in order: + pos = line.find(field) + assert pos > idx, f"{field} out of order in the per-turn log line" + idx = pos diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 7f05ff1..2cf9978 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -282,6 +282,59 @@ def test_related_max_docs_rejects_negative( _settings() +def test_suggested_docs_default_and_env_override( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Phase 118 (LOCKED A3): the start-here suggestion tier cap — + default 5 (the owner directive, TODO L3), env-tunable.""" + monkeypatch.delenv("BOR_SUGGESTED_DOCS", raising=False) + assert _settings().suggested_docs == 5 + monkeypatch.setenv("BOR_SUGGESTED_DOCS", "3") + assert _settings().suggested_docs == 3 + + +def test_suggested_docs_rejects_zero_and_negative( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """``0`` (no starting points) and a negative cap are typos (the + ``agent_max_rounds`` pattern); ``1`` is the minimum legal value.""" + monkeypatch.setenv("BOR_SUGGESTED_DOCS", "0") + with pytest.raises(ValidationError, match="suggested_docs"): + _settings() + monkeypatch.setenv("BOR_SUGGESTED_DOCS", "-2") + with pytest.raises(ValidationError, match="suggested_docs"): + _settings() + monkeypatch.setenv("BOR_SUGGESTED_DOCS", "1") + assert _settings().suggested_docs == 1 + + +def test_suggestion_preview_chars_default_and_env_override( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Phase 118, task 03 (LOCKED A5): the NULL-summary preview cap — + default 400, env-tunable via ``BOR_SUGGESTION_PREVIEW_CHARS``.""" + monkeypatch.delenv("BOR_SUGGESTION_PREVIEW_CHARS", raising=False) + assert _settings().suggestion_preview_chars == 400 + monkeypatch.setenv("BOR_SUGGESTION_PREVIEW_CHARS", "800") + assert _settings().suggestion_preview_chars == 800 + + +def test_suggestion_preview_chars_rejects_zero_and_negative( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """``0``/negative would preview an empty/absent prefix — typos that + fail loudly (the ``agent_max_rounds`` pattern); ``1`` is the minimum + legal value.""" + monkeypatch.setenv("BOR_SUGGESTION_PREVIEW_CHARS", "0") + with pytest.raises(ValidationError, match="suggestion_preview_chars"): + _settings() + monkeypatch.setenv("BOR_SUGGESTION_PREVIEW_CHARS", "-5") + with pytest.raises(ValidationError, match="suggestion_preview_chars"): + _settings() + monkeypatch.setenv("BOR_SUGGESTION_PREVIEW_CHARS", "1") + assert _settings().suggestion_preview_chars == 1 + + def test_read_max_chars_default_and_env_override( monkeypatch: pytest.MonkeyPatch, ) -> None: diff --git a/tests/unit/test_importer.py b/tests/unit/test_importer.py index 4f76e6d..d664087 100644 --- a/tests/unit/test_importer.py +++ b/tests/unit/test_importer.py @@ -4,8 +4,10 @@ The walk tests are pure filesystem (``tmp_path``); the delta and summary tests run against the local compose Postgres (preferred — a real vector table), skipping with clear instructions when the stack is not up. -Summaries (phase 30): non-markdown files get a ``lite``-model summary via -the fake's deterministic ``chat`` (``"Summary of "``); the +Summaries (phase 30; phase 118, A2: every file, markdown included): +every file gets a ``lite``-model summary via the fake's deterministic +``chat`` (``"Summary of "``); an unchanged doc whose summary +is NULL is backfilled on the next run (``summary_backfilled``); the sentinel word ``SUMMARY-BLOWUP`` makes ``chat`` raise :class:`LLMError` for the fail-soft path. """ @@ -13,10 +15,12 @@ from __future__ import annotations import asyncio import logging +from datetime import UTC, datetime from pathlib import Path import pytest from sqlalchemy import func, select +from sqlalchemy.orm import Session import app.rag.importer as importer from app.config import Settings @@ -29,7 +33,7 @@ from app.rag.importer import ( iter_importable_files, match_extension, ) -from app.rag.llm import EmbeddingError +from app.rag.llm import EmbeddingError, LLMError from tests.fakes import FakeEmbedder #: The original seven A9 formats as dotted suffixes (pre-phase-47 default @@ -47,6 +51,15 @@ class _PoisonEmbedder(FakeEmbedder): return await super().embed(texts) +class _FailingChatEmbedder(FakeEmbedder): + """A ``lite`` model that always fails (drives the summary fail-soft + path — including the phase-118 backfill).""" + + async def chat(self, messages: list[dict[str, str]], model: str | None = None) -> str: + self.chat_calls.append(list(messages)) + raise LLMError("simulated lite-model failure (test sentinel)") + + class _CapEmbedder(FakeEmbedder): """Simulates the endpoint's ~1024-token input cap at ~1.1 chars/token: any single text over 1000 chars is rejected (URL-dense worst case).""" @@ -266,16 +279,19 @@ def test_added_then_unchanged_then_updated_then_pruned(db, tmp_path: Path) -> No try: s1 = asyncio.run(import_sources([root], llm, session=db)) assert (s1.files, s1.added, s1.unchanged, s1.updated, s1.pruned) == (2, 2, 0, 0, 0) - # a.md has two sections (2 chunks), b.md one (1 chunk). + # a.md has two sections (2 chunks), b.md one (1 chunk) — content + # chunks only; the ``is_summary`` chunks live in ``summaries``. assert s1.chunks == 3 - # Embeddings are stored with the configured dimension. + assert s1.summaries == 2 # phase 118 (A2): markdown is summarized too + # Embeddings are stored with the configured dimension: 3 content + # chunks + 2 ``is_summary`` chunks (one per doc, phase 118 A2). n = db.scalar( select(func.count()) .select_from(Chunk) .join(Document, Document.id == Chunk.document_id) .where(Document.source == root.name) ) - assert n == 3 + assert n == 5 for c in db.scalars( select(Chunk) .join(Document, Document.id == Chunk.document_id) @@ -300,14 +316,15 @@ def test_added_then_unchanged_then_updated_then_pruned(db, tmp_path: Path) -> No assert db.scalar( select(Document).where(Document.source == root.name, Document.path == "a.md") ) is None - # Chunks of the pruned document are gone (FK cascade). + # Chunks of the pruned document are gone (FK cascade); b.md's + # content chunk + its ``is_summary`` chunk survive (phase 118 A2). n_after = db.scalar( select(func.count()) .select_from(Chunk) .join(Document, Document.id == Chunk.document_id) .where(Document.source == root.name) ) - assert n_after == 1 + assert n_after == 2 finally: _cleanup_source(db, root.name) @@ -438,8 +455,11 @@ def test_chunk_positions_and_titles(db, tmp_path: Path) -> None: select(Document).where(Document.source == root.name, Document.path == "multi.md") ) assert doc is not None - positions = sorted(c.position for c in doc.chunks) - assert positions == list(range(len(doc.chunks))) and len(doc.chunks) >= 2 + # 0-based CONTENT positions (the ``is_summary`` chunk sits at −1, + # phase 30/118). + content = [c for c in doc.chunks if not c.is_summary] + positions = sorted(c.position for c in content) + assert positions == list(range(len(content))) and len(content) >= 2 finally: _cleanup_source(db, root.name) @@ -535,7 +555,7 @@ def test_quadlet_and_j2_files_get_stem_titles_and_per_format_counts( _cleanup_source(db, root.name) -# ---------- phase 30: lite-model summaries for non-markdown files ---------- +# ---------- phase 30: lite-model summaries (phase 118: every file) ---------- def test_non_markdown_file_gets_stored_and_indexed_summary(db, tmp_path: Path) -> None: @@ -571,9 +591,11 @@ def test_non_markdown_file_gets_stored_and_indexed_summary(db, tmp_path: Path) - _cleanup_source(db, root.name) -def test_markdown_file_never_gets_summary(db, tmp_path: Path) -> None: - """Markdown is already natural language: no summary, no ``is_summary`` - chunk, and the ``lite`` model is never called.""" +def test_markdown_file_gets_stored_summary(db, tmp_path: Path) -> None: + """Phase 118 (A2): markdown is summarized too (the phase-30 exclusion + is retired) — ``documents.summary`` is set and one ``is_summary`` + chunk (position −1, embedded) is indexed alongside the content + chunks.""" root = tmp_path / "mdsrc" root.mkdir() (root / "note.md").write_text("# Note\n\nmarkdown body\n") @@ -581,14 +603,240 @@ def test_markdown_file_never_gets_summary(db, tmp_path: Path) -> None: try: summary = asyncio.run(import_sources([root], llm, session=db)) assert summary.added == 1 - assert summary.summaries == 0 and summary.summary_errors == 0 - assert llm.chat_calls == [] # the model was never asked + assert summary.summaries == 1 and summary.summary_errors == 0 + assert llm.chat_calls # the lite model WAS asked (phase 118) doc = db.scalar( select(Document).where(Document.source == root.name, Document.path == "note.md") ) assert doc is not None - assert doc.summary is None - assert doc.chunks and all(not c.is_summary for c in doc.chunks) + assert doc.summary is not None + # Deterministic fake reply + the code-appended pointer line. + assert doc.summary.startswith("Summary of") + assert doc.summary.endswith(f"Source: {root.name}/note.md") + schunks = [c for c in doc.chunks if c.is_summary] + assert len(schunks) == 1 + assert schunks[0].position == -1 + assert schunks[0].content == doc.summary + assert schunks[0].embedding is not None and len(schunks[0].embedding) == 768 + # Content chunks stay 0-based and are never flagged as summaries. + content = [c for c in doc.chunks if not c.is_summary] + assert sorted(c.position for c in content) == list(range(len(content))) + finally: + _cleanup_source(db, root.name) + + +# ---------- phase 118 (A2): NULL-summary backfill on the unchanged path ---------- + + +def _clear_stored_summary(db: Session, doc: Document) -> None: + """Simulate a NULL-summary row (a pre-phase-30 row, or a cleared + summary): the content stays, only the summary + its chunk go away.""" + doc.summary = None + for c in [c for c in doc.chunks if c.is_summary]: + doc.chunks.remove(c) + db.commit() + + +def test_unchanged_doc_with_null_summary_is_backfilled(db, tmp_path: Path) -> None: + """Phase 118 (A2): an unchanged doc whose summary is NULL gets a + summary-only backfill on the next sync: summary stored + one embedded + ``is_summary`` chunk, counted ``summary_backfilled`` — never + ``summaries``, never added/updated/pruned, no content re-embed.""" + root = tmp_path / "bfill" + root.mkdir() + (root / "note.md").write_text("# Note\n\nmarkdown body\n") + llm = FakeEmbedder() + try: + first = asyncio.run(import_sources([root], llm, session=db)) + assert (first.added, first.summaries) == (1, 1) + doc = db.scalar( + select(Document).where(Document.source == root.name, Document.path == "note.md") + ) + assert doc is not None and doc.summary is not None + _clear_stored_summary(db, doc) # the NULL-summary row the backfill targets + + embed_before = len(llm.calls) + second = asyncio.run(import_sources([root], llm, session=db)) + assert (second.added, second.updated, second.pruned) == (0, 0, 0) + assert second.unchanged == 1 + assert second.summary_backfilled == 1 + assert second.summaries == 0 and second.summary_errors == 0 + # No content re-embed: exactly one new embed batch, the summary + # text only. + assert len(llm.calls) == embed_before + 1 + + db.expire_all() + doc = db.scalar( + select(Document).where(Document.source == root.name, Document.path == "note.md") + ) + assert doc is not None + assert doc.summary is not None + assert llm.calls[-1] == [doc.summary] # only the backfilled summary + schunks = [c for c in doc.chunks if c.is_summary] + assert len(schunks) == 1 + assert schunks[0].position == -1 + assert schunks[0].content == doc.summary + assert schunks[0].embedding is not None and len(schunks[0].embedding) == 768 + # The content chunk is untouched. + content = [c for c in doc.chunks if not c.is_summary] + assert len(content) == 1 and content[0].embedding is not None + finally: + _cleanup_source(db, root.name) + + +def test_unchanged_doc_with_stored_summary_never_resummarizes(db, tmp_path: Path) -> None: + """Phase 118 (A2): an unchanged doc that ALREADY has a summary (the + third sync of the lifecycle) makes no summary LLM call at all and + gains no chunks — owner-edited (non-NULL) summaries are never + touched.""" + root = tmp_path / "noref" + root.mkdir() + (root / "note.md").write_text("# Note\n\nmarkdown body\n") + llm = FakeEmbedder() + try: + asyncio.run(import_sources([root], llm, session=db)) + chat_before = len(llm.chat_calls) + embed_before = len(llm.calls) + chunk_before = db.scalar( + select(func.count()) + .select_from(Chunk) + .join(Document, Document.id == Chunk.document_id) + .where(Document.source == root.name) + ) + second = asyncio.run(import_sources([root], llm, session=db)) + assert second.unchanged == 1 + assert second.summary_backfilled == 0 and second.summaries == 0 + assert second.summary_errors == 0 + assert len(llm.chat_calls) == chat_before # the model was never asked + assert len(llm.calls) == embed_before # no embedding of any kind + chunk_after = db.scalar( + select(func.count()) + .select_from(Chunk) + .join(Document, Document.id == Chunk.document_id) + .where(Document.source == root.name) + ) + assert chunk_after == chunk_before # no new chunk of any kind + finally: + _cleanup_source(db, root.name) + + +def test_unchanged_doc_with_empty_string_summary_is_never_backfilled( + db, tmp_path: Path +) -> None: + """Phase 118 (A2, strict ``is None``): an empty-string summary is + owner-set (phase 57) — the backfill skips it, the ``lite`` model is + never called, and the value stays byte-identical.""" + root = tmp_path / "emptysum" + root.mkdir() + (root / "note.md").write_text("# Note\n\nmarkdown body\n") + llm = FakeEmbedder() + try: + asyncio.run(import_sources([root], llm, session=db)) + doc = db.scalar( + select(Document).where(Document.source == root.name, Document.path == "note.md") + ) + assert doc is not None + doc.summary = "" # the owner-set empty string (never NULL) + db.commit() + + chat_before = len(llm.chat_calls) + second = asyncio.run(import_sources([root], llm, session=db)) + assert second.unchanged == 1 + assert second.summary_backfilled == 0 and second.summaries == 0 + assert second.summary_errors == 0 + assert len(llm.chat_calls) == chat_before # the model was never asked + + db.expire_all() + doc = db.scalar( + select(Document).where(Document.source == root.name, Document.path == "note.md") + ) + assert doc is not None + assert doc.summary == "" # byte-identical — never overwritten + finally: + _cleanup_source(db, root.name) + + +def test_backfill_runs_on_manually_dated_doc_without_touching_the_date( + db, tmp_path: Path +) -> None: + """Phase 118 (A2, assumption 7): ``created_at_manual`` protects the + DATE only (phase 106, D1) — a manually-dated, NULL-summary doc still + gets its backfilled summary, and the stored date stays byte-untouched + even though a refresh was due.""" + root = tmp_path / "manualdate" + root.mkdir() + (root / "note.md").write_text("# Note\n\nmarkdown body\n") + llm = FakeEmbedder() + try: + asyncio.run(import_sources([root], llm, session=db)) + doc = db.scalar( + select(Document).where(Document.source == root.name, Document.path == "note.md") + ) + assert doc is not None + manual = datetime(2020, 5, 4, 12, 0, 0, tzinfo=UTC) + doc.created_at = manual + doc.created_at_manual = True + _clear_stored_summary(db, doc) # the NULL-summary row the backfill targets + + second = asyncio.run(import_sources([root], llm, session=db)) + assert second.unchanged == 1 + assert second.summary_backfilled == 1 and second.summary_errors == 0 + # A date refresh WAS due (the mtime differs from the 2020 + # correction) but the manual flag withheld it — the backfill + # never touches the date either. + assert second.dates_updated == 0 + + db.expire_all() + doc = db.scalar( + select(Document).where(Document.source == root.name, Document.path == "note.md") + ) + assert doc is not None + assert doc.summary is not None # the backfill landed + assert doc.created_at == manual # byte-untouched + assert doc.created_at_manual is True + finally: + _cleanup_source(db, root.name) + + +def test_backfill_failure_is_fail_soft_and_date_still_refreshes( + db, tmp_path: Path +) -> None: + """Phase 118 (A2): a backfill whose ``lite`` call fails rolls back + its own session work only — ``summary_errors=1``, the doc row + untouched — while the UNCHANGED path's date refresh still runs + afterwards.""" + root = tmp_path / "bfillfail" + root.mkdir() + (root / "note.md").write_text("# Note\n\nmarkdown body\n") + llm = FakeEmbedder() + try: + asyncio.run(import_sources([root], llm, session=db)) + doc = db.scalar( + select(Document).where(Document.source == root.name, Document.path == "note.md") + ) + assert doc is not None + _clear_stored_summary(db, doc) # the NULL-summary row the backfill targets + # Force a date drift so the refresh is DUE on this run. + doc.created_at = datetime(2020, 1, 1, tzinfo=UTC) + db.commit() + + second = asyncio.run(import_sources([root], _FailingChatEmbedder(), session=db)) + assert second.unchanged == 1 + assert (second.added, second.updated, second.pruned) == (0, 0, 0) + assert second.summary_errors == 1 + assert second.summary_backfilled == 0 and second.summaries == 0 + + db.expire_all() + doc = db.scalar( + select(Document).where(Document.source == root.name, Document.path == "note.md") + ) + assert doc is not None + assert doc.summary is None # the failed backfill left the row untouched + assert not any(c.is_summary for c in doc.chunks) + # …but the date refresh ran (the failure only rolled back the + # summary's own session work). + assert second.dates_updated == 1 + assert doc.created_at != datetime(2020, 1, 1, tzinfo=UTC) finally: _cleanup_source(db, root.name) @@ -720,11 +968,13 @@ def test_import_summary_log_line_includes_summary_counters( caplog: pytest.LogCaptureFixture, ) -> None: """PLAN §9 summary line: the phase-30 counters sit between - ``embed_batches`` and ``formats``; the phase-106 date-refresh - counter sits between ``summary_errors`` and ``formats``.""" + ``embed_batches`` and ``formats``; the phase-118 backfill counter + sits between ``summary_errors`` and ``dates_updated``; the + phase-106 date-refresh counter sits before ``formats``.""" s = ImportSummary() s.files, s.added, s.chunks, s.embed_batches = 3, 3, 5, 4 s.summaries, s.summary_errors = 2, 1 + s.summary_backfilled = 1 s.dates_updated = 0 s.formats = {"md": 1, "yaml": 2} with caplog.at_level(logging.INFO, logger="app.importer"): @@ -732,8 +982,8 @@ def test_import_summary_log_line_includes_summary_counters( line = caplog.records[-1].getMessage() assert line == ( "import: summary files=3 added=3 updated=0 unchanged=0 pruned=0 errors=0 " - "chunks=5 embed_batches=4 summaries=2 summary_errors=1 dates_updated=0 " - "formats=yaml:2,md:1" + "chunks=5 embed_batches=4 summaries=2 summary_errors=1 summary_backfilled=1 " + "dates_updated=0 formats=yaml:2,md:1" ) diff --git a/tests/unit/test_prompt_lock.py b/tests/unit/test_prompt_lock.py index 64e239b..eb59cd9 100644 --- a/tests/unit/test_prompt_lock.py +++ b/tests/unit/test_prompt_lock.py @@ -16,6 +16,7 @@ import hashlib from app.rag.prompts import ( PERSONA, + SUGGEST_INTRO, TOOLS_SECTION, _base, build_deflect_prompt, @@ -67,9 +68,16 @@ def test_high_and_low_bases_byte_locked() -> None: # ---------- TOOLS_SECTION (the HIGH prompt's locked ```` copy) ---------- -#: Pre-phase-112 anchors for ``TOOLS_SECTION``. -TOOLS_SECTION_SHA256 = "b834cbe368055e65da82ae3e37a91e6c658c713954703fc79b849a6ebdf4aa53" -TOOLS_SECTION_LEN = 2273 +#: Anchors for ``TOOLS_SECTION`` — pre-phase-112 values, RE-CUT for +#: phase 118 (task 04, A6, owner directive 2026-09-15): the ``read`` +#: clause was rewritten for the summary-seed mode (the ```` +#: section holds SUMMARIES — ``read`` adds the full text; only an +#: already-read document is refused). Only that clause moved — the +#: prefix (the ``ls``-clause opening) and the suffix (the +#: discipline-rules ending) survived byte-identical, so they are the +#: same anchors as pre-phase-118. +TOOLS_SECTION_SHA256 = "87ee80faf0170da6ab1de518177313def6460785613fa074312a2a5f9f071750" +TOOLS_SECTION_LEN = 2465 TOOLS_SECTION_PREFIX = ( "\n" "You may extend your context with three tools. `ls` lists the " @@ -120,3 +128,51 @@ def test_deflect_body_byte_locked() -> None: assert prompt.index("DEFLECT_MODE") < prompt.index( "Reply in plain text only" ) # the marker precedes the plain-text line + + +#: The full LOW prompt build on the canonical fixture titles — sha-pinned +#: (phase 118, task 03): the deflection path is UNTOUCHED by the +#: summary-seed re-revision (LOCKED A8) — the same inputs must produce +#: the pre-phase bytes, so this anchor is a pre-phase-118 value. +LOW_PROMPT_SHA256 = "726eddb4eb3bcc26c840011f6f8635d6af55aa09d4d064bbb9e256caa9665837" +LOW_PROMPT_LEN = 968 + + +def test_low_prompt_build_byte_identical_to_pre_phase() -> None: + """Phase 118 contract: the LOW prompt output is byte-identical to + pre-phase for identical inputs — the summary seeding (and the new + ``SUGGEST_INTRO`` line) never leaks into the deflection path.""" + prompt = build_deflect_prompt(["T1", "T2"]) + assert len(prompt) == LOW_PROMPT_LEN + assert _sha256(prompt) == LOW_PROMPT_SHA256 + assert "SUGGEST_INTRO" not in prompt and "" not in prompt + + +# ---------- SUGGEST_INTRO (phase 118, task 03 — the start-here framing) ---------- + +#: Phase-118 anchors for ``SUGGEST_INTRO`` — the ```` +#: section's intro line (the owner's "start here if these summaries seem +#: right to you" framing, TODO L3). The E2E mock's ``_document_block`` +#: parser is regex-based over the block markup (which stays byte-stable +#: around the intro), so this constant is a prompt-copy lock, pinned the +#: way ``TOOLS_SECTION`` is: sha256 + prefix + total length. +SUGGEST_INTRO_SHA256 = "7b14d2dedc6ebc4e440d32dd1edb979a7461c94034b9541c9f37b9042f394d3a" +SUGGEST_INTRO_LEN = 323 +SUGGEST_INTRO_PREFIX = ( + "The blocks below are the summaries of the top-ranked documents for " + "your question — start here if one seems right to you: " +) + + +def test_suggest_intro_byte_locked() -> None: + """The start-here framing is LOCKED copy (phase 118): sha256 + exact + prefix + total length; the three contracts it must carry (summaries + are the starting points; ``read`` adds the full text, which is NOT + in the prompt until read; cite by path) are pinned as substrings.""" + assert len(SUGGEST_INTRO) == SUGGEST_INTRO_LEN + assert _sha256(SUGGEST_INTRO) == SUGGEST_INTRO_SHA256 + assert SUGGEST_INTRO.startswith(SUGGEST_INTRO_PREFIX) + assert "call `read`" in SUGGEST_INTRO + assert "combined `source/path`" in SUGGEST_INTRO + assert "its full text is not in the prompt until you read it" in SUGGEST_INTRO + assert "Cite the document(s) you used, by path." in SUGGEST_INTRO diff --git a/tests/unit/test_prompts.py b/tests/unit/test_prompts.py index ad4bae3..a460543 100644 --- a/tests/unit/test_prompts.py +++ b/tests/unit/test_prompts.py @@ -17,9 +17,13 @@ stated up front — the combined ``source/path`` identity for the drill-down tree contract — one level per call, sources at the top, folders + files below, ``grep`` as the without-listing locator — while the ``read``/``grep`` clauses and the discipline rules are -byte-identical): the teaching refusals in :mod:`app.rag.agent` -re-state the same contract; the ```` marker keying (HIGH -only) is unchanged. +byte-identical; phase 118, task 04: the ``read`` clause rewritten +for the summary-seed mode — the ```` section holds +SUMMARIES, ``read`` adds the full text, and only an already-read +document is refused — while the ``ls``/``grep`` clauses and the +discipline rules stay byte-identical): the teaching refusals in +:mod:`app.rag.agent` re-state the same contract; the ```` +marker keying (HIGH only) is unchanged. """ from __future__ import annotations @@ -32,6 +36,7 @@ from app.config import Settings from app.models import Document from app.rag.prompts import ( PERSONA, + SUGGEST_INTRO, TOOLS_SECTION, _base, build_deflect_prompt, @@ -55,7 +60,12 @@ KB_INTRO = "The basic categories of everything in this knowledge base (generated _FIXTURE_CREATED_AT = datetime(2024, 6, 15, 12, 0, 0, tzinfo=UTC) -def _doc(path: str, content: str, title: str) -> Document: +def _doc( + path: str, + content: str, + title: str, + summary: str | None = None, +) -> Document: return Document( id=uuid.uuid4(), source="Homelab", @@ -65,6 +75,7 @@ def _doc(path: str, content: str, title: str) -> Document: content=content, content_hash="0" * 64, created_at=_FIXTURE_CREATED_AT, + summary=summary, ) @@ -93,23 +104,64 @@ def test_persona_owner_edits_are_preserved() -> None: assert "HONESTY GATE" in PERSONA # the gate itself is intact -def test_high_prompt_carries_relevance_marker_and_full_documents() -> None: - doc = _doc("kubernetes.md", "Talos Linux on three nodes.", "Kubernetes Homelab Cluster") +def test_high_prompt_carries_relevance_marker_and_summary_block() -> None: + """Phase 118 (LOCKED A6): the grounded turn seeds the document's + SUMMARY — the full content never reaches the prompt (the ``read`` + tool is the only full-text path).""" + doc = _doc( + "kubernetes.md", + "Talos Linux on three nodes. FULL_CONTENT_SENTINEL_987654", + "Kubernetes Homelab Cluster", + summary="A Talos Linux cluster on three nodes.", + ) prompt = build_high_prompt([doc]) assert "HIGH" in prompt assert "DEFLECT_MODE" not in prompt assert "" in prompt and "" in prompt assert 'path="kubernetes.md"' in prompt - assert "Talos Linux on three nodes." in prompt + assert "A Talos Linux cluster on three nodes." in prompt # the summary + assert "FULL_CONTENT_SENTINEL_987654" not in prompt # never the content + assert "Talos Linux on three nodes." not in prompt # nor the full sentence assert "HONESTY GATE" in prompt # persona intact def test_high_prompt_lists_multiple_documents_in_order() -> None: - a = _doc("a.md", "CONTENT_A", "Title A") - b = _doc("b.md", "CONTENT_B", "Title B") + a = _doc("a.md", "CONTENT_A", "Title A", summary="Summary A.") + b = _doc("b.md", "CONTENT_B", "Title B", summary="Summary B.") prompt = build_high_prompt([a, b]) - assert prompt.index("CONTENT_A") < prompt.index("CONTENT_B") + assert prompt.index("Summary A.") < prompt.index("Summary B.") assert 'title="Title B"' in prompt + assert "CONTENT_A" not in prompt and "CONTENT_B" not in prompt + + +def test_high_prompt_seeds_summaries_of_all_suggested_docs() -> None: + """The phase-118 completion pin: a grounded prompt built from five + summary-bearing documents contains ALL FIVE summaries + the intro, + and ZERO full-content characters (the full texts are not in the + prompt at all).""" + docs = [ + _doc( + f"doc{i}.md", + f"FULL_CONTENT_SENTINEL_{i} " + "x" * 100, + f"Title {i}", + summary=f"SUMMARY_{i} of the document.", + ) + for i in range(5) + ] + prompt = build_high_prompt(docs) + assert SUGGEST_INTRO in prompt + for i in range(5): + assert f"SUMMARY_{i} of the document.\n" in prompt + assert f"FULL_CONTENT_SENTINEL_{i}" not in prompt + assert f'path="doc{i}.md"' in prompt + # One block per document, in the given order. + assert prompt.count(" → intro → first block. + i_open = prompt.index("") + i_block = prompt.index("\n{SUGGEST_INTRO}\n\n" def test_high_prompt_without_documents_stays_honest() -> None: @@ -143,7 +195,9 @@ def test_zero_note_prompt_is_byte_identical_to_pre_steering() -> None: """Phase 15 contract: with no steering notes the prompt is exactly what it was before the section existed. (Phase 37: the HIGH prompt additionally carries the ```` section after the mode body — the - fixtures account for it; the LOW prompt is untouched.)""" + fixtures account for it; phase 118: the ```` section leads + with the ``SUGGEST_INTRO`` line — the fixture accounts for it; the + LOW prompt is untouched.)""" doc = _doc("kubernetes.md", "Talos Linux on three nodes.", "Kubernetes Homelab Cluster") block = ( ' None: "" ) assert build_high_prompt([doc]) == ( - _base("HIGH") + "\n\n" + block + "\n" + "\n" + TOOLS_SECTION + _base("HIGH") + + "\n\n" + + SUGGEST_INTRO + + "\n\n" + + block + + "\n" + + "\n" + + TOOLS_SECTION ) # Phase 71: the LOW prompt carries the owner-permitted plain-text # line after the DEFLECT_MODE sentence (the marker-keying contract @@ -257,6 +318,86 @@ def test_tools_section_phase95_read_truncation_clause() -> None: assert "Very large documents are capped" in build_high_prompt([doc]) +def test_tools_section_phase118_summary_seed_read_clause() -> None: + """Phase 118 (task 04, A6): the ``read`` clause is rewritten for + the summary-seed mode — the ```` section holds + SUMMARIES (a suggested document's full text is not in the prompt + until ``read`` adds it); do not re-read an already-read document + (answer from the text already in the prompt); if the user asks to + open or read a suggested document, ``read`` it. The phase-72 "do + not call ``read`` for a ```` document" copy is retired. + The ``ls`` clause (phase 94 drill-down contract), the ``grep`` + clause, and the discipline rules stay byte-identical — the + prompt lock's prefix/suffix anchors survive (see + ``test_prompt_lock``).""" + # The new summary-seed copy (pinned byte-for-byte). + assert ( + "The section holds SUMMARIES — the full text of a " + "suggested document is not in your prompt until you `read` it. " + "Do not re-read a document you have already read — its full " + "text is already in your prompt; answer directly from it. If " + "the user asks you to open or read a suggested document, " + "`read` it — that is the point of the section." + ) in TOOLS_SECTION + # The read identity handoff now also names the summary + # blocks (the combined identity is shown there), keeping the + # "including the source name" contract. + assert ( + "exactly as shown in the `ls` output — including the source " + "name — or in the summary blocks — adding its " + "full content to your context" + ) in TOOLS_SECTION + # The retired pre-phase-118 copy is gone. + assert "Do not call `read` for a document already shown in" not in TOOLS_SECTION + assert "even when the user asks you to open or read it" not in TOOLS_SECTION + # The ls clause (phase 94 drill-down contract) — byte-identical. + assert ( + "`ls` lists the knowledge base as a tree, one level at a " + "time: with no `path` it lists every synced source with its " + "document count and a summary of its contents; with a source " + "name (e.g. 'homelab') it lists that source's top-level " + "folders and files; with a `source/folder` path it drills one " + "level deeper. A listing shows only that level's subfolders " + "and its own files — never the whole knowledge base in one " + "call — and each folder line's summary says what the folder " + "contains before you drill into it. File lines are `source: X " + "| path: Y | title: Z`; to find one specific document without " + "listing, use `grep`." + ) in TOOLS_SECTION + # The grep clause — byte-identical. + assert ( + "`grep` locates an exact string (case-insensitive) in the " + "indexed documents and returns up to 20 matching " + "`source/path:line: text` lines — a locator, not a " + "context-adder: read the winner with `read`. A grep pattern " + "is a plain substring, NEVER a regex — '.*' and '\\.' are " + "literal text there; if such a pattern returns no matches, " + "retry with the plain text you expect to see. For a normal " + "search pass only `pattern` — its optional `path` argument " + "limits the search to one document you already know, by the " + "same combined `source/path` string; never a source name — a " + "bare document path (without the source name) will not " + "resolve there either." + ) in TOOLS_SECTION + # The discipline rules — byte-identical. + assert ( + "Make exactly one tool call per reply — a reply carrying two " + "tool calls runs only the first, the second is discarded — " + "and wait for the result before the next call. Never repeat a " + "call that was refused or already succeeded — the refusal " + "already told you the correct form. Answer as soon as you " + "have what you need." + ) in TOOLS_SECTION + # The new read clause rides the built HIGH prompt and never the + # LOW (deflected) prompt. + doc = _doc("kubernetes.md", "Talos Linux on three nodes.", "Kubernetes", summary="K8S") + high = build_high_prompt([doc]) + assert "The section holds SUMMARIES" in high + assert "`read` it — that is the point of the section." in high + low = build_deflect_prompt(["T1"]) + assert "The section holds SUMMARIES" not in low + + def test_tools_section_phase72_clauses_in_high_prompt_not_low() -> None: """Phase 72/94: the contract clauses ride the HIGH prompt with the rest of the section and never leak into the LOW/deflection prompt @@ -273,20 +414,37 @@ def test_tools_section_phase72_clauses_in_high_prompt_not_low() -> None: assert "including the source name" not in low -def test_documents_section_has_no_leading_intro() -> None: - """Phase 72, task 05 (gate iterations 2-3, reverted): the - ```` section must NOT lead with an in-context reminder - or name the ```` blocks — the live telemetry showed that - copy primed the model to latch the seed documents' paths as - ``ls`` scopes (the incident turn regressed to a cap-reached loop - on run 2 and re-trapped on run 5), and the reminder never flipped - the seed-doc ``read``s (15/15 across gate runs 1-5). The section - is exactly the document blocks again.""" +def test_documents_section_leads_with_the_suggest_intro() -> None: + """Phase 118, task 03: the ```` section leads with the + start-here :data:`SUGGEST_INTRO` line BEFORE the first block (the + phase-15 ``_STEERING_INTRO`` / phase-31 ``_KB_INTRO`` precedent) — + only when at least one block is present. This is the summary-as- + starting-point framing, not the reverted phase-72 do-not-read + reminder (that copy taught the seed texts as already-read context; + with A6's summary seeding the blocks are starting points the model + may ``read`` through to full text).""" doc = _doc("kubernetes.md", "Talos Linux on three nodes.", "Kubernetes Homelab Cluster") high = build_high_prompt([doc]) i_open = high.index("") i_block = high.index('\n" # no intro line + assert high[i_open : i_block] == f"\n{SUGGEST_INTRO}\n\n" + # The intro occurs exactly once and sits inside the section. + assert high.count(SUGGEST_INTRO) == 1 + assert high.index(SUGGEST_INTRO) > high.index("") + assert high.index(SUGGEST_INTRO) < high.index("") + + +def test_documents_section_without_blocks_has_no_intro() -> None: + """The intro rides only on present blocks: an empty ```` + section is exactly the fallback line again (no intro, no blocks).""" + high = build_high_prompt([]) + assert SUGGEST_INTRO not in high + i_open = high.index("") + i_close = high.index("") + assert ( + high[i_open : i_close] + == "\n(no documents matched — do not invent specifics)\n" + ) def test_tools_section_old_names_and_budget_copy_gone() -> None: @@ -314,6 +472,117 @@ def test_high_prompt_still_ends_with_tools_section() -> None: assert old not in prompt +# ---------- phase 118 (task 03): the NULL-summary preview fallback (A5) ---------- + + +def _content_only_doc(content: str) -> Document: + return _doc("kubernetes.md", content, "Kubernetes Homelab Cluster") + + +def test_null_summary_falls_back_to_preview_plus_marker() -> None: + """A doc whose ``summary`` is None (a fail-soft import miss) seeds the + first ``suggestion_preview_chars`` (default 400) content characters + + the shared ``[…truncated…]`` marker on its own line — never the full + content, never an LLM call.""" + content = "A" * 400 + "B" * 600 # 1000 chars + prompt = build_high_prompt([_content_only_doc(content)]) + body_start = prompt.index('date="2024-06-15">\n') + len('date="2024-06-15">\n') + body_end = prompt.index("\n", body_start) + assert prompt[body_start:body_end] == content[:400] + "\n" + TRUNCATION_MARKER + assert "B" * 40 not in prompt # beyond the 400-char cut + assert prompt.count(TRUNCATION_MARKER) == 1 # only the block's marker + + +def test_whitespace_summary_uses_the_same_preview_fallback() -> None: + for blank in ("", " ", "\n \t "): + doc = _doc("kubernetes.md", "A" * 500 + "B" * 500, "T", summary=blank) + prompt = build_high_prompt([doc]) + assert "A" * 400 in prompt + assert "B" * 40 not in prompt + assert TRUNCATION_MARKER in prompt + + +def test_short_content_preview_is_the_whole_content_unmarked() -> None: + """Content at or under the cap rides whole — nothing was cut, so no + marker (the marker signals truncation, not the fallback).""" + for content in ("short body", "C" * 399, "D" * 400): + prompt = build_high_prompt([_content_only_doc(content)]) + assert content + "\n" in prompt + assert TRUNCATION_MARKER not in prompt + + +def test_summary_stripped_and_never_truncated_by_the_preview_cap() -> None: + """The summary body is the stripped ``doc.summary`` — even when it is + longer than the preview cap (the cap bounds only the FALLBACK; a + stored summary is trusted context, LOCKED A5).""" + summary = "S" * 900 + content = "CONTENT_SENTINEL " + "x" * 50 + prompt = build_high_prompt([_doc("kubernetes.md", content, "T", summary=f" {summary}\n")]) + assert summary in prompt + assert content not in prompt + assert TRUNCATION_MARKER not in prompt + + +def test_preview_cap_setting_is_honored_on_the_fallback_path( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A non-default ``suggestion_preview_chars`` (``BOR_SUGGESTION_PREVIEW_CHARS`` + — the env mapping is pinned in :mod:`tests.unit.test_config`) cuts the + preview at the setting's cap on the fallback path.""" + from app.rag import prompts as prompts_mod + + monkeypatch.setattr( + prompts_mod, + "get_settings", + lambda: Settings(_env_file=None, suggestion_preview_chars=10), # pyright: ignore[reportCallIssue] + ) + content = "A" * 100 + "B" * 100 + prompt = build_high_prompt([_content_only_doc(content)]) + assert "A" * 10 in prompt + assert "A" * 11 not in prompt + assert TRUNCATION_MARKER in prompt + + +def test_preview_fallback_never_reads_settings_for_summarized_docs( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """House pattern (``build_steering_section`` / ``build_kb_section``): + the fallback cap is read from settings on the fallback path ONLY — a + prompt built from summary-bearing docs makes no settings call for + it (a dead ``get_settings`` must not break such a build).""" + from app.rag import prompts as prompts_mod + + doc = _doc("kubernetes.md", "CONTENT_SENTINEL", "T", summary="the summary") + + def _dead() -> None: # pragma: no cover - must never be called + raise AssertionError("get_settings() called for a summarized doc") + + monkeypatch.setattr(prompts_mod, "get_settings", _dead) + prompt = build_high_prompt([doc]) + assert "the summary\n" in prompt + assert "CONTENT_SENTINEL" not in prompt + + +# ---------- phase 118: the LOW prompt stays byte-identical ---------- + + +def test_low_prompt_byte_identical_to_pre_task() -> None: + """LOCKED A8 surface: the deflection prompt is untouched by the + summary seeding — byte-identical build on the same inputs (the full + sha pin lives in :mod:`tests.unit.test_prompt_lock`).""" + for titles, notes, kb in ( + (["T1", "T2"], None, None), + (["T1"], ["be concise"], None), + ([], None, OVERVIEW), + (["T1", "T2"], ["be concise"], OVERVIEW), + ): + prompt = build_deflect_prompt(titles, notes=notes, kb_overview=kb) + assert "DEFLECT_MODE" in prompt + assert "" not in prompt + assert SUGGEST_INTRO not in prompt + assert "" not in prompt + + def test_relevance_placeholder_rejected_for_garbage() -> None: with pytest.raises(ValueError, match="HIGH or LOW"): _base("MEDIUM") @@ -401,7 +670,9 @@ def test_no_overview_prompt_is_byte_identical_to_pre_phase() -> None: every prompt is exactly what it was before the ```` section existed — with or without steering notes. (Phase 37: the HIGH prompt additionally carries the ```` section after the mode - body — the fixtures account for it; the LOW prompt is untouched.)""" + body; phase 118: the ```` section leads with the + ``SUGGEST_INTRO`` line — the fixtures account for both; the LOW + prompt is untouched.)""" doc = _doc("kubernetes.md", "Talos Linux on three nodes.", "Kubernetes Homelab Cluster") block = ( ' None: "Talos Linux on three nodes.\n" "" ) - docs_block = "\n\n" + block + "\n" + "\n" + TOOLS_SECTION + docs_block = ( + "\n\n" + + SUGGEST_INTRO + + "\n\n" + + block + + "\n" + + "\n" + + TOOLS_SECTION + ) high_plain = _base("HIGH") + docs_block high_steered = _base("HIGH") + "\n" + build_steering_section(["be concise"]) + docs_block # Phase 71: the owner-permitted plain-text line is part of the diff --git a/tests/unit/test_prompts_dates.py b/tests/unit/test_prompts_dates.py index 6c10fd6..e00cebc 100644 --- a/tests/unit/test_prompts_dates.py +++ b/tests/unit/test_prompts_dates.py @@ -58,6 +58,7 @@ def _doc( title: str = "T", content: str = "CONTENT", created_at: datetime = CREATED_AT, + summary: str | None = None, ) -> Document: return Document( id=uuid.uuid4(), @@ -68,6 +69,7 @@ def _doc( content=content, content_hash="0" * 64, created_at=created_at, + summary=summary, ) @@ -171,6 +173,32 @@ def test_high_block_date_always_present_for_every_document() -> None: assert prompt.count("\n' + "The stored summary.\n" + "" + ) + assert block in prompt + # Attribute order pinned: date directly after title, body after. + assert f'title="T" date="{DATE}">' in prompt + # The full content stays out (A6) — only the summary rides the block. + assert "FULL_CONTENT_SENTINEL_42" not in prompt + + # -------------------------------------------------------------------- # The deflection prompt — byte-identical to the pre-phase text (A8) # -------------------------------------------------------------------- diff --git a/tests/unit/test_retriever.py b/tests/unit/test_retriever.py index b929693..8514174 100644 --- a/tests/unit/test_retriever.py +++ b/tests/unit/test_retriever.py @@ -11,12 +11,16 @@ from types import SimpleNamespace import pytest +from app.config import Settings from app.models import Document +from app.rag import retriever from app.rag.retriever import ( TRUNCATION_MARKER, RetrievedChunk, select_documents, select_documents_tiered, + select_related, + select_suggested, ) @@ -258,6 +262,231 @@ def test_select_documents_wrapper_is_legacy_tiering() -> None: )[0] +# --------------------------------------------------------------------------- +# Phase 118 — select_suggested: the top-N "start here" tier, NO floor (A3) +# --------------------------------------------------------------------------- + + +def test_suggested_rank_order_by_first_seen_chunk() -> None: + """The SAME stable walk as ``select_documents_tiered``: a document's + rank is fixed by its FIRST seen chunk in score-descending order — a + doc whose best chunk appears later in the input list still ranks + where that chunk falls.""" + a = _doc("a.md", "A" * 50) + b = _doc("b.md", "B" * 50) + c = _doc("c.md", "C" * 50) + chunks = [ + _chunk(a, 0.4, position=0), # a's weak chunk comes first + _chunk(b, 0.8), + _chunk(a, 0.9, position=2), # a's best chunk comes last + _chunk(c, 0.5), + ] + out = select_suggested(chunks, n=5) + assert [d.path for d in out] == ["a.md", "b.md", "c.md"] + # The rows carry the full content byte-identical (A6: the content is + # what ``read`` serves later — never truncated). + assert out[0].content == "A" * 50 + assert TRUNCATION_MARKER not in out[0].content + + +def test_suggested_dedupes_multiple_chunks_to_one_row() -> None: + """Multiple hit chunks of one document collapse to a single row.""" + a = _doc("a.md", "A" * 50) + b = _doc("b.md", "B" * 50) + chunks = [ + _chunk(a, 0.2), + _chunk(b, 0.7), + _chunk(a, 0.9, position=2), + _chunk(a, 0.5), + ] + out = select_suggested(chunks, n=5) + assert [d.path for d in out] == ["a.md", "b.md"] # one row per document + assert out[0] is a + + +def test_suggested_caps_at_n_in_rank_order() -> None: + docs_in = [_doc(f"d{i}.md", "X" * 20) for i in range(7)] + chunks = [_chunk(d, 0.9 - 0.1 * i) for i, d in enumerate(docs_in)] + out = select_suggested(chunks, n=3) + assert [d.path for d in out] == ["d0.md", "d1.md", "d2.md"] + + +def test_suggested_default_cap_is_the_settings_value( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """``n`` omitted → ``BOR_SUGGESTED_DOCS`` caps the walk — default 5 + (LOCKED A3, the top-5 "start here" directive, TODO L3) — and the cap + is the setting's LIVE value, not a frozen constant.""" + docs_in = [_doc(f"d{i}.md", "X" * 20) for i in range(7)] + chunks = [_chunk(d, 0.9 - 0.1 * i) for i, d in enumerate(docs_in)] + settings = Settings(_env_file=None) # pyright: ignore[reportCallIssue] + assert settings.suggested_docs == 5 # the production default + monkeypatch.setattr(retriever, "get_settings", lambda: settings) + assert [d.path for d in select_suggested(chunks)] == [ + f"d{i}.md" for i in range(5) + ] + small = Settings(_env_file=None, suggested_docs=2) # pyright: ignore[reportCallIssue] + monkeypatch.setattr(retriever, "get_settings", lambda: small) + assert [d.path for d in select_suggested(chunks)] == ["d0.md", "d1.md"] + + +def test_suggested_never_filters_on_cosine_floor() -> None: + """NO floor (LOCKED A3): a lexical-only hit (cosine 0.0 by + construction) is a suggestion when it ranks — the contrast pin + against ``select_documents_tiered``'s floored cited tier on the SAME + input, which demotes it to the related tier.""" + a = _doc("a.md", "A" * 50) + b = _doc("b.md", "B" * 50) + lexical_only = RetrievedChunk( + chunk_id=uuid.uuid4(), + position=0, + content="X" * 10, + score=0.9, # top fused rank (the FTS hit) + document=a, + cosine=0.0, # no vector rank — lexical-only + fts_hit=True, + ) + chunks = [lexical_only, _cos_chunk(b, 0.8, 0.5)] + # Suggested: the floor never filters — a leads, b follows in rank order. + assert [d.path for d in select_suggested(chunks, n=5)] == ["a.md", "b.md"] + # Contrast: the same input through the phase-113 cited tier — the + # 0.35 usefulness bar demotes the lexical-only doc to related. + cited, related = select_documents_tiered(chunks, n=2, floor=0.35, related_cap=2) + assert [d.path for d in cited] == ["b.md"] + assert [d.path for d in related] == ["a.md"] + + +def test_suggested_tie_break_inherited_from_fused_order() -> None: + """Equal fused scores keep the input (fused) order — the stable + score-only walk inherits ``fuse()``'s (-score, -cosine, path, + position) tie-break; the selector never re-sorts it away.""" + a = _doc("a.md", "A" * 50) + b = _doc("b.md", "B" * 50) + chunks = [_cos_chunk(a, 0.7, 0.5), _cos_chunk(b, 0.7, 0.4)] # a wins on cosine + assert [d.path for d in select_suggested(chunks, n=5)] == ["a.md", "b.md"] + # A FULL tie (score AND cosine): the input position — ``fuse()``'s + # path/position tie-break already applied — decides. ``m.md`` sorts + # AFTER ``b2.md`` alphabetically, so any re-sort by path would flip + # the order; the fused input order must win. + m = _doc("m.md", "M" * 50) + b2 = _doc("b2.md", "B" * 50) + chunks = [_cos_chunk(m, 0.7, 0.4), _cos_chunk(b2, 0.7, 0.4)] + assert [d.path for d in select_suggested(chunks, n=5)] == ["m.md", "b2.md"] + + +def test_suggested_empty_chunks_yield_no_documents() -> None: + assert select_suggested([], n=5) == [] + + +# --------------------------------------------------------------------------- +# Phase 118, task 05 — select_related: the rank-6+ tier after the suggested set +# --------------------------------------------------------------------------- + + +def test_related_walk_order_after_excluded_set() -> None: + """The SAME stable score-descending walk as ``select_suggested``: + a document's rank is fixed by its FIRST seen chunk; documents in + *excluded_ids* (the suggested set) are skipped and the rest come + back in rank order.""" + a = _doc("a.md", "A" * 50) + b = _doc("b.md", "B" * 50) + c = _doc("c.md", "C" * 50) + d = _doc("d.md", "D" * 50) + chunks = [ + _chunk(a, 0.4, position=0), # a's weak chunk comes first + _chunk(b, 0.8), + _chunk(a, 0.9, position=2), # a's best chunk last — a ranks first + _chunk(c, 0.5), + _chunk(d, 0.3), + ] + out = select_related(chunks, {a.id, b.id}, cap=2) + assert [x.path for x in out] == ["c.md", "d.md"] + + +def test_related_skips_excluded_documents() -> None: + """Every document in *excluded_ids* is skipped, even when it would + rank inside the cap — the suggested set never rides the related row.""" + a = _doc("a.md", "A" * 50) + b = _doc("b.md", "B" * 50) + c = _doc("c.md", "C" * 50) + chunks = [_chunk(a, 0.9), _chunk(b, 0.8), _chunk(c, 0.5)] + out = select_related(chunks, {a.id, b.id}, cap=5) + assert out == [c] + # Every doc excluded → empty, even with room left in the cap. + assert select_related(chunks, {a.id, b.id, c.id}, cap=5) == [] + + +def test_related_caps_at_cap_in_rank_order() -> None: + """The phase-118 turn wiring on 9 docs: suggested = the top 5, + related = rank 6–7 (capped at 2), disjoint from the suggested set.""" + docs_in = [_doc(f"d{i}.md", "X" * 20) for i in range(9)] + chunks = [_chunk(d, 0.9 - 0.1 * i) for i, d in enumerate(docs_in)] + suggested = select_suggested(chunks, n=5) + out = select_related(chunks, {d.id for d in suggested}, cap=2) + assert [x.path for x in out] == ["d5.md", "d6.md"] # rank 6–7, capped + suggested_paths = {d.path for d in suggested} + assert suggested_paths.isdisjoint({x.path for x in out}) + + +def test_related_cap_zero_yields_empty() -> None: + """cap=0 is the kill switch (related_max_docs=0): no related docs, + the pre-phase-113 visibility.""" + a = _doc("a.md", "A" * 50) + b = _doc("b.md", "B" * 50) + c = _doc("c.md", "C" * 50) + chunks = [_chunk(a, 0.9), _chunk(b, 0.8), _chunk(c, 0.5)] + assert select_related(chunks, {a.id}, cap=0) == [] + + +def test_related_never_filters_on_cosine_floor() -> None: + """NO floor: a lexical-only (cosine 0.0) doc is related when it + ranks after the excluded set — the related tier is visibility, not + citation (phase 118 applies no cosine floor to it).""" + a = _doc("a.md", "A" * 50) + b = _doc("b.md", "B" * 50) + lexical_only = RetrievedChunk( + chunk_id=uuid.uuid4(), + position=0, + content="X" * 10, + score=0.9, # top fused rank (the FTS hit) + document=a, + cosine=0.0, # no vector rank — lexical-only + fts_hit=True, + ) + chunks = [lexical_only, _cos_chunk(b, 0.8, 0.5)] + out = select_related(chunks, set(), cap=5) + assert [x.path for x in out] == ["a.md", "b.md"] + + +def test_related_dedupes_multiple_chunks_to_one_row() -> None: + """Multiple hit chunks of one document collapse to a single row + (first-seen-chunk rank, dedupe by document.id — the shared walk).""" + a = _doc("a.md", "A" * 50) + b = _doc("b.md", "B" * 50) + chunks = [_chunk(a, 0.2), _chunk(b, 0.7), _chunk(a, 0.9, position=2)] + out = select_related(chunks, set(), cap=5) + assert [x.path for x in out] == ["a.md", "b.md"] # one row per document + assert out[0] is a + + +def test_related_tie_break_inherited_from_fused_order() -> None: + """Equal fused scores keep the input (fused) order — the stable + score-only walk inherits ``fuse()``'s (-score, -cosine, path, + position) tie-break; the selector never re-sorts it away.""" + a = _doc("a.md", "A" * 50) + b = _doc("b.md", "B" * 50) + chunks = [_cos_chunk(a, 0.7, 0.5), _cos_chunk(b, 0.7, 0.4)] # a wins on cosine + assert [x.path for x in select_related(chunks, set(), cap=5)] == ["a.md", "b.md"] + m = _doc("m.md", "M" * 50) + b2 = _doc("b2.md", "B" * 50) + chunks = [_cos_chunk(m, 0.7, 0.4), _cos_chunk(b2, 0.7, 0.4)] # full tie + assert [x.path for x in select_related(chunks, set(), cap=5)] == ["m.md", "b2.md"] + + +def test_related_empty_chunks_yield_no_documents() -> None: + assert select_related([], set(), cap=5) == [] + + # --------------------------------------------------------------------------- # Hybrid retrieval (A7): RRF fusion + lexical tsquery # --------------------------------------------------------------------------- diff --git a/tests/unit/test_schemas.py b/tests/unit/test_schemas.py index 1704de9..af5a31b 100644 --- a/tests/unit/test_schemas.py +++ b/tests/unit/test_schemas.py @@ -331,7 +331,16 @@ def test_extra_keys_still_forbidden() -> None: def test_minimal_message_still_validates() -> None: """Optional keys may be ABSENT exactly as pre-phase-83.""" msg = ChatMessage.model_validate({"who": "user", "text": "hi"}) - assert (msg.sources, msg.deflected, msg.suggestions, msg.thinking, msg.tools, msg.stopped) == ( + assert ( + msg.sources, + msg.related, + msg.deflected, + msg.suggestions, + msg.thinking, + msg.tools, + msg.stopped, + ) == ( + None, None, None, None, @@ -358,6 +367,7 @@ def test_realistic_bor_chat_v1_payload_round_trips() -> None: "who": "user", "text": "How did I install k3s on the new node?", "sources": None, + "related": None, "deflected": None, "suggestions": None, "thinking": None, @@ -371,6 +381,14 @@ def test_realistic_bor_chat_v1_payload_round_trips() -> None: {"source": "Homelab", "path": "kubernetes.md", "title": "Kubernetes Cluster"}, {"source": "Deployments", "path": "k3s-install.md", "title": "k3s Install Notes"}, ], + # Phase 113 related-doc tier — an ACCEPTED key (the phase-113 + # omission of this field made extra="forbid" 422 every + # done-time auto-save carrying it, so grounded turns' brain + # messages never persisted — the A2 quiet failure swallowed + # the 422). Round-trips like sources. + "related": [ + {"source": "Homelab", "path": "traefik.md", "title": "Traefik Notes"} + ], "deflected": False, "suggestions": None, "thinking": "The kubernetes doc covers the cluster layout…", @@ -398,6 +416,7 @@ def test_realistic_bor_chat_v1_payload_round_trips() -> None: "who": "user", "text": "And what ports does Traefik expose?", "sources": None, + "related": None, "deflected": None, "suggestions": None, "thinking": None, @@ -408,6 +427,7 @@ def test_realistic_bor_chat_v1_payload_round_trips() -> None: "who": "brain", "text": "Traefik exposes 80/443 on every node.", "sources": None, + "related": None, "deflected": None, "suggestions": ["What is the Traefik dashboard password?"], "thinking": None, @@ -433,6 +453,7 @@ def test_realistic_payload_round_trips_through_update_model() -> None: "who": "brain", "text": "answer", "sources": [_source_ref()], + "related": None, "deflected": None, "suggestions": ["follow-up?"], "thinking": "scratchpad", diff --git a/tests/unit/test_source_chip_quality.py b/tests/unit/test_source_chip_quality.py index 8941832..7c7f0fe 100644 --- a/tests/unit/test_source_chip_quality.py +++ b/tests/unit/test_source_chip_quality.py @@ -1,13 +1,15 @@ """Unit: the phase-113 source-chip-quality contract (TODO L5 + L2c — "the 2nd chip is often noise the answer never used"). -Phase 113 demotes sub-floor hits out of the citation surface: the -done frame carries the cited tier in ``sources`` (rendered by -``appendSources`` as ``.source-chip`` pills, UNCHANGED) and the -related tier in ``related`` (rendered by the NEW ``appendRelated`` as -the de-emphasized labeled row — ``.related-doc`` links, never -``.source-chip``). A deflected turn carries ``sources: []`` → zero -chips; its weak hits live in the related row only. +Phase 118 re-tiers the same frame (LOCKED A3/A4): ``sources`` carries +the suggested tier (top-5, NO floor) + the agent-read docs (deduped) +— rendered by ``appendSources`` as ``.source-chip`` pills, UNCHANGED — +and ``related`` carries rank 6+ after the suggested set (rendered by +``appendRelated`` as the de-emphasized labeled row — ``.related-doc`` +links, never ``.source-chip``). A deflected turn carries +``sources: []`` → zero chips; its weak hits are the suggested tier +(the durable record), and with ≤5 retrieved docs the related row is +empty. This module pins the STATIC SOURCES the UI contract stands on, in the house source-pin pattern (the test_chip_sizing_question_cap.py @@ -26,24 +28,23 @@ house source-pin pattern (the test_chip_sizing_question_cap.py 1. **both docs weak** ("What is the capital of Mongolia?" → ``Trooper_Nagraz.pl`` + ``Trooper_Begzei.pl``, both unrelated) — - cited tier empty, the weak hits ride the related tier, capped at - ``related_max_docs``; the FTS hit without vector corroboration - stays LOW (the A8-revised "Mongolia" case); + the weak hits are SUGGESTED (no floor, A3; the durable record), + nothing left for the related tier; the FTS hit without vector + corroboration stays LOW (the A8-revised "Mongolia" case); 2. **one strong + one weak** (the phase-gate question answered from - ``brain-of-reese/.agents/validate.sh``; the 2nd chip - ``ServMon/README.md`` unused) — exactly ONE cited ref, the weak - doc in ``related``; + ``brain-of-reese/.agents/validate.sh``; ``ServMon/README.md`` + alongside) — BOTH suggested (no floor) ⇒ both cited refs (A4), + no related tier; 3. **the Nagraz case** (``Trooper_Nagraz.pl`` strong, ``Trooper_Byzin.pl`` weak — same shape, different fixtures); - 4. **the meta/history question** (no doc clears the bar, the agent - reads nothing — chips ``app/api/suggestions.py`` + - ``108_history_wire_check/00_phase.md``, neither used) — pinned on - the DONE FRAME (endpoint-level, fake retriever/LLM/session): the - frame is row-only — ``sources: []`` (the UI's chip list — zero - chips) + the weak hits in ``related``; - 5. **the agent-read exemption** (LOCKED A2): a below-floor doc the - agent ``read`` via the tool joins ``sources`` (cited, last) and is - excluded from ``related``. + 4. **the meta/history question** (the agent reads nothing — + ``app/api/suggestions.py`` + ``108_history_wire_check/00_phase.md`` + alongside) — pinned on the DONE FRAME (endpoint-level, fake + retriever/LLM/session): ``sources: []`` (zero chips) and an EMPTY + related row (both weak docs are suggested, ≤5 docs retrieved); + 5. **the agent-read exemption** (LOCKED A4): a rank-6+ (related- + tier) doc the agent ``read`` via the tool joins ``sources`` + (cited, last) and is excluded from ``related``. The browser behavior (chip counts on a single-source question, zero chips on a deflected turn) is E2E-gated by @@ -411,14 +412,18 @@ def _shape_settings() -> Settings: """The PRODUCTION calibration (the code defaults, explicit) — the four shapes were observed LIVE under this threshold/floor pair. ``_env_file=None`` keeps the mock-calibrated values from - ``tests/conftest.py`` (and any local ``.env``) out of the pin.""" + ``tests/conftest.py`` (and any local ``.env``) out of the pin. + ``source_usefulness_floor`` / ``top_n_docs`` are legacy phase-113 + settings — phase 118 retired their seeding role (A6; ``plan_turn`` + never consults them), they are carried here for completeness.""" return Settings( _env_file=None, # pyright: ignore[reportCallIssue] relevance_threshold=0.62, lexical_support_floor=0.35, - source_usefulness_floor=0.35, # LOCKED A2 default - related_max_docs=2, # LOCKED A4 default - top_n_docs=2, # the ceiling — never a quota (LOCKED A2) + source_usefulness_floor=0.35, # retired by phase 118 (A6) — not consulted + related_max_docs=2, # the rank-6+ row cap (LOCKED A4) + top_n_docs=2, # retired by phase 118 (A6) — not consulted + suggested_docs=5, # the "start here" cap (LOCKED A3) ) @@ -461,12 +466,12 @@ def _chunk( def test_shape_1_mongolia_both_docs_weak_cite_nothing() -> None: """Observed shape 1 (TODO L110–113): "What is the capital of Mongolia?" → chips ``Trooper_Nagraz.pl`` + ``Trooper_Begzei.pl``, - BOTH unrelated. Both below the bar: the cited tier is EMPTY (zero - citation chips) and the weak hits ride the related tier — in rank - order, capped at ``related_max_docs`` (the 3rd weak doc drops out). - The FTS hit without vector corroboration (0.20 < the 0.35 lexical - floor) stays LOW — the A8-revised "Mongolia" case; the weak content - never reaches the LLM.""" + BOTH unrelated. Phase 118 (A3): the floor never filters — the weak + hits are the SUGGESTED tier (the durable record's input), and with + three retrieved docs nothing is left for the related tier (rank + 6+). The FTS hit without vector corroboration (0.20 < the 0.35 + lexical floor) stays LOW — the A8-revised "Mongolia" case; the weak + content never reaches the LLM (titles only).""" nagraz = _doc("scripts", "scripts/Trooper_Nagraz.pl", "Trooper_Nagraz.pl", "NAGRAZ_PL_CONTENT") begzei = _doc("scripts", "scripts/Trooper_Begzei.pl", "Trooper_Begzei.pl", @@ -476,64 +481,76 @@ def test_shape_1_mongolia_both_docs_weak_cite_nothing() -> None: chunks = [ _chunk(nagraz, 0.033, cosine=0.20, fts_hit=True), # rank 1, lexical hit _chunk(begzei, 0.031, cosine=0.12), - _chunk(third, 0.030, cosine=0.10), # below the cap — related drops it + _chunk(third, 0.030, cosine=0.10), ] plan = chat_api.plan_turn(chunks, _shape_settings()) assert plan.deflected is True # 0.20 < 0.62 AND 0.20 < the 0.35 lex floor - assert plan.docs == [] # NO citation slot below the bar - assert [d.title for d in plan.related_docs] == [ + # No floor (A3): the weak hits are suggested, in rank order (≤5). + assert [d.title for d in plan.suggested_docs] == [ "Trooper_Nagraz.pl", "Trooper_Begzei.pl", - ] # rank order, capped at related_max_docs (2) - assert len(plan.related_docs) <= 2 + "Trooper_Third.pl", + ] + assert plan.related_docs == [] # no rank-6+ doc among 3 retrieved # The LOW prompt is titles only — none of the weak content is sent. assert "NAGRAZ_PL_CONTENT" not in plan.system_prompt assert "Trooper_Nagraz.pl" in plan.system_prompt # weak-hit titles carried assert plan.suggestions # the "Maybe try" chips are unchanged -def test_shape_2_validate_sh_strong_plus_unused_second_chip() -> None: +def test_shape_2_validate_sh_strong_plus_weak_second_suggested() -> None: """Observed shape 2 (TODO L114–116): the phase-gate question is - answered from ``brain-of-reese/.agents/validate.sh`` — the 2nd chip - ``ServMon/README.md`` was NEVER used. The strong doc clears the bar - and takes the only cited slot (top_n_docs is a ceiling, not a - quota); the weak 2nd doc demotes to related — never a citation. - The HIGH prompt carries the cited content only.""" + answered from ``brain-of-reese/.agents/validate.sh`` with + ``ServMon/README.md`` retrieved alongside (weak cosine). Phase 118 + (A3): the floor never filters — the weak 2nd doc is SUGGESTED too + (both docs seed the HIGH prompt as summaries; the A5 fallback + carries the short fixture content whole), and A4 makes both + citation refs on the done frame — the "unused 2nd chip" is the + phase-113 shape, retired by the owner directive.""" validate = _doc("brain-of-reese", ".agents/validate.sh", "validate.sh", "VALIDATE_SH_CONTENT") servmon = _doc("ServMon", "README.md", "ServMon README", "SERVMON_README_CONTENT") chunks = [ - _chunk(validate, 0.90, cosine=0.70), # clears threshold AND bar + _chunk(validate, 0.90, cosine=0.70), # clears the threshold _chunk(servmon, 0.80, cosine=0.20), # high fused rank, weak cosine ] plan = chat_api.plan_turn(chunks, _shape_settings()) assert plan.deflected is False # 0.70 >= 0.62 - assert [d.title for d in plan.docs] == ["validate.sh"] # exactly ONE cited - assert [d.title for d in plan.related_docs] == ["ServMon README"] - assert "VALIDATE_SH_CONTENT" in plan.system_prompt - assert "SERVMON_README_CONTENT" not in plan.system_prompt + assert [d.title for d in plan.suggested_docs] == [ + "validate.sh", + "ServMon README", + ] # both suggested (no floor), rank order + assert plan.related_docs == [] # nothing beyond rank 2 for 2 docs + assert "VALIDATE_SH_CONTENT" in plan.system_prompt # A5 preview fallback + assert "SERVMON_README_CONTENT" in plan.system_prompt # ditto -def test_shape_3_nagraz_answered_by_own_doc_byzin_uncited() -> None: +def test_shape_3_nagraz_answered_by_own_doc_byzin_suggested() -> None: """Observed shape 3 (TODO L117–119): the Trooper_Nagraz question is - answered from ``Trooper_Nagraz.pl`` — the 2nd chip - ``Trooper_Byzin.pl`` uncited. The SAME shape as shape 2 with - different fixtures — the bar filters the 2nd chip; it is not a - coincidence of the validate.sh pair.""" + answered from ``Trooper_Nagraz.pl`` with ``Trooper_Byzin.pl`` + retrieved alongside (weak cosine). The SAME shape as shape 2 with + different fixtures — phase 118's no-floor tiering suggests BOTH + (the phase-113 "bar filters the 2nd chip" story is retired); the + HIGH prompt seeds both summaries (A5 fallback for the short + fixture content).""" nagraz = _doc("scripts", "scripts/Trooper_Nagraz.pl", "Trooper_Nagraz.pl", "NAGRAZ_PL_CONTENT") byzin = _doc("scripts", "scripts/Trooper_Byzin.pl", "Trooper_Byzin.pl", "BYZIN_PL_CONTENT") chunks = [ _chunk(nagraz, 0.85, cosine=0.70), - _chunk(byzin, 0.75, cosine=0.15), # below the bar + _chunk(byzin, 0.75, cosine=0.15), # weak cosine — still suggested (A3) ] plan = chat_api.plan_turn(chunks, _shape_settings()) assert plan.deflected is False - assert [d.title for d in plan.docs] == ["Trooper_Nagraz.pl"] # 1 cited - assert [d.title for d in plan.related_docs] == ["Trooper_Byzin.pl"] # 1 related - assert "BYZIN_PL_CONTENT" not in plan.system_prompt + assert [d.title for d in plan.suggested_docs] == [ + "Trooper_Nagraz.pl", + "Trooper_Byzin.pl", + ] # both suggested (no floor), rank order + assert plan.related_docs == [] + assert "NAGRAZ_PL_CONTENT" in plan.system_prompt # A5 preview fallback + assert "BYZIN_PL_CONTENT" in plan.system_prompt # ditto # ---------- task 03: done-frame wire (endpoint-level fakes, no stack) ---------- @@ -664,21 +681,22 @@ def _fake_retriever(chunks: list[RetrievedChunk]) -> Any: return retrieve -def test_shape_4_meta_question_deflected_frame_is_row_only( +def test_shape_4_meta_question_deflected_frame_has_no_chips_or_row( client: TestClient, chip_env: tuple[_FakeSession, _CannedLLM], monkeypatch: pytest.MonkeyPatch, ) -> None: """Observed shape 4 (TODO L120–123), pinned on the DONE FRAME: a - meta question about the conversation's own history → chips + meta question about the conversation's own history → weak hits ``app/api/suggestions.py`` + ``108_history_wire_check/00_phase.md``, - neither used. No doc clears the bar and the agent reads nothing — - the frame is ROW-ONLY: ``sources: []`` (the UI chips every source - entry — zero chips) with the weak hits in ``related`` (rank order, - ≤ ``related_max_docs``) — the de-emphasized row's links (the row's - rendering itself is pinned by task 02's source tests + the E2E). - The weak retrieval stays durably recorded (LOCKED A3); the weak - content never reaches the LLM (LOW prompt, titles only).""" + neither used. Phase 118: the agent reads nothing, the weak hits + are the SUGGESTED tier (no floor, A3 — the durable record's input) + and, with only two retrieved docs, nothing reaches rank 6+ — the + frame carries ``sources: []`` (zero chips — a deflected answer + cites nothing) AND an empty ``related`` row (the row's rendering + itself is pinned by task 02's source tests + the E2E). The weak + retrieval stays durably recorded (LOCKED A3); the weak content + never reaches the LLM (LOW prompt, titles only).""" session, llm = chip_env suggestions = _doc("brain-of-reese", "app/api/suggestions.py", "suggestions.py", "SUGGESTIONS_PY_CONTENT") @@ -701,13 +719,9 @@ def test_shape_4_meta_question_deflected_frame_is_row_only( assert done["type"] == "done" assert done["deflected"] is True assert done["sources"] == [] # zero citation chips on the wire - related = done["related"] - assert [(s["source"], s["path"]) for s in related] == [ - ("brain-of-reese", "app/api/suggestions.py"), - ("brain-of-reese", ".agents/108_history_wire_check/00_phase.md"), - ] # rank order - assert len(related) <= 2 # related_max_docs - assert all(s["title"] for s in related) # the row's links carry the identity + # Phase 118 (A3): both weak docs are suggested (≤5, no floor) — + # nothing reaches rank 6+, so the related row is empty. + assert done["related"] == [] assert done["suggestions"] # the "Maybe try" chips are unchanged (system, _user) = llm.seen[0][0], llm.seen[0][1] @@ -722,16 +736,17 @@ def test_shape_4_meta_question_deflected_frame_is_row_only( assert "108_history_wire_check/00_phase.md" in row.sources -def test_done_frame_single_cited_ref_strong_plus_weak( +def test_done_frame_carries_suggested_refs_strong_plus_weak( client: TestClient, chip_env: tuple[_FakeSession, _CannedLLM], monkeypatch: pytest.MonkeyPatch, ) -> None: - """Shape 2 on the wire — the single-document question's input to - "exactly one citation chip" (the E2E asserts the rendered chip): - the bar-clearing doc is the ONLY ``sources`` ref; the weak 2nd doc - rides ``related``; the tiers are disjoint (the done frame's dedupe). - The durable record keeps the FULL retrieval (LOCKED A3).""" + """Shape 2 on the wire under phase 118 (LOCKED A4): the citation + surface is the suggested tier + the agent's reads (deduped) — + with two retrieved docs and no read, BOTH docs are ``sources`` + refs (no floor — A3); nothing reaches rank 6+, so ``related`` is + empty; the tiers stay disjoint (the done frame's dedupe). The + durable record keeps the FULL retrieval (LOCKED A3).""" session, _llm = chip_env validate = _doc("brain-of-reese", ".agents/validate.sh", "validate.sh", "VALIDATE_SH_CONTENT") @@ -753,10 +768,9 @@ def test_done_frame_single_cited_ref_strong_plus_weak( assert done["deflected"] is False assert [(s["source"], s["path"]) for s in done["sources"]] == [ ("brain-of-reese", ".agents/validate.sh"), - ] # EXACTLY one citation chip on the wire - assert [(s["source"], s["path"]) for s in done["related"]] == [ - ("ServMon", "README.md"), + ("ServMon", "README.md"), # A4: suggested + read — both suggested (A3) ] + assert done["related"] == [] # nothing reaches rank 6+ for 2 docs cited = {(s["source"], s["path"]) for s in done["sources"]} related = {(s["source"], s["path"]) for s in done["related"]} assert cited.isdisjoint(related) @@ -769,20 +783,25 @@ def test_done_frame_single_cited_ref_strong_plus_weak( assert "ServMon/README.md" in row.sources -def test_agent_read_below_floor_doc_joins_sources( +def test_agent_read_related_doc_is_cited_not_related( client: TestClient, chip_env: tuple[_FakeSession, _CannedLLM], monkeypatch: pytest.MonkeyPatch, ) -> None: - """The agent-read exemption (LOCKED A2): a doc UNDER the bar that - the agent ``read`` via the tool is cited by definition — the model - read it, so it was used. It joins ``sources`` (after the retrieved - cited docs, deduped) and is EXCLUDED from ``related`` (a used doc - must never read as "nearby"); the other below-floor doc stays in - the tier. The read content reached the model (the tool result in - the follow-up request).""" + """The agent-read exemption (LOCKED A4, phase-118 tiering): a + rank-6+ doc — the related tier ("nearby docs") — that the agent + ``read`` via the tool is cited by definition: the model read it, so + it was used. It joins ``sources`` (after the suggested docs — it + was not suggested, so the read appends it last, deduped) and is + EXCLUDED from ``related`` (a used doc must never read as "nearby"); + the other rank-6+ doc stays in the tier. The read content reached + the model (the tool result in the follow-up request).""" session, _default_llm = chip_env strong = _doc("docs", "strong.md", "Strong", "STRONG_DOC_CONTENT") + fillers = [ + _doc("docs", f"filler{i}.md", f"Filler {i}", f"FILLER_{i}_CONTENT") + for i in range(1, 5) # ranks 2–5 — fill the suggested tier + ] weak_b = _doc("docs", "weak-b.md", "Weak B", "WEAK_B_READ_BY_AGENT") weak_c = _doc("docs", "weak-c.md", "Weak C", "WEAK_C_CONTENT") monkeypatch.setattr( @@ -790,9 +809,13 @@ def test_agent_read_below_floor_doc_joins_sources( "retrieve", _fake_retriever( [ - _chunk(strong, 0.90, cosine=0.70), # clears the bar - _chunk(weak_b, 0.80, cosine=0.20), # below the bar — read by the agent - _chunk(weak_c, 0.70, cosine=0.10), # below the bar — nobody reads it + _chunk(strong, 0.90, cosine=0.70), # clears the threshold (rank 1) + _chunk(fillers[0], 0.85, cosine=0.30), # ranks 2–5: suggested + _chunk(fillers[1], 0.80, cosine=0.30), + _chunk(fillers[2], 0.75, cosine=0.30), + _chunk(fillers[3], 0.72, cosine=0.30), + _chunk(weak_b, 0.70, cosine=0.20), # rank 6 — related; read by the agent + _chunk(weak_c, 0.65, cosine=0.10), # rank 7 — related; nobody reads it ] ), ) @@ -815,7 +838,10 @@ def test_agent_read_below_floor_doc_joins_sources( assert "WEAK_B_READ_BY_AGENT" in tool_msgs[0]["content"] sources = [(s["source"], s["path"]) for s in done["sources"]] - assert sources == [("docs", "strong.md"), ("docs", "weak-b.md")] # read ⇒ cited, last + # A4: suggested (5) + the read doc (last — it was not suggested). + assert sources[-1] == ("docs", "weak-b.md") # read ⇒ cited, last + assert len(sources) == 6 + assert ("docs", "weak-c.md") not in sources # never suggested, never read related = [(s["source"], s["path"]) for s in done["related"]] assert related == [("docs", "weak-c.md")] # the read doc is not "nearby" assert set(sources).isdisjoint(set(related)) @@ -823,3 +849,4 @@ def test_agent_read_below_floor_doc_joins_sources( (row,) = session.added assert isinstance(row, QueryLog) assert "weak-b.md" in row.sources # the full retrieval is recorded (A3) + assert "weak-c.md" in row.sources # … rank 6+ included (suggested + related + read)