Skip to content

Commit 5d2cbe0

Browse files
Create detailed report
1 parent eed3363 commit 5d2cbe0

5 files changed

Lines changed: 2367 additions & 84 deletions

File tree

.github/QUICK_REFERENCE.md

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# Quick Reference: Keyword Sync Workflow
2+
3+
## Before You Commit
4+
5+
`bash
6+
# 1. Install deps (one-time)
7+
pip install pyyaml
8+
9+
# 2. Validate locally
10+
python .github/scripts/sync-keywords.py \
11+
--reference ../multilingual/multilingualprogramming/resources/usm/keywords.json \
12+
--target data/keywords.yaml \
13+
--report sync-report.json
14+
15+
# 3. Check results
16+
python .github/scripts/check-sync-results.py sync-report.json
17+
# Exit code 0 = pass, 1 = fail
18+
19+
# 4. View details
20+
cat sync-report.json | jq .
21+
`
22+
23+
## Updating Keywords Interactively
24+
25+
`bash
26+
# Dry-run: see what would change
27+
python .github/scripts/update-keywords-from-reference.py \
28+
--reference ../multilingual/multilingualprogramming/resources/usm/keywords.json \
29+
--target data/keywords.yaml \
30+
--dry-run
31+
32+
# Actually update (creates backup)
33+
python .github/scripts/update-keywords-from-reference.py \
34+
--reference ../multilingual/multilingualprogramming/resources/usm/keywords.json \
35+
--target data/keywords.yaml \
36+
--interactive \
37+
--backup
38+
`
39+
40+
## GitHub Workflow Feedback
41+
42+
When you push/PR with keyword changes:
43+
44+
✓ Workflow runs automatically
45+
✓ Creates sync-report.json
46+
✓ Posts comment on PR with results
47+
✓ Artifacts available for download
48+
49+
**Example PR Comment:**
50+
`
51+
## Keyword Registry Sync Report
52+
53+
✅ All keywords are in sync with the reference implementation.
54+
55+
**Coverage**: 100%
56+
`
57+
58+
**Or if issues:**
59+
`
60+
## Keyword Registry Sync Report
61+
62+
⚠️ Keyword registry sync issues detected.
63+
64+
### Language Mismatches
65+
- if (ja): Expected 'もし', got '如果'
66+
- class (fr): Expected 'classe', got 'class'
67+
68+
**Coverage**: 95%
69+
`
70+
71+
## Common Issues
72+
73+
**"Coverage below 95%"**
74+
→ Add missing constructs to keywords.yaml or lower threshold
75+
76+
**"Language mismatches detected"**
77+
→ Update values in keywords.yaml to match reference
78+
79+
**"Missing language support"**
80+
→ Add language columns (sv, da, fi, etc.) to keywords.yaml
81+
82+
**Unicode errors**
83+
→ Ensure keywords.yaml is UTF-8 encoded
84+
85+
## File Locations
86+
87+
- Workflow: .github/workflows/sync-keywords.yml
88+
- Sync script: .github/scripts/sync-keywords.py
89+
- Check script: .github/scripts/check-sync-results.py
90+
- Update script: .github/scripts/update-keywords-from-reference.py
91+
- Keywords: data/keywords.yaml
92+
- Reference: ../multilingual/multilingualprogramming/resources/usm/keywords.json
93+
- Report: sync-report.json (generated)
94+
- Docs: .github/SYNC_WORKFLOW.md
95+
96+
## Configuration
97+
98+
In .github/workflows/sync-keywords.yml:
99+
100+
`yaml
101+
# Minimum coverage threshold
102+
check-sync-results.py sync-report.json --min-coverage 95
103+
104+
# Strict mode (fail on warnings)
105+
check-sync-results.py sync-report.json --strict
106+
`
107+
108+
## Resources
109+
110+
- 🔗 Multilingual Repo: https://github.com/multilingualprogramming/multilingual
111+
- 📖 Full Docs: .github/SYNC_WORKFLOW.md
112+
- 🐛 Issues: .github/scripts/ (check source code)

.github/scripts/check-sync-results.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,30 +43,30 @@ def main():
4343
failed = False
4444

4545
if status == 'error':
46-
print('❌ FAILED: Validation error occurred')
46+
sys.stdout.write('[FAIL] Validation error occurred\n')
4747
failed = True
4848

4949
if status == 'warning' and args.strict:
50-
print('⚠️ FAILED: Warnings detected in strict mode')
50+
sys.stdout.write('[FAIL] Warnings detected in strict mode\n')
5151
failed = True
5252

5353
if coverage < args.min_coverage:
54-
print(f'❌ FAILED: Coverage {coverage}% below minimum {args.min_coverage}%')
54+
sys.stdout.write(f'[FAIL] Coverage {coverage}% below minimum {args.min_coverage}%\n')
5555
failed = True
5656

5757
if missing_constructs > 0:
58-
print(f'⚠️ WARNING: {missing_constructs} missing constructs in target')
58+
sys.stdout.write(f'[WARN] {missing_constructs} missing constructs in target\n')
5959

6060
if mismatches > 0:
61-
print(f'⚠️ WARNING: {mismatches} keyword mismatches detected')
61+
sys.stdout.write(f'[WARN] {mismatches} keyword mismatches detected\n')
6262

6363
if not failed:
6464
if status == 'success':
65-
print('✅ PASSED: Keywords are in sync')
65+
sys.stdout.write('[PASS] Keywords are in sync\n')
6666
elif status == 'warning':
67-
print('✅ PASSED: Warnings detected but acceptable')
67+
sys.stdout.write('[PASS] Warnings detected but acceptable\n')
6868
else:
69-
print('✅ PASSED: Validation complete')
69+
sys.stdout.write('[PASS] Validation complete\n')
7070

7171
print()
7272
sys.exit(1 if failed else 0)

.github/scripts/sync-keywords.py

Lines changed: 86 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
#!/usr/bin/env python3
1+
#!/usr/bin/env python3
22
"""
33
Validate keyword registry sync with multilingual reference implementation.
44
55
Compares the tree-sitter-multilingual keywords.yaml with the reference
66
keywords.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

3636
def 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'\nMissing constructs:')
256+
sys.stdout.write(f'\nMissing 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'\nLanguage mismatches (first 5):')
263+
sys.stdout.write(f'\nLanguage 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

Comments
 (0)