feat(ledger): divergence readout — button B where button A is canon, shape history, and judged-shape recheck (#2793, milestone 294 step 7)
CI & Build / TypeScript typecheck (push) Failing after 2s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 28s
CI & Build / Python tests (push) Failing after 37s
CI & Build / Build & push image (push) Skipped

Every judgment now goes through one helper that remembers the fingerprint
judged (classified_sha) and writes a code_shape_events row; the sync writes
vanished / reappeared / drifted events and flags recheck_at when a body
moves under an instance/variant. The refresh flags diverges_from on shapes
new since the previous computation that sit where one canon dominates the
judged siblings of their directory+kind and were not proposed as that canon
(a first seed flags nothing); the write-path hint asks the same question
in-band for the shapes the hook names. list_shapes(flag=divergence|recheck),
shape_history(project_id, path, symbol) (read-only), coverage line/payload/
card carry divergent + recheck. Backup v8 carries the history. Plugin 0.1.36.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 23:13:39 -04:00
co-authored by Claude Fable 5
parent 386b27e422
commit d74a244b3a
17 changed files with 880 additions and 62 deletions
+51
View File
@@ -291,3 +291,54 @@ def test_proposer_tools_are_mounted():
assert mcp._tool_manager.get_tool("confirm_shape_proposals") is not None
tool = mcp._tool_manager.get_tool("list_shapes")
assert "proposal" in tool.parameters.get("properties", {})
# --- step 7: the divergence readout (pure) ----------------------------------
def _row(path, kind="sym", status="unclassified", snippet_id=None):
r = CodeShape(project_id=1, repo_key="r", path=path, symbol=path.rsplit("/", 1)[-1], kind=kind)
r.status, r.snippet_id = status, snippet_id
return r
def test_dominant_canon_needs_enough_judged_siblings_and_a_clear_majority():
from scribe.services.shape_ledger import dominant_canon
dense = [_row(f"c/{i}", status="instance", snippet_id=7) for i in range(4)] + [
_row("c/x", status="instance", snippet_id=8), _row("c/y")]
assert dominant_canon(dense) == (7, 4, 5)
sparse = [_row("c/a", status="instance", snippet_id=7), _row("c/b", status="instance", snippet_id=7)]
assert dominant_canon(sparse) is None # 2 judged < floor
split = [_row(f"c/{i}", status="instance", snippet_id=7) for i in range(2)] + [
_row(f"c/{i+5}", status="instance", snippet_id=8) for i in range(2)]
assert dominant_canon(split) is None # 50% < 60% share
# Variants are departures, not votes; canonical counts like an instance.
mixed = [_row("c/a", status="canonical", snippet_id=7)] + [
_row(f"c/{i}", status="instance", snippet_id=7) for i in range(2)] + [
_row("c/v", status="variant", snippet_id=9)]
assert dominant_canon(mixed) == (7, 3, 3)
def test_history_and_readout_tools_are_mounted_and_shape_history_is_read_only():
from scribe.mcp.server import _READ_ONLY_TOOLS, build_mcp_server
mcp = build_mcp_server()
assert mcp._tool_manager.get_tool("shape_history") is not None
assert "shape_history" in _READ_ONLY_TOOLS
assert "flag" in mcp._tool_manager.get_tool("list_shapes").parameters.get("properties", {})
def test_history_and_divergence_columns_are_pinned():
from scribe.models.code_shape import SHAPE_EVENTS, CodeShapeEvent
cols = CodeShape.__table__.c
for name in ("classified_sha", "recheck_at", "diverges_from"):
assert name in cols, name
assert "ix_code_shapes_diverges" in {ix.name for ix in CodeShape.__table__.indexes}
ev = CodeShapeEvent.__table__
fk = next(iter(ev.c.shape_id.foreign_keys))
assert fk.ondelete == "CASCADE" and fk.column.table.name == "code_shapes"
assert not ev.c.snippet_id.foreign_keys # history outlives the snippet
assert SHAPE_EVENTS == ("classified", "vanished", "reappeared", "drifted")
assert "code_shape_events" in Base.metadata.tables