Skip to content

fix(cli): prevent noisy directory consolidation - #445

Open
Maicololiveras wants to merge 1 commit into
Gentleman-Programming:mainfrom
Maicololiveras:fix/consolidate-shared-dir-grouping
Open

fix(cli): prevent noisy directory consolidation#445
Maicololiveras wants to merge 1 commit into
Gentleman-Programming:mainfrom
Maicololiveras:fix/consolidate-shared-dir-grouping

Conversation

@Maicololiveras

@Maicololiveras Maicololiveras commented May 28, 2026

Copy link
Copy Markdown

🔗 Linked Issue

Closes #283


🏷️ PR Type

  • type:bug: Bug fix
  • type:feature: New feature
  • type:docs: Documentation only
  • type:refactor: Code refactoring (no behavior change)
  • type:chore: Maintenance, dependencies, tooling
  • type:breaking-change: Breaking change

📝 Summary

  • Prevent projects consolidate --all from grouping unrelated projects through noisy shared directories such as $HOME.
  • Preserve legitimate grouping through small shared-directory sets and project-name similarity.

📂 Changes

File Change
cmd/engram/main.go Skip shared-directory grouping when more than three distinct projects use the directory.
cmd/engram/consolidate_grouping_test.go Cover noisy directories, legitimate shared directories, name similarity, and command-level dry-run behavior.

🧪 Test Plan

  • Focused unit tests pass locally: go test ./cmd/engram -run "Test(GroupSimilarProjects|CmdProjectsConsolidateAllDoesNotMergeNoisySharedDir)" -count=1
  • Full unit suite: go test ./... (not rerun; this Windows host has a known unrelated package-suite failure and timeout)
  • E2E tests: go test -tags e2e ./internal/server/... (not required for this CLI-only grouping change)
  • Scoped diff verified against main: only cmd/engram/main.go and cmd/engram/consolidate_grouping_test.go

🤖 Automated Checks

These run automatically and all must pass before merge:

Check What it verifies Status
Check Issue Reference PR body contains Closes #N / Fixes #N / Resolves #N Pending
Check Issue Has status:approved Linked issue has status:approved label Pending
Check PR Has type: Label* PR has exactly one type:* label Pending
Unit Tests go test ./... passes Pending
E2E Tests go test -tags e2e ./internal/server/... passes Pending

✅ Contributor Checklist

  • I linked an approved issue above (Closes #283)
  • I added exactly one type:* label to this PR
  • I ran unit tests locally: go test ./...
  • I ran e2e tests locally: go test -tags e2e ./internal/server/...
  • Docs updated (not required for this internal grouping safeguard)
  • Commits follow conventional commits format
  • No Co-Authored-By trailers in commits

💬 Notes for Reviewers

This PR is limited to issue #283 bug 4. The --paths-only cleanup belongs to bug 2 and has moved to #647.

Chain Context

Both slices target main and can merge independently.

main
├── 📍 #445: grouping safeguard (bug 4)
└── #647: path-only pruning (bug 2)

Start state: unrelated projects sharing a noisy ancestor directory can collapse into one consolidation group.

End state: noisy directories are ignored while legitimate grouping signals remain active.

Out of scope: path-named project cleanup, write-boundary validation, and unrelated consolidation heuristics.

@Maicololiveras
Maicololiveras force-pushed the fix/consolidate-shared-dir-grouping branch from 4f3a806 to 053923f Compare May 29, 2026 12:30
@Alan-TheGentleman Alan-TheGentleman added the type:bug Bug fix label Jun 13, 2026
@Alan-TheGentleman

Copy link
Copy Markdown
Collaborator

This is linked to the approved #283 scope and looks focused on the remaining consolidate/prune bug. I added the type:bug label so the process gate can run. Next step is code review after the required checks are green.

@github-actions

Copy link
Copy Markdown

This PR has been inactive for 30 days. Please rebase, update it, or leave a comment — otherwise it will be closed in 14 days.

@Alan-TheGentleman Alan-TheGentleman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reopening attention on this — the status:stale label is process drift, not code decay: groupSimilarProjects and cmdProjectsPrune are unchanged on main and the branch is still mergeable.

Your diagnosis is better than the issue's. #283 blamed a loose name-similarity threshold, but the actual collapse is the shared-directory union at cmd/engram/main.go:1946-1950, which unions every project sharing any directory — so one $HOME entry transitively merges the whole store. That matches Group 2 in the report containing <user-home> plus the path-named entries. Skipping directories touched by >3 distinct projects is the right lever, and leaving project.FindSimilar untouched is the right call.

One question before merge:

  • The --paths-only prune flag is #283 bug 2 cleanup, not bug 4. Would you split it into its own PR? It reuses existing prune machinery so it's low-risk, but this PR's title and tests are all about grouping.

Happy to merge the grouping fix as-is if you'd rather keep them together — just say so and I'll drop the request.

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The consolidation grouping heuristic now ignores shared directories associated with too many distinct projects. Tests cover noisy and legitimate directory grouping, name similarity, and dry-run consolidation output.

Changes

Consolidation grouping safeguard

Layer / File(s) Summary
Shared-directory noise filtering
cmd/engram/main.go
Adds a distinct-project counting helper and skips shared-directory unions when the directory exceeds the configured project cap.
Grouping and command regression coverage
cmd/engram/consolidate_grouping_test.go
Adds tests for noisy shared directories, legitimate shared directories, name similarity, and projects consolidate --all --dry-run output.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: gentleman-programming

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The grouping fix and tests address bug 4, but issue #283 also requires path-named project cleanup and other fixes that are not present here. Add the missing path-named project/prune changes or narrow the linked scope to only the consolidate grouping fix in bug 4.
✅ Passed checks (4 passed)
Check name Status Explanation
Out of Scope Changes check ✅ Passed The changes stay focused on consolidate grouping and matching regression tests, with no unrelated code paths introduced.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately captures the main change: preventing noisy shared directories from causing consolidation.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/engram/main.go`:
- Around line 1956-1962: Apply the same maxSharedProjectsForDirMatch noise
filter to the single-project grouping logic near the unchanged matching code,
using distinctProjectCount(idxs) before accepting directory matches. Ensure
noisy directories such as $HOME cannot produce cross-project merge candidates,
and add regression coverage for single-project consolidate mode.
- Around line 1891-1898: Remove the distinctProjectCount helper and replace its
call site in the directory/project statistics flow with len(idxs). Preserve the
existing behavior, relying on the uniqueness contract of
ProjectStats.Directories and sequential dirToProjects population.
🪄 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: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b5429372-7bca-4636-a5bf-734da55d45bf

📥 Commits

Reviewing files that changed from the base of the PR and between b625d77 and 5d006fb.

📒 Files selected for processing (2)
  • cmd/engram/consolidate_grouping_test.go
  • cmd/engram/main.go

Comment thread cmd/engram/main.go
Comment on lines +1891 to +1898
// distinctProjectCount returns the number of unique project indices in idxs.
func distinctProjectCount(idxs []int) int {
seen := make(map[int]struct{}, len(idxs))
for _, i := range idxs {
seen[i] = struct{}{}
}
return len(seen)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Remove unnecessary distinctProjectCount helper.

Since ProjectStats.Directories contains unique directories per project (as per its contract) and dirToProjects is populated by iterating sequentially, index i is appended exactly once per project. This means idxs already contains strictly unique project indices, making len(idxs) identical to the distinct project count.

Allocating a map for every directory to count distinct projects adds unnecessary overhead, especially for noisy directories shared by thousands of projects. Consider removing distinctProjectCount entirely and replacing its usage with len(idxs).

♻️ Proposed refactor
-// distinctProjectCount returns the number of unique project indices in idxs.
-func distinctProjectCount(idxs []int) int {
-	seen := make(map[int]struct{}, len(idxs))
-	for _, i := range idxs {
-		seen[i] = struct{}{}
-	}
-	return len(seen)
-}

And update the call site at line 1960:

-		if distinctProjectCount(idxs) > maxSharedProjectsForDirMatch {
+		if len(idxs) > maxSharedProjectsForDirMatch {
📝 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
// distinctProjectCount returns the number of unique project indices in idxs.
func distinctProjectCount(idxs []int) int {
seen := make(map[int]struct{}, len(idxs))
for _, i := range idxs {
seen[i] = struct{}{}
}
return len(seen)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/engram/main.go` around lines 1891 - 1898, Remove the distinctProjectCount
helper and replace its call site in the directory/project statistics flow with
len(idxs). Preserve the existing behavior, relying on the uniqueness contract of
ProjectStats.Directories and sequential dirToProjects population.

Comment thread cmd/engram/main.go
@Maicololiveras
Maicololiveras force-pushed the fix/consolidate-shared-dir-grouping branch from 5d006fb to 69dba41 Compare July 21, 2026 03:04

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
cmd/engram/main.go (2)

1891-1904: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Duplicate: distinctProjectCount still redundant.

Already flagged in a prior review round — idxs from dirToProjects should already contain unique project indices (assuming ProjectStats.Directories has no duplicate entries per project), making the map allocation per directory unnecessary. This remains unaddressed.

♻️ Proposed refactor
-// distinctProjectCount returns the number of unique project indices in idxs.
-func distinctProjectCount(idxs []int) int {
-	seen := make(map[int]struct{}, len(idxs))
-	for _, i := range idxs {
-		seen[i] = struct{}{}
-	}
-	return len(seen)
-}

And at the call site (line 1965): replace distinctProjectCount(idxs) with len(idxs).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/engram/main.go` around lines 1891 - 1904, Remove the redundant
distinctProjectCount helper and update its call site in the shared-directory
grouping logic to use len(idxs) directly, preserving the existing unique-project
counting behavior.

1961-1967: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Duplicate: single-project mode still unprotected against noisy shared directories.

Already flagged previously as a major issue — this fix only guards the --all grouping path (groupSimilarProjects). The default projects consolidate (single-project) path's shared-directory matching (unchanged, around lines 2075-2090) has no equivalent cap, so a canonical project whose cwd directory includes a noisy ancestor (e.g. $HOME) will still have every project sharing that directory proposed as a merge candidate. Issue #283 bug 4 is only half-fixed. Also, no regression test exists for this remaining single-project-mode gap.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/engram/main.go` around lines 1961 - 1967, The noisy shared-directory cap
currently only protects groupSimilarProjects; apply equivalent
distinctProjectCount and maxSharedProjectsForDirMatch filtering to the
single-project projects consolidate matching path around the canonical project
candidate logic. Ensure shared ancestors such as $HOME cannot produce merge
candidates for unrelated projects, and add a regression test covering
single-project mode.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/engram/consolidate_grouping_test.go`:
- Around line 22-71: Add boundary-focused tests around groupSimilarProjects
using exactly maxSharedProjectsForDirMatch distinct projects sharing one
directory and one additional case with four projects. Assert the three-project
case still forms a group and the four-project case produces no group, preserving
the existing noisy-directory behavior and detecting an off-by-one threshold
check.

---

Duplicate comments:
In `@cmd/engram/main.go`:
- Around line 1891-1904: Remove the redundant distinctProjectCount helper and
update its call site in the shared-directory grouping logic to use len(idxs)
directly, preserving the existing unique-project counting behavior.
- Around line 1961-1967: The noisy shared-directory cap currently only protects
groupSimilarProjects; apply equivalent distinctProjectCount and
maxSharedProjectsForDirMatch filtering to the single-project projects
consolidate matching path around the canonical project candidate logic. Ensure
shared ancestors such as $HOME cannot produce merge candidates for unrelated
projects, and add a regression test covering single-project mode.
🪄 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: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e7eff6b7-572a-4d83-96d1-50f7d375285d

📥 Commits

Reviewing files that changed from the base of the PR and between 5d006fb and 69dba41.

📒 Files selected for processing (2)
  • cmd/engram/consolidate_grouping_test.go
  • cmd/engram/main.go

Comment on lines +22 to +71
// TestGroupSimilarProjects_NoisySharedDirNotGrouped reproduces issue #283 bug 4:
// many unrelated projects that merely share a common parent/root directory
// (e.g. $HOME or a session root) must NOT be unioned into one giant component.
// Before the fix, the shared-directory union step grouped all of them together
// and consolidate would propose merging them into a single canonical project —
// a catastrophic data-loss footgun.
func TestGroupSimilarProjects_NoisySharedDirNotGrouped(t *testing.T) {
// Names are deliberately long and mutually dissimilar (no substring overlap,
// Levenshtein distance well above the scaled threshold) so the ONLY possible
// grouping signal is the shared "/home/user" directory.
const sharedHome = "/home/user"
projects := []store.ProjectStats{
{Name: "kubernetes", ObservationCount: 5, Directories: []string{"/home/user/kubernetes", sharedHome}},
{Name: "photoshop", ObservationCount: 7, Directories: []string{"/home/user/photoshop", sharedHome}},
{Name: "wireguard", ObservationCount: 3, Directories: []string{"/home/user/wireguard", sharedHome}},
{Name: "blender", ObservationCount: 9, Directories: []string{"/home/user/blender", sharedHome}},
{Name: "terraform", ObservationCount: 2, Directories: []string{"/home/user/terraform", sharedHome}},
}

groups := groupSimilarProjects(projects)

// The five names are mutually dissimilar, so the ONLY thing that could group
// them is the shared "/home/user" directory. Since that directory is touched
// by more than maxSharedProjectsForDirMatch distinct projects, it must be
// treated as noise and skipped — yielding no groups at all.
if len(groups) != 0 {
t.Fatalf("expected 0 groups (noisy shared dir must be skipped), got %d: %+v", len(groups), groups)
}
}

// TestGroupSimilarProjects_RealSharedDirStillGroups guards against over-correcting:
// when only a small number of projects share a directory (a genuine rename signal),
// they SHOULD still be grouped.
func TestGroupSimilarProjects_RealSharedDirStillGroups(t *testing.T) {
const sharedRepo = "/repos/shared-monorepo"
projects := []store.ProjectStats{
{Name: "webapp", ObservationCount: 12, Directories: []string{sharedRepo}},
{Name: "legacy-portal", ObservationCount: 4, Directories: []string{sharedRepo}},
}

groups := groupSimilarProjects(projects)

g := groupContaining(groups, "webapp")
if g == nil {
t.Fatalf("expected webapp and legacy-portal to be grouped via shared dir, got groups: %+v", groups)
}
if len(g.Names) != 2 {
t.Fatalf("expected group of 2, got %d: %+v", len(g.Names), g.Names)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add a boundary test at exactly maxSharedProjectsForDirMatch.

Existing tests only check well above (5) and well below (2) the cap of 3; there's no test for exactly 3 (should still group) vs exactly 4 (should be skipped) distinct projects sharing a directory — the precise boundary where an off-by-one in the > comparison would go unnoticed.

As per path instructions, "Verify coverage of happy path, error paths, and edge cases" for **/*_test.go files.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/engram/consolidate_grouping_test.go` around lines 22 - 71, Add
boundary-focused tests around groupSimilarProjects using exactly
maxSharedProjectsForDirMatch distinct projects sharing one directory and one
additional case with four projects. Assert the three-project case still forms a
group and the four-project case produces no group, preserving the existing
noisy-directory behavior and detecting an off-by-one threshold check.

Source: Path instructions

@Maicololiveras Maicololiveras changed the title fix(cli): close #283 bug 4 — tighten consolidate grouping + prune path-named projects fix(cli): prevent noisy directory consolidation Jul 21, 2026
@Maicololiveras

Copy link
Copy Markdown
Author

Agreed, splitting this is the cleaner review boundary. I moved --paths-only into #647 for issue #283 bug 2, and #445 is now grouping-only for bug 4. Please review both when you have a chance.

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

Labels

type:bug Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MCP server fails on multi-repo cwd + 3 related bugs (ambiguity, path-as-name, case-sensitivity, unsafe consolidate)

2 participants