docs(styleguides): clarify when to promote to per-aggregate dataclass
This commit is contained in:
@@ -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".
|
||||
|
||||
Reference in New Issue
Block a user