From cd828e52671e67b80dd7b01e152e612a34e32e7f Mon Sep 17 00:00:00 2001 From: Ed_ Date: Fri, 26 Jun 2026 07:19:37 -0400 Subject: [PATCH] refactor(mma): create src/mma.py with MMA Core (ThinkingSegment, Ticket, Track, WorkerContext, TrackMetadata, TrackState, EMPTY_TRACK_STATE) split from src/models.py 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. --- src/mma.py | 227 ++++++++++++++++++++++++++++++++++++++++++ src/models.py | 268 ++++---------------------------------------------- 2 files changed, 248 insertions(+), 247 deletions(-) create mode 100644 src/mma.py diff --git a/src/mma.py b/src/mma.py new file mode 100644 index 00000000..ddf5f672 --- /dev/null +++ b/src/mma.py @@ -0,0 +1,227 @@ +"""MMA (Multi-Model Architecture) core data structures. + +Per module_taxonomy_refactor_20260627 Phase 3.1, the MMA Core (ThinkingSegment, +Ticket, Track, WorkerContext, TrackMetadata, TrackState) moved from +src/models.py to this module. The data domain is the ticket/track lifecycle +that drives the 4-Tier MMA execution. + +The boundary wire schema `Metadata` (TypeAlias = dict[str, Any]) is +NOT defined here; it lives in src/type_aliases.py. This module's +TrackMetadata dataclass is the *typed* counterpart used for Track-level +metadata (id/name/status/timestamps). +""" +from __future__ import annotations + +import datetime +from dataclasses import dataclass, field +from typing import List, Optional + +from src.type_aliases import Metadata + + +@dataclass +class ThinkingSegment: + content: str + marker: str + + def to_dict(self) -> Metadata: + return {"content": self.content, "marker": self.marker} + + @classmethod + def from_dict(cls, data: Metadata) -> "ThinkingSegment": + return cls(content=data["content"], marker=data["marker"]) + + +@dataclass +class Ticket: + id: str + description: str + target_symbols: List[str] = field(default_factory=list) + context_requirements: List[str] = field(default_factory=list) + depends_on: List[str] = field(default_factory=list) + status: str = "todo" + assigned_to: str = "unassigned" + priority: str = "medium" + target_file: Optional[str] = None + blocked_reason: Optional[str] = None + step_mode: bool = False + retry_count: int = 0 + manual_block: bool = False + model_override: Optional[str] = None + persona_id: Optional[str] = None + + def mark_blocked(self, reason: str) -> None: + self.status = "blocked" + self.blocked_reason = reason + + def mark_manual_block(self, reason: str) -> None: + self.status = "blocked" + self.blocked_reason = f"[MANUAL] {reason}" + self.manual_block = True + + def clear_manual_block(self) -> None: + if self.manual_block: + self.status = "todo" + self.blocked_reason = None + self.manual_block = False + + def mark_complete(self) -> None: + self.status = "completed" + + def to_dict(self) -> Metadata: + return { + "id": self.id, + "description": self.description, + "status": self.status, + "assigned_to": self.assigned_to, + "priority": self.priority, + "target_file": self.target_file, + "target_symbols": self.target_symbols, + "context_requirements": self.context_requirements, + "depends_on": self.depends_on, + "blocked_reason": self.blocked_reason, + "step_mode": self.step_mode, + "retry_count": self.retry_count, + "manual_block": self.manual_block, + "model_override": self.model_override, + "persona_id": self.persona_id, + } + + @classmethod + def from_dict(cls, data: Metadata) -> "Ticket": + return cls( + id = data["id"], + description = data.get("description", ""), + status = data.get("status", "todo"), + assigned_to = data.get("assigned_to", "unassigned"), + priority = data.get("priority", "medium"), + target_file = data.get("target_file"), + target_symbols = data.get("target_symbols", []), + context_requirements = data.get("context_requirements", []), + depends_on = data.get("depends_on", []), + blocked_reason = data.get("blocked_reason"), + step_mode = data.get("step_mode", False), + retry_count = data.get("retry_count", 0), + manual_block = data.get("manual_block", False), + model_override = data.get("model_override"), + persona_id = data.get("persona_id"), + ) + + +@dataclass +class Track: + id: str + description: str + tickets: List["Ticket"] = field(default_factory=list) + + def to_dict(self) -> Metadata: + return { + "id": self.id, + "description": self.description, + "tickets": [t.to_dict() for t in self.tickets], + } + + @classmethod + def from_dict(cls, data: Metadata) -> "Track": + return cls( + id = data["id"], + description = data.get("description", ""), + tickets = [Ticket.from_dict(t) for t in data.get("tickets", [])], + ) + + +@dataclass +class WorkerContext: + ticket_id: str + model_name: str + messages: list[Metadata] = field(default_factory=list) + tool_preset: Optional[str] = None + persona_id: Optional[str] = None + + +@dataclass +class TrackMetadata: + id: str + name: str + status: Optional[str] = None + created_at: Optional[datetime.datetime] = None + updated_at: Optional[datetime.datetime] = None + + def to_dict(self) -> Metadata: + return { + "id": self.id, + "name": self.name, + "status": self.status, + "created_at": self.created_at.isoformat() if self.created_at else None, + "updated_at": self.updated_at.isoformat() if self.updated_at else None, + } + + @classmethod + def from_dict(cls, data: Metadata) -> "TrackMetadata": + created = data.get("created_at") + updated = data.get("updated_at") + if isinstance(created, str): + try: + created = datetime.datetime.fromisoformat(created) + except ValueError: + created = None + if isinstance(updated, str): + try: + updated = datetime.datetime.fromisoformat(updated) + except ValueError: + updated = None + return cls( + id = data["id"], + name = data.get("name", ""), + status = data.get("status"), + created_at = created, + updated_at = updated, + ) + + +@dataclass +class TrackState: + metadata: Metadata = field(default_factory=dict) + discussion: List[Metadata] = field(default_factory=list) + tasks: List["Ticket"] = field(default_factory=list) + + def to_dict(self) -> Metadata: + serialized_discussion = [] + for item in self.discussion: + if isinstance(item, dict): + new_item = dict(item) + if "ts" in new_item and isinstance(new_item["ts"], datetime.datetime): + new_item["ts"] = new_item["ts"].isoformat() + serialized_discussion.append(new_item) + else: + serialized_discussion.append(item) + return { + "metadata": self.metadata.to_dict(), + "discussion": serialized_discussion, + "tasks": [t.to_dict() for t in self.tasks], + } + + @classmethod + def from_dict(cls, data: Metadata) -> "TrackState": + discussion = data.get("discussion", []) + parsed_discussion = [] + for item in discussion: + if isinstance(item, dict): + new_item = dict(item) + ts = new_item.get("ts") + if isinstance(ts, str): + try: + new_item["ts"] = datetime.datetime.fromisoformat(ts) + except ValueError: + pass + parsed_discussion.append(new_item) + else: + parsed_discussion.append(item) + return cls( + metadata = TrackMetadata.from_dict(data["metadata"]), + discussion = parsed_discussion, + tasks = [Ticket.from_dict(t) for t in data.get("tasks", [])], + ) + + +EMPTY_TRACK_STATE: TrackState = TrackState() \ No newline at end of file diff --git a/src/models.py b/src/models.py index c49b93bb..45f98e6e 100644 --- a/src/models.py +++ b/src/models.py @@ -63,6 +63,23 @@ from src.type_aliases import ( ToolDefinition, ) +# Backward-compat re-exports for the MMA Core classes that moved to src/mma.py +# in module_taxonomy_refactor_20260627 Phase 3.1. Consumers still using +# 'from src.models import Ticket' continue to work; new code should import +# from src.mma directly. +from src.mma import ( + EMPTY_TRACK_STATE, + ThinkingSegment, + Ticket, + Track, + TrackMetadata, + TrackState, + WorkerContext, +) +# Alias the old `Metadata` dataclass name to TrackMetadata so existing +# `from src.models import Metadata` keeps resolving to the dataclass. +Metadata = TrackMetadata # noqa: F401 — legacy class name re-export + #region: Constants @@ -278,255 +295,12 @@ def __getattr__(name: str) -> Any: return cls raise AttributeError(f"module {__name__!r} has no attribute {name!r}") -#region: MMA Core - -@dataclass -class ThinkingSegment: - content: str - marker: str - - def to_dict(self) -> Metadata: - """ - [C: src/personas.py:PersonaManager.save_persona, src/presets.py:PresetManager.save_preset, src/project_manager.py:save_project, src/project_manager.py:save_track_state, src/tool_presets.py:ToolPresetManager.save_bias_profile, src/tool_presets.py:ToolPresetManager.save_preset, src/workspace_manager.py:WorkspaceManager.save_profile, tests/test_bias_models.py:test_bias_profile_model, tests/test_bias_models.py:test_tool_model, tests/test_bias_models.py:test_tool_preset_extension, tests/test_context_presets_models.py:test_context_preset_serialization, tests/test_context_presets_models.py:test_file_view_preset_serialization, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_round_trip_annotations, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_serialization_with_annotations, tests/test_event_serialization.py:test_user_request_event_serialization, tests/test_external_editor.py:TestExternalEditorConfig.test_to_dict, tests/test_external_editor.py:TestTextEditorConfig.test_to_dict, tests/test_file_item_model.py:test_file_item_to_dict, tests/test_gui_events_v2.py:test_user_request_event_payload, tests/test_history_manager.py:TestHistoryManager.test_snapshot_roundtrip, tests/test_mcp_config.py:test_mcp_configuration_to_from_dict, tests/test_mcp_config.py:test_mcp_server_config_to_from_dict, tests/test_per_ticket_model.py:test_model_override_serialization, tests/test_persona_id.py:test_ticket_persona_id_serialization, tests/test_persona_models.py:test_persona_defaults, tests/test_persona_models.py:test_persona_serialization, tests/test_slice_editor_behavior.py:test_add_slice_with_annotations, tests/test_thinking_gui.py:test_thinking_segment_model_compatibility, tests/test_ticket_queue.py:test_ticket_to_dict_priority, tests/test_tiered_aggregation.py:test_persona_aggregation_strategy, tests/test_track_state_schema.py:test_track_state_to_dict, tests/test_track_state_schema.py:test_track_state_to_dict_with_none, tests/test_ui_summary_only_removal.py:test_file_item_serialization_with_flags] - """ - return {"content": self.content, "marker": self.marker} - - @classmethod - def from_dict(cls, data: Metadata) -> "ThinkingSegment": - """ - [C: src/personas.py:PersonaManager.load_all, src/presets.py:PresetManager.load_all, src/project_manager.py:load_project, src/project_manager.py:load_track_state, src/tool_presets.py:ToolPresetManager.load_all_bias_profiles, src/tool_presets.py:ToolPresetManager.load_all_presets, src/workspace_manager.py:WorkspaceManager.load_all_profiles, tests/test_bias_models.py:test_bias_profile_model, tests/test_bias_models.py:test_tool_model, tests/test_bias_models.py:test_tool_preset_extension, tests/test_context_presets_models.py:test_context_preset_from_dict_legacy, tests/test_context_presets_models.py:test_context_preset_serialization, tests/test_context_presets_models.py:test_file_view_preset_serialization, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_deserialization_with_annotations, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_round_trip_annotations, tests/test_external_editor.py:TestExternalEditorConfig.test_from_dict_with_dict_editors, tests/test_external_editor.py:TestExternalEditorConfig.test_from_dict_with_string_editors, tests/test_external_editor.py:TestTextEditorConfig.test_from_dict_with_diff_args, tests/test_external_editor.py:TestTextEditorConfig.test_from_dict_without_diff_args, tests/test_file_item_model.py:test_file_item_from_dict, tests/test_file_item_model.py:test_file_item_from_dict_defaults, tests/test_history_manager.py:TestHistoryManager.test_snapshot_roundtrip, tests/test_mcp_config.py:test_mcp_configuration_to_from_dict, tests/test_mcp_config.py:test_mcp_server_config_to_from_dict, tests/test_per_ticket_model.py:test_model_override_default_on_deserialize, tests/test_per_ticket_model.py:test_model_override_deserialization, tests/test_persona_id.py:test_ticket_persona_id_deserialization, tests/test_persona_models.py:test_persona_defaults, tests/test_persona_models.py:test_persona_deserialization, tests/test_project_serialization.py:TestProjectSerialization.test_backward_compatibility_strings, tests/test_slice_editor_behavior.py:test_add_slice_with_annotations, tests/test_ticket_queue.py:test_ticket_from_dict_default_priority, tests/test_ticket_queue.py:test_ticket_from_dict_priority, tests/test_tiered_aggregation.py:test_persona_aggregation_strategy, tests/test_track_state_schema.py:test_track_state_from_dict, tests/test_track_state_schema.py:test_track_state_from_dict_empty_and_missing, tests/test_ui_summary_only_removal.py:test_file_item_serialization_with_flags] - """ - return cls(content=data["content"], marker=data["marker"]) - -@dataclass -class Ticket: - id: str - description: str - target_symbols: List[str] = field(default_factory=list) - context_requirements: List[str] = field(default_factory=list) - depends_on: List[str] = field(default_factory=list) - status: str = "todo" - assigned_to: str = "unassigned" - priority: str = "medium" - target_file: Optional[str] = None - blocked_reason: Optional[str] = None - step_mode: bool = False - retry_count: int = 0 - manual_block: bool = False - model_override: Optional[str] = None - persona_id: Optional[str] = None - - def mark_blocked(self, reason: str) -> None: - """ - [C: src/multi_agent_conductor.py:run_worker_lifecycle, tests/test_mma_models.py:test_ticket_mark_blocked] - """ - self.status = "blocked" - self.blocked_reason = reason - - def mark_manual_block(self, reason: str) -> None: - """ - [C: tests/test_manual_block.py:test_clear_manual_block_method, tests/test_manual_block.py:test_mark_manual_block_method] - """ - self.status = "blocked" - self.blocked_reason = f"[MANUAL] {reason}" - self.manual_block = True - - def clear_manual_block(self) -> None: - """ - [C: tests/test_manual_block.py:test_clear_manual_block_method] - """ - if self.manual_block: - self.status = "todo" - self.blocked_reason = None - self.manual_block = False - - def mark_complete(self) -> None: - """ - [C: src/multi_agent_conductor.py:run_worker_lifecycle, tests/test_conductor_engine_v2.py:do_work, tests/test_mma_models.py:test_ticket_mark_complete, tests/test_mma_models.py:test_track_get_executable_tickets_complex] - """ - self.status = "completed" - - - def to_dict(self) -> Metadata: - """ - [C: src/personas.py:PersonaManager.save_persona, src/presets.py:PresetManager.save_preset, src/project_manager.py:save_project, src/project_manager.py:save_track_state, src/tool_presets.py:ToolPresetManager.save_bias_profile, src/tool_presets.py:ToolPresetManager.save_preset, src/workspace_manager.py:WorkspaceManager.save_profile, tests/test_bias_models.py:test_bias_profile_model, tests/test_bias_models.py:test_tool_model, tests/test_bias_models.py:test_tool_preset_extension, tests/test_context_presets_models.py:test_context_preset_serialization, tests/test_context_presets_models.py:test_file_view_preset_serialization, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_round_trip_annotations, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_serialization_with_annotations, tests/test_event_serialization.py:test_user_request_event_serialization, tests/test_external_editor.py:TestExternalEditorConfig.test_to_dict, tests/test_external_editor.py:TestTextEditorConfig.test_to_dict, tests/test_file_item_model.py:test_file_item_to_dict, tests/test_gui_events_v2.py:test_user_request_event_payload, tests/test_history_manager.py:TestHistoryManager.test_snapshot_roundtrip, tests/test_mcp_config.py:test_mcp_configuration_to_from_dict, tests/test_mcp_config.py:test_mcp_server_config_to_from_dict, tests/test_per_ticket_model.py:test_model_override_serialization, tests/test_persona_id.py:test_ticket_persona_id_serialization, tests/test_persona_models.py:test_persona_defaults, tests/test_persona_models.py:test_persona_serialization, tests/test_slice_editor_behavior.py:test_add_slice_with_annotations, tests/test_thinking_gui.py:test_thinking_segment_model_compatibility, tests/test_ticket_queue.py:test_ticket_to_dict_priority, tests/test_tiered_aggregation.py:test_persona_aggregation_strategy, tests/test_track_state_schema.py:test_track_state_to_dict, tests/test_track_state_schema.py:test_track_state_to_dict_with_none, tests/test_ui_summary_only_removal.py:test_file_item_serialization_with_flags] - """ - return { - "id": self.id, - "description": self.description, - "status": self.status, - "assigned_to": self.assigned_to, - "priority": self.priority, - "target_file": self.target_file, - "target_symbols": self.target_symbols, - "context_requirements": self.context_requirements, - "depends_on": self.depends_on, - "blocked_reason": self.blocked_reason, - "step_mode": self.step_mode, - "retry_count": self.retry_count, - "manual_block": self.manual_block, - "model_override": self.model_override, - "persona_id": self.persona_id, - } - - @classmethod - def from_dict(cls, data: Metadata) -> "Ticket": - """ - [C: src/personas.py:PersonaManager.load_all, src/presets.py:PresetManager.load_all, src/project_manager.py:load_project, src/project_manager.py:load_track_state, src/tool_presets.py:ToolPresetManager.load_all_bias_profiles, src/tool_presets.py:ToolPresetManager.load_all_presets, src/workspace_manager.py:WorkspaceManager.load_all_profiles, tests/test_bias_models.py:test_bias_profile_model, tests/test_bias_models.py:test_tool_model, tests/test_bias_models.py:test_tool_preset_extension, tests/test_context_presets_models.py:test_context_preset_from_dict_legacy, tests/test_context_presets_models.py:test_context_preset_serialization, tests/test_context_presets_models.py:test_file_view_preset_serialization, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_deserialization_with_annotations, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_round_trip_annotations, tests/test_external_editor.py:TestExternalEditorConfig.test_from_dict_with_dict_editors, tests/test_external_editor.py:TestExternalEditorConfig.test_from_dict_with_string_editors, tests/test_external_editor.py:TestTextEditorConfig.test_from_dict_with_diff_args, tests/test_external_editor.py:TestTextEditorConfig.test_from_dict_without_diff_args, tests/test_file_item_model.py:test_file_item_from_dict, tests/test_file_item_model.py:test_file_item_from_dict_defaults, tests/test_history_manager.py:TestHistoryManager.test_snapshot_roundtrip, tests/test_mcp_config.py:test_mcp_configuration_to_from_dict, tests/test_mcp_config.py:test_mcp_server_config_to_from_dict, tests/test_per_ticket_model.py:test_model_override_default_on_deserialize, tests/test_per_ticket_model.py:test_model_override_deserialization, tests/test_persona_id.py:test_ticket_persona_id_deserialization, tests/test_persona_models.py:test_persona_defaults, tests/test_persona_models.py:test_persona_deserialization, tests/test_project_serialization.py:TestProjectSerialization.test_backward_compatibility_strings, tests/test_slice_editor_behavior.py:test_add_slice_with_annotations, tests/test_ticket_queue.py:test_ticket_from_dict_default_priority, tests/test_ticket_queue.py:test_ticket_from_dict_priority, tests/test_tiered_aggregation.py:test_persona_aggregation_strategy, tests/test_track_state_schema.py:test_track_state_from_dict, tests/test_track_state_schema.py:test_track_state_from_dict_empty_and_missing, tests/test_ui_summary_only_removal.py:test_file_item_serialization_with_flags] - """ - return cls( - id = data["id"], - description = data.get("description", ""), - status = data.get("status", "todo"), - assigned_to = data.get("assigned_to", "unassigned"), - priority = data.get("priority", "medium"), - target_file = data.get("target_file"), - target_symbols = data.get("target_symbols", []), - context_requirements = data.get("context_requirements", []), - depends_on = data.get("depends_on", []), - blocked_reason = data.get("blocked_reason"), - step_mode = data.get("step_mode", False), - retry_count = data.get("retry_count", 0), - manual_block = data.get("manual_block", False), - model_override = data.get("model_override"), - persona_id = data.get("persona_id"), - ) - -@dataclass -class Track: - id: str - description: str - tickets: List[Ticket] = field(default_factory=list) - - def to_dict(self) -> Metadata: - """ - """ - return { - "id": self.id, - "description": self.description, - "tickets": [t.to_dict() for t in self.tickets], - } - - @classmethod - def from_dict(cls, data: Metadata) -> "Track": - """ - """ - return cls( - id = data["id"], - description = data.get("description", ""), - tickets = [Ticket.from_dict(t) for t in data.get("tickets", [])], - ) - -@dataclass -class WorkerContext: - ticket_id: str - model_name: str - messages: list[Metadata] = field(default_factory=list) - tool_preset: Optional[str] = None - persona_id: Optional[str] = None - -@dataclass -class Metadata: - id: str - name: str - status: Optional[str] = None - created_at: Optional[datetime.datetime] = None - updated_at: Optional[datetime.datetime] = None - - def to_dict(self) -> Metadata: - """ - [C: src/personas.py:PersonaManager.save_persona, src/presets.py:PresetManager.save_preset, src/project_manager.py:save_project, src/project_manager.py:save_track_state, src/tool_presets.py:ToolPresetManager.save_bias_profile, src/tool_presets.py:ToolPresetManager.save_preset, src/workspace_manager.py:WorkspaceManager.save_profile, tests/test_bias_models.py:test_bias_profile_model, tests/test_bias_models.py:test_tool_model, tests/test_bias_models.py:test_tool_preset_extension, tests/test_context_presets_models.py:test_context_preset_serialization, tests/test_context_presets_models.py:test_file_view_preset_serialization, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_round_trip_annotations, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_serialization_with_annotations, tests/test_event_serialization.py:test_user_request_event_serialization, tests/test_external_editor.py:TestExternalEditorConfig.test_to_dict, tests/test_external_editor.py:TestTextEditorConfig.test_to_dict, tests/test_file_item_model.py:test_file_item_to_dict, tests/test_gui_events_v2.py:test_user_request_event_payload, tests/test_history_manager.py:TestHistoryManager.test_snapshot_roundtrip, tests/test_mcp_config.py:test_mcp_configuration_to_from_dict, tests/test_mcp_config.py:test_mcp_server_config_to_from_dict, tests/test_per_ticket_model.py:test_model_override_serialization, tests/test_persona_id.py:test_ticket_persona_id_serialization, tests/test_persona_models.py:test_persona_defaults, tests/test_persona_models.py:test_persona_serialization, tests/test_slice_editor_behavior.py:test_add_slice_with_annotations, tests/test_thinking_gui.py:test_thinking_segment_model_compatibility, tests/test_ticket_queue.py:test_ticket_to_dict_priority, tests/test_tiered_aggregation.py:test_persona_aggregation_strategy, tests/test_track_state_schema.py:test_track_state_to_dict, tests/test_track_state_schema.py:test_track_state_to_dict_with_none, tests/test_ui_summary_only_removal.py:test_file_item_serialization_with_flags] - """ - return { - "id": self.id, - "name": self.name, - "status": self.status, - "created_at": self.created_at.isoformat() if self.created_at else None, - "updated_at": self.updated_at.isoformat() if self.updated_at else None, - } - - @classmethod - def from_dict(cls, data: Metadata) -> "Metadata": - """ - [C: src/personas.py:PersonaManager.load_all, src/presets.py:PresetManager.load_all, src/project_manager.py:load_project, src/project_manager.py:load_track_state, src/tool_presets.py:ToolPresetManager.load_all_bias_profiles, src/tool_presets.py:ToolPresetManager.load_all_presets, src/workspace_manager.py:WorkspaceManager.load_all_profiles, tests/test_bias_models.py:test_bias_profile_model, tests/test_bias_models.py:test_tool_model, tests/test_bias_models.py:test_tool_preset_extension, tests/test_context_presets_models.py:test_context_preset_from_dict_legacy, tests/test_context_presets_models.py:test_context_preset_serialization, tests/test_context_presets_models.py:test_file_view_preset_serialization, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_deserialization_with_annotations, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_round_trip_annotations, tests/test_external_editor.py:TestExternalEditorConfig.test_from_dict_with_dict_editors, tests/test_external_editor.py:TestExternalEditorConfig.test_from_dict_with_string_editors, tests/test_external_editor.py:TestTextEditorConfig.test_from_dict_with_diff_args, tests/test_external_editor.py:TestTextEditorConfig.test_from_dict_without_diff_args, tests/test_file_item_model.py:test_file_item_from_dict, tests/test_file_item_model.py:test_file_item_from_dict_defaults, tests/test_history_manager.py:TestHistoryManager.test_snapshot_roundtrip, tests/test_mcp_config.py:test_mcp_configuration_to_from_dict, tests/test_mcp_config.py:test_mcp_server_config_to_from_dict, tests/test_per_ticket_model.py:test_model_override_default_on_deserialize, tests/test_per_ticket_model.py:test_model_override_deserialization, tests/test_persona_id.py:test_ticket_persona_id_deserialization, tests/test_persona_models.py:test_persona_defaults, tests/test_persona_models.py:test_persona_deserialization, tests/test_project_serialization.py:TestProjectSerialization.test_backward_compatibility_strings, tests/test_slice_editor_behavior.py:test_add_slice_with_annotations, tests/test_ticket_queue.py:test_ticket_from_dict_default_priority, tests/test_ticket_queue.py:test_ticket_from_dict_priority, tests/test_tiered_aggregation.py:test_persona_aggregation_strategy, tests/test_track_state_schema.py:test_track_state_from_dict, tests/test_track_state_schema.py:test_track_state_from_dict_empty_and_missing, tests/test_ui_summary_only_removal.py:test_file_item_serialization_with_flags] - """ - created = data.get("created_at") - updated = data.get("updated_at") - if isinstance(created, str): - try: - created = datetime.datetime.fromisoformat(created) - except ValueError: - created = None - if isinstance(updated, str): - try: - updated = datetime.datetime.fromisoformat(updated) - except ValueError: - updated = None - return cls( - id = data["id"], - name = data.get("name", ""), - status = data.get("status"), - created_at = created, - updated_at = updated, - ) +# MMA Core dataclasses (ThinkingSegment, Ticket, Track, WorkerContext, TrackMetadata) +# moved to src/mma.py in module_taxonomy_refactor_20260627 Phase 3.1. See +# the re-export block at the top of this module for backward-compat. #region: State & Config - -@dataclass -class TrackState: - metadata: Metadata = field(default_factory=dict) - discussion: List[str] = field(default_factory=list) - tasks: List[Ticket] = field(default_factory=list) - - def to_dict(self) -> Metadata: - """ - [C: src/personas.py:PersonaManager.save_persona, src/presets.py:PresetManager.save_preset, src/project_manager.py:save_project, src/project_manager.py:save_track_state, src/tool_presets.py:ToolPresetManager.save_bias_profile, src/tool_presets.py:ToolPresetManager.save_preset, src/workspace_manager.py:WorkspaceManager.save_profile, tests/test_bias_models.py:test_bias_profile_model, tests/test_bias_models.py:test_tool_model, tests/test_bias_models.py:test_tool_preset_extension, tests/test_context_presets_models.py:test_context_preset_serialization, tests/test_context_presets_models.py:test_file_view_preset_serialization, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_round_trip_annotations, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_serialization_with_annotations, tests/test_event_serialization.py:test_user_request_event_serialization, tests/test_external_editor.py:TestExternalEditorConfig.test_to_dict, tests/test_external_editor.py:TestTextEditorConfig.test_to_dict, tests/test_file_item_model.py:test_file_item_to_dict, tests/test_gui_events_v2.py:test_user_request_event_payload, tests/test_history_manager.py:TestHistoryManager.test_snapshot_roundtrip, tests/test_mcp_config.py:test_mcp_configuration_to_from_dict, tests/test_mcp_config.py:test_mcp_server_config_to_from_dict, tests/test_per_ticket_model.py:test_model_override_serialization, tests/test_persona_id.py:test_ticket_persona_id_serialization, tests/test_persona_models.py:test_persona_defaults, tests/test_persona_models.py:test_persona_serialization, tests/test_slice_editor_behavior.py:test_add_slice_with_annotations, tests/test_thinking_gui.py:test_thinking_segment_model_compatibility, tests/test_ticket_queue.py:test_ticket_to_dict_priority, tests/test_tiered_aggregation.py:test_persona_aggregation_strategy, tests/test_track_state_schema.py:test_track_state_to_dict, tests/test_track_state_schema.py:test_track_state_to_dict_with_none, tests/test_ui_summary_only_removal.py:test_file_item_serialization_with_flags] - """ - serialized_discussion = [] - for item in self.discussion: - if isinstance(item, dict): - new_item = dict(item) - if "ts" in new_item and isinstance(new_item["ts"], datetime.datetime): - new_item["ts"] = new_item["ts"].isoformat() - serialized_discussion.append(new_item) - else: - serialized_discussion.append(item) - return { - "metadata": self.metadata.to_dict(), - "discussion": serialized_discussion, - "tasks": [t.to_dict() for t in self.tasks], - } - - @classmethod - def from_dict(cls, data: Metadata) -> "TrackState": - """ - [C: src/personas.py:PersonaManager.load_all, src/presets.py:PresetManager.load_all, src/project_manager.py:load_project, src/project_manager.py:load_track_state, src/tool_presets.py:ToolPresetManager.load_all_bias_profiles, src/tool_presets.py:ToolPresetManager.load_all_presets, src/workspace_manager.py:WorkspaceManager.load_all_profiles, tests/test_bias_models.py:test_bias_profile_model, tests/test_bias_models.py:test_tool_model, tests/test_bias_models.py:test_tool_preset_extension, tests/test_context_presets_models.py:test_context_preset_from_dict_legacy, tests/test_context_presets_models.py:test_context_preset_serialization, tests/test_context_presets_models.py:test_file_view_preset_serialization, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_deserialization_with_annotations, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_round_trip_annotations, tests/test_external_editor.py:TestExternalEditorConfig.test_from_dict_with_dict_editors, tests/test_external_editor.py:TestExternalEditorConfig.test_from_dict_with_string_editors, tests/test_external_editor.py:TestTextEditorConfig.test_from_dict_with_diff_args, tests/test_external_editor.py:TestTextEditorConfig.test_from_dict_without_diff_args, tests/test_file_item_model.py:test_file_item_from_dict, tests/test_file_item_model.py:test_file_item_from_dict_defaults, tests/test_history_manager.py:TestHistoryManager.test_snapshot_roundtrip, tests/test_mcp_config.py:test_mcp_configuration_to_from_dict, tests/test_mcp_config.py:test_mcp_server_config_to_from_dict, tests/test_per_ticket_model.py:test_model_override_default_on_deserialize, tests/test_per_ticket_model.py:test_model_override_deserialization, tests/test_persona_id.py:test_ticket_persona_id_deserialization, tests/test_persona_models.py:test_persona_defaults, tests/test_persona_models.py:test_persona_deserialization, tests/test_project_serialization.py:TestProjectSerialization.test_backward_compatibility_strings, tests/test_slice_editor_behavior.py:test_add_slice_with_annotations, tests/test_ticket_queue.py:test_ticket_from_dict_default_priority, tests/test_ticket_queue.py:test_ticket_from_dict_priority, tests/test_tiered_aggregation.py:test_persona_aggregation_strategy, tests/test_track_state_schema.py:test_track_state_from_dict, tests/test_track_state_schema.py:test_track_state_from_dict_empty_and_missing, tests/test_ui_summary_only_removal.py:test_file_item_serialization_with_flags] - """ - discussion = data.get("discussion", []) - parsed_discussion = [] - for item in discussion: - if isinstance(item, dict): - new_item = dict(item) - ts = new_item.get("ts") - if isinstance(ts, str): - try: - new_item["ts"] = datetime.datetime.fromisoformat(ts) - except ValueError as e: - _ts_err = Result(data=ts, errors=[ErrorInfo(kind=ErrorKind.INVALID_INPUT, message=f"fromisoformat failed for ts={ts!r}: {e}", source="models.from_dict.discussion.ts", original=e)]) - import sys - sys.stderr.write(f"[models] fromisoformat failed for ts={ts!r}: {e}\n") - parsed_discussion.append(new_item) - else: - parsed_discussion.append(item) - return cls( - metadata = Metadata.from_dict(data["metadata"]), - discussion = parsed_discussion, - tasks = [Ticket.from_dict(t) for t in data.get("tasks", [])], - ) - - -EMPTY_TRACK_STATE: TrackState = TrackState() - +# TrackState moved to src/mma.py (re-exported at the top of this module for backward compat). @dataclass class FileItem: