validate.py
python
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1
docs: MuseHub-first before ISR #74 — staging solidify NEXT
Human
1 day ago
| 1 | """Fail-closed validator for landing assets (K12 + Landing + access clarity).""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | import re |
| 6 | from dataclasses import dataclass, field |
| 7 | from pathlib import Path |
| 8 | |
| 9 | import yaml |
| 10 | |
| 11 | from tools.landing.schema import LandingManifest, load_manifest |
| 12 | |
| 13 | # Frozen Auto v1 primary Download CTA (§LAC.2 / §LAC.12). |
| 14 | FROZEN_PRIMARY_DOWNLOAD_HREF = ( |
| 15 | "https://github.com/aaronrene/overseer-kit/releases/download/" |
| 16 | "v0.1.0/Overseer.Kit_0.1.0_aarch64.dmg" |
| 17 | ) |
| 18 | |
| 19 | LAC_SECTION_IDS: tuple[str, ...] = ( |
| 20 | "hero", |
| 21 | "kit-basics", |
| 22 | "problem", |
| 23 | "how-it-works", |
| 24 | "structure", |
| 25 | "console-access", |
| 26 | "musehub", |
| 27 | "next-steps", |
| 28 | "scenarios", |
| 29 | ) |
| 30 | |
| 31 | # Public main page must not advertise private/personal product doors or broken MuseHub TLS origin. |
| 32 | MAIN_PAGE_FORBIDDEN_PRODUCTS: tuple[str, ...] = ( |
| 33 | "Knowtation", |
| 34 | "Scooling", |
| 35 | "VideoFactory", |
| 36 | "musehub.ai", |
| 37 | ) |
| 38 | |
| 39 | DIAGRAM_REL_PATHS: tuple[str, ...] = ( |
| 40 | "assets/diagrams/lanes.svg", |
| 41 | "assets/diagrams/regimes.svg", |
| 42 | "assets/diagrams/layers.svg", |
| 43 | "assets/diagrams/kit-consumer.svg", |
| 44 | ) |
| 45 | |
| 46 | FORBIDDEN_LANDING_PHRASES: tuple[str, ...] = ( |
| 47 | "Sign up", |
| 48 | "Create account", |
| 49 | "executes tasks", |
| 50 | ) |
| 51 | |
| 52 | # Heuristic patterns — aligned with kit security tests; not exhaustive secret scanning. |
| 53 | SECRET_PATTERNS: tuple[re.Pattern[str], ...] = ( |
| 54 | re.compile(r"AKIA[0-9A-Z]{16}"), |
| 55 | re.compile(r"sk-[a-zA-Z0-9]{20,}"), |
| 56 | re.compile(r"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----"), |
| 57 | re.compile(r"(?i)(?:api[_-]?key|secret|password)\s*[:=]\s*['\"][^'\"]{8,}['\"]"), |
| 58 | ) |
| 59 | |
| 60 | EXTERNAL_SCRIPT_RE = re.compile( |
| 61 | r"""<script[^>]+src\s*=\s*["']https?://""", |
| 62 | re.IGNORECASE, |
| 63 | ) |
| 64 | |
| 65 | INLINE_EVAL_RE = re.compile(r"\beval\s*\(", re.IGNORECASE) |
| 66 | |
| 67 | # Public status-table residue (DONE/TODO/WIP boards on main landing). |
| 68 | STATUS_TABLE_RESIDUE_RE = re.compile( |
| 69 | r"<table[^>]*>.*?roadmap-public.*?</table>" |
| 70 | r"|<th[^>]*>\s*Status\s*</th>.*?\b(?:DONE|TODO|WIP)\b", |
| 71 | re.IGNORECASE | re.DOTALL, |
| 72 | ) |
| 73 | |
| 74 | MINT_CSRF_OR_SESSION_RE = re.compile( |
| 75 | r"\bmint\w*.{0,40}\b(?:csrf|session_credential)\b" |
| 76 | r"|\b(?:csrf|session_credential)\b.{0,40}\bmint\w*", |
| 77 | re.IGNORECASE, |
| 78 | ) |
| 79 | |
| 80 | PRIMARY_CTA_HREF_RE = re.compile( |
| 81 | r"""id=["']cta-download-mac["'][^>]*href=["']([^"']+)["']""" |
| 82 | r"""|href=["']([^"']+)["'][^>]*id=["']cta-download-mac["']""", |
| 83 | re.IGNORECASE, |
| 84 | ) |
| 85 | |
| 86 | GITHUB_RELEASE_DOWNLOAD_RE = re.compile( |
| 87 | r"^https://github\.com/aaronrene/overseer-kit/releases/download/" |
| 88 | r"v[0-9][^/]+/[^/]+\.dmg$" |
| 89 | ) |
| 90 | |
| 91 | |
| 92 | @dataclass |
| 93 | class ValidationResult: |
| 94 | """Outcome of ``validate_landing``.""" |
| 95 | |
| 96 | ok: bool |
| 97 | errors: list[str] = field(default_factory=list) |
| 98 | |
| 99 | def add(self, code: str, detail: str) -> None: |
| 100 | self.errors.append(f"{code}: {detail}") |
| 101 | self.ok = False |
| 102 | |
| 103 | |
| 104 | def _read(path: Path) -> str: |
| 105 | return path.read_text(encoding="utf-8") |
| 106 | |
| 107 | |
| 108 | def _check_html_sections(html: str, manifest: LandingManifest, result: ValidationResult) -> None: |
| 109 | for section_id in manifest.section_ids: |
| 110 | if f'id="{section_id}"' not in html and f"id='{section_id}'" not in html: |
| 111 | result.add("missing_section", section_id) |
| 112 | if tuple(manifest.section_ids) != LAC_SECTION_IDS: |
| 113 | result.add( |
| 114 | "section_order", |
| 115 | f"expected {list(LAC_SECTION_IDS)}, got {list(manifest.section_ids)}", |
| 116 | ) |
| 117 | |
| 118 | |
| 119 | def _check_personas(html: str, manifest: LandingManifest, result: ValidationResult) -> None: |
| 120 | for persona_id in manifest.persona_ids: |
| 121 | marker = f'id="persona-{persona_id}"' |
| 122 | alt = f"id='persona-{persona_id}'" |
| 123 | if marker not in html and alt not in html: |
| 124 | result.add("missing_persona", persona_id) |
| 125 | continue |
| 126 | idx = html.find(marker) if marker in html else html.find(alt) |
| 127 | window = html[max(0, idx - 200) : idx + 400] |
| 128 | if not any(f"badge-{badge}" in window for badge in manifest.status_badges): |
| 129 | result.add("missing_badge", persona_id) |
| 130 | |
| 131 | |
| 132 | def _check_secret_leaks(text: str, rel_path: str, result: ValidationResult) -> None: |
| 133 | for pattern in SECRET_PATTERNS: |
| 134 | if pattern.search(text): |
| 135 | result.add("secret_leak", f"{rel_path} matches {pattern.pattern}") |
| 136 | |
| 137 | |
| 138 | def _check_html_security(html: str, rel_path: str, result: ValidationResult) -> None: |
| 139 | if EXTERNAL_SCRIPT_RE.search(html): |
| 140 | result.add("external_script", rel_path) |
| 141 | if INLINE_EVAL_RE.search(html): |
| 142 | result.add("external_script", f"{rel_path} contains eval()") |
| 143 | |
| 144 | |
| 145 | def _check_relative_doc_links(html: str, kit_root: Path, html_path: Path, result: ValidationResult) -> None: |
| 146 | """Resolve href='../foo.md' style links from landing pages (fragments ignored).""" |
| 147 | for match in re.finditer(r"""href=["'](\.\./[^"'#]+(?:\.md|\.yml)?)(?:#[^"']*)?["']""", html): |
| 148 | target = (html_path.parent / match.group(1)).resolve() |
| 149 | try: |
| 150 | target.relative_to(kit_root.resolve()) |
| 151 | except ValueError: |
| 152 | result.add("broken_link", f"{html_path.name} -> {match.group(1)} escapes kit root") |
| 153 | continue |
| 154 | if not target.exists(): |
| 155 | result.add("broken_link", f"{html_path.name} -> {match.group(1)}") |
| 156 | |
| 157 | |
| 158 | def _check_lac_index_contract(html: str, landing: Path, result: ValidationResult) -> None: |
| 159 | """Enforce Download href, diagrams, forbidden copy, and residue strip (§LAC.12).""" |
| 160 | match = PRIMARY_CTA_HREF_RE.search(html) |
| 161 | if not match: |
| 162 | result.add("download_cta", "missing #cta-download-mac primary Download href") |
| 163 | else: |
| 164 | href = match.group(1) or match.group(2) |
| 165 | if href != FROZEN_PRIMARY_DOWNLOAD_HREF: |
| 166 | result.add("download_cta", f"href {href!r} != frozen {FROZEN_PRIMARY_DOWNLOAD_HREF!r}") |
| 167 | if not GITHUB_RELEASE_DOWNLOAD_RE.match(href): |
| 168 | result.add("download_cta", f"href host/path not GitHub releases .dmg: {href}") |
| 169 | |
| 170 | for phrase in FORBIDDEN_LANDING_PHRASES: |
| 171 | if phrase in html: |
| 172 | result.add("forbidden_copy", phrase) |
| 173 | |
| 174 | for phrase in MAIN_PAGE_FORBIDDEN_PRODUCTS: |
| 175 | if phrase in html: |
| 176 | result.add("forbidden_product", phrase) |
| 177 | |
| 178 | if MINT_CSRF_OR_SESSION_RE.search(html): |
| 179 | result.add("forbidden_copy", "mint+csrf/session_credential marketing") |
| 180 | |
| 181 | # Prefer GitHub-rendered docs over raw relative .md (file:// / Pages raw). |
| 182 | for match in re.finditer(r"""href=["'](\.\./[^"']+\.md)["']""", html): |
| 183 | result.add("raw_md_link", match.group(1)) |
| 184 | |
| 185 | if 'id="roadmap-public"' in html or "id='roadmap-public'" in html: |
| 186 | result.add("residue", "roadmap-public section present on main landing") |
| 187 | if STATUS_TABLE_RESIDUE_RE.search(html): |
| 188 | result.add("residue", "DONE/TODO/WIP status table residue on main landing") |
| 189 | |
| 190 | for rel in DIAGRAM_REL_PATHS: |
| 191 | if rel not in html: |
| 192 | result.add("missing_diagram_ref", rel) |
| 193 | diagram_path = landing / rel |
| 194 | if not diagram_path.is_file(): |
| 195 | result.add("missing_file", f"docs/landing/{rel}") |
| 196 | else: |
| 197 | text = _read(diagram_path) |
| 198 | if "<svg" not in text.lower() or len(text.strip()) < 32: |
| 199 | result.add("diagram_malformed", rel) |
| 200 | |
| 201 | for path_id in ("path-1", "path-2", "path-3", "console-access"): |
| 202 | if f'id="{path_id}"' not in html and f"id='{path_id}'" not in html: |
| 203 | result.add("missing_playbook", path_id) |
| 204 | |
| 205 | |
| 206 | def validate_landing(kit_root: Path) -> ValidationResult: |
| 207 | """Validate landing assets under ``kit_root`` (fail-closed).""" |
| 208 | result = ValidationResult(ok=True) |
| 209 | landing = kit_root / "docs" / "landing" |
| 210 | manifest_path = landing / "manifest.yaml" |
| 211 | |
| 212 | if not manifest_path.is_file(): |
| 213 | result.add("manifest_parse", "docs/landing/manifest.yaml missing") |
| 214 | return result |
| 215 | |
| 216 | try: |
| 217 | manifest = load_manifest(manifest_path) |
| 218 | except (OSError, yaml.YAMLError, ValueError) as exc: |
| 219 | result.add("manifest_parse", str(exc)) |
| 220 | return result |
| 221 | |
| 222 | index_path = landing / "index.html" |
| 223 | scenarios_path = landing / "scenarios" / "index.html" |
| 224 | license_path = kit_root / "LICENSE" |
| 225 | security_path = kit_root / "SECURITY.md" |
| 226 | |
| 227 | for path in (index_path, scenarios_path): |
| 228 | if not path.is_file(): |
| 229 | result.add("missing_file", str(path.relative_to(kit_root))) |
| 230 | continue |
| 231 | html = _read(path) |
| 232 | _check_secret_leaks(html, str(path.relative_to(kit_root)), result) |
| 233 | _check_html_security(html, str(path.relative_to(kit_root)), result) |
| 234 | _check_relative_doc_links(html, kit_root, path, result) |
| 235 | |
| 236 | if index_path.is_file(): |
| 237 | index_html = _read(index_path) |
| 238 | _check_html_sections(index_html, manifest, result) |
| 239 | _check_lac_index_contract(index_html, landing, result) |
| 240 | |
| 241 | if scenarios_path.is_file(): |
| 242 | _check_personas(_read(scenarios_path), manifest, result) |
| 243 | |
| 244 | if not license_path.is_file(): |
| 245 | result.add("license", "LICENSE missing") |
| 246 | else: |
| 247 | license_text = _read(license_path) |
| 248 | spdx = (manifest.license or "").strip() |
| 249 | if spdx == "MIT": |
| 250 | if "MIT License" not in license_text and "MIT" not in license_text: |
| 251 | result.add("license", "LICENSE must reference MIT") |
| 252 | if "Copyright 2026 Overseer Kit contributors" not in license_text: |
| 253 | result.add("license", "LICENSE missing frozen copyright holder line") |
| 254 | elif spdx and spdx not in license_text: |
| 255 | result.add("license", f"manifest expects {manifest.license}") |
| 256 | elif not spdx: |
| 257 | result.add("license", "manifest.license missing") |
| 258 | |
| 259 | if not security_path.is_file(): |
| 260 | result.add("security", "SECURITY.md missing") |
| 261 | else: |
| 262 | security_text = _read(security_path) |
| 263 | if "Reporting a vulnerability" not in security_text: |
| 264 | result.add("security", "SECURITY.md missing disclosure section heading") |
| 265 | |
| 266 | css_path = landing / "assets" / "style.css" |
| 267 | if not css_path.is_file(): |
| 268 | result.add("missing_file", "docs/landing/assets/style.css") |
| 269 | |
| 270 | return result |
| 271 | |
| 272 | |
| 273 | def main() -> int: |
| 274 | """CLI entry: ``python -m tools.landing.validate [KIT_ROOT]``.""" |
| 275 | import sys |
| 276 | |
| 277 | root = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else Path.cwd().resolve() |
| 278 | outcome = validate_landing(root) |
| 279 | if not outcome.ok: |
| 280 | for err in outcome.errors: |
| 281 | print(err, file=sys.stderr) |
| 282 | return 1 |
| 283 | print("landing: ok") |
| 284 | return 0 |
| 285 | |
| 286 | |
| 287 | if __name__ == "__main__": |
| 288 | raise SystemExit(main()) |
File History
2 commits
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1
docs: MuseHub-first before ISR #74 — staging solidify NEXT
Human
1 day ago
sha256:4671b7f787ddbe63ced31c895b688c77ab495653b65a730b423329f26b3c1439
feat: K1-P1 complete — agent provenance, build-verification…
Sonnet 4.6
patch
52 days ago