# Type Registry Threading Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Thread the `type_registry` from `Cendry`/`AsyncCendry` through all serialization, deserialization, and query paths so custom registries actually work.

**Architecture:** Add a `registry: TypeRegistry` parameter to every serialization function (required for internal, optional for public). Thread it through `Query`/`AsyncQuery` constructors and all internal construction sites. Context classes pass `self.type_registry` everywhere.

**Tech Stack:** Python 3.13+, google-cloud-firestore, pytest, anyio

**Spec:** `docs/superpowers/specs/2026-03-21-type-registry-threading-design.md`

---

## File Map

| File | Action | Responsibility |
|------|--------|---------------|
| `src/cendry/serialize.py` | Modify | Add `registry` param to all serialization functions |
| `src/cendry/query.py` | Modify | Add `registry` to Query/AsyncQuery, propagate through all construction sites |
| `src/cendry/context.py` | Modify | Pass `self.type_registry` to all serialize/deserialize/query calls |
| `tests/test_serialize.py` | Modify | Add tests for `from_dict`/`to_dict`/`deserialize` with custom registry |
| `tests/test_context.py` | Modify | Add tests for registry threading through context and queries |

---

### Task 1: Thread registry through `serialize.py` internal functions

**Files:**
- Modify: `src/cendry/serialize.py`
- Modify: `tests/test_serialize.py`

- [ ] **Step 1: Write the failing test**

Add to `tests/test_serialize.py`:

```python
from cendry.serialize import to_dict, from_dict, deserialize
from cendry.types import TypeRegistry, BaseTypeHandler


class Celsius:
    def __init__(self, value: float) -> None:
        self.value = value


class CelsiusHandler(BaseTypeHandler):
    def serialize(self, value: Celsius) -> float:
        return value.value

    def deserialize(self, value: float) -> Celsius:
        return Celsius(value)


class Weather(Model, collection="weather"):
    city: Field[str]
    temp: Field[Celsius]


def test_to_dict_with_custom_registry():
    custom = TypeRegistry()
    custom.register(Celsius, handler=CelsiusHandler())
    weather = Weather(city="SF", temp=Celsius(20.5))

    result = to_dict(weather, registry=custom)
    assert result["temp"] == 20.5


def test_from_dict_with_custom_registry():
    custom = TypeRegistry()
    custom.register(Celsius, handler=CelsiusHandler())

    weather = from_dict(Weather, {"city": "SF", "temp": 20.5}, registry=custom)
    assert isinstance(weather.temp, Celsius)
    assert weather.temp.value == 20.5


def test_deserialize_with_custom_registry():
    custom = TypeRegistry()
    custom.register(Celsius, handler=CelsiusHandler())

    weather = deserialize(Weather, "w1", {"city": "SF", "temp": 20.5}, registry=custom)
    assert isinstance(weather.temp, Celsius)
    assert weather.temp.value == 20.5


def test_to_dict_default_registry_unchanged():
    """Existing behavior: no registry param uses default_registry."""
    city = from_dict(City, CITY_DATA)
    result = to_dict(city)
    assert result["name"] == "SF"
```

- [ ] **Step 2: Run tests to verify they fail**

Run: `uv run pytest tests/test_serialize.py::test_to_dict_with_custom_registry tests/test_serialize.py::test_from_dict_with_custom_registry tests/test_serialize.py::test_deserialize_with_custom_registry -v`
Expected: FAIL with `TypeError: unexpected keyword argument 'registry'`

- [ ] **Step 3: Add `registry` parameter to all functions in `serialize.py`**

Update `_deserialize_value`:
```python
def _deserialize_value(value: Any, hint: Any, *, enum_by: str = "value", registry: TypeRegistry) -> Any:
    """Deserialize a single value, checking handlers, enums, then Map nesting."""
    import enum

    if value is None:
        return None

    # Direct type match (non-container)
    inner_type = _resolve_inner_type(hint)
    if inner_type is not None:
        handler = registry.get_handler(inner_type)
        if handler is not None:
            return handler.deserialize(value)
        # Enum conversion
        if isinstance(inner_type, type) and issubclass(inner_type, enum.Enum):
            if enum_by == "name":
                return inner_type[value]
            return inner_type(value)

    # Container types: recurse into elements
    origin = get_origin(hint)
    if origin in (list, set, tuple) and isinstance(value, (list, set, tuple)):
        args = get_args(hint)
        if args:
            if origin is tuple:  # pragma: no cover
                return tuple(
                    _deserialize_value(v, a, registry=registry)
                    for v, a in zip(value, args, strict=False)
                )
            elem_hint = args[0]
            converted = [_deserialize_value(v, elem_hint, registry=registry) for v in value]
            return set(converted) if origin is set else converted
    if origin is dict and isinstance(value, dict):
        args = get_args(hint)
        if args and len(args) > 1:
            val_hint = args[1]
            return {
                k: _deserialize_value(v, val_hint, registry=registry) for k, v in value.items()
            }

    # Map nesting
    if isinstance(value, dict):
        inner = resolve_map_type(hint)
        if inner is not None:
            return deserialize_map(inner, value, registry=registry)
    return value
```

Update `deserialize_map`:
```python
def deserialize_map(map_class: type, data: dict[str, Any], *, registry: TypeRegistry) -> Any:
    """Recursively deserialize a Map from a dict. Always reads by alias."""
    hints = _cached_type_hints(map_class)
    converted: dict[str, Any] = {}
    for f in dataclasses.fields(map_class):
        alias = _get_alias(f)
        value = _deserialize_value(
            data.get(alias), hints.get(f.name), enum_by=_get_enum_by(f), registry=registry
        )
        converted[f.name] = value
    return map_class(**converted)
```

Update `deserialize`:
```python
def deserialize[T: Model](
    model_class: type[T],
    doc_id: str | None,
    data: dict[str, Any],
    *,
    registry: TypeRegistry | None = None,
) -> T:
    """Convert a Firestore dict to a model instance. Always reads by alias."""
    from .types import default_registry

    registry = registry or default_registry
    hints = _cached_type_hints(model_class)
    converted: dict[str, Any] = {}
    for f in dataclasses.fields(model_class):
        if f.name == "id":
            continue
        alias = _get_alias(f)
        value = _deserialize_value(
            data.get(alias), hints.get(f.name), enum_by=_get_enum_by(f), registry=registry
        )
        converted[f.name] = value
    return model_class(id=doc_id, **converted)
```

Update `from_dict`:
```python
def from_dict[T: Model](
    model_class: type[T],
    data: dict[str, Any],
    *,
    doc_id: str | None = None,
    by_alias: bool = False,
    registry: TypeRegistry | None = None,
) -> T:
    """Construct a model instance from a dict.

    Args:
        model_class: The Model class to construct.
        data: Dict of field values.
        doc_id: Optional document ID.
        by_alias: If True, read keys by Firestore alias. If False (default), use Python names.
        registry: Optional TypeRegistry. Uses default if not provided.

    Returns:
        The constructed model instance.

    Raises:
        TypeError: If required fields are missing.
    """
    missing = []
    remapped: dict[str, Any] = {}
    for f in dataclasses.fields(model_class):
        if f.name == "id":
            continue
        alias = _get_alias(f)
        key = alias if by_alias else f.name
        has_default = (
            f.default is not dataclasses.MISSING or f.default_factory is not dataclasses.MISSING
        )
        if key not in data and not has_default:
            missing.append(f.name)
        elif not by_alias and f.name in data:
            remapped[alias] = data[f.name]
    if missing:
        fields = ", ".join(missing)
        raise TypeError(f"from_dict({model_class.__name__}): missing required fields: {fields}")
    if by_alias:
        return deserialize(model_class, doc_id, data, registry=registry)
    return deserialize(model_class, doc_id, remapped, registry=registry)
```

Update `_serialize_value`:
```python
def _serialize_value(
    value: Any, hint: Any, *, by_alias: bool, enum_by: str = "value", registry: TypeRegistry
) -> Any:
    """Serialize a single value, checking handlers, enums, then Map nesting."""
    import enum as enum_mod

    if value is None:
        return None

    # Direct type match
    inner_type = _resolve_inner_type(hint)
    if inner_type is not None:
        handler = registry.get_handler(inner_type)
        if handler is not None:
            return handler.serialize(value)
        # Enum conversion
        if isinstance(value, enum_mod.Enum):
            return value.name if enum_by == "name" else value.value

    # Container types: recurse into elements
    origin = get_origin(hint)
    if origin in (list, set, tuple) and isinstance(value, (list, set, tuple)):
        args = get_args(hint)
        if args:
            if origin is tuple:  # pragma: no cover
                return [
                    _serialize_value(v, a, by_alias=by_alias, registry=registry)
                    for v, a in zip(value, args, strict=False)
                ]
            elem_hint = args[0]
            return [
                _serialize_value(v, elem_hint, by_alias=by_alias, registry=registry) for v in value
            ]
    if origin is dict and isinstance(value, dict):
        args = get_args(hint)
        if args and len(args) > 1:
            val_hint = args[1]
            return {
                k: _serialize_value(v, val_hint, by_alias=by_alias, registry=registry)
                for k, v in value.items()
            }

    # Map nesting
    if isinstance(value, Map):
        return _map_to_dict(value, by_alias=by_alias, registry=registry)
    return value
```

Update `_map_to_dict`:
```python
def _map_to_dict(instance: Map, *, by_alias: bool, registry: TypeRegistry) -> dict[str, Any]:
    """Convert a Map instance to a dict recursively."""
    hints = _cached_type_hints(type(instance))
    result: dict[str, Any] = {}
    for f in dataclasses.fields(instance):
        value = getattr(instance, f.name)
        key = _get_alias(f) if by_alias else f.name
        value = _serialize_value(
            value,
            hints.get(f.name),
            by_alias=by_alias,
            enum_by=_get_enum_by(f),
            registry=registry,
        )
        result[key] = value
    return result
```

Update `to_dict`:
```python
def to_dict(
    instance: Model | Map,
    *,
    by_alias: bool = False,
    include_id: bool = False,
    registry: TypeRegistry | None = None,
) -> dict[str, Any]:
    """Convert a model/map instance to a dict.

    Args:
        instance: Model or Map instance to convert.
        by_alias: If True, use Firestore alias keys. If False (default), use Python field names.
        include_id: If True, include the document ID in the output.
        registry: Optional TypeRegistry. Uses default if not provided.

    Returns:
        Dict representation of the instance.
    """
    from .types import default_registry

    registry = registry or default_registry
    result = _map_to_dict(instance, by_alias=by_alias, registry=registry)
    if not include_id:
        result.pop("id", None)
    return result
```

Add import at the top of `serialize.py` (within `TYPE_CHECKING` to avoid circular):
```python
from typing import TYPE_CHECKING, Any, get_args, get_origin, get_type_hints

if TYPE_CHECKING:
    from .types import TypeRegistry
```

Wait — `TypeRegistry` is used at runtime in the function signatures, not just for type checking. Since `types.py` doesn't import from `serialize.py`, there's no circular dependency. Add a direct import:

```python
from .types import TypeRegistry, default_registry
```

And remove the inline `from .types import default_registry` imports inside `_deserialize_value` and `_serialize_value`. The top-level import of `default_registry` is only used in the public function default resolution.

- [ ] **Step 4: Run tests to verify they pass**

Run: `uv run pytest tests/test_serialize.py -v`
Expected: All pass (new and existing)

- [ ] **Step 5: Run full test suite**

Run: `uv run pytest -q`
Expected: All 349 tests pass (existing tests work unchanged since defaults are preserved)

- [ ] **Step 6: Commit**

```bash
git add src/cendry/serialize.py tests/test_serialize.py
git commit -m "feat: thread type_registry through serialize.py functions"
```

---

### Task 2: Thread registry through `Query` / `AsyncQuery`

**Files:**
- Modify: `src/cendry/query.py`
- Modify: `tests/test_query.py`

- [ ] **Step 1: Write the failing test**

Add to `tests/test_query.py`. First add the needed imports at the top of the file:

```python
from unittest.mock import MagicMock

import pytest

from cendry.types import TypeRegistry
from tests.conftest import SF_DATA, City, make_mock_document
```

Also add `AsyncQuery` to the existing cendry imports at the top of the file (alongside `Query`, `Asc`, `Desc`).

Then add the test functions:

```python
def test_query_passes_registry_to_deserialize():
    """Registry from Query.__init__ is passed to deserialize calls."""
    custom = TypeRegistry()
    docs = [make_mock_document("SF", SF_DATA)]
    mock_query = MagicMock()
    mock_query.stream.return_value = iter(docs)

    q = Query(mock_query, City, lambda q, f: q, registry=custom)
    results = list(q)

    assert len(results) == 1
    assert results[0].name == "San Francisco"


def test_query_filter_preserves_registry():
    custom = TypeRegistry()
    mock_query = MagicMock()
    q = Query(mock_query, City, lambda q, f: q, registry=custom)
    filtered = q.filter()
    assert filtered._registry is custom


def test_query_order_by_preserves_registry():
    custom = TypeRegistry()
    mock_query = MagicMock()
    q = Query(mock_query, City, lambda q, f: q, registry=custom)
    ordered = q.order_by(Asc("name"))
    assert ordered._registry is custom


def test_query_limit_preserves_registry():
    custom = TypeRegistry()
    mock_query = MagicMock()
    q = Query(mock_query, City, lambda q, f: q, registry=custom)
    limited = q.limit(10)
    assert limited._registry is custom


def test_query_first_preserves_registry():
    custom = TypeRegistry()
    docs = [make_mock_document("SF", SF_DATA)]
    mock_query = MagicMock()
    mock_query.limit.return_value = mock_query
    mock_query.stream.return_value = iter(docs)

    q = Query(mock_query, City, lambda q, f: q, registry=custom)
    result = q.first()
    assert result is not None
    assert result.name == "San Francisco"


def test_query_one_preserves_registry():
    custom = TypeRegistry()
    docs = [make_mock_document("SF", SF_DATA)]
    mock_query = MagicMock()
    mock_query.limit.return_value = mock_query
    mock_query.stream.return_value = iter(docs)

    q = Query(mock_query, City, lambda q, f: q, registry=custom)
    result = q.one()
    assert result.name == "San Francisco"


def test_query_chain_preserves_registry():
    """Registry survives filter().limit() chain."""
    custom = TypeRegistry()
    mock_query = MagicMock()
    q = Query(mock_query, City, lambda q, f: q, registry=custom)
    chained = q.filter().limit(10)
    assert chained._registry is custom


def test_async_query_filter_preserves_registry():
    custom = TypeRegistry()
    mock_query = MagicMock()
    q = AsyncQuery(mock_query, City, lambda q, f: q, registry=custom)
    filtered = q.filter()
    assert filtered._registry is custom


def test_async_query_limit_preserves_registry():
    custom = TypeRegistry()
    mock_query = MagicMock()
    q = AsyncQuery(mock_query, City, lambda q, f: q, registry=custom)
    limited = q.limit(10)
    assert limited._registry is custom


@pytest.mark.anyio
async def test_async_query_first_preserves_registry():
    custom = TypeRegistry()
    docs = [make_mock_document("SF", SF_DATA)]
    mock_query = MagicMock()
    mock_query.limit.return_value = mock_query

    async def mock_stream():
        for d in docs:
            yield d

    mock_query.stream = mock_stream

    q = AsyncQuery(mock_query, City, lambda q, f: q, registry=custom)
    result = await q.first()
    assert result is not None
    assert result.name == "San Francisco"


@pytest.mark.anyio
async def test_async_query_iteration_uses_registry():
    custom = TypeRegistry()
    docs = [make_mock_document("SF", SF_DATA)]
    mock_query = MagicMock()

    async def mock_stream():
        for d in docs:
            yield d

    mock_query.stream = mock_stream

    q = AsyncQuery(mock_query, City, lambda q, f: q, registry=custom)
    results = [item async for item in q]
    assert len(results) == 1
    assert results[0].name == "San Francisco"
```

- [ ] **Step 2: Run tests to verify they fail**

Run: `uv run pytest tests/test_query.py::test_query_passes_registry_to_deserialize -v`
Expected: FAIL with `TypeError: unexpected keyword argument 'registry'`

- [ ] **Step 3: Add `registry` to `Query` and `AsyncQuery`**

In `src/cendry/query.py`, add import:
```python
from .types import TypeRegistry, default_registry
```

Update `Query.__init__`:
```python
def __init__(
    self,
    firestore_query: Any,
    model_class: type[T],
    apply_filter: Callable[[Any, Any], Any],
    *,
    registry: TypeRegistry | None = None,
    _filters: list[Any] | None = None,
    _order_by: list[Any] | None = None,
    _limit: int | None = None,
) -> None:
    self._query = firestore_query
    self._model_class = model_class
    self._apply_filter = apply_filter
    self._registry = registry or default_registry
    self._filters_repr = _filters or []
    self._order_by_repr = _order_by or []
    self._limit_repr = _limit
```

Update `Query.__iter__`:
```python
def __iter__(self) -> Iterator[T]:
    for doc in self._query.stream():
        yield deserialize(self._model_class, doc.id, doc.to_dict(), registry=self._registry)
```

Update `Query.filter` — add `registry=self._registry` to the `Query(...)` construction:
```python
return Query(
    query,
    self._model_class,
    self._apply_filter,
    registry=self._registry,
    _filters=new_filters,
    _order_by=self._order_by_repr,
    _limit=self._limit_repr,
)
```

Update `Query.order_by` — same pattern:
```python
return Query(
    query,
    self._model_class,
    self._apply_filter,
    registry=self._registry,
    _filters=self._filters_repr,
    _order_by=new_orders,
    _limit=self._limit_repr,
)
```

Update `Query.limit`:
```python
return Query(
    self._query.limit(n),
    self._model_class,
    self._apply_filter,
    registry=self._registry,
    _filters=self._filters_repr,
    _order_by=self._order_by_repr,
    _limit=n,
)
```

Update `Query.first`:
```python
def first(self) -> T | None:
    for item in Query(
        self._query.limit(1), self._model_class, self._apply_filter, registry=self._registry
    ):
        return item
    return None
```

Update `Query.one`:
```python
def one(self) -> T:
    items = Query(
        self._query.limit(2), self._model_class, self._apply_filter, registry=self._registry
    ).to_list()
    if not items:
        raise DocumentNotFoundError(self._model_class.__collection__, "<query>")
    if len(items) > 1:
        raise CendryError(
            f"Expected exactly one {self._model_class.__name__}, got more than one"
        )
    return items[0]
```

Update `Query.paginate`:
```python
def paginate(self, page_size: int) -> Iterator[list[T]]:
    query = self._query
    while True:
        items: list[T] = []
        last_doc = None
        for doc in query.limit(page_size).stream():
            last_doc = doc
            items.append(
                deserialize(self._model_class, doc.id, doc.to_dict(), registry=self._registry)
            )
        if not items:
            break
        yield items
        if len(items) < page_size:
            break
        query = query.start_after(last_doc)
```

Apply the **exact same changes** to `AsyncQuery`:
- `__init__`: add `registry` kwarg, store as `self._registry`
- `__aiter__`: pass `registry=self._registry` to `deserialize`
- `filter`: pass `registry=self._registry` to `AsyncQuery(...)`
- `order_by`: pass `registry=self._registry` to `AsyncQuery(...)`
- `limit`: pass `registry=self._registry` to `AsyncQuery(...)`
- `first`: pass `registry=self._registry` to `AsyncQuery(...)`
- `one`: pass `registry=self._registry` to `AsyncQuery(...)`
- `paginate`: pass `registry=self._registry` to `deserialize`

- [ ] **Step 4: Run tests to verify they pass**

Run: `uv run pytest tests/test_query.py -v`
Expected: All pass

- [ ] **Step 5: Run full test suite**

Run: `uv run pytest -q`
Expected: All tests pass

- [ ] **Step 6: Commit**

```bash
git add src/cendry/query.py tests/test_query.py
git commit -m "feat: thread type_registry through Query/AsyncQuery"
```

---

### Task 3: Thread registry through `context.py`

**Files:**
- Modify: `src/cendry/context.py`
- Modify: `tests/test_context.py`

- [ ] **Step 1: Write the failing tests**

Add to `tests/test_context.py`:

```python
from cendry.types import TypeRegistry, BaseTypeHandler


class Celsius:
    def __init__(self, value: float) -> None:
        self.value = value


class CelsiusHandler(BaseTypeHandler):
    def serialize(self, value: Celsius) -> float:
        return value.value

    def deserialize(self, value: float) -> Celsius:
        return Celsius(value)


class Weather(Model, collection="weather"):
    city: Field[str]
    temp: Field[Celsius]


def test_get_uses_context_registry(mock_firestore_client: MagicMock):
    custom = TypeRegistry()
    custom.register(Celsius, handler=CelsiusHandler())
    doc = make_mock_document("w1", {"city": "SF", "temp": 20.5})
    mock_firestore_client.collection.return_value.document.return_value.get.return_value = doc

    ctx = Cendry(client=mock_firestore_client, type_registry=custom)
    weather = ctx.get(Weather, "w1")

    assert isinstance(weather.temp, Celsius)
    assert weather.temp.value == 20.5


def test_save_uses_context_registry(mock_firestore_client: MagicMock):
    custom = TypeRegistry()
    custom.register(Celsius, handler=CelsiusHandler())
    weather = Weather(city="SF", temp=Celsius(20.5), id="w1")

    ctx = Cendry(client=mock_firestore_client, type_registry=custom)
    ctx.save(weather)

    call_args = mock_firestore_client.collection.return_value.document.return_value.set.call_args
    data = call_args[0][0]
    assert data["temp"] == 20.5  # serialized by handler


def test_select_iteration_uses_context_registry(mock_firestore_client: MagicMock):
    custom = TypeRegistry()
    custom.register(Celsius, handler=CelsiusHandler())
    docs = [make_mock_document("w1", {"city": "SF", "temp": 20.5})]
    mock_firestore_client.collection.return_value.stream.return_value = iter(docs)

    ctx = Cendry(client=mock_firestore_client, type_registry=custom)
    results = list(ctx.select(Weather))

    assert len(results) == 1
    assert isinstance(results[0].temp, Celsius)


@pytest.mark.anyio
async def test_async_get_uses_context_registry(mock_firestore_client: MagicMock):
    custom = TypeRegistry()
    custom.register(Celsius, handler=CelsiusHandler())
    doc = make_mock_document("w1", {"city": "SF", "temp": 20.5})
    mock_firestore_client.collection.return_value.document.return_value.get = AsyncMock(
        return_value=doc,
    )

    ctx = AsyncCendry(client=mock_firestore_client, type_registry=custom)
    weather = await ctx.get(Weather, "w1")

    assert isinstance(weather.temp, Celsius)
    assert weather.temp.value == 20.5


@pytest.mark.anyio
async def test_async_save_uses_context_registry(mock_firestore_client: MagicMock):
    custom = TypeRegistry()
    custom.register(Celsius, handler=CelsiusHandler())
    weather = Weather(city="SF", temp=Celsius(20.5), id="w1")
    mock_firestore_client.collection.return_value.document.return_value.set = AsyncMock()

    ctx = AsyncCendry(client=mock_firestore_client, type_registry=custom)
    await ctx.save(weather)

    call_args = mock_firestore_client.collection.return_value.document.return_value.set.call_args
    data = call_args[0][0]
    assert data["temp"] == 20.5


@pytest.mark.anyio
async def test_async_select_iteration_uses_context_registry(mock_firestore_client: MagicMock):
    custom = TypeRegistry()
    custom.register(Celsius, handler=CelsiusHandler())
    docs = [make_mock_document("w1", {"city": "SF", "temp": 20.5})]

    async def mock_stream():
        for d in docs:
            yield d

    mock_firestore_client.collection.return_value.stream = mock_stream

    ctx = AsyncCendry(client=mock_firestore_client, type_registry=custom)
    results = [item async for item in ctx.select(Weather)]

    assert len(results) == 1
    assert isinstance(results[0].temp, Celsius)
```

- [ ] **Step 2: Run tests to verify they fail**

Run: `uv run pytest tests/test_context.py::test_get_uses_context_registry -v`
Expected: FAIL — `Celsius` type not in `default_registry`, so deserialization returns raw `float`

- [ ] **Step 3: Update `context.py` to pass `self.type_registry`**

In `Cendry`:
- `get`: `return deserialize(model_class, doc.id, doc.to_dict(), registry=self.type_registry)`
- `find`: `return deserialize(model_class, doc.id, doc.to_dict(), registry=self.type_registry)`
- `get_many`: `results.append(deserialize(model_class, doc.id, doc.to_dict(), registry=self.type_registry))`
- `select`: `return Query(q, model_class, self._apply_filter, registry=self.type_registry)`
- `select_group`: `return Query(q, model_class, self._apply_filter, registry=self.type_registry)`
- `save`: `to_dict(instance, by_alias=True, registry=self.type_registry)`
- `create`: `to_dict(instance, by_alias=True, registry=self.type_registry)`

In `AsyncCendry`:
- `get`: `return deserialize(model_class, doc.id, doc.to_dict(), registry=self.type_registry)`
- `find`: `return deserialize(model_class, doc.id, doc.to_dict(), registry=self.type_registry)`
- `get_many`: `results.append(deserialize(model_class, doc.id, doc.to_dict(), registry=self.type_registry))`
- `select`: `return AsyncQuery(q, model_class, self._apply_filter, registry=self.type_registry)`
- `select_group`: `return AsyncQuery(q, model_class, self._apply_filter, registry=self.type_registry)`
- `save`: `to_dict(instance, by_alias=True, registry=self.type_registry)`
- `create`: `to_dict(instance, by_alias=True, registry=self.type_registry)`

- [ ] **Step 4: Run tests to verify they pass**

Run: `uv run pytest tests/test_context.py -k "registry" -v`
Expected: All pass

- [ ] **Step 5: Run full test suite**

Run: `uv run pytest -q`
Expected: All tests pass

- [ ] **Step 6: Commit**

```bash
git add src/cendry/context.py tests/test_context.py
git commit -m "feat: thread type_registry from context through all call sites"
```

---

### Task 4: Type checking, linting, and final verification

**Files:** All modified files

- [ ] **Step 1: Run ruff**

Run: `uv run ruff check src/ tests/ && uv run ruff format --check src/ tests/`
Expected: Clean. Fix and format if needed.

- [ ] **Step 2: Run mypy**

Run: `uv run mypy src/cendry/ tests/`
Expected: Clean.

- [ ] **Step 3: Run ty**

Run: `uv run ty check src/cendry/ tests/`
Expected: Clean.

- [ ] **Step 4: Run full coverage**

Run: `uv run coverage run -m pytest && uv run coverage report --show-missing`
Expected: All pass, 100% coverage.

- [ ] **Step 5: Fix any issues and commit**

```bash
git add -u
git commit -m "fix: resolve type-checking and lint issues for registry threading"
```

(Skip if no fixes needed.)
