"""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")