Fission-AI/OpenSpec · error

validation failed - duplicate TO in RENAMED for header "###…

Error message

${specName} validation failed - duplicate TO in RENAMED for header "### Requirement: ${to}"

What it means

Validation inside buildUpdatedSpec for the RENAMED plan: two rename entries share the same normalized TO header. Two renames targeting the same requirement name would collide when applied, so the deltas are rejected; input at fault is the duplicated destination header "### Requirement: <to>".

Solutions

  1. Give each RENAMED target a distinct TO header
  2. Remove the conflicting RENAMED entry
Defensive patterns

Strategy: validation

When it happens

Trigger: Thrown at src/core/specs-apply.ts:239 when the library encounters an invalid state.

Common situations: See trigger scenarios.


AI-assisted analysis of Fission-AI/OpenSpec@6926ccb18a (2026-08-25). Data as JSON: /api/errors/bb8b3678795ba674. Report an issue: GitHub.

Appendix: source

Thrown at src/core/specs-apply.ts:239

    if (removedNamesSet.has(name)) {
      throw new Error(
        `${specName} validation failed - duplicate requirement in REMOVED for header "### Requirement: ${rem}"`
      );
    }
    removedNamesSet.add(name);
  }
  const renamedFromSet = new Set<string>();
  const renamedToSet = new Set<string>();
  for (const { from, to } of plan.renamed) {
    const fromNorm = normalizeRequirementName(from);
    const toNorm = normalizeRequirementName(to);
    if (renamedFromSet.has(fromNorm)) {
      throw new Error(
        `${specName} validation failed - duplicate FROM in RENAMED for header "### Requirement: ${from}"`
      );
    }
    if (renamedToSet.has(toNorm)) {
      throw new Error(
        `${specName} validation failed - duplicate TO in RENAMED for header "### Requirement: ${to}"`
      );
    }
    renamedFromSet.add(fromNorm);
    renamedToSet.add(toNorm);
  }

  // Pre-validate cross-section conflicts
  const conflicts: Array<{ name: string; a: string; b: string }> = [];
  for (const n of modifiedNames) {
    if (removedNamesSet.has(n)) conflicts.push({ name: n, a: 'MODIFIED', b: 'REMOVED' });
    if (addedNames.has(n)) conflicts.push({ name: n, a: 'MODIFIED', b: 'ADDED' });
  }
  for (const n of addedNames) {
    if (removedNamesSet.has(n)) conflicts.push({ name: n, a: 'ADDED', b: 'REMOVED' });
  }
  // Renamed interplay: MODIFIED must reference the NEW header, not FROM
  for (const { from, to } of plan.renamed) {

View on GitHub (pinned to 6926ccb18a)