11#!/usr/bin/env python3
2- """Enforce Core-wide media identity/presentation/security policy.
2+ """Enforce Core-wide media identity/presentation/branding/ security policy.
33
44Provider-specific media repair is intentionally excluded from this normalizer.
5- Identity, stream facts, presentation, platform compatibility and security are
6- repository-wide Core concerns. Provider rows may still describe capability or
7- official-domain discovery, but they must not own private copies of those Core
8- layers.
5+ Identity, stream facts, presentation, branding, platform compatibility and security
6+ are repository-wide Core concerns. Provider rows may still describe capability or
7+ official-domain discovery, but they must not own private copies of those Core layers.
98"""
109from __future__ import annotations
1110
1615
1716ROOT = Path (__file__ ).resolve ().parents [1 ]
1817OVERRIDES = ROOT / "provider-overrides.json"
18+ MANIFEST = ROOT / "manifest.json"
19+ PROVIDER_BRANDING = ROOT / "assets/providers/emojis.json"
1920DESKTOP_COMPAT = ROOT / "scripts/publish_desktop_runtime_compat.py"
2021GLOBAL_SECURITY_HOOK = "scripts/provider_patches/global_provider_security_hardening_v1.py"
22+ GLOBAL_BRANDING_HOOK = "scripts/provider_patches/global_provider_branding_v1.py"
2123ALLOWED_SHARED_PURSTREAM_SCRIPTS = {
2224 "scripts/provider_patches/native_sync_fetch_target_order_v1.py" ,
2325 "scripts/provider_patches/runtime_capability_media_safety_v4.py" ,
@@ -46,21 +48,23 @@ def load() -> dict[str, Any]:
4648 return value
4749
4850
49- def _normalize_global_security (value : dict [str , Any ], changed : list [str ]) -> None :
51+ def _normalize_global_tail (value : dict [str , Any ], changed : list [str ]) -> None :
5052 playback = value .get ("playback_integrity_policy" )
5153 if not isinstance (playback , dict ):
5254 raise ValueError ("playback_integrity_policy must be an object" )
5355 hooks = playback .get ("global_discovery_hooks" ) or []
5456 if not isinstance (hooks , list ):
5557 raise ValueError ("playback_integrity_policy.global_discovery_hooks must be an array" )
56- normalized = [str (path ) for path in hooks if str (path ).strip () and str (path ) != GLOBAL_SECURITY_HOOK ]
57- # Keep security last in the legacy-tail stage. Pre/post HLS hooks are removed
58- # from that tail by apply_provider_overrides.py, so this executes after media
59- # recovery and directly before the controlled Core presentation wrapper.
60- normalized .append (GLOBAL_SECURITY_HOOK )
58+ controlled = {GLOBAL_SECURITY_HOOK , GLOBAL_BRANDING_HOOK }
59+ normalized = [str (path ) for path in hooks if str (path ).strip () and str (path ) not in controlled ]
60+ # Pre/post HLS hooks are removed from this legacy-tail stage by
61+ # apply_provider_overrides.py. Security therefore executes after all media
62+ # recovery, branding then establishes the stable provider text identity, and
63+ # the separate controlled presentation wrapper executes last.
64+ normalized .extend ([GLOBAL_SECURITY_HOOK , GLOBAL_BRANDING_HOOK ])
6165 if normalized != hooks :
6266 playback ["global_discovery_hooks" ] = normalized
63- changed .append ("playback_integrity_policy.global_discovery_hooks:global_security " )
67+ changed .append ("playback_integrity_policy.global_discovery_hooks:security_branding_tail " )
6468
6569
6670def normalize (value : dict [str , Any ]) -> tuple [dict [str , Any ], list [str ]]:
@@ -105,7 +109,7 @@ def normalize(value: dict[str, Any]) -> tuple[dict[str, Any], list[str]]:
105109 row ["notes" ] = notes
106110 changed .append ("provider_patches.purstream.notes" )
107111
108- _normalize_global_security (value , changed )
112+ _normalize_global_tail (value , changed )
109113 return value , changed
110114
111115
@@ -119,6 +123,34 @@ def normalize_source_files(*, apply: bool) -> list[str]:
119123 return changed
120124
121125
126+ def _assert_branding_inventory () -> None :
127+ manifest = json .loads (MANIFEST .read_text (encoding = "utf-8" ))
128+ branding = json .loads (PROVIDER_BRANDING .read_text (encoding = "utf-8" ))
129+ if branding .get ("policy" ) != "committed-provider-default-emoji" :
130+ raise ValueError ("provider branding policy must be committed-provider-default-emoji" )
131+ rows = branding .get ("providers" )
132+ if not isinstance (rows , dict ):
133+ raise ValueError ("provider branding providers must be an object" )
134+ manifest_ids = {
135+ str (row .get ("id" ) or "" ).strip ().casefold ()
136+ for row in (manifest .get ("scrapers" ) or [])
137+ if isinstance (row , dict ) and str (row .get ("id" ) or "" ).strip ()
138+ }
139+ branding_ids = {str (value ).strip ().casefold () for value in rows }
140+ if branding_ids != manifest_ids :
141+ missing = sorted (manifest_ids - branding_ids )
142+ unknown = sorted (branding_ids - manifest_ids )
143+ raise ValueError (
144+ "provider branding coverage mismatch: "
145+ f"missing={ ',' .join (missing ) or 'none' } unknown={ ',' .join (unknown ) or 'none' } "
146+ )
147+ for provider_id , row in rows .items ():
148+ if not isinstance (row , dict ):
149+ raise ValueError (f"provider branding row must be an object: { provider_id } " )
150+ if not str (row .get ("name" ) or "" ).strip () or not str (row .get ("emoji" ) or "" ).strip ():
151+ raise ValueError (f"provider branding row requires clean name + emoji: { provider_id } " )
152+
153+
122154def assert_policy (value : dict [str , Any ]) -> None :
123155 row = value ["provider_patches" ]["purstream" ]
124156 scripts = {str (path ) for path in (row .get ("patch_scripts" ) or [])}
@@ -130,22 +162,29 @@ def assert_policy(value: dict[str, Any]) -> None:
130162 runtime = ROOT / "scripts/provider_patches/runtime_capability_media_safety_v4.py"
131163 presentation = ROOT / "scripts/provider_patches/global_stream_presentation_v1.py"
132164 security = ROOT / GLOBAL_SECURITY_HOOK
133- if not runtime .is_file () or not presentation .is_file () or not security .is_file ():
134- raise ValueError ("shared Core media/security implementation is missing" )
165+ branding = ROOT / GLOBAL_BRANDING_HOOK
166+ if not runtime .is_file () or not presentation .is_file () or not security .is_file () or not branding .is_file ():
167+ raise ValueError ("shared Core media/branding/security implementation is missing" )
135168 runtime_text = runtime .read_text (encoding = "utf-8" )
136169 presentation_text = presentation .read_text (encoding = "utf-8" )
137170 security_text = security .read_text (encoding = "utf-8" )
171+ branding_text = branding .read_text (encoding = "utf-8" )
138172 if "field-safety-v5-native-identity-collisions-all-rows" not in runtime_text :
139173 raise ValueError ("shared runtime identity safety revision is not current" )
140174 if "NUVIO_GLOBAL_STREAM_PRESENTATION_V1" not in presentation_text :
141175 raise ValueError ("shared stream presentation wrapper is missing" )
142176 if "NUVIO_GLOBAL_PROVIDER_SECURITY_HOOK_V1" not in security_text :
143177 raise ValueError ("shared provider security Core hook is missing" )
178+ if "NUVIO_GLOBAL_PROVIDER_BRANDING_V1" not in branding_text :
179+ raise ValueError ("shared provider branding Core hook is missing" )
180+ _assert_branding_inventory ()
144181
145182 playback = value .get ("playback_integrity_policy" ) or {}
146183 hooks = [str (path ) for path in (playback .get ("global_discovery_hooks" ) or [])]
147- if hooks .count (GLOBAL_SECURITY_HOOK ) != 1 or hooks [- 1 :] != [GLOBAL_SECURITY_HOOK ]:
148- raise ValueError ("global provider security hook must be present exactly once as final Core tail hook" )
184+ if hooks .count (GLOBAL_SECURITY_HOOK ) != 1 or hooks .count (GLOBAL_BRANDING_HOOK ) != 1 :
185+ raise ValueError ("global provider security/branding hooks must each be present exactly once" )
186+ if hooks [- 2 :] != [GLOBAL_SECURITY_HOOK , GLOBAL_BRANDING_HOOK ]:
187+ raise ValueError ("global Core tail must end security -> provider branding before presentation" )
149188
150189 desktop_text = DESKTOP_COMPAT .read_text (encoding = "utf-8" )
151190 if _PURSTREAM_DESKTOP_TARGET in desktop_text :
@@ -176,7 +215,8 @@ def main() -> int:
176215 print (
177216 "FIELD_CORE_MEDIA_POLICY "
178217 f"provider_specific_media_repairs=0 changed={ len (changed ) + len (source_changes )} "
179- "identity=global_runtime presentation=global_core compatibility=shared security=global_core"
218+ "identity=global_runtime presentation=global_core branding=global_core "
219+ "compatibility=shared security=global_core"
180220 )
181221 return 0
182222
0 commit comments