validate.py file-level

at feat/ove · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 💥 blast risk
sha256:f docs: recenter public story on governance loop · · Oct 11, 2026
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())