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 |
-
#
|
| 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 = {
|