youssefreda9 commited on
Commit
404f92d
·
1 Parent(s): 6319518

Phase 11: Inline telemetry in API response

Browse files

- Add _tel_events collector list in analyze_text()
- Append events to _tel_events alongside [FILTER-TEL] logger lines
- Include 'telemetry' key in API response JSON
- Update telemetry_capture.py to read from response instead of HF logs
- NO correction logic changes

src/app.py CHANGED
@@ -1248,6 +1248,9 @@ def analyze_text():
1248
  current_text = text # Local alias (updated alongside ctx.current_text)
1249
  suggestions = [] # Legacy — will be replaced by ctx.patches at response time
1250
  mappers = [] # Legacy — will be replaced by ctx._offset_mappers
 
 
 
1251
  total_start = time.time()
1252
  timing_ms = {'spelling_ms': 0, 'grammar_ms': 0, 'punctuation_ms': 0, 'total_ms': 0}
1253
 
@@ -1578,6 +1581,7 @@ def analyze_text():
1578
  # ── Phase 11: Telemetry — raw grammar output ──
1579
  import json as _tel_json
1580
  logger.info(f'[FILTER-TEL] {_tel_json.dumps({"event":"grammar_raw_output","input":_grammar_input_text[:200],"output":corrected_grammar[:200]})}')
 
1581
 
1582
  # FIX-03: Restore structured content in grammar output
1583
  if _structured_placeholders:
@@ -1590,10 +1594,12 @@ def analyze_text():
1590
  _grammar_accepted_diffs = [] # FIX-04: track accepted diffs
1591
  _grammar_total_diffs = len(diffs)
1592
  logger.info(f'[FILTER-TEL] {_tel_json.dumps({"event":"grammar_diffs_extracted","count":_grammar_total_diffs})}')
 
1593
  for d in diffs:
1594
  orig_text = d.get('original', '')
1595
  corr_text = d.get('correction', '')
1596
  logger.info(f'[FILTER-TEL] {_tel_json.dumps({"event":"grammar_diff","original":orig_text[:80],"correction":corr_text[:80],"start":d["start"],"end":d["end"]})}')
 
1597
  # StageLocker: skip diffs that overlap with locked ranges
1598
  if ctx.stage_locker.is_locked(d['start'], d['end']):
1599
  logger.info(
@@ -1601,6 +1607,7 @@ def analyze_text():
1601
  f"'{d.get('original','')}' — locked by previous stage"
1602
  )
1603
  logger.info(f'[FILTER-TEL] {_tel_json.dumps({"event":"filter_reject","filter":"StageLocker","original":orig_text[:80],"correction":corr_text[:80]})}')
 
1604
  continue
1605
 
1606
  # Reject grammar hallucinations (e.g. جالس→جاكسون)
@@ -1615,6 +1622,7 @@ def analyze_text():
1615
  f"(jaccard={jaccard:.2f})"
1616
  )
1617
  logger.info(f'[FILTER-TEL] {_tel_json.dumps({"event":"filter_reject","filter":"Jaccard_03","original":orig_text[:80],"correction":corr_text[:80],"jaccard":round(jaccard,3)})}')
 
1618
  continue
1619
 
1620
  # ── FIX-13: Named entity protection ──
@@ -1630,6 +1638,7 @@ def analyze_text():
1630
  f"'{orig_text}'→'{corr_text}'"
1631
  )
1632
  logger.info(f'[FILTER-TEL] {_tel_json.dumps({"event":"filter_reject","filter":"LatinGuard","original":orig_text[:80],"correction":corr_text[:80]})}')
 
1633
  continue
1634
 
1635
  # ── FIX-22: Emoji protection ──
@@ -1655,6 +1664,7 @@ def analyze_text():
1655
  f"'{orig_text}'→'{corr_text}'"
1656
  )
1657
  logger.info(f'[FILTER-TEL] {_tel_json.dumps({"event":"filter_reject","filter":"TanweenGuard","original":orig_text[:80],"correction":corr_text[:80]})}')
 
1658
  continue
1659
 
1660
  # ── FIX-24: Grammar punctuation stripping blocker ──
@@ -1672,6 +1682,7 @@ def analyze_text():
1672
  f"'{orig_text}'→'{corr_text}'"
1673
  )
1674
  logger.info(f'[FILTER-TEL] {_tel_json.dumps({"event":"filter_reject","filter":"PunctuationGuard","original":orig_text[:80],"correction":corr_text[:80]})}')
 
1675
  continue
1676
  # Also block combined tanween + punct stripping
1677
  _TANWEEN2 = '\u064B\u064C\u064D'
@@ -1683,6 +1694,7 @@ def analyze_text():
1683
  f"'{orig_text}'→'{corr_text}'"
1684
  )
1685
  logger.info(f'[FILTER-TEL] {_tel_json.dumps({"event":"filter_reject","filter":"PunctuationGuard","original":orig_text[:80],"correction":corr_text[:80]})}')
 
1686
  continue
1687
 
1688
  # ── FIX-25: Grammar punctuation spacing blocker ──
@@ -1716,6 +1728,7 @@ def analyze_text():
1716
  f"'{orig_text}'\u2192'{corr_text}'"
1717
  )
1718
  logger.info(f'[FILTER-TEL] {_tel_json.dumps({"event":"filter_reject","filter":"DigitGuard","original":orig_text[:80],"correction":corr_text[:80]})}')
 
1719
  continue
1720
 
1721
  # ── FIX-27b: Grammar hallucination guard (Jaccard) ──
@@ -1735,6 +1748,7 @@ def analyze_text():
1735
  f"'{orig_text}'\u2192'{corr_text}'"
1736
  )
1737
  logger.info(f'[FILTER-TEL] {_tel_json.dumps({"event":"filter_reject","filter":"Jaccard_05","original":orig_text[:80],"correction":corr_text[:80],"jaccard":round(_jac,3)})}')
 
1738
  continue
1739
 
1740
  # ── FIX-06: Directional block protection for grammar ──
@@ -1745,6 +1759,7 @@ def analyze_text():
1745
  f"[GRAMMAR] Directional block: '{orig_text}'→'{corr_text}'"
1746
  )
1747
  logger.info(f'[FILTER-TEL] {_tel_json.dumps({"event":"filter_reject","filter":"DirectionalBlock","original":orig_text[:80],"correction":corr_text[:80]})}')
 
1748
  continue
1749
  # Also check with clitic prefixes
1750
  _gram_dir_blocked = False
@@ -1823,6 +1838,7 @@ def analyze_text():
1823
  f"(valid word → non-word)"
1824
  )
1825
  logger.info(f'[FILTER-TEL] {_tel_json.dumps({"event":"filter_reject","filter":"IVtoOOV","original":orig_text[:80],"correction":corr_text[:80]})}')
 
1826
  continue
1827
  except Exception:
1828
  pass
@@ -1843,6 +1859,7 @@ def analyze_text():
1843
  stage_label = 'spelling'
1844
  _grammar_accepted_diffs.append(d) # FIX-04: track accepted
1845
  logger.info(f'[FILTER-TEL] {_tel_json.dumps({"event":"patch_accepted","stage":stage_label,"original":orig_text[:80],"correction":corr_text[:80],"start":d["start"],"end":d["end"]})}')
 
1846
  ctx.add_patch(
1847
  stage_label, d['start'], d['end'],
1848
  corr_text, confidence=1.0
@@ -1998,6 +2015,7 @@ def analyze_text():
1998
  'suggestions': suggestions,
1999
  'timing_ms': timing_ms,
2000
  'status': response_status,
 
2001
  }
2002
  if stage_errors:
2003
  response_data['warnings'] = stage_errors
 
1248
  current_text = text # Local alias (updated alongside ctx.current_text)
1249
  suggestions = [] # Legacy — will be replaced by ctx.patches at response time
1250
  mappers = [] # Legacy — will be replaced by ctx._offset_mappers
1251
+
1252
+ # ── Phase 11: In-memory telemetry collector ──
1253
+ _tel_events = []
1254
  total_start = time.time()
1255
  timing_ms = {'spelling_ms': 0, 'grammar_ms': 0, 'punctuation_ms': 0, 'total_ms': 0}
1256
 
 
1581
  # ── Phase 11: Telemetry — raw grammar output ──
1582
  import json as _tel_json
1583
  logger.info(f'[FILTER-TEL] {_tel_json.dumps({"event":"grammar_raw_output","input":_grammar_input_text[:200],"output":corrected_grammar[:200]})}')
1584
+ _tel_events.append({"event":"grammar_raw_output","input":_grammar_input_text[:200],"output":corrected_grammar[:200]})
1585
 
1586
  # FIX-03: Restore structured content in grammar output
1587
  if _structured_placeholders:
 
1594
  _grammar_accepted_diffs = [] # FIX-04: track accepted diffs
1595
  _grammar_total_diffs = len(diffs)
1596
  logger.info(f'[FILTER-TEL] {_tel_json.dumps({"event":"grammar_diffs_extracted","count":_grammar_total_diffs})}')
1597
+ _tel_events.append({"event":"grammar_diffs_extracted","count":_grammar_total_diffs})
1598
  for d in diffs:
1599
  orig_text = d.get('original', '')
1600
  corr_text = d.get('correction', '')
1601
  logger.info(f'[FILTER-TEL] {_tel_json.dumps({"event":"grammar_diff","original":orig_text[:80],"correction":corr_text[:80],"start":d["start"],"end":d["end"]})}')
1602
+ _tel_events.append({"event":"grammar_diff","original":orig_text[:80],"correction":corr_text[:80],"start":d["start"],"end":d["end"]})
1603
  # StageLocker: skip diffs that overlap with locked ranges
1604
  if ctx.stage_locker.is_locked(d['start'], d['end']):
1605
  logger.info(
 
1607
  f"'{d.get('original','')}' — locked by previous stage"
1608
  )
1609
  logger.info(f'[FILTER-TEL] {_tel_json.dumps({"event":"filter_reject","filter":"StageLocker","original":orig_text[:80],"correction":corr_text[:80]})}')
1610
+ _tel_events.append({"event":"filter_reject","filter":"StageLocker","original":orig_text[:80],"correction":corr_text[:80]})
1611
  continue
1612
 
1613
  # Reject grammar hallucinations (e.g. جالس→جاكسون)
 
1622
  f"(jaccard={jaccard:.2f})"
1623
  )
1624
  logger.info(f'[FILTER-TEL] {_tel_json.dumps({"event":"filter_reject","filter":"Jaccard_03","original":orig_text[:80],"correction":corr_text[:80],"jaccard":round(jaccard,3)})}')
1625
+ _tel_events.append({"event":"filter_reject","filter":"Jaccard_03","original":orig_text[:80],"correction":corr_text[:80],"jaccard":round(jaccard,3)})
1626
  continue
1627
 
1628
  # ── FIX-13: Named entity protection ──
 
1638
  f"'{orig_text}'→'{corr_text}'"
1639
  )
1640
  logger.info(f'[FILTER-TEL] {_tel_json.dumps({"event":"filter_reject","filter":"LatinGuard","original":orig_text[:80],"correction":corr_text[:80]})}')
1641
+ _tel_events.append({"event":"filter_reject","filter":"LatinGuard","original":orig_text[:80],"correction":corr_text[:80]})
1642
  continue
1643
 
1644
  # ── FIX-22: Emoji protection ──
 
1664
  f"'{orig_text}'→'{corr_text}'"
1665
  )
1666
  logger.info(f'[FILTER-TEL] {_tel_json.dumps({"event":"filter_reject","filter":"TanweenGuard","original":orig_text[:80],"correction":corr_text[:80]})}')
1667
+ _tel_events.append({"event":"filter_reject","filter":"TanweenGuard","original":orig_text[:80],"correction":corr_text[:80]})
1668
  continue
1669
 
1670
  # ── FIX-24: Grammar punctuation stripping blocker ──
 
1682
  f"'{orig_text}'→'{corr_text}'"
1683
  )
1684
  logger.info(f'[FILTER-TEL] {_tel_json.dumps({"event":"filter_reject","filter":"PunctuationGuard","original":orig_text[:80],"correction":corr_text[:80]})}')
1685
+ _tel_events.append({"event":"filter_reject","filter":"PunctuationGuard","original":orig_text[:80],"correction":corr_text[:80]})
1686
  continue
1687
  # Also block combined tanween + punct stripping
1688
  _TANWEEN2 = '\u064B\u064C\u064D'
 
1694
  f"'{orig_text}'→'{corr_text}'"
1695
  )
1696
  logger.info(f'[FILTER-TEL] {_tel_json.dumps({"event":"filter_reject","filter":"PunctuationGuard","original":orig_text[:80],"correction":corr_text[:80]})}')
1697
+ _tel_events.append({"event":"filter_reject","filter":"PunctuationGuard","original":orig_text[:80],"correction":corr_text[:80]})
1698
  continue
1699
 
1700
  # ── FIX-25: Grammar punctuation spacing blocker ──
 
1728
  f"'{orig_text}'\u2192'{corr_text}'"
1729
  )
1730
  logger.info(f'[FILTER-TEL] {_tel_json.dumps({"event":"filter_reject","filter":"DigitGuard","original":orig_text[:80],"correction":corr_text[:80]})}')
1731
+ _tel_events.append({"event":"filter_reject","filter":"DigitGuard","original":orig_text[:80],"correction":corr_text[:80]})
1732
  continue
1733
 
1734
  # ── FIX-27b: Grammar hallucination guard (Jaccard) ──
 
1748
  f"'{orig_text}'\u2192'{corr_text}'"
1749
  )
1750
  logger.info(f'[FILTER-TEL] {_tel_json.dumps({"event":"filter_reject","filter":"Jaccard_05","original":orig_text[:80],"correction":corr_text[:80],"jaccard":round(_jac,3)})}')
1751
+ _tel_events.append({"event":"filter_reject","filter":"Jaccard_05","original":orig_text[:80],"correction":corr_text[:80],"jaccard":round(_jac,3)})
1752
  continue
1753
 
1754
  # ── FIX-06: Directional block protection for grammar ──
 
1759
  f"[GRAMMAR] Directional block: '{orig_text}'→'{corr_text}'"
1760
  )
1761
  logger.info(f'[FILTER-TEL] {_tel_json.dumps({"event":"filter_reject","filter":"DirectionalBlock","original":orig_text[:80],"correction":corr_text[:80]})}')
1762
+ _tel_events.append({"event":"filter_reject","filter":"DirectionalBlock","original":orig_text[:80],"correction":corr_text[:80]})
1763
  continue
1764
  # Also check with clitic prefixes
1765
  _gram_dir_blocked = False
 
1838
  f"(valid word → non-word)"
1839
  )
1840
  logger.info(f'[FILTER-TEL] {_tel_json.dumps({"event":"filter_reject","filter":"IVtoOOV","original":orig_text[:80],"correction":corr_text[:80]})}')
1841
+ _tel_events.append({"event":"filter_reject","filter":"IVtoOOV","original":orig_text[:80],"correction":corr_text[:80]})
1842
  continue
1843
  except Exception:
1844
  pass
 
1859
  stage_label = 'spelling'
1860
  _grammar_accepted_diffs.append(d) # FIX-04: track accepted
1861
  logger.info(f'[FILTER-TEL] {_tel_json.dumps({"event":"patch_accepted","stage":stage_label,"original":orig_text[:80],"correction":corr_text[:80],"start":d["start"],"end":d["end"]})}')
1862
+ _tel_events.append({"event":"patch_accepted","stage":stage_label,"original":orig_text[:80],"correction":corr_text[:80],"start":d["start"],"end":d["end"]})
1863
  ctx.add_patch(
1864
  stage_label, d['start'], d['end'],
1865
  corr_text, confidence=1.0
 
2015
  'suggestions': suggestions,
2016
  'timing_ms': timing_ms,
2017
  'status': response_status,
2018
+ 'telemetry': _tel_events,
2019
  }
2020
  if stage_errors:
2021
  response_data['warnings'] = stage_errors
tests/phase11/analysis_suite.py ADDED
@@ -0,0 +1,572 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Phase 11 — Tasks 2, 3, 4, 6, 7: Analysis Suite
3
+
4
+ Consumes telemetry.jsonl from Task 8 and produces all Phase 11 reports:
5
+ - Task 2: FN Root Cause Classification (grammar_fn_analysis)
6
+ - Task 3: G028 Deep Investigation (G028_root_cause)
7
+ - Task 4: Rejection Matrix (rejection_matrix)
8
+ - Task 6: StageLocker Audit (stagelocker_audit)
9
+ - Task 7: PatchSet Audit (patchset_audit)
10
+ - Filter Telemetry Report (filter_telemetry)
11
+
12
+ Usage:
13
+ python tests/phase11/analysis_suite.py
14
+
15
+ Requires: tests/phase11/artifacts/telemetry.jsonl (from telemetry_capture.py)
16
+ tests/phase10/reports/phase10_results.json (from benchmark_runner.py)
17
+ """
18
+ import json
19
+ import os
20
+ import sys
21
+ import glob
22
+
23
+ PHASE11_DIR = os.path.dirname(os.path.abspath(__file__))
24
+ ARTIFACTS_DIR = os.path.join(PHASE11_DIR, 'artifacts')
25
+ REPORTS_DIR = os.path.join(PHASE11_DIR, 'reports')
26
+ PHASE10_RESULTS = os.path.join(PHASE11_DIR, '..', 'phase10', 'reports', 'phase10_results.json')
27
+
28
+ os.makedirs(REPORTS_DIR, exist_ok=True)
29
+
30
+
31
+ def load_telemetry():
32
+ """Load telemetry.jsonl"""
33
+ path = os.path.join(ARTIFACTS_DIR, 'telemetry.jsonl')
34
+ if not os.path.exists(path):
35
+ print(f"ERROR: {path} not found. Run telemetry_capture.py first.")
36
+ sys.exit(1)
37
+ records = []
38
+ with open(path, 'r', encoding='utf-8') as f:
39
+ for line in f:
40
+ if line.strip():
41
+ records.append(json.loads(line))
42
+ return records
43
+
44
+
45
+ def load_phase10_results():
46
+ """Load phase10 benchmark results."""
47
+ path = os.path.abspath(PHASE10_RESULTS)
48
+ if not os.path.exists(path):
49
+ print(f"WARNING: {path} not found. Some analyses will be incomplete.")
50
+ return None
51
+ return json.load(open(path, encoding='utf-8'))
52
+
53
+
54
+ # ══════════════════════════════════════════════════════════════════
55
+ # Task 1 — Filter Telemetry Report
56
+ # ══════════════════════════════════════════════════════════════════
57
+
58
+ def generate_filter_telemetry(records):
59
+ """Task 1: Measure grammar correction rejection by filter."""
60
+ print("\n[Task 1] Generating filter telemetry...")
61
+
62
+ all_events = []
63
+ for rec in records:
64
+ for evt in rec.get('telemetry_events', []):
65
+ evt['sample_id'] = rec['sample_id']
66
+ evt['dataset'] = rec['dataset']
67
+ all_events.append(evt)
68
+
69
+ # Count by filter
70
+ filter_counts = {}
71
+ accepted = 0
72
+ total_diffs = 0
73
+ raw_outputs = 0
74
+ rejection_details = []
75
+
76
+ for evt in all_events:
77
+ if evt.get('event') == 'grammar_diff':
78
+ total_diffs += 1
79
+ elif evt.get('event') == 'grammar_raw_output':
80
+ raw_outputs += 1
81
+ elif evt.get('event') == 'filter_reject':
82
+ f_name = evt.get('filter', 'Unknown')
83
+ filter_counts[f_name] = filter_counts.get(f_name, 0) + 1
84
+ rejection_details.append(evt)
85
+ elif evt.get('event') == 'patch_accepted':
86
+ accepted += 1
87
+
88
+ total_rejected = sum(filter_counts.values())
89
+
90
+ # JSON report
91
+ report = {
92
+ 'grammar_raw_outputs': raw_outputs,
93
+ 'grammar_diffs_generated': total_diffs,
94
+ 'accepted': accepted,
95
+ 'rejected': total_rejected,
96
+ 'rejection_by_filter': dict(sorted(filter_counts.items(), key=lambda x: -x[1])),
97
+ 'rejection_details': rejection_details,
98
+ }
99
+
100
+ json_path = os.path.join(REPORTS_DIR, 'filter_telemetry.json')
101
+ with open(json_path, 'w', encoding='utf-8') as f:
102
+ json.dump(report, f, indent=2, ensure_ascii=False)
103
+
104
+ # Markdown report
105
+ md_path = os.path.join(REPORTS_DIR, 'filter_telemetry.md')
106
+ with open(md_path, 'w', encoding='utf-8') as f:
107
+ f.write("# Grammar Filter Telemetry Report\n\n")
108
+ f.write("## Pipeline Funnel\n\n")
109
+ f.write(f"| Stage | Count |\n")
110
+ f.write(f"|---|---|\n")
111
+ f.write(f"| Grammar raw outputs | {raw_outputs} |\n")
112
+ f.write(f"| Diffs extracted | {total_diffs} |\n")
113
+ f.write(f"| **Accepted** | **{accepted}** |\n")
114
+ f.write(f"| **Rejected** | **{total_rejected}** |\n\n")
115
+ f.write("## Rejections by Filter\n\n")
116
+ f.write("| Filter | Rejections | % of Total |\n")
117
+ f.write("|---|---|---|\n")
118
+ for f_name, count in sorted(filter_counts.items(), key=lambda x: -x[1]):
119
+ pct = count / total_rejected * 100 if total_rejected > 0 else 0
120
+ f.write(f"| {f_name} | {count} | {pct:.1f}% |\n")
121
+ f.write(f"\n## Rejection Details\n\n")
122
+ for det in rejection_details:
123
+ f.write(f"- **{det.get('filter')}**: `{det.get('original','')}` → `{det.get('correction','')}` (sample: {det.get('sample_id','')})\n")
124
+
125
+ print(f" Generated: {json_path}")
126
+ print(f" Generated: {md_path}")
127
+ print(f" Diffs: {total_diffs}, Accepted: {accepted}, Rejected: {total_rejected}")
128
+ return report
129
+
130
+
131
+ # ══════════════════════════════════════════════════════════════════
132
+ # Task 2 — FN Root Cause Classification
133
+ # ══════════════════════════════════════════════════════════════════
134
+
135
+ def generate_fn_classification(records, p10_results):
136
+ """Task 2: Classify every grammar FN into a root cause category."""
137
+ print("\n[Task 2] Generating FN root cause classification...")
138
+
139
+ # Get grammar FN samples from phase10 results
140
+ grammar_fns = []
141
+ if p10_results:
142
+ for r in p10_results.get('results', []):
143
+ if r.get('dataset') == 'grammar' and r.get('pipeline_verdict') == 'FN':
144
+ grammar_fns.append(r)
145
+
146
+ # Cross-reference with telemetry
147
+ tel_by_id = {r['sample_id']: r for r in records}
148
+
149
+ classifications = []
150
+ category_counts = {}
151
+
152
+ for fn in grammar_fns:
153
+ sid = fn['id']
154
+ tel = tel_by_id.get(sid, {})
155
+ events = tel.get('telemetry_events', [])
156
+
157
+ # Determine classification
158
+ classification = 'UNKNOWN'
159
+ evidence = ''
160
+
161
+ # Check if grammar model produced any output change
162
+ raw_outputs = [e for e in events if e.get('event') == 'grammar_raw_output']
163
+ diffs = [e for e in events if e.get('event') == 'grammar_diff']
164
+ rejections = [e for e in events if e.get('event') == 'filter_reject']
165
+ accepted = [e for e in events if e.get('event') == 'patch_accepted']
166
+
167
+ if not raw_outputs:
168
+ classification = 'MODEL_FAILURE'
169
+ evidence = 'No grammar raw output event found'
170
+ elif not diffs:
171
+ # Model ran but no diffs extracted
172
+ raw = raw_outputs[0] if raw_outputs else {}
173
+ if raw.get('input', '')[:100] == raw.get('output', '')[:100]:
174
+ classification = 'MODEL_FAILURE'
175
+ evidence = 'Grammar model returned input unchanged'
176
+ else:
177
+ classification = 'DIFF_EXTRACTION_FAILURE'
178
+ evidence = f"Model changed text but get_word_diffs() found 0 diffs"
179
+ elif rejections and not accepted:
180
+ # All diffs were rejected
181
+ filter_names = [r.get('filter', '') for r in rejections]
182
+ if any(f == 'StageLocker' for f in filter_names):
183
+ classification = 'STAGELOCKER_FAILURE'
184
+ evidence = f"Rejected by StageLocker: {[r.get('original','') for r in rejections if r.get('filter')=='StageLocker']}"
185
+ else:
186
+ classification = 'FILTER_FAILURE'
187
+ evidence = f"Rejected by: {', '.join(set(filter_names))}"
188
+ elif accepted:
189
+ # Patches were accepted but output doesn't match expected
190
+ classification = 'PATCH_FAILURE'
191
+ evidence = f"Grammar patch accepted but final output doesn't match expected"
192
+ else:
193
+ classification = 'UNKNOWN'
194
+ evidence = f"Events: {len(events)} total, {len(diffs)} diffs, {len(rejections)} rejections"
195
+
196
+ classifications.append({
197
+ 'sample_id': sid,
198
+ 'input': fn.get('input', '')[:100],
199
+ 'expected': fn.get('expected', '')[:100],
200
+ 'pipeline_output': fn.get('pipeline_output', '')[:100],
201
+ 'classification': classification,
202
+ 'evidence': evidence,
203
+ 'filter_rejections': [r.get('filter', '') for r in rejections],
204
+ 'accepted_patches': len(accepted),
205
+ })
206
+ category_counts[classification] = category_counts.get(classification, 0) + 1
207
+
208
+ # JSON report
209
+ json_path = os.path.join(REPORTS_DIR, 'grammar_fn_analysis.json')
210
+ with open(json_path, 'w', encoding='utf-8') as f:
211
+ json.dump({
212
+ 'total_grammar_fn': len(grammar_fns),
213
+ 'category_counts': dict(sorted(category_counts.items(), key=lambda x: -x[1])),
214
+ 'classifications': classifications,
215
+ }, f, indent=2, ensure_ascii=False)
216
+
217
+ # Markdown report
218
+ md_path = os.path.join(REPORTS_DIR, 'grammar_fn_analysis.md')
219
+ with open(md_path, 'w', encoding='utf-8') as f:
220
+ f.write("# Grammar FN Root Cause Analysis\n\n")
221
+ f.write(f"**Total Grammar FN: {len(grammar_fns)}**\n\n")
222
+ f.write("## By Category\n\n")
223
+ f.write("| Category | Count | % |\n")
224
+ f.write("|---|---|---|\n")
225
+ for cat, cnt in sorted(category_counts.items(), key=lambda x: -x[1]):
226
+ pct = cnt / len(grammar_fns) * 100 if grammar_fns else 0
227
+ f.write(f"| {cat} | {cnt} | {pct:.0f}% |\n")
228
+ f.write("\n## Detail\n\n")
229
+ for c in classifications:
230
+ f.write(f"### {c['sample_id']} — {c['classification']}\n")
231
+ f.write(f"- **Input**: `{c['input']}`\n")
232
+ f.write(f"- **Expected**: `{c['expected']}`\n")
233
+ f.write(f"- **Pipeline output**: `{c['pipeline_output']}`\n")
234
+ f.write(f"- **Evidence**: {c['evidence']}\n")
235
+ if c['filter_rejections']:
236
+ f.write(f"- **Filters**: {', '.join(c['filter_rejections'])}\n")
237
+ f.write("\n")
238
+
239
+ print(f" Generated: {json_path}")
240
+ print(f" Generated: {md_path}")
241
+ print(f" Total FN: {len(grammar_fns)}, Categories: {category_counts}")
242
+ return classifications
243
+
244
+
245
+ # ══════════════════════════════════════════════════════════════════
246
+ # Task 3 — G028 Deep Investigation
247
+ # ══════════════════════════════════════════════════════════════════
248
+
249
+ def generate_g028_trace(records, p10_results):
250
+ """Task 3: Full lifecycle trace of G028."""
251
+ print("\n[Task 3] Generating G028 deep investigation...")
252
+
253
+ # Find G028 in telemetry
254
+ g028_tel = None
255
+ for r in records:
256
+ if r.get('sample_id') == 'G028':
257
+ g028_tel = r
258
+ break
259
+
260
+ # Find G028 in phase10 results
261
+ g028_p10 = None
262
+ if p10_results:
263
+ for r in p10_results.get('results', []):
264
+ if r.get('id') == 'G028':
265
+ g028_p10 = r
266
+ break
267
+
268
+ md_path = os.path.join(REPORTS_DIR, 'G028_root_cause.md')
269
+ with open(md_path, 'w', encoding='utf-8') as f:
270
+ f.write("# G028 Root Cause Investigation\n\n")
271
+
272
+ if not g028_tel:
273
+ f.write("> [!CAUTION]\n> G028 not found in telemetry data.\n\n")
274
+ print(" WARNING: G028 not found in telemetry")
275
+ return
276
+
277
+ f.write("## Input\n\n")
278
+ f.write(f"```\n{g028_tel.get('input', 'N/A')}\n```\n\n")
279
+
280
+ f.write("## Expected Output\n\n")
281
+ f.write(f"```\n{g028_tel.get('expected', 'N/A')}\n```\n\n")
282
+
283
+ f.write("## Pipeline Output\n\n")
284
+ f.write(f"```\n{g028_tel.get('pipeline_output', 'N/A')}\n```\n\n")
285
+
286
+ f.write(f"## Pass/Fail: {'✅ PASS' if g028_tel.get('passed') else '❌ FAIL'}\n\n")
287
+
288
+ # Telemetry events
289
+ events = g028_tel.get('telemetry_events', [])
290
+ f.write("## Telemetry Events (in order)\n\n")
291
+ f.write("| # | Event | Details |\n")
292
+ f.write("|---|---|---|\n")
293
+ for i, evt in enumerate(events):
294
+ event_type = evt.get('event', '')
295
+ if event_type == 'grammar_raw_output':
296
+ f.write(f"| {i+1} | grammar_raw_output | input=`{evt.get('input','')[:60]}` output=`{evt.get('output','')[:60]}` |\n")
297
+ elif event_type == 'grammar_diff':
298
+ f.write(f"| {i+1} | grammar_diff | `{evt.get('original','')}` → `{evt.get('correction','')}` [{evt.get('start')}-{evt.get('end')}] |\n")
299
+ elif event_type == 'filter_reject':
300
+ f.write(f"| {i+1} | **REJECT** | **{evt.get('filter','')}**: `{evt.get('original','')}` → `{evt.get('correction','')}` |\n")
301
+ elif event_type == 'patch_accepted':
302
+ f.write(f"| {i+1} | patch_accepted | `{evt.get('original','')}` → `{evt.get('correction','')}` [{evt.get('start')}-{evt.get('end')}] |\n")
303
+ else:
304
+ f.write(f"| {i+1} | {event_type} | {json.dumps(evt, ensure_ascii=False)[:80]} |\n")
305
+
306
+ # Phase 10 data
307
+ if g028_p10:
308
+ f.write("\n## Phase 10 Benchmark Data\n\n")
309
+ f.write(f"- **Verdict**: {g028_p10.get('pipeline_verdict')}\n")
310
+ f.write(f"- **Root cause stage**: {g028_p10.get('root_cause_stage', 'N/A')}\n")
311
+ f.write(f"- **Root cause detail**: {g028_p10.get('root_cause_detail', 'N/A')}\n")
312
+ f.write(f"- **Suggestions**: {len(g028_p10.get('pipeline_suggestions', []))}\n")
313
+ for s in g028_p10.get('pipeline_suggestions', []):
314
+ f.write(f" - [{s.get('type')}] `{s.get('original','')}` → `{s.get('correction','')}` (conf={s.get('confidence',0)})\n")
315
+
316
+ # Root cause determination
317
+ f.write("\n## Root Cause Determination\n\n")
318
+ raw_outputs = [e for e in events if e.get('event') == 'grammar_raw_output']
319
+ diffs = [e for e in events if e.get('event') == 'grammar_diff']
320
+ rejects = [e for e in events if e.get('event') == 'filter_reject']
321
+ accepts = [e for e in events if e.get('event') == 'patch_accepted']
322
+
323
+ if not raw_outputs:
324
+ f.write("**ROOT CAUSE: No grammar output** — Grammar model did not run or returned empty.\n")
325
+ elif raw_outputs:
326
+ raw = raw_outputs[0]
327
+ inp = raw.get('input', '')
328
+ out = raw.get('output', '')
329
+ if inp[:80] == out[:80]:
330
+ f.write("**ROOT CAUSE: MODEL_FAILURE** — Grammar model returned input unchanged. The model did not detect the error.\n")
331
+ elif not diffs:
332
+ f.write("**ROOT CAUSE: DIFF_EXTRACTION_FAILURE** — Model changed text but `get_word_diffs()` failed to extract diffs.\n")
333
+ f.write(f"\n- Model input: `{inp[:100]}`\n")
334
+ f.write(f"- Model output: `{out[:100]}`\n")
335
+ elif rejects and not accepts:
336
+ filters = set(r.get('filter', '') for r in rejects)
337
+ f.write(f"**ROOT CAUSE: FILTER_FAILURE** — Grammar model produced the correct fix but filters rejected it.\n")
338
+ f.write(f"\n- Rejected by: {', '.join(filters)}\n")
339
+ for r in rejects:
340
+ f.write(f"- `{r.get('original','')}` → `{r.get('correction','')}` (filter: {r.get('filter','')})\n")
341
+ elif accepts:
342
+ f.write("**ROOT CAUSE: PATCH_FAILURE or REBUILD_FAILURE** — Grammar patch was accepted but final output doesn't match expected.\n")
343
+ f.write("\nPossible causes:\n")
344
+ f.write("1. OffsetMapper corrupted patch coordinates during rebuild\n")
345
+ f.write("2. PatchSet conflict resolution dropped the patch\n")
346
+ f.write("3. Rebuild logic (accepted diffs → safe_grammar) lost the change\n")
347
+ else:
348
+ f.write("**ROOT CAUSE: UNKNOWN** — Insufficient telemetry data.\n")
349
+
350
+ print(f" Generated: {md_path}")
351
+
352
+
353
+ # ══════════════════════════════════════════════════════════════════
354
+ # Task 4 — Rejection Matrix
355
+ # ══════════════════════════════════════════════════════════════════
356
+
357
+ def generate_rejection_matrix(records, p10_results):
358
+ """Task 4: Measure filter quality — correct vs incorrect rejections."""
359
+ print("\n[Task 4] Generating rejection matrix...")
360
+
361
+ # Build lookup: sample_id → passed/failed + expected behavior
362
+ p10_by_id = {}
363
+ if p10_results:
364
+ for r in p10_results.get('results', []):
365
+ p10_by_id[r['id']] = r
366
+
367
+ # Collect all rejections
368
+ filter_stats = {} # filter → {total, correct, incorrect}
369
+
370
+ for rec in records:
371
+ sid = rec.get('sample_id', '')
372
+ p10 = p10_by_id.get(sid, {})
373
+ expected_unchanged = (p10.get('expected', '') == p10.get('input', ''))
374
+ is_fp = p10.get('pipeline_verdict') == 'FP'
375
+ is_fn = p10.get('pipeline_verdict') == 'FN'
376
+
377
+ for evt in rec.get('telemetry_events', []):
378
+ if evt.get('event') != 'filter_reject':
379
+ continue
380
+
381
+ f_name = evt.get('filter', 'Unknown')
382
+ if f_name not in filter_stats:
383
+ filter_stats[f_name] = {'total': 0, 'correct': 0, 'incorrect': 0, 'details': []}
384
+
385
+ filter_stats[f_name]['total'] += 1
386
+
387
+ # Determine if rejection was correct or incorrect
388
+ # Correct rejection: text was already correct (TN/TP scenario), or
389
+ # the proposed correction was wrong
390
+ # Incorrect rejection: text had an error AND the correction was right (FN)
391
+ if expected_unchanged:
392
+ # Text was already correct → rejecting a change is CORRECT
393
+ filter_stats[f_name]['correct'] += 1
394
+ elif is_fn:
395
+ # Text had an error AND pipeline didn't fix it → rejection MIGHT be incorrect
396
+ # Check if this rejection's correction matches expected
397
+ orig = evt.get('original', '')
398
+ corr = evt.get('correction', '')
399
+ expected = p10.get('expected', '')
400
+ if corr in expected and orig not in expected:
401
+ filter_stats[f_name]['incorrect'] += 1
402
+ filter_stats[f_name]['details'].append({
403
+ 'sample_id': sid,
404
+ 'original': orig,
405
+ 'correction': corr,
406
+ 'expected': expected[:80],
407
+ })
408
+ else:
409
+ filter_stats[f_name]['correct'] += 1
410
+ else:
411
+ # FP or TP — rejection is generally correct
412
+ filter_stats[f_name]['correct'] += 1
413
+
414
+ # Markdown report
415
+ md_path = os.path.join(REPORTS_DIR, 'rejection_matrix.md')
416
+ with open(md_path, 'w', encoding='utf-8') as f:
417
+ f.write("# Rejection Matrix\n\n")
418
+ f.write("> For every rejected grammar correction, determine whether rejection was correct or incorrect.\n\n")
419
+ f.write("| Filter | Total Rejections | Correct | Incorrect | Precision |\n")
420
+ f.write("|---|---|---|---|---|\n")
421
+ for f_name, stats in sorted(filter_stats.items(), key=lambda x: -x[1]['total']):
422
+ prec = stats['correct'] / stats['total'] * 100 if stats['total'] > 0 else 0
423
+ f.write(f"| {f_name} | {stats['total']} | {stats['correct']} | {stats['incorrect']} | {prec:.0f}% |\n")
424
+
425
+ total_all = sum(s['total'] for s in filter_stats.values())
426
+ correct_all = sum(s['correct'] for s in filter_stats.values())
427
+ incorrect_all = sum(s['incorrect'] for s in filter_stats.values())
428
+ prec_all = correct_all / total_all * 100 if total_all > 0 else 0
429
+ f.write(f"| **TOTAL** | **{total_all}** | **{correct_all}** | **{incorrect_all}** | **{prec_all:.0f}%** |\n")
430
+
431
+ # Incorrect rejection details
432
+ f.write("\n## Incorrect Rejections (Valid Corrections Blocked)\n\n")
433
+ has_incorrect = False
434
+ for f_name, stats in sorted(filter_stats.items(), key=lambda x: -x[1]['incorrect']):
435
+ for det in stats['details']:
436
+ has_incorrect = True
437
+ f.write(f"- **{f_name}** ({det['sample_id']}): `{det['original']}` → `{det['correction']}` (expected: `{det['expected']}`)\n")
438
+ if not has_incorrect:
439
+ f.write("None detected — all rejections appear correct.\n")
440
+
441
+ print(f" Generated: {md_path}")
442
+ print(f" Total rejections: {total_all}, Correct: {correct_all}, Incorrect: {incorrect_all}")
443
+
444
+
445
+ # ══════════════════════════════════════════════════════════════════
446
+ # Task 6 — StageLocker Audit
447
+ # ══════════════════════════════════════════════════════════════════
448
+
449
+ def generate_stagelocker_audit(records, p10_results):
450
+ """Task 6: Audit StageLocker lock/block behavior."""
451
+ print("\n[Task 6] Generating StageLocker audit...")
452
+
453
+ stagelocker_blocks = []
454
+ total_locks = 0 # We can't directly count locks from telemetry, so estimate
455
+
456
+ for rec in records:
457
+ for evt in rec.get('telemetry_events', []):
458
+ if evt.get('event') == 'filter_reject' and evt.get('filter') == 'StageLocker':
459
+ stagelocker_blocks.append({
460
+ 'sample_id': rec.get('sample_id', ''),
461
+ 'dataset': rec.get('dataset', ''),
462
+ 'original': evt.get('original', ''),
463
+ 'correction': evt.get('correction', ''),
464
+ })
465
+ if evt.get('event') == 'patch_accepted':
466
+ total_locks += 1 # Each accepted patch creates a lock
467
+
468
+ # Count grammar vs punctuation blocks
469
+ grammar_blocks = len(stagelocker_blocks) # All from grammar stage
470
+ # We'd need punctuation telemetry too for punc blocks
471
+
472
+ md_path = os.path.join(REPORTS_DIR, 'stagelocker_audit.md')
473
+ with open(md_path, 'w', encoding='utf-8') as f:
474
+ f.write("# StageLocker Audit\n\n")
475
+ f.write("## Statistics\n\n")
476
+ f.write(f"| Metric | Value |\n")
477
+ f.write(f"|---|---|\n")
478
+ f.write(f"| Total locks created (est.) | {total_locks} |\n")
479
+ f.write(f"| Grammar blocks | {grammar_blocks} |\n")
480
+ f.write(f"\n## Grammar Blocks Detail\n\n")
481
+ if stagelocker_blocks:
482
+ f.write("| Sample | Original | Correction |\n")
483
+ f.write("|---|---|---|\n")
484
+ for b in stagelocker_blocks:
485
+ f.write(f"| {b['sample_id']} | `{b['original']}` | `{b['correction']}` |\n")
486
+ else:
487
+ f.write("No StageLocker blocks detected in grammar stage.\n")
488
+
489
+ print(f" Generated: {md_path}")
490
+ print(f" Total locks: {total_locks}, Grammar blocks: {grammar_blocks}")
491
+
492
+
493
+ # ══════════════════════════════════════════════════════════════════
494
+ # Task 7 — PatchSet Audit
495
+ # ══════════════════════════════════════════════════════════════════
496
+
497
+ def generate_patchset_audit(records, p10_results):
498
+ """Task 7: Audit PatchSet conflict resolution."""
499
+ print("\n[Task 7] Generating PatchSet audit...")
500
+
501
+ total_patches = 0
502
+ total_conflicts = 0
503
+ ownership_by_stage = {}
504
+
505
+ if p10_results:
506
+ for r in p10_results.get('results', []):
507
+ suggestions = r.get('pipeline_suggestions', [])
508
+ for s in suggestions:
509
+ stage = s.get('type', 'unknown')
510
+ ownership_by_stage[stage] = ownership_by_stage.get(stage, 0) + 1
511
+ total_patches += 1
512
+
513
+ # Check for conflicts (overlapping patches from different stages)
514
+ if p10_results:
515
+ for r in p10_results.get('results', []):
516
+ suggestions = r.get('pipeline_suggestions', [])
517
+ for i, s1 in enumerate(suggestions):
518
+ for j, s2 in enumerate(suggestions):
519
+ if i >= j:
520
+ continue
521
+ if s1.get('start', 0) < s2.get('end', 0) and s1.get('end', 0) > s2.get('start', 0):
522
+ if s1.get('type') != s2.get('type'):
523
+ total_conflicts += 1
524
+
525
+ md_path = os.path.join(REPORTS_DIR, 'patchset_audit.md')
526
+ with open(md_path, 'w', encoding='utf-8') as f:
527
+ f.write("# PatchSet Audit\n\n")
528
+ f.write("## Statistics\n\n")
529
+ f.write(f"| Metric | Value |\n")
530
+ f.write(f"|---|---|\n")
531
+ f.write(f"| Total patches generated | {total_patches} |\n")
532
+ f.write(f"| Total cross-stage conflicts | {total_conflicts} |\n")
533
+ f.write(f"\n## Patch Ownership by Stage\n\n")
534
+ f.write("| Stage | Patches |\n")
535
+ f.write("|---|---|\n")
536
+ for stage, count in sorted(ownership_by_stage.items(), key=lambda x: -x[1]):
537
+ f.write(f"| {stage} | {count} |\n")
538
+
539
+ print(f" Generated: {md_path}")
540
+ print(f" Total patches: {total_patches}, Conflicts: {total_conflicts}")
541
+
542
+
543
+ # ══════════════════════════════════════════════════════════════════
544
+ # Main
545
+ # ══════════════════════════════════════════════════════════════════
546
+
547
+ def main():
548
+ print("Phase 11 Analysis Suite")
549
+ print("=" * 60)
550
+
551
+ records = load_telemetry()
552
+ p10_results = load_phase10_results()
553
+
554
+ print(f"Loaded {len(records)} telemetry records")
555
+ if p10_results:
556
+ print(f"Loaded phase10 results: {len(p10_results.get('results', []))} samples")
557
+
558
+ # Run all analysis tasks
559
+ tel_report = generate_filter_telemetry(records)
560
+ fn_class = generate_fn_classification(records, p10_results)
561
+ generate_g028_trace(records, p10_results)
562
+ generate_rejection_matrix(records, p10_results)
563
+ generate_stagelocker_audit(records, p10_results)
564
+ generate_patchset_audit(records, p10_results)
565
+
566
+ print(f"\n{'='*60}")
567
+ print(f"All reports generated in: {REPORTS_DIR}")
568
+ print(f"{'='*60}")
569
+
570
+
571
+ if __name__ == '__main__':
572
+ main()
tests/phase11/artifacts/telemetry.jsonl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {"sample_id": "E001", "dataset": "entities", "input": "محمد صلاح لاعب كرة قدم مصري", "expected": "محمد صلاح لاعب كرة قدم مصري", "pipeline_output": "محمد صلاح لاعب كرة قدم مصري", "passed": true, "suggestion_count": 0, "timing": {"grammar_ms": 5257, "punctuation_ms": 2817, "spelling_ms": 9905, "total_ms": 17983}, "telemetry_events": []}
2
+ {"sample_id": "E002", "dataset": "entities", "input": "عبدالله يدرس في الجامعة", "expected": "عبدالله يدرس في الجامعة", "pipeline_output": "عبد الله يدرس في الجامعة", "passed": false, "suggestion_count": 1, "timing": {"grammar_ms": 688, "punctuation_ms": 434, "spelling_ms": 2449, "total_ms": 3574}, "telemetry_events": []}
3
+ {"sample_id": "E003", "dataset": "entities", "input": "عبد الرحمن أخي الأكبر", "expected": "عبد الرحمن أخي الأكبر", "pipeline_output": "عبد الرحمن أخي الأكبر", "passed": true, "suggestion_count": 0, "timing": {"grammar_ms": 582, "punctuation_ms": 382, "spelling_ms": 1628, "total_ms": 2593}, "telemetry_events": []}
tests/phase11/artifacts/telemetry_summary.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "total_samples": 3,
3
+ "total_grammar_diffs": 0,
4
+ "total_accepted": 0,
5
+ "total_rejected": 0,
6
+ "rejection_by_filter": {},
7
+ "pass_rate": 66.66666666666666
8
+ }
tests/phase11/reports/benchmark_expansion_plan.md ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Benchmark Expansion Plan (Phase 12)
2
+
3
+ > [!NOTE]
4
+ > Design document only. No implementation in Phase 11.
5
+
6
+ ## Current Benchmark Weaknesses
7
+
8
+ | Gap | Current | Impact |
9
+ |---|---|---|
10
+ | No real user data | 0/270 from users | Benchmark may not represent production |
11
+ | No Arabic entities | Only Latin names protected | عبدالله, المدينة unprotected |
12
+ | No mixed Arabic-English | 0 samples | Common in tech writing |
13
+ | No JSON/HTML/Markdown | Only code blocks | Web content untested |
14
+ | No partial Quran | Only exact phrases | Real-world usage untested |
15
+ | No noisy Quran | Only clean quotes | Typos in religious text untested |
16
+ | No severity weighting | All errors equal | URL corruption = tanween fix |
17
+
18
+ ## Proposed New Datasets
19
+
20
+ ### Dataset 8: Arabic Named Entities (30 samples)
21
+
22
+ ```text
23
+ Categories:
24
+ - Person names with prepositions (عبد الله, محمد بن سلمان)
25
+ - Place names (المدينة المنورة, جبل الطور)
26
+ - Organization names (جامعة القاهرة, الأمم المتحدة)
27
+ - Historical/cultural names (صلاح الدين, ابن خلدون)
28
+ - Names with spelling errors in surrounding text
29
+
30
+ Expected behavior: Entity must remain unchanged.
31
+ ```
32
+
33
+ ### Dataset 9: Mixed Arabic-English (25 samples)
34
+
35
+ ```text
36
+ Categories:
37
+ - Technical text with English terms (استخدمت Python لبرمجة)
38
+ - Brand names embedded (يعمل على نظام Windows)
39
+ - Academic citations with English
40
+ - Code variables in Arabic context
41
+ - Email/URL with Arabic description
42
+
43
+ Expected behavior: English portions unchanged, Arabic corrected.
44
+ ```
45
+
46
+ ### Dataset 10: Structured Formats (20 samples)
47
+
48
+ ```text
49
+ Categories:
50
+ - JSON with Arabic values
51
+ - HTML with Arabic content
52
+ - Markdown with Arabic text
53
+ - CSV with Arabic columns
54
+ - XML/config files
55
+
56
+ Expected behavior: Structure preserved, Arabic within correctable.
57
+ ```
58
+
59
+ ### Dataset 11: Noisy Religious Text (20 samples)
60
+
61
+ ```text
62
+ Categories:
63
+ - Quran with missing diacritics
64
+ - Quran with hamza errors
65
+ - Truncated mid-verse fragments
66
+ - Mixed religious + regular text
67
+ - Hadith with common misspellings
68
+
69
+ Expected behavior:
70
+ - Clean quotes → no modification
71
+ - Quotes with errors → correction of errors only
72
+ - Structure preserved
73
+ ```
74
+
75
+ ### Dataset 12: Real User Samples (30 samples)
76
+
77
+ ```text
78
+ Sources:
79
+ - HF Spaces API logs (anonymized)
80
+ - Social media Arabic text (Twitter/X)
81
+ - Student essays
82
+ - Professional correspondence
83
+ - Academic writing
84
+
85
+ Expected behavior: Based on expert annotation.
86
+ ```
87
+
88
+ ### Dataset 13: Severity-Weighted Test Cases (20 samples)
89
+
90
+ ```text
91
+ Categories by severity:
92
+ - Critical: Data corruption (dates, numbers, URLs) — weight 5.0
93
+ - High: Meaning change (word substitution, tense change) — weight 3.0
94
+ - Medium: Grammar errors (agreement, case) — weight 2.0
95
+ - Low: Style issues (tanween, spacing) — weight 1.0
96
+
97
+ Expected behavior: Weighted pass rate replaces flat accuracy.
98
+ ```
99
+
100
+ ## Benchmark Infrastructure Changes
101
+
102
+ ### Severity Scoring
103
+
104
+ ```python
105
+ SEVERITY_WEIGHTS = {
106
+ 'data_corruption': 5.0,
107
+ 'meaning_change': 3.0,
108
+ 'grammar_error': 2.0,
109
+ 'style_issue': 1.0,
110
+ }
111
+
112
+ weighted_score = sum(w * pass for w, pass in results) / sum(weights)
113
+ ```
114
+
115
+ ### Regression Detection
116
+
117
+ Compare new results against baseline:
118
+ - Alert if any previously-passing test now fails
119
+ - Alert if weighted score drops > 1%
120
+
121
+ ### Coverage Metrics
122
+
123
+ Track which pipeline paths are exercised:
124
+ - Spelling only
125
+ - Grammar only
126
+ - Punctuation only
127
+ - Full pipeline
128
+ - Religious skip
129
+ - Structured protection
130
+ - StageLocker blocks
131
+
132
+ ## Implementation Timeline
133
+
134
+ | Week | Task |
135
+ |---|---|
136
+ | 1 | Create datasets 8-10 (entities, mixed, structured) |
137
+ | 2 | Create datasets 11-12 (religious, real user) |
138
+ | 3 | Implement severity scoring + regression detection |
139
+ | 4 | Run expanded benchmark, establish new baseline |
140
+
141
+ ## Total Expanded Benchmark
142
+
143
+ | Dataset | Current | New | Total |
144
+ |---|---|---|---|
145
+ | Spelling | 80 | 0 | 80 |
146
+ | Grammar | 45 | 0 | 45 |
147
+ | Punctuation | 20 | 0 | 20 |
148
+ | Entities | 30 | 30 | 60 |
149
+ | Religious | 30 | 20 | 50 |
150
+ | Structured | 35 | 20 | 55 |
151
+ | Hallucination | 30 | 0 | 30 |
152
+ | Mixed Ar-En | 0 | 25 | 25 |
153
+ | Real User | 0 | 30 | 30 |
154
+ | Severity | 0 | 20 | 20 |
155
+ | **Total** | **270** | **145** | **415** |
tests/phase11/telemetry_capture.py CHANGED
@@ -119,6 +119,7 @@ def run_telemetry_capture(url, max_samples=None):
119
  pipeline_output = result.get('corrected', text)
120
  suggestions = result.get('suggestions', [])
121
  timing = result.get('timing_ms', {})
 
122
  except Exception as e:
123
  print(f"ERROR: {e}")
124
  sample_results.append({
@@ -128,10 +129,7 @@ def run_telemetry_capture(url, max_samples=None):
128
  })
129
  continue
130
 
131
- # Small delay then fetch logs
132
- time.sleep(0.3)
133
- log_lines = fetch_logs(200)
134
- events = extract_telemetry_events(log_lines)
135
 
136
  # Write events for this sample
137
  sample_record = {
 
119
  pipeline_output = result.get('corrected', text)
120
  suggestions = result.get('suggestions', [])
121
  timing = result.get('timing_ms', {})
122
+ events = result.get('telemetry', []) # Phase 11: inline telemetry
123
  except Exception as e:
124
  print(f"ERROR: {e}")
125
  sample_results.append({
 
129
  })
130
  continue
131
 
132
+ # Events come directly from API response (no HF log parsing needed)
 
 
 
133
 
134
  # Write events for this sample
135
  sample_record = {