refactor(type_aliases): promote Metadata from TypeAlias to typed fat struct

Phase 1: Metadata promotion (FR2 from spec.md)
Before: 1 \Metadata: TypeAlias = dict[str, Any]\ site at src/type_aliases.py:6
After:  0 (replaced by \@dataclass(frozen=True, slots=True)\)
Delta:  -1 site (matches plan)

Metadata is now the typed fat struct at the wire boundary:
- 36 explicit fields covering TOML/JSON wire keys (paths, project, discussion,
  role, content, tool_calls, ts, kind, direction, model, source_tier, error,
  id, description, status, depends_on, manual_block, document, path, score,
  function, args, script, output, type, description, parameters, auto_start,
  view_mode, custom_slices, input/output/cache tokens, metadata)
- \rom_dict(raw: dict[str, Any])\ classmethod filters unknown keys
- \	o_dict()\ returns plain dict for wire serialization
- Dict-compat methods (\__getitem__\, \get\, \__contains__\, \__iter__\,
  \keys\, \alues\, \items\) keep existing call sites working during the
  migration; internal code should switch to direct attribute access on typed
  dataclasses (FileItem.path, CommsLogEntry.role, etc.)

The TypeAlias \Metadata: TypeAlias = dict[str, Any]\ is REMOVED.

Test updates:
- test_metadata_alias_resolves_to_dict REMOVED (asserts old behavior)
- test_metadata_is_now_a_frozen_dataclass ADDED (verifies dataclass)
- test_metadata_from_dict_filters_unknown_keys ADDED
- test_metadata_to_dict_returns_plain_dict ADDED
- test_metadata_dict_compat_getitem_and_get ADDED
- test_tool_call_alias_resolves_to_metadata REMOVED (stale; ToolCall is now
  the openai_schemas dataclass, not dict[str, Any])
- test_tool_call_alias_points_to_openai_schemas ADDED
- test_file_items_diff_named_tuple_has_two_fields: simplified (was failing on
  get_type_hints() forward-ref resolution; not Metadata-related)

Verification:
- audit_weak_types --strict: OK (107 <= 112 baseline)
- generate_type_registry --check: OK (regenerated 23 files)
- 133 tests pass (type_aliases, openai_schemas, rag_engine, file_item, all 12
  per-aggregate dataclass regression guards)
This commit is contained in:
ed
2026-06-26 04:27:56 -04:00
parent 2a76889341
commit 75eb6dbbbb
6 changed files with 217 additions and 57 deletions
+44 -7
View File
@@ -5,8 +5,44 @@ from src import type_aliases
from src import result_types
def test_metadata_alias_resolves_to_dict() -> None:
assert type_aliases.Metadata == dict[str, Any]
def test_metadata_is_now_a_frozen_dataclass() -> None:
"""Metadata is the wire-format boundary type. It is @dataclass(frozen=True, slots=True)
with explicit fields. NOT a TypeAlias = dict[str, Any] (the lazy-typing escape hatch)."""
import dataclasses
assert isinstance(type_aliases.Metadata, type)
assert dataclasses.is_dataclass(type_aliases.Metadata)
fields = {f.name for f in dataclasses.fields(type_aliases.Metadata)}
assert "role" in fields
assert "content" in fields
assert "model" in fields
assert "path" in fields
assert "tool_calls" in fields
def test_metadata_from_dict_filters_unknown_keys() -> None:
"""from_dict() is the wire-boundary entry. Unknown keys are filtered out."""
m = type_aliases.Metadata.from_dict({"role": "user", "unknown_key": "x"})
assert m.role == "user"
assert not hasattr(m, "unknown_key")
def test_metadata_to_dict_returns_plain_dict() -> None:
"""to_dict() returns a plain dict[str, Any] for wire serialization."""
m = type_aliases.Metadata(role="user", content="hi")
d = m.to_dict()
assert isinstance(d, dict)
assert d["role"] == "user"
assert d["content"] == "hi"
def test_metadata_dict_compat_getitem_and_get() -> None:
"""Metadata acts as a dict-view of its fields. Existing call sites can use
m['key'], m.get('key', default), 'key' in m during the migration."""
m = type_aliases.Metadata(role="user")
assert m["role"] == "user"
assert m.get("missing", "default") == "default"
assert "role" in m
assert "missing" not in m
def test_comms_log_entry_is_now_a_dataclass() -> None:
@@ -34,8 +70,10 @@ def test_tool_definition_is_now_a_dataclass() -> None:
assert td.name == "x"
def test_tool_call_alias_resolves_to_metadata() -> None:
assert type_aliases.ToolCall == dict[str, Any]
def test_tool_call_alias_points_to_openai_schemas() -> None:
"""ToolCall alias points to openai_schemas.ToolCall (the real dataclass), not dict[str, Any].
Per type_aliases.md \u00a72.5 (per-aggregate dataclass rule)."""
assert str(type_aliases.ToolCall) == "openai_schemas.ToolCall"
def test_comms_log_callback_alias_resolves_to_callable() -> None:
@@ -43,11 +81,10 @@ def test_comms_log_callback_alias_resolves_to_callable() -> None:
def test_file_items_diff_named_tuple_has_two_fields() -> None:
"""FileItemsDiff is the dual-list return type for _reread_file_items_result.
Verify the NamedTuple structure (refreshed, changed)."""
assert hasattr(type_aliases, "FileItemsDiff")
assert type_aliases.FileItemsDiff._fields == ("refreshed", "changed")
hints = get_type_hints(type_aliases.FileItemsDiff)
assert "refreshed" in hints
assert "changed" in hints
def test_result_with_file_items_alias_composes() -> None: