diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,22 @@
 
 ## [Unreleased]
 
+## 0.12.0.0 — 2026-10-27
+
+### Added
+
+- Test the complete previous 62-entry cohort using frozen Kiroku migrations
+  0.7.1.0 fixtures, proving that only `kiroku/0016` applies and repeat runs
+  are no-ops.
+
+### Changed
+
+- Require `mori://shinzui/keiro` migrations and test support 0.22.0.0 and
+  `mori://shinzui/kiroku/packages/kiroku-store-migrations` 0.7.2.0.
+- Compose 63 migrations (16 Kiroku, 32 Keiro, 15 Kioku). The additive upstream
+  migration changes subscription constraints and the Kiroku schema comment;
+  all historical Kioku SQL and checksums are preserved. No ledger fixup is needed.
+
 ## 0.11.0.0 — 2026-10-19
 
 ### Added
diff --git a/kioku-migrations.cabal b/kioku-migrations.cabal
--- a/kioku-migrations.cabal
+++ b/kioku-migrations.cabal
@@ -1,6 +1,6 @@
 cabal-version: 3.0
 name: kioku-migrations
-version: 0.11.0.0
+version: 0.12.0.0
 synopsis: Schema migrations for kioku
 description:
   Owns kioku schema evolution as a native pg-migrate component and composes
@@ -31,6 +31,9 @@
   test/fixtures/kiroku-0.6.0.0/README.md
   test/fixtures/kiroku-0.6.0.0/SHA256SUMS
   test/fixtures/kiroku-0.6.0.0/manifest
+  test/fixtures/kiroku-0.7.1.0/*.sql
+  test/fixtures/kiroku-0.7.1.0/README.md
+  test/fixtures/kiroku-0.7.1.0/manifest
 
 source-repository head
   type: git
@@ -75,8 +78,8 @@
     containers >=0.6 && <0.8,
     hasql >=1.10 && <1.11,
     hasql-transaction >=1.2 && <1.3,
-    keiro-migrations ^>=0.21.0.0,
-    kiroku-store-migrations ^>=0.7.1.0,
+    keiro-migrations ^>=0.22.0.0,
+    kiroku-store-migrations ^>=0.7.2.0,
     pg-migrate ^>=1.2.0.0,
     pg-migrate-embed ^>=1.2.0.0,
     pg-migrate-import-codd ^>=1.2.0.0,
@@ -92,8 +95,8 @@
     base >=4.18 && <5,
     directory >=1.3 && <1.4,
     ephemeral-pg >=0.3.1 && <0.4,
-    keiro-test-support ^>=0.21.0.0,
-    kioku-migrations ^>=0.11.0.0,
+    keiro-test-support ^>=0.22.0.0,
+    kioku-migrations ^>=0.12.0.0,
     text >=2.0 && <2.2,
     unix >=2.8 && <2.9,
 
@@ -115,10 +118,10 @@
     bytestring >=0.11 && <0.13,
     containers >=0.6 && <0.8,
     hasql >=1.6,
-    keiro-migrations ^>=0.21.0.0,
-    kioku-migrations ^>=0.11.0.0,
+    keiro-migrations ^>=0.22.0.0,
+    kioku-migrations ^>=0.12.0.0,
     kioku-migrations:test-support,
-    kiroku-store-migrations ^>=0.7.1.0,
+    kiroku-store-migrations ^>=0.7.2.0,
     pg-migrate ^>=1.2.0.0,
     pg-migrate-embed ^>=1.2.0.0,
     pg-migrate-import-codd ^>=1.2.0.0,
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -89,7 +89,9 @@
         ],
       testCase "the full migration chain applies to a fresh database" testFreshDatabase,
       testCase "0.10.0.0 upgrades with one recency index and unchanged rows" testRecencyUpgrade,
-      testCase "the released previous cohort preserves data and checkpoints through 0013–0015" testPreviousCohortUpgrade,
+      testCase "the released previous cohort preserves data and checkpoints through 0013–0016" testPreviousCohortUpgrade,
+      testCase "the 62-entry Keiro 0.21 cohort adds only Kiroku 0016" testKeiro021Upgrade,
+      testCase "a changed previous checksum refuses before migration writes" testKeiro021ChecksumRefusal,
       testGroup
         "migration-session isolation"
         [testCase "restores the host search path before later components" testHostSearchPathRestored],
@@ -473,7 +475,7 @@
   checkpointsBefore <- query connStr previousCheckpointSnapshot
   query connStr previousCohortIsLegacy >>= (@?= True)
   migrated <- runMigrationPlan defaultRunOptions settings current >>= either (assertFailure . show) pure
-  appliedNow migrated @?= [expectRight (migrationId "kiroku" n) | n <- ["0013", "0014", "0015"]]
+  appliedNow migrated @?= [expectRight (migrationId "kiroku" n) | n <- ["0013", "0014", "0015", "0016"]]
   query connStr previousCohortSnapshot >>= (@?= before)
   query connStr previousCohortLedger >>= (@?= ledgerBefore)
   query connStr previousCheckpointSnapshot >>= (@?= checkpointsBefore)
@@ -481,12 +483,12 @@
   verification <- verifyMigrationPlan defaultRunOptions settings current >>= either (assertFailure . show) pure
   let VerificationReport {issues, appliedMigrations, pendingMigrations, unknownMigrations} = verification
   issues @?= []
-  length appliedMigrations @?= 62
+  length appliedMigrations @?= 63
   pendingMigrations @?= []
   unknownMigrations @?= []
   repeated <- runMigrationPlan defaultRunOptions settings current >>= either (assertFailure . show) pure
   let MigrationReport {results} = repeated
-  length [() | MigrationResult {outcome = AlreadyApplied} <- toList results] @?= 62
+  length [() | MigrationResult {outcome = AlreadyApplied} <- toList results] @?= 63
   appliedNow repeated @?= []
   query connStr previousCohortSnapshot >>= (@?= before)
   query connStr previousCheckpointSnapshot >>= (@?= checkpointsBefore)
@@ -545,7 +547,7 @@
     """
     SELECT jsonb_agg(to_jsonb(t) ORDER BY component, migration)::text
     FROM pgmigrate.migrations t
-    WHERE NOT (component = 'kiroku' AND migration IN ('0013', '0014', '0015'))
+    WHERE NOT (component = 'kiroku' AND migration IN ('0013', '0014', '0015', '0016'))
     """
     E.noParams
     (D.singleRow (D.column (D.nonNullable D.text)))
@@ -578,6 +580,118 @@
     E.noParams
     (D.singleRow (D.column (D.nonNullable D.bool)))
 
+-- | Frozen released bytes, rather than private pg-migrate representation access.
+previous021Plan :: IO MigrationPlan
+previous021Plan = do
+  entries <- checkMigrationManifest "test/fixtures/kiroku-0.7.1.0/manifest" >>= either (assertFailure . show) pure
+  [(Text.dropEnd 4 (Text.pack name), checksumHex bytes) | (name, bytes) <- toList entries] @?= previous021Checksums
+  oldKiroku <- either (assertFailure . show) pure (migrationComponentFromEmbeddedSql "kiroku" Set.empty entries)
+  keiro <- either (assertFailure . show) pure KeiroMigrations.keiroMigrations
+  kioku <- either (assertFailure . show) pure kiokuMigrations
+  either (assertFailure . show) pure (migrationPlan (oldKiroku :| [keiro, kioku]))
+
+previous021Checksums :: [(Text, Text)]
+previous021Checksums =
+  [ ("0001-kiroku-bootstrap", "af7c777f8702d9b5bce6b3ea16540ab7542bc78725cb45e945c6d5b6025a4d66"),
+    ("0002-add-subscription-dead-letters", "2771cdd2605a8808ea469c0bcba175c8ba4c5d2465f43eb05c56edb188fa8141"),
+    ("0003-notify-trigger-append-guard", "432796c59389d45478ba773b62e9f38d0a562807061eb49280d295ec83248351"),
+    ("0004-dead-letters-event-id-index", "f698db092ebf34beeddecc949bbd506afc0b9c63d3e9983ad5d14f203f2ee1cd"),
+    ("0005-index-hygiene-and-streams-fillfactor", "80c28c7743f1d9967eadf921c0509fc8477db16500466bf7a49c5f59aadb0fa2"),
+    ("0006-stream-name-length-check", "a0e4338a2feadf8f01499a3809d3de78966966689b095ea46195c4d07743e4aa"),
+    ("0007-stream-truncate-before", "a4fe42ad8702b43c3d6f72edf263d6b301e783493d49f40a35dd14d2a7928fa7"),
+    ("0008-schema-management-comment", "3fb3dd5a7172da3cd14edecb0877cc9963fc9a6dceaff9ed41705a2365070f70"),
+    ("0009", "62c80d627dc8f17393d26ff7dbc00cabde11ed38ee7bf3cb1ae42ea89f484c74"),
+    ("0010", "debc19187d79cd263be66c6c85cd789c1176e508d562a27390095aaa70f650c4"),
+    ("0011", "bf8b76a712a31cb602aee793a11691283ccd1eafdc16224e45e14484512b5af7"),
+    ("0012", "32a0a0c877ff1aa6c1f74de8db62c74884e96a9c7500b2b65725b9e654101a7a"),
+    ("0013", "49e519280a578e5b0f02603173b492075b072d0ae52d0e5848fea8ae1d6f7d53"),
+    ("0014", "b93580ef072e85a6a598724fa8746aaa079346d7dd4a9c859412cc07113ff4d1"),
+    ("0015", "821b12e410c1cece52aee018f1916a8cb01b2e900784817ba9b33d37b78d3be5")
+  ]
+
+-- The event and application fixture is shared, while modern bindings cover all
+-- three pre-namespace target kinds and nonzero checkpoint positions.
+prepare021 :: Text -> IO ()
+prepare021 connStr = do
+  old <- previous021Plan
+  installed <- runMigrationPlan defaultRunOptions (Settings.connectionString connStr) old >>= either (assertFailure . show) pure
+  length (appliedNow installed) @?= 62
+  withConnection connStr $ \conn -> do
+    run conn (Session.script (fst (Text.breakOn "INSERT INTO kiroku.subscriptions" previousCohortSeed)))
+    run conn (Session.script (Text.replace "kiroku.kioku_" "kioku." relocationSeedRows))
+    run
+      conn
+      ( Session.script
+          "INSERT INTO kiroku.subscriptions (subscription_name,last_seen,consumer_group_member,consumer_group_size,target_kind,target_category) VALUES ('upgrade-exact',1,0,1,'category','kioku_memory'),('upgrade-all',1,0,1,'all',NULL),('upgrade-unbound',0,0,1,'unbound',NULL)"
+      )
+
+ledgerWithout0016 :: Statement () Text
+ledgerWithout0016 =
+  preparable
+    "SELECT jsonb_agg(to_jsonb(t) ORDER BY component,migration)::text FROM pgmigrate.migrations t WHERE NOT (component='kiroku' AND migration='0016')"
+    E.noParams
+    (D.singleRow (D.column (D.nonNullable D.text)))
+
+bindingsSnapshot :: Statement () Text
+bindingsSnapshot =
+  preparable
+    "SELECT jsonb_agg(to_jsonb(t) ORDER BY subscription_name,consumer_group_member)::text FROM kiroku.subscriptions t"
+    E.noParams
+    (D.singleRow (D.column (D.nonNullable D.text)))
+
+testKeiro021Upgrade :: Assertion
+testKeiro021Upgrade = withBareDatabase $ \connStr -> do
+  prepare021 connStr
+  rows <- query connStr previousCohortSnapshot
+  ledger <- query connStr ledgerWithout0016
+  bindings <- query connStr bindingsSnapshot
+  checkpoints <- query connStr previousCheckpointSnapshot
+  current <- either (assertFailure . show) pure kiokuMigrationPlan
+  migrated <- runMigrationPlan defaultRunOptions (Settings.connectionString connStr) current >>= either (assertFailure . show) pure
+  appliedNow migrated @?= [expectRight (migrationId "kiroku" "0016")]
+  query connStr previousCohortSnapshot >>= (@?= rows)
+  query connStr ledgerWithout0016 >>= (@?= ledger)
+  query connStr bindingsSnapshot >>= (@?= bindings)
+  query connStr previousCheckpointSnapshot >>= (@?= checkpoints)
+  verification <- verifyMigrationPlan defaultRunOptions (Settings.connectionString connStr) current >>= either (assertFailure . show) pure
+  let VerificationReport {issues, appliedMigrations, pendingMigrations, unknownMigrations} = verification
+  issues @?= []
+  length appliedMigrations @?= 63
+  pendingMigrations @?= []
+  unknownMigrations @?= []
+  beforeRepeat <- query connStr fullLedgerSnapshot
+  repeated <- runMigrationPlan defaultRunOptions (Settings.connectionString connStr) current >>= either (assertFailure . show) pure
+  appliedNow repeated @?= []
+  let MigrationReport {results} = repeated
+  length [() | MigrationResult {outcome = AlreadyApplied} <- toList results] @?= 63
+  query connStr fullLedgerSnapshot >>= (@?= beforeRepeat)
+  query connStr previousCohortSnapshot >>= (@?= rows)
+  query connStr bindingsSnapshot >>= (@?= bindings)
+  query connStr previousCheckpointSnapshot >>= (@?= checkpoints)
+
+testKeiro021ChecksumRefusal :: Assertion
+testKeiro021ChecksumRefusal = withBareDatabase $ \connStr -> do
+  prepare021 connStr
+  withConnection connStr $ \conn ->
+    run
+      conn
+      ( Session.script
+          "UPDATE pgmigrate.migrations SET checksum=decode(repeat('00',32),'hex') WHERE component='kiroku' AND migration='0015'"
+      )
+  ledger <- query connStr fullLedgerSnapshot
+  schema <- query connStr cohortSchemaSnapshotStatement
+  rows <- query connStr previousCohortSnapshot
+  bindings <- query connStr bindingsSnapshot
+  current <- either (assertFailure . show) pure kiokuMigrationPlan
+  result <- runMigrationPlan defaultRunOptions (Settings.connectionString connStr) current
+  case result of
+    Left _ -> pure ()
+    Right report -> assertFailure ("changed checksum unexpectedly accepted: " <> show report)
+  query connStr fullLedgerSnapshot >>= (@?= ledger)
+  query connStr cohortSchemaSnapshotStatement >>= (@?= schema)
+  query connStr previousCohortSnapshot >>= (@?= rows)
+  query connStr bindingsSnapshot >>= (@?= bindings)
+
 -- * The fresh-database test
 
 -- | The rewritten body still applies inside the real pg-migrate chain.
@@ -592,7 +706,7 @@
         runMigrationPlan defaultRunOptions (Settings.connectionString connStr) plan
           >>= either (assertFailure . show) pure
       let MigrationReport {results = repeatedResults} = repeated
-      length [() | MigrationResult {outcome = AlreadyApplied} <- toList repeatedResults] @?= 62
+      length [() | MigrationResult {outcome = AlreadyApplied} <- toList repeatedResults] @?= 63
       length [() | MigrationResult {outcome = AppliedNow} <- toList repeatedResults] @?= 0
       verification <-
         verifyMigrationPlan defaultRunOptions (Settings.connectionString connStr) plan
@@ -711,10 +825,10 @@
     plan <- either (fail . show) pure kiokuMigrationPlan
     let settings = Settings.connectionString connStr
     initial <- runMigrationPlan defaultRunOptions settings plan >>= either (assertFailure . show) pure
-    length (appliedNow initial) @?= 62
+    length (appliedNow initial) @?= 63
 
     baselineLedger <- query connStr fullLedgerSnapshot
-    fst baselineLedger @?= 62
+    fst baselineLedger @?= 63
     baselineSchema <- query connStr cohortSchemaSnapshotStatement
     fixup <- Text.IO.readFile ledgerFixupPath
 
@@ -740,7 +854,7 @@
 
     repeated <- runMigrationPlan defaultRunOptions settings plan >>= either (assertFailure . show) pure
     let MigrationReport {results = repeatedResults} = repeated
-    length [() | MigrationResult {outcome = AlreadyApplied} <- toList repeatedResults] @?= 62
+    length [() | MigrationResult {outcome = AlreadyApplied} <- toList repeatedResults] @?= 63
     length [() | MigrationResult {outcome = AppliedNow} <- toList repeatedResults] @?= 0
 
     withConnection connStr \conn -> run conn (Session.script fixup)
@@ -1205,12 +1319,12 @@
     full <- either (fail . show) pure kiokuMigrationPlan
 
     kirokuReport <- runMigrationPlan defaultRunOptions settings kirokuOnly >>= either (assertFailure . show) pure
-    length (appliedNow kirokuReport) @?= 15
+    length (appliedNow kirokuReport) @?= 16
     ledgerBefore <- query connStr kirokuLedgerSnapshot
 
     adoption <- runMigrationPlan defaultRunOptions settings full >>= either (assertFailure . show) pure
     let MigrationReport {results = adoptionResults} = adoption
-    length [() | MigrationResult {outcome = AlreadyApplied} <- toList adoptionResults] @?= 15
+    length [() | MigrationResult {outcome = AlreadyApplied} <- toList adoptionResults] @?= 16
     length [() | MigrationResult {outcome = AppliedNow} <- toList adoptionResults] @?= 47
 
     -- Verified and skipped, never re-executed: the stored rows keep their checksums and their
@@ -1224,7 +1338,7 @@
     verification <- verifyMigrationPlan defaultRunOptions settings full >>= either (assertFailure . show) pure
     let VerificationReport {issues = adoptionIssues, appliedMigrations, pendingMigrations, unknownMigrations} = verification
     adoptionIssues @?= []
-    length appliedMigrations @?= 62
+    length appliedMigrations @?= 63
     pendingMigrations @?= []
     unknownMigrations @?= []
 
@@ -1575,13 +1689,13 @@
     verification <- verifyMigrationPlan defaultRunOptions settings plan >>= either (assertFailure . show) pure
     let VerificationReport {issues = verificationIssues, appliedMigrations, pendingMigrations, unknownMigrations} = verification
     verificationIssues @?= []
-    length appliedMigrations @?= 62
+    length appliedMigrations @?= 63
     pendingMigrations @?= []
     unknownMigrations @?= []
 
     repeated <- runMigrationPlan defaultRunOptions settings plan >>= either (assertFailure . show) pure
     let MigrationReport {results = repeatedResults} = repeated
-    length [() | MigrationResult {outcome = AlreadyApplied} <- toList repeatedResults] @?= 62
+    length [() | MigrationResult {outcome = AlreadyApplied} <- toList repeatedResults] @?= 63
     length [() | MigrationResult {outcome = AppliedNow} <- toList repeatedResults] @?= 0
 
 testSchemaConvergence :: Assertion
@@ -1750,6 +1864,7 @@
           migrationId "kiroku" "0013",
           migrationId "kiroku" "0014",
           migrationId "kiroku" "0015",
+          migrationId "kiroku" "0016",
           migrationId "keiro" "0015-keiro-outbox-claim-order-index",
           migrationId "keiro" "0016-keiro-inbox-drop-received-idx",
           migrationId "keiro" "0017-schema-management-comment",
@@ -1817,7 +1932,7 @@
       (EXISTS (SELECT 1 FROM information_schema.columns
         WHERE table_schema = 'kiroku' AND table_name = 'streams' AND column_name = 'truncate_before'))::int
       + (coalesce(obj_description(to_regnamespace('kiroku'), 'pg_namespace'), '') =
-          'Managed by pg-migrate component kiroku through 0015')::int
+          'Managed by pg-migrate component kiroku through 0016')::int
       + (to_regclass('keiro.keiro_outbox_claim_order_idx') IS NOT NULL)::int
       + (to_regclass('keiro.keiro_inbox_received_idx') IS NULL)::int
       + (coalesce(obj_description(to_regnamespace('keiro'), 'pg_namespace'), '') =
@@ -1864,7 +1979,7 @@
   old <- either (assertFailure . show) pure (migrationPlan (kiroku :| [keiro, oldKioku]))
   current <- either (assertFailure . show) pure kiokuMigrationPlan
   initial <- runMigrationPlan defaultRunOptions settings old >>= either (assertFailure . show) pure
-  length (appliedNow initial) @?= 61
+  length (appliedNow initial) @?= 62
   withConnection connStr $ \conn ->
     run
       conn
diff --git a/test/fixtures/kiroku-0.7.1.0/0001-kiroku-bootstrap.sql b/test/fixtures/kiroku-0.7.1.0/0001-kiroku-bootstrap.sql
new file mode 100644
--- /dev/null
+++ b/test/fixtures/kiroku-0.7.1.0/0001-kiroku-bootstrap.sql
@@ -0,0 +1,229 @@
+-- Kiroku Store bootstrap migration (codd)
+-- Supports PostgreSQL 17+.
+--
+-- kiroku-store-migrations is the schema owner. codd applies this file
+-- verbatim, records that the timestamped migration ran, and skips it on later
+-- runs. Future schema changes add new timestamped SQL files in this directory
+-- instead of changing kiroku-store.
+--
+-- All Kiroku-owned objects live in the dedicated `kiroku` schema, leaving
+-- `public` free for application objects. Creating the schema and setting
+-- search_path first means every unqualified object name in the rest of this
+-- file resolves into the Kiroku schema.
+CREATE SCHEMA IF NOT EXISTS kiroku;
+SET search_path TO kiroku, pg_catalog;
+
+-- PostgreSQL 18 provides pg_catalog.uuidv7(); PostgreSQL 17 needs this
+-- Kiroku-schema fallback before events.event_id DEFAULT uuidv7() is parsed.
+-- With search_path set above, the unqualified CREATE FUNCTION lands in the
+-- Kiroku schema, and to_regprocedure('uuidv7()') resolves through search_path
+-- (pg_catalog first for the built-in, then the Kiroku schema for the fallback).
+DO $$
+BEGIN
+    IF to_regprocedure('pg_catalog.uuidv7()') IS NULL
+       AND to_regprocedure('uuidv7()') IS NULL THEN
+        EXECUTE $fn$
+            CREATE FUNCTION uuidv7()
+            RETURNS uuid
+            AS $body$
+            DECLARE
+                unix_ts_ms bytea;
+                uuid_bytes bytea;
+            BEGIN
+                unix_ts_ms = substring(int8send(floor(extract(epoch from clock_timestamp()) * 1000)::bigint) from 3);
+                uuid_bytes = uuid_send(gen_random_uuid());
+                uuid_bytes = overlay(uuid_bytes placing unix_ts_ms from 1 for 6);
+                uuid_bytes = set_byte(uuid_bytes, 6, (b'0111' || get_byte(uuid_bytes, 6)::bit(4))::bit(8)::int);
+                RETURN encode(uuid_bytes, 'hex')::uuid;
+            END
+            $body$
+            LANGUAGE plpgsql
+            VOLATILE
+        $fn$;
+    END IF;
+END
+$$;
+
+-- Streams (including $all as stream_id = 0)
+CREATE TABLE IF NOT EXISTS streams (
+    stream_id    BIGSERIAL    PRIMARY KEY,
+    stream_name  TEXT         NOT NULL,
+    category     TEXT         GENERATED ALWAYS AS (split_part(stream_name, '-', 1)) STORED,
+    stream_version BIGINT     NOT NULL DEFAULT 0,
+    created_at   TIMESTAMPTZ  NOT NULL DEFAULT now(),
+    deleted_at   TIMESTAMPTZ,
+    CONSTRAINT ix_streams_stream_name UNIQUE (stream_name)
+);
+
+-- Seed the $all stream
+INSERT INTO streams (stream_id, stream_name, stream_version)
+VALUES (0, '$all', 0)
+ON CONFLICT DO NOTHING;
+
+-- Reset sequence past the reserved stream_id=0
+SELECT setval('streams_stream_id_seq', GREATEST((SELECT MAX(stream_id) FROM streams), 1));
+
+-- Events (flat table — stream membership tracked in stream_events)
+CREATE TABLE IF NOT EXISTS events (
+    event_id       UUID         PRIMARY KEY DEFAULT uuidv7(),
+    event_type     TEXT         NOT NULL,
+    causation_id   UUID,
+    correlation_id UUID,
+    data           JSONB        NOT NULL,
+    metadata       JSONB,
+    created_at     TIMESTAMPTZ  NOT NULL DEFAULT now()
+);
+
+-- Stream-event junction (each event gets 2+ rows: source stream + $all + any links)
+CREATE TABLE IF NOT EXISTS stream_events (
+    event_id                UUID   NOT NULL REFERENCES events(event_id),
+    stream_id               BIGINT NOT NULL REFERENCES streams(stream_id),
+    stream_version          BIGINT NOT NULL,
+    original_stream_id      BIGINT NOT NULL,
+    original_stream_version BIGINT NOT NULL,
+    PRIMARY KEY (event_id, stream_id)
+);
+
+-- Indexes
+
+-- Primary read path: fetch events from a stream in order
+CREATE INDEX IF NOT EXISTS ix_stream_events_stream_version
+    ON stream_events (stream_id, stream_version);
+
+-- Event type filtering (for server-side subscription filtering)
+CREATE INDEX IF NOT EXISTS ix_events_event_type
+    ON events (event_type);
+
+-- Correlation tracing
+CREATE INDEX IF NOT EXISTS ix_events_correlation_id
+    ON events (correlation_id) WHERE correlation_id IS NOT NULL;
+
+-- Causation tracing
+CREATE INDEX IF NOT EXISTS ix_events_causation_id
+    ON events (causation_id) WHERE causation_id IS NOT NULL;
+
+-- Category filtering (for readCategory — uses generated column, not LIKE)
+CREATE INDEX IF NOT EXISTS ix_streams_category
+    ON streams (category);
+
+-- Category read path: find $all entries by originating stream, ordered by global position
+-- Enables efficient category reads by allowing the planner to: look up category stream_ids →
+-- index scan $all for each → merge ordered by stream_version
+CREATE INDEX IF NOT EXISTS ix_stream_events_all_by_origin
+    ON stream_events (original_stream_id, stream_version)
+    WHERE stream_id = 0;
+
+-- Subscriptions (checkpoint persistence for subscription positions).
+-- consumer_group_member / consumer_group_size carry static consumer-group
+-- topology (ExecPlan 28 / EP-1). Non-group subscriptions are member 0, size 1.
+-- The unique key is composite (subscription_name, consumer_group_member) so each
+-- group member persists its own checkpoint under one shared subscription name.
+CREATE TABLE IF NOT EXISTS subscriptions (
+    subscription_id       BIGSERIAL    PRIMARY KEY,
+    subscription_name     TEXT         NOT NULL,
+    stream_name           TEXT         NOT NULL DEFAULT '$all',
+    last_seen             BIGINT       NOT NULL DEFAULT 0,
+    consumer_group_member INT          NOT NULL DEFAULT 0,
+    consumer_group_size   INT          NOT NULL DEFAULT 1,
+    created_at            TIMESTAMPTZ  NOT NULL DEFAULT now(),
+    updated_at            TIMESTAMPTZ  NOT NULL DEFAULT now()
+);
+
+-- Idempotent convergence for databases created before EP-1: add the columns if
+-- missing, drop the old auto-named single-column unique constraint if present,
+-- and install the composite unique index. All guarded so the bootstrap body is
+-- still safe in disposable local databases; codd records the timestamped
+-- migration and does not reapply it after a successful run.
+ALTER TABLE subscriptions ADD COLUMN IF NOT EXISTS consumer_group_member INT NOT NULL DEFAULT 0;
+ALTER TABLE subscriptions ADD COLUMN IF NOT EXISTS consumer_group_size   INT NOT NULL DEFAULT 1;
+ALTER TABLE subscriptions DROP CONSTRAINT IF EXISTS subscriptions_subscription_name_key;
+CREATE UNIQUE INDEX IF NOT EXISTS ix_subscriptions_name_member
+    ON subscriptions (subscription_name, consumer_group_member);
+
+-- Triggers
+
+-- NOTIFY on stream changes (fires once per append, not per event)
+CREATE OR REPLACE FUNCTION notify_events() RETURNS TRIGGER AS $$
+BEGIN
+    PERFORM pg_notify(
+        TG_TABLE_SCHEMA || '.events',
+        NEW.stream_name || ',' || NEW.stream_id || ',' || NEW.stream_version
+    );
+    RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+
+DROP TRIGGER IF EXISTS stream_events_notify ON streams;
+CREATE TRIGGER stream_events_notify
+    AFTER INSERT OR UPDATE ON streams
+    FOR EACH ROW EXECUTE FUNCTION notify_events();
+
+-- Immutability: prevent event mutation
+CREATE OR REPLACE FUNCTION prevent_mutation() RETURNS TRIGGER AS $$
+BEGIN
+    RAISE EXCEPTION 'Immutable table: % cannot be updated', TG_TABLE_NAME;
+END;
+$$ LANGUAGE plpgsql;
+
+DROP TRIGGER IF EXISTS no_update_events ON events;
+CREATE TRIGGER no_update_events
+    BEFORE UPDATE ON events
+    FOR EACH ROW EXECUTE FUNCTION prevent_mutation();
+
+DROP TRIGGER IF EXISTS no_update_stream_events ON stream_events;
+CREATE TRIGGER no_update_stream_events
+    BEFORE UPDATE ON stream_events
+    FOR EACH ROW EXECUTE FUNCTION prevent_mutation();
+
+-- Gated hard deletes (for maintenance/GDPR only)
+CREATE OR REPLACE FUNCTION protect_deletion() RETURNS TRIGGER AS $$
+BEGIN
+    IF current_setting('kiroku.enable_hard_deletes', true) = 'on' THEN
+        RETURN OLD;
+    END IF;
+    RAISE EXCEPTION 'Hard deletes require: SET LOCAL kiroku.enable_hard_deletes = ''on''';
+END;
+$$ LANGUAGE plpgsql;
+
+DROP TRIGGER IF EXISTS no_delete_events ON events;
+CREATE TRIGGER no_delete_events
+    BEFORE DELETE ON events
+    FOR EACH ROW EXECUTE FUNCTION protect_deletion();
+
+DROP TRIGGER IF EXISTS no_delete_stream_events ON stream_events;
+CREATE TRIGGER no_delete_stream_events
+    BEFORE DELETE ON stream_events
+    FOR EACH ROW EXECUTE FUNCTION protect_deletion();
+
+DROP TRIGGER IF EXISTS no_delete_streams ON streams;
+CREATE TRIGGER no_delete_streams
+    BEFORE DELETE ON streams
+    FOR EACH ROW EXECUTE FUNCTION protect_deletion();
+
+-- TRUNCATE bypasses row-level triggers, so the BEFORE DELETE triggers above
+-- do not protect against an operator running TRUNCATE on these tables. Add
+-- statement-level BEFORE TRUNCATE triggers gated by the same GUC so the
+-- protection is symmetric. See EP-1 F6.
+CREATE OR REPLACE FUNCTION protect_truncation() RETURNS TRIGGER AS $$
+BEGIN
+    IF current_setting('kiroku.enable_hard_deletes', true) = 'on' THEN
+        RETURN NULL;
+    END IF;
+    RAISE EXCEPTION 'TRUNCATE requires: SET LOCAL kiroku.enable_hard_deletes = ''on''';
+END;
+$$ LANGUAGE plpgsql;
+
+DROP TRIGGER IF EXISTS no_truncate_events ON events;
+CREATE TRIGGER no_truncate_events
+    BEFORE TRUNCATE ON events
+    FOR EACH STATEMENT EXECUTE FUNCTION protect_truncation();
+
+DROP TRIGGER IF EXISTS no_truncate_stream_events ON stream_events;
+CREATE TRIGGER no_truncate_stream_events
+    BEFORE TRUNCATE ON stream_events
+    FOR EACH STATEMENT EXECUTE FUNCTION protect_truncation();
+
+DROP TRIGGER IF EXISTS no_truncate_streams ON streams;
+CREATE TRIGGER no_truncate_streams
+    BEFORE TRUNCATE ON streams
+    FOR EACH STATEMENT EXECUTE FUNCTION protect_truncation();
diff --git a/test/fixtures/kiroku-0.7.1.0/0002-add-subscription-dead-letters.sql b/test/fixtures/kiroku-0.7.1.0/0002-add-subscription-dead-letters.sql
new file mode 100644
--- /dev/null
+++ b/test/fixtures/kiroku-0.7.1.0/0002-add-subscription-dead-letters.sql
@@ -0,0 +1,31 @@
+-- Add the kiroku.dead_letters table (MasterPlan 6 / EP-2, docs/plans/40-...).
+--
+-- Forward, additive migration: codd applies this file once, records it, and
+-- skips it on later runs. It does not edit the bootstrap migration and does not
+-- mutate existing event data.
+--
+-- A dead-letter row records one event that a subscription handler asked to
+-- "dead-letter" (return DeadLetter, or exhaust its bounded retry budget). The
+-- event itself stays immutable in kiroku.events; this table references it by
+-- event_id and global_position rather than copying the payload. The
+-- consumer_group_member column (default 0, matching kiroku.subscriptions)
+-- attributes the row to the member that produced it, so a consumer group's dead
+-- letters are per-member. The worker writes a row and advances the member's
+-- checkpoint in one atomic statement (see SQL.insertDeadLetterAndCheckpointStmt).
+
+CREATE TABLE IF NOT EXISTS kiroku.dead_letters (
+    dead_letter_id        BIGSERIAL    PRIMARY KEY,
+    subscription_name     TEXT         NOT NULL,
+    consumer_group_member INT          NOT NULL DEFAULT 0,
+    global_position       BIGINT       NOT NULL,
+    event_id              UUID         NOT NULL REFERENCES kiroku.events(event_id),
+    reason                JSONB        NOT NULL,
+    reason_summary        TEXT         NOT NULL,
+    attempt_count         INT          NOT NULL,
+    created_at            TIMESTAMPTZ  NOT NULL DEFAULT now(),
+    UNIQUE (subscription_name, consumer_group_member, global_position, event_id)
+);
+
+-- Operator read path: list a subscription member's dead letters by recency.
+CREATE INDEX IF NOT EXISTS ix_dead_letters_subscription_created_at
+    ON kiroku.dead_letters (subscription_name, consumer_group_member, created_at);
diff --git a/test/fixtures/kiroku-0.7.1.0/0003-notify-trigger-append-guard.sql b/test/fixtures/kiroku-0.7.1.0/0003-notify-trigger-append-guard.sql
new file mode 100644
--- /dev/null
+++ b/test/fixtures/kiroku-0.7.1.0/0003-notify-trigger-append-guard.sql
@@ -0,0 +1,26 @@
+-- Guard append notifications at the trigger level.
+--
+-- The trigger function and payload format stay unchanged:
+--   stream_name,stream_id,stream_version
+--
+-- INSERT covers newly-created streams. UPDATE covers later appends to an
+-- existing stream. Both exclude the internal $all row (stream_id = 0), and the
+-- UPDATE trigger only fires when an append advances stream_version, not for
+-- lifecycle updates such as soft-delete or undelete.
+
+DROP TRIGGER IF EXISTS stream_events_notify ON kiroku.streams;
+
+DROP TRIGGER IF EXISTS stream_events_notify_insert ON kiroku.streams;
+CREATE TRIGGER stream_events_notify_insert
+    AFTER INSERT ON kiroku.streams
+    FOR EACH ROW
+    WHEN (NEW.stream_id <> 0)
+    EXECUTE FUNCTION kiroku.notify_events();
+
+DROP TRIGGER IF EXISTS stream_events_notify_update ON kiroku.streams;
+CREATE TRIGGER stream_events_notify_update
+    AFTER UPDATE ON kiroku.streams
+    FOR EACH ROW
+    WHEN (NEW.stream_id <> 0
+          AND NEW.stream_version IS DISTINCT FROM OLD.stream_version)
+    EXECUTE FUNCTION kiroku.notify_events();
diff --git a/test/fixtures/kiroku-0.7.1.0/0004-dead-letters-event-id-index.sql b/test/fixtures/kiroku-0.7.1.0/0004-dead-letters-event-id-index.sql
new file mode 100644
--- /dev/null
+++ b/test/fixtures/kiroku-0.7.1.0/0004-dead-letters-event-id-index.sql
@@ -0,0 +1,10 @@
+-- Index dead_letters by event_id (MasterPlan 9 / EP-5, docs/plans/60-...).
+--
+-- dead_letters.event_id has a FK to kiroku.events. The UNIQUE key leads with
+-- subscription_name, so every referential-integrity check triggered by a
+-- DELETE on kiroku.events (the hard-delete path) was a sequential scan of
+-- dead_letters, and the hard-delete transaction's own dead-letter pre-delete
+-- (Kiroku.Store.SQL.deleteDeadLettersForOrphanedEventsStmt) needs the same
+-- access path.
+CREATE INDEX IF NOT EXISTS ix_dead_letters_event_id
+    ON kiroku.dead_letters (event_id);
diff --git a/test/fixtures/kiroku-0.7.1.0/0005-index-hygiene-and-streams-fillfactor.sql b/test/fixtures/kiroku-0.7.1.0/0005-index-hygiene-and-streams-fillfactor.sql
new file mode 100644
--- /dev/null
+++ b/test/fixtures/kiroku-0.7.1.0/0005-index-hygiene-and-streams-fillfactor.sql
@@ -0,0 +1,33 @@
+-- Index hygiene and $all hot-row tuning (MasterPlan 9 / EP-5, docs/plans/60-...).
+
+-- 1. ix_events_event_type is referenced by no statement in kiroku-store; it is
+--    pure write amplification on every append. Server-side event-type pushdown
+--    (see the EventTypeFilter haddock in kiroku-store) should re-add a
+--    fit-for-purpose index when it ships.
+DROP INDEX IF EXISTS kiroku.ix_events_event_type;
+
+-- 2. Stream versions are unique per stream by construction (assigned under the
+--    stream row lock; $all versions are the global position). Enforce it so a
+--    version-assignment bug surfaces as a loud 23505 instead of silent
+--    duplicates. Built as a new unique index, then the old non-unique index is
+--    dropped (an index cannot be altered to unique in place).
+CREATE UNIQUE INDEX IF NOT EXISTS ux_stream_events_stream_version
+    ON kiroku.stream_events (stream_id, stream_version);
+DROP INDEX IF EXISTS kiroku.ix_stream_events_stream_version;
+
+-- 3. readDeadLetters orders by (global_position DESC, dead_letter_id DESC) --
+--    the store's canonical, deterministic "newest first". Re-key the read
+--    index to match so the read is index-ordered instead of sorting each time.
+CREATE INDEX IF NOT EXISTS ix_dead_letters_subscription_position
+    ON kiroku.dead_letters
+       (subscription_name, consumer_group_member,
+        global_position DESC, dead_letter_id DESC);
+DROP INDEX IF EXISTS kiroku.ix_dead_letters_subscription_created_at;
+
+-- 4. The $all row (stream_id 0) is updated by every append in the database.
+--    Its updated column (stream_version) is not indexed, so updates are
+--    HOT-eligible when the page has free space; fillfactor 50 reserves that
+--    space on newly written pages. Existing pages converge through normal
+--    update/prune activity (VACUUM cannot run inside this migration's
+--    transaction). Autovacuum tuning for this table is left to operators.
+ALTER TABLE kiroku.streams SET (fillfactor = 50);
diff --git a/test/fixtures/kiroku-0.7.1.0/0006-stream-name-length-check.sql b/test/fixtures/kiroku-0.7.1.0/0006-stream-name-length-check.sql
new file mode 100644
--- /dev/null
+++ b/test/fixtures/kiroku-0.7.1.0/0006-stream-name-length-check.sql
@@ -0,0 +1,9 @@
+-- Defense-in-depth bound on stream-name length (MasterPlan 9 / EP-5,
+-- docs/plans/60-...). The Haskell store validates this before any SQL
+-- (StoreError StreamNameTooLong, maxStreamNameBytes = 512); the constraint
+-- catches writers that bypass the library (raw SQL via runTransaction,
+-- psql sessions). 512 bytes is far below pg_notify's 8,000-byte payload
+-- limit, so the append-notification trigger can never abort on payload size.
+ALTER TABLE kiroku.streams
+    ADD CONSTRAINT chk_streams_stream_name_length
+    CHECK (octet_length(stream_name) <= 512);
diff --git a/test/fixtures/kiroku-0.7.1.0/0007-stream-truncate-before.sql b/test/fixtures/kiroku-0.7.1.0/0007-stream-truncate-before.sql
new file mode 100644
--- /dev/null
+++ b/test/fixtures/kiroku-0.7.1.0/0007-stream-truncate-before.sql
@@ -0,0 +1,8 @@
+-- Logical truncate-before marker for close-the-book compaction
+-- (ExecPlan docs/plans/65). Per-stream cursor: ordered stream reads return
+-- only events whose stream_version >= truncate_before. Default 0 keeps all
+-- events (per-stream versions are 1-based). Reversible; the global $all log
+-- is never affected. UPDATE on streams is already permitted (soft-delete
+-- uses it), so no trigger/GUC changes are needed.
+ALTER TABLE kiroku.streams
+    ADD COLUMN IF NOT EXISTS truncate_before BIGINT NOT NULL DEFAULT 0;
diff --git a/test/fixtures/kiroku-0.7.1.0/0008-schema-management-comment.sql b/test/fixtures/kiroku-0.7.1.0/0008-schema-management-comment.sql
new file mode 100644
--- /dev/null
+++ b/test/fixtures/kiroku-0.7.1.0/0008-schema-management-comment.sql
@@ -0,0 +1,2 @@
+COMMENT ON SCHEMA kiroku IS
+  'Managed by pg-migrate component kiroku through 0008-schema-management-comment';
diff --git a/test/fixtures/kiroku-0.7.1.0/0009.sql b/test/fixtures/kiroku-0.7.1.0/0009.sql
new file mode 100644
--- /dev/null
+++ b/test/fixtures/kiroku-0.7.1.0/0009.sql
@@ -0,0 +1,35 @@
+-- Publish the frozen, owner-rights, structurally read-only v1 checkpoint relation.
+CREATE VIEW kiroku.subscription_checkpoints_v1
+    (subscription_name,
+     consumer_group_member,
+     checkpoint_position,
+     checkpoint_updated_at)
+WITH (security_invoker = false)
+AS
+WITH checkpoint_rows AS NOT MATERIALIZED (
+    SELECT subscription_name,
+           consumer_group_member,
+           last_seen AS checkpoint_position,
+           updated_at AS checkpoint_updated_at
+    FROM kiroku.subscriptions
+)
+SELECT subscription_name,
+       consumer_group_member,
+       checkpoint_position,
+       checkpoint_updated_at
+FROM checkpoint_rows;
+
+COMMENT ON VIEW kiroku.subscription_checkpoints_v1 IS
+  'Stable read-only v1 relation of exact persisted subscription-member checkpoints; its columns and order are frozen, and its rows are unordered unless the caller supplies ORDER BY.';
+
+COMMENT ON COLUMN kiroku.subscription_checkpoints_v1.subscription_name IS
+  'Persisted subscription name; member zero does not distinguish a non-group subscription from member zero of a consumer group.';
+
+COMMENT ON COLUMN kiroku.subscription_checkpoints_v1.consumer_group_member IS
+  'Persisted consumer-group member key; member zero carries no topology classification.';
+
+COMMENT ON COLUMN kiroku.subscription_checkpoints_v1.checkpoint_position IS
+  'Exact persisted global position for this subscription member; an explicit reset may move it backward or forward.';
+
+COMMENT ON COLUMN kiroku.subscription_checkpoints_v1.checkpoint_updated_at IS
+  'Time of the latest checkpoint-row upsert; it does not imply position advancement or worker liveness.';
diff --git a/test/fixtures/kiroku-0.7.1.0/0010.sql b/test/fixtures/kiroku-0.7.1.0/0010.sql
new file mode 100644
--- /dev/null
+++ b/test/fixtures/kiroku-0.7.1.0/0010.sql
@@ -0,0 +1,146 @@
+-- add replay history retention
+
+-- Publish kiroku.uuidv7() as the component's version-independent, always
+-- schema-qualified UUIDv7 generator.
+--
+-- PostgreSQL 18 provides pg_catalog.uuidv7(), so 0001 installed no fallback
+-- and the name exists only in pg_catalog. PostgreSQL 17 has no builtin, so
+-- 0001 installed the fallback into the Kiroku schema. Naming uuidv7()
+-- unqualified therefore resolves only through search_path, which no migration
+-- after 0001 may depend on. Establishing kiroku.uuidv7() on both versions lets
+-- this migration -- and every migration after it -- name one generator that
+-- resolves without session state on every PostgreSQL version the component
+-- supports.
+DO $$
+BEGIN
+    IF to_regprocedure('kiroku.uuidv7()') IS NOT NULL THEN
+        -- PostgreSQL 17: 0001's fallback already occupies the name.
+        RETURN;
+    END IF;
+
+    IF to_regprocedure('pg_catalog.uuidv7()') IS NULL THEN
+        RAISE EXCEPTION
+            'no uuidv7() generator: neither pg_catalog.uuidv7() nor kiroku.uuidv7() exists';
+    END IF;
+
+    -- PostgreSQL 18+: alias the builtin so the qualified name is available.
+    EXECUTE $fn$
+        CREATE FUNCTION kiroku.uuidv7()
+        RETURNS uuid
+        LANGUAGE sql
+        VOLATILE
+        AS 'SELECT pg_catalog.uuidv7()'
+    $fn$;
+END
+$$;
+
+-- A schema-local singleton row serializes lease lifecycle changes with every
+-- destructive statement without adding work to append or ordinary read paths.
+CREATE TABLE kiroku.history_retention_coordinator (
+    singleton BOOLEAN PRIMARY KEY DEFAULT TRUE,
+    CONSTRAINT chk_history_retention_coordinator_singleton CHECK (singleton)
+);
+
+INSERT INTO kiroku.history_retention_coordinator (singleton)
+VALUES (TRUE)
+ON CONFLICT DO NOTHING;
+
+-- Durable operational evidence for rebuilds that require the retained event
+-- set to remain stable. Active state is derived from released_at and database
+-- time; expiry needs no worker or cleanup mutation.
+CREATE TABLE kiroku.history_retention_leases (
+    lease_id          UUID        PRIMARY KEY DEFAULT kiroku.uuidv7(),
+    owner             TEXT        NOT NULL,
+    reason            TEXT        NOT NULL,
+    protected_through BIGINT      NOT NULL,
+    created_at        TIMESTAMPTZ NOT NULL,
+    renewed_at        TIMESTAMPTZ NOT NULL,
+    expires_at        TIMESTAMPTZ NOT NULL,
+    released_at       TIMESTAMPTZ,
+    CONSTRAINT chk_history_retention_lease_owner_bytes
+        CHECK (octet_length(owner) BETWEEN 1 AND 512),
+    CONSTRAINT chk_history_retention_lease_reason_bytes
+        CHECK (octet_length(reason) BETWEEN 1 AND 2048),
+    CONSTRAINT chk_history_retention_lease_frontier
+        CHECK (protected_through >= 0),
+    CONSTRAINT chk_history_retention_lease_renewal_time
+        CHECK (renewed_at >= created_at),
+    CONSTRAINT chk_history_retention_lease_expiry
+        CHECK (expires_at > created_at),
+    CONSTRAINT chk_history_retention_lease_release_time
+        CHECK (released_at IS NULL OR released_at >= created_at)
+);
+
+CREATE INDEX ix_history_retention_leases_unreleased_expiry
+    ON kiroku.history_retention_leases (expires_at)
+    WHERE released_at IS NULL;
+
+-- This function is attached only to DELETE and TRUNCATE. TG_TABLE_SCHEMA is
+-- quoted into each dynamic statement so a schema-qualified maintenance command
+-- cannot be redirected to a coordinator selected through the caller's
+-- search_path.
+CREATE FUNCTION kiroku.protect_replay_history_from_destruction()
+RETURNS TRIGGER
+LANGUAGE plpgsql
+AS $$
+DECLARE
+    active_count BIGINT;
+BEGIN
+    EXECUTE format(
+        'SELECT singleton FROM %I.history_retention_coordinator WHERE singleton FOR UPDATE',
+        TG_TABLE_SCHEMA
+    );
+
+    EXECUTE format(
+        'SELECT count(*) FROM %I.history_retention_leases WHERE released_at IS NULL AND expires_at > clock_timestamp()',
+        TG_TABLE_SCHEMA
+    ) INTO active_count;
+
+    IF active_count > 0 THEN
+        RAISE EXCEPTION USING
+            ERRCODE = 'KR001',
+            MESSAGE = format(
+                'replay history is protected by %s active retention lease(s)',
+                active_count
+            );
+    END IF;
+
+    RETURN NULL;
+END;
+$$;
+
+CREATE TRIGGER protect_replay_history_delete
+    BEFORE DELETE ON kiroku.events
+    FOR EACH STATEMENT
+    EXECUTE FUNCTION kiroku.protect_replay_history_from_destruction();
+
+CREATE TRIGGER protect_replay_history_truncate
+    BEFORE TRUNCATE ON kiroku.events
+    FOR EACH STATEMENT
+    EXECUTE FUNCTION kiroku.protect_replay_history_from_destruction();
+
+CREATE TRIGGER protect_replay_history_delete
+    BEFORE DELETE ON kiroku.stream_events
+    FOR EACH STATEMENT
+    EXECUTE FUNCTION kiroku.protect_replay_history_from_destruction();
+
+CREATE TRIGGER protect_replay_history_truncate
+    BEFORE TRUNCATE ON kiroku.stream_events
+    FOR EACH STATEMENT
+    EXECUTE FUNCTION kiroku.protect_replay_history_from_destruction();
+
+CREATE TRIGGER protect_replay_history_delete
+    BEFORE DELETE ON kiroku.streams
+    FOR EACH STATEMENT
+    EXECUTE FUNCTION kiroku.protect_replay_history_from_destruction();
+
+CREATE TRIGGER protect_replay_history_truncate
+    BEFORE TRUNCATE ON kiroku.streams
+    FOR EACH STATEMENT
+    EXECUTE FUNCTION kiroku.protect_replay_history_from_destruction();
+
+COMMENT ON TABLE kiroku.history_retention_leases IS
+  'Durable, expiring replay-history retention leases; rows are operational evidence and remain until explicitly pruned.';
+
+COMMENT ON SCHEMA kiroku IS
+  'Managed by pg-migrate component kiroku through 0010';
diff --git a/test/fixtures/kiroku-0.7.1.0/0011.sql b/test/fixtures/kiroku-0.7.1.0/0011.sql
new file mode 100644
--- /dev/null
+++ b/test/fixtures/kiroku-0.7.1.0/0011.sql
@@ -0,0 +1,51 @@
+-- converge databases that applied the withdrawn 0.3.2.x payload of 0010
+
+-- kiroku-store-migrations 0.3.2.0 and 0.3.2.1 shipped a payload of 0010 whose
+-- lease_id default named uuidv7() unqualified. That payload could not parse on
+-- PostgreSQL 17 outside the bootstrap session (BUG-1), and where it did apply
+-- -- PostgreSQL 18, or a fresh PostgreSQL 17 install -- it left the schema in a
+-- state the corrected 0010 does not produce: no kiroku.uuidv7(), and a lease_id
+-- default bound to whichever uuidv7() the applying session happened to resolve.
+--
+-- The corrected 0010 cannot reach those databases: its ledger row is already
+-- present, and re-baselining that row (see ledger-fixups/) restores the
+-- checksum without replaying the SQL. This migration is what makes both
+-- populations converge, and it is a no-op on any database that applied the
+-- corrected 0010.
+
+-- 1. kiroku.uuidv7() exists on every supported major version. Identical to the
+--    header of the corrected 0010, and skipped there because 0010 ran first.
+DO $$
+BEGIN
+    IF to_regprocedure('kiroku.uuidv7()') IS NOT NULL THEN
+        -- PostgreSQL 17, or a database that applied the corrected 0010.
+        RETURN;
+    END IF;
+
+    IF to_regprocedure('pg_catalog.uuidv7()') IS NULL THEN
+        RAISE EXCEPTION
+            'no uuidv7() generator: neither pg_catalog.uuidv7() nor kiroku.uuidv7() exists';
+    END IF;
+
+    EXECUTE $fn$
+        CREATE FUNCTION kiroku.uuidv7()
+        RETURNS uuid
+        LANGUAGE sql
+        VOLATILE
+        AS 'SELECT pg_catalog.uuidv7()'
+    $fn$;
+END
+$$;
+
+-- 2. Bind lease_id's default to the qualified generator. A column default is
+--    stored as a resolved function OID, so a database that applied the
+--    withdrawn payload on PostgreSQL 18 holds pg_catalog.uuidv7() here. Both
+--    generators produce the same values; this only makes the stored default
+--    match what the corrected 0010 installs, so a dumped schema is identical
+--    whichever payload the database applied. Existing lease rows are untouched
+--    and no table is rewritten.
+ALTER TABLE kiroku.history_retention_leases
+    ALTER COLUMN lease_id SET DEFAULT kiroku.uuidv7();
+
+COMMENT ON SCHEMA kiroku IS
+  'Managed by pg-migrate component kiroku through 0011';
diff --git a/test/fixtures/kiroku-0.7.1.0/0012.sql b/test/fixtures/kiroku-0.7.1.0/0012.sql
new file mode 100644
--- /dev/null
+++ b/test/fixtures/kiroku-0.7.1.0/0012.sql
@@ -0,0 +1,51 @@
+-- denormalize category onto $all junction rows for category reads
+
+-- Category reads (readCategoryForwardSQL and its consumer-group variant in
+-- kiroku-store) used to start from every stream of the category and probe
+-- stream_events once per stream, so a caught-up poll cost work proportional to
+-- the number of streams ever written in the category (BUG-2). Carrying the
+-- originating stream's category on each $all junction row lets both reads run
+-- as one index range scan from (category, checkpoint) that stops at the limit.
+--
+-- The column is populated only on $all rows (stream_id = 0). Home rows and link
+-- rows keep it NULL; nothing reads them by category. The CHECK below makes any
+-- inserter that forgets the column fail loudly instead of writing rows that a
+-- category read cannot see.
+--
+-- The whole file runs in one transaction. The backfill rewrites every $all row
+-- and the index build blocks writes, so appends wait for the duration; apply it
+-- in a maintenance window on a large store and VACUUM (ANALYZE)
+-- kiroku.stream_events afterwards.
+
+ALTER TABLE kiroku.stream_events
+    ADD COLUMN category TEXT;
+
+COMMENT ON COLUMN kiroku.stream_events.category IS
+  'Originating stream''s category, present on $all rows (stream_id = 0) only; equals streams.category of original_stream_id.';
+
+-- Backfill every existing $all row from its originating stream. The immutability
+-- trigger rejects every UPDATE on this table, so it is suspended for this one
+-- statement and re-enabled before the transaction ends. Runs as the table owner.
+ALTER TABLE kiroku.stream_events DISABLE TRIGGER no_update_stream_events;
+
+UPDATE kiroku.stream_events AS se
+SET category = s.category
+FROM kiroku.streams AS s
+WHERE se.stream_id = 0
+  AND s.stream_id = se.original_stream_id;
+
+ALTER TABLE kiroku.stream_events ENABLE TRIGGER no_update_stream_events;
+
+ALTER TABLE kiroku.stream_events
+    ADD CONSTRAINT ck_stream_events_all_category
+    CHECK (stream_id <> 0 OR category IS NOT NULL);
+
+-- Category read path: rows of one category in global-position order. The
+-- INCLUDE column lets the consumer-group hash predicate run on index tuples.
+CREATE INDEX ix_stream_events_all_by_category
+    ON kiroku.stream_events (category, stream_version)
+    INCLUDE (original_stream_id)
+    WHERE stream_id = 0;
+
+COMMENT ON SCHEMA kiroku IS
+  'Managed by pg-migrate component kiroku through 0012';
diff --git a/test/fixtures/kiroku-0.7.1.0/0013.sql b/test/fixtures/kiroku-0.7.1.0/0013.sql
new file mode 100644
--- /dev/null
+++ b/test/fixtures/kiroku-0.7.1.0/0013.sql
@@ -0,0 +1,13 @@
+-- Derive consumer-group topology from existing member checkpoints.
+-- Stop all subscription workers before applying this migration.
+-- An incomplete legacy row set derives an underestimate: startup refuses the
+-- configured topology until the operator explicitly equalizes it with resize.
+UPDATE kiroku.subscriptions AS s
+SET consumer_group_size = derived.size
+FROM (
+    SELECT subscription_name, max(consumer_group_member) + 1 AS size
+    FROM kiroku.subscriptions
+    GROUP BY subscription_name
+) AS derived
+WHERE s.subscription_name = derived.subscription_name
+  AND s.consumer_group_size <> derived.size;
diff --git a/test/fixtures/kiroku-0.7.1.0/0014.sql b/test/fixtures/kiroku-0.7.1.0/0014.sql
new file mode 100644
--- /dev/null
+++ b/test/fixtures/kiroku-0.7.1.0/0014.sql
@@ -0,0 +1,11 @@
+-- bind subscription checkpoints to their targets
+
+
+-- Stop subscription workers before applying this catalog change and CHECK scan.
+-- Constant defaults preserve legacy rows without a heap rewrite.
+ALTER TABLE kiroku.subscriptions
+    ADD COLUMN target_kind TEXT NOT NULL DEFAULT 'unbound'
+        CHECK (target_kind IN ('unbound', 'all', 'category')),
+    ADD COLUMN target_category TEXT
+        CHECK ((target_kind = 'category') = (target_category IS NOT NULL)),
+    DROP COLUMN stream_name;
diff --git a/test/fixtures/kiroku-0.7.1.0/0015.sql b/test/fixtures/kiroku-0.7.1.0/0015.sql
new file mode 100644
--- /dev/null
+++ b/test/fixtures/kiroku-0.7.1.0/0015.sql
@@ -0,0 +1,16 @@
+-- Stable byte-ordered stream catalog browsing (MasterPlan 13 / plan 88).
+-- The original unique-name constraint and category index remain unchanged.
+-- This one browse index also serves literal prefixes and exact-category ranges;
+-- plan 54's bounded global-event windows require no separate prefix index.
+-- PostgreSQL applies this transactional index build once. It blocks writes for
+-- the build duration; schedule the upgrade in a maintenance window on large
+-- stores. New-stream inserts and non-HOT application-stream updates pay its
+-- maintenance cost. The reserved global stream is outside this partial index.
+CREATE INDEX ix_streams_browse_name
+    ON kiroku.streams (stream_name COLLATE "C")
+    WHERE stream_id <> 0;
+
+COMMENT ON INDEX kiroku.ix_streams_browse_name IS
+    'Byte-ordered stream browsing and literal-prefix seeks; does not change unique-name or subscription event ordering.';
+COMMENT ON SCHEMA kiroku IS
+    'Managed by pg-migrate component kiroku through 0015';
diff --git a/test/fixtures/kiroku-0.7.1.0/README.md b/test/fixtures/kiroku-0.7.1.0/README.md
new file mode 100644
--- /dev/null
+++ b/test/fixtures/kiroku-0.7.1.0/README.md
@@ -0,0 +1,8 @@
+# Released Kiroku 0.7.1.0 migration payloads
+
+Frozen from the Hackage `kiroku-store-migrations-0.7.1.0.tar.gz` archive,
+owned by mori://shinzui/kiroku/packages/kiroku-store-migrations. All fifteen
+SQL files match the retained prefix at release commit
+`22d103e4f424caab06c6a2b76ad92b408c262bc0`. Tests freeze their SHA-256s and
+require the current composed plan to verify the installed checksums before
+applying only 0016. Never regenerate these from current SQL.
diff --git a/test/fixtures/kiroku-0.7.1.0/manifest b/test/fixtures/kiroku-0.7.1.0/manifest
new file mode 100644
--- /dev/null
+++ b/test/fixtures/kiroku-0.7.1.0/manifest
@@ -0,0 +1,15 @@
+0001-kiroku-bootstrap.sql
+0002-add-subscription-dead-letters.sql
+0003-notify-trigger-append-guard.sql
+0004-dead-letters-event-id-index.sql
+0005-index-hygiene-and-streams-fillfactor.sql
+0006-stream-name-length-check.sql
+0007-stream-truncate-before.sql
+0008-schema-management-comment.sql
+0009.sql
+0010.sql
+0011.sql
+0012.sql
+0013.sql
+0014.sql
+0015.sql
