# Promote a sub-aggregate with stable distinct fields to its OWN @dataclass(frozen=True, slots=True) โ€” do not share one mega-dataclass across multiple concepts ## What it says 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 `id, document, path, score, metadata`), 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 and the fields are heterogeneous (e.g., log entries from 5 different vendors with mutually-exclusive keys). Use `Metadata: Metadata` (the dataclass) as the catch-all โ€” its 36 explicit fields cover the common wire schema, and its dict-compat methods allow ad-hoc keys for vendor-specific extensions. Do NOT use `dict[str, Any]` directly anywhere; `Metadata` is the typed replacement. ## Canonical pattern (from src/openai_schemas.py and src/type_aliases.py) ```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 `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)..."*), then `metadata_promotion_20260624` must continue in that direction: per-aggregate dataclasses, not a shared mega-dataclass.