# Type Aliases Convention

> **Status:** Active convention as of 2026-06-06. Established by the `data_structure_strengthening_20260606` track.
>
> Canonical reference for all Python type-alias decisions in this codebase. Companion to `error_handling.md` (the Result convention) and `data_oriented_design.md` (the canonical DOD).

This styleguide codifies the "names for shapes" pattern: every `dict[str, Any]`, `list[dict[...]]`, or anonymous tuple return should use a named `TypeAlias` from `src/type_aliases.py`. The 10 aliases cover the 86% of common patterns.

Reference: the audit script `scripts/audit_weak_types.py` is the ground truth. The track replaced 416 weak sites across 6 high-traffic files; the audit `--strict` mode (with baseline `scripts/audit_weak_types.baseline.json`) enforces the convention going forward.

---

## The 10 Aliases (the canonical set)

`src/type_aliases.py` defines 10 `TypeAlias`es + 1 `NamedTuple`:

| Alias | Resolves to | Semantic role |
|---|---|---|
| `Metadata` | `dict[str, Any]` | The root alias; any key-value record |
| `CommsLogEntry` | `Metadata` | A single entry in the AI comms log |
| `CommsLog` | `list[CommsLogEntry]` | The comms log ring buffer |
| `HistoryMessage` | `Metadata` | A single message in the AI provider history (UI-layer) |
| `History` | `list[HistoryMessage]` | The conversation history |
| `FileItem` | `Metadata` | A single file in the context (path, content, view_mode, etc.) |
| `FileItems` | `list[FileItem]` | The most common weak pattern in the codebase |
| `ToolDefinition` | `Metadata` | A single tool definition (name, description, parameters schema) |
| `ToolCall` | `Metadata` | A single tool call from the model (id, type, function) |
| `CommsLogCallback` | `Callable[[CommsLogEntry], None]` | The callback signature for comms log updates |

Plus the NamedTuple:

| NamedTuple | Fields | Semantic role |
|---|---|---|
| `FileItemsDiff` | `refreshed: FileItems`, `changed: FileItems` | Return of `_reread_file_items_result` |

---

## The 5 Decision Patterns

### 1. Use `Metadata` for any dict-shaped record

```python
def parse_metadata(raw: str) -> Metadata:
 return json.loads(raw)

def save_metadata(name: str, data: Metadata) -> None:
 ...
```

The alias is `dict[str, Any]` at runtime; the name documents the semantic role.

### 2. Use the more specific alias when the role is known

If the dict is specifically a comms log entry, call it `CommsLogEntry` not `Metadata`. The LLM reader (and the human reviewer) sees the role at the type level.

```python
def append_comms(entry: CommsLogEntry) -> None: ...

def get_history() -> History: ...
```

The underlying type is still `dict[str, Any]`; the alias name is the documentation.

### 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".

```python
def build_aggregate(file_items: FileItems) -> str: ...

@dataclass
class Context:
 files: FileItems = field(default_factory=list)
```

### 4. Use `FileItemsDiff` NamedTuple for the dual-list return pattern

When a function returns two parallel lists that mean different things, use a NamedTuple with semantic field names.

```python
class FileItemsDiff(NamedTuple):
 refreshed: FileItems
 changed: FileItems

def _reread_file_items_result(file_items: FileItems) -> Result[FileItemsDiff]: ...
```

Callers can unpack by position (`refreshed, changed = _reread_file_items_result(...).data`) or by name (`result.refreshed`).

### 5. Use `Optional[Alias]` for nullable fields (NOT `Optional[dict[str, Any]]`)

```python
last_error: Optional[Metadata] = None
file_items: Optional[FileItems] = None
```

The `Optional[X]` return-type ban from `error_handling.md` applies to the 3 refactored files (`mcp_client`, `ai_client`, `rag_engine`); argument types that may be `None` (caller choice) remain allowed.

---

## Decision Tree

```
Q: Is this a `dict[str, Any]` shape?
+-- yes:
| Q: What is its semantic role?
| +-- generic key-value record -> Metadata
| +-- comms log entry -> CommsLogEntry
| +-- file in the context -> FileItem
| +-- tool definition -> ToolDefinition
| +-- tool call from the model -> ToolCall
| +-- provider history message -> HistoryMessage (UI layer)
|
+-- no, it's `list[dict[...]]`:
| Q: What is the list?
| +-- comms log entries -> CommsLog
| +-- file items -> FileItems
| +-- provider history messages -> History
| +-- generic -> list[Metadata]
|
+-- no, it's a tuple return:
| Q: Are the elements semantically distinct?
| +-- yes (e.g., refreshed vs. changed) -> NamedTuple
| +-- no (positional coordinates, etc.) -> leave as tuple (rare)
|
+-- no, it's `Callable[[...], None]` for the comms log -> CommsLogCallback
```

---

## The Audit Enforcement

`scripts/audit_weak_types.py` is the ground truth for "weak types in the codebase."

**Default mode (informational):**

```bash
uv run python scripts/audit_weak_types.py
# Prints the full report. Exits 0 regardless of findings.
```

**JSON mode (for tooling):**

```bash
uv run python scripts/audit_weak_types.py --json
# Outputs the full report as JSON.
```

**Strict mode (CI gate):**

```bash
uv run python scripts/audit_weak_types.py --strict
# Exits 1 if the current count exceeds `scripts/audit_weak_types.baseline.json`.
# Wire this into CI to fail any PR that introduces new weak types.
```

**Regenerating the baseline:**

The baseline file records the post-refactor count. Regenerate it ONLY when a new track intentionally reduces the count:

```bash
uv run python scripts/audit_weak_types.py --json | \
  python -c "import json, sys; d = json.load(sys.stdin); print(json.dumps({'total_weak': d['total_weak'], 'files_with_findings': d['files_with_findings'], 'by_category': d['by_category'], 'by_severity': d['by_severity']}, indent=2))" \
  > scripts/audit_weak_types.baseline.json
```

---

## The Type Registry (Auto-Generated Docs)

The aliases' field information lives in `docs/type_registry/` — auto-generated by `scripts/generate_type_registry.py`. The script:

- Scans `src/` for `@dataclass`, `NamedTuple`, `TypeAlias`, and `TypedDict` definitions.
- Writes one `.md` per source file (e.g., `docs/type_registry/src_ai_client.md`).
- Writes a top-level `index.md` with the table of contents and cross-module index.

**Usage:**

```bash
# Generate / regenerate (default)
uv run python scripts/generate_type_registry.py

# CI mode; exit 1 if the registry would change
uv run python scripts/generate_type_registry.py --check

# Dry run; print what would change without writing
uv run python scripts/generate_type_registry.py --diff
```

**When the LLM needs the fields of a type:**

```bash
cat docs/type_registry/src_models.md    # for src/models.py types
cat docs/type_registry/type_aliases.md  # for the 10 TypeAliases
```

**The "delete to turn off" pattern** (per `feature_flags.md`): `rm -rf docs/type_registry/` disables the registry. Re-enable by running `python scripts/generate_type_registry.py`.

---

## How to Extend (Adding a New Alias)

When a new semantic role emerges (e.g., `RequestPayload`, `ResponsePayload`):

1. **Add the alias to `src/type_aliases.py`**:

```python
RequestPayload: TypeAlias = dict[str, Any]
ResponsePayload: TypeAlias = dict[str, Any]
```

2. **Add tests to `tests/test_type_aliases.py`**:

```python
def test_request_payload_alias_resolves_to_metadata() -> None:
 assert type_aliases.RequestPayload == dict[str, Any]
```

3. **Import and use** in the affected files:

```python
from src.type_aliases import RequestPayload

def parse_request(raw: str) -> RequestPayload: ...
```

4. **Re-run the audit** to confirm the new alias covers the sites:

```bash
uv run python scripts/audit_weak_types.py --strict
```

5. **Re-run the type registry** to update `docs/type_registry/`:

```bash
uv run python scripts/generate_type_registry.py
```

6. **Update the audit baseline** if the count dropped:

```bash
# Regenerate the baseline (see command above)
```

---

## Anti-Patterns

**DON'T do these things:**

1. **DON'T** use `dict[str, Any]` in production code. Use `Metadata` (or a more specific alias). The audit script catches new instances.
2. **DON'T** invent ad-hoc aliases (e.g., `RequestData`, `ResponseBody`). Add them to `src/type_aliases.py` instead — that's the canonical source.
3. **DON'T** use `list[dict[str, Any]]` for file items. Use `FileItems`.
4. **DON'T** use `list[dict[str, Any]]` for comms log. Use `CommsLog`.
5. **DON'T** use `list[dict[str, Any]]` for history. Use `History`.
6. **DON'T** return anonymous tuples. Use a NamedTuple with semantic field names.
7. **DON'T** write `Optional[dict[str, Any]]`. Use `Optional[Metadata]`.
8. **DON'T** disable the audit `--strict` mode in CI. The convention is the audit.
9. **DON'T** regenerate the baseline to mask a regression. The baseline documents an achieved count; a regression means new code violated the convention.

---

## Examples (the 6 refactored files as worked examples)

**`src/ai_client.py`** (192 sites replaced):
- 6 `*_history: list[dict[str, Any]]` -> `*_history: History`
- `_comms_log: deque[dict[str, Any]]` -> `deque[CommsLogEntry]`
- `comms_log_callback: Optional[Callable[[dict[str, Any]], None]]` -> `Optional[CommsLogCallback]`
- `_reread_file_items_result(...) -> Result[FileItemsDiff]` (NamedTuple return)
- `_build_file_context_text(file_items: FileItems) -> str`
- 79 `dict[str, Any]` -> `Metadata`
- 56 `list[dict[str, Any]]` -> `list[ToolDefinition]` / `list[Metadata]`

**`src/app_controller.py`**: 62 `dict[str, Any]` -> `Metadata`; 20 `list[dict[str, Any]]` -> `list[Metadata]`; 4 `Optional[dict[str, Any]]` -> `Optional[Metadata]`.

**`src/models.py`**: 48 dataclass field types converted to `Optional[Metadata]` / `list[Metadata]`.

**`src/api_hook_client.py`**: HTTP request/response payloads use `Metadata` (the canonical "API payload" shape).

**`src/project_manager.py`**: TOML config dicts use `Metadata`; discussion entry lists use `list[Metadata]`.

**`src/aggregate.py`**: Aggregation result dicts use `Metadata`; `FileItems` for the file item lists.

---

## Coexistence with `Result[T]`

The new aliases are VALUE-LEVEL (the data inside a container). The `Result[T]` from `data_oriented_error_handling_20260606` is CONTROL-LEVEL (the success-or-failure wrapper). They compose:

```python
Result[CommsLogEntry]   # a Result wrapping a single comms log entry
Result[History]         # a Result wrapping a list of history messages
Result[FileItems]       # a Result wrapping a list of file items
Result[FileItemsDiff]   # a Result wrapping a NamedTuple
```

The aliases name the `T` in `Result[T]`; `Result` wraps the control flow. Both conventions are complementary.

---

## Why Per-Source-File Docs (vs one giant registry file)

A per-source-file layout matches the project's per-source-file guide structure (`docs/guide_ai_client.md`, `docs/guide_mcp_client.md`, etc.). The coding agent reads `docs/type_registry/src_ai_client.md` when working in `src/ai_client.py` — locality of reference. The `index.md` provides the cross-cutting view.

**The token cost per LLM query is bounded:** a typical source file's registry is 200-500 lines of markdown. The LLM reads it once and caches the schema in context. Subsequent references to the same types don't re-fetch.

---

## Cross-References

- `src/type_aliases.py` — the 10 TypeAliases + FileItemsDiff NamedTuple
- `scripts/audit_weak_types.py` — the audit script (default + `--strict` + `--json` modes)
- `scripts/audit_weak_types.baseline.json` — the post-Phase-1 baseline count
- `scripts/generate_type_registry.py` — the auto-generated docs generator
- `docs/type_registry/` — the auto-generated registry (one .md per source file + `index.md` + `type_aliases.md`)
- `conductor/code_styleguides/error_handling.md` — the `Result[T]` convention (complementary)
- `conductor/code_styleguides/data_oriented_design.md` — the canonical DOD reference
- `conductor/tracks/data_structure_strengthening_20260606/` — the track that established this convention
- `docs/guide_state_lifecycle.md` — `App.__getattr__`/`__setattr__` state delegation (the runtime contract the aliases preserve)