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:
@@ -1260,9 +1260,12 @@ def _bonus_rc(
|
||||
cosine: float,
|
||||
position: int = 0,
|
||||
name_hit: bool = False,
|
||||
source_name_hit: bool = False,
|
||||
) -> RetrievedChunk:
|
||||
"""One fused-list candidate (name-hit rows follow the D1 lexical
|
||||
convention: ``cosine=0.0``, ``fts_hit=True``)."""
|
||||
convention: ``cosine=0.0``, ``fts_hit=True``). The
|
||||
``source_name_hit`` flag (phase 191, D2) is set at the retrieval
|
||||
boundary in chat.py — here directly, on the fused row."""
|
||||
return RetrievedChunk(
|
||||
chunk_id=uuid.uuid4(),
|
||||
position=position,
|
||||
@@ -1272,6 +1275,7 @@ def _bonus_rc(
|
||||
cosine=cosine,
|
||||
fts_hit=cosine == 0.0,
|
||||
name_hit=name_hit,
|
||||
source_name_hit=source_name_hit,
|
||||
)
|
||||
|
||||
|
||||
@@ -1479,24 +1483,185 @@ def test_weak_hit_titles_bonus_adjusted_order() -> None:
|
||||
|
||||
|
||||
def test_bonus_lives_in_the_selection_layer_only() -> None:
|
||||
"""LOCKED A3: the bonus never touches the chunk objects —
|
||||
"""LOCKED A3 (mirrored by phase 191 for the SECOND bonus): the
|
||||
bonuses never touch the chunk objects —
|
||||
``score``/``cosine``/``fts_hit`` (the A8 gate's inputs —
|
||||
``query_log.top_score`` is the best fused chunk score, the same
|
||||
values) are unchanged after every selection walk, even with the
|
||||
bonus lifting a document."""
|
||||
values) and the ``name_hit``/``source_name_hit`` flags themselves
|
||||
are unchanged after every selection walk, even with the bonuses
|
||||
lifting a document."""
|
||||
chunks = _bonus_chunks()
|
||||
chunks[-1].source_name_hit = True
|
||||
before = {
|
||||
rc.chunk_id: (rc.score, rc.cosine, rc.fts_hit, rc.name_hit) for rc in chunks
|
||||
rc.chunk_id: (
|
||||
rc.score,
|
||||
rc.cosine,
|
||||
rc.fts_hit,
|
||||
rc.name_hit,
|
||||
rc.source_name_hit,
|
||||
)
|
||||
for rc in chunks
|
||||
}
|
||||
select_suggested(chunks, n=5, bonus=0.005)
|
||||
select_related(chunks, set(), cap=5, bonus=0.005)
|
||||
weak_hit_titles(chunks, bonus=0.005)
|
||||
select_suggested(chunks, n=5, bonus=0.005, source_bonus=0.005)
|
||||
select_related(chunks, set(), cap=5, bonus=0.005, source_bonus=0.005)
|
||||
weak_hit_titles(chunks, bonus=0.005, source_bonus=0.005)
|
||||
after = {
|
||||
rc.chunk_id: (rc.score, rc.cosine, rc.fts_hit, rc.name_hit) for rc in chunks
|
||||
rc.chunk_id: (
|
||||
rc.score,
|
||||
rc.cosine,
|
||||
rc.fts_hit,
|
||||
rc.name_hit,
|
||||
rc.source_name_hit,
|
||||
)
|
||||
for rc in chunks
|
||||
}
|
||||
assert before == after
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 191, D3 — the bounded SOURCE-name bonus (the second, independent
|
||||
# knob; the phase-119 mirror: kill switch, bounded per-document apply,
|
||||
# the fuse OR-merge, the settings defaults)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_selection_order_source_bonus_zero_is_byte_identical_to_pre_phase() -> None:
|
||||
"""The kill switch (LOCKED A3, phase-119 mirror): with
|
||||
*source_bonus* ``0`` the walk is byte-identical to the
|
||||
pre-phase-191 walk even when EVERY chunk carries the flag — same
|
||||
order, same effective scores (the phase-119 name bonus alone). With
|
||||
BOTH knobs ``0`` the pre-phase golden walk returns byte-identical."""
|
||||
chunks = _bonus_chunks()
|
||||
for rc in chunks:
|
||||
rc.source_name_hit = True
|
||||
out = _selection_order(chunks, 0.005, 0.0)
|
||||
assert [d.path for d, _eff, _cos, _idx in out] == [
|
||||
"gitea/README.md", "a.md", "b.md", "c.md", "d.md"
|
||||
]
|
||||
# No +0.005 rides the effective scores — the name bonus alone.
|
||||
assert out[0][1] == pytest.approx(0.016 + 0.005)
|
||||
off = _selection_order(chunks, 0.0, 0.0)
|
||||
assert [d.path for d, *_ in off] == GOLDEN_PRE_PHASE_ORDER
|
||||
assert [eff for _d, eff, _cos, _idx in off] == [
|
||||
0.0200, 0.0160, 0.0150, 0.0150, 0.0100
|
||||
]
|
||||
|
||||
|
||||
def test_selection_order_source_bonus_inert_without_source_hits() -> None:
|
||||
"""No ``source_name_hit`` chunk present → the second bonus cannot
|
||||
fire: with both knobs on the walk equals the phase-119 name-bonus
|
||||
walk exactly (the flagged-signal presence, not just the knob, gates
|
||||
the re-sort)."""
|
||||
out = _selection_order(_bonus_chunks(), 0.005, 0.005)
|
||||
assert [d.path for d, _eff, _cos, _idx in out] == [
|
||||
"gitea/README.md", "a.md", "b.md", "c.md", "d.md"
|
||||
]
|
||||
assert out[0][1] == pytest.approx(0.016 + 0.005)
|
||||
|
||||
|
||||
def test_selection_order_source_bonus_lifts_named_doc_over_equal_fused() -> None:
|
||||
"""The boost (D3): a document whose source is a D2 hit
|
||||
(``source_name_hit=True``) ranks ABOVE an equal-fused-score
|
||||
non-hit — the fused tie normally resolves on cosine (a's 0.9 beats
|
||||
the hit's 0.0), the 0.005 head start decides instead."""
|
||||
gitserver = _doc("gitserver.md", "G" * 50)
|
||||
a = _doc("a.md", "A" * 50)
|
||||
chunks = [
|
||||
_bonus_rc(a, 0.0200, 0.9),
|
||||
_bonus_rc(gitserver, 0.0200, 0.0, source_name_hit=True),
|
||||
]
|
||||
assert [d.path for d, *_ in _selection_order(chunks, 0.0, 0.0)] == [
|
||||
"a.md", "gitserver.md"
|
||||
]
|
||||
out = _selection_order(chunks, 0.0, 0.005)
|
||||
assert [d.path for d, *_ in out] == ["gitserver.md", "a.md"]
|
||||
assert out[0][1] == pytest.approx(0.0200 + 0.005)
|
||||
|
||||
|
||||
def test_selection_order_two_bonuses_bounded_at_two_per_document() -> None:
|
||||
"""A document carrying BOTH flags gets EXACTLY +0.010 (the bounded
|
||||
two-bonus pin — no amplification): every one of the doc's THREE
|
||||
chunks is flagged for both signals, yet each bonus applies ONCE per
|
||||
document — best score + bonus + source_bonus, never more."""
|
||||
gitserver = _doc("gitserver.md", "G" * 50)
|
||||
a = _doc("a.md", "A" * 50)
|
||||
chunks = [
|
||||
_bonus_rc(a, 0.0300, 0.9),
|
||||
_bonus_rc(gitserver, 0.0260, 0.0, 0, name_hit=True, source_name_hit=True),
|
||||
_bonus_rc(gitserver, 0.0200, 0.0, 1, name_hit=True, source_name_hit=True),
|
||||
_bonus_rc(gitserver, 0.0100, 0.0, 2, name_hit=True, source_name_hit=True),
|
||||
]
|
||||
out = _selection_order(chunks, 0.005, 0.005)
|
||||
assert [d.path for d, *_ in out] == ["gitserver.md", "a.md"]
|
||||
assert out[0][1] == pytest.approx(0.0260 + 0.010)
|
||||
|
||||
|
||||
def test_selection_helpers_pull_the_source_bonus_from_settings(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""All three selection helpers default *source_bonus* to the LIVE
|
||||
``BOR_SOURCE_NAME_HIT_BONUS`` setting (the phase-119 ``bonus``
|
||||
pattern): the settings default 0.005 lifts the flagged doc, a
|
||||
settings kill switch (``0``) and an explicit ``source_bonus=0``
|
||||
both reproduce the pre-phase-191 walk."""
|
||||
chunks = _bonus_chunks()
|
||||
chunks[-1].score = 0.0199 # d.md just UNDER a.md's 0.0200
|
||||
chunks[-1].source_name_hit = True
|
||||
on = Settings(_env_file=None) # pyright: ignore[reportCallIssue]
|
||||
assert on.source_name_hit_bonus == 0.005 # the production default
|
||||
monkeypatch.setattr(retriever, "get_settings", lambda: on)
|
||||
lifted = ["d.md", "gitea/README.md", "a.md", "b.md", "c.md"]
|
||||
assert [d.path for d in select_suggested(chunks, n=5)] == lifted
|
||||
assert [d.path for d in select_related(chunks, set(), cap=5)] == lifted
|
||||
assert weak_hit_titles(chunks) == lifted
|
||||
# Explicit kill switch — the pre-phase-191 (name-bonus-only) walk.
|
||||
assert [d.path for d in select_suggested(chunks, n=5, source_bonus=0.0)] == [
|
||||
"gitea/README.md", "a.md", "d.md", "b.md", "c.md"
|
||||
]
|
||||
# Settings kill switch (BOR_SOURCE_NAME_HIT_BONUS=0) — the same walk.
|
||||
off = Settings(_env_file=None, source_name_hit_bonus=0.0) # pyright: ignore[reportCallIssue]
|
||||
monkeypatch.setattr(retriever, "get_settings", lambda: off)
|
||||
pre = ["gitea/README.md", "a.md", "d.md", "b.md", "c.md"]
|
||||
assert [d.path for d in select_suggested(chunks, n=5)] == pre
|
||||
assert [d.path for d in select_related(chunks, set(), cap=5)] == pre
|
||||
assert weak_hit_titles(chunks) == pre
|
||||
|
||||
|
||||
def test_fuse_or_s_source_name_hit_on_double_hit() -> None:
|
||||
"""A vector row merged with a flagged source-name row keeps
|
||||
``source_name_hit=True`` — the merge ORs the flag in (the
|
||||
``name_hit`` mirror), so the selection tier still sees the hit on
|
||||
the fused list."""
|
||||
v = _rc("gitserver.md", cosine=0.9)
|
||||
l1 = _rc("gitserver.md", cosine=0.1) # the lexical copy of the same chunk
|
||||
l1.chunk_id = v.chunk_id
|
||||
l1.source_name_hit = True
|
||||
out = fuse([v], [l1], k=60)
|
||||
assert len(out) == 1
|
||||
assert out[0].source_name_hit is True
|
||||
assert out[0].fts_hit is True
|
||||
assert out[0].score == pytest.approx(2 / 61)
|
||||
|
||||
|
||||
def test_fuse_keeps_source_name_hit_on_lexical_only_hit() -> None:
|
||||
"""A source-name-hit row with no vector rank keeps
|
||||
``source_name_hit=True`` through the fusion (the ``replace()``
|
||||
copy carries the field — the ``name_hit`` mirror)."""
|
||||
row = _rc("gitserver.md")
|
||||
row.source_name_hit = True
|
||||
out = fuse([], [row], k=60)
|
||||
assert len(out) == 1
|
||||
assert out[0].source_name_hit is True
|
||||
|
||||
|
||||
def test_fuse_default_source_name_hit_stays_false_for_ordinary_rows() -> None:
|
||||
"""Neither list flagged ⇒ fusion never invents a source-name hit —
|
||||
ordinary vector and FTS rows are ``source_name_hit=False``."""
|
||||
out = fuse([_rc("a.md", cosine=0.8)], [_rc("b.md", fts_hit=True)], k=60)
|
||||
assert len(out) == 2
|
||||
assert all(rc.source_name_hit is False for rc in out)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 162, D1 — the accent-folded name normalization (LOCKED A1): the
|
||||
# 2026-10-07 live gap, the ASCII filename vs the accented content layer
|
||||
|
||||
Reference in New Issue
Block a user