You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hugegraph.apache.org by ji...@apache.org on 2026/09/26 12:27:33 UTC

(hugegraph-ai) 06/11: feat(mcp): persist confirmed write plans

This is an automated email from the ASF dual-hosted git repository.

imbajin pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hugegraph-ai.git

commit 5132b59443deee7945b03e4a038993cb381e339d
Author: duyifeng01 <du...@baidu.com>
AuthorDate: Fri Sep 4 23:08:49 2026 +0800

    feat(mcp): persist confirmed write plans
    
    - Add durable server-issued plans and operation receipts.
    - Coordinate confirmation, replay protection, and reconciliation.
    - Test confirmed-write safety and failure outcomes.
---
 .../hugegraph_mcp/confirmable_workflow.py          | 205 ++++-
 hugegraph-mcp/hugegraph_mcp/confirmation_store.py  | 196 ++++-
 hugegraph-mcp/hugegraph_mcp/plan_store.py          | 854 +++++++++++++++++++++
 hugegraph-mcp/hugegraph_mcp/reconciler.py          | 202 +++++
 hugegraph-mcp/hugegraph_mcp/server.py              | 297 ++++---
 .../hugegraph_mcp/tools/graph_data_execute.py      | 485 ++++++++----
 .../hugegraph_mcp/tools/graph_data_gremlin.py      |  25 +-
 .../hugegraph_mcp/tools/graph_data_validate.py     |  89 ++-
 .../hugegraph_mcp/tools/graph_write_adapter.py     | 446 +++++++++++
 .../hugegraph_mcp/tools/graph_write_plan.py        | 230 ++++++
 .../hugegraph_mcp/tools/ingest_graph_data.py       | 448 ++++-------
 .../hugegraph_mcp/tools/manage_graph_data.py       | 308 ++++++--
 hugegraph-mcp/hugegraph_mcp/tools/manage_schema.py | 488 ++++++------
 .../hugegraph_mcp/tools/mutate_graph_properties.py | 337 ++------
 .../hugegraph_mcp/tools/property_validation.py     | 198 +++++
 .../hugegraph_mcp/tools/schema_write_adapter.py    | 214 ++++++
 hugegraph-mcp/hugegraph_mcp/write_contract.py      | 109 +++
 hugegraph-mcp/hugegraph_mcp/write_executor.py      | 317 ++++++++
 hugegraph-mcp/hugegraph_mcp/write_plan.py          | 321 ++++++++
 .../tests/contracts/write_confirmation_v2.json     |  42 +
 .../tests/integration/test_real_write_path.py      | 634 ++++++++++-----
 hugegraph-mcp/tests/test_confirm_write.py          | 243 ++++++
 hugegraph-mcp/tests/test_graph_write_workflow.py   | 408 ++++++++++
 hugegraph-mcp/tests/test_ingest_graph_data.py      | 377 +++++----
 hugegraph-mcp/tests/test_manage_graph_data.py      | 652 +++++++++++-----
 hugegraph-mcp/tests/test_manage_schema.py          | 682 ++++++++--------
 .../tests/test_mutate_graph_properties_tool.py     | 355 +++------
 hugegraph-mcp/tests/test_plan_hash.py              | 234 +++---
 hugegraph-mcp/tests/test_plan_store_interface.py   |  63 ++
 hugegraph-mcp/tests/test_plan_store_migration.py   | 440 +++++++++++
 hugegraph-mcp/tests/test_property_validation.py    | 170 ++++
 hugegraph-mcp/tests/test_reconciler.py             | 217 ++++++
 hugegraph-mcp/tests/test_schema_write_workflow.py  | 145 ++++
 hugegraph-mcp/tests/test_v1_stable_tools.py        | 243 ++++--
 .../tests/test_write_confirmation_contract.py      | 165 ++++
 hugegraph-mcp/tests/test_write_executor_faults.py  | 363 +++++++++
 hugegraph-mcp/tests/test_write_plan.py             | 224 ++++++
 37 files changed, 8848 insertions(+), 2578 deletions(-)

diff --git a/hugegraph-mcp/hugegraph_mcp/confirmable_workflow.py b/hugegraph-mcp/hugegraph_mcp/confirmable_workflow.py
index 24815bc2..2087852e 100644
--- a/hugegraph-mcp/hugegraph_mcp/confirmable_workflow.py
+++ b/hugegraph-mcp/hugegraph_mcp/confirmable_workflow.py
@@ -13,6 +13,7 @@
 
 """Small helpers for dry-run -> plan_hash -> confirm write workflows."""
 
+import time
 from typing import Any
 
 from hugegraph_mcp.config import MCPConfig
@@ -24,17 +25,24 @@ from hugegraph_mcp.confirmation_store import (
     ConfirmationStore,
     ConfirmationStoreUnavailableError,
 )
-from hugegraph_mcp.envelope import ErrorType, envelope_err
+from hugegraph_mcp.envelope import ErrorType, envelope_err, envelope_ok
 from hugegraph_mcp.plan_hash import PlanContext, verify_plan_hash
+from hugegraph_mcp.write_plan import ApplyStatus, PlanStatus
 
 
-def issue_plan(context: PlanContext, plan_hash: str) -> dict[str, Any] | None:
+def issue_plan(
+    context: PlanContext,
+    plan_hash: str,
+    *,
+    plan_payload: Any | None = None,
+) -> dict[str, Any] | None:
     """Persist a server-issued plan before its dry-run response is returned."""
     try:
         ConfirmationStore.from_config().issue(
             nonce=context.nonce,
             plan_hash=plan_hash,
             expires_at=context.expires_at,
+            plan_payload=plan_payload,
         )
     except ConfirmationPlanExpiredError:
         return plan_hash_error(
@@ -61,25 +69,99 @@ def issue_plan(context: PlanContext, plan_hash: str) -> dict[str, Any] | None:
     return None
 
 
-def replayed_plan_error(
-    nonce: str | None, *, source: str | None = None
-) -> dict[str, Any] | None:
+def load_issued_plan(
+    *, nonce: str | None, plan_hash: str | None, expires_at: float | None
+) -> tuple[Any | None, dict[str, Any] | None]:
+    """Load a server-issued payload without consuming its one-shot confirmation."""
+    if not nonce or not plan_hash or expires_at is None:
+        return None, plan_hash_error(
+            error_type=ErrorType.PLAN_HASH_MISMATCH,
+            details={"reason": "Plan confirmation fields are incomplete."},
+            mismatch_message="No server-issued dry-run plan matches this confirmation.",
+            suggestion="Run dry_run again and use all returned confirmation fields.",
+        )
+    try:
+        normalized_expires_at = int(expires_at)
+    except (TypeError, ValueError, OverflowError):
+        return None, plan_hash_error(
+            error_type=ErrorType.PLAN_HASH_MISMATCH,
+            details={"reason": "Plan expiry is invalid."},
+            mismatch_message="The confirmation plan expiry is invalid.",
+            suggestion="Run dry_run again and use the returned confirmation plan.",
+        )
+    if normalized_expires_at < int(time.time()):
+        return None, plan_hash_error(
+            error_type=ErrorType.PLAN_EXPIRED,
+            details={"reason": "The server-issued plan has expired."},
+            mismatch_message="The server-issued plan has expired.",
+            expired_message="The server-issued plan has expired.",
+            suggestion="Run dry_run again and use the returned confirmation plan.",
+        )
+    try:
+        payload = ConfirmationStore.from_config().issued_payload(
+            nonce=nonce,
+            plan_hash=plan_hash,
+            expires_at=normalized_expires_at,
+        )
+    except ConfirmationNotIssuedError:
+        error_type = ErrorType.PLAN_HASH_MISMATCH
+        reason = "No server-issued dry-run plan matches this confirmation."
+    except ConfirmationPlanMismatchError:
+        error_type = ErrorType.PLAN_HASH_MISMATCH
+        reason = "The confirmation does not match the server-issued plan."
+    except ConfirmationPlanExpiredError:
+        error_type = ErrorType.PLAN_EXPIRED
+        reason = "The server-issued plan has expired."
+    except ConfirmationStoreUnavailableError:
+        error_type = ErrorType.SERVER_ERROR
+        reason = "Confirmation state is unavailable."
+    else:
+        return payload, None
+    return None, plan_hash_error(
+        error_type=error_type,
+        details={"reason": reason},
+        mismatch_message="No server-issued dry-run plan matches this confirmation.",
+        expired_message="The server-issued plan has expired.",
+        suggestion="Run dry_run again and use the returned confirmation plan.",
+    )
+
+
+def replayed_plan_error(nonce: str | None, *, source: str | None = None) -> dict[str, Any] | None:
     """Return a replay error before live-state validation when already consumed."""
     try:
-        consumed = ConfirmationStore.from_config().has_consumed(nonce)
+        store = ConfirmationStore.from_config()
+        consumed = store.has_consumed(nonce)
     except ConfirmationStoreUnavailableError:
         # Unknown state is not treated as unused. Full validation continues and
         # the atomic consume remains fail-closed before the first write.
         return None
     if not consumed:
         return None
+    try:
+        operation = store.operation_for_nonce(nonce)
+    except ConfirmationStoreUnavailableError:
+        operation = None
+    unresolved_statuses = {
+        PlanStatus.EXECUTING.value,
+        PlanStatus.UNKNOWN.value,
+    }
+    if operation and operation.get("status") in unresolved_statuses:
+        return envelope_err(
+            ErrorType.WRITE_OUTCOME_UNKNOWN,
+            "The prior write attempt has no confirmed outcome; reconcile before retrying.",
+            suggestion="Inspect the target state and query the persisted write status.",
+            retryable=False,
+            details={
+                "status": PlanStatus.UNKNOWN.value,
+                "plan_hash": operation.get("plan_hash"),
+                "reconciliation_required": True,
+            },
+            source=source or "hugegraph-mcp",
+        )
     return plan_hash_error(
         error_type=ErrorType.PLAN_ALREADY_USED,
         details={
-            "reason": (
-                "This confirmation has already been used. Inspect the target "
-                "state and run dry_run again."
-            )
+            "reason": ("This confirmation has already been used. Inspect the target state and run dry_run again.")
         },
         mismatch_message="This confirmation plan has already been used.",
         suggestion="Inspect the current target state, then run dry_run again.",
@@ -87,6 +169,49 @@ def replayed_plan_error(
     )
 
 
+def record_write_outcome(
+    *,
+    plan_hash: str,
+    status: PlanStatus | ApplyStatus | str,
+    receipt: dict[str, Any],
+) -> bool:
+    """Persist an execution receipt; return False if durability is unavailable."""
+    persisted_status = PlanStatus(status.value if isinstance(status, (PlanStatus, ApplyStatus)) else status)
+    try:
+        ConfirmationStore.from_config().record_outcome(
+            plan_hash=plan_hash,
+            status=persisted_status,
+            receipt=receipt,
+        )
+    except (ConfirmationNotIssuedError, ConfirmationStoreUnavailableError):
+        return False
+    return True
+
+
+def get_write_status(plan_hash: str) -> dict[str, Any]:
+    """Read the durable state of a confirmed write without exposing its payload."""
+    try:
+        operation = ConfirmationStore.from_config().operation_for_plan(plan_hash)
+    except ConfirmationStoreUnavailableError:
+        return envelope_err(
+            ErrorType.SERVER_ERROR,
+            "Write status storage is unavailable.",
+            retryable=False,
+        )
+    if operation is None:
+        return envelope_err(
+            ErrorType.NOT_FOUND,
+            "No persisted write operation matches this plan hash.",
+            retryable=False,
+        )
+    public = dict(operation)
+    public.pop("plan", None)
+    if public.get("status") == PlanStatus.EXECUTING.value:
+        public["status"] = PlanStatus.UNKNOWN.value
+        public["reconciliation_required"] = True
+    return envelope_ok(public)
+
+
 def verify_and_consume_plan(
     *,
     submitted_hash: str,
@@ -99,6 +224,31 @@ def verify_and_consume_plan(
     extra_context: dict[str, Any] | None = None,
 ) -> tuple[bool, ErrorType | None, dict[str, Any] | None]:
     """Validate a plan without side effects, then atomically consume its nonce."""
+    valid, error_type, details, _payload = verify_and_consume_issued_plan(
+        submitted_hash=submitted_hash,
+        tool_name=tool_name,
+        mode=mode,
+        payload_digest=payload_digest,
+        schema_hash=schema_hash,
+        nonce=nonce,
+        expires_at=expires_at,
+        extra_context=extra_context,
+    )
+    return valid, error_type, details
+
+
+def verify_and_consume_issued_plan(
+    *,
+    submitted_hash: str,
+    tool_name: str,
+    mode: str,
+    payload_digest: str,
+    schema_hash: str | None = None,
+    nonce: str | None = None,
+    expires_at: float | None = None,
+    extra_context: dict[str, Any] | None = None,
+) -> tuple[bool, ErrorType | None, dict[str, Any] | None, Any | None]:
+    """Validate, atomically consume, and return the exact persisted payload."""
     valid, error_type, details = verify_plan_hash(
         submitted_hash=submitted_hash,
         tool_name=tool_name,
@@ -110,17 +260,18 @@ def verify_and_consume_plan(
         extra_context=extra_context,
     )
     if not valid:
-        return False, error_type, details
+        return False, error_type, details, None
 
     if MCPConfig.from_env().is_readonly():
         return (
             False,
             ErrorType.READONLY_VIOLATION,
             {"reason": "Write confirmation is disabled in readonly mode."},
+            None,
         )
 
     try:
-        ConfirmationStore.from_config().consume(
+        payload = ConfirmationStore.from_config().consume(
             nonce=nonce or "",
             plan_hash=submitted_hash,
             expires_at=int(expires_at or 0),
@@ -129,43 +280,38 @@ def verify_and_consume_plan(
         return (
             False,
             ErrorType.PLAN_ALREADY_USED,
-            {
-                "reason": (
-                    "This confirmation has already been used. Inspect the target "
-                    "state and run dry_run again."
-                )
-            },
+            {"reason": ("This confirmation has already been used. Inspect the target state and run dry_run again.")},
+            None,
         )
     except ConfirmationNotIssuedError:
         return (
             False,
             ErrorType.PLAN_HASH_MISMATCH,
             {"reason": "No server-issued dry-run plan matches this confirmation."},
+            None,
         )
     except ConfirmationPlanMismatchError:
         return (
             False,
             ErrorType.PLAN_HASH_MISMATCH,
             {"reason": "The confirmation does not match the server-issued plan."},
+            None,
         )
     except ConfirmationPlanExpiredError:
         return (
             False,
             ErrorType.PLAN_EXPIRED,
             {"reason": "The server-issued plan has expired."},
+            None,
         )
     except ConfirmationStoreUnavailableError:
         return (
             False,
             ErrorType.SERVER_ERROR,
-            {
-                "reason": (
-                    "Confirmation state is unavailable. The write was blocked "
-                    "before execution."
-                )
-            },
+            {"reason": ("Confirmation state is unavailable. The write was blocked before execution.")},
+            None,
         )
-    return True, None, None
+    return True, None, None, payload
 
 
 def mark_readonly_preview(
@@ -207,15 +353,10 @@ def plan_hash_error(
         message = expired_message
     elif resolved_error_type == ErrorType.PLAN_ALREADY_USED:
         message = "This confirmation plan has already been used."
-        suggestion = (
-            "Inspect the current target state, then run dry_run again and use the "
-            "new confirmation plan."
-        )
+        suggestion = "Inspect the current target state, then run dry_run again and use the new confirmation plan."
     elif resolved_error_type == ErrorType.SERVER_ERROR:
         message = "Confirmation state could not be recorded safely."
-        suggestion = (
-            "Restore writable persistent confirmation state, then run dry_run again."
-        )
+        suggestion = "Restore writable persistent confirmation state, then run dry_run again."
     else:
         message = mismatch_message
     return envelope_err(
diff --git a/hugegraph-mcp/hugegraph_mcp/confirmation_store.py b/hugegraph-mcp/hugegraph_mcp/confirmation_store.py
index 6a543776..c1f9376a 100644
--- a/hugegraph-mcp/hugegraph_mcp/confirmation_store.py
+++ b/hugegraph-mcp/hugegraph_mcp/confirmation_store.py
@@ -18,13 +18,16 @@
 from __future__ import annotations
 
 import hashlib
+import json
 import os
 import sqlite3
 import time
 from pathlib import Path
+from typing import Any
 
 from hugegraph_mcp.config import MCPConfig
 from hugegraph_mcp.plan_hash import DEFAULT_PLAN_TTL_SECONDS
+from hugegraph_mcp.write_plan import PlanStatus
 
 
 class ConfirmationAlreadyUsedError(Exception):
@@ -74,15 +77,80 @@ class ConfirmationStore:
                     """,
                     (nonce_digest,),
                 ).fetchone()
-        except (OSError, sqlite3.Error) as exc:
+        except (OSError, sqlite3.Error, json.JSONDecodeError) as exc:
             raise ConfirmationStoreUnavailableError from exc
         return row is not None
 
-    def issue(self, *, nonce: str, plan_hash: str, expires_at: int) -> None:
+    def operation_for_nonce(self, nonce: str | None) -> dict[str, Any] | None:
+        """Return durable execution state for a consumed nonce."""
+        if not nonce or not self.database_path.exists():
+            return None
+        nonce_digest = hashlib.sha256(nonce.encode("utf-8")).hexdigest()
+        return self._operation_row("nonce_digest", nonce_digest)
+
+    def operation_for_plan(self, plan_hash: str | None) -> dict[str, Any] | None:
+        """Return durable execution state for a plan hash."""
+        if not plan_hash or not self.database_path.exists():
+            return None
+        return self._operation_row("plan_hash", plan_hash)
+
+    def record_outcome(
+        self,
+        *,
+        plan_hash: str,
+        status: PlanStatus,
+        receipt: dict[str, Any],
+    ) -> None:
+        """Persist a final or reconcilable outcome after execution."""
+        try:
+            receipt_json = json.dumps(
+                receipt,
+                ensure_ascii=False,
+                separators=(",", ":"),
+                sort_keys=True,
+            )
+            self._prepare_storage()
+            with sqlite3.connect(self.database_path, timeout=30) as connection:
+                connection.execute("PRAGMA synchronous = FULL")
+                self._ensure_schema(connection)
+                updated = connection.execute(
+                    """
+                    UPDATE write_operations
+                    SET status = ?, receipt_json = ?, updated_at = ?
+                    WHERE plan_hash = ?
+                    """,
+                    (status.value, receipt_json, int(time.time()), plan_hash),
+                )
+                if updated.rowcount != 1:
+                    raise ConfirmationNotIssuedError
+                connection.commit()
+        except ConfirmationNotIssuedError:
+            raise
+        except (OSError, sqlite3.Error, TypeError) as exc:
+            raise ConfirmationStoreUnavailableError from exc
+
+    def issue(
+        self,
+        *,
+        nonce: str,
+        plan_hash: str,
+        expires_at: int,
+        plan_payload: Any | None = None,
+    ) -> None:
         """Persist a server-issued dry-run plan before returning it to the caller."""
         nonce_digest = hashlib.sha256(nonce.encode("utf-8")).hexdigest()
         issued_at = int(time.time())
         expires_at = int(expires_at)
+        payload_json = (
+            json.dumps(
+                plan_payload,
+                ensure_ascii=False,
+                separators=(",", ":"),
+                sort_keys=True,
+            )
+            if plan_payload is not None
+            else None
+        )
         if expires_at <= issued_at or expires_at > issued_at + DEFAULT_PLAN_TTL_SECONDS:
             raise ConfirmationPlanExpiredError
 
@@ -95,31 +163,66 @@ class ConfirmationStore:
                 connection.execute(
                     """
                     INSERT INTO issued_confirmations (
-                        nonce_digest, plan_hash, expires_at, issued_at
-                    ) VALUES (?, ?, ?, ?)
+                        nonce_digest, plan_hash, expires_at, issued_at, plan_payload_json
+                    ) VALUES (?, ?, ?, ?, ?)
                     ON CONFLICT(nonce_digest) DO UPDATE SET
                         plan_hash = excluded.plan_hash,
                         expires_at = excluded.expires_at,
-                        issued_at = excluded.issued_at
+                        issued_at = excluded.issued_at,
+                        plan_payload_json = excluded.plan_payload_json
                     WHERE issued_confirmations.plan_hash = excluded.plan_hash
                     """,
-                    (nonce_digest, plan_hash, expires_at, issued_at),
+                    (nonce_digest, plan_hash, expires_at, issued_at, payload_json),
                 )
                 row = connection.execute(
                     """
-                    SELECT plan_hash, expires_at FROM issued_confirmations
+                    SELECT plan_hash, expires_at, plan_payload_json
+                    FROM issued_confirmations
                     WHERE nonce_digest = ?
                     """,
                     (nonce_digest,),
                 ).fetchone()
-                if row != (plan_hash, expires_at):
+                if row != (plan_hash, expires_at, payload_json):
                     raise ConfirmationPlanMismatchError
         except (ConfirmationPlanExpiredError, ConfirmationPlanMismatchError):
             raise
-        except (OSError, sqlite3.Error) as exc:
+        except (OSError, sqlite3.Error, json.JSONDecodeError) as exc:
             raise ConfirmationStoreUnavailableError from exc
 
-    def consume(self, *, nonce: str, plan_hash: str, expires_at: int) -> None:
+    def issued_payload(self, *, nonce: str, plan_hash: str, expires_at: int) -> Any | None:
+        """Load the immutable server-side payload bound to an issued plan."""
+        nonce_digest = hashlib.sha256(nonce.encode("utf-8")).hexdigest()
+        current_time = int(time.time())
+        try:
+            if not self.database_path.exists():
+                raise ConfirmationNotIssuedError
+            with sqlite3.connect(self.database_path, timeout=30) as connection:
+                self._ensure_schema(connection)
+                row = connection.execute(
+                    """
+                    SELECT plan_hash, expires_at, issued_at, plan_payload_json
+                    FROM issued_confirmations WHERE nonce_digest = ?
+                    """,
+                    (nonce_digest,),
+                ).fetchone()
+            if row is None:
+                raise ConfirmationNotIssuedError
+            issued_hash, issued_expires_at, issued_at, payload_json = row
+            if issued_hash != plan_hash or issued_expires_at != int(expires_at):
+                raise ConfirmationPlanMismatchError
+            if current_time > issued_expires_at or issued_expires_at > issued_at + DEFAULT_PLAN_TTL_SECONDS:
+                raise ConfirmationPlanExpiredError
+            return json.loads(payload_json) if payload_json is not None else None
+        except (
+            ConfirmationNotIssuedError,
+            ConfirmationPlanMismatchError,
+            ConfirmationPlanExpiredError,
+        ):
+            raise
+        except (OSError, sqlite3.Error, json.JSONDecodeError) as exc:
+            raise ConfirmationStoreUnavailableError from exc
+
+    def consume(self, *, nonce: str, plan_hash: str, expires_at: int) -> Any | None:
         """Atomically validate and consume a server-issued dry-run plan."""
         nonce_digest = hashlib.sha256(nonce.encode("utf-8")).hexdigest()
         consumed_at = int(time.time())
@@ -146,20 +249,17 @@ class ConfirmationStore:
                         raise ConfirmationAlreadyUsedError
                     issued = connection.execute(
                         """
-                        SELECT plan_hash, expires_at, issued_at
+                        SELECT plan_hash, expires_at, issued_at, plan_payload_json
                         FROM issued_confirmations WHERE nonce_digest = ?
                         """,
                         (nonce_digest,),
                     ).fetchone()
                     if issued is None:
                         raise ConfirmationNotIssuedError
-                    issued_hash, issued_expires_at, issued_at = issued
+                    issued_hash, issued_expires_at, issued_at, payload_json = issued
                     if issued_hash != plan_hash or issued_expires_at != int(expires_at):
                         raise ConfirmationPlanMismatchError
-                    if (
-                        consumed_at > issued_expires_at
-                        or issued_expires_at > issued_at + DEFAULT_PLAN_TTL_SECONDS
-                    ):
+                    if consumed_at > issued_expires_at or issued_expires_at > issued_at + DEFAULT_PLAN_TTL_SECONDS:
                         raise ConfirmationPlanExpiredError
                     connection.execute(
                         """
@@ -169,11 +269,27 @@ class ConfirmationStore:
                         """,
                         (nonce_digest, plan_hash, int(expires_at), consumed_at),
                     )
+                    connection.execute(
+                        """
+                        INSERT INTO write_operations (
+                            plan_hash, nonce_digest, plan_payload_json,
+                            status, receipt_json, updated_at
+                        ) VALUES (?, ?, ?, ?, NULL, ?)
+                        """,
+                        (
+                            plan_hash,
+                            nonce_digest,
+                            payload_json,
+                            PlanStatus.EXECUTING.value,
+                            consumed_at,
+                        ),
+                    )
                     connection.execute(
                         "DELETE FROM issued_confirmations WHERE nonce_digest = ?",
                         (nonce_digest,),
                     )
                     connection.commit()
+                    return json.loads(payload_json) if payload_json is not None else None
                 except sqlite3.IntegrityError as exc:
                     raise ConfirmationAlreadyUsedError from exc
         except (
@@ -183,12 +299,10 @@ class ConfirmationStore:
             ConfirmationPlanExpiredError,
         ):
             raise
-        except (OSError, sqlite3.Error) as exc:
+        except (OSError, sqlite3.Error, json.JSONDecodeError) as exc:
             raise ConfirmationStoreUnavailableError from exc
 
-    def _cleanup_expired(
-        self, connection: sqlite3.Connection, current_time: int
-    ) -> None:
+    def _cleanup_expired(self, connection: sqlite3.Connection, current_time: int) -> None:
         """Best-effort cleanup that never authorizes or blocks a current plan."""
         connection.execute(
             "DELETE FROM consumed_confirmations WHERE expires_at < ?",
@@ -218,12 +332,52 @@ class ConfirmationStore:
                 nonce_digest TEXT PRIMARY KEY,
                 plan_hash TEXT NOT NULL,
                 expires_at INTEGER NOT NULL,
-                issued_at INTEGER NOT NULL
+                issued_at INTEGER NOT NULL,
+                plan_payload_json TEXT
+            )
+            """
+        )
+        columns = {row[1] for row in connection.execute("PRAGMA table_info(issued_confirmations)")}
+        if "plan_payload_json" not in columns:
+            connection.execute("ALTER TABLE issued_confirmations ADD COLUMN plan_payload_json TEXT")
+        connection.execute(
+            """
+            CREATE TABLE IF NOT EXISTS write_operations (
+                plan_hash TEXT PRIMARY KEY,
+                nonce_digest TEXT NOT NULL UNIQUE,
+                plan_payload_json TEXT,
+                status TEXT NOT NULL,
+                receipt_json TEXT,
+                updated_at INTEGER NOT NULL
             )
             """
         )
         connection.commit()
 
+    def _operation_row(self, field: str, value: str) -> dict[str, Any] | None:
+        if field not in {"nonce_digest", "plan_hash"}:
+            raise ValueError("Unsupported operation lookup field")
+        query = (
+            "SELECT plan_hash, status, plan_payload_json, receipt_json, updated_at "
+            f"FROM write_operations WHERE {field} = ?"
+        )
+        try:
+            with sqlite3.connect(self.database_path, timeout=30) as connection:
+                self._ensure_schema(connection)
+                row = connection.execute(query, (value,)).fetchone()
+            if row is None:
+                return None
+            plan_hash, status, payload_json, receipt_json, updated_at = row
+            return {
+                "plan_hash": plan_hash,
+                "status": status,
+                "plan": json.loads(payload_json) if payload_json else None,
+                "receipt": json.loads(receipt_json) if receipt_json else None,
+                "updated_at": updated_at,
+            }
+        except (OSError, sqlite3.Error, json.JSONDecodeError) as exc:
+            raise ConfirmationStoreUnavailableError from exc
+
     def _prepare_storage(self) -> None:
         self.state_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
         _restrict_permissions(self.state_dir, 0o700)
diff --git a/hugegraph-mcp/hugegraph_mcp/plan_store.py b/hugegraph-mcp/hugegraph_mcp/plan_store.py
new file mode 100644
index 00000000..42ba3fa7
--- /dev/null
+++ b/hugegraph-mcp/hugegraph_mcp/plan_store.py
@@ -0,0 +1,854 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0.
+
+"""Versioned SQLite persistence for immutable write plans and operations."""
+
+from __future__ import annotations
+
+import json
+import os
+import sqlite3
+import time
+import uuid
+from pathlib import Path
+from typing import Any, Protocol, runtime_checkable
+
+from hugegraph_mcp.write_plan import (
+    ApplyReceipt,
+    ApplyStatus,
+    GraphTarget,
+    OperationPlan,
+    PlanStatus,
+    WritePlan,
+    aggregate_plan_status,
+    can_transition,
+    canonical_plan_json,
+)
+
+SCHEMA_VERSION = 3
+
+
+class PlanTransitionError(Exception):
+    """The requested plan transition does not match durable state."""
+
+
+class PlanStoreUnavailableError(Exception):
+    """The configured durable plan store cannot safely serve writes."""
+
+
+@runtime_checkable
+class PlanStore(Protocol):
+    def prepare(self) -> None: ...
+
+    def save_plan(self, plan: WritePlan) -> None: ...
+
+    def get_plan(self, plan_id: str) -> WritePlan | None: ...
+
+    def get_plan_record(self, plan_id: str) -> dict[str, Any] | None: ...
+
+    def transition_plan(
+        self,
+        plan_id: str,
+        *,
+        expected: PlanStatus,
+        target: PlanStatus,
+    ) -> WritePlan: ...
+
+    def record_receipt(
+        self,
+        receipt: ApplyReceipt,
+        *,
+        owner_token: str | None = None,
+        attempt_token: str | None = None,
+    ) -> None: ...
+
+    def claim_plan(
+        self,
+        plan_id: str,
+        *,
+        owner_token: str | None = None,
+        lease_seconds: int = 30,
+        resume: bool = False,
+    ) -> tuple[WritePlan, bool]: ...
+
+    def claim_operation(
+        self,
+        plan_id: str,
+        operation_id: str,
+        *,
+        owner_token: str | None = None,
+        attempt_token: str | None = None,
+        lease_seconds: int = 30,
+    ) -> int: ...
+
+    def begin_reconcile(self, plan_id: str) -> WritePlan: ...
+
+    def record_reconciliation_receipt(self, receipt: ApplyReceipt) -> None: ...
+
+
+class SQLitePlanStore:
+    DATABASE_NAME = "write_plans.sqlite3"
+    LEGACY_DATABASE_NAME = "confirmations.sqlite3"
+
+    def __init__(self, state_dir: Path):
+        self.state_dir = Path(state_dir)
+        self.database_path = self.state_dir / self.DATABASE_NAME
+        self.legacy_database_path = self.state_dir / self.LEGACY_DATABASE_NAME
+
+    @classmethod
+    def from_config(cls, cfg=None) -> SQLitePlanStore:
+        if cfg is None:
+            from hugegraph_mcp.config import MCPConfig
+
+            cfg = MCPConfig.from_env()
+        return cls(cfg.state_dir)
+
+    @staticmethod
+    def legacy_plan_id(plan_hash: str) -> str:
+        return f"wp_legacy_{plan_hash}"
+
+    @staticmethod
+    def legacy_operation_id(plan_hash: str) -> str:
+        return f"op_legacy_{plan_hash}"
+
+    def prepare(self) -> None:
+        self._prepare_storage()
+        with sqlite3.connect(self.database_path, timeout=30) as connection:
+            connection.execute("PRAGMA foreign_keys = ON")
+            connection.execute("PRAGMA synchronous = FULL")
+            version = int(connection.execute("PRAGMA user_version").fetchone()[0])
+            if version > SCHEMA_VERSION:
+                raise PlanStoreUnavailableError(
+                    f"write plan database schema {version} is newer than supported {SCHEMA_VERSION}"
+                )
+            self._migrate_schema(connection, version)
+            self._migrate_legacy(connection)
+            connection.commit()
+
+    def save_plan(self, plan: WritePlan) -> None:
+        """Persist one immutable plan and its ordered operations atomically."""
+        self.prepare()
+        payload_json = canonical_plan_json(plan)
+        with sqlite3.connect(self.database_path, timeout=30) as connection:
+            connection.execute("PRAGMA foreign_keys = ON")
+            connection.execute("BEGIN IMMEDIATE")
+            connection.execute(
+                """
+                INSERT INTO write_plans (
+                    plan_id, tool_name, graph_target_json, principal,
+                    payload_json, payload_digest, schema_fingerprint, status,
+                    created_at, expires_at, confirmed_at
+                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)
+                """,
+                (
+                    plan.plan_id,
+                    plan.tool_name,
+                    self._canonical_json(plan.graph_target.to_dict()),
+                    plan.principal,
+                    payload_json,
+                    plan.payload_digest,
+                    plan.schema_fingerprint,
+                    plan.status.value,
+                    plan.created_at,
+                    plan.expires_at,
+                ),
+            )
+            for ordinal, operation in enumerate(plan.operations):
+                connection.execute(
+                    """
+                    INSERT INTO write_operations (
+                        operation_id, plan_id, ordinal, kind, payload_json,
+                        status, attempt, receipt_json, updated_at
+                    ) VALUES (?, ?, ?, ?, ?, ?, 0, NULL, ?)
+                    """,
+                    (
+                        operation.operation_id,
+                        plan.plan_id,
+                        ordinal,
+                        operation.kind,
+                        self._canonical_json(operation.to_dict()),
+                        PlanStatus.ISSUED.value,
+                        plan.created_at,
+                    ),
+                )
+            connection.commit()
+
+    def get_plan(self, plan_id: str) -> WritePlan | None:
+        """Rehydrate an immutable canonical plan from durable storage."""
+        record = self.get_plan_record(plan_id)
+        if record is None:
+            return None
+        # Legacy confirmation rows lack the canonical graph target and
+        # operation state required for safe execution or reconciliation.
+        if record["tool_name"] == "legacy":
+            return None
+        operations = tuple(
+            OperationPlan(
+                operation_id=item["payload"]["operation_id"],
+                kind=item["payload"]["kind"],
+                target=item["payload"]["target"],
+                expected_state=item["payload"]["expected_state"],
+                desired_state=item["payload"]["desired_state"],
+                depends_on=tuple(item["payload"].get("depends_on") or ()),
+                idempotency_key=item["payload"].get("idempotency_key"),
+            )
+            for item in record["operations"]
+        )
+        target = record["graph_target"]
+        return WritePlan(
+            plan_id=record["plan_id"],
+            tool_name=record["tool_name"],
+            graph_target=GraphTarget(
+                graph_url=target["graph_url"],
+                graph_name=target["graph_name"],
+                graphspace=target.get("graphspace"),
+            ),
+            principal=record["principal"],
+            operations=operations,
+            payload_digest=record["payload_digest"],
+            schema_fingerprint=record["schema_fingerprint"],
+            status=PlanStatus(record["status"]),
+            created_at=record["created_at"],
+            expires_at=record["expires_at"],
+        )
+
+    def transition_plan(
+        self,
+        plan_id: str,
+        *,
+        expected: PlanStatus,
+        target: PlanStatus,
+    ) -> WritePlan:
+        """Compare-and-set one legal plan transition in SQLite."""
+        if not can_transition(expected, target):
+            raise PlanTransitionError(f"illegal transition: {expected} -> {target}")
+        self.prepare()
+        with sqlite3.connect(self.database_path, timeout=30) as connection:
+            connection.execute("BEGIN IMMEDIATE")
+            confirmed_at_sql = (
+                ", confirmed_at = COALESCE(confirmed_at, strftime('%s','now'))"
+                if target is PlanStatus.EXECUTING
+                else ""
+            )
+            updated = connection.execute(
+                f"""
+                UPDATE write_plans SET status = ?{confirmed_at_sql}
+                WHERE plan_id = ? AND status = ?
+                """,
+                (target.value, plan_id, expected.value),
+            )
+            if updated.rowcount != 1:
+                connection.rollback()
+                raise PlanTransitionError("durable plan state did not match the expected state")
+            connection.commit()
+        plan = self.get_plan(plan_id)
+        if plan is None:
+            raise PlanTransitionError("plan disappeared after transition")
+        return plan
+
+    def claim_plan(
+        self,
+        plan_id: str,
+        *,
+        owner_token: str | None = None,
+        lease_seconds: int = 30,
+        resume: bool = False,
+    ) -> tuple[WritePlan, bool]:
+        """Atomically elect one executor and persist its fencing lease."""
+        if lease_seconds <= 0:
+            raise ValueError("lease_seconds must be positive")
+        self.prepare()
+        owner_token = owner_token or uuid.uuid4().hex
+        now = int(time.time())
+        claimed = False
+        with sqlite3.connect(self.database_path, timeout=30) as connection:
+            connection.execute("BEGIN IMMEDIATE")
+            row = connection.execute(
+                "SELECT status, lease_expires_at FROM write_plans WHERE plan_id = ?",
+                (plan_id,),
+            ).fetchone()
+            if row is None:
+                connection.rollback()
+                raise PlanTransitionError("plan was not found")
+            current = PlanStatus(row[0])
+            claimable = {PlanStatus.ISSUED}
+            if resume:
+                claimable.update({PlanStatus.RETRYABLE_NOT_APPLIED, PlanStatus.PARTIAL})
+            lease_available = row[1] is None or int(row[1]) <= now
+            if current in claimable and lease_available:
+                target = PlanStatus.EXECUTING if current is not PlanStatus.PARTIAL else current
+                updated = connection.execute(
+                    """
+                    UPDATE write_plans
+                    SET status = ?, confirmed_at = COALESCE(confirmed_at, ?),
+                        lease_owner = ?, lease_expires_at = ?
+                    WHERE plan_id = ? AND status = ?
+                      AND (lease_expires_at IS NULL OR lease_expires_at <= ?)
+                    """,
+                    (
+                        target.value,
+                        now,
+                        owner_token,
+                        now + lease_seconds,
+                        plan_id,
+                        current.value,
+                        now,
+                    ),
+                )
+                claimed = updated.rowcount == 1
+            connection.commit()
+        plan = self.get_plan(plan_id)
+        if plan is None:
+            raise PlanTransitionError("plan disappeared after claim")
+        return plan, claimed
+
+    def claim_operation(
+        self,
+        plan_id: str,
+        operation_id: str,
+        *,
+        owner_token: str | None = None,
+        attempt_token: str | None = None,
+        lease_seconds: int = 30,
+    ) -> int:
+        """Atomically claim one not-yet-applied operation and increment attempt."""
+        if lease_seconds <= 0:
+            raise ValueError("lease_seconds must be positive")
+        self.prepare()
+        claimable = {
+            PlanStatus.ISSUED.value,
+            PlanStatus.RETRYABLE_NOT_APPLIED.value,
+        }
+        with sqlite3.connect(self.database_path, timeout=30) as connection:
+            connection.execute("BEGIN IMMEDIATE")
+            plan_row = connection.execute(
+                "SELECT lease_owner, lease_expires_at FROM write_plans WHERE plan_id = ?",
+                (plan_id,),
+            ).fetchone()
+            now = int(time.time())
+            if plan_row is None or plan_row[1] is None or int(plan_row[1]) <= now:
+                connection.rollback()
+                raise PlanTransitionError("executor lease is absent or expired")
+            effective_owner = owner_token or str(plan_row[0])
+            if plan_row[0] != effective_owner:
+                connection.rollback()
+                raise PlanTransitionError("executor lease owner did not match")
+            row = connection.execute(
+                """
+                SELECT status, attempt FROM write_operations
+                WHERE plan_id = ? AND operation_id = ?
+                """,
+                (plan_id, operation_id),
+            ).fetchone()
+            if row is None or row[0] not in claimable:
+                connection.rollback()
+                raise PlanTransitionError("operation is not claimable")
+            attempt = int(row[1]) + 1
+            attempt_token = attempt_token or uuid.uuid4().hex
+            connection.execute(
+                """
+                UPDATE write_operations
+                SET status = ?, attempt = ?, attempt_token = ?, reconciled = 0,
+                    updated_at = ?
+                WHERE plan_id = ? AND operation_id = ?
+                """,
+                (
+                    PlanStatus.EXECUTING.value,
+                    attempt,
+                    attempt_token,
+                    now,
+                    plan_id,
+                    operation_id,
+                ),
+            )
+            renewed = connection.execute(
+                """UPDATE write_plans SET lease_expires_at = ?
+                   WHERE plan_id = ? AND lease_owner = ? AND lease_expires_at > ?""",
+                (now + lease_seconds, plan_id, effective_owner, now),
+            )
+            if renewed.rowcount != 1:
+                connection.rollback()
+                raise PlanTransitionError("executor lease could not be renewed")
+            connection.commit()
+        return attempt
+
+    def record_receipt(
+        self,
+        receipt: ApplyReceipt,
+        *,
+        owner_token: str | None = None,
+        attempt_token: str | None = None,
+    ) -> None:
+        """Persist one operation receipt and recompute the aggregate plan state."""
+        if not receipt.plan_id or not receipt.operation_id:
+            raise ValueError("canonical receipt requires plan_id and operation_id")
+        self.prepare()
+        operation_status = PlanStatus(receipt.status.value)
+        with sqlite3.connect(self.database_path, timeout=30) as connection:
+            connection.execute("BEGIN IMMEDIATE")
+            operation_row = connection.execute(
+                """
+                SELECT o.status, o.attempt, o.attempt_token, p.lease_owner,
+                       p.lease_expires_at
+                FROM write_operations o JOIN write_plans p ON p.plan_id = o.plan_id
+                WHERE o.plan_id = ? AND o.operation_id = ?
+                """,
+                (receipt.plan_id, receipt.operation_id),
+            ).fetchone()
+            if operation_row is None:
+                connection.rollback()
+                raise PlanTransitionError("operation receipt target was not found")
+            effective_owner = owner_token or operation_row[3]
+            effective_attempt_token = attempt_token or receipt.attempt_token or operation_row[2]
+            if (
+                operation_row[0] != PlanStatus.EXECUTING.value
+                or int(operation_row[1]) != receipt.attempt
+                or not effective_attempt_token
+                or operation_row[2] != effective_attempt_token
+                or operation_row[3] != effective_owner
+                or operation_row[4] is None
+                or int(operation_row[4]) <= int(time.time())
+            ):
+                connection.rollback()
+                raise PlanTransitionError("stale or unfenced operation receipt")
+            updated = connection.execute(
+                """
+                UPDATE write_operations
+                SET status = ?, attempt = ?, receipt_json = ?, updated_at = ?
+                WHERE plan_id = ? AND operation_id = ? AND status = ?
+                    AND attempt = ? AND attempt_token = ?
+                """,
+                (
+                    operation_status.value,
+                    receipt.attempt,
+                    self._canonical_json(receipt.to_dict()),
+                    receipt.committed_at or 0,
+                    receipt.plan_id,
+                    receipt.operation_id,
+                    PlanStatus.EXECUTING.value,
+                    receipt.attempt,
+                    effective_attempt_token,
+                ),
+            )
+            if updated.rowcount != 1:
+                connection.rollback()
+                raise PlanTransitionError("operation receipt target was not found")
+            rows = connection.execute(
+                "SELECT status FROM write_operations WHERE plan_id = ? ORDER BY ordinal",
+                (receipt.plan_id,),
+            ).fetchall()
+            aggregate = self._aggregate_persisted_statuses([str(row[0]) for row in rows])
+            current_row = connection.execute(
+                "SELECT status FROM write_plans WHERE plan_id = ?",
+                (receipt.plan_id,),
+            ).fetchone()
+            if current_row is None:
+                connection.rollback()
+                raise PlanTransitionError("receipt plan was not found")
+            current = PlanStatus(current_row[0])
+            if current is not aggregate and not can_transition(current, aggregate):
+                connection.rollback()
+                raise PlanTransitionError(f"illegal receipt transition: {current} -> {aggregate}")
+            keep_lease = receipt.status in {
+                ApplyStatus.APPLIED,
+                ApplyStatus.ALREADY_APPLIED,
+            } and aggregate in {PlanStatus.EXECUTING, PlanStatus.PARTIAL}
+            connection.execute(
+                """UPDATE write_plans SET status = ?,
+                       lease_owner = CASE WHEN ? THEN lease_owner ELSE NULL END,
+                       lease_expires_at = CASE WHEN ? THEN lease_expires_at ELSE NULL END
+                   WHERE plan_id = ?""",
+                (aggregate.value, keep_lease, keep_lease, receipt.plan_id),
+            )
+            connection.commit()
+
+    def begin_reconcile(self, plan_id: str) -> WritePlan:
+        """Fence an expired executor before any read-only reconciliation."""
+        self.prepare()
+        now = int(time.time())
+        with sqlite3.connect(self.database_path, timeout=30) as connection:
+            connection.execute("BEGIN IMMEDIATE")
+            row = connection.execute(
+                "SELECT status, lease_expires_at FROM write_plans WHERE plan_id = ?",
+                (plan_id,),
+            ).fetchone()
+            if row is None:
+                connection.rollback()
+                raise PlanTransitionError("plan was not found")
+            status = PlanStatus(row[0])
+            if row[1] is not None and int(row[1]) > now:
+                connection.rollback()
+                raise PlanTransitionError("execution lease is still active")
+            if status is PlanStatus.EXECUTING:
+                connection.execute(
+                    """UPDATE write_plans SET status = ?, lease_owner = NULL,
+                           lease_expires_at = NULL
+                       WHERE plan_id = ? AND status = ? AND
+                           (lease_expires_at IS NULL OR lease_expires_at <= ?)""",
+                    (
+                        PlanStatus.UNKNOWN.value,
+                        plan_id,
+                        PlanStatus.EXECUTING.value,
+                        now,
+                    ),
+                )
+            elif status in {
+                PlanStatus.UNKNOWN,
+                PlanStatus.LEGACY_UNKNOWN,
+                PlanStatus.PARTIAL,
+            }:
+                connection.execute(
+                    "UPDATE write_plans SET lease_owner = NULL, lease_expires_at = NULL WHERE plan_id = ?",
+                    (plan_id,),
+                )
+            connection.commit()
+        plan = self.get_plan(plan_id)
+        if plan is None:
+            raise PlanTransitionError("plan is not canonically rehydratable")
+        return plan
+
+    def record_reconciliation_receipt(self, receipt: ApplyReceipt) -> None:
+        """Persist evidence only after the executor has been fenced."""
+        if not receipt.plan_id or not receipt.operation_id:
+            raise ValueError("canonical receipt requires plan_id and operation_id")
+        self.prepare()
+        with sqlite3.connect(self.database_path, timeout=30) as connection:
+            connection.execute("BEGIN IMMEDIATE")
+            plan_row = connection.execute(
+                "SELECT lease_owner FROM write_plans WHERE plan_id = ?",
+                (receipt.plan_id,),
+            ).fetchone()
+            if plan_row is None or plan_row[0] is not None:
+                connection.rollback()
+                raise PlanTransitionError("reconciliation requires a fenced executor")
+            updated = connection.execute(
+                """UPDATE write_operations SET status = ?, receipt_json = ?,
+                       updated_at = ?, reconciled = 1
+                   WHERE plan_id = ? AND operation_id = ?
+                     AND status IN ('ISSUED','EXECUTING','UNKNOWN','LEGACY_UNKNOWN')""",
+                (
+                    receipt.status.value,
+                    self._canonical_json(receipt.to_dict()),
+                    receipt.committed_at or int(time.time()),
+                    receipt.plan_id,
+                    receipt.operation_id,
+                ),
+            )
+            if updated.rowcount != 1:
+                connection.rollback()
+                raise PlanTransitionError("operation is not reconcilable")
+            statuses = [
+                str(row[0])
+                for row in connection.execute(
+                    "SELECT status FROM write_operations WHERE plan_id = ? ORDER BY ordinal",
+                    (receipt.plan_id,),
+                )
+            ]
+            aggregate = self._aggregate_persisted_statuses(statuses)
+            connection.execute(
+                "UPDATE write_plans SET status = ? WHERE plan_id = ?",
+                (aggregate.value, receipt.plan_id),
+            )
+            connection.commit()
+
+    @staticmethod
+    def _aggregate_persisted_statuses(statuses: list[str]) -> PlanStatus:
+        completed: list[ApplyStatus] = []
+        pending = False
+        for status in statuses:
+            try:
+                completed.append(ApplyStatus(status))
+            except ValueError:
+                pending = True
+        if pending:
+            if any(status in {ApplyStatus.APPLIED, ApplyStatus.ALREADY_APPLIED} for status in completed):
+                return PlanStatus.PARTIAL
+            if ApplyStatus.UNKNOWN in completed:
+                return PlanStatus.UNKNOWN
+            if ApplyStatus.CONFLICT in completed:
+                return PlanStatus.CONFLICT
+            if ApplyStatus.RETRYABLE_NOT_APPLIED in completed:
+                return PlanStatus.RETRYABLE_NOT_APPLIED
+            if completed and all(status is ApplyStatus.REJECTED for status in completed):
+                return PlanStatus.REJECTED
+            return PlanStatus.EXECUTING
+        return aggregate_plan_status(completed)
+
+    def get_plan_record(self, plan_id: str) -> dict[str, Any] | None:
+        self.prepare()
+        with sqlite3.connect(self.database_path, timeout=30) as connection:
+            connection.row_factory = sqlite3.Row
+            plan = connection.execute(
+                "SELECT * FROM write_plans WHERE plan_id = ?",
+                (plan_id,),
+            ).fetchone()
+            if plan is None:
+                return None
+            operations = connection.execute(
+                """
+                SELECT * FROM write_operations
+                WHERE plan_id = ? ORDER BY ordinal
+                """,
+                (plan_id,),
+            ).fetchall()
+        return {
+            "plan_id": plan["plan_id"],
+            "tool_name": plan["tool_name"],
+            "graph_target": json.loads(plan["graph_target_json"]),
+            "principal": plan["principal"],
+            "payload": json.loads(plan["payload_json"]),
+            "payload_digest": plan["payload_digest"],
+            "schema_fingerprint": plan["schema_fingerprint"],
+            "status": plan["status"],
+            "created_at": plan["created_at"],
+            "expires_at": plan["expires_at"],
+            "confirmed_at": plan["confirmed_at"],
+            "lease_owner": plan["lease_owner"],
+            "lease_expires_at": plan["lease_expires_at"],
+            "operations": [
+                {
+                    "operation_id": operation["operation_id"],
+                    "ordinal": operation["ordinal"],
+                    "kind": operation["kind"],
+                    "payload": json.loads(operation["payload_json"]),
+                    "status": operation["status"],
+                    "attempt": operation["attempt"],
+                    "attempt_token": operation["attempt_token"],
+                    "reconciled": bool(operation["reconciled"]),
+                    "receipt": (json.loads(operation["receipt_json"]) if operation["receipt_json"] else None),
+                    "updated_at": operation["updated_at"],
+                }
+                for operation in operations
+            ],
+        }
+
+    def _migrate_schema(self, connection: sqlite3.Connection, version: int) -> None:
+        """Apply additive schema steps in order; never silently downgrade."""
+        if version < 1:
+            self._ensure_v1_schema(connection)
+            connection.execute("PRAGMA user_version = 1")
+            version = 1
+        if version < 2:
+            plan_columns = {row[1] for row in connection.execute("PRAGMA table_info(write_plans)")}
+            operation_columns = {row[1] for row in connection.execute("PRAGMA table_info(write_operations)")}
+            if "lease_owner" not in plan_columns:
+                connection.execute("ALTER TABLE write_plans ADD COLUMN lease_owner TEXT")
+            if "lease_expires_at" not in plan_columns:
+                connection.execute("ALTER TABLE write_plans ADD COLUMN lease_expires_at INTEGER")
+            if "attempt_token" not in operation_columns:
+                connection.execute("ALTER TABLE write_operations ADD COLUMN attempt_token TEXT")
+            connection.execute("PRAGMA user_version = 2")
+            version = 2
+        if version < 3:
+            operation_columns = {row[1] for row in connection.execute("PRAGMA table_info(write_operations)")}
+            if "reconciled" not in operation_columns:
+                connection.execute("ALTER TABLE write_operations ADD COLUMN reconciled INTEGER NOT NULL DEFAULT 0")
+            connection.execute("PRAGMA user_version = 3")
+
+    def _ensure_v1_schema(self, connection: sqlite3.Connection) -> None:
+        connection.execute(
+            """
+            CREATE TABLE IF NOT EXISTS write_plans (
+                plan_id TEXT PRIMARY KEY,
+                tool_name TEXT NOT NULL,
+                graph_target_json TEXT NOT NULL,
+                principal TEXT NOT NULL,
+                payload_json TEXT NOT NULL,
+                payload_digest TEXT NOT NULL,
+                schema_fingerprint TEXT,
+                status TEXT NOT NULL,
+                created_at INTEGER NOT NULL,
+                expires_at INTEGER NOT NULL,
+                confirmed_at INTEGER
+            )
+            """
+        )
+        connection.execute(
+            """
+            CREATE TABLE IF NOT EXISTS write_operations (
+                operation_id TEXT PRIMARY KEY,
+                plan_id TEXT NOT NULL,
+                ordinal INTEGER NOT NULL,
+                kind TEXT NOT NULL,
+                payload_json TEXT NOT NULL,
+                status TEXT NOT NULL,
+                attempt INTEGER NOT NULL DEFAULT 0,
+                receipt_json TEXT,
+                updated_at INTEGER NOT NULL,
+                UNIQUE(plan_id, ordinal),
+                FOREIGN KEY(plan_id) REFERENCES write_plans(plan_id)
+            )
+            """
+        )
+
+    def _migrate_legacy(self, connection: sqlite3.Connection) -> None:
+        if not self.legacy_database_path.exists():
+            return
+        with sqlite3.connect(self.legacy_database_path, timeout=30) as legacy:
+            legacy.row_factory = sqlite3.Row
+            issued = self._legacy_rows(legacy, "issued_confirmations", "plan_hash")
+            consumed = self._legacy_rows(legacy, "consumed_confirmations", "plan_hash")
+            operations = self._legacy_rows(legacy, "write_operations", "plan_hash")
+
+        for plan_hash in sorted(set(issued) | set(consumed) | set(operations)):
+            self._migrate_legacy_plan(
+                connection,
+                plan_hash=plan_hash,
+                issued=issued.get(plan_hash),
+                consumed=consumed.get(plan_hash),
+                operation=operations.get(plan_hash),
+            )
+
+    def _migrate_legacy_plan(
+        self,
+        connection: sqlite3.Connection,
+        *,
+        plan_hash: str,
+        issued: sqlite3.Row | None,
+        consumed: sqlite3.Row | None,
+        operation: sqlite3.Row | None,
+    ) -> None:
+        payload_text = self._row_value(operation, "plan_payload_json")
+        if payload_text is None:
+            payload_text = self._row_value(issued, "plan_payload_json")
+        payload, payload_valid = self._safe_json(payload_text)
+
+        receipt_text = self._row_value(operation, "receipt_json")
+        receipt, receipt_valid = self._safe_json(receipt_text)
+        status = self._legacy_status(
+            operation=operation,
+            consumed=consumed,
+            payload_valid=payload_valid,
+            receipt_valid=receipt_valid,
+        )
+        if not payload_valid or not receipt_valid:
+            receipt = {"reason_code": "CORRUPT_LEGACY_STATE"}
+
+        created_at = int(
+            self._row_value(issued, "issued_at")
+            or self._row_value(consumed, "consumed_at")
+            or self._row_value(operation, "updated_at")
+            or 0
+        )
+        expires_at = int(self._row_value(issued, "expires_at") or self._row_value(consumed, "expires_at") or created_at)
+        confirmed_at = int(self._row_value(consumed, "consumed_at")) if consumed else None
+        plan_id = self.legacy_plan_id(plan_hash)
+        payload_json = self._canonical_json(payload)
+        receipt_json = self._canonical_json(receipt) if receipt is not None else None
+
+        connection.execute(
+            """
+            INSERT OR IGNORE INTO write_plans (
+                plan_id, tool_name, graph_target_json, principal, payload_json,
+                payload_digest, schema_fingerprint, status, created_at,
+                expires_at, confirmed_at
+            ) VALUES (?, 'legacy', '{}', 'legacy', ?, ?, NULL, ?, ?, ?, ?)
+            """,
+            (
+                plan_id,
+                payload_json,
+                plan_hash,
+                status.value,
+                created_at,
+                expires_at,
+                confirmed_at,
+            ),
+        )
+        connection.execute(
+            """
+            INSERT OR IGNORE INTO write_operations (
+                operation_id, plan_id, ordinal, kind, payload_json, status,
+                attempt, receipt_json, updated_at
+            ) VALUES (?, ?, 0, 'LEGACY_CONFIRMATION', ?, ?, ?, ?, ?)
+            """,
+            (
+                self.legacy_operation_id(plan_hash),
+                plan_id,
+                payload_json,
+                status.value,
+                1 if consumed else 0,
+                receipt_json,
+                int(self._row_value(operation, "updated_at") or created_at),
+            ),
+        )
+
+    @staticmethod
+    def _legacy_status(
+        *,
+        operation: sqlite3.Row | None,
+        consumed: sqlite3.Row | None,
+        payload_valid: bool,
+        receipt_valid: bool,
+    ) -> PlanStatus:
+        if not payload_valid or not receipt_valid:
+            return PlanStatus.LEGACY_UNKNOWN
+        raw_status = SQLitePlanStore._row_value(operation, "status")
+        if raw_status is not None:
+            try:
+                return PlanStatus(raw_status)
+            except ValueError:
+                return PlanStatus.LEGACY_UNKNOWN
+        if consumed is not None:
+            return PlanStatus.LEGACY_UNKNOWN
+        return PlanStatus.ISSUED
+
+    @staticmethod
+    def _legacy_rows(
+        connection: sqlite3.Connection,
+        table: str,
+        key: str,
+    ) -> dict[str, sqlite3.Row]:
+        exists = connection.execute(
+            "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?",
+            (table,),
+        ).fetchone()
+        if exists is None:
+            return {}
+        return {str(row[key]): row for row in connection.execute(f"SELECT * FROM {table}")}
+
+    @staticmethod
+    def _row_value(row: sqlite3.Row | None, key: str) -> Any:
+        if row is None or key not in set(row.keys()):
+            return None
+        return row[key]
+
+    @staticmethod
+    def _safe_json(value: str | None) -> tuple[Any, bool]:
+        if value is None:
+            return None, True
+        try:
+            return json.loads(value), True
+        except (TypeError, json.JSONDecodeError):
+            return None, False
+
+    @staticmethod
+    def _canonical_json(value: Any) -> str:
+        return json.dumps(
+            value,
+            ensure_ascii=False,
+            separators=(",", ":"),
+            sort_keys=True,
+        )
+
+    def _prepare_storage(self) -> None:
+        self.state_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
+        self.state_dir.chmod(0o700)
+        descriptor = os.open(
+            self.database_path,
+            os.O_CREAT | os.O_APPEND | os.O_WRONLY,
+            0o600,
+        )
+        os.close(descriptor)
+        self.database_path.chmod(0o600)
+
+
+def plan_store_from_config(cfg=None) -> PlanStore:
+    if cfg is None:
+        from hugegraph_mcp.config import MCPConfig
+
+        cfg = MCPConfig.from_env()
+    if not cfg.has_safe_write_store():
+        raise PlanStoreUnavailableError("The configured plan store is not safe for this write topology")
+    return SQLitePlanStore(cfg.state_dir)
diff --git a/hugegraph-mcp/hugegraph_mcp/reconciler.py b/hugegraph-mcp/hugegraph_mcp/reconciler.py
new file mode 100644
index 00000000..05a33579
--- /dev/null
+++ b/hugegraph-mcp/hugegraph_mcp/reconciler.py
@@ -0,0 +1,202 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0.
+
+"""Read-only reconciliation for ambiguous persisted write outcomes."""
+
+from __future__ import annotations
+
+import time
+from collections.abc import Callable, Mapping
+from typing import Any
+
+from hugegraph_mcp.envelope import ErrorType, envelope_err
+from hugegraph_mcp.plan_store import (
+    PlanStore,
+    PlanTransitionError,
+    plan_store_from_config,
+)
+from hugegraph_mcp.write_executor import WriteExecutor, WriteExecutorRegistry
+from hugegraph_mcp.write_plan import (
+    ApplyReceipt,
+    ApplyStatus,
+    OperationPlan,
+    PlanStatus,
+    WritePlan,
+)
+
+ReconcileReader = Callable[[WritePlan, OperationPlan], Mapping[str, Any]]
+
+
+class ReconcileReaderRegistry:
+    def __init__(self) -> None:
+        self._readers: dict[str, ReconcileReader] = {}
+
+    def register(self, kind: str, reader: ReconcileReader) -> None:
+        if not kind:
+            raise ValueError("reader kind must be non-empty")
+        self._readers[kind] = reader
+
+    def reader_for(self, kind: str) -> ReconcileReader | None:
+        return self._readers.get(kind)
+
+
+def reconcile_operation_state(
+    kind: str,
+    expected_state: Mapping[str, Any],
+    desired_state: Mapping[str, Any],
+    observed_state: Mapping[str, Any],
+) -> ApplyStatus:
+    """Classify observed state without performing any write."""
+
+    observed = dict(observed_state)
+    if observed == dict(desired_state):
+        return ApplyStatus.APPLIED
+    if observed == dict(expected_state):
+        return ApplyStatus.RETRYABLE_NOT_APPLIED
+    if kind.startswith("DELETE_") and observed.get("exists") is False:
+        return ApplyStatus.APPLIED
+    return ApplyStatus.CONFLICT
+
+
+class WriteReconciler:
+    def __init__(self, *, store: PlanStore, readers: ReconcileReaderRegistry):
+        self.store = store
+        self.readers = readers
+
+    def reconcile(self, plan_id: str) -> dict:
+        record = self.store.get_plan_record(plan_id)
+        if record is None:
+            return envelope_err(
+                ErrorType.NOT_FOUND,
+                "No persisted write plan matches this plan_id.",
+                retryable=False,
+            )
+        if record["tool_name"] == "legacy":
+            return envelope_err(
+                ErrorType.FEATURE_DISABLED,
+                "Legacy confirmation state cannot be safely reconciled.",
+                retryable=False,
+                details={"reason_code": "LEGACY_PLAN_NOT_RECONCILABLE"},
+            )
+        try:
+            plan = self.store.begin_reconcile(plan_id)
+        except PlanTransitionError as exc:
+            return envelope_err(
+                ErrorType.WRITE_CONFLICT,
+                "The write plan still has an active execution lease.",
+                retryable=False,
+                details={"reason_code": "EXECUTION_LEASE_ACTIVE", "reason": str(exc)},
+            )
+        if plan.status not in {
+            PlanStatus.UNKNOWN,
+            PlanStatus.LEGACY_UNKNOWN,
+            PlanStatus.PARTIAL,
+        }:
+            return self._status(plan_id)
+
+        record = self.store.get_plan_record(plan_id)
+        assert record is not None
+        operation_records = {item["operation_id"]: item for item in record["operations"]}
+        unresolved = {
+            PlanStatus.EXECUTING.value,
+            PlanStatus.UNKNOWN.value,
+            PlanStatus.LEGACY_UNKNOWN.value,
+        }
+        for operation in plan.operations:
+            operation_record = operation_records[operation.operation_id]
+            if operation_record["status"] == PlanStatus.ISSUED.value:
+                self.store.record_reconciliation_receipt(
+                    ApplyReceipt(
+                        plan_id=plan_id,
+                        operation_id=operation.operation_id,
+                        status=ApplyStatus.RETRYABLE_NOT_APPLIED,
+                        reason_code="RECONCILED_OPERATION_NOT_CLAIMED",
+                        attempt=operation_record["attempt"],
+                        reconciliation_required=False,
+                    )
+                )
+                continue
+            if operation_record["status"] not in unresolved:
+                continue
+            reader = self.readers.reader_for(operation.kind)
+            if reader is None:
+                return envelope_err(
+                    ErrorType.FEATURE_DISABLED,
+                    "The write plan requires an unavailable reconciliation reader.",
+                    retryable=False,
+                    details={"missing_operation_kind": operation.kind},
+                )
+            observed = reader(plan, operation)
+            status = reconcile_operation_state(
+                operation.kind,
+                operation.expected_state,
+                operation.desired_state,
+                observed,
+            )
+            reason_code = "RECONCILED_CURRENT_STATE"
+            reconciliation_required = False
+            if status is ApplyStatus.RETRYABLE_NOT_APPLIED and operation_record["attempt"] > 0:
+                # A backend read cannot prove that an already-dispatched request
+                # will not commit later. Only never-claimed operations are safe
+                # to authorize for replay.
+                status = ApplyStatus.UNKNOWN
+                reason_code = "IN_FLIGHT_COMMIT_NOT_EXCLUDED"
+                reconciliation_required = True
+            self.store.record_reconciliation_receipt(
+                ApplyReceipt(
+                    plan_id=plan_id,
+                    operation_id=operation.operation_id,
+                    status=status,
+                    observed_state=observed,
+                    reason_code=reason_code,
+                    attempt=operation_record["attempt"],
+                    reconciliation_required=reconciliation_required,
+                    committed_at=(int(time.time()) if status is ApplyStatus.APPLIED else None),
+                )
+            )
+        return self._status(plan_id)
+
+    def _status(self, plan_id: str) -> dict:
+        return WriteExecutor(
+            store=self.store,
+            registry=WriteExecutorRegistry(),
+        ).status(plan_id)
+
+
+DEFAULT_RECONCILE_READERS = ReconcileReaderRegistry()
+
+from hugegraph_mcp.tools.graph_write_adapter import (
+    register_graph_reconcile_readers,
+)
+from hugegraph_mcp.tools.schema_write_adapter import (
+    register_schema_reconcile_readers,
+)
+
+register_graph_reconcile_readers(DEFAULT_RECONCILE_READERS)
+register_schema_reconcile_readers(DEFAULT_RECONCILE_READERS)
+
+
+def reconcile_write(plan_id: str) -> dict:
+    try:
+        store = plan_store_from_config()
+    except Exception as exc:  # noqa: BLE001 - public tool must remain enveloped
+        return envelope_err(
+            ErrorType.SERVER_ERROR,
+            "The durable plan store is unavailable.",
+            retryable=False,
+            details={"reason": type(exc).__name__},
+        )
+    try:
+        return WriteReconciler(
+            store=store,
+            readers=DEFAULT_RECONCILE_READERS,
+        ).reconcile(plan_id)
+    except PlanTransitionError as exc:
+        return envelope_err(
+            ErrorType.WRITE_CONFLICT,
+            "The persisted plan state changed during reconciliation.",
+            retryable=False,
+            details={"reason": str(exc)},
+        )
diff --git a/hugegraph-mcp/hugegraph_mcp/server.py b/hugegraph-mcp/hugegraph_mcp/server.py
index d039da09..157891d9 100644
--- a/hugegraph-mcp/hugegraph_mcp/server.py
+++ b/hugegraph-mcp/hugegraph_mcp/server.py
@@ -18,92 +18,45 @@ server.py 只负责参数校验和 mode 分发,具体业务逻辑委托给 too
 """
 
 import logging
-import logging.handlers
 import os
 import time
 from typing import Any
 
-# ---- Startup patch: prevent module-level pyhugegraph logging from writing files. ----
-# pyhugegraph creates a RotatingFileHandler under 'logs/' during import. In MCP
-# stdio mode this would corrupt the JSON protocol stream, so intercept makedirs
-# and RotatingFileHandler.
-
-_original_makedirs = os.makedirs
-
-
-def _safe_makedirs(name, mode=0o777, exist_ok=False):
-    if _is_logs_dir(name):
-        return None
-    return _original_makedirs(name, mode, exist_ok)
-
-
-def _is_logs_dir(name) -> bool:
-    try:
-        path = os.fspath(name)
-    except TypeError:
-        return False
-    return os.path.basename(os.path.normpath(path)).lower() == "logs"
-
-
-_OriginalRotatingFileHandler = logging.handlers.RotatingFileHandler
-
-
-class _NoOpFileHandler(logging.NullHandler):
-    """无操作日志处理器 — 用于禁用文件日志记录。"""
-
-    def __init__(self, *args, **kwargs):
-        super().__init__()
-
-
-def _patched_rotating_handler(filename, *args, **kwargs):
-    if _is_logs_file(filename):
-        return _NoOpFileHandler()
-    return _OriginalRotatingFileHandler(filename, *args, **kwargs)
-
-
-def _is_logs_file(filename) -> bool:
-    try:
-        path = os.path.normpath(os.fspath(filename))
-    except TypeError:
-        return False
-    return any(part.lower() == "logs" for part in path.split(os.sep))
-
-
-logging.handlers.RotatingFileHandler = _patched_rotating_handler
-
-os.makedirs = _safe_makedirs
-
-try:
-    # ---- Safely import pyhugegraph-dependent modules within the patch scope. ----
-    from fastmcp import FastMCP
-
-    from hugegraph_mcp.config import MCPConfig
-    from hugegraph_mcp.envelope import ErrorType, envelope_err
-    from hugegraph_mcp.gremlin_tools import execute_gremlin_read, execute_gremlin_write
-    from hugegraph_mcp.guard import Capability
-    from hugegraph_mcp.tools.extract_graph_data import extract_graph_data
-    from hugegraph_mcp.tools.generate_gremlin import generate_gremlin
-    from hugegraph_mcp.tools.inspect_graph import inspect_graph
-    from hugegraph_mcp.tools.inspect_schema import inspect_schema
-    from hugegraph_mcp.tools.manage_graph_data import manage_graph_data
-    from hugegraph_mcp.tools.manage_schema import manage_schema
-    from hugegraph_mcp.tools.mutate_graph_properties import mutate_graph_properties
-    from hugegraph_mcp.tools.query_graph_data import query_graph_data
-    from hugegraph_mcp.tools.refresh_vid_embeddings import refresh_vid_embeddings
-finally:
-    os.makedirs = _original_makedirs
-    logging.handlers.RotatingFileHandler = _OriginalRotatingFileHandler
+from fastmcp import FastMCP
+
+from hugegraph_mcp.backend_capabilities import BackendFeature, profile_for
+from hugegraph_mcp.config import MCPConfig
+from hugegraph_mcp.envelope import ErrorType, envelope_err
+from hugegraph_mcp.gremlin_tools import execute_gremlin_read, execute_gremlin_write
+from hugegraph_mcp.guard import Capability
+from hugegraph_mcp.reconciler import reconcile_write
+from hugegraph_mcp.tools.extract_graph_data import extract_graph_data
+from hugegraph_mcp.tools.generate_gremlin import generate_gremlin
+from hugegraph_mcp.tools.inspect_graph import inspect_graph
+from hugegraph_mcp.tools.inspect_schema import inspect_schema
+from hugegraph_mcp.tools.manage_graph_data import manage_graph_data
+from hugegraph_mcp.tools.manage_schema import manage_schema
+from hugegraph_mcp.tools.mutate_graph_properties import mutate_graph_properties
+from hugegraph_mcp.tools.query_graph_data import query_graph_data
+from hugegraph_mcp.tools.refresh_vid_embeddings import refresh_vid_embeddings
+from hugegraph_mcp.write_contract import resolve_optional_legacy_locator
+from hugegraph_mcp.write_executor import confirm_write, get_write_status
 
 READONLY = MCPConfig.from_env().is_readonly()
 MCP_TOOL_CONTRACT_VERSION = "2.0"
 DEFAULT_TOOLSET = "v2_core"
+VALID_TOOLSETS = frozenset({"v1", DEFAULT_TOOLSET})
+LOGGER = logging.getLogger("hugegraph_mcp.server")
 
 mcp = FastMCP("HugeGraph MCP")
 
 
 def _active_toolset() -> str:
     value = os.getenv("HUGEGRAPH_MCP_TOOLSET", DEFAULT_TOOLSET).strip()
-    return "v1" if value == "v1" else DEFAULT_TOOLSET
+    if value in VALID_TOOLSETS:
+        return value
+    LOGGER.error("Invalid HUGEGRAPH_MCP_TOOLSET value %r; falling back to v1", value)
+    return "v1"
 
 
 def _register_v2_core_tool(func):
@@ -151,6 +104,47 @@ def _call_public_tool(tool_name: str, func, *args, **kwargs) -> dict[str, Any]:
     )
 
 
+def _call_legacy_write_tool(
+    tool_name: str,
+    func,
+    *args,
+    plan_hash: str | None,
+    nonce: str | None,
+    expires_at: float | None,
+    **kwargs,
+) -> dict[str, Any]:
+    """Validate the deprecated locator before delegating to a write workflow."""
+
+    start = time.perf_counter()
+    resolution, error = resolve_optional_legacy_locator(
+        plan_hash=plan_hash,
+        nonce=nonce,
+        expires_at=expires_at,
+    )
+    if error is not None:
+        return _align_public_tool_envelope(
+            error,
+            tool_name=tool_name,
+            duration_ms=(time.perf_counter() - start) * 1000.0,
+        )
+
+    result = _call_public_tool(
+        tool_name,
+        func,
+        *args,
+        plan_hash=plan_hash,
+        nonce=nonce,
+        expires_at=expires_at,
+        **kwargs,
+    )
+    if resolution is not None:
+        result["warnings"] = [
+            *(result.get("warnings") or []),
+            *resolution.warnings,
+        ]
+    return result
+
+
 def _is_admin_mode_enabled() -> bool:
     return MCPConfig.from_env().admin_mode
 
@@ -164,10 +158,7 @@ def _admin_gate(tool_name: str, *, requires_write: bool = False) -> dict | None:
         if requires_write:
             enable_env["readonly"] = "HUGEGRAPH_MCP_READONLY"
             required_env["HUGEGRAPH_MCP_READONLY"] = "false"
-            suggestion = (
-                f"Set HUGEGRAPH_MCP_ADMIN_MODE=true and HUGEGRAPH_MCP_READONLY=false "
-                f"to enable {tool_name}."
-            )
+            suggestion = f"Set HUGEGRAPH_MCP_ADMIN_MODE=true and HUGEGRAPH_MCP_READONLY=false to enable {tool_name}."
         return envelope_err(
             ErrorType.FEATURE_DISABLED,
             f"{tool_name} is an admin/debug tool and is disabled by default.",
@@ -202,6 +193,45 @@ def _admin_gate(tool_name: str, *, requires_write: bool = False) -> dict | None:
     return None
 
 
+_RAW_GREMLIN_HARD_BUDGET_FEATURES = (
+    BackendFeature.READONLY_PRINCIPAL,
+    BackendFeature.GREMLIN_EVALUATION_TIMEOUT,
+    BackendFeature.REST_GREMLIN_WAIT_TIMEOUT,
+    BackendFeature.GREMLIN_RESULT_ITEM_LIMIT,
+    BackendFeature.HTTP_STREAMING_RESPONSE_LIMIT,
+)
+
+
+def _raw_gremlin_hard_budget_gate(tool_name: str) -> dict | None:
+    """Fail closed until the complete raw-query safety contract is verified.
+
+    The only evidence-backed deployment profile currently supported by this
+    package lacks both a general server result cap and a streaming client byte
+    cap. Post-materialization output guards cannot substitute for either.
+    """
+    profile = profile_for("1.7.0", "rocksdb")
+    evidence = {
+        feature.value: {
+            "status": profile.status_for(feature).value,
+            "reason": profile.evidence_for(feature).reason,
+        }
+        for feature in _RAW_GREMLIN_HARD_BUDGET_FEATURES
+    }
+    return envelope_err(
+        ErrorType.FEATURE_DISABLED,
+        f"{tool_name} is disabled because the raw Gremlin hard-budget contract is incomplete.",
+        suggestion="Use structured MCP query tools; generating Gremlin with execute=false remains available.",
+        source=tool_name,
+        details={
+            "tool": tool_name,
+            "required_capabilities": evidence,
+            "integration_contract_verified": False,
+            "post_materialization_limits_are_hard_budgets": False,
+        },
+        duration_ms=0.0,
+    )
+
+
 # ========== Tool 1: inspect graph status and schema ==========
 
 
@@ -233,6 +263,36 @@ def inspect_graph_tool(
     return result
 
 
+@_register_v2_core_tool
+def get_write_status_tool(plan_id: str) -> dict:
+    """Return the durable plan, operation, and receipt outcome by plan_id."""
+    return _call_public_tool(
+        "get_write_status_tool",
+        get_write_status,
+        plan_id=plan_id,
+    )
+
+
+@_register_v2_core_tool
+def confirm_write_tool(plan_id: str) -> dict:
+    """Confirm exactly one immutable server-persisted plan by plan_id only."""
+    return _call_public_tool(
+        "confirm_write_tool",
+        confirm_write,
+        plan_id=plan_id,
+    )
+
+
+@_register_v2_core_tool
+def reconcile_write_tool(plan_id: str) -> dict:
+    """Reconcile an UNKNOWN or PARTIAL plan by plan_id using read-only checks."""
+    return _call_public_tool(
+        "reconcile_write_tool",
+        reconcile_write,
+        plan_id=plan_id,
+    )
+
+
 @_register_v2_core_tool
 def inspect_schema_tool(
     include_raw_schema: bool = False,
@@ -309,15 +369,21 @@ def generate_gremlin_tool(
     query: str,
     execute: bool = False,
     output_types: list[str] | None = None,
-    limit_policy: str = "warn",
+    limit_policy: str = "reject_unbounded",
 ) -> dict:
-    """V1 稳定工具:自然语言 → Gremlin 生成。
+    """Generate Gremlin without executing it. Capability: GENERATE.
 
-    默认不执行(execute=false),返回生成的 Gremlin 查询。
-    设置 execute=true 可执行生成的只读 Gremlin。Capability: GENERATE.
-    limit_policy 仅在 execute=true 时传给只读执行:warn、reject_unbounded、
-    auto_append。auto_append 会返回 original_gremlin/executed_gremlin/rewrite_reason。
+    Every public raw Gremlin execution path is disabled until the hard resource
+    budget contract is verified, so execute=true currently returns
+    FEATURE_DISABLED. limit_policy remains as a compatibility parameter.
     """
+    if execute:
+        blocked = _raw_gremlin_hard_budget_gate("generate_gremlin_tool(execute=true)")
+        if blocked:
+            return blocked
+        blocked = _admin_gate("generate_gremlin_tool(execute=true)")
+        if blocked:
+            return blocked
     return _call_public_tool(
         "generate_gremlin_tool",
         generate_gremlin,
@@ -329,17 +395,19 @@ def generate_gremlin_tool(
 
 
 @mcp.tool()
-def execute_gremlin_read_tool(gremlin_query: str, limit_policy: str = "warn") -> dict:
-    """V1 稳定工具:执行只读 Gremlin 遍历查询。
+def execute_gremlin_read_tool(gremlin_query: str, limit_policy: str = "reject_unbounded") -> dict:
+    """Registered compatibility tool; raw read execution is currently disabled.
 
-    Capability: READ.
-    经过 GremlinPolicy 安全检查后执行。
-    limit_policy values:
-    - warn: execute and return cost warnings for unbounded reads.
-    - reject_unbounded: reject safe but unbounded reads before execution.
-    - auto_append: append .limit(100) to simple unbounded g.V()/g.E() queries and
-      return original_gremlin/executed_gremlin/rewrite_reason.
+    Public raw Gremlin remains FEATURE_DISABLED until the deployment proves the
+    complete server timeout/result cap, streaming byte cap, and readonly-principal
+    contract. Use query_graph_data_tool for executable bounded reads.
     """
+    blocked = _raw_gremlin_hard_budget_gate("execute_gremlin_read_tool")
+    if blocked:
+        return blocked
+    blocked = _admin_gate("execute_gremlin_read_tool")
+    if blocked:
+        return blocked
     return _call_public_tool(
         "execute_gremlin_read_tool",
         execute_gremlin_read,
@@ -398,9 +466,10 @@ def apply_schema_tool(
     mode values:
     - design: schema design guidance; operations optional.
     - validate: validate operations against live schema.
-    - dry_run: validate P0a create operations and return plan_hash/nonce/expires_at.
-    - apply: P0a only. Requires confirm=true, plan_hash, nonce, expires_at from
-      dry_run. Supports create_property_key, create_vertex_label, create_edge_label.
+    - dry_run: validate exactly one P0a create operation and issue a persisted plan.
+    - apply: deprecated compatibility confirmation for one create operation.
+      New clients confirm the returned plan_id with confirm_write_tool.
+      Supports create_property_key, create_vertex_label, create_edge_label.
       Rejects create_index_label, schema append/eliminate, remove/drop.
     """
     start = time.perf_counter()
@@ -413,7 +482,7 @@ def apply_schema_tool(
             details={"mode": mode, "tool": "apply_schema_tool"},
             duration_ms=(time.perf_counter() - start) * 1000.0,
         )
-    return _call_public_tool(
+    return _call_legacy_write_tool(
         "apply_schema_tool",
         manage_schema,
         mode=mode,
@@ -441,13 +510,11 @@ def mutate_graph_properties_tool(
 
     Capability: DATA_WRITE.
     target values: vertex, edge.
-    operation values: append, eliminate. Both require the same chain:
-    dry_run=true -> review before/after and plan_hash -> dry_run=false,
-    confirm=true with plan_hash, nonce, and expires_at.
-    The confirm step re-reads schema and target; changed targets return
-    TARGET_CHANGED and are not mutated.
+    operation values: append, eliminate. Dry-run returns a before/after preview
+    with confirmable=false. Confirmed mutation returns FEATURE_DISABLED because
+    HugeGraph 1.7.0 lacks a backend-enforced atomic compare-and-set update.
     """
-    return _call_public_tool(
+    return _call_legacy_write_tool(
         "mutate_graph_properties_tool",
         mutate_graph_properties,
         target=target,
@@ -478,11 +545,11 @@ def import_graph_data_tool(
     nonce: str | None = None,
     expires_at: float | None = None,
 ) -> dict:
-    """V1 图数据导入入口。
+    """Extract or plan structured graph data import.
 
-    mode="extract": 自然语言文本 → 候选 graph_data
-    mode="ingest": MCP 本地校验+dry_run/confirm+Gremlin 导入 graph_data
-    mode="table": 当前 MCP contract 不支持(返回 FEATURE_DISABLED)
+    mode="extract" returns candidate graph_data without writing.
+    mode="ingest" validates locally and issues an immutable plan_id; confirm it
+    with confirm_write_tool. mode="table" returns FEATURE_DISABLED.
     """
     start = time.perf_counter()
 
@@ -510,7 +577,7 @@ def import_graph_data_tool(
                 source="import_graph_data_tool",
                 duration_ms=(time.perf_counter() - start) * 1000.0,
             )
-        return _call_public_tool(
+        return _call_legacy_write_tool(
             "import_graph_data_tool",
             manage_graph_data,
             mode="import",
@@ -557,12 +624,14 @@ def delete_graph_data_tool(
     nonce: str | None = None,
     expires_at: float | None = None,
 ) -> dict:
-    """V1 稳定工具:受控删除图数据。
+    """Preview exact delete_vertex or delete_edge operations.
 
-    只支持精确 delete_vertex/delete_edge change_plan。
-    必须经过 dry_run -> plan_hash -> confirm;不支持批量条件删除或级联删除。
+    An exact edge delete can be confirmed by plan_id after dry-run. Isolated
+    vertex deletion is preview-only and confirmation returns FEATURE_DISABLED
+    because the backend lacks an atomic no-incident-edge condition. Bulk
+    conditional and cascade deletion are unsupported.
     """
-    return _call_public_tool(
+    return _call_legacy_write_tool(
         "delete_graph_data_tool",
         manage_graph_data,
         mode="delete",
@@ -594,11 +663,13 @@ def refresh_vid_embeddings_tool(confirm: bool = False) -> dict:
 
 @mcp.tool()
 def execute_gremlin_write_tool(gremlin_query: str) -> dict:
-    """Break-glass direct Gremlin write for an isolated trusted admin transport.
+    """Disabled direct Gremlin write pending a verified hard-budget contract.
 
-    This is the sole write-safety-chain exception: it has no preview, dry-run,
-    plan hash, or confirmation step. It requires admin mode and readonly=false.
+    Admin mode and readonly=false remain necessary but are not sufficient.
     """
+    blocked = _raw_gremlin_hard_budget_gate("execute_gremlin_write_tool")
+    if blocked:
+        return blocked
     blocked = _admin_gate("execute_gremlin_write_tool", requires_write=True)
     if blocked:
         return blocked
diff --git a/hugegraph-mcp/hugegraph_mcp/tools/graph_data_execute.py b/hugegraph-mcp/hugegraph_mcp/tools/graph_data_execute.py
index 9feb4e71..483d71b9 100644
--- a/hugegraph-mcp/hugegraph_mcp/tools/graph_data_execute.py
+++ b/hugegraph-mcp/hugegraph_mcp/tools/graph_data_execute.py
@@ -11,14 +11,15 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-"""Graph data change plan execution.
+"""Graph data change plan compilation and execution.
 
-Dry-run preview, write execution, pre-execution matched_count verification,
-and plan hash computation.
+Dry-run resolves delete predicates to immutable backend IDs. Execution consumes
+only those compiled IDs, preventing predicate drift between preview and apply.
 """
 
 import hashlib
 import json
+from copy import deepcopy
 from typing import Any
 
 from hugegraph_mcp import gremlin_tools
@@ -26,6 +27,7 @@ from hugegraph_mcp.config import MCPConfig
 from hugegraph_mcp.envelope import ErrorType, envelope_err, envelope_ok
 from hugegraph_mcp.guard import Capability
 from hugegraph_mcp.tools.graph_data_gremlin import (
+    _delete_target_query,
     _edge_match_query,
     _g,
     _source_vertex_match_query,
@@ -50,6 +52,7 @@ from hugegraph_mcp.write_limits import (
     collect_write_limit_errors,
     operation_count_from_list,
 )
+from hugegraph_mcp.write_plan import ApplyStatus, aggregate_plan_status
 
 # ---- Gremlin execution helpers ----
 
@@ -103,9 +106,15 @@ def _read_values(gremlin_query: str) -> dict[str, Any]:
             retryable=True,
         )
     data = result.get("data") if isinstance(result, dict) else result
-    if isinstance(data, dict) and "data" in data:
+    while isinstance(data, dict) and "data" in data:
         data = data["data"]
-    return envelope_ok({"values": data if isinstance(data, list) else [data]})
+    if data is None:
+        values = []
+    elif isinstance(data, list):
+        values = data
+    else:
+        values = [data]
+    return envelope_ok({"values": values})
 
 
 def _mutation_summary(operations: list[dict[str, Any]]) -> dict[str, int]:
@@ -156,8 +165,8 @@ def dry_run_graph_change_plan(
 ) -> dict[str, Any]:
     """干跑 — 校验 + 预览每个操作的影响(matched_count),不执行写入。
 
-    delete 操作通过只读 Gremlin 查询验证 matched_count==1,
-    delete_vertex cascade=false 时检查关联边。
+    delete 操作通过一次有界 ID 查询解析唯一后端目标,
+    并将 target_id 写入仅供服务端执行的 compiled_plan。
     """
     operations = _operations(change_plan)
     limit_errors = collect_write_limit_errors(
@@ -175,6 +184,8 @@ def dry_run_graph_change_plan(
         return validation
     preview: list[dict[str, Any]] = []
     errors: list[ValidationError] = []
+    compiled_plan = deepcopy(change_plan)
+    compiled_operations = _operations(compiled_plan)
 
     for idx, operation in enumerate(operations):
         op = str(operation.get("op") or operation.get("type"))
@@ -191,7 +202,8 @@ def dry_run_graph_change_plan(
                 op=op,
                 item=item,
                 errors=errors,
-                planned_operations=operations,
+                planned_operations=operations[:idx],
+                compiled_operation=compiled_operations[idx],
             )
             preview.append(item)
             if endpoint_failed:
@@ -234,23 +246,20 @@ def dry_run_graph_change_plan(
                 preview.append(item)
                 continue
 
-        match_query = (
-            _edge_match_query(operation)
-            if op == "delete_edge"
-            else _vertex_match_query(operation)
-        )
-        count_result = _read_count(match_query)
-        if not count_result.get("ok"):
+        match_query = _edge_match_query(operation) if op == "delete_edge" else _vertex_match_query(operation)
+        id_result = _read_values(f"{match_query}.limit(2).id()")
+        if not id_result.get("ok"):
             errors.append(
                 _validation_error(
                     idx,
                     operation,
-                    "matched_count query failed",
+                    "target id query failed",
                     "Verify HugeGraph Server is available and retry the dry run.",
                 )
             )
             continue
-        matched_count = count_result["data"]["matched_count"]
+        target_ids = id_result["data"]["values"]
+        matched_count = len(target_ids)
         item["matched_count"] = matched_count
 
         if op in {"delete_vertex", "delete_edge"} and matched_count != 1:
@@ -263,15 +272,16 @@ def dry_run_graph_change_plan(
                 )
             )
 
-        if (
-            op == "delete_vertex"
-            and operation.get("cascade", False) is False
-            and matched_count == 1
-        ):
+        target_id = target_ids[0] if matched_count == 1 else None
+        if target_id is not None:
+            item["target_id"] = target_id
+            compiled_operations[idx]["target_id"] = target_id
+
+        if op == "delete_vertex" and operation.get("cascade", False) is False and matched_count == 1:
             # By default, refuse to delete vertices with incident edges to avoid
             # unpredictable data loss from HugeGraph-side cascading behavior.
             # Users must explicitly delete the incident edges first.
-            edge_count_result = _read_count(f"{match_query}.bothE()")
+            edge_count_result = _read_count(f"g.V({_g(target_id)}).bothE()")
             if not edge_count_result.get("ok"):
                 errors.append(
                     _validation_error(
@@ -295,7 +305,7 @@ def dry_run_graph_change_plan(
                         )
                     )
         elif op == "delete_vertex" and operation.get("cascade", False) is True:
-            edge_result = _read_values(f"{match_query}.bothE().elementMap()")
+            edge_result = _read_values(f"g.V({_g(target_id)}).bothE().elementMap()")
             if not edge_result.get("ok"):
                 errors.append(
                     _validation_error(
@@ -333,10 +343,11 @@ def dry_run_graph_change_plan(
     return {
         "valid": True,
         "plan_hash": calculate_graph_change_plan_hash(
-            change_plan,
+            compiled_plan,
             schema_summary=normalized_schema_summary(live_schema),
             extra_hash_context=extra_hash_context,
         ),
+        "compiled_plan": compiled_plan,
         "mutation_summary": _mutation_summary(operations),
         "preview": preview,
         "warnings": validation.get("warnings", []),
@@ -351,30 +362,65 @@ def _append_edge_endpoint_counts(
     item: dict[str, Any],
     errors: list[ValidationError],
     planned_operations: list[Any] | None = None,
+    compiled_operation: dict[str, Any] | None = None,
 ) -> bool:
     endpoint_failed = False
     for endpoint, endpoint_query in (
         ("source", _source_vertex_match_query(operation)),
         ("target", _target_vertex_match_query(operation)),
     ):
-        planned_count = _planned_endpoint_match_count(
+        if compiled_operation is None:
+            endpoint_count_result = _read_count(endpoint_query)
+            if not endpoint_count_result.get("ok"):
+                errors.append(
+                    _validation_error(
+                        idx,
+                        operation,
+                        f"{endpoint} endpoint count query failed",
+                        "Verify HugeGraph Server is available and retry the dry run.",
+                    )
+                )
+                endpoint_failed = True
+                continue
+            endpoint_count = endpoint_count_result["data"]["matched_count"]
+            item[f"{endpoint}_matched_count"] = endpoint_count
+            if endpoint_count != 1:
+                errors.append(
+                    _validation_error(
+                        idx,
+                        operation,
+                        f"{op} {endpoint} endpoint matched_count must be 1, got {endpoint_count}",
+                        "Narrow the endpoint match criteria so exactly one vertex is selected.",
+                    )
+                )
+                endpoint_failed = True
+            continue
+
+        planned_ids, _has_unbound_planned_match = _planned_endpoint_ids(
             operation=operation,
             endpoint=endpoint,
             planned_operations=planned_operations,
         )
-        endpoint_count_result = _read_count(endpoint_query)
-        if not endpoint_count_result.get("ok"):
+        planned_indexes = _planned_endpoint_indexes(
+            operation=operation,
+            endpoint=endpoint,
+            planned_operations=planned_operations,
+        )
+        endpoint_id_result = _read_values(f"{endpoint_query}.limit(2).id()")
+        if not endpoint_id_result.get("ok"):
             errors.append(
                 _validation_error(
                     idx,
                     operation,
-                    f"{endpoint} endpoint count query failed",
+                    f"{endpoint} endpoint id query failed",
                     "Verify HugeGraph Server is available and retry the dry run.",
                 )
             )
             endpoint_failed = True
             continue
-        live_count = endpoint_count_result["data"]["matched_count"]
+        live_ids = endpoint_id_result["data"]["values"]
+        planned_count = len(planned_indexes)
+        live_count = len(live_ids)
         total_count = planned_count + live_count
         item[f"{endpoint}_planned_count"] = planned_count
         item[f"{endpoint}_live_count"] = live_count
@@ -389,17 +435,33 @@ def _append_edge_endpoint_counts(
                 )
             )
             endpoint_failed = True
+            continue
+
+        if planned_indexes:
+            dependency_index = planned_indexes[0]
+            item[f"{endpoint}_operation_index"] = dependency_index
+            compiled_operation[f"{endpoint}_operation_index"] = dependency_index
+            if planned_ids:
+                # Keep the explicit ID for the one-release legacy executor. The
+                # canonical executor still consumes the dependency receipt.
+                endpoint_id = planned_ids[0]
+                item[f"{endpoint}_id"] = endpoint_id
+                compiled_operation[f"{endpoint}_id"] = endpoint_id
+        else:
+            endpoint_id = live_ids[0]
+            item[f"{endpoint}_id"] = endpoint_id
+            compiled_operation[f"{endpoint}_id"] = endpoint_id
     return endpoint_failed
 
 
-def _planned_endpoint_match_count(
+def _planned_endpoint_ids(
     *,
     operation: dict[str, Any],
     endpoint: str,
     planned_operations: list[Any] | None,
-) -> int:
+) -> tuple[list[Any], bool]:
     if not planned_operations:
-        return 0
+        return [], False
 
     if endpoint == "source":
         label = operation.get("source_label") or operation.get("outVLabel")
@@ -409,9 +471,10 @@ def _planned_endpoint_match_count(
         match = operation.get("target_match")
 
     if not isinstance(label, str) or not isinstance(match, dict):
-        return 0
+        return [], False
 
-    count = 0
+    stable_ids: list[Any] = []
+    has_unbound_match = False
     for planned in planned_operations:
         if not isinstance(planned, dict):
             continue
@@ -419,8 +482,38 @@ def _planned_endpoint_match_count(
         if planned_op != "create_vertex" or planned.get("label") != label:
             continue
         if _planned_vertex_matches(planned, match):
-            count += 1
-    return count
+            planned_id = planned.get("id")
+            if planned_id is None or planned_id == "":
+                has_unbound_match = True
+            else:
+                stable_ids.append(planned_id)
+    return stable_ids, has_unbound_match
+
+
+def _planned_endpoint_indexes(
+    *,
+    operation: dict[str, Any],
+    endpoint: str,
+    planned_operations: list[Any] | None,
+) -> list[int]:
+    if not planned_operations:
+        return []
+    if endpoint == "source":
+        label = operation.get("source_label") or operation.get("outVLabel")
+        match = operation.get("source_match")
+    else:
+        label = operation.get("target_label") or operation.get("inVLabel")
+        match = operation.get("target_match")
+    if not isinstance(label, str) or not isinstance(match, dict):
+        return []
+    return [
+        index
+        for index, planned in enumerate(planned_operations)
+        if isinstance(planned, dict)
+        and str(planned.get("op") or planned.get("type") or "") == "create_vertex"
+        and planned.get("label") == label
+        and _planned_vertex_matches(planned, match)
+    ]
 
 
 def _planned_vertex_matches(operation: dict[str, Any], match: dict[str, Any]) -> bool:
@@ -442,9 +535,10 @@ def execute_graph_change_plan(
     change_plan: Any,
     live_schema: dict[str, Any] | None = None,
 ) -> dict[str, Any]:
-    """执行变更计划 — 写入前对每个操作再次校验 matched_count。
+    """执行已编译的变更计划。
 
-    防止 dry_run 和 execute 之间状态变化导致的误操作。
+    delete 操作必须由 dry-run 编译出 target_id,执行阶段不再使用
+    可变的属性谓词重新定位目标。
     """
     operations = _operations(change_plan)
     results: list[dict[str, Any]] = []
@@ -460,105 +554,73 @@ def execute_graph_change_plan(
                 return _execution_failure(conflict, operation, idx, results)
 
         if op == "create_edge":
-            for endpoint, endpoint_query in (
-                ("source", _source_vertex_match_query(operation)),
-                ("target", _target_vertex_match_query(operation)),
-            ):
-                endpoint_count_result = _read_count(endpoint_query)
-                if not endpoint_count_result.get("ok"):
-                    return _execution_failure(
-                        endpoint_count_result, operation, idx, results
-                    )
-                endpoint_count = endpoint_count_result["data"]["matched_count"]
-                if endpoint_count != 1:
-                    return _execution_failure(
-                        envelope_err(
-                            ErrorType.INVALID_GRAPH_DATA,
-                            f"{op} {endpoint} endpoint matched_count must be 1 before execution.",
-                            details={
-                                "operation_index": idx,
-                                "matched_count": endpoint_count,
-                            },
-                        ),
-                        operation,
-                        idx,
-                        results,
-                    )
+            missing_ids = [
+                endpoint
+                for endpoint in ("source", "target")
+                if operation.get(f"{endpoint}_id") is None or operation.get(f"{endpoint}_id") == ""
+            ]
+            if missing_ids:
+                return _execution_failure(
+                    envelope_err(
+                        ErrorType.INVALID_GRAPH_DATA,
+                        "create_edge requires dry-run compiled endpoint IDs.",
+                        suggestion="Run dry_run again and execute the server-issued compiled plan.",
+                        details={
+                            "operation_index": idx,
+                            "missing_endpoint_ids": missing_ids,
+                        },
+                    ),
+                    operation,
+                    idx,
+                    results,
+                )
 
         if op in WRITE_OPS:
-            # Read matched_count again before execution to handle graph changes
-            # between dry-run and confirmation. If the match is no longer unique,
-            # refuse the write to avoid TOCTOU issues.
-            if op == "delete_edge":
-                for endpoint, endpoint_query in (
-                    ("source", _source_vertex_match_query(operation)),
-                    ("target", _target_vertex_match_query(operation)),
-                ):
-                    endpoint_count_result = _read_count(endpoint_query)
-                    if not endpoint_count_result.get("ok"):
-                        return _execution_failure(
-                            endpoint_count_result, operation, idx, results
-                        )
-                    endpoint_count = endpoint_count_result["data"]["matched_count"]
-                    if endpoint_count != 1:
-                        return _execution_failure(
-                            envelope_err(
-                                ErrorType.INVALID_GRAPH_DATA,
-                                f"{op} {endpoint} endpoint matched_count must be 1 before execution.",
-                                details={
-                                    "operation_index": idx,
-                                    "matched_count": endpoint_count,
-                                },
-                            ),
-                            operation,
-                            idx,
-                            results,
-                        )
-            match_query = (
-                _edge_match_query(operation)
-                if op == "delete_edge"
-                else _vertex_match_query(operation)
-            )
-            count_result = _read_count(match_query)
-            if not count_result.get("ok"):
-                return _execution_failure(count_result, operation, idx, results)
-            matched_count = count_result["data"]["matched_count"]
-            if matched_count != 1:
+            target_id = operation.get("target_id")
+            if target_id is None or target_id == "":
                 return _execution_failure(
                     envelope_err(
                         ErrorType.INVALID_GRAPH_DATA,
-                        f"{op} matched_count must be 1 before execution.",
+                        f"{op} requires a dry-run compiled target_id.",
+                        suggestion="Run dry_run again and execute the server-issued compiled plan.",
+                        details={"operation_index": idx},
+                    ),
+                    operation,
+                    idx,
+                    results,
+                )
+            target_query = _delete_target_query(operation)
+            exists_result = _read_count(target_query)
+            if not exists_result.get("ok"):
+                return _execution_failure(exists_result, operation, idx, results)
+            existing_count = exists_result["data"]["matched_count"]
+            if existing_count == 0:
+                results.append(
+                    {
+                        "operation_index": idx,
+                        "op": op,
+                        "label": operation.get("label"),
+                        "target_id": target_id,
+                        "status": ApplyStatus.ALREADY_APPLIED.value,
+                        "result": None,
+                    }
+                )
+                continue
+            if existing_count != 1:
+                return _execution_failure(
+                    envelope_err(
+                        ErrorType.WRITE_CONFLICT,
+                        f"{op} compiled target_id resolved to an invalid count.",
                         details={
                             "operation_index": idx,
-                            "matched_count": matched_count,
+                            "target_id": target_id,
+                            "matched_count": existing_count,
                         },
                     ),
                     operation,
                     idx,
                     results,
                 )
-            if op == "delete_vertex" and operation.get("cascade", False) is False:
-                edge_count_result = _read_count(f"{match_query}.bothE()")
-                if not edge_count_result.get("ok"):
-                    return _execution_failure(
-                        edge_count_result, operation, idx, results
-                    )
-                edge_count = edge_count_result["data"]["matched_count"]
-                if edge_count > 0:
-                    return _execution_failure(
-                        envelope_err(
-                            "BLOCKED_BY_RELATIONSHIPS",
-                            "delete_vertex cascade=false but vertex has associated edges.",
-                            suggestion="Delete associated edges first, then retry the vertex delete.",
-                            details={
-                                "operation_index": idx,
-                                "associated_edge_count": edge_count,
-                            },
-                        ),
-                        operation,
-                        idx,
-                        results,
-                    )
             if op == "delete_vertex" and operation.get("cascade", False) is True:
                 return _execution_failure(
                     envelope_err(
@@ -576,8 +638,34 @@ def execute_graph_change_plan(
             capability=Capability.DATA_WRITE,
         )
         if isinstance(write_result, dict) and write_result.get("ok") is False:
+            if write_result.get("error", {}).get("type") in {
+                ErrorType.CONNECTION_FAILED.value,
+                ErrorType.SERVER_ERROR.value,
+                ErrorType.TIMEOUT.value,
+            }:
+                write_result = _unknown_write_outcome(
+                    operation=operation,
+                    operation_index=idx,
+                    cause=write_result,
+                )
             return _execution_failure(write_result, operation, idx, results)
         if isinstance(write_result, dict) and write_result.get("success") is False:
+            legacy_error_type = str(write_result.get("error_type", "")).lower()
+            if legacy_error_type in {
+                "connection_error",
+                "server_error",
+                "timeout_error",
+            }:
+                return _execution_failure(
+                    _unknown_write_outcome(
+                        operation=operation,
+                        operation_index=idx,
+                        cause=write_result,
+                    ),
+                    operation,
+                    idx,
+                    results,
+                )
             return _execution_failure(
                 envelope_err(
                     ErrorType.CONNECTION_FAILED,
@@ -592,15 +680,30 @@ def execute_graph_change_plan(
         if op in {"create_vertex", "create_edge"}:
             affected = _write_affected_count(write_result)
             if affected != 1:
+                if affected is not None:
+                    return _execution_failure(
+                        envelope_err(
+                            ErrorType.FLOW_EXECUTION_FAILED,
+                            f"{op} execution affected {affected} element(s), expected 1.",
+                            retryable=False,
+                            details={
+                                "status": ApplyStatus.REJECTED.value,
+                                "operation_index": idx,
+                                "op": op,
+                                "affected": affected,
+                            },
+                        ),
+                        operation,
+                        idx,
+                        results,
+                    )
                 return _execution_failure(
-                    envelope_err(
-                        ErrorType.FLOW_EXECUTION_FAILED,
-                        f"{op} execution affected {affected if affected is not None else 'unknown'} element(s), expected 1.",
-                        details={
-                            "operation_index": idx,
-                            "op": op,
+                    _unknown_write_outcome(
+                        operation=operation,
+                        operation_index=idx,
+                        cause={
+                            "reason": "unexpected affected count",
                             "affected": affected,
-                            "write_result": write_result,
                         },
                     ),
                     operation,
@@ -611,15 +714,55 @@ def execute_graph_change_plan(
             # Recheck immediately after deletion to ensure HugeGraph actually
             # removed the target. This catches silent backend failures and async
             # state anomalies instead of trusting only the write response.
-            verify_query = (
-                _edge_match_query(operation)
-                if op == "delete_edge"
-                else _vertex_match_query(operation)
-            )
+            verify_query = _delete_target_query(operation)
             verify_result = _read_count(verify_query)
             if not verify_result.get("ok"):
-                return _execution_failure(verify_result, operation, idx, results)
+                return _execution_failure(
+                    _unknown_write_outcome(
+                        operation=operation,
+                        operation_index=idx,
+                        cause=verify_result,
+                    ),
+                    operation,
+                    idx,
+                    results,
+                )
             if verify_result["data"]["matched_count"] != 0:
+                if op == "delete_vertex" and operation.get("cascade", False) is False:
+                    edge_count_result = _read_count(f"{verify_query}.bothE()")
+                    if not edge_count_result.get("ok"):
+                        return _execution_failure(edge_count_result, operation, idx, results)
+                    edge_count = edge_count_result["data"]["matched_count"]
+                    if edge_count > 0:
+                        return _execution_failure(
+                            envelope_err(
+                                "BLOCKED_BY_RELATIONSHIPS",
+                                "delete_vertex cascade=false was blocked by associated edges.",
+                                suggestion="Delete associated edges first, then retry the vertex delete.",
+                                details={
+                                    "operation_index": idx,
+                                    "target_id": operation["target_id"],
+                                    "associated_edge_count": edge_count,
+                                },
+                            ),
+                            operation,
+                            idx,
+                            results,
+                        )
+                    return _execution_failure(
+                        envelope_err(
+                            ErrorType.WRITE_CONFLICT,
+                            "delete_vertex conditional deletion did not remove the target.",
+                            suggestion="Inspect the target state and create a new dry-run plan.",
+                            details={
+                                "operation_index": idx,
+                                "target_id": operation["target_id"],
+                            },
+                        ),
+                        operation,
+                        idx,
+                        results,
+                    )
                 return _execution_failure(
                     envelope_err(
                         "DELETE_VERIFY_FAILED",
@@ -640,16 +783,40 @@ def execute_graph_change_plan(
                 "operation_index": idx,
                 "op": op,
                 "label": operation.get("label"),
+                "status": ApplyStatus.APPLIED.value,
                 "result": write_result,
             }
         )
+    status = aggregate_plan_status([_result_apply_status(result) for result in results])
     return {
         "success": True,
+        "status": status.value,
         "results": results,
         "mutation_summary": _mutation_summary(operations),
     }
 
 
+def _unknown_write_outcome(
+    *,
+    operation: dict[str, Any],
+    operation_index: int,
+    cause: Any,
+) -> dict[str, Any]:
+    return envelope_err(
+        ErrorType.WRITE_OUTCOME_UNKNOWN,
+        "The write may have committed; reconcile the target state before retrying.",
+        suggestion="Inspect the target by stable identity and do not replay this operation blindly.",
+        retryable=False,
+        details={
+            "status": ApplyStatus.UNKNOWN.value,
+            "reconciliation_required": True,
+            "operation_index": operation_index,
+            "operation": operation,
+            "cause": cause,
+        },
+    )
+
+
 def _write_affected_count(write_result: Any) -> int | None:
     data = write_result
     if isinstance(write_result, dict) and "ok" in write_result:
@@ -748,14 +915,9 @@ def _create_vertex_identity_queries(
     if (
         primary_keys
         and isinstance(properties, dict)
-        and all(
-            pk in properties and properties.get(pk) not in (None, "")
-            for pk in primary_keys
-        )
+        and all(pk in properties and properties.get(pk) not in (None, "") for pk in primary_keys)
     ):
-        query = f"g.V().hasLabel({_g(label)})" + "".join(
-            f".has({_g(pk)},{_g(properties[pk])})" for pk in primary_keys
-        )
+        query = f"g.V().hasLabel({_g(label)})" + "".join(f".has({_g(pk)},{_g(properties[pk])})" for pk in primary_keys)
         queries.append(("primary_key", query))
 
     return queries
@@ -784,15 +946,23 @@ def _execution_failure(
         return error_result
 
     error = _extract_execution_error(error_result)
+    operation_status = _error_apply_status(error)
+    plan_status = aggregate_plan_status(
+        [
+            *(_result_apply_status(result) for result in results),
+            operation_status,
+        ]
+    )
     return {
         "success": False,
-        "status": "partial",
+        "status": plan_status.value,
         "results": results,
         "failed_items": [
             {
                 "operation_index": operation_index,
                 "op": operation.get("op") or operation.get("type"),
                 "label": operation.get("label"),
+                "status": operation_status.value,
                 "error": error,
             }
         ],
@@ -801,6 +971,29 @@ def _execution_failure(
     }
 
 
+def _result_apply_status(result: dict[str, Any]) -> ApplyStatus:
+    """Return the canonical operation status from an executor result."""
+
+    try:
+        return ApplyStatus(result.get("status", ApplyStatus.APPLIED.value))
+    except ValueError:
+        return ApplyStatus.UNKNOWN
+
+
+def _error_apply_status(error: dict[str, Any]) -> ApplyStatus:
+    """Classify a failed operation from the evidence carried by its error."""
+
+    error_type = error.get("type")
+    if error_type == ErrorType.WRITE_OUTCOME_UNKNOWN.value:
+        return ApplyStatus.UNKNOWN
+    if error_type in {
+        ErrorType.WRITE_CONFLICT.value,
+        "BLOCKED_BY_RELATIONSHIPS",
+    }:
+        return ApplyStatus.CONFLICT
+    return ApplyStatus.REJECTED
+
+
 def _extract_execution_error(error_result: dict[str, Any]) -> dict[str, Any]:
     if isinstance(error_result, dict) and isinstance(error_result.get("error"), dict):
         return error_result["error"]
diff --git a/hugegraph-mcp/hugegraph_mcp/tools/graph_data_gremlin.py b/hugegraph-mcp/hugegraph_mcp/tools/graph_data_gremlin.py
index a5e364e4..dbc3a40f 100644
--- a/hugegraph-mcp/hugegraph_mcp/tools/graph_data_gremlin.py
+++ b/hugegraph-mcp/hugegraph_mcp/tools/graph_data_gremlin.py
@@ -107,11 +107,9 @@ def _create_vertex_query(operation: dict[str, Any]) -> str:
 
 
 def _create_edge_query(operation: dict[str, Any]) -> str:
-    source_label = operation.get("source_label") or operation.get("outVLabel")
-    target_label = operation.get("target_label") or operation.get("inVLabel")
     query = (
-        f"g.V().hasLabel({_g(source_label)}){_has_steps(operation['source_match'])}.as('s')"
-        f".V().hasLabel({_g(target_label)}){_has_steps(operation['target_match'])}"
+        f"g.V({_g(operation['source_id'])}).as('s')"
+        f".V({_g(operation['target_id'])})"
         f".addE({_g(operation['label'])}).from('s')"
     )
     for prop, value in (operation.get("properties") or {}).items():
@@ -120,11 +118,26 @@ def _create_edge_query(operation: dict[str, Any]) -> str:
 
 
 def _delete_vertex_query(operation: dict[str, Any]) -> str:
-    return f"{_vertex_match_query(operation)}.drop()"
+    query = _delete_target_query(operation)
+    if operation.get("cascade", False) is False:
+        # The no-incident-edge precondition and drop execute in one Gremlin
+        # request instead of a client-side check-then-act sequence.
+        query += ".not(bothE())"
+    return f"{query}.drop()"
 
 
 def _delete_edge_query(operation: dict[str, Any]) -> str:
-    return f"{_edge_match_query(operation)}.drop()"
+    return f"{_delete_target_query(operation)}.drop()"
+
+
+def _delete_target_query(operation: dict[str, Any]) -> str:
+    op = str(operation.get("op") or operation.get("type"))
+    target_id = operation["target_id"]
+    if op == "delete_vertex":
+        return f"g.V({_g(target_id)})"
+    if op == "delete_edge":
+        return f"g.E({_g(target_id)})"
+    raise ValueError(f"Unsupported delete op: {op}")
 
 
 def _write_query(operation: dict[str, Any]) -> str:
diff --git a/hugegraph-mcp/hugegraph_mcp/tools/graph_data_validate.py b/hugegraph-mcp/hugegraph_mcp/tools/graph_data_validate.py
index 9be4b608..88a6a87a 100644
--- a/hugegraph-mcp/hugegraph_mcp/tools/graph_data_validate.py
+++ b/hugegraph-mcp/hugegraph_mcp/tools/graph_data_validate.py
@@ -19,6 +19,7 @@ Schema checks, property field validation, primary key match verification.
 from typing import Any
 
 from hugegraph_mcp.tools.graph_data_mapping import GraphChangePlan
+from hugegraph_mcp.tools.property_validation import property_specs, property_value_error
 from hugegraph_mcp.tools.schema_utils import (
     edge_schema_endpoint_label as _edge_schema_endpoint_label,
 )
@@ -162,6 +163,38 @@ def _validate_field_map(
         )
 
 
+def _validate_property_values(
+    *,
+    idx: int,
+    operation: dict[str, Any],
+    item_kind: str,
+    specs: dict[str, tuple[str, str]],
+    errors: list[ValidationError],
+) -> None:
+    """Validate create-operation values with the shared schema validator."""
+
+    values = operation.get("properties")
+    if not isinstance(values, dict):
+        return
+    for name, value in values.items():
+        value_error = property_value_error(
+            item_kind=item_kind,
+            item_index=idx,
+            property_name=name,
+            value=value,
+            spec=specs.get(name),
+        )
+        if value_error is not None:
+            errors.append(
+                _validation_error(
+                    idx,
+                    operation,
+                    value_error,
+                    "Use a JSON value compatible with the live HugeGraph property schema.",
+                )
+            )
+
+
 def _validate_primary_key_match(
     *,
     idx: int,
@@ -183,9 +216,7 @@ def _validate_primary_key_match(
         return
     if match.get("id") not in (None, ""):
         return
-    missing = [
-        pk for pk in primary_keys if pk not in match or match.get(pk) in (None, "")
-    ]
+    missing = [pk for pk in primary_keys if pk not in match or match.get(pk) in (None, "")]
     if missing:
         errors.append(
             _validation_error(
@@ -206,7 +237,7 @@ def validate_graph_change_plan(
 ) -> dict[str, Any]:
     """校验 change_plan 中的操作是否与 live schema 兼容。
 
-    检查项:op 类型白名单、label 存在性、properties/match 字段合法性、
+    检查项: op 类型白名单、label 存在性、properties/match 字段合法性、
     主键匹配、边端点合法性。
     """
     errors: list[ValidationError] = []
@@ -226,9 +257,7 @@ def validate_graph_change_plan(
             "warnings": [],
         }
 
-    if isinstance(change_plan, dict) and not isinstance(
-        change_plan.get("operations"), list
-    ):
+    if isinstance(change_plan, dict) and not isinstance(change_plan.get("operations"), list):
         return {
             "valid": False,
             "errors": [
@@ -248,17 +277,10 @@ def validate_graph_change_plan(
     # lookups, while ensuring validation always uses one schema snapshot.
     vertex_labels = _vertex_labels(raw_schema)
     edge_labels = _edge_labels(raw_schema)
-    vertex_properties = {
-        label: _property_names(schema.get("properties"))
-        for label, schema in vertex_labels.items()
-    }
-    edge_properties = {
-        label: _property_names(schema.get("properties"))
-        for label, schema in edge_labels.items()
-    }
-    primary_keys = {
-        label: _primary_key_names(schema) for label, schema in vertex_labels.items()
-    }
+    vertex_properties = {label: _property_names(schema.get("properties")) for label, schema in vertex_labels.items()}
+    edge_properties = {label: _property_names(schema.get("properties")) for label, schema in edge_labels.items()}
+    primary_keys = {label: _primary_key_names(schema) for label, schema in vertex_labels.items()}
+    specs = property_specs(raw_schema)
 
     for idx, operation in enumerate(operations):
         if not isinstance(operation, dict):
@@ -320,9 +342,7 @@ def validate_graph_change_plan(
             allowed = vertex_properties.get(label, set())
             pks = primary_keys.get(label, [])
             if not pks:
-                warnings.append(
-                    f"operation {idx} references vertex label '{label}' with no primary_keys"
-                )
+                warnings.append(f"operation {idx} references vertex label '{label}' with no primary_keys")
             # properties/match may reference only properties defined for the label.
             # First apply the field allowlist, then enforce stricter op constraints.
             _validate_field_map(
@@ -332,6 +352,14 @@ def validate_graph_change_plan(
                 allowed_properties=allowed,
                 errors=errors,
             )
+            if op == "create_vertex":
+                _validate_property_values(
+                    idx=idx,
+                    operation=operation,
+                    item_kind="vertex",
+                    specs=specs,
+                    errors=errors,
+                )
             _validate_field_map(
                 idx=idx,
                 operation=operation,
@@ -369,10 +397,7 @@ def validate_graph_change_plan(
                 ("source_label", source_label),
                 ("target_label", target_label),
             ):
-                if (
-                    not isinstance(endpoint_label, str)
-                    or endpoint_label not in vertex_labels
-                ):
+                if not isinstance(endpoint_label, str) or endpoint_label not in vertex_labels:
                     errors.append(
                         _validation_error(
                             idx,
@@ -409,6 +434,14 @@ def validate_graph_change_plan(
                 allowed_properties=allowed,
                 errors=errors,
             )
+            if op == "create_edge":
+                _validate_property_values(
+                    idx=idx,
+                    operation=operation,
+                    item_kind="edge",
+                    specs=specs,
+                    errors=errors,
+                )
             if isinstance(source_label, str) and source_label in vertex_labels:
                 _validate_primary_key_match(
                     idx=idx,
@@ -448,10 +481,8 @@ def validate_graph_change_plan(
 # ---- Mode operation constraints ----
 
 
-def _validate_mode_operations(
-    mode: str, change_plan: GraphChangePlan
-) -> dict[str, Any]:
-    """确保 mode 下的所有操作类型匹配,例如 import 模式不允许删除。"""
+def _validate_mode_operations(mode: str, change_plan: GraphChangePlan) -> dict[str, Any]:
+    """确保 mode 下的所有操作类型匹配, 例如 import 模式不允许删除。"""
     allowed = MODE_OPS.get(mode)
     if allowed is None:
         return {
diff --git a/hugegraph-mcp/hugegraph_mcp/tools/graph_write_adapter.py b/hugegraph-mcp/hugegraph_mcp/tools/graph_write_adapter.py
new file mode 100644
index 00000000..4f00176a
--- /dev/null
+++ b/hugegraph-mcp/hugegraph_mcp/tools/graph_write_adapter.py
@@ -0,0 +1,446 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0.
+
+"""Canonical graph write adapters and read-only reconciliation readers."""
+
+from __future__ import annotations
+
+import time
+from collections.abc import Mapping
+from typing import Any
+
+from hugegraph_mcp import gremlin_tools
+from hugegraph_mcp.config import MCPConfig
+from hugegraph_mcp.guard import Capability
+from hugegraph_mcp.plan_store import PlanStore, plan_store_from_config
+from hugegraph_mcp.tools.graph_data_gremlin import _g, _has_steps, _write_query
+from hugegraph_mcp.tools.graph_write_plan import (
+    CREATE_EDGE,
+    CREATE_VERTEX,
+    DELETE_EDGE,
+    DELETE_VERTEX,
+)
+from hugegraph_mcp.write_plan import (
+    ApplyReceipt,
+    ApplyStatus,
+    OperationPlan,
+    WritePlan,
+)
+
+_AMBIGUOUS_ERRORS = {
+    "CONNECTION_FAILED",
+    "SERVER_ERROR",
+    "TIMEOUT",
+    "WRITE_OUTCOME_UNKNOWN",
+}
+
+
+def create_vertex_adapter(plan: WritePlan, operation: OperationPlan, attempt: int) -> ApplyReceipt:
+    mismatch = _target_mismatch_receipt(plan, operation, attempt)
+    if mismatch is not None:
+        return mismatch
+    before = _read_vertex(operation)
+    classified = _classify_create(before, operation)
+    if classified is not ApplyStatus.RETRYABLE_NOT_APPLIED:
+        if classified is ApplyStatus.APPLIED:
+            classified = ApplyStatus.ALREADY_APPLIED
+        return _receipt(plan, operation, attempt, classified, before)
+
+    write_result = gremlin_tools.execute_gremlin_write(
+        _write_query(dict(operation.target["mutation"])),
+        capability=Capability.DATA_WRITE,
+    )
+    after = _read_vertex(operation)
+    after_status = _classify_create(after, operation)
+    if after_status is ApplyStatus.APPLIED:
+        return _receipt(
+            plan,
+            operation,
+            attempt,
+            ApplyStatus.APPLIED,
+            after,
+            committed=True,
+        )
+    return _post_write_receipt(plan, operation, attempt, write_result, after_status, after)
+
+
+def create_edge_adapter(plan: WritePlan, operation: OperationPlan, attempt: int) -> ApplyReceipt:
+    mismatch = _target_mismatch_receipt(plan, operation, attempt)
+    if mismatch is not None:
+        return mismatch
+    source_id = _resolve_endpoint_id(plan, operation, "source")
+    target_id = _resolve_endpoint_id(plan, operation, "target")
+    endpoint_conflict = _validate_edge_endpoints(operation, source_id, target_id)
+    if endpoint_conflict is not None:
+        return _receipt(
+            plan,
+            operation,
+            attempt,
+            ApplyStatus.CONFLICT,
+            endpoint_conflict,
+            reason_code="ENDPOINT_STATE_CHANGED",
+        )
+
+    before = _read_edge(operation, source_id, target_id)
+    classified = _classify_create(before, operation)
+    if classified is not ApplyStatus.RETRYABLE_NOT_APPLIED:
+        if classified is ApplyStatus.APPLIED:
+            classified = ApplyStatus.ALREADY_APPLIED
+        return _receipt(plan, operation, attempt, classified, before)
+
+    mutation = dict(operation.target["mutation"])
+    mutation["source_id"] = source_id
+    mutation["target_id"] = target_id
+    write_result = gremlin_tools.execute_gremlin_write(_write_query(mutation), capability=Capability.DATA_WRITE)
+    after = _read_edge(operation, source_id, target_id)
+    after_status = _classify_create(after, operation)
+    if after_status is ApplyStatus.APPLIED:
+        observed = dict(after)
+        observed["source_id"] = source_id
+        observed["target_id"] = target_id
+        return _receipt(
+            plan,
+            operation,
+            attempt,
+            ApplyStatus.APPLIED,
+            observed,
+            committed=True,
+        )
+    return _post_write_receipt(plan, operation, attempt, write_result, after_status, after)
+
+
+def delete_vertex_adapter(plan: WritePlan, operation: OperationPlan, attempt: int) -> ApplyReceipt:
+    return _receipt(
+        plan,
+        operation,
+        attempt,
+        ApplyStatus.REJECTED,
+        {"capability": "isolated_vertex_delete", "supported": False},
+        reason_code="FEATURE_DISABLED",
+    )
+
+
+def delete_edge_adapter(plan: WritePlan, operation: OperationPlan, attempt: int) -> ApplyReceipt:
+    return _delete_adapter(plan, operation, attempt)
+
+
+def _delete_adapter(plan: WritePlan, operation: OperationPlan, attempt: int) -> ApplyReceipt:
+    mismatch = _target_mismatch_receipt(plan, operation, attempt)
+    if mismatch is not None:
+        return mismatch
+    before = _read_delete_target(operation)
+    if before.get("exists") is False:
+        return _receipt(plan, operation, attempt, ApplyStatus.ALREADY_APPLIED, before)
+    if before != dict(operation.expected_state):
+        return _receipt(
+            plan,
+            operation,
+            attempt,
+            ApplyStatus.CONFLICT,
+            before,
+            reason_code="DELETE_TARGET_CHANGED",
+        )
+    write_result = gremlin_tools.execute_gremlin_write(
+        _write_query(dict(operation.target["mutation"])),
+        capability=Capability.DATA_WRITE,
+    )
+    after = _read_delete_target(operation)
+    if after.get("exists") is False:
+        return _receipt(
+            plan,
+            operation,
+            attempt,
+            ApplyStatus.APPLIED,
+            after,
+            committed=True,
+        )
+    return _post_write_receipt(plan, operation, attempt, write_result, ApplyStatus.CONFLICT, after)
+
+
+def create_vertex_reader(_plan: WritePlan, operation: OperationPlan) -> Mapping[str, Any]:
+    return _reconcile_create_state(_read_vertex(operation), operation)
+
+
+def create_edge_reader(plan: WritePlan, operation: OperationPlan) -> Mapping[str, Any]:
+    source_id = _resolve_endpoint_id(plan, operation, "source")
+    target_id = _resolve_endpoint_id(plan, operation, "target")
+    return _reconcile_create_state(_read_edge(operation, source_id, target_id), operation)
+
+
+def delete_reader(_plan: WritePlan, operation: OperationPlan) -> Mapping[str, Any]:
+    return _read_delete_target(operation)
+
+
+def register_graph_write_adapters(registry: Any) -> None:
+    """Register only operations backed by verified HugeGraph 1.7 primitives."""
+
+    registry.register(DELETE_EDGE, delete_edge_adapter)
+
+
+def register_graph_reconcile_readers(registry: Any) -> None:
+    registry.register(CREATE_VERTEX, create_vertex_reader)
+    registry.register(CREATE_EDGE, create_edge_reader)
+    registry.register(DELETE_VERTEX, delete_reader)
+    registry.register(DELETE_EDGE, delete_reader)
+
+
+def _read_vertex(operation: OperationPlan) -> dict[str, Any]:
+    target = operation.target
+    identity = dict(target["identity"])
+    query = f"g.V().hasLabel({_g(operation.desired_state['label'])})"
+    if "id" in identity:
+        query += f".hasId({_g(identity['id'])})"
+    else:
+        query += _has_steps(dict(identity["primary_keys"]))
+    elements = _read_elements(f"{query}.limit(2).elementMap()")
+    return _create_observed_state(elements, operation)
+
+
+def _read_edge(operation: OperationPlan, source_id: Any, target_id: Any) -> dict[str, Any]:
+    query = (
+        f"g.V({_g(source_id)}).outE({_g(operation.desired_state['label'])})"
+        f".where(inV().hasId({_g(target_id)}))"
+        f"{_has_steps(dict(operation.target['identity_properties']))}"
+        ".limit(2).elementMap()"
+    )
+    elements = _read_elements(query)
+    return _create_observed_state(elements, operation)
+
+
+def _read_delete_target(operation: OperationPlan) -> dict[str, Any]:
+    expected = dict(operation.expected_state)
+    prefix = "g.V" if expected["element_type"] == "vertex" else "g.E"
+    elements = _read_elements(f"{prefix}({_g(expected['id'])}).limit(2).elementMap()")
+    if not elements:
+        return {
+            "exists": False,
+            "element_type": expected["element_type"],
+            "id": expected["id"],
+            "label": expected["label"],
+        }
+    if len(elements) != 1:
+        return {"exists": True, "ambiguous": True}
+    element = _normalize_element(elements[0])
+    return {
+        "exists": True,
+        "element_type": expected["element_type"],
+        "id": element.get("id", expected["id"]),
+        "label": element.get("label"),
+    }
+
+
+def _create_observed_state(elements: list[Any], operation: OperationPlan) -> dict[str, Any]:
+    if not elements:
+        return {"exists": False}
+    if len(elements) != 1:
+        return {"exists": True, "ambiguous": True, "matched_count": len(elements)}
+    element = _normalize_element(elements[0])
+    desired = dict(operation.desired_state)
+    observed_properties = {key: value for key, value in element.items() if key not in {"id", "label"}}
+    state: dict[str, Any] = {
+        "exists": True,
+        "element_type": desired["element_type"],
+        "label": element.get("label"),
+        "properties": observed_properties,
+    }
+    if "identity" in desired:
+        state["identity"] = dict(desired["identity"])
+    if "source" in desired:
+        state["source"] = dict(desired["source"])
+        state["target"] = dict(desired["target"])
+        state["identity_properties"] = dict(desired["identity_properties"])
+    if "id" in element:
+        state["id"] = element["id"]
+    return state
+
+
+def _classify_create(observed: Mapping[str, Any], operation: OperationPlan) -> ApplyStatus:
+    if observed.get("exists") is False:
+        return ApplyStatus.RETRYABLE_NOT_APPLIED
+    if observed.get("ambiguous"):
+        return ApplyStatus.CONFLICT
+    desired = dict(operation.desired_state)
+    comparable = dict(observed)
+    comparable.pop("id", None)
+    return ApplyStatus.APPLIED if comparable == desired else ApplyStatus.CONFLICT
+
+
+def _reconcile_create_state(observed: Mapping[str, Any], operation: OperationPlan) -> Mapping[str, Any]:
+    status = _classify_create(observed, operation)
+    if status is ApplyStatus.APPLIED:
+        return dict(operation.desired_state)
+    if status is ApplyStatus.RETRYABLE_NOT_APPLIED:
+        return dict(operation.expected_state)
+    return dict(observed)
+
+
+def _validate_edge_endpoints(operation: OperationPlan, source_id: Any, target_id: Any) -> dict[str, Any] | None:
+    mutation = operation.target["mutation"]
+    checks = (
+        ("source", source_id, mutation.get("source_label") or mutation.get("outVLabel")),
+        ("target", target_id, mutation.get("target_label") or mutation.get("inVLabel")),
+    )
+    for endpoint, endpoint_id, label in checks:
+        elements = _read_elements(f"g.V({_g(endpoint_id)}).hasLabel({_g(label)}).limit(2).elementMap()")
+        if len(elements) != 1:
+            return {
+                "exists": True,
+                "endpoint": endpoint,
+                "id": endpoint_id,
+                "label": label,
+                "matched_count": len(elements),
+            }
+    return None
+
+
+def _resolve_endpoint_id(
+    plan: WritePlan,
+    operation: OperationPlan,
+    endpoint: str,
+    *,
+    store: PlanStore | None = None,
+) -> Any:
+    endpoint_target = dict(operation.target[endpoint])
+    if "id" in endpoint_target:
+        return endpoint_target["id"]
+    dependency_id = endpoint_target["operation_id"]
+    store = store or plan_store_from_config()
+    record = store.get_plan_record(plan.plan_id)
+    if record is None:
+        raise RuntimeError("dependency plan is unavailable")
+    dependency = next(
+        (item for item in record["operations"] if item["operation_id"] == dependency_id),
+        None,
+    )
+    receipt = dependency.get("receipt") if dependency is not None else None
+    observed = receipt.get("observed_state") if isinstance(receipt, dict) else None
+    endpoint_id = observed.get("id") if isinstance(observed, dict) else None
+    if endpoint_id in (None, ""):
+        raise RuntimeError("dependency receipt has no stable vertex ID")
+    return endpoint_id
+
+
+def _read_elements(query: str) -> list[Any]:
+    result = gremlin_tools.execute_gremlin_read(query)
+    if isinstance(result, dict) and result.get("ok") is False:
+        raise RuntimeError(str(result.get("error", {}).get("type") or "READ_FAILED"))
+    if isinstance(result, dict) and result.get("success") is False:
+        raise RuntimeError(str(result.get("error_type") or "READ_FAILED"))
+    value: Any = result.get("data") if isinstance(result, dict) else result
+    while isinstance(value, dict) and "data" in value:
+        value = value["data"]
+    if value is None:
+        return []
+    return value if isinstance(value, list) else [value]
+
+
+def _normalize_element(element: Any) -> dict[str, Any]:
+    if not isinstance(element, Mapping):
+        return {"value": element}
+    normalized: dict[str, Any] = {}
+    for raw_key, value in element.items():
+        key = str(raw_key)
+        lowered = key.lower()
+        if lowered in {"id", "t.id", "~id"} or lowered.endswith(" id"):
+            normalized["id"] = value
+        elif lowered in {"label", "t.label", "~label"} or lowered.endswith(" label"):
+            normalized["label"] = value
+        else:
+            normalized[key] = value
+    return normalized
+
+
+def _post_write_receipt(
+    plan: WritePlan,
+    operation: OperationPlan,
+    attempt: int,
+    write_result: Any,
+    observed_status: ApplyStatus,
+    observed: Mapping[str, Any],
+) -> ApplyReceipt:
+    write_reported_success = isinstance(write_result, dict) and (
+        write_result.get("ok") is True or write_result.get("success") is True
+    )
+    if (
+        observed_status is ApplyStatus.CONFLICT
+        and operation.kind in {CREATE_VERTEX, CREATE_EDGE}
+        and write_reported_success
+    ):
+        status = ApplyStatus.UNKNOWN
+        reason = "POST_CREATE_IDENTITY_AMBIGUOUS"
+    elif observed_status is ApplyStatus.CONFLICT:
+        status = ApplyStatus.CONFLICT
+        reason = "POST_WRITE_STATE_CONFLICT"
+    elif _is_ambiguous_write_result(write_result):
+        status = ApplyStatus.UNKNOWN
+        reason = "WRITE_RESULT_AMBIGUOUS"
+    else:
+        status = ApplyStatus.REJECTED
+        reason = "WRITE_REJECTED_WITHOUT_SIDE_EFFECT"
+    return _receipt(
+        plan,
+        operation,
+        attempt,
+        status,
+        observed,
+        reason_code=reason,
+        reconciliation_required=status is ApplyStatus.UNKNOWN,
+    )
+
+
+def _is_ambiguous_write_result(result: Any) -> bool:
+    if not isinstance(result, dict):
+        return True
+    if result.get("ok") is False:
+        return str(result.get("error", {}).get("type")) in _AMBIGUOUS_ERRORS
+    if result.get("success") is False:
+        return str(result.get("error_type", "")).upper() in _AMBIGUOUS_ERRORS
+    return False
+
+
+def _target_mismatch_receipt(plan: WritePlan, operation: OperationPlan, attempt: int) -> ApplyReceipt | None:
+    cfg = MCPConfig.from_env()
+    current = (cfg.url, cfg.graph, cfg.graphspace or "DEFAULT", cfg.user)
+    planned = (
+        plan.graph_target.graph_url,
+        plan.graph_target.graph_name,
+        plan.graph_target.graphspace or "DEFAULT",
+        plan.principal,
+    )
+    if current == planned:
+        return None
+    return _receipt(
+        plan,
+        operation,
+        attempt,
+        ApplyStatus.REJECTED,
+        {"target_matches": False},
+        reason_code="TARGET_CHANGED",
+    )
+
+
+def _receipt(
+    plan: WritePlan,
+    operation: OperationPlan,
+    attempt: int,
+    status: ApplyStatus,
+    observed_state: Mapping[str, Any],
+    *,
+    reason_code: str | None = None,
+    reconciliation_required: bool = False,
+    committed: bool = False,
+) -> ApplyReceipt:
+    if reason_code is None:
+        reason_code = status.value
+    return ApplyReceipt(
+        plan_id=plan.plan_id,
+        operation_id=operation.operation_id,
+        status=status,
+        observed_state=observed_state,
+        reason_code=reason_code,
+        attempt=attempt,
+        reconciliation_required=reconciliation_required,
+        committed_at=int(time.time()) if committed else None,
+    )
diff --git a/hugegraph-mcp/hugegraph_mcp/tools/graph_write_plan.py b/hugegraph-mcp/hugegraph_mcp/tools/graph_write_plan.py
new file mode 100644
index 00000000..b13c8923
--- /dev/null
+++ b/hugegraph-mcp/hugegraph_mcp/tools/graph_write_plan.py
@@ -0,0 +1,230 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0.
+
+"""Compile validated graph changes into the canonical durable write model."""
+
+from __future__ import annotations
+
+import hashlib
+import json
+import time
+from typing import Any
+from uuid import uuid4
+
+from hugegraph_mcp.plan_hash import PlanContext
+from hugegraph_mcp.tools.graph_data_validate import _operations
+from hugegraph_mcp.tools.schema_utils import (
+    normalized_schema_summary,
+    primary_key_names,
+    schema_payload,
+)
+from hugegraph_mcp.write_plan import (
+    GraphTarget,
+    OperationPlan,
+    PlanStatus,
+    WritePlan,
+)
+
+CREATE_VERTEX = "CREATE_VERTEX"
+CREATE_EDGE = "CREATE_EDGE"
+DELETE_VERTEX = "DELETE_VERTEX"
+DELETE_EDGE = "DELETE_EDGE"
+GRAPH_OPERATION_KINDS = (CREATE_VERTEX, CREATE_EDGE, DELETE_VERTEX, DELETE_EDGE)
+
+
+def compile_graph_write_plan(
+    change_plan: Any,
+    *,
+    plan_context: PlanContext,
+    live_schema: dict[str, Any],
+    tool_name: str = "manage_graph_data",
+) -> WritePlan:
+    """Return an ordered immutable plan from a validated compiled change plan."""
+
+    plan_id = f"wp_{uuid4().hex}"
+    operation_ids = [f"{plan_id}:op:{index:04d}" for index, _ in enumerate(_operations(change_plan))]
+    canonical_operations: list[OperationPlan] = []
+    for index, raw in enumerate(_operations(change_plan)):
+        canonical_operations.append(
+            _compile_operation(
+                raw,
+                operation_id=operation_ids[index],
+                operation_ids=operation_ids,
+                live_schema=live_schema,
+            )
+        )
+
+    created_at = int(time.time())
+    expires_at = max(created_at + 1, int(plan_context.expires_at))
+    payload_digest = _digest(change_plan)
+    return WritePlan(
+        plan_id=plan_id,
+        tool_name=tool_name,
+        graph_target=GraphTarget(
+            graph_url=plan_context.graph_url,
+            graph_name=plan_context.graph_name,
+            graphspace=plan_context.graphspace,
+        ),
+        principal=plan_context.principal,
+        operations=tuple(canonical_operations),
+        payload_digest=payload_digest,
+        schema_fingerprint=_digest(normalized_schema_summary(live_schema)),
+        status=PlanStatus.ISSUED,
+        created_at=created_at,
+        expires_at=expires_at,
+    )
+
+
+def _compile_operation(
+    raw: dict[str, Any],
+    *,
+    operation_id: str,
+    operation_ids: list[str],
+    live_schema: dict[str, Any],
+) -> OperationPlan:
+    mutation = dict(raw)
+    op = str(raw.get("op") or raw.get("type"))
+    if op == "create_vertex":
+        identity = _vertex_identity(raw, live_schema)
+        desired = {
+            "exists": True,
+            "element_type": "vertex",
+            "label": raw["label"],
+            "identity": identity,
+            "properties": dict(raw.get("properties") or {}),
+        }
+        return _operation(
+            operation_id,
+            CREATE_VERTEX,
+            {"mutation": mutation, "identity": identity},
+            {"exists": False},
+            desired,
+        )
+    if op == "create_edge":
+        source, source_dependencies = _endpoint_target(raw, "source", operation_ids)
+        target, target_dependencies = _endpoint_target(raw, "target", operation_ids)
+        identity_properties = _edge_identity_properties(raw, live_schema)
+        desired = {
+            "exists": True,
+            "element_type": "edge",
+            "label": raw["label"],
+            "source": source,
+            "target": target,
+            "identity_properties": identity_properties,
+            "properties": dict(raw.get("properties") or {}),
+        }
+        dependencies = tuple(dict.fromkeys(source_dependencies + target_dependencies))
+        return _operation(
+            operation_id,
+            CREATE_EDGE,
+            {
+                "mutation": mutation,
+                "source": source,
+                "target": target,
+                "identity_properties": identity_properties,
+            },
+            {"exists": False},
+            desired,
+            depends_on=dependencies,
+        )
+    if op in {"delete_vertex", "delete_edge"}:
+        target_id = raw.get("target_id")
+        if target_id in (None, ""):
+            raise ValueError(f"{op} requires a compiled target_id")
+        element_type = "vertex" if op == "delete_vertex" else "edge"
+        kind = DELETE_VERTEX if op == "delete_vertex" else DELETE_EDGE
+        return _operation(
+            operation_id,
+            kind,
+            {"mutation": mutation, "id": target_id, "label": raw["label"]},
+            {
+                "exists": True,
+                "element_type": element_type,
+                "id": target_id,
+                "label": raw["label"],
+            },
+            {"exists": False},
+        )
+    raise ValueError(f"unsupported graph operation: {op}")
+
+
+def _operation(
+    operation_id: str,
+    kind: str,
+    target: dict[str, Any],
+    expected_state: dict[str, Any],
+    desired_state: dict[str, Any],
+    *,
+    depends_on: tuple[str, ...] = (),
+) -> OperationPlan:
+    return OperationPlan(
+        operation_id=operation_id,
+        kind=kind,
+        target=target,
+        expected_state=expected_state,
+        desired_state=desired_state,
+        depends_on=depends_on,
+        idempotency_key=_digest({"kind": kind, "target": target, "desired_state": desired_state}),
+    )
+
+
+def _endpoint_target(raw: dict[str, Any], endpoint: str, operation_ids: list[str]) -> tuple[dict[str, Any], list[str]]:
+    dependency_index = raw.get(f"{endpoint}_operation_index")
+    if dependency_index is not None:
+        if not isinstance(dependency_index, int) or dependency_index < 0 or dependency_index >= len(operation_ids):
+            raise ValueError(f"invalid {endpoint} dependency operation index")
+        operation_id = operation_ids[dependency_index]
+        return {"operation_id": operation_id}, [operation_id]
+    endpoint_id = raw.get(f"{endpoint}_id")
+    if endpoint_id in (None, ""):
+        raise ValueError(f"create_edge requires compiled {endpoint}_id")
+    return {"id": endpoint_id}, []
+
+
+def _vertex_identity(operation: dict[str, Any], live_schema: dict[str, Any]) -> dict[str, Any]:
+    explicit_id = operation.get("id")
+    if explicit_id not in (None, ""):
+        return {"id": explicit_id}
+    label = _vertex_label(live_schema, str(operation["label"]))
+    primary_keys = primary_key_names(label) if label is not None else []
+    properties = operation.get("properties") or {}
+    if not primary_keys or any(key not in properties for key in primary_keys):
+        raise ValueError("create_vertex requires a stable ID or complete primary-key identity")
+    return {"primary_keys": {key: properties[key] for key in primary_keys}}
+
+
+def _edge_identity_properties(operation: dict[str, Any], live_schema: dict[str, Any]) -> dict[str, Any]:
+    edge_label = _edge_label(live_schema, str(operation["label"]))
+    if edge_label is None:
+        raise ValueError(f"unknown edge label: {operation['label']}")
+    sort_keys = edge_label.get("sort_keys") or edge_label.get("sortKeys") or []
+    frequency = str(edge_label.get("frequency") or "SINGLE").upper()
+    properties = operation.get("properties") or {}
+    if frequency == "MULTIPLE" and not sort_keys:
+        raise ValueError("MULTIPLE create_edge requires schema sort keys for stable identity")
+    if any(key not in properties for key in sort_keys):
+        raise ValueError("create_edge is missing a required sort-key identity value")
+    return {key: properties[key] for key in sort_keys}
+
+
+def _vertex_label(live_schema: dict[str, Any], name: str) -> dict[str, Any] | None:
+    schema = schema_payload(live_schema) or {}
+    return next(
+        (item for item in schema.get("vertexlabels") or [] if isinstance(item, dict) and item.get("name") == name),
+        None,
+    )
+
+
+def _edge_label(live_schema: dict[str, Any], name: str) -> dict[str, Any] | None:
+    schema = schema_payload(live_schema) or {}
+    return next(
+        (item for item in schema.get("edgelabels") or [] if isinstance(item, dict) and item.get("name") == name),
+        None,
+    )
+
+
+def _digest(value: Any) -> str:
+    encoded = json.dumps(value, sort_keys=True, separators=(",", ":"), default=str).encode("utf-8")
+    return hashlib.sha256(encoded).hexdigest()
diff --git a/hugegraph-mcp/hugegraph_mcp/tools/ingest_graph_data.py b/hugegraph-mcp/hugegraph_mcp/tools/ingest_graph_data.py
index 94fb391a..6b8b8ff2 100644
--- a/hugegraph-mcp/hugegraph_mcp/tools/ingest_graph_data.py
+++ b/hugegraph-mcp/hugegraph_mcp/tools/ingest_graph_data.py
@@ -40,20 +40,18 @@ validate_graph_payload() 对 vertices/edges 做全面 schema 校验:
 import json
 from copy import deepcopy
 from typing import Any
-from uuid import uuid4
 
 from hugegraph_mcp.config import MCPConfig
-from hugegraph_mcp.confirmable_workflow import (
-    issue_plan,
-    mark_readonly_preview,
-    plan_hash_error,
-    replayed_plan_error,
-    verify_and_consume_plan,
-)
+from hugegraph_mcp.confirmable_workflow import mark_readonly_preview
 from hugegraph_mcp.envelope import ErrorType, envelope_err, envelope_ok
 from hugegraph_mcp.guard import Capability, guard
-from hugegraph_mcp.hugegraph_ai_client import post
 from hugegraph_mcp.tools.live_schema import fetch_live_schema_or_none
+from hugegraph_mcp.tools.property_validation import (
+    property_specs as _property_specs,
+)
+from hugegraph_mcp.tools.property_validation import (
+    property_value_error as _property_value_error,
+)
 from hugegraph_mcp.tools.schema_utils import (
     edge_schema_endpoint_label as _edge_schema_endpoint_label,
 )
@@ -69,96 +67,7 @@ from hugegraph_mcp.tools.schema_utils import (
 from hugegraph_mcp.tools.schema_utils import (
     schema_payload as _schema_payload,
 )
-
-_DATA_TYPE_ALIASES = {"INTEGER": "INT", "BOOL": "BOOLEAN"}
-_SUPPORTED_DATA_TYPES = {
-    "TEXT",
-    "UUID",
-    "INT",
-    "LONG",
-    "DOUBLE",
-    "FLOAT",
-    "BOOLEAN",
-    "DATE",
-    "BYTE",
-    "BLOB",
-    "OBJECT",
-}
-_SUPPORTED_CARDINALITIES = {"SINGLE", "LIST", "SET"}
-
-
-def _property_specs(raw_schema: dict[str, Any]) -> dict[str, tuple[str, str]]:
-    specs: dict[str, tuple[str, str]] = {}
-    normalized_schema = normalized_schema_summary(raw_schema) or {}
-    for prop in normalized_schema.get("propertykeys", []):
-        if not isinstance(prop, dict):
-            continue
-        name = prop.get("name")
-        data_type = prop.get("data_type")
-        if isinstance(name, str) and isinstance(data_type, str):
-            cardinality = prop.get("cardinality") or "SINGLE"
-            normalized_data_type = data_type.strip().upper()
-            specs[name] = (
-                _DATA_TYPE_ALIASES.get(normalized_data_type, normalized_data_type),
-                str(cardinality).strip().upper(),
-            )
-    return specs
-
-
-def _value_matches_type(value: Any, data_type: str) -> bool:
-    if value is None:
-        return True
-    if data_type in {"TEXT", "UUID"}:
-        return isinstance(value, str)
-    if data_type in {"INT", "LONG", "BYTE"}:
-        return isinstance(value, int) and not isinstance(value, bool)
-    if data_type in {"FLOAT", "DOUBLE"}:
-        return isinstance(value, (int, float)) and not isinstance(value, bool)
-    if data_type == "BOOLEAN":
-        return isinstance(value, bool)
-    if data_type in {"DATE", "BLOB"}:
-        return isinstance(value, str)
-    if data_type == "OBJECT":
-        return isinstance(value, dict)
-    return False
-
-
-def _property_value_error(
-    *,
-    item_kind: str,
-    item_index: int,
-    property_name: str,
-    value: Any,
-    spec: tuple[str, str] | None,
-) -> str | None:
-    if spec is None:
-        return None
-
-    data_type, cardinality = spec
-    prefix = f"{item_kind} {item_index} property '{property_name}'"
-    if data_type not in _SUPPORTED_DATA_TYPES:
-        return f"{prefix} unsupported data_type '{data_type}'"
-    if cardinality not in _SUPPORTED_CARDINALITIES:
-        return f"{prefix} unsupported cardinality '{cardinality}'"
-    if cardinality in {"LIST", "SET"}:
-        if value is None:
-            return None
-        if not isinstance(value, list):
-            return (
-                f"{prefix} expects {cardinality} of {data_type}, "
-                f"got {type(value).__name__}"
-            )
-        for element_index, element in enumerate(value):
-            if element is None or not _value_matches_type(element, data_type):
-                return (
-                    f"{prefix} element {element_index} expects {data_type}, "
-                    f"got {type(element).__name__}"
-                )
-        return None
-
-    if not _value_matches_type(value, data_type):
-        return f"{prefix} expects {data_type}, got {type(value).__name__}"
-    return None
+from hugegraph_mcp.write_plan import ApplyStatus, PlanStatus, aggregate_plan_status
 
 
 def _indexed_labels(raw_schema: dict[str, Any]) -> dict[str, set[str]]:
@@ -217,18 +126,12 @@ def _endpoint_identities(
         if _identity_value_present(explicit_id):
             identities.append((label, "id", explicit_id))
         if primary_keys:
-            missing = [
-                pk
-                for pk in primary_keys
-                if pk not in value or not _identity_value_present(value.get(pk))
-            ]
+            missing = [pk for pk in primary_keys if pk not in value or not _identity_value_present(value.get(pk))]
             if missing:
                 if identities:
                     return identities, None
                 return identities, missing[0]
-            identities.append(
-                (label, "pk", tuple(value.get(pk) for pk in primary_keys))
-            )
+            identities.append((label, "pk", tuple(value.get(pk) for pk in primary_keys)))
         return identities, None
 
     if _identity_value_present(value):
@@ -259,10 +162,7 @@ def _canonical_json_key(value: Any) -> str:
 
 def _normalize_value(value: Any) -> Any:
     if isinstance(value, dict):
-        return {
-            key: _normalize_value(value[key])
-            for key in sorted(value, key=lambda item: str(item))
-        }
+        return {key: _normalize_value(value[key]) for key in sorted(value, key=lambda item: str(item))}
     if isinstance(value, list):
         return [_normalize_value(item) for item in value]
     return value
@@ -281,7 +181,7 @@ def _vertex_sort_key(
         props = vertex.get("properties")
         primary_keys = schema_primary_keys.get(label, [])
         if isinstance(props, dict) and primary_keys:
-            identity = props.get(primary_keys[0])
+            identity = tuple(props.get(primary_key) for primary_key in primary_keys)
         elif isinstance(props, dict) and props:
             first_key = min(props, key=lambda item: str(item))
             identity = props.get(first_key)
@@ -353,9 +253,7 @@ def _schema_vertex_info(raw_schema: dict[str, Any]) -> dict[str, dict[str, Any]]
             info[name] = {
                 "id": vertex_label.get("id"),
                 "id_strategy": str(
-                    vertex_label.get("id_strategy")
-                    or vertex_label.get("idStrategy")
-                    or "PRIMARY_KEY"
+                    vertex_label.get("id_strategy") or vertex_label.get("idStrategy") or "PRIMARY_KEY"
                 ).upper(),
                 "primary_keys": _primary_key_names(vertex_label),
             }
@@ -392,9 +290,7 @@ def _vertex_backend_id(
     primary_keys = vertex_info.get(label, {}).get("primary_keys", [])
     if not primary_keys:
         return None
-    if not all(
-        pk in props and _identity_value_present(props.get(pk)) for pk in primary_keys
-    ):
+    if not all(pk in props and _identity_value_present(props.get(pk)) for pk in primary_keys):
         return None
 
     values = tuple(props.get(pk) for pk in primary_keys)
@@ -409,9 +305,7 @@ def _vertex_identity_map(
     # A vertex may have an explicit ID, a PRIMARY_KEY backend ID, and a primary-key
     # tuple; resolving any of them must target the same backend vertex.
     vertex_info = _schema_vertex_info(raw_schema)
-    schema_primary_keys = {
-        label: info.get("primary_keys", []) for label, info in vertex_info.items()
-    }
+    schema_primary_keys = {label: info.get("primary_keys", []) for label, info in vertex_info.items()}
     identities: dict[tuple[str, str, Any], Any] = {}
 
     for vertex in vertices:
@@ -435,10 +329,7 @@ def _vertex_identity_map(
         if (
             isinstance(props, dict)
             and primary_keys
-            and all(
-                pk in props and _identity_value_present(props.get(pk))
-                for pk in primary_keys
-            )
+            and all(pk in props and _identity_value_present(props.get(pk)) for pk in primary_keys)
         ):
             values = tuple(props.get(pk) for pk in primary_keys)
             identities[(label, "pk", values)] = backend_id or explicit_id
@@ -470,10 +361,7 @@ def _endpoint_backend_id(
             return explicit_id
 
         primary_keys = schema_primary_keys.get(label, [])
-        if primary_keys and all(
-            pk in value and _identity_value_present(value.get(pk))
-            for pk in primary_keys
-        ):
+        if primary_keys and all(pk in value and _identity_value_present(value.get(pk)) for pk in primary_keys):
             values = tuple(value.get(pk) for pk in primary_keys)
             return _canonical_primary_key_id(label, values, vertex_info)
         return None
@@ -579,9 +467,7 @@ def validate_graph_payload(
                 schema_vlabels.add(name)
                 schema_props[name] = _property_names(vl.get("properties", []))
                 schema_primary_keys[name] = _primary_key_names(vl)
-                schema_id_strategies[name] = str(
-                    vl.get("id_strategy") or vl.get("idStrategy") or "PRIMARY_KEY"
-                ).upper()
+                schema_id_strategies[name] = str(vl.get("id_strategy") or vl.get("idStrategy") or "PRIMARY_KEY").upper()
         for el in raw.get("edgelabels", []):
             if not isinstance(el, dict):
                 continue
@@ -611,17 +497,9 @@ def validate_graph_payload(
                 schema_prop_names = schema_props.get(label, set())
                 for prop_name, prop_value in props.items():
                     if prop_value is None or prop_value == "":
-                        warnings.append(
-                            f"vertex {idx} property '{prop_name}' has empty value"
-                        )
-                    if (
-                        schema_available
-                        and label in schema_props
-                        and prop_name not in schema_prop_names
-                    ):
-                        errors.append(
-                            f"vertex {idx} property '{prop_name}' does not exist on label '{label}'"
-                        )
+                        warnings.append(f"vertex {idx} property '{prop_name}' has empty value")
+                    if schema_available and label in schema_props and prop_name not in schema_prop_names:
+                        errors.append(f"vertex {idx} property '{prop_name}' does not exist on label '{label}'")
                     value_error = _property_value_error(
                         item_kind="vertex",
                         item_index=idx,
@@ -636,9 +514,7 @@ def validate_graph_payload(
             explicit_id = vertex.get("id")
             if id_strategy == "CUSTOMIZE_STRING":
                 if not _identity_value_present(explicit_id):
-                    errors.append(
-                        f"vertex {idx} missing required id for CUSTOMIZE_STRING label '{label}'"
-                    )
+                    errors.append(f"vertex {idx} missing required id for CUSTOMIZE_STRING label '{label}'")
                 elif not isinstance(explicit_id, str):
                     errors.append(
                         f"vertex {idx} id for CUSTOMIZE_STRING label '{label}' must be a string, "
@@ -646,9 +522,7 @@ def validate_graph_payload(
                     )
             elif id_strategy == "CUSTOMIZE_NUMBER":
                 if not _identity_value_present(explicit_id):
-                    errors.append(
-                        f"vertex {idx} missing required id for CUSTOMIZE_NUMBER label '{label}'"
-                    )
+                    errors.append(f"vertex {idx} missing required id for CUSTOMIZE_NUMBER label '{label}'")
                 elif not isinstance(explicit_id, int) or isinstance(explicit_id, bool):
                     errors.append(
                         f"vertex {idx} id for CUSTOMIZE_NUMBER label '{label}' must be an integer, "
@@ -661,13 +535,8 @@ def validate_graph_payload(
                     props = {}
                 for pk in primary_keys:
                     if pk not in props or not _identity_value_present(props.get(pk)):
-                        errors.append(
-                            f"vertex {idx} missing primary key value for label '{label}': {pk}"
-                        )
-                if all(
-                    pk in props and _identity_value_present(props.get(pk))
-                    for pk in primary_keys
-                ):
+                        errors.append(f"vertex {idx} missing primary key value for label '{label}': {pk}")
+                if all(pk in props and _identity_value_present(props.get(pk)) for pk in primary_keys):
                     identity = (
                         label,
                         "pk",
@@ -704,11 +573,13 @@ def validate_graph_payload(
             tgt_label, target = _edge_endpoint(edge, "target")
             if _has_mixed_endpoint_forms(edge, "source"):
                 errors.append(
-                    f"edge {idx} mixes source and outV endpoint forms; use either source/source_label or outV/outVLabel, not both"
+                    f"edge {idx} mixes source and outV endpoint forms; use either "
+                    "source/source_label or outV/outVLabel, not both"
                 )
             if _has_mixed_endpoint_forms(edge, "target"):
                 errors.append(
-                    f"edge {idx} mixes target and inV endpoint forms; use either target/target_label or inV/inVLabel, not both"
+                    f"edge {idx} mixes target and inV endpoint forms; use either "
+                    "target/target_label or inV/inVLabel, not both"
                 )
             if label in (None, ""):
                 errors.append(f"edge {idx} missing required field: label")
@@ -719,46 +590,34 @@ def validate_graph_payload(
             if label:
                 edge_labels.add(label)
                 if schema_available and label not in schema_elabels:
-                    errors.append(
-                        f"edge {idx} label '{label}' does not exist in schema"
-                    )
+                    errors.append(f"edge {idx} label '{label}' does not exist in schema")
             if schema_available:
                 if src_label and src_label not in schema_vlabels:
-                    errors.append(
-                        f"edge {idx} source_label '{src_label}' does not exist in schema"
-                    )
+                    errors.append(f"edge {idx} source_label '{src_label}' does not exist in schema")
                 if tgt_label and tgt_label not in schema_vlabels:
-                    errors.append(
-                        f"edge {idx} target_label '{tgt_label}' does not exist in schema"
-                    )
+                    errors.append(f"edge {idx} target_label '{tgt_label}' does not exist in schema")
             if label and label in schema_elabels:
                 schema_edge = schema_elabels[label]
                 expected_src = _edge_schema_endpoint_label(schema_edge, "source")
                 expected_tgt = _edge_schema_endpoint_label(schema_edge, "target")
                 if src_label and expected_src and src_label != expected_src:
                     errors.append(
-                        f"edge {idx} source_label '{src_label}' does not match edge label '{label}' source_label '{expected_src}'"
+                        f"edge {idx} source_label '{src_label}' does not match edge "
+                        f"label '{label}' source_label '{expected_src}'"
                     )
                 if tgt_label and expected_tgt and tgt_label != expected_tgt:
                     errors.append(
-                        f"edge {idx} target_label '{tgt_label}' does not match edge label '{label}' target_label '{expected_tgt}'"
+                        f"edge {idx} target_label '{tgt_label}' does not match edge "
+                        f"label '{label}' target_label '{expected_tgt}'"
                     )
             props = edge.get("properties")
             if isinstance(props, dict):
                 schema_prop_names = schema_eprops.get(label, set())
                 for prop_name, prop_value in props.items():
                     if prop_value is None or prop_value == "":
-                        warnings.append(
-                            f"edge {idx} property '{prop_name}' has empty value"
-                        )
-                    if (
-                        schema_available
-                        and label in schema_eprops
-                        and prop_name not in schema_prop_names
-                    ):
-                        errors.append(
-                            f"edge {idx} property '{prop_name}' does not exist on label '{label}'"
-                        )
+                        warnings.append(f"edge {idx} property '{prop_name}' has empty value")
+                    if schema_available and label in schema_eprops and prop_name not in schema_prop_names:
+                        errors.append(f"edge {idx} property '{prop_name}' does not exist on label '{label}'")
                     value_error = _property_value_error(
                         item_kind="edge",
                         item_index=idx,
@@ -769,10 +628,11 @@ def validate_graph_payload(
                     if value_error:
                         errors.append(value_error)
             if source is None and target is None:
-                continue
-            if source is None:
+                errors.append(f"edge {idx} missing source endpoint")
+                errors.append(f"edge {idx} missing target endpoint")
+            elif source is None:
                 errors.append(f"edge {idx} has target but missing source")
-            if target is None:
+            elif target is None:
                 errors.append(f"edge {idx} has source but missing target")
             for endpoint_name, endpoint_label, endpoint_value in (
                 ("source", src_label, source),
@@ -789,7 +649,8 @@ def validate_graph_payload(
                     # Report an error when an endpoint object lacks a primary key; do not fall back
                     # to string matching, which could silently write invalid dangling edges.
                     errors.append(
-                        f"edge {idx} {endpoint_name} endpoint missing primary key for label '{endpoint_label}': {missing_pk}"
+                        f"edge {idx} {endpoint_name} endpoint missing primary key "
+                        f"for label '{endpoint_label}': {missing_pk}"
                     )
                     continue
                 if not identities:
@@ -801,9 +662,7 @@ def validate_graph_payload(
                     )
                     continue
                 matched_vertex_indices = {
-                    vertex_identity_index[identity]
-                    for identity in identities
-                    if identity in vertex_identity_index
+                    vertex_identity_index[identity] for identity in identities if identity in vertex_identity_index
                 }
                 if len(matched_vertex_indices) > 1:
                     errors.append(
@@ -904,11 +763,6 @@ def ingest_graph_data_via_ai(
     dry_run=False + confirm=True + plan_hash 匹配: 执行写入
     nonce/expires_at: dry_run 返回的 plan_context 中的字段,confirm 时必须传回
     """
-    if not dry_run and confirm:
-        replay_error = replayed_plan_error(nonce)
-        if replay_error is not None:
-            return replay_error
-
     from hugegraph_mcp.write_limits import (
         graph_data_operation_count,
         write_limit_envelope,
@@ -982,7 +836,8 @@ def ingest_graph_data_via_ai(
             },
             "mutation_summary": mutation_summary,
             "warnings": warnings,
-            "confirmable": True,
+            "confirmable": False,
+            "preview_only": True,
         }
         next_actions: list[str] = []
         if MCPConfig.from_env().is_readonly():
@@ -992,15 +847,16 @@ def ingest_graph_data_via_ai(
                     "This dry-run was generated while HUGEGRAPH_MCP_READONLY=true. "
                     "Set HUGEGRAPH_MCP_READONLY=false and rerun dry_run before confirming."
                 ),
-                next_action=(
-                    "Set HUGEGRAPH_MCP_READONLY=false and rerun dry_run before confirm."
-                ),
+                next_action=("Set HUGEGRAPH_MCP_READONLY=false and rerun dry_run before confirm."),
             )
             warnings = list(warnings) + readonly_warnings
-        else:
-            issue_error = issue_plan(plan_context, plan_hash_value)
-            if issue_error is not None:
-                return issue_error
+        warnings = list(warnings) + [
+            (
+                "Legacy HugeGraph-AI graph import is preview-only because it has no "
+                "durable per-operation receipts or verified create-if-absent semantics."
+            )
+        ]
+        next_actions.append("Do not confirm this import until atomic graph-create capabilities are verified.")
         return envelope_ok(payload, warnings=warnings, next_actions=next_actions)
 
     violation = guard(Capability.DATA_WRITE)
@@ -1011,103 +867,44 @@ def ingest_graph_data_via_ai(
         return envelope_err(
             ErrorType.CONFIRM_REQUIRED,
             "Graph data import requires confirm=True after a dry_run.",
-            suggestion="Run dry_run=True, review mutation_summary and warnings, then pass confirm=True with the returned plan_hash.",
-        )
-
-    # Validate against the target by rereading config and schema and recomputing the hash.
-    # nonce must be returned from the plan_context produced by dry_run.
-    valid, error_type, details = verify_and_consume_plan(
-        submitted_hash=plan_hash,
-        tool_name="ingest_graph_data",
-        mode="import",
-        payload_digest=payload_digest,
-        schema_hash=schema_hash,
-        nonce=nonce or plan_context.nonce,
-        expires_at=expires_at,
-    )
-    if not valid:
-        return plan_hash_error(
-            error_type=error_type,
-            details=details,
-            mismatch_message=(
-                "Plan hash mismatch: config, schema, or payload has changed since dry_run."
-            ),
-            expired_message=(
-                "Plan has expired. Run dry_run=True again and use the returned plan_hash."
+            suggestion=(
+                "Run dry_run=True, review mutation_summary and warnings, then pass "
+                "confirm=True with the returned plan_hash."
             ),
-            suggestion="Run dry_run=True again and use the returned plan_hash.",
         )
-
-    batch_id = f"batch-{uuid4().hex[:12]}"
-    request_id = f"req-{uuid4().hex[:12]}"
-    cfg = MCPConfig.from_env()
-    planned = mutation_summary
-
-    import_data = _prepare_graph_import_data(graph_data, live_schema)
-    # Actual writes still use HugeGraph-AI's graph-import HTTP endpoint. MCP handles
-    # schema validation, safety confirmation, and payload normalization rather than
-    # invoking the hugegraph-llm import flow directly.
-    try:
-        ai_result = post(
-            "/graph-import",
-            json={"data": json.dumps(import_data, sort_keys=True), "schema": cfg.graph},
-        )
-    except Exception as exc:  # noqa: BLE001 - normalize import boundary failures
-        return envelope_err(
-            ErrorType.FLOW_EXECUTION_FAILED,
-            f"Import execution failed: {exc}",
-            details=_import_error_result(
-                planned=planned, batch_id=batch_id, request_id=request_id, cfg=cfg
-            ),
-        )
-
-    if not ai_result.get("ok"):
-        # M5: Normalize AI error responses instead of passing through raw results.
-        return envelope_err(
-            ErrorType.FLOW_EXECUTION_FAILED,
-            "HugeGraph-AI import returned an error.",
-            details=_normalize_import_result(
-                ai_result=None,
-                planned=planned,
-                batch_id=batch_id,
-                request_id=request_id,
-                cfg=cfg,
-            ),
-        )
-
-    import_result = _unwrap_ai_payload(ai_result.get("data"))
-    if isinstance(import_result, dict) and import_result.get("ok") is False:
-        return envelope_err(
-            ErrorType.FLOW_EXECUTION_FAILED,
-            "HugeGraph-AI import returned an error in payload.",
-            details=_normalize_import_result(
-                ai_result=None,
-                planned=planned,
-                batch_id=batch_id,
-                request_id=request_id,
-                cfg=cfg,
-            ),
-        )
-
-    # M5: Normalize the import result.
-    normalized = _normalize_import_result(
-        ai_result=import_result,
-        planned=planned,
-        batch_id=batch_id,
-        request_id=request_id,
-        cfg=cfg,
+    return envelope_err(
+        ErrorType.FEATURE_DISABLED,
+        "Legacy HugeGraph-AI graph import execution is disabled.",
+        suggestion=(
+            "Use dry_run for validation only. Enable graph import only after the "
+            "canonical workflow has verified atomic create adapters."
+        ),
+        retryable=False,
     )
 
-    if normalized.get("status") != "success":
-        return envelope_err(
-            ErrorType.FLOW_EXECUTION_FAILED,
-            "Graph data import did not complete successfully.",
-            retryable=bool(normalized.get("retryable")),
-            details=normalized,
-            warnings=normalized.get("warnings", []),
-        )
 
-    return envelope_ok(normalized, warnings=normalized.get("warnings", []))
+def _unknown_import_result(
+    *,
+    planned: dict[str, int],
+    batch_id: str,
+    request_id: str,
+    cfg: MCPConfig,
+    cause: Any,
+) -> dict[str, Any]:
+    return {
+        "status": PlanStatus.UNKNOWN.value,
+        "planned": planned,
+        "batch_id": batch_id,
+        "request_id": request_id,
+        "target": {
+            "graph_url": cfg.url,
+            "graph_name": cfg.graph,
+            "graphspace": cfg.graphspace or "DEFAULT",
+        },
+        "cause": cause,
+        "retryable": False,
+        "reconciliation_required": True,
+    }
 
 
 def _mutation_summary(graph_data: dict[str, Any]) -> dict[str, int]:
@@ -1134,7 +931,8 @@ def _normalize_import_result(
 ) -> dict[str, Any]:
     """将 AI 导入结果规范化为 V1 标准格式。
 
-    M5: success / partial / degraded / error
+    The remote service's legacy success/partial/degraded/error vocabulary is
+    interpreted as evidence, then exposed through the canonical plan state model.
     """
     target = {
         "graph_url": cfg.url,
@@ -1142,11 +940,10 @@ def _normalize_import_result(
         "graphspace": cfg.graphspace or "DEFAULT",
     }
 
-    if ai_result is None or (
-        isinstance(ai_result, dict) and ai_result.get("ok") is False
-    ):
+    if ai_result is None or (isinstance(ai_result, dict) and ai_result.get("ok") is False):
         return {
-            "status": "error",
+            "status": PlanStatus.UNKNOWN.value,
+            "import_classification": "error",
             "planned": planned,
             "written": {"vertices": 0, "edges": 0},
             "failed_items": [],
@@ -1172,25 +969,15 @@ def _normalize_import_result(
         if failed_items:
             written = {"vertices": 0, "edges": 0}
             status = "error" if remote_success is False else "degraded"
-            warnings.append(
-                "HugeGraph-AI import returned failures without explicit written counts."
-            )
+            warnings.append("HugeGraph-AI import returned failures without explicit written counts.")
         elif remote_success is False:
             written = {"vertices": 0, "edges": 0}
             status = "error"
-            warnings.append(
-                "HugeGraph-AI import reported failure without explicit written counts."
-            )
+            warnings.append("HugeGraph-AI import reported failure without explicit written counts.")
         else:
             written = {"vertices": 0, "edges": 0}
-            status = (
-                remote_status
-                if remote_status in {"partial", "error", "degraded"}
-                else "degraded"
-            )
-            warnings.append(
-                "HugeGraph-AI import did not return explicit written counts; write outcome is unknown."
-            )
+            status = remote_status if remote_status in {"partial", "error", "degraded"} else "degraded"
+            warnings.append("HugeGraph-AI import did not return explicit written counts; write outcome is unknown.")
     elif remote_success is False and (written["vertices"] > 0 or written["edges"] > 0):
         status = "partial"
     elif remote_success is False:
@@ -1208,11 +995,18 @@ def _normalize_import_result(
 
     if count_source == "total":
         warnings.append(
-            "HugeGraph-AI import returned only a total written count; vertices/edges were estimated from the planned import order."
+            "HugeGraph-AI import returned only a total written count; vertices/edges "
+            "were estimated from the planned import order."
         )
 
+    canonical_status = _canonical_import_status(
+        classification=status,
+        count_source=count_source,
+        written=written,
+    )
     return {
-        "status": status,
+        "status": canonical_status.value,
+        "import_classification": status,
         "planned": planned,
         "written": written,
         "failed_items": failed_items,
@@ -1220,7 +1014,7 @@ def _normalize_import_result(
         "retryable": False,
         "compensation_suggestions": (
             ["Review failed_items, inspect graph state, and run a fresh dry-run."]
-            if status in {"partial", "degraded", "error"}
+            if canonical_status is not PlanStatus.APPLIED
             else []
         ),
         "target": target,
@@ -1229,9 +1023,26 @@ def _normalize_import_result(
     }
 
 
-def _extract_written_counts(
-    ai_result: Any, planned: dict[str, int]
-) -> tuple[dict[str, int] | None, str | None]:
+def _canonical_import_status(
+    *,
+    classification: str,
+    count_source: str | None,
+    written: dict[str, int],
+) -> PlanStatus:
+    """Convert legacy import evidence into the shared write state model."""
+
+    if classification == "success":
+        operation_statuses = [ApplyStatus.APPLIED]
+    elif classification == "partial":
+        operation_statuses = [ApplyStatus.APPLIED, ApplyStatus.REJECTED]
+    elif classification == "error" and count_source is not None and not any(written.values()):
+        operation_statuses = [ApplyStatus.REJECTED]
+    else:
+        operation_statuses = [ApplyStatus.UNKNOWN]
+    return aggregate_plan_status(operation_statuses)
+
+
+def _extract_written_counts(ai_result: Any, planned: dict[str, int]) -> tuple[dict[str, int] | None, str | None]:
     """从 AI 结果中提取写入计数。
 
     返回 (counts, source),无法识别时 counts 为 None。source="total" 表示
@@ -1332,7 +1143,8 @@ def _import_error_result(
 ) -> dict[str, Any]:
     """构建导入执行错误的规范化结果。"""
     return {
-        "status": "error",
+        "status": PlanStatus.UNKNOWN.value,
+        "import_classification": "error",
         "planned": planned,
         "written": {"vertices": 0, "edges": 0},
         "failed_items": [],
diff --git a/hugegraph-mcp/hugegraph_mcp/tools/manage_graph_data.py b/hugegraph-mcp/hugegraph_mcp/tools/manage_graph_data.py
index 9f273e4a..5b9b97a0 100644
--- a/hugegraph-mcp/hugegraph_mcp/tools/manage_graph_data.py
+++ b/hugegraph-mcp/hugegraph_mcp/tools/manage_graph_data.py
@@ -25,10 +25,12 @@ from hugegraph_mcp.config import MCPConfig
 from hugegraph_mcp.confirmable_workflow import (
     confirm_required_error,
     issue_plan,
+    load_issued_plan,
     mark_readonly_preview,
     plan_hash_error,
+    record_write_outcome,
     replayed_plan_error,
-    verify_and_consume_plan,
+    verify_and_consume_issued_plan,
 )
 from hugegraph_mcp.envelope import ErrorType, envelope_err, envelope_ok
 from hugegraph_mcp.guard import Capability, guard
@@ -37,6 +39,7 @@ from hugegraph_mcp.plan_hash import (
     compute_payload_digest,
     compute_plan_hash,
 )
+from hugegraph_mcp.plan_store import plan_store_from_config
 from hugegraph_mcp.tools import ingest_graph_data
 from hugegraph_mcp.tools.graph_data_execute import (
     _fetch_live_schema,
@@ -56,11 +59,17 @@ from hugegraph_mcp.tools.graph_data_validate import (
     _validate_mode_operations,
     validate_graph_change_plan,
 )
+from hugegraph_mcp.tools.graph_write_plan import compile_graph_write_plan
 from hugegraph_mcp.write_limits import (
     graph_data_operation_count,
     operation_count_from_list,
     write_limit_envelope,
 )
+from hugegraph_mcp.write_plan import (
+    ApplyStatus,
+    PlanStatus,
+    aggregate_plan_status,
+)
 
 # Only export public entry points; import private helpers from graph_data_* modules.
 __all__ = [
@@ -114,11 +123,7 @@ def manage_graph_data(
                 ErrorType.VALIDATION_ERROR,
                 f"change_plan is required for mode='{mode}'",
             )
-        plan = (
-            change_plan
-            if isinstance(change_plan, dict)
-            else _change_plan_from_operations(change_plan)
-        )
+        plan = change_plan if isinstance(change_plan, dict) else _change_plan_from_operations(change_plan)
     else:
         return envelope_err(
             ErrorType.VALIDATION_ERROR,
@@ -139,10 +144,55 @@ def manage_graph_data(
     if limit_error is not None:
         return limit_error
 
+    isolated_vertex_delete_unavailable = mode == "delete" and any(
+        str(operation.get("op") or operation.get("type")) == "delete_vertex"
+        and operation.get("cascade", False) is False
+        for operation in _operations(plan)
+    )
+    graph_create_unavailable = mode == "import"
+    issued_plan = None
     if not dry_run and confirm:
+        violation = guard(Capability.DATA_WRITE)
+        if violation is not None:
+            return violation
+        if isolated_vertex_delete_unavailable:
+            return envelope_err(
+                ErrorType.FEATURE_DISABLED,
+                "Confirmed isolated vertex deletion is disabled for this backend.",
+                suggestion=(
+                    "Delete incident edges explicitly and wait for a backend-proven "
+                    "atomic isolated-delete capability before confirming the vertex delete."
+                ),
+                retryable=False,
+            )
+        if graph_create_unavailable:
+            return envelope_err(
+                ErrorType.FEATURE_DISABLED,
+                "Confirmed graph import is disabled for this backend.",
+                suggestion=(
+                    "Use the preview for validation only. Enable import only after "
+                    "atomic vertex and edge create-if-absent capabilities are verified."
+                ),
+                retryable=False,
+            )
         replay_error = replayed_plan_error(nonce)
         if replay_error is not None:
             return replay_error
+        issued_plan, issued_plan_error = load_issued_plan(
+            nonce=nonce,
+            plan_hash=plan_hash,
+            expires_at=expires_at,
+        )
+        if issued_plan_error is not None:
+            return issued_plan_error
+        if issued_plan is None:
+            return plan_hash_error(
+                error_type=ErrorType.PLAN_HASH_MISMATCH,
+                details={"reason": "The issued plan has no server-side payload."},
+                mismatch_message="The confirmation predates server-side plan storage.",
+                suggestion="Run dry_run=True again and use the new confirmation plan.",
+            )
+        plan = issued_plan
 
     # Read the live schema before writing. Do not execute without it because
     # primary keys, endpoints, and property validity depend on the real schema.
@@ -155,7 +205,7 @@ def manage_graph_data(
             retryable=True,
         )
 
-    if mode == "import" and graph_data is not None:
+    if mode == "import" and graph_data is not None and issued_plan is None:
         # import additionally validates the original graph_data for rules that
         # are difficult to express in change_plan: complete vertex keys,
         # resolvable edge endpoints, and duplicate identities in the payload.
@@ -185,30 +235,29 @@ def manage_graph_data(
             details={"errors": mode_validation["errors"]},
         )
 
-    dry_run_result = dry_run_graph_change_plan(
-        plan, live_schema, extra_hash_context=extra_hash_context
-    )
-    if not dry_run_result["valid"]:
-        errors = dry_run_result["errors"]
-        error_type = next(
-            (
-                error["error_type"]
-                for error in errors
-                if isinstance(error, dict) and error.get("error_type")
-            ),
-            ErrorType.INVALID_GRAPH_DATA,
-        )
-        return envelope_err(
-            error_type,
-            "Graph change plan is invalid.",
-            details={"errors": errors},
-            warnings=dry_run_result.get("warnings", []),
-        )
+    if issued_plan is None:
+        dry_run_result = dry_run_graph_change_plan(plan, live_schema, extra_hash_context=extra_hash_context)
+        if not dry_run_result["valid"]:
+            errors = dry_run_result["errors"]
+            error_type = next(
+                (error["error_type"] for error in errors if isinstance(error, dict) and error.get("error_type")),
+                ErrorType.INVALID_GRAPH_DATA,
+            )
+            return envelope_err(
+                error_type,
+                "Graph change plan is invalid.",
+                details={"errors": errors},
+                warnings=dry_run_result.get("warnings", []),
+            )
+        compiled_plan = dry_run_result.pop("compiled_plan", plan)
+    else:
+        compiled_plan = plan
+        dry_run_result = {"valid": True, "warnings": []}
 
     plan_context, _ = _build_manage_graph_data_plan_context(
         tool_name=plan_tool_name,
         mode=mode,
-        plan=plan,
+        plan=compiled_plan,
         live_schema=live_schema,
         nonce=nonce,
         extra_hash_context=extra_hash_context,
@@ -222,23 +271,77 @@ def manage_graph_data(
         warnings = list(dry_run_result.get("warnings", []))
         next_actions: list[str] = []
         if MCPConfig.from_env().is_readonly():
-            dry_run_result, readonly_warnings, readonly_next_actions = (
-                mark_readonly_preview(
-                    dry_run_result,
-                    warning=(
-                        "This dry-run was generated while HUGEGRAPH_MCP_READONLY=true. "
-                        "Its plan_hash is preview-only; set HUGEGRAPH_MCP_READONLY=false "
-                        "and rerun dry_run before confirming writes."
-                    ),
-                    next_action="Set HUGEGRAPH_MCP_READONLY=false and rerun dry_run before confirm.",
-                )
+            dry_run_result, readonly_warnings, readonly_next_actions = mark_readonly_preview(
+                dry_run_result,
+                warning=(
+                    "This dry-run was generated while HUGEGRAPH_MCP_READONLY=true. "
+                    "Its plan_hash is preview-only; set HUGEGRAPH_MCP_READONLY=false "
+                    "and rerun dry_run before confirming writes."
+                ),
+                next_action="Set HUGEGRAPH_MCP_READONLY=false and rerun dry_run before confirm.",
             )
             warnings.extend(readonly_warnings)
             next_actions.extend(readonly_next_actions)
-        else:
-            issue_error = issue_plan(plan_context, target_bound_hash)
+        if isolated_vertex_delete_unavailable or graph_create_unavailable:
+            dry_run_result["confirmable"] = False
+            dry_run_result["preview_only"] = True
+            if isolated_vertex_delete_unavailable:
+                warnings.append(
+                    "Isolated vertex deletion is preview-only because the configured "
+                    "backend has no verified atomic delete-if-no-incident-edges primitive."
+                )
+                next_actions.append("Delete incident edges explicitly; confirmed vertex deletion remains disabled.")
+            else:
+                try:
+                    workflow_preview = compile_graph_write_plan(
+                        compiled_plan,
+                        plan_context=plan_context,
+                        live_schema=live_schema,
+                        tool_name=plan_tool_name,
+                    )
+                except ValueError as exc:
+                    return envelope_err(
+                        ErrorType.FEATURE_DISABLED,
+                        "The import cannot be compiled to stable operation identities.",
+                        retryable=False,
+                        details={"reason": str(exc)},
+                    )
+                dry_run_result["workflow_preview"] = {
+                    "operations": [operation.to_dict() for operation in workflow_preview.operations]
+                }
+                warnings.append(
+                    "Graph import is preview-only because atomic vertex and edge "
+                    "create-if-absent capabilities are not verified for this backend."
+                )
+                next_actions.append(
+                    "Do not confirm this import until the required backend capabilities pass integration tests."
+                )
+        elif not MCPConfig.from_env().is_readonly():
+            issue_error = issue_plan(
+                plan_context,
+                target_bound_hash,
+                plan_payload=compiled_plan,
+            )
             if issue_error is not None:
                 return issue_error
+            try:
+                canonical_plan = compile_graph_write_plan(
+                    compiled_plan,
+                    plan_context=plan_context,
+                    live_schema=live_schema,
+                    tool_name=plan_tool_name,
+                )
+                plan_store_from_config().save_plan(canonical_plan)
+            except Exception as exc:  # noqa: BLE001 - fail closed at persistence boundary
+                return envelope_err(
+                    ErrorType.FEATURE_DISABLED,
+                    "The graph change cannot be persisted as an executable atomic plan.",
+                    retryable=False,
+                    details={"reason": str(exc)},
+                )
+            dry_run_result["plan_id"] = canonical_plan.plan_id
+            dry_run_result["status"] = canonical_plan.status.value
+            dry_run_result["expires_at"] = canonical_plan.expires_at
         return envelope_ok(
             dry_run_result,
             warnings=warnings,
@@ -255,16 +358,18 @@ def manage_graph_data(
     if not confirm:
         return confirm_required_error(
             message="Graph data changes require confirm=True after a dry_run.",
-            suggestion="Run dry_run=True, review preview and warnings, then pass confirm=True with the returned plan_hash.",
+            suggestion=(
+                "Run dry_run=True, review preview and warnings, then pass confirm=True with the returned plan_hash."
+            ),
         )
 
     schema_summary = _schema_summary(live_schema)
-    valid, error_type, details = verify_and_consume_plan(
+    valid, error_type, details, consumed_plan = verify_and_consume_issued_plan(
         submitted_hash=plan_hash,
         tool_name=plan_tool_name,
         mode=mode,
         payload_digest=_manage_graph_data_payload_digest(
-            plan,
+            compiled_plan,
             extra_hash_context=extra_hash_context,
         ),
         schema_hash=compute_payload_digest(schema_summary) if schema_summary else None,
@@ -280,9 +385,31 @@ def manage_graph_data(
             expired_message="Graph data change plan has expired.",
             suggestion="Run dry_run=True again and use the returned plan_hash.",
         )
+    if consumed_plan is None:
+        return envelope_err(
+            ErrorType.SERVER_ERROR,
+            "The consumed confirmation did not contain its server-side plan.",
+            retryable=False,
+        )
+    compiled_plan = consumed_plan
 
-    execute_result = execute_graph_change_plan(plan, live_schema=live_schema)
+    execute_result = execute_graph_change_plan(compiled_plan, live_schema=live_schema)
     if isinstance(execute_result, dict) and execute_result.get("ok") is False:
+        error_type_value = execute_result["error"]["type"]
+        if error_type_value == ErrorType.WRITE_OUTCOME_UNKNOWN.value:
+            outcome = PlanStatus.UNKNOWN
+        elif error_type_value in {
+            ErrorType.WRITE_CONFLICT.value,
+            "BLOCKED_BY_RELATIONSHIPS",
+        }:
+            outcome = PlanStatus.CONFLICT
+        else:
+            outcome = PlanStatus.REJECTED
+        record_write_outcome(
+            plan_hash=plan_hash,
+            status=outcome.value,
+            receipt=execute_result,
+        )
         return envelope_err(
             execute_result["error"]["type"],
             execute_result["error"]["message"],
@@ -291,23 +418,40 @@ def manage_graph_data(
             # have applied an unreported side effect. Require a fresh dry-run
             # before any retry instead of encouraging blind replay.
             retryable=False,
-            details=_normalize_execute_result(execute_result, plan),
+            details=_normalize_execute_result(execute_result, compiled_plan),
         )
 
-    normalized = _normalize_execute_result(execute_result, plan)
-    if normalized.get("success") is False or normalized.get("status") in {
-        "partial",
-        "error",
-        "degraded",
-    }:
+    normalized = _normalize_execute_result(execute_result, compiled_plan)
+    if normalized.get("success") is False:
+        outcome = PlanStatus(normalized["status"])
+        record_write_outcome(
+            plan_hash=plan_hash,
+            status=outcome.value,
+            receipt=normalized,
+        )
+        response_error = ErrorType.PARTIAL_APPLY if outcome is PlanStatus.PARTIAL else ErrorType.FLOW_EXECUTION_FAILED
         return envelope_err(
-            ErrorType.FLOW_EXECUTION_FAILED,
+            response_error,
             "Graph change execution did not complete successfully.",
             retryable=bool(normalized.get("retryable")),
             details=normalized,
             warnings=normalized.get("warnings", []),
         )
 
+    if not record_write_outcome(
+        plan_hash=plan_hash,
+        status=normalized["status"],
+        receipt=normalized,
+    ):
+        return envelope_err(
+            ErrorType.WRITE_OUTCOME_UNKNOWN,
+            "The write completed, but its durable receipt could not be recorded.",
+            retryable=False,
+            details={
+                "status": PlanStatus.UNKNOWN.value,
+                "reconciliation_required": True,
+            },
+        )
     return envelope_ok(normalized)
 
 
@@ -362,8 +506,9 @@ def _normalize_execute_result(execute_result: Any, plan: Any) -> dict[str, Any]:
     planned = _mutation_summary(operations)
 
     if isinstance(execute_result, dict) and execute_result.get("ok") is False:
+        failed_status = _failed_item_status(execute_result["error"])
         return {
-            "status": "error",
+            "status": aggregate_plan_status([failed_status]).value,
             "success": False,
             "planned": planned,
             "written": {},
@@ -377,7 +522,7 @@ def _normalize_execute_result(execute_result: Any, plan: Any) -> dict[str, Any]:
 
     if not isinstance(execute_result, dict):
         return {
-            "status": "degraded",
+            "status": PlanStatus.UNKNOWN.value,
             "success": False,
             "planned": planned,
             "written": {},
@@ -395,25 +540,25 @@ def _normalize_execute_result(execute_result: Any, plan: Any) -> dict[str, Any]:
     if not isinstance(failed_items, list):
         failed_items = []
 
-    if (
-        execute_result.get("success") is True
-        and len(results) == len(operations)
-        and not failed_items
-    ):
-        status = "success"
-    elif results:
-        status = "partial"
-    elif execute_result.get("success") is False or failed_items:
-        status = "error"
+    operation_statuses = [_result_status(result) for result in results]
+    operation_statuses.extend(_failed_item_status(item) for item in failed_items)
+    if operation_statuses:
+        status = aggregate_plan_status(operation_statuses)
+    elif execute_result.get("success") is True and not operations:
+        status = PlanStatus.APPLIED
     else:
-        status = "degraded"
+        status = PlanStatus.UNKNOWN
 
     written = _mutation_summary(
-        [operation for operation, _result in zip(operations, results, strict=False)]
+        [
+            operation
+            for operation, result in zip(operations, results, strict=False)
+            if _result_status(result) is ApplyStatus.APPLIED
+        ]
     )
     return {
-        "status": status,
-        "success": status == "success",
+        "status": status.value,
+        "success": status in {PlanStatus.APPLIED, PlanStatus.ALREADY_APPLIED},
         "planned": planned,
         "written": written,
         "failed_items": failed_items,
@@ -421,9 +566,36 @@ def _normalize_execute_result(execute_result: Any, plan: Any) -> dict[str, Any]:
         "retryable": False,
         "compensation_suggestions": (
             ["Inspect graph state before retrying remaining operations."]
-            if status in ("partial", "degraded")
+            if status in {PlanStatus.PARTIAL, PlanStatus.UNKNOWN}
             else []
         ),
         "results": results,
         "mutation_summary": execute_result.get("mutation_summary", planned),
     }
+
+
+def _result_status(result: dict[str, Any]) -> ApplyStatus:
+    try:
+        return ApplyStatus(result.get("status", ApplyStatus.APPLIED.value))
+    except ValueError:
+        return ApplyStatus.UNKNOWN
+
+
+def _failed_item_status(item: dict[str, Any]) -> ApplyStatus:
+    raw_status = item.get("status")
+    if raw_status is not None:
+        try:
+            return ApplyStatus(raw_status)
+        except ValueError:
+            return ApplyStatus.UNKNOWN
+
+    error = item.get("error") if isinstance(item.get("error"), dict) else item
+    error_type = error.get("type")
+    if error_type == ErrorType.WRITE_OUTCOME_UNKNOWN.value:
+        return ApplyStatus.UNKNOWN
+    if error_type in {
+        ErrorType.WRITE_CONFLICT.value,
+        "BLOCKED_BY_RELATIONSHIPS",
+    }:
+        return ApplyStatus.CONFLICT
+    return ApplyStatus.REJECTED
diff --git a/hugegraph-mcp/hugegraph_mcp/tools/manage_schema.py b/hugegraph-mcp/hugegraph_mcp/tools/manage_schema.py
index 7046a068..2f4252ad 100644
--- a/hugegraph-mcp/hugegraph_mcp/tools/manage_schema.py
+++ b/hugegraph-mcp/hugegraph_mcp/tools/manage_schema.py
@@ -30,6 +30,7 @@ from hugegraph_mcp.confirmable_workflow import (
     confirm_required_error,
     issue_plan,
     plan_hash_error,
+    record_write_outcome,
     replayed_plan_error,
     verify_and_consume_plan,
 )
@@ -42,6 +43,7 @@ from hugegraph_mcp.plan_hash import (
     compute_payload_digest,
     compute_plan_hash,
 )
+from hugegraph_mcp.plan_store import plan_store_from_config
 from hugegraph_mcp.tools.live_schema import current_live_schema
 from hugegraph_mcp.tools.schema_contract import (
     SUPPORTED_FIELDS,
@@ -54,11 +56,13 @@ from hugegraph_mcp.tools.schema_utils import (
     schema_payload,
     user_data_without_server_metadata,
 )
+from hugegraph_mcp.tools.schema_write_adapter import compile_schema_write_plan
 from hugegraph_mcp.write_limits import (
     collect_write_limit_errors,
     operation_count_from_list,
     write_limit_envelope,
 )
+from hugegraph_mcp.write_plan import ApplyReceipt, ApplyStatus, PlanStatus
 
 ALLOWED_OPERATION_TYPES = frozenset(
     {
@@ -90,9 +94,7 @@ IDENTIFIER_FIELDS = {
     "create_index_label": ("name", "base_label"),
 }
 
-UNSUPPORTED_EDGE_LABEL_FIELDS = frozenset(
-    {"parent_label", "parentLabel", "edgelabel_type", "edgeLabelType"}
-)
+UNSUPPORTED_EDGE_LABEL_FIELDS = frozenset({"parent_label", "parentLabel", "edgelabel_type", "edgeLabelType"})
 
 PROPERTY_KEY_DATA_TYPES = frozenset(
     {
@@ -111,12 +113,8 @@ PROPERTY_KEY_DATA_TYPES = frozenset(
     }
 )
 PROPERTY_KEY_CARDINALITIES = frozenset({"SINGLE", "SET", "LIST"})
-PROPERTY_KEY_AGGREGATE_TYPES = frozenset(
-    {"NONE", "OLD", "SUM", "MIN", "MAX", "SET", "LIST"}
-)
-VERTEX_LABEL_ID_STRATEGIES = frozenset(
-    {"PRIMARY_KEY", "CUSTOMIZE_STRING", "CUSTOMIZE_NUMBER", "AUTOMATIC"}
-)
+PROPERTY_KEY_AGGREGATE_TYPES = frozenset({"NONE", "OLD", "SUM", "MIN", "MAX", "SET", "LIST"})
+VERTEX_LABEL_ID_STRATEGIES = frozenset({"PRIMARY_KEY", "CUSTOMIZE_STRING", "CUSTOMIZE_NUMBER", "AUTOMATIC"})
 EDGE_LABEL_FREQUENCIES = frozenset({"SINGLE", "MULTIPLE"})
 
 PROPERTY_KEY_DATA_TYPE_METHODS = {
@@ -212,10 +210,7 @@ def _validate_supported_fields(
     unknown = [
         field
         for field in operation
-        if field not in supported
-        and not (
-            op_type == "create_edge_label" and field in UNSUPPORTED_EDGE_LABEL_FIELDS
-        )
+        if field not in supported and not (op_type == "create_edge_label" and field in UNSUPPORTED_EDGE_LABEL_FIELDS)
     ]
     if not unknown:
         return
@@ -275,9 +270,7 @@ def _validate_index_labels_field(
     if "index_labels" not in operation:
         return
     values = operation["index_labels"]
-    if not isinstance(values, list) or any(
-        not isinstance(value, str) or not value for value in values
-    ):
+    if not isinstance(values, list) or any(not isinstance(value, str) or not value for value in values):
         errors.append(
             _validation_error(
                 idx,
@@ -294,8 +287,7 @@ def _validate_index_labels_field(
             _validation_error(
                 idx,
                 operation,
-                "index_labels contains duplicate name(s): "
-                + ", ".join(duplicate_names),
+                "index_labels contains duplicate name(s): " + ", ".join(duplicate_names),
                 "Remove duplicate index label names from index_labels.",
             )
         )
@@ -364,10 +356,7 @@ def _collect_planned_creates(
                     idx,
                     operation,
                     f"duplicate {op_type} name {name} within the same batch",
-                    (
-                        f"Define each {create_type_to_label[op_type]} only once "
-                        "per schema operation batch."
-                    ),
+                    (f"Define each {create_type_to_label[op_type]} only once per schema operation batch."),
                 )
             )
             continue
@@ -579,14 +568,8 @@ def _validate_property_key_aggregate_cardinality(
         _validation_error(
             idx,
             operation,
-            (
-                f"aggregate_type {aggregate_type!r} is not allowed with "
-                f"cardinality {cardinality!r}"
-            ),
-            (
-                f"Use cardinality in {sorted(allowed_cardinalities)} for "
-                f"aggregate_type {aggregate_type!r}."
-            ),
+            (f"aggregate_type {aggregate_type!r} is not allowed with cardinality {cardinality!r}"),
+            (f"Use cardinality in {sorted(allowed_cardinalities)} for aggregate_type {aggregate_type!r}."),
         )
     )
 
@@ -682,8 +665,7 @@ def _validate_vertex_primary_keys(
             _validation_error(
                 idx,
                 operation,
-                "primary_keys must be included in properties: "
-                + ", ".join(missing_from_properties),
+                "primary_keys must be included in properties: " + ", ".join(missing_from_properties),
                 "Add each primary key to properties before using it as a primary key.",
             )
         )
@@ -694,8 +676,7 @@ def _validate_vertex_primary_keys(
             _validation_error(
                 idx,
                 operation,
-                "primary_keys references undefined property key(s): "
-                + ", ".join(missing_property_keys),
+                "primary_keys references undefined property key(s): " + ", ".join(missing_property_keys),
                 "Create these property keys first and rerun validation after they exist in the live schema.",
             )
         )
@@ -726,9 +707,7 @@ def _validation_warnings(operations: list[dict[str, Any]]) -> list[str]:
             and id_strategy != "PRIMARY_KEY"
             and not operation.get("primary_keys")
         ):
-            warnings.append(
-                f"operation {idx} (create_vertex_label) has no primary_keys definition"
-            )
+            warnings.append(f"operation {idx} (create_vertex_label) has no primary_keys definition")
     return warnings
 
 
@@ -843,16 +822,11 @@ def validate_schema_operations(
                         f"Add {field} to the {op_type} operation.",
                     )
                 )
-        if any(
-            field not in operation or operation[field] in (None, "")
-            for field in REQUIRED_FIELDS[op_type]
-        ):
+        if any(field not in operation or operation[field] in (None, "") for field in REQUIRED_FIELDS[op_type]):
             continue
 
         identifier_ok = all(
-            _validate_identifier_field(
-                idx=idx, operation=operation, field=field, errors=errors
-            )
+            _validate_identifier_field(idx=idx, operation=operation, field=field, errors=errors)
             for field in IDENTIFIER_FIELDS.get(op_type, ())
         )
         if not identifier_ok:
@@ -954,22 +928,14 @@ def validate_schema_operations(
                         "Use a new edge label name or remove this create_edge_label operation.",
                     )
                 )
-            unsupported_fields = sorted(
-                field for field in UNSUPPORTED_EDGE_LABEL_FIELDS if field in operation
-            )
+            unsupported_fields = sorted(field for field in UNSUPPORTED_EDGE_LABEL_FIELDS if field in operation)
             if unsupported_fields:
                 errors.append(
                     _validation_error(
                         idx,
                         operation,
-                        (
-                            "unsupported parent/sub edge label field(s): "
-                            f"{', '.join(unsupported_fields)}"
-                        ),
-                        (
-                            "Parent/sub edge labels are not supported by manage_schema; "
-                            "remove these fields."
-                        ),
+                        (f"unsupported parent/sub edge label field(s): {', '.join(unsupported_fields)}"),
+                        ("Parent/sub edge labels are not supported by manage_schema; remove these fields."),
                     )
                 )
             for field in ("source_label", "target_label"):
@@ -1062,15 +1028,9 @@ def validate_schema_operations(
                 errors=errors,
             )
 
-        if (
-            op_type == "create_property_key"
-            and name in planned_creates["property_keys"]
-        ):
+        if op_type == "create_property_key" and name in planned_creates["property_keys"]:
             available_property_keys.add(name)
-        elif (
-            op_type == "create_vertex_label"
-            and name in planned_creates["vertex_labels"]
-        ):
+        elif op_type == "create_vertex_label" and name in planned_creates["vertex_labels"]:
             available_vertex_labels.add(name)
         elif op_type == "create_edge_label" and name in planned_creates["edge_labels"]:
             available_edge_labels.add(name)
@@ -1112,9 +1072,7 @@ def _build_schema_plan_context(
     return context
 
 
-def calculate_plan_hash(
-    operations: list[dict[str, Any]], live_schema: dict[str, Any] | None = None
-) -> str:
+def calculate_plan_hash(operations: list[dict[str, Any]], live_schema: dict[str, Any] | None = None) -> str:
     """Compatibility wrapper backed by the unified target-bound PlanContext."""
 
     return compute_plan_hash(
@@ -1138,9 +1096,7 @@ def _plan_context_payload(plan_context: PlanContext) -> dict[str, Any]:
     }
 
 
-def _risk_warnings(
-    operations: list[dict[str, Any]], live_schema: dict[str, Any] | None = None
-) -> list[str]:
+def _risk_warnings(operations: list[dict[str, Any]], live_schema: dict[str, Any] | None = None) -> list[str]:
     warnings: list[str] = []
     live_schema = current_live_schema(live_schema)
     property_keys = _schema_items(live_schema, "propertykeys")
@@ -1152,9 +1108,7 @@ def _risk_warnings(
     created_vertex_labels = planned_creates["vertex_labels"]
     created_edge_labels = planned_creates["edge_labels"]
     indexed_labels = {
-        op.get("base_label")
-        for op in operations
-        if op.get("type") == "create_index_label" and op.get("base_label")
+        op.get("base_label") for op in operations if op.get("type") == "create_index_label" and op.get("base_label")
     }
 
     for operation in operations:
@@ -1218,10 +1172,7 @@ def _safe_fetch_live_schema() -> tuple[dict[str, Any] | None, dict[str, Any] | N
         return None, envelope_err(
             ErrorType.CONNECTION_FAILED,
             "Cannot read live schema from HugeGraph Server for schema validation.",
-            suggestion=(
-                "Ensure HugeGraph Server is running and credentials/graphspace are "
-                "correct, then retry."
-            ),
+            suggestion=("Ensure HugeGraph Server is running and credentials/graphspace are correct, then retry."),
             retryable=True,
             details={"stage": "schema_fetch", "error": str(exc)},
         )
@@ -1246,6 +1197,19 @@ def dry_run_schema_operations(
     validation = validate_schema_operations(operations, live_schema)
     if not validation["valid"]:
         return validation
+    if len(operations) != 1:
+        return {
+            "valid": False,
+            "errors": [
+                _validation_error(
+                    -1,
+                    operations,
+                    "a confirmable schema plan must contain exactly one create operation",
+                    "Create and confirm one schema object per plan.",
+                )
+            ],
+            "warnings": validation.get("warnings", []),
+        }
     apply_scope_errors = _validate_apply_scope(operations)
     if apply_scope_errors:
         return {
@@ -1266,8 +1230,7 @@ def dry_run_schema_operations(
         "plan_context": _plan_context_payload(plan_context),
         "confirmable": True,
         "mutation_summary": _mutation_summary(operations),
-        "warnings": validation.get("warnings", [])
-        + _risk_warnings(operations, live_schema),
+        "warnings": validation.get("warnings", []) + _risk_warnings(operations, live_schema),
     }
 
 
@@ -1284,8 +1247,11 @@ def _design_from_operations(operations: list[dict[str, Any]]) -> dict[str, Any]:
 
 
 def _schema_manager():
+    cfg = MCPConfig.from_env()
     return build_hugegraph_client(
-        MCPConfig.from_env(), client_cls=PyHugeClient
+        cfg,
+        client_cls=PyHugeClient,
+        request_timeout_seconds=cfg.write_timeout_seconds,
     ).schema()
 
 
@@ -1295,58 +1261,89 @@ def apply_schema_operations(
     live_schema: dict[str, Any],
     stop_on_first_error: bool = True,
 ) -> dict[str, Any]:
-    """Apply P0a create operations and verify each operation by post-read schema."""
+    """Apply exactly one schema create and report ambiguous outcomes explicitly."""
 
-    manager = _schema_manager()
-    applied_operations: list[dict[str, Any]] = []
-    operation_results: list[dict[str, Any]] = []
-
-    for idx, operation in enumerate(operations):
-        try:
-            _apply_one_operation(manager, operation)
-            observed_schema = current_live_schema()
-        except Exception as exc:  # noqa: BLE001 - preserve partial-apply result
-            return _partial_apply_result(
-                operations=operations,
-                applied_operations=applied_operations,
-                operation_results=operation_results,
-                failed_operation=operation,
-                failed_operation_index=idx,
-                error=str(exc),
-            )
+    if len(operations) != 1:
+        raise ValueError("A schema apply plan must contain exactly one operation")
 
-        if not _operation_observed(operation, observed_schema):
-            failed = _partial_apply_result(
-                operations=operations,
-                applied_operations=applied_operations,
-                operation_results=operation_results,
-                failed_operation=operation,
-                failed_operation_index=idx,
-                error="post-read schema did not contain the created object",
-            )
-            if stop_on_first_error:
-                return failed
-
-        applied_operations.append(operation)
-        operation_results.append(
-            {
-                "operation_index": idx,
-                "operation": operation,
-                "status": "applied",
-            }
-        )
-        live_schema = observed_schema
+    operation = operations[0]
+    existing_state, existing_object = _schema_object_state(operation, live_schema)
+    if existing_state == "identical":
+        return ApplyReceipt(
+            status=ApplyStatus.ALREADY_APPLIED,
+            operation=operation,
+            observed_state=existing_object,
+            reconciliation_required=False,
+        ).to_dict()
+    if existing_state == "conflict":
+        return ApplyReceipt(
+            status=ApplyStatus.CONFLICT,
+            operation=operation,
+            observed_state=existing_object,
+            reason="a different schema object already exists with the confirmed name",
+            reconciliation_required=False,
+        ).to_dict()
 
-    return {
-        "status": "applied",
-        "valid": True,
-        "applied_operations": applied_operations,
-        "operation_results": operation_results,
-        "mutation_summary": _mutation_summary(applied_operations),
-        "schema_summary": normalized_schema_summary(
-            live_schema, include_user_data=True
-        ),
-    }
+    failure_stage = "schema create"
+    try:
+        # Client/manager construction belongs to the write attempt: once a
+        # confirmation has been consumed, any failure through verification
+        # must yield a durable, reconcilable outcome instead of escaping and
+        # leaving the plan indefinitely EXECUTING.
+        manager = _schema_manager()
+        _apply_one_operation(manager, operation)
+        failure_stage = "post-read schema"
+        observed_schema = current_live_schema()
+    except Exception as exc:  # noqa: BLE001 - request may already have committed
+        return ApplyReceipt(
+            status=ApplyStatus.UNKNOWN,
+            operation=operation,
+            reason=f"{failure_stage} failed: {exc}",
+            reconciliation_required=True,
+        ).to_dict()
+
+    observed_state, observed_object = _schema_object_state(operation, observed_schema)
+    if observed_state == "conflict":
+        return ApplyReceipt(
+            status=ApplyStatus.CONFLICT,
+            operation=operation,
+            observed_state=observed_object,
+            reason="post-read found a different schema object with the confirmed name",
+            reconciliation_required=False,
+        ).to_dict()
+    if observed_state != "identical":
+        return ApplyReceipt(
+            status=ApplyStatus.UNKNOWN,
+            operation=operation,
+            observed_state=normalized_schema_summary(observed_schema, include_user_data=True),
+            reason="post-read schema did not match the confirmed object",
+            reconciliation_required=True,
+        ).to_dict()
+
+    receipt = ApplyReceipt(
+        status=ApplyStatus.APPLIED,
+        operation=operation,
+        observed_state=observed_object,
+        reconciliation_required=False,
+    ).to_dict()
+    # Preserve the existing public success fields for one compatibility cycle;
+    # their status is sourced exclusively from the canonical receipt.
+    receipt.update(
+        {
+            "valid": True,
+            "applied_operations": operations,
+            "operation_results": [
+                {
+                    "operation_index": 0,
+                    "operation": operation,
+                    "status": receipt["status"],
+                }
+            ],
+            "mutation_summary": _mutation_summary(operations),
+            "schema_summary": normalized_schema_summary(observed_schema, include_user_data=True),
+        }
+    )
+    return receipt
 
 
 def _apply_one_operation(manager, operation: dict[str, Any]) -> None:
@@ -1354,9 +1351,7 @@ def _apply_one_operation(manager, operation: dict[str, Any]) -> None:
     field_specs = field_specs_for_operation(op_type)
     apply_fields = apply_fields_for_operation(op_type)
     if not field_specs or any(field not in apply_fields for field in operation):
-        raise ValueError(
-            f"Unsupported or non-forwardable schema field(s) for {op_type}"
-        )
+        raise ValueError(f"Unsupported or non-forwardable schema field(s) for {op_type}")
     if op_type == "create_property_key":
         builder = manager.propertyKey(operation["name"])
         _apply_property_key_options(builder, operation)
@@ -1401,9 +1396,7 @@ def _apply_property_key_options(builder, operation: dict[str, Any]) -> None:
         else:
             method_name = PROPERTY_KEY_AGGREGATE_METHODS.get(normalized)
             if method_name is None:
-                raise ValueError(
-                    f"Unsupported property key aggregate_type: {aggregate_type}"
-                )
+                raise ValueError(f"Unsupported property key aggregate_type: {aggregate_type}")
             getattr(builder, method_name)()
 
     _forward_parameter_fields(
@@ -1426,9 +1419,7 @@ def _set_builder_parameter(
         raise ValueError(f"Unsupported {op_type} field: {field}")
     setter = getattr(builder, "add_parameter", None)
     if not callable(setter):
-        raise TypeError(
-            f"Schema builder cannot forward supported {op_type} field: {field}"
-        )
+        raise TypeError(f"Schema builder cannot forward supported {op_type} field: {field}")
     setter(field, value)
 
 
@@ -1442,11 +1433,7 @@ def _forward_parameter_fields(
     """Forward every table-declared parameter field present in an operation."""
 
     for field, spec in field_specs_for_operation(op_type).items():
-        if (
-            field in operation
-            and field not in exclude
-            and spec.apply_mode == "parameter"
-        ):
+        if field in operation and field not in exclude and spec.apply_mode == "parameter":
             _set_builder_parameter(builder, op_type, field, operation[field])
 
 
@@ -1466,10 +1453,7 @@ def _apply_vertex_label_options(builder, operation: dict[str, Any]) -> None:
     if "enable_label_index" in operation:
         method = getattr(builder, "enableLabelIndex", None)
         if not callable(method):
-            raise ValueError(
-                "Schema builder cannot forward supported create_vertex_label field: "
-                "enable_label_index"
-            )
+            raise ValueError("Schema builder cannot forward supported create_vertex_label field: enable_label_index")
         method(operation["enable_label_index"])
     _forward_parameter_fields(builder, "create_vertex_label", operation)
 
@@ -1493,10 +1477,7 @@ def _apply_edge_label_options(builder, operation: dict[str, Any]) -> None:
     if "enable_label_index" in operation:
         method = getattr(builder, "enableLabelIndex", None)
         if not callable(method):
-            raise ValueError(
-                "Schema builder cannot forward supported create_edge_label field: "
-                "enable_label_index"
-            )
+            raise ValueError("Schema builder cannot forward supported create_edge_label field: enable_label_index")
         method(operation["enable_label_index"])
     _forward_parameter_fields(builder, "create_edge_label", operation)
 
@@ -1505,14 +1486,22 @@ def _operation_observed(
     operation: dict[str, Any],
     live_schema: dict[str, Any] | None,
 ) -> bool:
-    schema = (
-        (live_schema or {}).get("schema") if isinstance(live_schema, dict) else None
-    )
+    state, _ = _schema_object_state(operation, live_schema)
+    return state == "identical"
+
+
+def _schema_object_state(
+    operation: dict[str, Any],
+    live_schema: dict[str, Any] | None,
+) -> tuple[str, dict[str, Any] | None]:
+    """Classify a schema object's name-bound state for create/reconcile."""
+
+    schema = (live_schema or {}).get("schema") if isinstance(live_schema, dict) else None
     if not isinstance(schema, dict):
         schema = live_schema if isinstance(live_schema, dict) else {}
     collection = schema_collection_for_operation(_operation_type(operation))
     if collection is None:
-        return False
+        return "missing", None
 
     name_spec = field_specs_for_operation(_operation_type(operation)).get("name")
     observed = _find_schema_item(
@@ -1522,8 +1511,10 @@ def _operation_observed(
         name_aliases=name_spec.aliases if name_spec else ("name",),
     )
     if observed is None:
-        return False
-    return _operation_fields_match(operation, observed)
+        return "missing", None
+    if _operation_fields_match(operation, observed):
+        return "identical", observed
+    return "conflict", observed
 
 
 def _find_schema_item(
@@ -1615,9 +1606,7 @@ def _enum_field_matches(
     )
     if observed_value is None:
         return False
-    return _canonical_enum_value(field, observed_value) == _canonical_enum_value(
-        field, str(expected).upper()
-    )
+    return _canonical_enum_value(field, observed_value) == _canonical_enum_value(field, str(expected).upper())
 
 
 def _string_field_matches(
@@ -1628,10 +1617,7 @@ def _string_field_matches(
 ) -> bool:
     if field not in operation:
         return True
-    return (
-        _field_value(observed, *(aliases or (field, _camel_case_schema_field(field))))
-        == operation[field]
-    )
+    return _field_value(observed, *(aliases or (field, _camel_case_schema_field(field)))) == operation[field]
 
 
 def _mapping_field_matches(
@@ -1643,9 +1629,7 @@ def _mapping_field_matches(
     if field not in operation:
         return True
     expected = operation[field]
-    actual = _field_value(
-        observed, *(aliases or (field, _camel_case_schema_field(field), "userdata"))
-    )
+    actual = _field_value(observed, *(aliases or (field, _camel_case_schema_field(field), "userdata")))
     # HugeGraph may omit an empty optional user_data object in its response.
     if actual is None and expected == {}:
         return True
@@ -1665,10 +1649,7 @@ def _scalar_field_matches(
 ) -> bool:
     if field not in operation:
         return True
-    return (
-        _field_value(observed, *(aliases or (field, _camel_case_schema_field(field))))
-        == operation[field]
-    )
+    return _field_value(observed, *(aliases or (field, _camel_case_schema_field(field)))) == operation[field]
 
 
 def _camel_case_schema_field(field: str) -> str:
@@ -1690,9 +1671,7 @@ def _supported_field_matches(
     if spec.kind == "enum":
         default = None
         default = spec.default
-        return _enum_field_matches(
-            observed, operation, field, default=default, aliases=aliases
-        )
+        return _enum_field_matches(observed, operation, field, default=default, aliases=aliases)
     if spec.kind == "list":
         return _list_field_matches(observed, operation, field, aliases)
     if spec.kind == "mapping":
@@ -1705,9 +1684,7 @@ def _supported_field_matches(
     return field in {"type", "name"}
 
 
-def _operation_fields_match(
-    operation: dict[str, Any], observed: dict[str, Any]
-) -> bool:
+def _operation_fields_match(operation: dict[str, Any], observed: dict[str, Any]) -> bool:
     op_type = _operation_type(operation)
     if op_type not in SUPPORTED_FIELDS:
         return False
@@ -1719,56 +1696,15 @@ def _operation_fields_match(
         return False
 
     fields = {
-        field
-        for field in field_specs_for_operation(op_type)
-        if field in operation and field not in {"type", "name"}
+        field for field in field_specs_for_operation(op_type) if field in operation and field not in {"type", "name"}
     }
     # These options have always had an apply-time default. Keep checking the
     # effective value even when the caller omitted the optional field.
     for field, spec in field_specs_for_operation(op_type).items():
-        if (
-            field not in {"type", "name"}
-            and field not in operation
-            and spec.default is not None
-        ):
+        if field not in {"type", "name"} and field not in operation and spec.default is not None:
             fields.add(field)
 
-    return all(
-        _supported_field_matches(op_type, field, operation, observed)
-        for field in fields
-    )
-
-
-def _partial_apply_result(
-    *,
-    operations: list[dict[str, Any]],
-    applied_operations: list[dict[str, Any]],
-    operation_results: list[dict[str, Any]],
-    failed_operation: dict[str, Any],
-    failed_operation_index: int,
-    error: str,
-) -> dict[str, Any]:
-    remaining_operations = operations[failed_operation_index:]
-    return {
-        "status": "partial" if applied_operations else "failed",
-        "valid": False,
-        "applied_operations": applied_operations,
-        "operation_results": operation_results,
-        "failed_operation": failed_operation,
-        "failed_operation_index": failed_operation_index,
-        "error": error,
-        "remaining_operations": remaining_operations,
-        "recovery_suggestions": _recovery_suggestions(),
-        "mutation_summary": _mutation_summary(applied_operations),
-    }
-
-
-def _recovery_suggestions() -> list[str]:
-    return [
-        "Call inspect_schema_tool to observe the current schema state.",
-        "Remove already-applied operations and dry-run the remaining operations again.",
-        "Do not retry the original full batch without checking which operations were applied.",
-    ]
+    return all(_supported_field_matches(op_type, field, operation, observed) for field in fields)
 
 
 def manage_schema(
@@ -1831,6 +1767,23 @@ def manage_schema(
             issue_error = issue_plan(plan_context, result["plan_hash"])
             if issue_error is not None:
                 return issue_error
+            try:
+                canonical_plan = compile_schema_write_plan(
+                    operations[0],
+                    plan_context=plan_context,
+                    live_schema=live_schema,
+                )
+                plan_store_from_config().save_plan(canonical_plan)
+            except Exception as exc:  # noqa: BLE001 - fail closed at persistence boundary
+                return envelope_err(
+                    ErrorType.SERVER_ERROR,
+                    "The schema create plan could not be persisted safely.",
+                    retryable=False,
+                    details={"reason": type(exc).__name__},
+                )
+            result["plan_id"] = canonical_plan.plan_id
+            result["status"] = canonical_plan.status.value
+            result["expires_at"] = canonical_plan.expires_at
         if result.get("valid") and MCPConfig.from_env().is_readonly():
             result["confirmable"] = False
             result["readonly_preview_only"] = True
@@ -1843,15 +1796,22 @@ def manage_schema(
                         "Set HUGEGRAPH_MCP_READONLY=false and rerun dry_run before confirming writes."
                     )
                 ],
-                next_actions=[
-                    "Set HUGEGRAPH_MCP_READONLY=false and rerun dry_run before confirm."
-                ],
+                next_actions=["Set HUGEGRAPH_MCP_READONLY=false and rerun dry_run before confirm."],
             )
         return envelope_ok(
             result,
-            next_actions=[
-                "Review schema preview, then call apply_schema_tool(mode='apply', confirm=true, plan_hash, nonce, expires_at)."
-            ]
+            warnings=list(result.get("warnings", []))
+            + (
+                [
+                    (
+                        "LEGACY_CONFIRMATION_DEPRECATED: use plan_id with confirm_write_tool; "
+                        "plan_hash, nonce, and expires_at will be removed after one release."
+                    )
+                ]
+                if result.get("valid") and result.get("plan_id")
+                else []
+            ),
+            next_actions=["Review schema preview, then call confirm_write_tool with plan_id."]
             if result.get("valid")
             else ["Fix validation errors and rerun apply_schema_tool(mode='dry_run')."],
         )
@@ -1871,9 +1831,7 @@ def manage_schema(
                 "Schema operations are not valid for P0a apply.",
                 details={"errors": dry_run_result.get("errors", [])},
                 warnings=dry_run_result.get("warnings", []),
-                next_actions=[
-                    "Fix validation errors and rerun apply_schema_tool(mode='dry_run')."
-                ],
+                next_actions=["Fix validation errors and rerun apply_schema_tool(mode='dry_run')."],
             )
         violation = guard(Capability.SCHEMA_WRITE)
         if violation is not None:
@@ -1882,8 +1840,7 @@ def manage_schema(
             return confirm_required_error(
                 message="Schema apply requires confirm=True after dry_run.",
                 suggestion=(
-                    "Run mode='dry_run', review the plan, then pass confirm=True "
-                    "with plan_hash, nonce, and expires_at."
+                    "Run mode='dry_run', review the plan, then pass confirm=True with plan_hash, nonce, and expires_at."
                 ),
             )
         valid, error_type, details = verify_and_consume_plan(
@@ -1904,15 +1861,76 @@ def manage_schema(
             )
 
         apply_result = apply_schema_operations(operations, live_schema=live_schema)
-        if apply_result.get("status") in {"failed", "partial"}:
+        try:
+            result_status = ApplyStatus(apply_result["status"])
+        except (KeyError, TypeError, ValueError) as exc:
+            apply_result = ApplyReceipt(
+                status=ApplyStatus.UNKNOWN,
+                operation=operations[0],
+                reason=f"schema executor returned a non-canonical receipt: {exc}",
+                reconciliation_required=True,
+            ).to_dict()
+            result_status = ApplyStatus.UNKNOWN
+        if result_status is ApplyStatus.UNKNOWN:
+            record_write_outcome(
+                plan_hash=plan_hash,
+                status=PlanStatus.UNKNOWN,
+                receipt=apply_result,
+            )
+            return envelope_err(
+                ErrorType.WRITE_OUTCOME_UNKNOWN,
+                "The schema create may have committed; reconcile before retrying.",
+                suggestion="Inspect the schema and compare it with the confirmed operation before issuing another write.",
+                retryable=False,
+                details=apply_result,
+                next_actions=["Call inspect_schema_tool and reconcile the confirmed schema object."],
+            )
+        if result_status is ApplyStatus.CONFLICT:
+            if not record_write_outcome(
+                plan_hash=plan_hash,
+                status=PlanStatus.CONFLICT,
+                receipt=apply_result,
+            ):
+                return envelope_err(
+                    ErrorType.WRITE_OUTCOME_UNKNOWN,
+                    "The schema conflict was observed, but its durable receipt could not be recorded.",
+                    retryable=False,
+                    details={
+                        "status": ApplyStatus.UNKNOWN.value,
+                        "reconciliation_required": True,
+                    },
+                )
             return envelope_err(
-                ErrorType.PARTIAL_APPLY,
-                "Schema apply did not complete all operations.",
+                ErrorType.WRITE_CONFLICT,
+                "A different schema object exists with the confirmed name.",
+                retryable=False,
                 details=apply_result,
-                next_actions=apply_result.get("recovery_suggestions", []),
+            )
+        persisted_status = (
+            PlanStatus.ALREADY_APPLIED if result_status is ApplyStatus.ALREADY_APPLIED else PlanStatus.APPLIED
+        )
+        if not record_write_outcome(
+            plan_hash=plan_hash,
+            status=persisted_status,
+            receipt=apply_result,
+        ):
+            return envelope_err(
+                ErrorType.WRITE_OUTCOME_UNKNOWN,
+                "The schema create completed, but its durable receipt could not be recorded.",
+                retryable=False,
+                details={
+                    "status": ApplyStatus.UNKNOWN.value,
+                    "reconciliation_required": True,
+                },
             )
         return envelope_ok(
             apply_result,
+            warnings=[
+                (
+                    "LEGACY_CONFIRMATION_DEPRECATED: use plan_id with confirm_write_tool; "
+                    "plan_hash, nonce, and expires_at will be removed after one release."
+                )
+            ],
             next_actions=["Call inspect_schema_tool to verify the applied schema."],
         )
 
diff --git a/hugegraph-mcp/hugegraph_mcp/tools/mutate_graph_properties.py b/hugegraph-mcp/hugegraph_mcp/tools/mutate_graph_properties.py
index f3ba6615..44ce88d1 100644
--- a/hugegraph-mcp/hugegraph_mcp/tools/mutate_graph_properties.py
+++ b/hugegraph-mcp/hugegraph_mcp/tools/mutate_graph_properties.py
@@ -18,20 +18,20 @@ from uuid import uuid4
 
 from pyhugegraph.client import PyHugeClient
 
+from hugegraph_mcp.backend_capabilities import (
+    BackendFeature,
+    CapabilityEvidence,
+    profile_for,
+)
 from hugegraph_mcp.config import MCPConfig
 from hugegraph_mcp.confirmable_workflow import (
     confirm_required_error,
-    issue_plan,
     mark_readonly_preview,
-    plan_hash_error,
-    replayed_plan_error,
-    verify_and_consume_plan,
 )
 from hugegraph_mcp.envelope import (
     ErrorType,
     envelope_err,
     envelope_ok,
-    sanitize_for_response,
 )
 from hugegraph_mcp.error_mapping import classify_hugegraph_exception
 from hugegraph_mcp.guard import Capability, guard
@@ -42,12 +42,14 @@ from hugegraph_mcp.plan_hash import (
     compute_plan_hash,
 )
 from hugegraph_mcp.tools.live_schema import current_live_schema
+from hugegraph_mcp.tools.property_validation import property_specs, property_value_error
 from hugegraph_mcp.tools.schema_utils import (
     normalized_schema_summary,
     property_cardinalities,
     property_names,
     schema_payload,
 )
+from hugegraph_mcp.write_plan import ApplyStatus, PlanStatus, aggregate_plan_status
 
 TARGETS = frozenset({"vertex", "edge"})
 OPERATIONS = frozenset({"append", "eliminate"})
@@ -80,8 +82,9 @@ def mutate_graph_properties(
 
     The dry-run reads live schema and the target object, validates property keys
     against the target label, previews before/after state, and binds a target
-    snapshot digest into plan_hash. Confirm re-reads schema and target; if either
-    changed, it returns TARGET_CHANGED or PLAN_HASH_MISMATCH before writing.
+    snapshot digest into plan_hash. Applying a confirmed mutation is disabled
+    until HugeGraph exposes a backend-enforced conditional property update. The
+    current append/eliminate REST endpoints cannot provide atomic compare-and-set.
     """
 
     validation_error = _validate_inputs(
@@ -93,11 +96,6 @@ def mutate_graph_properties(
     if validation_error is not None:
         return validation_error
 
-    if not dry_run and confirm:
-        replay_error = replayed_plan_error(nonce, source="mutate_graph_properties_tool")
-        if replay_error is not None:
-            return replay_error
-
     live_schema, target_item, read_error = _read_schema_and_target(target=target, id=id)
     if read_error is not None:
         return read_error
@@ -144,15 +142,20 @@ def mutate_graph_properties(
         "after": preview["after"],
         "plan_hash": plan_hash_value,
         "plan_context": _plan_context_payload(plan_context),
-        "status": "planned",
-        "confirmable": True,
+        "status": PlanStatus.ISSUED.value,
+        "confirmable": False,
+        "cas_request": {
+            "target_type": target,
+            "target_id": id,
+            "expected_properties": preview["before"].get("properties", {}),
+            "desired_properties": preview["after"].get("properties", {}),
+            "operation_id": f"property-cas:{plan_hash_value[:24]}",
+        },
     }
 
     if dry_run:
-        warnings: list[str] = []
-        next_actions = [
-            "Review before/after, then call mutate_graph_properties_tool with dry_run=false, confirm=true, plan_hash, nonce, and expires_at.",
-        ]
+        warnings = ["Preview only: HugeGraph does not expose an atomic conditional property update."]
+        next_actions = ["Enable confirmation only after the backend provides compare-and-set for property replacement."]
         if MCPConfig.from_env().is_readonly():
             payload, warnings, next_actions = mark_readonly_preview(
                 payload,
@@ -162,10 +165,6 @@ def mutate_graph_properties(
                 ),
                 next_action="Set HUGEGRAPH_MCP_READONLY=false and rerun dry_run before confirm.",
             )
-        else:
-            issue_error = issue_plan(plan_context, plan_hash_value)
-            if issue_error is not None:
-                return issue_error
         return envelope_ok(payload, warnings=warnings, next_actions=next_actions)
 
     violation = guard(Capability.DATA_WRITE)
@@ -176,62 +175,38 @@ def mutate_graph_properties(
         return confirm_required_error(
             message="Property mutations require confirm=True after dry_run.",
             suggestion=(
-                "Run dry_run=True, review the preview, then pass confirm=True "
-                "with plan_hash, nonce, and expires_at."
+                "Run dry_run=True, review the preview, then pass confirm=True with plan_hash, nonce, and expires_at."
             ),
             source="mutate_graph_properties_tool",
         )
 
-    expected_snapshot = _snapshot_from_nonce(nonce)
-    if (
-        expected_snapshot is not None
-        and expected_snapshot != preview["target_snapshot_digest"][:16]
-    ):
-        return envelope_err(
-            ErrorType.TARGET_CHANGED,
-            "Target changed since dry_run; property mutation was not applied.",
-            suggestion="Run dry_run=True again and review the new before/after preview.",
-            source="mutate_graph_properties_tool",
-            details={
-                "expected_target_snapshot_digest_prefix": expected_snapshot,
-                "current_target_snapshot_digest": preview["target_snapshot_digest"],
-            },
-            next_actions=["Call query_graph_data_tool to inspect the current target."],
-        )
-
-    valid, error_type, details = verify_and_consume_plan(
-        submitted_hash=plan_hash,
-        tool_name="mutate_graph_properties_tool",
-        mode="mutate",
-        payload_digest=_payload_digest(target, operation, id, properties),
-        schema_hash=_schema_hash(live_schema),
-        nonce=nonce,
-        expires_at=expires_at,
-        extra_context={
+    evidence = _property_cas_evidence()
+    return envelope_err(
+        ErrorType.FEATURE_DISABLED,
+        "Confirmed property mutation is disabled because HugeGraph does not expose atomic compare-and-set updates.",
+        suggestion=(
+            "Add a backend-enforced conditional property replacement API that compares "
+            "the expected state and writes the desired state in one atomic operation."
+        ),
+        source="mutate_graph_properties_tool",
+        details={
             "target": target,
-            "operation": operation,
-            "target_snapshot_digest": preview["target_snapshot_digest"],
+            "id": id,
+            "status": aggregate_plan_status([ApplyStatus.REJECTED]).value,
+            "required_capability": "conditional_property_compare_and_set",
+            "backend_capability": BackendFeature.PROPERTY_COMPARE_AND_SET.value,
+            "capability_status": evidence.status.value,
+            "capability_evidence": evidence.reason,
+            "write_attempted": False,
         },
+        next_actions=["Keep this plan as a preview; do not retry confirmation until backend CAS is available."],
     )
-    if not valid:
-        return plan_hash_error(
-            error_type=error_type,
-            details=details,
-            mismatch_message="Provided plan_hash does not match the current property mutation plan.",
-            suggestion="Run dry_run=True again and use the returned plan_hash.",
-            source="mutate_graph_properties_tool",
-        )
 
-    return _execute_and_verify(
-        target=target,
-        operation=operation,
-        id=id,
-        properties=properties,
-        before=target_item,
-        planned_after=preview["after"],
-        cardinalities=cardinalities,
-        payload=payload,
-    )
+
+def _property_cas_evidence() -> CapabilityEvidence:
+    """Return the locked capability evidence for the supported deployment."""
+
+    return profile_for("1.7.0", "rocksdb").evidence_for(BackendFeature.PROPERTY_COMPARE_AND_SET)
 
 
 def _validate_inputs(
@@ -282,7 +257,7 @@ def _read_schema_and_target(
 ) -> tuple[dict[str, Any] | None, dict[str, Any] | None, dict[str, Any] | None]:
     try:
         live_schema = current_live_schema()
-    except Exception as exc:  # noqa: BLE001 - return structured schema error
+    except Exception as exc:  # noqa: BLE001 - normalize external schema failure
         return (
             None,
             None,
@@ -298,10 +273,8 @@ def _read_schema_and_target(
 
     try:
         manager = _graph_manager()
-        raw_item = (
-            manager.getVertexById(id) if target == "vertex" else manager.getEdgeById(id)
-        )
-    except Exception as exc:  # noqa: BLE001 - classify client failure
+        raw_item = manager.getVertexById(id) if target == "vertex" else manager.getEdgeById(id)
+    except Exception as exc:  # noqa: BLE001 - normalize external graph failure
         classification = classify_hugegraph_exception(exc)
         return (
             live_schema,
@@ -383,14 +356,32 @@ def _validate_properties_against_schema(
             details={"label": label, "unknown_properties": unknown},
         )
     if operation == "append":
+        specs = property_specs(live_schema or {})
         for name, value in properties.items():
-            cardinality = cardinalities.get(name, "SINGLE")
-            if cardinality in {"LIST", "SET"} and not isinstance(value, list):
+            spec = specs.get(name)
+            value_error = property_value_error(
+                item_kind=target,
+                item_index=0,
+                property_name=name,
+                value=value,
+                spec=spec,
+            )
+            if value_error is not None:
+                data_type, cardinality = spec or (
+                    None,
+                    cardinalities.get(name, "SINGLE"),
+                )
+                suggestion = (
+                    f"Pass a JSON array whose elements match {data_type} for {cardinality} property {name!r}."
+                    if cardinality in {"LIST", "SET"}
+                    else f"Pass a value matching HugeGraph data type {data_type}."
+                )
                 return _validation_error(
-                    f"Property {name!r} requires a collection value for append.",
-                    f"Pass a JSON array for {cardinality} property {name!r}.",
+                    value_error,
+                    suggestion,
                     {
                         "property": name,
+                        "data_type": data_type,
                         "cardinality": cardinality,
                         "value_type": type(value).__name__,
                     },
@@ -442,9 +433,7 @@ def _apply_property_preview(
             if cardinality == "LIST":
                 after[name] = [*_existing_collection(after.get(name)), *value]
             elif cardinality == "SET":
-                after[name] = _stable_unique(
-                    [*_existing_collection(after.get(name)), *value]
-                )
+                after[name] = _stable_unique([*_existing_collection(after.get(name)), *value])
             else:
                 after[name] = value
         return after
@@ -453,150 +442,6 @@ def _apply_property_preview(
     return after
 
 
-def _execute_and_verify(
-    *,
-    target: str,
-    operation: str,
-    id: Any,
-    properties: dict[str, Any],
-    before: dict[str, Any],
-    planned_after: dict[str, Any],
-    cardinalities: dict[str, str],
-    payload: dict[str, Any],
-) -> dict[str, Any]:
-    try:
-        manager = _graph_manager()
-        if target == "vertex":
-            raw_result = (
-                manager.appendVertex(id, properties)
-                if operation == "append"
-                else manager.eliminateVertex(id, properties)
-            )
-        else:
-            raw_result = (
-                manager.appendEdge(id, properties)
-                if operation == "append"
-                else manager.eliminateEdge(id, properties)
-            )
-    except Exception as exc:  # noqa: BLE001 - classify client failure
-        classification = classify_hugegraph_exception(exc)
-        return envelope_err(
-            classification.error_type,
-            f"Property mutation execution failed: {exc!s}",
-            suggestion=classification.suggestion,
-            retryable=classification.retryable,
-            source="mutate_graph_properties_tool",
-            details={
-                "stage": "mutation_execute",
-                "error": str(exc),
-                "target": target,
-                "id": id,
-                "reason": classification.reason,
-            },
-        )
-
-    raw_result_item = _plain_item(raw_result)
-    if raw_result_item is None:
-        raw_result_item = planned_after
-
-    try:
-        manager = _graph_manager()
-        post_read = (
-            _plain_item(manager.getVertexById(id))
-            if target == "vertex"
-            else _plain_item(manager.getEdgeById(id))
-        )
-    except Exception as exc:  # noqa: BLE001 - classify client failure
-        return _post_write_verification_error(
-            message="Mutation returned from HugeGraph, but post-read verification failed.",
-            details={
-                "stage": "post_write_verification",
-                "status": "unknown",
-                "target": target,
-                "id": id,
-                "before": before,
-                "planned_after": planned_after,
-                "operation_result": raw_result_item,
-                "post_read_error": sanitize_for_response(str(exc)),
-            },
-            warnings=[
-                "Mutation returned from HugeGraph, but post-read verification failed."
-            ],
-            next_actions=["Call query_graph_data_tool to verify the target state."],
-        )
-
-    if post_read is None:
-        return _post_write_verification_error(
-            message="Mutation returned from HugeGraph, but the target was not found on post-read.",
-            details={
-                "stage": "post_write_verification",
-                "status": "unknown",
-                "target": target,
-                "id": id,
-                "before": before,
-                "planned_after": planned_after,
-                "operation_result": raw_result_item,
-                "post_read": None,
-            },
-            warnings=[
-                "Mutation returned from HugeGraph, but the target was not found on post-read."
-            ],
-            next_actions=[
-                "Call query_graph_data_tool to verify whether the target still exists."
-            ],
-        )
-
-    if not _properties_match(post_read, planned_after, cardinalities):
-        return _post_write_verification_error(
-            message="Post-read state did not match the planned preview.",
-            details={
-                "stage": "post_write_verification",
-                "status": "unknown",
-                "target": target,
-                "id": id,
-                "before": before,
-                "planned_after": planned_after,
-                "operation_result": raw_result_item,
-                "post_read": post_read,
-            },
-            warnings=["Post-read state did not match the planned preview."],
-            next_actions=["Call query_graph_data_tool to inspect the target state."],
-        )
-
-    result_payload = dict(payload)
-    result_payload.update(
-        {
-            "status": "applied",
-            "before": before,
-            "after": post_read,
-            "operation_result": raw_result_item,
-        }
-    )
-    return envelope_ok(
-        result_payload,
-        warnings=[],
-        next_actions=["Call query_graph_data_tool to inspect the updated target."],
-    )
-
-
-def _post_write_verification_error(
-    *,
-    message: str,
-    details: dict[str, Any],
-    warnings: list[str],
-    next_actions: list[str],
-) -> dict[str, Any]:
-    return envelope_err(
-        ErrorType.PARTIAL_APPLY,
-        message,
-        suggestion="Inspect the target state before retrying or issuing another write.",
-        source="mutate_graph_properties_tool",
-        details=details,
-        warnings=warnings,
-        next_actions=next_actions,
-    )
-
-
 def _build_mutation_plan_context(
     *,
     target: str,
@@ -629,16 +474,6 @@ def _nonce_with_snapshot(nonce: str | None, snapshot_digest: str) -> str:
     return f"{base}|ts:{snapshot_digest[:16]}"
 
 
-def _snapshot_from_nonce(nonce: str | None) -> str | None:
-    if nonce is None:
-        return None
-    marker = "|ts:"
-    value = str(nonce)
-    if marker not in value:
-        return None
-    return value.rsplit(marker, 1)[1] or None
-
-
 def _payload_digest(
     target: str,
     operation: str,
@@ -681,25 +516,6 @@ def _find_label_schema(labels: Any, label_name: str) -> dict[str, Any] | None:
     return None
 
 
-def _properties_match(
-    post_read: dict[str, Any],
-    planned_after: dict[str, Any],
-    cardinalities: dict[str, str],
-) -> bool:
-    observed = post_read.get("properties") or {}
-    expected = planned_after.get("properties") or {}
-    if observed.keys() != expected.keys():
-        return False
-    for name, expected_value in expected.items():
-        observed_value = observed[name]
-        if cardinalities.get(name, "SINGLE") == "SET":
-            if not _set_values_match(observed_value, expected_value):
-                return False
-        elif observed_value != expected_value:
-            return False
-    return True
-
-
 def _existing_collection(value: Any) -> list[Any]:
     if value is None:
         return []
@@ -714,17 +530,6 @@ def _stable_unique(values: list[Any]) -> list[Any]:
     return result
 
 
-def _set_values_match(observed: Any, expected: Any) -> bool:
-    if not isinstance(observed, list) or not isinstance(expected, list):
-        return observed == expected
-    observed_unique = _stable_unique(observed)
-    expected_unique = _stable_unique(expected)
-    return len(observed_unique) == len(expected_unique) and all(
-        any(observed_value == expected_value for observed_value in observed_unique)
-        for expected_value in expected_unique
-    )
-
-
 def _plain_item(item: Any) -> dict[str, Any] | None:
     if item is None:
         return None
diff --git a/hugegraph-mcp/hugegraph_mcp/tools/property_validation.py b/hugegraph-mcp/hugegraph_mcp/tools/property_validation.py
new file mode 100644
index 00000000..711c6c0d
--- /dev/null
+++ b/hugegraph-mcp/hugegraph_mcp/tools/property_validation.py
@@ -0,0 +1,198 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#     http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Shared validation for JSON property values against HugeGraph schema."""
+
+import base64
+import binascii
+import math
+from datetime import datetime
+from typing import Any
+from uuid import UUID
+
+from hugegraph_mcp.tools.schema_utils import normalized_schema_summary
+
+PropertySpec = tuple[str, str]
+
+DATA_TYPE_ALIASES = {
+    "BOOL": "BOOLEAN",
+    "INTEGER": "INT",
+    "STRING": "TEXT",
+}
+SUPPORTED_DATA_TYPES = frozenset(
+    {
+        "TEXT",
+        "UUID",
+        "INT",
+        "LONG",
+        "DOUBLE",
+        "FLOAT",
+        "BOOLEAN",
+        "DATE",
+        "BYTE",
+        "BLOB",
+        "OBJECT",
+    }
+)
+SUPPORTED_CARDINALITIES = frozenset({"SINGLE", "LIST", "SET"})
+
+_INTEGER_RANGES = {
+    "BYTE": (-(2**7), 2**7 - 1),
+    "INT": (-(2**31), 2**31 - 1),
+    "LONG": (-(2**63), 2**63 - 1),
+}
+
+# HugeGraph FLOAT and DOUBLE map to IEEE-754 binary32 and binary64 values.
+# Keep the bounds as exactly representable Python floats, and compare integers
+# directly so arbitrary-precision JSON integers never need a lossy conversion.
+_FLOATING_MAXIMUMS = {
+    "FLOAT": float.fromhex("0x1.fffffep+127"),
+    "DOUBLE": float.fromhex("0x1.fffffffffffffp+1023"),
+}
+
+
+def property_specs(live_schema: dict[str, Any]) -> dict[str, PropertySpec]:
+    """Return normalized ``name -> (data type, cardinality)`` schema entries."""
+
+    specs: dict[str, PropertySpec] = {}
+    normalized_schema = normalized_schema_summary(live_schema) or {}
+    for prop in normalized_schema.get("propertykeys", []):
+        if not isinstance(prop, dict):
+            continue
+        name = prop.get("name")
+        data_type = prop.get("data_type")
+        if not isinstance(name, str) or not isinstance(data_type, str):
+            continue
+        normalized_type = data_type.strip().upper()
+        specs[name] = (
+            DATA_TYPE_ALIASES.get(normalized_type, normalized_type),
+            str(prop.get("cardinality") or "SINGLE").strip().upper(),
+        )
+    return specs
+
+
+def property_value_error(
+    *,
+    item_kind: str,
+    item_index: int,
+    property_name: str,
+    value: Any,
+    spec: PropertySpec | None,
+) -> str | None:
+    """Return a stable validation message, or ``None`` for a valid value.
+
+    ``None`` retains the existing import contract: required/nullable-property
+    policy is owned by label validation and HugeGraph, not guessed here.
+    Collection elements remain non-null because HugeGraph cannot store a null
+    property value inside LIST/SET cardinalities.
+    """
+
+    if spec is None:
+        return None
+
+    data_type, cardinality = spec
+    prefix = f"{item_kind} {item_index} property '{property_name}'"
+    if data_type not in SUPPORTED_DATA_TYPES:
+        return f"{prefix} unsupported data_type '{data_type}'"
+    if cardinality not in SUPPORTED_CARDINALITIES:
+        return f"{prefix} unsupported cardinality '{cardinality}'"
+    if cardinality in {"LIST", "SET"}:
+        if value is None:
+            return None
+        if not isinstance(value, list):
+            return f"{prefix} expects {cardinality} of {data_type}, got {type(value).__name__}"
+        for element_index, element in enumerate(value):
+            if element is None or not value_matches_type(element, data_type):
+                return f"{prefix} element {element_index} expects {data_type}, got {type(element).__name__}"
+        return None
+
+    if not value_matches_type(value, data_type):
+        return f"{prefix} expects {data_type}, got {type(value).__name__}"
+    return None
+
+
+def value_matches_type(value: Any, data_type: str) -> bool:
+    """Check whether a JSON-compatible value can be stored by HugeGraph."""
+
+    if value is None:
+        return True
+    if data_type == "TEXT":
+        return isinstance(value, str)
+    if data_type in _INTEGER_RANGES:
+        if not isinstance(value, int) or isinstance(value, bool):
+            return False
+        minimum, maximum = _INTEGER_RANGES[data_type]
+        return minimum <= value <= maximum
+    if data_type in {"FLOAT", "DOUBLE"}:
+        if isinstance(value, bool) or not isinstance(value, (int, float)):
+            return False
+        maximum = _FLOATING_MAXIMUMS[data_type]
+        if isinstance(value, int):
+            return -maximum <= value <= maximum
+        return math.isfinite(value) and -maximum <= value <= maximum
+    if data_type == "BOOLEAN":
+        return isinstance(value, bool)
+    if data_type == "UUID":
+        return _is_uuid(value)
+    if data_type == "DATE":
+        return _is_date(value)
+    if data_type == "BLOB":
+        return _is_blob(value)
+    if data_type == "OBJECT":
+        return isinstance(value, dict)
+    return False
+
+
+def _is_uuid(value: Any) -> bool:
+    if not isinstance(value, str):
+        return False
+    try:
+        UUID(value)
+    except (ValueError, AttributeError):
+        return False
+    return True
+
+
+def _is_date(value: Any) -> bool:
+    if isinstance(value, int) and not isinstance(value, bool):
+        return -(2**63) <= value <= 2**63 - 1
+    if not isinstance(value, str) or not value.strip():
+        return False
+    candidate = value.strip()
+    if candidate.endswith("Z"):
+        candidate = f"{candidate[:-1]}+00:00"
+    try:
+        datetime.fromisoformat(candidate)
+    except ValueError:
+        return False
+    return True
+
+
+def _is_blob(value: Any) -> bool:
+    if isinstance(value, str):
+        if value.startswith("0x"):
+            try:
+                bytes.fromhex(value[2:])
+            except ValueError:
+                return False
+            return True
+        try:
+            base64.b64decode(value, validate=True)
+        except (binascii.Error, ValueError):
+            return False
+        return True
+    if isinstance(value, list):
+        return all(
+            isinstance(element, int) and not isinstance(element, bool) and -128 <= element <= 255 for element in value
+        )
+    return False
diff --git a/hugegraph-mcp/hugegraph_mcp/tools/schema_write_adapter.py b/hugegraph-mcp/hugegraph_mcp/tools/schema_write_adapter.py
new file mode 100644
index 00000000..46b367a4
--- /dev/null
+++ b/hugegraph-mcp/hugegraph_mcp/tools/schema_write_adapter.py
@@ -0,0 +1,214 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0.
+
+"""Canonical plan compiler, executor adapter, and reader for schema creates."""
+
+from __future__ import annotations
+
+import hashlib
+import json
+import time
+from collections.abc import Mapping
+from copy import deepcopy
+from typing import Any
+from uuid import uuid4
+
+from hugegraph_mcp.config import MCPConfig
+from hugegraph_mcp.guard import Capability, guard
+from hugegraph_mcp.plan_hash import PlanContext
+from hugegraph_mcp.tools.schema_utils import normalized_schema_summary
+from hugegraph_mcp.write_plan import (
+    ApplyReceipt,
+    ApplyStatus,
+    GraphTarget,
+    OperationPlan,
+    PlanStatus,
+    WritePlan,
+)
+
+CREATE_SCHEMA = "CREATE_SCHEMA"
+
+
+def compile_schema_write_plan(
+    operation: dict[str, Any],
+    *,
+    plan_context: PlanContext,
+    live_schema: dict[str, Any],
+) -> WritePlan:
+    """Compile exactly one validated schema create into an immutable plan."""
+
+    canonical_operation = deepcopy(operation)
+    plan_id = f"wp_{uuid4().hex}"
+    operation_id = f"{plan_id}:op:0000"
+    schema_kind = str(canonical_operation["type"])
+    name = str(canonical_operation["name"])
+    target = {
+        "schema_kind": schema_kind,
+        "name": name,
+        "operation": canonical_operation,
+    }
+    desired = {
+        "exists": True,
+        "schema_kind": schema_kind,
+        "name": name,
+        "operation": canonical_operation,
+    }
+    created_at = int(time.time())
+    return WritePlan(
+        plan_id=plan_id,
+        tool_name="apply_schema_tool",
+        graph_target=GraphTarget(
+            graph_url=plan_context.graph_url,
+            graph_name=plan_context.graph_name,
+            graphspace=plan_context.graphspace,
+        ),
+        principal=plan_context.principal,
+        operations=(
+            OperationPlan(
+                operation_id=operation_id,
+                kind=CREATE_SCHEMA,
+                target=target,
+                expected_state={"exists": False},
+                desired_state=desired,
+                idempotency_key=_digest(target),
+            ),
+        ),
+        payload_digest=_digest({"operations": [canonical_operation]}),
+        schema_fingerprint=_digest(normalized_schema_summary(live_schema)),
+        status=PlanStatus.ISSUED,
+        created_at=created_at,
+        expires_at=max(created_at + 1, int(plan_context.expires_at)),
+    )
+
+
+def schema_create_adapter(plan: WritePlan, operation: OperationPlan, attempt: int) -> ApplyReceipt:
+    """Execute a persisted schema create through the shared safe boundary."""
+
+    mismatch = _target_mismatch(plan, operation, attempt)
+    if mismatch is not None:
+        return mismatch
+    if guard(Capability.SCHEMA_WRITE) is not None:
+        return _receipt(
+            plan,
+            operation,
+            attempt,
+            ApplyStatus.REJECTED,
+            {"write_allowed": False},
+            reason_code="READONLY_VIOLATION",
+        )
+
+    # Delayed import prevents a module cycle: manage_schema imports the plan
+    # compiler, while the default executor registry imports this adapter.
+    from hugegraph_mcp.tools import manage_schema as schema_module
+
+    raw_operation = operation.to_dict()["target"]["operation"]
+    live_schema = schema_module.current_live_schema()
+    result = schema_module.apply_schema_operations(
+        [raw_operation],
+        live_schema=live_schema,
+    )
+    status = ApplyStatus(result["status"])
+    return _receipt(
+        plan,
+        operation,
+        attempt,
+        status,
+        result.get("observed_state"),
+        reason_code=_reason_code(status),
+        reconciliation_required=status is ApplyStatus.UNKNOWN,
+        committed=status is ApplyStatus.APPLIED,
+    )
+
+
+def schema_reconcile_reader(_plan: WritePlan, operation: OperationPlan) -> Mapping[str, Any]:
+    """Read one schema object by kind and name without mutating it."""
+
+    from hugegraph_mcp.tools import manage_schema as schema_module
+
+    raw_operation = operation.to_dict()["target"]["operation"]
+    state, observed = schema_module._schema_object_state(
+        raw_operation,
+        schema_module.current_live_schema(),
+    )
+    if state == "identical":
+        return dict(operation.desired_state)
+    if state == "missing":
+        return dict(operation.expected_state)
+    return {
+        "exists": True,
+        "schema_kind": operation.target["schema_kind"],
+        "name": operation.target["name"],
+        "conflicting_object": observed or {},
+    }
+
+
+def register_schema_write_adapters(registry: Any) -> None:
+    registry.register(CREATE_SCHEMA, schema_create_adapter)
+
+
+def register_schema_reconcile_readers(registry: Any) -> None:
+    registry.register(CREATE_SCHEMA, schema_reconcile_reader)
+
+
+def _target_mismatch(
+    plan: WritePlan,
+    operation: OperationPlan,
+    attempt: int,
+) -> ApplyReceipt | None:
+    cfg = MCPConfig.from_env()
+    current = (cfg.url, cfg.graph, cfg.graphspace or "DEFAULT", cfg.user)
+    planned = (
+        plan.graph_target.graph_url,
+        plan.graph_target.graph_name,
+        plan.graph_target.graphspace or "DEFAULT",
+        plan.principal,
+    )
+    if current == planned:
+        return None
+    return _receipt(
+        plan,
+        operation,
+        attempt,
+        ApplyStatus.REJECTED,
+        {"target_matches": False},
+        reason_code="TARGET_CHANGED",
+    )
+
+
+def _receipt(
+    plan: WritePlan,
+    operation: OperationPlan,
+    attempt: int,
+    status: ApplyStatus,
+    observed_state: Mapping[str, Any] | None,
+    *,
+    reason_code: str,
+    reconciliation_required: bool = False,
+    committed: bool = False,
+) -> ApplyReceipt:
+    return ApplyReceipt(
+        plan_id=plan.plan_id,
+        operation_id=operation.operation_id,
+        status=status,
+        observed_state=observed_state,
+        reason_code=reason_code,
+        attempt=attempt,
+        reconciliation_required=reconciliation_required,
+        committed_at=int(time.time()) if committed else None,
+    )
+
+
+def _reason_code(status: ApplyStatus) -> str:
+    return {
+        ApplyStatus.APPLIED: "SCHEMA_CREATED",
+        ApplyStatus.ALREADY_APPLIED: "SCHEMA_ALREADY_APPLIED",
+        ApplyStatus.CONFLICT: "SCHEMA_OBJECT_CONFLICT",
+        ApplyStatus.UNKNOWN: "SCHEMA_CREATE_OUTCOME_UNKNOWN",
+    }.get(status, status.value)
+
+
+def _digest(value: Any) -> str:
+    encoded = json.dumps(value, ensure_ascii=False, separators=(",", ":"), sort_keys=True).encode("utf-8")
+    return hashlib.sha256(encoded).hexdigest()
diff --git a/hugegraph-mcp/hugegraph_mcp/write_contract.py b/hugegraph-mcp/hugegraph_mcp/write_contract.py
new file mode 100644
index 00000000..d648acc3
--- /dev/null
+++ b/hugegraph-mcp/hugegraph_mcp/write_contract.py
@@ -0,0 +1,109 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0.
+
+"""Compatibility rules for canonical and legacy confirmation locators."""
+
+from dataclasses import dataclass
+from enum import Enum
+
+from hugegraph_mcp.envelope import ErrorType, envelope_err
+
+LEGACY_DEPRECATION_CODE = "LEGACY_CONFIRMATION_DEPRECATED"
+
+
+class ConfirmationProtocol(str, Enum):
+    CANONICAL = "canonical"
+    LEGACY = "legacy"
+
+
+@dataclass(frozen=True)
+class ConfirmationResolution:
+    protocol: ConfirmationProtocol
+    plan_id: str | None
+    plan_hash: str | None
+    nonce: str | None
+    expires_at: float | None
+    warnings: tuple[str, ...] = ()
+
+
+def resolve_confirmation_protocol(
+    *,
+    plan_id: str | None = None,
+    plan_hash: str | None = None,
+    nonce: str | None = None,
+    expires_at: float | None = None,
+) -> tuple[ConfirmationResolution | None, dict | None]:
+    """Resolve one mutually exclusive confirmation protocol fail-closed."""
+
+    canonical_present = bool(plan_id)
+    legacy_values = (plan_hash, nonce, expires_at)
+    legacy_present = any(value is not None for value in legacy_values)
+    legacy_complete = all(value is not None for value in legacy_values)
+
+    if canonical_present and legacy_present:
+        return None, _protocol_error("plan_id cannot be combined with plan_hash, nonce, or expires_at.")
+    if canonical_present:
+        return (
+            ConfirmationResolution(
+                protocol=ConfirmationProtocol.CANONICAL,
+                plan_id=plan_id,
+                plan_hash=None,
+                nonce=None,
+                expires_at=None,
+            ),
+            None,
+        )
+    if legacy_present and not legacy_complete:
+        return None, _protocol_error("plan_hash, nonce, and expires_at must be supplied together.")
+    if legacy_complete:
+        return (
+            ConfirmationResolution(
+                protocol=ConfirmationProtocol.LEGACY,
+                plan_id=None,
+                plan_hash=plan_hash,
+                nonce=nonce,
+                expires_at=expires_at,
+                warnings=(
+                    (
+                        f"{LEGACY_DEPRECATION_CODE}: use plan_id with confirm_write_tool; "
+                        "legacy confirmation fields will be removed after one release."
+                    ),
+                ),
+            ),
+            None,
+        )
+    return None, _protocol_error("A plan_id or complete legacy locator is required.")
+
+
+def resolve_optional_legacy_locator(
+    *,
+    plan_hash: str | None = None,
+    nonce: str | None = None,
+    expires_at: float | None = None,
+) -> tuple[ConfirmationResolution | None, dict | None]:
+    """Validate an optional legacy locator before a public write wrapper runs.
+
+    Dry-run and validation calls legitimately omit confirmation fields. Once
+    any legacy field is present, however, the complete deprecated locator is
+    required. The returned resolution is metadata only: execution must still
+    load the server-persisted plan rather than trusting the request payload.
+    """
+
+    if plan_hash is None and nonce is None and expires_at is None:
+        return None, None
+    return resolve_confirmation_protocol(
+        plan_hash=plan_hash,
+        nonce=nonce,
+        expires_at=expires_at,
+    )
+
+
+def _protocol_error(reason: str) -> dict:
+    return envelope_err(
+        ErrorType.VALIDATION_ERROR,
+        "Invalid write confirmation protocol.",
+        retryable=False,
+        details={"reason": reason},
+    )
diff --git a/hugegraph-mcp/hugegraph_mcp/write_executor.py b/hugegraph-mcp/hugegraph_mcp/write_executor.py
new file mode 100644
index 00000000..38105b03
--- /dev/null
+++ b/hugegraph-mcp/hugegraph_mcp/write_executor.py
@@ -0,0 +1,317 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0.
+
+"""Unified plan-ID confirmation and executor dispatch."""
+
+from __future__ import annotations
+
+import time
+import uuid
+from collections.abc import Callable
+from dataclasses import replace
+from math import ceil
+
+from hugegraph_mcp.envelope import ErrorType, envelope_err, envelope_ok
+from hugegraph_mcp.plan_store import (
+    PlanStore,
+    PlanTransitionError,
+    plan_store_from_config,
+)
+from hugegraph_mcp.write_plan import (
+    ApplyReceipt,
+    ApplyStatus,
+    OperationPlan,
+    PlanStatus,
+    WritePlan,
+)
+
+WriteAdapter = Callable[[WritePlan, OperationPlan, int], ApplyReceipt]
+LEASE_SAFETY_MARGIN_SECONDS = 5
+
+
+class WriteExecutorRegistry:
+    def __init__(self) -> None:
+        self._adapters: dict[str, WriteAdapter] = {}
+
+    def register(self, kind: str, adapter: WriteAdapter) -> None:
+        if not kind:
+            raise ValueError("adapter kind must be non-empty")
+        self._adapters[kind] = adapter
+
+    def adapter_for(self, kind: str) -> WriteAdapter | None:
+        return self._adapters.get(kind)
+
+
+class WriteExecutor:
+    def __init__(
+        self,
+        *,
+        store: PlanStore,
+        registry: WriteExecutorRegistry,
+        lease_seconds: int = 300,
+    ):
+        self.store = store
+        self.registry = registry
+        self.lease_seconds = lease_seconds
+
+    def confirm(self, plan_id: str) -> dict:
+        plan = self.store.get_plan(plan_id)
+        if plan is None:
+            return envelope_err(
+                ErrorType.NOT_FOUND,
+                "No server-issued write plan matches this plan_id.",
+                retryable=False,
+            )
+        if int(time.time()) > plan.expires_at and plan.status is PlanStatus.ISSUED:
+            self.store.transition_plan(
+                plan_id,
+                expected=PlanStatus.ISSUED,
+                target=PlanStatus.EXPIRED,
+            )
+            return self._confirmation_result(plan_id)
+
+        missing = [operation.kind for operation in plan.operations if self.registry.adapter_for(operation.kind) is None]
+        if missing:
+            return envelope_err(
+                ErrorType.FEATURE_DISABLED,
+                "The confirmed plan requires an unavailable atomic write adapter.",
+                retryable=False,
+                details={"missing_operation_kinds": sorted(set(missing))},
+            )
+
+        try:
+            owner_token = uuid.uuid4().hex
+            claimed_plan, claimed = self.store.claim_plan(
+                plan_id,
+                owner_token=owner_token,
+                lease_seconds=self.lease_seconds,
+            )
+        except PlanTransitionError:
+            return self._confirmation_result(plan_id)
+        if not claimed:
+            return self._confirmation_result(plan_id)
+
+        return self._execute_claimed(
+            claimed_plan,
+            claimable={PlanStatus.ISSUED.value},
+            owner_token=owner_token,
+        )
+
+    def resume(self, plan_id: str) -> dict:
+        plan = self.store.get_plan(plan_id)
+        record = self.store.get_plan_record(plan_id)
+        if plan is None or record is None:
+            raise PlanTransitionError("resume requires a reconciled RETRYABLE_NOT_APPLIED plan")
+        operation_statuses = {operation["operation_id"]: operation["status"] for operation in record["operations"]}
+        reconciled_operations = {
+            operation["operation_id"]: operation["reconciled"] for operation in record["operations"]
+        }
+        statuses = set(operation_statuses.values())
+        resumable = {
+            PlanStatus.APPLIED.value,
+            PlanStatus.ALREADY_APPLIED.value,
+            PlanStatus.RETRYABLE_NOT_APPLIED.value,
+        }
+        if (
+            plan.status not in {PlanStatus.RETRYABLE_NOT_APPLIED, PlanStatus.PARTIAL}
+            or PlanStatus.RETRYABLE_NOT_APPLIED.value not in statuses
+            or not statuses <= resumable
+            or any(
+                not reconciled_operations[operation_id]
+                for operation_id, status in operation_statuses.items()
+                if status == PlanStatus.RETRYABLE_NOT_APPLIED.value
+            )
+        ):
+            raise PlanTransitionError(
+                "resume requires reconciled operations whose remaining state is RETRYABLE_NOT_APPLIED"
+            )
+        missing = [
+            operation.kind
+            for operation in plan.operations
+            if operation_statuses[operation.operation_id] == PlanStatus.RETRYABLE_NOT_APPLIED.value
+            and self.registry.adapter_for(operation.kind) is None
+        ]
+        if missing:
+            return envelope_err(
+                ErrorType.FEATURE_DISABLED,
+                "The reconciled plan requires an unavailable atomic write adapter.",
+                retryable=False,
+                details={"missing_operation_kinds": sorted(set(missing))},
+            )
+        owner_token = uuid.uuid4().hex
+        claimed_plan, claimed = self.store.claim_plan(
+            plan_id,
+            owner_token=owner_token,
+            lease_seconds=self.lease_seconds,
+            resume=True,
+        )
+        if not claimed:
+            raise PlanTransitionError("resumable plan could not be exclusively leased")
+        return self._execute_claimed(
+            claimed_plan,
+            claimable={PlanStatus.RETRYABLE_NOT_APPLIED.value},
+            owner_token=owner_token,
+        )
+
+    def _execute_claimed(
+        self,
+        claimed_plan: WritePlan,
+        *,
+        claimable: set[str],
+        owner_token: str,
+    ) -> dict:
+        plan_id = claimed_plan.plan_id
+        record = self.store.get_plan_record(plan_id)
+        if record is None:
+            raise PlanTransitionError("claimed plan disappeared before execution")
+        operation_statuses = {operation["operation_id"]: operation["status"] for operation in record["operations"]}
+
+        for operation in claimed_plan.operations:
+            if operation_statuses[operation.operation_id] not in claimable:
+                continue
+            adapter = self.registry.adapter_for(operation.kind)
+            assert adapter is not None
+            attempt_token = uuid.uuid4().hex
+            attempt = self.store.claim_operation(
+                plan_id,
+                operation.operation_id,
+                owner_token=owner_token,
+                attempt_token=attempt_token,
+                lease_seconds=self.lease_seconds,
+            )
+            try:
+                receipt = adapter(claimed_plan, operation, attempt)
+            except Exception as exc:  # noqa: BLE001 - persist ambiguous adapter outcome
+                receipt = ApplyReceipt(
+                    plan_id=plan_id,
+                    operation_id=operation.operation_id,
+                    status=ApplyStatus.UNKNOWN,
+                    reason_code=type(exc).__name__,
+                    attempt=attempt,
+                    reconciliation_required=True,
+                )
+            receipt = replace(receipt, attempt_token=attempt_token)
+            self.store.record_receipt(
+                receipt,
+                owner_token=owner_token,
+                attempt_token=attempt_token,
+            )
+            if receipt.status not in {
+                ApplyStatus.APPLIED,
+                ApplyStatus.ALREADY_APPLIED,
+            }:
+                break
+        return self._confirmation_result(plan_id)
+
+    def _confirmation_result(self, plan_id: str) -> dict:
+        status_result = self.status(plan_id)
+        if not status_result.get("ok"):
+            return status_result
+        data = status_result["data"]
+        status = PlanStatus(data["status"])
+        if status in {PlanStatus.APPLIED, PlanStatus.ALREADY_APPLIED}:
+            return status_result
+        error_type = {
+            PlanStatus.PARTIAL: ErrorType.PARTIAL_APPLY,
+            PlanStatus.CONFLICT: ErrorType.WRITE_CONFLICT,
+            PlanStatus.REJECTED: ErrorType.FLOW_EXECUTION_FAILED,
+            PlanStatus.EXPIRED: ErrorType.PLAN_EXPIRED,
+        }.get(status, ErrorType.WRITE_OUTCOME_UNKNOWN)
+        return envelope_err(
+            error_type,
+            "The confirmed write plan did not reach an applied final state.",
+            retryable=False,
+            details=data,
+        )
+
+    def status(self, plan_id: str) -> dict:
+        record = self.store.get_plan_record(plan_id)
+        if record is None:
+            return envelope_err(
+                ErrorType.NOT_FOUND,
+                "No persisted write plan matches this plan_id.",
+                retryable=False,
+            )
+        public = dict(record)
+        public.pop("payload", None)
+        public["operations"] = [
+            {
+                key: (
+                    {
+                        receipt_key: receipt_value
+                        for receipt_key, receipt_value in value.items()
+                        if receipt_key != "attempt_token"
+                    }
+                    if key == "receipt" and isinstance(value, dict)
+                    else value
+                )
+                for key, value in operation.items()
+                if key not in {"payload", "attempt_token", "reconciled"}
+            }
+            for operation in record["operations"]
+        ]
+        if public["status"] == PlanStatus.LEGACY_UNKNOWN.value:
+            public["status"] = PlanStatus.UNKNOWN.value
+            public["reconciliation_required"] = True
+        elif public["status"] == PlanStatus.EXECUTING.value:
+            lease_expires_at = public.get("lease_expires_at")
+            if lease_expires_at is None or int(lease_expires_at) <= int(time.time()):
+                public["status"] = PlanStatus.UNKNOWN.value
+                public["reconciliation_required"] = True
+            else:
+                public["reconciliation_required"] = False
+        public.pop("lease_owner", None)
+        return envelope_ok(public)
+
+
+DEFAULT_WRITE_EXECUTOR_REGISTRY = WriteExecutorRegistry()
+
+# Graph adapters are registered here, after the registry type is defined, to
+# keep the adapter module independent of executor orchestration.
+from hugegraph_mcp.tools.graph_write_adapter import (
+    register_graph_write_adapters,
+)
+from hugegraph_mcp.tools.schema_write_adapter import (
+    register_schema_write_adapters,
+)
+
+register_graph_write_adapters(DEFAULT_WRITE_EXECUTOR_REGISTRY)
+register_schema_write_adapters(DEFAULT_WRITE_EXECUTOR_REGISTRY)
+
+
+def confirm_write(plan_id: str) -> dict:
+    try:
+        from hugegraph_mcp.config import MCPConfig
+
+        cfg = MCPConfig.from_env()
+        store = plan_store_from_config(cfg)
+    except Exception as exc:  # noqa: BLE001 - public tool must remain enveloped
+        return envelope_err(
+            ErrorType.SERVER_ERROR,
+            "The durable plan store is unavailable.",
+            retryable=False,
+            details={"reason": type(exc).__name__},
+        )
+    return WriteExecutor(
+        store=store,
+        registry=DEFAULT_WRITE_EXECUTOR_REGISTRY,
+        lease_seconds=ceil(cfg.write_timeout_seconds) + LEASE_SAFETY_MARGIN_SECONDS,
+    ).confirm(plan_id)
+
+
+def get_write_status(plan_id: str) -> dict:
+    try:
+        store = plan_store_from_config()
+    except Exception as exc:  # noqa: BLE001 - public tool must remain enveloped
+        return envelope_err(
+            ErrorType.SERVER_ERROR,
+            "The durable plan store is unavailable.",
+            retryable=False,
+            details={"reason": type(exc).__name__},
+        )
+    return WriteExecutor(
+        store=store,
+        registry=DEFAULT_WRITE_EXECUTOR_REGISTRY,
+    ).status(plan_id)
diff --git a/hugegraph-mcp/hugegraph_mcp/write_plan.py b/hugegraph-mcp/hugegraph_mcp/write_plan.py
new file mode 100644
index 00000000..03c87b14
--- /dev/null
+++ b/hugegraph-mcp/hugegraph_mcp/write_plan.py
@@ -0,0 +1,321 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0.
+
+"""Canonical immutable models and state rules for confirmed writes."""
+
+from __future__ import annotations
+
+import hashlib
+import json
+from collections.abc import Mapping, Sequence
+from dataclasses import dataclass
+from enum import Enum
+from types import MappingProxyType
+from typing import Any
+
+
+class ApplyStatus(str, Enum):
+    """Evidence-backed outcome of one write operation."""
+
+    APPLIED = "APPLIED"
+    ALREADY_APPLIED = "ALREADY_APPLIED"
+    REJECTED = "REJECTED"
+    CONFLICT = "CONFLICT"
+    PARTIAL = "PARTIAL"
+    UNKNOWN = "UNKNOWN"
+    LEGACY_UNKNOWN = "LEGACY_UNKNOWN"
+    RETRYABLE_NOT_APPLIED = "RETRYABLE_NOT_APPLIED"
+
+
+class PlanStatus(str, Enum):
+    """Lifecycle and aggregate outcome of one immutable write plan."""
+
+    ISSUED = "ISSUED"
+    EXECUTING = "EXECUTING"
+    APPLIED = "APPLIED"
+    ALREADY_APPLIED = "ALREADY_APPLIED"
+    REJECTED = "REJECTED"
+    CONFLICT = "CONFLICT"
+    PARTIAL = "PARTIAL"
+    UNKNOWN = "UNKNOWN"
+    LEGACY_UNKNOWN = "LEGACY_UNKNOWN"
+    RETRYABLE_NOT_APPLIED = "RETRYABLE_NOT_APPLIED"
+    EXPIRED = "EXPIRED"
+
+
+ALLOWED_PLAN_TRANSITIONS: Mapping[PlanStatus, frozenset[PlanStatus]] = MappingProxyType(
+    {
+        PlanStatus.ISSUED: frozenset({PlanStatus.EXECUTING, PlanStatus.EXPIRED}),
+        PlanStatus.EXECUTING: frozenset(
+            {
+                PlanStatus.APPLIED,
+                PlanStatus.ALREADY_APPLIED,
+                PlanStatus.REJECTED,
+                PlanStatus.CONFLICT,
+                PlanStatus.PARTIAL,
+                PlanStatus.UNKNOWN,
+            }
+        ),
+        PlanStatus.UNKNOWN: frozenset(
+            {
+                PlanStatus.APPLIED,
+                PlanStatus.CONFLICT,
+                PlanStatus.RETRYABLE_NOT_APPLIED,
+            }
+        ),
+        PlanStatus.LEGACY_UNKNOWN: frozenset(
+            {
+                PlanStatus.APPLIED,
+                PlanStatus.CONFLICT,
+                PlanStatus.RETRYABLE_NOT_APPLIED,
+            }
+        ),
+        PlanStatus.RETRYABLE_NOT_APPLIED: frozenset({PlanStatus.EXECUTING}),
+        PlanStatus.PARTIAL: frozenset({PlanStatus.PARTIAL, PlanStatus.APPLIED, PlanStatus.UNKNOWN}),
+        PlanStatus.APPLIED: frozenset(),
+        PlanStatus.ALREADY_APPLIED: frozenset(),
+        PlanStatus.REJECTED: frozenset(),
+        PlanStatus.CONFLICT: frozenset(),
+        PlanStatus.EXPIRED: frozenset(),
+    }
+)
+
+
+@dataclass(frozen=True)
+class GraphTarget:
+    graph_url: str
+    graph_name: str
+    graphspace: str | None
+
+    def to_dict(self) -> dict[str, Any]:
+        return {
+            "graph_url": self.graph_url,
+            "graph_name": self.graph_name,
+            "graphspace": self.graphspace,
+        }
+
+
+@dataclass(frozen=True)
+class OperationPlan:
+    operation_id: str
+    kind: str
+    target: Mapping[str, Any]
+    expected_state: Mapping[str, Any]
+    desired_state: Mapping[str, Any]
+    depends_on: tuple[str, ...] = ()
+    idempotency_key: str | None = None
+
+    def __post_init__(self) -> None:
+        if not self.operation_id:
+            raise ValueError("operation_id must be non-empty")
+        if not self.kind:
+            raise ValueError("operation kind must be non-empty")
+        object.__setattr__(self, "target", _freeze_mapping(self.target))
+        object.__setattr__(
+            self,
+            "expected_state",
+            _freeze_mapping(self.expected_state),
+        )
+        object.__setattr__(
+            self,
+            "desired_state",
+            _freeze_mapping(self.desired_state),
+        )
+        object.__setattr__(self, "depends_on", tuple(self.depends_on))
+
+    def to_dict(self) -> dict[str, Any]:
+        return {
+            "operation_id": self.operation_id,
+            "kind": self.kind,
+            "target": _thaw(self.target),
+            "expected_state": _thaw(self.expected_state),
+            "desired_state": _thaw(self.desired_state),
+            "depends_on": list(self.depends_on),
+            "idempotency_key": self.idempotency_key,
+        }
+
+
+@dataclass(frozen=True)
+class WritePlan:
+    plan_id: str
+    tool_name: str
+    graph_target: GraphTarget
+    principal: str
+    operations: tuple[OperationPlan, ...]
+    payload_digest: str
+    schema_fingerprint: str | None
+    status: PlanStatus
+    created_at: int
+    expires_at: int
+
+    def __post_init__(self) -> None:
+        if not self.plan_id:
+            raise ValueError("plan_id must be non-empty")
+        if not self.tool_name:
+            raise ValueError("tool_name must be non-empty")
+        operations = tuple(self.operations)
+        object.__setattr__(self, "operations", operations)
+
+        seen: set[str] = set()
+        for operation in operations:
+            if operation.operation_id in seen:
+                raise ValueError(f"duplicate operation_id: {operation.operation_id}")
+            missing = [dependency for dependency in operation.depends_on if dependency not in seen]
+            if missing:
+                raise ValueError(
+                    f"operation {operation.operation_id} depends on an earlier operation: {', '.join(missing)}"
+                )
+            seen.add(operation.operation_id)
+
+        if self.created_at < 0 or self.expires_at <= self.created_at:
+            raise ValueError("expires_at must be greater than created_at")
+
+    def to_dict(self) -> dict[str, Any]:
+        return {
+            "plan_id": self.plan_id,
+            "tool_name": self.tool_name,
+            "graph_target": self.graph_target.to_dict(),
+            "principal": self.principal,
+            "operations": [operation.to_dict() for operation in self.operations],
+            "payload_digest": self.payload_digest,
+            "schema_fingerprint": self.schema_fingerprint,
+            "status": self.status.value,
+            "created_at": self.created_at,
+            "expires_at": self.expires_at,
+        }
+
+
+@dataclass(frozen=True)
+class ApplyReceipt:
+    """Canonical receipt with a temporary legacy construction shape."""
+
+    status: ApplyStatus
+    plan_id: str | None = None
+    operation_id: str | None = None
+    observed_state: Mapping[str, Any] | None = None
+    reason_code: str | None = None
+    attempt: int = 0
+    attempt_token: str | None = None
+    reconciliation_required: bool = False
+    committed_at: int | None = None
+    operation: Mapping[str, Any] | None = None
+    reason: str | None = None
+
+    def __post_init__(self) -> None:
+        if self.attempt < 0:
+            raise ValueError("attempt must not be negative")
+        if self.observed_state is not None:
+            object.__setattr__(
+                self,
+                "observed_state",
+                _freeze_mapping(self.observed_state),
+            )
+        if self.operation is not None:
+            object.__setattr__(
+                self,
+                "operation",
+                _freeze_mapping(self.operation),
+            )
+
+    def to_dict(self) -> dict[str, Any]:
+        if self.plan_id is None and self.operation_id is None:
+            return {
+                "status": self.status.value,
+                "operation": _thaw(self.operation),
+                "observed_state": _thaw(self.observed_state),
+                "reason": self.reason,
+                "reconciliation_required": self.reconciliation_required,
+            }
+        return {
+            "plan_id": self.plan_id,
+            "operation_id": self.operation_id,
+            "status": self.status.value,
+            "observed_state": _thaw(self.observed_state),
+            "reason_code": self.reason_code,
+            "attempt": self.attempt,
+            "reconciliation_required": self.reconciliation_required,
+            "committed_at": self.committed_at,
+            **({"attempt_token": self.attempt_token} if self.attempt_token is not None else {}),
+        }
+
+
+def can_transition(current: PlanStatus, target: PlanStatus) -> bool:
+    """Return whether the explicit state machine permits this transition."""
+
+    return target in ALLOWED_PLAN_TRANSITIONS.get(current, frozenset())
+
+
+def aggregate_plan_status(
+    operation_statuses: Sequence[ApplyStatus],
+) -> PlanStatus:
+    """Aggregate operation evidence without hiding known partial writes."""
+
+    statuses = tuple(operation_statuses)
+    if not statuses:
+        return PlanStatus.ISSUED
+
+    success = {ApplyStatus.APPLIED, ApplyStatus.ALREADY_APPLIED}
+    if all(status in success for status in statuses):
+        if all(status is ApplyStatus.ALREADY_APPLIED for status in statuses):
+            return PlanStatus.ALREADY_APPLIED
+        return PlanStatus.APPLIED
+    if any(status in success for status in statuses):
+        return PlanStatus.PARTIAL
+    if ApplyStatus.PARTIAL in statuses:
+        return PlanStatus.PARTIAL
+    if ApplyStatus.UNKNOWN in statuses:
+        return PlanStatus.UNKNOWN
+    if ApplyStatus.CONFLICT in statuses:
+        return PlanStatus.CONFLICT
+    if ApplyStatus.RETRYABLE_NOT_APPLIED in statuses:
+        return PlanStatus.RETRYABLE_NOT_APPLIED
+    return PlanStatus.REJECTED
+
+
+def canonical_plan_json(plan: WritePlan) -> str:
+    """Serialize one plan deterministically for integrity checks."""
+
+    return json.dumps(
+        plan.to_dict(),
+        ensure_ascii=False,
+        separators=(",", ":"),
+        sort_keys=True,
+    )
+
+
+def canonical_plan_digest(plan: WritePlan) -> str:
+    """Return the full SHA-256 digest of a canonical plan."""
+
+    return hashlib.sha256(canonical_plan_json(plan).encode("utf-8")).hexdigest()
+
+
+def _freeze_mapping(value: Mapping[str, Any]) -> Mapping[str, Any]:
+    if not isinstance(value, Mapping):
+        raise TypeError("write plan state must be a mapping")
+    return MappingProxyType({str(key): _freeze(item) for key, item in value.items()})
+
+
+def _freeze(value: Any) -> Any:
+    if isinstance(value, Mapping):
+        return _freeze_mapping(value)
+    if isinstance(value, (list, tuple)):
+        return tuple(_freeze(item) for item in value)
+    if isinstance(value, (set, frozenset)):
+        return frozenset(_freeze(item) for item in value)
+    return value
+
+
+def _thaw(value: Any) -> Any:
+    if value is None:
+        return None
+    if isinstance(value, Mapping):
+        return {str(key): _thaw(item) for key, item in value.items()}
+    if isinstance(value, tuple):
+        return [_thaw(item) for item in value]
+    if isinstance(value, frozenset):
+        return sorted((_thaw(item) for item in value), key=repr)
+    if isinstance(value, Enum):
+        return value.value
+    return value
diff --git a/hugegraph-mcp/tests/contracts/write_confirmation_v2.json b/hugegraph-mcp/tests/contracts/write_confirmation_v2.json
new file mode 100644
index 00000000..185e0bdb
--- /dev/null
+++ b/hugegraph-mcp/tests/contracts/write_confirmation_v2.json
@@ -0,0 +1,42 @@
+{
+  "contract_version": "2.1",
+  "canonical": {
+    "authoritative_source": "server_persisted_plan",
+    "confirm_tool": "confirm_write_tool",
+    "confirm_request_fields": [
+      "plan_id"
+    ],
+    "status_tool": "get_write_status_tool",
+    "reconcile_tool": "reconcile_write_tool",
+    "repeated_confirmation": "return_persisted_status",
+    "mixed_protocol": "reject",
+    "client_payload_authoritative": false
+  },
+  "legacy": {
+    "accepted_for_releases": 1,
+    "locator_fields": [
+      "plan_hash",
+      "nonce",
+      "expires_at"
+    ],
+    "fields_are_all_or_nothing": true,
+    "authoritative_source": "server_persisted_plan",
+    "client_payload_authoritative": false,
+    "response_warning_code": "LEGACY_CONFIRMATION_DEPRECATED"
+  },
+  "outcomes": [
+    "APPLIED",
+    "ALREADY_APPLIED",
+    "REJECTED",
+    "CONFLICT",
+    "PARTIAL",
+    "UNKNOWN"
+  ],
+  "direct_retryable_outcomes": [],
+  "errors": {
+    "ambiguous": "WRITE_OUTCOME_UNKNOWN",
+    "conflict": "WRITE_CONFLICT",
+    "partial": "PARTIAL_APPLY",
+    "unsupported_atomic_primitive": "FEATURE_DISABLED"
+  }
+}
diff --git a/hugegraph-mcp/tests/integration/test_real_write_path.py b/hugegraph-mcp/tests/integration/test_real_write_path.py
index 1a2e5727..86218a3f 100644
--- a/hugegraph-mcp/tests/integration/test_real_write_path.py
+++ b/hugegraph-mcp/tests/integration/test_real_write_path.py
@@ -16,16 +16,17 @@
 from __future__ import annotations
 
 import time
+from concurrent.futures import ThreadPoolExecutor
+from threading import Barrier
 from uuid import uuid4
 
 import pytest
-from pyhugegraph.client import PyHugeClient
-
 from hugegraph_mcp import server
 from hugegraph_mcp.config import MCPConfig
 from hugegraph_mcp.hugegraph_client import build_hugegraph_client
 from hugegraph_mcp.tools import manage_graph_data as manage_graph_data_module
 from hugegraph_mcp.tools.graph_data_gremlin import _g
+from pyhugegraph.client import PyHugeClient
 
 pytestmark = [pytest.mark.integration, pytest.mark.real_hugegraph]
 
@@ -33,14 +34,10 @@ pytestmark = [pytest.mark.integration, pytest.mark.real_hugegraph]
 @pytest.fixture
 def hugegraph_client(monkeypatch):
     if _env("RUN_MCP_REAL_HUGEGRAPH_TESTS") != "1":
-        pytest.skip(
-            "set RUN_MCP_REAL_HUGEGRAPH_TESTS=1 to run real HugeGraph write tests"
-        )
+        pytest.skip("set RUN_MCP_REAL_HUGEGRAPH_TESTS=1 to run real HugeGraph write tests")
 
     monkeypatch.setenv("HUGEGRAPH_URL", _env("HUGEGRAPH_URL", "http://127.0.0.1:8080"))
-    monkeypatch.setenv(
-        "HUGEGRAPH_GRAPH_PATH", _env("HUGEGRAPH_GRAPH_PATH", "DEFAULT/hugegraph")
-    )
+    monkeypatch.setenv("HUGEGRAPH_GRAPH_PATH", _env("HUGEGRAPH_GRAPH_PATH", "DEFAULT/hugegraph"))
     monkeypatch.setenv("HUGEGRAPH_USER", _env("HUGEGRAPH_USER", "admin"))
     monkeypatch.setenv("HUGEGRAPH_PASSWORD", _env("HUGEGRAPH_PASSWORD", "admin"))
     monkeypatch.setenv("HUGEGRAPH_MCP_READONLY", "false")
@@ -62,24 +59,27 @@ def hugegraph_client(monkeypatch):
     return client
 
 
-def test_id_based_ingest_writes_the_intended_edge(hugegraph_client):
+def test_id_based_ingest_is_preview_only_without_create_capability(
+    hugegraph_client,
+):
     names = _schema_names("id_edge")
     _ensure_custom_id_schema(hugegraph_client, names)
     _exec(
         hugegraph_client,
-        f"g.addV({_g(names.vertex_label)}).property(T.id,'decoy')"
-        f".property({_g(names.name_key)},'Alice')",
+        f"g.addV({_g(names.vertex_label)}).property(T.id,'decoy').property({_g(names.name_key)},'Alice')",
     )
+    alice_id = f"alice_{uuid4().hex}"
+    bob_id = f"bob_{uuid4().hex}"
 
     graph_data = {
         "vertices": [
             {
-                "id": "alice",
+                "id": alice_id,
                 "label": names.vertex_label,
                 "properties": {names.name_key: "Alice"},
             },
             {
-                "id": "bob",
+                "id": bob_id,
                 "label": names.vertex_label,
                 "properties": {names.name_key: "Bob"},
             },
@@ -89,21 +89,29 @@ def test_id_based_ingest_writes_the_intended_edge(hugegraph_client):
                 "label": names.edge_label,
                 "source_label": names.vertex_label,
                 "target_label": names.vertex_label,
-                "source": {"id": "alice"},
-                "target": {"id": "bob"},
+                "source": {"id": alice_id},
+                "target": {"id": bob_id},
             }
         ],
     }
 
     result = _import_graph_data(graph_data)
 
-    assert result["ok"] is True
+    assert result["ok"] is False
+    assert result["error"]["type"] == "FEATURE_DISABLED"
     assert (
         _count(
             hugegraph_client,
-            f"g.V().hasId('alice').out({_g(names.edge_label)}).hasId('bob')",
+            f"g.V().hasId({_g(alice_id)},{_g(bob_id)})",
         )
-        == 1
+        == 0
+    )
+    assert (
+        _count(
+            hugegraph_client,
+            f"g.V().hasId({_g(alice_id)}).out({_g(names.edge_label)}).hasId({_g(bob_id)})",
+        )
+        == 0
     )
     assert (
         _count(
@@ -144,46 +152,25 @@ def test_apply_schema_forwards_and_verifies_supported_fields(hugegraph_client):
         },
     ]
 
-    dry_run = server.apply_schema_tool(mode="dry_run", operations=operations)
-    assert dry_run["ok"] is True
-    assert dry_run["data"]["valid"] is True
-    context = dry_run["data"]["plan_context"]
-
-    applied = server.apply_schema_tool(
-        mode="apply",
-        operations=operations,
-        confirm=True,
-        plan_hash=dry_run["data"]["plan_hash"],
-        nonce=context["nonce"],
-        expires_at=context["expires_at"],
-    )
-    assert applied["ok"] is True
-    assert applied["data"]["status"] == "applied"
+    for operation in operations:
+        applied = _apply_schema_operation(operation)
+        assert applied["data"]["status"] == "APPLIED"
+        assert applied["data"]["operations"][0]["status"] == "APPLIED"
+        receipt = applied["data"]["operations"][0]["receipt"]
+        assert receipt["status"] == "APPLIED"
+        assert receipt["reason_code"] == "SCHEMA_CREATED"
+        assert receipt["committed_at"] is not None
 
     schema = hugegraph_client.schema().getSchema()
     raw_schema = schema.get("schema", schema)
-    property_key = next(
-        item for item in raw_schema["propertykeys"] if item.get("name") == property_name
-    )
-    vertex_label = next(
-        item for item in raw_schema["vertexlabels"] if item.get("name") == label_name
-    )
-    edge_label = next(
-        item
-        for item in raw_schema["edgelabels"]
-        if item.get("name") == f"mcp_schema_edge_{suffix}"
-    )
-    assert _user_data_without_server_metadata(property_key.get("user_data")) == {
-        "owner": "mcp-integration"
-    }
+    property_key = next(item for item in raw_schema["propertykeys"] if item.get("name") == property_name)
+    vertex_label = next(item for item in raw_schema["vertexlabels"] if item.get("name") == label_name)
+    edge_label = next(item for item in raw_schema["edgelabels"] if item.get("name") == f"mcp_schema_edge_{suffix}")
+    assert _user_data_without_server_metadata(property_key.get("user_data")) == {"owner": "mcp-integration"}
     assert vertex_label.get("enable_label_index") is False
-    assert _user_data_without_server_metadata(vertex_label.get("user_data")) == {
-        "owner": "mcp-integration"
-    }
+    assert _user_data_without_server_metadata(vertex_label.get("user_data")) == {"owner": "mcp-integration"}
     assert edge_label.get("enable_label_index") is True
-    assert _user_data_without_server_metadata(edge_label.get("user_data")) == {
-        "owner": "mcp-integration-edge"
-    }
+    assert _user_data_without_server_metadata(edge_label.get("user_data")) == {"owner": "mcp-integration-edge"}
 
 
 def test_create_edge_rejects_missing_endpoint(hugegraph_client):
@@ -191,8 +178,7 @@ def test_create_edge_rejects_missing_endpoint(hugegraph_client):
     _ensure_custom_id_schema(hugegraph_client, names)
     _exec(
         hugegraph_client,
-        f"g.addV({_g(names.vertex_label)}).property(T.id,'alice')"
-        f".property({_g(names.name_key)},'Alice')",
+        f"g.addV({_g(names.vertex_label)}).property(T.id,'alice').property({_g(names.name_key)},'Alice')",
     )
     change_plan = {
         "operations": [
@@ -213,10 +199,7 @@ def test_create_edge_rejects_missing_endpoint(hugegraph_client):
     )
 
     assert result["valid"] is False
-    assert any(
-        "target endpoint matched_count must be 1" in error["reason"]
-        for error in result["errors"]
-    )
+    assert any("target endpoint matched_count must be 1" in error["reason"] for error in result["errors"])
     assert (
         _count(
             hugegraph_client,
@@ -231,18 +214,15 @@ def test_create_edge_rejects_non_unique_property_match(hugegraph_client):
     _ensure_custom_id_schema(hugegraph_client, names)
     _exec(
         hugegraph_client,
-        f"g.addV({_g(names.vertex_label)}).property(T.id,'alice_1')"
-        f".property({_g(names.name_key)},'Alice')",
+        f"g.addV({_g(names.vertex_label)}).property(T.id,'alice_1').property({_g(names.name_key)},'Alice')",
     )
     _exec(
         hugegraph_client,
-        f"g.addV({_g(names.vertex_label)}).property(T.id,'alice_2')"
-        f".property({_g(names.name_key)},'Alice')",
+        f"g.addV({_g(names.vertex_label)}).property(T.id,'alice_2').property({_g(names.name_key)},'Alice')",
     )
     _exec(
         hugegraph_client,
-        f"g.addV({_g(names.vertex_label)}).property(T.id,'bob')"
-        f".property({_g(names.name_key)},'Bob')",
+        f"g.addV({_g(names.vertex_label)}).property(T.id,'bob').property({_g(names.name_key)},'Bob')",
     )
     change_plan = {
         "operations": [
@@ -263,10 +243,7 @@ def test_create_edge_rejects_non_unique_property_match(hugegraph_client):
     )
 
     assert result["valid"] is False
-    assert any(
-        "source endpoint matched_count must be 1" in error["reason"]
-        for error in result["errors"]
-    )
+    assert any("source endpoint matched_count must be 1" in error["reason"] for error in result["errors"])
     assert (
         _count(
             hugegraph_client,
@@ -276,6 +253,67 @@ def test_create_edge_rejects_non_unique_property_match(hugegraph_client):
     )
 
 
+def test_create_edge_uses_endpoint_ids_bound_before_predicate_expands(
+    hugegraph_client,
+):
+    names = _schema_names("edge_bound_ids")
+    _ensure_custom_id_schema(hugegraph_client, names)
+    _exec(
+        hugegraph_client,
+        f"g.addV({_g(names.vertex_label)}).property(T.id,'alice-original').property({_g(names.name_key)},'Alice')",
+    )
+    _exec(
+        hugegraph_client,
+        f"g.addV({_g(names.vertex_label)}).property(T.id,'bob').property({_g(names.name_key)},'Bob')",
+    )
+    change_plan = {
+        "operations": [
+            {
+                "op": "create_edge",
+                "label": names.edge_label,
+                "source_label": names.vertex_label,
+                "source_match": {names.name_key: "Alice"},
+                "target_label": names.vertex_label,
+                "target_match": {names.name_key: "Bob"},
+            }
+        ]
+    }
+
+    dry_run = manage_graph_data_module.dry_run_graph_change_plan(
+        change_plan,
+        manage_graph_data_module._fetch_live_schema(),
+    )
+    assert dry_run["valid"] is True
+    operation = dry_run["compiled_plan"]["operations"][0]
+    assert operation["source_id"] == "alice-original"
+    assert operation["target_id"] == "bob"
+
+    # Expand the approved predicate after planning. The executor must retain
+    # the original backend identity instead of matching both Alice vertices.
+    _exec(
+        hugegraph_client,
+        f"g.addV({_g(names.vertex_label)}).property(T.id,'alice-concurrent').property({_g(names.name_key)},'Alice')",
+    )
+    result = manage_graph_data_module.execute_graph_change_plan(dry_run["compiled_plan"])
+
+    assert result["success"] is True
+    assert _count(hugegraph_client, f"g.E().hasLabel({_g(names.edge_label)})") == 1
+    assert (
+        _count(
+            hugegraph_client,
+            f"g.V('alice-original').out({_g(names.edge_label)}).hasId('bob')",
+        )
+        == 1
+    )
+    assert (
+        _count(
+            hugegraph_client,
+            f"g.V('alice-concurrent').out({_g(names.edge_label)})",
+        )
+        == 0
+    )
+
+
 def test_create_vertex_existing_id_rejected_in_dry_run_without_writes(
     hugegraph_client,
 ):
@@ -283,8 +321,7 @@ def test_create_vertex_existing_id_rejected_in_dry_run_without_writes(
     _ensure_custom_id_schema(hugegraph_client, names)
     _exec(
         hugegraph_client,
-        f"g.addV({_g(names.vertex_label)}).property(T.id,'conflict')"
-        f".property({_g(names.name_key)},'Existing')",
+        f"g.addV({_g(names.vertex_label)}).property(T.id,'conflict').property({_g(names.name_key)},'Existing')",
     )
     graph_data = {
         "vertices": [
@@ -307,8 +344,7 @@ def test_create_vertex_existing_id_rejected_in_dry_run_without_writes(
     assert dry_run["ok"] is False
     assert dry_run["error"]["type"] == "INVALID_GRAPH_DATA"
     assert any(
-        "create_vertex id identity already exists" in error["reason"]
-        for error in dry_run["error"]["details"]["errors"]
+        "create_vertex id identity already exists" in error["reason"] for error in dry_run["error"]["details"]["errors"]
     )
     assert (
         _count(
@@ -360,16 +396,14 @@ def test_create_vertex_existing_primary_key_rejected_in_dry_run_without_writes(
     assert (
         _count(
             hugegraph_client,
-            f"g.V().hasLabel({_g(names.vertex_label)})"
-            f".has({_g(names.name_key)},'Alice')",
+            f"g.V().hasLabel({_g(names.vertex_label)}).has({_g(names.name_key)},'Alice')",
         )
         == 0
     )
     assert (
         _count(
             hugegraph_client,
-            f"g.V().hasLabel({_g(names.vertex_label)})"
-            f".has({_g(names.name_key)},'Conflict')",
+            f"g.V().hasLabel({_g(names.vertex_label)}).has({_g(names.name_key)},'Conflict')",
         )
         == 1
     )
@@ -379,32 +413,20 @@ def test_public_delete_edge_and_vertices_real_graph_state_matches(
     hugegraph_client,
 ):
     names = _schema_names("delete")
-    _ensure_primary_key_schema(hugegraph_client, names)
-    graph_data = {
-        "vertices": [
-            {
-                "label": names.vertex_label,
-                "properties": {names.name_key: "Alice"},
-            },
-            {
-                "label": names.vertex_label,
-                "properties": {names.name_key: "Bob"},
-            },
-        ],
-        "edges": [
-            {
-                "label": names.edge_label,
-                "source_label": names.vertex_label,
-                "source": {names.name_key: "Alice"},
-                "target_label": names.vertex_label,
-                "target": {names.name_key: "Bob"},
-            }
-        ],
-    }
-
-    result = _import_graph_data(graph_data)
+    _ensure_custom_id_schema(hugegraph_client, names)
+    alice_id = f"{names.vertex_label}_alice"
+    bob_id = f"{names.vertex_label}_bob"
+    _exec(
+        hugegraph_client,
+        f"g.addV({_g(names.vertex_label)}).property(T.id,{_g(alice_id)}).property({_g(names.name_key)},'Alice')",
+    )
+    _exec(
+        hugegraph_client,
+        f"g.addV({_g(names.vertex_label)}).property(T.id,{_g(bob_id)}).property({_g(names.name_key)},'Bob')",
+    )
+    edge_create = f"g.V({_g(alice_id)}).as('s').V({_g(bob_id)}).addE({_g(names.edge_label)}).from('s')"
+    _exec(hugegraph_client, edge_create)
 
-    assert result["ok"] is True
     assert _count(hugegraph_client, f"g.V().hasLabel({_g(names.vertex_label)})") == 2
     assert _count(hugegraph_client, f"g.E().hasLabel({_g(names.edge_label)})") == 1
 
@@ -426,6 +448,15 @@ def test_public_delete_edge_and_vertices_real_graph_state_matches(
     assert _count(hugegraph_client, f"g.E().hasLabel({_g(names.edge_label)})") == 0
     assert _count(hugegraph_client, f"g.V().hasLabel({_g(names.vertex_label)})") == 2
 
+    # The canonical plan-id path deletes the same stable edge identity.
+    _exec(hugegraph_client, edge_create)
+    canonical_preview = server.delete_graph_data_tool(change_plan=edge_delete_plan)
+    assert canonical_preview["ok"] is True
+    canonical_result = server.confirm_write_tool(plan_id=canonical_preview["data"]["plan_id"])
+    assert canonical_result["ok"] is True
+    assert canonical_result["data"]["status"] == "APPLIED"
+    assert _count(hugegraph_client, f"g.E().hasLabel({_g(names.edge_label)})") == 0
+
     vertex_delete_plan = {
         "operations": [
             {
@@ -442,40 +473,256 @@ def test_public_delete_edge_and_vertices_real_graph_state_matches(
     }
     result = _delete_graph_data(vertex_delete_plan)
 
-    assert result["ok"] is True
-    assert _count(hugegraph_client, f"g.V().hasLabel({_g(names.vertex_label)})") == 0
+    assert result["ok"] is False
+    assert result["error"]["type"] == "FEATURE_DISABLED"
+    assert _count(hugegraph_client, f"g.V().hasLabel({_g(names.vertex_label)})") == 2
 
 
-def test_edge_by_id_query_and_mutate_handles_real_hugegraph_edge_id(
+def test_delete_confirmation_uses_dry_run_bound_vertex_id(hugegraph_client):
+    names = _schema_names("bound_delete")
+    _ensure_custom_id_schema(hugegraph_client, names)
+    original_id = f"original_{uuid4().hex}"
+    late_match_id = f"late_match_{uuid4().hex}"
+    _exec(
+        hugegraph_client,
+        f"g.addV({_g(names.vertex_label)}).property(T.id,{_g(original_id)}).property({_g(names.name_key)},'Shared')",
+    )
+    change_plan = {
+        "operations": [
+            {
+                "op": "delete_vertex",
+                "label": names.vertex_label,
+                "match": {names.name_key: "Shared"},
+                "cascade": False,
+            }
+        ]
+    }
+
+    dry_run = server.delete_graph_data_tool(change_plan=change_plan)
+    assert dry_run["ok"] is True
+    assert dry_run["data"]["confirmable"] is False
+    assert dry_run["data"]["preview"][0]["target_id"] == original_id
+
+    _exec(
+        hugegraph_client,
+        f"g.addV({_g(names.vertex_label)}).property(T.id,{_g(late_match_id)}).property({_g(names.name_key)},'Shared')",
+    )
+    context = dry_run["data"]["plan_context"]
+    result = server.delete_graph_data_tool(
+        change_plan=change_plan,
+        dry_run=False,
+        confirm=True,
+        plan_hash=dry_run["data"]["plan_hash"],
+        nonce=context["nonce"],
+        expires_at=context["expires_at"],
+    )
+
+    assert result["ok"] is False
+    assert result["error"]["type"] == "FEATURE_DISABLED"
+    assert _count(hugegraph_client, f"g.V({_g(original_id)})") == 1
+    assert _count(hugegraph_client, f"g.V({_g(late_match_id)})") == 1
+
+
+def test_delete_vertex_without_cascade_is_blocked_if_edge_added_after_dry_run(
     hugegraph_client,
 ):
-    names = _schema_names("edge_id")
-    _ensure_primary_key_schema(hugegraph_client, names)
-    graph_data = {
-        "vertices": [
+    names = _schema_names("conditional_delete")
+    _ensure_custom_id_schema(hugegraph_client, names)
+    alice_id = f"alice_{uuid4().hex}"
+    bob_id = f"bob_{uuid4().hex}"
+    _exec(
+        hugegraph_client,
+        f"g.addV({_g(names.vertex_label)}).property(T.id,{_g(alice_id)}).property({_g(names.name_key)},'Alice')",
+    )
+    _exec(
+        hugegraph_client,
+        f"g.addV({_g(names.vertex_label)}).property(T.id,{_g(bob_id)}).property({_g(names.name_key)},'Bob')",
+    )
+    change_plan = {
+        "operations": [
             {
+                "op": "delete_vertex",
                 "label": names.vertex_label,
-                "properties": {names.name_key: "Alice"},
-            },
+                "match": {"id": alice_id},
+                "cascade": False,
+            }
+        ]
+    }
+
+    dry_run = server.delete_graph_data_tool(change_plan=change_plan)
+    assert dry_run["ok"] is True
+    _exec(
+        hugegraph_client,
+        f"g.V({_g(alice_id)}).as('s').V({_g(bob_id)}).addE({_g(names.edge_label)}).from('s').next()",
+    )
+    assert _count(hugegraph_client, f"g.V({_g(alice_id)}).bothE()") == 1
+    context = dry_run["data"]["plan_context"]
+    result = server.delete_graph_data_tool(
+        change_plan=change_plan,
+        dry_run=False,
+        confirm=True,
+        plan_hash=dry_run["data"]["plan_hash"],
+        nonce=context["nonce"],
+        expires_at=context["expires_at"],
+    )
+
+    assert result["ok"] is False
+    assert result["error"]["type"] == "FEATURE_DISABLED"
+    assert _count(hugegraph_client, f"g.V({_g(alice_id)})") == 1
+    assert _count(hugegraph_client, f"g.V({_g(bob_id)})") == 1
+    assert _count(hugegraph_client, f"g.E().hasLabel({_g(names.edge_label)})") == 1
+
+
+@pytest.mark.xfail(
+    reason="HugeGraph 1.7.0/RocksDB did not isolate concurrent edge-add and conditional vertex-delete",
+    # This is a stochastic concurrency probe. An XPASS means only that this
+    # run did not observe the known bad interleaving; it is not proof that the
+    # backend provides the isolation capability.
+    strict=False,
+)
+def test_rocksdb_isolates_conditional_vertex_delete_from_concurrent_edge_add(
+    hugegraph_client,
+):
+    """Probe the HugeGraph 1.7.0/RocksDB delete capability profile.
+
+    A client-side check followed by a delete cannot establish this property.  Both
+    writes must race as separate server requests, released by the same barrier,
+    and every committed outcome must be one of the two serializable outcomes:
+
+    * delete wins, so edge creation fails and the target vertex is absent; or
+    * edge creation wins, so ``not(bothE())`` prevents the vertex deletion.
+
+    In particular, a successful edge creation followed by the vertex and edge
+    disappearing would be an implicit cascade and must fail this test.
+    """
+
+    names = _schema_names("isolated_delete")
+    _ensure_custom_id_schema(hugegraph_client, names)
+    iterations = int(_env("MCP_ISOLATED_DELETE_STRESS_ITERATIONS", "50"))
+
+    def race_write(query: str, start: Barrier) -> tuple[object | None, str | None]:
+        client = build_hugegraph_client(MCPConfig.from_env(), client_cls=PyHugeClient)
+        start.wait(timeout=10.0)
+        try:
+            result = _extract_count(_exec(client, query))
+        except Exception as exc:  # noqa: BLE001 - record concurrent backend outcome
+            return None, str(exc)
+        return result, None
+
+    violations: list[str] = []
+    for iteration in range(iterations):
+        alice_id = f"isolated_alice_{iteration}_{uuid4().hex}"
+        bob_id = f"isolated_bob_{iteration}_{uuid4().hex}"
+        _exec(
+            hugegraph_client,
+            f"g.addV({_g(names.vertex_label)}).property(T.id,{_g(alice_id)})"
+            f".property({_g(names.name_key)},'Alice').iterate()",
+        )
+        _exec(
+            hugegraph_client,
+            f"g.addV({_g(names.vertex_label)}).property(T.id,{_g(bob_id)})"
+            f".property({_g(names.name_key)},'Bob').iterate()",
+        )
+
+        start = Barrier(3)
+        # count() returns 1 only when addE actually produced an edge.  An empty
+        # traversal may complete without an exception after the concurrent
+        # delete; treating that as a successful create would be false evidence
+        # of an implicit cascade.
+        add_query = f"g.V({_g(alice_id)}).as('s').V({_g(bob_id)}).addE({_g(names.edge_label)}).from('s').count()"
+        delete_query = f"g.V({_g(alice_id)}).not(bothE()).drop().iterate(); true"
+        with ThreadPoolExecutor(max_workers=2) as pool:
+            add_future = pool.submit(race_write, add_query, start)
+            delete_future = pool.submit(race_write, delete_query, start)
+            start.wait(timeout=10.0)
+            add_affected, add_error = add_future.result(timeout=30.0)
+            delete_completed, delete_error = delete_future.result(timeout=30.0)
+
+        alice_count = _count(hugegraph_client, f"g.V({_g(alice_id)})")
+        bob_count = _count(hugegraph_client, f"g.V({_g(bob_id)})")
+        edge_count = _count(
+            hugegraph_client,
+            f"g.V({_g(alice_id)}).outE({_g(names.edge_label)}).where(inV().hasId({_g(bob_id)}))",
+        )
+        observed = (add_affected, delete_completed, alice_count, bob_count, edge_count)
+        allowed = {
+            (None, True, 0, 1, 0),
+            (0, True, 0, 1, 0),
+            (1, True, 1, 1, 1),
+        }
+        if observed not in allowed:
+            violations.append(
+                f"iteration {iteration}: {observed}; add_error={add_error!r}, delete_error={delete_error!r}"
+            )
+
+        # Each iteration has unique IDs, but explicit cleanup keeps the stress
+        # test bounded when the edge-add outcome wins repeatedly.
+        _exec(hugegraph_client, f"g.V({_g(alice_id)},{_g(bob_id)}).drop().iterate()")
+
+    assert not violations, f"{len(violations)} non-isolated outcomes; first 10:\n" + "\n".join(violations[:10])
+
+
+def test_dry_run_rejects_edge_whose_target_vertex_is_created_later(
+    hugegraph_client,
+):
+    names = _schema_names("future_endpoint")
+    _ensure_custom_id_schema(hugegraph_client, names)
+    change_plan = {
+        "operations": [
             {
+                "op": "create_vertex",
                 "label": names.vertex_label,
-                "properties": {names.name_key: "Bob"},
+                "id": "alice",
+                "properties": {names.name_key: "Alice"},
             },
-        ],
-        "edges": [
             {
+                "op": "create_edge",
                 "label": names.edge_label,
                 "source_label": names.vertex_label,
-                "source": {names.name_key: "Alice"},
+                "source_match": {"id": "alice"},
                 "target_label": names.vertex_label,
-                "target": {names.name_key: "Bob"},
-            }
-        ],
+                "target_match": {"id": "bob"},
+            },
+            {
+                "op": "create_vertex",
+                "label": names.vertex_label,
+                "id": "bob",
+                "properties": {names.name_key: "Bob"},
+            },
+        ]
     }
 
-    result = _import_graph_data(graph_data)
+    result = manage_graph_data_module.dry_run_graph_change_plan(
+        change_plan,
+        manage_graph_data_module._fetch_live_schema(),
+    )
 
-    assert result["ok"] is True
+    assert result["valid"] is False
+    assert result["preview"][1]["source_planned_count"] == 1
+    assert result["preview"][1]["target_planned_count"] == 0
+    assert any("target endpoint matched_count must be 1, got 0" in error["reason"] for error in result["errors"])
+    assert _count(hugegraph_client, f"g.V().hasLabel({_g(names.vertex_label)})") == 0
+
+
+def test_edge_by_id_query_and_mutate_handles_real_hugegraph_edge_id(
+    hugegraph_client,
+):
+    names = _schema_names("edge_id")
+    _ensure_custom_id_schema(hugegraph_client, names)
+    alice_id = f"{names.vertex_label}_alice"
+    bob_id = f"{names.vertex_label}_bob"
+    _exec(
+        hugegraph_client,
+        f"g.addV({_g(names.vertex_label)}).property(T.id,{_g(alice_id)}).property({_g(names.name_key)},'Alice')",
+    )
+    _exec(
+        hugegraph_client,
+        f"g.addV({_g(names.vertex_label)}).property(T.id,{_g(bob_id)}).property({_g(names.name_key)},'Bob')",
+    )
+    _exec(
+        hugegraph_client,
+        f"g.V({_g(alice_id)}).as('s').V({_g(bob_id)}).addE({_g(names.edge_label)}).from('s')",
+    )
     edge_id = _edge_id(hugegraph_client, names)
     assert ">" in edge_id
 
@@ -496,6 +743,7 @@ def test_edge_by_id_query_and_mutate_handles_real_hugegraph_edge_id(
     )
     assert dry_run["ok"] is True
     plan_context = dry_run["data"]["plan_context"]
+    before = hugegraph_client.graph().getEdgeById(edge_id).properties
     apply_result = server.mutate_graph_properties_tool(
         target="edge",
         operation="append",
@@ -508,43 +756,10 @@ def test_edge_by_id_query_and_mutate_handles_real_hugegraph_edge_id(
         expires_at=plan_context["expires_at"],
     )
 
-    assert apply_result["ok"] is True
-    query_result = server.query_graph_data_tool(
-        target="edge",
-        operation="get_by_id",
-        id=edge_id,
-    )
-    assert (
-        query_result["data"]["items"][0]["properties"][names.name_key] == "friendship"
-    )
-
-    dry_run = server.mutate_graph_properties_tool(
-        target="edge",
-        operation="eliminate",
-        id=edge_id,
-        properties={names.name_key: "friendship"},
-    )
-    assert dry_run["ok"] is True
-    plan_context = dry_run["data"]["plan_context"]
-    apply_result = server.mutate_graph_properties_tool(
-        target="edge",
-        operation="eliminate",
-        id=edge_id,
-        properties={names.name_key: "friendship"},
-        dry_run=False,
-        confirm=True,
-        plan_hash=dry_run["data"]["plan_hash"],
-        nonce=plan_context["nonce"],
-        expires_at=plan_context["expires_at"],
-    )
-
-    assert apply_result["ok"] is True
-    query_result = server.query_graph_data_tool(
-        target="edge",
-        operation="get_by_id",
-        id=edge_id,
-    )
-    assert names.name_key not in query_result["data"]["items"][0]["properties"]
+    assert apply_result["ok"] is False
+    assert apply_result["error"]["type"] == "FEATURE_DISABLED"
+    assert apply_result["error"]["details"]["write_attempted"] is False
+    assert hugegraph_client.graph().getEdgeById(edge_id).properties == before
 
 
 def test_collection_append_matches_real_vertex_list_and_edge_set(hugegraph_client):
@@ -590,8 +805,10 @@ def test_collection_append_matches_real_vertex_list_and_edge_set(hugegraph_clien
         nonce=vertex_context["nonce"],
         expires_at=vertex_context["expires_at"],
     )
-    assert vertex_result["ok"] is True
-    assert graph.getVertexById(alice.id).properties[names.list_key] == ["a", "b", "b"]
+    assert vertex_result["ok"] is False
+    assert vertex_result["error"]["type"] == "FEATURE_DISABLED"
+    assert vertex_result["error"]["details"]["write_attempted"] is False
+    assert graph.getVertexById(alice.id).properties[names.list_key] == ["a"]
 
     edge_dry_run = server.mutate_graph_properties_tool(
         target="edge",
@@ -613,11 +830,13 @@ def test_collection_append_matches_real_vertex_list_and_edge_set(hugegraph_clien
         nonce=edge_context["nonce"],
         expires_at=edge_context["expires_at"],
     )
-    assert edge_result["ok"] is True
-    assert set(graph.getEdgeById(edge.id).properties[names.set_key]) == {"a", "b"}
+    assert edge_result["ok"] is False
+    assert edge_result["error"]["type"] == "FEATURE_DISABLED"
+    assert edge_result["error"]["details"]["write_attempted"] is False
+    assert set(graph.getEdgeById(edge.id).properties[names.set_key]) == {"a"}
 
 
-def test_partial_write_returns_error_envelope_and_real_graph_state_matches(
+def test_import_partial_scenario_is_preview_only_and_has_no_side_effects(
     hugegraph_client,
 ):
     names = _schema_names("partial")
@@ -639,6 +858,7 @@ def test_partial_write_returns_error_envelope_and_real_graph_state_matches(
     }
     dry_run = server.import_graph_data_tool(mode="ingest", graph_data=graph_data)
     assert dry_run["ok"] is True
+    assert dry_run["data"]["confirmable"] is False
 
     plan_context = dry_run["data"]["plan_context"]
 
@@ -653,19 +873,18 @@ def test_partial_write_returns_error_envelope_and_real_graph_state_matches(
     )
 
     assert result["ok"] is False
-    assert result["error"]["details"]["status"] == "partial"
+    assert result["error"]["type"] == "FEATURE_DISABLED"
     assert (
         _count(
             hugegraph_client,
             f"g.V().hasLabel({_g(names.vertex_label)}).hasId('alice')",
         )
-        == 1
+        == 0
     )
     assert (
         _count(
             hugegraph_client,
-            f"g.V().hasLabel({_g(names.vertex_label)})"
-            f".hasId('conflict').has({_g(names.name_key)},'Duplicate')",
+            f"g.V().hasLabel({_g(names.vertex_label)}).hasId('conflict').has({_g(names.name_key)},'Duplicate')",
         )
         == 0
     )
@@ -718,8 +937,7 @@ def test_admin_write_tool_gate_prevents_real_write(hugegraph_client, monkeypatch
 
     result = server.execute_gremlin_write_tool(
         gremlin_query=(
-            f"g.addV({_g(names.vertex_label)}).property(T.id,'admin_blocked')"
-            f".property({_g(names.name_key)},'Blocked')"
+            f"g.addV({_g(names.vertex_label)}).property(T.id,'admin_blocked').property({_g(names.name_key)},'Blocked')"
         )
     )
 
@@ -736,7 +954,7 @@ def test_admin_write_tool_gate_prevents_real_write(hugegraph_client, monkeypatch
 
 def _import_graph_data(graph_data: dict) -> dict:
     dry_run = server.import_graph_data_tool(mode="ingest", graph_data=graph_data)
-    assert dry_run["ok"] is True
+    assert dry_run["ok"] is True, dry_run
     plan_context = dry_run["data"]["plan_context"]
     return server.import_graph_data_tool(
         mode="ingest",
@@ -749,6 +967,21 @@ def _import_graph_data(graph_data: dict) -> dict:
     )
 
 
+def _apply_schema_operation(operation: dict) -> dict:
+    operations = [operation]
+    dry_run = server.apply_schema_tool(mode="dry_run", operations=operations)
+    assert dry_run["ok"] is True
+    assert dry_run["data"]["valid"] is True
+    plan_id = dry_run["data"]["plan_id"]
+    assert plan_id.startswith("wp_")
+    result = server.confirm_write_tool(plan_id=plan_id)
+    assert result["ok"] is True, result
+    status = server.get_write_status_tool(plan_id=plan_id)
+    assert status["ok"] is True
+    assert status["data"] == result["data"]
+    return result
+
+
 def _delete_graph_data(change_plan: dict) -> dict:
     dry_run = server.delete_graph_data_tool(change_plan=change_plan)
     assert dry_run["ok"] is True
@@ -786,15 +1019,13 @@ def _ensure_custom_id_schema(
 ) -> None:
     schema = client.schema()
     schema.propertyKey(names.name_key).asText().ifNotExist().create()
-    schema.vertexLabel(names.vertex_label).properties(
+    schema.vertexLabel(names.vertex_label).properties(names.name_key).useCustomizeStringId().nullableKeys(
         names.name_key
-    ).useCustomizeStringId().nullableKeys(names.name_key).ifNotExist().create()
-    schema.edgeLabel(names.edge_label).sourceLabel(names.vertex_label).targetLabel(
-        names.vertex_label
-    ).properties(names.name_key).nullableKeys(names.name_key).ifNotExist().create()
-    index = (
-        schema.indexLabel(names.name_index).onV(names.vertex_label).by(names.name_key)
-    )
+    ).ifNotExist().create()
+    schema.edgeLabel(names.edge_label).sourceLabel(names.vertex_label).targetLabel(names.vertex_label).properties(
+        names.name_key
+    ).nullableKeys(names.name_key).ifNotExist().create()
+    index = schema.indexLabel(names.name_index).onV(names.vertex_label).by(names.name_key)
     if unique_name:
         index.unique().ifNotExist().create()
     else:
@@ -805,12 +1036,10 @@ def _ensure_custom_id_schema(
 def _ensure_primary_key_schema(client, names: _Names) -> None:
     schema = client.schema()
     schema.propertyKey(names.name_key).asText().ifNotExist().create()
-    schema.vertexLabel(names.vertex_label).properties(names.name_key).primaryKeys(
+    schema.vertexLabel(names.vertex_label).properties(names.name_key).primaryKeys(names.name_key).ifNotExist().create()
+    schema.edgeLabel(names.edge_label).sourceLabel(names.vertex_label).targetLabel(names.vertex_label).properties(
         names.name_key
-    ).ifNotExist().create()
-    schema.edgeLabel(names.edge_label).sourceLabel(names.vertex_label).targetLabel(
-        names.vertex_label
-    ).properties(names.name_key).nullableKeys(names.name_key).ifNotExist().create()
+    ).nullableKeys(names.name_key).ifNotExist().create()
     _wait_for_schema_visibility(client, names)
 
 
@@ -819,12 +1048,12 @@ def _ensure_collection_schema(client, names: _Names) -> None:
     schema.propertyKey(names.name_key).asText().ifNotExist().create()
     schema.propertyKey(names.list_key).asText().valueList().ifNotExist().create()
     schema.propertyKey(names.set_key).asText().valueSet().ifNotExist().create()
-    schema.vertexLabel(names.vertex_label).properties(
-        names.name_key, names.list_key
-    ).primaryKeys(names.name_key).nullableKeys(names.list_key).ifNotExist().create()
-    schema.edgeLabel(names.edge_label).sourceLabel(names.vertex_label).targetLabel(
-        names.vertex_label
-    ).properties(names.set_key).nullableKeys(names.set_key).ifNotExist().create()
+    schema.vertexLabel(names.vertex_label).properties(names.name_key, names.list_key).primaryKeys(
+        names.name_key
+    ).nullableKeys(names.list_key).ifNotExist().create()
+    schema.edgeLabel(names.edge_label).sourceLabel(names.vertex_label).targetLabel(names.vertex_label).properties(
+        names.set_key
+    ).nullableKeys(names.set_key).ifNotExist().create()
     _wait_for_schema_visibility(
         client,
         names,
@@ -859,24 +1088,14 @@ def _wait_for_schema_visibility(
     while time.monotonic() < deadline:
         try:
             schema = client.schema().getSchema()
-            schema_payload = (
-                schema.get("schema", schema) if isinstance(schema, dict) else {}
-            )
+            schema_payload = schema.get("schema", schema) if isinstance(schema, dict) else {}
             property_keys = {
-                item.get("name")
-                for item in schema_payload.get("propertykeys", [])
-                if isinstance(item, dict)
+                item.get("name") for item in schema_payload.get("propertykeys", []) if isinstance(item, dict)
             }
             vertex_labels = {
-                item.get("name")
-                for item in schema_payload.get("vertexlabels", [])
-                if isinstance(item, dict)
-            }
-            edge_labels = {
-                item.get("name")
-                for item in schema_payload.get("edgelabels", [])
-                if isinstance(item, dict)
+                item.get("name") for item in schema_payload.get("vertexlabels", []) if isinstance(item, dict)
             }
+            edge_labels = {item.get("name") for item in schema_payload.get("edgelabels", []) if isinstance(item, dict)}
             if (
                 (required_property_keys or {names.name_key}) <= property_keys
                 and names.vertex_label in vertex_labels
@@ -887,10 +1106,7 @@ def _wait_for_schema_visibility(
         except Exception as exc:  # noqa: BLE001 - retry until service is ready
             last_error = exc
         time.sleep(0.1)
-    pytest.fail(
-        "Timed out waiting for HugeGraph schema visibility: "
-        f"{names.vertex_label}, last_error={last_error}"
-    )
+    pytest.fail(f"Timed out waiting for HugeGraph schema visibility: {names.vertex_label}, last_error={last_error}")
 
 
 def _write_and_drop_schema_probe(
@@ -920,11 +1136,7 @@ def _extract_count(data):
 def _user_data_without_server_metadata(value):
     if not isinstance(value, dict):
         return None
-    return {
-        key: item
-        for key, item in value.items()
-        if not (isinstance(key, str) and key.startswith("~"))
-    }
+    return {key: item for key, item in value.items() if not (isinstance(key, str) and key.startswith("~"))}
 
 
 def _env(name: str, default: str | None = None) -> str | None:
diff --git a/hugegraph-mcp/tests/test_confirm_write.py b/hugegraph-mcp/tests/test_confirm_write.py
new file mode 100644
index 00000000..88b210f0
--- /dev/null
+++ b/hugegraph-mcp/tests/test_confirm_write.py
@@ -0,0 +1,243 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0.
+
+import sqlite3
+import time
+from concurrent.futures import ThreadPoolExecutor
+from threading import Barrier, Event, Lock
+
+import pytest
+from hugegraph_mcp.envelope import envelope_ok
+from hugegraph_mcp.plan_store import PlanTransitionError, SQLitePlanStore
+from hugegraph_mcp.reconciler import ReconcileReaderRegistry, WriteReconciler
+from hugegraph_mcp.server import confirm_write_tool, get_write_status_tool
+from hugegraph_mcp.write_executor import WriteExecutor, WriteExecutorRegistry
+from hugegraph_mcp.write_plan import (
+    ApplyReceipt,
+    ApplyStatus,
+    GraphTarget,
+    OperationPlan,
+    PlanStatus,
+    WritePlan,
+)
+
+
+def _plan() -> WritePlan:
+    return WritePlan(
+        plan_id="wp-confirm",
+        tool_name="test",
+        graph_target=GraphTarget("http://127.0.0.1:8080", "hugegraph", "DEFAULT"),
+        principal="admin",
+        operations=(
+            OperationPlan(
+                operation_id="op-confirm",
+                kind="TEST_APPLY",
+                target={"id": "v-1"},
+                expected_state={"value": 1},
+                desired_state={"value": 2},
+            ),
+        ),
+        payload_digest="digest",
+        schema_fingerprint=None,
+        status=PlanStatus.ISSUED,
+        created_at=100,
+        expires_at=4_102_444_800,
+    )
+
+
+def test_repeated_confirmation_returns_persisted_status_without_second_attempt(
+    tmp_path,
+):
+    store = SQLitePlanStore(tmp_path)
+    store.save_plan(_plan())
+    calls = []
+    registry = WriteExecutorRegistry()
+
+    def apply(plan, operation, attempt):
+        calls.append((plan.plan_id, operation.operation_id, attempt))
+        return ApplyReceipt(
+            plan_id=plan.plan_id,
+            operation_id=operation.operation_id,
+            status=ApplyStatus.APPLIED,
+            observed_state={"value": 2},
+            attempt=attempt,
+            committed_at=150,
+        )
+
+    registry.register("TEST_APPLY", apply)
+    executor = WriteExecutor(store=store, registry=registry)
+
+    first = executor.confirm("wp-confirm")
+    second = executor.confirm("wp-confirm")
+
+    assert first["ok"] is True
+    assert second["ok"] is True
+    assert first["data"]["status"] == "APPLIED"
+    assert second["data"]["status"] == "APPLIED"
+    assert calls == [("wp-confirm", "op-confirm", 1)]
+
+
+def test_concurrent_confirmation_has_one_adapter_attempt(tmp_path):
+    store = SQLitePlanStore(tmp_path)
+    store.save_plan(_plan())
+    registry = WriteExecutorRegistry()
+    calls = 0
+    calls_lock = Lock()
+
+    def apply(plan, operation, attempt):
+        nonlocal calls
+        with calls_lock:
+            calls += 1
+        return ApplyReceipt(
+            plan_id=plan.plan_id,
+            operation_id=operation.operation_id,
+            status=ApplyStatus.APPLIED,
+            attempt=attempt,
+            committed_at=150,
+        )
+
+    registry.register("TEST_APPLY", apply)
+    executor = WriteExecutor(store=store, registry=registry)
+    with ThreadPoolExecutor(max_workers=8) as pool:
+        results = list(pool.map(lambda _: executor.confirm("wp-confirm"), range(8)))
+
+    assert calls == 1
+    assert all(
+        (result["data"]["status"] if result["ok"] else result["error"]["details"]["status"]) in {"APPLIED", "EXECUTING"}
+        for result in results
+    )
+    assert executor.status("wp-confirm")["data"]["status"] == "APPLIED"
+
+
+def test_expired_executor_is_fenced_from_overwriting_new_attempt(tmp_path):
+    store = SQLitePlanStore(tmp_path)
+    store.save_plan(_plan())
+    entered = Barrier(2)
+    release_old = Event()
+    old_registry = WriteExecutorRegistry()
+
+    def delayed_old(plan, operation, attempt):
+        entered.wait()
+        assert release_old.wait(timeout=5)
+        return ApplyReceipt(
+            plan_id=plan.plan_id,
+            operation_id=operation.operation_id,
+            status=ApplyStatus.CONFLICT,
+            attempt=attempt,
+        )
+
+    old_registry.register("TEST_APPLY", delayed_old)
+    old_executor = WriteExecutor(store=store, registry=old_registry)
+    with ThreadPoolExecutor(max_workers=1) as pool:
+        old_result = pool.submit(old_executor.confirm, "wp-confirm")
+        entered.wait()
+        with sqlite3.connect(store.database_path) as connection:
+            connection.execute("UPDATE write_plans SET lease_expires_at = 0 WHERE plan_id = 'wp-confirm'")
+
+        readers = ReconcileReaderRegistry()
+        readers.register(
+            "TEST_APPLY",
+            lambda _plan, _operation: {"value": 1},
+        )
+        reconciled = WriteReconciler(store=store, readers=readers).reconcile("wp-confirm")
+        assert reconciled["data"]["status"] == "UNKNOWN"
+        assert reconciled["data"]["operations"][0]["receipt"]["reason_code"] == ("IN_FLIGHT_COMMIT_NOT_EXCLUDED")
+        with pytest.raises(PlanTransitionError):
+            WriteExecutor(store=store, registry=WriteExecutorRegistry()).resume("wp-confirm")
+        release_old.set()
+        with pytest.raises(PlanTransitionError, match="stale or unfenced"):
+            old_result.result()
+
+    record = store.get_plan_record("wp-confirm")
+    assert record is not None
+    assert record["status"] == "UNKNOWN"
+    assert record["operations"][0]["attempt"] == 1
+
+
+def test_claiming_each_operation_renews_lease_and_blocks_reconcile(tmp_path):
+    store = SQLitePlanStore(tmp_path)
+    store.save_plan(_plan())
+    owner = "executor-owner"
+    store.claim_plan("wp-confirm", owner_token=owner, lease_seconds=10)
+    with sqlite3.connect(store.database_path) as connection:
+        connection.execute(
+            "UPDATE write_plans SET lease_expires_at = strftime('%s','now') + 1 WHERE plan_id = 'wp-confirm'"
+        )
+
+    store.claim_operation(
+        "wp-confirm",
+        "op-confirm",
+        owner_token=owner,
+        attempt_token="attempt-one",
+        lease_seconds=40,
+    )
+
+    record = store.get_plan_record("wp-confirm")
+    assert record is not None
+    assert record["lease_expires_at"] >= int(time.time()) + 39
+    result = WriteReconciler(
+        store=store,
+        readers=ReconcileReaderRegistry(),
+    ).reconcile("wp-confirm")
+    assert result["error"]["details"]["reason_code"] == "EXECUTION_LEASE_ACTIVE"
+
+
+def test_missing_adapter_does_not_claim_plan(tmp_path):
+    store = SQLitePlanStore(tmp_path)
+    store.save_plan(_plan())
+    executor = WriteExecutor(store=store, registry=WriteExecutorRegistry())
+
+    result = executor.confirm("wp-confirm")
+
+    assert result["ok"] is False
+    assert result["error"]["type"] == "FEATURE_DISABLED"
+    assert store.get_plan("wp-confirm").status is PlanStatus.ISSUED
+
+
+def test_status_preserves_active_executing_lease_and_redacts_payload(tmp_path):
+    store = SQLitePlanStore(tmp_path)
+    store.save_plan(_plan())
+    store.claim_plan("wp-confirm")
+    executor = WriteExecutor(store=store, registry=WriteExecutorRegistry())
+
+    result = executor.status("wp-confirm")
+
+    assert result["ok"] is True
+    assert result["data"]["status"] == "EXECUTING"
+    assert result["data"]["reconciliation_required"] is False
+    assert "payload" not in result["data"]
+    assert "payload" not in result["data"]["operations"][0]
+
+
+def test_public_confirm_tool_routes_only_plan_id(monkeypatch):
+    expected = envelope_ok({"plan_id": "wp-confirm", "status": "APPLIED"})
+    calls = []
+
+    def fake_confirm(plan_id):
+        calls.append(plan_id)
+        return expected
+
+    monkeypatch.setattr("hugegraph_mcp.server.confirm_write", fake_confirm)
+
+    result = confirm_write_tool(plan_id="wp-confirm")
+
+    assert result["ok"] is True
+    assert calls == ["wp-confirm"]
+
+
+def test_public_status_tool_routes_only_plan_id(monkeypatch):
+    expected = envelope_ok({"plan_id": "wp-confirm", "status": "UNKNOWN"})
+    calls = []
+
+    def fake_status(plan_id):
+        calls.append(plan_id)
+        return expected
+
+    monkeypatch.setattr("hugegraph_mcp.server.get_write_status", fake_status)
+
+    result = get_write_status_tool(plan_id="wp-confirm")
+
+    assert result["ok"] is True
+    assert calls == ["wp-confirm"]
diff --git a/hugegraph-mcp/tests/test_graph_write_workflow.py b/hugegraph-mcp/tests/test_graph_write_workflow.py
new file mode 100644
index 00000000..2ef4ea7a
--- /dev/null
+++ b/hugegraph-mcp/tests/test_graph_write_workflow.py
@@ -0,0 +1,408 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0.
+
+from __future__ import annotations
+
+from hugegraph_mcp.config import MCPConfig
+from hugegraph_mcp.plan_hash import PlanContext
+from hugegraph_mcp.plan_store import SQLitePlanStore
+from hugegraph_mcp.reconciler import reconcile_operation_state
+from hugegraph_mcp.tools import graph_write_adapter
+from hugegraph_mcp.tools.graph_write_plan import (
+    CREATE_EDGE,
+    CREATE_VERTEX,
+    DELETE_EDGE,
+    compile_graph_write_plan,
+)
+from hugegraph_mcp.tools.manage_graph_data import manage_graph_data
+from hugegraph_mcp.write_executor import (
+    DEFAULT_WRITE_EXECUTOR_REGISTRY,
+    WriteExecutor,
+    confirm_write,
+)
+from hugegraph_mcp.write_plan import ApplyStatus
+
+
+def _schema():
+    return {
+        "schema": {
+            "propertykeys": [{"name": "name", "data_type": "TEXT"}],
+            "vertexlabels": [
+                {
+                    "name": "person",
+                    "properties": ["name"],
+                    "primary_keys": ["name"],
+                }
+            ],
+            "edgelabels": [
+                {
+                    "name": "knows",
+                    "source_label": "person",
+                    "target_label": "person",
+                    "frequency": "SINGLE",
+                    "properties": [],
+                }
+            ],
+        }
+    }
+
+
+def _context() -> PlanContext:
+    cfg = MCPConfig.from_env()
+    return PlanContext(
+        tool_name="manage_graph_data",
+        mode="import",
+        graph_url=cfg.url,
+        graph_name=cfg.graph,
+        graphspace=cfg.graphspace or "DEFAULT",
+        principal=cfg.user,
+        readonly=False,
+        payload_digest="legacy-digest",
+        schema_hash="schema",
+        nonce="nonce",
+        expires_at=4_102_444_800,
+    )
+
+
+def _import_plan():
+    return compile_graph_write_plan(
+        {
+            "operations": [
+                {
+                    "op": "create_vertex",
+                    "label": "person",
+                    "properties": {"name": "Alice"},
+                },
+                {
+                    "op": "create_vertex",
+                    "label": "person",
+                    "properties": {"name": "Bob"},
+                },
+                {
+                    "op": "create_edge",
+                    "label": "knows",
+                    "source_label": "person",
+                    "target_label": "person",
+                    "source_operation_index": 0,
+                    "target_operation_index": 1,
+                    "source_match": {"name": "Alice"},
+                    "target_match": {"name": "Bob"},
+                    "properties": {},
+                },
+            ]
+        },
+        plan_context=_context(),
+        live_schema=_schema(),
+    )
+
+
+def test_import_compiles_ordered_receipt_dependencies():
+    plan = _import_plan()
+
+    assert [operation.kind for operation in plan.operations] == [
+        CREATE_VERTEX,
+        CREATE_VERTEX,
+        CREATE_EDGE,
+    ]
+    edge = plan.operations[2]
+    assert edge.depends_on == (
+        plan.operations[0].operation_id,
+        plan.operations[1].operation_id,
+    )
+    assert edge.target["source"] == {"operation_id": plan.operations[0].operation_id}
+    assert "source_match" not in edge.target["source"]
+
+
+def test_default_registry_fails_closed_for_unverified_graph_creates():
+    assert DEFAULT_WRITE_EXECUTOR_REGISTRY.adapter_for(CREATE_VERTEX) is None
+    assert DEFAULT_WRITE_EXECUTOR_REGISTRY.adapter_for(CREATE_EDGE) is None
+    assert DEFAULT_WRITE_EXECUTOR_REGISTRY.adapter_for(DELETE_EDGE) is not None
+
+
+def test_executor_persists_vertex_ids_before_dependent_edge(monkeypatch, tmp_path):
+    plan = _import_plan()
+    store = SQLitePlanStore(tmp_path / "state")
+    store.save_plan(plan)
+    monkeypatch.setattr(graph_write_adapter, "plan_store_from_config", lambda: store)
+    state = {"vertices": {}, "edge": None}
+
+    def fake_read(query):
+        if ".outE('knows')" in query:
+            data = [state["edge"]] if state["edge"] is not None else []
+        elif query.startswith("g.V('alice-id')"):
+            data = [state["vertices"]["Alice"]] if "Alice" in state["vertices"] else []
+        elif query.startswith("g.V('bob-id')"):
+            data = [state["vertices"]["Bob"]] if "Bob" in state["vertices"] else []
+        elif ".has('name','Alice')" in query:
+            data = [state["vertices"]["Alice"]] if "Alice" in state["vertices"] else []
+        elif ".has('name','Bob')" in query:
+            data = [state["vertices"]["Bob"]] if "Bob" in state["vertices"] else []
+        else:
+            data = []
+        return {"ok": True, "data": {"data": data}}
+
+    def fake_write(query, **_kwargs):
+        if "addV('person')" in query and "'Alice'" in query:
+            state["vertices"]["Alice"] = {
+                "id": "alice-id",
+                "label": "person",
+                "name": "Alice",
+            }
+        elif "addV('person')" in query and "'Bob'" in query:
+            state["vertices"]["Bob"] = {
+                "id": "bob-id",
+                "label": "person",
+                "name": "Bob",
+            }
+        elif ".addE('knows')" in query:
+            assert "g.V('alice-id')" in query
+            assert ".V('bob-id')" in query
+            state["edge"] = {"id": "edge-id", "label": "knows"}
+        return {"ok": True, "data": {"affected": 1}}
+
+    monkeypatch.setattr(graph_write_adapter.gremlin_tools, "execute_gremlin_read", fake_read)
+    monkeypatch.setattr(graph_write_adapter.gremlin_tools, "execute_gremlin_write", fake_write)
+
+    result = WriteExecutor(
+        store=store,
+        registry=graph_write_adapter_registry(),
+    ).confirm(plan.plan_id)
+
+    assert result["ok"] is True
+    assert result["data"]["status"] == "APPLIED"
+    record = store.get_plan_record(plan.plan_id)
+    assert record is not None
+    assert record["operations"][0]["receipt"]["observed_state"]["id"] == "alice-id"
+    assert record["operations"][1]["receipt"]["observed_state"]["id"] == "bob-id"
+    assert record["operations"][2]["receipt"]["observed_state"]["id"] == "edge-id"
+
+
+def test_create_reconcile_classifies_exact_missing_conflict_and_duplicates(monkeypatch):
+    vertex = _import_plan().operations[0]
+    responses = iter(
+        [
+            [{"id": "v1", "label": "person", "name": "Alice"}],
+            [],
+            [{"id": "v1", "label": "person", "name": "Mallory"}],
+            [
+                {"id": "v1", "label": "person", "name": "Alice"},
+                {"id": "v2", "label": "person", "name": "Alice"},
+            ],
+        ]
+    )
+    monkeypatch.setattr(
+        graph_write_adapter.gremlin_tools,
+        "execute_gremlin_read",
+        lambda _query: {"ok": True, "data": {"data": next(responses)}},
+    )
+
+    exact = graph_write_adapter.create_vertex_reader(_import_plan(), vertex)
+    missing = graph_write_adapter.create_vertex_reader(_import_plan(), vertex)
+    conflict = graph_write_adapter.create_vertex_reader(_import_plan(), vertex)
+    duplicate = graph_write_adapter.create_vertex_reader(_import_plan(), vertex)
+
+    assert exact == vertex.desired_state
+    assert missing == vertex.expected_state
+    assert conflict["properties"] == {"name": "Mallory"}
+    assert duplicate["ambiguous"] is True
+
+
+def test_create_edge_reconcile_rejects_indistinguishable_duplicates(monkeypatch):
+    plan = compile_graph_write_plan(
+        {
+            "operations": [
+                {
+                    "op": "create_edge",
+                    "label": "knows",
+                    "source_label": "person",
+                    "target_label": "person",
+                    "source_id": "alice-id",
+                    "target_id": "bob-id",
+                    "properties": {},
+                }
+            ]
+        },
+        plan_context=_context(),
+        live_schema=_schema(),
+    )
+    edge = plan.operations[0]
+    responses = iter(
+        [
+            [{"id": "e1", "label": "knows"}],
+            [],
+            [{"id": "e1", "label": "other"}],
+            [
+                {"id": "e1", "label": "knows"},
+                {"id": "e2", "label": "knows"},
+            ],
+        ]
+    )
+    monkeypatch.setattr(
+        graph_write_adapter.gremlin_tools,
+        "execute_gremlin_read",
+        lambda _query: {"ok": True, "data": {"data": next(responses)}},
+    )
+
+    assert graph_write_adapter.create_edge_reader(plan, edge) == edge.desired_state
+    assert graph_write_adapter.create_edge_reader(plan, edge) == edge.expected_state
+    assert graph_write_adapter.create_edge_reader(plan, edge)["label"] == "other"
+    assert graph_write_adapter.create_edge_reader(plan, edge)["ambiguous"] is True
+
+
+def test_delete_edge_receipt_and_reader_keep_stable_backend_identity(
+    monkeypatch,
+):
+    plan = compile_graph_write_plan(
+        {
+            "operations": [
+                {
+                    "op": "delete_edge",
+                    "label": "knows",
+                    "source_label": "person",
+                    "target_label": "person",
+                    "source_match": {"id": "alice-id"},
+                    "target_match": {"id": "bob-id"},
+                    "target_id": "edge-id",
+                }
+            ]
+        },
+        plan_context=_context(),
+        live_schema=_schema(),
+    )
+    operation = plan.operations[0]
+    assert operation.kind == DELETE_EDGE
+    responses = iter([[{"id": "edge-id", "label": "knows"}], []])
+    monkeypatch.setattr(
+        graph_write_adapter.gremlin_tools,
+        "execute_gremlin_read",
+        lambda _query: {"ok": True, "data": {"data": next(responses)}},
+    )
+    writes = []
+    monkeypatch.setattr(
+        graph_write_adapter.gremlin_tools,
+        "execute_gremlin_write",
+        lambda query, **_kwargs: writes.append(query) or {"ok": True, "data": {"affected": 1}},
+    )
+
+    receipt = graph_write_adapter.delete_edge_adapter(plan, operation, 1)
+
+    assert receipt.status is ApplyStatus.APPLIED
+    assert receipt.observed_state == {
+        "exists": False,
+        "element_type": "edge",
+        "id": "edge-id",
+        "label": "knows",
+    }
+    assert writes == ["g.E('edge-id').drop()"]
+
+
+def test_delete_edge_reconcile_distinguishes_absent_expected_and_conflict(
+    monkeypatch,
+):
+    plan = compile_graph_write_plan(
+        {
+            "operations": [
+                {
+                    "op": "delete_edge",
+                    "label": "knows",
+                    "source_label": "person",
+                    "target_label": "person",
+                    "source_match": {"id": "alice-id"},
+                    "target_match": {"id": "bob-id"},
+                    "target_id": "edge-id",
+                }
+            ]
+        },
+        plan_context=_context(),
+        live_schema=_schema(),
+    )
+    operation = plan.operations[0]
+    responses = iter(
+        [
+            [],
+            [{"id": "edge-id", "label": "knows"}],
+            [{"id": "edge-id", "label": "other"}],
+        ]
+    )
+    monkeypatch.setattr(
+        graph_write_adapter.gremlin_tools,
+        "execute_gremlin_read",
+        lambda _query: {"ok": True, "data": {"data": next(responses)}},
+    )
+
+    statuses = [
+        reconcile_operation_state(
+            operation.kind,
+            operation.expected_state,
+            operation.desired_state,
+            graph_write_adapter.delete_reader(plan, operation),
+        )
+        for _ in range(3)
+    ]
+
+    assert statuses == [
+        ApplyStatus.APPLIED,
+        ApplyStatus.RETRYABLE_NOT_APPLIED,
+        ApplyStatus.CONFLICT,
+    ]
+
+
+def test_delete_edge_dry_run_plan_id_routes_through_canonical_executor(monkeypatch):
+    monkeypatch.setenv("HUGEGRAPH_MCP_READONLY", "false")
+    monkeypatch.setattr(
+        "hugegraph_mcp.tools.manage_graph_data._fetch_live_schema",
+        _schema,
+    )
+    state = {"exists": True}
+
+    def fake_read(query):
+        if query.endswith(".count()"):
+            data = [1]
+        elif query.endswith(".limit(2).id()"):
+            data = ["edge-id"]
+        elif query == "g.E('edge-id').limit(2).elementMap()":
+            data = [{"id": "edge-id", "label": "knows"}] if state["exists"] else []
+        else:
+            data = []
+        return {"ok": True, "data": {"data": data}}
+
+    def fake_write(query, **_kwargs):
+        assert query == "g.E('edge-id').drop()"
+        state["exists"] = False
+        return {"ok": True, "data": {"affected": 1}}
+
+    monkeypatch.setattr(graph_write_adapter.gremlin_tools, "execute_gremlin_read", fake_read)
+    monkeypatch.setattr(graph_write_adapter.gremlin_tools, "execute_gremlin_write", fake_write)
+    preview = manage_graph_data(
+        mode="delete",
+        change_plan={
+            "operations": [
+                {
+                    "op": "delete_edge",
+                    "label": "knows",
+                    "source_label": "person",
+                    "target_label": "person",
+                    "source_match": {"name": "Alice"},
+                    "target_match": {"name": "Bob"},
+                }
+            ]
+        },
+    )
+
+    result = confirm_write(preview["data"]["plan_id"])
+
+    assert preview["data"]["confirmable"] is True
+    assert result["ok"] is True
+    assert result["data"]["status"] == "APPLIED"
+    assert result["data"]["operations"][0]["receipt"]["observed_state"]["id"] == "edge-id"
+
+
+def graph_write_adapter_registry():
+    from hugegraph_mcp.write_executor import WriteExecutorRegistry
+
+    registry = WriteExecutorRegistry()
+    registry.register(CREATE_VERTEX, graph_write_adapter.create_vertex_adapter)
+    registry.register(CREATE_EDGE, graph_write_adapter.create_edge_adapter)
+    return registry
diff --git a/hugegraph-mcp/tests/test_ingest_graph_data.py b/hugegraph-mcp/tests/test_ingest_graph_data.py
index dfc7b54e..5e46d5bb 100644
--- a/hugegraph-mcp/tests/test_ingest_graph_data.py
+++ b/hugegraph-mcp/tests/test_ingest_graph_data.py
@@ -11,13 +11,10 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import json
 import re
 from unittest.mock import Mock
 
 import pytest
-
-from hugegraph_mcp import server
 from hugegraph_mcp.envelope import envelope_ok
 from hugegraph_mcp.tools import ingest_graph_data as ingest_graph_data_module
 from hugegraph_mcp.tools import manage_graph_data as manage_graph_data_module
@@ -64,9 +61,7 @@ def _live_schema():
 
 
 def _mock_schema(monkeypatch):
-    monkeypatch.setattr(
-        ingest_graph_data_module, "_fetch_live_schema", lambda: _live_schema()
-    )
+    monkeypatch.setattr(ingest_graph_data_module, "_fetch_live_schema", lambda: _live_schema())
 
 
 def test_ingest_graph_data_accepts_string_property_schema(monkeypatch):
@@ -114,12 +109,8 @@ def test_ingest_graph_data_dry_run_same_input_same_hash(monkeypatch):
     monkeypatch.setattr("hugegraph_mcp.plan_hash.time.time", lambda: 1000)
 
     # Same nonce + same payload + same expiry window = same hash.
-    first = ingest_graph_data_module.ingest_graph_data(
-        _graph_data(), nonce="fixed_nonce"
-    )
-    second = ingest_graph_data_module.ingest_graph_data(
-        _graph_data(), nonce="fixed_nonce"
-    )
+    first = ingest_graph_data_module.ingest_graph_data(_graph_data(), nonce="fixed_nonce")
+    second = ingest_graph_data_module.ingest_graph_data(_graph_data(), nonce="fixed_nonce")
 
     assert first["data"]["plan_hash"] == second["data"]["plan_hash"]
 
@@ -131,9 +122,7 @@ def test_ingest_graph_data_plan_hash_includes_schema(monkeypatch):
     schema_with_age_text["schema"]["propertykeys"][1]["data_type"] = "TEXT"
 
     first = ingest_graph_data_module.calculate_plan_hash(graph_data, schema)
-    second = ingest_graph_data_module.calculate_plan_hash(
-        graph_data, schema_with_age_text
-    )
+    second = ingest_graph_data_module.calculate_plan_hash(graph_data, schema_with_age_text)
 
     assert first != second
 
@@ -142,9 +131,7 @@ def test_ingest_plan_hash_schema_field_order_same_hash():
     graph_data = _graph_data()
     schema = _live_schema()
     reordered_schema = _live_schema()
-    reordered_schema["schema"]["propertykeys"] = list(
-        reversed(reordered_schema["schema"]["propertykeys"])
-    )
+    reordered_schema["schema"]["propertykeys"] = list(reversed(reordered_schema["schema"]["propertykeys"]))
     reordered_schema["schema"]["vertexlabels"][0]["properties"] = [
         {"name": "age"},
         {"name": "name"},
@@ -156,6 +143,27 @@ def test_ingest_plan_hash_schema_field_order_same_hash():
     assert first == second
 
 
+def test_ingest_plan_hash_composite_primary_key_vertex_order_same_hash():
+    schema = _live_schema()
+    schema["schema"]["vertexlabels"][0]["primary_keys"] = ["name", "age"]
+    graph_data = {
+        "vertices": [
+            {"label": "person", "properties": {"name": "Alice", "age": 42}},
+            {"label": "person", "properties": {"name": "Alice", "age": 31}},
+        ],
+        "edges": [],
+    }
+    reordered_graph_data = {
+        "vertices": list(reversed(graph_data["vertices"])),
+        "edges": [],
+    }
+
+    first = ingest_graph_data_module.calculate_plan_hash(graph_data, schema)
+    second = ingest_graph_data_module.calculate_plan_hash(reordered_graph_data, schema)
+
+    assert first == second
+
+
 def test_ingest_plan_hash_schema_primary_key_change_different_hash():
     graph_data = _graph_data()
     schema = _live_schema()
@@ -205,9 +213,7 @@ def test_ingest_plan_hash_schema_id_strategy_change_different_hash():
         ),
     ],
 )
-def test_validate_graph_payload_rejects_invalid_customize_id(
-    id_strategy, vertex_id, expected_error
-):
+def test_validate_graph_payload_rejects_invalid_customize_id(id_strategy, vertex_id, expected_error):
     schema = _live_schema()
     vertex_label = schema["schema"]["vertexlabels"][0]
     vertex_label["id_strategy"] = id_strategy
@@ -216,9 +222,7 @@ def test_validate_graph_payload_rejects_invalid_customize_id(
     if vertex_id is not None:
         vertex["id"] = vertex_id
 
-    result = ingest_graph_data_module.validate_graph_payload(
-        {"vertices": [vertex], "edges": []}, live_schema=schema
-    )
+    result = ingest_graph_data_module.validate_graph_payload({"vertices": [vertex], "edges": []}, live_schema=schema)
 
     assert result["valid"] is False
     assert any(expected_error in error for error in result["errors"])
@@ -235,15 +239,11 @@ def test_validate_graph_payload_rejects_invalid_customize_id(
 def test_validate_graph_payload_accepts_uuid_property(cardinality, value):
     schema = _live_schema()
     schema["schema"]["vertexlabels"][0]["properties"].append({"name": "uid"})
-    schema["schema"]["propertykeys"].append(
-        {"name": "uid", "data_type": "UUID", "cardinality": cardinality}
-    )
+    schema["schema"]["propertykeys"].append({"name": "uid", "data_type": "UUID", "cardinality": cardinality})
 
     result = ingest_graph_data_module.validate_graph_payload(
         {
-            "vertices": [
-                {"label": "person", "properties": {"name": "Alice", "uid": value}}
-            ],
+            "vertices": [{"label": "person", "properties": {"name": "Alice", "uid": value}}],
             "edges": [],
         },
         live_schema=schema,
@@ -255,14 +255,32 @@ def test_validate_graph_payload_accepts_uuid_property(cardinality, value):
 def test_validate_graph_payload_rejects_non_string_uuid():
     schema = _live_schema()
     schema["schema"]["vertexlabels"][0]["properties"].append({"name": "uid"})
-    schema["schema"]["propertykeys"].append(
-        {"name": "uid", "data_type": "UUID", "cardinality": "SINGLE"}
+    schema["schema"]["propertykeys"].append({"name": "uid", "data_type": "UUID", "cardinality": "SINGLE"})
+
+    result = ingest_graph_data_module.validate_graph_payload(
+        {
+            "vertices": [{"label": "person", "properties": {"name": "Alice", "uid": 1}}],
+            "edges": [],
+        },
+        live_schema=schema,
     )
 
+    assert result["valid"] is False
+    assert any("expects UUID, got int" in error for error in result["errors"])
+
+
+def test_validate_graph_payload_rejects_malformed_uuid():
+    schema = _live_schema()
+    schema["schema"]["vertexlabels"][0]["properties"].append({"name": "uid"})
+    schema["schema"]["propertykeys"].append({"name": "uid", "data_type": "UUID", "cardinality": "SINGLE"})
+
     result = ingest_graph_data_module.validate_graph_payload(
         {
             "vertices": [
-                {"label": "person", "properties": {"name": "Alice", "uid": 1}}
+                {
+                    "label": "person",
+                    "properties": {"name": "Alice", "uid": "not-a-uuid"},
+                }
             ],
             "edges": [],
         },
@@ -270,7 +288,49 @@ def test_validate_graph_payload_rejects_non_string_uuid():
     )
 
     assert result["valid"] is False
-    assert any("expects UUID, got int" in error for error in result["errors"])
+    assert any("expects UUID, got str" in error for error in result["errors"])
+
+
+def test_validate_graph_payload_rejects_out_of_range_integer():
+    schema = _live_schema()
+
+    result = ingest_graph_data_module.validate_graph_payload(
+        {
+            "vertices": [
+                {
+                    "label": "person",
+                    "properties": {"name": "Alice", "age": 2**31},
+                }
+            ],
+            "edges": [],
+        },
+        live_schema=schema,
+    )
+
+    assert result["valid"] is False
+    assert any("expects INT, got int" in error for error in result["errors"])
+
+
+def test_validate_graph_payload_rejects_double_overflow_without_raising():
+    schema = _live_schema()
+    schema["schema"]["vertexlabels"][0]["properties"].append({"name": "weight"})
+    schema["schema"]["propertykeys"].append({"name": "weight", "data_type": "DOUBLE", "cardinality": "SINGLE"})
+
+    result = ingest_graph_data_module.validate_graph_payload(
+        {
+            "vertices": [
+                {
+                    "label": "person",
+                    "properties": {"name": "Alice", "weight": 10**1000},
+                }
+            ],
+            "edges": [],
+        },
+        live_schema=schema,
+    )
+
+    assert result["valid"] is False
+    assert "vertex 0 property 'weight' expects DOUBLE, got int" in result["errors"]
 
 
 @pytest.mark.parametrize(
@@ -310,9 +370,7 @@ def test_ingest_plan_hash_schema_metadata_ignored_same_hash():
     schema_with_metadata["server_time"] = "2026-05-26T00:00:00Z"
 
     first = ingest_graph_data_module.calculate_plan_hash(graph_data, schema)
-    second = ingest_graph_data_module.calculate_plan_hash(
-        graph_data, schema_with_metadata
-    )
+    second = ingest_graph_data_module.calculate_plan_hash(graph_data, schema_with_metadata)
 
     assert first == second
 
@@ -357,9 +415,7 @@ def test_ingest_graph_data_validate_invalid(monkeypatch):
 def test_ingest_graph_data_rejects_when_live_schema_unavailable(monkeypatch):
     monkeypatch.setattr(ingest_graph_data_module, "_fetch_live_schema", lambda: None)
 
-    result = ingest_graph_data_module.ingest_graph_data(
-        {"vertices": [{"label": "x"}], "edges": []}
-    )
+    result = ingest_graph_data_module.ingest_graph_data({"vertices": [{"label": "x"}], "edges": []})
 
     assert result["ok"] is False
     assert result["error"]["type"] == "CONNECTION_FAILED"
@@ -385,9 +441,7 @@ def test_ingest_graph_data_schema_mismatch(monkeypatch):
 
     assert result["ok"] is False
     assert result["error"]["type"] == "SCHEMA_MISMATCH"
-    assert any(
-        "source_label 'ghost'" in e for e in result["error"]["details"]["errors"]
-    )
+    assert any("source_label 'ghost'" in e for e in result["error"]["details"]["errors"])
 
 
 def test_validate_graph_payload_rejects_labels_when_live_schema_is_empty():
@@ -435,9 +489,7 @@ def test_ingest_graph_data_rejects_property_type_mismatch(monkeypatch):
 
     assert result["ok"] is False
     assert result["error"]["type"] == "SCHEMA_MISMATCH"
-    assert any(
-        "property 'age' expects INT" in e for e in result["error"]["details"]["errors"]
-    )
+    assert any("property 'age' expects INT" in e for e in result["error"]["details"]["errors"])
 
 
 def _collection_schema():
@@ -502,9 +554,7 @@ def test_ingest_graph_data_accepts_vertex_list_and_edge_set_properties(monkeypat
     assert result["data"]["mutation_summary"] == {"vertices": 2, "edges": 1}
 
 
-@pytest.mark.parametrize(
-    "property_keys_field", ["propertykeys", "property_keys", "propertyKeys"]
-)
+@pytest.mark.parametrize("property_keys_field", ["propertykeys", "property_keys", "propertyKeys"])
 def test_validate_graph_payload_accepts_property_key_collection_aliases(
     property_keys_field,
 ):
@@ -549,10 +599,7 @@ def test_validate_graph_payload_preserves_top_level_none_but_rejects_none_elemen
 
     assert allowed["valid"] is True
     assert rejected["valid"] is False
-    assert (
-        "vertex 0 property 'aliases' element 1 expects TEXT, got NoneType"
-        in rejected["errors"]
-    )
+    assert "vertex 0 property 'aliases' element 1 expects TEXT, got NoneType" in rejected["errors"]
     assert all("Al" not in error for error in rejected["errors"])
 
 
@@ -563,14 +610,10 @@ def test_validate_graph_payload_preserves_top_level_none_but_rejects_none_elemen
         ("data_type", "DECIMAL", "unsupported data_type 'DECIMAL'"),
     ],
 )
-def test_validate_graph_payload_rejects_unsupported_property_spec(
-    field, value, expected
-):
+def test_validate_graph_payload_rejects_unsupported_property_spec(field, value, expected):
     schema = _collection_schema()
     aliases = next(
-        item
-        for item in schema["schema"]["propertykeys"]
-        if (item.get("name") or item.get("propertyName")) == "aliases"
+        item for item in schema["schema"]["propertykeys"] if (item.get("name") or item.get("propertyName")) == "aliases"
     )
     aliases[field] = value
     if field == "cardinality":
@@ -614,9 +657,7 @@ def test_validate_graph_payload_rejects_scalar_for_collection_property():
     )
 
     assert result["valid"] is False
-    assert (
-        "vertex 0 property 'aliases' expects LIST of TEXT, got str" in result["errors"]
-    )
+    assert "vertex 0 property 'aliases' expects LIST of TEXT, got str" in result["errors"]
 
 
 def test_validate_graph_payload_rejects_tuple_for_collection_property():
@@ -629,10 +670,7 @@ def test_validate_graph_payload_rejects_tuple_for_collection_property():
     )
 
     assert result["valid"] is False
-    assert (
-        "vertex 0 property 'aliases' expects LIST of TEXT, got tuple"
-        in result["errors"]
-    )
+    assert "vertex 0 property 'aliases' expects LIST of TEXT, got tuple" in result["errors"]
 
 
 def test_validate_graph_payload_rejects_invalid_collection_element_without_value():
@@ -659,9 +697,7 @@ def test_validate_graph_payload_rejects_bool_in_int_collection():
     )
 
     assert result["valid"] is False
-    assert (
-        "vertex 0 property 'scores' element 1 expects INT, got bool" in result["errors"]
-    )
+    assert "vertex 0 property 'scores' element 1 expects INT, got bool" in result["errors"]
 
 
 def test_validate_graph_payload_accepts_boolean_collection_and_rejects_wrong_element():
@@ -679,10 +715,7 @@ def test_validate_graph_payload_accepts_boolean_collection_and_rejects_wrong_ele
 
     assert valid["valid"] is True
     assert invalid["valid"] is False
-    assert (
-        "vertex 0 property 'flags' element 1 expects BOOLEAN, got int"
-        in invalid["errors"]
-    )
+    assert "vertex 0 property 'flags' element 1 expects BOOLEAN, got int" in invalid["errors"]
 
 
 def test_validate_graph_payload_rejects_list_for_single_property():
@@ -708,16 +741,25 @@ def test_manage_graph_data_import_accepts_collections_in_dry_run(monkeypatch):
     monkeypatch.setattr(
         manage_graph_data_module.gremlin_tools,
         "execute_gremlin_read",
-        lambda _query: {"data": [0], "total": 1, "is_read": True},
+        lambda query: {
+            "data": [] if ".id()" in query else [0],
+            "total": 0 if ".id()" in query else 1,
+            "is_read": True,
+        },
     )
 
+    graph_data = _collection_graph_data()
+    graph_data["vertices"][0]["id"] = "alice"
+    graph_data["vertices"][1]["id"] = "bob"
     result = manage_graph_data_module.manage_graph_data(
         mode="import",
-        graph_data=_collection_graph_data(),
+        graph_data=graph_data,
     )
 
     assert result["ok"] is True
-    assert result["data"]["confirmable"] is True
+    assert result["data"]["confirmable"] is False
+    assert "plan_id" not in result["data"]
+    assert len(result["data"]["workflow_preview"]["operations"]) == 3
     assert result["data"]["plan_hash"]
     assert result["data"]["mutation_summary"] == {
         "create_edge": 1,
@@ -725,7 +767,7 @@ def test_manage_graph_data_import_accepts_collections_in_dry_run(monkeypatch):
     }
 
 
-def test_public_import_graph_data_rejects_invalid_collection_before_execute(
+def test_manage_import_rejects_invalid_collection_before_execute(
     monkeypatch,
 ):
     graph_data = _collection_graph_data()
@@ -738,20 +780,14 @@ def test_public_import_graph_data_rejects_invalid_collection_before_execute(
     )
     monkeypatch.setattr(manage_graph_data_module, "execute_graph_change_plan", execute)
 
-    result = server.import_graph_data_tool(
-        mode="ingest",
+    result = manage_graph_data_module.manage_graph_data(
+        mode="import",
         graph_data=graph_data,
-        dry_run=False,
-        confirm=True,
     )
 
     assert result["ok"] is False
     assert result["error"]["type"] == "SCHEMA_MISMATCH"
-    assert result["error"]["source"] == "import_graph_data_tool"
-    assert (
-        "vertex 0 property 'aliases' expects LIST of TEXT, got str"
-        in result["error"]["details"]["errors"]
-    )
+    assert "vertex 0 property 'aliases' expects LIST of TEXT, got str" in result["error"]["details"]["errors"]
     execute.assert_not_called()
 
 
@@ -765,8 +801,7 @@ def test_ingest_graph_data_rejects_missing_schema_primary_key(monkeypatch):
     assert result["ok"] is False
     assert result["error"]["type"] == "SCHEMA_MISMATCH"
     assert any(
-        "vertex 0 missing primary key value for label 'person': name" in e
-        for e in result["error"]["details"]["errors"]
+        "vertex 0 missing primary key value for label 'person': name" in e for e in result["error"]["details"]["errors"]
     )
 
 
@@ -858,6 +893,26 @@ def test_validate_graph_payload_rejects_mixed_target_endpoint_forms():
     assert any("mixes target and inV endpoint forms" in e for e in result["errors"])
 
 
+def test_validate_graph_payload_rejects_edge_with_both_endpoints_missing():
+    result = ingest_graph_data_module.validate_graph_payload(
+        {
+            "vertices": [],
+            "edges": [
+                {
+                    "label": "knows",
+                    "source_label": "person",
+                    "target_label": "person",
+                }
+            ],
+        },
+        live_schema=_live_schema(),
+    )
+
+    assert result["valid"] is False
+    assert "edge 0 missing source endpoint" in result["errors"]
+    assert "edge 0 missing target endpoint" in result["errors"]
+
+
 def test_ingest_graph_data_rejects_edge_endpoint_missing_primary_key(monkeypatch):
     _mock_schema(monkeypatch)
 
@@ -913,9 +968,7 @@ def test_validate_graph_payload_rejects_ambiguous_scalar_endpoint():
     )
 
     assert result["valid"] is False
-    assert any(
-        "source scalar endpoint is ambiguous" in error for error in result["errors"]
-    )
+    assert any("source scalar endpoint is ambiguous" in error for error in result["errors"])
 
 
 def test_ingest_graph_data_dry_run_rejects_too_many_operations_before_plan(
@@ -923,9 +976,7 @@ def test_ingest_graph_data_dry_run_rejects_too_many_operations_before_plan(
 ):
     _mock_schema(monkeypatch)
     graph_data = {
-        "vertices": [
-            {"label": "person", "properties": {"name": f"n{idx}"}} for idx in range(201)
-        ],
+        "vertices": [{"label": "person", "properties": {"name": f"n{idx}"}} for idx in range(201)],
         "edges": [],
     }
 
@@ -1020,9 +1071,7 @@ def test_ingest_graph_data_rejects_edge_label_mismatch(monkeypatch):
     errors = result["error"]["details"]["errors"]
     assert any("edge 0 label 'likes' does not exist in schema" in e for e in errors)
     assert any("edge 1 target_label 'ghost'" in e for e in errors)
-    assert any(
-        "does not match edge label 'knows' target_label 'person'" in e for e in errors
-    )
+    assert any("does not match edge label 'knows' target_label 'person'" in e for e in errors)
 
 
 def test_ingest_graph_data_warns_for_labels_without_schema_index(monkeypatch):
@@ -1035,9 +1084,7 @@ def test_ingest_graph_data_warns_for_labels_without_schema_index(monkeypatch):
     result = ingest_graph_data_module.ingest_graph_data(_graph_data())
 
     assert result["ok"] is True
-    assert (
-        "no edge index found in schema for label: knows" in result["data"]["warnings"]
-    )
+    assert "no edge index found in schema for label: knows" in result["data"]["warnings"]
 
 
 def test_ingest_graph_data_missing_confirm(monkeypatch):
@@ -1071,7 +1118,7 @@ def test_ingest_graph_data_plan_hash_mismatch(monkeypatch):
     )
 
     assert result["ok"] is False
-    assert result["error"]["type"] == "PLAN_HASH_MISMATCH"
+    assert result["error"]["type"] == "FEATURE_DISABLED"
 
 
 def test_ingest_graph_data_plan_hash_expired(monkeypatch):
@@ -1091,7 +1138,7 @@ def test_ingest_graph_data_plan_hash_expired(monkeypatch):
     )
 
     assert result["ok"] is False
-    assert result["error"]["type"] == "PLAN_EXPIRED"
+    assert result["error"]["type"] == "FEATURE_DISABLED"
 
 
 def test_ingest_graph_data_readonly(monkeypatch):
@@ -1113,7 +1160,7 @@ def test_ingest_graph_data_readonly_preview_does_not_issue_plan(monkeypatch):
     _mock_schema(monkeypatch)
     monkeypatch.setenv("HUGEGRAPH_MCP_READONLY", "true")
     issue = Mock()
-    monkeypatch.setattr(ingest_graph_data_module, "issue_plan", issue)
+    monkeypatch.setattr(ingest_graph_data_module, "issue_plan", issue, raising=False)
 
     result = ingest_graph_data_module.ingest_graph_data(_graph_data())
 
@@ -1126,12 +1173,8 @@ def test_ingest_graph_data_readonly_preview_does_not_issue_plan(monkeypatch):
 def test_ingest_graph_data_success(monkeypatch):
     _mock_schema(monkeypatch)
     monkeypatch.setenv("HUGEGRAPH_MCP_READONLY", "false")
-    post = Mock(
-        return_value=envelope_ok(
-            {"ok": True, "data": {"written": {"vertices": 2, "edges": 1}}}
-        )
-    )
-    monkeypatch.setattr(ingest_graph_data_module, "post", post)
+    post = Mock(return_value=envelope_ok({"ok": True, "data": {"written": {"vertices": 2, "edges": 1}}}))
+    monkeypatch.setattr(ingest_graph_data_module, "post", post, raising=False)
     graph_data = _graph_data()
     dry_run = ingest_graph_data_module.ingest_graph_data(graph_data)
 
@@ -1146,37 +1189,18 @@ def test_ingest_graph_data_success(monkeypatch):
         expires_at=plan_ctx["expires_at"],
     )
 
-    assert result["ok"] is True
-    assert result["data"]["batch_id"].startswith("batch-")
-    assert result["data"]["status"] == "success"
-    assert result["data"]["planned"] == {"vertices": 2, "edges": 1}
-    assert result["data"]["written"] == {"vertices": 2, "edges": 1}
-    post.assert_called_once()
-    assert post.call_args.args == ("/graph-import",)
-    assert post.call_args.kwargs["json"]["schema"] == "hugegraph"
-    import_payload = json.loads(post.call_args.kwargs["json"]["data"])
-    assert import_payload["vertices"][0]["id"] == "1:Alice"
-    assert import_payload["vertices"][1]["id"] == "1:Bob"
-    assert import_payload["edges"][0]["outV"] == "1:Alice"
-    assert import_payload["edges"][0]["outVLabel"] == "person"
-    assert import_payload["edges"][0]["inV"] == "1:Bob"
-    assert import_payload["edges"][0]["inVLabel"] == "person"
-    assert import_payload["edges"][0]["properties"] == {}
+    assert result["ok"] is False
+    assert result["error"]["type"] == "FEATURE_DISABLED"
+    post.assert_not_called()
 
 
 def test_ingest_graph_data_replayed_confirmation_does_not_post_twice(monkeypatch):
     _mock_schema(monkeypatch)
     monkeypatch.setenv("HUGEGRAPH_MCP_READONLY", "false")
-    post = Mock(
-        return_value=envelope_ok(
-            {"ok": True, "data": {"written": {"vertices": 2, "edges": 1}}}
-        )
-    )
-    monkeypatch.setattr(ingest_graph_data_module, "post", post)
+    post = Mock(return_value=envelope_ok({"ok": True, "data": {"written": {"vertices": 2, "edges": 1}}}))
+    monkeypatch.setattr(ingest_graph_data_module, "post", post, raising=False)
     graph_data = _graph_data()
-    dry_run = ingest_graph_data_module.ingest_graph_data(
-        graph_data, nonce="ingest-replay"
-    )
+    dry_run = ingest_graph_data_module.ingest_graph_data(graph_data, nonce="ingest-replay")
     context = dry_run["data"]["plan_context"]
     arguments = {
         "graph_data": graph_data,
@@ -1190,23 +1214,20 @@ def test_ingest_graph_data_replayed_confirmation_does_not_post_twice(monkeypatch
     first = ingest_graph_data_module.ingest_graph_data(**arguments)
     second = ingest_graph_data_module.ingest_graph_data(**arguments)
 
-    assert first["ok"] is True
+    assert first["ok"] is False
+    assert first["error"]["type"] == "FEATURE_DISABLED"
     assert second["ok"] is False
-    assert second["error"]["type"] == "PLAN_ALREADY_USED"
-    assert "already been used" in second["error"]["message"]
-    assert "Inspect the current target state" in second["error"]["suggestion"]
-    post.assert_called_once()
+    assert second["error"]["type"] == "FEATURE_DISABLED"
+    post.assert_not_called()
 
 
 def test_ingest_partial_apply_consumes_confirmation(monkeypatch):
     _mock_schema(monkeypatch)
     monkeypatch.setenv("HUGEGRAPH_MCP_READONLY", "false")
     post = Mock(return_value=envelope_ok({"inserted": 2}))
-    monkeypatch.setattr(ingest_graph_data_module, "post", post)
+    monkeypatch.setattr(ingest_graph_data_module, "post", post, raising=False)
     graph_data = _graph_data()
-    dry_run = ingest_graph_data_module.ingest_graph_data(
-        graph_data, nonce="ingest-partial"
-    )
+    dry_run = ingest_graph_data_module.ingest_graph_data(graph_data, nonce="ingest-partial")
     context = dry_run["data"]["plan_context"]
     arguments = {
         "graph_data": graph_data,
@@ -1221,18 +1242,19 @@ def test_ingest_partial_apply_consumes_confirmation(monkeypatch):
     replay = ingest_graph_data_module.ingest_graph_data(**arguments)
 
     assert partial["ok"] is False
-    assert partial["error"]["details"]["status"] == "partial"
+    assert partial["error"]["type"] == "FEATURE_DISABLED"
     assert partial["error"]["retryable"] is False
     assert replay["ok"] is False
-    assert replay["error"]["type"] == "PLAN_ALREADY_USED"
-    post.assert_called_once()
+    assert replay["error"]["type"] == "FEATURE_DISABLED"
+    assert replay["error"]["retryable"] is False
+    post.assert_not_called()
 
 
 def test_ingest_graph_data_degrades_when_ai_omits_counts(monkeypatch):
     _mock_schema(monkeypatch)
     monkeypatch.setenv("HUGEGRAPH_MCP_READONLY", "false")
     post = Mock(return_value=envelope_ok({"message": "import finished"}))
-    monkeypatch.setattr(ingest_graph_data_module, "post", post)
+    monkeypatch.setattr(ingest_graph_data_module, "post", post, raising=False)
     graph_data = _graph_data()
     dry_run = ingest_graph_data_module.ingest_graph_data(graph_data)
     plan_ctx = dry_run["data"]["plan_context"]
@@ -1247,19 +1269,16 @@ def test_ingest_graph_data_degrades_when_ai_omits_counts(monkeypatch):
     )
 
     assert result["ok"] is False
-    assert result["error"]["type"] == "FLOW_EXECUTION_FAILED"
-    details = result["error"]["details"]
-    assert details["status"] == "degraded"
-    assert details["written"] == {"vertices": 0, "edges": 0}
+    assert result["error"]["type"] == "FEATURE_DISABLED"
     assert result["error"]["retryable"] is False
-    assert any("write outcome is unknown" in w for w in result["warnings"])
+    post.assert_not_called()
 
 
 def test_ingest_graph_data_splits_total_written_count(monkeypatch):
     _mock_schema(monkeypatch)
     monkeypatch.setenv("HUGEGRAPH_MCP_READONLY", "false")
     post = Mock(return_value=envelope_ok({"inserted": 2}))
-    monkeypatch.setattr(ingest_graph_data_module, "post", post)
+    monkeypatch.setattr(ingest_graph_data_module, "post", post, raising=False)
     graph_data = _graph_data()
     dry_run = ingest_graph_data_module.ingest_graph_data(graph_data)
     plan_ctx = dry_run["data"]["plan_context"]
@@ -1274,16 +1293,15 @@ def test_ingest_graph_data_splits_total_written_count(monkeypatch):
     )
 
     assert result["ok"] is False
-    assert result["error"]["details"]["status"] == "partial"
-    assert result["error"]["details"]["written"] == {"vertices": 2, "edges": 0}
-    assert any("total written count" in w for w in result["warnings"])
+    assert result["error"]["type"] == "FEATURE_DISABLED"
+    post.assert_not_called()
 
 
 def test_ingest_graph_data_does_not_promote_ai_failure_without_counts(monkeypatch):
     _mock_schema(monkeypatch)
     monkeypatch.setenv("HUGEGRAPH_MCP_READONLY", "false")
     post = Mock(return_value=envelope_ok({"success": False, "status": "partial"}))
-    monkeypatch.setattr(ingest_graph_data_module, "post", post)
+    monkeypatch.setattr(ingest_graph_data_module, "post", post, raising=False)
     graph_data = _graph_data()
     dry_run = ingest_graph_data_module.ingest_graph_data(graph_data)
     plan_ctx = dry_run["data"]["plan_context"]
@@ -1298,15 +1316,50 @@ def test_ingest_graph_data_does_not_promote_ai_failure_without_counts(monkeypatc
     )
 
     assert result["ok"] is False
-    assert result["error"]["details"]["status"] == "error"
-    assert result["error"]["details"]["written"] == {"vertices": 0, "edges": 0}
+    assert result["error"]["type"] == "FEATURE_DISABLED"
+    post.assert_not_called()
+
+
+def test_ingest_graph_data_maps_proven_zero_writes_to_rejected(monkeypatch):
+    _mock_schema(monkeypatch)
+    monkeypatch.setenv("HUGEGRAPH_MCP_READONLY", "false")
+    post = Mock(
+        return_value=envelope_ok(
+            {
+                "success": False,
+                "written": {"vertices": 0, "edges": 0},
+            }
+        )
+    )
+    monkeypatch.setattr(
+        ingest_graph_data_module,
+        "post",
+        post,
+        raising=False,
+    )
+    graph_data = _graph_data()
+    dry_run = ingest_graph_data_module.ingest_graph_data(graph_data)
+    context = dry_run["data"]["plan_context"]
+
+    result = ingest_graph_data_module.ingest_graph_data(
+        graph_data,
+        dry_run=False,
+        confirm=True,
+        plan_hash=dry_run["data"]["plan_hash"],
+        nonce=context["nonce"],
+        expires_at=context["expires_at"],
+    )
+
+    assert result["ok"] is False
+    assert result["error"]["type"] == "FEATURE_DISABLED"
+    post.assert_not_called()
 
 
 def test_ingest_graph_data_does_not_promote_failed_items_without_counts(monkeypatch):
     _mock_schema(monkeypatch)
     monkeypatch.setenv("HUGEGRAPH_MCP_READONLY", "false")
     post = Mock(return_value=envelope_ok({"failed_items": [{"index": 0}]}))
-    monkeypatch.setattr(ingest_graph_data_module, "post", post)
+    monkeypatch.setattr(ingest_graph_data_module, "post", post, raising=False)
     graph_data = _graph_data()
     dry_run = ingest_graph_data_module.ingest_graph_data(graph_data)
     plan_ctx = dry_run["data"]["plan_context"]
@@ -1321,5 +1374,5 @@ def test_ingest_graph_data_does_not_promote_failed_items_without_counts(monkeypa
     )
 
     assert result["ok"] is False
-    assert result["error"]["details"]["status"] == "degraded"
-    assert result["error"]["details"]["failed_items"] == [{"index": 0}]
+    assert result["error"]["type"] == "FEATURE_DISABLED"
+    post.assert_not_called()
diff --git a/hugegraph-mcp/tests/test_manage_graph_data.py b/hugegraph-mcp/tests/test_manage_graph_data.py
index be0d4d57..a976b7a2 100644
--- a/hugegraph-mcp/tests/test_manage_graph_data.py
+++ b/hugegraph-mcp/tests/test_manage_graph_data.py
@@ -19,6 +19,9 @@ from hugegraph_mcp.tools import manage_graph_data as manage_graph_data_module
 from hugegraph_mcp.tools.graph_data_gremlin import (
     _create_edge_query,
     _create_vertex_query,
+    _delete_edge_query,
+    _delete_target_query,
+    _delete_vertex_query,
     _g,
 )
 
@@ -113,6 +116,46 @@ def test_validate_graph_change_plan_rejects_unknown_op():
     assert "unsupported op" in result["errors"][0]["reason"]
 
 
+def test_create_vertex_plan_uses_shared_property_type_validation():
+    result = manage_graph_data_module.validate_graph_change_plan(
+        {
+            "operations": [
+                {
+                    "op": "create_vertex",
+                    "label": "person",
+                    "properties": {"name": "Alice", "age": 2**31},
+                }
+            ]
+        },
+        _live_schema(),
+    )
+
+    assert result["valid"] is False
+    assert "vertex 0 property 'age' expects INT, got int" in result["errors"][0]["reason"]
+
+
+def test_create_edge_plan_uses_shared_property_type_validation():
+    result = manage_graph_data_module.validate_graph_change_plan(
+        {
+            "operations": [
+                {
+                    "op": "create_edge",
+                    "label": "knows",
+                    "source_label": "person",
+                    "source_match": {"name": "Alice"},
+                    "target_label": "person",
+                    "target_match": {"name": "Bob"},
+                    "properties": {"since": "recently"},
+                }
+            ]
+        },
+        _live_schema(),
+    )
+
+    assert result["valid"] is False
+    assert "edge 0 property 'since' expects INT, got str" in result["errors"][0]["reason"]
+
+
 def test_public_import_dry_run_rejects_customize_string_vertex_without_id(
     monkeypatch,
 ):
@@ -125,9 +168,7 @@ def test_public_import_dry_run_rejects_customize_string_vertex_without_id(
     result = manage_graph_data_module.manage_graph_data(
         mode="import",
         graph_data={
-            "vertices": [
-                {"label": "person", "properties": {"name": "Alice", "age": 30}}
-            ],
+            "vertices": [{"label": "person", "properties": {"name": "Alice", "age": 30}}],
             "edges": [],
         },
         dry_run=True,
@@ -135,9 +176,7 @@ def test_public_import_dry_run_rejects_customize_string_vertex_without_id(
 
     assert result["ok"] is False
     assert result["error"]["type"] == "SCHEMA_MISMATCH"
-    assert result["error"]["details"]["errors"] == [
-        "vertex 0 missing required id for CUSTOMIZE_STRING label 'person'"
-    ]
+    assert result["error"]["details"]["errors"] == ["vertex 0 missing required id for CUSTOMIZE_STRING label 'person'"]
 
 
 def test_public_import_dry_run_rejects_customize_number_vertex_with_bool_id(
@@ -234,23 +273,22 @@ def test_validate_edge_rejects_unknown_endpoint_label():
     )
 
     assert result["valid"] is False
-    assert any(
-        "source_label references undefined" in e["reason"] for e in result["errors"]
-    )
+    assert any("source_label references undefined" in e["reason"] for e in result["errors"])
 
 
 def test_dry_run_delete_edge_rejects_non_single_match(monkeypatch):
-    counts = iter([1, 1, 2])
+    def fake_read(query):
+        return {
+            "data": ["edge-1", "edge-2"] if query.endswith(".id()") else [1],
+            "total": 1,
+            "duration_ms": 1,
+            "is_read": True,
+        }
 
     monkeypatch.setattr(
         manage_graph_data_module.gremlin_tools,
         "execute_gremlin_read",
-        lambda _query: {
-            "data": [next(counts)],
-            "total": 1,
-            "duration_ms": 1,
-            "is_read": True,
-        },
+        fake_read,
     )
 
     result = manage_graph_data_module.dry_run_graph_change_plan(
@@ -264,20 +302,17 @@ def test_dry_run_delete_edge_rejects_non_single_match(monkeypatch):
 
 def test_dry_run_delete_edge_returns_preview_and_hash(monkeypatch):
... 5451 lines suppressed ...