diff --git a/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json index f969db7..0715aba 100644 --- a/plugin/.claude-plugin/plugin.json +++ b/plugin/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "scribe", "description": "Scribe for Claude Code: connects the scribe MCP server, adds the hooks that deliver live project state and relevant records at the right moment, ships the shared client-neutral Scribe skills (using-scribe, writing-plans, reporting-back, systematic-debugging, verification, brainstorming, reusing-code, shape-accounting), and syncs your saved Scribe Processes as skills (/scribe:sync).", - "version": "2026.10.01.1239", + "version": "2026.10.01.1242", "author": { "name": "Bryan Van Deusen" }, diff --git a/plugin/skills/reusing-code/SKILL.md b/plugin/skills/reusing-code/SKILL.md index 86b8388..5d4fe98 100644 --- a/plugin/skills/reusing-code/SKILL.md +++ b/plugin/skills/reusing-code/SKILL.md @@ -34,9 +34,9 @@ through recall/auto-inject; this skill is the active reflex around that. to ask both at once. - If a snippet fits, pull it in full with `get_snippet(id)` and reuse it — its `location` points at the reference implementation. Adapt, don't re-derive. - The pull also does the accounting: the code you then write that references - or resembles it is stamped an `instance` of that canon in the shape ledger - (classified_by=hook) — reuse from memory leaves no row. + The pull also feeds the accounting: the code you then write that references + or resembles it is offered back to you as "looks like #N" when you judge the + turn's shapes — reuse from memory leaves no such evidence. - If auto-inject already surfaced a snippet title that looks relevant, that's your cue to `get_snippet` it rather than start from scratch. - **Prior art offered beside a write is not noise — read it.** When Scribe notes diff --git a/src/scribe/routes/plugin.py b/src/scribe/routes/plugin.py index da71341..90f42b1 100644 --- a/src/scribe/routes/plugin.py +++ b/src/scribe/routes/plugin.py @@ -270,10 +270,12 @@ async def write_path_prior_art(): css|sym. The shape ledger's write-path feed (#2791): when the session recently PULLED a snippet this payload references or resembles, - these land as instance rows (classified_by=hook). - Honoured only for a caller allowed to write — a - read-scoped key still gets the hint, and never - changes accounting on a GET. + these carry it as a proposal — evidence for the + agent's own end-of-turn judgment, never a + verdict (milestone 439). Honoured only for a + caller allowed to write — a read-scoped key still + gets the hint, and never changes the ledger on a + GET. """ path = (request.args.get("path") or "").strip() code = request.args.get("code") or "" diff --git a/src/scribe/services/plugin_context.py b/src/scribe/services/plugin_context.py index dccf733..3ae2c43 100644 --- a/src/scribe/services/plugin_context.py +++ b/src/scribe/services/plugin_context.py @@ -2083,16 +2083,18 @@ async def build_write_path_hint( ``stamp_shapes`` turns the same request into the ledger's write-path feed (#2791): the (kind, name) definitions the hook saw in — or enclosing — the payload. When the session has PULLED a snippet recently and this - payload references or resembles it, those shapes land as `instance` rows - (classified_by=hook, see shape_ledger.stamp_write_path_instances) and - the result's ``stamped`` lists them. The route passes it only for a - caller allowed to write — a read-scoped key gets the hint, never the - stamp. ``repo_key`` (the hook's remote, normalised) homes a provisional - row for a shape the ledger has not synced yet. + payload references or resembles it, those shapes carry it as a PROPOSAL + (see shape_ledger.suggest_write_path_instances) and the result's + ``suggested`` lists them. Never a verdict since milestone 439: the agent + that wrote the code judges it at the end of the turn, shown this as + evidence. The route passes it only for a caller allowed to write — a + read-scoped key gets the hint, never a ledger write. ``repo_key`` (the + hook's remote, normalised) homes a provisional row for a shape the + ledger has not synced yet. """ cfg = await get_writepath_config(user_id) empty = {"context": "", "note_ids": [], "sync_note_ids": [], "config": cfg, - "stamped": [], "divergence": [], "derive": [], "derive_keys": [], + "suggested": [], "divergence": [], "derive": [], "derive_keys": [], "rule_ids": []} path = (path or "").strip() if not cfg["enabled"] or not path: @@ -2356,18 +2358,18 @@ async def build_write_path_hint( menu = (placed + scored)[:max(0, top_k - len(synced))] - # The stamp runs whether or not anything is rendered — after dedup, the - # common case is a silent hint and a pulled canon being instantiated. - stamped: list[dict] = [] + # The suggestion runs whether or not anything is rendered — after dedup, + # the common case is a silent hint and a pulled canon being instantiated. + suggested: list[dict] = [] if stamp_shapes and pulled: try: - stamped = await shape_ledger_svc.stamp_write_path_instances( + suggested = await shape_ledger_svc.suggest_write_path_instances( user_id, project_id, path=path, shapes=stamp_shapes, code=code or "", pulled=pulled, resembles=resembles, repo_key=repo_key, ) except Exception: - logger.warning("Write-path ledger stamping failed", exc_info=True) + logger.warning("Write-path ledger suggestion failed", exc_info=True) # 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, @@ -2376,7 +2378,7 @@ async def build_write_path_hint( if stamp_shapes and project_id: try: divergence = await shape_ledger_svc.write_time_divergence( - project_id, path, stamp_shapes, stamped, code or "", + project_id, path, stamp_shapes, suggested, code or "", ) except Exception: logger.warning("write-time divergence check failed", exc_info=True) @@ -2430,7 +2432,7 @@ async def build_write_path_hint( design_text, design_dedup = await _design_arm( user_id, project_id, path, set(exclude_derive or []), ) - if not staleness and not synced and not menu and not stamped and not divergence and not derive: + if not staleness and not synced and not menu and not suggested and not divergence and not derive: if design_text: return {**empty, "context": design_text, "derive_keys": [design_dedup]} return empty @@ -2531,8 +2533,8 @@ async def build_write_path_hint( if passage: lines.append(f"> ↳ {passage}") - if stamped: - lines.append(_stamp_line(path, stamped)) + if suggested: + lines.append(_suggest_line(path, suggested)) if divergence: lines.append(_divergence_line(path, divergence)) if derive: @@ -2694,7 +2696,7 @@ async def build_write_path_hint( "note_ids": note_ids, "sync_note_ids": sync_note_ids, "config": cfg, - "stamped": stamped, + "suggested": suggested, "divergence": divergence, "derive": derive, "derive_keys": [d["key"] for d in derive] + ( @@ -3017,22 +3019,23 @@ def _divergence_line(path: str, divergence: list[dict]) -> str: ) -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.""" +def _suggest_line(path: str, suggested: list[dict]) -> str: + """One line naming the recorded shape this code looks like, as evidence + for the judgment the agent gives at the end of the turn (milestone 439) + — not a record of one already made.""" by_snippet: dict[int, list[str]] = {} - for row in stamped: + for row in suggested: label = f".{row['symbol']}" if row["kind"] == "css" else row["symbol"] by_snippet.setdefault(int(row["snippet_id"]), []).append(f"`{label}`") parts = [ - f"{', '.join(names)} → instance of #{sid}" + f"{', '.join(names)} → looks like #{sid}" for sid, names in by_snippet.items() ] return ( - f"> Shape accounting: recorded at `{path}` — {'; '.join(parts)} " - "(classified_by=hook: you pulled that snippet this session and this " - "code references/resembles it). Not an instance? `classify_shapes` " - "overrides a hook stamp." + f"> Shape accounting: at `{path}` — {'; '.join(parts)} " + "(you pulled that snippet this session and this code names or resembles " + "it). When you judge this turn's shapes, say whether it is: `instance` " + "of it, `variant` with the why, or something else." ) diff --git a/src/scribe/services/shape_ledger.py b/src/scribe/services/shape_ledger.py index f0511ae..e1d69c5 100644 --- a/src/scribe/services/shape_ledger.py +++ b/src/scribe/services/shape_ledger.py @@ -908,11 +908,13 @@ async def snippet_consumers(user_id: int, note_id: int) -> dict: # semantic arm scored it above the write-path threshold for this # very payload. Either is evidence; the pull alone is not. # Both hold → every shape the hook named at that path, of the snippet's kind, -# becomes instance-of-N with classified_by="hook" and the evidence as reason. +# carries N as a PROPOSAL (milestone 439). Until then it became instance-of-N +# with classified_by="hook" — a verdict nobody read, and the rows written that +# way are what `stamps_to_review` still lists. # -# A hook row is EVIDENCE, not judgment: it only ever lands on rows nobody has -# judged (unclassified) or rows an earlier hook stamped, never on a canonical -# row or an agent/audit/import judgment. Re-judge with classify_shapes. +# A proposal is EVIDENCE, not judgment: it lands only on rows nobody has +# judged, never touches a status, and is what the end-of-turn question shows +# the agent as "looks like #N". The agent's classify_shapes is the verdict. # "The write path actually pulled it": a working session's reach. The # precision comes from the in-play test above, not from this window. @@ -1354,7 +1356,7 @@ def _stamp_allowed(rank: int, mine: str, canon: str) -> bool: return forms_agree(mine, canon) -async def stamp_write_path_instances( +async def suggest_write_path_instances( user_id: int, project_id: int, *, @@ -1365,22 +1367,35 @@ async def stamp_write_path_instances( resembles: dict[int, float] | None = None, repo_key: str = "", ) -> list[dict]: - """Land hook evidence as `instance` rows for the shapes being written. + """Offer hook evidence as a PROPOSAL on the shapes being written. + + This used to land the evidence as `instance` rows, classified_by=hook — + a permanent classification written with nobody reading it. That is what + poisoned two ledgers (132 and 488 rows below today's floor, #4608), and + what milestone 439 retires: the agent that wrote the code says what it is, + at the end of the turn, and this evidence is what it is shown when it + does. So the row keeps its status; the candidate canon goes into the + proposal fields (`proposed_snippet_id`, `proposal_basis` "reference" for a + by-name hit or "semantic" for a resemblance, `proposal_score`), where the + end-of-turn question reads it as "looks like #N". A judged row is left + alone entirely. ``shapes`` is the hook's (kind, name) list for ``path``; ``pulled`` is recent_pulls(); ``resembles`` maps snippet ids the semantic arm scored - for this payload to their score. Returns the rows stamped, each + for this payload to their score. Returns the suggestions, each {path, symbol, kind, snippet_id, reason} — empty in the common case. A shape the ledger has no live row for yet (it is being written right - now) gets a PROVISIONAL row under ``repo_key`` — first/last-seen empty — - so the stamp is not lost to the next sync, which either confirms the - shape (sets its seen marker) or stamps it vanished. No repo key → only - existing rows are stamped. + now) gets a PROVISIONAL unclassified row under ``repo_key`` — first/ + last-seen empty — so the suggestion survives to the end of the turn and + the next sync either confirms the shape or stamps it vanished. No repo + key → only existing rows carry a suggestion. + + Every pulled canon the payload NAMES is still recorded as a `uses` edge: + that is a fact about the code, not a verdict about what it is. When more than one pulled snippet is in play for a shape, a by-name - reference beats resemblance and the most recent pull breaks ties: a row - holds one canon (the known model limit logged on #2790). + reference beats resemblance and the most recent pull breaks ties. """ from scribe.services import access from scribe.services import snippets as snippets_svc @@ -1433,8 +1448,7 @@ async def stamp_write_path_instances( for bucket in in_play.values(): bucket.sort(key=lambda t: (t[0], t[1]), reverse=True) - now = datetime.now(timezone.utc) - stamped: list[dict] = [] + suggested: list[dict] = [] async with async_session() as session: rows = ( await session.execute( @@ -1486,9 +1500,12 @@ async def stamp_write_path_instances( by_key[(name, kind)] = row elif not (row.status in _MECHANICAL_TODO or row.classified_by == "hook"): continue # a judgment — or the canon itself — stands - await _judge(session, row, status="instance", snippet_id=sid, by="hook", - reason=why, at=now) - stamped.append({ + # A proposal, never a status: the writer decides (milestone 439). + row.proposed_snippet_id = sid + row.proposal_basis = "reference" if _rank >= 2 else "semantic" + row.proposal_score = resembles.get(sid) if _rank < 2 else 1.0 + row.proposal_group = None + suggested.append({ "path": path, "symbol": name, "kind": kind, "snippet_id": sid, "reason": why, }) @@ -1504,9 +1521,9 @@ async def stamp_write_path_instances( [t[2] for t in bucket if t[0] == 2], basis="hook", evidence="write path: pulled the snippet, payload names its symbol", ) - if stamped: + if suggested: await session.commit() - return stamped + return suggested # --- the mechanical proposer (#2792): the machine proposes, judgment classifies @@ -2210,8 +2227,11 @@ async def confirm_proposals( # A canon dominates a directory+kind when at least this many siblings are # judged (canonical/instance) and this share of them answer to one snippet. -# How much a payload must resemble a canon before the hook may assert, with -# nobody watching, that a shape IS an instance of it. +# How much a payload must resemble a canon before the hook SUGGESTS it as the +# shape's canon — and, for the rows stamped before milestone 439, the bar below +# which a stored stamp is weak (`is_weak_stamp`). Until milestone 439 this was +# the bar for the hook to ASSERT, unattended, that a shape IS an instance; the +# hook no longer asserts anything, and the history below is why. # # WHY A FLOOR AT ALL. There was none: any score the semantic arm produced # counted, so 0.69 asserted as confidently as 0.95, and the score went into @@ -2364,15 +2384,15 @@ async def canon_density( async def write_time_divergence( - project_id: int, path: str, shapes: list[tuple[str, str]], stamped: list[dict], + project_id: int, path: str, shapes: list[tuple[str, str]], suggested: list[dict], code: str = "", ) -> list[dict]: """The in-band check for the shapes the hook named at ``path``: for each kind whose directory has a dominant canon, the named shapes that are - not (already or just now) that canon's instance/canonical — new or + not that canon's instance/canonical (judged, or just suggested as it) — new or unclassified rows only; a judged shape is not re-litigated at every edit. Returns [{symbol, kind, canon_snippet_id, instances, judged}].""" - just_stamped = {(s["symbol"], s["kind"]): s["snippet_id"] for s in stamped} + just_stamped = {(s["symbol"], s["kind"]): s["snippet_id"] for s in suggested} out: list[dict] = [] if not shapes: return out diff --git a/tests/test_integration_shape_classify.py b/tests/test_integration_shape_classify.py index 3029e2a..8336248 100644 --- a/tests/test_integration_shape_classify.py +++ b/tests/test_integration_shape_classify.py @@ -322,15 +322,16 @@ async def test_sync_refiles_rows_whose_snippet_was_purged(seeded): @pytest.mark.integration -async def test_write_path_stamp_is_evidence_that_yields_to_judgment(seeded): - """Pulled + referenced → every named shape of the snippet's kind becomes - an instance row, classified_by=hook, carrying the evidence as reason. A - later agent judgment on one of them stands against a re-stamp; the hook - may only overwrite nobody's judgment or its own. The outsider stamps - nothing (write-gated like every other ledger write).""" +async def test_write_path_evidence_is_a_proposal_never_a_verdict(seeded): + """Milestone 439. Pulled + referenced → every named shape of the + snippet's kind CARRIES the snippet as a proposal (basis "reference"), + and its status is untouched: the agent that wrote it says what it is. + A judgment is never touched, not even by a proposal. The outsider + writes nothing (write-gated like every other ledger write). The uses + edge — a fact about the code — is still recorded.""" from datetime import datetime, timezone - from scribe.services.shape_ledger import stamp_write_path_instances + from scribe.services.shape_ledger import suggest_write_path_instances owner, other, pid, sid = ( seeded["owner"], seeded["other"], seeded["pid"], seeded["snippet"] @@ -338,84 +339,98 @@ async def test_write_path_stamp_is_evidence_that_yields_to_judgment(seeded): pulled = {sid: datetime.now(timezone.utc)} code = "app = factory()\nreturn app\n" # references the snippet's symbol - assert await stamp_write_path_instances( + assert await suggest_write_path_instances( other, pid, path="src/app.py", shapes=[("sym", "make_app")], code=code, pulled=pulled, ) == [] - stamped = await stamp_write_path_instances( + suggested = await suggest_write_path_instances( owner, pid, path="src/app.py", shapes=[("sym", "make_app"), ("sym", "Config"), ("css", "nope")], code=code, pulled=pulled, ) - assert {s["symbol"] for s in stamped} == {"make_app", "Config"} # css skipped: no css canon - rows, _ = await list_project_shapes(owner, pid, snippet_id=sid) + assert {s["symbol"] for s in suggested} == {"make_app", "Config"} # css skipped: no css canon + rows, _ = await list_project_shapes(owner, pid, path="src/app.py") by_symbol = {r.symbol: r for r in rows} - assert by_symbol["make_app"].status == "instance" - assert by_symbol["make_app"].classified_by == "hook" - assert by_symbol["make_app"].reason == f"hook: pulled #{sid}; payload references `factory`" + for name in ("make_app", "Config"): + row = by_symbol[name] + assert row.status == "unclassified" and row.classified_by is None, name + assert (row.proposed_snippet_id, row.proposal_basis) == (sid, "reference"), name + # The by-name reference is still recorded as a call-site fact. + users, _ = await list_project_shapes(owner, pid, uses=sid) + assert {"make_app", "Config"} <= {r.symbol for r in users} - # A judgment lands; the next stamp must leave it alone but may re-stamp - # its own earlier row. + # A judgment stands: the next suggestion leaves the judged row alone. await classify_shapes(owner, pid, [ {"path": "src/app.py", "symbol": "make_app", "status": "exempt", "reason": "the app factory is its own thing"}, ]) - again = await stamp_write_path_instances( + again = await suggest_write_path_instances( owner, pid, path="src/app.py", shapes=[("sym", "make_app"), ("sym", "Config")], code=code, pulled=pulled, ) assert {s["symbol"] for s in again} == {"Config"} rows, _ = await list_project_shapes(owner, pid, path="src/app.py") - by_symbol = {r.symbol: r for r in rows} - assert by_symbol["make_app"].status == "exempt" - assert by_symbol["Config"].status == "instance" + assert {r.symbol: r.status for r in rows}["make_app"] == "exempt" # Neither pulled nor in play → nothing, even with shapes named. - assert await stamp_write_path_instances( + assert await suggest_write_path_instances( owner, pid, path="src/util.py", shapes=[("sym", "helper")], code="print('unrelated')", pulled=pulled, ) == [] + # And nothing the hook does writes classified_by="hook" any more. + async with async_session() as s: + hook_rows = (await s.execute(select(CodeShape).where( + CodeShape.project_id == pid, CodeShape.classified_by == "hook", + ))).scalars().all() + assert hook_rows == [] + @pytest.mark.integration -async def test_a_brand_new_shape_gets_a_provisional_row_the_sync_settles(seeded): +async def test_a_brand_new_shape_gets_a_provisional_row_carrying_the_suggestion(seeded): """The shape being written right now has no ledger row yet. With the - hook's repo key it gets a provisional one — seen markers empty — so the - stamp survives until the next sync, which confirms it (sets the marker) - or stamps it vanished. Without a repo key only existing rows are touched.""" + hook's repo key it gets a provisional, UNCLASSIFIED one — seen markers + empty — carrying the suggestion to the end-of-turn question; the sync + confirms it or stamps it vanished. Without a repo key only existing rows + carry a suggestion.""" from datetime import datetime, timezone - from scribe.services.shape_ledger import stamp_write_path_instances + from scribe.services.shape_ledger import suggest_write_path_instances, unjudged_shapes owner, pid, sid = seeded["owner"], seeded["pid"], seeded["snippet"] pulled = {sid: datetime.now(timezone.utc)} code = "def build():\n return factory()\n" - assert await stamp_write_path_instances( + assert await suggest_write_path_instances( owner, pid, path="src/new.py", shapes=[("sym", "build")], code=code, pulled=pulled, # no repo_key ) == [] - stamped = await stamp_write_path_instances( + suggested = await suggest_write_path_instances( owner, pid, path="src/new.py", shapes=[("sym", "build")], code=code, pulled=pulled, repo_key=REPO, ) - assert [s["symbol"] for s in stamped] == ["build"] + assert [s["symbol"] for s in suggested] == ["build"] async with async_session() as s: row = (await s.execute(select(CodeShape).where( CodeShape.project_id == pid, CodeShape.path == "src/new.py", ))).scalar_one() - assert row.status == "instance" and row.classified_by == "hook" + assert row.status == "unclassified" and row.classified_by is None + assert row.proposed_snippet_id == sid # "Unset" is the column's empty default — the markers are non-null # Text, and the sync is what first fills them. assert row.first_seen_commit == "" and row.last_seen_commit == "" - # The sync sees the shape in the tree → confirmed, stamp intact. + # The end-of-turn question shows the suggestion as evidence. + asked = await unjudged_shapes(pid, [("src/new.py", "sym", "build")], repo_key=REPO) + assert asked[0]["evidence"]["looks_like"]["snippet_id"] == sid + + # The sync sees the shape in the tree → confirmed, still unjudged. await sync_repo_shapes( pid, REPO, SHAPES + [("src/new.py", "sym", "build")], seen_marker="abc123", ) rows, _ = await list_project_shapes(owner, pid, path="src/new.py") - assert rows[0].status == "instance" and rows[0].last_seen_commit == "abc123" + assert rows[0].status == "unclassified" and rows[0].last_seen_commit == "abc123" # The sync no longer sees it → vanished, out of the live accounting. await sync_repo_shapes(pid, REPO, SHAPES, seen_marker="def456") @@ -866,19 +881,19 @@ async def test_a_second_confirm_dialog_is_detected_and_named(seeded): # In-band: the hook names the shape at write time → the check names the canon. named = await write_time_divergence( - pid, f"{comp}/Danger.vue", [("sym", "confirmDanger")], stamped=[] + pid, f"{comp}/Danger.vue", [("sym", "confirmDanger")], suggested=[] ) assert named == [{"symbol": "confirmDanger", "kind": "sym", "canon_snippet_id": sid, "instances": 4, "judged": 4}] # ...but an already-judged shape, or one just stamped as the canon's # instance, is not re-litigated. - assert await write_time_divergence(pid, f"{comp}/Trash.vue", [("sym", "onTrash")], stamped=[]) == [] + assert await write_time_divergence(pid, f"{comp}/Trash.vue", [("sym", "onTrash")], suggested=[]) == [] assert await write_time_divergence( pid, f"{comp}/New.vue", [("sym", "onNew")], - stamped=[{"symbol": "onNew", "kind": "sym", "snippet_id": sid}], + suggested=[{"symbol": "onNew", "kind": "sym", "snippet_id": sid}], ) == [] # A directory with no dominant canon is silent. - assert await write_time_divergence(pid, "src/other.py", [("sym", "thing")], stamped=[]) == [] + assert await write_time_divergence(pid, "src/other.py", [("sym", "thing")], suggested=[]) == [] # The judgment answers the question and clears the flag. await classify_shapes(owner, pid, [ @@ -956,7 +971,7 @@ async def test_weak_stamps_elect_no_canon_and_their_flags_are_withdrawn(seeded): rows, total = await list_project_shapes(owner, pid, flag="divergence") assert total == 0, "a flag whose canon no longer dominates is withdrawn" assert await write_time_divergence( - pid, f"{comp}/Danger.vue", [("sym", "confirmDanger")], stamped=[] + pid, f"{comp}/Danger.vue", [("sym", "confirmDanger")], suggested=[] ) == [] # Nothing was un-judged: the stamps stand until someone reads them. async with async_session() as session: diff --git a/tests/test_write_path_trigger.py b/tests/test_write_path_trigger.py index 02c2f9b..8800f5f 100644 --- a/tests/test_write_path_trigger.py +++ b/tests/test_write_path_trigger.py @@ -1291,9 +1291,9 @@ async def test_stamping_needs_named_shapes_and_a_recent_pull(): patch.object(pc, "semantic_search_notes", AsyncMock(return_value=[])), \ patch.object(pc, "record_retrieval", MagicMock()), \ patch.object(pc.shape_ledger_svc, "recent_pulls", pulls), \ - patch.object(pc.shape_ledger_svc, "stamp_write_path_instances", stamp): + patch.object(pc.shape_ledger_svc, "suggest_write_path_instances", stamp): out = await pc.build_write_path_hint(1, "src/x.py", code=REAL_CODE, project_id=4) - assert out["stamped"] == [] + assert out["suggested"] == [] pulls.assert_not_awaited() # no shapes → no read out = await pc.build_write_path_hint( 1, "src/x.py", code=REAL_CODE, project_id=4, @@ -1301,7 +1301,7 @@ async def test_stamping_needs_named_shapes_and_a_recent_pull(): ) pulls.assert_awaited_once() stamp.assert_not_awaited() # shapes, but no pull - assert out["stamped"] == [] + assert out["suggested"] == [] @pytest.mark.asyncio @@ -1330,7 +1330,7 @@ async def test_a_pulled_snippet_already_seen_is_evidence_not_menu(): patch.object(pc, "record_retrieval", MagicMock()), \ patch.object(pc, "owner_names_for", AsyncMock(return_value={})), \ patch.object(pc.shape_ledger_svc, "recent_pulls", AsyncMock(return_value={7: _ts()})), \ - patch.object(pc.shape_ledger_svc, "stamp_write_path_instances", stamp): + patch.object(pc.shape_ledger_svc, "suggest_write_path_instances", stamp): out = await pc.build_write_path_hint( 1, "src/x.py", code=REAL_CODE, project_id=4, exclude_ids=[7], stamp_shapes=[("sym", "debounce")], repo_key="git.example.com/a/b", @@ -1355,11 +1355,11 @@ async def test_a_pulled_snippet_already_seen_is_evidence_not_menu(): assert skw["resembles"] == {7: 0.91} assert skw["shapes"] == [("sym", "debounce")] assert skw["repo_key"] == "git.example.com/a/b" - assert out["stamped"][0]["snippet_id"] == 7 - # And the session is told what landed, with the way to correct it. + assert out["suggested"][0]["snippet_id"] == 7 + # And the session is shown it as evidence for its own end-of-turn judgment. assert "Shape accounting" in out["context"] - assert "`debounce` → instance of #7" in out["context"] - assert "classify_shapes" in out["context"] + assert "`debounce` → looks like #7" in out["context"] + assert "When you judge this turn's shapes" in out["context"] @pytest.mark.asyncio @@ -1376,15 +1376,15 @@ async def test_a_stamp_renders_even_when_the_hint_is_otherwise_silent(): patch.object(pc, "record_retrieval", MagicMock()), \ patch.object(pc, "owner_names_for", AsyncMock(return_value={})), \ patch.object(pc.shape_ledger_svc, "recent_pulls", AsyncMock(return_value={5: _ts()})), \ - patch.object(pc.shape_ledger_svc, "stamp_write_path_instances", + patch.object(pc.shape_ledger_svc, "suggest_write_path_instances", AsyncMock(return_value=stamped)): out = await pc.build_write_path_hint( 1, "web/b.css", code=".btn-primary { color: red; }" * 4, project_id=4, stamp_shapes=[("css", "btn-primary")], ) assert out["note_ids"] == [] - assert out["stamped"] == stamped - assert "`.btn-primary` → instance of #5" in out["context"] + assert out["suggested"] == stamped + assert "`.btn-primary` → looks like #5" in out["context"] @pytest.mark.asyncio @@ -1397,13 +1397,13 @@ async def test_a_failing_stamp_does_not_sink_the_hint(): patch.object(pc, "record_retrieval", MagicMock()), \ patch.object(pc, "owner_names_for", AsyncMock(return_value={})), \ patch.object(pc.shape_ledger_svc, "recent_pulls", AsyncMock(return_value={12: _ts()})), \ - patch.object(pc.shape_ledger_svc, "stamp_write_path_instances", + patch.object(pc.shape_ledger_svc, "suggest_write_path_instances", AsyncMock(side_effect=RuntimeError("ledger down"))): out = await pc.build_write_path_hint( 1, "src/x.py", code=REAL_CODE, project_id=4, stamp_shapes=[("sym", "f")], ) assert out["sync_note_ids"] == [12] - assert out["stamped"] == [] + assert out["suggested"] == [] def test_route_stamps_only_for_a_caller_allowed_to_write(): @@ -1760,7 +1760,7 @@ async def test_a_record_this_arm_withheld_itself_is_not_a_near_miss(): patch.object(pc, "owner_names_for", AsyncMock(return_value={})), \ patch.object(pc.shape_ledger_svc, "recent_pulls", AsyncMock(return_value={7: _ts()})), \ - patch.object(pc.shape_ledger_svc, "stamp_write_path_instances", + patch.object(pc.shape_ledger_svc, "suggest_write_path_instances", AsyncMock(return_value=[])), \ patch.object(pc, "semantic_search_notes", _search_reporting(0.9, fake_note(