Files
markitect-main/markitect/prompts/resolver/models.py
tegwick 5f463e5b20 feat(prompts): implement Phase 3 - Resolver Engine (FR-3)
Implement deterministic multi-space resolution with configurable search order.

Core Features:
- ResolutionContext and ResolutionResult for tracking resolution state
- MultiSpaceResolutionStrategy implementing FR-3.1 search order:
  1. Local InformationSpace
  2. Explicitly included InformationSpaces
  3. Default InformationSpace
  4. Team/Shared InformationSpace
- PromptResolver with macro resolution logic
- ContextCompiler for assembling resolved prompts
- ResolutionConfig for configurable resolution behavior

Resolution Behavior:
- Required macros fail if not found (FR-3.2)
- Optional macros resolve to empty (FR-3.3)
- Generate macros detected for deferred execution (FR-3.4)
- Deterministic search order with duplicate removal
- Partial compilation support for debugging

Tests (31 passing):
- 14 strategy tests (search order, duplicates, priority)
- 9 resolver tests (required, optional, generate, multi-space)
- 8 compiler tests (substitution, dependencies, digests)

Implements:
- FR-3.1: Deterministic resolution order
- FR-3.2: Required macro validation
- FR-3.3: Optional macro fallback
- FR-3.4: Generate macro detection
- FR-3.5: Max generation depth configuration

Files Created:
- markitect/prompts/resolver/models.py
- markitect/prompts/resolver/strategy.py
- markitect/prompts/resolver/resolver.py
- markitect/prompts/resolver/compiler.py
- migrations/prompts/002_create_resolution_config.sql
- tests/unit/prompts/test_resolution_strategy.py
- tests/unit/prompts/test_prompt_resolver.py
- tests/unit/prompts/test_context_compiler.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 22:45:46 +01:00

184 lines
6.2 KiB
Python

"""
Models for resolution engine.
Defines resolution context, results, and error types for macro resolution.
"""
from dataclasses import dataclass, field
from typing import List, Dict, Optional, Any
from enum import Enum
from markitect.prompts.templates.models import ContentMacro, MacroKind
from markitect.prompts.models import Artifact
class ResolutionStatus(Enum):
"""Status of resolution operation."""
SUCCESS = "success"
PARTIAL = "partial" # Some optional macros missing
FAILED = "failed" # Required macros missing
class ResolutionError(Exception):
"""Raised when macro resolution fails."""
def __init__(self, message: str, macro: Optional[ContentMacro] = None):
super().__init__(message)
self.macro = macro
@dataclass
class ResolvedMacro:
"""
Result of resolving a single macro.
Attributes:
macro: Original macro
artifact: Resolved artifact (None if not found)
resolved: Whether artifact was found
space_id: Space where artifact was found
content: Resolved content (empty string if not found for optional)
"""
macro: ContentMacro
artifact: Optional[Artifact] = None
resolved: bool = False
space_id: Optional[str] = None
content: str = ""
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary."""
return {
"macro": self.macro.to_dict(),
"artifact_id": self.artifact.id if self.artifact else None,
"artifact_name": self.artifact.name if self.artifact else None,
"resolved": self.resolved,
"space_id": self.space_id,
"content_preview": self.content[:100] + "..." if len(self.content) > 100 else self.content,
}
@dataclass
class ResolutionContext:
"""
Context for macro resolution.
Tracks resolution state, search order, and resolved artifacts.
Attributes:
template_id: ID of template being resolved
space_id: Primary space ID
search_order: Ordered list of space IDs to search
resolved_macros: List of resolved macros
unresolved_required: List of unresolved required macros
unresolved_optional: List of unresolved optional macros
generator_macros: List of generate macros (deferred)
errors: List of resolution errors
metadata: Additional context metadata
"""
template_id: str
space_id: str
search_order: List[str] = field(default_factory=list)
resolved_macros: List[ResolvedMacro] = field(default_factory=list)
unresolved_required: List[ContentMacro] = field(default_factory=list)
unresolved_optional: List[ContentMacro] = field(default_factory=list)
generator_macros: List[ContentMacro] = field(default_factory=list)
errors: List[str] = field(default_factory=list)
metadata: Dict[str, Any] = field(default_factory=dict)
def add_resolved(self, resolved: ResolvedMacro) -> None:
"""Add a resolved macro to context."""
self.resolved_macros.append(resolved)
def add_unresolved_required(self, macro: ContentMacro) -> None:
"""Record an unresolved required macro."""
self.unresolved_required.append(macro)
self.errors.append(
f"Required macro '{macro.target}' not found (line {macro.line_number})"
)
def add_unresolved_optional(self, macro: ContentMacro) -> None:
"""Record an unresolved optional macro."""
self.unresolved_optional.append(macro)
def add_generator(self, macro: ContentMacro) -> None:
"""Record a generator macro for deferred execution."""
self.generator_macros.append(macro)
def has_errors(self) -> bool:
"""Check if any resolution errors occurred."""
return len(self.errors) > 0
def get_status(self) -> ResolutionStatus:
"""Get overall resolution status."""
if self.unresolved_required:
return ResolutionStatus.FAILED
elif self.unresolved_optional:
return ResolutionStatus.PARTIAL
else:
return ResolutionStatus.SUCCESS
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary."""
return {
"template_id": self.template_id,
"space_id": self.space_id,
"search_order": self.search_order,
"resolved_count": len(self.resolved_macros),
"unresolved_required": [m.target for m in self.unresolved_required],
"unresolved_optional": [m.target for m in self.unresolved_optional],
"generator_count": len(self.generator_macros),
"status": self.get_status().value,
"errors": self.errors,
}
@dataclass
class ResolutionResult:
"""
Result of template resolution.
Contains resolved content and metadata about the resolution process.
Attributes:
context: Resolution context with full state
success: Whether all required macros were resolved
resolved_content: Dictionary of macro -> resolved content
dependency_artifacts: List of artifact IDs used
needs_generation: Whether any generate macros were found
"""
context: ResolutionContext
success: bool
resolved_content: Dict[str, str] = field(default_factory=dict)
dependency_artifacts: List[str] = field(default_factory=list)
needs_generation: bool = False
@property
def status(self) -> ResolutionStatus:
"""Get resolution status."""
return self.context.get_status()
def get_resolved_macro(self, target: str) -> Optional[ResolvedMacro]:
"""
Get resolved macro by target name.
Args:
target: Macro target name
Returns:
ResolvedMacro if found, None otherwise
"""
for resolved in self.context.resolved_macros:
if resolved.macro.target == target:
return resolved
return None
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary."""
return {
"success": self.success,
"status": self.status.value,
"context": self.context.to_dict(),
"dependency_count": len(self.dependency_artifacts),
"needs_generation": self.needs_generation,
}