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

  1. Pick one canonical casing that matches the real on-disk name and update every declaration to it
  2. Remove the stale entry when a file was renamed to a different case
  3. 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

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


AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/72e4f51857d326b4. Report an issue: GitHub.