Skip to content

Commit e68c9e9

Browse files
committed
fix(project-registry): surface normalized-key collisions instead of silent first-wins
Code-review hardening: the resolver index keeps the first entry on a normalized-form clash, so a second project normalizing to the same form would silently mis-resolve. Detect clashes during index build and emit them under warnings.normalized_key_collisions (live registry has none today). Adds 2 tests.
1 parent a3b7881 commit e68c9e9

2 files changed

Lines changed: 32 additions & 1 deletion

File tree

src/project_registry.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,9 +305,23 @@ def build_project_registry(
305305

306306
by_key = {e.canonical_key: e for e in entries}
307307
index: dict[str, _Entry] = {}
308+
collisions: list[dict] = []
308309
for entry in entries:
309310
for form in entry.matchset:
310-
index.setdefault(form, entry)
311+
existing = index.get(form)
312+
if existing is None:
313+
index[form] = entry
314+
elif existing is not entry:
315+
# Two distinct projects normalize to the same form: the index
316+
# keeps the first (stable), so the second would mis-resolve.
317+
# Surface it as a warning rather than failing silently.
318+
collisions.append(
319+
{
320+
"normalized_form": form,
321+
"kept": existing.canonical_key,
322+
"shadowed": entry.canonical_key,
323+
}
324+
)
311325
override_norm = {normalize(raw): key for raw, key in overrides.items()}
312326

313327
def resolve_entry(raw: str) -> _Entry | None:
@@ -395,6 +409,7 @@ def resolve_entry(raw: str) -> _Entry | None:
395409
"notion_local": sorted(notion_orphans),
396410
"notion_pageid_map": sorted(pageid_unmatched),
397411
},
412+
"warnings": {"normalized_key_collisions": collisions},
398413
}
399414

400415

tests/test_project_registry.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,22 @@ def test_build_attaches_external_sources(tmp_path: Path):
159159
assert by_key["PortfolioCommandCenter"]["notion_local_title"] is None
160160

161161

162+
def test_normalized_key_collision_is_surfaced_not_silent():
163+
# Two distinct projects whose display names normalize to the same form.
164+
colliding = _snapshot(
165+
_ident("NetMapper", "Net Mapper", "saagpatel/NetMapper"),
166+
_ident("NetworkMapperAlt", "NetMapper", "saagpatel/NetworkMapperAlt"),
167+
)
168+
registry = build_project_registry(colliding, overrides_config_path=None)
169+
collisions = registry["warnings"]["normalized_key_collisions"]
170+
assert any(c["normalized_form"] == "netmapper" for c in collisions)
171+
172+
173+
def test_real_snapshot_shape_has_no_collisions_block_when_clean():
174+
registry = build_project_registry(SNAPSHOT, overrides_config_path=None)
175+
assert registry["warnings"]["normalized_key_collisions"] == []
176+
177+
162178
def test_scoring_pageids_attach_to_matching_entries():
163179
registry = build_project_registry(
164180
SNAPSHOT,

0 commit comments

Comments
 (0)