diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,23 @@
 
 ## Unreleased
 
+## 0.2.0.3 — 2026-08-29
+
+### Changed
+
+- Every library dependency now carries a PVP upper bound: `aeson`, `bytestring`,
+  `containers`, `directory`, `effectful`, `generic-lens`, `scientific`, `time`,
+  `vector`. `cabal check` reported these under `missing-upper-bounds`. Without
+  one, a future breaking release of a dependency enters a consumer's build plan
+  unchecked — which is the failure the bound exists to prevent.
+
+  Each bound admits the version this package is built and tested against and
+  stops below the next major.
+
+  `aeson` stops at `<2.3` rather than `<2.4`: baikai-openai 0.5 constrains it to
+  `^>=2.2`, so aeson 2.3 is not reachable for this cohort and a wider bound
+  would assert compatibility that cannot be exercised here.
+
 ## 0.2.0.2 — 2026-08-07
 
 ### Changed
diff --git a/shikumi-trace.cabal b/shikumi-trace.cabal
--- a/shikumi-trace.cabal
+++ b/shikumi-trace.cabal
@@ -1,6 +1,6 @@
 cabal-version:   3.4
 name:            shikumi-trace
-version:         0.2.0.2
+version:         0.2.0.3
 synopsis:
   Hierarchical tracing, observability, and deterministic replay for shikumi (EP-7)
 
@@ -50,21 +50,21 @@
     Shikumi.Trace.Store
 
   build-depends:
-    , aeson
-    , baikai         >=0.5      && <0.6
+    , aeson          >=2.2      && <2.3
+    , baikai         >=0.6      && <0.7
     , base           >=4.20     && <5
-    , bytestring
-    , containers
-    , directory
-    , effectful
-    , generic-lens
+    , bytestring     >=0.11     && <0.13
+    , containers     >=0.6      && <0.9
+    , directory      >=1.3      && <1.4
+    , effectful      >=2.5      && <2.7
+    , generic-lens   >=2.2      && <2.4
     , lens           ^>=5.3
-    , scientific
+    , scientific     >=0.3      && <0.4
     , shikumi        ^>=0.3.0.0
     , shikumi-cache  ^>=0.1.2.0
     , text           ^>=2.1
-    , time
-    , vector
+    , time           >=1.12     && <1.17
+    , vector         >=0.13     && <0.14
 
 executable shikumi-trace-demo
   import:         common-options
@@ -84,7 +84,7 @@
   other-modules:  TraceFixtures
   build-depends:
     , aeson
-    , baikai            >=0.5      && <0.6
+    , baikai            >=0.6      && <0.7
     , base
     , bytestring
     , containers
diff --git a/src/Shikumi/Trace/Demo.hs b/src/Shikumi/Trace/Demo.hs
--- a/src/Shikumi/Trace/Demo.hs
+++ b/src/Shikumi/Trace/Demo.hs
@@ -35,12 +35,12 @@
     Response,
     TextContent (..),
     UserContent (UserText),
+    emptyContext,
+    emptyModel,
+    emptyOptions,
+    emptyResponse,
+    emptyTextContent,
     user,
-    _Context,
-    _Model,
-    _Options,
-    _Response,
-    _TextContent,
   )
 import Control.Lens ((&), (.~), (^.))
 import Data.Generics.Labels ()
@@ -72,7 +72,7 @@
 -- | The demo model's routing identity (LM-call spans read @stub/stub-model@).
 demoModel :: Model
 demoModel =
-  _Model
+  emptyModel
     & #provider .~ "stub"
     & #modelId .~ "stub-model"
     & #api .~ Custom "stub"
@@ -145,15 +145,15 @@
 -- ---------------------------------------------------------------------------
 
 opts :: Options
-opts = _Options
+opts = emptyOptions
 
 ctxFor :: Text -> Context
-ctxFor t = _Context & #messages .~ V.singleton (user t)
+ctxFor t = emptyContext & #messages .~ V.singleton (user t)
 
 mkResponse :: Text -> Response
 mkResponse t =
-  _Response
-    & #message . #content .~ V.singleton (AssistantText (_TextContent & #text .~ t))
+  emptyResponse
+    & #message . #content .~ V.singleton (AssistantText (emptyTextContent & #text .~ t))
     & #message . #usage . #inputTokens .~ 24
     & #message . #usage . #outputTokens .~ 9
     & #latencyMs .~ 7
diff --git a/src/Shikumi/Trace/Internal/Spike.hs b/src/Shikumi/Trace/Internal/Spike.hs
--- a/src/Shikumi/Trace/Internal/Spike.hs
+++ b/src/Shikumi/Trace/Internal/Spike.hs
@@ -33,11 +33,11 @@
     Context,
     Response,
     TextContent (..),
-    _Context,
-    _Model,
-    _Options,
-    _Response,
-    _TextContent,
+    emptyContext,
+    emptyModel,
+    emptyOptions,
+    emptyResponse,
+    emptyTextContent,
   )
 import Control.Lens ((&), (.~), (^.))
 import Data.Generics.Labels ()
@@ -68,10 +68,10 @@
       pure ()
   liftIO (readIORef captured)
   where
-    oneCall = complete _Model neutralCtx _Options
+    oneCall = complete emptyModel neutralCtx emptyOptions
 
 neutralCtx :: Context
-neutralCtx = _Context
+neutralCtx = emptyContext
 
 -- ---------------------------------------------------------------------------
 -- The mechanism under test
@@ -134,7 +134,7 @@
 -- | A 'Response' carrying @t@ as its single assistant text block.
 stubResponse :: Text -> Response
 stubResponse t =
-  _Response & #message . #content .~ V.singleton (AssistantText (_TextContent & #text .~ t))
+  emptyResponse & #message . #content .~ V.singleton (AssistantText (emptyTextContent & #text .~ t))
 
 -- | Concatenate the assistant text blocks of a response.
 responseText :: Response -> Text
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -2,7 +2,7 @@
 -- @spike@ (M0), @tree@ (M1), @store@ (M2), @replay@ (M3), @e2e@ (M5).
 module Main (main) where
 
-import Baikai (Context, Model, Options, Response, user, _Context, _Model, _Options)
+import Baikai (Context, Model, Options, Response, emptyContext, emptyModel, emptyOptions, user)
 import Control.Exception (try)
 import Control.Lens ((&), (.~))
 import Data.Aeson (Value (Object), decode, encode, object, (.=))
@@ -337,16 +337,16 @@
 -- | The EP-6 golden fixture and its pinned digest, copied here so the two plans
 -- are proven byte-for-byte identical.
 fixModel :: Model
-fixModel = _Model & #modelId .~ "claude-sonnet-4-6" & #provider .~ "anthropic"
+fixModel = emptyModel & #modelId .~ "claude-sonnet-4-6" & #provider .~ "anthropic"
 
 fixCtx :: Context
 fixCtx =
-  _Context
+  emptyContext
     & #systemPrompt .~ Just "You are helpful."
     & #messages .~ V.singleton (user "ping")
 
 fixOpts :: Options
-fixOpts = _Options & #temperature .~ Just 0.0 & #maxTokens .~ Just 1024
+fixOpts = emptyOptions & #temperature .~ Just 0.0 & #maxTokens .~ Just 1024
 
 pinnedKey :: Text
 pinnedKey = "b31fd70140abbd0198c6b7caec748a8389bf93be909164bdcc340731b7032564"
diff --git a/test/TraceFixtures.hs b/test/TraceFixtures.hs
--- a/test/TraceFixtures.hs
+++ b/test/TraceFixtures.hs
@@ -27,12 +27,12 @@
     Response,
     TextContent (..),
     UserContent (UserText),
+    emptyContext,
+    emptyModel,
+    emptyOptions,
+    emptyResponse,
+    emptyTextContent,
     user,
-    _Context,
-    _Model,
-    _Options,
-    _Response,
-    _TextContent,
   )
 import Control.Lens ((&), (.~), (^.))
 import Data.Generics.Labels ()
@@ -47,25 +47,25 @@
 -- @stub/stub-model@.
 stubModel :: Model
 stubModel =
-  _Model
+  emptyModel
     & #provider .~ "stub"
     & #modelId .~ "stub-model"
     & #api .~ Custom "stub"
 
 -- | A one-user-turn request context carrying @t@ (varies the cache key per stage).
 ctxFor :: Text -> Context
-ctxFor t = _Context & #messages .~ V.singleton (user t)
+ctxFor t = emptyContext & #messages .~ V.singleton (user t)
 
 -- | Default options.
 optsFor :: Options
-optsFor = _Options
+optsFor = emptyOptions
 
 -- | A response carrying @t@ as its single assistant text block, with small fixed
 -- token counts and latency so span attributes are populated.
 mkResponse :: Text -> Response
 mkResponse t =
-  _Response
-    & #message . #content .~ V.singleton (AssistantText (_TextContent & #text .~ t))
+  emptyResponse
+    & #message . #content .~ V.singleton (AssistantText (emptyTextContent & #text .~ t))
     & #message . #usage . #inputTokens .~ 10
     & #message . #usage . #outputTokens .~ 5
     & #latencyMs .~ 12
