fix(init): case-insensitive project dedup across manifest and convo sources

`discover_entities` was deduping the convo_scanner results against the
manifest/git scan with a case-sensitive key, while every other dedup
path in the pipeline (`_merge_detected`, `miner.add_to_known_entities`)
uses case-insensitive matching. A project named `foo` in a manifest
plus `Foo` as a Claude Code `cwd` variant would surface as two review
entries instead of collapsing to one.

Fix keys `by_name` by `name.lower()` while preserving the first-seen
casing, matching the rest of the pipeline. Flagged by Copilot on #1175.

Regression test asserts a manifest project + a CamelCase-variant convo
cwd for the same real project collapse to one entry.
This commit is contained in:
Igor Lins e Silva
2026-04-24 14:11:54 -03:00
parent 19ce58c143
commit 55c83e9f3d
2 changed files with 38 additions and 5 deletions
+10 -5
View File
@@ -627,13 +627,18 @@ def discover_entities(
root_path = Path(project_dir).expanduser().resolve()
if is_claude_projects_root(root_path):
convo_projects = scan_claude_projects(root_path)
# Dedup by name against the git-manifest list, preferring entries with
# more user_commits as signal strength.
by_name: dict[str, ProjectInfo] = {p.name: p for p in projects}
# Dedup by name against the git-manifest list, preferring entries
# with more user_commits as signal strength. Keyed case-insensitively
# so a `pyproject.toml` name like `mempalace` and a Claude Code
# `cwd` variant like `MemPalace` collapse into one entry — matches
# the case-insensitive dedup used in `_merge_detected` and
# `miner.add_to_known_entities`.
by_name: dict[str, ProjectInfo] = {p.name.lower(): p for p in projects}
for cp in convo_projects:
existing = by_name.get(cp.name)
key = cp.name.lower()
existing = by_name.get(key)
if existing is None or cp.user_commits > existing.user_commits:
by_name[cp.name] = cp
by_name[key] = cp
projects = sorted(
by_name.values(),
key=lambda p: (not p.is_mine, -p.user_commits, -p.total_commits, p.name),