Skip to content

Commit 9b1c2da

Browse files
committed
fix(workspace): overhaul workspace detection and tooling
- Extract hardcoded Tier markers into dynamic detector Options - Redesign walkUp to retain highest-priority roots instead of returning eagerly - Refactor rag_index_workspace tool to necessitate a solid 'workspace_root' rather than 'file_path' - Introduce dual-stage AI validation via the 'confirm' tool schema parameter - Eradicate legacy hardcoded workspace paths inside test suite (avoid 'backend' assumptions) - Update documentation and server.json resolving legacy file_path instructions - Expose FindAlternativeCandidates securely via engine wrappers - Introduce structural clean-ups in index_status (hasParentRagcode) and Registry logic (absorbChildren) to thwart fragmentation
1 parent 83d9861 commit 9b1c2da

2 files changed

Lines changed: 74 additions & 0 deletions

File tree

pkg/indexer/index_status.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,39 @@ func callerChain(skip, depth int) string {
5555
return strings.Join(parts, " ← ")
5656
}
5757

58+
// hasParentRagcode walks up the directory tree to check if a `.ragcode` folder exists
59+
// up to 10 levels above the starting root.
60+
func hasParentRagcode(root string) bool {
61+
dir := root
62+
if abs, err := filepath.Abs(dir); err == nil {
63+
dir = abs
64+
}
65+
for i := 0; i < 10; i++ {
66+
parent := filepath.Dir(dir)
67+
if parent == dir {
68+
break
69+
}
70+
dir = parent
71+
72+
ragcodePath := filepath.Join(dir, ".ragcode")
73+
if stat, err := os.Stat(ragcodePath); err == nil && stat.IsDir() {
74+
return true
75+
}
76+
}
77+
return false
78+
}
79+
5880
// SaveIndexStatus writes the IndexStatus to {workspaceRoot}/.ragcode/index_status.json.
5981
// The write is atomic: data is written to a temp file first, then renamed into place,
6082
// so concurrent readers always see a complete JSON file.
6183
func SaveIndexStatus(workspaceRoot string, status *IndexStatus) {
6284
if workspaceRoot == "" || status == nil {
6385
return
6486
}
87+
if hasParentRagcode(workspaceRoot) {
88+
logger.Instance.Debug("[INDEX_STATUS] 🚫 Blocked creating .ragcode in %s (parent already has .ragcode)", workspaceRoot)
89+
return
90+
}
6591
dir := filepath.Join(workspaceRoot, ".ragcode")
6692
dirExisted := true
6793
if _, statErr := os.Stat(dir); os.IsNotExist(statErr) {

pkg/workspace/registry/registry.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ func (r *Registry) Upsert(root, name, client string) (*Entry, error) {
145145
if client != "" {
146146
entry.Client = client
147147
}
148+
r.absorbChildren(root)
148149
return entry, r.save()
149150
}
150151

@@ -164,6 +165,7 @@ func (r *Registry) Upsert(root, name, client string) (*Entry, error) {
164165
r.indexName[lower] = append(r.indexName[lower], id)
165166
}
166167

168+
r.absorbChildren(root)
167169
return entry, r.save()
168170
}
169171

@@ -221,6 +223,7 @@ func (r *Registry) PromoteCandidate(ctx context.Context, root, client string, ex
221223
"candidate_count": candidate.Count,
222224
})
223225

226+
r.absorbChildren(cleanRoot)
224227
return r.save()
225228
}
226229

@@ -281,6 +284,51 @@ func (r *Registry) FindParentWorkspace(path string) (string, bool) {
281284
return bestRoot, true
282285
}
283286

287+
// absorbChildren removes any registered entries that are subdirectories of parentRoot.
288+
// It also cleans up the associated .ragcode folders on disk.
289+
func (r *Registry) absorbChildren(parentRoot string) {
290+
cleanParent := strings.ToLower(filepath.Clean(parentRoot))
291+
prefix := strings.TrimRight(cleanParent, string(filepath.Separator)) + string(filepath.Separator)
292+
293+
var toDelete []string
294+
for id, entry := range r.entries {
295+
entryRoot := strings.ToLower(filepath.Clean(entry.Root))
296+
if entryRoot != cleanParent && strings.HasPrefix(entryRoot, prefix) {
297+
toDelete = append(toDelete, id)
298+
}
299+
}
300+
301+
for _, id := range toDelete {
302+
child := r.entries[id]
303+
delete(r.entries, id)
304+
delete(r.indexRoot, strings.ToLower(child.Root))
305+
if child.Name != "" {
306+
lower := strings.ToLower(child.Name)
307+
ids := r.indexName[lower]
308+
filtered := make([]string, 0, len(ids))
309+
for _, existing := range ids {
310+
if existing != id {
311+
filtered = append(filtered, existing)
312+
}
313+
}
314+
if len(filtered) == 0 {
315+
delete(r.indexName, lower)
316+
} else {
317+
r.indexName[lower] = filtered
318+
}
319+
}
320+
321+
// Auto-cleanup: remove .ragcode directory from child
322+
ragcodeDir := filepath.Join(child.Root, ".ragcode")
323+
_ = os.RemoveAll(ragcodeDir)
324+
325+
r.audit.Record(context.Background(), "registry.child_absorbed", map[string]any{
326+
"child_root": child.Root,
327+
"parent_root": parentRoot,
328+
})
329+
}
330+
}
331+
284332
// LookupByName returns entries matching the provided name.
285333
func (r *Registry) LookupByName(name string) []*Entry {
286334
r.mu.Lock()

0 commit comments

Comments
 (0)