{"record":{"id":"50b6b0bf6bc96b1d","repo":"JuliusBrussee/caveman","slug":"caveman-build-imported-source-escapes-project-roo","errorCode":null,"errorMessage":"caveman build: imported source escapes project root","messagePattern":"caveman build: imported source escapes project root","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/agent/src/source-graph.ts","lineNumber":98,"sourceCode":"          if ((error as NodeJS.ErrnoException).code !== \"ENOENT\") throw error;\n        }\n      }\n      if (!found) {\n        throw new Error(`caveman build: unresolved relative source import ${JSON.stringify(specifier)}`);\n      }\n      if (bare) {\n        await collectPackageClosure(packageRoot!, files, packageRoots);\n        continue;\n      }\n      const lexicalFound = found;\n      found = await realpath(found);\n      const insideRoot = isPathWithin(canonicalRoot, found);\n      if (!insideRoot && !bare && !external.has(path)) {\n        const lexicalInsideRoot = isPathWithin(canonicalRoot, lexicalFound);\n        if (lexicalInsideRoot) {\n          throw new Error(\"caveman build: source graph symlink escapes project root\");\n        }\n        throw new Error(\"caveman build: imported source escapes project root\");\n      }\n      if (!insideRoot) external.add(found);\n      if (!files.has(found)) {\n        files.add(found);\n        if (!traversalStopRoots.some((stop) => isPathWithin(stop, found))) {\n          queue.push(found);\n        }\n      }\n    }\n  }\n  return files;\n}\n\nfunction explicitImportCandidates(base: string, importer: string): string[] {\n  const importerExtension = extname(importer);\n  if (![\".ts\", \".tsx\", \".mts\", \".cts\"].includes(importerExtension)) return [base];\n  const extension = extname(base);\n  const stem = base.slice(0, -extension.length);","sourceCodeStart":80,"sourceCodeEnd":116,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/agent/src/source-graph.ts#L80-L116","documentation":"A relative import's resolved file lies outside the project root by both lexical path and realpath — the import genuinely reaches beyond the project (e.g. \"../../..\"). The builder's project closure must stay inside the canonical root; importing out of it is not lockable through the project path, so the build aborts with this error.","triggerScenarios":"A source file uses ../ or ../../ specifiers that climb past the project root directory, or a moved file kept its old deep-relative import which now escapes.","commonSituations":"Files moved into the project from a sibling directory without rewriting imports; projects nested inside a workspace where sources reach at workspace siblings instead of using package imports.","solutions":["Rewrite the import as a workspace/package dependency (bare specifier) declared in package.json so it resolves through the package closure path.","Move the target file inside the project root and use a contained relative path.","Fix stale ../ chains after restructuring — run tsc to list imports that no longer resolve inside the project."],"exampleFix":"// before: project/a/b/src.ts\nimport { helper } from \"../../../outside/helper.ts\";\n\n// after\nimport { helper } from \"my-workspace-helper\"; // declared dependency","handlingStrategy":"validation","validationCode":"import { resolve, relative, isAbsolute } from \"node:path\";\nfunction importStaysInRoot(root: string, importer: string, spec: string): boolean {\n  if (!spec.startsWith(\".\")) return true; // bare: package path\n  const target = resolve(dirname(importer), spec);\n  const rel = relative(root, target);\n  return !isAbsolute(rel) && !rel.startsWith(\"..\");\n}","typeGuard":null,"tryCatchPattern":"try {\n  await buildSourceGraph(root);\n} catch (error) {\n  if (error instanceof Error && error.message === \"caveman build: imported source escapes project root\") {\n    // convert the ../.. import into a declared package dependency or move the file inside\n  } else throw error;\n}","preventionTips":["Forbid ../ chains that climb past the project root in lint rules (e.g. eslint import/no-relative-parent-imports).","Use package dependencies for cross-package code instead of filesystem reach-arounds.","Rewrite imports immediately after moving files between projects."],"tags":["build","imports","containment","monorepo"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}