From b096a8bea91d00ba6843133c80711415804351b2 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Thu, 25 Jun 2026 20:54:10 -0400 Subject: [PATCH] =?UTF-8?q?docs(styleguide):=20add=20Python=20Type=20Promo?= =?UTF-8?q?tion=20Mandate=20(DOD=20=C2=A78.5-8.7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../code_styleguides/data_oriented_design.md | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/conductor/code_styleguides/data_oriented_design.md b/conductor/code_styleguides/data_oriented_design.md index acb93d03..28011b59 100644 --- a/conductor/code_styleguides/data_oriented_design.md +++ b/conductor/code_styleguides/data_oriented_design.md @@ -173,6 +173,55 @@ Systems communicate through **explicit data protocols**, modeled after network p Design with the actual hardware's properties — cache hierarchy, memory bandwidth, alignment, latency vs throughput — and to its strengths. +### 8.5 The Python Type Promotion Mandate (added 2026-06-25) + +**C11/Odin/Jai semantics in a Python runtime.** This codebase is written in Python because of practical constraints (time, dependencies, LLM codegen ability), but the convention is to make Python behave as close to a statically-typed value-typed language as the runtime allows. **LLMs default to opaque types (`dict[str, Any]`, `Any`, `Optional[T]`, `hasattr()` polymorphism) because that's what idiomatic Python training data looks like. That defaults to mediocrity; this rule overrides it.** + +**The 7 banned patterns** (any of these in a non-boundary file is an anti-pattern; the audit scripts flag them): + +| Banned | Why | Use instead | +|---|---|---| +| `dict[str, Any]` (parameter or return) | Open-ended; hides the schema; invites `.get('any_key', default)` defensive checks | A typed dataclass (`@dataclass(frozen=True, slots=True)`) with explicit fields | +| `Any` (parameter, return, or field) | Same problem; LLMs use it to avoid thinking about types | A specific typed dataclass or one of the concrete types in `src/type_aliases.py` | +| `Optional[T]` (return) | `None` requires a runtime check; propagates through call sites | `Result[T]` (with errors as data) or a `NIL_T` sentinel (zero-initialized frozen dataclass) | +| `hasattr(x, 'field')` for entity type dispatch | Runtime type check; defeats the type system | `isinstance(x, TypedDataclass)` against a typed Union, or refactor so the function takes a typed parameter (no dispatch needed) | +| `getattr(x, 'field', default)` on a known-typed value | Same; the type system should guarantee the field exists | `x.field` direct access; if the field is nullable, the dataclass has `Optional[T]` as a field type (and the value is checked at construction, not at every read) | +| `.get('field', default)` on a `dict[str, Any]` for a known field | Runtime type-dispatch branch | Direct attribute access on the typed dataclass | +| `if 'field' in dict` checks | Same | Direct attribute access (the dataclass has a default value) | + +**The one exception (the boundary layer):** at the literal wire boundary (TOML parsing, JSON parsing, vendor SDK response parsing), the data is open-ended for the 100ns between parsing and `from_dict()` conversion. At that boundary: + +- The function that calls `tomllib.load()` or `json.loads()` may return `Metadata` (the typed fat struct — see §8.6). +- Every consumer of that function IMMEDIATELY calls `SomeTypedDataclass.from_dict(metadata)` and uses the typed result. +- The boundary is 2-3 functions per file (one per wire entry point). + +**No other code uses `Metadata` or `dict[str, Any]` or `Any`.** This is enforced by `scripts/audit_weak_types.py --strict` (existing) + the boundary-layer audit (planned in `conductor/tracks/cruft_elimination_20260627/spec.md`). + +### 8.6 The Boundary Layer (the wire schema) + +The codebase has ONE typed fat struct at the boundary: `Metadata` in `src/type_aliases.py`. It is `@dataclass(frozen=True, slots=True)` with explicit fields covering the TOML/JSON wire schema (paths, project, discussion, role, content, ts, source_tier, model, depends_on, document, script, args, etc.). It is used in exactly 2 places: +1. TOML loaders (`tomllib.load()` → `Metadata.from_dict(...)` → typed config) +2. JSON wire parsers (`json.loads()` → `Metadata.from_dict(...)` → typed request/response) + +After the boundary, every value is a typed componentized dataclass (`CommsLogEntry`, `HistoryMessage`, `FileItem`, `Ticket`, `ToolCall`, `ChatMessage`, `UsageStats`, `RAGChunk`, `SessionInsights`, `DiscussionSettings`, `CustomSlice`, `MMAUsageStats`, `ProviderPayload`, `UIPanelConfig`, `PathInfo`, `ToolDefinition`). + +**The componentized dataclasses exist for specific paths.** A function that handles ONE entity type takes that type's dataclass directly. A function that genuinely handles multiple entity types in ONE generalized path takes a Union: `def handle(x: CommsLogEntry | FileItem | HistoryMessage) -> None:` with `isinstance(x, CommsLogEntry)` dispatch. **NOT** `def handle(x: Metadata) -> None:` with `hasattr(x, 'tool_calls')` dispatch. + +**Why this matters:** the dispatcher functions in `src/app_controller.py` and `src/gui_2.py` had `if hasattr(...)` chains that contributed to the 4.01e+22 effective-codepaths metric (`Σ 2^branches(f)`). After this rule is enforced, those functions take typed parameters, the `hasattr` chains collapse to single `isinstance` checks or are eliminated entirely, and the metric drops by 4+ orders of magnitude. + +### 8.7 The "C11/Odin/Jai in Python" framing + +| C11/Odin/Jai concept | Python equivalent | +|---|---| +| Value type (`struct Foo { int x; string y; }`) | `@dataclass(frozen=True, slots=True) class Foo: x: int = 0; y: str = ""` | +| Static type (`int`, `string`) | Type hint + mypy in CI | +| No null | `Result[T]` (errors as data) or `NIL_T` sentinel (zero-initialized frozen dataclass) | +| Direct field access (`foo.x`) | `foo.x` direct attribute access (not `foo.get('x', default)`) | +| No dynamic dispatch (`if hasfield`) | Compile-time-typed function params (no `hasattr()` runtime dispatch) | +| Explicit conversion at boundary (`parse_wire(bytes) -> Foo`) | `Foo.from_dict(wire_dict)` at the wire entry; internal code never sees the wire format | + +**If you find yourself writing `dict[str, Any]`, `Any`, `Optional[T]`, `hasattr()`, or `.get()` for type dispatch, stop and ask: "what typed dataclass should this be?"** The answer is usually in `src/type_aliases.py` (12 existing) or you need to add one. + - **Latency and throughput are only the same thing in a sequential system.** For every performance requirement, identify which one it actually is before designing for it. - The compiler and language are tools, not magic: memory layout, access order, and the choice of what work to do at all are your job, not theirs — and they are roughly 90% of the problem. Know what the compiler can reasonably do with what you wrote, and don't delegate what it can't.