Files
markitect-main/markitect/prompts/resolver/resolver.py
tegwick 706981c39f fix(prompts): fix three infrastructure bugs in prompt dependency resolution
- ContentMacro: add __post_init__ to auto-derive raw_text when built
  programmatically, preventing str.replace("", X) corruption
- MacroParser: add @{target} shorthand syntax support mapped to REQUIRED kind,
  updating parse, has_macros, count_macros, and find_macro_positions
- Artifact: store content in model and SQLite DB, replace resolver placeholder
  with actual artifact content, add migration for existing databases

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 20:53:02 +01:00

222 lines
6.6 KiB
Python

"""
PromptResolver for resolving template macros.
Implements FR-3: PromptResolver Behavior
"""
from typing import List, Optional
from markitect.prompts.templates.models import PromptTemplate, ContentMacro, MacroKind
from markitect.prompts.resolver.models import (
ResolutionContext,
ResolutionResult,
ResolutionError,
ResolvedMacro,
)
from markitect.prompts.resolver.strategy import ResolutionStrategy, ResolutionConfig
from markitect.prompts.services.artifact_service import ArtifactService
class PromptResolver:
"""
Resolver for prompt template macros.
Implements FR-3: PromptResolver Behavior
- Deterministic resolution order (FR-3.1)
- Required macro validation (FR-3.2)
- Optional macro fallback (FR-3.3)
- Generate macro detection (FR-3.4)
"""
def __init__(
self,
artifact_service: ArtifactService,
strategy: ResolutionStrategy,
):
"""
Initialize resolver.
Args:
artifact_service: Service for artifact lookup
strategy: Resolution strategy for search order
"""
self.artifact_service = artifact_service
self.strategy = strategy
def resolve_template(
self,
template: PromptTemplate,
config: ResolutionConfig,
) -> ResolutionResult:
"""
Resolve all macros in a template.
Implements FR-3: PromptResolver Behavior
Args:
template: Template to resolve
config: Resolution configuration
Returns:
ResolutionResult with resolved macros and status
Raises:
ValueError: If template hasn't been analyzed
"""
if not template.analyzed:
raise ValueError(
f"Template '{template.name}' must be analyzed before resolution. "
"Call TemplateAnalyzer.analyze() first."
)
# Get search order
search_order = self.strategy.get_search_order(config)
# Create resolution context
context = ResolutionContext(
template_id=template.id,
space_id=config.space_id,
search_order=search_order,
)
# Track resolved content and dependencies
resolved_content = {}
dependency_artifacts = []
# Resolve each macro
for macro in template.macros:
resolved = self._resolve_macro(macro, search_order, context)
if resolved.resolved and resolved.artifact:
resolved_content[macro.target] = resolved.content
dependency_artifacts.append(resolved.artifact.id)
# Create result
result = ResolutionResult(
context=context,
success=len(context.unresolved_required) == 0,
resolved_content=resolved_content,
dependency_artifacts=dependency_artifacts,
needs_generation=len(context.generator_macros) > 0,
)
return result
def _resolve_macro(
self,
macro: ContentMacro,
search_order: List[str],
context: ResolutionContext,
) -> ResolvedMacro:
"""
Resolve a single macro.
Implements:
- FR-3.2: Required macro failure
- FR-3.3: Optional macro empty fallback
- FR-3.4: Generate macro detection
Args:
macro: Macro to resolve
search_order: Ordered space IDs to search
context: Resolution context
Returns:
ResolvedMacro with resolution result
"""
# Handle generate macros separately (FR-3.4)
if macro.kind == MacroKind.GENERATE:
context.add_generator(macro)
return ResolvedMacro(
macro=macro,
artifact=None,
resolved=False, # Will be resolved during generation phase
space_id=None,
content="",
)
# Try to resolve in each space
for space_id in search_order:
artifact = self.artifact_service.repository.get_by_name(
space_id,
macro.target,
)
if artifact:
content = artifact.content
resolved = ResolvedMacro(
macro=macro,
artifact=artifact,
resolved=True,
space_id=space_id,
content=content,
)
context.add_resolved(resolved)
return resolved
# Not found in any space
if macro.kind == MacroKind.REQUIRED:
# FR-3.2: Required macros fail if not found
context.add_unresolved_required(macro)
return ResolvedMacro(
macro=macro,
artifact=None,
resolved=False,
space_id=None,
content="",
)
else:
# FR-3.3: Optional macros resolve to empty
context.add_unresolved_optional(macro)
return ResolvedMacro(
macro=macro,
artifact=None,
resolved=False,
space_id=None,
content="", # Empty content for optional
)
def validate_resolution(self, result: ResolutionResult) -> None:
"""
Validate resolution result.
Args:
result: Resolution result to validate
Raises:
ResolutionError: If required macros are missing
"""
if not result.success:
unresolved = result.context.unresolved_required
targets = [m.target for m in unresolved]
raise ResolutionError(
f"Failed to resolve required macros: {', '.join(targets)}"
)
def get_resolution_summary(self, result: ResolutionResult) -> dict:
"""
Get human-readable summary of resolution.
Args:
result: Resolution result
Returns:
Dictionary with summary information
"""
return {
"status": result.status.value,
"success": result.success,
"resolved_count": len(result.context.resolved_macros),
"unresolved_required": [
m.target for m in result.context.unresolved_required
],
"unresolved_optional": [
m.target for m in result.context.unresolved_optional
],
"needs_generation": result.needs_generation,
"generator_count": len(result.context.generator_macros),
"dependency_count": len(result.dependency_artifacts),
"search_order": result.context.search_order,
}