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
+142
View File
@@ -0,0 +1,142 @@
"""The shape ledger judging against family ideas, without a database
(milestone 463 step 5).
Pinned here: what a project's shapes say about an idea, when an answer
contradicts them, which reference a shape in a given language is judged
against, and the family arm's rule — measured on 2026-10-06 (#4991), and
replayed below as the table it was chosen from. The two ledgers moving
together against Postgres are in tests/test_integration_family_adoption.py.
"""
from __future__ import annotations
from types import SimpleNamespace
from unittest.mock import AsyncMock, patch
import pytest
from scribe.services.family_adoption import (
_contradiction, path_speaks, shape_outcome, shapes_say,
)
from scribe.services.shape_ledger import _FAMILY_FLOOR, _semantic_canon, pick_semantic
from tests.helpers import tool_doc
def _shape(status: str, symbol: str = "f", reason: str = "", snippet_id: int = 7):
return SimpleNamespace(id=1, path=f"src/{symbol}.py", symbol=symbol, status=status,
reason=reason, snippet_id=snippet_id)
# --- what the shapes say ---------------------------------------------------------
@pytest.mark.parametrize("statuses,expected", [
(["instance"], "adopted"),
(["canonical"], "adopted"),
(["variant", "instance"], "adopted"),
(["variant"], "variant"),
([], None),
])
def test_shape_outcome(statuses, expected):
assert shape_outcome(statuses) == expected
def test_no_shape_is_silence_not_owed():
assert shapes_say([]) is None
def test_an_adopted_reason_is_fixed_text_so_another_instance_logs_nothing():
one = shapes_say([_shape("instance", "a")])
two = shapes_say([_shape("instance", "a"), _shape("instance", "b")])
assert one["reason"] == two["reason"]
assert two["evidence"] == ["src/a.py::a", "src/b.py::b"]
def test_a_variant_carries_the_shapes_why_and_only_variants_are_its_evidence():
said = shapes_say([_shape("variant", "k", reason="a kiosk installs it")])
assert said["outcome"] == "variant" and "a kiosk installs it" in said["reason"]
mixed = shapes_say([_shape("variant", "k", reason="x"), _shape("instance", "i")])
assert mixed["outcome"] == "adopted" and mixed["evidence"] == ["src/i.py::i"]
# --- the two ledgers never disagree -----------------------------------------------
def test_an_answer_the_shapes_contradict_names_them_and_the_way_out():
said = shapes_say([_shape("instance", "a")])
refused = _contradiction("owed", said, idea_id=5)
assert "src/a.py::a" in refused and "classify_shapes" in refused and "#5" in refused
@pytest.mark.parametrize("outcome,said", [
("adopted", shapes_say([_shape("instance")])),
("owed", None),
("exempt", None),
])
def test_an_agreeing_answer_or_silent_shapes_refuse_nothing(outcome, said):
assert _contradiction(outcome, said, idea_id=5) is None
# --- which reference a shape is judged against --------------------------------------
@pytest.mark.parametrize("path,language,expected", [
("tools/release.py", "python", True),
("app/Updater.kt", "kotlin", True),
("cmd/main.go", "go", True),
("cmd/main.go", "kotlin", False),
("web/App.vue", "vue", True),
("README", "python", False),
("x.py", "", False),
])
def test_path_speaks(path, language, expected):
assert path_speaks(path, language) is expected
# --- the family arm, and the measurement it was chosen from -----------------------
OWN, REF, OTHER = 1, 2, 3
@pytest.mark.parametrize("name,hits,expected", [
# #4991's pairs, the reference as REF and the best other hit as OTHER.
("A py->go", [(0.728, REF), (0.703, OTHER)], "family"),
("B py->go", [(0.715, REF), (0.675, OTHER)], "family"),
("D kt->kt", [(0.816, REF), (0.734, OTHER)], "family"),
("E go->go", [(0.610, REF), (0.604, OTHER)], None),
# No reference exists: the best hit is unrelated, the reference trails.
("N4", [(0.715, OTHER), (0.706, REF)], None),
("N1", [(0.666, OTHER)], None),
])
def test_the_family_arm_replays_its_measurement(name, hits, expected):
found = pick_semantic(hits, allowed=set(), family={REF}, floor=0.68)
assert (found[2] if found else None) == expected, name
def test_a_family_reference_must_be_the_top_hit_not_merely_the_first_allowed():
assert pick_semantic([(0.80, OTHER), (0.79, REF)], set(), {REF}, floor=0.68) is None
def test_the_own_project_arm_still_looks_past_disallowed_hits():
assert pick_semantic([(0.80, OTHER), (0.70, OWN)], {OWN}, {REF}, floor=0.68) == (
OWN, 0.7, "semantic")
def test_the_family_floor_sits_between_the_bands_it_was_measured_on():
assert 0.610 < _FAMILY_FLOOR <= 0.715
async def test_a_project_on_a_shared_platform_is_searched_with_no_own_canon():
"""The own-project allowed set is often empty (the language gate); the
family references alone are reason to search."""
body = "def throttle(key: str) -> bool:\n return attempts[key] < LIMIT and not locked(key)\n"
hit = SimpleNamespace(id=REF)
with patch("scribe.services.embeddings.semantic_search_notes",
AsyncMock(return_value=[(0.72, hit)])):
assert await _semantic_canon(1, body, set(), {REF}) == (REF, 0.72, "family")
# --- the agent-facing contract -----------------------------------------------------
def test_the_tools_teach_idea_id_and_the_family_basis():
classify = tool_doc("scribe.mcp.tools.shapes", "classify_shapes")
assert "idea_id" in classify and "shapes contradict" in classify
assert "family" in tool_doc("scribe.mcp.tools.shapes", "list_shapes")
assert '"family"' in tool_doc("scribe.mcp.tools.shapes", "confirm_shape_proposals")
assert "classify_shapes" in tool_doc("scribe.mcp.tools.family", "assess_family_adoption")