diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,20 @@
 # Changelog for settei-dhall
 
-## 0.1.0.0 — 2026-07-18
+## 0.2.0.0 — 2026-07-19
 
+- Use the family-standard `lens` package in test suites instead of the leftover
+  `microlens` prototype dependency.
+- Add `renderDhallErrorText` and `renderDhallErrorsText` for stable, operator-readable,
+  secret-safe adapter diagnostics with optional source positions.
+
+## 0.1.0.0 — 2026-07-19
+
 - Initial experimental release.
 - Add typed Dhall-to-Settei translation with NoImports and LocalImportsWithin policies.
 - Retain root and transitive import-closure provenance without claiming unavailable
   leaf-level import attribution.
+- Document that import-policy preflight and file reads are not atomic, so an
+  untrusted-writable import root remains vulnerable to a time-of-check/time-of-use race.
+- Add optional one-based line and column fields to `DhallSourceError`, expose them through
+  `dhallErrorLine` and `dhallErrorColumn`, and populate them for root and local-import
+  parse failures.
diff --git a/settei-dhall.cabal b/settei-dhall.cabal
--- a/settei-dhall.cabal
+++ b/settei-dhall.cabal
@@ -1,6 +1,6 @@
 cabal-version:      3.8
 name:               settei-dhall
-version:            0.1.0.0
+version:            0.2.0.0
 synopsis:           Dhall sources for Settei
 description:
   Evaluate typed Dhall expressions into provenance-aware Settei sources with
@@ -48,8 +48,9 @@
     , directory     >=1.3.8   && <1.4
     , filepath      >=1.5.4   && <1.6
     , generic-lens  >=2.2     && <2.4
+    , megaparsec    >=9       && <10
     , scientific    >=0.3.7   && <0.4
-    , settei        ==0.1.0.0
+    , settei        ==0.2.0.0
     , text          >=2.1     && <2.2
     , transformers  >=0.6.1   && <0.7
 
@@ -65,7 +66,7 @@
     , dhall         >=1.42.3 && <1.43
     , directory     >=1.3.8  && <1.4
     , filepath      >=1.5.4  && <1.6
-    , microlens     >=0.4.14 && <0.6
+    , lens          >=5.3    && <5.4
     , tasty         >=1.5    && <1.6
     , tasty-hunit   >=0.10.2 && <0.11
     , temporary     >=1.3    && <1.4
@@ -89,9 +90,9 @@
     , directory     >=1.3.8   && <1.4
     , filepath      >=1.5.4   && <1.6
     , generic-lens  >=2.2     && <2.4
-    , microlens     >=0.4.14  && <0.6
-    , settei        ==0.1.0.0
-    , settei-dhall  ==0.1.0.0
+    , lens          >=5.3     && <5.4
+    , settei        ==0.2.0.0
+    , settei-dhall  ==0.2.0.0
     , tasty         >=1.5     && <1.6
     , tasty-hunit   >=0.10.2  && <0.11
     , temporary     >=1.3     && <1.4
diff --git a/src/Settei/Dhall.hs b/src/Settei/Dhall.hs
--- a/src/Settei/Dhall.hs
+++ b/src/Settei/Dhall.hs
@@ -14,6 +14,8 @@
     DhallSourceOptions,
     annotateDhallSourceOptions,
     dhallErrorCategory,
+    dhallErrorColumn,
+    dhallErrorLine,
     dhallErrorMessage,
     dhallErrorName,
     dhallErrorPath,
@@ -29,6 +31,8 @@
     dhallSourceOptions,
     loadDhallSource,
     loadDhallSourceDetailed,
+    renderDhallErrorText,
+    renderDhallErrorsText,
   )
 where
 
@@ -58,6 +62,7 @@
 import Settei.Prelude
 import System.Directory qualified as Directory
 import System.FilePath qualified as FilePath
+import Text.Megaparsec qualified as Megaparsec
 
 -- | Import capabilities available to version-one callers.
 --
@@ -129,12 +134,15 @@
 
 -- | A secret-safe Dhall input failure.
 --
--- The error retains only a stable category, source name, optional safe path,
--- and fixed message. It never retains source text or an upstream exception.
+-- The error retains only a stable category, source name, optional safe path, optional
+-- one-based position, and fixed message. It never retains source text or an upstream
+-- exception.
 data DhallSourceError = DhallSourceError
   { category :: !DhallErrorCategory,
     name :: !Text,
     path :: !(Maybe FilePath),
+    line :: !(Maybe Int),
+    column :: !(Maybe Int),
     message :: !Text
   }
   deriving stock (Generic, Eq, Show)
@@ -150,6 +158,8 @@
 data PreflightFailure = PreflightFailure
   { category :: !DhallErrorCategory,
     path :: !(Maybe FilePath),
+    line :: !(Maybe Int),
+    column :: !(Maybe Int),
     message :: !Text
   }
   deriving stock (Generic)
@@ -187,10 +197,44 @@
 dhallErrorPath :: DhallSourceError -> Maybe FilePath
 dhallErrorPath problem = problem ^. #path
 
+-- | Return the one-based line associated with an error, when known.
+dhallErrorLine :: DhallSourceError -> Maybe Int
+dhallErrorLine problem = problem ^. #line
+
+-- | Return the one-based column associated with an error, when known.
+dhallErrorColumn :: DhallSourceError -> Maybe Int
+dhallErrorColumn problem = problem ^. #column
+
 -- | Return a fixed secret-safe error message.
 dhallErrorMessage :: DhallSourceError -> Text
 dhallErrorMessage problem = problem ^. #message
 
+-- | Render one Dhall input failure as a single operator-readable line.
+--
+-- The line leads with the stable source name, then a parenthetical containing
+-- the colon-joined available path, line, and column, followed by the fixed
+-- message. The parenthetical is omitted when no location is known. No raw
+-- configuration value can appear because 'DhallSourceError' retains none.
+renderDhallErrorText :: DhallSourceError -> Text
+renderDhallErrorText problem =
+  problem ^. #name
+    <> locationText
+    <> ": "
+    <> problem ^. #message
+  where
+    locationText = case locationPieces of
+      [] -> ""
+      pieces -> " (" <> Text.intercalate ":" pieces <> ")"
+    locationPieces =
+      maybe [] (pure . Text.pack) (problem ^. #path)
+        <> maybe [] (pure . Text.pack . show) (problem ^. #line)
+        <> maybe [] (pure . Text.pack . show) (problem ^. #column)
+
+-- | Render every Dhall input failure, one line per problem, matching
+-- 'Settei.Render.renderErrorsText' in shape and trailing newline.
+renderDhallErrorsText :: NonEmpty DhallSourceError -> Text
+renderDhallErrorsText = Text.unlines . fmap renderDhallErrorText . NonEmpty.toList
+
 -- | Return a local import's canonical path.
 dhallImportPath :: DhallImport -> FilePath
 dhallImportPath imported = imported ^. #path
@@ -234,7 +278,17 @@
     Left problem -> pure (Left (NonEmpty.singleton problem))
     Right prepared ->
       case DhallParser.exprFromText (Text.unpack (prepared ^. #label)) (prepared ^. #input) of
-        Left _ -> pure (failure options DhallParseError (prepared ^. #filePath) "invalid Dhall syntax")
+        Left parseError ->
+          let (line, column) = parseErrorPosition parseError
+           in pure
+                ( positionedFailure
+                    options
+                    DhallParseError
+                    (prepared ^. #filePath)
+                    line
+                    column
+                    "invalid Dhall syntax"
+                )
         Right expression -> do
           resolvedResult <- resolveExpression options prepared expression
           pure $ do
@@ -320,10 +374,12 @@
         case preflightResult of
           Left problem ->
             pure
-              ( failure
+              ( positionedFailure
                   options
                   (problem ^. #category)
                   (problem ^. #path)
+                  (problem ^. #line)
+                  (problem ^. #column)
                   (problem ^. #message)
               )
           Right imports -> do
@@ -397,7 +453,14 @@
             Left _ -> throwPreflight DhallImportError (Just canonical) "cannot read local import"
             Right value -> pure value
           imported <- case DhallParser.exprFromText canonical input of
-            Left _ -> throwPreflight DhallParseError (Just canonical) "invalid syntax in local import"
+            Left parseError ->
+              let (line, column) = parseErrorPosition parseError
+               in throwPositionedPreflight
+                    DhallParseError
+                    (Just canonical)
+                    line
+                    column
+                    "invalid syntax in local import"
             Right value -> pure value
           visitExpression
             allowedRoot
@@ -497,8 +560,18 @@
 importModeName DhallLocationImport = "location"
 
 throwPreflight :: DhallErrorCategory -> Maybe FilePath -> Text -> ExceptT PreflightFailure IO a
-throwPreflight category path message = throwE PreflightFailure {category, path, message}
+throwPreflight category path = throwPositionedPreflight category path Nothing Nothing
 
+throwPositionedPreflight ::
+  DhallErrorCategory ->
+  Maybe FilePath ->
+  Maybe Int ->
+  Maybe Int ->
+  Text ->
+  ExceptT PreflightFailure IO a
+throwPositionedPreflight category path line column message =
+  throwE PreflightFailure {category, path, line, column, message}
+
 isWithin :: FilePath -> FilePath -> Bool
 isWithin root pathCandidate =
   let relative = FilePath.makeRelative root pathCandidate
@@ -508,11 +581,42 @@
           _ -> True
 
 failure :: DhallSourceOptions -> DhallErrorCategory -> Maybe FilePath -> Text -> Either (NonEmpty DhallSourceError) a
-failure options category path message = Left (NonEmpty.singleton (singleError options category path message))
+failure options category path = positionedFailure options category path Nothing Nothing
 
+positionedFailure ::
+  DhallSourceOptions ->
+  DhallErrorCategory ->
+  Maybe FilePath ->
+  Maybe Int ->
+  Maybe Int ->
+  Text ->
+  Either (NonEmpty DhallSourceError) a
+positionedFailure options category path line column message =
+  Left (NonEmpty.singleton (positionedError options category path line column message))
+
 singleError :: DhallSourceOptions -> DhallErrorCategory -> Maybe FilePath -> Text -> DhallSourceError
-singleError options category path message =
-  DhallSourceError {category, name = options ^. #name, path, message}
+singleError options category path = positionedError options category path Nothing Nothing
+
+positionedError ::
+  DhallSourceOptions ->
+  DhallErrorCategory ->
+  Maybe FilePath ->
+  Maybe Int ->
+  Maybe Int ->
+  Text ->
+  DhallSourceError
+positionedError options category path line column message =
+  DhallSourceError {category, name = options ^. #name, path, line, column, message}
+
+parseErrorPosition :: DhallParser.ParseError -> (Maybe Int, Maybe Int)
+parseErrorPosition parseError =
+  let bundle = DhallParser.unwrap parseError
+      offset = Megaparsec.errorOffset (NonEmpty.head (Megaparsec.bundleErrors bundle))
+      reached = Megaparsec.reachOffsetNoLine offset (Megaparsec.bundlePosState bundle)
+      position = Megaparsec.pstateSourcePos reached
+   in ( Just (Megaparsec.unPos (Megaparsec.sourceLine position)),
+        Just (Megaparsec.unPos (Megaparsec.sourceColumn position))
+      )
 
 firstOne ::
   DhallSourceOptions ->
diff --git a/test/Settei/DhallPrototypeTest.hs b/test/Settei/DhallPrototypeTest.hs
--- a/test/Settei/DhallPrototypeTest.hs
+++ b/test/Settei/DhallPrototypeTest.hs
@@ -3,6 +3,7 @@
 module Settei.DhallPrototypeTest (tests) where
 
 import Control.Exception (SomeException, try)
+import Control.Lens qualified as Lens
 import Control.Monad (foldM, unless, when)
 import Control.Monad.IO.Class (liftIO)
 import Control.Monad.Trans.Except (ExceptT, runExceptT, throwE)
@@ -17,7 +18,6 @@
 import Dhall.Core qualified as Dhall
 import Dhall.Import qualified as DhallImport
 import Dhall.Parser qualified as DhallParser
-import Lens.Micro qualified as Lens
 import System.Directory qualified as Directory
 import System.FilePath qualified as FilePath
 import System.IO.Temp (withSystemTempDirectory)
diff --git a/test/Settei/DhallTest.hs b/test/Settei/DhallTest.hs
--- a/test/Settei/DhallTest.hs
+++ b/test/Settei/DhallTest.hs
@@ -25,7 +25,8 @@
 tests =
   testGroup
     "Settei.Dhall"
-    [ testCase "a typed record becomes a nested source" $ do
+    [ errorRenderingTests,
+      testCase "a typed record becomes a nested source" $ do
         loaded <-
           expectDetailed
             noImportOptions
@@ -105,7 +106,8 @@
           annotations ^. at "dhall.import-count" @?= Just "2"
           annotations ^. at "dhall.provenance-precision"
             @?= Just "root-and-import-closure; leaf-level import attribution unavailable after normalization"
-          result <- expectResolution (resolve defaultResolveOptions [input] (required portSetting))
+          let result = resolve defaultResolveOptions [input] (required portSetting)
+          _ <- expectResolution result
           let rendered = renderResolutionText (result ^. #report)
           assertBool "text report omitted Dhall root" (Text.pack application `Text.isInfixOf` rendered)
           assertBool
@@ -181,14 +183,13 @@
       testCase "higher Dhall sources override leaves through the core resolver" $ do
         low <- expectSource (dhallSourceOptions "low" NoImports) (dhallExpression "low" "{ service = { host = \"old.internal\", port = 7000 } }")
         high <- expectSource (dhallSourceOptions "high" NoImports) (dhallExpression "high" "{ service = { port = 9000 } }")
-        result <-
-          expectResolution
-            ( resolve
+        let result =
+              resolve
                 defaultResolveOptions
                 [low, high]
                 ((,) <$> required hostSetting <*> required portSetting)
-            )
-        result ^. #value @?= ("old.internal", 9000),
+        value <- expectResolution result
+        value @?= ("old.internal", 9000),
       testCase "stable error phases and Show output never retain source secrets" $ do
         let cases =
               [ (dhallExpression "parse" ("{ password = \"" <> secretSentinel <> "\", broken ="), DhallParseError),
@@ -206,13 +207,40 @@
                 (not (secretSentinel `Text.isInfixOf` Text.pack (show problem)))
           )
           cases,
+      testCase "root parse errors carry exact one-based positions" $ do
+        firstProblem <- expectError noImportOptions (dhallExpression "broken" "{ port =")
+        dhallErrorCategory firstProblem @?= DhallParseError
+        dhallErrorLine firstProblem @?= Just 1
+        dhallErrorColumn firstProblem @?= Just 9
+        secondProblem <-
+          expectError
+            noImportOptions
+            (dhallExpression "broken2" "let a = 1\nin { port = }")
+        dhallErrorCategory secondProblem @?= DhallParseError
+        dhallErrorLine secondProblem @?= Just 2
+        dhallErrorColumn secondProblem @?= Just 13
+        dhallErrorMessage secondProblem @?= "invalid Dhall syntax",
+      testCase "local-import parse errors retain the imported-file position" $
+        withSystemTempDirectory "settei-dhall-parse-position" $ \root -> do
+          let imported = root FilePath.</> "broken.dhall"
+          TextIO.writeFile imported "let a = 1\nin { port = }"
+          problem <-
+            expectError
+              (localOptions root)
+              (dhallExpression "importing root" "./broken.dhall")
+          dhallErrorCategory problem @?= DhallParseError
+          dhallErrorPath problem @?= Just imported
+          dhallErrorLine problem @?= Just 2
+          dhallErrorColumn problem @?= Just 13
+          dhallErrorMessage problem @?= "invalid syntax in local import",
       testCase "resolved secret values are redacted while Dhall provenance remains" $ do
         input <-
           expectSource
             noImportOptions
             (dhallExpression "secret root" ("{ database = { password = \"" <> secretSentinel <> "\" } }"))
-        result <- expectResolution (resolve defaultResolveOptions [input] (required passwordSetting))
-        result ^. #value @?= secretSentinel
+        let result = resolve defaultResolveOptions [input] (required passwordSetting)
+        value <- expectResolution result
+        value @?= secretSentinel
         let textOutput = renderResolutionText (result ^. #report)
             jsonOutput = renderResolutionJson (result ^. #report)
         assertBool "secret reached text report" (not (secretSentinel `Text.isInfixOf` textOutput))
@@ -220,6 +248,75 @@
         assertBool "Dhall provenance was redacted with the value" ("secret root" `Text.isInfixOf` textOutput)
     ]
 
+errorRenderingTests :: TestTree
+errorRenderingTests =
+  testGroup
+    "error rendering"
+    [ testCase "IO" $ do
+        problem <- expectError noImportOptions (DhallFile "test/fixtures/does-not-exist.dhall")
+        dhallErrorCategory problem @?= DhallIoError
+        assertBool
+          "IO renderer omitted its stable located prefix"
+          ("application Dhall (" `Text.isPrefixOf` renderDhallErrorText problem)
+        assertBool
+          "IO renderer omitted its fixed message"
+          (": cannot read Dhall root file" `Text.isSuffixOf` renderDhallErrorText problem),
+      rendererCase
+        "parse"
+        DhallParseError
+        (dhallExpression "broken" "{ port =")
+        "application Dhall (1:9): invalid Dhall syntax",
+      rendererCase
+        "import policy"
+        DhallImportPolicyError
+        (dhallExpression "disabled import" "env:HOME as Text")
+        "application Dhall: imports are disabled",
+      testCase "import resolution" $
+        withSystemTempDirectory "settei-dhall-render-cycle" $ \root -> do
+          TextIO.writeFile (root FilePath.</> "a.dhall") "./b.dhall"
+          TextIO.writeFile (root FilePath.</> "b.dhall") "./a.dhall"
+          problem <- expectError (localOptions root) (dhallExpression "cycle" "./a.dhall")
+          dhallErrorCategory problem @?= DhallImportError
+          renderDhallErrorText problem @?= "application Dhall: Dhall import resolution failed",
+      rendererCase
+        "type"
+        DhallTypeError
+        (dhallExpression "type" "{ broken = if True then True else 1 }")
+        "application Dhall: Dhall expression does not type-check",
+      rendererCase
+        "conversion"
+        DhallConversionError
+        (dhallExpression "conversion" "{ bytes = 0x\"00\" }")
+        "application Dhall: Dhall value is not JSON-compatible",
+      rendererCase
+        "invalid key"
+        DhallInvalidKey
+        (dhallExpression "key" "{ `service.port` = 8080 }")
+        "application Dhall: Dhall record field is not a valid Settei key segment",
+      rendererCase
+        "top-level type"
+        DhallTopLevelType
+        (dhallExpression "top-level" "[ 1, 2 ]")
+        "application Dhall: top-level Dhall value must be a record",
+      testCase "a missing path and position omit the location parenthetical" $ do
+        problem <- expectError noImportOptions (dhallExpression "type" "1 + True")
+        dhallErrorCategory problem @?= DhallTypeError
+        assertBool
+          "position-free Dhall renderer invented a location"
+          (not ("application Dhall (" `Text.isPrefixOf` renderDhallErrorText problem)),
+      testCase "plural rendering is singular rendering plus a newline" $ do
+        problem <- expectError noImportOptions (dhallExpression "broken" "{ port =")
+        renderDhallErrorsText (NonEmpty.singleton problem)
+          @?= renderDhallErrorText problem <> "\n"
+    ]
+
+rendererCase :: String -> DhallErrorCategory -> DhallRoot -> Text -> TestTree
+rendererCase label expectedCategory root expected =
+  testCase label $ do
+    problem <- expectError noImportOptions root
+    dhallErrorCategory problem @?= expectedCategory
+    renderDhallErrorText problem @?= expected
+
 noImportOptions :: DhallSourceOptions
 noImportOptions = dhallSourceOptions "application Dhall" NoImports
 
@@ -248,8 +345,8 @@
   Right (Just found) -> pure found
   _ -> assertFailure "expected Dhall candidate" >> error "unreachable"
 
-expectResolution :: Either (NonEmpty ConfigError) a -> IO a
-expectResolution = \case
+expectResolution :: ResolveResult a -> IO a
+expectResolution result = case result ^. #answer of
   Left _ -> assertFailure "expected Dhall resolution to succeed" >> error "unreachable"
   Right value -> pure value
 
