@@ -178,6 +178,34 @@ class CoverageIssue:
178178 detail : str
179179
180180
181+ def _percentage (covered : int , total : int ) -> float :
182+ if total <= 0 :
183+ return 100.0
184+ return round ((covered / total ) * 100.0 , 2 )
185+
186+
187+ def _metric (covered : int , total : int ) -> dict [str , int | float ]:
188+ covered = max (0 , min (int (covered ), int (total )))
189+ total = int (total )
190+ return {"covered" : covered , "total" : total , "percent" : _percentage (covered , total )}
191+
192+
193+ def _present_count (required : Sequence [str ], actual : Sequence [str ] | set [str ]) -> int :
194+ return len (set (required ) & set (actual ))
195+
196+
197+ def _coverage_summary (metrics : dict [str , dict [str , int | float ]]) -> dict [str , Any ]:
198+ covered = sum (int (metric ["covered" ]) for metric in metrics .values ())
199+ total = sum (int (metric ["total" ]) for metric in metrics .values ())
200+ return {
201+ "covered_items" : covered ,
202+ "required_items" : total ,
203+ "coverage_percent" : _percentage (covered , total ),
204+ "target_percent" : 100.0 ,
205+ "metrics" : metrics ,
206+ }
207+
208+
181209def _load_module (name : str , path : Path ) -> Any :
182210 spec = importlib .util .spec_from_file_location (name , path )
183211 if spec is None or spec .loader is None :
@@ -298,6 +326,43 @@ def apps_page_coverage_issues(public_views: Sequence[str]) -> list[CoverageIssue
298326 return issues
299327
300328
329+ def _apps_page_metric (public_views : Sequence [str ]) -> dict [str , int | float ]:
330+ covered = 0
331+ for view in set (public_views ):
332+ if view in BROWSER_ASSERTED_APPS_PAGES :
333+ covered += 1
334+ continue
335+ disposition = APPS_PAGE_RENDER_ONLY_DISPOSITIONS .get (view )
336+ if disposition is None :
337+ continue
338+ reason , focused_test = disposition
339+ focused_path = Path (focused_test )
340+ if not focused_path .is_absolute ():
341+ focused_path = REPO_ROOT / focused_path
342+ if reason .strip () and focused_path .is_file ():
343+ covered += 1
344+ return _metric (covered , len (set (public_views )))
345+
346+
347+ def _hf_robot_metric (hf_robot_scenarios : dict [str , dict [str , list [str ]]]) -> dict [str , int | float ]:
348+ covered = 0
349+ total = 0
350+ for scenario_name , requirements in REQUIRED_HF_ROBOT_SCENARIOS .items ():
351+ actual = hf_robot_scenarios .get (scenario_name , {})
352+ total += 1
353+ if actual :
354+ covered += 1
355+ for key in ("pages" , "apps_pages" , "flags" ):
356+ required_values = requirements .get (key , ())
357+ total += len (required_values )
358+ covered += _present_count (required_values , actual .get (key , []))
359+ required_actions = tuple (str (value ).strip ().lower () for value in requirements .get ("actions" , ()))
360+ actual_actions = {str (value ).strip ().lower () for value in actual .get ("actions" , [])}
361+ total += len (required_actions )
362+ covered += _present_count (required_actions , actual_actions )
363+ return _metric (covered , total )
364+
365+
301366def evaluate_contract () -> dict [str , Any ]:
302367 widget_robot = _load_module ("agilab_widget_robot_contract" , WIDGET_ROBOT_PATH )
303368 matrix = _load_module ("agilab_widget_robot_matrix_contract" , MATRIX_PATH )
@@ -795,9 +860,147 @@ def evaluate_contract() -> dict[str, Any]:
795860 )
796861 )
797862
863+ editor_route_covered = 0
864+ editor_route_total = 0
865+ for route in REQUIRED_EDITOR_ROUTES :
866+ route_contract = editor_route_contract .get (route , {})
867+ route_scenarios = route_contract .get ("scenarios" , [])
868+ required_text = route_contract .get ("required_text" , [])
869+ forbidden_text = route_contract .get ("forbidden_text" , [])
870+ editor_route_total += 1
871+ if route_scenarios :
872+ editor_route_covered += 1
873+ required_text_values = REQUIRED_EDITOR_ROUTE_TEXT .get (route , ())
874+ forbidden_text_values = REQUIRED_EDITOR_ROUTE_FORBIDDEN_TEXT .get (route , ())
875+ editor_route_total += len (required_text_values ) + len (forbidden_text_values )
876+ editor_route_covered += _present_count (required_text_values , required_text )
877+ editor_route_covered += _present_count (forbidden_text_values , forbidden_text )
878+
879+ pytorch_required_text = pytorch_analysis .get ("required_text" , [])
880+ pytorch_required_links = pytorch_analysis .get ("required_links" , [])
881+ pytorch_required_actions = pytorch_analysis .get ("required_actions" , [])
882+ pytorch_forbidden_sidebar_text = pytorch_analysis .get ("forbidden_sidebar_text" , [])
883+ pytorch_flags = pytorch_analysis .get ("flags" , [])
884+ pytorch_metric_total = (
885+ 3
886+ + len (REQUIRED_PYTORCH_ANALYSIS_TEXT )
887+ + len (REQUIRED_PYTORCH_ANALYSIS_FORBIDDEN_SIDEBAR_TEXT )
888+ + len (REQUIRED_PYTORCH_ANALYSIS_LINKS )
889+ + len (REQUIRED_PYTORCH_ANALYSIS_ACTIONS )
890+ + 1
891+ )
892+ pytorch_metric_covered = (
893+ int (bool (pytorch_analysis ))
894+ + int ("ANALYSIS" in pytorch_analysis .get ("pages" , []))
895+ + int (REQUIRED_PYTORCH_ANALYSIS_APP in pytorch_analysis .get ("apps" , []))
896+ + _present_count (REQUIRED_PYTORCH_ANALYSIS_TEXT , pytorch_required_text )
897+ + _present_count (REQUIRED_PYTORCH_ANALYSIS_FORBIDDEN_SIDEBAR_TEXT , pytorch_forbidden_sidebar_text )
898+ + _present_count (REQUIRED_PYTORCH_ANALYSIS_LINKS , pytorch_required_links )
899+ + _present_count (REQUIRED_PYTORCH_ANALYSIS_ACTIONS , pytorch_required_actions )
900+ + int ("browser_error_check" in pytorch_flags )
901+ )
902+
903+ orchestrate_pool_metric_total = 2 + len (REQUIRED_ORCHESTRATE_POOL_TEXT ) + 1
904+ orchestrate_pool_metric_covered = (
905+ int (bool (orchestrate_pool ))
906+ + int ("ORCHESTRATE" in orchestrate_pool .get ("pages" , []))
907+ + _present_count (REQUIRED_ORCHESTRATE_POOL_TEXT , orchestrate_pool .get ("required_text" , []))
908+ + int ("browser_error_check" in orchestrate_pool .get ("flags" , []))
909+ )
910+
911+ execution_pandas_pool_metric_total = 3 + len (REQUIRED_EXECUTION_PANDAS_POOL_TEXT ) + 1
912+ execution_pandas_pool_metric_covered = (
913+ int (bool (execution_pandas_pool ))
914+ + int (REQUIRED_EXECUTION_PANDAS_POOL_APP in execution_pandas_pool .get ("apps" , []))
915+ + int ("ORCHESTRATE" in execution_pandas_pool .get ("pages" , []))
916+ + _present_count (REQUIRED_EXECUTION_PANDAS_POOL_TEXT , execution_pandas_pool .get ("required_text" , []))
917+ + int ("browser_error_check" in execution_pandas_pool .get ("flags" , []))
918+ )
919+
920+ configured_apps_pages_total = 1 if configured_apps_pages else 0
921+ metrics = {
922+ "built_in_app_inventory" : _metric (int (bool (built_in_apps )), 1 ),
923+ "built_in_app_matrix" : _metric (int (bool (built_in_apps and default_scenarios )), 1 ),
924+ "core_pages" : _metric (
925+ sum (1 for page in REQUIRED_CORE_PAGES if page_to_scenarios .get (page )),
926+ len (REQUIRED_CORE_PAGES ),
927+ ),
928+ "editor_routes" : _metric (editor_route_covered , editor_route_total ),
929+ "configured_apps_pages" : _metric (int (bool (configured_scenarios )), configured_apps_pages_total ),
930+ "high_risk_actions" : _metric (
931+ sum (1 for action in REQUIRED_HIGH_RISK_ACTIONS if action_to_scenarios .get (action )),
932+ len (REQUIRED_HIGH_RISK_ACTIONS ),
933+ ),
934+ "hf_first_proof_apps" : _metric (
935+ _present_count (REQUIRED_HF_FIRST_PROOF_APPS , hf_first_proof_apps )
936+ + len (set (FORBIDDEN_HF_FIRST_PROOF_APPS ) - set (hf_first_proof_apps )),
937+ len (REQUIRED_HF_FIRST_PROOF_APPS ) + len (FORBIDDEN_HF_FIRST_PROOF_APPS ),
938+ ),
939+ "hf_first_proof_pages" : _metric (
940+ _present_count (REQUIRED_HF_FIRST_PROOF_PAGES , hf_first_proof_pages ),
941+ len (REQUIRED_HF_FIRST_PROOF_PAGES ),
942+ ),
943+ "apps_pages" : _apps_page_metric (public_apps_pages ),
944+ "hf_robot_scenarios" : _hf_robot_metric (hf_robot_scenarios ),
945+ "hf_visual_smoke_profile" : _metric (
946+ _present_count (REQUIRED_HF_VISUAL_SMOKE_ROBOT_SCENARIOS , hf_visual_smoke_profile_scenarios )
947+ + _present_count (hf_first_proof_apps , hf_visual_smoke_profile_apps ),
948+ len (REQUIRED_HF_VISUAL_SMOKE_ROBOT_SCENARIOS ) + len (hf_first_proof_apps ),
949+ ),
950+ "hf_install_profile" : _metric (
951+ int ("hf-first-proof-install" in hf_install_profile_scenarios )
952+ + _present_count (hf_first_proof_apps , hf_install_profile_apps ),
953+ 1 + len (hf_first_proof_apps ),
954+ ),
955+ "ui_robot_matrix_profile" : _metric (
956+ _present_count (REQUIRED_EDITOR_ROUTE_PROFILE_SCENARIOS , ui_robot_matrix_profile_scenarios )
957+ + int (REQUIRED_PYTORCH_ANALYSIS_SCENARIO in ui_robot_matrix_profile_scenarios )
958+ + int (REQUIRED_RELEASE_EVIDENCE_SCENARIO in ui_robot_matrix_profile_scenarios )
959+ + int (REQUIRED_EXECUTION_PANDAS_POOL_SCENARIO in ui_robot_matrix_profile_scenarios ),
960+ len (REQUIRED_EDITOR_ROUTE_PROFILE_SCENARIOS ) + 3 ,
961+ ),
962+ "pytorch_analysis_robot" : _metric (pytorch_metric_covered , pytorch_metric_total ),
963+ "orchestrate_pool_robot" : _metric (orchestrate_pool_metric_covered , orchestrate_pool_metric_total ),
964+ "execution_pandas_pool_robot" : _metric (
965+ execution_pandas_pool_metric_covered ,
966+ execution_pandas_pool_metric_total ,
967+ ),
968+ "public_demo_docs" : _metric (
969+ len (REQUIRED_DEMO_DOC_SNIPPETS ) - len (missing_demo_doc_snippets ),
970+ len (REQUIRED_DEMO_DOC_SNIPPETS ),
971+ ),
972+ "public_demo_ui_apps" : _metric (
973+ len (REQUIRED_DEMO_UI_APPS ) - len (missing_demo_ui_apps ),
974+ len (REQUIRED_DEMO_UI_APPS ),
975+ ),
976+ "public_demo_apps_pages" : _metric (
977+ len (REQUIRED_DEMO_UI_PAGES ) - len (missing_demo_pages ),
978+ len (REQUIRED_DEMO_UI_PAGES ),
979+ ),
980+ "public_demo_proofs" : _metric (
981+ len (REQUIRED_DEMO_PROOF_SCENARIOS ) - len (missing_proof_scenarios ),
982+ len (REQUIRED_DEMO_PROOF_SCENARIOS ),
983+ ),
984+ }
985+ summary = _coverage_summary (metrics )
986+ if summary ["coverage_percent" ] < summary ["target_percent" ]:
987+ issues .append (
988+ CoverageIssue (
989+ "coverage_percent" ,
990+ (
991+ "UI robot deterministic coverage is "
992+ f"{ summary ['coverage_percent' ]:.2f} % "
993+ f"({ summary ['covered_items' ]} /{ summary ['required_items' ]} ); expected 100.00%."
994+ ),
995+ )
996+ )
997+ summary ["issue_count" ] = len (issues )
998+ summary ["status" ] = "pass" if not issues else "fail"
999+
7981000 return {
7991001 "schema" : SCHEMA ,
8001002 "success" : not issues ,
1003+ "summary" : summary ,
8011004 "issues" : [asdict (issue ) for issue in issues ],
8021005 "coverage" : {
8031006 "built_in_apps" : built_in_apps ,
@@ -838,9 +1041,15 @@ def evaluate_contract() -> dict[str, Any]:
8381041
8391042
8401043def render_human (payload : dict [str , Any ]) -> str :
1044+ summary = payload .get ("summary" , {})
8411045 lines = [
8421046 "AGILAB UI robot coverage contract" ,
8431047 f"verdict: { 'PASS' if payload .get ('success' ) else 'FAIL' } " ,
1048+ (
1049+ "coverage: "
1050+ f"{ float (summary .get ('coverage_percent' , 0.0 )):.2f} % "
1051+ f"({ int (summary .get ('covered_items' , 0 ))} /{ int (summary .get ('required_items' , 0 ))} )"
1052+ ),
8441053 ]
8451054 for issue in payload .get ("issues" , []):
8461055 lines .append (f"- { issue .get ('kind' )} : { issue .get ('detail' )} " )
0 commit comments