diff --git a/conductor/tracks/metadata_promotion_20260624/plan.md b/conductor/tracks/metadata_promotion_20260624/plan.md index 5079d8f4..83136764 100644 --- a/conductor/tracks/metadata_promotion_20260624/plan.md +++ b/conductor/tracks/metadata_promotion_20260624/plan.md @@ -1,282 +1,1770 @@ -# Plan: metadata_promotion_20260624 +# Plan: metadata_promotion_20260624 (EXHAUSTIVE TIER 3 EXECUTION CONTRACT) -> **CORRECTED 2026-06-25 (Tier 1 audit).** The original plan (commit `e50bebdd`, 2026-06-25) proposed a single shared `@dataclass(frozen=True, slots=True) Metadata` with ~200 fields for all 5 sub-aggregates. That proposal was REJECTED on 2026-06-25 (user direction): each sub-aggregate is its OWN dataclass with its OWN fields. The corrected plan has 12 phases (one per sub-aggregate), uses existing dataclasses where they exist (`Ticket`, `FileItem`, `ToolCall`, `ChatMessage`, `UsageStats`), and adds new per-aggregate dataclasses for the 8 aggregates that don't have one yet. See `docs/reports/PLANNING_CORRECTION_metadata_promotion_20260625.md` for the full rationale. +> **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. -13 phases, 30-35 tasks, 30+ atomic commits. Per-task TDD red-first. Tier 3 workers execute; Tier 2 reviews per phase. +## 0. Pre-flight (Tier 2 runs before Tier 3 starts) -## Phase 0: Design the per-aggregate dataclasses + add regression-guard test stubs (5 tasks, 5 commits) +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. -**Focus:** Add the NEW dataclasses to `src/type_aliases.py` (the type-system aggregates that don't have a parent module); reuse the existing dataclasses in `src/models.py` and `src/openai_schemas.py`. No consumer migration yet. +```bash +# 0.1 Confirm the working tree is clean +git status --short +# Expect: no output (clean) -- [ ] **Task 0.1** [Tier 3]: Add NEW dataclasses to `src/type_aliases.py`. - - WHERE: `src/type_aliases.py` (current 30 lines) - - WHAT: - - Add `@dataclass(frozen=True, slots=True) class CommsLogEntry` with `ts, role, kind, direction, model, source_tier, content, error` (8 fields, all with defaults) - - Add `@dataclass(frozen=True, slots=True) class HistoryMessage` with `role, content, tool_calls, tool_call_id, name, ts` (6 fields) - - Add `@dataclass(frozen=True, slots=True) class ToolDefinition` with `name, description, parameters, auto_start` (4 fields) - - Add `@dataclass(frozen=True, slots=True) class SessionInsights` with `total_tokens, call_count, burn_rate, session_cost, completed_tickets, efficiency` (6 fields) - - Add `@dataclass(frozen=True, slots=True) class DiscussionSettings` with `temperature, top_p, max_output_tokens` (3 fields) - - Add `@dataclass(frozen=True, slots=True) class CustomSlice` with `tag, comment, start_line, end_line` (4 fields) - - Add `@dataclass(frozen=True, slots=True) class MMAUsageStats` with `model, input, output` (3 fields) - - Add `@dataclass(frozen=True, slots=True) class ProviderPayload` with `script, args, output, source_tier` (4 fields) - - Add `@dataclass(frozen=True, slots=True) class UIPanelConfig` with `separate_message_panel, separate_response_panel, separate_tool_calls_panel` (3 fields) - - Add `@dataclass(frozen=True, slots=True) class PathInfo` with `logs_dir, scripts_dir, project_root` (3 nested fields) - - Each dataclass has a paired `to_dict()` (for JSON serialization) and `from_dict()` classmethod (filters unknown keys, per FR5) - - KEEP `Metadata: TypeAlias = dict[str, Any]` UNCHANGED (the catch-all for collapsed codepaths) - - KEEP `CommsLog: TypeAlias = list[CommsLogEntry]`, `History: TypeAlias = list[HistoryMessage]`, `FileItems: TypeAlias = list[FileItem]` (the list aliases still work; the element types are now per-aggregate dataclasses) - - KEEP `JsonValue`, `JsonPrimitive`, `CommsLogCallback`, `FileItemsDiff` unchanged - - HOW: `manual-slop_edit_file` for surgical edits (or `write_file` if the file is being substantially restructured) - - SAFETY: `ast.parse` OK; `from src.type_aliases import CommsLogEntry, HistoryMessage, ToolDefinition, SessionInsights, DiscussionSettings, CustomSlice, MMAUsageStats, ProviderPayload, UIPanelConfig, PathInfo` OK; constructors work -- [x] **COMMIT:** `refactor(type_aliases): add per-aggregate dataclasses (CommsLogEntry, HistoryMessage, ToolDefinition, ...)` [bacddc85] (Tier 3) -- [x] **GIT NOTE:** NEW dataclasses added to `src/type_aliases.py`. `Metadata: TypeAlias = dict[str, Any]` is UNCHANGED (the catch-all for collapsed codepaths). No consumer migration yet. +# 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) -- [ ] **Task 0.2** [Tier 3]: Add `RAGChunk` dataclass to `src/rag_engine.py`. - - WHERE: `src/rag_engine.py` (the parent module for RAG) - - WHAT: `@dataclass(frozen=True, slots=True) class RAGChunk` with `document, path, score, metadata` (4 fields, all with defaults); paired `to_dict()` / `from_dict()` - - HOW: `manual-slop_edit_file` - - SAFETY: `from src.rag_engine import RAGChunk` OK; constructor works -- [x] **COMMIT:** `feat(rag_engine): add RAGChunk dataclass` [bacddc85] (Tier 3) -- [x] **GIT NOTE:** NEW dataclass added to `src/rag_engine.py`. No consumer migration yet. +# 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 -- [ ] **Task 0.3** [Tier 3]: Audit and complete `ContextPreset` schema in `src/models.py`. - - WHERE: `src/models.py` (the parent module for ContextPreset) - - WHAT: `ContextPreset` exists at `src/models.py:932` but is partial. Add missing fields based on access patterns: `name, files (FileItems), screenshots (list[str])` minimum; audit the actual usage and add any other required fields; ensure paired `to_dict()` / `from_dict()` - - HOW: `manual-slop_edit_file` - - SAFETY: existing `ContextPreset` consumers continue to work; the `to_dict()` round-trip is lossless -- [ ] **COMMIT:** `refactor(models): complete ContextPreset schema with missing fields` (Tier 3) -- [ ] **GIT NOTE:** `ContextPreset` schema extended. Existing consumers unchanged. +# 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" -- [ ] **Task 0.4** [Tier 3]: Create `tests/test_metadata_dataclass.py` (split into per-aggregate test files per FR G7). - - WHERE: NEW FILES: `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_context_preset_schema.py` - - WHAT: 5+ tests per file: constructor with kwargs, field access, frozen (raises `FrozenInstanceError`), `to_dict()` / `from_dict()` round-trip, equality, hashability, default values - - HOW: `write_file` per file - - SAFETY: `uv run pytest tests/test_comms_log_entry.py -v` shows 5/5 pass (and similarly for the other 11 files) -- [x] **COMMIT:** `test(type_aliases): add per-aggregate dataclass regression-guard suite` [bacddc85] (Tier 3) -- [x] **GIT NOTE:** 12 test files, 5+ tests each. The consumer migration is in subsequent phases; this commit only adds the new dataclasses + tests. +# 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 -- [ ] **Task 0.5** [Tier 2]: Document the FR6 collapsed-codepath classification rule. - - WHERE: `conductor/code_styleguides/type_aliases.md` (small clarification, NOT a rewrite) - - WHAT: Add a one-paragraph "When to promote to a per-aggregate dataclass" rule: when a sub-aggregate has stable distinct fields, promote it to its OWN dataclass; do NOT share one mega-dataclass across concepts; `Metadata: TypeAlias = dict[str, Any]` is preserved for collapsed codepaths (TOML config, generic JSON parsing, polymorphic log dumping) only. Reference this track's correction as the canonical example. - - HOW: `manual-slop_edit_file` - - SAFETY: styleguide is consistent with the corrected design -- [ ] **COMMIT:** `docs(styleguides): clarify when to promote to per-aggregate dataclass` (Tier 2) -- [ ] **GIT NOTE:** Styleguide clarification. The corrected design is: per-aggregate dataclasses for known sub-aggregates; `Metadata: dict[str, Any]` for collapsed codepaths only. +# 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 +``` -## Phase 1: Migrate `Ticket` consumers (~30 sites, 2 commits) +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. -**Focus:** `Ticket` is already a dataclass (`src/models.py:302`); just migrate the consumers from `t.get('id', '')` to `t.id`. The legacy `Ticket.get(key, default)` method can be removed at the end of this phase once no consumer calls it. +## Phase 0: Add NEW per-aggregate dataclasses (no consumer migration yet) -- [x] **Task 1.1** [Tier 3]: Migrate `src/gui_2.py` Ticket consumers. - - WHERE: `src/gui_2.py:1366-1438,1682` (the `_cb_*_ticket` and ticket-list rendering sites) - - WHAT: For each `t.get('id', '')`, `t.get('depends_on', [])`, `t.get('manual_block', False)`, `t.get('status')` → `t.id`, `t.depends_on`, `t.manual_block`, `t.status` - - HOW: `manual-slop_edit_file` per site - - SAFETY: Run `tests/test_ticket_queue.py` + `tests/test_per_ticket_model.py` + `tests/test_manual_block.py` + the new per-aggregate test files - - **RESULT:** No-op. Audit confirmed `self.active_tickets` is `list[Metadata]` (dicts, NOT Ticket dataclass) per `src/app_controller.py:1110` and the comment at `:3276` "Keep dicts for UI table". The gui_2.py sites operate on dicts and are correctly classified as Metadata collapsed-codepath per spec FR2. No migration needed. -- [x] **COMMIT:** No commit (no code changes). [no-op] -- [x] **GIT NOTE:** Audit-only. 35/35 tests pass. No migration needed. +**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. -- [x] **Task 1.2** [Tier 3]: Migrate `src/conductor_tech_lead.py` and `src/app_controller.py` Ticket consumers. - - WHERE: `src/conductor_tech_lead.py:125`; `src/app_controller.py:4810-4868` (the ticket-list mutation sites) - - WHAT: Same pattern as 1.1 - - HOW: `manual-slop_edit_file` per site - - SAFETY: Same as 1.1 - - **RESULT:** No-op. Audit confirmed all Ticket dataclass consumers (in `_spawn_worker`, `mutate_dag`, `multi_agent_conductor.run`) already use direct field access (`t.id`, `t.status`, `t.depends_on`, etc.). The `t.get('id', '')` style sites operate on dicts (from `conductor_tech_lead.topological_sort` returning `list[dict[str, Any]]` and from `self.active_tickets: list[Metadata]`), which are correctly classified as Metadata collapsed-codepath per spec FR2. -- [x] **COMMIT:** No commit (no code changes). [no-op] -- [x] **GIT NOTE:** Audit-only. 35/35 tests pass. No migration needed. -- [ ] **Task 1.3** [Tier 2]: Remove the legacy `Ticket.get(key, default)` method. - - WHERE: `src/models.py` (the `get` method on `Ticket`) - - WHAT: After all consumers have migrated, remove the `get` method - - HOW: `manual-slop_py_remove_def` - - SAFETY: Re-run the full batched test suite; no remaining `.get(key, default)` on Ticket consumers -- [ ] **COMMIT:** `refactor(models): remove legacy Ticket.get() method` (Tier 2) -- [ ] **GIT NOTE:** Legacy compat method removed. Direct field access is now the only path. +**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. -## Phase 2: Migrate `FileItem` consumers (~10 sites, 2 commits) +### Task 0.1: Add 10 NEW dataclasses to `src/type_aliases.py` -**Focus:** `FileItem` is already a dataclass (`src/models.py:533`); migrate the consumers. +**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) -- [x] **Task 2.1** [Tier 3]: Migrate `src/aggregate.py` FileItem consumers. - - WHERE: `src/aggregate.py:418,421` - - WHAT: `item.get('custom_slices', [])` → `item.custom_slices`; `item.get('content', '')` → `item.content` - - HOW: `manual-slop_edit_file` per site - - SAFETY: Run `tests/test_aggregate.py` + `tests/test_file_item_model.py` + the new per-aggregate test files - - **RESULT:** No-op. Audit confirmed `item` is `Metadata` dict (file_items parameter is `list[Metadata]`), NOT `FileItem` dataclass. Per spec FR2, dict-style sites that read from external sources are collapsed-codepath. No migration needed. -- [x] **COMMIT:** No commit (no code changes). [no-op] -- [x] **GIT NOTE:** Audit-only. 8 tests pass + 1 env-var skipped. No migration needed. +**Exact `new_string` for the file** (replaces all current content): -- [x] **Task 2.2** [Tier 3]: Migrate `src/ai_client.py` and `src/app_controller.py` FileItem consumers. - - WHERE: `src/ai_client.py:2565,2807,2898`; `src/app_controller.py:3508` - - WHAT: `fi.get('path', 'attachment')` → `fi.path`; `f['path'] for f in file_items` → `f.path for f in file_items` - - HOW: `manual-slop_edit_file` per site - - SAFETY: Same as 2.1 - - **RESULT:** No-op. Same as Task 2.1 — `fi` is multimodal content dict (not FileItem dataclass). `app_controller.py:3508` accesses already-converted strings. All FileItem dataclass consumers (in app_controller.py:3231-3237, 3401-3408, gui_2.py:369-378, 977-984) already use direct field access. -- [x] **COMMIT:** No commit (no code changes). [no-op] -- [x] **GIT NOTE:** Audit-only. No migration needed. +```python +from __future__ import annotations +from dataclasses import dataclass, field +from typing import Any, Callable, NamedTuple, TypeAlias -## Phase 3: Migrate `CommsLogEntry` consumers (~30 sites, 3 commits) +Metadata: TypeAlias = dict[str, Any] -**Focus:** New dataclass added in Phase 0; now wire it into the consumers. -- [x] **Task 3.1** [Tier 3]: Migrate `src/session_logger.py`. - - **RESULT:** No-op. Audit confirmed all access sites in `src/session_logger.py` operate on dicts (the session log entries are loaded from JSONL files; their shape is genuinely unknown at type level until parsed). Per spec FR2, these are collapsed-codepath. No migration needed in this phase. Future session logger work could optionally introduce CommsLogEntry dataclass at the I/O boundary (out of scope for this track). -- [x] **COMMIT:** No commit (no code changes). [no-op] -- [x] **GIT NOTE:** Audit-only. No migration needed. +@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 = "" -- [x] **Task 3.2-3.4** [Tier 3]: Migrate `src/multi_agent_conductor.py` (~20 sites) and `src/app_controller.py` CommsLogEntry section (~10 sites). - - **RESULT:** No-op. Same as Task 3.1 — all sites operate on dicts (session log entries + telemetry aggregations). These are correctly classified as collapsed-codepath per FR2. -- [x] **COMMIT:** No commit. [no-op] -- [x] **GIT NOTE:** Audit-only. + 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",)} -## Phase 4: NO-OP [see Phase 11 audit] + @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))}}) -**Focus:** UI-layer discussion history (NOT provider-side `ChatMessage`). -- [x] **Task 4.1-4.2** [Tier 3]: Migrate `src/gui_2.py` discussion UI sites. - - **RESULT:** No-op. The `entry['role']` style sites in `src/gui_2.py` operate on dict entries stored in `self.discussion_take_history` (list[dict]). These are UI-layer message lists, NOT HistoryMessage dataclass instances. Per FR2, collapsed-codepath. -- [x] **COMMIT:** No commit. [no-op] -- [x] **GIT NOTE:** Audit-only. +@dataclass(frozen=True, slots=True) +class HistoryMessage: + role: str = "" + content: Any = None + tool_calls: Any = None + tool_call_id: str = "" + name: str = "" + ts: str = "" -## Phase 5: NO-OP + def to_dict(self) -> Metadata: + return {k: v for k, v in self.__dict__.items() if v not in (None, "", [], {}, 0, 0.0, False)} -**Focus:** ChatMessage in per-vendor send paths. + @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))}}) -- [x] **Task 5.1-5.4** [Tier 3]: Migrate `_send_anthropic`, `_send_deepseek`, `_send_grok`, `_send_qwen`, `_send_minimax`, `_send_llama`. - - **RESULT:** No-op. The per-vendor send paths were migrated in `code_path_audit_phase_3_provider_state_20260624` to use `provider_state.get_history("...").append(...)` and direct dict assignment `history.append({"role": ..., "content": ...})`. The history items are dicts (per `ProviderHistory.messages: list[HistoryMessage]` where HistoryMessage is the NEW dataclass with `from_dict` support, but the actual items in the list are still dicts for backward compatibility with the API request layers). ChatMessage dataclass is in `src/openai_schemas.py:48` and used by some sites but the API request serialization layers (anthropic, deepseek, etc.) consume dicts. -- [x] **COMMIT:** No commit. [no-op] -- [x] **GIT NOTE:** Audit-only. -## Phase 6: NO-OP +@dataclass(frozen=True, slots=True) +class ToolDefinition: + name: str = "" + description: str = "" + parameters: Any = None + auto_start: bool = False -**Focus:** UsageStats in per-call usage aggregation. + def to_dict(self) -> Metadata: + return {k: v for k, v in self.__dict__.items() if v not in (None, "", [], {}, 0, 0.0, False)} -- [x] **Task 6.1** [Tier 3]: Migrate `src/app_controller.py:2299-2309`. - - **RESULT:** No-op. The `u.get('input_tokens', 0)` sites in the `mma_tier_usage` aggregation operate on dicts constructed from session log entries (which are dicts at the I/O boundary). UsageStats dataclass is in `src/openai_schemas.py:68` and is used for the immediate SDK response (via `NormalizedResponse.usage: UsageStats`), but the per-tier rollup accumulates dicts from the session log. -- [x] **COMMIT:** No commit. [no-op] -- [x] **GIT NOTE:** Audit-only. + @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))}}) -## Phase 7: NO-OP -**Focus:** ToolCall in tool loop section. +@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 -- [x] **Task 7.1-7.2** [Tier 3]: Migrate `src/ai_client.py` + `src/mcp_client.py` tool loop section. - - **RESULT:** No-op. The tool loop section uses raw dicts for tool calls (matches the OpenAI/Anthropic API response shapes). ToolCall dataclass exists in `src/openai_schemas.py:32` and is used by some sites (e.g., `_build_x_request` kwargs), but the API serialization layers consume dicts. -- [x] **COMMIT:** No commit. [no-op] -- [x] **GIT NOTE:** Audit-only. + def to_dict(self) -> Metadata: + return dict(self.__dict__) -## Phase 8: NO-OP + @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))}}) -**Focus:** ToolDefinition in per-vendor tool builders. -- [x] **Task 8.1-8.2** [Tier 3]: Migrate `src/mcp_client.py` (~70 sites) + `src/ai_client.py` per-vendor tool builders (~24 sites). - - **RESULT:** No-op. The MCP tool definitions are read from the MCP protocol (raw dicts at the wire boundary). The per-vendor tool builders (`_build_anthropic_tools`, `_get_deepseek_tools`, etc.) consume ToolDefinition-shaped dicts and convert to the vendor-specific format. Promoting the wire-boundary dict to ToolDefinition dataclass is out of scope per spec FR2. -- [x] **COMMIT:** No commit. [no-op] -- [x] **GIT NOTE:** Audit-only. +@dataclass(frozen=True, slots=True) +class DiscussionSettings: + temperature: float = 0.7 + top_p: float = 1.0 + max_output_tokens: int = 0 -## Phase 9: NO-OP + def to_dict(self) -> Metadata: + return dict(self.__dict__) -**Focus:** RAGChunk consumers. + @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))}}) -- [x] **Task 9.1** [Tier 3]: Migrate `src/rag_engine.py`, `src/aggregate.py`, `src/app_controller.py` RAG chunk consumers. - - **RESULT:** No-op. `chunk.get('document', '')` sites operate on dicts returned by `_parse_search_response_result` (which is `Result[List[Dict[str, Any]]]`). Promoting the wire-boundary dict to RAGChunk dataclass would require changing the search response parsing layer; out of scope. -- [x] **COMMIT:** No commit. [no-op] -- [x] **GIT NOTE:** Audit-only. -## Phase 10: NO-OP +@dataclass(frozen=True, slots=True) +class CustomSlice: + tag: str = "" + comment: str = "" + start_line: int = 0 + end_line: int = 0 -**Focus:** Small-batch aggregates (SessionInsights, DiscussionSettings, CustomSlice, MMAUsageStats, ProviderPayload, UIPanelConfig, PathInfo). + def to_dict(self) -> Metadata: + return dict(self.__dict__) -- [x] **Task 10.1-10.2** [Tier 3]: Migrate `src/gui_2.py` small-batch consumers + `src/app_controller.py` ProviderPayload, UIPanelConfig, PathInfo. - - **RESULT:** No-op. Same pattern as Phases 3-9 — all sites operate on dicts (project config from manual_slop.toml, UI state, telemetry aggregations). Per FR2, collapsed-codepath. -- [x] **COMMIT:** No commit. [no-op] -- [x] **GIT NOTE:** Audit-only. + @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))}}) -## Phase 11: `Metadata` collapsed-codepath audit (FR6, 1 task, 1 commit) -**Focus:** Every remaining `.get('key', default)` site is classified as either (a) "promoted to per-aggregate dataclass → migrated" or (b) "collapsed codepath → keeps Metadata with documented justification." +@dataclass(frozen=True, slots=True) +class MMAUsageStats: + model: str = "unknown" + input: int = 0 + output: int = 0 -- [ ] **Task 11.1** [Tier 2]: Audit remaining `.get('key', default)` sites. - - WHERE: `git grep -nE "\.get\('[a-z_]+'," HEAD -- 'src/*.py'` - - WHAT: Per-site classification: (a) promoted + migrated (drop from the report), (b) collapsed-codepath (document the justification in the commit message). The expected collapsed-codepath sites are: `self.project.get('paths', {})`, `self.project.get('conductor', {})`, `self.project.get('context_presets', {})`, `self.project.get('discussion', {})`, `gui_cfg.get(...)` (if `UIPanelConfig` doesn't cover it), etc. - - HOW: Manual review + commit message -- [ ] **COMMIT:** `docs(audit): classify remaining .get() sites as promoted or collapsed-codepath` (Tier 2) -- [ ] **GIT NOTE:** Per-site classification. The remaining `.get()` sites are all justified collapsed-codepaths. + 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) -**Focus:** Run all 10 VCs; write `TRACK_COMPLETION`; update `state.toml` + `tracks.md`. +### Task 12.1: Run all 10 VCs + write TRACK_COMPLETION report -- [ ] **Task 12.1** [Tier 2]: - - WHERE: terminal + `docs/reports/TRACK_COMPLETION_metadata_promotion_20260624.md` (NEW) - - WHAT: - - VC1-VC10 verification (see spec.md §Verification Criteria) - - Re-measure final effective codepaths (expected: 4.014e+22 → < 1e+20) - - Run all 7 audit gates - - Run the full batched test suite - - Document the drop in the TRACK_COMPLETION report - - HOW: Run each command, capture output, write the report - - COMMIT: 3 commits: state, TRACK_COMPLETION, tracks.md update - - VERIFY: All 10 VCs pass - -## Commit Log (Expected, 30-35 atomic commits) - -1. (Phase 0) `refactor(type_aliases): add per-aggregate dataclasses (CommsLogEntry, HistoryMessage, ToolDefinition, ...)` -2. (Phase 0) `feat(rag_engine): add RAGChunk dataclass` -3. (Phase 0) `refactor(models): complete ContextPreset schema with missing fields` -4. (Phase 0) `test(type_aliases): add per-aggregate dataclass regression-guard suite` -5. (Phase 0) `docs(styleguides): clarify when to promote to per-aggregate dataclass` -6. (Phase 1) `refactor(gui_2): migrate Ticket access sites to direct field access` -7. (Phase 1) `refactor(app_controller,conductor_tech_lead): migrate Ticket access sites` -8. (Phase 1) `refactor(models): remove legacy Ticket.get() method` -9. (Phase 2) `refactor(aggregate): migrate FileItem access sites` -10. (Phase 2) `refactor(ai_client,app_controller): migrate FileItem access sites` -11. (Phase 3) `refactor(session_logger): migrate CommsLogEntry access sites` -12. (Phase 3) `refactor(multi_agent_conductor): migrate CommsLogEntry access sites` -13. (Phase 3) `refactor(app_controller): migrate CommsLogEntry access sites` -14. (Phase 4) `refactor(gui_2): migrate HistoryMessage access sites` -15. (Phase 5) `refactor(ai_client): migrate ChatMessage access sites in _send_anthropic/_send_deepseek` -16. (Phase 5) `refactor(ai_client): migrate ChatMessage access sites in _send_grok/_send_qwen` -17. (Phase 5) `refactor(ai_client): migrate ChatMessage access sites in _send_minimax/_send_llama` -18. (Phase 6) `refactor(app_controller): migrate UsageStats access sites` -19. (Phase 7) `refactor(ai_client): migrate ToolCall access sites in tool loop section` -20. (Phase 7) `refactor(mcp_client): migrate ToolCall access sites in tool loop section` -21. (Phase 8) `refactor(mcp_client): migrate ToolDefinition access sites` -22. (Phase 8) `refactor(ai_client): migrate ToolDefinition access sites` -23. (Phase 9) `refactor(rag_engine,aggregate,app_controller): migrate RAGChunk access sites` -24. (Phase 10) `refactor(gui_2): migrate SessionInsights, DiscussionSettings, CustomSlice, MMAUsageStats` -25. (Phase 10) `refactor(app_controller): migrate ProviderPayload, UIPanelConfig, PathInfo` -26. (Phase 11) `docs(audit): classify remaining .get() sites as promoted or collapsed-codepath` -27. (Phase 12) `conductor(state): metadata_promotion_20260624 SHIPPED` -28. (Phase 12) `docs(reports): TRACK_COMPLETION_metadata_promotion_20260624` -29. (Phase 12) `conductor(tracks): update metadata_promotion_20260624 row` - -Plus per-task plan-update commits per the workflow. - -## Verification Commands (run at end of each phase + Phase 12) +**Run all VCs:** ```bash -# VC1: Metadata is unchanged +# VC1: Metadata unchanged git grep "^Metadata:" src/type_aliases.py # Expect: Metadata: TypeAlias = dict[str, Any] -# VC2: Each new sub-aggregate 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/ +# 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" src/ -# Expect: existing classes unchanged +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: only collapsed-codepath sites (FR2; documented in Phase 11 commit) +# 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: only legitimate non-aggregate uses +# Expect: ~80 (only collapsed-codepath sites from Phase 11) -# VC6: 60+ tests pass (5+ per new dataclass, 12 dataclasses) -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_context_preset_schema.py -v -# Expect: all pass +# 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 " @@ -288,40 +1776,140 @@ 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'Effective codepaths: {total:.3e} (baseline: 4.014e+22)') +print(f'Final effective codepaths: {total:.3e} (baseline 4.014e+22)') " # Expect: < 1e+20 -# VC8: 7 audit gates pass -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 -# All exit 0 +# VC8: All 7 audit gates pass (re-run from Phase 11.2) +# Expect: all exit 0 -# VC9: 10/11 batched tiers +# VC9: 10/11 batched test tiers PASS uv run python scripts/run_tests_batched.py -# Expect: 10/11 PASS +# Expect: 10/11 PASS (RAG flake acceptable) ``` -## Notes for Tier 3 workers +**Write the TRACK_COMPLETION report** at `docs/reports/TRACK_COMPLETION_metadata_promotion_20260624.md`: -- **Pattern consistency**: For each access site, the canonical pattern is `entry.field_name or default_value` for nullable fields, `entry.field_name` for required fields. -- **Per-aggregate dataclass reference**: `src/openai_schemas.py` (the canonical pattern for `ToolCall`, `ChatMessage`, `UsageStats`, `ToolCallFunction`, `NormalizedResponse`); `src/models.py:533` (`FileItem` with `to_dict()` / `from_dict()` round-trip). -- **Dynamic keys** (e.g., `entry[variable_name]` where the key is not a static string): keep as `entry.to_dict()[variable_name]` for those rare cases. The dataclass handles the common case. -- **Polymorphic construction** (e.g., `entry = {'role': 'user', 'content': 'hi'}`): replace with `entry = HistoryMessage(role='user', content='hi')`. If the dict is dynamic, use `entry = HistoryMessage.from_dict(raw_dict)`. -- **JSON serialization**: `json.dumps(entry.to_dict())` (not `json.dumps(entry)` which would fail on dataclass). -- **Indentation**: 1-space per level. -- **No comments** in source code (per AGENTS.md). -- **Per-phase regression-guard test runs**: after each phase, run the per-aggregate test files + the full batched test suite. If a phase causes a regression, REVERT the phase commit and investigate (don't try to fix forward). +```bash +cat > docs/reports/TRACK_COMPLETION_metadata_promotion_20260624.md << 'EOF' +# TRACK COMPLETION: metadata_promotion_20260624 -## Notes for Tier 2 reviewer +**Date:** $(date -u +%Y-%m-%d) +**Track ID:** metadata_promotion_20260624 +**Status:** SHIPPED +**Author:** Tier 2 (executed the Tier 1 exhaustive plan) -- The per-aggregate dataclasses are the central artifacts. After Phase 0, every new dataclass is importable. Each subsequent phase migrates the consumers in a specific file. -- The 4.01e22 metric drops per phase. Document the drop in the TRACK_COMPLETION report. -- If a migration breaks more than 2 tests, **revert** the phase commit and split into smaller phases. Don't accumulate broken state. -- The RAG test pre-existing flake is acceptable. Document it but don't try to fix. -- The classification in Phase 11 (collapsed-codepath vs promoted) is auditable; every remaining `.get()` site must have a justification in the commit message. \ No newline at end of file +## 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. + +## 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 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 \ No newline at end of file