feat(shapes): the write-path hook suggests and no longer stamps (milestone 439 step 4)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 54s
CI & Build / Python tests (push) Successful in 1m49s
CI & Build / Build & push image (push) Successful in 27s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 54s
CI & Build / Python tests (push) Successful in 1m49s
CI & Build / Build & push image (push) Successful in 27s
The hook used to land its evidence as `instance` rows, classified_by=hook — a permanent verdict nobody read, and the source of the weak stamps that poisoned two ledgers (#4608). Now the agent that wrote the code judges it at the end of the turn, and the hook's evidence is what it is shown. - shape_ledger.suggest_write_path_instances (was stamp_write_path_instances): same evidence and form gate, but it writes a PROPOSAL — proposed_snippet_id, basis "reference" (named) or "semantic" (resembles), score — and never a status. A judged row is left alone; a new shape gets an unclassified provisional row so the suggestion reaches the end-of-turn question as "looks like #N". By-name uses edges stay: a fact, not a verdict. - The prior-art line says "looks like #N … when you judge this turn's shapes, say whether it is", and the result key is `suggested`. - write_time_divergence takes `suggested`; _RESEMBLE_MIN re-documented as the suggestion bar and the weak-stamp line. - reusing-code skill no longer says the pull stamps an instance. - Tests moved to the new contract, plus a guard that nothing the hook does writes classified_by="hook". Plugin version minted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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 ""
|
||||
|
||||
@@ -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."
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user