22
33from dataclasses import dataclass , field
44from importlib import import_module
5+ from pathlib import Path
56from types import ModuleType
6- from typing import Any , Iterable , Mapping
7+ from typing import Any , Callable , Iterable , Mapping
78
89from .strategy_contracts import (
910 CallableStrategyEntrypoint ,
1011 StrategyContractValidationError ,
1112 StrategyEntrypoint ,
1213 StrategyManifest ,
14+ StrategyRuntimeAdapter ,
1315 validate_strategy_manifest ,
1416)
1517
@@ -39,6 +41,18 @@ class StrategyDefinition:
3941 required_inputs : frozenset [str ] = frozenset ()
4042 compatible_capabilities : frozenset [str ] = frozenset ()
4143 default_config : Mapping [str , Any ] = field (default_factory = dict )
44+ target_mode : str | None = None
45+ bundled_config_relpath : str | None = None
46+
47+
48+ @dataclass (frozen = True )
49+ class StrategyArtifactPaths :
50+ artifact_root : Path | None = None
51+ artifact_dir : Path | None = None
52+ bundled_config_path : Path | None = None
53+ feature_snapshot_path : Path | None = None
54+ feature_snapshot_manifest_path : Path | None = None
55+ reconciliation_output_dir : Path | None = None
4256
4357
4458@dataclass (frozen = True )
@@ -72,6 +86,18 @@ class PlatformStrategyPolicy:
7286 require_explicit_profile : bool = False
7387
7488
89+ @dataclass (frozen = True )
90+ class PlatformCapabilityMatrix :
91+ platform_id : str
92+ supported_domains : frozenset [str ]
93+ supported_target_modes : frozenset [str ]
94+ supported_inputs : frozenset [str ] = frozenset ()
95+ supported_capabilities : frozenset [str ] = frozenset ()
96+
97+
98+ _SUPPORTED_TARGET_MODES = frozenset ({"weight" , "value" })
99+
100+
75101def normalize_profile_name (profile : str | None ) -> str :
76102 return str (profile or "" ).strip ().lower ()
77103
@@ -141,6 +167,19 @@ def build_strategy_catalog(
141167 )
142168
143169
170+ def _resolve_target_mode (definition : StrategyDefinition ) -> str | None :
171+ target_mode = str (definition .target_mode or "" ).strip ().lower ()
172+ if not target_mode :
173+ return None
174+ if target_mode not in _SUPPORTED_TARGET_MODES :
175+ supported_text = ", " .join (sorted (_SUPPORTED_TARGET_MODES ))
176+ raise ValueError (
177+ f"Unsupported target_mode={ definition .target_mode !r} for strategy profile "
178+ f"{ definition .profile !r} ; supported values: { supported_text } "
179+ )
180+ return target_mode
181+
182+
144183def _unsupported_profile_error (
145184 * ,
146185 profile : str | None ,
@@ -234,11 +273,59 @@ def build_strategy_index_rows(strategy_catalog: StrategyCatalog) -> list[dict[st
234273 or "entrypoint" in {component .name for component in definition .components },
235274 "required_inputs" : definition .required_inputs ,
236275 "compatible_capabilities" : definition .compatible_capabilities ,
276+ "target_mode" : _resolve_target_mode (definition ),
277+ "bundled_config_relpath" : definition .bundled_config_relpath ,
237278 }
238279 )
239280 return rows
240281
241282
283+ def get_catalog_target_mode (
284+ strategy_catalog : StrategyCatalog ,
285+ profile : str ,
286+ ) -> str | None :
287+ definition = get_catalog_strategy_definition (strategy_catalog , profile )
288+ return _resolve_target_mode (definition )
289+
290+
291+ def derive_strategy_artifact_paths (
292+ strategy_catalog : StrategyCatalog ,
293+ profile : str ,
294+ * ,
295+ artifact_root : str | Path | None = None ,
296+ repo_root : str | Path | None = None ,
297+ ) -> StrategyArtifactPaths :
298+ definition = get_catalog_strategy_definition (strategy_catalog , profile )
299+ artifact_root_path = Path (artifact_root ).expanduser () if artifact_root else None
300+ artifact_dir = artifact_root_path / definition .profile if artifact_root_path else None
301+ repo_root_path = Path (repo_root ).expanduser () if repo_root else None
302+
303+ bundled_config_path = None
304+ bundled_config_relpath = str (definition .bundled_config_relpath or "" ).strip ()
305+ if bundled_config_relpath and repo_root_path is not None :
306+ bundled_config_path = repo_root_path / bundled_config_relpath
307+
308+ feature_snapshot_path = None
309+ feature_snapshot_manifest_path = None
310+ if artifact_dir is not None and "feature_snapshot" in frozenset (definition .required_inputs ):
311+ feature_snapshot_filename = f"{ definition .profile } _feature_snapshot_latest.csv"
312+ feature_snapshot_path = artifact_dir / feature_snapshot_filename
313+ feature_snapshot_manifest_path = artifact_dir / f"{ feature_snapshot_filename } .manifest.json"
314+
315+ reconciliation_output_dir = None
316+ if artifact_dir is not None :
317+ reconciliation_output_dir = artifact_dir / "reconciliation"
318+
319+ return StrategyArtifactPaths (
320+ artifact_root = artifact_root_path ,
321+ artifact_dir = artifact_dir ,
322+ bundled_config_path = bundled_config_path ,
323+ feature_snapshot_path = feature_snapshot_path ,
324+ feature_snapshot_manifest_path = feature_snapshot_manifest_path ,
325+ reconciliation_output_dir = reconciliation_output_dir ,
326+ )
327+
328+
242329def get_enabled_profiles_for_platform (
243330 platform_id : str ,
244331 * ,
@@ -249,6 +336,78 @@ def get_enabled_profiles_for_platform(
249336 return policy .enabled_profiles
250337
251338
339+ def _matches_platform_capability_matrix (
340+ definition : StrategyDefinition ,
341+ * ,
342+ runtime_adapter : StrategyRuntimeAdapter ,
343+ capability_matrix : PlatformCapabilityMatrix ,
344+ ) -> bool :
345+ if definition .domain not in capability_matrix .supported_domains :
346+ return False
347+
348+ target_mode = _resolve_target_mode (definition )
349+ if target_mode is not None and target_mode not in capability_matrix .supported_target_modes :
350+ return False
351+
352+ adapter_inputs = frozenset (runtime_adapter .available_inputs )
353+ if definition .required_inputs - adapter_inputs :
354+ return False
355+ if adapter_inputs - capability_matrix .supported_inputs :
356+ return False
357+
358+ adapter_capabilities = frozenset (runtime_adapter .available_capabilities )
359+ if definition .compatible_capabilities - adapter_capabilities :
360+ return False
361+ if adapter_capabilities - capability_matrix .supported_capabilities :
362+ return False
363+
364+ return True
365+
366+
367+ def derive_eligible_profiles_for_platform (
368+ strategy_catalog : StrategyCatalog ,
369+ * ,
370+ capability_matrix : PlatformCapabilityMatrix ,
371+ runtime_adapter_loader : Callable [[str ], StrategyRuntimeAdapter ],
372+ ) -> frozenset [str ]:
373+ eligible_profiles : list [str ] = []
374+ for profile in sorted (strategy_catalog .definitions ):
375+ definition = strategy_catalog .definitions [profile ]
376+ try :
377+ runtime_adapter = runtime_adapter_loader (profile )
378+ except ValueError :
379+ continue
380+ if _matches_platform_capability_matrix (
381+ definition ,
382+ runtime_adapter = runtime_adapter ,
383+ capability_matrix = capability_matrix ,
384+ ):
385+ eligible_profiles .append (definition .profile )
386+ return frozenset (eligible_profiles )
387+
388+
389+ def derive_enabled_profiles_for_platform (
390+ strategy_catalog : StrategyCatalog ,
391+ * ,
392+ capability_matrix : PlatformCapabilityMatrix ,
393+ runtime_adapter_loader : Callable [[str ], StrategyRuntimeAdapter ],
394+ rollout_allowlist : Iterable [str ] | None = None ,
395+ ) -> frozenset [str ]:
396+ eligible_profiles = derive_eligible_profiles_for_platform (
397+ strategy_catalog ,
398+ capability_matrix = capability_matrix ,
399+ runtime_adapter_loader = runtime_adapter_loader ,
400+ )
401+ if rollout_allowlist is None :
402+ return eligible_profiles
403+
404+ normalized_allowlist = {
405+ resolve_catalog_profile (profile , strategy_catalog = strategy_catalog )
406+ for profile in rollout_allowlist
407+ }
408+ return frozenset (sorted (eligible_profiles & normalized_allowlist ))
409+
410+
252411def build_platform_profile_matrix (
253412 strategy_catalog : StrategyCatalog ,
254413 * ,
@@ -273,6 +432,36 @@ def build_platform_profile_matrix(
273432 return rows
274433
275434
435+ def build_platform_profile_status_matrix (
436+ strategy_catalog : StrategyCatalog ,
437+ * ,
438+ policy : PlatformStrategyPolicy ,
439+ eligible_profiles : Iterable [str ],
440+ ) -> list [dict [str , object ]]:
441+ eligible = {
442+ resolve_catalog_profile (profile , strategy_catalog = strategy_catalog )
443+ for profile in eligible_profiles
444+ }
445+ visible_profiles = sorted (eligible | set (policy .enabled_profiles ))
446+ rows : list [dict [str , object ]] = []
447+ for profile in visible_profiles :
448+ definition = get_catalog_strategy_definition (strategy_catalog , profile )
449+ metadata = strategy_catalog .metadata .get (definition .profile )
450+ rows .append (
451+ {
452+ "platform" : policy .platform_id ,
453+ "canonical_profile" : definition .profile ,
454+ "display_name" : metadata .display_name if metadata else definition .profile ,
455+ "eligible" : definition .profile in eligible ,
456+ "enabled" : definition .profile in policy .enabled_profiles ,
457+ "is_default" : definition .profile == policy .default_profile ,
458+ "is_rollback" : definition .profile == policy .rollback_profile ,
459+ "domain" : definition .domain ,
460+ }
461+ )
462+ return rows
463+
464+
276465def resolve_platform_strategy_definition (
277466 raw_value : str | None ,
278467 * ,
0 commit comments