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
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:
@@ -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")
|
||||
Reference in New Issue
Block a user