feat(write-path): the derive arm — the hint names a duplicate family (no canon) or a canon elsewhere for the shapes being written; exclude_derive channel (#2900, milestone 299 step 2)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Failing after 9s
CI & Build / integration (push) Successful in 25s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Successful in 58s
CI & Build / Build & push image (push) Successful in 23s

shape_ledger.write_time_derive asks the ledger what it knows about each
named (kind, symbol): a derive-grouped family (identical body / same name
in N other files) -> "derive it now, do not add a copy"; a canonical row
at another path -> "canon #N at <path>, reuse". Judged rows at the path and
the canon own file stay silent. Rendered by _derive_line beside the
divergence line; keyed (group id / canon:<id>) on a third per-session dedup
channel in the hook (.derive.ids -> exclude_derive=).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-22 13:30:33 -04:00
co-authored by Claude Fable 5
parent bb242ca566
commit 2324c15418
6 changed files with 295 additions and 3 deletions
+49 -2
View File
@@ -706,6 +706,7 @@ async def build_write_path_hint(
exclude_sync_ids: list[int] | None = None,
stamp_shapes: list[tuple[str, str]] | None = None,
repo_key: str = "",
exclude_derive: list[str] | None = None,
) -> dict:
"""Prior-art hint for the plugin's PreToolUse hook on Write/Edit.
@@ -765,7 +766,7 @@ async def build_write_path_hint(
"""
cfg = await get_writepath_config(user_id)
empty = {"context": "", "note_ids": [], "sync_note_ids": [], "config": cfg,
"stamped": [], "divergence": []}
"stamped": [], "divergence": [], "derive": [], "derive_keys": []}
path = (path or "").strip()
if not cfg["enabled"] or not path:
return empty
@@ -935,7 +936,20 @@ async def build_write_path_hint(
)
except Exception:
logger.warning("write-time divergence check failed", exc_info=True)
if not synced and not menu and not stamped and not divergence:
# The in-band DERIVE check (#2900): the ledger's own knowledge of the
# names being written — a duplicate family with no canon, or a canon
# recorded elsewhere. This is the arm the by-name local grep could not
# be: it knows whether the other copies are canon or stray. Keyed per
# session (`exclude_derive`) so a family is named once, not per edit.
derive: list[dict] = []
if stamp_shapes and project_id:
try:
found = await shape_ledger_svc.write_time_derive(project_id, path, stamp_shapes)
skip = set(exclude_derive or [])
derive = [d for d in found if d.get("key") not in skip]
except Exception:
logger.warning("write-time derive check failed", exc_info=True)
if not synced and not menu and not stamped and not divergence and not derive:
return empty
owners = await owner_names_for({
@@ -1003,6 +1017,8 @@ async def build_write_path_hint(
lines.append(_stamp_line(path, stamped))
if divergence:
lines.append(_divergence_line(path, divergence))
if derive:
lines.append(_derive_line(path, derive))
# 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
@@ -1027,9 +1043,40 @@ async def build_write_path_hint(
"config": cfg,
"stamped": stamped,
"divergence": divergence,
"derive": derive,
"derive_keys": [d["key"] for d in derive],
}
def _derive_line(path: str, derive: list[dict]) -> str:
"""The ledger's word on the names being written (#2900): a duplicate
family to derive, or a canon to reuse — said at the write."""
parts = []
for d in derive:
if d.get("canon"):
c = d["canon"]
parts.append(
f"`{c['label']}` is canon — snippet #{c['snippet_id']} at `{c['path']}`; "
"pull it and reuse, don't redefine"
)
continue
f = d["family"]
how = "identical body" if f.get("identical") else "same name defined"
files = ", ".join(f"`{x}`" for x in f.get("files") or [])
more = f.get("file_count", 0) - len(f.get("files") or [])
if more > 0:
files += f" +{more} more"
parts.append(
f"`{f['label']}` is a duplicate family with no canon — {how} in "
f"{f.get('file_count', 0)} other file(s): {files}; derive it now: "
"record the canon (create_snippet) and make the copies instances "
"(classify_shapes) — or, if these are convention not copies, "
"`classify_shapes(..., status=\"exempt\", reason_code=\"convention-plumbing\")` "
"dismisses the family — rather than adding another copy"
)
return f"> Shape ledger at `{path}`: " + "; ".join(parts) + "."
def _divergence_line(path: str, divergence: list[dict]) -> str:
"""Button B where button A is canon — named at the write (#2793)."""
parts = [