"""Unified project resolution across MCP, API, and CLI."""
import os
from dataclasses import dataclass
from enum import Enum, auto
from typing import Optional
from loguru import logger
class ResolutionMode(Enum):
"""How the project was resolved."""
ENV_CONSTRAINT = auto() # BASIC_MEMORY_MCP_PROJECT env var
EXPLICIT = auto() # Explicit project parameter
DEFAULT = auto() # default_project from config
DISCOVERY = auto() # Discovery mode allowed (no project)
NONE = auto() # No resolution possible
@dataclass(frozen=True)
class ResolvedProject:
"""Result of project resolution."""
project: Optional[str]
mode: ResolutionMode
reason: str
@property
def is_resolved(self) -> bool:
return self.project is not None
@property
def is_discovery_mode(self) -> bool:
return self.mode in {ResolutionMode.DISCOVERY, ResolutionMode.NONE} and self.project is None
@dataclass
class ProjectResolver:
"""Unified project resolution logic."""
default_project: Optional[str] = None
constrained_project: Optional[str] = None
@classmethod
def from_env(
cls,
default_project: Optional[str] = None,
) -> "ProjectResolver":
"""Create resolver with constrained_project from environment."""
return cls(
default_project=default_project,
constrained_project=os.environ.get("BASIC_MEMORY_MCP_PROJECT"),
)
def resolve(
self,
project: Optional[str] = None,
allow_discovery: bool = False,
) -> ResolvedProject:
"""Resolve project using a unified linear priority chain."""
if self.constrained_project:
logger.debug(f"Using constrained project from env: {self.constrained_project}")
return ResolvedProject(
project=self.constrained_project,
mode=ResolutionMode.ENV_CONSTRAINT,
reason=f"Environment constraint: BASIC_MEMORY_MCP_PROJECT={self.constrained_project}",
)
if project:
logger.debug(f"Using explicit project parameter: {project}")
return ResolvedProject(
project=project,
mode=ResolutionMode.EXPLICIT,
reason=f"Explicit parameter: {project}",
)
if self.default_project:
logger.debug(f"Using default project from config: {self.default_project}")
return ResolvedProject(
project=self.default_project,
mode=ResolutionMode.DEFAULT,
reason=f"Default project: {self.default_project}",
)
if allow_discovery:
logger.debug("No project resolved, using discovery mode")
return ResolvedProject(
project=None,
mode=ResolutionMode.DISCOVERY,
reason="Discovery mode enabled",
)
logger.debug("No project resolution possible")
return ResolvedProject(
project=None,
mode=ResolutionMode.NONE,
reason="No project specified and no default project configured",
)
def require_project(
self,
project: Optional[str] = None,
error_message: Optional[str] = None,
) -> ResolvedProject:
"""Resolve project, raising an error if not resolved."""
result = self.resolve(project, allow_discovery=False)
if not result.is_resolved:
msg = error_message or (
"No project specified. Either set 'default_project' in config, "
"or provide a 'project' argument."
)
raise ValueError(msg)
return result
__all__ = [
"ProjectResolver",
"ResolvedProject",
"ResolutionMode",
]