Files
markitect-main/markitect/prompts/resolver/compiler.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

203 lines
6.9 KiB
Python

"""
Context compiler for assembling resolved prompts.
Compiles resolved macros into final prompt context.
"""
from dataclasses import dataclass, field
from datetime import datetime
from typing import Dict, List, Optional
from markitect.prompts.templates.models import PromptTemplate
from markitect.prompts.resolver.models import ResolutionResult
from markitect.prompts.models import calculate_content_digest
@dataclass
class CompiledPrompt:
"""
Compiled prompt ready for execution.
Contains the final assembled prompt with all macros resolved.
Attributes:
template_id: Source template ID
template_name: Source template name
content: Compiled prompt content with macros substituted
content_digest: SHA-256 digest of compiled content
resolution_result: Original resolution result
dependency_digests: Map of artifact name -> content digest
compiled_at: Compilation timestamp
metadata: Additional metadata
"""
template_id: str
template_name: str
content: str
content_digest: str
resolution_result: ResolutionResult
dependency_digests: Dict[str, str] = field(default_factory=dict)
compiled_at: datetime = field(default_factory=datetime.utcnow)
metadata: Dict[str, str] = field(default_factory=dict)
def to_dict(self) -> dict:
"""Convert to dictionary."""
return {
"template_id": self.template_id,
"template_name": self.template_name,
"content_digest": self.content_digest,
"content_length": len(self.content),
"dependency_count": len(self.dependency_digests),
"compiled_at": self.compiled_at.isoformat(),
"resolution_status": self.resolution_result.status.value,
}
class ContextCompiler:
"""
Compiler for assembling resolved context into final prompt.
Takes resolution results and produces CompiledPrompt with all
macros substituted.
"""
def compile(
self,
template: PromptTemplate,
template_content: str,
resolution_result: ResolutionResult,
) -> CompiledPrompt:
"""
Compile template with resolved macros into final prompt.
Args:
template: Source template
template_content: Original template content
resolution_result: Resolution result with resolved macros
Returns:
CompiledPrompt with macros substituted
Raises:
ValueError: If resolution failed
"""
if not resolution_result.success:
raise ValueError(
f"Cannot compile template '{template.name}': "
f"Resolution failed with unresolved required macros"
)
# Start with original template content
compiled_content = template_content
# Track dependency digests
dependency_digests = {}
# Substitute each resolved macro
for resolved in resolution_result.context.resolved_macros:
if resolved.resolved and resolved.artifact:
# Replace macro with resolved content
compiled_content = compiled_content.replace(
resolved.macro.raw_text,
resolved.content,
)
# Track dependency
dependency_digests[resolved.artifact.name] = resolved.artifact.content_digest
# Substitute unresolved optional macros with empty string
for macro in resolution_result.context.unresolved_optional:
compiled_content = compiled_content.replace(macro.raw_text, "")
# Calculate digest of compiled content
content_digest = calculate_content_digest(compiled_content)
return CompiledPrompt(
template_id=template.id,
template_name=template.name,
content=compiled_content,
content_digest=content_digest,
resolution_result=resolution_result,
dependency_digests=dependency_digests,
)
def compile_partial(
self,
template: PromptTemplate,
template_content: str,
resolution_result: ResolutionResult,
placeholder: str = "[UNRESOLVED]",
) -> CompiledPrompt:
"""
Compile template even with unresolved required macros.
Useful for debugging or preview. Unresolved required macros
are replaced with placeholder text.
Args:
template: Source template
template_content: Original template content
resolution_result: Resolution result (may have failures)
placeholder: Text to use for unresolved macros
Returns:
CompiledPrompt with partial resolution
"""
# Start with original template content
compiled_content = template_content
# Track dependency digests
dependency_digests = {}
# Substitute resolved macros
for resolved in resolution_result.context.resolved_macros:
if resolved.resolved and resolved.artifact:
compiled_content = compiled_content.replace(
resolved.macro.raw_text,
resolved.content,
)
dependency_digests[resolved.artifact.name] = resolved.artifact.content_digest
# Substitute unresolved required with placeholder
for macro in resolution_result.context.unresolved_required:
placeholder_text = f"{placeholder}:{macro.target}"
compiled_content = compiled_content.replace(
macro.raw_text,
placeholder_text,
)
# Substitute unresolved optional with empty
for macro in resolution_result.context.unresolved_optional:
compiled_content = compiled_content.replace(macro.raw_text, "")
content_digest = calculate_content_digest(compiled_content)
return CompiledPrompt(
template_id=template.id,
template_name=template.name,
content=compiled_content,
content_digest=content_digest,
resolution_result=resolution_result,
dependency_digests=dependency_digests,
metadata={"partial": "true", "placeholder": placeholder},
)
def get_compilation_info(self, compiled: CompiledPrompt) -> dict:
"""
Get information about compilation.
Args:
compiled: Compiled prompt
Returns:
Dictionary with compilation metadata
"""
return {
"template_id": compiled.template_id,
"template_name": compiled.template_name,
"content_length": len(compiled.content),
"content_digest": compiled.content_digest,
"dependencies": list(compiled.dependency_digests.keys()),
"dependency_count": len(compiled.dependency_digests),
"compiled_at": compiled.compiled_at.isoformat(),
"is_partial": compiled.metadata.get("partial") == "true",
}