{"record":{"id":"7f40dfc1f0c602e0","repo":"tobi/qmd","slug":"outside-collection","errorCode":"OUTSIDE_COLLECTION","errorMessage":"OUTSIDE_COLLECTION","messagePattern":"OUTSIDE_COLLECTION","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"info","filePath":"src/cli/qmd.ts","lineNumber":1962,"sourceCode":"    console.log(\"No files found matching pattern.\");\n    // Continue so the deactivation pass can mark previously indexed docs as inactive.\n  }\n\n  let indexed = 0, updated = 0, unchanged = 0, processed = 0;\n  const skippedFiles: { file: string; code: string }[] = [];\n  const seenPaths = new Set<string>();\n  // Literal paths of every file in this scan. Passed to the legacy-path\n  // migration so it never adopts a row that still belongs to a live file.\n  const livePaths = new Set(files.map(f => f.replace(/\\\\/g, '/')));\n  const startTime = Date.now();\n\n  for (const relativeFile of files) {\n    const filepath = getRealPath(resolve(resolvedPwd, relativeFile));\n    // Store the literal relative path — handelize() is NOT applied at index time.\n    const path = relativeFile.replace(/\\\\/g, '/');\n    if (!isPathInsideDir(resolvedPwd, filepath)) {\n      processed++;\n      skippedFiles.push({ file: relativeFile, code: \"OUTSIDE_COLLECTION\" });\n      progress.set((processed / total) * 100);\n      continue;\n    }\n    seenPaths.add(path);\n\n    let content: string;\n    try {\n      content = readFileSync(filepath, \"utf-8\");\n    } catch (err) {\n      // Skip files that can't be read (ETIMEDOUT, EAGAIN, EACCES, …) (#460)\n      processed++;\n      skippedFiles.push({ file: relativeFile, code: fsErrorCode(err) });\n      progress.set((processed / total) * 100);\n      continue;\n    }\n\n    // Skip empty files - nothing useful to index\n    if (!content.trim()) {","sourceCodeStart":1944,"sourceCodeEnd":1980,"githubUrl":"https://github.com/tobi/qmd/blob/dbfd0b4736aeaf761d1a16ca8e424f071df8feb9/src/cli/qmd.ts#L1944-L1980","documentation":"During collection indexing (qmd collection add / update), a candidate file resolved to a real path outside the collection root directory (isPathInsideDir failed). The file is skipped and recorded with code OUTSIDE_COLLECTION — symlinks escaping the collection are the usual cause (getRealPath resolves them).","triggerScenarios":"Indexing a directory containing a symlink whose target lives outside the collection root; files listed by the scanner whose realpath resolves elsewhere (e.g. symlinks into ~/.config or another collection's dir).","commonSituations":"Symlinked dotfile dirs (e.g. vim/nvim config symlinking to a repo), monorepo workspaces symlinked from a parent folder, or masked paths pointing at absolute targets.","solutions":["If the target should be indexed, add its real location as its own collection instead of a symlink","Replace the symlink with the actual directory or copy files inside the root","Ignore the skip — it is informational; the rest of the collection still indexes"],"exampleFix":"# before\n~/notes/repo -> /home/me/src/repo   (symlink escapes collection)\n# after\nqmd collection add /home/me/src/repo --name repo   # index real path directly","handlingStrategy":"fallback","validationCode":"const real = fs.realpathSync(file); if (isPathInsideDir(collectionRoot, real)) indexFile(file); else skip();","typeGuard":"const isInsideCollection = (root: string, f: string) => isPathInsideDir(root, fs.realpathSync(f));","tryCatchPattern":null,"preventionTips":["Avoid symlinks pointing outside collection roots","Add real locations as their own collections","Treat OUTSIDE_COLLECTION skips as informational, not fatal"],"tags":["symlink","collection","indexing","path"],"backgroundTag":"symlink-escapes-root","analyzedSha":"dbfd0b4736aeaf761d1a16ca8e424f071df8feb9","analyzedAt":"2026-08-28T18:07:46.628Z","schemaVersion":2},"datasetVersion":"2026-08-28T21:17:43.275Z"}