From 7ad2b0dafaac3a222100c5db2a3ea7f4c8b6fdb0 Mon Sep 17 00:00:00 2001
From: safishamsi
Date: Mon, 6 Jul 2026 17:05:05 +0100
Subject: [PATCH 1/3] docs(readme): reorder badges for click-through; split
into top strip + footer
Lead with what keeps a first-time visitor moving toward install/community
instead of a terminal exit. Top strip trimmed 9 -> 5: PyPI, Downloads, CI,
Discord, YC S26 (product -> adoption -> health, then the one converting click,
with YC last: its credibility value is in being seen, not intercepting the
front-door slot). Moved Sponsor, X and the book to a new "Community and links"
footer (higher-intent readers who reach the bottom), added a contextual book
link under "Learn more", and dropped the LinkedIn badge (near-zero value to a
repo visitor; the org profile/site already carry it). Trendshift unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context)
---
README.md | 22 +++++++++++++++-------
1 file changed, 15 insertions(+), 7 deletions(-)
diff --git a/README.md b/README.md
index 1952acbc2..40a488b97 100644
--- a/README.md
+++ b/README.md
@@ -15,15 +15,11 @@
-
-
-
-
-
-
-
+
+
+
Type `/graphify` in your AI coding assistant and it maps your entire project (code, docs, PDFs, images, videos) into a **knowledge graph** you can **query instead of grepping** through files.
@@ -757,6 +753,7 @@ graphify label ./my-project --backend=openai --model gpt-4o # force a specific
- [How it works](docs/how-it-works.md) — the extraction pipeline, community detection, confidence scoring, benchmarks
- [ARCHITECTURE.md](ARCHITECTURE.md) — module breakdown, how to add a language
- [Optional integrations](docs/docker-mcp-sqlite.md) — Docker MCP Toolkit + SQLite
+- [The Memory Layer](https://safishamsi.gumroad.com/l/qetvlo) — the book on the ideas behind graphify, the architecture end to end
---
@@ -828,3 +825,14 @@ See [ARCHITECTURE.md](ARCHITECTURE.md) for module responsibilities and how to ad
+
+---
+
+## Community and links
+
+
+
+
+
+
+
From 8a608d1a99c8c79a249e61306ff092b9d99ec748 Mon Sep 17 00:00:00 2001
From: safishamsi
Date: Mon, 6 Jul 2026 17:07:03 +0100
Subject: [PATCH 2/3] docs(readme): swap CI badge for LinkedIn in the top strip
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Maintainer preference: drop the CI-passing badge from the hero strip and keep
LinkedIn visible up top instead. Top strip stays 5: PyPI, Downloads, Discord,
LinkedIn, YC S26 — trust signals lead, Discord is the converting click, and the
two company/social links group at the tail.
Co-Authored-By: Claude Opus 4.8 (1M context)
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 40a488b97..26143a143 100644
--- a/README.md
+++ b/README.md
@@ -17,8 +17,8 @@
-
+
From 4214d7f4fc7d2b84647f1d649074abcbe793858f Mon Sep 17 00:00:00 2001
From: Farouk Khawaja
Date: Mon, 6 Jul 2026 12:15:43 -0400
Subject: [PATCH 3/3] fix: scale per-term exact/prefix score tiers by squared
label-term coverage (fixes #1602)
In a multi-term query, a single generic term that exactly equals a
short leaf label (query term "home" vs a home() controller action,
"list" vs a list() function) receives _EXACT_MATCH_BONUS x IDF and
outscores every node that matches several of the query's terms by
roughly 10x. _pick_seeds' 20% gap cutoff then discards the genuinely
relevant candidates, so BFS explores the neighborhood of one unrelated
leaf. #1596 mitigated the downstream symptom by guaranteeing one seed
per distinct term; this fixes the root ranking: nothing in _score_nodes
rewarded matching more of the query.
_score_nodes now scales the per-term exact/prefix tier contributions by
squared term coverage: tiered * (matched_terms / total_terms) ** 2.
- Squared, not linear, because the exact tier is 10x the prefix tier;
at linear coverage a 1-of-10-terms exact hit still edges out a
3-of-10-terms prefix+substring match. The regression test pins this
ratio coupling: retuning either tier constant re-fails it.
- Coverage counts label hits (exact/prefix/substring) only. Source-path
hits still score but do not count: a colliding leaf that lives in the
target's own directory (the common case) must not win back its exact
tier via path fragments.
- Substring and source bonuses stay unscaled, as does the full-query
tier (a full-label match already implies full coverage).
- Query tokens are now deduped, order-preserving, as _pick_seeds
already does: a repeated query word must not double-count every tier,
and with coverage scaling it would also inflate the matched ratio.
- Single-term and full-coverage queries are unchanged (coverage == 1),
so identifier lookups keep exact-match dominance
(test_score_nodes_coverage_full_coverage_query_is_unchanged pins the
exact pre-change score).
On a real 5,700-node graph (the #1602 repros): the "home" collision
drops from 6087.9 to 63.2 and the relevant cluster takes rank 1-2, so
seeds recover it inside the gap window instead of relying solely on the
per-term guarantee; the "list" collision drops from 4871.7 to 194.9
with the intended identifier back inside the gap window. Identifier
and well-formed natural-language queries produce identical seeds
before and after.
Known residual, documented in _pick_seeds' docstring: a relevant node
matched only via interior substrings still scores below a dampened
collision (flat tier ~1 x IDF vs exact/n^2), which is exactly why the
#1596 per-term seed guarantee remains load-bearing alongside this.
Adds a regression test reproducing the #1602 second repro (the query
contains the target's literal identifier yet three same-directory
list() leaves outrank it) and a no-change test pinning full-coverage
scores.
tests/test_serve.py (78 tests) and ruff both pass; the full suite's
only failures are environment-specific and reproduce identically on
the unpatched base.
---
graphify/serve.py | 36 +++++++++++++++++++++++---
tests/test_serve.py | 62 +++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 95 insertions(+), 3 deletions(-)
diff --git a/graphify/serve.py b/graphify/serve.py
index 28cb46616..96a1b6d13 100644
--- a/graphify/serve.py
+++ b/graphify/serve.py
@@ -285,7 +285,11 @@ def _trigram_candidates(G: nx.Graph, needles: list[str], *, guard_frac: float =
def _score_nodes(G: nx.Graph, terms: list[str]) -> list[tuple[float, str]]:
scored = []
- norm_terms = [tok for t in terms for tok in _search_tokens(t)]
+ # Dedupe tokens, order-preserving (as _pick_seeds already does): a repeated
+ # query word must not double-count every tier, and with coverage scaling
+ # below it would also inflate the matched-term ratio (#1602).
+ norm_terms = list(dict.fromkeys(tok for t in terms for tok in _search_tokens(t)))
+ n_terms = len(norm_terms)
idf = _compute_idf(G, norm_terms)
# Whole-query string for full-label matching (mirrors _find_node's `term`).
joined = " ".join(norm_terms)
@@ -328,18 +332,38 @@ def _score_nodes(G: nx.Graph, terms: list[str]) -> list[tuple[float, str]]:
or label_tokens.startswith(joined)
):
score += _PREFIX_MATCH_BONUS * 10 * joined_w
+ # Term coverage (#1602): scale the per-term exact/prefix tiers by the
+ # squared fraction of query terms the node's LABEL matches, so a lone
+ # generic word that happens to equal a short label (query term "home"
+ # vs. a home() leaf) cannot bury nodes that match several of the
+ # query's terms. Squaring matters because the exact tier is 10x the
+ # prefix tier: at linear coverage a 1-of-10-terms exact match still
+ # outscores a 3-of-10 prefix+substring match. Single-term and
+ # full-coverage queries are unchanged (coverage == 1), so identifier
+ # lookups keep exact-match dominance. Source-file hits score but do
+ # not count as coverage: a colliding leaf whose directory shares
+ # tokens with the query (common near the intended target) must not
+ # win back its exact tier via path fragments. The substring/source
+ # bonuses and the full-query tier above stay unscaled.
+ matched = 0
+ tiered = 0.0
for t in norm_terms:
w = idf.get(t, 1.0)
# Three-tier precedence: exact > prefix > substring (take the
# strongest tier per term so a single term cannot double-count).
if t == norm_label or t == bare_label:
- score += _EXACT_MATCH_BONUS * w
+ tiered += _EXACT_MATCH_BONUS * w
+ matched += 1
elif norm_label.startswith(t) or bare_label.startswith(t):
- score += _PREFIX_MATCH_BONUS * w
+ tiered += _PREFIX_MATCH_BONUS * w
+ matched += 1
elif t in norm_label:
score += _SUBSTRING_MATCH_BONUS * w
+ matched += 1
if t in source:
score += _SOURCE_MATCH_BONUS * w
+ if tiered:
+ score += tiered * (matched / n_terms) ** 2
if score > 0:
scored.append((score, nid))
# Sort by score desc; break ties toward the shorter label so a concise exact
@@ -378,6 +402,12 @@ def _pick_seeds(
collision cannot starve out the others. Ties within a term are broken by
graph degree (structural centrality), so an isolated incidental match
doesn't out-rank a real, well-connected hub for that term.
+
+ Coverage scaling in _score_nodes (#1602) now dampens a lone collision's
+ exact tier on multi-term queries, which brings label-matching relevant
+ nodes back inside the gap window; this per-term guarantee remains
+ load-bearing for relevant nodes matched only via substrings, whose flat
+ scores a dampened collision can still exceed.
"""
if not scored:
return []
diff --git a/tests/test_serve.py b/tests/test_serve.py
index 2647aa1a8..3f5dd024f 100644
--- a/tests/test_serve.py
+++ b/tests/test_serve.py
@@ -8,6 +8,8 @@
_communities_from_graph,
_score_nodes,
_compute_idf,
+ _EXACT_MATCH_BONUS,
+ _SOURCE_MATCH_BONUS,
_pick_seeds,
_bfs,
_dfs,
@@ -123,6 +125,66 @@ def _add(nid, label, src):
assert scored[0][0] > scored[1][0], "exact label must strictly outrank superset/token-bag matches"
+def test_score_nodes_coverage_lone_generic_exact_hit_loses_to_multi_term_match():
+ """A lone generic-word exact match must not bury a multi-term match.
+
+ Reproduces #1602: in a multi-term query, a single generic term that
+ exactly equals a short leaf label (query term "list" vs a list() function
+ node) received the full exact-tier bonus and outranked every node matching
+ several of the query's terms, even when the query contained the target's
+ literal identifier. The per-term exact/prefix tiers are now scaled by
+ squared term coverage, so a 1-of-5-terms collision drops below a
+ multi-term match. The leaves live in the same directory as the target
+ (the realistic case) to pin that source-path hits do not count as
+ coverage and hand the collision its exact tier back.
+ """
+ G = nx.Graph()
+
+ def _add(nid, label, src):
+ G.add_node(nid, label=label, norm_label=label.lower(),
+ source_file=src, community=0)
+
+ _add("target", "ClientLive.Index", "lib/clients_live/index.ex")
+ _add("form", "ClientLive.Form", "lib/clients_live/form.ex")
+ _add("show", "ClientLive.Show", "lib/clients_live/show.ex")
+ # Same-named tiny leaf functions: "list" == bare label fires the exact
+ # tier. Placed in the target's own directory so their source paths also
+ # substring-match the query term "clients": a path hit must not inflate
+ # the coverage that multiplies the exact tier.
+ for i in range(3):
+ _add(f"leaf{i}", "list()", f"lib/clients_live/helpers{i}.ex")
+ # Filler making "list" a common (low-IDF) token, as in a real graph where
+ # list()/get()/new() style names are ubiquitous.
+ for i in range(24):
+ _add(f"filler{i}", f"shopping list {i}", f"lib/filler{i}.ex")
+
+ # The user pastes the real identifier plus context words; tokenization
+ # yields 5 terms: clientlive, index, clients, list, columns.
+ scored = _score_nodes(G, [t.lower() for t in "ClientLive.Index clients list columns".split()])
+ by_id = {nid: s for s, nid in scored}
+
+ assert scored[0][1] == "target"
+ assert by_id["target"] > by_id["leaf0"], (
+ "a 1-of-5-terms exact collision must not outrank the node matching 3 of 5 terms"
+ )
+
+
+def test_score_nodes_coverage_full_coverage_query_is_unchanged():
+ """Coverage scaling must not touch full-coverage queries (coverage == 1).
+
+ A single-term identifier lookup keeps the exact tier's full magnitude, so
+ `query "FooBarService"` behavior is byte-identical to before #1602.
+ """
+ G = _make_graph()
+ scored = _score_nodes(G, ["extract"])
+ w = _compute_idf(G, ["extract"])["extract"]
+ assert scored[0][1] == "n1"
+ # Full-query exact tier (10x) + per-term exact tier + source hit
+ # ("extract" in "extract.py"), all undampened.
+ expected = (_EXACT_MATCH_BONUS * 10 + _EXACT_MATCH_BONUS + _SOURCE_MATCH_BONUS) * w
+ assert scored[0][0] == pytest.approx(expected)
+
+
def test_find_node_ignores_trailing_punctuation():
G = _make_graph()
assert _find_node(G, "extract?") == ["n1"]