Private
Public Access
0
0

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:
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
globals()[name] = 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}")
# MMA Core dataclasses (ThinkingSegment, Ticket, Track, WorkerContext, TrackMetadata)
@@ -291,31 +295,10 @@ def __getattr__(name: str) -> Any:
# src.project_files directly.
#region: Tool Models
# Tool + ToolPreset moved to src/tool_presets.py in
# module_taxonomy_refactor_20260627 Phase 3d. The re-exports at the top of
# this module keep 'from src.models import Tool' (and ToolPreset) working for
# legacy callers. BiasProfile stays here until Phase 3e.
@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", {}),
)
# Tool + ToolPreset moved to src/tool_presets.py in Phase 3d. BiasProfile
# moved to src/tool_bias.py in Phase 3e. All three are re-exported lazily
# via the __getattr__ below to avoid the circular import (tool_presets and
# tool_bias both want to import from each other via models).
#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:
+4 -2
View File
@@ -1,3 +1,5 @@
from __future__ import annotations
import tomllib
import tomli_w
@@ -6,7 +8,6 @@ from pathlib import Path
from typing import Dict, List, Optional, Union, Any
from src import paths
from src.models import BiasProfile
from src.type_aliases import Metadata
@@ -135,10 +136,11 @@ class ToolPresetManager:
del data["presets"][name]
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]
"""
from src.tool_bias import BiasProfile
global_path = paths.get_global_tool_presets_path()
global_data = self._read_raw(global_path).get("bias_profiles", {})