All gates re-verified against a stable tree. Report: **Verified (all 6 tasks in `complete/`; this was the final pass)** - `SourceLabel`+alembic `0030` (dev DB `alembic current` = `0030 (head)`, table present, 0 rows); admin-only `GET/PUT/DELETE /api/source-labels` (`require_admin` router-wide, `resolve_source_name`, partial-update caps/404s); Sources-page "Names, labels and tags" card (read-only derived-name `<code>`, per-row `saveInFlight`, 404 note that survives the settle, chips reuse `.git-source-hidden-count`); `question_name_hits` D2 rule; ONE `all_labels` read per turn in `chat.py` feeding both the `source_name_hit` flag and the prompt surfaces; `source_name_hit_bonus=0.005` in `_selection_order`; ` name=`/` label=` in `build_high_prompt` + `build_overview_prompt(names=…)`. - **Completion criteria:** E2E `tests/e2e/test_source_name_labels.py` ✅ 7 passed (arms a–e: editor all kinds, control/named question, rename-never-breaks-identity, 404 note, reset) · full suite ✅ · coverage ✅ · README/AGENTS ✅ · phase dir in `complete/` ✅ · commit left to the harness. - **Gates:** `uv run pytest --cov=app --cov-report=term-missing` → **exit 0, TOTAL 7160 stmts / 16 missed = 99%** (>90%) · `uv run pytest tests/e2e/test_source_name_labels.py -v --no-cov` → **7 passed, exit 0** · regression E2E isolated, exit 0: `test_document_viewer`, `test_admin_auth`, `test_api_tokens`, `test_git_sources_admin`, `test_kb_tree`, `test_local_directory_sources` · `uv run ruff check .` → All checks passed · `uv run pyright` → 0 errors. - **Battery (task 04 criterion):** `uv run python -m scripts.eval_retrieval --from-file tests/fixtures/retrieval_battery.txt` at default vs `BOR_SOURCE_NAME_HIT_BONUS=0` → **`diff` empty (line-for-line identical)**, matching the recorded arms (dev KB is empty, so every question deflects; the observable boost is unit/E2E-pinned). - **Documentation:** README — fixed the `/git-sources.html` rows (nav label is **Sources**) and cross-linked the phase-191 card from that section; full audit otherwise clean: all 86 `BOR_*` settings documented with defaults matching `app/config.py`, 0 broken file/dir references and 0 broken anchors in README+AGENTS, AGENTS rule 17 re-checked line-by-line against the code. - **Notable / deviations:** (1) a **concurrent pipeline process** (`phased-execution/progress.mjs` for `191_…_00_phase`) was mutating this same working tree mid-pass — it rewrote `tests/e2e/test_source_name_labels.py` (11→7 tests), `frontend/assets/git-sources.js`, `tests/unit/test_source_labels_editor_ui.py` and `AGENTS.md` at 05:27; every gate above was (re-)run after those edits with the tree no source file newer than the green full run. (2) The task-06 report/phase text claims regression suites `test_chat_token_gate.py`, `test_git_sources_page.py`, `test_rag_browser.py` are green — **those files have never existed in git history** (those runs exit 4); I ran the real equivalents listed above instead (phase files are not mine to edit). (3) **No live changes**: the VM DB already has `source_labels` (0 rows) but the deployed app predates this uncommitted code, so deploying `/api/source-labels` to the live service is the owner's post-commit step, not part of this phase's criteria. - **Next pending phase:** none — `191_source_name_labels_tags` is the only entry in `.agents/phases/todo/`.
86 lines
3.5 KiB
Python
86 lines
3.5 KiB
Python
"""source_labels: per-source name, label and tags (phase 191, task 01)
|
|
|
|
Revision ID: 0030
|
|
Revises: 0029
|
|
Create Date: 2026-10-18
|
|
|
|
``TODO.md`` L4: "Sources need a name, a label, tag(s) so you can focus
|
|
the model when prompting if you have a project name or a standard name
|
|
you reference things by." This revision carries the storage half
|
|
(phase 191 D1) — the matching rule (D2), the boost (D3), the API (D5)
|
|
and the UI (D6) are the later tasks.
|
|
|
|
* ``source_labels`` — one row per SOURCE the owner has named:
|
|
|
|
* ``source`` — VARCHAR(120) NOT NULL, PRIMARY KEY. The source name
|
|
EXACTLY as ``documents.source`` carries it (the derived basename —
|
|
the clone dir / local dir / upload file stem): the join key to the
|
|
documents and to the ``git_sources`` row. The PK is the uniqueness
|
|
guard (one label row per source — the ``app_settings`` 0028
|
|
reasoning). **An ALIAS key only (D1):** a label row never renames a
|
|
document, a ``source/path`` locator, a citation or the KB tree.
|
|
* ``display_name`` — TEXT nullable (the owner's name for the source,
|
|
"the git server"). Plain TEXT: the ≤ 120-char trimmed limit is
|
|
enforced at the API boundary (the phase-121 ``git_sources.branch``
|
|
pattern — plain column, boundary validation). NULL = no name.
|
|
* ``label`` — TEXT nullable (the short one-line description /
|
|
category, ≤ 200 trimmed, same boundary rule). NULL = no label.
|
|
* ``tags`` — JSONB NOT NULL with server default ``'[]'`` (the
|
|
phase-89 ``git_sources.ignore_paths`` shape): the focus tokens,
|
|
each ≤ 32 chars, ≤ 10 entries, guarded at the API boundary
|
|
(``app.rag.source_labels.normalize_tags``).
|
|
* ``updated_at`` — TIMESTAMPTZ NOT NULL, ``now()`` server default
|
|
(the ``app_settings`` / ``folder_summaries`` house style).
|
|
|
|
NO backfill (D1): the table is created EMPTY — a source without a row
|
|
has no name/label/tags and behaves exactly as pre-phase, so "no label
|
|
row → every surface byte-identical" holds without a data step. No index
|
|
beyond the PK: every read is a PK lookup or a whole-table walk of a
|
|
handful of rows (the ``app_settings`` "never a query surface" note).
|
|
|
|
One additive, fully reversible migration (A13).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import sqlalchemy as sa
|
|
from sqlalchemy.dialects import postgresql
|
|
|
|
from alembic import op
|
|
|
|
revision = "0030"
|
|
down_revision = "0029"
|
|
branch_labels = None
|
|
depends_on = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
# No index beyond the PK (every read is a PK lookup or the whole
|
|
# handful-row table), no backfill (an empty table = every source
|
|
# keeps its pre-phase "no row" behavior).
|
|
op.create_table(
|
|
"source_labels",
|
|
sa.Column("source", sa.String(length=120), primary_key=True),
|
|
sa.Column("display_name", sa.Text(), nullable=True),
|
|
sa.Column("label", sa.Text(), nullable=True),
|
|
sa.Column(
|
|
"tags",
|
|
postgresql.JSONB(astext_type=sa.Text()),
|
|
server_default=sa.text("'[]'"),
|
|
nullable=False,
|
|
),
|
|
sa.Column(
|
|
"updated_at",
|
|
sa.DateTime(timezone=True),
|
|
server_default=sa.func.now(),
|
|
nullable=False,
|
|
),
|
|
)
|
|
|
|
|
|
def downgrade() -> None:
|
|
# The table is the only 0030 artefact — dropping it leaves 0029's
|
|
# schema byte-identical (A13, fully reversible). No owner data is
|
|
# trapped: the names/labels/tags are re-enterable from the Sources
|
|
# page (task 03), and nothing else was mutated by 0030.
|
|
op.drop_table("source_labels")
|