{"record":{"id":"4a48d96736824051","repo":"JuliusBrussee/caveman","slug":"caveman-build-unresolved-relative-source-import","errorCode":null,"errorMessage":"caveman build: unresolved relative source import ${JSON.stringify(specifier)}","messagePattern":"caveman build: unresolved relative source import (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/agent/src/source-graph.ts","lineNumber":84,"sourceCode":"      const candidates = extname(base)\n        ? explicitImportCandidates(base, path)\n        : [\n          base,\n          ...RESOLUTION_EXTENSIONS.map((extension) => base + extension),\n          ...SOURCE_EXTENSIONS.map((extension) => resolve(base, `index${extension}`)),\n        ];\n      let found: string | undefined;\n      for (const candidate of candidates) {\n        try {\n          await readFile(candidate);\n          found = candidate;\n          break;\n        } catch (error) {\n          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);","sourceCodeStart":66,"sourceCodeEnd":102,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/agent/src/source-graph.ts#L66-L102","documentation":"A relative import's candidate files (the specifier plus extension/index variants tried by the resolver) all returned ENOENT, so the source graph cannot find the imported module. The build requires a complete, finite file closure; an unresolved relative import makes the graph non-lockable and aborts with the failing specifier in the message.","triggerScenarios":"A source file imports \"./utils/helper\" when the file is helper.ts in a directory the candidate list does not cover, the file was renamed/deleted, or the specifier has a casing mismatch on case-sensitive filesystems.","commonSituations":"Renaming or moving a module without updating importers; case mismatches after cloning onto Linux from macOS/Windows; a missing file that TypeScript's own resolution tolerates via different fallback rules than this builder.","solutions":["Check the specifier in the error message and create/rename the target file so one of the standard candidates exists (exact path, .ts/.js/.tsx variants, /index.*).","Run the TypeScript compiler to surface the same broken import in your editor before building.","On Linux, verify path casing matches the import exactly."],"exampleFix":"// before: src/a.ts\nimport { x } from \"./b/c\"; // src/b/c.ts was moved to src/b/c/index.ts? or deleted\n\n// after: restore the file at src/b/c.ts (or update the import to the real path)","handlingStrategy":"validation","validationCode":"import { access } from \"node:fs/promises\";\nasync function relativeImportResolves(from: string, spec: string): Promise<boolean> {\n  const base = resolve(dirname(from), spec);\n  const candidates = extname(base)\n    ? [base]\n    : [base, `${base}.ts`, `${base}.tsx`, `${base}.js`, `${base}.mjs`, `${base}.cjs`, `${base}/index.ts`, `${base}/index.js`];\n  return access(candidates[0]!).then(() => true, () => false); // check each candidate similarly\n}","typeGuard":null,"tryCatchPattern":"try {\n  await buildSourceGraph(root);\n} catch (error) {\n  if (error instanceof Error && error.message.startsWith(\"caveman build: unresolved relative source import\")) {\n    // the failing specifier is quoted in the message; create/rename the file or fix the path\n  } else throw error;\n}","preventionTips":["Keep TypeScript strict so broken relative imports fail in the editor first.","After moving/renaming files, run a project-wide import check before building.","Match path casing exactly on case-sensitive filesystems."],"tags":["build","imports","file-not-found","resolution"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}