{"version":3,"file":"inputs.mjs","names":[],"sources":["../../src/compiler/inputs.ts"],"sourcesContent":["import crypto from 'node:crypto';\nimport fs from 'node:fs';\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n  return value !== null && typeof value === 'object' && !Array.isArray(value);\n}\n\n/**\n * One recorded build input: the stat pair for speed, the digest for truth.\n *\n * Comparing only `size` and `mtimeMs` makes every rebuild look like a change,\n * including one that restored byte-identical output from a build cache, and a\n * sync triggered that way republishes a generation nobody asked for. Comparing\n * content alone is correct but reads every input on every check, and the\n * cockpit polls this. Recording both lets the stat pair answer the common\n * \"nothing was touched\" case without opening a file, while the digest stays the\n * authority for anything whose stat moved.\n */\nexport interface InputFingerprint {\n  path: string;\n  size: number;\n  mtimeMs: number;\n  sha256: string;\n}\n\n/**\n * Verdicts already reached, keyed by both the receipt and the stats now on disk,\n * so one receipt cannot answer the freshness question for another.\n */\nconst freshnessVerdicts = new Map<string, boolean>();\n\nfunction digestOf(filePath: string): string | undefined {\n  try {\n    return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');\n  } catch {\n    // Unreadable is indistinguishable from absent here: either way the recorded\n    // digest cannot be confirmed, so the caller must treat the build as stale.\n    return undefined;\n  }\n}\n\n/** Records one build input, or undefined when it is not a readable regular file. */\nexport function fingerprintInput(target: string): InputFingerprint | undefined {\n  let stat: fs.Stats;\n  try {\n    stat = fs.statSync(target);\n  } catch {\n    // A path that vanished between discovery and recording is simply not an\n    // input; the compiler records the set it could read.\n    return undefined;\n  }\n  if (!stat.isFile()) return undefined;\n  const sha256 = digestOf(target);\n  if (sha256 === undefined) return undefined;\n  return { path: target, size: stat.size, mtimeMs: stat.mtimeMs, sha256 };\n}\n\n/** Parses one recorded input, rejecting a record written before digests existed. */\nexport function parseInputFingerprint(value: unknown): InputFingerprint | undefined {\n  if (!isRecord(value)) return undefined;\n  if (\n    typeof value.path !== 'string' ||\n    typeof value.size !== 'number' ||\n    typeof value.mtimeMs !== 'number' ||\n    typeof value.sha256 !== 'string'\n  ) {\n    return undefined;\n  }\n  return { path: value.path, size: value.size, mtimeMs: value.mtimeMs, sha256: value.sha256 };\n}\n\n/** Whether every recorded input still holds the bytes it was recorded with. */\nexport function inputsAreFresh(inputs: readonly InputFingerprint[]): boolean {\n  const moved: InputFingerprint[] = [];\n  const key = crypto.createHash('sha256');\n  for (const input of inputs) {\n    let stat: fs.Stats;\n    try {\n      stat = fs.statSync(input.path);\n    } catch {\n      // A recorded input that is gone cannot be confirmed, and no digest will\n      // bring it back; the build it describes is stale.\n      return false;\n    }\n    if (!stat.isFile()) return false;\n    key.update(JSON.stringify([input.path, input.size, input.mtimeMs, input.sha256, stat.size, stat.mtimeMs]));\n    if (stat.size !== input.size || stat.mtimeMs !== input.mtimeMs) moved.push(input);\n  }\n  if (moved.length === 0) return true;\n\n  const cacheKey = key.digest('hex');\n  const remembered = freshnessVerdicts.get(cacheKey);\n  if (remembered !== undefined) return remembered;\n  const fresh = moved.every((input) => digestOf(input.path) === input.sha256);\n  freshnessVerdicts.set(cacheKey, fresh);\n  return fresh;\n}\n\n/** Test seam: the memo is process-local and must not leak between cases. */\nexport function resetInputFreshnessCache(): void {\n  freshnessVerdicts.clear();\n}\n"],"mappings":";;;AAEA,SAAS,SAAS,OAAkD;CAClE,OAAO,UAAU,QAAQ,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK;AAC5E;;;;;AAwBA,MAAM,oCAAoB,IAAI,IAAqB;AAEnD,SAAS,SAAS,UAAsC;CACtD,IAAI;EACF,OAAO,OAAO,WAAW,QAAQ,CAAC,CAAC,OAAO,GAAG,aAAa,QAAQ,CAAC,CAAC,CAAC,OAAO,KAAK;CACnF,QAAQ;EAGN;CACF;AACF;;AAGA,SAAgB,iBAAiB,QAA8C;CAC7E,IAAI;CACJ,IAAI;EACF,OAAO,GAAG,SAAS,MAAM;CAC3B,QAAQ;EAGN;CACF;CACA,IAAI,CAAC,KAAK,OAAO,GAAG,OAAO,KAAA;CAC3B,MAAM,SAAS,SAAS,MAAM;CAC9B,IAAI,WAAW,KAAA,GAAW,OAAO,KAAA;CACjC,OAAO;EAAE,MAAM;EAAQ,MAAM,KAAK;EAAM,SAAS,KAAK;EAAS;CAAO;AACxE;;AAGA,SAAgB,sBAAsB,OAA8C;CAClF,IAAI,CAAC,SAAS,KAAK,GAAG,OAAO,KAAA;CAC7B,IACE,OAAO,MAAM,SAAS,YACtB,OAAO,MAAM,SAAS,YACtB,OAAO,MAAM,YAAY,YACzB,OAAO,MAAM,WAAW,UAExB;CAEF,OAAO;EAAE,MAAM,MAAM;EAAM,MAAM,MAAM;EAAM,SAAS,MAAM;EAAS,QAAQ,MAAM;CAAO;AAC5F;;AAGA,SAAgB,eAAe,QAA8C;CAC3E,MAAM,QAA4B,CAAC;CACnC,MAAM,MAAM,OAAO,WAAW,QAAQ;CACtC,KAAK,MAAM,SAAS,QAAQ;EAC1B,IAAI;EACJ,IAAI;GACF,OAAO,GAAG,SAAS,MAAM,IAAI;EAC/B,QAAQ;GAGN,OAAO;EACT;EACA,IAAI,CAAC,KAAK,OAAO,GAAG,OAAO;EAC3B,IAAI,OAAO,KAAK,UAAU;GAAC,MAAM;GAAM,MAAM;GAAM,MAAM;GAAS,MAAM;GAAQ,KAAK;GAAM,KAAK;EAAO,CAAC,CAAC;EACzG,IAAI,KAAK,SAAS,MAAM,QAAQ,KAAK,YAAY,MAAM,SAAS,MAAM,KAAK,KAAK;CAClF;CACA,IAAI,MAAM,WAAW,GAAG,OAAO;CAE/B,MAAM,WAAW,IAAI,OAAO,KAAK;CACjC,MAAM,aAAa,kBAAkB,IAAI,QAAQ;CACjD,IAAI,eAAe,KAAA,GAAW,OAAO;CACrC,MAAM,QAAQ,MAAM,OAAO,UAAU,SAAS,MAAM,IAAI,MAAM,MAAM,MAAM;CAC1E,kBAAkB,IAAI,UAAU,KAAK;CACrC,OAAO;AACT"}