validate.py
python
sha256:c079680aa72b38cf20f7891daeadbb28f6ec71301dff7c1973565401e6f23a34
docs: align public documentation and website with bounded v1
Human
minor
⚠ breaking
7 days ago
| 1 | """Validate the bounded-v1 informational site and its static content.""" |
| 2 | from __future__ import annotations |
| 3 | from dataclasses import dataclass, field |
| 4 | from html.parser import HTMLParser |
| 5 | from pathlib import Path |
| 6 | import re |
| 7 | from urllib.parse import unquote, urlsplit |
| 8 | import yaml |
| 9 | from tools.landing.schema import load_manifest |
| 10 | |
| 11 | RELEASE = 'https://github.com/aaronrene/overseer-kit/releases/tag/v1.0.0-rc.1' |
| 12 | COMMANDS = {'status', 'init', 'sync', 'adopt', 'mirror', 'next', 'next-write'} |
| 13 | SECTIONS = ('hero', 'context', 'how-it-works', 'modes', 'boundaries', 'start') |
| 14 | RETIRED = re.compile(r'\b(?:check-ok|governance-sync|handover-compact|honesty-status|hosted-dashboard|land-closeout|pr-land|upgrade-regime|verify-step)\b|\bok\s+(?:app|ledger|review|route|workspace|hook)\b|status\s+--check-footprint', re.I) |
| 15 | SECRET_PATTERNS = ( |
| 16 | re.compile(r'AKIA[0-9A-Z]{16}|sk-[a-zA-Z0-9]{20,}'), |
| 17 | re.compile(r'-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----'), |
| 18 | re.compile(r'(?i)(?:api[_-]?key|secret|password)\s*[:=]\s*[\'"][^\'"]{8,}[\'"]'), |
| 19 | ) |
| 20 | VOID = {'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'} |
| 21 | |
| 22 | |
| 23 | @dataclass |
| 24 | class ValidationResult: |
| 25 | ok: bool = True |
| 26 | errors: list[str] = field(default_factory=list) |
| 27 | |
| 28 | def add(self, code: str, detail: str) -> None: |
| 29 | self.ok = False |
| 30 | self.errors.append(f'{code}: {detail}') |
| 31 | |
| 32 | |
| 33 | class Page(HTMLParser): |
| 34 | def __init__(self, path: Path, result: ValidationResult): |
| 35 | super().__init__(convert_charrefs=True) |
| 36 | self.path, self.result = path, result |
| 37 | self.ids: set[str] = set() |
| 38 | self.links: list[str] = [] |
| 39 | self.stack: list[str] = [] |
| 40 | self.counts: dict[str, int] = {} |
| 41 | self.lang = self.viewport = False |
| 42 | self.codes: list[str] = [] |
| 43 | self.code: list[str] | None = None |
| 44 | self.text: list[str] = [] |
| 45 | |
| 46 | def handle_starttag(self, tag, attrs): |
| 47 | a = dict(attrs) |
| 48 | self.counts[tag] = self.counts.get(tag, 0) + 1 |
| 49 | if tag not in VOID: |
| 50 | self.stack.append(tag) |
| 51 | if tag == 'html': |
| 52 | self.lang = a.get('lang') == 'en' |
| 53 | if tag == 'meta' and a.get('name') == 'viewport': |
| 54 | self.viewport = 'width=device-width' in a.get('content', '') |
| 55 | if 'id' in a: |
| 56 | if a['id'] in self.ids: |
| 57 | self.result.add('duplicate_id', f'{self.path.name}: {a["id"]}') |
| 58 | self.ids.add(a['id']) |
| 59 | if tag == 'img' and 'alt' not in a: |
| 60 | self.result.add('image_alt', self.path.name) |
| 61 | if tag == 'th' and a.get('scope') not in {'row', 'col'}: |
| 62 | self.result.add('table_scope', self.path.name) |
| 63 | for key in ('href', 'src'): |
| 64 | if key in a: |
| 65 | self.links.append(a[key]) |
| 66 | if tag == 'script' and urlsplit(a.get('src', '')).netloc: |
| 67 | self.result.add('external_script', self.path.name) |
| 68 | if tag == 'code': |
| 69 | self.code = [] |
| 70 | |
| 71 | def handle_startendtag(self, tag, attrs): |
| 72 | self.handle_starttag(tag, attrs) |
| 73 | if tag not in VOID: |
| 74 | self.handle_endtag(tag) |
| 75 | |
| 76 | def handle_endtag(self, tag): |
| 77 | if not self.stack or self.stack[-1] != tag: |
| 78 | self.result.add('html_structure', f'{self.path.name}: unexpected </{tag}>') |
| 79 | else: |
| 80 | self.stack.pop() |
| 81 | if tag == 'code' and self.code is not None: |
| 82 | self.codes.append(''.join(self.code)) |
| 83 | self.code = None |
| 84 | |
| 85 | def handle_data(self, data): |
| 86 | if self.code is not None: |
| 87 | self.code.append(data) |
| 88 | if not any(tag in self.stack for tag in ('script', 'style')): |
| 89 | self.text.append(data) |
| 90 | |
| 91 | |
| 92 | def validate_landing(kit_root: Path) -> ValidationResult: |
| 93 | result = ValidationResult() |
| 94 | landing = (kit_root / 'docs/landing').resolve() |
| 95 | try: |
| 96 | manifest = load_manifest(landing / 'manifest.yaml') |
| 97 | except (OSError, ValueError, yaml.YAMLError) as exc: |
| 98 | result.add('manifest_parse', str(exc)) |
| 99 | return result |
| 100 | if manifest.section_ids != SECTIONS: |
| 101 | result.add('section_contract', str(manifest.section_ids)) |
| 102 | if manifest.primary_download_href != RELEASE: |
| 103 | result.add('release_target', str(manifest.primary_download_href)) |
| 104 | if manifest.status_badges != {'example'}: |
| 105 | result.add('scenario_status', 'Scenarios must be labelled examples') |
| 106 | if manifest.funnel_steps != ('install', 'init', 'status', 'next', 'next-write'): |
| 107 | result.add('flow', 'Expected the bounded-v1 installation and handoff flow') |
| 108 | pages = {} |
| 109 | for rel in ('index.html', 'docs.html', 'scenarios/index.html'): |
| 110 | path = landing / rel |
| 111 | if not path.is_file(): |
| 112 | result.add('missing_file', rel) |
| 113 | continue |
| 114 | html = path.read_text(encoding='utf-8') |
| 115 | page = Page(path, result) |
| 116 | page.feed(html) |
| 117 | page.close() |
| 118 | pages[path] = page |
| 119 | if page.stack or not html.lower().startswith('<!doctype html>'): |
| 120 | result.add('html_structure', rel) |
| 121 | if not page.lang or not page.viewport: |
| 122 | result.add('document_metadata', rel) |
| 123 | for tag in ('h1', 'main', 'title'): |
| 124 | if page.counts.get(tag) != 1: |
| 125 | result.add('landmark', f'{rel}: expected one {tag}') |
| 126 | if '#main' not in page.links: |
| 127 | result.add('skip_link', rel) |
| 128 | text = ' '.join(page.text) |
| 129 | if 'prerelease' not in text.lower() or 'does not run Overseer' not in text: |
| 130 | result.add('product_disclosure', rel) |
| 131 | if RETIRED.search(html) or re.search(r'\.dmg|127\.0\.0\.1|localhost|session_credential', html): |
| 132 | result.add('retired_surface', rel) |
| 133 | if re.search(r'\beval\s*\(', html): |
| 134 | result.add('script_eval', rel) |
| 135 | for path, page in pages.items(): |
| 136 | for link in page.links: |
| 137 | u = urlsplit(link) |
| 138 | if u.scheme or u.netloc: |
| 139 | if u.scheme != 'https' or not u.netloc or u.username or u.password: |
| 140 | result.add('unsafe_link', link) |
| 141 | continue |
| 142 | target = (path.parent / unquote(u.path)).resolve() if u.path else path |
| 143 | if target.is_dir(): |
| 144 | target /= 'index.html' |
| 145 | if not target.is_relative_to(landing) or not target.is_file(): |
| 146 | result.add('broken_link', f'{path.name}: {link}') |
| 147 | elif u.fragment and (target not in pages or unquote(u.fragment) not in pages[target].ids): |
| 148 | result.add('broken_anchor', f'{path.name}: {link}') |
| 149 | home = pages.get(landing / 'index.html') |
| 150 | docs = pages.get(landing / 'docs.html') |
| 151 | scenarios = pages.get(landing / 'scenarios/index.html') |
| 152 | if home and (not set(SECTIONS).issubset(home.ids) or 'cta-release' not in home.ids or RELEASE not in home.links): |
| 153 | result.add('home_contract', 'Missing sections or explicit rc.1 release action') |
| 154 | if docs and not COMMANDS.issubset(set(docs.codes)): |
| 155 | result.add('commands', 'Docs must list all seven public commands') |
| 156 | if scenarios and not {f'persona-{p}' for p in manifest.persona_ids}.issubset(scenarios.ids): |
| 157 | result.add('scenarios', 'Missing example scenario') |
| 158 | for path in landing.rglob('*'): |
| 159 | if path.is_file() and path.suffix in {'.html', '.css', '.js', '.svg', '.md', '.yaml'}: |
| 160 | text = path.read_text(encoding='utf-8') |
| 161 | if any(pattern.search(text) for pattern in SECRET_PATTERNS): |
| 162 | result.add('secret_leak', path.relative_to(landing).as_posix()) |
| 163 | if re.search(r'/Users/|/home/|RECOVERY-RUNS/|overseer-kit-v1-recovery', text): |
| 164 | result.add('private_path', path.relative_to(landing).as_posix()) |
| 165 | for rel in ('assets/style.css', 'assets/theme.js'): |
| 166 | if not (landing / rel).is_file(): |
| 167 | result.add('missing_file', rel) |
| 168 | if not (kit_root / 'LICENSE').is_file() or manifest.license != 'MIT': |
| 169 | result.add('license', 'Expected repository MIT license') |
| 170 | if not (kit_root / 'SECURITY.md').is_file(): |
| 171 | result.add('security', 'Missing security policy') |
| 172 | return result |
| 173 | |
| 174 | |
| 175 | def main() -> int: |
| 176 | import sys |
| 177 | root = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else Path.cwd().resolve() |
| 178 | result = validate_landing(root) |
| 179 | if not result.ok: |
| 180 | for error in result.errors: |
| 181 | print(error, file=sys.stderr) |
| 182 | return 1 |
| 183 | print('landing: ok') |
| 184 | return 0 |
| 185 | |
| 186 | |
| 187 | if __name__ == '__main__': |
| 188 | raise SystemExit(main()) |
File History
1 commit
sha256:c079680aa72b38cf20f7891daeadbb28f6ec71301dff7c1973565401e6f23a34
docs: align public documentation and website with bounded v1
Human
minor
⚠
7 days ago