Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions codebase_rag/constants/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -1648,6 +1648,21 @@ class AuditCheck(StrEnum):
KEY_ANCHOR_STATE = "anchor_state"
KEY_MENTIONS = "mentions"
KEY_MENTION_QNS = "mention_qns"
# Each mention's anchors, recorded at write time beside `mention_qns` and in
# its order, so a renamed or moved mention is followed the way the subject is
# (issue #3230). An empty string stands for "none": the wire carries string
# lists, not lists with nulls. `mentions_lost` names the mentions the repair
# could not place; they stay in `mention_qns` with no edge.
KEY_MENTION_HASHES = "mention_hashes"
KEY_MENTION_QUOTES = "mention_quotes"
KEY_MENTIONS_LOST = "mentions_lost"
KEY_ATTACHED = "attached"
KEY_ATTACH_QNS = "attach_qns"
KEY_ATTACH_KEYS = "attach_keys"
KEY_KEY_SEPARATOR = "key_separator"
# Joins a followed mention's new name to the hash it was followed by, in one
# string the write compares as a whole; a qualified name never holds it.
GLOSS_MENTION_KEY_SEPARATOR = "\x1f"
KEY_ANCHOR_HASH = "anchor_hash"
# Written by the repair tiers (issue #1808, stage four). `moved_from` is the
# name a MOVED gloss was written against, kept so the move stays visible;
Expand Down
62 changes: 56 additions & 6 deletions codebase_rag/cypher_queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -1151,6 +1151,8 @@ def build_missing_rel_endpoints_query(
g.anchor_quote = $anchor_quote, g.anchor_prefix = $anchor_prefix,
g.anchor_suffix = $anchor_suffix,
g.write_id = $write_id, g.mention_qns = $mention_qns,
g.mention_hashes = $mention_hashes, g.mention_quotes = $mention_quotes,
g.mentions_lost = null,
g.project = $project_name, g.moved_from = null, g.candidate_qns = null
WITH g, t, mentioned
OPTIONAL MATCH (g)-[old:{_ANNOTATES}]->(prev)
Expand Down Expand Up @@ -1294,11 +1296,58 @@ def build_missing_rel_endpoints_query(
MERGE (g)-[:{_ANNOTATES}]->(t)"""
CYPHER_GLOSS_MARK = f"""MATCH (g:{_GLOSS} {{qualified_name: $qn}})
SET g.anchor_state = $anchor_state, g.candidate_qns = $candidate_qns"""
CYPHER_REANCHOR_GLOSS_MENTIONS = f"""MATCH (g:{_GLOSS})-[:{_ANNOTATES}]->()
WHERE g.mention_qns IS NOT NULL AND {_GLOSS_OF_SYNCING_PROJECT}
UNWIND g.mention_qns AS mention_qn
MATCH (m {{qualified_name: mention_qn}})
WHERE {_any_label("m", _GRAPH_DEFINITION_LABELS)}
# A note's mentions are placed after its subject (issue #3230), by
# `gloss_repair.repair_mentions`: the attached notes that mention anything,
# with each mention's recorded anchors and the edges it has now; the
# definitions currently under the mentioned names; and one statement per note
# whose mentions changed. A mention is never bound by its name alone once
# that name holds other code and the recorded code is found elsewhere, and
# one that cannot be placed is listed in `mentions_lost` rather than dropped.
CYPHER_GLOSS_MENTION_ANCHORS = f"""MATCH (g:{_GLOSS})-[:{_ANNOTATES}]->()
WITH DISTINCT g
WHERE g.mention_qns IS NOT NULL AND size(g.mention_qns) > 0
OPTIONAL MATCH (g)-[:{_MENTIONS}]->(m)
RETURN g.qualified_name AS qualified_name, g.project AS project,
g.target_qn AS target_qn, g.mention_qns AS mention_qns,
g.mention_hashes AS mention_hashes, g.mention_quotes AS mention_quotes,
g.mentions_lost AS mentions_lost, g.write_id AS write_id,
collect(m.qualified_name) AS attached"""
CYPHER_DEFINITIONS_BY_QNS = f"""UNWIND $qns AS qn
MATCH (t {{qualified_name: qn}})
WHERE {_any_label("t", _GRAPH_DEFINITION_LABELS)}
RETURN t.qualified_name AS qualified_name, t.anchor_hash AS anchor_hash"""
# The note's mentions as the repair placed them, and its MENTIONS edges
# rebuilt to match. A mention kept on its name is bound by `attach_qns`; one
# followed to a new name by `attach_keys`, its name and the hash the pass
# read joined by `key_separator`, so a definition replaced between the read
# and this write is not bound on the strength of its name. The targets are
# matched BEFORE any edge is deleted, and only an edge to a node that is no
# longer a target is deleted: with the definition indexes in place, Memgraph
# 3 lost the edge when one statement deleted it and then merged it again.
# The note must still carry the `write_id` the pass read: an `annotate` that
# landed in between wrote newer mentions, which this result would overwrite.
# And every followed key must still match a target: a definition replaced
# under its new name would otherwise have that name saved without an edge,
# and the next pass would read the newcomer as an in-place edit and bind it.
# Either way nothing is written, and the next sync places the note afresh.
CYPHER_GLOSS_SET_MENTIONS = f"""MATCH (g:{_GLOSS} {{qualified_name: $qn}})
WHERE coalesce(g.write_id, '') = $write_id
OPTIONAL MATCH (m)
WHERE {_any_label("m", _GRAPH_DEFINITION_LABELS)} AND m.qualified_name STARTS WITH $project_prefix
AND (m.qualified_name IN $attach_qns
OR (m.qualified_name + $key_separator + m.anchor_hash) IN $attach_keys)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
WITH g, collect(DISTINCT m) AS targets
WHERE all(key IN $attach_keys WHERE any(target IN targets
WHERE target.qualified_name + $key_separator + target.anchor_hash = key))
SET g.mention_qns = $mention_qns, g.mention_hashes = $mention_hashes,
g.mention_quotes = $mention_quotes, g.mentions_lost = $mentions_lost
WITH g, targets
OPTIONAL MATCH (g)-[old:{_MENTIONS}]->(t)
WHERE NOT t IN targets
WITH g, targets, collect(old) AS stale_edges
FOREACH (edge IN stale_edges | DELETE edge)
WITH g, targets
UNWIND targets AS m
MERGE (g)-[:{_MENTIONS}]->(m)"""
# Staleness (issue #1808): a gloss recorded its subject's `anchor_hash` as
# `target_hash` when it was written; after a sync the two are compared and the
Expand All @@ -1325,7 +1374,8 @@ def build_missing_rel_endpoints_query(
"g.commit_sha AS commit_sha, g.target_qn AS target_qn, "
"g.target_hash AS target_hash, g.anchor_state AS anchor_state, "
"g.moved_from AS moved_from, g.candidate_qns AS candidate_qns, "
"g.write_id AS write_id, collect(m.qualified_name) AS mentions"
"g.write_id AS write_id, g.mentions_lost AS mentions_lost, "
"collect(m.qualified_name) AS mentions"
)
CYPHER_GLOSS_READ = f"""MATCH (g:{_GLOSS} {{qualified_name: $qn}})
OPTIONAL MATCH (g)-[:{_MENTIONS}]->(m)
Expand Down
30 changes: 29 additions & 1 deletion codebase_rag/gloss.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ class GlossRow(TypedDict):
moved_from: str | None
candidate_qns: list[str]
mentions: list[str]
# Mentions whose definition the repair could not place after a rename,
# move or delete: still named, with no MENTIONS edge (issue #3230).
mentions_lost: list[str]


class GlossRefusal(TypedDict, total=False):
Expand Down Expand Up @@ -274,6 +277,25 @@ class _TargetFacts(NamedTuple):
anchor: TextAnchor | None


def _mention_anchor_props(
fetch_all: QueryFn,
project_name: str,
mention_qns: list[str],
read_source: SourceReader | None,
) -> PropertyDict:
# Each mention records the anchors the subject does, in `mention_qns`
# order, so the repair can follow a renamed or moved mention instead of
# dropping it or handing it to a newcomer with the old name (#3230).
facts = [
_target_facts(fetch_all, project_name, qn, read_source) for qn in mention_qns
]
return {
cs.KEY_MENTION_QNS: mention_qns,
cs.KEY_MENTION_HASHES: [f.target_hash or "" for f in facts],
cs.KEY_MENTION_QUOTES: [f.anchor.quote if f.anchor else "" for f in facts],
}


def _target_facts(
fetch_all: QueryFn,
project_name: str,
Expand Down Expand Up @@ -338,6 +360,7 @@ def _gloss_row(row: ResultRow) -> GlossRow:
moved_from=_opt_str(row.get(cs.KEY_MOVED_FROM)),
candidate_qns=_str_list(row.get(cs.KEY_CANDIDATE_QNS)),
mentions=mentions,
mentions_lost=_str_list(row.get(cs.KEY_MENTIONS_LOST)),
)


Expand Down Expand Up @@ -419,7 +442,12 @@ def write_gloss(
cs.KEY_QN: key,
cs.KEY_PROJECT_PREFIX: _prefix(project_name),
cs.KEY_TARGET_QN: target_qn,
cs.KEY_MENTION_QNS: sorted({m["qualified_name"] for m in mentioned}),
**_mention_anchor_props(
fetch_all,
project_name,
sorted({m["qualified_name"] for m in mentioned}),
read_source,
),
cs.KEY_KIND: kind,
cs.KEY_STATUS: cs.GLOSS_STATUS_ACCEPTED,
cs.KEY_BODY: text,
Expand Down
Loading
Loading