From: 
Subject: Debian changes

The Debian packaging of python-probatio is maintained in git, using a workflow
similar to the one described in dgit-maint-merge(7).
The Debian delta is represented by this one combined patch; there isn't a
patch queue that can be represented as a quilt series.

A detailed breakdown of the changes is available from their canonical
representation -- git commits in the packaging repository.
For example, to see the changes made by the Debian maintainer in the first
upload of upstream version 1.2.3, you could use:

    % git clone https://git.dgit.debian.org/python-probatio
    % cd python-probatio
    % git log --oneline 1.2.3..debian/1.2.3-1 -- . ':!debian'

(If you have dgit, use `dgit clone python-probatio`, rather than plain `git clone`.)

We don't use debian/source/options single-debian-patch because it has bugs.
Therefore, NMUs etc. may nevertheless have made additional patches.

---

diff --git a/src/probatio/_engine.py b/src/probatio/_engine.py
index daa698a..13af3e6 100644
--- a/src/probatio/_engine.py
+++ b/src/probatio/_engine.py
@@ -272,7 +272,7 @@ class _MappingValidator:
             # turn (a ``str: str`` may take it) and ``_match_validator`` applies
             # the extra-key policy if none match.
             index, candidate = match
-            if not self._apply(key, value, index, candidate, out, errors, seen):
+            if not self._apply(key, key, value, index, candidate, out, errors, seen):
                 self._match_validator(key, value, out, errors, seen)
 
         if seen is not None:
@@ -346,7 +346,9 @@ class _MappingValidator:
             if key_type is not None:
                 # Inlined type key (``{str: int}``): isinstance, no key-check call.
                 if isinstance(key, key_type):
-                    if self._apply(key, value, index, candidate, out, errors, seen):
+                    if self._apply(
+                        key, key, value, index, candidate, out, errors, seen
+                    ):
                         return
                     # A Remove key matched but its value did not validate: keep
                     # trying the remaining candidates (a ``str: str`` may take it).
@@ -360,7 +362,7 @@ class _MappingValidator:
                 if key_error is None or len(exc.path) > len(key_error.path):
                     key_error = exc
                 continue
-            if self._apply(new_key, value, index, candidate, out, errors, seen):
+            if self._apply(key, new_key, value, index, candidate, out, errors, seen):
                 return
 
         self._unmatched(key, value, key_error, out, errors)
@@ -368,6 +370,7 @@ class _MappingValidator:
     def _apply(  # noqa: PLR0913, PLR0917
         self,
         key: Any,
+        output_key: Any,
         value: Any,
         index: int,
         candidate: _Candidate,
@@ -377,6 +380,8 @@ class _MappingValidator:
     ) -> bool:
         """Validate a matched pair; return whether the candidate handled the key.
 
+        Store under the transformed key, but report errors under the input key.
+
         Returns ``True`` once the pair is stored, removed, or reported. Returns
         ``False`` only when a ``Remove`` key matched but its value did not
         validate, so the key is not consumed and the caller keeps looking (another
@@ -405,13 +410,16 @@ class _MappingValidator:
         if value_type is not None:
             # Inlined type value (the ``int`` of ``{str: int}``): no value call.
             if isinstance(value, value_type):
-                out[key] = value
+                out[output_key] = value
             else:
                 errors.append(
                     _type_error(value_type.__name__, [key], self._invalid_msg)
                 )
             return True
-        self._store(key, value, candidate.check_value, out, errors)
+        try:
+            out[output_key] = candidate.check_value(value)
+        except Invalid as exc:
+            self._collect(key, exc, errors)
         return True
 
     @staticmethod
diff --git a/tests/strategies.py b/tests/strategies.py
index 39d32fa..3732e92 100644
--- a/tests/strategies.py
+++ b/tests/strategies.py
@@ -97,9 +97,12 @@ def _names() -> st.SearchStrategy[str]:
     return st.sampled_from(_KEY_ALPHABET) | st.text(min_size=1, max_size=3)
 
 
-def specs(*, no_narrowing: bool = False) -> st.SearchStrategy[Any]:
+def specs(*, no_narrowing: bool = False, floats: bool = True) -> st.SearchStrategy[Any]:
     """A recursive strategy yielding library-agnostic schema specs.
 
+    With ``floats=False`` bare float schemas are excluded for comparison with
+    voluptuous, which does not honor the numeric tower.
+
     With ``no_narrowing=True`` the strategy drops the constructs whose JSON Schema
     rendering is a documented lossy narrowing (``Coerce`` cannot express its input
     coercion; ``Clamp`` accepts out-of-range values it then clamps) and drops
@@ -107,8 +110,13 @@ def specs(*, no_narrowing: bool = False) -> st.SearchStrategy[Any]:
     which the emitted schema must never reject an input probatio accepts, so the
     differential oracle can assert exactly that.
     """
+    # Bare float intentionally differs from voluptuous (ADR-017): it accepts
+    # integers and rejects booleans. Differential engine tests exclude it, while
+    # robustness and codec tests retain the numeric-tower behavior.
     literals = _json_literals if no_narrowing else _literals
     types = [int, str, float] if no_narrowing else [int, str, bool, float]
+    if not floats:
+        types.remove(float)
     leaf_options = [
         st.sampled_from(types).map(lambda t: ("type", t)),
         literals.map(lambda v: ("literal", v)),
diff --git a/tests/test_conformance.py b/tests/test_conformance.py
index 81ca802..5a35836 100644
--- a/tests/test_conformance.py
+++ b/tests/test_conformance.py
@@ -179,6 +179,7 @@ CASES: list[tuple[Any, Any]] = [
     (two_type_keys, {"x": "v"}),
     (callable_key, {"5": "v"}),
     (callable_key, {"x": "v"}),
+    (callable_key, {"1": None}),
     (two_callable_keys, {"x": "v"}),
     (remove_extra_mapping, {"a": 1, "x": 2}),
     (literal_value, "on"),
@@ -261,3 +262,21 @@ _DETAIL_CASES = [case for case in CASES if case[0] not in _MESSAGE_DEVIATIONS]
 def test_matches_voluptuous_error_detail(builder: Any, data: Any) -> None:
     """probatio agrees with voluptuous on the error class and rendered message."""
     assert _detail(builder, data, probatio) == _detail(builder, data, voluptuous)
+
+
+@pytest.mark.parametrize(
+    ("value_schema", "value", "path"),
+    [
+        (int, None, ["1"]),
+        (probatio.Coerce(int), None, ["1"]),
+        ({"child": int}, {"child": None}, ["1", "child"]),
+    ],
+)
+def test_coerced_key_value_error_path(
+    value_schema: Any, value: Any, path: list[Any]
+) -> None:
+    """Value errors retain the input key after key coercion, including nested paths."""
+    schema = probatio.Schema({probatio.Coerce(int): value_schema})
+    with pytest.raises(probatio.MultipleInvalid) as exc:
+        schema({"1": value})
+    assert [error.path for error in exc.value.errors] == [path]
diff --git a/tests/test_fuzz_engine.py b/tests/test_fuzz_engine.py
index e015fee..e77b414 100644
--- a/tests/test_fuzz_engine.py
+++ b/tests/test_fuzz_engine.py
@@ -41,7 +41,7 @@ def _result(spec: Any, lib: Any, value: Any) -> Any:
         return ("err", {tuple(exc.path)})
 
 
-@given(spec=strategies.specs(), value=strategies.data())
+@given(spec=strategies.specs(floats=False), value=strategies.data())
 @settings(
     max_examples=600, derandomize=True, suppress_health_check=[HealthCheck.too_slow]
 )
