Files
claude-mem/src/shared/path-utils.ts
T
Alexander Knigge 182097ef1c fix: resolve path format mismatch in folder CLAUDE.md generation (#794) (#813)
The isDirectChild() function failed to match files when the API used
absolute paths (/Users/x/project/app/api) but the database stored
relative paths (app/api/router.py). This caused all folder CLAUDE.md
files to incorrectly show "No recent activity".

Changes:
- Create shared path-utils module with proper path normalization
- Implement suffix matching strategy for mixed path formats
- Update SessionSearch.ts to use shared utilities
- Update regenerate-claude-md.ts to use shared utilities (was using
  outdated broken logic)
- Prevent spurious directory creation from malformed paths
- Add comprehensive test coverage for path matching edge cases

This is the proper fix for #794, replacing PR #809 which only masked
the bug by skipping file creation when "no activity" was shown.

Co-authored-by: bigphoot <bigphoot@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 15:48:31 -05:00

83 lines
3.1 KiB
TypeScript

/**
* Shared path utilities for CLAUDE.md file generation
*
* These utilities handle path normalization and matching, particularly
* for comparing absolute and relative paths in folder CLAUDE.md generation.
*
* @see Issue #794 - Path format mismatch causes folder CLAUDE.md files to show "No recent activity"
*/
/**
* Normalize path separators to forward slashes, collapse consecutive slashes,
* and remove trailing slashes.
*
* @example
* normalizePath('app\\api\\router.py') // 'app/api/router.py'
* normalizePath('app//api///router.py') // 'app/api/router.py'
* normalizePath('app/api/') // 'app/api'
*/
export function normalizePath(p: string): string {
return p.replace(/\\/g, '/').replace(/\/+/g, '/').replace(/\/+$/, '');
}
/**
* Check if a file is a direct child of a folder (not in a subfolder).
*
* Handles path format mismatches where folderPath may be absolute but
* filePath is stored as relative in the database.
*
* NOTE: This uses suffix matching which assumes both paths are relative to
* the same project root. It may produce false positives if used across
* different project roots, but this is mitigated by project-scoped queries.
*
* @param filePath - Path to the file (e.g., "app/api/router.py" or "/Users/x/project/app/api/router.py")
* @param folderPath - Path to the folder (e.g., "app/api" or "/Users/x/project/app/api")
* @returns true if file is directly in folder, false if in a subfolder or different folder
*
* @example
* // Same format (both relative)
* isDirectChild('app/api/router.py', 'app/api') // true
* isDirectChild('app/api/v1/router.py', 'app/api') // false (in subfolder)
*
* @example
* // Mixed format (absolute folder, relative file) - fixes #794
* isDirectChild('app/api/router.py', '/Users/dev/project/app/api') // true
*/
export function isDirectChild(filePath: string, folderPath: string): boolean {
const normFile = normalizePath(filePath);
const normFolder = normalizePath(folderPath);
// Strategy 1: Direct prefix match (both paths in same format)
if (normFile.startsWith(normFolder + '/')) {
const remainder = normFile.slice(normFolder.length + 1);
return !remainder.includes('/');
}
// Strategy 2: Handle absolute folderPath with relative filePath
// e.g., folderPath="/Users/x/project/app/api" and filePath="app/api/router.py"
const folderSegments = normFolder.split('/');
const fileSegments = normFile.split('/');
if (fileSegments.length < 2) return false; // Need at least folder/file
const fileDir = fileSegments.slice(0, -1).join('/'); // Directory part of file
const fileName = fileSegments[fileSegments.length - 1]; // Actual filename
// Check if folder path ends with the file's directory path
if (normFolder.endsWith('/' + fileDir) || normFolder === fileDir) {
// File is a direct child (no additional subdirectories)
return !fileName.includes('/');
}
// Check if file's directory is contained at the end of folder path
// by progressively checking suffixes
for (let i = 0; i < folderSegments.length; i++) {
const folderSuffix = folderSegments.slice(i).join('/');
if (folderSuffix === fileDir) {
return true;
}
}
return false;
}