phase: 191_source_name_labels_tags
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/`.
This commit is contained in:
+75
-17
@@ -291,6 +291,16 @@ class RetrievedChunk:
|
||||
:func:`fuse`'s ``replace()`` copies it (the double-hit merge ORs it
|
||||
in — a vector row that is also the name hit's representative chunk
|
||||
keeps the flag).
|
||||
* ``source_name_hit`` — True for a chunk whose parent document's
|
||||
SOURCE the question named (phase 191, D2/D3): the flag is set at
|
||||
the retrieval boundary in ``app/api/chat.py`` from the turn's ONE
|
||||
``all_labels`` read (the pure D2 rule per view — the same read that
|
||||
feeds the ``name``/``label`` prompt attributes, task 05) — the
|
||||
selection tier's SECOND bonus input (``source_name_hit_bonus``, a
|
||||
separate knob). Default
|
||||
``False`` keeps every ordinary construction valid; :func:`fuse`'s
|
||||
``replace()`` copies it (the double-hit merge ORs it in, mirroring
|
||||
``name_hit``).
|
||||
"""
|
||||
|
||||
chunk_id: uuid.UUID
|
||||
@@ -302,6 +312,7 @@ class RetrievedChunk:
|
||||
fts_hit: bool = False
|
||||
is_summary: bool = False
|
||||
name_hit: bool = False
|
||||
source_name_hit: bool = False
|
||||
|
||||
|
||||
def lexical_tsquery(question: str) -> str | None:
|
||||
@@ -339,9 +350,11 @@ def fuse(
|
||||
Lexical-only hits (no vector rank) enter with ``cosine=0.0`` and
|
||||
``fts_hit=True``; vector chunks matched by the lexical list get
|
||||
``fts_hit=True`` in place (the input objects are mutated — callers
|
||||
should not reuse them afterwards), and ``name_hit=True`` is ORed in
|
||||
should not reuse them afterwards), ``name_hit=True`` is ORed in
|
||||
(a vector row that is also the name hit's representative chunk is a
|
||||
name-hit row — the phase-119 selection tier must see it).
|
||||
name-hit row — the phase-119 selection tier must see it), and
|
||||
``source_name_hit`` is ORed in the SAME way (phase 191, D3 — a
|
||||
vector row merged with a flagged source-name row keeps the flag).
|
||||
"""
|
||||
if k <= 0:
|
||||
raise ValueError("rrf k must be > 0")
|
||||
@@ -355,7 +368,10 @@ def fuse(
|
||||
if rc.chunk_id in by_id:
|
||||
existing = by_id[rc.chunk_id]
|
||||
by_id[rc.chunk_id] = replace(
|
||||
existing, fts_hit=True, name_hit=existing.name_hit or rc.name_hit
|
||||
existing,
|
||||
fts_hit=True,
|
||||
name_hit=existing.name_hit or rc.name_hit,
|
||||
source_name_hit=existing.source_name_hit or rc.source_name_hit,
|
||||
)
|
||||
fused[rc.chunk_id] += term
|
||||
else:
|
||||
@@ -754,17 +770,23 @@ def retrieve(
|
||||
def _selection_order(
|
||||
chunks: Sequence[RetrievedChunk],
|
||||
bonus: float,
|
||||
source_bonus: float = 0.0,
|
||||
) -> list[tuple[Document, float, float, int]]:
|
||||
"""The shared selection walk of the phase-119 name-hit bonus (D2,
|
||||
LOCKED A3) — one walk for ``select_suggested``, ``select_related``,
|
||||
and ``weak_hit_titles``.
|
||||
and ``weak_hit_titles``, carrying the phase-191 source-name bonus
|
||||
(D3) as its SECOND, independent knob.
|
||||
|
||||
Returns ``(document, effective_score, best_cosine, first_seen_index)``
|
||||
for each distinct document, where *effective_score* is the
|
||||
document's best fused chunk score plus *bonus* when ANY of its
|
||||
chunks carries ``name_hit`` (the D1 path match — the bonus is per
|
||||
DOCUMENT, applied ONCE no matter how many of the document's chunks
|
||||
are name hits).
|
||||
are name hits), plus *source_bonus* when ANY of its chunks carries
|
||||
``source_name_hit`` (the phase-191 D2 source-name match — the SAME
|
||||
per-DOCUMENT, apply-ONCE rule; at most TWO bonuses per document
|
||||
on the fused scale, bounded: name_hit_bonus + source_name_hit_bonus
|
||||
= 0.010 at the shipped defaults, never more).
|
||||
|
||||
The walk keeps the EXISTING selection semantics (not just the loop):
|
||||
the same stable score-descending order as
|
||||
@@ -773,16 +795,18 @@ def _selection_order(
|
||||
best cosine tracked across ALL of its chunks (the tiered walk's
|
||||
tracking).
|
||||
|
||||
* *bonus* ``== 0`` (the kill switch) or no name-hit chunk present:
|
||||
* *bonus* ``== 0`` (the kill switch) or no name-hit chunk present,
|
||||
AND *source_bonus* ``== 0`` or no source-name-hit chunk present:
|
||||
the document order is IDENTICAL to the pre-phase walk — no re-sort
|
||||
happens at all (byte-identical, LOCKED A3);
|
||||
happens at all (byte-identical, LOCKED A3, mirrored for the second
|
||||
bonus by LOCKED A3 of phase 191);
|
||||
* otherwise the documents are ordered by
|
||||
``(−effective_score, −best_cosine, document.path,
|
||||
first_seen_index)`` — the bounded re-rank: a name-hit document
|
||||
gets a head start on the fused scale, and an effective-score tie
|
||||
resolves by cosine, then path, then the pre-bonus rank.
|
||||
|
||||
The bonus lives in the SELECTION layer only: the chunk objects are
|
||||
The bonuses live in the SELECTION layer only: the chunk objects are
|
||||
never modified — their ``score``/``cosine``/``fts_hit`` (the A8
|
||||
gate's inputs) and ``query_log.top_score`` are untouched.
|
||||
"""
|
||||
@@ -790,6 +814,7 @@ def _selection_order(
|
||||
best_score: dict[uuid.UUID, float] = {}
|
||||
best_cosine: dict[uuid.UUID, float] = {}
|
||||
any_name_hit: dict[uuid.UUID, bool] = {}
|
||||
any_source_hit: dict[uuid.UUID, bool] = {}
|
||||
first_seen: dict[uuid.UUID, int] = {}
|
||||
for idx, rc in enumerate(sorted(chunks, key=lambda c: c.score, reverse=True)):
|
||||
doc = rc.document
|
||||
@@ -798,17 +823,24 @@ def _selection_order(
|
||||
best_cosine[doc.id] = rc.cosine
|
||||
if rc.name_hit:
|
||||
any_name_hit[doc.id] = True
|
||||
if rc.source_name_hit:
|
||||
any_source_hit[doc.id] = True
|
||||
else:
|
||||
order.append(doc)
|
||||
best_score[doc.id] = rc.score
|
||||
best_cosine[doc.id] = rc.cosine
|
||||
any_name_hit[doc.id] = rc.name_hit
|
||||
any_source_hit[doc.id] = rc.source_name_hit
|
||||
first_seen[doc.id] = idx
|
||||
effective = {
|
||||
doc.id: best_score[doc.id] + (bonus if any_name_hit[doc.id] else 0.0)
|
||||
doc.id: best_score[doc.id]
|
||||
+ (bonus if any_name_hit[doc.id] else 0.0)
|
||||
+ (source_bonus if any_source_hit[doc.id] else 0.0)
|
||||
for doc in order
|
||||
}
|
||||
if bonus > 0.0 and any(any_name_hit[doc.id] for doc in order):
|
||||
if (bonus > 0.0 and any(any_name_hit[doc.id] for doc in order)) or (
|
||||
source_bonus > 0.0 and any(any_source_hit[doc.id] for doc in order)
|
||||
):
|
||||
order.sort(
|
||||
key=lambda doc: (
|
||||
-effective[doc.id],
|
||||
@@ -826,17 +858,26 @@ def _selection_order(
|
||||
def weak_hit_titles(
|
||||
chunks: Sequence[RetrievedChunk],
|
||||
bonus: float | None = None,
|
||||
source_bonus: float | None = None,
|
||||
) -> list[str]:
|
||||
"""Distinct parent-document titles of *chunks*, best SELECTION score
|
||||
first (the phase-119 name-hit bonus applied — see
|
||||
:func:`_selection_order`, D2, LOCKED A3).
|
||||
first (the phase-119 name-hit bonus and the phase-191 source-name
|
||||
bonus applied — see :func:`_selection_order`, D2/D3, LOCKED A3).
|
||||
|
||||
Deflection mode (PLAN §6, A8) is built from these *titles only* — the
|
||||
LOW prompt and the "Maybe try" chips never see document content.
|
||||
|
||||
*source_bonus* defaults to the ``BOR_SOURCE_NAME_HIT_BONUS`` setting
|
||||
(0.005); ``0`` reproduces the pre-phase-191 walk byte-identically.
|
||||
"""
|
||||
if bonus is None:
|
||||
bonus = get_settings().name_hit_bonus
|
||||
return [doc.title for doc, _eff, _cos, _idx in _selection_order(chunks, bonus)]
|
||||
if source_bonus is None:
|
||||
source_bonus = get_settings().source_name_hit_bonus
|
||||
return [
|
||||
doc.title
|
||||
for doc, _eff, _cos, _idx in _selection_order(chunks, bonus, source_bonus)
|
||||
]
|
||||
|
||||
|
||||
def select_documents_tiered(
|
||||
@@ -953,6 +994,7 @@ def select_suggested(
|
||||
chunks: Sequence[RetrievedChunk],
|
||||
n: int | None = None,
|
||||
bonus: float | None = None,
|
||||
source_bonus: float | None = None,
|
||||
) -> list[Document]:
|
||||
"""Top-N distinct parent documents in SELECTION rank order — the
|
||||
phase-118 "start here" suggestion tier (LOCKED A3, re-revised by
|
||||
@@ -975,7 +1017,10 @@ def select_suggested(
|
||||
|
||||
*bonus* defaults to the ``BOR_NAME_HIT_BONUS`` setting (0.005 — the
|
||||
owner-tunable starting point); ``0`` reproduces the pre-phase walk
|
||||
byte-identically (the kill switch, LOCKED A3).
|
||||
byte-identically (the kill switch, LOCKED A3). *source_bonus*
|
||||
defaults to the ``BOR_SOURCE_NAME_HIT_BONUS`` setting (phase 191,
|
||||
D3 — the same bounded shape, the same kill switch, an independent
|
||||
knob).
|
||||
|
||||
The returned rows carry the full document content, byte-identical —
|
||||
the content is what the agent's ``read`` tool serves later (never
|
||||
@@ -985,7 +1030,14 @@ def select_suggested(
|
||||
top_n = n if n is not None else get_settings().suggested_docs
|
||||
if bonus is None:
|
||||
bonus = get_settings().name_hit_bonus
|
||||
return [doc for doc, _eff, _cos, _idx in _selection_order(chunks, bonus)[:top_n]]
|
||||
if source_bonus is None:
|
||||
source_bonus = get_settings().source_name_hit_bonus
|
||||
return [
|
||||
doc
|
||||
for doc, _eff, _cos, _idx in _selection_order(chunks, bonus, source_bonus)[
|
||||
:top_n
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
def select_related(
|
||||
@@ -993,6 +1045,7 @@ def select_related(
|
||||
excluded_ids: set[uuid.UUID],
|
||||
cap: int,
|
||||
bonus: float | None = None,
|
||||
source_bonus: float | None = None,
|
||||
) -> 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*
|
||||
@@ -1012,7 +1065,10 @@ def select_related(
|
||||
|
||||
*bonus* defaults to the ``BOR_NAME_HIT_BONUS`` setting (0.005 — the
|
||||
owner-tunable starting point); ``0`` reproduces the pre-phase walk
|
||||
byte-identically (the kill switch, LOCKED A3).
|
||||
byte-identically (the kill switch, LOCKED A3). *source_bonus*
|
||||
defaults to the ``BOR_SOURCE_NAME_HIT_BONUS`` setting (phase 191,
|
||||
D3 — the same bounded shape, the same kill switch, an independent
|
||||
knob).
|
||||
|
||||
The returned rows carry the full document content, byte-identical
|
||||
(the tier is metadata for the ``done`` frame and the durable
|
||||
@@ -1020,8 +1076,10 @@ def select_related(
|
||||
"""
|
||||
if bonus is None:
|
||||
bonus = get_settings().name_hit_bonus
|
||||
if source_bonus is None:
|
||||
source_bonus = get_settings().source_name_hit_bonus
|
||||
out: list[Document] = []
|
||||
for doc, _eff, _cos, _idx in _selection_order(chunks, bonus):
|
||||
for doc, _eff, _cos, _idx in _selection_order(chunks, bonus, source_bonus):
|
||||
if len(out) >= cap:
|
||||
break
|
||||
if doc.id in excluded_ids:
|
||||
|
||||
Reference in New Issue
Block a user