@@ -69,6 +69,7 @@ def build_parser() -> argparse.ArgumentParser:
6969 add_simple_command (
7070 subparsers , "check-exercise-output-contracts" , handle_check_exercise_output_contracts
7171 )
72+ add_simple_command (subparsers , "check-exercise-parity" , handle_check_exercise_parity )
7273 add_simple_command (
7374 subparsers , "check-cross-language-parity" , handle_check_cross_language_parity
7475 )
@@ -134,6 +135,11 @@ def handle_check_exercise_output_contracts(ctx: RepoContext, _: argparse.Namespa
134135 return 0
135136
136137
138+ def handle_check_exercise_parity (ctx : RepoContext , _ : argparse .Namespace ) -> int :
139+ check_exercise_parity (ctx )
140+ return 0
141+
142+
137143def handle_check_cross_language_parity (ctx : RepoContext , _ : argparse .Namespace ) -> int :
138144 check_cross_language_parity (ctx )
139145 return 0
@@ -2005,6 +2011,162 @@ def check_exercise_output_contracts(ctx: RepoContext) -> None:
20052011 print (f"Exercise output contracts passed for { executed_jobs } jobs." )
20062012
20072013
2014+ def exercise_contract_key (
2015+ target : str , * , language : str , extension : str
2016+ ) -> tuple [str , str , str ] | None :
2017+ normalized = target .replace ("\\ " , "/" )
2018+ pattern = (
2019+ rf"^languages/{ re .escape (language )} /([^/]+)/([^/]+)/exercises/(0[12])\."
2020+ rf"{ re .escape (extension )} $"
2021+ )
2022+ match = re .match (pattern , normalized )
2023+ if not match :
2024+ return None
2025+ return match .group (1 ), match .group (2 ), match .group (3 )
2026+
2027+
2028+ def check_exercise_parity (ctx : RepoContext ) -> None :
2029+ failures : list [str ] = []
2030+ languages = ["cpp" , "csharp" , "go" , "python" , "typescript" ]
2031+ extension_map = {
2032+ language : ctx .manifest .languages [language ]["extension" ] for language in languages
2033+ }
2034+
2035+ for level , modules in ctx .manifest .module_order .items ():
2036+ for module in modules :
2037+ for language in languages :
2038+ if level not in ctx .manifest .languages [language ].get ("module_levels" , []):
2039+ continue
2040+ extension = extension_map [language ]
2041+ for exercise_id in ("01" , "02" ):
2042+ exercise_path = (
2043+ ctx .root
2044+ / "languages"
2045+ / language
2046+ / level
2047+ / module
2048+ / "exercises"
2049+ / f"{ exercise_id } .{ extension } "
2050+ )
2051+ if not exercise_path .is_file ():
2052+ failures .append (f"{ exercise_path } : missing exercise file for parity check" )
2053+
2054+ contracts = load_exercise_output_contracts (ctx )
2055+ expected_levels = set (ctx .manifest .module_order .keys ())
2056+ expected_modules = {
2057+ (level , module )
2058+ for level , modules in ctx .manifest .module_order .items ()
2059+ for module in modules
2060+ }
2061+ contract_keys_by_language : dict [str , set [tuple [str , str , str ]]] = {}
2062+
2063+ unknown_languages = sorted (language for language in contracts if language not in languages )
2064+ for language in unknown_languages :
2065+ failures .append (
2066+ f"scripts/exercise_output_contracts.json: unexpected language key '{ language } '"
2067+ )
2068+
2069+ for language in languages :
2070+ jobs = contracts .get (language , [])
2071+ if not jobs :
2072+ failures .append (
2073+ f"scripts/exercise_output_contracts.json: no contracts configured for { language } "
2074+ )
2075+ contract_keys_by_language [language ] = set ()
2076+ continue
2077+
2078+ extension = extension_map [language ]
2079+ keys : set [tuple [str , str , str ]] = set ()
2080+ for job in jobs :
2081+ target = job .get ("program" )
2082+ if not target :
2083+ failures .append (
2084+ "scripts/exercise_output_contracts.json: "
2085+ f"{ language } contract missing 'program' field"
2086+ )
2087+ continue
2088+
2089+ target_path = repo_path (ctx , target )
2090+ if not target_path .is_file ():
2091+ failures .append (
2092+ "scripts/exercise_output_contracts.json: "
2093+ f"{ language } contract target does not exist -> { target } "
2094+ )
2095+
2096+ key = exercise_contract_key (target , language = language , extension = extension )
2097+ if key is None :
2098+ failures .append (
2099+ "scripts/exercise_output_contracts.json: "
2100+ f"{ language } contract target must be an exercises/01|02 file -> { target } "
2101+ )
2102+ continue
2103+
2104+ level , module , exercise_id = key
2105+ if level not in expected_levels :
2106+ failures .append (
2107+ "scripts/exercise_output_contracts.json: "
2108+ f"{ language } contract uses unknown level '{ level } ' -> { target } "
2109+ )
2110+ if (level , module ) not in expected_modules :
2111+ failures .append (
2112+ "scripts/exercise_output_contracts.json: "
2113+ f"{ language } contract uses unknown module '{ module } ' in level '{ level } ' -> "
2114+ f"{ target } "
2115+ )
2116+ if exercise_id not in {"01" , "02" }:
2117+ failures .append (
2118+ "scripts/exercise_output_contracts.json: "
2119+ f"{ language } contract exercise id must be 01 or 02 -> { target } "
2120+ )
2121+ if key in keys :
2122+ failures .append (
2123+ "scripts/exercise_output_contracts.json: "
2124+ f"duplicate { language } contract key { level } /{ module } /exercises/{ exercise_id } "
2125+ )
2126+ keys .add (key )
2127+
2128+ if not job .get ("required_stdout_contains" ) and not job .get ("required_stdout_patterns" ):
2129+ failures .append (
2130+ "scripts/exercise_output_contracts.json: "
2131+ f"{ language } contract has no stdout expectations -> { target } "
2132+ )
2133+
2134+ contract_keys_by_language [language ] = keys
2135+
2136+ baseline_language = next (
2137+ (language for language in languages if contract_keys_by_language [language ]),
2138+ None ,
2139+ )
2140+ if baseline_language is None :
2141+ failures .append (
2142+ "scripts/exercise_output_contracts.json: no exercise contracts parsed for any language"
2143+ )
2144+ else :
2145+ baseline_keys = contract_keys_by_language [baseline_language ]
2146+ for language in languages :
2147+ current_keys = contract_keys_by_language [language ]
2148+ missing = sorted (baseline_keys - current_keys )
2149+ extra = sorted (current_keys - baseline_keys )
2150+ for level , module , exercise_id in missing :
2151+ failures .append (
2152+ "scripts/exercise_output_contracts.json: "
2153+ f"{ language } missing contract for { level } /{ module } /exercises/{ exercise_id } "
2154+ )
2155+ for level , module , exercise_id in extra :
2156+ failures .append (
2157+ "scripts/exercise_output_contracts.json: "
2158+ f"{ language } has extra contract for { level } /{ module } /exercises/{ exercise_id } "
2159+ )
2160+
2161+ if failures :
2162+ print ("Exercise parity validation failed:" )
2163+ for failure in failures :
2164+ print (f" - { failure } " )
2165+ raise AutomationError ("Exercise parity validation failed." )
2166+
2167+ print ("Exercise parity validation passed." )
2168+
2169+
20082170def module_focus_comment (path : Path ) -> tuple [str | None , str | None ]:
20092171 lines = path .read_text (encoding = "utf-8" ).splitlines ()
20102172 header_comment_lines : list [str ] = []
@@ -2155,34 +2317,37 @@ def check_cross_language_parity(ctx: RepoContext) -> None:
21552317def verify_repo (ctx : RepoContext ) -> None :
21562318 python_cmd = find_python_command ()
21572319
2158- print ("[1/10 ] Checking markdown links..." )
2320+ print ("[1/11 ] Checking markdown links..." )
21592321 run_command ([python_cmd , str (ctx .scripts_dir / "check-links.py" )], action = "Markdown link check" )
21602322
2161- print ("[2/10 ] Checking README structure..." )
2323+ print ("[2/11 ] Checking README structure..." )
21622324 check_readme_structure (ctx )
21632325
2164- print ("[3/10 ] Checking module completeness..." )
2326+ print ("[3/11 ] Checking module completeness..." )
21652327 check_module_completeness (ctx )
21662328
2167- print ("[4/10 ] Checking checkpoint completeness..." )
2329+ print ("[4/11 ] Checking checkpoint completeness..." )
21682330 check_checkpoint_completeness (ctx )
21692331
2170- print ("[5/10 ] Checking documentation sync..." )
2332+ print ("[5/11 ] Checking documentation sync..." )
21712333 check_doc_sync (ctx )
21722334
2173- print ("[6/10 ] Checking example comments..." )
2335+ print ("[6/11 ] Checking example comments..." )
21742336 check_example_comments (ctx )
21752337
2176- print ("[7/10 ] Checking cross-language parity..." )
2338+ print ("[7/11 ] Checking cross-language parity..." )
21772339 check_cross_language_parity (ctx )
21782340
2179- print ("[8/10] Checking example output contracts..." )
2341+ print ("[8/11] Checking exercise parity..." )
2342+ check_exercise_parity (ctx )
2343+
2344+ print ("[9/11] Checking example output contracts..." )
21802345 check_example_output_contracts (ctx )
21812346
2182- print ("[9/10 ] Checking exercise output contracts..." )
2347+ print ("[10/11 ] Checking exercise output contracts..." )
21832348 check_exercise_output_contracts (ctx )
21842349
2185- print ("[10/10 ] Compiling compiled-language tracks..." )
2350+ print ("[11/11 ] Compiling compiled-language tracks..." )
21862351 build_all (ctx )
21872352
21882353 print ("Repository verification completed successfully." )
0 commit comments