diff --git a/conductor/code_styleguides/type_aliases.md b/conductor/code_styleguides/type_aliases.md new file mode 100644 index 00000000..c854e48a --- /dev/null +++ b/conductor/code_styleguides/type_aliases.md @@ -0,0 +1,319 @@ +# 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) \ No newline at end of file