fix(suggest-packages): name a member by its path under the package (#446) - #447
Conversation
) Members rendered as their last FQN segment, so `p/sub/` and `p/sub/sub.py` both printed as `sub` — in two different communities. A reader told to split the package could not tell which one belonged where, and the same collision made a bridge endpoint point at the wrong file. `pkg/sub/sub.py` is an ordinary layout; cgis's own `query/drift/drift.py` is one. Members are now named by their path under the analysed prefix, which is unique because full FQNs are, and no longer than the ambiguity requires. The graph itself was never wrong — it is keyed on full FQNs, 27 files and 27 distinct on `cgis.query`, so Q = 0.492 was computed over a proper partition. The issue as first filed claimed otherwise; that claim came from reading the rendered table instead of `build_file_graph`, and is corrected on the issue. A side effect worth noting: the readable output makes the *other* half of #446 visible rather than hidden. `analysis` now plainly sits alone in one community while `analysis.analyzer`, `analysis.anomaly` and `analysis.health` sit in another — a sub-package as a peer of its own contents. Whether membership should be the package's direct children is a modelling decision to take with numbers, and stays open. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Z1HtHgNsrKkQoc7wN6xeG
There was a problem hiding this comment.
Code Review
This pull request replaces the _leaf function with _member_name to prevent name collisions in package reports when multiple files or sub-packages share the same trailing segment, and adds corresponding unit tests. The reviewer identified a potential name collision when the analyzed package itself is a node in the graph (i.e., fqn == prefix), which would cause _member_name to return the same name as a sub-module of the same name (e.g., p and p.p). A code suggestion was provided to explicitly handle fqn == prefix by returning "init".
| if not prefix or not fqn.startswith(f"{prefix}."): | ||
| return fqn.rsplit(".", 1)[-1] | ||
| return fqn[len(prefix) + 1 :] |
There was a problem hiding this comment.
When the analyzed package itself is a node in the graph (i.e., fqn == prefix, representing the package's __init__.py file), fqn.startswith(f"{prefix}.") is False. As a result, _member_name falls back to returning the last segment of the prefix (e.g., "drift" for "cgis.query.drift").
However, if there is also a sub-module with the same name as the package (e.g., "cgis.query.drift.drift", which is extremely common in Python layouts), _member_name will return "drift" for it as well. This creates a name collision between the package root and the sub-module, which defeats the purpose of this PR.
To prevent this collision, we should explicitly handle the case where fqn == prefix and return "__init__" (or another distinct placeholder) to represent the package root.
Here is a test case that demonstrates this collision:
def test_package_root_and_submodule_collision(tmp_path: Path) -> None:
# p is the package root (e.g., representing p/__init__.py)
# p.p is a module inside it (e.g., representing p/p.py)
files = [make_file_node("p"), make_file_node("p.p")]
edges = [make_import_edge("p", "p.p")]
db = _store_with(tmp_path, files, edges)
report = suggest_packages(db, prefix="p", with_calls=False)
rendered = [f for community in report.communities for f in community.files]
assert len(rendered) == len(set(rendered)), f"Colliding names found: {rendered}"
assert "__init__" in rendered
assert "p" in rendered| if not prefix or not fqn.startswith(f"{prefix}."): | |
| return fqn.rsplit(".", 1)[-1] | |
| return fqn[len(prefix) + 1 :] | |
| if fqn == prefix: | |
| return "__init__" | |
| if not prefix or not fqn.startswith(f"{prefix}."): | |
| return fqn.rsplit(".", 1)[-1] | |
| return fqn[len(prefix) + 1 :] |
… review)
Naming members relative to the prefix left the analysed package itself — where
`fqn == prefix` — falling through to the last-segment fallback, which
reintroduced the collision one level down. Real case, measured:
cgis suggest-packages cgis.query.drift
members: ['_scc', 'drift', 'drift', 'drift_service', ...] duplicated: ['drift']
for `drift/__init__.py` and `drift/drift.py`. It renders as `__init__` now, the
file it stands for, which cannot clash: a nested `__init__.py` has its id folded
into its own package's name, so only the root ever produces it.
Same shape as the bug this PR fixes, one level down — which is a fair comment on
the first fix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Z1HtHgNsrKkQoc7wN6xeG
|
Taken, and it is a fair comment on my own fix: the same bug, one level down. Naming members relative to the prefix left the analysed package's own node — where
Suite 2372 passed, 2 skipped, 1 xfailed. |
…er name (#447 review) Two findings, and fixing the first showed the previous two attempts had each closed one shape and left another. **`__init__` was the wrong name, twice over.** It is a Python filename, while this tool reads FILE nodes in any language and the TypeScript extractor folds `/index` exactly as the Python one folds `/__init__` — a TS package would have listed a file that exists nowhere. And it did not resolve: every other member satisfies `prefix + "." + name == node id`, so an agent can read a community, pick a member and ask about it, but `prefix + ".__init__"` names nothing. This tool is MCP-facing; one unresolvable member per report is a real cost. Rendering the package's own node as its FQN instead looked right and was not: with prefix `p`, the root renders `p` and a module `p.p` renders `p` too. Same collision, third shape. So uniqueness is now decided once over the whole file set rather than argued per name: relative paths under the prefix, the package's own node by its FQN, and if those still clash — only possible when a module is named exactly after its package — the whole report falls back to full FQNs. Degrading the entire table keeps one rule visible instead of one row spelled unlike its neighbours for a reason the reader cannot see. Every name still maps back to a node id. **The shipped contract said something else.** `Community` and `Bridge` docstrings, and `docs/specs/2026-06-13-suggest-packages-design.md:226`, all still described members as "the last FQN segment". Those three are what an integrator sizes fields and matches names against, and they had been false since the first commit on this branch. Corrected. Dogfooded at HEAD: `cgis.query` 27/27 distinct, `cgis.query.drift` 9/9, `cgis.guardian` 26/26, no duplicates anywhere. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Z1HtHgNsrKkQoc7wN6xeG
|
Both taken. The first one made it clear that my previous two attempts had each closed one shape and left another — so the fix now decides uniqueness over the whole set instead of arguing it per name.
Rendering the root as its own FQN — my first replacement — was also wrong, and I caught it only because I ran it: with prefix That is what changed the approach. The stale contract — Dogfooded at HEAD: |
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request updates the package suggestion service to use unique display names (paths under the analyzed package or full FQNs) instead of just the last FQN segment, preventing name collisions in communities and bridges. It introduces the _member_names helper function to resolve these names uniquely and adds comprehensive unit tests to verify the behavior. The review feedback suggests simplifying the nested ternary conditional expression in _member_names by handling the empty prefix case upfront, which would significantly improve readability.
| relative = { | ||
| fid: fid if fid == prefix else fid[len(prefix) + 1 :] if prefix else fid for fid in file_ids | ||
| } | ||
| if len(set(relative.values())) == len(relative): | ||
| return relative | ||
| return {fid: fid for fid in file_ids} |
There was a problem hiding this comment.
The dictionary comprehension in _member_names uses a nested ternary conditional expression (fid if fid == prefix else fid[len(prefix) + 1 :] if prefix else fid) which is difficult to read and contains a redundant check on prefix for every element. Since prefix is constant for the function call, we can handle the empty/falsy prefix case upfront. This simplifies the comprehension significantly and improves readability.
if not prefix:
return {fid: fid for fid in file_ids}
relative = {
fid: fid if fid == prefix else fid[len(prefix) + 1 :]
for fid in file_ids
}
if len(set(relative.values())) == len(relative):
return relative
return {fid: fid for fid in file_ids}…447 review) The nested ternary re-tested `prefix` for every element, though it is constant for the call. Handled once up front, which also lets the collision fallback and the empty-prefix case share one `absolute` map instead of building it twice. Behaviour identical, checked rather than assumed: 2373 passed, and the dogfood numbers are unchanged — cgis.query 27/27 distinct, cgis.query.drift 9/9, cgis.guardian 26/26. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Z1HtHgNsrKkQoc7wN6xeG
|
Taken — and this round is readability only, no correctness findings, which after a round that turned up two real ones is the convergence signal. The nested ternary did re-test Behaviour checked rather than asserted: 2373 passed, and the dogfood numbers are unchanged — |
|



Addresses the rendering half of #446.
The bug
Members were rendered as their last FQN segment, so
p/sub/andp/sub/sub.pyboth printed assub— in two different communities. The report tells a maintainer to split a package, and they cannot tell whichsubbelongs where. The same collision made a bridge endpoint name the wrong file.cgis's own tree has one:
query/drift/holdingquery/drift/drift.py.pkg/sub/sub.pyis an ordinary Python layout.Members are now named by their path under the analysed prefix — unique because full FQNs are, and no longer than the ambiguity requires:
A correction to the issue as I filed it
I wrote that the partition was non-disjoint and modularity therefore meaningless. That was wrong.
build_file_graphkeys on full FQNs — 27 files, 27 distinct oncgis.query— soQ = 0.492was computed over a proper partition all along. The duplication lived one layer away, in a display function.I inferred the computation from its rendering without reading it. Corrected on the issue, and recorded here because the PR would otherwise carry the same false claim.
What the fix made visible
Readable names expose the other half of #446 rather than hiding it.
analysisnow plainly sits alone in one community whileanalysis.analyzer,analysis.anomalyandanalysis.healthsit in another — a sub-package rendered as a peer of its own contents. That is a modelling question (should membership be the package's direct children?), it moves Q legitimately, and it stays open for a decision taken with numbers on the #179 corpus.Verification
or Truethat could not fail, and then passed under the old rendering for the wrong reason —subhappened to be a valid member name. It now asserts that the bridge touchingp.sub.subnames itsub.sub, and was checked red against the old rendering and green against the new.make format && make lint && make type-check && make doc-coverage— pass.🤖 Generated with Claude Code
https://claude.ai/code/session_018Z1HtHgNsrKkQoc7wN6xeG