diff --git a/conductor/tracks/metadata_promotion_20260624/spec.md b/conductor/tracks/metadata_promotion_20260624/spec.md index a442eb86..24fec6eb 100644 --- a/conductor/tracks/metadata_promotion_20260624/spec.md +++ b/conductor/tracks/metadata_promotion_20260624/spec.md @@ -1,175 +1,158 @@ # Track Specification: metadata_promotion_20260624 +> **Status:** ACTIVE — corrected 2026-06-25 (Tier 1 audit). The original spec (commit `e50bebdd`, 2026-06-25) proposed a single `@dataclass(frozen=True, slots=True) Metadata` with ~200 fields shared across all 5 sub-aggregates. That proposal was REJECTED on 2026-06-25 (user direction): the 5 sub-aggregates are distinct concepts with distinct field sets; lifting them into one mega-dataclass hides the type information that direct field access is supposed to reveal. The corrected design promotes each sub-aggregate to its OWN dataclass with its OWN fields. See `docs/reports/PLANNING_CORRECTION_metadata_promotion_20260625.md` for the full rationale. + ## Overview -The actual fix for the 4.01e22 combinatoric explosion. Promotes `Metadata: TypeAlias = dict[str, Any]` to a typed `@dataclass(frozen=True)` and migrates all 695 consumer functions + 213 access sites (107 `.get('key', ...)` + 106 subscript `['key']`) to use direct field access. +Promotes the 5 distinct sub-aggregates (`CommsLogEntry`, `HistoryMessage`, `FileItem`, `ToolDefinition`, `ToolCall`) to their own typed `@dataclass(frozen=True, slots=True)` classes (or reuses the existing typed dataclasses where they already exist: `models.FileItem`, `openai_schemas.ToolCall`), then migrates the 107 `.get('key', ...)` + 106 subscript `['key']` access sites on those aggregates to direct field access (`entry.ts`, `t.depends_on`, `chunk.document`). `Metadata: TypeAlias = dict[str, Any]` is preserved as the catch-all for **truly collapsed codepaths** (generic JSON parsing at wire boundaries, `manual_slop.toml` project config, polymorphic containers where the element type is genuinely unknown) and is NOT promoted to a shared mega-dataclass. + +The combinatoric explosion (`4.01e22` effective codepaths) is addressed by **per-aggregate type promotion**: each known concept gets its own dataclass with its own fields, the `.get()` / `[]` runtime type-dispatch collapses at the source, and the audit's branch count drops per consumer function. ## Current State Audit (master `dc397db7`, measured 2026-06-25) | Metric | Value | Source | |---|---:|---| -| `Metadata` consumers in `src/` | **695** | `scripts/code_path_audit.build_pcg` (was 751 in older measurements; some refactors reduced) | +| `Metadata` consumers in `src/` | **695** | `scripts/code_path_audit.build_pcg` | | Top consumer files | `app_controller.py: 123`, `mcp_client.py: 94`, `ai_client.py: 73`, `gui_2.py: 44`, `models.py: 29` | `Counter` over `pcg.consumers['Metadata']` | | Total branches in Metadata consumers | 3,454 | `scripts/code_path_audit_ssdl.count_branches_in_function` | | **Effective codepaths (the 4.01e22)** | **4.014e+22** | `compute_effective_codepaths` | -| `Metadata` definition | `src/type_aliases.py:5` | `Metadata: TypeAlias = dict[str, Any]` | -| `.get('key', ...)` access sites | 107 | `git grep` in `src/` | +| `.get('key', ...)` access sites (all sub-aggregates) | 107 | `git grep` in `src/` | | `['key']` subscript access sites | 106 | `git grep` in `src/` | -| `is None` / `== None` / `!= None` sites | 106 | `git grep` in `src/` (most are unrelated to Metadata; some are redundant defensive checks) | -| Distinct `.get` keys (top 20) | `ai, args, ast_elements, auto_start, burn_rate, call_count, comment, completed_tickets, conductor, content, context_presets, custom_slices, depends_on, description, dir, discussion, discussions, document, efficiency, files` | `git grep -hoE "\.get\('[a-z_]+',"` | -| Distinct subscript keys (top 20) | `_toggle_command_palette, app_debug_info, args, blocked_reason, bloom, cache_creation_input_tokens, cache_read_input_tokens, command, comment, content, crt, delete_context_preset, depends_on, discussion, discussions, end_line, fps, frame_time_ms_avg, full_path, get_app_debug_info` | `git grep -hoE "\[[ ]*'[a-z_]+'[ ]*\]"` | -| TypeAlias chain | `Metadata` is the root; `CommsLogEntry`, `HistoryMessage`, `FileItem`, `ToolDefinition`, `ToolCall` are all aliases to `Metadata` | `src/type_aliases.py` | +| `is None` / `== None` / `!= None` sites | 106 | `git grep` in `src/` (mostly unrelated to Metadata) | +| TypeAlias chain (current state, before this track) | `Metadata: dict[str, Any]`; `CommsLogEntry: Metadata`; `HistoryMessage: Metadata`; `FileItem: "models.FileItem"`; `ToolDefinition: Metadata`; `ToolCall: "openai_schemas.ToolCall"` | `src/type_aliases.py` | +| Existing per-aggregate dataclasses | `models.Ticket` (15 fields), `models.FileItem` (10 fields), `models.Track` (3 fields), `openai_schemas.ToolCall` (3 fields), `openai_schemas.ChatMessage` (5 fields), `openai_schemas.UsageStats` (4 fields), `openai_schemas.ToolCallFunction` (2 fields), `openai_schemas.NormalizedResponse` (4 fields), `vendor_capabilities.VendorCapabilities` (22 fields) | `git grep "^class .*(dataclass\|frozen=True)" src/` | +| Missing per-aggregate dataclasses | `CommsLogEntry`, `HistoryMessage`, `ToolDefinition`, `RAGChunk`, `SessionInsights`, `DiscussionSettings`, `CustomSlice`, `MMAUsageStats`, `ProviderPayload`, `UIPanelConfig`, `ContextPreset` (full schema), `PathInfo` | actual access patterns from `git grep` on `src/` | -### Why this matters +### Why the corrected design (per-aggregate dataclasses) — not one mega-dataclass -The combinatoric explosion (`4.01e22`) is **not from nil-checks** (per the SSDL post-mortem at `docs/reports/SSDL_CAMPAIGN_ABORTED_20260624.md`): +The 107 `.get('key', default)` and 106 `['key']` access sites in `src/` span **at least 12 distinct aggregates**, not 5. A sampling of the actual access patterns: -> "The 4.01e22 is **not from nil-checks**. It's from `Metadata: TypeAlias = dict[str, Any]`. Every consumer function that does `entry.get('key', default)` is a runtime type-dispatch branch. The combinatoric explosion is from the unknown type, not from missing sentinels." +| Access pattern | Site | Aggregate it actually represents | +|---|---|---| +| `item.get('custom_slices', [])`, `item.get('content', '')` | `src/aggregate.py:418,421` | **FileItem** (per-file curation) | +| `fi.get('path', 'attachment')` | `src/ai_client.py:2565,2807,2898` | **FileItem** | +| `chunk.get('document', '')` | `src/aggregate.py:3259`, `src/app_controller.py:251,4162` | **RAGChunk** (RAG retrieval result) | +| `entry.get('source_tier', 'main')`, `entry.get('model', 'unknown')` | `src/app_controller.py:2277,2302,2310` | **CommsLogEntry** (AI comms log) | +| `u.get('input_tokens', 0)`, `u.get('output_tokens', 0)` | `src/app_controller.py:2304-2309` | **UsageStats** (per-call token usage) | +| `t.get('id', '')`, `t.get('depends_on', [])`, `t.get('manual_block', False)`, `t.get('status')` | `src/gui_2.py:1366-1438` | **Ticket** (MMA ticket — already a dataclass) | +| `stats.get('model', 'unknown')`, `stats.get('input', 0)`, `stats.get('output', 0)` | `src/gui_2.py:2199-2201,2216` | **MMAUsageStats** (per-tier rollup) | +| `insights.get('total_tokens', 0)`, `insights.get('call_count', 0)`, `insights.get('burn_rate', 0)`, `insights.get('session_cost', 0)`, `insights.get('completed_tickets', 0)`, `insights.get('efficiency', 0)` | `src/gui_2.py:4926-4931` | **SessionInsights** (overall session stats) | +| `entry.get('temperature', 0.7)`, `entry.get('top_p', 1.0)`, `entry.get('max_output_tokens', 0)` | `src/gui_2.py:3535` | **DiscussionSettings** (per-turn settings) | +| `slc.get('tag', '')`, `slc.get('comment', '')` | `src/gui_2.py:4048-4054` | **CustomSlice** (visual slice editor) | +| `preset.get('files', [])`, `preset.get('screenshots', [])` | `src/gui_2.py:4184-4185` | **ContextPreset** (file composition) | +| `payload.get('script')`, `payload.get('args', {})`, `payload.get('output', '')`, `payload.get('content', '')` | `src/app_controller.py:2274,2287` | **ProviderPayload** (script-execution payload) | +| `self.project.get('paths', {})`, `self.project.get('conductor', {})`, `self.project.get('context_presets', {})` | `src/app_controller.py:1972,2016,2033`; `src/gui_2.py:820,4181,4333,4448` | **ProjectConfig** (`manual_slop.toml` — TRUE catch-all dict; uses `Metadata`) | +| `gui_cfg.get('separate_message_panel', False)`, `gui_cfg.get('separate_response_panel', False)`, `gui_cfg.get('separate_tool_calls_panel', False)` | `src/app_controller.py:2068-2070` | **UIPanelConfig** | +| `self.project.get('discussion', {}).get('discussions', {})` | `src/gui_2.py:5036,5046` | **DiscussionStore** | +| `path_info['logs_dir']['path']` | `src/app_controller.py:1984` | **PathInfo** (nested) | -The 3 SSDL techniques the user mentioned (redundant nil-checks, preemptive dependency resolution, no-op nil types) are **half the fix** — they reduce the AROUND the type-dispatch but not the type-dispatch itself. **The actual primary fix is type promotion:** +**There is no single "Metadata" shape.** The 107 `.get()` sites access ~12 distinct aggregates, each with its own field set. The original spec (commit `e50bebdd`) proposed a single `@dataclass(frozen=True, slots=True) Metadata` with ~200 fields merging all 12 aggregates into one polymorphic mega-struct. That is the wrong direction: -```python -# BEFORE (runtime type-dispatch per access): -entry.get('key', default_value) # 3 branches: is None, getattr, default -if 'key' in entry: ... # 1 branch -entry['key'] # 1 branch + potential KeyError +- It hides the type distinctions that direct field access is supposed to reveal. +- A consumer that has a `Ticket` can read `.source_tier` (a `CommsLogEntry` field) — silently get the empty default — and ship a bug that no type checker will catch. +- It is "less defined" than the current `dict[str, Any]`: today, reading `.source_tier` on a `Ticket` raises `AttributeError` immediately; after the mega-dataclass, it silently returns `""`. -# AFTER (direct field access): -entry.field_name # 0 branches -if entry.field_name is not None: ... # only if nullable -entry.field_name # direct, no KeyError -``` - -For 213 access sites × ~2 branches each = 426 branches reduced. The exponential `2^N` for the highest-branch-count functions drops by orders of magnitude. +The corrected design is **per-aggregate dataclasses**: each known concept gets its own typed dataclass with its own fields. `Metadata: TypeAlias = dict[str, Any]` is preserved for the **truly collapsed codepaths** where the shape is genuinely unknown (TOML project config, generic JSON parsing, polymorphic log dumping). ## Goals | ID | Goal | Acceptance | |---|---|---| -| G1 | Promote `Metadata` to `@dataclass(frozen=True)` with explicit fields | `git grep "^Metadata:" HEAD:src/type_aliases.py` shows `Metadata: TypeAlias = CommsLogEntry` (or similar — the dataclass), NOT `dict[str, Any]` | -| G2 | Migrate all 213 access sites (107 `.get` + 106 `['key']`) to direct field access | `git grep -E "\.get\('[a-z_]+'," HEAD -- 'src/*.py'` returns 0 hits in promoted files; `git grep -E "\[[ ]*'[a-z_]+'[ ]*\]" HEAD -- 'src/*.py'` returns only allowed-pattern hits | -| G3 | All 5 sub-aggregates share the same dataclass (per type_aliases.py chain) | `CommsLogEntry`, `HistoryMessage`, `FileItem`, `ToolDefinition`, `ToolCall` all point to the same `Metadata` dataclass | +| G1 | Each known sub-aggregate is its OWN `@dataclass(frozen=True, slots=True)` with its OWN fields (or reuses the existing typed dataclass where one already exists) | `git grep "^@dataclass\|^class .*dataclass" src/` shows `CommsLogEntry`, `HistoryMessage`, `RAGChunk`, `SessionInsights`, `DiscussionSettings`, `CustomSlice`, `MMAUsageStats`, `ProviderPayload`, `UIPanelConfig`, `DiscussionStore`, `ContextPreset` (full), `PathInfo`, `ToolDefinition` each as its own class; the existing `FileItem`, `ToolCall`, `Ticket`, `ChatMessage`, `UsageStats` are reused unchanged | +| G2 | `Metadata: TypeAlias = dict[str, Any]` is preserved as the catch-all for collapsed codepaths; NOT promoted to a shared mega-dataclass | `git grep "^Metadata:" src/type_aliases.py` shows `Metadata: TypeAlias = dict[str, Any]` (unchanged); the type is not a dataclass | +| G3 | Migrate the 107 `.get('key', ...)` + 106 `['key']` access sites on the KNOWN sub-aggregates to direct field access on the per-aggregate dataclass | `git grep -E "\.get\('[a-z_]+'," HEAD -- 'src/*.py'` returns only legitimate non-aggregate uses (e.g., `.get('mtime', 0)` on file paths, `.get('auto_start', False)` on config dicts); the per-aggregate sites are gone | | G4 | Effective codepaths drops by ≥ 2 orders of magnitude | `compute_effective_codepaths` returns `< 1e+20` (was 4.014e+22) | | G5 | All 7 audit gates pass `--strict` (no regression) | `weak_types`, `type_registry`, `main_thread_imports`, `no_models_config_io`, `code_path_audit_coverage`, `exception_handling`, `optional_in_3_files` all exit 0 | | G6 | All existing tests pass (10/11 batched tiers — RAG flake acceptable) | `scripts/run_tests_batched.py` → 10/11 PASS | -| G7 | New regression-guard tests for the dataclass | `tests/test_metadata_dataclass.py` with 10+ tests for: field access, immutable, `__post_init__` validation, `to_dict()` for backward-compat with JSON serialization | +| G7 | New regression-guard tests for each new per-aggregate dataclass | `tests/test_metadata_dataclass.py` is split into `tests/test_comms_log_entry.py`, `tests/test_history_message.py`, `tests/test_tool_definition.py`, `tests/test_rag_chunk.py`, `tests/test_session_insights.py`, etc.; each has 5+ tests for: constructor, field access, `to_dict()`/`from_dict()` round-trip, frozen, equality | +| G8 | `Metadata` (the catch-all dict) is used ONLY at the genuinely collapsed codepaths — never as a stand-in for a known sub-aggregate | Code review confirms: every `.get('key', default)` site has been classified as either (a) a known sub-aggregate → migrated to direct field access, or (b) a genuinely collapsed codepath (TOML project config, generic JSON parsing, polymorphic log dumping) → keeps `Metadata` | ## Non-Goals - Modifications to `src/code_path_audit*.py` (the audit infrastructure is correct; the migration is on the consumer side) -- The 4 NG1 + 7 NG2 audit violations (already addressed in phase 2 + dc397db7) -- The 4.01e22's nil-check component (per the post-mortem, this is a minor contributor; the type-dispatch is the dominant cause) -- The RAG test pre-existing flake (per `docs/reports/SSDL_CAMPAIGN_ABORTED_20260624.md` "Out of Scope") -- New `src/.py` files (per AGENTS.md hard rule; the dataclass goes in `src/type_aliases.py`) -- Polishing the 5 sub-aggregates with custom dataclasses each (overkill; one shared dataclass suffices) +- The 4 NG1 + 7 NG2 audit violations (already addressed in phase 2 + `dc397db7`) +- The 4.01e22's nil-check component (per the post-mortem at `docs/reports/SSDL_CAMPAIGN_ABORTED_20260624.md`, this is a minor contributor; the per-aggregate type-dispatch collapse is the dominant cause) +- The RAG test pre-existing flake (per the SSDL post-mortem "Out of Scope") +- New `src/.py` files (per AGENTS.md hard rule; new dataclasses go in `src/type_aliases.py` for type-system aggregates, or in the existing module for the aggregate — `models.FileItem` stays in `models.py`, `openai_schemas.ToolCall` stays in `openai_schemas.py`, etc.) +- Promoting `Metadata: TypeAlias = dict[str, Any]` to a shared mega-dataclass (this is the original spec's bad inference; rejected 2026-06-25) +- The collapsed-codepath sites (`self.project.get('paths', {})`, `self.project.get('conductor', {})`, etc.) — these read `manual_slop.toml` and the shape is genuinely unknown at type level; they keep `Metadata` as `dict[str, Any]` ## Functional Requirements -### FR1: Design the Metadata dataclass +### FR1: Per-aggregate dataclasses (not one mega-dataclass) -`Metadata` is a polymorphic dict shape used in 5 sub-aggregates: -- `CommsLogEntry` (app_controller's session log entries) -- `HistoryMessage` (ai_client's per-vendor history) -- `FileItem` (context composition's file items) -- `ToolDefinition` (mcp_client's tool schema) -- `ToolCall` (ai_client's tool call records) +Each known sub-aggregate becomes its OWN dataclass. The design follows the existing pattern at `src/openai_schemas.py` (`ToolCall`, `ChatMessage`, `UsageStats`, `ToolCallFunction`, `NormalizedResponse` — all separate frozen dataclasses with their own fields). -The distinct keys used across all 213 access sites are: -- **From `.get()`**: `ai, args, ast_elements, auto_start, burn_rate, call_count, comment, completed_tickets, conductor, content, context_presets, custom_slices, depends_on, description, dir, discussion, discussions, document, efficiency, files, ...` (107 keys total) -- **From `[]`**: `_toggle_command_palette, app_debug_info, args, blocked_reason, bloom, cache_creation_input_tokens, cache_read_input_tokens, command, comment, content, crt, delete_context_preset, depends_on, discussion, discussions, end_line, fps, frame_time_ms_avg, full_path, get_app_debug_info, ...` (106 keys total) +#### Existing dataclasses — REUSED UNCHANGED -After deduplication, the union has ~150-200 distinct keys. The dataclass will have all of them as `Optional[T]` fields (or `T` with a default for required ones). This is wider than ideal but: -- `@dataclass(frozen=True, slots=True)` keeps memory overhead low -- Direct attribute access (`entry.field_name`) compiles to a single C-level field read -- Removes ALL `dict.get()` and `dict['key']` runtime branches at the consumer level +| Class | Location | Fields | Consumers that need migration | +|---|---|---|---| +| `Ticket` | `src/models.py:302` | `id, description, target_symbols, context_requirements, depends_on, status, assigned_to, priority, target_file, blocked_reason, step_mode, retry_count, manual_block, model_override, persona_id` (15 fields) | `src/gui_2.py:1366-1438,1682,4810,4820,4868`; `src/conductor_tech_lead.py:125`; `src/app_controller.py:4810-4868` | +| `FileItem` | `src/models.py:533` | `path, auto_aggregate, force_full, view_mode, selected, ast_signatures, ast_definitions, ast_mask, custom_slices, injected_at` (10 fields) | `src/aggregate.py:418,421`; `src/ai_client.py:2565,2807,2898`; `src/app_controller.py:3508` | +| `ToolCall` | `src/openai_schemas.py:32` | `id, function (ToolCallFunction), type` (3 fields) | `src/mcp_client.py` (tool loop section) | +| `ChatMessage` | `src/openai_schemas.py:48` | `role, content, tool_calls, tool_call_id, name` (5 fields) | provider-side history (will replace the per-vendor `_X_history` aliases that were removed in `code_path_audit_phase_3_provider_state_20260624`) | +| `UsageStats` | `src/openai_schemas.py:68` | `input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens` (4 fields) | per-call token usage in `src/app_controller.py:2299-2309` | -```python -# src/type_aliases.py -from __future__ import annotations -from dataclasses import dataclass, field -from typing import Any, Callable, NamedTuple, Optional, TypeAlias +#### NEW dataclasses — to be added +| Class | Module | Fields | Consumers that need migration | +|---|---|---|---| +| `CommsLogEntry` | `src/type_aliases.py` | `ts, role, kind, direction, model, source_tier, content, error` (8 fields) | `src/app_controller.py:2277,2302,2310`; `src/session_logger.py`; `src/multi_agent_conductor.py` | +| `HistoryMessage` | `src/type_aliases.py` | `role, content, tool_calls, tool_call_id, name, ts` (6 fields) | UI-layer discussion history (the per-turn editable list, NOT the provider-side `ChatMessage` — these are distinct layers per `data_structure_strengthening_20260606` §3.1) | +| `ToolDefinition` | `src/type_aliases.py` | `name, description, parameters, auto_start` (4 fields) | `src/mcp_client.py:_build_anthropic_tools` and equivalent per-vendor tool builders | +| `RAGChunk` | `src/rag_engine.py` | `document, path, score, metadata` (4 fields) | `src/aggregate.py:3259`; `src/app_controller.py:251,4162` | +| `SessionInsights` | `src/type_aliases.py` | `total_tokens, call_count, burn_rate, session_cost, completed_tickets, efficiency` (6 fields) | `src/gui_2.py:4926-4931` | +| `DiscussionSettings` | `src/type_aliases.py` | `temperature, top_p, max_output_tokens` (3 fields) | `src/gui_2.py:3535` | +| `CustomSlice` | `src/type_aliases.py` | `tag, comment, start_line, end_line` (4 fields) | `src/gui_2.py:4048-4054,1301-1302` | +| `MMAUsageStats` | `src/type_aliases.py` | `model, input, output` (3 fields) | `src/gui_2.py:2199-2201,2216` | +| `ProviderPayload` | `src/type_aliases.py` | `script, args, output, source_tier` (4 fields) | `src/app_controller.py:2274,2287` | +| `UIPanelConfig` | `src/type_aliases.py` | `separate_message_panel, separate_response_panel, separate_tool_calls_panel` (3 fields) | `src/app_controller.py:2068-2070` | +| `PathInfo` | `src/type_aliases.py` | `logs_dir, scripts_dir, project_root` (3 fields, nested) | `src/app_controller.py:1984-1985` | +| `ContextPreset` | `src/models.py` (full schema) | `name, files (FileItems), screenshots (list[str])` (3 fields minimum) | `src/gui_2.py:4184-4185,4333,4448` | -@dataclass(frozen=True, slots=True) -class Metadata: - role: str = "" - content: Any = None - tool_calls: Any = None - tool_call_id: str = "" - name: str = "" - args: Any = None - source_tier: str = "main" - model: str = "unknown" - id: str = "" - ts: str = "" - role_: str = "" # For dicts that used 'role' as a key - description: str = "" - depends_on: tuple[str, ...] = () - status: str = "" - manual_block: bool = False - completed_tickets: int = 0 - auto_start: bool = False - command: str = "" - script: str = "" - output: Any = None - error: str = "" - tier: str = "" - path: str = "" - full_path: str = "" - filename: str = "" - mtime: float = 0.0 - size: int = 0 - # ... ~200 fields total, all Optional or with sensible defaults ... +#### Why per-aggregate dataclasses, not one shared mega-dataclass +- **Each aggregate has its own field set.** A `Ticket` has `depends_on: List[str]`, `manual_block: bool`. A `CommsLogEntry` has `source_tier: str`, `model: str`. A `RAGChunk` has `document: str`, `score: float`. They share NO common fields beyond `id`. There is no "common Metadata base" to extract. +- **A shared mega-dataclass defeats the type system.** A consumer that has a `Ticket` can read `.source_tier` (a `CommsLogEntry` field) — silently get the empty default — and ship a bug that no type checker will catch. Today, with `dict[str, Any]`, reading `.source_tier` on a `Ticket` raises `AttributeError` immediately. The mega-dataclass is **less defined** than the current state. +- **The original convention anticipated per-concept promotion.** Per `data_structure_strengthening_20260606` §3.3: *"Phase 2 can convert `Metadata` to a `TypedDict` (or split into per-concept `TypedDict`s) and the aliases continue to work without breaking changes. The aliases are STABLE NAMES; the underlying type can evolve."* The original 2026-06-06 design intent was per-concept promotion, NOT a mega-dataclass. The original 2026-06-25 metadata_promotion_20260624 spec reversed this direction; the corrected spec restores the original intent. -CommsLogEntry: TypeAlias = Metadata -CommsLog: TypeAlias = list[CommsLogEntry] -HistoryMessage: TypeAlias = Metadata -History: TypeAlias = list[HistoryMessage] -FileItem: TypeAlias = Metadata -FileItems: TypeAlias = list[FileItem] -ToolDefinition: TypeAlias = Metadata -ToolCall: TypeAlias = Metadata -CommsLogCallback: TypeAlias = Callable[[CommsLogEntry], None] -JsonPrimitive: TypeAlias = str | int | float | bool | None -JsonValue: TypeAlias = JsonPrimitive | list["JsonValue"] | dict[str, "JsonValue"] +### FR2: `Metadata` stays as the catch-all for collapsed codepaths +`Metadata: TypeAlias = dict[str, Any]` is preserved unchanged. It is used at sites where the shape is genuinely unknown at type level: -class FileItemsDiff(NamedTuple): - refreshed: FileItems - changed: FileItems -``` +- `manual_slop.toml` project config loading (`self.project.get('paths', {})`, `self.project.get('conductor', {})`, `self.project.get('context_presets', {})`, `self.project.get('discussion', {})`) — these are top-level TOML keys; the aggregator doesn't know which key it's about to read. +- Generic JSON parsing at the wire boundary (REST API payloads, WebSocket messages) — the body shape is defined by the producer, not the consumer. +- Polymorphic log dumping — a function that serializes a list of mixed-aggregate entries to JSON without caring about their individual types. -**Migration helper**: the dataclass also has a `to_dict()` method for JSON serialization (used by `CommsLog` writer, session restoration, etc.): +These sites keep `Metadata` and `.get('key', default)` because there is no per-aggregate type to promote to. The audit MUST classify every remaining `.get('key', default)` site as one of: (a) "promoted to per-aggregate dataclass → migrated" or (b) "collapsed codepath → keeps Metadata with documented justification in code comment or commit message." -```python -@dataclass(frozen=True, slots=True) -class Metadata: - ...fields... - def to_dict(self) -> dict[str, Any]: - return {k: v for k, v in asdict(self).items() if v is not None or k in _NON_NULL_KEYS} -``` +### FR3: Phase-by-phase migration (12+ sub-aggregates, 1 phase per aggregate) -### FR2: Phase-by-phase migration (5 sub-aggregates) - -The 695 consumer functions distribute across the 5 sub-aggregates. To minimize blast radius, migrate sub-aggregate by sub-aggregate: +The migration is per-aggregate: each aggregate gets its own phase. Phases are ordered to maximize early feedback: | Phase | Sub-aggregate | Est. consumers | Primary files | |---|---|---:|---| -| 1 | `CommsLogEntry` | ~150 | `app_controller.py`, `multi_agent_conductor.py`, `session_logger.py` | -| 2 | `HistoryMessage` | ~80 | `ai_client.py` (per-vendor history) | -| 3 | `FileItem` | ~200 | `aggregate.py`, `gui_2.py`, `app_controller.py` | -| 4 | `ToolDefinition` + `ToolCall` | ~150 | `mcp_client.py`, `ai_client.py` | -| 5 | Other (`Metadata` direct usage) | ~115 | `gui_2.py` (general), `models.py`, `paths.py`, etc. | +| 0 | Design the new dataclasses + add regression-guard test stubs | 0 (design only) | `src/type_aliases.py` (and the existing modules for in-place additions) | +| 1 | `Ticket` (already a dataclass; migrate consumers only) | ~30 sites | `src/gui_2.py`, `src/conductor_tech_lead.py`, `src/app_controller.py` | +| 2 | `FileItem` (already a dataclass; migrate consumers only) | ~10 sites | `src/aggregate.py`, `src/ai_client.py`, `src/app_controller.py` | +| 3 | `CommsLogEntry` (NEW dataclass + migrate consumers) | ~30 sites | `src/type_aliases.py`, `src/session_logger.py`, `src/multi_agent_conductor.py`, `src/app_controller.py` | +| 4 | `HistoryMessage` (NEW dataclass + migrate UI-layer consumers) | ~20 sites | `src/type_aliases.py`, `src/gui_2.py` | +| 5 | `ChatMessage` (already in `openai_schemas.py`; wire it into the per-vendor send paths) | ~27 sites | `src/ai_client.py` | +| 6 | `UsageStats` (already in `openai_schemas.py`; wire into the per-call usage aggregation) | ~10 sites | `src/app_controller.py` | +| 7 | `ToolCall` (already in `openai_schemas.py`; wire into the tool loop section) | ~56 sites | `src/ai_client.py`, `src/mcp_client.py` | +| 8 | `ToolDefinition` (NEW dataclass + migrate per-vendor tool builders) | ~94 sites | `src/type_aliases.py`, `src/mcp_client.py` | +| 9 | `RAGChunk` (NEW dataclass + migrate consumers) | ~5 sites | `src/rag_engine.py`, `src/aggregate.py`, `src/app_controller.py` | +| 10 | `SessionInsights`, `DiscussionSettings`, `CustomSlice`, `MMAUsageStats`, `ProviderPayload`, `UIPanelConfig`, `PathInfo`, `ContextPreset` (small aggregates, batched) | ~25 sites | `src/type_aliases.py`, `src/models.py`, `src/gui_2.py`, `src/app_controller.py` | +| 11 | `Metadata` collapsed-codepath audit + classification (per FR2) | ~80 sites | every `.get('key', default)` site that is NOT promoted to a per-aggregate dataclass | +| 12 | Verification + end-of-track (1 task, 3 commits) | 0 | terminal + `docs/reports/TRACK_COMPLETION_metadata_promotion_20260624.md` (NEW) | Each phase: -1. Add the new field to the `Metadata` dataclass (if not already present) -2. Update consumers in that sub-aggregate's primary files: `entry.get('key', default)` → `entry.key or default` (or similar) -3. Update consumers: `entry['key']` → `entry.key` -4. Add regression-guard tests for the migrated access pattern -5. Re-measure effective codepaths after the phase +1. For NEW dataclasses: define the dataclass in the appropriate module; add regression-guard test +2. For ALL phases: migrate the consumer sites from `.get('key', default)` → `.field_name` (or `.field_name or default` for nullable fields) +3. Per-phase regression-guard test runs +4. Re-measure effective codepaths after the phase -### FR3: Migration patterns (canonical) +### FR4: Migration patterns (canonical) ```python # BEFORE: @@ -182,43 +165,53 @@ role = entry['role'] if 'depends_on' in entry: deps = entry['depends_on'] -# AFTER (with Metadata dataclass): -x = entry.model or 'unknown' -y = entry.input_tokens or 0 -z = entry.source_tier or 'main' -if entry.manual_block: +# AFTER (with per-aggregate dataclass): +x = entry.model or 'unknown' # CommsLogEntry +y = entry.input_tokens or 0 # UsageStats +z = entry.source_tier or 'main' # CommsLogEntry +if entry.manual_block: # Ticket ... -role = entry.role -if entry.depends_on: +role = entry.role # HistoryMessage / CommsLogEntry +if entry.depends_on: # Ticket deps = entry.depends_on ``` The migration is mechanical but requires care: -- For `Optional[T]` fields: use `entry.field or default_value` +- For nullable fields: use `entry.field or default_value` - For required fields: use `entry.field` directly -- For polymorphic keys (some entries have the key, some don't): the dataclass default handles this (all fields have defaults) -- For `['key']` (subscript) where the key is dynamic: rare; keep as `dict[str, Any]` (e.g., `entry.to_dict()['dynamic_key']`) +- For polymorphic keys (some entries have the key, some don't): the dataclass default handles this (all fields have defaults; `frozen=True, slots=True` ensures immutability) +- For `['key']` (subscript) where the key is dynamic: rare; keep as `dict[str, Any]` access (e.g., `entry.to_dict()['dynamic_key']`) — but ONLY if the entry is genuinely a dict, not a dataclass -### FR4: Edge cases +### FR5: Edge cases -**Polymorphic constructors**: many sites do `entry = {'role': 'user', 'content': 'hi'}`. After migration: `entry = Metadata(role='user', content='hi')`. The dataclass has all the fields as `Optional` or with defaults, so this works. +**Polymorphic constructors**: many sites do `entry = {'role': 'user', 'content': 'hi'}`. After migration: `entry = HistoryMessage(role='user', content='hi')`. The dataclass has all the fields as `Optional` or with defaults, so this works. -**Dynamic dict construction**: `for k, v in raw.items(): entry[k] = v`. After migration: `entry = Metadata(**raw)`. The `**` syntax requires that all keys in `raw` are valid field names; if `raw` has unknown keys, this fails. Solution: use a `from_dict` classmethod that filters out unknown keys: +**Dynamic dict construction**: `for k, v in raw.items(): entry[k] = v`. After migration: `entry = HistoryMessage(**raw)`. The `**` syntax requires that all keys in `raw` are valid field names; if `raw` has unknown keys, this fails. Solution: use a `from_dict` classmethod that filters out unknown keys (the canonical pattern, already used by `models.FileItem.from_dict` at `src/models.py:600-619` and `openai_schemas.NormalizedResponse.from_dict`): ```python @classmethod -def from_dict(cls, raw: dict[str, Any]) -> 'Metadata': +def from_dict(cls, raw: dict[str, Any]) -> 'HistoryMessage': valid_fields = {f.name for f in fields(cls)} return cls(**{k: v for k, v in raw.items() if k in valid_fields}) ``` -**JSON serialization**: `json.dumps(entry)` fails on dataclass. Solution: `json.dumps(entry.to_dict())`. +**JSON serialization**: `json.dumps(entry)` fails on dataclass. Solution: `json.dumps(entry.to_dict())` (per the canonical `to_dict()` pattern at `src/models.py:567-579` and `src/openai_schemas.py:36-43`). **Pickle**: `pickle.dumps(entry)` works (dataclass supports pickle natively via `__reduce__`). **Equality**: `entry1 == entry2` now works (dataclass generates `__eq__`); before it was `False` for distinct dict instances even with the same content. -### FR5: Re-measurement +**JSON round-trip preservation**: every dataclass in this track has a paired `to_dict()` + `from_dict()` (no information loss). This is enforced by the per-dataclass regression-guard test. + +### FR6: `Metadata` collapsed-codepath classification (per FR2) + +For every remaining `.get('key', default)` site after all phases: + +1. The site is classified as either (a) "promoted to per-aggregate dataclass" (migrated) or (b) "collapsed codepath" (keeps `Metadata`). +2. For (b), the justification is documented in the commit message (one line: "this site reads `manual_slop.toml`; the shape is unknown until the TOML is parsed"). +3. The audit `scripts/audit_weak_types.py --strict` continues to flag anonymous dict accesses; the gate is the per-aggregate dataclass promotion, NOT the elimination of all `.get()`. + +### FR7: Re-measurement After each phase, re-measure: @@ -237,7 +230,7 @@ print(f'Consumers: {len(metadata_consumers)}') " ``` -Expected: drops from 4.014e+22 to < 1e+20 after Phase 1 (just CommsLogEntry); further drops after each subsequent phase. +Expected: drops from 4.014e+22 to < 1e+20 after the aggregate-promotion phases (each phase drops it further as more consumers migrate to direct field access). ## Non-Functional Requirements @@ -247,64 +240,72 @@ Expected: drops from 4.014e+22 to < 1e+20 after Phase 1 (just CommsLogEntry); fu - NFR4: Per-task atomic commits with git notes - NFR5: No new pip dependencies (dataclass is stdlib) - NFR6: `Result[T]` returns for fallible fns (per `error_handling.md`) -- NFR7: No new `src/.py` files (per AGENTS.md hard rule; the dataclass goes in `src/type_aliases.py`) +- NFR7: No new `src/.py` files (per AGENTS.md hard rule; new type-system aggregates go in `src/type_aliases.py`, in-module aggregates stay in their parent module) ## Architecture Reference -- `conductor/code_styleguides/data_oriented_design.md` — the "Prefer Fewer Types" principle (the canonical rationale) +- `conductor/code_styleguides/data_oriented_design.md` — the canonical DOD reference ("Prefer Fewer Types" — but the types are still distinct) - `conductor/code_styleguides/error_handling.md` — the `Result[T]` convention -- `conductor/code_styleguides/type_aliases.md` — the 10 TypeAliases convention (per the data_structure_strengthening_20260606 track) -- `src/type_aliases.py` — the current Metadata definition (line 5) +- `conductor/code_styleguides/type_aliases.md` — the alias convention (preserved; `Metadata: dict[str, Any]` stays as the catch-all) +- `src/openai_schemas.py` — the canonical per-aggregate dataclass pattern (`ToolCall`, `ChatMessage`, `UsageStats`); the reference implementation for the NEW dataclasses in this track +- `src/models.py:533` — `FileItem` (the canonical in-module dataclass pattern with `to_dict()` / `from_dict()` round-trip) +- `src/models.py:302` — `Ticket` (the canonical dataclass with `get()` legacy-compat method, used during migration) +- `docs/reports/SSDL_CAMPAIGN_ABORTED_20260624.md` — the post-mortem: the 4.01e22 is from type-dispatch, not nil-checks; the fix is type promotion +- `docs/reports/PLANNING_CORRECTION_metadata_promotion_20260625.md` — the corrected-design rationale (this track's correction) +- `conductor/tracks/any_type_componentization_20260621/spec.md` — the grandparent track (89 sites promoted to dataclasses across 5 candidates); the per-aggregate pattern this track follows +- `conductor/tracks/data_structure_strengthening_20260606/spec.md` §3.3 — the original 2026-06-06 design intent: *"Phase 2 can convert `Metadata` to a `TypedDict` (or split into per-concept `TypedDict`s) and the aliases continue to work without breaking changes. The aliases are STABLE NAMES; the underlying type can evolve."* - `scripts/code_path_audit/code_path_audit.py` — the consumer detection (3-pass AST) - `scripts/code_path_audit/code_path_audit_ssdl.py` — the effective codepaths metric -- `docs/reports/SSDL_CAMPAIGN_ABORTED_20260624.md` — the post-mortem explaining why this is a type-dispatch problem, not a nil-check problem -- `conductor/tracks/any_type_componentization_20260621/plan.md` — the grandparent track (48/89 sites promoted, then reverted at `751b94d4`) -- `conductor/tracks/code_path_audit_20260607/spec_v2.md` — the audit that established the 4.01e22 baseline -- `docs/reports/code_path_audit/2026-06-22/AUDIT_REPORT.md` — the original 6797-line audit report ## Out of Scope - Modifications to `src/code_path_audit*.py` (the audit infrastructure is correct) -- The 4 NG1 + 7 NG2 audit violations (already addressed) +- The 4 NG1 + 7 NG2 audit violations (already addressed in `dc397db7`) +- The 4.01e22's nil-check component (per SSDL post-mortem; minor contributor) - The RAG test pre-existing flake (per SSDL post-mortem) -- The 5 sub-aggregates (`CommsLogEntry`, `HistoryMessage`, `FileItem`, `ToolDefinition`, `ToolCall`) becoming separate dataclasses (overkill; they share the same `Metadata` base) - New `src/.py` files (per AGENTS.md hard rule) -- Backward-compat support for `dict[str, Any]` (the migration is a hard break; any code that does `Metadata(...).__class__ is dict` will break, but no such code exists per the audit) +- A shared mega-dataclass across the 5+ sub-aggregates (the original spec's bad inference; rejected 2026-06-25) +- Promoting `Metadata: TypeAlias = dict[str, Any]` itself to a dataclass (it's the catch-all for collapsed codepaths; not a known sub-aggregate) +- Migration of the collapsed-codepath sites (`self.project.get('paths', {})`, etc.) — these read `manual_slop.toml`; the shape is genuinely unknown +- Pydantic migration (the canonical pattern in this codebase is stdlib `@dataclass(frozen=True, slots=True)`; Pydantic is for input validation, not for the data structures used internally) ## Verification Criteria (Definition of Done) | # | Criterion | Verification command | |---|---|---| -| VC1 | `Metadata` is a `@dataclass(frozen=True, slots=True)`, not `dict[str, Any]` | `git show HEAD:src/type_aliases.py \| head -10` shows `@dataclass(frozen=True, slots=True) class Metadata:` | -| VC2 | All 107 `.get('key', ...)` sites on Metadata consumers replaced | `git grep -E "\.get\('[a-z_]+'," HEAD -- 'src/*.py' \| wc -l` returns 0 (or only legitimate non-Metadata uses like `.get('mtime', 0)` on file paths) | -| VC3 | All 106 `['key']` subscript sites on Metadata consumers replaced | `git grep -E "\[[ ]*'[a-z_]+'[ ]*\]" HEAD -- 'src/*.py' \| wc -l` returns 0 (or only legitimate non-Metadata uses) | -| VC4 | `Metadata(...)` constructor works for all common patterns | `tests/test_metadata_dataclass.py` passes 10+ tests (constructor, field access, `to_dict()`, `from_dict()`, frozen, slots, equality) | -| VC5 | All 5 sub-aggregate TypeAliases point to the new `Metadata` | `git grep "TypeAlias = " HEAD:src/type_aliases.py` shows `CommsLogEntry: TypeAlias = Metadata` etc. | -| VC6 | Effective codepaths drops by ≥ 2 orders of magnitude | `compute_effective_codepaths` returns `< 1e+20` (was 4.014e+22) | -| VC7 | All 7 audit gates pass `--strict` (no regression) | `weak_types` 102 ≤ 112; `type_registry` 22 files; `main_thread_imports` 17; `no_models_config_io` 0; `code_path_audit_coverage` 0; `exception_handling` 0; `optional_in_3_files` 0 | -| VC8 | 10/11 batched test tiers PASS (RAG flake acceptable) | `scripts/run_tests_batched.py` → 10/11 | -| VC9 | End-of-track report written | `docs/reports/TRACK_COMPLETION_metadata_promotion_20260624.md` exists with the new effective-codepaths number | -| VC10 | New regression-guard test file | `tests/test_metadata_dataclass.py` exists with 10+ tests passing | +| VC1 | `Metadata: TypeAlias = dict[str, Any]` is UNCHANGED in `src/type_aliases.py` | `git grep "^Metadata:" src/type_aliases.py` shows `Metadata: TypeAlias = dict[str, Any]` | +| VC2 | Each new sub-aggregate is its OWN `@dataclass(frozen=True, slots=True)` in the appropriate module | `git grep -A 2 "^class CommsLogEntry\|^class HistoryMessage\|^class ToolDefinition\|^class RAGChunk\|^class SessionInsights\|^class DiscussionSettings\|^class CustomSlice\|^class MMAUsageStats\|^class ProviderPayload\|^class UIPanelConfig\|^class PathInfo" src/` shows each as a separate frozen dataclass | +| VC3 | Existing per-aggregate dataclasses (`Ticket`, `FileItem`, `ToolCall`, `ChatMessage`, `UsageStats`) are REUSED unchanged | `git grep "class Ticket\|class FileItem\|class ToolCall\|class ChatMessage\|class UsageStats" src/` shows the existing classes; consumers migrate to direct field access on them | +| VC4 | All 107 `.get('key', ...)` access sites on KNOWN sub-aggregates replaced | `git grep -E "\.get\('[a-z_]+'," HEAD -- 'src/*.py'` returns only the FR2 collapsed-codepath sites (documented in the per-site classification) | +| VC5 | All 106 `['key']` subscript access sites on KNOWN sub-aggregates replaced | `git grep -E "\[[ ]*'[a-z_]+'[ ]*\]" HEAD -- 'src/*.py'` returns only legitimate non-aggregate uses | +| VC6 | Per-aggregate regression-guard tests exist and pass | `uv run pytest tests/test_comms_log_entry.py tests/test_history_message.py tests/test_tool_definition.py tests/test_rag_chunk.py tests/test_session_insights.py -v` → all pass (5+ tests per file) | +| VC7 | Effective codepaths drops by ≥ 2 orders of magnitude | `compute_effective_codepaths` returns `< 1e+20` (was 4.014e+22) | +| VC8 | All 7 audit gates pass `--strict` (no regression) | `weak_types` ≤ 112; `type_registry` 22 files; `main_thread_imports` 17; `no_models_config_io` 0; `code_path_audit_coverage` 0; `exception_handling` 0; `optional_in_3_files` 0 | +| VC9 | 10/11 batched test tiers PASS (RAG flake acceptable) | `scripts/run_tests_batched.py` → 10/11 | +| VC10 | End-of-track report written | `docs/reports/TRACK_COMPLETION_metadata_promotion_20260624.md` exists with the new effective-codepaths number and the per-aggregate classification of the remaining `.get()` sites | ## Risks | # | Risk | Likelihood | Mitigation | |---|---|---|---| -| R1 | The 213 access sites have polymorphic keys that don't fit cleanly into a single dataclass | medium | Use `Optional[T]` for all fields; use `from_dict` classmethod that filters unknown keys; use `to_dict()` for JSON serialization | -| R2 | Some sites do `entry['key']` where `key` is dynamic (e.g., `entry[variable_name]`) | low | These are rare; keep as `dict[str, Any]` access for dynamic keys; the static field access handles the common case | -| R3 | The `to_dict()` round-trip loses information (e.g., nested dicts) | low | Implement `to_dict()` carefully; nested dicts pass through as `dict[str, Any]` (not recursively converted) | -| R4 | Some sites mutate `entry` (e.g., `entry['key'] = value`); dataclass is frozen | medium | These sites are rare; audit them; if found, replace with `dataclasses.replace(entry, field_name=value)` | -| R5 | Migration breaks the regression-guard tests for `test_provider_state_migration.py` (post-phase 3) | low | The migration is on Metadata, not provider_state; orthogonal changes; per-phase regression-guard test runs | -| R6 | The 695 consumer functions are too many for one track | high | Break into 5 phases (FR2); each phase is a sub-aggregate; the dataclass is added once and all 5 sub-aggregates reuse it | -| R7 | The dict-shape is used for JSON-serialized payloads (e.g., comms.log); the dataclass breaks the JSON layer | medium | The dataclass has `to_dict()` + `from_dict()`; the JSON layer converts via these. Verify the comms.log reader/writer (session_logger.py) uses these methods | +| R1 | Some sub-aggregate has fields that don't fit cleanly into a frozen dataclass (e.g., mutability needed) | low | The canonical reference is `src/openai_schemas.py`; all 5 existing dataclasses there are `frozen=True`. If a field needs mutability, refactor to use `dataclasses.replace()` instead of mutating in place | +| R2 | Some sites mutate `entry` (e.g., `entry['key'] = value`); dataclass is frozen | medium | Audit these sites; if found, replace with `dataclasses.replace(entry, field_name=value)` | +| R3 | The dynamic-key subscript sites (`entry[variable_name]`) are not covered by direct field access | low | These sites are rare and already classified as collapsed-codepath per FR2; keep them as `entry.to_dict()[var_name]` if the entry is a dataclass, or `entry[var_name]` if the entry is a dict | +| R4 | `to_dict()` round-trip loses information for nested dicts (e.g., `custom_slices: list[dict]` in `FileItem`) | low | `FileItem.to_dict()` already handles this (passes nested dicts through as `dict[str, Any]`); mirror the pattern in the new dataclasses | +| R5 | The 695 consumer functions are too many for one track | high | The track is broken into 12 phases (FR3); each phase is independent and per-aggregate; the per-phase regression-guard test catches regressions early | +| R6 | A collapsed-codepath site is misclassified as a known sub-aggregate (or vice versa) | medium | The FR6 classification is auditable: every remaining `.get()` site is either (a) "promoted" or (b) "collapsed with documented justification"; the audit `--strict` gate catches drift | +| R7 | The dataclass names collide with existing names (e.g., `Metadata` exists in both `src/type_aliases.py` and `src/models.py`) | medium | Use module-qualified imports: `from src.type_aliases import Metadata` for the dict alias; `from src.models import Metadata` for the small dataclass. Document the collision in the per-aggregate test file | ## See also -- `docs/reports/SSDL_CAMPAIGN_ABORTED_20260624.md` — the post-mortem explaining why this is a type-dispatch problem -- `conductor/tracks/any_type_componentization_20260621/plan.md` — the grandparent plan +- `docs/reports/SSDL_CAMPAIGN_ABORTED_20260624.md` — the post-mortem: type promotion fixes the 4.01e22, not nil-checks +- `docs/reports/PLANNING_CORRECTION_metadata_promotion_20260625.md` — the corrected-design rationale +- `conductor/code_styleguides/type_aliases.md` — the alias convention (preserved; `Metadata: dict[str, Any]` stays as the catch-all) +- `conductor/code_styleguides/data_oriented_design.md` — the canonical DOD reference +- `conductor/tracks/any_type_componentization_20260621/spec.md` — the grandparent track (89 sites already promoted to dataclasses) +- `conductor/tracks/data_structure_strengthening_20260606/spec.md` §3.3 — the original 2026-06-06 design intent: per-concept promotion +- `src/openai_schemas.py` — the canonical per-aggregate dataclass pattern +- `src/models.py:533` — `FileItem` (canonical in-module dataclass with `to_dict()` / `from_dict()`) +- `src/models.py:302` — `Ticket` (canonical dataclass with legacy `get()` compat) - `conductor/tracks/code_path_audit_20260607/spec_v2.md` — the audit that established the 4.01e22 baseline -- `docs/reports/code_path_audit/2026-06-22/AUDIT_REPORT.md` — the original 6797-line audit report -- `src/type_aliases.py` — the current Metadata definition -- `scripts/code_path_audit/code_path_audit.py` — the consumer detection -- `scripts/code_path_audit/code_path_audit_ssdl.py` — the effective codepaths metric -- `conductor/code_styleguides/data_oriented_design.md` — the "Prefer Fewer Types" principle +- `docs/reports/code_path_audit/2026-06-22/AUDIT_REPORT.md` — the original 6797-line audit report \ No newline at end of file