1- #!/usr/bin/env python3
1+ #!/usr/bin/env python3
22"""
33Validate keyword registry sync with multilingual reference implementation.
44
55Compares the tree-sitter-multilingual keywords.yaml with the reference
66keywords.json from the multilingual repository, checking for:
77- Missing or extra constructs
8- - Language mismatches
8+ - Language mismatches with normalization
99- Keyword value differences
1010- Coverage gaps
1111
@@ -34,7 +34,9 @@ def load_target_keywords(path: str) -> Dict[str, Any]:
3434
3535
3636def normalize_keyword (kw : str ) -> str :
37- """Normalize keyword for comparison (lowercase, strip spaces)."""
37+ """Normalize keyword for comparison (lowercase, strip spaces, handle variants)."""
38+ if not isinstance (kw , str ):
39+ kw = str (kw )
3840 return kw .lower ().strip ().replace (' ' , '_' ).replace ('-' , '_' )
3941
4042
@@ -137,13 +139,7 @@ def compare_keywords(
137139 ref_value = ref_langs [lang ]
138140 target_value = target_langs .get (lang )
139141
140- # Normalize for comparison
141- ref_normalized = normalize_keyword (ref_value ) if isinstance (ref_value , str ) else \
142- [normalize_keyword (v ) for v in ref_value ]
143- target_normalized = normalize_keyword (target_value ) if isinstance (target_value , str ) and target_value else \
144- [normalize_keyword (v ) for v in target_value ] if isinstance (target_value , list ) else None
145-
146- # Check if values match (allowing for normalization)
142+ # Check if values match
147143 if target_value is None :
148144 construct_details ['missing_languages' ].append (lang )
149145 construct_details ['complete' ] = False
@@ -153,35 +149,35 @@ def compare_keywords(
153149 })
154150 report ['status' ] = 'warning'
155151 else :
152+ # Normalize both for comparison
153+ if isinstance (ref_value , list ):
154+ ref_normalized = set (normalize_keyword (v ) for v in ref_value )
155+ else :
156+ ref_normalized = set ([normalize_keyword (ref_value )])
157+
158+ if isinstance (target_value , list ):
159+ target_normalized = set (normalize_keyword (v ) for v in target_value )
160+ else :
161+ target_normalized = set ([normalize_keyword (target_value )])
162+
156163 # Compare normalized values
157- if isinstance (ref_normalized , list ) and isinstance (target_normalized , list ):
158- if set (ref_normalized ) != set (target_normalized ):
159- construct_details ['mismatches' ].append ({
160- 'language' : lang ,
161- 'expected' : ref_value ,
162- 'actual' : target_value ,
163- })
164- report ['language_mismatches' ].append ({
165- 'construct' : construct ,
166- 'language' : lang ,
167- 'expected' : ref_value if isinstance (ref_value , str ) else ref_value [0 ],
168- 'actual' : target_value if isinstance (target_value , str ) else target_value [0 ],
169- })
170- report ['status' ] = 'warning'
171- elif isinstance (ref_normalized , str ) and isinstance (target_normalized , str ):
172- if ref_normalized != target_normalized :
173- construct_details ['mismatches' ].append ({
174- 'language' : lang ,
175- 'expected' : ref_value ,
176- 'actual' : target_value ,
177- })
178- report ['language_mismatches' ].append ({
179- 'construct' : construct ,
180- 'language' : lang ,
181- 'expected' : ref_value ,
182- 'actual' : target_value ,
183- })
184- report ['status' ] = 'warning'
164+ if ref_normalized != target_normalized :
165+ # Get display values
166+ ref_display = ref_value if isinstance (ref_value , str ) else ref_value [0 ] if ref_value else ''
167+ target_display = target_value if isinstance (target_value , str ) else target_value [0 ] if target_value else ''
168+
169+ construct_details ['mismatches' ].append ({
170+ 'language' : lang ,
171+ 'expected' : ref_value ,
172+ 'actual' : target_value ,
173+ })
174+ report ['language_mismatches' ].append ({
175+ 'construct' : construct ,
176+ 'language' : lang ,
177+ 'expected' : ref_display ,
178+ 'actual' : target_display ,
179+ })
180+ report ['status' ] = 'warning'
185181
186182 report ['details' ][construct ] = construct_details
187183
@@ -213,73 +209,91 @@ def main():
213209 args = parser .parse_args ()
214210
215211 try :
216- # Load data
217- print ('Loading reference keywords...' )
212+ # Force UTF-8 output
213+ if sys .stdout .encoding != 'utf-8' :
214+ import io
215+ sys .stdout = io .TextIOWrapper (sys .stdout .buffer , encoding = 'utf-8' , line_buffering = True )
216+ if sys .stderr .encoding != 'utf-8' :
217+ import io
218+ sys .stderr = io .TextIOWrapper (sys .stderr .buffer , encoding = 'utf-8' , line_buffering = True )
219+
220+ sys .stdout .write ('Loading reference keywords...\n ' )
221+ sys .stdout .flush ()
218222 ref_data = load_reference_keywords (args .reference )
219223 ref_languages = ref_data .get ('languages' , [])
220224
221- print ('Loading target keywords...' )
225+ sys .stdout .write ('Loading target keywords...\n ' )
226+ sys .stdout .flush ()
222227 target_data = load_target_keywords (args .target )
223228
224229 # Extract and normalize
225- print ('Extracting reference keywords...' )
230+ sys .stdout .write ('Extracting reference keywords...\n ' )
231+ sys .stdout .flush ()
226232 ref_keywords = extract_reference_keywords (ref_data )
227233
228234 # Compare
229- print ('Comparing keywords...' )
235+ sys .stdout .write ('Comparing keywords...\n ' )
236+ sys .stdout .flush ()
230237 report , all_good = compare_keywords (ref_keywords , target_data , ref_languages )
231238
232239 # Write report
233- print (f'Writing report to { args .report } ...' )
240+ sys .stdout .write (f'Writing report to { args .report } ...\n ' )
241+ sys .stdout .flush ()
234242 with open (args .report , 'w' , encoding = 'utf-8' ) as f :
235243 json .dump (report , f , ensure_ascii = False , indent = 2 )
236244
237245 # Print summary
238- print ('\n ' + '=' * 60 )
239- print ('SYNC VALIDATION REPORT' )
240- print ('=' * 60 )
241- print (f'Status: { report ["status" ].upper ()} ' )
242- print (f'Coverage: { report ["coverage" ]} %' )
243- print (f'Missing constructs: { len (report ["missing_constructs" ])} ' )
244- print (f'Language mismatches: { len (report ["language_mismatches" ])} ' )
245- print (f'Missing language support: { len (report ["missing_languages" ])} ' )
246+ sys . stdout . write ('\n ' + '=' * 60 + ' \n ' )
247+ sys . stdout . write ('SYNC VALIDATION REPORT\n ' )
248+ sys . stdout . write ('=' * 60 + ' \n ' )
249+ sys . stdout . write (f'Status: { report ["status" ].upper ()} \n ' )
250+ sys . stdout . write (f'Coverage: { report ["coverage" ]} %\n ' )
251+ sys . stdout . write (f'Missing constructs: { len (report ["missing_constructs" ])} \n ' )
252+ sys . stdout . write (f'Language mismatches: { len (report ["language_mismatches" ])} \n ' )
253+ sys . stdout . write (f'Missing language support: { len (report ["missing_languages" ])} \n ' )
246254
247255 if report ['missing_constructs' ]:
248- print (f'\n Missing constructs:' )
256+ sys . stdout . write (f'\n Missing constructs:\n ' )
249257 for c in report ['missing_constructs' ][:5 ]:
250- print (f' - { c } ' )
258+ sys . stdout . write (f' - { c } \n ' )
251259 if len (report ['missing_constructs' ]) > 5 :
252- print (f' ... and { len (report ["missing_constructs" ]) - 5 } more' )
260+ sys . stdout . write (f' ... and { len (report ["missing_constructs" ]) - 5 } more\n ' )
253261
254262 if report ['language_mismatches' ]:
255- print (f'\n Language mismatches (first 5):' )
263+ sys . stdout . write (f'\n Language mismatches (first 5):\n ' )
256264 for m in report ['language_mismatches' ][:5 ]:
257- print (f' - { m ["construct" ]} ({ m ["language" ]} ): { m ["expected" ]} != { m ["actual" ]} ' )
265+ construct = m .get ('construct' , '?' )
266+ lang = m .get ('language' , '?' )
267+ sys .stdout .write (f' - { construct } ({ lang } )\n ' )
258268 if len (report ['language_mismatches' ]) > 5 :
259- print (f' ... and { len (report ["language_mismatches" ]) - 5 } more' )
269+ sys . stdout . write (f' ... and { len (report ["language_mismatches" ]) - 5 } more\n ' )
260270
261- print ('=' * 60 )
271+ sys .stdout .write ('=' * 60 + '\n ' )
272+ sys .stdout .flush ()
262273
263274 # Exit code
264275 sys .exit (0 if all_good else 1 )
265276
266277 except Exception as e :
267- error_report = {
268- 'status' : 'error' ,
269- 'coverage' : 0 ,
270- 'missing_constructs' : [],
271- 'extra_constructs' : [],
272- 'language_mismatches' : [],
273- 'missing_languages' : [],
274- 'details' : {},
275- 'error' : str (e ),
276- }
278+ import traceback
279+ sys .stderr .write (f'ERROR: { e } \n ' )
280+ sys .stderr .flush ()
281+ traceback .print_exc (file = sys .stderr )
282+
283+ # Write error report
277284 try :
278285 with open (args .report , 'w' , encoding = 'utf-8' ) as f :
279- json .dump (error_report , f , ensure_ascii = False , indent = 2 )
280- except Exception :
286+ json .dump ({
287+ 'status' : 'error' ,
288+ 'error' : str (e ),
289+ 'missing_constructs' : [],
290+ 'language_mismatches' : [],
291+ 'missing_languages' : [],
292+ 'coverage' : 0 ,
293+ }, f , ensure_ascii = False , indent = 2 )
294+ except :
281295 pass
282- print ( f'ERROR: { e } ' , file = sys . stderr )
296+
283297 sys .exit (1 )
284298
285299
0 commit comments