* Resolve a markdown file path within a project root. * * @param input - User-provided path (absolute, relative, or bare filename) * @param projectRoot - Project root directory to search within
( input: string, projectRoot: string, )
| 374 | * @param projectRoot - Project root directory to search within |
| 375 | */ |
| 376 | function resolveMarkdownFileCore( |
| 377 | input: string, |
| 378 | projectRoot: string, |
| 379 | ): ResolveResult { |
| 380 | const normalizedInput = normalizeUserPathInput(input); |
| 381 | const searchInput = normalizeSeparators(normalizedInput); |
| 382 | const isBareFilename = !searchInput.includes("/"); |
| 383 | const targetLookupKey = getLookupKey(searchInput, isBareFilename); |
| 384 | |
| 385 | // Restrict to markdown files |
| 386 | if (!isSearchableMarkdownPath(normalizedInput)) { |
| 387 | return { kind: "not_found", input }; |
| 388 | } |
| 389 | |
| 390 | // 1. Absolute path — use as-is (no project root restriction; |
| 391 | // the user explicitly typed the full path) |
| 392 | if (isAbsoluteNormalizedUserPath(normalizedInput)) { |
| 393 | const absolutePath = resolveAbsolutePath(normalizedInput); |
| 394 | if (fileExists(absolutePath)) { |
| 395 | return { kind: "found", path: absolutePath }; |
| 396 | } |
| 397 | return { kind: "not_found", input }; |
| 398 | } |
| 399 | |
| 400 | // 2. Exact relative path from project root |
| 401 | const fromRoot = resolve(projectRoot, searchInput); |
| 402 | if (isWithinProjectRoot(fromRoot, projectRoot) && fileExists(fromRoot)) { |
| 403 | return { kind: "found", path: fromRoot }; |
| 404 | } |
| 405 | |
| 406 | // 3. Case-insensitive search (only scan markdown files) |
| 407 | const allFiles: string[] = []; |
| 408 | walkMarkdownFiles(projectRoot, projectRoot, allFiles, IGNORED_DIRS); |
| 409 | const matches: string[] = []; |
| 410 | |
| 411 | for (const match of allFiles) { |
| 412 | const normalizedMatch = normalizeSeparators(match); |
| 413 | const matchLookupKey = getLookupKey(normalizedMatch, isBareFilename); |
| 414 | |
| 415 | if (matchLookupKey === targetLookupKey) { |
| 416 | const full = resolve(projectRoot, normalizedMatch); |
| 417 | if (isWithinProjectRoot(full, projectRoot)) { |
| 418 | matches.push(full); |
| 419 | } |
| 420 | } |
| 421 | } |
| 422 | |
| 423 | if (matches.length === 1) { |
| 424 | return { kind: "found", path: matches[0] }; |
| 425 | } |
| 426 | if (matches.length > 1) { |
| 427 | const projectRootPrefix = `${normalizeComparablePath(projectRoot)}/`; |
| 428 | const relative = matches.map((match) => |
| 429 | normalizeComparablePath(match).replace(projectRootPrefix, ""), |
| 430 | ); |
| 431 | return { kind: "ambiguous", input, matches: relative }; |
| 432 | } |
| 433 |
no test coverage detected