""" Self-contained rules processor Processes JSON-based rules without hardcoded logic """ import json import re from pathlib import Path from typing import List, Tuple, Dict, Any class RulesProcessor: def __init__(self, rules_dir: Path): self.rules_dir = rules_dir self.rules = self._load_rules() def _load_rules(self) -> Dict[str, List[dict]]: """Load all rule files and organize by field""" rules_by_field = {} if self.rules_dir.exists(): for rule_file in self.rules_dir.glob('*.json'): with open(rule_file, 'r', encoding='utf-8') as f: rule = json.load(f) field = rule.get('field', '*') if field not in rules_by_field: rules_by_field[field] = [] rules_by_field[field].append(rule) # Sort rules by priority (lower number = higher priority) for field in rules_by_field: rules_by_field[field].sort(key=lambda r: r.get('priority', 100)) return rules_by_field def apply_rules(self, frontmatter: dict) -> Tuple[dict, bool, List[str]]: """ Apply all rules to frontmatter Returns: (modified_fm, is_conformant, violations) """ if not frontmatter: return frontmatter, True, [] modified = dict(frontmatter) violations = [] # Process global rules (field: "*") first if '*' in self.rules: for rule in self.rules['*']: modified, rule_violations = self._apply_rule(modified, rule, None) violations.extend(rule_violations) # Process rename rules to create new fields for field in list(modified.keys()): if field in self.rules: for rule in self.rules[field]: if rule.get('action') == 'rename_field': modified, rule_violations = self._apply_rule(modified, rule, field) violations.extend(rule_violations) # Process other field-specific rules on current fields for field in list(modified.keys()): if field in self.rules: for rule in self.rules[field]: if rule.get('action') != 'rename_field': # Skip renames, already done modified, rule_violations = self._apply_rule(modified, rule, field) violations.extend(rule_violations) is_conformant = len(violations) == 0 return modified, is_conformant, violations def _apply_rule(self, frontmatter: dict, rule: dict, field: str = None) -> Tuple[dict, List[str]]: """Apply a single rule to frontmatter""" modified = dict(frontmatter) violations = [] action = rule.get('action', 'validate') if action == 'normalize_keys': modified, violated = self._action_normalize_keys(modified, rule) if violated: violations.append(rule.get('name', 'unknown')) elif action == 'rename_field': modified, violated = self._action_rename_field(modified, rule) if violated: violations.append(rule.get('name', 'unknown')) elif action == 'normalize_value': if field and field in modified: modified[field], violated = self._action_normalize_value(modified[field], rule) if violated: violations.append(rule.get('name', 'unknown')) elif action == 'validate': if field and field in modified: violated = not self._action_validate(modified[field], rule) if violated: violations.append(rule.get('name', 'unknown')) return modified, violations def _action_normalize_keys(self, frontmatter: dict, rule: dict) -> Tuple[dict, bool]: """Normalize all keys using regex pattern or transform""" pattern = rule.get('pattern', '') replacement = rule.get('replacement', '') transform = rule.get('transform', '') modified = {} violated = False for key, value in frontmatter.items(): new_key = key # Apply transform (e.g., lower, upper) if transform == 'lower': new_key = key.lower() elif transform == 'upper': new_key = key.upper() elif pattern: # Apply regex pattern new_key = re.sub(pattern, replacement, key, flags=re.MULTILINE if rule.get('multiline') else 0) if new_key != key: violated = True modified[new_key] = value return modified, violated def _action_rename_field(self, frontmatter: dict, rule: dict) -> Tuple[dict, bool]: """Rename a specific field""" from_field = rule.get('from') to_field = rule.get('to') if not from_field or not to_field: return frontmatter, False modified = dict(frontmatter) if from_field in modified: modified[to_field] = modified.pop(from_field) return modified, True return modified, False def _action_normalize_value(self, value: Any, rule: dict) -> Tuple[Any, bool]: """Normalize a value using regex pattern(s) or transform""" pattern = rule.get('pattern', '') replacement = rule.get('replacement', '') transform = rule.get('transform', '') split_on = rule.get('split_on', '') violated = False # Handle comma-separated string first (convert to list) if split_on and isinstance(value, str): items = [item.strip() for item in value.split(split_on)] normalized = [] for item in items: if transform == 'lower': normalized_item = item.lower() elif transform == 'upper': normalized_item = item.upper() elif pattern: normalized_item = re.sub( pattern, replacement, item, flags=re.MULTILINE if rule.get('multiline') else 0 ) else: normalized_item = item if normalized_item: normalized.append(normalized_item) violated = True # Format changed from string to list return normalized, violated # Handle list values (like tags) elif isinstance(value, list): normalized = [] for item in value: item_str = str(item) # Apply transform first if transform == 'lower': normalized_item = item_str.lower() elif transform == 'upper': normalized_item = item_str.upper() elif pattern: normalized_item = re.sub( pattern, replacement, item_str, flags=re.MULTILINE if rule.get('multiline') else 0 ) else: normalized_item = item_str if normalized_item != item_str: violated = True if normalized_item: # Only keep non-empty normalized.append(normalized_item) return normalized, violated # Handle string values elif isinstance(value, str): if transform == 'lower': normalized = value.lower() elif transform == 'upper': normalized = value.upper() elif pattern: normalized = re.sub( pattern, replacement, value, flags=re.MULTILINE if rule.get('multiline') else 0 ) else: normalized = value violated = normalized != value return normalized, violated return value, False def _action_validate(self, value: Any, rule: dict) -> bool: """Validate a value against regex pattern""" pattern = rule.get('pattern', '') if not pattern: return True # Handle list values if isinstance(value, list): for item in value: if not re.match(pattern, str(item), flags=re.MULTILINE if rule.get('multiline') else 0): return False return True # Handle string values if isinstance(value, str): return bool(re.match(pattern, value, flags=re.MULTILINE if rule.get('multiline') else 0)) return True def get_llm_prompt_for_field(self, field: str) -> str: """Get LLM prompt text for a field if defined in rules""" if field in self.rules: for rule in self.rules[field]: llm_prompt = rule.get('llm_prompt') if llm_prompt: return llm_prompt return ""