diff --git a/docs/type_registry/index.md b/docs/type_registry/index.md index 2a1cdd50..6b145a56 100644 --- a/docs/type_registry/index.md +++ b/docs/type_registry/index.md @@ -83,6 +83,7 @@ Generated by `scripts/generate_type_registry.py`. Re-run the script (or invoke ` - `StartupProfiler` (dataclass) - [`src\startup_profiler.py`](src\startup_profiler.md#src\startup_profiler.py::StartupProfiler) - `ThemePalette` (dataclass) - [`src\theme_models.py`](src\theme_models.md#src\theme_models.py::ThemePalette) - `ThemeFile` (dataclass) - [`src\theme_models.py`](src\theme_models.md#src\theme_models.py::ThemeFile) +- `Metadata` (dataclass) - [`src\type_aliases.py`](src\type_aliases.md#src\type_aliases.py::Metadata) - `CommsLogEntry` (dataclass) - [`src\type_aliases.py`](src\type_aliases.md#src\type_aliases.py::CommsLogEntry) - `HistoryMessage` (dataclass) - [`src\type_aliases.py`](src\type_aliases.md#src\type_aliases.py::HistoryMessage) - `ToolDefinition` (dataclass) - [`src\type_aliases.py`](src\type_aliases.md#src\type_aliases.py::ToolDefinition) @@ -94,7 +95,6 @@ Generated by `scripts/generate_type_registry.py`. Re-run the script (or invoke ` - `UIPanelConfig` (dataclass) - [`src\type_aliases.py`](src\type_aliases.md#src\type_aliases.py::UIPanelConfig) - `PathInfo` (dataclass) - [`src\type_aliases.py`](src\type_aliases.md#src\type_aliases.py::PathInfo) - `FileItemsDiff` (NamedTuple) - [`src\type_aliases.py`](src\type_aliases.md#src\type_aliases.py::FileItemsDiff) -- `Metadata` (TypeAlias) - [`src\type_aliases.py`](src\type_aliases.md#src\type_aliases.py::Metadata) - `CommsLog` (TypeAlias) - [`src\type_aliases.py`](src\type_aliases.md#src\type_aliases.py::CommsLog) - `History` (TypeAlias) - [`src\type_aliases.py`](src\type_aliases.md#src\type_aliases.py::History) - `FileItem` (TypeAlias) - [`src\type_aliases.py`](src\type_aliases.md#src\type_aliases.py::FileItem) diff --git a/docs/type_registry/src_openai_schemas.md b/docs/type_registry/src_openai_schemas.md index f145140f..74073403 100644 --- a/docs/type_registry/src_openai_schemas.md +++ b/docs/type_registry/src_openai_schemas.md @@ -5,7 +5,7 @@ Auto-generated from source. 6 struct(s) defined in this module. ## `src\openai_schemas.py::ChatMessage` **Kind:** `dataclass` -**Defined at:** line 49 +**Defined at:** line 58 **Fields:** - `role: str` @@ -18,7 +18,7 @@ Auto-generated from source. 6 struct(s) defined in this module. ## `src\openai_schemas.py::NormalizedResponse` **Kind:** `dataclass` -**Defined at:** line 76 +**Defined at:** line 102 **Fields:** - `text: str` @@ -30,7 +30,7 @@ Auto-generated from source. 6 struct(s) defined in this module. ## `src\openai_schemas.py::OpenAICompatibleRequest` **Kind:** `dataclass` -**Defined at:** line 97 +**Defined at:** line 123 **Fields:** - `messages: list[ChatMessage]` @@ -48,7 +48,7 @@ Auto-generated from source. 6 struct(s) defined in this module. ## `src\openai_schemas.py::ToolCall` **Kind:** `dataclass` -**Defined at:** line 32 +**Defined at:** line 36 **Fields:** - `id: str` @@ -59,7 +59,7 @@ Auto-generated from source. 6 struct(s) defined in this module. ## `src\openai_schemas.py::ToolCallFunction` **Kind:** `dataclass` -**Defined at:** line 26 +**Defined at:** line 30 **Fields:** - `name: str` @@ -69,7 +69,7 @@ Auto-generated from source. 6 struct(s) defined in this module. ## `src\openai_schemas.py::UsageStats` **Kind:** `dataclass` -**Defined at:** line 68 +**Defined at:** line 90 **Fields:** - `input_tokens: int` diff --git a/docs/type_registry/src_type_aliases.md b/docs/type_registry/src_type_aliases.md index 1bfed696..416dd400 100644 --- a/docs/type_registry/src_type_aliases.md +++ b/docs/type_registry/src_type_aliases.md @@ -5,7 +5,7 @@ Auto-generated from source. 20 struct(s) defined in this module. ## `src\type_aliases.py::CommsLog` **Kind:** `TypeAlias` -**Defined at:** line 29 +**Defined at:** line 125 **Resolves to:** `list[CommsLogEntry]` **Used by:** `CommsLogCallback` @@ -14,7 +14,7 @@ Auto-generated from source. 20 struct(s) defined in this module. ## `src\type_aliases.py::CommsLogCallback` **Kind:** `TypeAlias` -**Defined at:** line 151 +**Defined at:** line 275 **Resolves to:** `Callable[[CommsLogEntry], None]` **Note:** `CommsLogCallback` is a semantic alias. The type registry is auto-generated from the source code. @@ -22,7 +22,7 @@ Auto-generated from source. 20 struct(s) defined in this module. ## `src\type_aliases.py::CommsLogEntry` **Kind:** `dataclass` -**Defined at:** line 10 +**Defined at:** line 106 **Fields:** - `ts: str` @@ -38,7 +38,7 @@ Auto-generated from source. 20 struct(s) defined in this module. ## `src\type_aliases.py::CustomSlice` **Kind:** `dataclass` -**Defined at:** line 100 +**Defined at:** line 204 **Fields:** - `tag: str` @@ -50,7 +50,7 @@ Auto-generated from source. 20 struct(s) defined in this module. ## `src\type_aliases.py::DiscussionSettings` **Kind:** `dataclass` -**Defined at:** line 90 +**Defined at:** line 190 **Fields:** - `temperature: float` @@ -61,7 +61,7 @@ Auto-generated from source. 20 struct(s) defined in this module. ## `src\type_aliases.py::FileItem` **Kind:** `TypeAlias` -**Defined at:** line 53 +**Defined at:** line 149 **Resolves to:** `'models.FileItem'` **Used by:** `FileItems`, `FileItemsDiff` @@ -70,7 +70,7 @@ Auto-generated from source. 20 struct(s) defined in this module. ## `src\type_aliases.py::FileItems` **Kind:** `TypeAlias` -**Defined at:** line 54 +**Defined at:** line 150 **Resolves to:** `list[FileItem]` **Used by:** `FileItemsDiff` @@ -79,7 +79,7 @@ Auto-generated from source. 20 struct(s) defined in this module. ## `src\type_aliases.py::FileItemsDiff` **Kind:** `NamedTuple` -**Defined at:** line 157 +**Defined at:** line 281 **Fields:** - `refreshed: FileItems` @@ -89,7 +89,7 @@ Auto-generated from source. 20 struct(s) defined in this module. ## `src\type_aliases.py::History` **Kind:** `TypeAlias` -**Defined at:** line 50 +**Defined at:** line 146 **Resolves to:** `list[HistoryMessage]` **Used by:** `ProviderHistory` @@ -98,7 +98,7 @@ Auto-generated from source. 20 struct(s) defined in this module. ## `src\type_aliases.py::HistoryMessage` **Kind:** `dataclass` -**Defined at:** line 33 +**Defined at:** line 129 **Fields:** - `role: str` @@ -112,7 +112,7 @@ Auto-generated from source. 20 struct(s) defined in this module. ## `src\type_aliases.py::JsonPrimitive` **Kind:** `TypeAlias` -**Defined at:** line 153 +**Defined at:** line 277 **Resolves to:** `str | int | float | bool | None` **Used by:** `JsonValue` @@ -121,7 +121,7 @@ Auto-generated from source. 20 struct(s) defined in this module. ## `src\type_aliases.py::JsonValue` **Kind:** `TypeAlias` -**Defined at:** line 154 +**Defined at:** line 278 **Resolves to:** `JsonPrimitive | list['JsonValue'] | dict[str, 'JsonValue']` **Used by:** `OpenAICompatibleRequest`, `WebSocketMessage` @@ -130,7 +130,7 @@ Auto-generated from source. 20 struct(s) defined in this module. ## `src\type_aliases.py::MMAUsageStats` **Kind:** `dataclass` -**Defined at:** line 111 +**Defined at:** line 219 **Fields:** - `model: str` @@ -140,17 +140,53 @@ Auto-generated from source. 20 struct(s) defined in this module. ## `src\type_aliases.py::Metadata` -**Kind:** `TypeAlias` -**Defined at:** line 6 -**Resolves to:** `dict[str, Any]` -**Used by:** `PathInfo`, `Persona`, `ProviderPayload`, `RAGChunk`, `Session`, `ToolDefinition`, `TrackState`, `WorkerContext`, `WorkspaceProfile` +**Kind:** `dataclass` +**Defined at:** line 16 + +**Fields:** +- `paths: dict[str, Any]` +- `project: dict[str, Any]` +- `discussion: dict[str, Any]` +- `role: str` +- `content: Any` +- `tool_calls: list[Any]` +- `tool_call_id: str` +- `name: str` +- `ts: str` +- `kind: str` +- `direction: str` +- `model: str` +- `source_tier: str` +- `error: str` +- `id: str` +- `description: str` +- `status: str` +- `depends_on: tuple` +- `manual_block: bool` +- `document: str` +- `path: str` +- `score: float` +- `function: dict[str, Any]` +- `args: dict[str, Any]` +- `script: str` +- `output: str` +- `type: str` +- `description: str` +- `parameters: dict[str, Any]` +- `auto_start: bool` +- `view_mode: str` +- `custom_slices: list[Any]` +- `input_tokens: int` +- `output_tokens: int` +- `cache_read_input_tokens: int` +- `cache_creation_input_tokens: int` +- `metadata: dict[str, Any]` -**Note:** `Metadata` is a semantic alias. The type registry is auto-generated from the source code. ## `src\type_aliases.py::PathInfo` **Kind:** `dataclass` -**Defined at:** line 142 +**Defined at:** line 262 **Fields:** - `logs_dir: Metadata` @@ -161,7 +197,7 @@ Auto-generated from source. 20 struct(s) defined in this module. ## `src\type_aliases.py::ProviderPayload` **Kind:** `dataclass` -**Defined at:** line 121 +**Defined at:** line 233 **Fields:** - `script: str` @@ -173,7 +209,7 @@ Auto-generated from source. 20 struct(s) defined in this module. ## `src\type_aliases.py::SessionInsights` **Kind:** `dataclass` -**Defined at:** line 77 +**Defined at:** line 173 **Fields:** - `total_tokens: int` @@ -187,7 +223,7 @@ Auto-generated from source. 20 struct(s) defined in this module. ## `src\type_aliases.py::ToolCall` **Kind:** `TypeAlias` -**Defined at:** line 73 +**Defined at:** line 169 **Resolves to:** `'openai_schemas.ToolCall'` **Used by:** `ChatMessage`, `NormalizedResponse`, `ToolCall` @@ -196,7 +232,7 @@ Auto-generated from source. 20 struct(s) defined in this module. ## `src\type_aliases.py::ToolDefinition` **Kind:** `dataclass` -**Defined at:** line 58 +**Defined at:** line 154 **Fields:** - `name: str` @@ -208,7 +244,7 @@ Auto-generated from source. 20 struct(s) defined in this module. ## `src\type_aliases.py::UIPanelConfig` **Kind:** `dataclass` -**Defined at:** line 132 +**Defined at:** line 248 **Fields:** - `separate_message_panel: bool` diff --git a/docs/type_registry/type_aliases.md b/docs/type_registry/type_aliases.md index 8c312110..08da71e8 100644 --- a/docs/type_registry/type_aliases.md +++ b/docs/type_registry/type_aliases.md @@ -2,12 +2,12 @@ # Module: `src/type_aliases.py (TypeAliases only)` -Auto-generated from source. 9 struct(s) defined in this module. +Auto-generated from source. 8 struct(s) defined in this module. ## `src\type_aliases.py::CommsLog` **Kind:** `TypeAlias` -**Defined at:** line 29 +**Defined at:** line 125 **Resolves to:** `list[CommsLogEntry]` **Used by:** `CommsLogCallback` @@ -16,7 +16,7 @@ Auto-generated from source. 9 struct(s) defined in this module. ## `src\type_aliases.py::CommsLogCallback` **Kind:** `TypeAlias` -**Defined at:** line 151 +**Defined at:** line 275 **Resolves to:** `Callable[[CommsLogEntry], None]` **Note:** `CommsLogCallback` is a semantic alias. The type registry is auto-generated from the source code. @@ -24,7 +24,7 @@ Auto-generated from source. 9 struct(s) defined in this module. ## `src\type_aliases.py::FileItem` **Kind:** `TypeAlias` -**Defined at:** line 53 +**Defined at:** line 149 **Resolves to:** `'models.FileItem'` **Used by:** `FileItems`, `FileItemsDiff` @@ -33,7 +33,7 @@ Auto-generated from source. 9 struct(s) defined in this module. ## `src\type_aliases.py::FileItems` **Kind:** `TypeAlias` -**Defined at:** line 54 +**Defined at:** line 150 **Resolves to:** `list[FileItem]` **Used by:** `FileItemsDiff` @@ -42,7 +42,7 @@ Auto-generated from source. 9 struct(s) defined in this module. ## `src\type_aliases.py::History` **Kind:** `TypeAlias` -**Defined at:** line 50 +**Defined at:** line 146 **Resolves to:** `list[HistoryMessage]` **Used by:** `ProviderHistory` @@ -51,7 +51,7 @@ Auto-generated from source. 9 struct(s) defined in this module. ## `src\type_aliases.py::JsonPrimitive` **Kind:** `TypeAlias` -**Defined at:** line 153 +**Defined at:** line 277 **Resolves to:** `str | int | float | bool | None` **Used by:** `JsonValue` @@ -60,25 +60,16 @@ Auto-generated from source. 9 struct(s) defined in this module. ## `src\type_aliases.py::JsonValue` **Kind:** `TypeAlias` -**Defined at:** line 154 +**Defined at:** line 278 **Resolves to:** `JsonPrimitive | list['JsonValue'] | dict[str, 'JsonValue']` **Used by:** `OpenAICompatibleRequest`, `WebSocketMessage` **Note:** `JsonValue` is a semantic alias. The type registry is auto-generated from the source code. -## `src\type_aliases.py::Metadata` - -**Kind:** `TypeAlias` -**Defined at:** line 6 -**Resolves to:** `dict[str, Any]` -**Used by:** `PathInfo`, `Persona`, `ProviderPayload`, `RAGChunk`, `Session`, `ToolDefinition`, `TrackState`, `WorkerContext`, `WorkspaceProfile` - -**Note:** `Metadata` is a semantic alias. The type registry is auto-generated from the source code. - ## `src\type_aliases.py::ToolCall` **Kind:** `TypeAlias` -**Defined at:** line 73 +**Defined at:** line 169 **Resolves to:** `'openai_schemas.ToolCall'` **Used by:** `ChatMessage`, `NormalizedResponse`, `ToolCall` diff --git a/src/type_aliases.py b/src/type_aliases.py index 5518873d..5cde7725 100644 --- a/src/type_aliases.py +++ b/src/type_aliases.py @@ -3,7 +3,103 @@ from dataclasses import dataclass, field, fields as dc_fields from typing import Any, Callable, NamedTuple, TypeAlias -Metadata: TypeAlias = dict[str, Any] +# The wire-format boundary type. ONLY used at TOML/JSON parse functions. +# Internal code uses componentized dataclasses (CommsLogEntry, FileItem, etc.). +# This dataclass has explicit fields covering the wire format. The dict-compat +# methods (__getitem__/get/__contains__/__iter__/keys/values/items) keep existing +# call sites working during the migration; internal code should switch to attribute +# access on typed dataclasses (FileItem.path, CommsLogEntry.role, etc.). +_NON_NULL_FIELDS: frozenset[str] = frozenset({"model", "source_tier"}) + + +@dataclass(frozen=True, slots=True) +class Metadata: + # TOML/JSON config keys (project paths, settings) + paths: dict[str, Any] = field(default_factory=dict) + project: dict[str, Any] = field(default_factory=dict) + discussion: dict[str, Any] = field(default_factory=dict) + # Per-vendor chat message keys + role: str = "" + content: Any = None + tool_calls: list[Any] = field(default_factory=list) + tool_call_id: str = "" + name: str = "" + # Session log / comms / MMA telemetry keys + ts: str = "" + kind: str = "" + direction: str = "" + model: str = "unknown" + source_tier: str = "main" + error: str = "" + # MMA ticket keys + id: str = "" + description: str = "" + status: str = "todo" + depends_on: tuple = () + manual_block: bool = False + # RAG result keys + document: str = "" + path: str = "" + score: float = 0.0 + # Tool definition + tool call keys + function: dict[str, Any] = field(default_factory=dict) + args: dict[str, Any] = field(default_factory=dict) + script: str = "" + output: str = "" + type: str = "" + description: str = "" + parameters: dict[str, Any] = field(default_factory=dict) + auto_start: bool = False + # File item keys + view_mode: str = "full" + custom_slices: list[Any] = field(default_factory=list) + # Token usage keys + input_tokens: int = 0 + output_tokens: int = 0 + cache_read_input_tokens: int = 0 + cache_creation_input_tokens: int = 0 + # Generic pass-through (arbitrary keys; filtered by from_dict) + metadata: dict[str, Any] = field(default_factory=dict) + + def to_dict(self) -> dict[str, Any]: + return {f.name: getattr(self, f.name) for f in dc_fields(self) if getattr(self, f.name) not in (None, "", [], {}, 0, 0.0, False) or f.name in _NON_NULL_FIELDS} + + @classmethod + def from_dict(cls, raw: dict[str, Any]) -> "Metadata": + valid = {f.name for f in dc_fields(cls)} + return cls(**{k: v for k, v in raw.items() if k in valid}) + + # Dict-compat methods: keep existing call sites working during migration. + # These treat the dataclass as a "view" of its fields with dict-like access. + # New code should use direct attribute access (metadata.role, metadata.path, etc.). + def __getitem__(self, key: str) -> Any: + if key in {f.name for f in dc_fields(self)}: + return getattr(self, key) + raise KeyError(key) + + def get(self, key: str, default: Any = None) -> Any: + if key in {f.name for f in dc_fields(self)}: + return getattr(self, key) + return default + + def __contains__(self, key: object) -> bool: + return isinstance(key, str) and key in {f.name for f in dc_fields(self)} + + def __iter__(self): + for f in dc_fields(self): + yield f.name + + def keys(self): + for f in dc_fields(self): + yield f.name + + def values(self): + for f in dc_fields(self): + yield getattr(self, f.name) + + def items(self): + for f in dc_fields(self): + yield f.name, getattr(self, f.name) @dataclass(frozen=True) diff --git a/tests/test_type_aliases.py b/tests/test_type_aliases.py index 69da9555..0bd0a597 100644 --- a/tests/test_type_aliases.py +++ b/tests/test_type_aliases.py @@ -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: