fix(6477): a body uses a canon by name only when the name can resolve to it
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / TypeScript typecheck (push) Successful in 43s
CI & Build / integration (push) Successful in 1m36s
CI & Build / Python tests (push) Successful in 2m7s
CI & Build / Build & push image (push) Successful in 42s

The proposer's reference basis, and every uses edge it writes, counted any
word-bounded mention of a canon's symbol. Three holes made that wrong at
scale (snippet #5448, symbol `file`, held 2,161 uses edges):

- Go, Rust, Kotlin, Swift, Dart, C, C#, Ruby and PHP were outside the
  language-family maps, so their canons read as "unknown, no gate" and were
  matched in every Python, TS and CSS body.
- Canons in other projects were matched by name, though one repo never
  calls a function defined in another's.
- A plain word (`file`, `seen`) or a module's own helper (`_run`) matched
  wherever it appeared.

names_canon is now the one by-name test, used by match_canon,
reference_canons and the write-path hint: same project, not a name the file
defines itself (unless the canon is recorded there), no generic verb with or
without a leading underscore, and a bare lowercase word only where it is
called. The write path keeps the payload's own names in play, since a
redefinition of a pulled canon is the copy it exists to flag.

Repair: record_uses only adds, so replace_reference_uses now makes a row's
machine-written reference edges exactly what the current rule finds,
leaving judged and hook edges alone. _PROPOSER_VERSION 7 re-reads every row
on its project's next refresh. Migration 0125 deletes the cross-project
reference edges already stored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-26 17:41:21 -04:00
co-authored by Claude Opus 5.5
parent 0b98a3a5cd
commit cdb71e64c4
4 changed files with 229 additions and 23 deletions
+47
View File
@@ -560,6 +560,53 @@ def test_reference_canons_names_every_used_canon_not_just_the_best():
assert reference_canons("sym", "f/v.vue", "show", "fmtDate(x); hash_token(y)", [a, ts]) == [3]
def test_a_go_canon_says_nothing_about_a_python_or_vue_body():
"""Go was outside the family maps, so it read as "unknown, no gate" and a
Go whole-file canon whose symbol is `file` was matched in every body."""
from grimoire.services.shape_ledger import Canon, _norm_text, language_family, match_canon, reference_canons
go = Canon(5, "sym", "Ledger", (("internal/files/nextcloud.go", "Ledger"),),
"type Ledger struct {", _norm_text("x"), 0, "go")
assert language_family("go") == "go"
body = "def sync(path):\n return Ledger(path)\n"
assert reference_canons("sym", "tests/test_x.py", "sync", body, [go]) == []
assert match_canon("sym", "a.vue", "x", "function x() {", "new Ledger()", [go]) is None
assert reference_canons("sym", "internal/sync/run.go", "Sync", "l := Ledger(p)", [go]) == [5]
def test_a_canon_in_another_project_is_never_used_by_name():
from grimoire.services.shape_ledger import Canon, _norm_text, match_canon, reference_canons
steward = Canon(6, "sym", "run_control", (("steward/ctl.py", "run_control"),),
"def run_control(cmd):", _norm_text("x"), 30, "python")
body = "def go():\n run_control(['ls'])\n"
assert reference_canons("sym", "src/a.py", "go", body, [steward], project_id=2) == []
assert match_canon("sym", "src/a.py", "go", "def go():", body, [steward], project_id=2) is None
assert reference_canons("sym", "src/a.py", "go", body, [steward], project_id=30) == [6]
def test_a_name_the_file_defines_itself_resolves_there():
"""A test module's own `run_helper` is not a call of another file's
canon of that name; in the canon's own file it is."""
from grimoire.services.shape_ledger import Canon, _norm_text, reference_canons
canon = Canon(7, "sym", "run_helper", (("src/tools.py", "run_helper"),),
"def run_helper(x):", _norm_text("x"), 2, "python")
body = "def test_a():\n run_helper(1)\n"
local = frozenset({"run_helper", "test_a"})
assert reference_canons("sym", "tests/test_a.py", "test_a", body, [canon], local_names=local) == []
assert reference_canons("sym", "src/tools.py", "caller", body, [canon], local_names=local) == [7]
assert reference_canons("sym", "tests/test_b.py", "test_a", body, [canon]) == [7]
def test_a_plain_word_counts_only_where_it_is_called():
"""`file`, `seen`: everyone's variable. `seal(...)` is still a call."""
from grimoire.services.shape_ledger import Canon, _norm_text, reference_canons
seen = Canon(8, "sym", "seen", (("src/tree.py", "seen"),), "def seen(f):", _norm_text("x"), 2, "python")
assert reference_canons("sym", "src/a.py", "walk", "if path in seen:\n return", [seen]) == []
assert reference_canons("sym", "src/a.py", "walk", "if seen (f):\n return", [seen]) == [8]
# A leading underscore does not get a generic verb past the stoplist.
run = Canon(9, "sym", "_run", (("src/x.py", "_run"),), "def _run(cmd):", _norm_text("x"), 2, "python")
assert reference_canons("sym", "src/a.py", "go", "_run(['ls'])", [run]) == []
def test_reason_codes_are_a_fixed_catalogue_and_validated():
"""An optional index beside the prose reason; unknown codes are a
structural error (the batch applies nothing)."""