# Plan: metadata_promotion_20260624 (EXHAUSTIVE TIER 3 EXECUTION CONTRACT) > **Tier 1 exhaustive plan — 2026-06-25.** This plan is the EXECUTABLE CONTRACT for Tier 2/Tier 3. Tier 2 reviews per phase; Tier 3 executes per task. **No decisions remain for Tier 2/3 to make** — every task has exact file:line refs, exact before/after code, exact test commands, and explicit rollback steps. If a Tier 3 encounters an unanticipated situation, they STOP and report to Tier 2 — do NOT improvise. > > **Scope summary:** 12 phases (one per aggregate + collapsed-codepath audit + verification). 30 atomic tasks. ~36 atomic commits. Estimated 1 module-extensions to existing files + 12 new dataclasses added + 12 new test files (60+ tests) + 9 consumer files migrated (~213 access sites) + 1 styleguide clarification + 2 docs reports. NO day estimates. > > **Acceptance:** `compute_effective_codepaths` returns `< 1e+20` (was 4.014e+22); 7 audit gates pass `--strict`; 10/11 batched test tiers PASS; new per-aggregate regression-guard tests pass. ## 0. Pre-flight (Tier 2 runs before Tier 3 starts) These commands establish the baseline. Tier 2 captures the output and saves it as `docs/reports/metadata_promotion_baseline_.txt`. Tier 3 refers to this baseline throughout. ```bash # 0.1 Confirm the working tree is clean git status --short # Expect: no output (clean) # 0.2 Confirm the spec+plan+metadata.json are committed git log --oneline -1 -- conductor/tracks/metadata_promotion_20260624/ # Expect: 5 commits from the correction (spec + plan + metadata + styleguide + correction report) # 0.3 Measure the baseline effective codepaths uv run python -c " import sys sys.path.insert(0, 'scripts/code_path_audit') sys.path.insert(0, 'src') from code_path_audit import build_pcg from code_path_audit_ssdl import count_branches_in_function pcg = build_pcg('src').data metadata_consumers = pcg.consumers.get('Metadata', []) total = sum(2 ** count_branches_in_function(f, 'src') for f in metadata_consumers) print(f'Baseline effective codepaths: {total:.3e}') print(f'Metadata consumers: {len(metadata_consumers)}') " # Expect: 4.014e+22 ; 695 consumers # 0.4 Confirm all 7 audit gates pass --strict (or note which are pre-existing failures) uv run python scripts/audit_weak_types.py --strict uv run python scripts/generate_type_registry.py --check uv run python scripts/audit_main_thread_imports.py uv run python scripts/audit_no_models_config_io.py uv run python scripts/audit_code_path_audit_coverage.py --input-dir docs/reports/code_path_audit/latest --strict uv run python scripts/audit_exception_handling.py --strict uv run python scripts/audit_optional_in_3_files.py --strict # Expect: all exit 0; note any failures as "pre-existing, not introduced by this track" # 0.5 Confirm the baseline test suite is green (10/11 acceptable; RAG flake documented) uv run python scripts/run_tests_batched.py # Expect: 10/11 PASS # 0.6 Capture baseline counts git grep -nE "\.get\('[a-z_]+'," -- 'src/*.py' | wc -l # Expect: 107 git grep -nE "\[[ ]*'[a-z_]+'[ ]*\]" -- 'src/*.py' | wc -l # Expect: 106 ``` If the baseline differs from these expected values, Tier 2 STOPS and reports. Do NOT proceed with a different baseline — the plan assumes these counts. ## Phase 0: Add NEW per-aggregate dataclasses (no consumer migration yet) **Focus:** Add the 11 NEW dataclasses (in their parent modules — per AGENTS.md "no new src/.py files" rule). No consumer migration in this phase. The existing dataclasses (`Ticket`, `FileItem`, `ToolCall`, `ChatMessage`, `UsageStats`, `ContextPreset`, `MCPServerConfig`) are REUSED UNCHANGED. **Acceptance:** `from src.type_aliases import CommsLogEntry, HistoryMessage, ToolDefinition, SessionInsights, DiscussionSettings, CustomSlice, MMAUsageStats, ProviderPayload, UIPanelConfig, PathInfo` works; `from src.rag_engine import RAGChunk` works; constructors work with kwargs; `to_dict()` / `from_dict()` round-trip is lossless. ### Task 0.1: Add 10 NEW dataclasses to `src/type_aliases.py` **WHERE:** `src/type_aliases.py` (current 30 lines; will grow to ~200 lines) **HOW:** `manual-slop_edit_file` with surgical old_string/new_string; OR `write_file` to overwrite (preferred for this scale of change) **Exact `new_string` for the file** (replaces all current content): ```python from __future__ import annotations from dataclasses import dataclass, field from typing import Any, Callable, NamedTuple, TypeAlias Metadata: TypeAlias = dict[str, Any] @dataclass(frozen=True, slots=True) class CommsLogEntry: ts: str = "" role: str = "" kind: str = "" direction: str = "" model: str = "unknown" source_tier: str = "main" content: Any = None error: str = "" def to_dict(self) -> Metadata: return {k: v for k, v in self.__dict__.items() if v not in (None, "", [], {}, 0, 0.0, False) or k in ("model",)} @classmethod def from_dict(cls, raw: Metadata) -> "CommsLogEntry": valid = {f.name for f in field(cls) for _ in [None]} # noqa return cls(**{k: v for k, v in raw.items() if k in {f.name for f in (__import__("dataclasses").fields(cls))}}) @dataclass(frozen=True, slots=True) class HistoryMessage: role: str = "" content: Any = None tool_calls: Any = None tool_call_id: str = "" name: str = "" ts: str = "" def to_dict(self) -> Metadata: return {k: v for k, v in self.__dict__.items() if v not in (None, "", [], {}, 0, 0.0, False)} @classmethod def from_dict(cls, raw: Metadata) -> "HistoryMessage": return cls(**{k: v for k, v in raw.items() if k in {f.name for f in (__import__("dataclasses").fields(cls))}}) @dataclass(frozen=True, slots=True) class ToolDefinition: name: str = "" description: str = "" parameters: Any = None auto_start: bool = False def to_dict(self) -> Metadata: return {k: v for k, v in self.__dict__.items() if v not in (None, "", [], {}, 0, 0.0, False)} @classmethod def from_dict(cls, raw: Metadata) -> "ToolDefinition": return cls(**{k: v for k, v in raw.items() if k in {f.name for f in (__import__("dataclasses").fields(cls))}}) @dataclass(frozen=True, slots=True) class SessionInsights: total_tokens: int = 0 call_count: int = 0 burn_rate: float = 0.0 session_cost: float = 0.0 completed_tickets: int = 0 efficiency: float = 0.0 def to_dict(self) -> Metadata: return dict(self.__dict__) @classmethod def from_dict(cls, raw: Metadata) -> "SessionInsights": return cls(**{k: v for k, v in raw.items() if k in {f.name for f in (__import__("dataclasses").fields(cls))}}) @dataclass(frozen=True, slots=True) class DiscussionSettings: temperature: float = 0.7 top_p: float = 1.0 max_output_tokens: int = 0 def to_dict(self) -> Metadata: return dict(self.__dict__) @classmethod def from_dict(cls, raw: Metadata) -> "DiscussionSettings": return cls(**{k: v for k, v in raw.items() if k in {f.name for f in (__import__("dataclasses").fields(cls))}}) @dataclass(frozen=True, slots=True) class CustomSlice: tag: str = "" comment: str = "" start_line: int = 0 end_line: int = 0 def to_dict(self) -> Metadata: return dict(self.__dict__) @classmethod def from_dict(cls, raw: Metadata) -> "CustomSlice": return cls(**{k: v for k, v in raw.items() if k in {f.name for f in (__import__("dataclasses").fields(cls))}}) @dataclass(frozen=True, slots=True) class MMAUsageStats: model: str = "unknown" input: int = 0 output: int = 0 def to_dict(self) -> Metadata: return {k: v for k, v in self.__dict__.items() if v not in (None, "", [], {}, 0, 0.0, False)} @classmethod def from_dict(cls, raw: Metadata) -> "MMAUsageStats": return cls(**{k: v for k, v in raw.items() if k in {f.name for f in (__import__("dataclasses").fields(cls))}}) @dataclass(frozen=True, slots=True) class ProviderPayload: script: str = "" args: Metadata = field(default_factory=dict) output: str = "" source_tier: str = "main" def to_dict(self) -> Metadata: return {k: v for k, v in self.__dict__.items() if v not in (None, "", [], {}, 0, 0.0, False)} @classmethod def from_dict(cls, raw: Metadata) -> "ProviderPayload": return cls(**{k: v for k, v in raw.items() if k in {f.name for f in (__import__("dataclasses").fields(cls))}}) @dataclass(frozen=True, slots=True) class UIPanelConfig: separate_message_panel: bool = False separate_response_panel: bool = False separate_tool_calls_panel: bool = False def to_dict(self) -> Metadata: return dict(self.__dict__) @classmethod def from_dict(cls, raw: Metadata) -> "UIPanelConfig": return cls(**{k: v for k, v in raw.items() if k in {f.name for f in (__import__("dataclasses").fields(cls))}}) @dataclass(frozen=True, slots=True) class PathInfo: logs_dir: str = "" scripts_dir: str = "" project_root: str = "" def to_dict(self) -> Metadata: return dict(self.__dict__) @classmethod def from_dict(cls, raw: Metadata) -> "PathInfo": return cls(**{k: v for k, v in raw.items() if k in {f.name for f in (__import__("dataclasses").fields(cls))}}) FileItem: TypeAlias = "models.FileItem" ToolCall: TypeAlias = "openai_schemas.ToolCall" ChatMessage: TypeAlias = "openai_schemas.ChatMessage" CommsLog: TypeAlias = list[CommsLogEntry] History: TypeAlias = list[HistoryMessage] FileItems: TypeAlias = list[FileItem] CommsLogCallback: TypeAlias = Callable[[CommsLogEntry], None] JsonPrimitive: TypeAlias = str | int | float | bool | None JsonValue: TypeAlias = JsonPrimitive | list["JsonValue"] | dict[str, "JsonValue"] class FileItemsDiff(NamedTuple): refreshed: FileItems changed: FileItems ``` **Wait** — the `from_dict` classmethods above use `__import__("dataclasses").fields(cls)` which is ugly. Use the cleaner pattern (already used at `src/models.py:600` and `src/openai_schemas.py:36-43`): **Final `new_string`** (the version Tier 3 actually writes): ```python from __future__ import annotations from dataclasses import dataclass, field, fields from typing import Any, Callable, NamedTuple, TypeAlias Metadata: TypeAlias = dict[str, Any] def _filter_known(raw: dict[str, Any], cls: type) -> dict[str, Any]: valid = {f.name for f in fields(cls)} return {k: v for k, v in raw.items() if k in valid} @dataclass(frozen=True, slots=True) class CommsLogEntry: ts: str = "" role: str = "" kind: str = "" direction: str = "" model: str = "unknown" source_tier: str = "main" content: Any = None error: str = "" def to_dict(self) -> Metadata: out: Metadata = {} for k, v in self.__dict__.items(): if k == "model" or v not in (None, "", [], {}, 0, 0.0, False): out[k] = v return out @classmethod def from_dict(cls, raw: Metadata) -> "CommsLogEntry": return cls(**_filter_known(raw, cls)) @dataclass(frozen=True, slots=True) class HistoryMessage: role: str = "" content: Any = None tool_calls: Any = None tool_call_id: str = "" name: str = "" ts: str = "" def to_dict(self) -> Metadata: out: Metadata = {} for k, v in self.__dict__.items(): if v not in (None, "", [], {}, 0, 0.0, False): out[k] = v return out @classmethod def from_dict(cls, raw: Metadata) -> "HistoryMessage": return cls(**_filter_known(raw, cls)) @dataclass(frozen=True, slots=True) class ToolDefinition: name: str = "" description: str = "" parameters: Any = None auto_start: bool = False def to_dict(self) -> Metadata: out: Metadata = {} for k, v in self.__dict__.items(): if v not in (None, "", [], {}, 0, 0.0, False): out[k] = v return out @classmethod def from_dict(cls, raw: Metadata) -> "ToolDefinition": return cls(**_filter_known(raw, cls)) @dataclass(frozen=True, slots=True) class SessionInsights: total_tokens: int = 0 call_count: int = 0 burn_rate: float = 0.0 session_cost: float = 0.0 completed_tickets: int = 0 efficiency: float = 0.0 def to_dict(self) -> Metadata: return dict(self.__dict__) @classmethod def from_dict(cls, raw: Metadata) -> "SessionInsights": return cls(**_filter_known(raw, cls)) @dataclass(frozen=True, slots=True) class DiscussionSettings: temperature: float = 0.7 top_p: float = 1.0 max_output_tokens: int = 0 def to_dict(self) -> Metadata: return dict(self.__dict__) @classmethod def from_dict(cls, raw: Metadata) -> "DiscussionSettings": return cls(**_filter_known(raw, cls)) @dataclass(frozen=True, slots=True) class CustomSlice: tag: str = "" comment: str = "" start_line: int = 0 end_line: int = 0 def to_dict(self) -> Metadata: return dict(self.__dict__) @classmethod def from_dict(cls, raw: Metadata) -> "CustomSlice": return cls(**_filter_known(raw, cls)) @dataclass(frozen=True, slots=True) class MMAUsageStats: model: str = "unknown" input: int = 0 output: int = 0 def to_dict(self) -> Metadata: out: Metadata = {} for k, v in self.__dict__.items(): if v not in (None, "", [], {}, 0, 0.0, False): out[k] = v return out @classmethod def from_dict(cls, raw: Metadata) -> "MMAUsageStats": return cls(**_filter_known(raw, cls)) @dataclass(frozen=True, slots=True) class ProviderPayload: script: str = "" args: Metadata = field(default_factory=dict) output: str = "" source_tier: str = "main" def to_dict(self) -> Metadata: out: Metadata = {} for k, v in self.__dict__.items(): if v not in (None, "", [], {}, 0, 0.0, False): out[k] = v return out @classmethod def from_dict(cls, raw: Metadata) -> "ProviderPayload": return cls(**_filter_known(raw, cls)) @dataclass(frozen=True, slots=True) class UIPanelConfig: separate_message_panel: bool = False separate_response_panel: bool = False separate_tool_calls_panel: bool = False def to_dict(self) -> Metadata: return dict(self.__dict__) @classmethod def from_dict(cls, raw: Metadata) -> "UIPanelConfig": return cls(**_filter_known(raw, cls)) @dataclass(frozen=True, slots=True) class PathInfo: logs_dir: str = "" scripts_dir: str = "" project_root: str = "" def to_dict(self) -> Metadata: return dict(self.__dict__) @classmethod def from_dict(cls, raw: Metadata) -> "PathInfo": return cls(**_filter_known(raw, cls)) FileItem: TypeAlias = "models.FileItem" ToolCall: TypeAlias = "openai_schemas.ToolCall" ChatMessage: TypeAlias = "openai_schemas.ChatMessage" CommsLog: TypeAlias = list[CommsLogEntry] History: TypeAlias = list[HistoryMessage] FileItems: TypeAlias = list[FileItem] CommsLogCallback: TypeAlias = Callable[[CommsLogEntry], None] JsonPrimitive: TypeAlias = str | int | float | bool | None JsonValue: TypeAlias = JsonPrimitive | list["JsonValue"] | dict[str, "JsonValue"] class FileItemsDiff(NamedTuple): refreshed: FileItems changed: FileItems ``` **SAFETY (run after the edit):** ```bash uv run python -c "from src.type_aliases import CommsLogEntry, HistoryMessage, ToolDefinition, SessionInsights, DiscussionSettings, CustomSlice, MMAUsageStats, ProviderPayload, UIPanelConfig, PathInfo; print('OK')" # Expect: OK uv run python -c "from src.type_aliases import CommsLogEntry; e = CommsLogEntry(role='user', ts='2025-01-01'); print(e.role, e.ts, e.model)" # Expect: user 2025-01-01 unknown uv run python -c "from src.type_aliases import CommsLogEntry; e = CommsLogEntry.from_dict({'role': 'user', 'ts': '2025-01-01', 'unknown_field': 'x'}); print(e.role, e.ts)" # Expect: user 2025-01-01 (unknown_field filtered) uv run python -c "from src.type_aliases import CommsLogEntry; e = CommsLogEntry(role='user'); d = e.to_dict(); print(sorted(d.items()))" # Expect: [('model', 'unknown'), ('role', 'user')] uv run python -c "from src.type_aliases import Metadata; print(type(Metadata))" # Expect: # it's a TypeAlias, so the str representation ``` **COMMIT:** `refactor(type_aliases): add 10 per-aggregate dataclasses (CommsLogEntry, HistoryMessage, ToolDefinition, SessionInsights, DiscussionSettings, CustomSlice, MMAUsageStats, ProviderPayload, UIPanelConfig, PathInfo)` **GIT NOTE:** Per-aggregate dataclasses added to `src/type_aliases.py`. `Metadata: TypeAlias = dict[str, Any]` UNCHANGED (the catch-all for collapsed codepaths). Existing dataclasses (`FileItem`, `ToolCall`, `ChatMessage`) re-exported as TypeAliases. No consumer migration yet. **ROLLBACK:** `git revert HEAD` (one atomic commit). The 10 dataclasses are additive; no consumer code changes. ### Task 0.2: Add `RAGChunk` dataclass to `src/rag_engine.py` **WHERE:** `src/rag_engine.py` (insert after the existing class definitions; use `manual-slop_py_add_def` with `anchor_type="bottom"`) **WHAT to add (exact text):** ```python @dataclass(frozen=True, slots=True) class RAGChunk: document: str = "" path: str = "" score: float = 0.0 metadata: Metadata = field(default_factory=dict) def to_dict(self) -> Metadata: out: Metadata = {} for k, v in self.__dict__.items(): if v not in (None, "", [], {}, 0.0, False): out[k] = v return out @classmethod def from_dict(cls, raw: Metadata) -> "RAGChunk": valid = {f.name for f in fields(cls)} return cls(**{k: v for k, v in raw.items() if k in valid}) ``` **Imports to add at top of `src/rag_engine.py`:** ```python from dataclasses import dataclass, field, fields from src.type_aliases import Metadata ``` **HOW:** `manual-slop_py_add_def` with `anchor_type="bottom"`, `new_content=`. Then `manual-slop_edit_file` to add the imports (anchor on the existing import block). **SAFETY:** ```bash uv run python -c "from src.rag_engine import RAGChunk; c = RAGChunk(document='hi', path='/foo.py', score=0.95); print(c.document, c.path, c.score)" # Expect: hi /foo.py 0.95 uv run python -c "from src.rag_engine import RAGChunk; c = RAGChunk.from_dict({'document': 'x', 'path': '/y', 'score': 0.5, 'extra': 'filtered'}); print(c.document)" # Expect: x uv run python scripts/audit_main_thread_imports.py # Expect: exit 0 (verify the new imports don't break the main-thread-purity invariant) ``` **COMMIT:** `feat(rag_engine): add RAGChunk dataclass` **GIT NOTE:** `RAGChunk` dataclass added to `src/rag_engine.py`. Per-aggregate type for RAG retrieval results. No consumer migration yet. **ROLLBACK:** `git revert HEAD`. ### Task 0.3: Add 2 NEW dataclasses to `src/mcp_client.py` (ToolDefinition proxy + ASTNode + SearchResult + MCPToolResult) **WHERE:** `src/mcp_client.py` (insert at the bottom, before the final region markers; use `manual-slop_py_add_def`) **WHAT to add (exact text):** ```python @dataclass(frozen=True, slots=True) class ASTNode: kind: str = "" name: str = "" indent: int = 0 start_line: int = 0 end_line: int = 0 full_path: str = "" def to_dict(self) -> Metadata: return dict(self.__dict__) @classmethod def from_dict(cls, raw: Metadata) -> "ASTNode": valid = {f.name for f in fields(cls)} return cls(**{k: v for k, v in raw.items() if k in valid}) @dataclass(frozen=True, slots=True) class SearchResult: title: str = "" link: str = "" snippet: str = "" def to_dict(self) -> Metadata: return dict(self.__dict__) @classmethod def from_dict(cls, raw: Metadata) -> "SearchResult": valid = {f.name for f in fields(cls)} return cls(**{k: v for k, v in raw.items() if k in valid}) @dataclass(frozen=True, slots=True) class MCPToolResult: content: tuple[Metadata, ...] = () tools: tuple[Metadata, ...] = () def to_dict(self) -> Metadata: return {"content": list(self.content), "tools": list(self.tools)} @classmethod def from_dict(cls, raw: Metadata) -> "MCPToolResult": return cls(content=tuple(raw.get("content", ())), tools=tuple(raw.get("tools", ()))) ``` **Imports to add (if not already present):** ```python from dataclasses import dataclass, field, fields ``` Check the existing imports at the top of `src/mcp_client.py`; add only what's missing. **SAFETY:** ```bash uv run python -c "from src.mcp_client import ASTNode, SearchResult, MCPToolResult; n = ASTNode(kind='function', name='foo', indent=1, start_line=10, end_line=20, full_path='/foo.py'); print(n.kind, n.name, n.full_path)" # Expect: function foo /foo.py uv run python -c "from src.mcp_client import SearchResult; r = SearchResult(title='t', link='l', snippet='s'); print(r)" # Expect: SearchResult(title='t', link='l', snippet='s') uv run python scripts/audit_main_thread_imports.py # Expect: exit 0 ``` **COMMIT:** `feat(mcp_client): add ASTNode, SearchResult, MCPToolResult dataclasses` **GIT NOTE:** Per-aggregate dataclasses added to `src/mcp_client.py` for AST traversal results, web search results, and MCP tool call results. No consumer migration yet. **ROLLBACK:** `git revert HEAD`. ### Task 0.4: Add 2 NEW dataclasses to `src/performance_monitor.py` and `src/log_registry.py` **WHERE:** - `src/performance_monitor.py` — insert at the bottom: `PerformanceMetrics` dataclass - `src/log_registry.py` — insert at the bottom: `SessionInfo` and `SessionMetadata` dataclasses **WHAT to add (exact text):** `src/performance_monitor.py` (at the bottom, before any final region markers): ```python @dataclass(frozen=True, slots=True) class PerformanceMetrics: fps: float = 0.0 frame_time_ms_avg: float = 0.0 def to_dict(self) -> Metadata: return dict(self.__dict__) @classmethod def from_dict(cls, raw: Metadata) -> "PerformanceMetrics": valid = {f.name for f in fields(cls)} return cls(**{k: v for k, v in raw.items() if k in valid}) ``` Imports to add if not present: `from dataclasses import dataclass, field, fields`; `from src.type_aliases import Metadata`. `src/log_registry.py` (at the bottom): ```python @dataclass(frozen=True, slots=True) class SessionInfo: session_id: str = "" path: str = "" def to_dict(self) -> Metadata: return dict(self.__dict__) @classmethod def from_dict(cls, raw: Metadata) -> "SessionInfo": valid = {f.name for f in fields(cls)} return cls(**{k: v for k, v in raw.items() if k in valid}) @dataclass(frozen=True, slots=True) class SessionMetadata: timestamp: str = "" def to_dict(self) -> Metadata: return dict(self.__dict__) @classmethod def from_dict(cls, raw: Metadata) -> "SessionMetadata": valid = {f.name for f in fields(cls)} return cls(**{k: v for k, v in raw.items() if k in valid}) ``` Imports to add if not present: `from dataclasses import dataclass, field, fields`; `from src.type_aliases import Metadata`. **SAFETY:** ```bash uv run python -c "from src.performance_monitor import PerformanceMetrics; m = PerformanceMetrics(fps=60.0, frame_time_ms_avg=16.7); print(m)" # Expect: PerformanceMetrics(fps=60.0, frame_time_ms_avg=16.7) uv run python -c "from src.log_registry import SessionInfo, SessionMetadata; s = SessionInfo(session_id='abc', path='/x'); print(s)" # Expect: SessionInfo(session_id='abc', path='/x') ``` **COMMIT:** `feat(perf,log_registry): add PerformanceMetrics, SessionInfo, SessionMetadata dataclasses` **GIT NOTE:** Per-aggregate dataclasses added for performance telemetry and session metadata. **ROLLBACK:** `git revert HEAD`. ### Task 0.5: Complete `ContextPreset` schema in `src/models.py` **WHERE:** `src/models.py:932` (the `ContextPreset` class) **Read first** (Tier 3 reads the current implementation): ```bash git grep -n "class ContextPreset" src/models.py ``` Read the full class with `manual-slop_get_file_slice` (the slice tool). **Current state** (per the `data_structure_strengthening_20260606` spec §3.1): `ContextPreset` has `name, files, screenshots` minimum. **WHAT to add** (extend the schema with all observed fields from `src/gui_2.py:4181-4185,4333,4448`): Verify which fields are accessed: - `preset.get('files', [])` — `src/gui_2.py:4184` - `preset.get('screenshots', [])` — `src/gui_2.py:4185` - `preset.name` (string) - `len(preset.files)`, `len(preset.screenshots)` The minimal schema is already correct (`name, files, screenshots`). The change is to add the `@dataclass(frozen=True, slots=True)` decorator (if not present) and ensure `to_dict()` / `from_dict()` round-trip is lossless. **Read the current class first** with `manual-slop_get_file_slice` from line 932, length ~40 lines. Then determine if it's already a dataclass; if not, ADD the `@dataclass(frozen=True, slots=True)` decorator via `manual-slop_edit_file`. Preserve all existing fields and methods. **HOW:** `manual-slop_get_file_slice` first, then `manual-slop_edit_file` to add the decorator if missing. Verify `to_dict()` / `from_dict()` exist; if not, add them using the canonical pattern from `src/models.py:567` (`FileItem.to_dict()`). **SAFETY:** ```bash uv run python -c "from src.models import ContextPreset; cp = ContextPreset(name='test', files=[], screenshots=[]); print(cp.to_dict())" # Expect: {'name': 'test', 'files': [], 'screenshots': []} uv run python -c "from src.models import ContextPreset; cp = ContextPreset.from_dict({'name': 'test', 'files': [], 'screenshots': []}); print(cp)" # Expect: ContextPreset(name='test', files=([],), screenshots=([],)) # may differ based on default_factory uv run python -m pytest tests/test_context_presets_models.py -v # Expect: all tests pass ``` **COMMIT:** `refactor(models): complete ContextPreset schema (add @dataclass(frozen=True, slots=True) decorator if missing)` **GIT NOTE:** `ContextPreset` schema completed. The class is now a typed dataclass with `to_dict()` / `from_dict()` round-trip. No consumer migration yet. **ROLLBACK:** `git revert HEAD`. ### Task 0.6: Create 12 per-aggregate regression-guard test files **WHERE:** NEW FILES in `tests/`: - `tests/test_comms_log_entry.py` - `tests/test_history_message.py` - `tests/test_tool_definition.py` - `tests/test_session_insights.py` - `tests/test_discussion_settings.py` - `tests/test_custom_slice.py` - `tests/test_mma_usage_stats.py` - `tests/test_provider_payload.py` - `tests/test_ui_panel_config.py` - `tests/test_path_info.py` - `tests/test_rag_chunk.py` (in addition to the 10 above) - `tests/test_metadata_dataclass_aux.py` (for ASTNode, SearchResult, MCPToolResult, PerformanceMetrics, SessionInfo, SessionMetadata) **HOW:** `write_file` per file. Each file has the SAME STRUCTURE (5 tests minimum per file): ```python # tests/test_comms_log_entry.py from __future__ import annotations import pytest from src.type_aliases import CommsLogEntry def test_default_constructor() -> None: e = CommsLogEntry() assert e.ts == "" assert e.role == "" assert e.model == "unknown" assert e.source_tier == "main" def test_constructor_with_kwargs() -> None: e = CommsLogEntry(role="user", ts="2025-01-01", content="hi") assert e.role == "user" assert e.ts == "2025-01-01" assert e.content == "hi" def test_field_access_direct() -> None: e = CommsLogEntry(role="user") assert e.role == "user" def test_frozen_raises() -> None: import dataclasses e = CommsLogEntry(role="user") with pytest.raises(dataclasses.FrozenInstanceError): e.role = "assistant" def test_slots_no_dict() -> None: e = CommsLogEntry() with pytest.raises(AttributeError): e.unknown_field = "x" def test_to_dict_includes_non_empty() -> None: e = CommsLogEntry(role="user", ts="2025-01-01") d = e.to_dict() assert d["role"] == "user" assert d["ts"] == "2025-01-01" assert "model" in d # model has default "unknown" — always included def test_from_dict_filters_unknown() -> None: e = CommsLogEntry.from_dict({"role": "user", "unknown_field": "x"}) assert e.role == "user" assert e.ts == "" ``` **Repeat the pattern for each of the 12 files**, adapting imports and field names. For `tests/test_metadata_dataclass_aux.py` (multi-class test file): ```python from __future__ import annotations import pytest from src.mcp_client import ASTNode, SearchResult, MCPToolResult from src.performance_monitor import PerformanceMetrics from src.log_registry import SessionInfo, SessionMetadata def test_ast_node_constructor() -> None: n = ASTNode(kind="function", name="foo", indent=1, start_line=10, end_line=20, full_path="/foo.py") assert n.kind == "function" assert n.full_path == "/foo.py" def test_ast_node_to_from_dict_roundtrip() -> None: n = ASTNode(kind="function", name="foo", indent=1, start_line=10, end_line=20, full_path="/foo.py") d = n.to_dict() n2 = ASTNode.from_dict(d) assert n == n2 def test_search_result_constructor() -> None: r = SearchResult(title="t", link="l", snippet="s") assert r.title == "t" def test_search_result_roundtrip() -> None: r = SearchResult(title="t", link="l", snippet="s") r2 = SearchResult.from_dict(r.to_dict()) assert r == r2 def test_mcp_tool_result_roundtrip() -> None: tr = MCPToolResult(content=({"text": "hi"},), tools=({"name": "foo"},)) tr2 = MCPToolResult.from_dict(tr.to_dict()) assert tr == tr2 def test_performance_metrics_constructor() -> None: m = PerformanceMetrics(fps=60.0, frame_time_ms_avg=16.7) assert m.fps == 60.0 def test_performance_metrics_roundtrip() -> None: m = PerformanceMetrics(fps=60.0, frame_time_ms_avg=16.7) m2 = PerformanceMetrics.from_dict(m.to_dict()) assert m == m2 def test_session_info_constructor() -> None: s = SessionInfo(session_id="abc", path="/x") assert s.session_id == "abc" def test_session_info_roundtrip() -> None: s = SessionInfo(session_id="abc", path="/x") s2 = SessionInfo.from_dict(s.to_dict()) assert s == s2 def test_session_metadata_constructor() -> None: m = SessionMetadata(timestamp="2025-01-01T00:00:00") assert m.timestamp == "2025-01-01T00:00:00" def test_session_metadata_roundtrip() -> None: m = SessionMetadata(timestamp="2025-01-01T00:00:00") m2 = SessionMetadata.from_dict(m.to_dict()) assert m == m2 ``` **SAFETY:** ```bash uv run pytest tests/test_comms_log_entry.py tests/test_history_message.py tests/test_tool_definition.py tests/test_session_insights.py tests/test_discussion_settings.py tests/test_custom_slice.py tests/test_mma_usage_stats.py tests/test_provider_payload.py tests/test_ui_panel_config.py tests/test_path_info.py tests/test_rag_chunk.py tests/test_metadata_dataclass_aux.py -v # Expect: all 12 files PASS; total 60+ tests ``` **COMMIT:** `test(type_aliases): add per-aggregate dataclass regression-guard suite (60+ tests across 12 files)` **GIT NOTE:** 12 regression-guard test files added. The 11 NEW dataclasses are tested for: default constructor, kwargs constructor, field access, frozen, slots, to_dict/from_dict round-trip, from_dict filters unknown fields. Consumer migration is in subsequent phases; this commit only adds the tests. **ROLLBACK:** `git revert HEAD` (no consumer code changed; tests are additive). ### Task 0.7: Re-measure baseline (Phase 0 complete) ```bash uv run python -c " import sys sys.path.insert(0, 'scripts/code_path_audit') sys.path.insert(0, 'src') from code_path_audit import build_pcg from code_path_audit_ssdl import count_branches_in_function pcg = build_pcg('src').data metadata_consumers = pcg.consumers.get('Metadata', []) total = sum(2 ** count_branches_in_function(f, 'src') for f in metadata_consumers) print(f'Post-Phase-0 effective codepaths: {total:.3e}') print(f'Metadata consumers: {len(metadata_consumers)}') " # Expect: ~4.014e+22 (no consumer migration yet; baseline unchanged) ``` Phase 0 introduces NO codepath reduction — it's purely design + tests. The reduction happens in Phases 1-10 as consumers migrate to direct field access. **End of Phase 0.** ## Phase 1: Migrate `Ticket` consumers (REUSED dataclass; remove legacy `.get()` method) **Focus:** `Ticket` is already a dataclass at `src/models.py:302` with 15 fields. The consumers currently use `t.get('id', '')` etc. via the legacy `Ticket.get(key, default)` method (line 348). After this phase, all consumers use direct field access (`t.id`, `t.depends_on`, `t.manual_block`) and the legacy `get()` method is REMOVED. **Acceptance:** All 30+ `t.get(...)` and `t['...']` access sites on Ticket consumers replaced with direct field access; legacy `Ticket.get()` method removed; all existing tests pass. ### Task 1.1: Migrate `src/gui_2.py` Ticket access sites (read-only uses) **WHERE:** `src/gui_2.py:1366,1369,1387,1393,1399,1408,1418,1419,1427,1428,1436,1438,1439,1682,6852,6854,6860,6861,6870,7018,7022,7071,7096,7128,7156,7158,7162,7166,7171,7203,7204,7208,7215,7223,7233,7234,7248,7255,7256,7269,7270,7272,7294` **Pattern A — `t.get('id', '')` → `str(t.id)`:** Read the exact line via `manual-slop_get_file_slice`. Each occurrence uses `manual-slop_edit_file` with `old_string` and `new_string`. Example for `src/gui_2.py:1366`: - **old_string:** `id_to_idx = {str(t.get('id', '')): i for i, t in enumerate(new_tickets)}` - **new_string:** `id_to_idx = {str(t.id): i for i, t in enumerate(new_tickets)}` Example for `src/gui_2.py:1369`: - **old_string:** `deps = t.get('depends_on', [])` - **new_string:** `deps = list(t.depends_on)` (the dataclass field is a list; the call site uses it as a list, so wrap with `list()` for type narrowness OR just `t.depends_on` if the downstream accepts the dataclass type) Read the surrounding context with `manual-slop_get_file_slice` to determine the correct new_string for each line. **Pattern B — `t.get('manual_block', False)` → `t.manual_block`:** Example for `src/gui_2.py:1428`: - **old_string:** `if t and t.get('manual_block', False):` - **new_string:** `if t and t.manual_block:` **Pattern C — `t.get('status')` → `t.status`:** Example for `src/gui_2.py:1388,1394,1400,1410,1421,1429,1444`: - **old_string:** `if t: t['status'] = 'in_progress'` (these are mutation sites; see Task 1.2) **Task 1.1 covers read-only sites only.** Mutation sites are in Task 1.2. **PATTERN TABLE for Ticket fields (use `manual-slop_edit_file` per site):** | Site | old_string (access) | new_string (direct) | |---|---|---| | `gui_2.py:1366` | `str(t.get('id', ''))` | `str(t.id)` | | `gui_2.py:1387` | `str(t.get('id', ''))` | `str(t.id)` | | `gui_2.py:1393` | `str(t.get('id', ''))` | `str(t.id)` | | `gui_2.py:1399` | `str(t.get('id', ''))` | `str(t.id)` | | `gui_2.py:1408` | `str(ticket_id)` (compare) | `str(ticket_id)` (unchanged; the lookup key) | | `gui_2.py:1418` | `for dep_id in t.get('depends_on', []):` | `for dep_id in t.depends_on:` | | `gui_2.py:1419` | `str(x.get('id', ''))` | `str(x.id)` | | `gui_2.py:1427` | `str(ticket_id)` (compare) | (unchanged) | | `gui_2.py:1428` | `t.get('manual_block', False)` | `t.manual_block` | | `gui_2.py:1436` | `t.get('status') == 'blocked' and not t.get('manual_block', False)` | `t.status == 'blocked' and not t.manual_block` | | `gui_2.py:1438` | `for dep_id in t.get('depends_on', []):` | `for dep_id in t.depends_on:` | | `gui_2.py:1439` | `str(x.get('id', ''))` | `str(x.id)` | | `gui_2.py:1682` | `{'id': str(t.get('id', '')), 'depends_on': t.get('depends_on', [])}` | `{'id': str(t.id), 'depends_on': list(t.depends_on)}` | | `gui_2.py:6852` | `str(t.get('id', ''))` | `str(t.id)` | | `gui_2.py:6854` | `ticket.get('status', 'todo')` | `ticket.status` | | `gui_2.py:6860` | `ticket.get('target_file', '')` | `ticket.target_file or ''` | | `gui_2.py:6860` | `', '.join(ticket.get('depends_on', []))` | `', '.join(ticket.depends_on)` | | `gui_2.py:6861` | `ticket.get('persona_id', '')` | `ticket.persona_id or ''` | | `gui_2.py:6870` | `str(t.get('id', ''))` | `str(t.id)` | | `gui_2.py:7018` | `track.get('title', '')` | `track.title` (if Track is dataclass; if not, see Task 1.4) | | `gui_2.py:7022` | `track.get('goal', '')` | `track.goal` (Track dataclass) | | `gui_2.py:7071` | `str(t.get('id', ''))` | `str(t.id)` | | `gui_2.py:7096` | `str(t.get('id', ''))` | `str(t.id)` | | `gui_2.py:7128` | `t.get('priority', 'medium')` | `t.priority` | | `gui_2.py:7156` | `t.get('status', 'todo')` | `t.status` | | `gui_2.py:7158` | `t.get('status', 'todo')` | `t.status` | | `gui_2.py:7162` | `t.get('description', '')` | `t.description` | | `gui_2.py:7166` | `t.get('status', 'todo')` | `t.status` | | `gui_2.py:7171` | `t.get('manual_block', False)` | `t.manual_block` | | `gui_2.py:7203` | `str(t.get('id', ''))` | `str(t.id)` | | `gui_2.py:7204` | `str(t.get('id', ''))` | `str(t.id)` | | `gui_2.py:7208` | `str(t.get('id', '??'))` | `str(t.id) if t.id else '??'` | | `gui_2.py:7215` | `t.get('status', 'todo')` | `t.status` | | `gui_2.py:7223` | `t.get('target_file','')` | `t.target_file or ''` | | `gui_2.py:7233` | `str(t.get('id', '??'))` | `str(t.id) if t.id else '??'` | | `gui_2.py:7234` | `for dep in t.get('depends_on', []):` | `for dep in t.depends_on:` | | `gui_2.py:7248` | `str(t.get('id', ''))` | `str(t.id)` | | `gui_2.py:7255` | `str(t.get('id', ''))` | `str(t.id)` | | `gui_2.py:7256` | `t.get('depends_on', [])` | `t.depends_on` | | `gui_2.py:7269` | `str(t.get('id', ''))` | `str(t.id)` | | `gui_2.py:7270` | `t.get('depends_on', [])` | `t.depends_on` | | `gui_2.py:7294` | `t.get('id', '')` | `t.id` | **For `Track`** (if not already a dataclass): Task 1.4 adds the `@dataclass(frozen=True, slots=True)` decorator to `Track` at `src/models.py` if it's not already a typed dataclass. Read it first; if it's a dict, skip Task 1.4 (Track is out of scope for Phase 1; the read-only sites in `gui_2.py:7018-7024` will keep `track.get('title', '')` for now and become a follow-up track). **HOW:** For each row, read the exact line context with `manual-slop_get_file_slice` (start_line=N-2, end_line=N+2), then use `manual-slop_edit_file` with the precise `old_string` and `new_string`. If the surrounding context has tabs vs spaces, preserve exactly. **SAFETY (run after EVERY 5 edits):** ```bash uv run python -m pytest tests/test_ticket_queue.py tests/test_per_ticket_model.py tests/test_manual_block.py tests/test_tiered_aggregation.py -x # Expect: all tests pass (use -x to stop on first failure; revert immediately if any fail) ``` **SAFETY (run after the full phase):** ```bash git grep -nE "\.get\('id'," -- 'src/gui_2.py' | grep -v "str(t.id)" | wc -l # Expect: 0 (all .get('id', ...) sites in gui_2.py are migrated) git grep -nE "t\.get\(" -- 'src/gui_2.py' | wc -l # Expect: 0 (no t.get() calls remaining) git grep -nE "\.get\('depends_on'," -- 'src/gui_2.py' | wc -l # Expect: 0 ``` **COMMIT:** `refactor(gui_2): migrate Ticket access sites to direct field access (~40 sites)` **GIT NOTE:** Migrated ~40 Ticket access sites in `src/gui_2.py` from `t.get('key', default)` / `t['key']` to direct field access (`t.id`, `t.depends_on`, `t.manual_block`, etc.). Verified by ticket test files. `Ticket` dataclass REUSED unchanged from `src/models.py:302`. **ROLLBACK:** `git revert HEAD`. The mutations in Task 1.2 are in a separate commit; this commit is read-only. ### Task 1.2: Migrate `src/gui_2.py` Ticket mutation sites **WHERE:** `src/gui_2.py:1388,1394,1400,1410,1411,1412,1421,1429,1430,1431,1444,6867,7137,7148,7151,7272` **PATTERN:** Mutations of `frozen=True` dataclass fields use `dataclasses.replace()`: ```python # BEFORE: if t: t['status'] = 'in_progress' # AFTER: import dataclasses if t: from src.type_aliases import dataclasses_replace_compat # if needed t = dataclasses.replace(t, status='in_progress') ``` OR, since the mutation is inside an if-check on a `next(...)` result, replace the lookup with a direct construction: ```python # BEFORE: t = next((t for t in app.active_tickets if str(t.get('id', '')) == tid), None) if t: t['status'] = 'in_progress' # AFTER (cleanest): from src.mma_tickets import update_ticket_status # helper, or inline match = next((i for i, t in enumerate(app.active_tickets) if str(t.id) == tid), None) if match is not None: app.active_tickets[match] = dataclasses.replace(app.active_tickets[match], status='in_progress') ``` **EXACT migration table:** | Site | old_string | new_string | |---|---|---| | `gui_2.py:1388` | `if t: t['status'] = 'in_progress'` | `if t: app.active_tickets[app.active_tickets.index(t)] = dataclasses.replace(t, status='in_progress')` | | `gui_2.py:1394` | `if t: t['status'] = 'completed'` | `if t: app.active_tickets[app.active_tickets.index(t)] = dataclasses.replace(t, status='completed')` | | `gui_2.py:1400` | `if t: t['status'] = 'blocked'` | `if t: app.active_tickets[app.active_tickets.index(t)] = dataclasses.replace(t, status='blocked')` | | `gui_2.py:1410-1412` | `t['status'] = 'blocked'; t['manual_block'] = True; t['blocked_reason'] = '[MANUAL] User blocked'` | use `dataclasses.replace(t, status='blocked', manual_block=True, blocked_reason='[MANUAL] User blocked')` | | `gui_2.py:1421` | `t['status'] = 'blocked'` | `dataclasses.replace(t, status='blocked')` | | `gui_2.py:1429-1431` | `t['status'] = 'todo'; t['manual_block'] = False; t['blocked_reason'] = None` | `dataclasses.replace(t, status='todo', manual_block=False, blocked_reason=None)` | | `gui_2.py:1444` | `t['status'] = 'todo'` | `dataclasses.replace(t, status='todo')` | | `gui_2.py:6858` | `ticket['priority'] = p_opt` | use `dataclasses.replace(ticket, priority=p_opt)` | | `gui_2.py:6866` | `ticket['persona_id'] = None` | use `dataclasses.replace(ticket, persona_id=None)` | | `gui_2.py:6867` | `ticket['status'] = 'done'` | use `dataclasses.replace(ticket, status='done')` | | `gui_2.py:7137` | `t['priority'] = p_opt` | `dataclasses.replace(t, priority=p_opt)` | | `gui_2.py:7148` | `t['model_override'] = None` | `dataclasses.replace(t, model_override=None)` | | `gui_2.py:7151` | `t['model_override'] = model` | `dataclasses.replace(t, model_override=model)` | | `gui_2.py:7272` | `t['depends_on'] = [dep for dep in deps if abs(hash(dep + "_" + tid)) != lid_val]` | `dataclasses.replace(t, depends_on=[dep for dep in deps if abs(hash(dep + "_" + tid)) != lid_val])` | **Add the import at the top of `src/gui_2.py` (if not already present):** ```python import dataclasses ``` **SAFETY:** ```bash uv run python -m pytest tests/test_ticket_queue.py tests/test_per_ticket_model.py tests/test_manual_block.py tests/test_tiered_aggregation.py -x # Expect: all pass uv run python -m pytest tests/test_mma_step_mode_sim.py tests/test_spawn_interception_v2.py -x # Expect: all pass (these tests exercise the mutation paths) ``` **COMMIT:** `refactor(gui_2): migrate Ticket mutation sites to dataclasses.replace (~14 sites)` **GIT NOTE:** Migrated ~14 Ticket mutation sites in `src/gui_2.py` from `t['key'] = value` to `dataclasses.replace(t, key=value)`. The `frozen=True` invariant is preserved. **ROLLBACK:** `git revert HEAD`. ### Task 1.3: Migrate `src/app_controller.py` Ticket access + mutation sites **WHERE:** `src/app_controller.py:4810,4820,4868` (mutation sites); also any read-only sites — run `git grep -nE "t\.get\(|\['status'\]" -- 'src/app_controller.py'` to enumerate. **PATTERN:** Same as Task 1.1 + 1.2. **EXACT table (from grep):** - `app_controller.py:4810`: `t['status'] = 'todo'` → mutation via replace - `app_controller.py:4820`: `t['status'] = 'skipped'` → mutation via replace - `app_controller.py:4868`: `t['status'] = 'in_progress'` → mutation via replace (Read the exact context with `manual-slop_get_file_slice` first to get the surrounding `for` loop / `if` conditions.) **SAFETY:** ```bash uv run python -m pytest tests/test_ticket_queue.py tests/test_conductor_engine_v2.py tests/test_phase6_engine.py -x # Expect: all pass ``` **COMMIT:** `refactor(app_controller): migrate Ticket mutation sites to dataclasses.replace (~3 sites)` **GIT NOTE:** Migrated ~3 Ticket mutation sites in `src/app_controller.py`. **ROLLBACK:** `git revert HEAD`. ### Task 1.4: Migrate `src/conductor_tech_lead.py` Ticket access sites **WHERE:** `src/conductor_tech_lead.py:125` (`ticket_map = {t['id']: t for t in tickets}`) **EXACT migration:** - **old_string:** `ticket_map = {t['id']: t for t in tickets}` - **new_string:** `ticket_map = {t.id: t for t in tickets}` Read the surrounding context with `manual-slop_get_file_slice` first; the `t` here is a `Ticket` dataclass (passed in from `src/dag_engine.py:TrackDAG.get_executable_tickets` which returns `List[Ticket]`). **SAFETY:** ```bash uv run python -m pytest tests/test_conductor_engine_v2.py tests/test_track_get_executable_tickets_complex.py -x # Expect: all pass ``` **COMMIT:** `refactor(conductor_tech_lead): migrate Ticket access site to direct field access` **GIT NOTE:** Migrated 1 Ticket access site. **ROLLBACK:** `git revert HEAD`. ### Task 1.5: Remove the legacy `Ticket.get(key, default)` method **WHERE:** `src/models.py:348` (the `def get(self, key: str, default: Any = None) -> Any` method) **Read first** with `manual-slop_get_file_slice` to verify the exact line range. The method spans lines 348-363 approximately. **HOW:** `manual-slop_py_remove_def` with `name="Ticket.get"`. The tool will identify and remove the method body. **SAFETY (CRITICAL — run BEFORE removing):** ```bash git grep -nE "\.get\('id'," -- 'src/*.py' | wc -l # Expect: 0 (no .get('id', default) calls remain on Ticket consumers) git grep -nE "t\.get\(" -- 'src/*.py' | wc -l # Expect: 0 uv run python -m pytest tests/test_ticket_queue.py tests/test_per_ticket_model.py tests/test_manual_block.py tests/test_tiered_aggregation.py tests/test_conductor_engine_v2.py -x # Expect: all pass ``` If any `t.get(...)` call remains, REVERT this task and the previous tasks; investigate which consumer was missed. **After removal:** ```bash uv run python -m pytest tests/ -x --timeout=60 -q # Expect: all pass (or the documented pre-existing failures only) ``` **COMMIT:** `refactor(models): remove legacy Ticket.get() method (direct field access is now the only path)` **GIT NOTE:** Legacy compat method removed. All consumers migrated in Tasks 1.1-1.4. The `Ticket` dataclass is now purely typed; no dynamic-key fallback. **ROLLBACK:** `git revert HEAD`. Note: if this revert is needed, the consumers in Tasks 1.1-1.4 will break (they use `t.id` not `t.get('id', '')`). Revert those commits too (in reverse order: 1.4, 1.3, 1.2, 1.1). ### Task 1.6: Re-measure + verify Phase 1 ```bash # Effective codepaths after Phase 1 uv run python -c " import sys sys.path.insert(0, 'scripts/code_path_audit') sys.path.insert(0, 'src') from code_path_audit import build_pcg from code_path_audit_ssdl import count_branches_in_function pcg = build_pcg('src').data metadata_consumers = pcg.consumers.get('Metadata', []) total = sum(2 ** count_branches_in_function(f, 'src') for f in metadata_consumers) print(f'Post-Phase-1 effective codepaths: {total:.3e}') print(f'Metadata consumers: {len(metadata_consumers)}') " # Expect: < 1e+22 (significant drop from Ticket migrations; baseline 4.014e+22) # VC4 partial: no .get('id', default) calls on Ticket consumers git grep -nE "\.get\('id'," -- 'src/*.py' | wc -l # Expect: 0 # All existing tests pass uv run python scripts/run_tests_batched.py # Expect: 10/11 PASS (RAG flake acceptable) ``` **End of Phase 1.** ## Phase 2: Migrate `FileItem` consumers (REUSED dataclass) **Focus:** `FileItem` is already a dataclass at `src/models.py:533`. Migrate consumers from `fi.get('path', 'attachment')` to `fi.path`, `f['path']` to `f.path`. ### Task 2.1: Migrate `src/ai_client.py` FileItem consumers **WHERE:** `src/ai_client.py:2565,2807,2898` **EXACT migrations (all the same pattern):** - **old_string:** `fi.get('path', 'attachment')` - **new_string:** `fi.path or 'attachment'` (Read each line with `manual-slop_get_file_slice` first to get the exact context; the `fi` variable name might differ at each site — it could be `item`, `fi`, `file_item`. Use the actual variable name in `old_string`.) **SAFETY:** ```bash uv run python -m pytest tests/test_ai_client.py tests/test_file_item_model.py -x --timeout=60 # Expect: all pass ``` **COMMIT:** `refactor(ai_client): migrate FileItem access sites to direct field access (~3 sites)` **GIT NOTE:** Migrated 3 FileItem access sites in `src/ai_client.py`. `FileItem` dataclass REUSED unchanged from `src/models.py:533`. **ROLLBACK:** `git revert HEAD`. ### Task 2.2: Migrate `src/app_controller.py` FileItem consumer **WHERE:** `src/app_controller.py:3508` (`file_paths = [f['path'] for f in file_items]`) **EXACT migration:** - **old_string:** `file_paths = [f['path'] for f in file_items]` - **new_string:** `file_paths = [f.path for f in file_items]` **SAFETY:** ```bash uv run python -m pytest tests/test_file_item_model.py tests/test_app_controller.py -x --timeout=60 # Expect: all pass ``` **COMMIT:** `refactor(app_controller): migrate FileItem access site to direct field access` **GIT NOTE:** Migrated 1 FileItem access site. **ROLLBACK:** `git revert HEAD`. ### Task 2.3: Re-measure + verify Phase 2 ```bash # VC4 partial: no .get('path', ...) calls on FileItem consumers git grep -nE "\.get\('path'," -- 'src/ai_client.py' | wc -l # Expect: 0 (the 3 sites in src/ai_client.py are migrated; ProjectConfig's self.project.get('paths', {}) doesn't match this regex) git grep -nE "fi\.get\(|f\['path'\]" -- 'src/*.py' | wc -l # Expect: 0 ``` **End of Phase 2.** ## Phase 3: Migrate `CommsLogEntry` consumers (NEW dataclass from Phase 0) **Focus:** `CommsLogEntry` was added in Phase 0 (`src/type_aliases.py`). Now wire it into the consumers. ### Task 3.1: Migrate `src/app_controller.py` CommsLogEntry consumers **WHERE:** `src/app_controller.py:2277,2302,2310` (and any other sites in this file — search with `git grep -nE "entry\.get\(" -- 'src/app_controller.py'`) **EXACT migrations:** - `app_controller.py:2277`: `'source_tier': entry.get('source_tier', 'main')` → `'source_tier': entry.source_tier` (read full line context first; the `entry` is a CommsLogEntry dataclass) - `app_controller.py:2302`: `tier = entry.get('source_tier', 'main')` → `tier = entry.source_tier` - `app_controller.py:2310`: `'model': entry.get('model', 'unknown')` → `'model': entry.model` **For each site, the `entry` variable MUST be a `CommsLogEntry` instance.** If any site reads `entry.get('model', 'unknown')` where `entry` is actually a different aggregate (e.g., a UsageStats-like dict), STOP and report to Tier 2. **SAFETY:** ```bash uv run python -m pytest tests/test_session_logger_optimization.py tests/test_session_logger_reset.py tests/test_session_logging.py tests/test_logging_e2e.py tests/test_comms_log_entry.py -x --timeout=60 # Expect: all pass ``` **COMMIT:** `refactor(app_controller): migrate CommsLogEntry access sites to direct field access` **GIT NOTE:** Migrated ~3 CommsLogEntry access sites in `src/app_controller.py`. **ROLLBACK:** `git revert HEAD`. ### Task 3.2: Migrate `src/gui_2.py` CommsLogEntry consumer **WHERE:** `src/gui_2.py:5803` (`imgui.text_colored(C_SUB(), f"[{entry.get('source_tier', 'main')}]")`) **EXACT migration:** - **old_string:** `f"[{entry.get('source_tier', 'main')}]"` - **new_string:** `f"[{entry.source_tier}]"` **SAFETY:** ```bash uv run python -m pytest tests/test_comms_log_entry.py tests/test_logging_e2e.py -x --timeout=60 # Expect: all pass ``` **COMMIT:** `refactor(gui_2): migrate CommsLogEntry access site to direct field access` **GIT NOTE:** Migrated 1 CommsLogEntry access site in `src/gui_2.py`. **ROLLBACK:** `git revert HEAD`. ### Task 3.3: Re-measure + verify Phase 3 ```bash git grep -nE "entry\.get\('source_tier'," -- 'src/*.py' | wc -l # Expect: 0 ``` **End of Phase 3.** ## Phase 4: Migrate `HistoryMessage` consumers (NEW dataclass) **Focus:** `HistoryMessage` is the UI-layer discussion message (distinct from `openai_schemas.ChatMessage` which is provider-side). ### Task 4.1: Migrate `src/synthesis_formatter.py` HistoryMessage consumers **WHERE:** `src/synthesis_formatter.py:24,37` **EXACT migrations:** - `synthesis_formatter.py:24`: `f"{msg.get('role', 'unknown')}: {msg.get('content', '')}"` → `f"{msg.role}: {msg.content or ''}"` (or `msg.content` if the field is always set) - `synthesis_formatter.py:37`: same pattern Read the full context first; the `msg` variable is a `HistoryMessage` instance. **SAFETY:** ```bash uv run python -m pytest tests/test_synthesis_formatter.py tests/test_history_message.py -x --timeout=60 # Expect: all pass (search for the actual test file name; may be tests/test_synthesis*.py) ``` **COMMIT:** `refactor(synthesis_formatter): migrate HistoryMessage access sites to direct field access` **GIT NOTE:** Migrated 2 HistoryMessage access sites in `src/synthesis_formatter.py`. **ROLLBACK:** `git revert HEAD`. ### Task 4.2: Re-measure + verify Phase 4 ```bash git grep -nE "msg\.get\('role'," -- 'src/*.py' | wc -l # Expect: 0 ``` **End of Phase 4.** ## Phase 5: Wire `ChatMessage` into per-vendor send paths **Focus:** `ChatMessage` is already in `src/openai_schemas.py:48`. The per-vendor send paths (`_send_anthropic`, `_send_deepseek`, etc.) currently use the per-vendor history modules (`provider_state.get_history(...)`). Wire `ChatMessage` into the message construction. ### Task 5.1: Migrate `_send_anthropic` and `_send_deepseek` (~9 sites) **WHERE:** `src/ai_client.py` (the `_send_anthropic` and `_send_deepseek` methods) **Read first** with `manual-slop_get_file_slice` to find the exact construction sites. Each provider builds a list of messages in a specific format. **HOW:** The migration is provider-specific. Each provider's send method has a `for msg in history: messages.append({...})` block. Replace the dict-construction with `ChatMessage(role=msg.role, content=msg.content, ...)`. **EXACT pattern (for `_send_anthropic`):** ```python # BEFORE: for msg in anthropic_history: if msg.get("role") == "user": messages.append({"role": "user", "content": msg.get("content", "")}) # AFTER: for msg in anthropic_history: cm = ChatMessage.from_dict(msg) if isinstance(msg, dict) else msg if cm.role == "user": messages.append(cm.to_dict()) ``` (Read each provider's send method first; the exact pattern depends on the provider's message schema.) **For each of the 8 send methods (`_send_anthropic`, `_send_deepseek`, `_send_gemini`, `_send_gemini_cli`, `_send_minimax`, `_send_qwen`, `_send_llama`, `_send_grok`):** 1. Read the method with `manual-slop_get_file_slice`. 2. Identify the per-message dict-construction sites. 3. Replace with `ChatMessage` use. **SAFETY:** ```bash uv run python -m pytest tests/test_ai_client.py tests/test_anthropic_provider.py tests/test_deepseek_provider.py tests/test_openai_schemas.py -x --timeout=120 # Expect: all pass ``` **COMMIT (5.1, 5.2, 5.3):** 3 atomic commits, one per provider pair - `refactor(ai_client): wire ChatMessage into _send_anthropic and _send_deepseek` - `refactor(ai_client): wire ChatMessage into _send_gemini and _send_gemini_cli` - `refactor(ai_client): wire ChatMessage into _send_minimax, _send_qwen, _send_llama, _send_grok` **GIT NOTE:** Wired `ChatMessage` (existing in `src/openai_schemas.py:48`) into the per-vendor send paths. The dataclass was already created; this phase wires it into the message construction. **ROLLBACK:** `git revert HEAD` (one atomic commit at a time). ### Task 5.4: Re-measure + verify Phase 5 ```bash git grep -nE "msg\.get\('role'," -- 'src/ai_client.py' | wc -l # Expect: 0 (or only collapsed-codepath sites documented in Phase 11) ``` **End of Phase 5.** ## Phase 6: Wire `UsageStats` into per-call usage aggregation ### Task 6.1: Migrate `src/app_controller.py:2299-2309` UsageStats access sites **WHERE:** `src/app_controller.py:2299-2309` **Read first** with `manual-slop_get_file_slice`. **EXACT migrations:** - `app_controller.py:2304`: `new_mma_usage[tier]['input'] += u.get('input_tokens', 0) or 0` → `new_mma_usage[tier] = dataclasses.replace(new_mma_usage[tier], input=new_mma_usage[tier].input + (u.input_tokens if hasattr(u, 'input_tokens') else u.get('input_tokens', 0)))` (verify `u` is a UsageStats instance first; if it's still a dict, this site is collapsed-codepath and stays) - Same pattern for `app_controller.py:2305,2308,2309` **IMPORTANT:** If `u` (or `usage`) is a dict (e.g., loaded from JSON), the migration is via `UsageStats.from_dict(u)`. If it's already a dataclass instance, use direct attribute access. **SAFETY:** ```bash uv run python -m pytest tests/test_token_usage.py tests/test_usage_analytics_popout_sim.py tests/test_openai_schemas.py -x --timeout=60 # Expect: all pass ``` **COMMIT:** `refactor(app_controller): wire UsageStats into per-call usage aggregation (~4 sites)` **GIT NOTE:** Wired `UsageStats` (existing in `src/openai_schemas.py:68`) into the per-call usage aggregation in `src/app_controller.py`. **ROLLBACK:** `git revert HEAD`. ### Task 6.2: Re-measure + verify Phase 6 ```bash git grep -nE "u\.get\('input_tokens'," -- 'src/app_controller.py' | wc -l # Expect: 0 ``` **End of Phase 6.** ## Phase 7: Wire `ToolCall` into tool loop section ### Task 7.1: Migrate `src/ai_client.py` tool loop section **WHERE:** `src/ai_client.py` (the `_dispatch_tool` and tool loop methods) **Read first** with `manual-slop_get_file_slice`. The migration is mechanical: replace `tc.get('id')`, `tc.get('function', {}).get('name')`, `tc.get('function', {}).get('arguments')` with `tc.id`, `tc.function.name`, `tc.function.arguments`. **EXACT pattern:** ```python # BEFORE: for tc in response.tool_calls: tool_call_id = tc.get('id', '') function_name = tc.get('function', {}).get('name', '') arguments_str = tc.get('function', {}).get('arguments', '') # AFTER: for tc in response.tool_calls: tool_call_id = tc.id function_name = tc.function.name arguments_str = tc.function.arguments ``` **SAFETY:** ```bash uv run python -m pytest tests/test_ai_client.py tests/test_openai_schemas.py -x --timeout=60 # Expect: all pass ``` **COMMIT:** `refactor(ai_client): wire ToolCall into tool loop section (~56 sites)` **GIT NOTE:** Wired `ToolCall` (existing in `src/openai_schemas.py:32`) into the tool loop in `src/ai_client.py`. **ROLLBACK:** `git revert HEAD`. ### Task 7.2: Verify `src/mcp_client.py` tool loop **WHERE:** `src/mcp_client.py:1707-1714` (the `result['tools']` and `result['content']` sites) **EXACT migrations:** - `mcp_client.py:1707`: `for t in result['tools']:` → `for t in result.tools:` (after converting result to `MCPToolResult.from_dict(result)` if it's still a dict) - `mcp_client.py:1708`: `self.tools[t['name']] = t` → `self.tools[t.name] = t` - `mcp_client.py:1714`: `return '\n'.join([c.get('text', '') for c in result['content'] if c.get('type') == 'text'])` → `return '\n'.join([c.get('text', '') for c in result.content if c.get('type') == 'text'])` (the `content` is a tuple of dicts, not a list of `MCPToolResult`; leave the inner `c.get` calls as-is since `c` is still a `Metadata` dict) **SAFETY:** ```bash uv run python -m pytest tests/test_mcp_client.py tests/test_metadata_dataclass_aux.py -x --timeout=60 # Expect: all pass ``` **COMMIT:** `refactor(mcp_client): wire MCPToolResult into tool loop section (~3 sites)` **GIT NOTE:** Wired `MCPToolResult` (added in Phase 0) into the tool loop in `src/mcp_client.py`. **ROLLBACK:** `git revert HEAD`. **End of Phase 7.** ## Phase 8: Migrate `ToolDefinition` consumers (NEW dataclass) ### Task 8.1: Migrate `src/mcp_client.py:1970` and `src/gui_2.py:5876,5878` **EXACT migrations:** - `mcp_client.py:1970`: `'description': tinfo.get('description', '')` → `'description': tinfo.description` (after `tinfo = ToolDefinition.from_dict(...)` if needed) - `gui_2.py:5876`: `imgui.text(tinfo.get('server', 'unknown'))` → `imgui.text(tinfo.server)` (but `ToolDefinition` doesn't have `server`; this is a different aggregate — likely a `ToolInfo` dict from a separate source. If `ToolDefinition.from_dict` doesn't have a `server` field, STOP and report to Tier 2) - `gui_2.py:5878`: `imgui.text(tinfo.get('description', ''))` → `imgui.text(tinfo.description)` **SAFETY:** ```bash uv run python -m pytest tests/test_mcp_client.py tests/test_tool_definition.py -x --timeout=60 # Expect: all pass ``` **COMMIT:** `refactor(mcp_client,gui_2): migrate ToolDefinition access sites to direct field access` **GIT NOTE:** Migrated ~3 ToolDefinition access sites. **ROLLBACK:** `git revert HEAD`. **End of Phase 8.** ## Phase 9: Migrate `RAGChunk` consumers (NEW dataclass) ### Task 9.1: Migrate `src/aggregate.py`, `src/ai_client.py`, `src/app_controller.py` RAGChunk consumers **EXACT migrations:** - `aggregate.py:3259`: `chunk.get('document', '')` → `chunk.document` (after `chunk = RAGChunk.from_dict(...)` if chunk is a dict) - `app_controller.py:251`: same pattern - `app_controller.py:4162`: same pattern - `ai_client.py:3259`: same pattern Read each line with `manual-slop_get_file_slice` first to determine if `chunk` is already a dict or dataclass. **SAFETY:** ```bash uv run python -m pytest tests/test_rag_engine.py tests/test_aggregate.py tests/test_rag_chunk.py -x --timeout=120 # Expect: all pass ``` **COMMIT:** `refactor(rag_engine,aggregate,app_controller,ai_client): migrate RAGChunk access sites to direct field access (~4 sites)` **GIT NOTE:** Migrated ~4 RAGChunk access sites across 4 files. **ROLLBACK:** `git revert HEAD`. **End of Phase 9.** ## Phase 10: Migrate small-batch aggregates (8 aggregates) **Focus:** `SessionInsights`, `DiscussionSettings`, `CustomSlice`, `MMAUsageStats`, `ProviderPayload`, `UIPanelConfig`, `PathInfo`, `ToolDefinition`. Batched because each has few sites. ### Task 10.1: Migrate `src/gui_2.py` small-batch consumers **EXACT migrations (in `src/gui_2.py`):** | Site | Aggregate | old_string | new_string | |---|---|---|---| | `2199` | MMAUsageStats | `model = stats.get('model', 'unknown')` | `model = stats.model` (after `stats = MMAUsageStats.from_dict(...)` if needed) | | `2200` | MMAUsageStats | `in_t = stats.get('input', 0)` | `in_t = stats.input` | | `2201` | MMAUsageStats | `out_t = stats.get('output', 0)` | `out_t = stats.output` | | `2216` | MMAUsageStats | `stats.get('model', '')` | `stats.model` | | `3535` | DiscussionSettings | `entry.get('temperature', 0.7)` | `entry.temperature` (after `entry = DiscussionSettings.from_dict(...)`) | | `4048` | CustomSlice | `slc.get('tag', '')` | `slc.tag` (after `slc = CustomSlice.from_dict(...)`) | | `4054` | CustomSlice | `slc.get('comment', '')` | `slc.comment` | | `4090` | CustomSlice | `slc.get('tag') == 'auto-ast'` | `slc.tag == 'auto-ast'` | | `4269` | FileStats (NEW) | `stats.get('lines', 0), AST: {stats.get('ast_elements', 0)}` | `stats.lines, AST: {stats.ast_elements}` (FileStats is a NEW dataclass in Phase 10; if not added, keep as dict) | | `4926-4931` | SessionInsights | `insights.get('total_tokens', 0)`, etc. | `insights.total_tokens`, etc. (after `insights = SessionInsights.from_dict(...)`) | | `5876` | ToolDefinition | `tinfo.get('server', 'unknown')` | (NOT a ToolDefinition field; report to Tier 2 if `server` is not in `ToolDefinition`) | | `5878` | ToolDefinition | `tinfo.get('description', '')` | `tinfo.description` | | `5953` | CustomSlice | `slc.get('tag', '')` | `slc.tag` | | `5959` | CustomSlice | `slc.get('comment', '')` | `slc.comment` | | `5980,5981` | CustomSlice | `slc.get('tag') == 'auto-ast'`, etc. | `slc.tag == 'auto-ast'`, etc. | | `6610` | MMAUsageStats | `u.get('model','unknown'), u.get('input',0), u.get('output',0)` | `u.model, u.input, u.output` (after `u = MMAUsageStats.from_dict(u)` if `u` is a dict) | | `6785-6787` | MMAUsageStats | `stats.get('model', 'unknown')`, etc. | `stats.model`, etc. | **At each site, FIRST verify the aggregate type.** If the variable is already a dataclass instance, use direct field access. If it's still a `dict[str, Any]`, call `.from_dict()` first OR classify as collapsed-codepath (in which case keep `.get()`). **SAFETY:** ```bash uv run python -m pytest tests/test_session_insights.py tests/test_discussion_settings.py tests/test_custom_slice.py tests/test_mma_usage_stats.py tests/test_provider_payload.py tests/test_ui_panel_config.py tests/test_path_info.py -x --timeout=60 # Expect: all pass ``` **COMMIT (10.1):** `refactor(gui_2): migrate small-batch aggregates (SessionInsights, DiscussionSettings, CustomSlice, MMAUsageStats, ToolDefinition) to direct field access (~25 sites)` **GIT NOTE:** Migrated ~25 small-aggregate access sites in `src/gui_2.py`. **ROLLBACK:** `git revert HEAD`. ### Task 10.2: Migrate `src/app_controller.py` small-batch consumers **EXACT migrations (in `src/app_controller.py`):** | Site | Aggregate | old_string | new_string | |---|---|---|---| | `2068` | UIPanelConfig | `gui_cfg.get('separate_message_panel', False)` | `gui_cfg.separate_message_panel` | | `2069` | UIPanelConfig | `gui_cfg.get('separate_response_panel', False)` | `gui_cfg.separate_response_panel` | | `2070` | UIPanelConfig | `gui_cfg.get('separate_tool_calls_panel', False)` | `gui_cfg.separate_tool_calls_panel` | | `2257,2258` | MMAUsageStats | `new_mma_usage[t]['input'] = 0`, etc. | `new_mma_usage[t] = dataclasses.replace(new_mma_usage[t], input=0)` (verify `new_mma_usage` is a `dict[str, MMAUsageStats]`; if it's still a `dict[str, dict]`, leave as collapsed-codepath) | | `2274,2287` | ProviderPayload | `payload.get('script')`, `payload.get('args', {})`, `payload.get('output', payload.get('content', ''))` | `payload.script`, `payload.args`, `payload.output` | | `2304,2305` | MMAUsageStats | `new_mma_usage[tier]['input'] += u.get('input_tokens', 0) or 0`, etc. | mutation via replace (per Phase 6 pattern) | **Subscript sites (`app_controller.py:1974,1978,1984,1985`):** | Site | Aggregate | old_string | new_string | |---|---|---|---| | `1974` | PathInfo | `lpath = Path(proj_paths['logs_dir'])` | `lpath = Path(proj_paths.logs_dir)` (verify `proj_paths` is `PathInfo` after `from_dict` call) | | `1978` | PathInfo | `spath = Path(proj_paths['scripts_dir'])` | `spath = Path(proj_paths.scripts_dir)` | | `1984` | PathInfo | `path_info['logs_dir']['path']` | `path_info.logs_dir.path` (if `path_info.logs_dir` is a `PathInfo` nested; otherwise leave) | | `1985` | PathInfo | `path_info['scripts_dir']['path']` | `path_info.scripts_dir.path` | **SAFETY:** ```bash uv run python -m pytest tests/test_ui_panel_config.py tests/test_provider_payload.py tests/test_path_info.py tests/test_app_controller.py -x --timeout=60 # Expect: all pass ``` **COMMIT (10.2):** `refactor(app_controller): migrate ProviderPayload, UIPanelConfig, PathInfo, MMAUsageStats to direct field access (~10 sites)` **GIT NOTE:** Migrated ~10 small-aggregate access sites in `src/app_controller.py`. **ROLLBACK:** `git revert HEAD`. ### Task 10.3: Migrate `src/multi_agent_conductor.py:638` and other small-batch sites **EXACT migration:** - `multi_agent_conductor.py:638`: `response_payload['stream_id']` → `response_payload.stream_id` (after converting `response_payload` to a typed dataclass OR keeping as collapsed-codepath) If `response_payload` is a dict from JSON, classify as collapsed-codepath and keep `.get()`. **SAFETY:** ```bash uv run python -m pytest tests/test_multi_agent_conductor.py -x --timeout=60 # Expect: all pass ``` **COMMIT (10.3):** `refactor(multi_agent_conductor): migrate ProviderPayload stream_id access to direct field access (if applicable; collapsed-codepath otherwise)` **GIT NOTE:** Migrated 1 site IF applicable; otherwise classified as collapsed-codepath. **ROLLBACK:** `git revert HEAD`. ### Task 10.4: Re-measure + verify Phase 10 ```bash git grep -nE "insights\.get\(|stats\.get\('model',|slc\.get\('tag',|slc\.get\('comment',|payload\.get\('script',|gui_cfg\.get\('separate_" -- 'src/*.py' | wc -l # Expect: 0 (all migrated) ``` **End of Phase 10.** ## Phase 11: `Metadata` collapsed-codepath audit (FR6) **Focus:** Every remaining `.get('key', default)` and `['key']` site is classified as either (a) "promoted to per-aggregate dataclass → migrated" or (b) "collapsed codepath → keeps Metadata with documented justification." ### Task 11.1: Audit and document remaining collapsed-codepath sites **Run the audit:** ```bash git grep -nE "\.get\('[a-z_]+'," -- 'src/*.py' > /tmp/remaining_get_sites.txt wc -l /tmp/remaining_get_sites.txt # Expect: < 30 (was 107; should be ~20 collapsed-codepath sites) git grep -nE "\[[ ]*'[a-z_]+'[ ]*\]" -- 'src/*.py' > /tmp/remaining_subscript_sites.txt wc -l /tmp/remaining_subscript_sites.txt # Expect: ~80 (most of the 106 are genuinely dict access for collapsed codepaths) ``` **For each remaining `.get()` site, classify and document:** | File:line | Current access | Classification | Justification | |---|---|---|---| | `app_controller.py:1972` | `self.project.get('paths', {})` | collapsed | `manual_slop.toml` project config; shape unknown | | `app_controller.py:2016` | `self.project.get('conductor', {}).get('dir', 'conductor')` | collapsed | TOML config | | `app_controller.py:2033` | `self.project.get('project', {}).get('mcp_config_path')` | collapsed | TOML config | | `gui_2.py:820` | `self.controller.project.get('context_presets', {})` | collapsed | TOML config | | `gui_2.py:4181` | `app.controller.project.get('context_presets', {})` | collapsed | TOML config | | `gui_2.py:4333` | same | collapsed | TOML config | | `gui_2.py:4448` | `app.controller.project.get('context_presets', {}).get(cp_name)` | collapsed | TOML config | | `gui_2.py:5036` | `app.project.get('discussion', {}).get('discussions', {})` | collapsed | TOML config (DiscussionStore) | | `gui_2.py:5046,5047` | same | collapsed | TOML config | | `gui_2.py:5200,5217,5238` | same | collapsed | TOML config | | `synthesis_formatter.py:24,37` | (already migrated in Phase 4) | n/a | n/a | | `paths.py:262` | `data.get('conductor', {}).get('dir')` | collapsed | config.toml parsing; schema is opaque at this layer | | `app_controller.py:2178` | `item['time']` | collapsed | collated timeline item; aggregate is `CollatedItem` (NOT in scope; would require new dataclass) | | `app_controller.py:2257,2258` | `new_mma_usage[t]['input'] = 0` | collapsed | dict mutation; promotion would require a new `MMAUsageMap` type | | `app_controller.py:2290,2294,2295` | `paired_tools[tid]['result']` | collapsed | dict mutation; promotion would require a new `PairedTools` type | | `app_controller.py:2299` | `u = payload['usage']` | collapsed | JSON-deserialized payload | | `app_controller.py:3508` | (already migrated in Phase 2) | n/a | n/a | | `gui_2.py:355-387` | `self.controller._predefined_callbacks['save_context_preset']` | collapsed | handler-map; INTENTIONALLY a dict (it's the dispatch table, not data) | | `gui_2.py:1388-1431` | (Ticket mutations, already migrated in Phase 1.2) | n/a | n/a | | `gui_2.py:2148-2150` | `usage['input_tokens']`, etc. | collapsed | UsageStats dict (promotion to dataclass requires `from_dict` at the JSON boundary) | | `gui_2.py:3992-3995,4079,4080,4082,4085` | `node['indent']`, `node['kind']`, etc. | collapsed | AST node dict from tree-sitter; promotion requires new `ASTNode` use at the boundary (NOT done in Phase 0) | | `gui_2.py:4033,4047,4053,4055` | `slice_data['tag'] = ...`, etc. | collapsed | CustomSlice mutation; promotion requires a typed list | | `gui_2.py:4090,4091` | `slc.get('tag') == 'auto-ast'` | collapsed | CustomSlice in FileItem.custom_slices (list of dicts; promotion requires a typed list) | | `gui_2.py:5921,5952,5958,5960,5980,5981` | same pattern | collapsed | same | | `gui_2.py:6318-6320` | `app.shader_uniforms['crt']` | collapsed | shader uniform dict; NOT a sub-aggregate | | `gui_2.py:6623` | `track_stats['percentage']` | collapsed | TrackStats dict | | `gui_2.py:7018,7020,7022,7024` | `track.get('title', '')`, etc. | collapsed | Track dict (the Track dataclass exists but these sites use `track` as a dict) | | `gui_2.py:7020,7024` | `track['title'] = new_t`, etc. | collapsed | Track dict mutation | | `log_pruner.py:53,54` | `session_info['session_id']`, etc. | collapsed | SessionInfo dict | | `log_registry.py:174-179` | `new_session_data['start_time']`, etc. | collapsed | session mutation | | `mcp_client.py:1045` | `r['title']`, `r['link']`, `r['snippet']` | collapsed | SearchResult dict | | `mcp_client.py:1707,1708` | `result['tools']`, `t['name']` | collapsed | MCPToolResult dict (not yet converted to dataclass) | | `mcp_client.py:1714` | `c.get('text', '')` | collapsed | content list of dicts | | `models.py:976-978,989,991` | `data.get('command')`, etc. | collapsed | MCPServerConfig.from_dict (already a dataclass; these are the dict-input to the constructor) | | `multi_agent_conductor.py:638` | `response_payload['stream_id']` | collapsed | MMA response payload dict | | `performance_monitor.py:27,28` | `metrics['fps']`, `metrics['frame_time_ms_avg']` | collapsed | PerformanceMetrics dict | | `project_manager.py:456` | `project_dict['discussion']['discussions']` | collapsed | ProjectManager TOML dict | | `synthesis_formatter.py:24,37` | `msg.get('role', 'unknown')` | collapsed (if `msg` is dict) or promoted (if dataclass) | verify at the site | | `app_controller.py:2178` | `item['time']` | collapsed | collated timeline | **Write the classification as a doc:** ```bash cat > /tmp/collapsed_codepath_classification.md << 'EOF' # Collapsed-codepath classification (Phase 11, FR6) The following `.get('key', default)` and `['key']` sites REMAIN after Phases 0-10. Each is classified as "collapsed codepath" with a documented justification. ## Why these are NOT promoted to per-aggregate dataclasses Each site reads from a source where the shape is genuinely unknown at type level (TOML config, JSON wire payload, polymorphic log entry, handler-map dispatch table). The `Metadata: TypeAlias = dict[str, Any]` catch-all is the correct type here. ## Per-site classification | File:line | Aggregate | Justification | |---|---|---| | (full table here) | EOF ``` **COMMIT:** `docs(audit): classify remaining .get() sites as collapsed-codepath (FR6)` **GIT NOTE:** Per-site classification of the remaining `.get('key', default)` and `['key']` sites. Each site is documented as "collapsed codepath" with a justification (TOML config, JSON wire, polymorphic log, handler-map). The `Metadata: TypeAlias = dict[str, Any]` catch-all is preserved for these sites. **ROLLBACK:** `git revert HEAD` (no code changed; documentation only). ### Task 11.2: Verify VC8 (no regression in audit gates) ```bash uv run python scripts/audit_weak_types.py --strict uv run python scripts/generate_type_registry.py --check uv run python scripts/audit_main_thread_imports.py uv run python scripts/audit_no_models_config_io.py uv run python scripts/audit_code_path_audit_coverage.py --input-dir docs/reports/code_path_audit/latest --strict uv run python scripts/audit_exception_handling.py --strict uv run python scripts/audit_optional_in_3_files.py --strict # Expect: all exit 0 (or only pre-existing failures documented in Phase 0) ``` **End of Phase 11.** ## Phase 12: Verification + end-of-track (1 task, 3 commits) ### Task 12.1: Run all 10 VCs + write TRACK_COMPLETION report **Run all VCs:** ```bash # VC1: Metadata unchanged git grep "^Metadata:" src/type_aliases.py # Expect: Metadata: TypeAlias = dict[str, Any] # VC2: Each new dataclass is its OWN @dataclass(frozen=True, slots=True) git grep -A 1 "^class CommsLogEntry\|^class HistoryMessage\|^class ToolDefinition\|^class RAGChunk\|^class SessionInsights\|^class DiscussionSettings\|^class CustomSlice\|^class MMAUsageStats\|^class ProviderPayload\|^class UIPanelConfig\|^class PathInfo" src/type_aliases.py src/rag_engine.py src/mcp_client.py src/performance_monitor.py src/log_registry.py # Expect: each followed by @dataclass(frozen=True, slots=True) # VC3: Existing dataclasses reused git grep "class Ticket\|class FileItem\|class ToolCall\|class ChatMessage\|class UsageStats\|class ContextPreset\|class MCPServerConfig" src/models.py src/openai_schemas.py # Expect: all exist # VC4: 107 .get('key', ...) sites on known aggregates replaced git grep -E "\.get\('[a-z_]+'," HEAD -- 'src/*.py' | wc -l # Expect: < 30 (only collapsed-codepath sites from Phase 11) # VC5: 106 ['key'] subscript sites on known aggregates replaced git grep -E "\[[ ]*'[a-z_]+'[ ]*\]" HEAD -- 'src/*.py' | wc -l # Expect: ~80 (only collapsed-codepath sites from Phase 11) # VC6: 60+ tests 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 tests/test_discussion_settings.py tests/test_custom_slice.py tests/test_mma_usage_stats.py tests/test_provider_payload.py tests/test_ui_panel_config.py tests/test_path_info.py tests/test_metadata_dataclass_aux.py -v # Expect: all pass (60+ tests across 12 files) # VC7: Effective codepaths drops by >= 2 orders of magnitude uv run python -c " import sys sys.path.insert(0, 'scripts/code_path_audit') sys.path.insert(0, 'src') from code_path_audit import build_pcg from code_path_audit_ssdl import count_branches_in_function pcg = build_pcg('src').data metadata_consumers = pcg.consumers.get('Metadata', []) total = sum(2 ** count_branches_in_function(f, 'src') for f in metadata_consumers) print(f'Final effective codepaths: {total:.3e} (baseline 4.014e+22)') " # Expect: < 1e+20 # VC8: All 7 audit gates pass (re-run from Phase 11.2) # Expect: all exit 0 # VC9: 10/11 batched test tiers PASS uv run python scripts/run_tests_batched.py # Expect: 10/11 PASS (RAG flake acceptable) ``` **Write the TRACK_COMPLETION report** at `docs/reports/TRACK_COMPLETION_metadata_promotion_20260624.md`: ```bash cat > docs/reports/TRACK_COMPLETION_metadata_promotion_20260624.md << 'EOF' # TRACK COMPLETION: metadata_promotion_20260624 **Date:** $(date -u +%Y-%m-%d) **Track ID:** metadata_promotion_20260624 **Status:** SHIPPED **Author:** Tier 2 (executed the Tier 1 exhaustive plan) ## Summary Promoted 11 NEW per-aggregate `@dataclass(frozen=True, slots=True)` classes: - CommsLogEntry, HistoryMessage, ToolDefinition, SessionInsights, DiscussionSettings, CustomSlice, MMAUsageStats, ProviderPayload, UIPanelConfig, PathInfo (in src/type_aliases.py) - RAGChunk (in src/rag_engine.py) - ASTNode, SearchResult, MCPToolResult (in src/mcp_client.py) - PerformanceMetrics (in src/performance_monitor.py) - SessionInfo, SessionMetadata (in src/log_registry.py) Reused 8 EXISTING dataclasses unchanged: Ticket, FileItem, ToolCall, ChatMessage, UsageStats, ContextPreset, MCPServerConfig (in src/models.py and src/openai_schemas.py). Migrated ~213 access sites across 9 consumer files from `.get('key', default)` / `['key']` to direct field access. `Metadata: TypeAlias = dict[str, Any]` UNCHANGED — preserved as the catch-all for collapsed codepaths (TOML config, JSON wire, polymorphic log, handler-map). ## Verification results | VC | Criterion | Result | |---|---|---| | VC1 | Metadata unchanged | PASS | | VC2 | Each new dataclass is its OWN @dataclass | PASS | | VC3 | Existing dataclasses reused | PASS | | VC4 | 107 .get() sites on known aggregates replaced | PASS (< 30 remain; all classified as collapsed) | | VC5 | 106 ['key'] subscript sites on known aggregates replaced | PASS (~80 remain; all classified as collapsed) | | VC6 | 60+ regression-guard tests pass | PASS | | VC7 | Effective codepaths < 1e+20 | PASS (was 4.014e+22) | | VC8 | All 7 audit gates pass --strict | PASS | | VC9 | 10/11 batched tiers PASS | PASS | | VC10 | TRACK_COMPLETION written | PASS | ## Effective codepaths metric Baseline: 4.014e+22 Final: $()e+ Drop: $() orders of magnitude ## Per-phase progress [Phase-by-phase table here] ## Collapsed-codepath classification [Reference to /tmp/collapsed_codepath_classification.md] ## Pre-existing failures [Any pre-existing test failures documented at track start] ## Lessons learned [Per-phase observations] EOF ``` **COMMIT (3 commits):** 1. `conductor(state): metadata_promotion_20260624 SHIPPED` (updates `state.toml` to `status = "completed"`, `current_phase = "complete"`, all phases `completed`) 2. `docs(reports): TRACK_COMPLETION_metadata_promotion_20260624` (the new report) 3. `conductor(tracks): update metadata_promotion_20260624 row` (updates `conductor/tracks.md`) **End of Phase 12. Track SHIPPED.** ## Tier 3 hard rules (DO NOT VIOLATE) 1. **Do NOT use `git restore`, `git checkout --`, or `git reset`** — banned per AGENTS.md. If you need to revert, use `git revert ` (one atomic commit per revert). 2. **Do NOT use the native `edit` tool on Python files** — it destroys 1-space indentation. Use `manual-slop_edit_file`, `manual-slop_py_update_definition`, `manual-slop_py_add_def`, or `manual-slop_set_file_slice`. 3. **Do NOT add comments to source code** — banned per AGENTS.md. Documentation lives in `/docs`. 4. **Do NOT create new `src/.py` files** — banned per AGENTS.md hard rule. Helpers go in the parent module. 5. **Do NOT skip a failing test with `@pytest.mark.skip`** — fix the bug instead. If you can't, report to Tier 2. 6. **Do NOT batch commits** — one atomic commit per task. Per-task commits enable precise rollback. 7. **Do NOT improvise decisions not in the plan** — if the plan doesn't cover your situation, STOP and report to Tier 2. 8. **Do NOT exceed 5 nesting levels** — extract to functions if you hit the limit. 9. **Do NOT modify `src/code_path_audit*.py`** — the audit infrastructure is correct. 10. **Do NOT promote `Metadata: TypeAlias = dict[str, Any]` itself** — it's preserved as the catch-all. 11. **NO-OP CLASSIFICATION BAN (added 2026-06-25 after a Tier 2 failure)** — Do NOT classify any planned task as "no-op" without explicit user authorization in the same session. If a planned migration site exists in the plan's Task table, MIGRATE it. The plan is the contract; FR2 is a rule in the spec; the plan's tasks are what you execute. If FR2 and the plan conflict at a specific site: - Read the site's variable type via `git grep -B 5 -A 5 ` to determine if it's a dict or a dataclass instance. - If it's a dataclass instance: migrate to direct field access per the plan. - If it's a dict at a genuinely collapsed codepath (TOML config, JSON wire, handler-map): migrate anyway by inserting `entry = .from_dict(entry)` (or equivalent) BEFORE the access site, OR classify the SPECIFIC site as collapsed-codepath and document in the per-site commit message (do NOT classify the whole phase). - If you can't decide: STOP. Write a 1-sentence question. Wait for the user. Do NOT invent a reconciliation. 12. **METRIC REGRESSION REVERT (added 2026-06-25)** — After every consumer-migration phase (Phases 1-10), run: ```bash uv run python -c " import sys sys.path.insert(0, 'scripts/code_path_audit') sys.path.insert(0, 'src') from code_path_audit import build_pcg from code_path_audit_ssdl import count_branches_in_function pcg = build_pcg('src').data metadata_consumers = pcg.consumers.get('Metadata', []) total = sum(2 ** count_branches_in_function(f, 'src') for f in metadata_consumers) print(f'Post-Phase-N effective codepaths: {total:.3e}') " ``` If the metric did NOT decrease vs. the previous measurement, `git revert ` IMMEDIATELY. Do NOT add a followup task to "investigate". Do NOT rationalize in the commit message or TRACK_COMPLETION. The 4.014e+22 → < 1e+20 drop is the success criterion; if it's not happening, the implementation is wrong; revert and report. ## Per-phase Tier 2 review checklist Before approving each phase, Tier 2 verifies: 1. All tasks in the phase have commits. 2. All test files for the new dataclasses exist and pass. 3. The pre-phase git grep counts decreased by the expected amount (e.g., Phase 1 should remove ~50 `.get('id', default)` sites). 4. The audit gates (`audit_weak_types.py --strict`, `audit_main_thread_imports.py`, etc.) still pass. 5. The batched test suite (`scripts/run_tests_batched.py`) still passes 10/11 tiers. 6. The effective codepaths metric decreased (or held steady for design-only phases). **If metric did NOT decrease after a consumer-migration phase (1-10), REVERT that phase commit immediately per Hard Rule #12. Do NOT approve.** If any check fails, Tier 2 REVERTS the phase commit and reports to the user. ## Anti-pattern guard (per AGENTS.md) If you observe any of these patterns in your own work, STOP and re-read AGENTS.md: 1. **The Deduction Loop**: running a test 4+ times in one investigation. STOP after 2 failures. 2. **The Report-Instead-of-Fix Pattern**: writing a 200-line status report instead of fixing. 3. **The Scope-Creep Track-Doc Pattern**: writing a 5-phase spec for a 1-line fix. 4. **The Inherited-Cruft Pattern**: trying to "fix" a broken file from a previous agent. 5. **No Diagnostic Noise in Production**: `sys.stderr.write` lines in `src/*.py` are technical debt. 6. **The "I Am Not Going To Attempt Another Fix" Surrender**: only surrender after 5-step protocol. 7. **The Verbose-Commit-Message Pattern**: commit messages > 15 lines are reports. 8. **The Isolated-Pass Verification Fallacy**: verifying in isolation but not in batch. ## See also - `conductor/tracks/metadata_promotion_20260624/spec.md` — the corrected spec (rewritten 2026-06-25) - `conductor/tracks/metadata_promotion_20260624/metadata.json` — the corrected metadata - `conductor/code_styleguides/type_aliases.md` §2.5 — the new "per-aggregate dataclass" rule - `docs/reports/PLANNING_CORRECTION_metadata_promotion_20260625.md` — the planning-correction rationale - `conductor/code_styleguides/data_oriented_design.md` — canonical DOD reference - `conductor/code_styleguides/error_handling.md` — `Result[T]` convention - `conductor/code_styleguides/python.md` — Python style (1-space indent, CRLF, no comments) - `conductor/workflow.md` — task workflow + commit discipline - `src/openai_schemas.py` — canonical per-aggregate dataclass pattern - `src/models.py:533` — `FileItem` canonical in-module dataclass pattern - `src/models.py:302` — `Ticket` canonical dataclass with legacy `.get()` removal example - `docs/reports/SSDL_CAMPAIGN_ABORTED_20260624.md` — the post-mortem that established the type-dispatch thesis