fix(hashing): stable canonical names for orcapod types in function-pod signature hashing (ITL-638) - #262
Conversation
…hing (ITL-638) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…anonicalization (ITL-638) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ule (ITL-638) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…logical types (ITL-638) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…cal names (ITL-638)
… fix relocation test wiring (ITL-638)
…ignatureExtractor (ITL-638) - Add optional logical_type_registry constructor arg with lazy fallback to default context (same pattern as TypeObjectHandler) - Extend param annotation replacement to cover all annotation types (not just unions): uses canonical_annotation_str(annotation, registry) and replaces only when the canonical form differs from formatannotation - For return annotations: store canonical string only when the annotation contains a registered logical type; otherwise keep raw type object to avoid invalidating existing cached hashes for builtin return types - Add _annotation_contains_registered_type helper for the return-type guard, recursing through unions and generic aliases - All 28 TestFunctionSignatureExtractor and TestFunctionSignatureExtractorWithRegistry tests pass; golden-diff tests confirm builtins unchanged and orcapod types now use stable logical_type_name Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…python_type_handlers (ITL-638) Add optional logical_type_registry parameter to register_builtin_python_type_handlers and forward it to both TypeObjectHandler and FunctionSignatureExtractor at construction time. When None (the default), both handlers retain their existing lazy fallback to get_default_context() — no behaviour change for callers that don't pass the argument. Explicit injection is now available for test isolation and future custom-context wiring. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
| following the same pattern as ``ArrowTableHandler``. | ||
| """ | ||
|
|
||
| def __init__(self, logical_type_registry: Any = None) -> None: |
There was a problem hiding this comment.
do not use Any here -- specifically type it to the type registry or rather type converter as logical type registry is something that's hidden within the type converter
There was a problem hiding this comment.
Fixed. Changed the parameter in TypeObjectHandler.__init__ to type_converter: TypeConverterProtocol | None. We also added get_logical_type(python_type: type) -> LogicalTypeProtocol | None to TypeConverterProtocol (mirroring the method that already existed on UniversalTypeConverter) so the handler can look up the canonical name without ever touching the registry object directly.
There was a problem hiding this comment.
Fixed. The parameter was changed from logical_type_registry: Any to type_converter: TypeConverterProtocol | None — the registry is an implementation detail hidden inside TypeConverter, so the handler accepts the converter itself and calls type_converter.get_logical_type(obj) to resolve canonical names.
| def _get_registry(self) -> Any: | ||
| if self._logical_type_registry is not None: | ||
| return self._logical_type_registry | ||
| from orcapod.contexts import get_default_context |
There was a problem hiding this comment.
use proper typing and do not default to grabbing default context. Aside from directly user-facing code, get_default_context should never be invoked and rather, one should be passed an explicit context object
There was a problem hiding this comment.
Removed entirely. The lazy get_default_context() fallback has been deleted from both TypeObjectHandler and FunctionSignatureExtractor. Both now require explicit injection — TypeObjectHandler(type_converter=...) and FunctionSignatureExtractor(type_converter=...). The v0.1.json context spec now wires the type_converter ref directly into both handlers, so the default context wiring is explicit rather than implicit.
There was a problem hiding this comment.
Fixed. The lazy _get_registry() fallback that called get_default_context().logical_type_registry has been deleted entirely. TypeObjectHandler now requires explicit injection: TypeObjectHandler(type_converter=tc). The default context wires it in via v0.1.json, so user-facing code remains unchanged.
| return self._logical_type_registry | ||
| from orcapod.contexts import get_default_context | ||
| ctx = get_default_context() | ||
| return getattr(ctx.type_converter, "_logical_type_registry", None) |
There was a problem hiding this comment.
do not every use private property _logical_type_registry of type_converter. If there is any property/method that needs to access knowlege of logical type registry, this must be exposed via type_converter
There was a problem hiding this comment.
Fixed. get_logical_type(python_type: type) -> LogicalTypeProtocol | None was already implemented on UniversalTypeConverter and is now added to TypeConverterProtocol as a proper public method. TypeObjectHandler calls type_converter.get_logical_type(obj) directly; canonical_annotation_str and _annotation_contains_registered_type also use it via the converter rather than touching the registry. No code outside of UniversalTypeConverter itself accesses _logical_type_registry anymore.
There was a problem hiding this comment.
Fixed. get_logical_type(python_type: type) -> LogicalTypeProtocol | None was added to TypeConverterProtocol (the method already existed on UniversalTypeConverter). All logical-type lookups in the hashing layer go through this public method — _logical_type_registry is never accessed directly by callers.
…ical values (ITL-638) Adds TestSchemaHashStability to test_type_annotation_golden.py alongside a new hash_samples/schema_hash_golden.json fixture. Every schema hash — including schemas containing op.File, op.Directory, op.Path — must remain stable after future refactors.
…remove direct registry access (ITL-638) Address PR review feedback: - Add get_logical_type(python_type) -> LogicalTypeProtocol | None to TypeConverterProtocol, mirroring the already-existing UniversalTypeConverter method. - TypeObjectHandler and FunctionSignatureExtractor now accept type_converter: TypeConverterProtocol | None instead of Any; no lazy get_default_context() fallback; no private _logical_type_registry access. - canonical_annotation_str and _annotation_contains_registered_type take TypeConverterProtocol | None instead of LogicalTypeRegistry, resolving types via get_logical_type(). - v0.1.json wires type_converter ref into TypeObjectHandler and FunctionSignatureExtractor configs. - Tests updated to use type_converter fixture / get_logical_type() throughout.
Review round 2 — changes summaryAll three comments addressed in commit b2becea. No more No more No more private All 677 tests pass (0 failures, 31 skipped). |
| args = getattr(annotation, "__args__", ()) or () | ||
| return any(_annotation_contains_registered_type(a, type_converter) for a in args) | ||
| # Generic aliases (list[X], dict[K, V], etc.): check args | ||
| origin = getattr(annotation, "__origin__", None) |
There was a problem hiding this comment.
isn't this implicitly assuming that the origin part cannot be the type that is "registered type"
There was a problem hiding this comment.
You're right. The generic-alias branch was only iterating over __args__ and skipping __origin__ entirely — so if a registered type appeared as the origin of a generic alias, the function would silently return False. Fixed by also recursing into origin before checking args, consistent with how canonical_annotation_str already handles it. Added test_registered_type_as_generic_origin_detected to exercise this path directly.
There was a problem hiding this comment.
Update: after fixing the origin check, we re-examined whether _annotation_contains_registered_type was the right approach at all. It wasn't — the whole string-conversion branch for return annotations was unnecessary and created an inconsistency: registered return types were stored as plain strings (e.g. 'orcapod.file') losing the 'type:' prefix, while unregistered types remained as type objects and went through TypeObjectHandler (producing 'type:builtins.float').
The fix: _annotation_contains_registered_type has been deleted entirely. FunctionSignatureExtractor now always stores parts['returns'] = ret_ann (the raw annotation object), exactly as it did originally. TypeObjectHandler — already updated with type_converter — handles canonicalisation at hash time uniformly for all return types, registered or not. No special casing in the extractor.
…r generic aliases (ITL-638) The generic-alias branch previously only checked __args__, silently skipping __origin__. If a registered type appeared as the origin of a generic alias, _annotation_contains_registered_type would incorrectly return False and the return annotation would not be stored as a canonical string. Now mirrors canonical_annotation_str which already recurses into origin. Added test_registered_type_as_generic_origin_detected to exercise this path.
Review round 3 — changes summarySingle fix in commit 2088e70. Bug: Fix: Now also recurses into Test added: |
…ler canonicalises at hash time Remove `_annotation_contains_registered_type` and the branch that stored registered return types as plain strings. `parts["returns"]` now always holds the raw annotation object, regardless of whether it is an orcapod logical type. `TypeObjectHandler` (already updated with `type_converter`) canonicalises it to `"type:orcapod.file"` etc. at hash time, so the `"type:"` prefix is consistently present for all return annotations. This fixes an inconsistency introduced during the ITL-638 fix where registered types in return position were serialised as `"orcapod.file"` (no `"type:"` prefix) while unregistered types went through TypeObjectHandler and produced `"type:builtins.float"`. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary of latest changes (e4189ba)This commit removes the What changed and why: During review of the round-3 fix (checking The correct approach (now implemented):
What is unchanged:
|
…er a string Add test_return_annotation_is_never_a_string covering 8 annotation shapes (bare registered type, union with None, Optional, multi-type union, generic alias, and builtins) — each explicitly asserts parts["returns"] is not a str. This catches the specific regression where registered return types were stored as plain strings (e.g. "orcapod.file"), bypassing TypeObjectHandler and losing the "type:" prefix. Also strengthen the two existing single-case return tests with explicit not-isinstance-str guards, and import types/typing at the module level so they are available throughout the test file. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
There are confirmed correctness/maintainability issues in the diff (unused import likely to break lint, and multiple docstrings/spec sections referencing non-existent APIs/behavior) that should be fixed before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR fixes ITL-638 by making semantic hashing of function-pod type annotations stable across internal module relocations, using canonical logical type names (via the type converter’s logical-type registry) instead of fully-qualified import paths.
Changes:
- Extend
canonical_annotation_strto canonicalize registered logical types (and recurse through unions/generic aliases) usingTypeConverterProtocol.get_logical_type(...). - Update
TypeObjectHandlerandFunctionSignatureExtractorto use the type converter for canonical type identity during hashing (params canonicalized at extraction; returns remain raw and are canonicalized byTypeObjectHandlerat hash time). - Add golden/regression tests and committed golden fixtures to ensure builtins remain unchanged while registered orcapod logical types switch to canonical names.
File summaries
| File | Description |
|---|---|
| tests/test_hashing/test_type_annotation_golden.py | Adds golden-diff regression tests ensuring only orcapod logical types change hashing behavior. |
| tests/test_hashing/test_semantic_hasher.py | Adds coverage for registry-aware TypeObjectHandler, including simulated module relocation stability. |
| tests/test_hashing/test_hash_utils.py | Adds tests for canonical_annotation_str(..., type_converter) covering registry resolution + recursion through unions/generics. |
| tests/test_hashing/test_function_info_extractors.py | Adds coverage that params are canonicalized while returns remain raw objects (hashed via TypeObjectHandler). |
| tests/test_hashing/hash_samples/type_annotation_golden.json | Stores pre-fix golden hashes for annotations/functions to detect unintended changes. |
| tests/test_hashing/hash_samples/schema_hash_golden.json | Stores post-fix stable schema hashes to lock in canonical behavior going forward. |
| tests/test_hashing/generate_type_annotation_golden.py | Adds a script to generate the pre-fix golden fixture. |
| superpowers/specs/2026-09-02-itl-638-stable-type-annotation-hashing-design.md | Design doc for the change (currently contains return-annotation details that conflict with implementation). |
| superpowers/plans/2026-09-02-itl-638-stable-type-annotation-hashing.md | Implementation plan artifact for ITL-638. |
| src/orcapod/protocols/semantic_types_protocols.py | Extends TypeConverterProtocol with get_logical_type(...) for registry-backed canonicalization. |
| src/orcapod/hashing/semantic_hashing/function_info_extractors.py | Canonicalizes parameter annotation substrings using canonical_annotation_str(..., type_converter); keeps return annotations as raw objects. |
| src/orcapod/hashing/semantic_hashing/builtin_handlers.py | Updates TypeObjectHandler to emit type:<logical_type_name> when resolvable via the type converter; threads type_converter through registration/default context wiring. |
| src/orcapod/hashing/hash_utils.py | Extends canonical_annotation_str with type_converter support + recursion through unions/generic aliases. |
| src/orcapod/contexts/data/v0.1.json | Wires type_converter into the configured FunctionSignatureExtractor and TypeObjectHandler in the default context. |
Review details
Suppressed comments (2)
src/orcapod/hashing/semantic_hashing/builtin_handlers.py:475
register_builtin_python_type_handlersclaims canonical type-name resolution happens viaget_logical_type_registry(), but the code path usesTypeConverterProtocol.get_logical_type(...). Adjust the docstring to reflect the actual API so downstream implementers ofTypeConverterProtocolknow which method is required.
type_converter: Optional ``TypeConverterProtocol`` forwarded to
``TypeObjectHandler`` and ``FunctionSignatureExtractor`` for stable
canonical type-name resolution via ``get_logical_type_registry()``.
When ``None`` (the default), both handlers fall back to the raw
``"type:<module>.<qualname>"`` serialisation.
superpowers/specs/2026-09-02-itl-638-stable-type-annotation-hashing-design.md:165
- The concrete "After" example shows
"returns": "orcapod.directory", but the implementation in this PR keeps return annotations as raw objects (op.Directory) and leaves canonicalisation toTypeObjectHandlerat hash time. Update the example to avoid documenting behavior that doesn't exist.
# After
{
"module": "mymodule",
"name": "fn",
"params": "f: orcapod.file, n: int", # canonical name; int unchanged
"returns": "orcapod.directory", # canonical string, same path as params
}
- Files reviewed: 14/14 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| Args: | ||
| type_converter: Optional ``TypeConverterProtocol``. When provided, | ||
| ``type_converter.get_logical_type_registry()`` is called to resolve | ||
| registered logical types to their stable canonical name. When | ||
| ``None`` (the default), the fallback ``"type:<module>.<qualname>"`` | ||
| serialisation is always used. |
There was a problem hiding this comment.
Fixed in both locations. TypeObjectHandler docstring now says get_logical_type(obj) and register_builtin_python_type_handlers docstring says get_logical_type() — matching the actual call in handle().
| **Return annotation** — was stored as raw type object (`parts["returns"] = sig.return_annotation`), | ||
| then handled by `TypeObjectHandler` at hash time. Now stored as canonical string upfront, | ||
| consistent with how params are handled: | ||
|
|
||
| ```python | ||
| # Before | ||
| parts["returns"] = sig.return_annotation # raw type object | ||
|
|
||
| # After | ||
| parts["returns"] = canonical_annotation_str(sig.return_annotation, registry) # canonical string | ||
| ``` | ||
|
|
||
| This eliminates the asymmetry and means `TypeObjectHandler` no longer participates in the | ||
| return annotation path (params and returns are both plain strings by the time the hasher sees | ||
| them). |
There was a problem hiding this comment.
Fixed throughout the spec. The return-annotation section now describes the actual behavior: parts['returns'] stays as a raw type object, and TypeObjectHandler canonicalises it at hash time via type_converter.get_logical_type(). The 'After' concrete example, the scope description, the goals statement, and the Dependencies & Risks note about 'returns becoming a string' are all updated to match the implementation.
…o match implementation - Remove unused `is_union_annotation` import from function_info_extractors.py (the simplification that dropped _annotation_contains_registered_type left it behind; Ruff F401 would flag it) - Fix TypeObjectHandler docstring: says get_logical_type_registry() but the actual call is type_converter.get_logical_type(obj) - Fix register_builtin_python_type_handlers docstring: same stale method name - Update spec to match shipped behavior: return annotations remain raw type objects (not canonical strings); TypeObjectHandler canonicalises at hash time. Corrects the "After" code example, scope description, goals statement, and the Dependencies & Risks note that said "returns" becomes a string. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Review round summary (795cfe9)Three issues raised by Copilot, all fixed in a single commit:
No logic changes — docs and imports only. All 669 tests still pass. |
| # only). The ": " prefix avoids accidentally replacing the | ||
| # annotation string inside a default value. | ||
| param_str = param_str.replace(f": {old_ann}", f": {new_ann}", 1) | ||
| if not self.include_defaults and "=" in param_str: |
There was a problem hiding this comment.
Rather than replacing string, wouldn't it better for the annotation per parameter to be replaced cleanly and then reconstruct the full function string rather than doing substitution? Technically string substitution can accidentally replace part of the function string that it's not meant to replace
There was a problem hiding this comment.
Done. Replaced the str(param).replace() / split('=') approach with _format_param(), a module-level helper that reconstructs each parameter string directly from inspect.Parameter's structured attributes: name, kind, canonical annotation, and default.
This eliminates both failure modes you identified:
- Annotations containing '=' (e.g. Literal["a=b"]) were being corrupted by split('=')[0] when include_defaults=False — now we check param.default is not inspect.Parameter.empty directly.
- A default value whose repr contained ': ' as a substring could have caused unexpected matches — now annotation and default are assembled independently, never searched/replaced.
Output format is identical to str(inspect.Parameter) for all normal inputs, so no hash change for existing signatures. Two regression tests added to verify both failure modes are caught.
There was a problem hiding this comment.
Agreed — done. Replaced the and approach with a new helper that reconstructs each parameter string from structured inspect.Parameter attributes. No string search/replace anywhere in the param building path.
The helper uses param.kind, param.name, param.default, and the pre-computed canonical_annotation directly, so:
- The annotation and default value are assembled as separate parts — a default whose
reprcontains': <annotation>'cannot accidentally trigger substitution. - Stripping defaults uses
param.default is not inspect.Parameter.empty, notsplit('='), so annotations containing=(e.g.Literal["a=b"]) are preserved correctly.
Output is identical to str(inspect.Parameter) for all real-world inputs, so no existing hash values change.
…of string substitution
Replace the str(param).replace(':old_ann', ':new_ann') approach in
FunctionSignatureExtractor with _format_param(), a helper that builds each
parameter string directly from inspect.Parameter's structured attributes
(name, kind, canonical annotation, default).
This eliminates two failure modes of the substitution approach:
1. Annotations that contain '=' (e.g. Literal["a=b"]) were silently
truncated by the split("=")[0] used when include_defaults=False.
2. If a default value's repr contained ': <annotation>' as a substring,
the replace() could produce unexpected results.
Output format is identical to str(inspect.Parameter) for all normal inputs,
so there is no hash change for existing function signatures.
Two regression tests added to TestFunctionSignatureExtractor:
- test_annotation_containing_equals_preserved_when_defaults_stripped
- test_default_value_repr_containing_annotation_substring_not_duplicated
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Review round summary (e3c0c14)Comment: String substitution in param building can accidentally replace the wrong text. Fix: Replaced Two actual bugs fixed in the process:
Output format is identical to Two regression tests added to
|
…rgs and keyword-only tests The previous test_default_value_repr_containing_annotation_substring_not_duplicated claimed to catch a bug with old string-substitution but actually passed with both old and new code — the count=1 guard in replace() was sufficient for that case. Replace it with two tests that exercise real _format_param code paths: - test_varargs_and_kwargs_with_annotations: verifies VAR_POSITIONAL and VAR_KEYWORD parameters get the correct '*' / '**' prefix - test_keyword_only_param_formatted_without_star_prefix: verifies KEYWORD_ONLY params do not get a '*' prefix (the bare '*' separator is a signature-level token, not a parameter attribute) Also tighten the docstring on test_annotation_containing_equals_preserved_when_ defaults_stripped to explicitly state it fails with the old implementation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ality Replace loose `in`/`not in` substring checks with exact `==` assertions throughout TestFunctionSignatureExtractor and TestFunctionSignatureExtractorWithRegistry. The one exception is `test_annotation_containing_equals_preserved_when_defaults_stripped`, which must stay compound because `from __future__ import annotations` causes `eval_str=True` to fall back when `Literal` is not in the function's global scope, wrapping the annotation string in an extra layer of quotes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Review round summary (bca94e0)Tightened test assertions to exact equality per review feedback that checking string phrases with What changedAll
One deliberate exception
All 32 tests pass. |
pathlib.Path and uuid.UUID are registered orcapod logical types (op.Path and op.UUID respectively), so they should be canonicalized to 'orcapod.path' and 'orcapod.uuid' in param annotations when the registry is wired in. Add three tests to TestFunctionSignatureExtractorWithRegistry covering: - bare pathlib.Path param → 'orcapod.path' - bare uuid.UUID param → 'orcapod.uuid' - str | Path union param → 'orcapod.path | str' (sorting preserved post-canonicalization) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Review round summary (6ec4aad)Added missing test coverage for and canonicalization. is and is — both registered orcapod logical types. With the registry wired in, param annotations using these types should be canonicalized to their logical type names, not their module paths. This was working correctly (the implementation already handled it via ) but was never explicitly tested. Three new tests in :
Note: the existing |
…ition Add two groups of tests: 1. test_function_info_extractors.py — test_path_return_annotation_type_object_hashes_to_canonical: Two-step round-trip: extractor stores raw pathlib.Path type object in parts["returns"], then TypeObjectHandler.handle() produces "type:orcapod.path" (not the old "type:pathlib.Path" module-path form). 2. test_semantic_hasher.py — TestTypeObjectHandlerWithRegistry: Add test_registered_path_returns_canonical_name and test_registered_uuid_returns_canonical_name alongside the existing File/Directory cases, covering all four orcapod logical types registered over stdlib classes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Review round summary (c043922)Added pathlib.Path and uuid.UUID in the return position. Two groups of new tests:
Two-step round-trip covering the full chain for
Two new cases alongside the existing
|
Review round — thread reply catch-upThree eywalker comment threads from the first CHANGES_REQUESTED review (2026-09-02) had not received individual replies, only coverage in the batch summary. Replied to all three now:
All code changes were already in place from commit |
Summary
Fixes ITL-638: function pod signature hashing encoded orcapod logical types by their fully-qualified import path (
type:orcapod.logical_types.file_type.File), so any internal module reorganisation silently invalidated every cached result for functions annotated withop.File,op.Directory, etc.Root causes (two surfaces)
TypeObjectHandlerserialised type objects as"type:{module}.{qualname}"with no registry lookup.FunctionSignatureExtractorhad two asymmetric problems:str(param)→inspect.formatannotation, embedding the full import path.TypeObjectHandler— full path again.Fix (Approach B)
canonical_annotation_strinhash_utils.py— extended with an optionalregistryparameter. Registered types resolve to theirlogical_type_name(e.g."orcapod.file"); generic aliases and unions recurse; everything else falls back toinspect.formatannotation, preserving existing behaviour exactly.TypeObjectHandlerinbuiltin_handlers.py— accepts an optionallogical_type_registry; lazy-falls-back toget_default_context().type_converter._logical_type_registry(safegetattraccess). Registered types now emit"type:orcapod.file"instead of the full path.FunctionSignatureExtractorinfunction_info_extractors.py— accepts the same optionallogical_type_registry. Params: all annotation substrings are now replaced withcanonical_annotation_str(annotation, registry)(not just unions). Returns: stores a canonical string only when the return type contains a registered orcapod type; keeps the raw type object for builtins and user types to avoid invalidating existing hashes.register_builtin_python_type_handlers— newlogical_type_registryparameter forwarded to bothTypeObjectHandlerandFunctionSignatureExtractor. Callers that omit it get the same lazy-fallback behaviour as before.Regression tests
tests/test_hashing/test_type_annotation_golden.py— pre-fix golden values captured inhash_samples/type_annotation_golden.json; diff assertions verify builtins are unchanged and orcapod types changed to canonical names.tests/test_hashing/test_hash_utils.py— 12 new tests forcanonical_annotation_strwith registry.tests/test_hashing/test_semantic_hasher.py— 6 new tests forTypeObjectHandlerwith registry including a relocation-stability test.tests/test_hashing/test_function_info_extractors.py— 8 new tests inTestFunctionSignatureExtractorWithRegistrycovering params, returns, generics, unions, relocation stability.Backward compatibility
One-time cache invalidation for any cached function pod signature that contains
op.File,op.Directory,op.Path, orop.UUID. Pre-v0.1.0 — no deprecation wrappers per CLAUDE.md.Test plan
uv run pytest tests/test_hashing/— 666 passed, 31 skippeduv run pytest --ignore=tests/test_hashing/— 4141 passed, 62 skipped, 2 xfailedCloses ITL-638