From 3e2227e8c5d23b2ea9a0fc6447d760a6f5dee593 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Mon, 26 Oct 2026 18:29:54 -0400 Subject: [PATCH 1/2] fix(6483): drop the cross-project uses edges the write path wrote Migration 0125 removed the proposer's cross-project `reference` edges, but the write path records its by-name evidence with basis `hook`, so those survived it. A hook edge across projects is impossible for the same reason a reference edge is, and the write path now applies the same by-name test (names_canon), so 0126 removes the ones written before. Judged edges keep their own basis and are untouched. Co-Authored-By: Claude Opus 5.5 --- .../0126_drop_cross_project_hook_uses.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 alembic/versions/0126_drop_cross_project_hook_uses.py diff --git a/alembic/versions/0126_drop_cross_project_hook_uses.py b/alembic/versions/0126_drop_cross_project_hook_uses.py new file mode 100644 index 00000000..03721556 --- /dev/null +++ b/alembic/versions/0126_drop_cross_project_hook_uses.py @@ -0,0 +1,34 @@ +"""drop write-path uses edges that cross projects + +Revision ID: 0126 +Revises: 0125 +Create Date: 2026-10-26 + +The write path records a `uses` edge with basis `hook` when a session pulled a +snippet and then wrote code naming its symbol. That is by-name evidence, like +the proposer's `reference` edges 0125 removed, and a body never calls a +function defined in another project's repository. The write path now applies +the same by-name test, so it writes no such edge; this removes the ones it +wrote before. Judged edges keep their own basis and are left alone. + +Downgrade restores nothing: the removed edges were never facts. +""" +from alembic import op + +revision = "0126" +down_revision = "0125" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.execute( + "DELETE FROM code_shape_uses u USING code_shapes s, notes n " + "WHERE u.shape_id = s.id AND u.snippet_id = n.id " + "AND u.basis = 'hook' " + "AND n.project_id IS NOT NULL AND n.project_id <> s.project_id" + ) + + +def downgrade() -> None: + pass From 2a6d9080badea2972873102cfc921131121f2be0 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Mon, 26 Oct 2026 19:07:03 -0400 Subject: [PATCH 2/2] fix(6477): a plain-word call needs the parenthesis right after the word Read against Fabled Archives after the cleanup: of the 9 rows still linked to snippet #5448 (symbol `file`), all but one matched prose in a comment, "a file (not a directory)", "the file (ScanWaiting". The only real call was `r.file(ctx, ...)`. Formatted code never puts a space before a call's parenthesis and prose does, so the plain-word test is now `word(` with no whitespace. _PROPOSER_VERSION 8 re-reads every row once. Co-Authored-By: Claude Opus 5.5 --- src/grimoire/services/shape_ledger.py | 8 +++++--- tests/test_shape_ledger.py | 4 +++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/grimoire/services/shape_ledger.py b/src/grimoire/services/shape_ledger.py index db17e4f8..7bdb78be 100644 --- a/src/grimoire/services/shape_ledger.py +++ b/src/grimoire/services/shape_ledger.py @@ -1798,7 +1798,7 @@ def _semantic_priority(row) -> tuple: # were examined under, so a tightened rule re-examines everything once. A # change that does not alter what is proposed needs no bump. A stale "miss" # basis on an old row is inert: a miss is not evidence (see _SEMANTIC_LIMIT). -_PROPOSER_VERSION = 7 +_PROPOSER_VERSION = 8 # 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. @@ -1939,7 +1939,9 @@ def names_canon( unless the canon is recorded in this very file; - never a generic verb, with or without a leading underscore; - a bare lowercase word (`file`, `seen`, `seal`) only where it is called - as `word(`, because as a plain word it is everyone's variable. + as `word(`, because as a plain word it is everyone's variable. No space + is allowed before the parenthesis: formatted code never has one, and + prose does ("a file (not a directory)"). Same kind and language family are checked by the callers' loops.""" sym = _norm_symbol(canon.symbol or "") if not sym: @@ -1954,7 +1956,7 @@ def names_canon( if not any(lp and _path_touches(lp, path) for lp, _ls in canon.locations): return False if kind == "sym" and re.fullmatch(r"[a-z]+", sym): - return re.search(rf"(?