{"record":{"id":"72e4f51857d326b4","repo":"ruvnet/ruflo","slug":"case-fold-collision-in-declared-build-inputs","errorCode":null,"errorMessage":"case-fold collision in declared build inputs","messagePattern":"case-fold collision in declared build inputs","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/codex/src/harness/build-evidence.ts","lineNumber":126,"sourceCode":"    name: requireText(input.name, 'build input name'),\n    path: normalizePath(input.path),\n    digest: requireDigest(input.digest, 'build input digest'),\n    bytes: input.bytes,\n  })).sort((left, right) => compare(left.path, right.path) || compare(left.name, right.name));\n  const tools = toolchains.map((toolchain) => ({\n    name: requireText(toolchain.name, 'toolchain name'),\n    version: requireText(toolchain.version, 'toolchain version'),\n    digest: requireDigest(toolchain.digest, 'toolchain digest'),\n  })).sort((left, right) => compare(left.name, right.name) || compare(left.version, right.version));\n\n  if (inputs.some(({ bytes }) => !Number.isSafeInteger(bytes) || bytes < 0)) {\n    throw new Error('build input bytes must be a non-negative safe integer');\n  }\n  const inputKeys = inputs.map(({ name, path }) => `${name}\\0${path}`);\n  if (new Set(inputKeys).size !== inputKeys.length) throw new Error('duplicate declared build input');\n  const foldedPaths = inputs.map(({ path }) => portableCaseFold(path));\n  if (new Set(foldedPaths).size !== foldedPaths.length) {\n    throw new Error('case-fold collision in declared build inputs');\n  }\n  const toolKeys = tools.map(({ name, version }) => `${name}\\0${version}`);\n  if (new Set(toolKeys).size !== toolKeys.length) throw new Error('duplicate declared toolchain');\n\n  const body = {\n    contractVersion: 1 as const,\n    assurance: 'declared-unsigned' as const,\n    sourceStateId: requireDigest(sourceState.sourceStateId, 'source state id'),\n    buildInputs: inputs,\n    toolchains: tools,\n  };\n  return { ...body, evidenceDigest: sha256(canonicalJson(body)) };\n}\n\n/**\n * Recompute declared evidence from local bytes. It does not prove the\n * declaration set is complete and does not sign or authorize a release.\n */","sourceCodeStart":108,"sourceCodeEnd":144,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/v3/@claude-flow/codex/src/harness/build-evidence.ts#L108-L144","documentation":"Even when (name, path) pairs are distinct, createBuildEvidence case-folds every path (portableCaseFold) and rejects sets whose folded paths collide. 'Config/app.json' and 'config/app.json' are distinct POSIX paths but would overwrite each other on case-insensitive filesystems (macOS, Windows), so the evidence contract refuses them to stay portable and unambiguous.","triggerScenarios":"Two declared input paths that differ only by letter case; paths that fold alike under Unicode simple case folding; mixing 'Assets/' and 'assets/' prefixes across declaration sources.","commonSituations":"Merging declarations authored on Linux (case-sensitive) with ones authored on macOS/Windows; a file renamed to lowercase while the old-cased declaration remains; two teams declaring the same generated file with different casing conventions.","solutions":["Pick one canonical casing that matches the real on-disk name and update every declaration to it","Remove the stale entry when a file was renamed to a different case","Apply the same case-fold comparison yourself when assembling lists from multiple sources so the conflict surfaces with your own context"],"exampleFix":"// before\nconst buildInputs = [\n  { name: 'config', path: 'Config/app.json', digest: d1, bytes: 1 },\n  { name: 'config-lower', path: 'config/app.json', digest: d2, bytes: 1 },\n];\n\n// after\nconst buildInputs = [\n  { name: 'config', path: 'config/app.json', digest: d, bytes: 1 }, // single canonical casing\n];","handlingStrategy":"validation","validationCode":"import { portableCaseFold } from '@claude-flow/codex';\nfunction hasCaseFoldCollision(paths: string[]): boolean {\n  const folded = paths.map((p) => portableCaseFold(p));\n  return new Set(folded).size !== folded.length;\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Standardize on one casing convention (usually lowercase) for generated artifact paths","When merging declaration sources, compare case-folded paths, not raw strings","Check the real on-disk name case after renames, especially Linux-to-macOS handoffs"],"tags":["build-evidence","case-sensitivity","path-validation","harness","portability"],"backgroundTag":"case-insensitive-path-collision","analyzedSha":"fa13ee4ad60ac2090b1480656eb233521790d640","analyzedAt":"2026-08-18T21:34:22.708Z","schemaVersion":2},"datasetVersion":"2026-08-22T09:17:25.309Z"}