feat(family): the shape ledger judges against family ideas, across languages (milestone 463 step 5, #4991)
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / TypeScript typecheck (push) Successful in 58s
CI & Build / integration (push) Successful in 1m26s
CI & Build / Python tests (push) Successful in 2m9s
CI & Build / Build & push image (push) Successful in 1m27s

- classify_shapes takes idea_id: the shape is judged against the idea's
  reference in its language, or its first.
- A shape classified against a canon idea's reference moves the project's
  adoption row (instance -> adopted, variant -> variant). Withdrawing the
  shapes that gave an answer returns it to unassessed. This runs on
  classify_shapes, the sweep, confirm_proposals and the coverage refresh.
- assess and undo refuse an answer the shapes contradict. A conflict
  resolution re-judges the variant shapes on both sides.
- The proposer's family arm offers a canon idea's reference on a shared
  platform, in any language. It proposes only when the reference is the top
  hit over every readable snippet and scores at least 0.70. The pairs this
  was measured on are recorded beside _FAMILY_FLOOR. The proposer is now v6.
- The matrix cell shows the code that answers it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-06 13:06:45 -04:00
co-authored by Claude Opus 5.5
parent 233fa3eea9
commit 5f41dbd283
10 changed files with 823 additions and 38 deletions
+130 -17
View File
@@ -514,7 +514,7 @@ def validate_classifications(items: list[dict]) -> str | None:
snippet_id = item.get("snippet_id") or 0
if status in _NEEDS_TARGET and not snippet_id:
return (
f"classifications[{i}]: status {status!r} needs snippet_id — "
f"classifications[{i}]: status {status!r} needs snippet_id (or idea_id) — "
"the snippet this shape is (or departs from)"
)
if status in ("variant", "exempt") and not (item.get("reason") or "").strip():
@@ -570,6 +570,7 @@ async def classify_shapes(
if via not in _CALLER_VIAS:
raise ValueError(f"via must be one of: {', '.join(_CALLER_VIAS)}")
classifications = await _resolve_ideas(user_id, classifications)
error = validate_classifications(classifications)
if error:
raise ValueError(error)
@@ -602,6 +603,7 @@ async def classify_shapes(
classified = 0
provisional = 0
unmatched: list[dict] = []
touched: set[int] = set()
async with async_session() as session:
rows = (
await session.execute(
@@ -639,6 +641,7 @@ async def classify_shapes(
continue
status = item["status"]
for row in matches:
touched.update(x for x in (row.snippet_id, item.get("snippet_id")) if x)
await _judge(
session, row, status=status,
snippet_id=int(item["snippet_id"]) if status in _NEEDS_TARGET else None,
@@ -653,9 +656,52 @@ async def classify_shapes(
out = {"classified": classified, "unmatched": unmatched}
if provisional:
out["provisional"] = provisional
moved = await _sync_family(user_id, project_id, touched)
if moved:
out["family"] = moved
return out
async def _resolve_ideas(user_id: int, items: list[dict]) -> list[dict]:
"""An item may name a family idea (`idea_id`) in place of a snippet
(milestone 463 step 5): it is judged against the idea's reference in the
shape's language, or its first when there is none in that language — an
idea is shared across languages. An explicit snippet_id wins."""
from scribe.services import family_adoption
out = []
for i, item in enumerate(items or []):
if (isinstance(item, dict) and item.get("idea_id") and not item.get("snippet_id")
and item.get("status") in _NEEDS_TARGET):
try:
sid = await family_adoption.reference_for(
user_id, int(item["idea_id"]), (item.get("path") or "").strip())
except (TypeError, ValueError) as exc:
raise ValueError(f"classifications[{i}]: {exc}") from None
item = {**item, "snippet_id": sid}
out.append(item)
return out
async def _sync_family(user_id: int, project_id: int, snippet_ids: set[int]) -> list[dict]:
"""The adoption ledger follows the shapes (milestone 463 step 5): after a
judgment commits, the canon ideas whose references it touched — as the
new target or the one it replaced — are answered from the shapes.
A failure is logged and the judgment stands; the next judgment touching
those references, or the next coverage refresh, brings the two back in
line."""
from scribe.services import family_adoption
if not snippet_ids:
return []
try:
return await family_adoption.sync_from_shapes(user_id, project_id, snippet_ids)
except Exception:
logger.warning("family adoption sync failed for project %s", project_id, exc_info=True)
return []
def rule_matches(row: CodeShape, *, path: str, pattern: str, kind: str) -> bool:
"""Does a ledger row fall under a rule-form classification (#2868)?
``path`` is a file or a directory (everything beneath it), ``pattern``
@@ -721,6 +767,7 @@ async def classify_shapes_where(
now = datetime.now(timezone.utc)
judged: list[str] = []
touched: set[int] = {int(snippet_id)} if snippet_id and status in _NEEDS_TARGET else set()
async with async_session() as session:
conds = [CodeShape.project_id == project_id, CodeShape.vanished_at.is_(None)]
if not include_judged:
@@ -729,6 +776,8 @@ async def classify_shapes_where(
for row in rows:
if not rule_matches(row, path=path, pattern=pattern, kind=kind):
continue
if row.snippet_id:
touched.add(row.snippet_id)
await _judge(
session, row, status=status,
snippet_id=int(snippet_id) if status in _NEEDS_TARGET else None,
@@ -738,7 +787,11 @@ async def classify_shapes_where(
await record_uses(session, row, uses, basis=via, evidence=reason)
judged.append(f"{row.path}::{row.symbol}")
await session.commit()
return {"classified": len(judged), "sample": sorted(judged)[:12]}
out = {"classified": len(judged), "sample": sorted(judged)[:12]}
moved = await _sync_family(user_id, project_id, touched)
if moved:
out["family"] = moved
return out
async def list_project_shapes(
@@ -1611,6 +1664,29 @@ _SEMANTIC_LIMIT = 8
# against a prose-forward snippet document measures the wrong field (#2518);
# no floor separates the two bands. The arm PROPOSES on a hit; its silence
# says nothing.
#
# THE FAMILY ARM (milestone 463 step 5). The arm above is held to the shape's
# own project and language family, because across projects it was pure noise
# (#2871). A family idea is the exception the hold was waiting for: an idea
# shared across projects ON PURPOSE, whose reference may be in another
# language (a Python throttle is an instance of a Go one). So the arm also
# proposes the reference of a canon idea that reaches the shape's project
# through a shared platform — any language, any project — under a stricter
# rule of its own, measured 2026-10-06 (#4991) on cross-project pairs:
#
# true pairs A py→go 0.728 (next 0.703) B py→go 0.715 (next 0.675)
# D kt→kt 0.816 (next 0.734) E go→go 0.610 (next 0.604)
# no reference N1 0.666 N2 0.623 N3 0.626 N4 0.715 N5 0.614 (best hit,
# none of them a family reference); unrelated hits to 0.774
#
# No floor separates those bands — the false band reaches 0.715–0.774, a
# true pair sits at 0.610. RANK does: every true pair was the single best
# snippet the user can read. So a family reference is proposed only when it
# is the TOP hit over everything readable (not merely the first allowed one,
# as above) and clears _FAMILY_FLOOR: A, B, D proposed; E missed; no
# negative proposed. A miss is still not evidence — the judge can classify
# against the idea by idea_id.
_FAMILY_FLOOR = 0.70
def _semantic_priority(row) -> tuple:
@@ -1640,7 +1716,9 @@ def _semantic_priority(row) -> tuple:
# v5: the semantic arm's floor dropped from 0.8 to the write-path floor (#4208),
# and the signature floor from 0.8 to 0.75 (#4306), so rows examined and
# found nothing for must be read once more.
_PROPOSER_VERSION = 5
# v6: the family arm (milestone 463 step 5) — a canon idea's reference on a
# shared platform, any language, as the top hit at _FAMILY_FLOOR.
_PROPOSER_VERSION = 6
# Signature resemblance floor, name blanked (difflib ratio) — and a length
# floor, because `def NAME():` resembles `def NAME(x):` at 0.95 while saying
# nothing; a family shape has parameters to resemble.
@@ -1883,28 +1961,47 @@ def _substance(text: str) -> int:
return len("".join((text or "").split()))
def pick_semantic(
hits: list[tuple[float, int]], allowed: set[int], family: set[int], *, floor: float,
) -> tuple[int, float, str] | None:
"""Which ranked hit the semantic arm proposes, as (snippet_id, score,
basis). Pure; ``hits`` are (score, snippet_id) over everything the user
can read, best first. The TOP hit may be a family reference (basis
"family", at _FAMILY_FLOOR); any hit at ``floor`` may be an own-project
canon (basis "semantic")."""
for rank, (score, sid) in enumerate(hits):
if rank == 0 and sid in family and score >= _FAMILY_FLOOR:
return sid, round(float(score), 3), "family"
if sid in allowed and score >= floor:
return sid, round(float(score), 3), "semantic"
return None
async def _semantic_canon(
user_id: int, body: str, allowed: set[int],
) -> tuple[int, float] | None:
"""The canon this body MEANS, or None. None is "no proposal", never "not
the canon" — see the note above `_SEMANTIC_LIMIT`."""
user_id: int, body: str, allowed: set[int], family: set[int] | None = None,
) -> tuple[int, float, str] | None:
"""The canon this body MEANS, as (snippet_id, score, basis), or None.
None is "no proposal", never "not the canon" — see the note above
`_SEMANTIC_LIMIT`; the family arm's rule is the note above
`_FAMILY_FLOOR`."""
from scribe.services.embeddings import semantic_search_notes
from scribe.services.plugin_context import (
WRITEPATH_DEFAULT_THRESHOLD, WRITEPATH_MIN_CODE_CHARS, concept_query,
)
if _substance(body) < WRITEPATH_MIN_CODE_CHARS or not allowed:
family = family or set()
if _substance(body) < WRITEPATH_MIN_CODE_CHARS or not (allowed or family):
return None
query = concept_query(body) or body
hits = await semantic_search_notes(
user_id, query, limit=_SEMANTIC_LIMIT,
threshold=WRITEPATH_DEFAULT_THRESHOLD,
threshold=min(WRITEPATH_DEFAULT_THRESHOLD, _FAMILY_FLOOR),
note_type="snippet", scope="browse",
)
for score, note in hits:
if int(note.id) in allowed:
return int(note.id), round(float(score), 3)
return None
return pick_semantic(
[(float(score), int(note.id)) for score, note in hits], allowed, family,
floor=WRITEPATH_DEFAULT_THRESHOLD,
)
async def propose_for_repo(
@@ -1933,6 +2030,16 @@ async def propose_for_repo(
def semantic_allowed(path: str) -> set[int]:
return {c.snippet_id for c in sym_canons if same_family(path, c.language)}
# The family arm: references of canon ideas on a platform this project
# shares, in any language (see _FAMILY_FLOOR). A project on no shared
# platform gets none, so nothing is proposed across to it.
try:
from scribe.services import family_adoption
family_refs = await family_adoption.family_reference_ids(project_id)
except Exception:
logger.warning("family references unreadable for project %s", project_id, exc_info=True)
family_refs = set()
now = datetime.now(timezone.utc)
examined = proposed = checked = 0
async with async_session() as session:
@@ -1998,13 +2105,13 @@ async def propose_for_repo(
continue
checked += 1
try:
found = await _semantic_canon(user_id, d[5], semantic_allowed(row.path))
found = await _semantic_canon(
user_id, d[5], semantic_allowed(row.path), family_refs)
except Exception:
logger.warning("semantic proposal failed", exc_info=True)
found = None
if found:
row.proposed_snippet_id, row.proposal_score = found
row.proposal_basis = "semantic"
row.proposed_snippet_id, row.proposal_score, row.proposal_basis = found
row.proposal_group = None
proposed += 1
await session.commit()
@@ -2210,11 +2317,13 @@ async def confirm_proposals(
conds.append(CodeShape.proposal_basis == basis.strip())
now = datetime.now(timezone.utc)
confirmed = 0
touched: set[int] = set()
async with async_session() as session:
rows = (await session.execute(select(CodeShape).where(*conds))).scalars().all()
for row in rows:
if (row.proposal_score or 0.0) < min_score:
continue
touched.add(row.proposed_snippet_id)
await _judge(
session, row, status="instance", snippet_id=row.proposed_snippet_id,
by="agent", at=now,
@@ -2225,7 +2334,11 @@ async def confirm_proposals(
)
confirmed += 1
await session.commit()
return {"confirmed": confirmed}
out = {"confirmed": confirmed}
moved = await _sync_family(user_id, project_id, touched)
if moved:
out["family"] = moved
return out
# --- the divergence readout (#2793): button B where button A is canon -------