ruvnet/ruflo · error

duplicate declared build input

Error message

duplicate declared build input

What it means

createBuildEvidence keys every declared build input by the pair name\0path and throws if the same pair appears twice. The contract refuses to silently merge duplicate declarations because each (name, path) pair binds a distinct digest in the hashed evidence body; deduplication is the caller's responsibility.

Source

Thrown at v3/@claude-flow/codex/src/harness/build-evidence.ts:123

  toolchains: readonly DeclaredToolchain[],
): BuildEvidence {
  const inputs = buildInputs.map((input) => ({
    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)) };
}

/**

View on GitHub (pinned to fa13ee4ad6)

Solutions

  1. Deduplicate by `${name}\0${path}` before calling createBuildEvidence
  2. If the same path must be declared twice, give each entry a distinct logical name
  3. When merging lists from multiple sources, build a Map keyed by the pair so later entries replace earlier ones deliberately

Example fix

// before
const buildInputs = [
  { name: 'bundle', path: 'dist/app.js', digest: d, bytes: 1 },
  { name: 'bundle', path: 'dist/app.js', digest: d, bytes: 1 }, // duplicate pair
];

// after
const buildInputs = [...new Map(
  rawInputs.map((i) => [`${i.name}\0${i.path}`, i]),
).values()];
Defensive patterns

Strategy: validation

Validate before calling

function dedupeInputs<T extends { name: string; path: string }>(inputs: T[]): T[] {
  return [...new Map(inputs.map((i) => [`${i.name}\0${i.path}`, i])).values()];
}

Prevention

When it happens

Trigger: Two DeclaredBuildInput entries with identical name and path, typically from concatenating declaration lists (defaults + overrides) without merging, or from a generator that appends on each run instead of replacing.

Common situations: Layered configuration where preset inputs and user inputs are simply concatenated; re-running an initialization step that appends the same declaration again; merging evidence requests from multiple components.

Related errors


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