schema.py python
80 lines 2.9 KB
Raw
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1 docs: MuseHub-first before ISR #74 — staging solidify NEXT Human 3 days ago
1 """Frozen landing manifest types (K12)."""
2
3 from __future__ import annotations
4
5 from dataclasses import dataclass
6 from pathlib import Path
7 from typing import Any
8
9 import yaml
10
11
12 @dataclass(frozen=True)
13 class LandingManifest:
14 """Parsed ``docs/landing/manifest.yaml`` contract."""
15
16 version: int
17 license: str
18 section_ids: tuple[str, ...]
19 persona_ids: tuple[str, ...]
20 status_badges: frozenset[str]
21 funnel_steps: tuple[str, ...]
22 primary_download_href: str | None = None
23
24
25 def load_manifest(manifest_path: Path) -> LandingManifest:
26 """Load and normalize the landing manifest."""
27 raw: dict[str, Any] = yaml.safe_load(manifest_path.read_text(encoding="utf-8"))
28 if not isinstance(raw, dict):
29 raise ValueError("manifest root must be a mapping")
30
31 version = raw.get("version")
32 if version != 1:
33 raise ValueError(f"unsupported manifest version: {version!r}")
34
35 license_id = raw.get("license")
36 if not isinstance(license_id, str) or not license_id.strip():
37 raise ValueError("manifest.license must be a non-empty string")
38
39 sections = raw.get("sections")
40 if not isinstance(sections, list) or not sections:
41 raise ValueError("manifest.sections must be a non-empty list")
42 section_ids: list[str] = []
43 for item in sections:
44 if not isinstance(item, dict) or "id" not in item:
45 raise ValueError("each section must be a mapping with id")
46 section_ids.append(str(item["id"]))
47
48 personas = raw.get("personas")
49 if not isinstance(personas, list) or not personas:
50 raise ValueError("manifest.personas must be a non-empty list")
51 persona_ids: list[str] = []
52 for item in personas:
53 if not isinstance(item, dict) or "id" not in item:
54 raise ValueError("each persona must be a mapping with id")
55 persona_ids.append(str(item["id"]))
56 status = item.get("status")
57 if status not in raw.get("status_badges", []):
58 raise ValueError(f"persona {item['id']} has unknown status {status!r}")
59
60 badges = raw.get("status_badges")
61 if not isinstance(badges, list) or not badges:
62 raise ValueError("manifest.status_badges must be a non-empty list")
63
64 funnel = raw.get("funnel_steps")
65 if not isinstance(funnel, list) or not funnel:
66 raise ValueError("manifest.funnel_steps must be a non-empty list")
67
68 primary = raw.get("primary_download_href")
69 if primary is not None and (not isinstance(primary, str) or not primary.strip()):
70 raise ValueError("manifest.primary_download_href must be a non-empty string when set")
71
72 return LandingManifest(
73 version=int(version),
74 license=license_id.strip(),
75 section_ids=tuple(section_ids),
76 persona_ids=tuple(persona_ids),
77 status_badges=frozenset(str(b) for b in badges),
78 funnel_steps=tuple(str(s) for s in funnel),
79 primary_download_href=primary.strip() if isinstance(primary, str) else None,
80 )
File History 2 commits
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1 docs: MuseHub-first before ISR #74 — staging solidify NEXT Human 3 days ago
sha256:4671b7f787ddbe63ced31c895b688c77ab495653b65a730b423329f26b3c1439 feat: K1-P1 complete — agent provenance, build-verification… Sonnet 4.6 patch 55 days ago