ruvnet/ruflo · error
case-fold collision in declared build inputs
Error message
case-fold collision in declared build inputs
What it means
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.
Source
Thrown at v3/@claude-flow/codex/src/harness/build-evidence.ts:126
name: requireText(input.name, 'build input name'),
path: normalizePath(input.path),
digest: requireDigest(input.digest, 'build input digest'),
bytes: input.bytes,
})).sort((left, right) => compare(left.path, right.path) || compare(left.name, right.name));
const tools = toolchains.map((toolchain) => ({
name: requireText(toolchain.name, 'toolchain name'),
version: requireText(toolchain.version, 'toolchain version'),
digest: requireDigest(toolchain.digest, 'toolchain digest'),
})).sort((left, right) => compare(left.name, right.name) || compare(left.version, right.version));
if (inputs.some(({ bytes }) => !Number.isSafeInteger(bytes) || bytes < 0)) {
throw new Error('build input bytes must be a non-negative safe integer');
}
const inputKeys = inputs.map(({ name, path }) => `${name}\0${path}`);
if (new Set(inputKeys).size !== inputKeys.length) throw new Error('duplicate declared build input');
const foldedPaths = inputs.map(({ path }) => portableCaseFold(path));
if (new Set(foldedPaths).size !== foldedPaths.length) {
throw new Error('case-fold collision in declared build inputs');
}
const toolKeys = tools.map(({ name, version }) => `${name}\0${version}`);
if (new Set(toolKeys).size !== toolKeys.length) throw new Error('duplicate declared toolchain');
const body = {
contractVersion: 1 as const,
assurance: 'declared-unsigned' as const,
sourceStateId: requireDigest(sourceState.sourceStateId, 'source state id'),
buildInputs: inputs,
toolchains: tools,
};
return { ...body, evidenceDigest: sha256(canonicalJson(body)) };
}
/**
* Recompute declared evidence from local bytes. It does not prove the
* declaration set is complete and does not sign or authorize a release.
*/View on GitHub (pinned to fa13ee4ad6)
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
Example fix
// before
const buildInputs = [
{ name: 'config', path: 'Config/app.json', digest: d1, bytes: 1 },
{ name: 'config-lower', path: 'config/app.json', digest: d2, bytes: 1 },
];
// after
const buildInputs = [
{ name: 'config', path: 'config/app.json', digest: d, bytes: 1 }, // single canonical casing
]; Defensive patterns
Strategy: validation
Validate before calling
import { portableCaseFold } from '@claude-flow/codex';
function hasCaseFoldCollision(paths: string[]): boolean {
const folded = paths.map((p) => portableCaseFold(p));
return new Set(folded).size !== folded.length;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- unsafe build input path: ${value}
- ${label} must be non-empty
- ${label} must be a canonical sha256 digest
- build evidence path is not a file or symlink: ${path}
- build input bytes must be a non-negative safe integer
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/72e4f51857d326b4.
Report an issue: GitHub.