Skip to content

script_apply_edits anchor_replace duplicates content when replacement extends past anchor match #898

@raddue

Description

@raddue

Bug

script_apply_edits with anchor_replace produced a duplicated method when the replacement text contained more lines than the anchor matched. The tool returned success: true with editsApplied: 1 despite the file being broken afterward.

This is distinct from #790 (retry duplication during domain reload) — no domain reload was involved here. The duplication happened on a single, non-retried edit call.

Reproduction

Full tool call:

{
  "name": "ProgressionManager",
  "path": "Assets/_Project/Scripts/Characters/Components",
  "edits": [
    {
      "op": "anchor_replace",
      "anchor": "\\[System\\.Obsolete.*\\]\\s*\n\\s*/// <summary>\\s*\n\\s*/// Restore level and XP from saved data without triggering events or rewards\\.\\.",
      "text": "        /// <summary>\n        /// Restore level and XP from saved data without triggering events or rewards.\n        /// </summary>\n        [System.Obsolete(\"Use RestoreFromSave(level, xp, statPoints, talentPoints) to ensure _points dictionary is restored.\")]\n        public void RestoreFromSave(int savedLevel, int savedXP)\n        {\n            _level = savedLevel;\n            _currentXP = savedXP;\n        }\n\n        /// <summary>\n        /// Restore level, XP, and points from saved data without triggering events."
    }
  ],
  "options": {"validate": "relaxed", "refresh": "immediate"}
}

Note: validate: "relaxed" was used. I did not test whether validate: "standard" would have caught the duplicate method.

File before edit (exact region):

        [System.Obsolete("Use RestoreFromSave(level, xp, statPoints, talentPoints) to ensure _points dictionary is restored.")]
        /// <summary>
        /// Restore level and XP from saved data without triggering events or rewards..
        /// Called during save game loading after Character serialized fields are set.
        /// </summary>
        public void RestoreFromSave(int savedLevel, int savedXP)
        {
            _level = savedLevel;
            _currentXP = savedXP;
        }

        /// <summary>
        /// Restore level, XP, and points from saved data without triggering events..
        /// Called during save game loading. Does NOT fire OnPointsChanged to avoid
        /// double-counting via Character's event forwarding handler.
        /// </summary>
        public void RestoreFromSave(int savedLevel, int savedXP, int statPoints, int talentPoints)

Intent: Reorder the [Obsolete] attribute to come after the XML doc comment (C# convention), and fix a double period in the doc comment.

Tool response:

{
  "success": true,
  "message": "Applied 2 structured edit(s) to 'Assets/_Project/Scripts/Characters/Components/ProgressionManager.cs'.",
  "data": {
    "path": "Assets/_Project/Scripts/Characters/Components/ProgressionManager.cs",
    "editsApplied": 2,
    "scheduledRefresh": false,
    "sha256": "1ac0ed4ba6044c877dd16b801f8a00708b000bfb559307cc1859986fa8f1030e",
    "routing": "structured"
  }
}

(Note: 2 edits were in the batch — the second edit was a separate anchor_replace on a different doc comment in the same file. The second edit succeeded correctly. Only the first edit produced corruption.)

File after edit (broken — two copies of the 2-param method):

                /// <summary>
        /// Restore level and XP from saved data without triggering events or rewards.
        /// </summary>
        [System.Obsolete("Use RestoreFromSave(level, xp, statPoints, talentPoints) to ensure _points dictionary is restored.")]
        public void RestoreFromSave(int savedLevel, int savedXP)
        {
            _level = savedLevel;
            _currentXP = savedXP;
        }

        /// <summary>
        /// Restore level, XP, and points from saved data without triggering events.
        /// Called during save game loading after Character serialized fields are set.
        /// </summary>
        public void RestoreFromSave(int savedLevel, int savedXP)
        {
            _level = savedLevel;
            _currentXP = savedXP;
        }

        /// <summary>
        /// Restore level, XP, and points from saved data without triggering events.
        /// Called during save game loading. Does NOT fire OnPointsChanged to avoid
        /// double-counting via Character's event forwarding handler.
        /// </summary>
        public void RestoreFromSave(int savedLevel, int savedXP, int statPoints, int talentPoints)

The anchor matched ~3 lines (attribute + doc comment start). The replacement text included a full method body + start of the next doc comment. The original method body below the anchor match was not consumed — it remained in place, producing the duplicate.

Expected Behavior

Either:

  1. anchor_replace should fail or warn when the replacement text contains significantly more lines than the anchor match (the mismatch is a strong signal of user error or an upcoming corruption), OR
  2. validate: "standard" (or even "relaxed") should catch the resulting duplicate method signature and reject the edit before writing the file

Workaround

  • Use replace_method op instead (safer method-level boundaries)
  • Use external editor tools for multi-line replacements near method boundaries
  • Always read the file after any anchor_* operation to verify integrity

Environment

  • MCP For Unity v9.5.3-beta.1
  • Unity 6000.3.9f1
  • Transport: SSE
  • Client: Claude Code (claude-opus-4-6)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions