diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,28 @@
 # Changelog for clickhouse-migrate
 
+## 0.2.0.0 — 2026-11-08
+
+### Breaking Changes
+
+- Add public checksum, target-error, transport-error, ledger-error, history and
+  run-error constructors; update exhaustive matches for replicated states.
+- Add checksum v2, SQL envelope v2 and replicated ledger initialization and
+  completion policies. Replicated targets require v2; direct targets retain v1.
+
+### New Features
+
+- Validate single-shard replica-set targets and replica identity on new connections.
+- Support replicated MergeTree SQL, topology-specific preflight, structurally
+  validated replicated ledgers and full-replica quorum completion.
+- Expose topology and per-format checksum accessors.
+- Admit exact Altinity `26.3.60.10001.altinitystable` and
+  `26.3.60.10002.altinitystable` Linux arm64 engines.
+
+### Other Changes
+
+- Centralize server policy and add checksum v2 reference vectors, public API
+  checks and replicated contract documentation.
+
 ## 0.1.0.0 — 2026-10-16
 
 Initial release.
diff --git a/clickhouse-migrate.cabal b/clickhouse-migrate.cabal
--- a/clickhouse-migrate.cabal
+++ b/clickhouse-migrate.cabal
@@ -1,6 +1,6 @@
 cabal-version: 3.12
 name: clickhouse-migrate
-version: 0.1.0.0
+version: 0.2.0.0
 synopsis: Core definitions and execution for ClickHouse migrations
 description:
   Core definitions and execution for ClickHouse migrations.
@@ -92,6 +92,7 @@
     ClickHouseMigrate.Internal.Reporting
     ClickHouseMigrate.Internal.RuntimeTypes
     ClickHouseMigrate.Internal.ServerPolicy
+    ClickHouseMigrate.Internal.ServerProfile
     ClickHouseMigrate.Internal.SqlEnvelope
     ClickHouseMigrate.Internal.TargetTypes
     ClickHouseMigrate.Internal.Transport
@@ -138,6 +139,7 @@
     ClickHouseMigrate.Internal.Reporting
     ClickHouseMigrate.Internal.RuntimeTypes
     ClickHouseMigrate.Internal.ServerPolicy
+    ClickHouseMigrate.Internal.ServerProfile
     ClickHouseMigrate.Internal.SqlEnvelope
     ClickHouseMigrate.Internal.TargetTypes
     ClickHouseMigrate.Internal.Transport
@@ -190,6 +192,7 @@
     ClickHouseMigrate.Internal.Reporting
     ClickHouseMigrate.Internal.RuntimeTypes
     ClickHouseMigrate.Internal.ServerPolicy
+    ClickHouseMigrate.Internal.ServerProfile
     ClickHouseMigrate.Internal.SqlEnvelope
     ClickHouseMigrate.Internal.TargetTypes
     ClickHouseMigrate.Internal.Transport
diff --git a/src/ClickHouseMigrate/Adoption.hs b/src/ClickHouseMigrate/Adoption.hs
--- a/src/ClickHouseMigrate/Adoption.hs
+++ b/src/ClickHouseMigrate/Adoption.hs
@@ -61,7 +61,7 @@
 resumeAdoption = executeAdoption True
 
 executeAdoption :: Bool -> MutationContext -> Plan.MigrationPlan -> AdoptionPlan -> IO (Either R.RunFailure R.RunReport)
-executeAdoption resume context@(MutationContext (T.TargetConfig actual _ _ _ binding _ _) _ _ _) plan (AdoptionPlan request@(AdoptionRequest target _ authorization quiet audit _ _ _ _) batch entries) =
+executeAdoption resume context@(MutationContext (T.TargetConfig {T.configKey = actual, T.configBinding = binding, T.configTopology = topology}) _ _ _) plan (AdoptionPlan request@(AdoptionRequest target _ authorization quiet audit _ _ _ _) batch entries) =
   let validate snapshot = either (Left . R.AdoptionRejected) Right $ do
         (current, definitions) <- Validation.buildBatch plan request
         Validation.require MappingDefinitionMismatch (current == batch && definitions == entries)
@@ -74,7 +74,9 @@
           R.HistoryReport _ _ presence _ _ _ _ _ | presence `elem` [R.LedgerMissing, R.LedgerUninitialized] -> Validation.require AdoptionInitializationRequired (binding == T.FirstUse && isJust authorization)
           _ -> pure ()
         pure committed
-      prerequisite = if target == actual then Right () else Left (R.AdoptionRejected AdoptionTargetMismatch)
+      prerequisite = case topology of
+        T.ReplicatedCluster _ _ -> Left (R.LedgerFailed L.UnsupportedLedgerVersion)
+        T.DirectServer _ -> if target == actual then Right () else Left (R.AdoptionRejected AdoptionTargetMismatch)
    in withRuntimeObservation prerequisite (Just validate) context audit authorization (Just quiet) plan $ \runtime -> do
         projection <- runtimeProjection runtime
         case L.adoption projection of
diff --git a/src/ClickHouseMigrate/Inspection.hs b/src/ClickHouseMigrate/Inspection.hs
--- a/src/ClickHouseMigrate/Inspection.hs
+++ b/src/ClickHouseMigrate/Inspection.hs
@@ -16,6 +16,7 @@
 import ClickHouseMigrate.Internal.Reporting qualified as Reporting
 import ClickHouseMigrate.Internal.RuntimeTypes (InspectionOptions (..), VerifyOptions (..))
 import ClickHouseMigrate.Internal.ServerPolicy (TargetCheckError (..), withTransportChecked)
+import ClickHouseMigrate.Internal.ServerProfile qualified as Profile
 import ClickHouseMigrate.Internal.TargetTypes (QueryId (..), TargetConfig (..))
 import ClickHouseMigrate.Internal.Transport qualified as Transport
 import ClickHouseMigrate.Plan (MigrationPlan)
@@ -28,7 +29,7 @@
 verifyOptions = VerifyOptions
 
 history :: InspectionOptions -> TargetConfig -> IO (Either R.InspectionFailure R.HistoryReport)
-history options config@(TargetConfig target _ _ _ _ _ _) = do
+history options config@(TargetConfig {configKey = target}) = do
   latest <- newIORef Nothing
   observed <- try @SomeException $ do
     probe <- QueryId . ("chm-" <>) <$> freshUuid
@@ -64,7 +65,7 @@
     Right (Right report, []) -> pure (Right report)
 
 status :: InspectionOptions -> TargetConfig -> MigrationPlan -> IO (Either R.InspectionFailure R.InspectionReport)
-status options config plan = fmap (Reporting.inspectHistory plan) <$> history options config
+status options config plan = fmap (Reporting.inspectHistoryFor (Profile.checksumFormat (configTopology config)) plan) <$> history options config
 
 verify :: InspectionOptions -> TargetConfig -> VerifyOptions -> MigrationPlan -> IO (Either R.InspectionFailure R.VerificationReport)
 verify options config (VerifyOptions requireCurrent) plan = do
diff --git a/src/ClickHouseMigrate/Internal/Checksum.hs b/src/ClickHouseMigrate/Internal/Checksum.hs
--- a/src/ClickHouseMigrate/Internal/Checksum.hs
+++ b/src/ClickHouseMigrate/Internal/Checksum.hs
@@ -2,12 +2,15 @@
 module ClickHouseMigrate.Internal.Checksum
   ( completionPolicyV1,
     encodeStepV1,
+    encodeStepV2,
     encodeMigrationV1,
+    encodeMigrationV2,
     digest,
     digestHex,
   )
 where
 
+import ClickHouseMigrate.Internal.ServerProfile qualified as Profile
 import ClickHouseMigrate.Prelude
 import Crypto.Hash.SHA256 qualified as SHA256
 import Data.ByteString qualified as BS
@@ -18,7 +21,7 @@
 import Numeric (showHex)
 
 completionPolicyV1 :: ByteString
-completionPolicyV1 = "clickhouse-migrate/local-sync-v1;async_insert=0;mutations_sync=1;alter_sync=1"
+completionPolicyV1 = Profile.completionPolicyV1
 
 field :: ByteString -> Builder
 field bs = word64BE (fromIntegral (BS.length bs)) <> byteString bs
@@ -38,6 +41,25 @@
     toLazyByteString $
       byteString "clickhouse-migrate/migration\0v1\0"
         <> field completionPolicyV1
+        <> foldMap (field . Text.encodeUtf8) [component, migration]
+        <> word64BE (fromIntegral (length steps))
+        <> foldMap field steps
+
+encodeStepV2 :: Text -> Text -> Text -> ByteString -> ByteString
+encodeStepV2 component migration step sql =
+  Lazy.toStrict $
+    toLazyByteString $
+      byteString "clickhouse-migrate/step\0v2\0"
+        <> field Profile.completionPolicyV2
+        <> foldMap (field . Text.encodeUtf8) [component, migration, step]
+        <> field sql
+
+encodeMigrationV2 :: Text -> Text -> [ByteString] -> ByteString
+encodeMigrationV2 component migration steps =
+  Lazy.toStrict $
+    toLazyByteString $
+      byteString "clickhouse-migrate/migration\0v2\0"
+        <> field Profile.completionPolicyV2
         <> foldMap (field . Text.encodeUtf8) [component, migration]
         <> word64BE (fromIntegral (length steps))
         <> foldMap field steps
diff --git a/src/ClickHouseMigrate/Internal/Execution.hs b/src/ClickHouseMigrate/Internal/Execution.hs
--- a/src/ClickHouseMigrate/Internal/Execution.hs
+++ b/src/ClickHouseMigrate/Internal/Execution.hs
@@ -6,14 +6,15 @@
 import ClickHouseMigrate.Internal.Exclusive (checkGuard, guardDescription, withExclusive)
 import ClickHouseMigrate.Internal.ExclusiveTypes (AccessDescription (..), AccessResult (..), ExclusiveAccess (..), Guard)
 import ClickHouseMigrate.Internal.Identity (diagnosticTime, freshUuid)
-import ClickHouseMigrate.Internal.LedgerCodec (contextFor, definitions)
+import ClickHouseMigrate.Internal.LedgerCodec (contextFor, definitionsFor)
 import ClickHouseMigrate.Internal.LedgerStore qualified as Store
 import ClickHouseMigrate.Internal.LedgerTypes qualified as L
-import ClickHouseMigrate.Internal.Preflight (preflight, validateRecoveryEvent)
+import ClickHouseMigrate.Internal.Preflight (preflight, preflightPending, validateRecoveryEvent)
 import ClickHouseMigrate.Internal.Projection (appendProjection)
 import ClickHouseMigrate.Internal.Reporting qualified as Reporting
 import ClickHouseMigrate.Internal.RuntimeTypes
 import ClickHouseMigrate.Internal.ServerPolicy (TargetCheckError (..), withTransportChecked)
+import ClickHouseMigrate.Internal.ServerProfile qualified as Profile
 import ClickHouseMigrate.Internal.TargetTypes qualified as T
 import ClickHouseMigrate.Internal.Transport qualified as Transport
 import ClickHouseMigrate.Ledger qualified as Ledger
@@ -59,10 +60,11 @@
 -- Adoption validates its fresh read before openStore may initialize anything.
 -- Ordinary execution retains its existing single-load path.
 withRuntimeObservation :: Either R.RunError () -> Maybe (R.HistoryReport -> Either R.RunError Bool) -> MutationContext -> L.AuditContext -> Maybe L.InitializationAuthorization -> Maybe L.QuiescenceEvidence -> Plan.MigrationPlan -> (Runtime -> IO ()) -> IO (Either R.RunFailure R.RunReport)
-withRuntimeObservation prerequisite validateObservation context@(MutationContext config@(T.TargetConfig target _ _ _ _ _ _) provider@(ExclusiveAccess description _ _ _ _) _ _) audit initialization quiet plan action = do
+withRuntimeObservation prerequisite validateObservation context@(MutationContext config@(T.TargetConfig {T.configKey = target}) provider@(ExclusiveAccess description _ _ _ _) _ _) audit initialization quiet plan action = do
   seed <- try @SomeException freshUuid
-  let identity = either (const "00000000-0000-4000-8000-000000000000") id seed
-  latest <- newIORef (Reporting.initialRun identity target description plan)
+  let format = Profile.checksumFormat (T.configTopology config)
+      identity = either (const "00000000-0000-4000-8000-000000000000") id seed
+  latest <- newIORef (Reporting.initialRunFor format identity target description plan)
   secondary <- newIORef []
   result <- case seed of
     Left exception | isAsync exception -> throwIO exception
@@ -89,7 +91,7 @@
               modifyIORef'
                 latest
                 ( \report -> case Reporting.runHistory report of
-                    R.HistoryReport key _ presence position access events migrations unresolved -> Reporting.updateRun plan (R.HistoryReport key server presence position access events migrations unresolved) report
+                    R.HistoryReport key _ presence position access events migrations unresolved -> Reporting.updateRunFor format plan (R.HistoryReport key server presence position access events migrations unresolved) report
                 )
               let AccessDescription strategy scope = guardDescription guard
                   -- The caller's coordinator contract is the authority for a new
@@ -101,25 +103,29 @@
                 Just validate -> do
                   observation <- callStore (Store.load (Just guard) transport config)
                   let snapshot = Reporting.observationReport target server (R.GuardedObservation (guardDescription guard)) observation
-                  modifyIORef' latest (Reporting.updateRun plan snapshot)
+                  modifyIORef' latest (Reporting.updateRunFor format plan snapshot)
                   either abort pure (validate snapshot)
               if observationOnly
                 then do
                   emitAt context identity latest Event.RunCompleted Nothing Nothing Nothing
                   callStore (Store.checkAccess guard)
                 else do
+                  when (Profile.requiresLedgerDatabase (T.configTopology config)) $ do
+                    before <- callStore (Store.load (Just guard) transport config)
+                    let applied = case before of Store.History projection -> length (L.completed projection); _ -> 0
+                    either abort pure (preflightPending config plan applied)
                   opened <- Store.storeResult (Store.openStore guard transport config initialization established identity)
                   store <- case opened of
-                    Left (Store.LedgerProblem L.InitializationNotAuthorized) | T.TargetConfig _ _ _ _ (T.ExistingLedger _) _ _ <- config, isNothing initialization -> abort R.LedgerRequired
+                    Left (Store.LedgerProblem L.InitializationNotAuthorized) | T.TargetConfig {T.configBinding = T.ExistingLedger _} <- config, isNothing initialization -> abort R.LedgerRequired
                     Left failure -> abort (Reporting.storeError failure)
                     Right value -> pure value
                   let runtime = Runtime context audit plan guard transport store identity latest secondary
                   refresh runtime
                   case (config, initialization) of
-                    (T.TargetConfig _ _ _ _ T.FirstUse _ _, Just (L.InitializationAuthorization _ ledger _)) -> modifyIORef' latest (\report -> report {R.initialized = Just ledger})
+                    (T.TargetConfig {T.configBinding = T.FirstUse}, Just (L.InitializationAuthorization _ ledger _)) -> modifyIORef' latest (\report -> report {R.initialized = Just ledger})
                     _ -> pure ()
                   report <- readIORef latest
-                  let R.InspectionReport _ _ issues = Reporting.inspectHistory plan (Reporting.runHistory report)
+                  let R.InspectionReport _ _ issues = Reporting.inspectHistoryFor format plan (Reporting.runHistory report)
                       changes = [issue | issue@R.DefinitionChanged {} <- issues]
                   unless (null changes) (abort (R.IncompatibleHistory changes))
                   action runtime
@@ -168,7 +174,7 @@
 refresh runtime@(Runtime _ _ plan guard transport _ _ latest _) = do
   projection <- runtimeProjection runtime
   let snapshot = Reporting.fromProjection (Just (Transport.transportIdentity transport)) (R.GuardedObservation (guardDescription guard)) projection
-  modifyIORef' latest (Reporting.updateRun plan snapshot)
+  modifyIORef' latest (Reporting.updateRunFor (L.projectionChecksumFormat projection) plan snapshot)
 
 emitAt :: MutationContext -> Text -> IORef R.RunReport -> Event.EventPhase -> Maybe L.Definition -> Maybe Int -> Maybe T.QueryId -> IO ()
 emitAt (MutationContext _ _ sink _) identity latest phase definition index query = do
@@ -276,5 +282,5 @@
   appendEvent runtime (Just attempt) L.CompleteMigration context
   emit runtime Event.MigrationCompleted (Just definition) Nothing Nothing
 
-orderedMigrations :: Plan.MigrationPlan -> [(L.Definition, Plan.MigrationView)]
-orderedMigrations plan = zip (definitions plan) (NE.toList (Plan.inspectSchedule plan))
+orderedMigrations :: Plan.ChecksumFormat -> Plan.MigrationPlan -> [(L.Definition, Plan.MigrationView)]
+orderedMigrations format plan = zip (definitionsFor format plan) (NE.toList (Plan.inspectSchedule plan))
diff --git a/src/ClickHouseMigrate/Internal/Http.hs b/src/ClickHouseMigrate/Internal/Http.hs
--- a/src/ClickHouseMigrate/Internal/Http.hs
+++ b/src/ClickHouseMigrate/Internal/Http.hs
@@ -1,6 +1,7 @@
 -- | Dedicated direct HTTP resources, bounded full responses, and no replay.
 module ClickHouseMigrate.Internal.Http (Http, withHttp, withHttpAdmission, withHttpAdmissionChecked, withHttpFinalizer, probeConnection, submit, readJsonRowStream) where
 
+import ClickHouseMigrate.Internal.ServerProfile qualified as Profile
 import ClickHouseMigrate.Internal.TargetTypes
 import ClickHouseMigrate.Internal.Transport hiding (request)
 import ClickHouseMigrate.Prelude
@@ -125,14 +126,14 @@
     Right value -> pure (ScopeResult (Right value) cleanup)
 
 submit :: Http -> QueryId -> RequestKind -> ByteString -> IO TransportOutcome
-submit (Http (TargetConfig _ (DirectEndpoint endpoint) _ _ _ (Credentials user password) (HttpLimits micros maxBytes)) manager submitted gate active inflight) (QueryId identity) kind sql = registered inflight $ withMVar gate $ \() -> do
+submit (Http config@(TargetConfig {configEndpoint = DirectEndpoint endpoint, configCredentials = Credentials user password, configLimits = HttpLimits micros maxBytes}) manager submitted gate active inflight) (QueryId identity) kind sql = registered inflight $ withMVar gate $ \() -> do
   writeIORef submitted False
   live <- readIORef active
   if not live || BS.null sql || BS.elem 0 sql
     then pure (NotSubmitted (safeError InvalidRequest))
     else do
       result <- try @SomeException $ timeout micros $ do
-        request <- makeRequest endpoint user password micros identity kind sql
+        request <- makeRequest config endpoint user password micros identity kind sql
         HTTP.withResponse request manager $ \response -> do
           let status = statusCode (HTTP.responseStatus response)
               exceptionHeader = lookup "X-ClickHouse-Exception-Code" (HTTP.responseHeaders response)
@@ -216,9 +217,9 @@
 -- dummyManaged supplies no cleanup/replay; the factory owns this socket and closes
 -- it on failure. The bounded response parser is the same as for ordinary requests.
 probeConnection :: TargetConfig -> Internal.Connection -> QueryId -> ByteString -> IO TransportOutcome
-probeConnection (TargetConfig _ (DirectEndpoint endpoint) _ _ _ (Credentials user password) (HttpLimits micros maxBytes)) connection (QueryId identity) sql = do
+probeConnection config@(TargetConfig {configEndpoint = DirectEndpoint endpoint, configCredentials = Credentials user password, configLimits = HttpLimits micros maxBytes}) connection (QueryId identity) sql = do
   result <- try @SomeException $ timeout micros $ do
-    request <- makeRequest endpoint user password micros identity JsonRows sql
+    request <- makeRequest config endpoint user password micros identity JsonRows sql
     continuation <- Internal.requestBuilder request connection
     bracket
       (Internal.getResponse (Just (Internal.MaxHeaderLength 4096)) (Just (Internal.MaxNumberHeaders 64)) (Just micros) request (Internal.dummyManaged connection) continuation)
@@ -236,33 +237,13 @@
     Right Nothing -> pure (SubmissionUncertain (safeError DeadlineExpired))
     Right (Just outcome) -> pure outcome
 
-makeRequest :: Text -> ByteString -> ByteString -> Int -> Text -> RequestKind -> ByteString -> IO HTTP.Request
-makeRequest endpoint user password micros identity kind sql = do
+makeRequest :: TargetConfig -> Text -> ByteString -> ByteString -> Int -> Text -> RequestKind -> ByteString -> IO HTTP.Request
+makeRequest config endpoint user password micros identity kind sql = do
   base <- HTTP.parseRequest (Text.unpack endpoint)
   let request =
         HTTP.applyBasicAuth user password $
           HTTP.setQueryString
-            ( [ ("query_id", Just (Encoding.encodeUtf8 identity)),
-                ("async_insert", Just "0"),
-                ("mutations_sync", Just "1"),
-                ("alter_sync", Just "1"),
-                ("fsync_metadata", Just "1"),
-                ("wait_end_of_query", Just "1"),
-                ("default_format", Just "JSONEachRow")
-              ]
-                ++ if kind /= Command
-                  then
-                    [ ("max_rows_to_read", Just "0"),
-                      ("max_bytes_to_read", Just "0"),
-                      ("max_result_rows", Just "0"),
-                      ("max_result_bytes", Just "0"),
-                      ("read_overflow_mode", Just "throw"),
-                      ("result_overflow_mode", Just "throw"),
-                      ("timeout_overflow_mode", Just "throw"),
-                      ("sort_overflow_mode", Just "throw")
-                    ]
-                  else []
-            )
+            (Profile.requestSettings (configTopology config) (configLimits config) identity kind)
             base
               { HTTP.method = "POST",
                 HTTP.requestBody = HTTP.RequestBodyBS sql,
diff --git a/src/ClickHouseMigrate/Internal/LedgerCodec.hs b/src/ClickHouseMigrate/Internal/LedgerCodec.hs
--- a/src/ClickHouseMigrate/Internal/LedgerCodec.hs
+++ b/src/ClickHouseMigrate/Internal/LedgerCodec.hs
@@ -6,6 +6,7 @@
     eventValue,
     definitionValue,
     definitions,
+    definitionsFor,
     validateEvent,
     eventKind,
     encodeValue,
@@ -18,6 +19,7 @@
 import ClickHouseMigrate.Internal.Checksum qualified as Checksum
 import ClickHouseMigrate.Internal.LedgerTypes
 import ClickHouseMigrate.Internal.LedgerValidation (auditContext, ledgerPositionText, parseLedgerPosition)
+import ClickHouseMigrate.Internal.ServerProfile qualified as Profile
 import ClickHouseMigrate.Plan qualified as Plan
 import ClickHouseMigrate.Prelude
 import ClickHouseMigrate.Target qualified as Target
@@ -80,7 +82,7 @@
 
 bodyValue :: EventBody -> Value
 bodyValue = \case
-  Initialize target -> object ["target" .= Target.targetKeyText target, "schemaVersion" .= (1 :: Int), "checksumFormat" .= ("sha256-v1" :: Text), "policy" .= Encoding.decodeUtf8 Plan.completionPolicy]
+  InitializeFor target format -> object ["target" .= Target.targetKeyText target, "schemaVersion" .= (1 :: Int), "checksumFormat" .= Plan.checksumFormatName format, "policy" .= Encoding.decodeUtf8 (Profile.policyForFormat format)]
   StartMigration definition -> definitionValue definition
   StartStep step query -> stepValue step query
   SucceedStep step query -> stepValue step query
@@ -153,7 +155,10 @@
 recoveryStep = \case RetryStep step -> Just step; AcceptStep step -> Just step; _ -> Nothing
 
 definitions :: Plan.MigrationPlan -> [Definition]
-definitions plan =
+definitions = definitionsFor Plan.ChecksumV1
+
+definitionsFor :: Plan.ChecksumFormat -> Plan.MigrationPlan -> [Definition]
+definitionsFor format plan =
   zipWith
     bind
     [0 ..]
@@ -170,8 +175,8 @@
         (Def.componentNameText (Plan.componentViewName component))
         (map Def.componentNameText (Set.toAscList (Plan.componentDependencies component)))
         (Def.migrationNameText (Def.migrationIdName (Plan.migrationId migration)))
-        (Plan.migrationChecksumHex (Plan.migrationChecksum migration))
-        [StepDefinition (Def.stepNameText (Plan.stepViewName step)) (Plan.stepChecksumHex (Plan.stepChecksum step)) | step <- NE.toList (Plan.migrationSteps migration)]
+        (Plan.migrationChecksumHex (Plan.migrationChecksumFor format migration))
+        [StepDefinition (Def.stepNameText (Plan.stepViewName step)) (Plan.stepChecksumHex (Plan.stepChecksumFor format step)) | step <- NE.toList (Plan.migrationSteps migration)]
 
 definitionsDigest :: [Definition] -> Text
 definitionsDigest values = Checksum.digestHex (Checksum.digest (encodeValue (Aeson.toJSON (map definitionValue values))))
@@ -204,8 +209,9 @@
     version <- o .: "schemaVersion" :: Parser Int
     format <- o .: "checksumFormat" :: Parser Text
     policy <- o .: "policy" :: Parser Text
-    unless (version == 1 && format == "sha256-v1" && policy == Encoding.decodeUtf8 Plan.completionPolicy) (fail "unsupported format")
-    Initialize <$> (o .: "target" >>= validated Target.targetKey)
+    parsed <- validated Plan.parseChecksumFormat format
+    unless (version == 1 && policy == Encoding.decodeUtf8 (Profile.policyForFormat parsed)) (fail "unsupported format")
+    (\target -> InitializeFor target parsed) <$> (o .: "target" >>= validated Target.targetKey)
   "migration-started" -> fmap StartMigration . parseDefinition
   "step-started" -> parseStep StartStep
   "step-succeeded" -> parseStep SucceedStep
diff --git a/src/ClickHouseMigrate/Internal/LedgerSchema.hs b/src/ClickHouseMigrate/Internal/LedgerSchema.hs
--- a/src/ClickHouseMigrate/Internal/LedgerSchema.hs
+++ b/src/ClickHouseMigrate/Internal/LedgerSchema.hs
@@ -14,21 +14,6 @@
 qualifiedTable :: LedgerNamespace -> Text
 qualifiedTable namespace = quoteIdentifier (ledgerNamespaceText namespace) <> ".`events`"
 
-createDatabase :: LedgerNamespace -> ByteString
-createDatabase namespace = Encoding.encodeUtf8 ("CREATE DATABASE IF NOT EXISTS " <> quoteIdentifier (ledgerNamespaceText namespace) <> " ENGINE=Atomic")
-
-createTable :: LedgerNamespace -> ByteString
-createTable namespace = Encoding.encodeUtf8 ("CREATE TABLE IF NOT EXISTS " <> qualifiedTable namespace <> " " <> columnsSql <> " ENGINE=MergeTree ORDER BY sequence SETTINGS fsync_after_insert=1, fsync_part_directory=1, storage_policy='default'")
-
-columnsSql :: Text
-columnsSql = "(sequence UInt64, event_id UUID, ledger_id UUID, event String)"
-
-expectedCreate :: LedgerNamespace -> Text
-expectedCreate namespace = "CREATE TABLE " <> ledgerNamespaceText namespace <> ".events (`sequence` UInt64, `event_id` UUID, `ledger_id` UUID, `event` String) ENGINE = " <> expectedEngine
-
-expectedEngine :: Text
-expectedEngine = "MergeTree ORDER BY sequence SETTINGS fsync_after_insert = 1, fsync_part_directory = 1, storage_policy = 'default', index_granularity = 8192"
-
 -- Only validated ASCII namespace names enter literals; event/audit data are JSON.
 namespaceLiteral :: LedgerNamespace -> Text
 namespaceLiteral = (\value -> "'" <> value <> "'") . ledgerNamespaceText
diff --git a/src/ClickHouseMigrate/Internal/LedgerStore.hs b/src/ClickHouseMigrate/Internal/LedgerStore.hs
--- a/src/ClickHouseMigrate/Internal/LedgerStore.hs
+++ b/src/ClickHouseMigrate/Internal/LedgerStore.hs
@@ -10,6 +10,7 @@
 import ClickHouseMigrate.Internal.LedgerSchema qualified as Schema
 import ClickHouseMigrate.Internal.LedgerTypes
 import ClickHouseMigrate.Internal.Projection (appendProjection, project)
+import ClickHouseMigrate.Internal.ServerProfile qualified as Profile
 import ClickHouseMigrate.Internal.TargetTypes (TargetConfig (..))
 import ClickHouseMigrate.Internal.Transport
 import ClickHouseMigrate.Prelude
@@ -74,8 +75,10 @@
 
 -- Shape state is separate from event projection to avoid inventing an empty
 -- ledger identity. Return False for absent table after checking every object.
-shape :: Maybe Guard -> Transport -> LedgerNamespace -> IO (Bool, Bool)
-shape guard transport namespace = do
+shape :: Maybe Guard -> Transport -> TargetConfig -> IO (Bool, Bool)
+shape guard transport config = do
+  let namespace = configNamespace config
+      topology = configTopology config
   checkLocalStorage guard transport
   settings <- readRows guard transport "SELECT toUInt8(getSetting('fsync_metadata')) AS fsync_metadata"
   case settings of
@@ -86,21 +89,24 @@
     _ -> invalid
   databases <- readRows guard transport (Schema.databaseQuery namespace)
   case databases of
-    [] -> pure (False, False)
+    [] -> if Profile.requiresLedgerDatabase topology then abort (LedgerProblem LedgerDatabaseMissing) else pure (False, False)
     [row] -> do
       keys ["name", "engine"] row
       name <- member "name" row
       engine <- member "engine" row :: IO Text
-      unless (name == ledgerNamespaceText namespace && engine == "Atomic") invalid
+      unless (name == ledgerNamespaceText namespace && engine == Profile.databaseEngine topology) invalid
       tables <- readRows guard transport (Schema.tableQuery namespace)
       case tables of
         [] -> pure (True, False)
         [table] -> do
           keys ["name", "engine", "engine_full", "sorting_key", "primary_key", "partition_key", "sampling_key", "create_table_query"] table
           actual <- mapM (`member` table) ["name", "engine", "engine_full", "sorting_key", "primary_key", "partition_key", "sampling_key", "create_table_query"] :: IO [Text]
-          let base = ["events", "MergeTree", Schema.expectedEngine, "sequence", "sequence", "", ""]
-              quotedCreate = Text.replace ("CREATE TABLE " <> ledgerNamespaceText namespace <> ".") ("CREATE TABLE " <> Schema.quoteIdentifier (ledgerNamespaceText namespace) <> ".") (Schema.expectedCreate namespace)
-          unless (actual == base ++ [Schema.expectedCreate namespace] || actual == base ++ [quotedCreate]) invalid
+          unless (Profile.validTableShape topology namespace actual) invalid
+          case Profile.replicaHealthQuery topology namespace of
+            Nothing -> pure ()
+            Just sql -> do
+              health <- readRows guard transport sql
+              unless (Profile.validReplicaHealth topology health) invalid
           pure (True, True)
         _ -> invalid
     _ -> invalid
@@ -136,8 +142,8 @@
     invalid = abort (LedgerProblem UnsupportedLedgerStorage)
 
 load :: Maybe Guard -> Transport -> TargetConfig -> IO Observation
-load guard transport (TargetConfig target _ _ namespace binding _ _) = do
-  (database, table) <- shape guard transport namespace
+load guard transport config@(TargetConfig {configKey = target, configNamespace = namespace, configBinding = binding}) = do
+  (database, table) <- shape guard transport config
   if not database
     then pure Missing
     else
@@ -150,6 +156,7 @@
             else do
               events <- mapM decodeRow rows
               projection <- checked (project target events)
+              unless (projectionChecksumFormat projection == Profile.checksumFormat (configTopology config)) (abort (LedgerProblem UnsupportedLedgerVersion))
               case binding of
                 ExistingLedger expected -> let LedgerPosition actual _ _ = position projection in unless (expected == actual) (abort (LedgerProblem LedgerIdentityMismatch))
                 FirstUse -> pure ()
@@ -168,7 +175,7 @@
   pure event
 
 openStore :: Guard -> Transport -> TargetConfig -> Maybe InitializationAuthorization -> QuiescenceEvidence -> Text -> IO LedgerStore
-openStore guard transport config@(TargetConfig target _ _ namespace binding _ _) authorization (QuiescenceEvidence quietTarget _) run = do
+openStore guard transport config@(TargetConfig {configKey = target, configNamespace = namespace, configBinding = binding}) authorization (QuiescenceEvidence quietTarget _) run = do
   checkAccess guard
   case authorization of
     Just (InitializationAuthorization authorized identity _) -> do
@@ -195,15 +202,15 @@
   pure (LedgerStore guard transport target namespace state gate)
 
 initialize :: Guard -> Transport -> TargetConfig -> LedgerId -> AuditContext -> Text -> IO Projection
-initialize guard transport (TargetConfig target _ _ namespace _ _ _) ledger audit run = do
+initialize guard transport config@(TargetConfig {configKey = target, configNamespace = namespace}) ledger audit run = do
   identity <- freshUuid
   time <- diagnosticTime
-  let first = LedgerEvent 1 identity ledger run Nothing time audit "0.1.0.0" Nothing (Initialize target)
+  let first = LedgerEvent 1 identity ledger run Nothing time audit "0.1.0.0" Nothing (InitializeFor target (Profile.checksumFormat (configTopology config)))
   _ <- checked (project target [first])
-  ddl "database" (Schema.createDatabase namespace)
-  _ <- shape (Just guard) transport namespace
-  ddl "table" (Schema.createTable namespace)
-  (_, exists) <- shape (Just guard) transport namespace
+  mapM_ (ddl "database") (Profile.createDatabase (configTopology config) namespace)
+  _ <- shape (Just guard) transport config
+  ddl "table" (Profile.createTable (configTopology config) namespace)
+  (_, exists) <- shape (Just guard) transport config
   unless exists (abort (LedgerProblem InvalidLedgerSchema))
   -- Even under exclusivity do not write over preexisting nonempty history.
   rows <- readRowsKind JsonRowStream (Just guard) transport (Schema.readQuery namespace)
@@ -242,7 +249,7 @@
 -- A fresh guarded FULL read, including strict corruption validation. NotObserved
 -- is evidence of this read only; it grants no authority to reuse a sequence.
 reconcile :: Guard -> Transport -> TargetConfig -> AppendReceipt -> IO (Reconciliation, Observation)
-reconcile guard transport config@(TargetConfig target _ _ _ _ _ _) (AppendReceipt expected ledger number identity digest _) = do
+reconcile guard transport config@(TargetConfig {configKey = target}) (AppendReceipt expected ledger number identity digest _) = do
   unless (target == expected) (abort (LedgerProblem LedgerIdentityMismatch))
   observation <- load (Just guard) transport config
   let result = case observation of
diff --git a/src/ClickHouseMigrate/Internal/LedgerTypes.hs b/src/ClickHouseMigrate/Internal/LedgerTypes.hs
--- a/src/ClickHouseMigrate/Internal/LedgerTypes.hs
+++ b/src/ClickHouseMigrate/Internal/LedgerTypes.hs
@@ -1,6 +1,9 @@
+{-# LANGUAGE PatternSynonyms #-}
+
 -- | Private ledger protocol. No unchecked construction is exported publicly.
 module ClickHouseMigrate.Internal.LedgerTypes where
 
+import ClickHouseMigrate.Internal.Types (ChecksumFormat (..))
 import ClickHouseMigrate.Prelude
 import ClickHouseMigrate.Target (LedgerId, QueryId, TargetKey)
 import Data.Word (Word64)
@@ -39,6 +42,7 @@
   | MissingInitialization
   | ImpossibleTransition
   | DefinitionMismatch
+  | LedgerDatabaseMissing
   | InvalidLedgerSchema
   | UnsupportedLedgerStorage
   | LedgerAbsent
@@ -72,7 +76,7 @@
 data AdoptionBatch = AdoptionBatch !Text !Text !Text !Text !Int deriving stock (Eq, Show)
 
 data EventBody
-  = Initialize !TargetKey
+  = InitializeFor !TargetKey !ChecksumFormat
   | StartMigration !Definition
   | StartStep !Int !Text
   | SucceedStep !Int !Text
@@ -84,6 +88,18 @@
   | AdoptEntry !Definition !Evidence
   | CompleteAdoption !Text
   deriving stock (Eq, Show)
+
+pattern Initialize :: TargetKey -> EventBody
+pattern Initialize target <- InitializeFor target _
+  where
+    Initialize target = InitializeFor target ChecksumV1
+
+{-# COMPLETE Initialize, StartMigration, StartStep, SucceedStep, FailStep, CompleteMigration, AuthorizeRecovery, AcceptCompletion, StartAdoption, AdoptEntry, CompleteAdoption #-}
+
+projectionChecksumFormat :: Projection -> ChecksumFormat
+projectionChecksumFormat state = case events state of
+  LedgerEvent {body = InitializeFor _ format} : _ -> format
+  _ -> ChecksumV1
 
 data EventContext = EventContext !Text !Text !Text !(Maybe (Text, Text)) deriving stock (Eq, Show)
 
diff --git a/src/ClickHouseMigrate/Internal/Preflight.hs b/src/ClickHouseMigrate/Internal/Preflight.hs
--- a/src/ClickHouseMigrate/Internal/Preflight.hs
+++ b/src/ClickHouseMigrate/Internal/Preflight.hs
@@ -1,10 +1,11 @@
 -- | Force the complete definition/target envelope before contacting a server.
-module ClickHouseMigrate.Internal.Preflight (preflight, validateRecoveryEvent, validateRecoveryCapacity) where
+module ClickHouseMigrate.Internal.Preflight (preflight, preflightPending, validateRecoveryEvent, validateRecoveryCapacity) where
 
-import ClickHouseMigrate.Internal.LedgerCodec (contextFor, definitions, encodeEvent, maxEventBytes, validateEvent)
+import ClickHouseMigrate.Internal.LedgerCodec (contextFor, definitionsFor, encodeEvent, maxEventBytes, validateEvent)
 import ClickHouseMigrate.Internal.LedgerTypes qualified as L
 import ClickHouseMigrate.Internal.Reporting (stepReference, summaryDefinition)
-import ClickHouseMigrate.Internal.SqlEnvelope (mentionsIdentifier, validateSql)
+import ClickHouseMigrate.Internal.ServerProfile qualified as Profile
+import ClickHouseMigrate.Internal.SqlEnvelope (createsDatabase, mentionsIdentifier, validateReplicatedPolicy, validateSql)
 import ClickHouseMigrate.Internal.TargetTypes qualified as T
 import ClickHouseMigrate.Plan qualified as Plan
 import ClickHouseMigrate.Report qualified as R
@@ -14,11 +15,11 @@
 import Data.Word (Word64)
 
 preflight :: T.TargetConfig -> L.AuditContext -> Plan.MigrationPlan -> Either R.RunError ()
-preflight (T.TargetConfig target _ _ namespace _ _ _) audit plan = do
+preflight config@(T.TargetConfig {T.configKey = target, T.configNamespace = namespace}) audit plan = do
   let uuid = "00000000-0000-4000-8000-000000000000"
       envelope body context = L.LedgerEvent 1 uuid (T.LedgerId uuid) uuid (Just uuid) "2026-10-16T00:00:00.999999999999Z" audit "0.1.0.0" context body
       authored = [(migration, NE.toList (Plan.migrationSteps migration)) | migration <- NE.toList (Plan.inspectSchedule plan)]
-      sources = zip (definitions plan) authored
+      sources = zip (definitionsFor (Profile.checksumFormat (T.configTopology config)) plan) authored
   if BS.length (encodeEvent (envelope (L.Initialize target) Nothing)) > 61440 then Left R.AuditTooLarge else pure ()
   mapM_
     ( \(definition, (_, steps)) -> do
@@ -46,3 +47,21 @@
 validateRecoveryCapacity current needed
   | needed < 0 || toInteger current + needed > toInteger (maxBound :: Word64) = Left (R.LedgerFailed L.LedgerLimitExceeded)
   | otherwise = Right ()
+
+-- Requires an observed prefix. It runs before bootstrap or application writes;
+-- applied migrations are excluded without pretending to know history offline.
+preflightPending :: T.TargetConfig -> Plan.MigrationPlan -> Int -> Either R.RunError ()
+preflightPending config plan applied = case T.configTopology config of
+  T.DirectServer _ -> Right ()
+  topology@(T.ReplicatedCluster _ _) -> mapM_ check (drop applied (zip (definitionsFor (Profile.checksumFormat topology) plan) (NE.toList (Plan.inspectSchedule plan))))
+  where
+    check (definition, migration) =
+      mapM_
+        ( \(index, step) -> do
+            creates <- either (const (Left R.UnexpectedRuntimeFailure)) Right (createsDatabase (Plan.stepSql step))
+            case stepReference definition index of
+              Just reference | creates -> Left (R.DatabaseCreationOnReplicatedTarget reference)
+              Just reference -> either (const (Left (R.InvalidRuntimePlan reference))) Right (validateReplicatedPolicy (Plan.stepSql step))
+              _ -> Left R.UnexpectedRuntimeFailure
+        )
+        (zip [0 ..] (NE.toList (Plan.migrationSteps migration)))
diff --git a/src/ClickHouseMigrate/Internal/Projection.hs b/src/ClickHouseMigrate/Internal/Projection.hs
--- a/src/ClickHouseMigrate/Internal/Projection.hs
+++ b/src/ClickHouseMigrate/Internal/Projection.hs
@@ -1,7 +1,7 @@
 -- | Pure immutable history fold. No timestamp or merge order chooses a winner.
 module ClickHouseMigrate.Internal.Projection (project, appendProjection, comparePlan, unresolved, resolvedStep) where
 
-import ClickHouseMigrate.Internal.LedgerCodec (contextFor, definitions, definitionsDigest, validateEvent)
+import ClickHouseMigrate.Internal.LedgerCodec (contextFor, definitionsDigest, definitionsFor, validateEvent)
 import ClickHouseMigrate.Internal.LedgerTypes
 import ClickHouseMigrate.Plan (MigrationPlan)
 import ClickHouseMigrate.Prelude
@@ -143,7 +143,7 @@
 
 comparePlan :: MigrationPlan -> Projection -> Either LedgerError ()
 comparePlan plan state = do
-  let supplied = definitions plan
+  let supplied = definitionsFor (projectionChecksumFormat state) plan
       recorded =
         [definition | RecordedMigration definition _ _ <- completed state]
           ++ [definition | Just (ActiveMigration definition _ _ _) <- [active state]]
diff --git a/src/ClickHouseMigrate/Internal/Reporting.hs b/src/ClickHouseMigrate/Internal/Reporting.hs
--- a/src/ClickHouseMigrate/Internal/Reporting.hs
+++ b/src/ClickHouseMigrate/Internal/Reporting.hs
@@ -2,7 +2,7 @@
 module ClickHouseMigrate.Internal.Reporting where
 
 import ClickHouseMigrate.Internal.ExclusiveTypes (AccessDescription)
-import ClickHouseMigrate.Internal.LedgerCodec (definitions)
+import ClickHouseMigrate.Internal.LedgerCodec (definitionsFor)
 import ClickHouseMigrate.Internal.LedgerStore qualified as Store
 import ClickHouseMigrate.Internal.LedgerTypes qualified as L
 import ClickHouseMigrate.Internal.TargetTypes qualified as T
@@ -99,9 +99,9 @@
     Nothing
     [R.StepSummary name checksum R.StepPending Nothing Nothing | L.StepDefinition name checksum <- L.steps definition]
 
-inspectHistory :: Plan.MigrationPlan -> R.HistoryReport -> R.InspectionReport
-inspectHistory plan history@(R.HistoryReport _ _ _ _ _ events recorded unresolved) =
-  let supplied = definitions plan
+inspectHistoryFor :: Plan.ChecksumFormat -> Plan.MigrationPlan -> R.HistoryReport -> R.InspectionReport
+inspectHistoryFor format plan history@(R.HistoryReport _ _ _ _ _ events recorded unresolved) =
+  let supplied = definitionsFor format plan
       actual = [definition | R.MigrationSummary definition _ _ _ <- recorded]
       expected = map summaryDefinition supplied
       changes = [R.DefinitionChanged index (at index expected) (Just definition) | (index, definition) <- zip [0 ..] actual, at index expected /= Just definition]
@@ -133,16 +133,16 @@
 runPosition :: R.RunReport -> Maybe L.LedgerPosition
 runPosition report = case runHistory report of R.HistoryReport _ _ _ position _ _ _ _ -> position
 
-updateRun :: Plan.MigrationPlan -> R.HistoryReport -> R.RunReport -> R.RunReport
-updateRun plan history (R.RunReport identity target access acquired _ _ observations initialized) = let R.InspectionReport _ migrations _ = inspectHistory plan history in R.RunReport identity target access acquired history migrations observations initialized
+updateRunFor :: Plan.ChecksumFormat -> Plan.MigrationPlan -> R.HistoryReport -> R.RunReport -> R.RunReport
+updateRunFor format plan history (R.RunReport identity target access acquired _ _ observations initialized) = let R.InspectionReport _ migrations _ = inspectHistoryFor format plan history in R.RunReport identity target access acquired history migrations observations initialized
 
 recordSql :: R.SqlObservation -> R.RunReport -> R.RunReport
 recordSql value report@(R.RunReport _ _ _ _ _ _ observations _) = report {R.observations = observations ++ [value]}
 
-initialRun :: Text -> TargetKey -> AccessDescription -> Plan.MigrationPlan -> R.RunReport
-initialRun run target access plan =
+initialRunFor :: Plan.ChecksumFormat -> Text -> TargetKey -> AccessDescription -> Plan.MigrationPlan -> R.RunReport
+initialRunFor format run target access plan =
   let history = emptyHistory target (R.GuardedObservation access)
-      R.InspectionReport _ migrations _ = inspectHistory plan history
+      R.InspectionReport _ migrations _ = inspectHistoryFor format plan history
    in R.RunReport run target access False history migrations [] Nothing
 
 historyPosition :: R.HistoryReport -> Maybe L.LedgerPosition
@@ -169,6 +169,7 @@
 
 bodyReport :: L.EventBody -> R.HistoryBody
 bodyReport = \case
+  L.InitializeFor target Plan.ChecksumV2 -> R.LedgerInitializedReplicated target
   L.Initialize target -> R.LedgerInitialized target
   L.StartMigration definition -> R.MigrationDefinition (summaryDefinition definition)
   L.StartStep index query -> R.StepIntent index (T.QueryId query)
@@ -206,3 +207,18 @@
 at index values
   | index < 0 = Nothing
   | otherwise = case drop index values of value : _ -> Just value; _ -> Nothing
+
+-- Legacy pure helpers keep their v1/recorded-history defaults. Production paths
+-- pass the target format explicitly, including before ledger initialization.
+inspectHistory :: Plan.MigrationPlan -> R.HistoryReport -> R.InspectionReport
+inspectHistory plan history = inspectHistoryFor (historyChecksumFormat history) plan history
+
+updateRun :: Plan.MigrationPlan -> R.HistoryReport -> R.RunReport -> R.RunReport
+updateRun plan history = updateRunFor (historyChecksumFormat history) plan history
+
+initialRun :: Text -> TargetKey -> AccessDescription -> Plan.MigrationPlan -> R.RunReport
+initialRun = initialRunFor Plan.ChecksumV1
+
+historyChecksumFormat :: R.HistoryReport -> Plan.ChecksumFormat
+historyChecksumFormat (R.HistoryReport _ _ _ _ _ events _ _) =
+  if any (\(R.HistoryEvent _ _ _ _ _ _ _ _ _ kind) -> case kind of R.LedgerInitializedReplicated _ -> True; _ -> False) events then Plan.ChecksumV2 else Plan.ChecksumV1
diff --git a/src/ClickHouseMigrate/Internal/ServerPolicy.hs b/src/ClickHouseMigrate/Internal/ServerPolicy.hs
--- a/src/ClickHouseMigrate/Internal/ServerPolicy.hs
+++ b/src/ClickHouseMigrate/Internal/ServerPolicy.hs
@@ -12,57 +12,24 @@
 
 import ClickHouseMigrate.Internal.Checksum qualified as Checksum
 import ClickHouseMigrate.Internal.Http (Http, probeConnection, submit, withHttpAdmissionChecked)
+import ClickHouseMigrate.Internal.ServerProfile (TargetCheckError (..))
+import ClickHouseMigrate.Internal.ServerProfile qualified as Profile
 import ClickHouseMigrate.Internal.TargetTypes
 import ClickHouseMigrate.Internal.Transport
 import ClickHouseMigrate.Prelude
-import ClickHouseMigrate.Target (serverId)
-import Data.Aeson (FromJSON, Key, Object, Result (..), fromJSON)
-import Data.Aeson.KeyMap qualified as KM
 import Data.IORef (atomicModifyIORef', newIORef)
-import Data.Set qualified as Set
 import Data.Text qualified as Text
 import Data.Text.Encoding qualified as Encoding
 
 -- Exact tested-patch policy. Keep coordinated with the checked-in candidate matrix.
 supportedVersions :: [Text]
-supportedVersions = ["26.8.28.14"]
-
-data TargetCheckError = ProbeFailure !TransportOutcome | TargetRejected !TransportError
-  deriving stock (Eq, Show)
+supportedVersions = ["26.8.28.14", "26.3.60.10001.altinitystable", "26.3.60.10002.altinitystable"]
 
 probeTarget :: Http -> TargetConfig -> QueryId -> IO (Either TargetCheckError ServerIdentity)
-probeTarget http config identity = validateOutcome config <$> submit http identity JsonRows identityQuery
+probeTarget http config identity = validateOutcome config <$> submit http identity JsonRows (Profile.identityQuery (configTopology config))
 
 validateOutcome :: TargetConfig -> TransportOutcome -> Either TargetCheckError ServerIdentity
-validateOutcome (TargetConfig _ _ expected _ _ _ _) outcome = case outcome of
-  AcknowledgedSuccess (Rows [row]) -> validate row
-  AcknowledgedSuccess _ -> rejected UnexpectedResponse
-  failure -> Left (ProbeFailure failure)
-  where
-    rejected code = Left (TargetRejected (TransportError code Nothing Nothing))
-    validate row
-      | Set.fromList (KM.keys row) /= Set.fromList ["server_id", "version", "host", "async_insert", "mutations_sync", "alter_sync"] = rejected UnexpectedResponse
-      | otherwise = case (get "server_id" row, get "version" row, get "host" row, get "async_insert" row, get "mutations_sync" row, get "alter_sync" row) of
-          (Just uuid, Just version, Just host, Just (async :: Int), Just (mutation :: Int), Just (alter :: Int)) -> case serverId uuid of
-            Left _ -> rejected UnexpectedResponse
-            Right actual
-              | actual /= expected -> rejected ServerIdentityMismatch
-              | version `notElem` supportedVersions -> rejected UnsupportedServer
-              | async /= 0 || mutation /= 1 || alter /= 1 -> rejected CompletionPolicyMismatch
-              | otherwise -> Right (ServerIdentity actual version host)
-          _ -> rejected UnexpectedResponse
-
-get :: (FromJSON a) => Key -> Object -> Maybe a
-get key row = KM.lookup key row >>= \value -> case fromJSON value of Success result -> Just result; Error _ -> Nothing
-
-identityQuery :: ByteString
-identityQuery =
-  """
-  SELECT toString(serverUUID()) AS server_id, version() AS version, hostName() AS host,
-         toUInt8(getSetting('async_insert')) AS async_insert,
-         toUInt8(getSetting('mutations_sync')) AS mutations_sync,
-         toUInt8(getSetting('alter_sync')) AS alter_sync
-  """
+validateOutcome config = Profile.validateOutcome supportedVersions (configTopology config)
 
 -- The only production constructor for a checked transport. Its Http resources
 -- close at scope exit; a capability used after exit cannot submit again.
@@ -75,7 +42,7 @@
   let prefix = "chm-admission-" <> Checksum.digestHex (Checksum.digest (Encoding.encodeUtf8 rootId)) <> "-"
       admit connection = do
         n <- atomicModifyIORef' counter (\old -> (old + 1, old))
-        outcome <- probeConnection config connection (QueryId (prefix <> Text.pack (show n))) identityQuery
+        outcome <- probeConnection config connection (QueryId (prefix <> Text.pack (show n))) (Profile.identityQuery (configTopology config))
         pure $ case validateOutcome config outcome of
           Right _ -> Right ()
           Left (TargetRejected failure) -> Left failure
@@ -87,4 +54,10 @@
     checked <- probeTarget http config probeId
     case checked of
       Left failure -> pure (Left failure)
-      Right identity -> Right <$> action (Transport (submit http) identity)
+      Right identity -> do
+        membership <- case Profile.membershipQuery (configTopology config) of
+          Nothing -> pure (Right ())
+          Just sql -> Profile.validateMembership (configTopology config) <$> submit http (QueryId (rootId <> "-members")) JsonRows sql
+        case membership of
+          Left failure -> pure (Left failure)
+          Right () -> Right <$> action (Transport (submit http) identity)
diff --git a/src/ClickHouseMigrate/Internal/ServerProfile.hs b/src/ClickHouseMigrate/Internal/ServerProfile.hs
new file mode 100644
--- /dev/null
+++ b/src/ClickHouseMigrate/Internal/ServerProfile.hs
@@ -0,0 +1,202 @@
+{-# LANGUAGE MultilineStrings #-}
+
+-- | Server-dependent bytes and expectations. Direct policy is frozen by unit pins.
+module ClickHouseMigrate.Internal.ServerProfile where
+
+import ClickHouseMigrate.Internal.SqlEnvelope (replicatedEngineSuffix)
+import ClickHouseMigrate.Internal.TargetTypes
+import ClickHouseMigrate.Internal.Transport
+import ClickHouseMigrate.Internal.Types (ChecksumFormat (..))
+import ClickHouseMigrate.Prelude
+import ClickHouseMigrate.Target (ledgerNamespaceText, serverId)
+import Data.Aeson (FromJSON, Key, Object, Result (..), fromJSON)
+import Data.Aeson.Key qualified as DataKey
+import Data.Aeson.KeyMap qualified as KM
+import Data.List.NonEmpty qualified as NE
+import Data.Set qualified as Set
+import Data.Text qualified as Text
+import Data.Text.Encoding qualified as Encoding
+
+data TargetCheckError = ProbeFailure !TransportOutcome | TargetRejected !TransportError
+  deriving stock (Eq, Show)
+
+validateOutcome :: [Text] -> TargetTopology -> TransportOutcome -> Either TargetCheckError ServerIdentity
+validateOutcome supportedVersions (DirectServer expected) outcome = case outcome of
+  AcknowledgedSuccess (Rows [row]) -> validate row
+  AcknowledgedSuccess _ -> rejected UnexpectedResponse
+  failure -> Left (ProbeFailure failure)
+  where
+    rejected code = Left (TargetRejected (TransportError code Nothing Nothing))
+    validate row
+      | Set.fromList (KM.keys row) /= Set.fromList ["server_id", "version", "host", "async_insert", "mutations_sync", "alter_sync"] = rejected UnexpectedResponse
+      | otherwise = case (get "server_id" row, get "version" row, get "host" row, get "async_insert" row, get "mutations_sync" row, get "alter_sync" row) of
+          (Just uuid, Just version, Just host, Just (async :: Int), Just (mutation :: Int), Just (alter :: Int)) -> case serverId uuid of
+            Left _ -> rejected UnexpectedResponse
+            Right actual
+              | actual /= expected -> rejected ServerIdentityMismatch
+              | version `notElem` supportedVersions -> rejected UnsupportedServer
+              | async /= 0 || mutation /= 1 || alter /= 1 -> rejected CompletionPolicyMismatch
+              | otherwise -> Right (ServerIdentity actual version host)
+          _ -> rejected UnexpectedResponse
+validateOutcome versions topology@(ReplicatedCluster (ClusterName cluster) replicas) outcome = case outcome of
+  AcknowledgedSuccess (Rows [row]) -> do
+    actual <- maybe (rejected UnexpectedResponse) (either (const (rejected UnexpectedResponse)) Right . serverId) (get "server_id" row)
+    version <- maybe (rejected UnexpectedResponse) Right (get "version" row)
+    host <- maybe (rejected UnexpectedResponse) Right (get "host" row)
+    macro <- maybe (rejected UnexpectedResponse) Right (get "cluster" row)
+    unless (actual `elem` replicas) (rejected ServerIdentityMismatch)
+    unless (version `elem` versions) (rejected UnsupportedServer)
+    unless (macro == cluster) (rejected ReplicaSetMismatch)
+    unless (get "storage_ok" row == Just (1 :: Int)) (rejected CompletionPolicyMismatch)
+    unless (all (\(key, value) -> get key row == Just value) (replicatedEffective topology)) (rejected CompletionPolicyMismatch)
+    Right (ServerIdentity actual version host)
+  AcknowledgedSuccess _ -> rejected UnexpectedResponse
+  failure -> Left (ProbeFailure failure)
+  where
+    rejected code = Left (TargetRejected (TransportError code Nothing Nothing))
+
+replicatedEffective :: TargetTopology -> [(Key, Text)]
+replicatedEffective (DirectServer _) = []
+replicatedEffective (ReplicatedCluster _ replicas) =
+  [("async_insert", "0"), ("mutations_sync", "2"), ("alter_sync", "2"), ("insert_quorum", Text.pack (show (NE.length replicas))), ("insert_quorum_parallel", "0"), ("distributed_ddl_output_mode", "none"), ("select_sequential_consistency", "1"), ("fsync_metadata", "1")]
+
+get :: (FromJSON a) => Key -> Object -> Maybe a
+get key row = KM.lookup key row >>= \value -> case fromJSON value of Success result -> Just result; Error _ -> Nothing
+
+identityQuery :: TargetTopology -> ByteString
+identityQuery (DirectServer _) =
+  """
+  SELECT toString(serverUUID()) AS server_id, version() AS version, hostName() AS host,
+         toUInt8(getSetting('async_insert')) AS async_insert,
+         toUInt8(getSetting('mutations_sync')) AS mutations_sync,
+         toUInt8(getSetting('alter_sync')) AS alter_sync
+  """
+identityQuery topology@(ReplicatedCluster _ _) =
+  "SELECT toString(serverUUID()) AS server_id, version() AS version, hostName() AS host, getMacro('cluster') AS cluster, "
+    <> Encoding.encodeUtf8 (Text.intercalate ", " [(if key == "distributed_ddl_output_mode" then "toString(getSetting('" <> key <> "'))" else "toString(toUInt64(getSetting('" <> key <> "')))") <> " AS " <> key | (key, _) <- map (\(key, value) -> (DataKey.toText key, value)) (replicatedEffective topology)])
+    <> ", toUInt8((SELECT count() FROM system.storage_policies WHERE policy_name='default' AND length(disks)>0)>0 AND (SELECT count() FROM system.disks WHERE name IN (SELECT arrayJoin(disks) FROM system.storage_policies WHERE policy_name='default') AND (type!='Local' OR is_remote OR is_read_only OR is_broken))=0) AS storage_ok"
+
+requestSettings :: TargetTopology -> HttpLimits -> Text -> RequestKind -> [(ByteString, Maybe ByteString)]
+requestSettings (DirectServer _) _ identity kind =
+  [ ("query_id", Just (Encoding.encodeUtf8 identity)),
+    ("async_insert", Just "0"),
+    ("mutations_sync", Just "1"),
+    ("alter_sync", Just "1"),
+    ("fsync_metadata", Just "1"),
+    ("wait_end_of_query", Just "1"),
+    ("default_format", Just "JSONEachRow")
+  ]
+    ++ if kind /= Command
+      then
+        [ ("max_rows_to_read", Just "0"),
+          ("max_bytes_to_read", Just "0"),
+          ("max_result_rows", Just "0"),
+          ("max_result_bytes", Just "0"),
+          ("read_overflow_mode", Just "throw"),
+          ("result_overflow_mode", Just "throw"),
+          ("timeout_overflow_mode", Just "throw"),
+          ("sort_overflow_mode", Just "throw")
+        ]
+      else []
+requestSettings topology@(ReplicatedCluster _ _) limits@(HttpLimits micros _) identity kind =
+  [("query_id", Just (Encoding.encodeUtf8 identity))]
+    ++ [(Encoding.encodeUtf8 (DataKey.toText key), Just (Encoding.encodeUtf8 value)) | (key, value) <- replicatedEffective topology]
+    ++ [("distributed_ddl_task_timeout", Just seconds), ("replication_wait_for_inactive_replica_timeout", Just seconds), ("insert_quorum_timeout", Just millis)]
+    ++ [("wait_end_of_query", Just "1"), ("default_format", Just "JSONEachRow")]
+    ++ drop 7 (requestSettings (DirectServer (NE.head replicas)) limits identity kind)
+  where
+    ReplicatedCluster _ replicas = topology
+    -- DDL and ALTER waits may occur in sequence. Share the budget rather than
+    -- allowing each wait to consume the whole HTTP deadline.
+    waitSeconds = max 1 ((micros `div` 1000000 - 2) `div` 3)
+    seconds = Encoding.encodeUtf8 (Text.pack (show waitSeconds))
+    millis = Encoding.encodeUtf8 (Text.pack (show (waitSeconds * 1000)))
+
+-- A scoped membership read needs no REMOTE privilege. Individual sockets still
+-- require expected UUIDs; counts do not attest UUIDs on unvisited replicas.
+membershipQuery :: TargetTopology -> Maybe ByteString
+membershipQuery (DirectServer _) = Nothing
+membershipQuery (ReplicatedCluster (ClusterName cluster) _) = Just (Encoding.encodeUtf8 ("SELECT count() AS replicas, uniqExact(shard_num) AS shards FROM system.clusters WHERE cluster='" <> cluster <> "'"))
+
+validateMembership :: TargetTopology -> TransportOutcome -> Either TargetCheckError ()
+validateMembership (DirectServer _) _ = Right ()
+validateMembership (ReplicatedCluster _ replicas) outcome = case outcome of
+  AcknowledgedSuccess (Rows [row]) | get "replicas" row == Just (NE.length replicas), get "shards" row == Just (1 :: Int) -> Right ()
+  AcknowledgedSuccess _ -> Left (TargetRejected (TransportError ReplicaSetMismatch Nothing Nothing))
+  failure -> Left (ProbeFailure failure)
+
+completionPolicyV1 :: ByteString
+completionPolicyV1 = "clickhouse-migrate/local-sync-v1;async_insert=0;mutations_sync=1;alter_sync=1"
+
+completionPolicy :: TargetTopology -> ByteString
+completionPolicy (DirectServer _) = completionPolicyV1
+completionPolicy (ReplicatedCluster _ _) = completionPolicyV2
+
+checksumFormat :: TargetTopology -> ChecksumFormat
+checksumFormat (DirectServer _) = ChecksumV1
+checksumFormat (ReplicatedCluster _ _) = ChecksumV2
+
+databaseEngine :: TargetTopology -> Text
+databaseEngine (DirectServer _) = "Atomic"
+databaseEngine (ReplicatedCluster _ _) = "Replicated"
+
+quoteIdentifier :: Text -> Text
+quoteIdentifier value = "`" <> Text.replace "`" "``" value <> "`"
+
+createDatabase :: TargetTopology -> LedgerNamespace -> Maybe ByteString
+createDatabase (DirectServer _) namespace = Just (Encoding.encodeUtf8 ("CREATE DATABASE IF NOT EXISTS " <> quoteIdentifier (ledgerNamespaceText namespace) <> " ENGINE=Atomic"))
+createDatabase (ReplicatedCluster _ _) _ = Nothing
+
+createTable :: TargetTopology -> LedgerNamespace -> ByteString
+createTable topology namespace = Encoding.encodeUtf8 ("CREATE TABLE IF NOT EXISTS " <> (quoteIdentifier (ledgerNamespaceText namespace) <> ".`events`") <> " " <> columnsSql <> " ENGINE=" <> tableEngine topology <> " ORDER BY sequence SETTINGS fsync_after_insert=1, fsync_part_directory=1, storage_policy='default'")
+
+columnsSql :: Text
+columnsSql = "(sequence UInt64, event_id UUID, ledger_id UUID, event String)"
+
+expectedCreate :: TargetTopology -> LedgerNamespace -> Text
+expectedCreate topology namespace = "CREATE TABLE " <> ledgerNamespaceText namespace <> ".events (`sequence` UInt64, `event_id` UUID, `ledger_id` UUID, `event` String) ENGINE = " <> expectedEngine topology
+
+expectedEngine :: TargetTopology -> Text
+expectedEngine topology = tableEngine topology <> engineTail
+
+engineTail :: Text
+engineTail = " ORDER BY sequence SETTINGS fsync_after_insert = 1, fsync_part_directory = 1, storage_policy = 'default', index_granularity = 8192"
+
+tableEngine :: TargetTopology -> Text
+tableEngine (DirectServer _) = "MergeTree"
+tableEngine (ReplicatedCluster _ _) = "ReplicatedMergeTree"
+
+-- | Entire direct physical shape, including accepted server identifier quoting.
+validTableShape :: TargetTopology -> LedgerNamespace -> [Text] -> Bool
+validTableShape topology@(DirectServer _) namespace actual =
+  let base = ["events", "MergeTree", expectedEngine topology, "sequence", "sequence", "", ""]
+      create = expectedCreate topology namespace
+      quoted = Text.replace ("CREATE TABLE " <> ledgerNamespaceText namespace <> ".") ("CREATE TABLE " <> quoteIdentifier (ledgerNamespaceText namespace) <> ".") create
+   in actual == base ++ [create] || actual == base ++ [quoted]
+validTableShape (ReplicatedCluster _ _) namespace actual = case actual of
+  ["events", "ReplicatedMergeTree", engine, "sequence", "sequence", "", "", create] ->
+    replicatedEngineSuffix engine == Just engineTail
+      && let prefix = "CREATE TABLE " <> ledgerNamespaceText namespace <> ".events (`sequence` UInt64, `event_id` UUID, `ledger_id` UUID, `event` String) ENGINE = "
+             quoted = Text.replace ("CREATE TABLE " <> ledgerNamespaceText namespace <> ".") ("CREATE TABLE " <> quoteIdentifier (ledgerNamespaceText namespace) <> ".") prefix
+          in create == prefix <> engine || create == quoted <> engine
+  _ -> False
+
+replicaHealthQuery :: TargetTopology -> LedgerNamespace -> Maybe ByteString
+replicaHealthQuery (DirectServer _) _ = Nothing
+replicaHealthQuery (ReplicatedCluster _ _) (LedgerNamespace namespace) = Just (Encoding.encodeUtf8 ("SELECT is_readonly, total_replicas FROM system.replicas WHERE database='" <> namespace <> "' AND table='events'"))
+
+validReplicaHealth :: TargetTopology -> [Object] -> Bool
+validReplicaHealth (DirectServer _) _ = True
+validReplicaHealth (ReplicatedCluster _ replicas) [row] = get "is_readonly" row == Just (0 :: Int) && get "total_replicas" row == Just (NE.length replicas)
+validReplicaHealth _ _ = False
+
+requiresLedgerDatabase :: TargetTopology -> Bool
+requiresLedgerDatabase (DirectServer _) = False
+requiresLedgerDatabase (ReplicatedCluster _ _) = True
+
+completionPolicyV2 :: ByteString
+completionPolicyV2 = "clickhouse-migrate/replicated-sync-v1;async_insert=0;mutations_sync=2;alter_sync=2;insert_quorum=all;insert_quorum_parallel=0;distributed_ddl_output_mode=none;select_sequential_consistency=1"
+
+policyForFormat :: ChecksumFormat -> ByteString
+policyForFormat ChecksumV1 = completionPolicyV1
+policyForFormat ChecksumV2 = completionPolicyV2
diff --git a/src/ClickHouseMigrate/Internal/SqlEnvelope.hs b/src/ClickHouseMigrate/Internal/SqlEnvelope.hs
--- a/src/ClickHouseMigrate/Internal/SqlEnvelope.hs
+++ b/src/ClickHouseMigrate/Internal/SqlEnvelope.hs
@@ -1,5 +1,5 @@
 -- | Conservative lexical envelope; never a general SQL safety proof.
-module ClickHouseMigrate.Internal.SqlEnvelope (validateSql, mentionsIdentifier) where
+module ClickHouseMigrate.Internal.SqlEnvelope (validateSql, mentionsIdentifier, createsDatabase, validateReplicatedPolicy, replicatedEngines, replicatedEngineSuffix) where
 
 import ClickHouseMigrate.Error
 import ClickHouseMigrate.Prelude
@@ -116,7 +116,7 @@
   case find protected tokens of
     Just (Token n _) -> failure ProtectedSetting (Just n)
     Nothing -> pure ()
-  case find forbidden (localViewHeader tokens) of
+  case find forbidden (withoutReplicatedEngines (localViewHeader tokens)) of
     Just (Token n _) -> failure UnsupportedSql (Just n)
     Nothing -> pure ()
   if any onCluster (pairs tokens) || quotedSettings tokens then failure UnsupportedSql Nothing else pure ()
@@ -146,6 +146,22 @@
 protectedNames :: [Text]
 protectedNames = ["ASYNC_INSERT", "MUTATIONS_SYNC", "ALTER_SYNC"]
 
+-- Target-independent binding keeps all previously valid v1 definitions valid.
+-- Additional replicated policy identifiers are refused only for pending work on
+-- that topology, before any ledger/bootstrap or application write.
+validateReplicatedPolicy :: ByteString -> Either DefinitionError ()
+validateReplicatedPolicy sql = do
+  text <- either (const (failure InvalidUtf8 Nothing)) Right (Encoding.decodeUtf8' sql)
+  tokens <- scan 0 (Text.unpack text)
+  case find protected tokens of
+    Just (Token offset _) -> failure ProtectedSetting (Just offset)
+    Nothing -> Right ()
+  where
+    names = ["INSERT_QUORUM", "INSERT_QUORUM_PARALLEL", "INSERT_QUORUM_TIMEOUT", "DISTRIBUTED_DDL_OUTPUT_MODE", "DISTRIBUTED_DDL_TASK_TIMEOUT", "REPLICATION_WAIT_FOR_INACTIVE_REPLICA_TIMEOUT", "SELECT_SEQUENTIAL_CONSISTENCY"]
+    protected (Token _ (Word name)) = name `elem` names
+    protected (Token _ (Identifier name)) = name `elem` names
+    protected _ = False
+
 forbiddenWords :: [Text]
 forbiddenWords =
   [ "FORMAT",
@@ -210,3 +226,44 @@
       matches (Token _ (Identifier value)) = wanted `elem` Text.splitOn "." value
       matches _ = False
   pure (any matches tokens)
+
+replicatedEngines :: [Text]
+replicatedEngines = ["REPLICATEDMERGETREE", "REPLICATEDREPLACINGMERGETREE", "REPLICATEDAGGREGATINGMERGETREE", "REPLICATEDSUMMINGMERGETREE", "REPLICATEDCOLLAPSINGMERGETREE", "REPLICATEDVERSIONEDCOLLAPSINGMERGETREE"]
+
+-- Remove only an engine word in the table/MV engine position. Quoted engine
+-- spellings, explicit string arguments and every other occurrence stay forbidden.
+withoutReplicatedEngines :: [Token] -> [Token]
+withoutReplicatedEngines tokens
+  | tableCreate tokens = walk tokens
+  | otherwise = tokens
+  where
+    tableCreate (a : b : _) | keyword "CREATE" a && keyword "TABLE" b = True
+    tableCreate (a : b : c : _) = keyword "CREATE" a && keyword "MATERIALIZED" b && keyword "VIEW" c
+    tableCreate _ = False
+    walk (engine : equal : Token _ (Word word) : rest)
+      | keyword "ENGINE" engine && equalIs equal && word `elem` replicatedEngines && allowed rest = engine : equal : walk rest
+    walk (engine : Token _ (Word word) : rest)
+      | keyword "ENGINE" engine && word `elem` replicatedEngines && allowed rest = engine : walk rest
+    walk (token : rest) = token : walk rest
+    walk [] = []
+    equalIs (Token _ (Symbol '=')) = True
+    equalIs _ = False
+    allowed (Token _ (Symbol '(') : Token _ Literal : _) = False
+    allowed _ = True
+
+createsDatabase :: ByteString -> Either DefinitionError Bool
+createsDatabase sql = do
+  text <- either (const (failure InvalidUtf8 Nothing)) Right (Encoding.decodeUtf8' sql)
+  tokens <- scan 0 (Text.unpack text)
+  pure (case tokens of a : b : _ -> keyword "CREATE" a && keyword "DATABASE" b; _ -> False)
+
+-- Validate the server's canonical argument prefix, preserving the suffix bytes.
+replicatedEngineSuffix :: Text -> Maybe Text
+replicatedEngineSuffix text = case scan 0 (Text.unpack text) of
+  Right (Token _ (Word engine) : Token _ (Symbol '(') : Token first Literal : Token _ (Symbol ',') : Token second Literal : Token end (Symbol ')') : _)
+    | engine == "REPLICATEDMERGETREE",
+      engine `elem` replicatedEngines,
+      Text.isPrefixOf "'" (Text.drop first text),
+      Text.isPrefixOf "'" (Text.drop second text) ->
+        Just (Text.drop (end + 1) text)
+  _ -> Nothing
diff --git a/src/ClickHouseMigrate/Internal/TargetTypes.hs b/src/ClickHouseMigrate/Internal/TargetTypes.hs
--- a/src/ClickHouseMigrate/Internal/TargetTypes.hs
+++ b/src/ClickHouseMigrate/Internal/TargetTypes.hs
@@ -21,6 +21,18 @@
 
 data HttpLimits = HttpLimits !Int !Int deriving stock (Eq, Show)
 
-data TargetConfig = TargetConfig !TargetKey !DirectEndpoint !ServerId !LedgerNamespace !LedgerBinding !Credentials !HttpLimits
+data ClusterName = ClusterName !Text deriving stock (Eq, Ord, Show)
+
+data TargetTopology = DirectServer !ServerId | ReplicatedCluster !ClusterName !(NonEmpty ServerId) deriving stock (Eq, Show)
+
+data TargetConfig = TargetConfig
+  { configKey :: !TargetKey,
+    configEndpoint :: !DirectEndpoint,
+    configTopology :: !TargetTopology,
+    configNamespace :: !LedgerNamespace,
+    configBinding :: !LedgerBinding,
+    configCredentials :: !Credentials,
+    configLimits :: !HttpLimits
+  }
 
 data ServerIdentity = ServerIdentity !ServerId !Text !Text deriving stock (Eq, Show)
diff --git a/src/ClickHouseMigrate/Internal/Transport.hs b/src/ClickHouseMigrate/Internal/Transport.hs
--- a/src/ClickHouseMigrate/Internal/Transport.hs
+++ b/src/ClickHouseMigrate/Internal/Transport.hs
@@ -9,7 +9,7 @@
 
 data ResponsePayload = CommandAccepted | Rows ![Object] deriving stock (Eq, Show)
 
-data TransportErrorCode = AdministrativeAccessLost | InvalidRequest | ClientScopeFailed | ConnectionFailed | DeadlineExpired | ResponseInterrupted | ResponseTooLarge | UnexpectedResponse | ServerRejected | RedirectRejected | ServerIdentityMismatch | UnsupportedServer | CompletionPolicyMismatch | CleanupFailed | CleanupTimedOut
+data TransportErrorCode = AdministrativeAccessLost | InvalidRequest | ClientScopeFailed | ConnectionFailed | DeadlineExpired | ResponseInterrupted | ResponseTooLarge | UnexpectedResponse | ServerRejected | RedirectRejected | ServerIdentityMismatch | UnsupportedServer | CompletionPolicyMismatch | ReplicaSetMismatch | CleanupFailed | CleanupTimedOut
   deriving stock (Eq, Ord, Show)
 
 -- Only safe structured diagnostics; raw server/HTTP exceptions are never rendered.
diff --git a/src/ClickHouseMigrate/Internal/Types.hs b/src/ClickHouseMigrate/Internal/Types.hs
--- a/src/ClickHouseMigrate/Internal/Types.hs
+++ b/src/ClickHouseMigrate/Internal/Types.hs
@@ -20,14 +20,14 @@
 
 data MigrationPlan = MigrationPlan !(NonEmpty ComponentView) !(NonEmpty MigrationView) deriving stock (Eq)
 
-data ChecksumFormat = ChecksumV1 deriving stock (Eq, Ord, Show)
+data ChecksumFormat = ChecksumV1 | ChecksumV2 deriving stock (Eq, Ord, Show)
 
-data StepChecksum = StepChecksum !ByteString deriving stock (Eq, Ord)
+data StepChecksum = StepChecksum !ByteString | StepChecksumV2 !ByteString deriving stock (Eq, Ord)
 
-data MigrationChecksum = MigrationChecksum !ByteString deriving stock (Eq, Ord)
+data MigrationChecksum = MigrationChecksum !ByteString | MigrationChecksumV2 !ByteString deriving stock (Eq, Ord)
 
 data ComponentView = ComponentView !ComponentName !(Set ComponentName) !(NonEmpty MigrationView) deriving stock (Eq)
 
-data MigrationView = MigrationView !MigrationId !(Maybe Text) !(NonEmpty StepView) !MigrationChecksum deriving stock (Eq)
+data MigrationView = MigrationView !MigrationId !(Maybe Text) !(NonEmpty StepView) !MigrationChecksum !MigrationChecksum deriving stock (Eq)
 
-data StepView = StepView !StepName !ByteString !StepChecksum deriving stock (Eq)
+data StepView = StepView !StepName !ByteString !StepChecksum !StepChecksum deriving stock (Eq)
diff --git a/src/ClickHouseMigrate/Ledger.hs b/src/ClickHouseMigrate/Ledger.hs
--- a/src/ClickHouseMigrate/Ledger.hs
+++ b/src/ClickHouseMigrate/Ledger.hs
@@ -57,12 +57,12 @@
 
 -- | Read-only exact-event reconciliation. NotObserved never authorizes a write.
 reconcileAppend :: TargetConfig -> ExclusiveAccess -> AppendReceipt -> IO (LedgerCommandResult (Reconciliation, ExpectedLedgerState))
-reconcileAppend config@(TargetConfig target _ _ _ _ _ _) provider receipt = command config provider $ \guard transport -> do
+reconcileAppend config@(TargetConfig {configKey = target}) provider receipt = command config provider $ \guard transport -> do
   (classification, observation) <- Store.reconcile guard transport config receipt
   pure (classification, case observation of Store.History state -> ExpectPosition (position state); _ -> ExpectAbsent target)
 
 command :: TargetConfig -> ExclusiveAccess -> (Guard -> Transport -> IO a) -> IO (LedgerCommandResult a)
-command config@(TargetConfig target _ _ _ _ _ _) provider@(ExclusiveAccess description _ _ _ _) action = do
+command config@(TargetConfig {configKey = target}) provider@(ExclusiveAccess description _ _ _ _) action = do
   observed <- try @SomeException $ do
     probe <- freshUuid >>= either (const (fail "Invalid diagnostic identity")) pure . queryId . ("chm-" <>)
     withExclusive provider target $ \guard ->
diff --git a/src/ClickHouseMigrate/Plan.hs b/src/ClickHouseMigrate/Plan.hs
--- a/src/ClickHouseMigrate/Plan.hs
+++ b/src/ClickHouseMigrate/Plan.hs
@@ -16,13 +16,15 @@
     migrationDescription,
     migrationSteps,
     migrationChecksum,
+    migrationChecksumFor,
     StepView,
     stepViewName,
     stepSql,
     stepChecksum,
+    stepChecksumFor,
     StepChecksum,
     MigrationChecksum,
-    ChecksumFormat (ChecksumV1),
+    ChecksumFormat (ChecksumV1, ChecksumV2),
     checksumFormatName,
     parseChecksumFormat,
     stepChecksumFormat,
@@ -133,11 +135,12 @@
 bindComponent (MigrationComponent component deps migrations) = ComponentView component deps (fmap bindMigration migrations)
   where
     bindMigration (Migration name description steps) =
-      let encodings = fmap (encodeStep name) steps
-          boundSteps = NE.zipWith (\(SqlStep step sql) encoded -> StepView step sql (StepChecksum (Checksum.digest encoded))) steps encodings
-          checksum = MigrationChecksum (Checksum.digest (Checksum.encodeMigrationV1 (componentNameText component) (migrationNameText name) (NE.toList encodings)))
-       in MigrationView (MigrationId component name) description boundSteps checksum
-    encodeStep migration (SqlStep step sql) = Checksum.encodeStepV1 (componentNameText component) (migrationNameText migration) (stepNameText step) sql
+      let encode encoder (SqlStep step sql) = encoder (componentNameText component) (migrationNameText name) (stepNameText step) sql
+          encodingsV1 = fmap (encode Checksum.encodeStepV1) steps
+          encodingsV2 = fmap (encode Checksum.encodeStepV2) steps
+          boundSteps = NE.zipWith (\(SqlStep step sql) (v1, v2) -> StepView step sql (StepChecksum (Checksum.digest v1)) (StepChecksumV2 (Checksum.digest v2))) steps (NE.zip encodingsV1 encodingsV2)
+          checksum encoder encodings = Checksum.digest (encoder (componentNameText component) (migrationNameText name) (NE.toList encodings))
+       in MigrationView (MigrationId component name) description boundSteps (MigrationChecksum (checksum Checksum.encodeMigrationV1 encodingsV1)) (MigrationChecksumV2 (checksum Checksum.encodeMigrationV2 encodingsV2))
 
 inspectPlan :: MigrationPlan -> NonEmpty ComponentView
 inspectPlan (MigrationPlan components _) = components
@@ -152,44 +155,58 @@
 componentMigrations (ComponentView _ _ migrations) = migrations
 
 migrationId :: MigrationView -> MigrationId
-migrationId (MigrationView identity _ _ _) = identity
+migrationId (MigrationView identity _ _ _ _) = identity
 
 migrationDescription :: MigrationView -> Maybe Text
-migrationDescription (MigrationView _ description _ _) = description
+migrationDescription (MigrationView _ description _ _ _) = description
 
 migrationSteps :: MigrationView -> NonEmpty StepView
-migrationSteps (MigrationView _ _ steps _) = steps
+migrationSteps (MigrationView _ _ steps _ _) = steps
 
 migrationChecksum :: MigrationView -> MigrationChecksum
-migrationChecksum (MigrationView _ _ _ checksum) = checksum
+migrationChecksum (MigrationView _ _ _ checksum _) = checksum
 
 stepViewName :: StepView -> StepName
-stepViewName (StepView name _ _) = name
+stepViewName (StepView name _ _ _) = name
 
 stepSql :: StepView -> ByteString
-stepSql (StepView _ sql _) = sql
+stepSql (StepView _ sql _ _) = sql
 
 stepChecksum :: StepView -> StepChecksum
-stepChecksum (StepView _ _ checksum) = checksum
+stepChecksum (StepView _ _ checksum _) = checksum
 
 checksumFormatName :: ChecksumFormat -> Text
 checksumFormatName ChecksumV1 = "sha256-v1"
+checksumFormatName ChecksumV2 = "sha256-v2"
 
 parseChecksumFormat :: Text -> Either DefinitionError ChecksumFormat
 parseChecksumFormat "sha256-v1" = Right ChecksumV1
+parseChecksumFormat "sha256-v2" = Right ChecksumV2
 parseChecksumFormat _ = Left (DefinitionError UnknownChecksumFormat ["checksum", "format"] Nothing)
 
 stepChecksumFormat :: StepChecksum -> ChecksumFormat
-stepChecksumFormat _ = ChecksumV1
+stepChecksumFormat (StepChecksum _) = ChecksumV1
+stepChecksumFormat (StepChecksumV2 _) = ChecksumV2
 
 migrationChecksumFormat :: MigrationChecksum -> ChecksumFormat
-migrationChecksumFormat _ = ChecksumV1
+migrationChecksumFormat (MigrationChecksum _) = ChecksumV1
+migrationChecksumFormat (MigrationChecksumV2 _) = ChecksumV2
 
 stepChecksumHex :: StepChecksum -> Text
 stepChecksumHex (StepChecksum bytes) = Checksum.digestHex bytes
+stepChecksumHex (StepChecksumV2 bytes) = Checksum.digestHex bytes
 
 migrationChecksumHex :: MigrationChecksum -> Text
 migrationChecksumHex (MigrationChecksum bytes) = Checksum.digestHex bytes
+migrationChecksumHex (MigrationChecksumV2 bytes) = Checksum.digestHex bytes
 
 completionPolicy :: ByteString
 completionPolicy = Checksum.completionPolicyV1
+
+migrationChecksumFor :: ChecksumFormat -> MigrationView -> MigrationChecksum
+migrationChecksumFor ChecksumV1 = migrationChecksum
+migrationChecksumFor ChecksumV2 = \(MigrationView _ _ _ _ checksum) -> checksum
+
+stepChecksumFor :: ChecksumFormat -> StepView -> StepChecksum
+stepChecksumFor ChecksumV1 = stepChecksum
+stepChecksumFor ChecksumV2 = \(StepView _ _ _ checksum) -> checksum
diff --git a/src/ClickHouseMigrate/Repair.hs b/src/ClickHouseMigrate/Repair.hs
--- a/src/ClickHouseMigrate/Repair.hs
+++ b/src/ClickHouseMigrate/Repair.hs
@@ -75,7 +75,7 @@
   FinalizeRequest position attempt evidence -> (position, attempt, evidence)
 
 repair :: MutationContext -> Plan.MigrationPlan -> RepairRequest -> IO (Either R.RunFailure R.RunReport)
-repair context@(MutationContext (T.TargetConfig target _ _ _ binding _ _) _ _ _) plan request =
+repair context@(MutationContext (T.TargetConfig {T.configKey = target, T.configBinding = binding}) _ _ _) plan request =
   let (expected@(L.LedgerPosition ledger _ _), requested, RecoveryEvidence audit quiet receipts) = requestParts request
       prerequisite = case binding of
         T.FirstUse -> Left R.ExistingBindingRequired
@@ -94,7 +94,7 @@
         let queries = Set.toAscList (Set.fromList ([T.QueryId query | event <- L.events projection, L.StartStep _ query <- [L.body event]] ++ [query | L.AppendReceipt _ _ _ _ _ query <- receipts]))
         active <- activeQueries runtime queries
         unless (null active) (abort (R.PriorQueriesActive active))
-        migration <- case drop (L.ordinal definition) (orderedMigrations plan) of
+        migration <- case drop (L.ordinal definition) (orderedMigrations (L.projectionChecksumFormat projection) plan) of
           (actual, migration) : _ | actual == definition -> pure migration
           _ -> abort (R.LedgerFailed L.DefinitionMismatch)
         (action, selection, query) <- case request of
diff --git a/src/ClickHouseMigrate/Report.hs b/src/ClickHouseMigrate/Report.hs
--- a/src/ClickHouseMigrate/Report.hs
+++ b/src/ClickHouseMigrate/Report.hs
@@ -72,6 +72,7 @@
 
 data HistoryBody
   = LedgerInitialized !TargetKey
+  | LedgerInitializedReplicated !TargetKey
   | MigrationDefinition !DefinitionSummary
   | StepIntent !Int !QueryId
   | StepCompletion !Int !QueryId
@@ -155,6 +156,7 @@
 data RunError
   = InvalidRuntimePlan !StepReference
   | LedgerNamespaceInSql !StepReference
+  | DatabaseCreationOnReplicatedTarget !StepReference
   | DefinitionTooLarge !DefinitionSummary
   | AccessFailed !AccessError
   | TargetFailed !TransportError
@@ -191,6 +193,7 @@
 
 historyKind :: HistoryBody -> Text
 historyKind = \case
+  LedgerInitializedReplicated _ -> "initialized"
   LedgerInitialized _ -> "initialized"
   MigrationDefinition _ -> "migration-started"
   StepIntent _ _ -> "step-started"
diff --git a/src/ClickHouseMigrate/Runner.hs b/src/ClickHouseMigrate/Runner.hs
--- a/src/ClickHouseMigrate/Runner.hs
+++ b/src/ClickHouseMigrate/Runner.hs
@@ -33,4 +33,4 @@
 up context (UpOptions audit authorization) plan = withRuntime context audit authorization Nothing plan $ \runtime -> do
   projection <- runtimeProjection runtime
   when (unresolved projection) (abort (LedgerFailed L.UnresolvedHistory))
-  mapM_ (uncurry (executeMigration runtime)) (drop (length (L.completed projection)) (orderedMigrations plan))
+  mapM_ (uncurry (executeMigration runtime)) (drop (length (L.completed projection)) (orderedMigrations (L.projectionChecksumFormat projection) plan))
diff --git a/src/ClickHouseMigrate/Target.hs b/src/ClickHouseMigrate/Target.hs
--- a/src/ClickHouseMigrate/Target.hs
+++ b/src/ClickHouseMigrate/Target.hs
@@ -27,6 +27,12 @@
     defaultHttpLimits,
     TargetConfig,
     targetConfig,
+    TargetTopology (DirectServer, ReplicatedCluster),
+    ClusterName,
+    clusterName,
+    clusterNameText,
+    replicatedTargetConfig,
+    configuredTopology,
     configuredTargetKey,
     configuredLedgerBinding,
     TargetError (..),
@@ -41,11 +47,13 @@
 import ClickHouseMigrate.Internal.TargetTypes
 import ClickHouseMigrate.Prelude
 import Data.Char (isAsciiLower, isAsciiUpper)
+import Data.List.NonEmpty qualified as NE
+import Data.Set qualified as Set
 import Data.Text qualified as Text
 import Data.Text.Encoding qualified as Encoding
 import Text.Read (readMaybe)
 
-data TargetError = InvalidTargetKey | InvalidEndpoint | InvalidServerId | InvalidQueryId | InvalidLedgerId | InvalidLedgerNamespace | InvalidCredentials | InvalidHttpLimits
+data TargetError = InvalidTargetKey | InvalidEndpoint | InvalidServerId | InvalidQueryId | InvalidLedgerId | InvalidLedgerNamespace | InvalidCredentials | InvalidHttpLimits | InvalidClusterName | DuplicateReplica
   deriving stock (Eq, Ord, Show)
 
 targetKey :: Text -> Either TargetError TargetKey
@@ -103,7 +111,7 @@
 defaultHttpLimits = HttpLimits 30000000 1048576
 
 targetConfig :: TargetKey -> DirectEndpoint -> ServerId -> LedgerNamespace -> LedgerBinding -> Credentials -> HttpLimits -> TargetConfig
-targetConfig = TargetConfig
+targetConfig key endpoint identity = TargetConfig key endpoint (DirectServer identity)
 
 targetKeyText :: TargetKey -> Text
 targetKeyText (TargetKey value) = value
@@ -151,8 +159,28 @@
 
 -- | Read-only nonsecret configuration identity for application adapters.
 configuredTargetKey :: TargetConfig -> TargetKey
-configuredTargetKey (TargetConfig key _ _ _ _ _ _) = key
+configuredTargetKey = configKey
 
 -- | Binding mode is explicit; this accessor grants no initialization authority.
 configuredLedgerBinding :: TargetConfig -> LedgerBinding
-configuredLedgerBinding (TargetConfig _ _ _ _ binding _ _) = binding
+configuredLedgerBinding = configBinding
+
+-- | The admitted server topology, without credentials.
+configuredTopology :: TargetConfig -> TargetTopology
+configuredTopology = configTopology
+
+clusterName :: Text -> Either TargetError ClusterName
+clusterName value
+  | valid 128 (\c -> lowerDigit c || c `elem` ("_-" :: String)) value && maybe False (\(c, _) -> isAsciiLower c || c == '_') (Text.uncons value) = Right (ClusterName value)
+  | otherwise = Left InvalidClusterName
+
+clusterNameText :: ClusterName -> Text
+clusterNameText (ClusterName value) = value
+
+-- | Every expected replica participates in acknowledged writes. The deadline must
+-- leave room for bounded server waits and a complete HTTP error response.
+replicatedTargetConfig :: TargetKey -> DirectEndpoint -> ClusterName -> NonEmpty ServerId -> LedgerNamespace -> LedgerBinding -> Credentials -> HttpLimits -> Either TargetError TargetConfig
+replicatedTargetConfig key endpoint cluster replicas namespace binding credentials limits@(HttpLimits micros _)
+  | Set.size (Set.fromList (NE.toList replicas)) /= NE.length replicas = Left DuplicateReplica
+  | micros < 5000000 = Left InvalidHttpLimits
+  | otherwise = Right (TargetConfig key endpoint (ReplicatedCluster cluster replicas) namespace binding credentials limits)
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -5,8 +5,11 @@
 import ClickHouseMigrate.Internal.Checksum qualified as Checksum
 import ClickHouseMigrate.Internal.Http (readJsonRowStream)
 import ClickHouseMigrate.Internal.LedgerCodec qualified as Codec
+import ClickHouseMigrate.Internal.Preflight qualified as Preflight
+import ClickHouseMigrate.Internal.ServerProfile qualified as Profile
 import ClickHouseMigrate.Internal.Transport qualified as Transport
-import Control.Monad (forM_, unless)
+import ClickHouseMigrate.Report qualified as Report
+import Control.Monad (forM_, unless, void)
 import Data.Aeson qualified as Aeson
 import Data.ByteString (ByteString)
 import Data.ByteString qualified as BS
@@ -45,6 +48,8 @@
   streamValidation
   scheduleValidation
   targetValidation
+  directProfilePins
+  replicatedTargetValidation
   let sql = "CREATE TABLE app.events (id UInt64) ENGINE=MergeTree ORDER BY id"
   create <- right (sqlStep "create" sql)
   migration <- right (sqlMigration "0001" (create :| []))
@@ -80,7 +85,7 @@
     assert "multiple SQL statements" (isDefinitionError MultipleStatements (sqlStep "x" multiple))
   forM_ ["INSERT INTO app.t VALUES ('open", "INSERT INTO app.t VALUES ($x$open)", "CREATE TABLE app.t (x UInt8) /*", "CREATE TABLE app.\"t (x UInt8)"] $ \unterminated ->
     assert "unterminated SQL construct" (isDefinitionError UnterminatedSql (sqlStep "x" unterminated))
-  forM_ ["BEGIN", "COMMIT", "ROLLBACK", "USE app", "SET x=1", "SELECT 1", "SYSTEM FLUSH LOGS", "CREATE TABLE app.t ON /* gap */ CLUSTER c (x UInt8)", "INSERT INTO app.t FORMAT CSV\n1", "CREATE MATERIALIZED VIEW app.v REFRESH EVERY 1 SECOND AS SELECT 1", "ALTER TABLE app.t MOVE PARTITION 1 TO TABLE app.other", "CREATE TABLE app.t ENGINE=ReplicatedMergeTree ORDER BY tuple()", "CREATE TABLE unqualified (x UInt8)", "\\i file.sql"] $ \unsupported ->
+  forM_ ["BEGIN", "COMMIT", "ROLLBACK", "USE app", "SET x=1", "SELECT 1", "SYSTEM FLUSH LOGS", "CREATE TABLE app.t ON /* gap */ CLUSTER c (x UInt8)", "INSERT INTO app.t FORMAT CSV\n1", "CREATE MATERIALIZED VIEW app.v REFRESH EVERY 1 SECOND AS SELECT 1", "ALTER TABLE app.t MOVE PARTITION 1 TO TABLE app.other", "CREATE TABLE unqualified (x UInt8)", "\\i file.sql"] $ \unsupported ->
     assert "unsupported SQL" (isDefinitionError UnsupportedSql (sqlStep "x" unsupported))
   forM_ ["async_insert", "mutations_sync", "alter_sync", "AsYnC_InSeRt", "`alter_sync`", "\"mutations_sync\""] $ \setting ->
     assert "protected setting" (isDefinitionError ProtectedSetting (sqlStep "x" (Encoding.encodeUtf8 ("INSERT INTO app.t VALUES (1) SETTINGS " <> setting <> " /*comment*/ = 0"))))
@@ -90,6 +95,8 @@
     c <- right (migrationComponent "c" Set.empty (m :| []))
     p <- right (migrationPlan (c :| []))
     assert "SQL exact bytes preserved" (stepSql (NE.head (migrationSteps (NE.head (componentMigrations (NE.head (inspectPlan p)))))) == valid)
+  forM_ ["CREATE TABLE app.t (x UInt64) ENGINE=ReplicatedMergeTree ORDER BY x", "CREATE TABLE app.t (x UInt64, updated_at UInt64) ENGINE=ReplicatedReplacingMergeTree(updated_at) ORDER BY x", "CREATE MATERIALIZED VIEW app.v ENGINE ReplicatedAggregatingMergeTree ORDER BY x AS SELECT x FROM app.t"] $ \sql -> void (right (sqlStep "replicated" sql))
+  forM_ ["CREATE TABLE app.t (x UInt8) ENGINE=ReplicatedMergeTree('/keeper','replica') ORDER BY x", "CREATE DATABASE app ENGINE=Replicated", "CREATE TABLE app.\"ReplicatedMergeTree\" (x UInt8) ENGINE=Memory", "CREATE TABLE app.t (x UInt8) ENGINE=Distributed(c,app,t)"] $ \sql -> assert "replicated envelope refuses unsafe form" (either (const True) (const False) (sqlStep "replicated" sql))
   -- Boundary combinations cover escaped delimiters, comment forms and heredoc tags.
   forM_ [";", "'; DROP TABLE app.t; --", "/* ; */", "雪;", "\\;", "# ;"] $ \payload ->
     forM_ ["tag", "other", "_x9"] $ \tag -> do
@@ -100,18 +107,20 @@
   let firstMigration = NE.head . componentMigrations . NE.head . inspectPlan
   assert "description excluded from checksum" (migrationChecksum (firstMigration plan) == migrationChecksum (firstMigration describedPlan))
   assert "description available" (migrationDescription (firstMigration describedPlan) == Just "metadata")
-  assert "unknown checksum format" (isDefinitionError UnknownChecksumFormat (parseChecksumFormat "sha256-v2"))
+  assert "unknown checksum format" (isDefinitionError UnknownChecksumFormat (parseChecksumFormat "sha256-v3"))
   assert "known checksum format" (parseChecksumFormat "sha256-v1" == Right ChecksumV1)
   vectorLines <- filter (not . Text.isPrefixOf "#") . Text.lines <$> Text.readFile "test/vectors/checksum-v1.tsv"
-  forM_ vectorLines checkVector
-  putStrLn "Definition, ordering, SQL boundary and 24 independent checksum-vector checks passed."
+  forM_ vectorLines (checkVectorFor ChecksumV1)
+  vectorV2 <- filter (not . Text.isPrefixOf "#") . Text.lines <$> Text.readFile "test/vectors/checksum-v2.tsv"
+  forM_ vectorV2 (checkVectorFor ChecksumV2)
+  putStrLn "Definition, ordering, SQL boundary and 48 independent checksum-vector checks passed."
 
-checkVector :: Text -> IO ()
-checkVector line = case Text.splitOn "\t" line of
+checkVectorFor :: ChecksumFormat -> Text -> IO ()
+checkVectorFor format line = case Text.splitOn "\t" line of
   [label, kind, component, name, sources, expectedInput, expectedDigest] -> do
     steps <- mapM parseStep (Text.splitOn "," sources)
-    let encodedSteps = fmap (\(key, sql) -> Checksum.encodeStepV1 component name key sql) (NE.fromList steps)
-        input = if kind == "step" then NE.head encodedSteps else Checksum.encodeMigrationV1 component name (NE.toList encodedSteps)
+    let encodedSteps = fmap (\(key, sql) -> (if format == ChecksumV1 then Checksum.encodeStepV1 else Checksum.encodeStepV2) component name key sql) (NE.fromList steps)
+        input = if kind == "step" then NE.head encodedSteps else (if format == ChecksumV1 then Checksum.encodeMigrationV1 else Checksum.encodeMigrationV2) component name (NE.toList encodedSteps)
     expected <- unhex expectedInput
     assert (Text.unpack (label <> " encoded bytes")) (input == expected)
     assert (Text.unpack (label <> " SHA-256")) (Checksum.digestHex (Checksum.digest input) == expectedDigest)
@@ -120,7 +129,7 @@
     c <- right (migrationComponent component Set.empty (m :| []))
     p <- right (migrationPlan (c :| []))
     let bound = NE.head (componentMigrations (NE.head (inspectPlan p)))
-        actual = if kind == "step" then stepChecksumHex (stepChecksum (NE.head (migrationSteps bound))) else migrationChecksumHex (migrationChecksum bound)
+        actual = if kind == "step" then stepChecksumHex (stepChecksumFor format (NE.head (migrationSteps bound))) else migrationChecksumHex (migrationChecksumFor format bound)
     assert (Text.unpack (label <> " public bound checksum")) (actual == expectedDigest)
   _ -> fail "Invalid checked-in checksum vector"
   where
@@ -203,3 +212,66 @@
   late <- run 1024 [bytes <> "\nCode: 999. synthetic failure\n"]
   assert "late error discards rows" (case late of Transport.ObservedServerFailure (Transport.TransportError Transport.ServerRejected _ (Just 999)) -> True; _ -> False)
   assert "empty stream" . success 0 =<< run 1024 []
+
+-- Exact direct policy bytes are compatibility obligations, not inferred behavior.
+directProfilePins :: IO ()
+directProfilePins = do
+  identity <- right (serverId "00000000-0000-0000-0000-000000000000")
+  namespace <- right (ledgerNamespace "ledger")
+  let topology = DirectServer identity
+      base = [("query_id", Just "pin"), ("async_insert", Just "0"), ("mutations_sync", Just "1"), ("alter_sync", Just "1"), ("fsync_metadata", Just "1"), ("wait_end_of_query", Just "1"), ("default_format", Just "JSONEachRow")]
+      readSettings = [("max_rows_to_read", Just "0"), ("max_bytes_to_read", Just "0"), ("max_result_rows", Just "0"), ("max_result_bytes", Just "0"), ("read_overflow_mode", Just "throw"), ("result_overflow_mode", Just "throw"), ("timeout_overflow_mode", Just "throw"), ("sort_overflow_mode", Just "throw")]
+  assert "direct command settings byte pin" (Profile.requestSettings topology defaultHttpLimits "pin" Transport.Command == base)
+  forM_ [Transport.JsonRows, Transport.JsonRowStream] $ \kind ->
+    assert "direct read settings byte pin" (Profile.requestSettings topology defaultHttpLimits "pin" kind == base ++ readSettings)
+  assert "direct policy byte pin" (Profile.completionPolicy topology == "clickhouse-migrate/local-sync-v1;async_insert=0;mutations_sync=1;alter_sync=1")
+  assert "direct checksum format pin" (Profile.checksumFormat topology == ChecksumV1)
+  assert "direct database DDL pin" (Profile.createDatabase topology namespace == Just "CREATE DATABASE IF NOT EXISTS `ledger` ENGINE=Atomic")
+  assert "direct table DDL pin" (Profile.createTable topology namespace == "CREATE TABLE IF NOT EXISTS `ledger`.`events` (sequence UInt64, event_id UUID, ledger_id UUID, event String) ENGINE=MergeTree ORDER BY sequence SETTINGS fsync_after_insert=1, fsync_part_directory=1, storage_policy='default'")
+  assert "direct engine metadata pin" (Profile.expectedEngine topology == "MergeTree ORDER BY sequence SETTINGS fsync_after_insert = 1, fsync_part_directory = 1, storage_policy = 'default', index_granularity = 8192")
+  assert "direct create metadata pin" (Profile.expectedCreate topology namespace == "CREATE TABLE ledger.events (`sequence` UInt64, `event_id` UUID, `ledger_id` UUID, `event` String) ENGINE = MergeTree ORDER BY sequence SETTINGS fsync_after_insert = 1, fsync_part_directory = 1, storage_policy = 'default', index_granularity = 8192")
+
+replicatedTargetValidation :: IO ()
+replicatedTargetValidation = do
+  identity <- right (serverId "00000000-0000-0000-0000-000000000000")
+  key <- right (targetKey "replicated/unit")
+  endpoint <- right (directEndpoint "https://example.com")
+  cluster <- right (clusterName "tan-production")
+  namespace <- right (ledgerNamespace "ledger")
+  credentials <- right (basicCredentials "app" "password")
+  let build replicas limits = replicatedTargetConfig key endpoint cluster replicas namespace FirstUse credentials limits
+  assert "duplicate replicas rejected" (either (== DuplicateReplica) (const False) (build (identity :| [identity]) defaultHttpLimits))
+  short <- right (httpLimits 4999999 1024)
+  assert "replicated deadline leaves a response margin" (either (== InvalidHttpLimits) (const False) (build (identity :| []) short))
+  config <- right (build (identity :| []) defaultHttpLimits)
+  assert "replicated topology retained" (configuredTopology config == ReplicatedCluster cluster (identity :| []))
+  forM_ ["", "x' OR 1=1", "x.y", "-start", "雪"] $ \value -> assert "cluster name cannot inject SQL" (clusterName value == Left InvalidClusterName)
+  let topology = configuredTopology config
+      engine = "ReplicatedMergeTree('/operator/path', 'replica')" <> Profile.engineTail
+      create = "CREATE TABLE ledger.events (`sequence` UInt64, `event_id` UUID, `ledger_id` UUID, `event` String) ENGINE = " <> engine
+      shape = ["events", "ReplicatedMergeTree", engine, "sequence", "sequence", "", "", create]
+  assert "replicated engine metadata accepts operator paths" (Profile.validTableShape topology namespace shape)
+  assert "replicated metadata rejects trailing engine changes" (not (Profile.validTableShape topology namespace ["events", "ReplicatedMergeTree", engine <> " SETTINGS a=0", "sequence", "sequence", "", "", create]))
+
+  let probe actual version macro =
+        Aeson.object ([("server_id", Aeson.String actual), ("version", Aeson.String version), ("host", Aeson.String "owned-host"), ("cluster", Aeson.String macro), ("storage_ok", Aeson.Number 1)] ++ [(key, Aeson.String value) | (key, value) <- Profile.replicatedEffective topology])
+      outcome value = case Aeson.fromJSON value of Aeson.Success row -> Transport.AcknowledgedSuccess (Transport.Rows [row]); Aeson.Error _ -> error "Invalid probe fixture"
+      validate actual version macro = Profile.validateOutcome ["26.3.60.10002.altinitystable"] topology (outcome (probe actual version macro))
+      rejects code = \case Left (Profile.TargetRejected (Transport.TransportError actual _ _)) -> actual == code; _ -> False
+  assert "replicated probe admits expected socket" (fmap identityServerId (validate (serverIdText identity) "26.3.60.10002.altinitystable" "tan-production") == Right identity)
+  assert "replicated probe refuses a foreign socket" (rejects Transport.ServerIdentityMismatch (validate "11111111-1111-1111-1111-111111111111" "26.3.60.10002.altinitystable" "tan-production"))
+  assert "replicated probe refuses unqualified versions" (rejects Transport.UnsupportedServer (validate (serverIdText identity) "unlisted" "tan-production"))
+  assert "replicated probe refuses wrong cluster macros" (rejects Transport.ReplicaSetMismatch (validate (serverIdText identity) "26.3.60.10002.altinitystable" "other-cluster"))
+
+  let heredocEngine = "ReplicatedMergeTree($$path$$, $$replica$$)" <> Profile.engineTail
+      heredocCreate = "CREATE TABLE ledger.events (`sequence` UInt64, `event_id` UUID, `ledger_id` UUID, `event` String) ENGINE = " <> heredocEngine
+  assert "replicated metadata requires single-quoted engine arguments" (not (Profile.validTableShape topology namespace ["events", "ReplicatedMergeTree", heredocEngine, "sequence", "sequence", "", "", heredocCreate]))
+
+  guardedStep <- right (sqlStep "guarded" "INSERT INTO app.t VALUES (1) SETTINGS insert_quorum=0")
+  guardedMigration <- right (sqlMigration "guarded" (guardedStep :| []))
+  guardedComponent <- right (migrationComponent "guarded" Set.empty (guardedMigration :| []))
+  guardedPlan <- right (migrationPlan (guardedComponent :| []))
+  let direct = targetConfig key endpoint identity namespace FirstUse credentials defaultHttpLimits
+  assert "direct v1 definitions keep accepting unrelated settings" (Preflight.preflightPending direct guardedPlan 0 == Right ())
+  assert "replicated pending policy overrides rejected" (case Preflight.preflightPending config guardedPlan 0 of Left (Report.InvalidRuntimePlan _) -> True; _ -> False)
+  assert "applied replicated definitions are not rechecked" (Preflight.preflightPending config guardedPlan 1 == Right ())
diff --git a/test/public/TargetCoercion.hs b/test/public/TargetCoercion.hs
new file mode 100644
--- /dev/null
+++ b/test/public/TargetCoercion.hs
@@ -0,0 +1,7 @@
+module TargetCoercion where
+
+import ClickHouseMigrate.Target
+import Data.Coerce (coerce)
+
+invalid :: () -> TargetConfig
+invalid = coerce
diff --git a/test/public/TargetConstructor.hs b/test/public/TargetConstructor.hs
new file mode 100644
--- /dev/null
+++ b/test/public/TargetConstructor.hs
@@ -0,0 +1,5 @@
+module TargetConstructor where
+
+import ClickHouseMigrate.Target
+
+invalid = TargetConfig
diff --git a/test/vectors/checksum-v2.tsv b/test/vectors/checksum-v2.tsv
new file mode 100644
--- /dev/null
+++ b/test/vectors/checksum-v2.tsv
@@ -0,0 +1,25 @@
+# label	kind	component	migration	steps(name:sql-hex,...)	input-hex	sha256-hex
+baseline	step	analytics	0001	create:435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303100000000000000066372656174650000000000000040435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	8f88bdc18d2842dc722223c041a59aac87b84cfedecd4b04da172bc08bc59640
+baseline	migration	analytics	0001	create:435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	636c69636b686f7573652d6d6967726174652f6d6967726174696f6e0076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303100000000000000010000000000000154636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303100000000000000066372656174650000000000000040435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	55be6f229c0fec938ebb19639a460057a3f37de40737cd54ad75c1f61677d38f
+whitespace	step	analytics	0001	create:435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f524445522042592069640a	636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303100000000000000066372656174650000000000000041435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f524445522042592069640a	718eaeba20757b1b5a91a4a90f1628137d2bd9d7ed219723f35d9102c30810e8
+whitespace	migration	analytics	0001	create:435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f524445522042592069640a	636c69636b686f7573652d6d6967726174652f6d6967726174696f6e0076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303100000000000000010000000000000155636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303100000000000000066372656174650000000000000041435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f524445522042592069640a	a1681a577010e10da8cc19b50ffe9ae5ae78f5a2b8b71448fd6ce43f5ebf53d6
+component	step	analytics2	0001	create:435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d31000000000000000a616e616c79746963733200000000000000043030303100000000000000066372656174650000000000000040435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	c084dfe095d02b0747389e5a95cdadace41fe4600420c30f6e38b3ec8ee54a19
+component	migration	analytics2	0001	create:435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	636c69636b686f7573652d6d6967726174652f6d6967726174696f6e0076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d31000000000000000a616e616c79746963733200000000000000043030303100000000000000010000000000000155636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d31000000000000000a616e616c79746963733200000000000000043030303100000000000000066372656174650000000000000040435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	23ac982172c5ddf03ba507d6d0bb475d46ad94426585c3fccd2e9bf8a7c1f9df
+migration	step	analytics	0002	create:435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303200000000000000066372656174650000000000000040435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	ddde1d492def08641805382a030ae1b7cf7e72247cf472b315be5327464d0922
+migration	migration	analytics	0002	create:435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	636c69636b686f7573652d6d6967726174652f6d6967726174696f6e0076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303200000000000000010000000000000154636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303200000000000000066372656174650000000000000040435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	d5d946d2bdf9a65cdd41cfdf1024296a0f7ab854864e224d48e5a557c769dd23
+step-name	step	analytics	0001	create2:435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c79746963730000000000000004303030310000000000000007637265617465320000000000000040435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	39f0f1bdf7c06c59842f4b83892c165afbe414c10f2ad1b134511454863de793
+step-name	migration	analytics	0001	create2:435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	636c69636b686f7573652d6d6967726174652f6d6967726174696f6e0076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303100000000000000010000000000000155636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c79746963730000000000000004303030310000000000000007637265617465320000000000000040435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	ead6a0aebd7bde3b95c3b8e884f02b42694892a3bed0a722e61a1e34521f3f79
+two-steps	step	analytics	0001	create:435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303100000000000000066372656174650000000000000040435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	8f88bdc18d2842dc722223c041a59aac87b84cfedecd4b04da172bc08bc59640
+two-steps	migration	analytics	0001	create:435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964,seed:494e5345525420494e544f206170702e6576656e74732056414c55455320283129	636c69636b686f7573652d6d6967726174652f6d6967726174696f6e0076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303100000000000000020000000000000154636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303100000000000000066372656174650000000000000040435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f524445522042592069640000000000000133636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c79746963730000000000000004303030310000000000000004736565640000000000000021494e5345525420494e544f206170702e6576656e74732056414c55455320283129	887d899e3171ab3919de5451e6db7b2bd73a7542927d1f3391081c022924cf51
+reordered	step	analytics	0001	seed:494e5345525420494e544f206170702e6576656e74732056414c55455320283129	636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c79746963730000000000000004303030310000000000000004736565640000000000000021494e5345525420494e544f206170702e6576656e74732056414c55455320283129	f306bff56533d318684a335ed4b81cd7cc617a3d7f249580d10714a8fe4b9700
+reordered	migration	analytics	0001	seed:494e5345525420494e544f206170702e6576656e74732056414c55455320283129,create:435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	636c69636b686f7573652d6d6967726174652f6d6967726174696f6e0076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303100000000000000020000000000000133636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c79746963730000000000000004303030310000000000000004736565640000000000000021494e5345525420494e544f206170702e6576656e74732056414c554553202831290000000000000154636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303100000000000000066372656174650000000000000040435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	25fadd85b11a98cd38736cc0214ad26b91406399d51d8857c467b06c03895023
+boundary-a	step	analytics	0001	create:435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f524445522042592069640a	636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303100000000000000066372656174650000000000000041435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f524445522042592069640a	718eaeba20757b1b5a91a4a90f1628137d2bd9d7ed219723f35d9102c30810e8
+boundary-a	migration	analytics	0001	create:435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f524445522042592069640a,seed:494e5345525420494e544f206170702e6576656e74732056414c55455320283129	636c69636b686f7573652d6d6967726174652f6d6967726174696f6e0076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303100000000000000020000000000000155636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303100000000000000066372656174650000000000000041435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f524445522042592069640a0000000000000133636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c79746963730000000000000004303030310000000000000004736565640000000000000021494e5345525420494e544f206170702e6576656e74732056414c55455320283129	d33a51cc7a8aec8cc8829aa39aaa6ddc7f08b0854bc465eaf44dc4915a3b509b
+boundary-b	step	analytics	0001	create:435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303100000000000000066372656174650000000000000040435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	8f88bdc18d2842dc722223c041a59aac87b84cfedecd4b04da172bc08bc59640
+boundary-b	migration	analytics	0001	create:435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964,seed:0a494e5345525420494e544f206170702e6576656e74732056414c55455320283129	636c69636b686f7573652d6d6967726174652f6d6967726174696f6e0076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303100000000000000020000000000000154636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303100000000000000066372656174650000000000000040435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f524445522042592069640000000000000134636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303100000000000000047365656400000000000000220a494e5345525420494e544f206170702e6576656e74732056414c55455320283129	470b59f8d8565b8b1a228a963521d4ea6a5c79f89f70e3f39e7d77a121a89707
+ambiguous-a	step	a	bc	create:435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000001610000000000000002626300000000000000066372656174650000000000000040435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	78a0e2f41f2778b7f24585477bf374aaf61da102626feb549ff05485279913a8
+ambiguous-a	migration	a	bc	create:435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	636c69636b686f7573652d6d6967726174652f6d6967726174696f6e0076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d31000000000000000161000000000000000262630000000000000001000000000000014a636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000001610000000000000002626300000000000000066372656174650000000000000040435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	009a2dc827323890e1fd165147ed17a1a752c94c2e1d275e5cf7c8941bca92e4
+ambiguous-b	step	ab	c	create:435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000002616200000000000000016300000000000000066372656174650000000000000040435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	bf423f20e0fafa04f023fbe3df3b01173679e6a3d1ebb877ad16f271bf6edf9f
+ambiguous-b	migration	ab	c	create:435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	636c69636b686f7573652d6d6967726174652f6d6967726174696f6e0076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d31000000000000000261620000000000000001630000000000000001000000000000014a636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000002616200000000000000016300000000000000066372656174650000000000000040435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	ae6a82d1f50d6b4d729407e694aac50b9a70420615f9be7b75d1b77f50aa2c91
+unicode	step	analytics	0001	seed:494e5345525420494e544f206170702e746578742056414c554553202827e99baa3b2729	636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c79746963730000000000000004303030310000000000000004736565640000000000000024494e5345525420494e544f206170702e746578742056414c554553202827e99baa3b2729	5022a7e4cd20146e455aa3b0e64a4a1ce35c80f021e22b1ce89a86798b2a346e
+unicode	migration	analytics	0001	seed:494e5345525420494e544f206170702e746578742056414c554553202827e99baa3b2729	636c69636b686f7573652d6d6967726174652f6d6967726174696f6e0076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303100000000000000010000000000000136636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c79746963730000000000000004303030310000000000000004736565640000000000000024494e5345525420494e544f206170702e746578742056414c554553202827e99baa3b2729	c55b63ceb8fbeb7c7efafe084c8dc9d0c0590307da883ecfaf683a689ecfa7ef
