refactor(tool_bias): merge BiasProfile from models.py into tool_bias.py

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).
This commit is contained in:
ed
2026-06-26 10:10:28 -04:00
parent 6adaae2ec3
commit ecd8e82f2f
3 changed files with 40 additions and 29 deletions
+8 -25
View File
@@ -276,6 +276,10 @@ def __getattr__(name: str) -> Any:
val = _Tool if name == "Tool" else _ToolPreset val = _Tool if name == "Tool" else _ToolPreset
globals()[name] = val globals()[name] = val
return val return val
if name == "BiasProfile":
from src.tool_bias import BiasProfile as _BiasProfile
globals()[name] = _BiasProfile
return _BiasProfile
raise AttributeError(f"module {__name__!r} has no attribute {name!r}") raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
# MMA Core dataclasses (ThinkingSegment, Ticket, Track, WorkerContext, TrackMetadata) # MMA Core dataclasses (ThinkingSegment, Ticket, Track, WorkerContext, TrackMetadata)
@@ -291,31 +295,10 @@ def __getattr__(name: str) -> Any:
# src.project_files directly. # src.project_files directly.
#region: Tool Models #region: Tool Models
# Tool + ToolPreset moved to src/tool_presets.py in # Tool + ToolPreset moved to src/tool_presets.py in Phase 3d. BiasProfile
# module_taxonomy_refactor_20260627 Phase 3d. The re-exports at the top of # moved to src/tool_bias.py in Phase 3e. All three are re-exported lazily
# this module keep 'from src.models import Tool' (and ToolPreset) working for # via the __getattr__ below to avoid the circular import (tool_presets and
# legacy callers. BiasProfile stays here until Phase 3e. # tool_bias both want to import from each other via models).
@dataclass
class BiasProfile:
name: str
tool_weights: Dict[str, int] = field(default_factory=dict)
category_multipliers: Dict[str, float] = field(default_factory=dict)
def to_dict(self) -> Metadata:
return {
"name": self.name,
"tool_weights": self.tool_weights,
"category_multipliers": self.category_multipliers,
}
@classmethod
def from_dict(cls, data: Metadata) -> "BiasProfile":
return cls(
name = data["name"],
tool_weights = data.get("tool_weights", {}),
category_multipliers = data.get("category_multipliers", {}),
)
#region: UI/Editor #region: UI/Editor
+28 -2
View File
@@ -1,6 +1,32 @@
from typing import List, Dict, Any, Optional from __future__ import annotations
from src.models import Tool, ToolPreset, BiasProfile from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional
from src.tool_presets import Tool, ToolPreset
from src.type_aliases import Metadata
@dataclass
class BiasProfile:
name: str
tool_weights: Dict[str, int] = field(default_factory=dict)
category_multipliers: Dict[str, float] = field(default_factory=dict)
def to_dict(self) -> Metadata:
return {
"name": self.name,
"tool_weights": self.tool_weights,
"category_multipliers": self.category_multipliers,
}
@classmethod
def from_dict(cls, data: Metadata) -> "BiasProfile":
return cls(
name = data["name"],
tool_weights = data.get("tool_weights", {}),
category_multipliers = data.get("category_multipliers", {}),
)
class ToolBiasEngine: class ToolBiasEngine:
+4 -2
View File
@@ -1,3 +1,5 @@
from __future__ import annotations
import tomllib import tomllib
import tomli_w import tomli_w
@@ -6,7 +8,6 @@ from pathlib import Path
from typing import Dict, List, Optional, Union, Any from typing import Dict, List, Optional, Union, Any
from src import paths from src import paths
from src.models import BiasProfile
from src.type_aliases import Metadata from src.type_aliases import Metadata
@@ -135,10 +136,11 @@ class ToolPresetManager:
del data["presets"][name] del data["presets"][name]
self._write_raw(path, data) self._write_raw(path, data)
def load_all_bias_profiles(self) -> Dict[str, BiasProfile]: def load_all_bias_profiles(self) -> Dict[str, "BiasProfile"]:
""" """
[C: tests/test_tool_preset_manager.py:test_bias_profiles_merged, tests/test_tool_preset_manager.py:test_delete_bias_profile, tests/test_tool_preset_manager.py:test_save_bias_profile] [C: tests/test_tool_preset_manager.py:test_bias_profiles_merged, tests/test_tool_preset_manager.py:test_delete_bias_profile, tests/test_tool_preset_manager.py:test_save_bias_profile]
""" """
from src.tool_bias import BiasProfile
global_path = paths.get_global_tool_presets_path() global_path = paths.get_global_tool_presets_path()
global_data = self._read_raw(global_path).get("bias_profiles", {}) global_data = self._read_raw(global_path).get("bias_profiles", {})