CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 56s
CI & Build / Python tests (push) Successful in 1m34s
CI & Build / Build & push image (push) Successful in 22s
0.8 was sized for proposals nobody reads; every semantic proposal is read by a judge before it is confirmed. Measured, it proposed 0 of 150 while judged instances of a canon score 0.68-0.71 - the write-path hint's own floor asks the same question of the same documents at 0.68. The arm now uses that floor, scans 8 hits instead of 3 (a true instance ranked 4th behind snippets of other language families), and the proposer version bumps to 5 so rows examined under the old floor are read again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
158 lines
6.5 KiB
Python
158 lines
6.5 KiB
Python
"""A semantic MISS is not evidence, and the divergence check never reads one (#4208).
|
||
|
||
WHAT THIS IS ABOUT. #4204 gave the divergence check a structural gate, which
|
||
silenced one of the five false prompts it was filed for. The other four are
|
||
`def` helpers beside an `async def` service canon — callables beside a
|
||
callable — and differ from #2793's acceptance case (a hand-rolled sync
|
||
`confirmDanger` where an async confirm helper is canon) only by the JOB they
|
||
do. A signature does not carry a job.
|
||
|
||
#4208 tried meaning: store the proposer's semantic "compared, nothing cleared
|
||
the floor" and let it withdraw the prompt. Measured live on 2026-09-22 it
|
||
cannot discriminate. Judged instances of the service-unit canon score 0.68–
|
||
0.71 against it at best, most below 0.66; helpers score 0.66–0.75 against
|
||
snippets unrelated to them. The then-floor of 0.8 was reached by nothing, so
|
||
the "miss" fired for nearly every body — the real divergence silenced exactly
|
||
as the helper was. The gate was removed; the prompt asks and the judge
|
||
answers. The same measurement moved the arm's floor down to the write-path
|
||
hint's: 0.8 was sized for proposals nobody reads, and it proposed 0 of 150.
|
||
|
||
These tests pin what stayed: the arm proposes on a hit and says nothing on a
|
||
miss, the divergence check does not consult it, and the capped pass reads new
|
||
shapes first so the proposals it CAN make land where the check looks.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import ast
|
||
import inspect
|
||
import textwrap
|
||
from unittest.mock import AsyncMock, patch
|
||
|
||
from scribe.services import shape_ledger
|
||
from scribe.services.shape_ledger import _semantic_canon
|
||
|
||
# Comfortably over WRITEPATH_MIN_CODE_CHARS (48 non-whitespace characters), so
|
||
# these tests exercise the comparison rather than the substance guard. One of
|
||
# #4204's four survivors, quoted rather than invented.
|
||
BODY = (
|
||
"def is_registered(source: str) -> bool:\n"
|
||
" return source in _REGISTRY and _REGISTRY[source].enabled\n"
|
||
)
|
||
TOO_THIN = "def f():\n pass\n"
|
||
|
||
CANON = 2860 # the allowed canon, as a caller would pass it
|
||
OTHER = 9999 # a snippet that is not in the allowed set
|
||
|
||
|
||
class _FakeNote:
|
||
"""Only `.id` is read off a hit."""
|
||
|
||
def __init__(self, note_id: int) -> None:
|
||
self.id = note_id
|
||
|
||
|
||
def _hits(*hits: tuple[float, int]) -> AsyncMock:
|
||
return AsyncMock(return_value=[(score, _FakeNote(nid)) for score, nid in hits])
|
||
|
||
|
||
def _patch(mock: AsyncMock):
|
||
return patch("scribe.services.embeddings.semantic_search_notes", mock)
|
||
|
||
|
||
# ── the arm proposes on a hit ────────────────────────────────────────────
|
||
|
||
|
||
async def test_a_hit_returns_the_canon() -> None:
|
||
with _patch(_hits((0.88, CANON))):
|
||
assert await _semantic_canon(1, BODY, {CANON}) == (CANON, 0.88)
|
||
|
||
|
||
async def test_an_allowed_canon_below_the_top_hit_still_wins() -> None:
|
||
"""The scan is over the whole result set, so a disallowed snippet ranking
|
||
first does not hide an allowed one behind it."""
|
||
with _patch(_hits((0.95, OTHER), (0.83, CANON))):
|
||
assert await _semantic_canon(1, BODY, {CANON}) == (CANON, 0.83)
|
||
|
||
|
||
async def test_a_miss_is_none_and_nothing_more() -> None:
|
||
with _patch(_hits((0.91, OTHER))):
|
||
assert await _semantic_canon(1, BODY, {CANON}) is None
|
||
|
||
|
||
async def test_a_body_too_thin_to_embed_spends_no_embedding() -> None:
|
||
mock = _hits()
|
||
with _patch(mock):
|
||
assert await _semantic_canon(1, TOO_THIN, {CANON}) is None
|
||
mock.assert_not_awaited()
|
||
|
||
|
||
async def test_no_allowed_canon_spends_no_search() -> None:
|
||
"""The language-family gate (#2871) empties the allowed set routinely."""
|
||
mock = _hits()
|
||
with _patch(mock):
|
||
assert await _semantic_canon(1, BODY, set()) is None
|
||
mock.assert_not_awaited()
|
||
|
||
|
||
async def test_the_arm_proposes_at_the_write_path_floor() -> None:
|
||
"""It asks the write-path hint's question of the same documents, so it
|
||
uses that surface's floor — not a stricter private one that yields no
|
||
proposals for the judge to weigh (#4208)."""
|
||
from scribe.services.plugin_context import WRITEPATH_DEFAULT_THRESHOLD
|
||
|
||
mock = _hits()
|
||
with _patch(mock):
|
||
await _semantic_canon(1, BODY, {CANON})
|
||
assert mock.await_args.kwargs["threshold"] == WRITEPATH_DEFAULT_THRESHOLD
|
||
|
||
|
||
# ── ...and its silence reaches nothing ───────────────────────────────────
|
||
|
||
|
||
def test_the_divergence_check_does_not_read_the_proposal_basis() -> None:
|
||
"""The retired gate keyed on `proposal_basis`. Asserted on the function's
|
||
structure (rule 167): any attribute read of it inside `flag_divergence` is
|
||
the miss being consulted again, whatever the constant is called."""
|
||
tree = ast.parse(textwrap.dedent(inspect.getsource(shape_ledger.flag_divergence)))
|
||
reads = [n for n in ast.walk(tree)
|
||
if isinstance(n, ast.Attribute) and n.attr == "proposal_basis"]
|
||
assert not reads, (
|
||
"flag_divergence reads proposal_basis — a semantic miss is 'cannot "
|
||
"tell' at the arm's floor (#4208), never 'not the canon'"
|
||
)
|
||
|
||
|
||
def test_no_miss_basis_is_stored() -> None:
|
||
"""A miss basis sharing the column with real bases is a thing
|
||
`confirm_shape_proposals(basis=…)` could filter on and confirm."""
|
||
assert not hasattr(shape_ledger, "BASIS_NO_SEMANTIC_MATCH")
|
||
|
||
|
||
# ── the cap spends itself where proposals matter ─────────────────────────
|
||
|
||
|
||
class _AgedRow:
|
||
def __init__(self, name: str, status: str, created) -> None:
|
||
self.name, self.status, self.created_at = name, status, created
|
||
|
||
|
||
def test_the_capped_pass_reads_new_shapes_first() -> None:
|
||
"""Measured on the first live refresh after a version bump: 1,533 rows
|
||
queued, the cap read 150 of them in row order, and every shape NEW since
|
||
the previous refresh — the only rows `flag_divergence` acts on, and the
|
||
highest ids — was flagged before the arm could propose for it."""
|
||
from datetime import datetime, timedelta, timezone
|
||
|
||
from scribe.services.shape_ledger import _semantic_priority
|
||
|
||
t = datetime(2026, 9, 22, tzinfo=timezone.utc)
|
||
rows = [
|
||
_AgedRow("scoped_new", "scoped", t),
|
||
_AgedRow("old", "unclassified", t - timedelta(days=30)),
|
||
_AgedRow("undated", "unclassified", None),
|
||
_AgedRow("new", "unclassified", t),
|
||
_AgedRow("scoped_old", "scoped", t - timedelta(days=30)),
|
||
]
|
||
order = [r.name for r in sorted(rows, key=_semantic_priority)]
|
||
assert order == ["new", "old", "undated", "scoped_new", "scoped_old"]
|