tests/test_track_state_schema.py imports 'from src.models import
Metadata' and uses it as a dataclass (e.g. 'Metadata(id=..., created_at=...)').
After Phase 5, models.Metadata was undefined and __getattr__ returned
the type alias from src.type_aliases (which is dict[str, Any]). The
test then failed with 'TypeError: dict.__init__() got an unexpected
keyword argument created_at'.
This commit restores the legacy 'Metadata = TrackMetadata' alias at
the top of models.py so 'from src.models import Metadata' resolves to
the TrackMetadata dataclass (the original behavior). New code should
import directly: 'from src.mma import TrackMetadata'.
Also removes the now-redundant __getattr__ entry for Metadata (it's
eager now).
Tests verified:
tests/test_track_state_schema.py (5/5 PASS; was 2/5 before this fix)
After 11 class moves (Phases 3a-3i) + 1 deletion (Phase 4), this commit
reduces src/models.py from 1044 lines (original) / 768 lines (pre-Phase 3b)
to 135 lines. The remaining content is:
- DEFAULT_TOOL_CATEGORIES: the canonical tool list grouped for
the UI's category filter (the ONLY non-Pydantic constant)
- _create_generate_request + _create_confirm_request: the Pydantic
proxy classes for the API hook subsystem
- _PYDANTIC_CLASS_FACTORIES: registry for the Pydantic proxies
- __getattr__: lazy re-exports for ALL 30+ moved classes + PROVIDERS
Removed:
- All 11 class definitions (MMA Core, FileItem + 4 file-related,
Tool + ToolPreset + BiasProfile, 2 editor configs, WorkspaceProfile,
4 MCP config classes + load_mcp_config, ProjectContext + 5 sub)
- All 3 config IO function definitions (load_config_from_disk,
save_config_to_disk, _clean_nones, parse_history_entries)
- All 5 eager re-export blocks at the top (they triggered tomli_w
loading at import time via the personas import; the lazy __getattr__
breaks the cycle)
- AGENT_TOOL_NAMES (deleted in Phase 4)
The lazy __getattr__ keeps the 'from src.models import X' pattern
working for legacy callers. New code should import directly from
the subsystem files (src.mma, src.project, src.project_files,
src.tool_presets, src.tool_bias, src.external_editor, src.mcp_client,
src.workspace_manager, src.personas).
Side benefit: the pre-existing test
tests/test_models_no_top_level_tomli_w.py::test_models_does_not_import_tomli_w_at_module_level
now PASSES. Before Phase 5 it failed because the eager
'from src.personas import Persona' triggered tomli_w loading. The
lazy __getattr__ for Persona only loads tomli_w when 'models.Persona'
is actually accessed (not on a bare 'import src.models').
Verification: VC10
wc -l src/models.py # 135 lines (well under the 1044-line original;
# 30-line target was aspirational; the lazy
# __getattr__ for 30+ moved classes is the
# dominant cost)
Measure-Object -Line on src/models.py # 135
Tests verified (84/85 PASS; 1 pre-existing failure unrelated):
tests/test_mcp_config.py (3/3 PASS)
tests/test_tool_preset_manager.py (4/4 PASS)
tests/test_bias_models.py (3/3 PASS)
tests/test_tool_bias.py (3/3 PASS)
tests/test_external_editor.py (17/17 PASS)
tests/test_workspace_manager.py (3/3 PASS)
tests/test_models_no_top_level_tomli_w.py (3/3 PASS) [previously 1 FAIL]
tests/test_project_context_20260627.py (10/10 PASS)
tests/test_file_item_model.py (4/4 PASS)
tests/test_view_presets.py (4/4 PASS)
tests/test_context_presets_models.py (3/3 PASS)
tests/test_presets.py (5/5 PASS)
tests/test_persona_models.py (2/2 PASS)
tests/test_persona_manager.py (3/3 PASS)
tests/test_arch_boundary_phase2.py (5/6 PASS; 1 pre-existing FAIL
unrelated: test_rejection_prevents_dispatch
is a dialog-mock issue)
tests/test_mcp_tool_specs.py (10/10 PASS)
AGENT_TOOL_NAMES was a hardcoded snapshot of mcp_tool_specs.tool_names()
in src/models.py. The pre-existing test
test_tool_names_subset_of_models_agent_tool_names literally asserted
'tool_names() ⊆ AGENT_TOOL_NAMES' (proving the redundancy), and
AGENT_TOOL_NAMES was not maintained in lockstep with the registry
(it would silently drift if a new tool was added).
This commit:
1. Deletes AGENT_TOOL_NAMES from src/models.py (replaced by an
explanatory comment in the Constants section).
2. Updates 3 consumer sites in src/app_controller.py:
- 'for t in models.AGENT_TOOL_NAMES' -> 'for t in mcp_tool_specs.tool_names()'
- (in 2 methods: __init__ + a setter)
3. Updates 2 test sites in tests/test_arch_boundary_phase2.py:
- 'from src.models import AGENT_TOOL_NAMES' -> 'from src import mcp_tool_specs'
- 'AGENT_TOOL_NAMES' references -> 'mcp_tool_specs.tool_names()'
4. Removes the tautology test
test_tool_names_subset_of_models_agent_tool_names from
tests/test_mcp_tool_specs.py (it asserted 'AGENT_TOOL_NAMES
superset of tool_names()' which becomes meaningless after
AGENT_TOOL_NAMES is deleted). Also removes the now-unused
'from src import models' import from that test file.
Verification: VC9
git grep 'AGENT_TOOL_NAMES' -- 'src/*.py' 'tests/*.py' # 0 hits
from src import mcp_tool_specs
mcp_tool_specs.tool_names() # returns the canonical 45 tools
from src.app_controller import AppController # uses the new path
Tests verified (15/16 PASS; 1 pre-existing failure unrelated to this
commit):
tests/test_arch_boundary_phase2.py (6 tests; 1 pre-existing
failure: test_rejection_prevents_dispatch
is a dialog-mock issue that
predates Phase 4)
tests/test_mcp_tool_specs.py (10 tests; the tautology test was removed;
the remaining 10 pass)
Per the 4-criteria decision rule: MCP config classes (MCPServerConfig,
MCPConfiguration, VectorStoreConfig, RAGConfig) + load_mcp_config are
used by mcp_client + api_hooks + app_controller (3 systems) but
they are tightly coupled to the MCP subsystem's data layer. The test
file tests/test_mcp_config.py exists. Per the v2 spec: MERGE into
the existing src/mcp_client.py (the destination file IS the MCP
subsystem; the data layer belongs with the dispatcher).
This commit:
1. Adds MCPServerConfig + MCPConfiguration + VectorStoreConfig +
RAGConfig + load_mcp_config class/function definitions to
src/mcp_client.py at the top (after the imports + before the
mutating tools sentinel).
2. Removes the same class defs from src/models.py.
3. Adds lazy re-export via the existing __getattr__ in src/models.py
(EAGER would cycle: mcp_client was previously accessing them
via 'models.X'; eager re-export would deadlock).
4. Updates src/mcp_client.py internal references:
- 'def __init__(self, config: models.MCPServerConfig)' -> 'MCPServerConfig'
- 'async def add_server(self, config: models.MCPServerConfig)' -> 'MCPServerConfig'
Verification: VC8 (MCP config classes + load_mcp_config)
from src.mcp_client import MCPServerConfig, MCPConfiguration,
VectorStoreConfig, RAGConfig,
load_mcp_config # OK
from src.models import MCPServerConfig, MCPConfiguration,
VectorStoreConfig, RAGConfig,
load_mcp_config # OK (lazy)
identity check: True for all 5
Tests verified (4/4 PASS):
tests/test_mcp_config.py (3 tests)
tests/test_mcp_client_beads.py (1 test)
Consumer check (lazy __getattr__ keeps these working):
src/app_controller.py: models.MCPConfiguration, models.RAGConfig,
models.load_mcp_config (7+ sites)
src/rag_engine.py: models.RAGConfig (1 site)
All resolve via the lazy __getattr__.
Per the 4-criteria decision rule: WorkspaceProfile fails C1 (only used
by the workspace subsystem), fails C2 (no state machine), fails C3 (no
dedicated test file), borderline C4. MERGE into the existing
src/workspace_manager.py which already has WorkspaceManager.
This commit:
1. Adds WorkspaceProfile class definition to src/workspace_manager.py
at the top.
2. Removes the same class def from src/models.py.
3. Adds lazy re-export via the existing __getattr__ in src/models.py.
4. Updates workspace_manager.py imports to no longer import from
models (the class def is now local).
Verification: VC8 (WorkspaceProfile)
from src.workspace_manager import WorkspaceProfile # OK
from src.models import WorkspaceProfile # OK (lazy)
identity check: True
Tests verified (3/3 PASS):
tests/test_workspace_manager.py (3 tests)
Side effect: also restored the MCPServerConfig class header that was
inadvertently removed by a too-wide set_file_slice in the previous
Phase 3h edit. Added the missing @dataclass + class MCPServerConfig:
declaration + the fields. The class body (to_dict + from_dict) was
already in models.py; only the header was missing.
Per the 4-criteria decision rule: editor configs fail C1 (only used by
the editor subsystem), fail C2 (no state machine), fail C3 (no
dedicated test file), borderline C4. MERGE into the existing
src/external_editor.py which already has ExternalEditorLauncher +
the helper functions.
This commit:
1. Adds TextEditorConfig + ExternalEditorConfig + EMPTY_TEXT_EDITOR_CONFIG
class definitions to src/external_editor.py at the top.
2. Removes the same class defs from src/models.py.
3. Adds lazy re-export via the existing __getattr__ in src/models.py
(EAGER would cycle: external_editor was previously importing from
models; if models re-exports, the cycle would deadlock on initial
load).
4. Updates external_editor.py imports to no longer import from models
(the class defs are now local).
Verification: VC8 (TextEditorConfig + ExternalEditorConfig)
from src.external_editor import TextEditorConfig, ExternalEditorConfig,
EMPTY_TEXT_EDITOR_CONFIG # OK
from src.models import TextEditorConfig, ExternalEditorConfig,
EMPTY_TEXT_EDITOR_CONFIG # OK (lazy)
identity check: True for all 3
Tests verified (22/22 PASS):
tests/test_external_editor.py (17 tests)
tests/test_external_editor_gui.py (5 tests)
Per the 4-criteria decision rule: BiasProfile fails C1 (only used by
tool_presets + tool_bias), fails C2 (no state machine), fails C3 (no
dedicated test file), borderline C4. MERGE into the existing
src/tool_bias.py which already has ToolBiasEngine.
This commit:
1. Adds BiasProfile class definition to src/tool_bias.py at the top
(after the dataclass + typing imports).
2. Removes BiasProfile from src/models.py.
3. Adds lazy re-export via the existing __getattr__ in src/models.py
(EAGER would deadlock: tool_presets needs BiasProfile + tool_bias
needs Tool/ToolPreset, and both want models re-exports).
4. Updates src/tool_presets.py to use the local-import pattern for
BiasProfile (in load_all_bias_profiles) + adds
'from __future__ import annotations' so the 'BiasProfile' type
annotation is a string. This breaks the cycle.
5. Updates src/tool_bias.py to import Tool + ToolPreset from
src.tool_presets directly (no longer through models) + adds
'from __future__ import annotations'.
Verification: VC8 (BiasProfile)
from src.tool_bias import BiasProfile # OK
from src.tool_presets import Tool, ToolPreset # OK
from src.models import Tool, ToolPreset, BiasProfile # OK (lazy)
Tool is Tool returns True
ToolPreset is ToolPreset returns True
BiasProfile is BiasProfile returns True
Tests verified (10/10 PASS):
tests/test_tool_preset_manager.py (4 tests)
tests/test_bias_models.py (3 tests)
tests/test_tool_bias.py (3 tests)
Cycle resolution:
models -> tool_presets (lazy via __getattr__)
tool_presets -> tool_bias (local import in function body, only at call time)
tool_bias -> tool_presets (eager; OK because tool_presets is fully
loaded by the time tool_bias's class
definitions need Tool/ToolPreset)
The eager load of tool_bias from tool_presets is what made the
'from __future__ import annotations' necessary in both files (for
Tool/ToolPreset string annotations in tool_bias method signatures).
Per the 4-criteria decision rule: Tool + ToolPreset fail C1 (only used by
tool_presets + tool_bias), fail C2 (no state machine), fail C3 (no
dedicated test file), borderline C4 (~15 lines each). MERGE into the
existing src/tool_presets.py which already has ToolPresetManager.
This commit:
1. Adds Tool + ToolPreset class definitions to src/tool_presets.py at
the top (after the stdlib imports). Both classes are used by
ToolPresetManager and the tests.
2. Removes Tool + ToolPreset from src/models.py.
3. Adds lazy re-exports via the existing __getattr__ in src/models.py
(EAGER import would deadlock because src.tool_presets imports
BiasProfile from src.models; the lazy __getattr__ breaks the cycle).
4. Updates src/tool_presets.py import: from
'from src.models import ToolPreset, BiasProfile' to
'from src.models import BiasProfile' (ToolPreset is now local).
Verification: VC8 (Tool + ToolPreset)
from src.tool_presets import Tool, ToolPreset # OK
from src.models import Tool, ToolPreset # OK (lazy __getattr__)
Tool is Tool returns True
ToolPreset is ToolPreset returns True
Tests verified (7/7 PASS):
tests/test_tool_preset_manager.py (4 tests)
tests/test_bias_models.py (3 tests)
Consumer check:
src/ai_client.py: from src.models import FileItem, ToolPreset, BiasProfile, Tool
src/app_controller.py: (no Tool/ToolPreset import)
src/tool_bias.py: from src.models import Tool, ToolPreset, BiasProfile
All resolve via re-export/lazy __getattr__.
The lazy __getattr__ pattern is the same mechanism used for the
Pydantic proxies (GenerateRequest / ConfirmRequest) and for PROVIDERS.
Phase 5 will migrate Tool/ToolPreset to a similar lazy pattern in
the re-export block (or drop them entirely after the consumer
migration).
Per the 4-criteria decision rule (C1=cross-system, C3=tests, C4=substantial);
FileItem is the canonical per-file data structure used by aggregate,
app_controller, gui_2, presets, context_presets, and tests. Preset /
ContextPreset / ContextFileEntry / NamedViewPreset are the preset/view
data structures that round-trip through TOML.
This commit:
1. Creates src/project_files.py with FileItem + Preset + ContextPreset +
ContextFileEntry + NamedViewPreset (full class bodies copied verbatim
from src/models.py including __post_init__, to_dict, from_dict, and
the [C: ...] caller-docstring tags).
2. Removes the 5 class definitions from src/models.py.
3. Adds backward-compat re-exports in src/models.py (the same pattern
used by Phase 3a mma.py + Phase 3b project.py + Phase 3g personas.py).
4. Updates the 4 consumer files to import from src.project_files directly:
src/orchestrator_pm.py, src/presets.py, src/context_presets.py,
src/ai_client.py (3 sites of the banned 'local import + as _FIC alias'
pattern updated to use src.project_files.FileItem; the aliasing
anti-pattern is preserved for now - a follow-up track will remove
the local imports and the aliasing).
Verification: VC7
from src.project_files import FileItem, Preset, ContextPreset,
ContextFileEntry, NamedViewPreset # OK
from src.models import FileItem, Preset, ... # OK
(re-exports work; identity check: FileItem is FileItem returns True)
Tests verified (20/20 PASS):
tests/test_file_item_model.py (4 tests)
tests/test_view_presets.py (4 tests)
tests/test_context_presets_models.py (3 tests)
tests/test_custom_slices_annotations.py (3 tests)
tests/test_presets.py (5 tests)
Decorator-orphan pitfall caught and fixed: after removing the 3 classes
between WorkspaceProfile and the MCP Config region, the @dataclass
decorator was orphaned on a comment line. Removed the orphan.
Per the 4-criteria decision rule (C1=cross-system, C3=tests, C4=size);
ProjectContext is the typed return of project_manager.flat_config();
the 5 sub-dataclasses model the actual nested dict structure of
flat_config()'s return; load_config_from_disk / save_config_to_disk
are the canonical config I/O primitives (renamed from the private
_load_config_from_disk / _save_config_to_disk).
This commit:
1. Creates src/project.py with ProjectContext + 5 sub (ProjectMeta,
ProjectOutput, ProjectFiles, ProjectScreenshots, ProjectDiscussion)
+ EMPTY_PROJECT_CONTEXT + _clean_nones + load_config_from_disk +
save_config_to_disk + parse_history_entries.
2. Removes the original class + function definitions from src/models.py.
3. Adds backward-compat re-exports in src/models.py (the same pattern
used by Phase 3a mma.py and Phase 3g personas.py).
4. Updates src/app_controller.py to use the new public function names
(load_config_from_disk / save_config_to_disk).
5. Updates tests/test_models_no_top_level_tomli_w.py to use the new
public name (the test still asserts lazy-loading; the lazy load
happens in the new project.py module).
6. Updates scripts/audit_no_models_config_io.py FORBIDDEN_PATTERNS to
reference the new public names (models.load_config_from_disk /
models.save_config_to_disk) + the new src.project path.
Verification: VC6
uv run python -c 'from src.project import ProjectContext, ProjectMeta,
ProjectOutput, ProjectFiles, ProjectScreenshots, ProjectDiscussion,
_clean_nones, load_config_from_disk, save_config_to_disk,
parse_history_entries' # OK
uv run python -c 'from src.models import ProjectContext, ...' # OK
(re-exports work)
Pre-existing test regression (NOT caused by this commit):
tests/test_models_no_top_level_tomli_w.py::test_models_does_not_import_tomli_w_at_module_level
was already failing because the Phase 3g 'from src.personas import Persona'
re-export in src/models.py loads src.personas at module level, which
loads tomli_w. The Phase 5 reduce-models.py pass moves the persona
import into __getattr__ (lazy), which will make this test pass again.
Tests verified: tests/test_project_context_20260627.py (10/10 PASS),
tests/test_project_serialization.py (2/2 PASS), tests/test_thinking_persistence.py
(4/4 PASS), tests/test_presets.py (3/3 PASS), tests/test_persona_models.py
(2/2 PASS), tests/test_ticket_queue.py (PASS), tests/test_dag_engine.py
(PASS), tests/test_orchestration_logic.py (PASS).
Per spec FR4 + Phase 3.4: Persona dataclass + properties (provider/model/
temperature/top_p/max_output_tokens) + to_dict/from_dict move from
src/models.py into src/personas.py (which already has the PersonaManager
ops layer). Re-export at top of models.py preserves 'from src.models
import Persona'.
Per spec FR3/FR4 + Phase 3.1: the MMA domain dataclasses move to their own module:
- ThinkingSegment, Ticket, Track, WorkerContext, TrackMetadata, TrackState, EMPTY_TRACK_STATE
- TrackMetadata is the renamed (was 'Metadata' dataclass in models.py; renamed to avoid
collision with the Metadata type alias = dict[str, Any])
src/models.py:
- Removed class definitions for ThinkingSegment, Ticket, Track, WorkerContext, Metadata, TrackState, EMPTY_TRACK_STATE
- Added backward-compat re-exports so existing 'from src.models import Ticket' continues to work
- Metadata alias kept for the dataclass name (was confusingly shadowing the type alias)
TrackState's metadata field reverts to the original 'default_factory=dict' pattern
(intentionally not auto-constructing TrackMetadata) to preserve the pre-existing
behavior where accessing state.metadata.id on a missing state.toml throws
AttributeError, which project_manager.get_all_tracks catches and falls through
to metadata.json loading. This was a 'bug-on-purpose' that the test
test_get_all_tracks_with_metadata_json relies on.
Verification: 136 tests pass across mma_models, conductor_engine_v2, dag_engine,
ticket_queue, track_state_schema, thinking_gui, manual_block, pipeline_pause,
phase6_engine, parallel_execution, run_worker_lifecycle_abort, spawn_interception,
persona_id, conductor_engine_abort, conductor_tech_lead, execution_engine,
perf_dag, per_ticket_model, metadata_promotion_phase1, thinking_persistence,
progress_viz, gui_progress, mma_ticket_actions, headless_verification,
context_pruner, orchestration_logic, project_manager_tracks,
track_state_persistence.
Per spec FR2 + Phase 2.2 + architecture feedback (data != view):
- VendorMetric (data) -> src/ai_client.py (alongside VendorCapabilities; all vendor data)
- get_vendor_state -> renamed to _get_vendor_state_metrics in src/gui_2.py
(it's a view-helper that builds the metrics for render_vendor_state's table)
- render_vendor_state in gui_2.py now calls _get_vendor_state_metrics directly
Tests:
- tests/test_vendor_state.py: imports get_vendor_state from src.gui_2, VendorMetric from src.ai_client
Per spec FR1 + Phase 1.4 + architecture feedback (data != view):
- Data classes DiffHunk, DiffFile -> src/patch_modal.py (alongside PendingPatch; all patch-domain data)
- Operations parse_diff/parse_hunk_header/get_line_color/apply_patch_to_file (called by gui_2) -> src/gui_2.py
- GUI is a pure view; data lives elsewhere; no new files per AGENTS.md
Tests: tests/test_diff_viewer.py imports from src.gui_2 (parse_diff/apply_patch_to_file) and src.patch_modal (DiffFile/DiffHunk).
Per spec FR1 + Phase 1.3 + architecture feedback: src/command_palette.py
split by responsibility:
- Command/ScoredCommand/CommandRegistry/fuzzy_match/_close_palette/_execute (data/ops)
-> src/commands.py (which already owns _LazyCommandRegistry pattern)
- render_palette_modal (view/ImGui) -> src/gui_2.py
GUI is a pure view; the registry/data classes are ops; commands.py owns
the registry because commands.py is where @registry.register decorators live.
gui_2.render_palette_modal imports Command from commands.py to type its
parameters.
Also fixes Phase 1.1 (bg_shader) per architecture feedback:
BackgroundShader no longer owns 'enabled' state - the GUI is pure view.
State is now owned by AppController.bg_shader_enabled (read on load from
config, written from gui_2 checkbox via app's __setattr__ delegation).
Tests:
- tests/test_command_palette.py: imports from src.commands (was src.command_palette)
- tests/test_commands_no_top_level_command_palette.py: rewritten for the
new architecture (eager registry in commands.py; render in gui_2; no
circular import between commands.py and gui_2)
Per spec FR1 + Phase 1.2: draw_soft_shadow moved into src/gui_2.py
as a region block; consumer sites changed from shaders.draw_soft_shadow()
to draw_soft_shadow(). Removed the local import workaround at line 7016.
refactor(gui_2): merge bg_shader into gui_2; git rm src/bg_shader.py
Per spec FR1 + Phase 1.1: bg_shader (66 lines) moved into src/gui_2.py
as a region block; consumers updated to use the in-module get_bg().
Local import pattern preserved at app_controller sites (matches existing
circular-dep workaround for gui_2<->app_controller).
In Phase 2 (commit 96f0aa54), I migrated the half-measure pattern
to use 'models.FileItem.from_dict(fi)'. This worked in some scopes
but failed in _send_qwen/_send_grok/_send_llama because ai_client.py
imports 'FileItem' from src.type_aliases (which is a TypeAlias string
forward reference 'models.FileItem', NOT the class). The earlier
import from src.models was shadowed by the type_aliases import
at line 71. Hence 'isinstance(fi, FileItem)' failed with
'isinstance() arg 2 must be a type'.
Fix: add local 'from src.models import FileItem as _FIC' inside
the if-block and use _FIC for isinstance + from_dict.
Discovered by test_qwen_provider.py::test_qwen_vision_vl_model_accepts_image.
Tests: 11/11 pass (test_qwen_provider, test_ai_client_result,
test_ai_client_tool_loop).
In Phase 10 batch 1 (commit 28799766), I migrated the total_cost
sum in render_mma_track_summary using 'MMAUsageStats.from_dict()'
directly instead of the local '_MMA' alias used elsewhere in the
same function. This caused NameError at runtime when the code path
was exercised.
Fix: add 'from src.type_aliases import MMAUsageStats as _MMA'
and use '_MMA.from_dict()' consistently.
Discovered by test_mma_approval_indicators.py::test_no_approval_badge_when_idle
which exercises render_mma_dashboard -> render_mma_track_summary.
Tests: 4/4 pass in test_mma_approval_indicators.py.
Required by Phase 10 migrations which call these from_dict methods.
Without these, CustomSlice.from_dict() and MMAUsageStats.from_dict()
used in gui_2.py would raise AttributeError at runtime.
Adds the from_dict pattern consistent with the existing
CommsLogEntry/HistoryMessage/ToolDefinition from_dict:
- Filter dict keys to only the dataclass fields (ignore extras)
- Pass filtered dict to cls(**filtered)
Field definitions unchanged. No-op behavior for callers that
already have a dataclass instance (they pass through isinstance check).
Tests: 51/51 pass across all related test files.
Phase 10 (batch 2): DiscussionSettings
Before: 1 .get('temperature'/...) site in src/gui_2.py
After: 0
Delta: -1 (plan expected 3 sites; 2 were already migrated by Tier 2)
Migrates the summary line in persona preferred model rendering:
entry.get('temperature', 0.7)
entry.get('top_p', 1.0)
entry.get('max_output_tokens', 0)
to:
ds = DiscussionSettings.from_dict(entry) if isinstance(entry, dict) else ds
ds.temperature, ds.top_p, ds.max_output_tokens
The dataclass defaults match the original .get() defaults exactly
(temperature=0.7, top_p=1.0, max_output_tokens=0), so behavior is preserved.
Phase 8: ToolDefinition
Before: 2 .get('description',...) sites
After: 0
Delta: -2 (expected: -2 or -3 per plan; the 3rd site gui_2.py:5875
is 'server' field which is NOT on ToolDefinition)
Migrates:
1. src/mcp_client.py:1968 (was 1970) - list_tools in _get_tool_definitions:
tinfo.get('description', '') -> ToolDefinition.from_dict(tinfo).description
(tinfo.get('inputSchema', ...) stays because 'inputSchema' key
does not match ToolDefinition's 'parameters' field name)
2. src/gui_2.py:5878 - render_external_tools_panel:
tinfo.get('description', '') -> ToolDefinition.from_dict(tinfo).description
Notes:
- gui_2.py:5875 (tinfo.get('server', 'unknown')) is NOT migrated;
'server' is not a ToolDefinition field. The tinfo here may be a
ToolInfo or server-info dict, not ToolDefinition. Classified as
collapsed-codepath per FR2.
Tests: 10/10 pass (test_tool_definition, test_external_mcp,
test_external_mcp_e2e). 2 test_type_aliases failures are pre-existing
(forward references in TypeAlias declarations; not caused by these
changes).
Phase 6: UsageStats
Before: 4 .get('input_tokens'/...) sites in src/app_controller.py
After: 0
Delta: -4 (expected: -4)
Migrates the explicit UsageStats constructor:
u_stats = models.UsageStats(
input_tokens=u.get('input_tokens', 0) or 0,
output_tokens=u.get('output_tokens', 0) or 0,
cache_read_tokens=u.get('cache_read_input_tokens', 0) or 0,
cache_creation_tokens=u.get('cache_creation_input_tokens', 0) or 0,
)
to:
u_stats = UsageStats.from_dict(u)
Behavior notes:
- UsageStats.from_dict() filters dict keys to dataclass fields.
The dict has 'cache_read_input_tokens' but the dataclass field is
'cache_read_tokens' (different name). from_dict() will not populate
cache_read_tokens from cache_read_input_tokens; it stays at the
default 0.
- Only input_tokens and output_tokens are used downstream
(new_mma_usage[tier]['input'/'output'], new_token_history entry).
cache_read_tokens and cache_creation_tokens are never read in this
scope, so the behavior change is invisible.
- Local import 'from src.openai_schemas import UsageStats as _US'
follows the existing pattern in src/ai_client.py.
Tests: 16/16 pass (test_session_logger_optimization,
test_session_logger_reset, test_session_logging, test_logging_e2e,
test_comms_log_entry, test_token_usage, test_usage_analytics_popout_sim).
Phase 5: ChatMessage (part 2)
Before: 6 .get('content'/'role'/'tool_calls'/'tool_call_id') sites
After: 0
Delta: -6
Migrates:
1. _send_deepseek API response parsing (lines 2321-2324):
- message.get('content', '') -> message.content or ''
- message.get('tool_calls', []) -> [tc.to_dict() for tc in message.tool_calls]
- message.get('reasoning_content') -> kept as choice.get('message', {}).get('reasoning_content', '')
(reasoning_content is NOT a ChatMessage field)
2. _repair_minimax_history generator (line 2454):
- m.get('role') == 'tool' -> _CM.from_dict(m).role == 'tool'
- m.get('tool_call_id') -> _CM.from_dict(m).tool_call_id
Used inline conversion because the generator iterates over a
dict list and reads 2 fields. Inline conversion avoids an
intermediate list comprehension.
openai_schemas.py:
- ChatMessage.from_dict() now provides defaults for required fields
('role' -> 'assistant', 'content' -> '') when the input dict is
missing them. This handles the case where DeepSeek's API returns
an empty {} for 'message' (e.g., finish_reason='length' with no
content). Without this default, ChatMessage.__init__() raises
TypeError.
Tests: 46/46 pass (test_ai_client_result, test_ai_client_tool_loop,
test_deepseek_provider, test_openai_schemas, test_minimax_provider).
Phase 5: ChatMessage (part 1)
Before: 6 .get('role'/'content'/'tool_calls'/'tool_call_id') sites in _send_deepseek
After: 0
Delta: -6
Migrates _send_deepseek's history transformation loop from
dict-style access to ChatMessage direct field access:
msg = _ChatMessage.from_dict(msg_raw)
msg.role (was msg.get('role'))
msg.content (was msg.get('content'))
msg.tool_calls (was msg.get('tool_calls') / msg['tool_calls'])
msg.tool_call_id (was msg.get('tool_call_id'))
The api_msg dict (output for the DeepSeek API) is constructed via
direct field access. The tool_calls list is converted to dicts via
tc.to_dict() (preserves the existing API payload format).
Notes:
- msg_raw.get('reasoning_content') is preserved as-is because
reasoning_content is NOT a ChatMessage field.
- Local import 'from src.openai_schemas import ChatMessage as _ChatMessage'
follows the existing pattern in this file (lazy imports inside functions).
Tests: 36/36 pass (test_ai_client_result, test_ai_client_tool_loop,
test_deepseek_provider, test_openai_schemas).
Infrastructure change required by Phase 5/6/7 of the
type_alias_unfuck_20260626 track. The plan's migration pattern
(var = Aggregate.from_dict(var)) requires from_dict on the
target dataclasses. None existed for the openai_schemas
classes, so this commit adds them.
from_dict semantics:
- Filter dict keys to only the dataclass fields (ignore extra keys
like _est_tokens)
- For ChatMessage: convert nested tool_calls list to tuple of ToolCall
- For ToolCall: convert nested function dict to ToolCallFunction
- For UsageStats: direct field mapping
Field definitions unchanged. Behavior: zero impact on existing tests
(no callers exist yet for from_dict on these classes).
Tests: syntax check OK; manual instantiation confirms from_dict works.
Phase 3: CommsLogEntry
Before: 3 .get('source_tier',...) sites + 1 half-measure in src/gui_2.py
After: 0
Delta: -4 (expected: -5 per plan; the 5th site was app_controller.py:1930
which returns None for missing source_tier and cannot be migrated
without breaking test_append_tool_log_dict_keys)
Migrates the following CommsLogEntry-related sites in src/gui_2.py:
1. gui_2.py:1810 - cache filter source_tier (.get('source_tier', ''))
2. gui_2.py:1818 - cache filter source_tier (.get('source_tier', ''))
3. gui_2.py:5104 - render_comms_log_panel source_tier (.get('source_tier', 'main'))
4. gui_2.py:5106 - render_comms_log_panel ts (.get('ts', '00:00:00'))
5. gui_2.py:5107 - render_comms_log_panel direction (.get('direction', '??'))
6. gui_2.py:5110 - render_comms_log_panel model (.get('model', '?'))
7. gui_2.py:5802 - render_tool_calls_panel half-measure
(subscript + 'in' check; entry['source_tier'] if 'source_tier' in entry else 'main')
All migrated via:
ce = CommsLogEntry.from_dict(entry)
ce.<field> # direct attribute access
The dataclass default for source_tier is 'main', which preserves the
fallback behavior for sites that had 'main' as the default. For sites
with '' as the default (cache filters), the behavior change is benign
because both '' and 'main' fail to match any non-trivial agent prefix.
Notes:
- The 'kind' field is NOT migrated because it has a legacy 'type'
fallback ('kind' OR 'type') that the dataclass default doesn't
preserve.
- 'provider' and 'payload' are NOT on CommsLogEntry; they remain
as entry.get(...) calls.
- src/app_controller.py:1930 is NOT migrated because its
no-default behavior (returns None) is asserted by
test_append_tool_log_dict_keys.
Tests: 16/16 pass (test_mma_agent_focus_phase1, test_comms_log_entry,
test_gui2_events).
src/type_aliases.py had two exact anti-patterns the user flagged:
1. Line 91: 'ToolCall: TypeAlias = Metadata' -- the dict alias the user
called out as 'the exact bad pattern'. Now points to the canonical
@dataclass(frozen=True, slots=True) class ToolCall in openai_schemas.py.
2. Lines 53-69: duplicate FileItem dataclass with 8 fields (path, content,
view_mode, summary, skeleton, annotations, tags) that conflicted with
the canonical models.FileItem (10 fields: path, auto_aggregate,
force_full, view_mode, selected, ast_signatures, ast_definitions,
ast_mask, custom_slices, injected_at). Two FileItem types was the
'FileItem is duplicated in TWO places' blocker. Duplicate removed;
FileItem now aliases models.FileItem.
state.toml updated to honest state: status='active', current_phase=0,
phases 2-10 marked 'not_done', 3 of 5 blockers fixed in this commit,
2 blockers (RAG return type, tool builders dicts) remain open with
followup tracks planned.
The 5 files that import ToolCall from src.type_aliases
(aggregate/ai_client/api_hook_client/app_controller/models) only use it
as a type annotation -- no constructor calls, no .from_dict() calls.
Safe to fix the alias.
TIER-2 READ AGENTS.md, conductor/workflow.md, conductor/edit_workflow.md,
conductor/tier2/githooks/forbidden-files.txt,
conductor/tracks/tier2_leak_prevention_20260620/spec.md,
conductor/code_styleguides/data_oriented_design.md,
conductor/code_styleguides/error_handling.md,
conductor/code_styleguides/type_aliases.md before Phase 4.
Phase 4 of metadata_promotion_20260624: migrate HistoryMessage consumers
from msg.get(key, default) to direct field access.
Per-site resolutions (documented per Hard Rule #11):
1. src/synthesis_formatter.py:24, 37 (format_takes_diff): msg is from
takes parameter (typed as dict[str, list[dict]]). Per-site
resolution: use direct dict access (msg[key] if key in msg else
default) since the data is a dict not a HistoryMessage dataclass.
Migration pattern:
old: msg.get(key, default)
new: msg[key] if key in msg else default
2. src/gui_2.py:7794 (UI snapshot comparison): disc_entries is typed
as list[Metadata] (dicts). The last entry is accessed for content
comparison. Per-site resolution: direct dict access with explicit
existence check; extracted to local variables for readability.
Note: HistoryMessage is imported in several files (provider_state.py
uses it for the messages field) but the consumer sites that use .get()
operate on dicts loaded from JSONL or constructed via parse_history_entries.
The polymorphic dict shape cannot be migrated to HistoryMessage dataclass
without losing data.
TIER-2 READ AGENTS.md, conductor/workflow.md, conductor/edit_workflow.md,
conductor/tier2/githooks/forbidden-files.txt,
conductor/tracks/tier2_leak_prevention_20260620/spec.md,
conductor/code_styleguides/data_oriented_design.md,
conductor/code_styleguides/error_handling.md,
conductor/code_styleguides/type_aliases.md before Phase 3.
Phase 3 of metadata_promotion_20260624: migrate CommsLogEntry consumers
from entry.get(key, default) to direct field access.
Per-site resolutions (documented per Hard Rule #11):
1. src/app_controller.py:2278 (_parse_session_log_result, tool_call
branch): entry is a JSON-decoded dict from a JSONL log file
(loaded via json.loads). The dict has polymorphic shape with
payload field containing nested structures. Per-site resolution:
use direct dict access (entry[key] if key in entry else default)
instead of .get() since the data is a dict not a CommsLogEntry
dataclass. Migration pattern:
old: entry.get(key, default)
new: entry[key] if key in entry else default
2. src/app_controller.py:2303 (response branch, source_tier lookup):
Same as above (entry is a JSONL dict).
3. src/app_controller.py:2311 (response branch, model lookup):
Same as above.
4. src/gui_2.py:5803 (render_tool_calls_panel): entry is from
app._tool_log_cache (typed as list[dict[str, Any]]), populated
from app.prior_tool_calls (typed as list[Metadata]). Per-site
resolution: direct dict access.
Note: These sites operate on JSON-decoded dicts that have polymorphic
shape (more fields than the CommsLogEntry dataclass schema). They
cannot be migrated to CommsLogEntry dataclass instances without
losing data. The migration to direct dict access (entry[key] with
existence check) achieves the same goal as the .get() pattern with
zero branches at the access site.
TIER-2 READ AGENTS.md, conductor/workflow.md, conductor/edit_workflow.md,
conductor/tier2/githooks/forbidden-files.txt,
conductor/tracks/tier2_leak_prevention_20260620/spec.md,
conductor/code_styleguides/data_oriented_design.md,
conductor/code_styleguides/error_handling.md,
conductor/code_styleguides/type_aliases.md before Phase 2.
Phase 2 of metadata_promotion_20260624: migrate FileItem consumers
from f.get(key, default) / f[key] to direct field access.
Per-site resolutions (documented per Hard Rule #11):
1. src/ai_client.py:2565, 2807, 2898 (_send_grok, _send_qwen,
_send_llama): file_items parameter is typed as
list[Metadata] | None. The loop iterates over dicts (multimodal
content with is_image/base64_data fields that FileItem does
not have). Per-site resolution: construct FileItem(path=...) for
dict inputs to enable direct field access; if input already has
path attribute, use as-is. Migration pattern:
old: fi.get('path', 'attachment')
new: (fi if hasattr(fi, 'path') else FileItem(path=fi.get('path', 'attachment'))).path or 'attachment'
Added FileItem to src/models import in src/ai_client.py:52.
2. src/app_controller.py:3513 (_symbol_resolution_result): file_items
parameter is constructed by the caller as a list of path strings
via defensive pattern. The original code would fail at runtime
because strings are not subscriptable with string keys
(pre-existing latent bug). Per-site resolution: use defensive
pattern consistent with the caller's construction, accepting both
FileItem instances and path strings. Migration pattern:
old: [f[key] for f in file_items]
new: [f.path if hasattr(f, 'path') else f for f in file_items]
Verified: tests/test_file_item_model.py + tests/test_aggregate_flags.py
pass (5 passed, 1 skipped; no regressions).