""" Section Validator for markdown documents. Validates that document sections comply with x-markitect-sections classifications: - REQUIRED: Section must be present (ERROR if missing) - RECOMMENDED: Section should be present (WARNING if missing) - OPTIONAL: Section may be present (no check) - DISCOURAGED: Section should not be present (WARNING if present) - IMPROPER: Section must not be present (ERROR if present) """ from dataclasses import dataclass from typing import List, Dict, Any, Optional from pathlib import Path @dataclass class SectionIssue: """Base class for section validation issues.""" section_name: str severity: str # 'ERROR', 'WARNING', 'INFO' message: str classification: str # 'required', 'recommended', etc. line_number: Optional[int] = None def __str__(self) -> str: location = f" (line {self.line_number})" if self.line_number else "" return f"[{self.severity}]{location} {self.section_name}: {self.message}" @dataclass class SectionMissing(SectionIssue): """Section is missing from document.""" pass @dataclass class SectionImproper(SectionIssue): """Improper section found in document.""" pass @dataclass class SectionDiscouraged(SectionIssue): """Discouraged section found in document.""" pass @dataclass class SectionValidationResult: """Result of section validation.""" issues: List[SectionIssue] sections_checked: int sections_found: int def has_errors(self) -> bool: """Check if there are any ERROR-level issues.""" return any(issue.severity == 'ERROR' for issue in self.issues) def has_warnings(self) -> bool: """Check if there are any WARNING-level issues.""" return any(issue.severity == 'WARNING' for issue in self.issues) def is_valid(self) -> bool: """Check if validation passed (no errors).""" return not self.has_errors() def get_errors(self) -> List[SectionIssue]: """Get all ERROR-level issues.""" return [issue for issue in self.issues if issue.severity == 'ERROR'] def get_warnings(self) -> List[SectionIssue]: """Get all WARNING-level issues.""" return [issue for issue in self.issues if issue.severity == 'WARNING'] class SectionValidator: """ Validates section presence and classification compliance. Checks that markdown documents have the correct sections based on x-markitect-sections classifications in the schema. """ def __init__(self, schema: Dict[str, Any]): """ Initialize validator with a schema. Args: schema: JSON schema with x-markitect-sections extension """ self.schema = schema self.sections_spec = schema.get('x-markitect-sections', {}) def check(self, document: 'MarkdownDocument') -> SectionValidationResult: """ Validate section presence against schema classifications. Args: document: Parsed markdown document Returns: SectionValidationResult with any issues found """ issues = [] # Get level-2 headings (main sections) from document doc_sections = self._get_document_sections(document) # Check each specification for section_name, spec in self.sections_spec.items(): classification = spec.get('classification') section_in_doc = self._find_section(section_name, doc_sections, spec) if classification == 'required': if not section_in_doc: issues.append(SectionMissing( section_name=section_name, severity='ERROR', message=spec.get('error_message', f'{section_name} section is required'), classification='required' )) elif classification == 'improper': if section_in_doc: issues.append(SectionImproper( section_name=section_name, severity='ERROR', message=spec.get('error_message', f'{section_name} section must not appear'), classification='improper', line_number=section_in_doc.get('line_number') )) elif classification == 'recommended': if not section_in_doc: issues.append(SectionMissing( section_name=section_name, severity='WARNING', message=spec.get('warning_if_missing', f'{section_name} section is recommended'), classification='recommended' )) elif classification == 'discouraged': if section_in_doc: issues.append(SectionDiscouraged( section_name=section_name, severity='WARNING', message=spec.get('warning_if_present', f'{section_name} section is discouraged'), classification='discouraged', line_number=section_in_doc.get('line_number') )) return SectionValidationResult( issues=issues, sections_checked=len(self.sections_spec), sections_found=len(doc_sections) ) def _get_document_sections(self, document: 'MarkdownDocument') -> List[Dict[str, Any]]: """ Extract level-2 headings from document. Args: document: Parsed markdown document Returns: List of section dicts with name and line_number """ sections = [] # Get headings from document if hasattr(document, 'get_headings_by_level'): level_2_headings = document.get_headings_by_level(2) elif hasattr(document, 'headings'): level_2_headings = [ h for h in document.headings if h.get('level') == 2 ] else: # Fallback: parse from AST level_2_headings = [] for heading in level_2_headings: if isinstance(heading, dict): sections.append({ 'name': heading.get('content', '').strip().upper(), 'line_number': heading.get('line_number') }) elif isinstance(heading, str): sections.append({ 'name': heading.strip().upper(), 'line_number': None }) return sections def _find_section(self, section_name: str, doc_sections: List[Dict[str, Any]], spec: Dict[str, Any]) -> Optional[Dict[str, Any]]: """ Find a section in document, checking alternatives. Args: section_name: Primary section name to find doc_sections: List of sections in document spec: Section specification with potential alternatives Returns: Section dict if found, None otherwise """ # Normalize section name for comparison normalized_name = section_name.upper().strip() # Check primary name for section in doc_sections: if section['name'] == normalized_name: return section # Check alternatives alternatives = spec.get('alternatives', []) for alt_name in alternatives: normalized_alt = alt_name.upper().strip() for section in doc_sections: if section['name'] == normalized_alt: return section return None