|
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +import re |
| 4 | +from dataclasses import dataclass |
| 5 | +from typing import Any |
| 6 | + |
| 7 | +from .period_contract import PeriodContractError, canonical_period_identity |
| 8 | + |
| 9 | + |
| 10 | +FINGERPRINT_VERSION = "semantic_fingerprint.v1.sha256" |
| 11 | + |
| 12 | +_DATE = r"(?P<as_of>\d{4}-\d{2}-\d{2})" |
| 13 | +_DIGEST = r"(?P<digest>[0-9a-f]{64})" |
| 14 | +_JSON_PATTERN = re.compile(rf"^advisory_report_{_DATE}(?:\.variant-{_DIGEST})?\.json$") |
| 15 | +_HTML_PATTERN = re.compile( |
| 16 | + rf"^{_DATE}-(?P<cadence>daily|weekly|monthly)-model-recommendations" |
| 17 | + rf"(?:\.variant-{_DIGEST})?\.html$" |
| 18 | +) |
| 19 | +_MD_PATTERN = re.compile(rf"^advisory_report_{_DATE}(?:\.variant-{_DIGEST})?\.md$") |
| 20 | +_MANIFEST_PATTERN = re.compile( |
| 21 | + rf"^advisory_report_{_DATE}(?:\.variant-{_DIGEST})?\.json\.manifest\.json$" |
| 22 | +) |
| 23 | + |
| 24 | + |
| 25 | +class IdentityMetadataError(ValueError): |
| 26 | + """Stable, sanitized identity lifecycle error.""" |
| 27 | + |
| 28 | + def __init__(self, code: str) -> None: |
| 29 | + self.code = code |
| 30 | + super().__init__(code) |
| 31 | + |
| 32 | + |
| 33 | +@dataclass(frozen=True, slots=True) |
| 34 | +class V1ProvisionalBinding: |
| 35 | + period_key: str |
| 36 | + as_of: str |
| 37 | + cadence: str |
| 38 | + json_name: str |
| 39 | + html_name: str |
| 40 | + status: str = "PROVISIONAL" |
| 41 | + |
| 42 | + |
| 43 | +@dataclass(frozen=True, slots=True) |
| 44 | +class VerifiedReportEvidence: |
| 45 | + period_key: str |
| 46 | + as_of: str |
| 47 | + cadence: str |
| 48 | + schema_version: str |
| 49 | + fingerprint_version: str |
| 50 | + fingerprint_digest: str |
| 51 | + status: str = "VERIFIED_REPORT_EVIDENCE" |
| 52 | + |
| 53 | + |
| 54 | +@dataclass(frozen=True, slots=True) |
| 55 | +class V2IdentityBinding: |
| 56 | + period_key: str |
| 57 | + as_of: str |
| 58 | + cadence: str |
| 59 | + schema_version: str |
| 60 | + fingerprint_version: str |
| 61 | + fingerprint_digest: str |
| 62 | + json_name: str |
| 63 | + html_name: str |
| 64 | + markdown_name: str | None |
| 65 | + manifest_name: str | None |
| 66 | + canonical_identity: bool |
| 67 | + display_primary: bool |
| 68 | + display_order: int |
| 69 | + status: str = "PENDING_IDENTITY_VALIDATION" |
| 70 | + |
| 71 | + |
| 72 | +@dataclass(frozen=True, slots=True) |
| 73 | +class V1ProvisionalIndex: |
| 74 | + schema_version: int |
| 75 | + bindings: tuple[V1ProvisionalBinding, ...] |
| 76 | + |
| 77 | + |
| 78 | +@dataclass(frozen=True, slots=True) |
| 79 | +class V2IdentityIndex: |
| 80 | + schema_version: int |
| 81 | + bindings: tuple[V2IdentityBinding, ...] |
| 82 | + |
| 83 | + |
| 84 | +def _error(code: str) -> IdentityMetadataError: |
| 85 | + return IdentityMetadataError(code) |
| 86 | + |
| 87 | + |
| 88 | +def _require_exact_type(value: object, expected: type, code: str) -> Any: |
| 89 | + if type(value) is not expected: |
| 90 | + raise _error(code) |
| 91 | + return value |
| 92 | + |
| 93 | + |
| 94 | +def _period_key(as_of: str, cadence: str) -> str: |
| 95 | + try: |
| 96 | + return canonical_period_identity(cadence, as_of).key |
| 97 | + except (PeriodContractError, TypeError, ValueError, OverflowError) as exc: |
| 98 | + raise _error("period_mismatch") from exc |
| 99 | + |
| 100 | + |
| 101 | +def _is_basename(value: object) -> bool: |
| 102 | + return isinstance(value, str) and bool(value) and "/" not in value and "\\" not in value |
| 103 | + |
| 104 | + |
| 105 | +def _name_digest(name: str, pattern: re.Pattern[str], *, as_of: str, code: str) -> str | None: |
| 106 | + if not _is_basename(name): |
| 107 | + raise _error("invalid_identity_name") |
| 108 | + match = pattern.fullmatch(name) |
| 109 | + if match is None or match.group("as_of") != as_of: |
| 110 | + raise _error("identity_name_mismatch") |
| 111 | + return match.groupdict().get("digest") |
| 112 | + |
| 113 | + |
| 114 | +def _validate_v1_entry(entry: object) -> V1ProvisionalBinding: |
| 115 | + if type(entry) is not dict or set(entry) != {"as_of", "cadence", "json", "html"}: |
| 116 | + raise _error("invalid_v1_entry") |
| 117 | + as_of = _require_exact_type(entry["as_of"], str, "invalid_as_of") |
| 118 | + cadence = _require_exact_type(entry["cadence"], str, "invalid_cadence") |
| 119 | + period_key = _period_key(as_of, cadence) |
| 120 | + json_name = _require_exact_type(entry["json"], str, "invalid_identity_name") |
| 121 | + html_name = _require_exact_type(entry["html"], str, "invalid_identity_name") |
| 122 | + if _name_digest(json_name, _JSON_PATTERN, as_of=as_of, code="invalid_identity_name") is not None: |
| 123 | + raise _error("v1_variant_unverified") |
| 124 | + if _name_digest(html_name, _HTML_PATTERN, as_of=as_of, code="invalid_identity_name") is not None: |
| 125 | + raise _error("v1_variant_unverified") |
| 126 | + if html_name != f"{as_of}-{cadence}-model-recommendations.html": |
| 127 | + raise _error("identity_name_mismatch") |
| 128 | + return V1ProvisionalBinding(period_key, as_of, cadence, json_name, html_name) |
| 129 | + |
| 130 | + |
| 131 | +def _validate_v2_entry(entry: object) -> V2IdentityBinding: |
| 132 | + required = { |
| 133 | + "period_key", "as_of", "cadence", "schema_version", "fingerprint_version", |
| 134 | + "fingerprint_digest", "json", "html", "canonical_identity", "display_primary", "display_order", |
| 135 | + } |
| 136 | + optional = {"md", "manifest"} |
| 137 | + if type(entry) is not dict or not required.issubset(entry) or set(entry) - required - optional: |
| 138 | + raise _error("invalid_v2_entry") |
| 139 | + as_of = _require_exact_type(entry["as_of"], str, "invalid_as_of") |
| 140 | + cadence = _require_exact_type(entry["cadence"], str, "invalid_cadence") |
| 141 | + period_key = _period_key(as_of, cadence) |
| 142 | + if entry["period_key"] != period_key: |
| 143 | + raise _error("period_mismatch") |
| 144 | + schema_version = _require_exact_type(entry["schema_version"], str, "invalid_schema_version") |
| 145 | + if schema_version not in {"5", "6"}: |
| 146 | + raise _error("invalid_schema_version") |
| 147 | + fingerprint_version = _require_exact_type(entry["fingerprint_version"], str, "invalid_fingerprint_version") |
| 148 | + if fingerprint_version != FINGERPRINT_VERSION: |
| 149 | + raise _error("invalid_fingerprint_version") |
| 150 | + fingerprint_digest = _require_exact_type(entry["fingerprint_digest"], str, "invalid_fingerprint_digest") |
| 151 | + if re.fullmatch(r"[0-9a-f]{64}", fingerprint_digest) is None: |
| 152 | + raise _error("invalid_fingerprint_digest") |
| 153 | + canonical_identity = _require_exact_type(entry["canonical_identity"], bool, "invalid_boolean") |
| 154 | + display_primary = _require_exact_type(entry["display_primary"], bool, "invalid_boolean") |
| 155 | + display_order = _require_exact_type(entry["display_order"], int, "invalid_display_order") |
| 156 | + if display_order < 0: |
| 157 | + raise _error("invalid_display_order") |
| 158 | + json_name = _require_exact_type(entry["json"], str, "invalid_identity_name") |
| 159 | + html_name = _require_exact_type(entry["html"], str, "invalid_identity_name") |
| 160 | + md_name = entry.get("md") |
| 161 | + manifest_name = entry.get("manifest") |
| 162 | + if "md" in entry: |
| 163 | + md_name = _require_exact_type(md_name, str, "invalid_identity_name") |
| 164 | + if "manifest" in entry: |
| 165 | + manifest_name = _require_exact_type(manifest_name, str, "invalid_identity_name") |
| 166 | + json_digest = _name_digest(json_name, _JSON_PATTERN, as_of=as_of, code="invalid_identity_name") |
| 167 | + html_digest = _name_digest(html_name, _HTML_PATTERN, as_of=as_of, code="invalid_identity_name") |
| 168 | + md_digest = None if md_name is None else _name_digest(md_name, _MD_PATTERN, as_of=as_of, code="invalid_identity_name") |
| 169 | + manifest_digest = None if manifest_name is None else _name_digest( |
| 170 | + manifest_name, _MANIFEST_PATTERN, as_of=as_of, code="invalid_identity_name" |
| 171 | + ) |
| 172 | + expected_html = f"{as_of}-{cadence}-model-recommendations.html" |
| 173 | + if html_name != expected_html and html_digest is None: |
| 174 | + raise _error("identity_name_mismatch") |
| 175 | + if any(digest != json_digest for digest in (html_digest, md_digest, manifest_digest)): |
| 176 | + raise _error("identity_name_mismatch") |
| 177 | + if json_digest is None: |
| 178 | + if canonical_identity is not True: |
| 179 | + raise _error("identity_metadata_mismatch") |
| 180 | + elif json_digest != fingerprint_digest or canonical_identity is not False: |
| 181 | + raise _error("identity_digest_mismatch" if json_digest != fingerprint_digest else "identity_metadata_mismatch") |
| 182 | + return V2IdentityBinding( |
| 183 | + period_key, as_of, cadence, schema_version, fingerprint_version, fingerprint_digest, |
| 184 | + json_name, html_name, md_name, manifest_name, canonical_identity, display_primary, display_order, |
| 185 | + ) |
| 186 | + |
| 187 | + |
| 188 | +def _validate_v1_index(bindings: tuple[V1ProvisionalBinding, ...]) -> None: |
| 189 | + artifact_map: dict[str, tuple[str, str, str, str]] = {} |
| 190 | + identity_map: set[tuple[str, str, str, str]] = set() |
| 191 | + for binding in bindings: |
| 192 | + logical = (binding.period_key, binding.json_name, binding.html_name, "v1") |
| 193 | + if logical in identity_map: |
| 194 | + raise _error("identity_artifact_conflict") |
| 195 | + identity_map.add(logical) |
| 196 | + for name in (binding.json_name, binding.html_name): |
| 197 | + previous = artifact_map.get(name) |
| 198 | + if previous is not None and previous != logical: |
| 199 | + raise _error("identity_artifact_conflict") |
| 200 | + artifact_map[name] = logical |
| 201 | + |
| 202 | + |
| 203 | +def _validate_v2_index(bindings: tuple[V2IdentityBinding, ...]) -> None: |
| 204 | + identity_map: set[tuple[str, tuple[str, str, str | None, str | None]]] = set() |
| 205 | + digest_map: dict[tuple[str, str, str], tuple[str, str, str | None, str | None]] = {} |
| 206 | + artifact_map: dict[str, tuple[str, tuple[str, str, str | None, str | None]]] = {} |
| 207 | + canonical_periods: set[str] = set() |
| 208 | + for binding in bindings: |
| 209 | + identity = (binding.json_name, binding.html_name, binding.markdown_name, binding.manifest_name) |
| 210 | + identity_key = (binding.period_key, identity) |
| 211 | + if identity_key in identity_map: |
| 212 | + raise _error("identity_content_conflict") |
| 213 | + identity_map.add(identity_key) |
| 214 | + if binding.canonical_identity: |
| 215 | + if binding.period_key in canonical_periods: |
| 216 | + raise _error("identity_canonical_conflict") |
| 217 | + canonical_periods.add(binding.period_key) |
| 218 | + logical = (binding.period_key, identity) |
| 219 | + for name in identity: |
| 220 | + if name is None: |
| 221 | + continue |
| 222 | + previous = artifact_map.get(name) |
| 223 | + if previous is not None and previous != logical: |
| 224 | + raise _error("identity_artifact_conflict") |
| 225 | + artifact_map[name] = logical |
| 226 | + digest_key = (binding.period_key, binding.fingerprint_version, binding.fingerprint_digest) |
| 227 | + previous = digest_map.get(digest_key) |
| 228 | + if previous is not None and previous != identity: |
| 229 | + raise _error("identity_digest_conflict") |
| 230 | + digest_map[digest_key] = identity |
| 231 | + |
| 232 | + |
| 233 | +def parse_v1_index(payload: object) -> V1ProvisionalIndex: |
| 234 | + if type(payload) is not dict or set(payload) != {"schema_version", "reports"}: |
| 235 | + raise _error("invalid_reports_index") |
| 236 | + if type(payload["schema_version"]) is not int or payload["schema_version"] != 1: |
| 237 | + raise _error("unsupported_index_version") |
| 238 | + reports = payload["reports"] |
| 239 | + if type(reports) is not list: |
| 240 | + raise _error("invalid_reports_index") |
| 241 | + bindings = tuple(_validate_v1_entry(entry) for entry in reports) |
| 242 | + _validate_v1_index(bindings) |
| 243 | + return V1ProvisionalIndex(1, bindings) |
| 244 | + |
| 245 | + |
| 246 | +def parse_v2_index(payload: object) -> V2IdentityIndex: |
| 247 | + if type(payload) is not dict or set(payload) != {"schema_version", "reports"}: |
| 248 | + raise _error("invalid_reports_index") |
| 249 | + if type(payload["schema_version"]) is not int or payload["schema_version"] != 2: |
| 250 | + raise _error("unsupported_index_version") |
| 251 | + reports = payload["reports"] |
| 252 | + if type(reports) is not list: |
| 253 | + raise _error("invalid_reports_index") |
| 254 | + bindings = tuple(_validate_v2_entry(entry) for entry in reports) |
| 255 | + _validate_v2_index(bindings) |
| 256 | + return V2IdentityIndex(2, bindings) |
| 257 | + |
| 258 | + |
| 259 | +def make_verified_report_evidence( |
| 260 | + *, as_of: str, cadence: str, schema_version: str, fingerprint_digest: str, |
| 261 | +) -> VerifiedReportEvidence: |
| 262 | + period_key = _period_key(as_of, cadence) |
| 263 | + if type(schema_version) is not str or schema_version not in {"5", "6"}: |
| 264 | + raise _error("invalid_schema_version") |
| 265 | + if type(fingerprint_digest) is not str or re.fullmatch(r"[0-9a-f]{64}", fingerprint_digest) is None: |
| 266 | + raise _error("invalid_fingerprint_digest") |
| 267 | + return VerifiedReportEvidence(period_key, as_of, cadence, schema_version, FINGERPRINT_VERSION, fingerprint_digest) |
0 commit comments