Files

2.3 KiB

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)

@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 TypedDicts)..."), then metadata_promotion_20260624 must continue in that direction: per-aggregate dataclasses, not a shared mega-dataclass.