fix(shapes): the capped meaning pass reads new shapes first, and its miss withdraws an early flag (#4208)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / integration (push) Failing after 51s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / Python tests (push) Successful in 1m33s
CI & Build / Build & push image (push) Successful in 21s

Measured on the first live refresh after 91cde6c deployed: the version bump
queued 1,533 rows for the semantic arm, the cap read 150 of them in row
order, and all six shapes new since the previous refresh - the only rows
flag_divergence acts on, and the highest ids - were flagged before the arm
reached them. The gate silenced nothing because it never got to look, and
since a flag persists until judged, a later conclusive miss could not take
it back.

- propose_for_repo sorts the semantic todo by _semantic_priority:
  unclassified before scoped, newest first.
- flag_divergence withdraws a standing flag when the row now carries a
  conclusive miss; that evidence alone withdraws one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
This commit is contained in:
2026-09-22 08:05:49 -04:00
co-authored by Claude Opus 5
parent cef91f4cd4
commit 19438cc894
3 changed files with 169 additions and 4 deletions
+103
View File
@@ -974,6 +974,109 @@ async def test_a_conclusive_meaning_miss_silences_what_the_signature_cannot(seed
assert total == 0, "a shape the proposer measured as unrelated must not be urged"
async def _two_generations(seeded, tag: str):
"""A canon-dense directory, an OLD unjudged helper, then one NEW shape.
The same density as the acceptance fixture above, with an extra unjudged
row from the first sync so there is an old row competing with the new one
for the capped semantic pass.
"""
from datetime import datetime, timezone
from scribe.services import snippets as snippets_svc
owner, pid = seeded["owner"], seeded["pid"]
canon = await snippets_svc.create_snippet(
owner, name=f"cls_confirm_factory_{tag}",
code="export async function factory(): Promise<boolean> {\n return true;\n}\n",
language="typescript", repo="Widget",
path="frontend/src/composables/useConfirm.ts", symbol="factory",
project_id=pid,
)
sid = int(canon.id)
comp = "frontend/src/components"
base = _defs(
*[(f"{comp}/{n}.vue", "sym", f"on{n}", f"async function on{n}() {{",
f"async function on{n}() {{\n const ok = await factory();\n if (!ok) return;\n}}")
for n in ("Trash", "Delete", "Remove", "Restore")],
(f"{comp}/Old.vue", "sym", "oldHelper", "function oldHelper() {",
"function oldHelper() {\n return document.title.length > 0;\n}"),
)
await sync_repo_shapes(pid, REPO, base, seen_marker=f"{tag}-1")
await classify_shapes(owner, pid, [
{"path": f"{comp}/{n}.vue", "symbol": f"on{n}", "status": "instance", "snippet_id": sid}
for n in ("Trash", "Delete", "Remove", "Restore")
], via="audit")
previous = datetime.now(timezone.utc)
later = base + _defs(
(f"{comp}/Danger.vue", "sym", "confirmDanger", "function confirmDanger() {",
"function confirmDanger() {\n return window.confirm('Really?');\n}"),
)
await sync_repo_shapes(pid, REPO, later, seen_marker=f"{tag}-2")
return owner, pid, later, previous
def _conclusive(*_a, report=None, **_k):
if report is not None:
report["conclusive"] = True
return None
@pytest.mark.integration
async def test_the_capped_semantic_pass_reaches_the_new_shape_first(seeded):
"""Measured live: after the #4208 version bump, 1,533 rows competed for 150
semantic checks in row order and the six new shapes — the only ones the
divergence check acts on — were all left unread and flagged. With room for
ONE check, the new shape must be the one that gets it."""
from unittest.mock import AsyncMock, patch
from scribe.services import shape_ledger
from scribe.services.shape_ledger import (
BASIS_NO_SEMANTIC_MATCH, live_rows, propose_for_repo,
)
owner, pid, later, _previous = await _two_generations(seeded, "cap")
with patch.object(shape_ledger, "_semantic_canon",
AsyncMock(side_effect=_conclusive)):
stats = await propose_for_repo(owner, pid, REPO, later, semantic_cap=1)
assert stats["semantic_checked"] == 1
rows = {r.symbol: r for r in await live_rows(pid)}
assert rows["confirmDanger"].proposal_basis == BASIS_NO_SEMANTIC_MATCH
# The old row is left for the next refresh, unexamined — never stamped.
assert rows["oldHelper"].proposal_basis is None
assert rows["oldHelper"].proposed_sha == ""
@pytest.mark.integration
async def test_a_later_conclusive_miss_withdraws_a_flag_raised_before_it(seeded):
"""A flag raised while the arm had not yet read the row was raised on
"cannot tell". When a later refresh reads the body and finds it is none of
the canons, the flag goes — and only that evidence withdraws one."""
from datetime import timedelta
from unittest.mock import AsyncMock, patch
from scribe.services import shape_ledger
from scribe.services.shape_ledger import flag_divergence, propose_for_repo
owner, pid, later, previous = await _two_generations(seeded, "withdraw")
since = previous - timedelta(seconds=1)
# First refresh: the arm is quiet (formed no opinion), so the prompt is raised.
with patch.object(shape_ledger, "_semantic_canon", AsyncMock(return_value=None)):
await propose_for_repo(owner, pid, REPO, later)
assert await flag_divergence(pid, since=since) == 1
# A later refresh re-reads the body (a ruleset bump forces it) and is sure.
with patch.object(shape_ledger, "_PROPOSER_VERSION", 10_000), \
patch.object(shape_ledger, "_semantic_canon",
AsyncMock(side_effect=_conclusive)):
await propose_for_repo(owner, pid, REPO, later)
assert await flag_divergence(pid, since=since) == 0
_, total = await list_project_shapes(owner, pid, flag="divergence")
assert total == 0
@pytest.mark.integration
async def test_history_records_what_was_used_when_and_drift_asks_for_a_recheck(seeded):
from scribe.services.shape_ledger import shape_history