feat(shapes): the agent judges what it wrote, at the end of the turn (milestone 439 steps 1-3)
CI & Build / Python lint (push) Successful in 5s
CI & Build / Plugin hooks (push) Successful in 18s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / integration (push) Successful in 52s
CI & Build / Python tests (push) Successful in 1m38s
CI & Build / Build & push image (push) Successful in 33s
CI & Build / Python lint (push) Successful in 5s
CI & Build / Plugin hooks (push) Successful in 18s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / integration (push) Successful in 52s
CI & Build / Python tests (push) Successful in 1m38s
CI & Build / Build & push image (push) Successful in 33s
Recording used to be decided by machinery — the only "record it" prompt fired when a same-named copy already existed (#2664), so a first instance of a reusable piece was never asked about, and judgment arrived only through audits. Now the question is asked where the knowledge is: the end of the turn that wrote the code, of the agent that wrote it. - Write hooks keep `<sid>.written.ids` (path, kind, name) for every definition a write names; a new file adds a `file` line for its stem — a candidate in any language without a framework rule (scribe_written_append). - Stop hook scribe_shape_check.sh sends the ledger to GET /api/plugin/shape-check and blocks once, in the server's words, when anything is unjudged. Same discipline as the report check: never twice, never without a recorded check, another hook's loop left alone; the ledger is kept when the instance cannot be reached. - shape_ledger.unjudged_shapes: no row, unclassified, scoped and hook stamps are unjudged; an agent/audit/import verdict is not. A snippet recorded at the shape answers for it until the refresh stamps it canonical. - services/shape_check owns the reason text and records every outcome in app_logs (passed / blocked / judged_after_block / left_after_block). - classify_shapes(repo=…) judges a shape the ledger has not synced yet via a provisional row under a bound repo; the sync confirms it, or vanishes and revives it with the verdict intact. An unbound repo is refused. Plugin version minted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1138,3 +1138,81 @@ async def test_history_records_what_was_used_when_and_drift_asks_for_a_recheck(s
|
||||
# reads nothing.
|
||||
assert len((await shape_history(owner, pid, "src"))["events"]) == 6
|
||||
assert await shape_history(other, pid, "src/app.py") == {}
|
||||
|
||||
|
||||
# --- milestone 439: the writer judges what it wrote, before the sync has seen it
|
||||
|
||||
@pytest.mark.integration
|
||||
async def test_a_shape_written_this_turn_is_judged_before_the_sync_sees_it(seeded):
|
||||
"""The end-of-turn verdict lands on a shape no sync has read yet: with a
|
||||
bound `repo` it becomes a provisional row and is judged, the sync that
|
||||
then reads the shape keeps the judgment, and a sync that does NOT have it
|
||||
yet (the code has not reached the bound ref) keeps it through the vanish
|
||||
and the reappearance. Without `repo` the old contract holds."""
|
||||
from scribe.services import repo_bindings as repo_bindings_svc
|
||||
|
||||
owner, pid = seeded["owner"], seeded["pid"]
|
||||
await repo_bindings_svc.set_binding(owner, REPO, pid)
|
||||
verdict = [{"path": "web/src/lib/StatusChip.svelte", "symbol": "tone",
|
||||
"kind": "sym", "status": "exempt",
|
||||
"reason": "the chip's own colour switch", "reason_code": "pure-helper"}]
|
||||
|
||||
out = await classify_shapes(owner, pid, verdict)
|
||||
assert out["unmatched"] == [{"path": "web/src/lib/StatusChip.svelte", "symbol": "tone"}]
|
||||
|
||||
with pytest.raises(ValueError, match="not bound"):
|
||||
await classify_shapes(owner, pid, verdict, repo="git.example.com/alice/elsewhere")
|
||||
|
||||
out = await classify_shapes(owner, pid, verdict, repo=REPO)
|
||||
assert out == {"classified": 1, "unmatched": [], "provisional": 1}
|
||||
|
||||
# A sync that has not got the code yet: the row vanishes, judgment intact…
|
||||
await sync_repo_shapes(pid, REPO, SHAPES, seen_marker="c1")
|
||||
# …and the sync that has it brings the row back with the verdict.
|
||||
await sync_repo_shapes(
|
||||
pid, REPO, SHAPES + [("web/src/lib/StatusChip.svelte", "sym", "tone")], seen_marker="c2",
|
||||
)
|
||||
rows, _ = await list_project_shapes(owner, pid, path="web/src/lib/StatusChip.svelte")
|
||||
assert len(rows) == 1
|
||||
row = rows[0]
|
||||
assert (row.status, row.classified_by, row.reason_code) == ("exempt", "agent", "pure-helper")
|
||||
assert row.vanished_at is None and row.last_seen_commit == "c2"
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
async def test_unjudged_is_what_nobody_read(seeded):
|
||||
"""The end-of-turn question: a shape with no row, an unclassified one, a
|
||||
sync-scoped one and a hook stamp are all asked about; an agent's or an
|
||||
audit's verdict is not. Evidence travels; a verdict is never inferred."""
|
||||
from sqlalchemy import update
|
||||
|
||||
from scribe.services.shape_ledger import _RESEMBLE_REASON, unjudged_shapes
|
||||
|
||||
owner, pid, sid = seeded["owner"], seeded["pid"], seeded["snippet"]
|
||||
await classify_shapes(owner, pid, [
|
||||
{"path": "src/app.py", "symbol": "make_app", "status": "instance", "snippet_id": sid},
|
||||
{"path": "src/util.py", "symbol": "helper", "status": "exempt", "reason": "x"},
|
||||
])
|
||||
async with async_session() as session:
|
||||
await session.execute(
|
||||
update(CodeShape)
|
||||
.where(CodeShape.project_id == pid, CodeShape.symbol == "helper")
|
||||
.values(classified_by="hook", status="instance", snippet_id=sid,
|
||||
reason=_RESEMBLE_REASON.format(sid=sid, score=0.7))
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
written = [
|
||||
("src/app.py", "sym", "make_app"), # judged by an agent → not asked
|
||||
("src/app.py", "sym", "Config"), # unclassified → asked
|
||||
("src/util.py", "sym", "helper"), # a hook stamp → asked, with its evidence
|
||||
("src/new.py", "sym", "fresh"), # no row → asked as new
|
||||
("src/new.py", "sym", "fresh"), # duplicates collapse
|
||||
]
|
||||
got = await unjudged_shapes(pid, written, repo_key=REPO)
|
||||
assert [(g["symbol"], g["status"]) for g in got] == [
|
||||
("Config", "unclassified"), ("helper", "instance"), ("fresh", "new"),
|
||||
]
|
||||
assert got[1]["evidence"]["stamped"] == {"snippet_id": sid, "score": 0.7}
|
||||
assert got[2]["evidence"] == {}
|
||||
assert await unjudged_shapes(pid, []) == []
|
||||
|
||||
Reference in New Issue
Block a user