{"record":{"id":"d823cd0d7e5439ce","repo":"garrytan/gstack","slug":"image-not-found-src-resolved-to-filepath","errorCode":null,"errorMessage":"image not found: ${src} (resolved to ${filePath})","messagePattern":"image not found: (.+?) \\(resolved to (.+?)\\)","errorType":"validation","errorClass":"StrictModeError","httpStatus":null,"severity":"warning","filePath":"make-pdf/src/diagram-prepass.ts","lineNumber":635,"sourceCode":"    // decodeURIComponent throws on malformed escapes (foo%zz.png) — a broken\n    // URL must degrade to the missing-image path, not crash the run.\n    let decodedSrc = src;\n    try {\n      decodedSrc = decodeURIComponent(src);\n    } catch { /* keep raw src */ }\n\n    const filePath = src.startsWith(\"file:\")\n      ? fileURLToPath(src)\n      : isDrivePath\n        ? path.resolve(src)\n        : path.resolve(opts.inputDir, decodedSrc);\n\n    const cached = memo.get(filePath);\n    if (cached !== undefined) return rewriteImgTag(tag, cached);\n\n    if (!fs.existsSync(filePath)) {\n      const msg = `image not found: ${src} (resolved to ${filePath})`;\n      if (opts.strict) throw new StrictModeError(msg);\n      opts.warn(msg);\n      return buildMissingImagePlaceholder(src);\n    }\n\n    // Out-of-tree reads are legal (local CLI semantics — like pandoc) but\n    // never silent: an agent PDF-ing untrusted markdown should not quietly\n    // embed ~/.ssh/config into a shareable document. --strict makes it fatal.\n    // Compare REAL paths — a symlink inside the input dir pointing outside\n    // would otherwise pass a string-prefix check (Codex adversarial finding).\n    // Runs after the existence check: realpath of a missing file can't\n    // resolve, and on macOS /var vs /private/var would false-positive.\n    const inputRoot = safeRealpath(path.resolve(opts.inputDir)) + path.sep;\n    const realFilePath = safeRealpath(filePath);\n    if (!realFilePath.startsWith(inputRoot)) {\n      const msg = `image resolves OUTSIDE the input directory: ${src} → ${realFilePath}`;\n      if (opts.strict) throw new StrictModeError(msg + \" — move it under the markdown's directory or drop --strict\");\n      opts.warn(msg);\n    }","sourceCodeStart":617,"sourceCodeEnd":653,"githubUrl":"https://github.com/garrytan/gstack/blob/94993f74012782fd94416dd44b8314f6363a13a4/make-pdf/src/diagram-prepass.ts#L617-L653","documentation":"StrictModeError thrown by inlineLocalImages() when a local image src resolves to a filePath that does not exist on disk (fs.existsSync is false), under --strict. In non-strict mode the same condition produces a warn and a missing-image placeholder; strict makes it fatal so broken image links fail the build rather than shipping a degraded PDF.","triggerScenarios":"inlineLocalImages() with opts.strict=true, an <img> whose decoded src path does not exist relative to opts.inputDir (or as a file:// URL / drive path). The existence check fails and the strict branch throws before any realpath or stat call.","commonSituations":"Markdown references an image that was never committed; a renamed asset whose markdown was not updated; wrong inputDir (CWD-relative resolution); a typo in the filename; a build that copies markdown but not its assets folder.","solutions":["Verify the image exists at the resolved path printed in the message.","Fix the markdown src to match the actual relative path from the markdown's directory.","Ensure the assets directory is copied alongside the markdown in CI.","Drop --strict to degrade to a warn + placeholder if a missing image is acceptable."],"exampleFix":"<!-- before: file does not exist -->\n![diagram](./arch.png)  <!-- actually committed as architecture.png -->\n\n<!-- after -->\n![diagram](./architecture.png)","handlingStrategy":"validation","validationCode":"import fs from 'node:fs';\nimport path from 'node:path';\n\nfunction findMissingImages(markdown: string, inputDir: string): string[] {\n  const re = /!\\[[^\\]]*\\]\\((?!https?:|data:)([^)]+)\\)/g;\n  const missing: string[] = [];\n  let m: RegExpExecArray | null;\n  while ((m = re.exec(markdown))) {\n    const p = path.resolve(inputDir, decodeURIComponent(m[1]));\n    if (!fs.existsSync(p)) missing.push(m[1]);\n  }\n  return missing;\n}\n\nconst missing = findMissingImages(markdown, inputDir);\nif (missing.length) throw new Error(`missing images: ${missing.join(', ')}`);","typeGuard":"import { StrictModeError } from './diagram-prepass';\nfunction isStrictModeError(e: unknown): e is StrictModeError {\n  return e instanceof StrictModeError;\n}","tryCatchPattern":null,"preventionTips":["Commit image assets alongside markdown in the same PR.","Run a missing-image check in CI before the strict render.","Use relative paths from the markdown's own directory consistently.","Avoid case-mismatched filenames on case-sensitive filesystems."],"tags":["make-pdf","image","filesystem","strict-mode"],"backgroundTag":null,"analyzedSha":"94993f74012782fd94416dd44b8314f6363a13a4","analyzedAt":"2026-08-12T04:06:23.140Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}