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
- Deduplicate by `${name}\0${path}` before calling createBuildEvidence
- If the same path must be declared twice, give each entry a distinct logical name
- 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
- Merge declaration lists through a keyed Map instead of array concat
- Make re-runs idempotent: upsert by (name, path), never append
- Unit-test the merge path with overlapping default and user inputs
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
- duplicate declared toolchain
- ${label} must be non-empty
- ${label} must be a canonical sha256 digest
- build evidence path is not a file or symlink: ${path}
- unsafe build input path: ${value}
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/9be2cac3792393c7.
Report an issue: GitHub.