Files
markitect-main/markitect/prompts/templates/analyzer.py
tegwick e6840fe696 feat(prompts): implement Phase 2 - Templates & Macros (FR-2)
Implement PromptTemplate models with ContentMacro parsing and analysis.

Core Features:
- PromptTemplate extending Artifact for template-specific operations
- ContentMacro model supporting REQUIRED, OPTIONAL, GENERATE kinds
- MacroParser for extracting macros from template content
- TemplateAnalyzer for dependency extraction and validation
- TemplateService for high-level template operations
- Template metadata for model hints and expected inputs

Macro Syntax:
- {{require:artifact-name}} - Required dependency
- {{optional:artifact-name}} - Optional dependency
- {{generate:template-name|param=value}} - Nested generation

Tests (38 passing):
- 18 template model tests (macros, templates, metadata)
- 20 parser tests (parsing, validation, parameters, aliases)

Implements:
- FR-2.1: PromptTemplate as content artifact with macros
- FR-2.2: ContentMacro detection and extraction
- FR-2.3: Required/Optional/Generate macro kinds

Files Created:
- markitect/prompts/templates/models.py
- markitect/prompts/templates/parser.py
- markitect/prompts/templates/analyzer.py
- markitect/prompts/services/template_service.py
- tests/unit/prompts/test_template_models.py
- tests/unit/prompts/test_macro_parser.py

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

194 lines
6.2 KiB
Python

"""
Template analyzer for dependency extraction.
Implements FR-2.2: TemplateAnalysis
Analyzes templates to extract and catalog content macros.
"""
from typing import List, Set
from markitect.prompts.templates.models import PromptTemplate, MacroKind
from markitect.prompts.templates.parser import MacroParser
class TemplateAnalysisResult:
"""
Result of template analysis.
Provides summary information about template dependencies.
Attributes:
template: Analyzed template
total_macros: Total number of macros found
required_count: Number of required macros
optional_count: Number of optional macros
generate_count: Number of generate macros
unique_targets: Set of unique target names
has_circular_potential: Whether template references itself
"""
def __init__(self, template: PromptTemplate):
self.template = template
self.total_macros = len(template.macros)
self.required_count = len(template.get_required_dependencies())
self.optional_count = len(template.get_optional_dependencies())
self.generate_count = len(template.get_generators())
self.unique_targets = self._get_unique_targets()
self.has_circular_potential = template.name in self.unique_targets
def _get_unique_targets(self) -> Set[str]:
"""Get set of unique target names from all macros."""
return {macro.target for macro in self.template.macros}
def to_dict(self) -> dict:
"""Convert result to dictionary."""
return {
"template_id": self.template.id,
"template_name": self.template.name,
"total_macros": self.total_macros,
"required_count": self.required_count,
"optional_count": self.optional_count,
"generate_count": self.generate_count,
"unique_targets": list(self.unique_targets),
"has_circular_potential": self.has_circular_potential,
}
class TemplateAnalyzer:
"""
Analyzer for extracting dependencies from templates.
Implements FR-2.2: TemplateAnalysis
Uses MacroParser to detect and extract ContentMacros, then populates
the template's macro list and provides analysis results.
"""
def __init__(self):
self.parser = MacroParser()
def analyze(self, template: PromptTemplate, content: str) -> TemplateAnalysisResult:
"""
Analyze template content and extract macros.
Updates the template's macros list and marks it as analyzed.
Args:
template: Template to analyze
content: Template content to parse
Returns:
Analysis result with summary information
Raises:
MacroParsingError: If macro syntax is invalid
"""
# Parse macros from content
macros = self.parser.parse(content)
# Update template
template.macros = macros
template.analyzed = True
# Create and return analysis result
return TemplateAnalysisResult(template)
def quick_check(self, content: str) -> dict:
"""
Quick check of template content without full analysis.
Useful for validation before template creation.
Args:
content: Template content
Returns:
Dictionary with macro counts and basic info
"""
has_macros = self.parser.has_macros(content)
if not has_macros:
return {
"has_macros": False,
"required": 0,
"optional": 0,
"generate": 0,
"total": 0,
}
counts = self.parser.count_macros(content)
return {
"has_macros": True,
"required": counts['required'],
"optional": counts['optional'],
"generate": counts['generate'],
"total": sum(counts.values()),
}
def find_dependencies(self, template: PromptTemplate) -> dict:
"""
Extract dependency information from analyzed template.
Args:
template: Analyzed template
Returns:
Dictionary with categorized dependencies
Raises:
ValueError: If template has not been analyzed
"""
if not template.analyzed:
raise ValueError(
f"Template '{template.name}' has not been analyzed. "
"Call analyze() first."
)
return {
"required": template.get_required_dependencies(),
"optional": template.get_optional_dependencies(),
"generators": template.get_generators(),
"all_targets": list({m.target for m in template.macros}),
}
def validate_references(
self,
template: PromptTemplate,
available_artifacts: Set[str],
) -> dict:
"""
Validate that template references can be resolved.
Args:
template: Template to validate
available_artifacts: Set of available artifact names
Returns:
Dictionary with validation results:
- missing_required: List of missing required artifacts
- missing_optional: List of missing optional artifacts
- missing_generators: List of missing generator templates
- is_valid: True if all required deps are available
Raises:
ValueError: If template has not been analyzed
"""
if not template.analyzed:
raise ValueError(
f"Template '{template.name}' has not been analyzed. "
"Call analyze() first."
)
required = set(template.get_required_dependencies())
optional = set(template.get_optional_dependencies())
generators = set(template.get_generators())
missing_required = required - available_artifacts
missing_optional = optional - available_artifacts
missing_generators = generators - available_artifacts
return {
"missing_required": list(missing_required),
"missing_optional": list(missing_optional),
"missing_generators": list(missing_generators),
"is_valid": len(missing_required) == 0,
}