docs(styleguide): add Python Type Promotion Mandate (DOD §8.5-8.7)
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user