Private
Public Access
0
0
Files
manual_slop/src/mma.py
T
ed cd828e5267 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.
2026-06-26 07:19:37 -04:00

227 lines
6.8 KiB
Python

"""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()