@@ -161,6 +161,16 @@ class LegacyFormatConversionContext:
161161LegacyFormatConverter = Callable [[LegacyFormatConversionContext ], Format | Mapping [str , Any ] | None ]
162162
163163
164+ @dataclass (frozen = True )
165+ class _SnapshotLegacyFormatConverter :
166+ """Distinct trusted channel for pre-resolved catalog snapshots."""
167+
168+ convert : LegacyFormatConverter
169+
170+ def __call__ (self , context : LegacyFormatConversionContext ) -> Format | Mapping [str , Any ] | None :
171+ return self .convert (context )
172+
173+
164174@dataclass (frozen = True )
165175class CanonicalFormatLegacyResolutionContext :
166176 declaration : Format
@@ -396,6 +406,22 @@ def project_legacy_format_id(
396406 resolution_failure = "no_match" ,
397407 )
398408 )
409+ snapshot_converter = (
410+ legacy_format_converter
411+ if isinstance (legacy_format_converter , _SnapshotLegacyFormatConverter )
412+ else None
413+ )
414+ if snapshot_converter is not None :
415+ # Pre-resolved publisher/community snapshots are validated catalog
416+ # sources, not an arbitrary compatibility callback. Their declared
417+ # precedence is publisher -> approved mirror -> configured/agent, all
418+ # ahead of the SDK's bundled AAO fallback.
419+ converted = _converted_format (
420+ snapshot_converter ,
421+ LegacyFormatConversionContext (ref , product_id , field ),
422+ )
423+ if converted is not None :
424+ return converted
399425 exact_key = (owner , ref .id )
400426 exact = index .by_owner_and_id .get (exact_key )
401427 if exact_key in index .by_owner_and_id and exact is None :
@@ -411,7 +437,7 @@ def project_legacy_format_id(
411437
412438 if exact is None :
413439 unique = index .by_unique_id .get (ref .id ) if index ._allow_bare_id_fallback else None
414- if legacy_format_converter is not None :
440+ if legacy_format_converter is not None and snapshot_converter is None :
415441 converted = _converted_format (
416442 legacy_format_converter ,
417443 LegacyFormatConversionContext (ref , product_id , field ),
@@ -444,7 +470,7 @@ def project_legacy_format_id(
444470
445471 canonical = entry .get ("canonical" ) if isinstance (entry , Mapping ) else None
446472 if not isinstance (canonical , Mapping ) or not isinstance (canonical .get ("kind" ), str ):
447- if legacy_format_converter is not None :
473+ if legacy_format_converter is not None and snapshot_converter is None :
448474 converted = _converted_format (
449475 legacy_format_converter ,
450476 LegacyFormatConversionContext (ref , product_id , field ),
@@ -654,7 +680,18 @@ def visit(item: Any, field_path: str) -> Any:
654680 if not isinstance (item , Mapping ):
655681 return item
656682
657- result = {key : visit (child , f"{ field_path } .{ key } " ) for key , child in item .items ()}
683+ # Context and extension bags are application-owned opaque values. They
684+ # are neither creative-dialect evidence nor protocol selectors, so the
685+ # projector must preserve them verbatim rather than interpreting a
686+ # coincidental ``format_id``/``format_ids`` key inside the bag.
687+ result = {
688+ key : (
689+ deepcopy (child )
690+ if key in {"context" , "ext" }
691+ else visit (child , f"{ field_path } .{ key } " )
692+ )
693+ for key , child in item .items ()
694+ }
658695 fields = result .get ("fields" )
659696 if isinstance (fields , list ):
660697 result ["fields" ] = list (
@@ -801,6 +838,8 @@ def register_declaration(
801838 option_id : str ,
802839 declaration : Format ,
803840 ) -> None :
841+ if scope == "publisher" :
842+ owner = _normalized_publisher_domain (owner ) or None
804843 key = (scope , owner , option_id )
805844 if key not in declaration_routes :
806845 declaration_routes [key ] = declaration
@@ -875,7 +914,11 @@ def declaration_for_ref(
875914 raise CanonicalFormatLegacyResolutionError (
876915 f"{ field_path } publisher reference has no publisher_domain"
877916 )
878- key = ("publisher" , publisher_domain , option_id )
917+ key = (
918+ "publisher" ,
919+ _normalized_publisher_domain (publisher_domain ),
920+ option_id ,
921+ )
879922 else :
880923 raise CanonicalFormatLegacyResolutionError (
881924 f"{ field_path } has unsupported format option scope { scope !r} "
@@ -1051,6 +1094,102 @@ def _snapshot_priority(snapshot: Mapping[str, Any]) -> int:
10511094 return 2
10521095
10531096
1097+ def _normalized_publisher_domain (value : object ) -> str :
1098+ """Match RC3 publisher option identity normalization."""
1099+
1100+ return str (value or "" ).strip ().lower ().rstrip ("." )
1101+
1102+
1103+ _CANONICAL_ONLY_FORMAT_KINDS = {
1104+ "agent_placement" ,
1105+ "image_carousel" ,
1106+ "responsive_creative" ,
1107+ "sponsored_placement" ,
1108+ }
1109+
1110+
1111+ def _legacy_route_key (ref : LegacyFormatId ) -> tuple [str , str , int | None , int | None , float | None ]:
1112+ owner = _catalog_owner (ref .agent_url )
1113+ if owner is None or not _is_safe_public_https_owner (ref .agent_url ):
1114+ raise ValueError ("bidirectional projection adapters require safe public HTTPS owners" )
1115+ return (owner , ref .id , ref .width , ref .height , ref .duration_ms )
1116+
1117+
1118+ def _assert_bidirectional_projection_catalogs (
1119+ snapshots : Sequence [Mapping [str , Any ]],
1120+ ) -> None :
1121+ """Reject asymmetric or ambiguous durable routes exactly as RC3 does."""
1122+
1123+ route_by_canonical : dict [
1124+ tuple [str , str ], tuple [str , str , int | None , int | None , float | None ]
1125+ ] = {}
1126+ canonical_by_legacy : dict [
1127+ tuple [str , str , int | None , int | None , float | None ], tuple [str , str ]
1128+ ] = {}
1129+ for snapshot in snapshots :
1130+ declarations_at_tier : set [
1131+ tuple [
1132+ tuple [str , str ],
1133+ tuple [str , str , int | None , int | None , float | None ],
1134+ ]
1135+ ] = set ()
1136+ for raw in _snapshot_formats (snapshot ):
1137+ refs = raw .get ("v1_format_ref" )
1138+ if raw .get ("canonical_formats_only" ) is True or not isinstance (refs , list ) or not refs :
1139+ continue
1140+ kind = raw .get ("format_kind" )
1141+ if kind in _CANONICAL_ONLY_FORMAT_KINDS :
1142+ raise ValueError (
1143+ "bidirectional projection adapters cannot downgrade a canonical-only "
1144+ "format kind"
1145+ )
1146+ option_id = raw .get ("format_option_id" )
1147+ if not isinstance (option_id , str ) or not option_id :
1148+ raise ValueError (
1149+ "bidirectional projection adapters require a stable format_option_id"
1150+ )
1151+ publisher = _normalized_publisher_domain (
1152+ raw .get ("publisher_domain" , snapshot .get ("publisher_domain" ))
1153+ )
1154+ if not publisher :
1155+ raise ValueError (
1156+ "bidirectional projection adapters require publisher-scoped format options"
1157+ )
1158+ try :
1159+ parsed_refs = [LegacyFormatId .model_validate (ref ) for ref in refs ]
1160+ except ValidationError as exc :
1161+ raise ValueError (
1162+ "bidirectional projection adapters contain an invalid legacy route"
1163+ ) from exc
1164+ legacy_keys = {_legacy_route_key (ref ) for ref in parsed_refs }
1165+ if len (legacy_keys ) != 1 :
1166+ raise ValueError (
1167+ "bidirectional projection adapters require exactly one legacy route per "
1168+ "canonical format option"
1169+ )
1170+ canonical_key = (publisher , option_id )
1171+ legacy_key = next (iter (legacy_keys ))
1172+ declaration_key = (canonical_key , legacy_key )
1173+ if declaration_key in declarations_at_tier :
1174+ raise ValueError (
1175+ "bidirectional projection adapters contain duplicate declarations at one "
1176+ "precedence tier"
1177+ )
1178+ declarations_at_tier .add (declaration_key )
1179+ existing_legacy = route_by_canonical .get (canonical_key )
1180+ if existing_legacy is not None and existing_legacy != legacy_key :
1181+ raise ValueError (
1182+ "bidirectional projection adapters contain conflicting reverse routes"
1183+ )
1184+ existing_canonical = canonical_by_legacy .get (legacy_key )
1185+ if existing_canonical is not None and existing_canonical != canonical_key :
1186+ raise ValueError (
1187+ "bidirectional projection adapters contain conflicting forward routes"
1188+ )
1189+ route_by_canonical [canonical_key ] = legacy_key
1190+ canonical_by_legacy [legacy_key ] = canonical_key
1191+
1192+
10541193def canonical_format_legacy_resolver_from_catalog_snapshots (
10551194 snapshots : Iterable [Mapping [str , Any ]],
10561195) -> CanonicalFormatLegacyResolver :
@@ -1071,6 +1210,7 @@ def canonical_format_legacy_resolver_from_catalog_snapshots(
10711210 if (
10721211 not isinstance (option_id , str )
10731212 or not isinstance (kind , str )
1213+ or kind in _CANONICAL_ONLY_FORMAT_KINDS
10741214 or not isinstance (refs , list )
10751215 ):
10761216 continue
@@ -1080,8 +1220,10 @@ def canonical_format_legacy_resolver_from_catalog_snapshots(
10801220 continue
10811221 if not parsed :
10821222 continue
1083- publisher = raw .get ("publisher_domain" , snapshot .get ("publisher_domain" ))
1084- key = (publisher if isinstance (publisher , str ) else None , option_id , kind )
1223+ publisher = _normalized_publisher_domain (
1224+ raw .get ("publisher_domain" , snapshot .get ("publisher_domain" ))
1225+ )
1226+ key = (publisher or None , option_id , kind )
10851227 candidate = (deepcopy (raw .get ("params" ) or {}), parsed )
10861228 existing = routes .get (key )
10871229 if existing is None or priority < existing [0 ]:
@@ -1095,11 +1237,15 @@ def resolve(context: CanonicalFormatLegacyResolutionContext) -> Sequence[LegacyF
10951237 return None
10961238 ranked = routes .get (
10971239 (
1098- declaration .publisher_domain ,
1240+ _normalized_publisher_domain ( declaration .publisher_domain ) or None ,
10991241 declaration .format_option_id ,
11001242 declaration .format_kind .value ,
11011243 )
11021244 )
1245+ if ranked is not None and ranked [1 ] is None :
1246+ raise CanonicalFormatLegacyResolutionError (
1247+ "projection catalog contains ambiguous canonical format option aliases"
1248+ )
11031249 route = ranked [1 ] if ranked else None
11041250 if not route :
11051251 return None
@@ -1131,6 +1277,8 @@ def legacy_format_converter_from_catalog_snapshots(
11311277 for raw in _snapshot_formats (snapshot ):
11321278 if raw .get ("canonical_formats_only" ) is True :
11331279 continue
1280+ if raw .get ("format_kind" ) in _CANONICAL_ONLY_FORMAT_KINDS :
1281+ continue
11341282 refs = raw .get ("v1_format_ref" )
11351283 if not isinstance (refs , list ):
11361284 continue
@@ -1159,10 +1307,17 @@ def convert(context: LegacyFormatConversionContext) -> Mapping[str, Any] | None:
11591307 if owner is None :
11601308 return None
11611309 ranked = routes .get ((owner , ref .id , ref .width , ref .height , ref .duration_ms ))
1310+ if ranked is not None and ranked [1 ] is None :
1311+ raise LegacyCreativeProjectionError (
1312+ "projection catalog contains ambiguous legacy aliases"
1313+ )
11621314 route = ranked [1 ] if ranked else None
11631315 return deepcopy (route ) if route else None
11641316
1165- return convert
1317+ # Catalog snapshots are pre-resolved, validated sources with protocol
1318+ # precedence above the bundled AAO fallback. The private wrapper makes the
1319+ # trusted channel structurally distinct from an ordinary adopter callback.
1320+ return _SnapshotLegacyFormatConverter (convert )
11661321
11671322
11681323def projection_adapters_from_catalog_snapshots (
@@ -1171,6 +1326,7 @@ def projection_adapters_from_catalog_snapshots(
11711326 """Build symmetric forward and durable reverse routes from one corpus."""
11721327
11731328 materialized = list (snapshots )
1329+ _assert_bidirectional_projection_catalogs (materialized )
11741330 return ProjectionCatalogAdapters (
11751331 legacy_format_converter = legacy_format_converter_from_catalog_snapshots (materialized ),
11761332 canonical_format_legacy_resolver = canonical_format_legacy_resolver_from_catalog_snapshots (
0 commit comments