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
23 changes: 21 additions & 2 deletions code_review_graph/communities.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,28 @@ def _split_name(name: str) -> list[str]:
return [p for p in re.split(r"[_\-.\s]+", s) if p]


_SLUG_MAX_LEN = 30


def _to_slug(s: str) -> str:
"""Convert a string to a short lowercase slug."""
return re.sub(r"[^a-z0-9]+", "-", s.lower()).strip("-")[:30]
"""Convert a string to a short lowercase slug.

CamelCase and snake_case inputs are split into hyphenated words, and
truncation happens at a word boundary so a slug never ends mid-word:
``TestBuildPreAnalysisPromptBlock`` becomes
``test-build-pre-analysis-prompt`` rather than
``testbuildpreanalysispromptbloc``.
"""
normalized = re.sub(r"[^A-Za-z0-9]+", " ", s)
slug = "-".join(w.lower() for w in _split_name(normalized))
if len(slug) <= _SLUG_MAX_LEN:
return slug
cut = slug[:_SLUG_MAX_LEN]
if slug[_SLUG_MAX_LEN] != "-":
head, sep, _ = cut.rpartition("-")
if sep:
cut = head
return cut.rstrip("-")


# ---------------------------------------------------------------------------
Expand Down
44 changes: 44 additions & 0 deletions tests/test_communities.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
_compute_cohesion_batch,
_detect_file_based,
_generate_community_name,
_to_slug,
detect_communities,
get_architecture_overview,
get_communities,
Expand Down Expand Up @@ -590,3 +591,46 @@ def test_incremental_detect_redetects_affected(self):
# Pass a file that IS part of existing communities
result = incremental_detect_communities(self.store, ["auth.py"])
assert result > 0


class TestToSlug:
"""Slug generation: word splitting and word-boundary truncation."""

def test_short_snake_case_unchanged(self):
assert _to_slug("rate_sheet") == "rate-sheet"

def test_camel_case_split_into_words(self):
assert _to_slug("AuthService") == "auth-service"

def test_long_camel_case_truncates_at_word_boundary(self):
# Lowercased raw form is 31+ chars; the old implementation produced
# the mid-word cut "testbuildpreanalysispromptbloc".
result = _to_slug("TestBuildPreAnalysisPromptBlock")
assert result == "test-build-pre-analysis-prompt"

def test_truncation_never_ends_mid_word(self):
result = _to_slug("normalize_service_level_values_for_carrier")
assert len(result) <= 30
# Every word in the slug must be a complete word of the input.
input_words = set("normalize_service_level_values_for_carrier".split("_"))
assert set(result.split("-")) <= input_words

def test_exact_boundary_cut_keeps_full_word(self):
# 30th char lands exactly on a hyphen: keep all complete words.
result = _to_slug("aaaaaaaaaa_bbbbbbbbbb_cccccccc_dd")
assert result == "aaaaaaaaaa-bbbbbbbbbb-cccccccc"

def test_mid_word_cut_drops_partial_word(self):
# The 30-char cut lands inside the third word: drop the fragment.
result = _to_slug("aaaaaaaaaa_bbbbbbbbbb_ccccccccc")
assert result == "aaaaaaaaaa-bbbbbbbbbb"

def test_single_overlong_word_falls_back_to_hard_cut(self):
result = _to_slug("a" * 40)
assert result == "a" * 30

def test_punctuation_becomes_word_separator(self):
assert _to_slug("foo/bar.baz") == "foo-bar-baz"

def test_empty_string_gives_empty_slug(self):
assert _to_slug("") == ""