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
+32 -1
View File
@@ -923,7 +923,19 @@ async def build_write_path_hint(
)
except Exception:
logger.warning("Write-path ledger stamping failed", exc_info=True)
if not synced and not menu and not stamped:
# The in-band button-B check (#2793): the hook named the shapes being
# written; if this directory+kind is canon-dense and a named shape isn't
# (about to be) an instance of that canon, say so NOW — at the write,
# not at the next audit.
divergence: list[dict] = []
if stamp_shapes and project_id:
try:
divergence = await shape_ledger_svc.write_time_divergence(
project_id, path, stamp_shapes, stamped
)
except Exception:
logger.warning("write-time divergence check failed", exc_info=True)
if not synced and not menu and not stamped and not divergence:
return empty
owners = await owner_names_for({
@@ -989,6 +1001,8 @@ async def build_write_path_hint(
if stamped:
lines.append(_stamp_line(path, stamped))
if divergence:
lines.append(_divergence_line(path, divergence))
# Split by arm, which is the whole reason this table exists. The place arm
# carries no score and so has no home in retrieval_logs; before #2085 a
@@ -1012,9 +1026,26 @@ async def build_write_path_hint(
"sync_note_ids": sync_note_ids,
"config": cfg,
"stamped": stamped,
"divergence": divergence,
}
def _divergence_line(path: str, divergence: list[dict]) -> str:
"""Button B where button A is canon — named at the write (#2793)."""
parts = [
f"`{('.' if d['kind'] == 'css' else '') + d['symbol']}` → #{d['canon_snippet_id']} "
f"({d['instances']} of {d['judged']} judged siblings are its instances)"
for d in divergence
]
return (
f"> Divergence check at `{path}`: a canon dominates this directory — "
f"{'; '.join(parts)}. If this is a new instance, pull that snippet "
"and build from it; if it is a deliberate departure, "
"`classify_shapes(..., status=\"variant\", reason=…)` records the why; "
"otherwise it reads as unintended divergence."
)
def _stamp_line(path: str, stamped: list[dict]) -> str:
"""One line saying what the ledger just recorded, so the session can
correct a wrong stamp in the moment rather than an audit finding it."""