"""journal_validator.py — Jamie Journal Editor validation tool. Usage: python journal_validator.py --input article.md python journal_validator.py --input article.md --check structure,compliance python journal_validator.py --input article.md --output report.json --verbose Checks available: structure, compliance, spelling, links (default: all) Exit codes: 0 — validation passed (no critical/high issues) 1 — validation failed (critical or high issues found) 2 — input error (file not found, bad arguments, etc.) """ import re import json import sys from typing import Dict, List, Tuple, Optional from dataclasses import dataclass, asdict, field import argparse # --------------------------------------------------------------------------- # Data model # --------------------------------------------------------------------------- @dataclass class ValidationIssue: checker: str issue_type: str severity: str # critical | high | medium | info line_number: int matched_text: str message: str suggestion: str # --------------------------------------------------------------------------- # Stub checker classes # --------------------------------------------------------------------------- class StructureValidator: """Validates document structure (headings, sections, word count, etc.).""" def check(self, content: str) -> List[ValidationIssue]: return [] class ComplianceChecker: """Checks brand compliance rules for Jamie Clinic journal content.""" def check(self, content: str) -> List[ValidationIssue]: return [] class SpellingChecker: """Checks for common spelling/terminology issues in medical/beauty content.""" def check(self, content: str) -> List[ValidationIssue]: return [] class LinkRecommender: """Recommends internal/external links and detects broken or missing ones.""" def check(self, content: str) -> List[ValidationIssue]: return [] # --------------------------------------------------------------------------- # Orchestrator # --------------------------------------------------------------------------- VALID_CHECKS = {"structure", "compliance", "spelling", "links"} _CHECKER_MAP = { "structure": StructureValidator, "compliance": ComplianceChecker, "spelling": SpellingChecker, "links": LinkRecommender, } class JournalValidator: """Orchestrates all validation checks and returns a consolidated report. Args: checks: List of check names to run. Defaults to all valid checks. Valid values: 'structure', 'compliance', 'spelling', 'links'. """ def __init__(self, checks: Optional[List[str]] = None): if checks is None: self.checks = list(VALID_CHECKS) else: unknown = set(checks) - VALID_CHECKS if unknown: raise ValueError( f"Unknown check(s): {', '.join(sorted(unknown))}. " f"Valid options: {', '.join(sorted(VALID_CHECKS))}" ) self.checks = checks def validate(self, content: str) -> Dict: """Run all enabled checkers against content and return a report dict. Returns: { "is_valid": bool, # False if any critical or high issue exists "total_issues": int, "issues_by_severity": { "critical": int, "high": int, "medium": int, "info": int, }, "issues": [dict, ...], # serialised ValidationIssue dicts "summary": str, } """ all_issues: List[ValidationIssue] = [] for check_name in self.checks: checker_cls = _CHECKER_MAP[check_name] checker = checker_cls() issues = checker.check(content) all_issues.extend(issues) counts: Dict[str, int] = {"critical": 0, "high": 0, "medium": 0, "info": 0} for issue in all_issues: severity = issue.severity if issue.severity in counts else "info" counts[severity] += 1 is_valid = counts["critical"] == 0 and counts["high"] == 0 if not all_issues: summary = "All checks passed." else: parts = [] if counts["critical"]: parts.append(f"{counts['critical']} critical") if counts["high"]: parts.append(f"{counts['high']} high") if counts["medium"]: parts.append(f"{counts['medium']} medium") if counts["info"]: parts.append(f"{counts['info']} info") summary = f"Found {len(all_issues)} issue(s): {', '.join(parts)}." return { "is_valid": is_valid, "total_issues": len(all_issues), "issues_by_severity": counts, "issues": [asdict(i) for i in all_issues], "summary": summary, } # --------------------------------------------------------------------------- # CLI # --------------------------------------------------------------------------- def _build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( prog="journal_validator", description="Validate a Jamie Journal article against editorial rules.", ) parser.add_argument( "--input", "-i", required=True, metavar="FILE", help="Path to the markdown article to validate.", ) parser.add_argument( "--check", "-c", default="all", metavar="CHECKS", help=( "Comma-separated list of checks to run " "(structure, compliance, spelling, links). " "Default: all" ), ) parser.add_argument( "--output", "-o", metavar="FILE", help="Optional path to write the JSON report.", ) parser.add_argument( "--verbose", "-v", action="store_true", help="Print detailed issue list.", ) return parser def _print_summary_table(report: Dict, verbose: bool = False) -> None: sev = report["issues_by_severity"] print("\n=== Journal Validator Report ===") print(f" Status : {'PASS' if report['is_valid'] else 'FAIL'}") print(f" Total : {report['total_issues']}") print(f" Critical: {sev['critical']}") print(f" High : {sev['high']}") print(f" Medium : {sev['medium']}") print(f" Info : {sev['info']}") print(f"\n {report['summary']}") if verbose and report["issues"]: print("\n--- Issues ---") for i, issue in enumerate(report["issues"], start=1): print( f" [{i}] [{issue['severity'].upper()}] " f"Line {issue['line_number']} | {issue['checker']} | " f"{issue['message']}" ) if issue.get("suggestion"): print(f" Suggestion: {issue['suggestion']}") print() def main() -> None: parser = _build_parser() args = parser.parse_args() # --- Resolve checks --- if args.check.strip().lower() == "all": checks = list(VALID_CHECKS) else: checks = [c.strip() for c in args.check.split(",") if c.strip()] # --- Read input file --- try: with open(args.input, "r", encoding="utf-8") as fh: content = fh.read() except FileNotFoundError: print(f"Error: file not found: {args.input}", file=sys.stderr) sys.exit(2) except OSError as exc: print(f"Error reading file: {exc}", file=sys.stderr) sys.exit(2) # --- Run validation --- try: validator = JournalValidator(checks=checks) except ValueError as exc: print(f"Error: {exc}", file=sys.stderr) sys.exit(2) report = validator.validate(content) # --- Output --- _print_summary_table(report, verbose=args.verbose) if args.output: try: with open(args.output, "w", encoding="utf-8") as fh: json.dump(report, fh, ensure_ascii=False, indent=2) print(f"Report written to: {args.output}") except OSError as exc: print(f"Error writing report: {exc}", file=sys.stderr) sys.exit(2) sys.exit(0 if report["is_valid"] else 1) if __name__ == "__main__": main()