Skip to content

fix(graph2md): register disambiguated slugs and correct line_count for Function/Class/Type#95

Merged
greynewell merged 1 commit intomainfrom
fix-graph2md-slug-and-linecount
Apr 9, 2026
Merged

fix(graph2md): register disambiguated slugs and correct line_count for Function/Class/Type#95
greynewell merged 1 commit intomainfrom
fix-graph2md-slug-and-linecount

Conversation

@greynewell
Copy link
Copy Markdown
Contributor

@greynewell greynewell commented Apr 9, 2026

Summary

  • Slug collision: when a base slug such as fn-handler-go-run was already taken and a -2 suffix was appended to resolve the collision, the new slug was never recorded in usedSlugs. A later node that naturally generated fn-handler-go-run-2 would pass the uniqueness check, receive the same slug, and silently overwrite the first collision-resolved page on disk. The fix is a one-liner: register the disambiguated slug in usedSlugs immediately after computing it.

  • line_count off-by-N for Function/Class/Type nodes: line_count was computed as endLine - startLine + 1 without guarding against startLine == 0 (the sentinel returned by getNum when the property is absent). With startLine == 0 and endLine == 50 the output was line_count: 51 instead of 50. File nodes already defaulted startLine to 1; the three symbol types now do the same via a local effectiveStart variable.

Test plan

  • TestSlugCollisionResolution: three nodes where two naturally produce the same base slug and a third naturally produces the collision-resolved slug — verifies all three output files are distinct.
  • TestLineCountMissingStartLine: function node with endLine=50 and no startLine — verifies line_count: 50 (not 51) in the generated frontmatter.
  • go test ./... — all existing tests pass.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Fixed slug collision handling to properly register disambiguated identifiers and prevent duplicate conflicts.
    • Corrected line count calculation in generated documentation when start line information is missing or invalid.
  • Tests

    • Added integration tests for slug collision resolution and line count edge case handling.

…r Function/Class/Type

Slug collision: when a base slug (e.g. "fn-handler-go-run") was already
taken and a "-2" suffix was appended, the new slug was never recorded in
usedSlugs. A later node naturally producing "fn-handler-go-run-2" would
pass the uniqueness check and get the same slug, causing one output file
to silently overwrite the other. Fixed by marking the resolved slug used.

line_count: Function, Class, and Type frontmatter computed
endLine - startLine + 1 without guarding against startLine == 0 (the
sentinel for "not provided"). This produced line_count = endLine + 1
instead of endLine. File nodes already defaulted startLine to 1;
the three symbol node types now do the same.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 9, 2026

Walkthrough

This change adds collision-resistant slug generation and fixes line-count calculations for nodes with missing start-line values. Two new tests verify both the slug disambiguation strategy and the line-count defaulting behavior.

Changes

Cohort / File(s) Summary
Core Logic Improvements
internal/archdocs/graph2md/graph2md.go
Enhanced slug collision handling to register disambiguated slugs, preventing subsequent collisions. Fixed line_count frontmatter calculation to default missing startLine values to 1, using endLine-effectiveStart+1 instead of endLine-startLine+1.
Test Coverage
internal/archdocs/graph2md/graph2md_test.go
New integration tests for slug collision resolution and line-count calculation with missing start-line values, validating unique file outputs and correct count derivation.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~18 minutes

Suggested reviewers

  • jonathanpopham

Poem

🏷️ Slugs once collided, now they dance apart
Each registered name gets its unique start
Line counts default when beginnings are lost
Tests guard the logic—no silent cost
✨ Collisions resolved, precision refined

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the two main fixes in the changeset: slug collision handling and line_count calculation for symbol types.
Description check ✅ Passed The description provides clear explanations of both bugs, their fixes, and concrete test cases, though it uses a checklist format rather than the template's structured sections.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-graph2md-slug-and-linecount

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/archdocs/graph2md/graph2md_test.go`:
- Around line 124-127: The test currently discards the error returned by
os.ReadDir when assigning entries; change the call in graph2md_test.go to
capture the error (e.g., entries, err := os.ReadDir(outDir)) and immediately
fail the test if err != nil (e.g., if err != nil { t.Fatalf("ReadDir(%q) failed:
%v", outDir, err) }) before asserting len(entries); update the t.Fatalf
assertion to run only after the error check so filesystem/setup errors are
surfaced instead of hidden.

In `@internal/archdocs/graph2md/graph2md.go`:
- Around line 373-377: The current logic that disambiguates duplicate slugs by
doing slug = fmt.Sprintf("%s-%d", slug, n+1) and immediately registering that
new slug in usedSlugs can still collide if a naturally-created slug already
equals that candidate; update the code in the block that manipulates
usedSlugs/slug (the disambiguation logic using fmt.Sprintf and the usedSlugs
map) to loop/increment until you find a truly unused candidate (e.g., compute
candidate := fmt.Sprintf("%s-%d", base, i) and check usedSlugs[candidate] in a
for loop), then set usedSlugs[base] = i and usedSlugs[candidate] = 1 for the
final unique slug; ensure you reference and update both the base count and the
final slug key rather than assuming a single step is sufficient.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c3360408-e6bc-48eb-90df-9fdda497876e

📥 Commits

Reviewing files that changed from the base of the PR and between 61c5888 and 4545445.

📒 Files selected for processing (2)
  • internal/archdocs/graph2md/graph2md.go
  • internal/archdocs/graph2md/graph2md_test.go

Comment on lines +124 to +127
entries, _ := os.ReadDir(outDir)
if len(entries) != 1 {
t.Fatalf("expected 1 output file, got %d", len(entries))
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Don’t discard ReadDir errors in the test.

At Line 124, ignoring the error can hide filesystem/setup failures and make assertions noisy.

Suggested fix
-	entries, _ := os.ReadDir(outDir)
+	entries, err := os.ReadDir(outDir)
+	if err != nil {
+		t.Fatalf("ReadDir: %v", err)
+	}
 	if len(entries) != 1 {
 		t.Fatalf("expected 1 output file, got %d", len(entries))
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
entries, _ := os.ReadDir(outDir)
if len(entries) != 1 {
t.Fatalf("expected 1 output file, got %d", len(entries))
}
entries, err := os.ReadDir(outDir)
if err != nil {
t.Fatalf("ReadDir: %v", err)
}
if len(entries) != 1 {
t.Fatalf("expected 1 output file, got %d", len(entries))
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/archdocs/graph2md/graph2md_test.go` around lines 124 - 127, The test
currently discards the error returned by os.ReadDir when assigning entries;
change the call in graph2md_test.go to capture the error (e.g., entries, err :=
os.ReadDir(outDir)) and immediately fail the test if err != nil (e.g., if err !=
nil { t.Fatalf("ReadDir(%q) failed: %v", outDir, err) }) before asserting
len(entries); update the t.Fatalf assertion to run only after the error check so
filesystem/setup errors are surfaced instead of hidden.

Comment on lines 373 to 377
if n, ok := usedSlugs[slug]; ok {
usedSlugs[slug] = n + 1
slug = fmt.Sprintf("%s-%d", slug, n+1)
usedSlugs[slug] = 1 // register the disambiguated slug so a later node can't claim it
} else {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Handle secondary collisions when the -N candidate is already taken.

At Line 375, the code picks base-(n+1) directly. If that slug already exists (from a naturally-generated slug seen earlier), this still reuses the same filename and can overwrite output.

Suggested fix
-		slug := generateSlug(node, primaryLabel)
+		baseSlug := generateSlug(node, primaryLabel)
+		slug := baseSlug
 		if slug == "" {
 			continue
 		}
 
 		// Handle slug collisions
-		if n, ok := usedSlugs[slug]; ok {
-			usedSlugs[slug] = n + 1
-			slug = fmt.Sprintf("%s-%d", slug, n+1)
-			usedSlugs[slug] = 1 // register the disambiguated slug so a later node can't claim it
+		if n, ok := usedSlugs[baseSlug]; ok {
+			i := n + 1
+			for {
+				candidate := fmt.Sprintf("%s-%d", baseSlug, i)
+				if _, exists := usedSlugs[candidate]; !exists {
+					slug = candidate
+					usedSlugs[baseSlug] = i
+					break
+				}
+				i++
+			}
 		} else {
-			usedSlugs[slug] = 1
+			usedSlugs[baseSlug] = 1
 		}
+		usedSlugs[slug] = 1
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/archdocs/graph2md/graph2md.go` around lines 373 - 377, The current
logic that disambiguates duplicate slugs by doing slug = fmt.Sprintf("%s-%d",
slug, n+1) and immediately registering that new slug in usedSlugs can still
collide if a naturally-created slug already equals that candidate; update the
code in the block that manipulates usedSlugs/slug (the disambiguation logic
using fmt.Sprintf and the usedSlugs map) to loop/increment until you find a
truly unused candidate (e.g., compute candidate := fmt.Sprintf("%s-%d", base, i)
and check usedSlugs[candidate] in a for loop), then set usedSlugs[base] = i and
usedSlugs[candidate] = 1 for the final unique slug; ensure you reference and
update both the base count and the final slug key rather than assuming a single
step is sufficient.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant