|
| 1 | +"""Strict shared validator for the clean-slate vNext identity namespace.""" |
| 2 | + |
| 3 | +from __future__ import annotations |
| 4 | + |
| 5 | +import re |
| 6 | + |
| 7 | +from .artifact_integrity import ARTIFACT_INTEGRITY_VERSION |
| 8 | +from .identity_lifecycle import FINGERPRINT_VERSION |
| 9 | +from .identity_v3 import PENDING_ARTIFACT_VALIDATION, V3_CANONICAL, V3_VARIANT, V3IdentityBinding |
| 10 | +from .period_contract import PeriodContractError, canonical_period_identity |
| 11 | +from .time_contract import TimeContractError, contract_version_for_schema |
| 12 | + |
| 13 | + |
| 14 | +class VNextBindingError(ValueError): |
| 15 | + """Stable, sanitized clean-slate binding error.""" |
| 16 | + |
| 17 | + def __init__(self, code: str) -> None: |
| 18 | + self.code = code |
| 19 | + super().__init__(code) |
| 20 | + |
| 21 | + |
| 22 | +_DATE = r"(?P<as_of>\d{4}-\d{2}-\d{2})" |
| 23 | +_DIGEST = r"(?P<digest>[0-9a-f]{64})" |
| 24 | +_CADENCE = r"(?P<cadence>daily|weekly|monthly)" |
| 25 | +_JSON = re.compile(rf"^advisory_report_{_DATE}-{_CADENCE}(?:\.variant-{_DIGEST})?\.json$") |
| 26 | +_HTML = re.compile(rf"^{_DATE}-{_CADENCE}-model-recommendations(?:\.variant-{_DIGEST})?\.html$") |
| 27 | +_MD = re.compile(rf"^advisory_report_{_DATE}-{_CADENCE}(?:\.variant-{_DIGEST})?\.md$") |
| 28 | +_MANIFEST = re.compile( |
| 29 | + rf"^advisory_report_{_DATE}-{_CADENCE}(?:\.variant-{_DIGEST})?\.json\.manifest\.json$" |
| 30 | +) |
| 31 | + |
| 32 | + |
| 33 | +def _error(code: str) -> VNextBindingError: |
| 34 | + return VNextBindingError(code) |
| 35 | + |
| 36 | + |
| 37 | +def binding_payload(binding: V3IdentityBinding) -> dict[str, object]: |
| 38 | + payload: dict[str, object] = { |
| 39 | + "period_key": binding.period_key, |
| 40 | + "as_of": binding.as_of, |
| 41 | + "cadence": binding.cadence, |
| 42 | + "report_schema_version": binding.report_schema_version, |
| 43 | + "contract_version": binding.contract_version, |
| 44 | + "semantic_fingerprint_version": binding.semantic_fingerprint_version, |
| 45 | + "semantic_digest": binding.semantic_digest, |
| 46 | + "artifact_integrity_version": binding.artifact_integrity_version, |
| 47 | + "artifact_integrity_digest": binding.artifact_integrity_digest, |
| 48 | + "json": binding.json_name, |
| 49 | + "html": binding.html_name, |
| 50 | + "identity_class": binding.identity_class, |
| 51 | + "canonical_identity": binding.canonical_identity, |
| 52 | + "display_primary": binding.display_primary, |
| 53 | + "display_order": binding.display_order, |
| 54 | + } |
| 55 | + if binding.markdown_name is not None: |
| 56 | + payload["md"] = binding.markdown_name |
| 57 | + if binding.manifest_name is not None: |
| 58 | + payload["manifest"] = binding.manifest_name |
| 59 | + return payload |
| 60 | + |
| 61 | + |
| 62 | +def _name_digest(name: object, pattern: re.Pattern[str], *, as_of: str, cadence: str) -> str | None: |
| 63 | + if type(name) is not str or not name or "/" in name or "\\" in name: |
| 64 | + raise _error("identity_name_invalid") |
| 65 | + match = pattern.fullmatch(name) |
| 66 | + if match is None or match.group("as_of") != as_of or match.group("cadence") != cadence: |
| 67 | + raise _error("identity_name_mismatch") |
| 68 | + return match.groupdict().get("digest") |
| 69 | + |
| 70 | + |
| 71 | +def validate_vnext_binding(entry: object) -> V3IdentityBinding: |
| 72 | + """Validate a clean wire entry or binding; no legacy compatibility exists.""" |
| 73 | + |
| 74 | + if isinstance(entry, V3IdentityBinding): |
| 75 | + if entry.status != PENDING_ARTIFACT_VALIDATION: |
| 76 | + raise _error("identity_binding_invalid") |
| 77 | + entry = binding_payload(entry) |
| 78 | + required = { |
| 79 | + "period_key", "as_of", "cadence", "report_schema_version", "contract_version", |
| 80 | + "semantic_fingerprint_version", "semantic_digest", "artifact_integrity_version", |
| 81 | + "artifact_integrity_digest", "json", "html", "identity_class", "canonical_identity", |
| 82 | + "display_primary", "display_order", |
| 83 | + } |
| 84 | + optional = {"md", "manifest"} |
| 85 | + if type(entry) is not dict or not required.issubset(entry) or set(entry) - required - optional: |
| 86 | + raise _error("identity_binding_invalid") |
| 87 | + as_of = entry["as_of"] |
| 88 | + cadence = entry["cadence"] |
| 89 | + if type(as_of) is not str or type(cadence) is not str: |
| 90 | + raise _error("identity_binding_invalid") |
| 91 | + try: |
| 92 | + period_key = canonical_period_identity(cadence, as_of).key |
| 93 | + except (PeriodContractError, TypeError, ValueError, OverflowError): |
| 94 | + raise _error("period_mismatch") from None |
| 95 | + if type(entry["period_key"]) is not str or entry["period_key"] != period_key: |
| 96 | + raise _error("period_mismatch") |
| 97 | + |
| 98 | + schema = entry["report_schema_version"] |
| 99 | + contract = entry["contract_version"] |
| 100 | + if type(schema) is not str: |
| 101 | + raise _error("invalid_schema_version") |
| 102 | + if type(contract) is not str: |
| 103 | + raise _error("invalid_contract_version") |
| 104 | + try: |
| 105 | + expected_contract = contract_version_for_schema(schema) |
| 106 | + except (TimeContractError, TypeError, ValueError): |
| 107 | + raise _error("invalid_schema_version") from None |
| 108 | + if contract != expected_contract: |
| 109 | + raise _error("contract_version_mismatch") |
| 110 | + |
| 111 | + if entry["semantic_fingerprint_version"] != FINGERPRINT_VERSION: |
| 112 | + raise _error("invalid_fingerprint_version") |
| 113 | + semantic_digest = entry["semantic_digest"] |
| 114 | + if type(semantic_digest) is not str or re.fullmatch(r"[0-9a-f]{64}", semantic_digest) is None: |
| 115 | + raise _error("invalid_semantic_digest") |
| 116 | + if entry["artifact_integrity_version"] != ARTIFACT_INTEGRITY_VERSION: |
| 117 | + raise _error("invalid_artifact_integrity_version") |
| 118 | + artifact_digest = entry["artifact_integrity_digest"] |
| 119 | + if type(artifact_digest) is not str or re.fullmatch(r"[0-9a-f]{64}", artifact_digest) is None: |
| 120 | + raise _error("invalid_artifact_integrity_digest") |
| 121 | + |
| 122 | + identity_class = entry["identity_class"] |
| 123 | + if type(identity_class) is not str or identity_class not in {V3_CANONICAL, V3_VARIANT}: |
| 124 | + raise _error("legacy_identity_rejected" if identity_class == "LEGACY_V2" else "invalid_identity_class") |
| 125 | + canonical = entry["canonical_identity"] |
| 126 | + primary = entry["display_primary"] |
| 127 | + order = entry["display_order"] |
| 128 | + if type(canonical) is not bool or type(primary) is not bool or type(order) is not int or order < 0: |
| 129 | + raise _error("identity_binding_invalid") |
| 130 | + if (identity_class == V3_CANONICAL) != canonical: |
| 131 | + raise _error("identity_metadata_mismatch") |
| 132 | + |
| 133 | + names = [ |
| 134 | + (_name_digest(entry["json"], _JSON, as_of=as_of, cadence=cadence), entry["json"]), |
| 135 | + (_name_digest(entry["html"], _HTML, as_of=as_of, cadence=cadence), entry["html"]), |
| 136 | + ] |
| 137 | + if "md" in entry and type(entry["md"]) is not str: |
| 138 | + raise _error("identity_name_invalid") |
| 139 | + if "manifest" in entry and type(entry["manifest"]) is not str: |
| 140 | + raise _error("identity_name_invalid") |
| 141 | + markdown = entry["md"] if "md" in entry else None |
| 142 | + manifest = entry["manifest"] if "manifest" in entry else None |
| 143 | + if markdown is not None: |
| 144 | + names.append((_name_digest(markdown, _MD, as_of=as_of, cadence=cadence), markdown)) |
| 145 | + if manifest is not None: |
| 146 | + names.append((_name_digest(manifest, _MANIFEST, as_of=as_of, cadence=cadence), manifest)) |
| 147 | + expected_suffix = None if canonical else artifact_digest |
| 148 | + if any(name_digest != expected_suffix for name_digest, _name in names): |
| 149 | + raise _error("identity_digest_mismatch") |
| 150 | + return V3IdentityBinding( |
| 151 | + period_key, as_of, cadence, schema, contract, FINGERPRINT_VERSION, semantic_digest, |
| 152 | + ARTIFACT_INTEGRITY_VERSION, artifact_digest, entry["json"], entry["html"], markdown, manifest, |
| 153 | + identity_class, canonical, primary, order, PENDING_ARTIFACT_VALIDATION, |
| 154 | + ) |
| 155 | + |
| 156 | + |
| 157 | +__all__ = ["VNextBindingError", "binding_payload", "validate_vnext_binding"] |
0 commit comments