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>
This commit is contained in:
183
markitect/prompts/resolver/models.py
Normal file
183
markitect/prompts/resolver/models.py
Normal file
@@ -0,0 +1,183 @@
|
||||
"""
|
||||
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,
|
||||
}
|
||||
Reference in New Issue
Block a user