{"record":{"id":"55d320a75fa2b138","repo":"CoplayDev/unity-mcp","slug":"anchor-not-found-anchor","errorCode":null,"errorMessage":"anchor not found: {anchor}","messagePattern":"anchor not found: (.+?)","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"Server/src/services/tools/script_apply_edits.py","lineNumber":421,"sourceCode":"            if not text.endswith(\"\\n\"):\n                text += \"\\n\"\n            text += append_text\n            if not text.endswith(\"\\n\"):\n                text += \"\\n\"\n        elif op == \"anchor_insert\":\n            anchor = edit.get(\"anchor\", \"\")\n            position = (edit.get(\"position\") or \"before\").lower()\n            insert_text = edit.get(\"text\", \"\")\n            flags = re.MULTILINE | (\n                re.IGNORECASE if edit.get(\"ignore_case\") else 0)\n\n            # Find the best match using improved heuristics\n            match = _find_best_anchor_match(\n                anchor, text, flags, bool(edit.get(\"prefer_last\", True)))\n            if not match:\n                if edit.get(\"allow_noop\", True):\n                    continue\n                raise RuntimeError(f\"anchor not found: {anchor}\")\n            idx = match.start() if position == \"before\" else match.end()\n            text = text[:idx] + insert_text + text[idx:]\n        elif op == \"replace_range\":\n            start_line = int(edit.get(\"startLine\", 1))\n            start_col = int(edit.get(\"startCol\", 1))\n            end_line = int(edit.get(\"endLine\", start_line))\n            end_col = int(edit.get(\"endCol\", 1))\n            replacement = edit.get(\"text\", \"\")\n            lines = text.splitlines(keepends=True)\n            max_line = len(lines) + 1  # 1-based, exclusive end\n            if (start_line < 1 or end_line < start_line or end_line > max_line\n                    or start_col < 1 or end_col < 1):\n                raise RuntimeError(\"replace_range out of bounds\")\n\n            def index_of(line: int, col: int) -> int:\n                if line <= len(lines):\n                    return sum(len(l) for l in lines[: line - 1]) + (col - 1)\n                return sum(len(l) for l in lines)","sourceCodeStart":403,"sourceCodeEnd":439,"githubUrl":"https://github.com/CoplayDev/unity-mcp/blob/c21bf496bca87d54e75bad048563c3adb1782081/Server/src/services/tools/script_apply_edits.py#L403-L439","documentation":"In script_apply_edits, the anchor_insert operation searches for an anchor pattern in the file text using _find_best_anchor_match (a heuristic regex matcher). If no match is found AND the edit's allow_noop flag is False (or absent, since the default is True), this RuntimeError fires with the unmatched anchor. When allow_noop is True (default), a missing anchor is silently skipped (continue), so this error only fires when the caller explicitly set allow_noop=False.","triggerScenarios":"An edit with op=anchor_insert, allow_noop=false, and an anchor string that does not appear in the file (e.g., the file changed since the anchor was computed); the anchor has leading/trailing whitespace that doesn't match; ignore_case is False and the case differs; the anchor was computed against a different version of the file.","commonSituations":"An AI computed an anchor from a stale file read (file was edited by another process or a prior edit in the same batch shifted content); the anchor has subtle whitespace differences; case sensitivity mismatch; the anchor targets a code region that was already removed.","solutions":["Re-read the current file content and recompute the anchor to match the latest text.","Set allow_noop=true (or omit it — default is True) so a missing anchor is skipped instead of raising.","Enable ignore_case=true if the mismatch is case-related.","Simplify the anchor to a more stable, unique substring (e.g., a method signature line)."],"exampleFix":"// before\nedits=[{\"op\": \"anchor_insert\", \"anchor\": \"class OldName {\", \"position\": \"before\", \"text\": \"// header\\n\", \"allow_noop\": false}]\n// after — re-read file, use current anchor, and allow noop\nedits=[{\"op\": \"anchor_insert\", \"anchor\": \"class NewName {\", \"position\": \"before\", \"text\": \"// header\\n\"}]","handlingStrategy":"validation","validationCode":"import re\n\ndef anchor_exists(anchor: str, text: str, ignore_case: bool = False) -> bool:\n    flags = re.MULTILINE | (re.IGNORECASE if ignore_case else 0)\n    return re.search(anchor, text, flags) is not None\n\n# Before applying edits, verify anchors against the CURRENT file text:\nfor edit in edits:\n    if edit.get(\"op\") == \"anchor_insert\":\n        if not anchor_exists(edit[\"anchor\"], current_text, edit.get(\"ignore_case\", False)):\n            if not edit.get(\"allow_noop\", True):\n                print(f\"Anchor not found and allow_noop is False: {edit['anchor']}\")","typeGuard":"import re\n\ndef will_anchor_match(edit: dict, text: str) -> bool:\n    if edit.get(\"op\") != \"anchor_insert\":\n        return True\n    flags = re.MULTILINE | (re.IGNORECASE if edit.get(\"ignore_case\") else 0)\n    return re.search(edit.get(\"anchor\", \"\"), text, flags) is not None","tryCatchPattern":null,"preventionTips":["Re-read the file and compute anchors against the latest text before applying edits.","Leave allow_noop at its default (True) so missing anchors are skipped, not fatal.","Use unique, stable anchors (e.g., method signatures) to avoid ambiguity and drift."],"tags":["script-edit","anchor-insert","pattern-not-found","mcp-tool"],"backgroundTag":null,"analyzedSha":"c21bf496bca87d54e75bad048563c3adb1782081","analyzedAt":"2026-08-13T17:36:56.095Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}