(tour_path: str, repo_root: str = ".")
| 54 | |
| 55 | |
| 56 | def validate_tour(tour_path: str, repo_root: str = ".") -> dict: |
| 57 | repo = Path(repo_root).resolve() |
| 58 | errors = [] |
| 59 | warnings = [] |
| 60 | info = [] |
| 61 | |
| 62 | # ── 1. JSON validity ──────────────────────────────────────────────────── |
| 63 | try: |
| 64 | with open(tour_path, errors="replace") as f: |
| 65 | tour = json.load(f) |
| 66 | except json.JSONDecodeError as e: |
| 67 | return { |
| 68 | "passed": False, |
| 69 | "errors": [f"Invalid JSON: {e}"], |
| 70 | "warnings": [], |
| 71 | "info": [], |
| 72 | "stats": {}, |
| 73 | } |
| 74 | except FileNotFoundError: |
| 75 | return { |
| 76 | "passed": False, |
| 77 | "errors": [f"File not found: {tour_path}"], |
| 78 | "warnings": [], |
| 79 | "info": [], |
| 80 | "stats": {}, |
| 81 | } |
| 82 | |
| 83 | # ── 2. Required top-level fields ──────────────────────────────────────── |
| 84 | if "title" not in tour: |
| 85 | errors.append("Missing required field: 'title'") |
| 86 | if "steps" not in tour: |
| 87 | errors.append("Missing required field: 'steps'") |
| 88 | return {"passed": False, "errors": errors, "warnings": warnings, "info": info, "stats": {}} |
| 89 | |
| 90 | steps = tour["steps"] |
| 91 | if not isinstance(steps, list): |
| 92 | errors.append("'steps' must be an array") |
| 93 | return {"passed": False, "errors": errors, "warnings": warnings, "info": info, "stats": {}} |
| 94 | |
| 95 | if len(steps) == 0: |
| 96 | errors.append("Tour has no steps") |
| 97 | return {"passed": False, "errors": errors, "warnings": warnings, "info": info, "stats": {}} |
| 98 | |
| 99 | # ── 3. Tour-level optional fields ─────────────────────────────────────── |
| 100 | if "nextTour" in tour: |
| 101 | tours_dir = Path(tour_path).parent |
| 102 | next_title = tour["nextTour"] |
| 103 | found_next = False |
| 104 | for tf in tours_dir.glob("*.tour"): |
| 105 | if tf.resolve() == Path(tour_path).resolve(): |
| 106 | continue |
| 107 | try: |
| 108 | other = json.loads(tf.read_text()) |
| 109 | if other.get("title") == next_title: |
| 110 | found_next = True |
| 111 | break |
| 112 | except Exception: |
| 113 | pass |
no test coverage detected