From c6748634a845d6499150664e688530760ce685b1 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Thu, 25 Jun 2026 14:31:31 -0400 Subject: [PATCH] docs(styleguides): clarify when to promote to per-aggregate dataclass --- conductor/code_styleguides/type_aliases.md | 35 ++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/conductor/code_styleguides/type_aliases.md b/conductor/code_styleguides/type_aliases.md index c854e48a..b4a6bf5e 100644 --- a/conductor/code_styleguides/type_aliases.md +++ b/conductor/code_styleguides/type_aliases.md @@ -61,6 +61,41 @@ def get_history() -> History: ... The underlying type is still `dict[str, Any]`; the alias name is the documentation. +### 2.5. When the role has stable distinct fields, promote it to its OWN dataclass + +**Added 2026-06-25 (correction to `metadata_promotion_20260624`).** When a sub-aggregate has a known set of stable, distinct fields (e.g., `CommsLogEntry` has `ts, role, kind, direction, model, source_tier, content, error`; `FileItem` has `path, view_mode, custom_slices`; `RAGChunk` has `document, path, score`), promote it to its OWN `@dataclass(frozen=True, slots=True)` with its OWN fields. Do **NOT** share one mega-dataclass across multiple concepts. + +**Why:** the per-aggregate dataclass is the "names for shapes" pattern extended to the structural level. Each concept gets its own type, its own fields, its own `to_dict()` / `from_dict()` round-trip. Consumers use direct field access (`entry.ts`, `t.depends_on`, `chunk.document`) which compiles to a single C-level field read with 0 branches. + +**When NOT to promote:** when the shape is genuinely unknown at type level (TOML project config, generic JSON parsing at a wire boundary, polymorphic log dumping). These are **collapsed codepaths** and they keep `Metadata: TypeAlias = dict[str, Any]` as the catch-all. + +**Canonical pattern (from `src/openai_schemas.py` and `src/models.py:533`):** + +```python +@dataclass(frozen=True, slots=True) +class CommsLogEntry: + ts: str = "" + role: str = "" + kind: str = "" + direction: str = "" + model: str = "unknown" + source_tier: str = "main" + content: Any = None + error: str = "" + + def to_dict(self) -> Metadata: + return asdict(self) + + @classmethod + def from_dict(cls, raw: Metadata) -> "CommsLogEntry": + valid = {f.name for f in fields(cls)} + return cls(**{k: v for k, v in raw.items() if k in valid}) +``` + +**The rule (Tier 1 audit 2026-06-25):** if the original 2026-06-06 `data_structure_strengthening_20260606` design intent was per-concept promotion (it was — see `spec.md §3.3`: *"Phase 2 can convert `Metadata` to a `TypedDict` (or split into per-concept `TypedDict`s)..."*), the metadata_promotion_20260624 track must continue in that direction: per-aggregate dataclasses, not a shared mega-dataclass. The corrected design is in `conductor/tracks/metadata_promotion_20260624/spec.md` (rewrite of `G3`, `FR1`, and `Out of Scope` on 2026-06-25). + +**For a worked example of the per-aggregate pattern in production:** `src/openai_schemas.py` defines `ToolCall`, `ToolCallFunction`, `ChatMessage`, `UsageStats`, `NormalizedResponse` as separate frozen dataclasses — each with its own fields. `src/models.py:533` defines `FileItem` with paired `to_dict()` / `from_dict()` round-trip. `src/models.py:302` defines `Ticket` with 15 typed fields. These are the reference implementations. + ### 3. Use `FileItems` for any list of file items `FileItems = list[FileItem]`. The most common weak pattern in the codebase. Replace `list[dict[str, Any]]` with `FileItems` whenever the list is "files in scope for the current context".