validate.py python
288 lines 10.1 KB
Raw
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