| """ |
| Phase 11 — Tasks 2, 3, 4, 6, 7: Analysis Suite |
| |
| Consumes telemetry.jsonl from Task 8 and produces all Phase 11 reports: |
| - Task 2: FN Root Cause Classification (grammar_fn_analysis) |
| - Task 3: G028 Deep Investigation (G028_root_cause) |
| - Task 4: Rejection Matrix (rejection_matrix) |
| - Task 6: StageLocker Audit (stagelocker_audit) |
| - Task 7: PatchSet Audit (patchset_audit) |
| - Filter Telemetry Report (filter_telemetry) |
| |
| Usage: |
| python tests/phase11/analysis_suite.py |
| |
| Requires: tests/phase11/artifacts/telemetry.jsonl (from telemetry_capture.py) |
| tests/phase10/reports/phase10_results.json (from benchmark_runner.py) |
| """ |
| import json |
| import os |
| import sys |
| import glob |
|
|
| PHASE11_DIR = os.path.dirname(os.path.abspath(__file__)) |
| ARTIFACTS_DIR = os.path.join(PHASE11_DIR, 'artifacts') |
| REPORTS_DIR = os.path.join(PHASE11_DIR, 'reports') |
| PHASE10_RESULTS = os.path.join(PHASE11_DIR, '..', 'phase10', 'reports', 'phase10_results.json') |
|
|
| os.makedirs(REPORTS_DIR, exist_ok=True) |
|
|
|
|
| def load_telemetry(): |
| """Load telemetry.jsonl""" |
| path = os.path.join(ARTIFACTS_DIR, 'telemetry.jsonl') |
| if not os.path.exists(path): |
| print(f"ERROR: {path} not found. Run telemetry_capture.py first.") |
| sys.exit(1) |
| records = [] |
| with open(path, 'r', encoding='utf-8') as f: |
| for line in f: |
| if line.strip(): |
| records.append(json.loads(line)) |
| return records |
|
|
|
|
| def load_phase10_results(): |
| """Load phase10 benchmark results.""" |
| path = os.path.abspath(PHASE10_RESULTS) |
| if not os.path.exists(path): |
| print(f"WARNING: {path} not found. Some analyses will be incomplete.") |
| return None |
| return json.load(open(path, encoding='utf-8')) |
|
|
|
|
| |
| |
| |
|
|
| def generate_filter_telemetry(records): |
| """Task 1: Measure grammar correction rejection by filter.""" |
| print("\n[Task 1] Generating filter telemetry...") |
|
|
| all_events = [] |
| for rec in records: |
| for evt in rec.get('telemetry_events', []): |
| evt['sample_id'] = rec['sample_id'] |
| evt['dataset'] = rec['dataset'] |
| all_events.append(evt) |
|
|
| |
| filter_counts = {} |
| accepted = 0 |
| total_diffs = 0 |
| raw_outputs = 0 |
| rejection_details = [] |
|
|
| for evt in all_events: |
| if evt.get('event') == 'grammar_diff': |
| total_diffs += 1 |
| elif evt.get('event') == 'grammar_raw_output': |
| raw_outputs += 1 |
| elif evt.get('event') == 'filter_reject': |
| f_name = evt.get('filter', 'Unknown') |
| filter_counts[f_name] = filter_counts.get(f_name, 0) + 1 |
| rejection_details.append(evt) |
| elif evt.get('event') == 'patch_accepted': |
| accepted += 1 |
|
|
| total_rejected = sum(filter_counts.values()) |
|
|
| |
| report = { |
| 'grammar_raw_outputs': raw_outputs, |
| 'grammar_diffs_generated': total_diffs, |
| 'accepted': accepted, |
| 'rejected': total_rejected, |
| 'rejection_by_filter': dict(sorted(filter_counts.items(), key=lambda x: -x[1])), |
| 'rejection_details': rejection_details, |
| } |
|
|
| json_path = os.path.join(REPORTS_DIR, 'filter_telemetry.json') |
| with open(json_path, 'w', encoding='utf-8') as f: |
| json.dump(report, f, indent=2, ensure_ascii=False) |
|
|
| |
| md_path = os.path.join(REPORTS_DIR, 'filter_telemetry.md') |
| with open(md_path, 'w', encoding='utf-8') as f: |
| f.write("# Grammar Filter Telemetry Report\n\n") |
| f.write("## Pipeline Funnel\n\n") |
| f.write(f"| Stage | Count |\n") |
| f.write(f"|---|---|\n") |
| f.write(f"| Grammar raw outputs | {raw_outputs} |\n") |
| f.write(f"| Diffs extracted | {total_diffs} |\n") |
| f.write(f"| **Accepted** | **{accepted}** |\n") |
| f.write(f"| **Rejected** | **{total_rejected}** |\n\n") |
| f.write("## Rejections by Filter\n\n") |
| f.write("| Filter | Rejections | % of Total |\n") |
| f.write("|---|---|---|\n") |
| for f_name, count in sorted(filter_counts.items(), key=lambda x: -x[1]): |
| pct = count / total_rejected * 100 if total_rejected > 0 else 0 |
| f.write(f"| {f_name} | {count} | {pct:.1f}% |\n") |
| f.write(f"\n## Rejection Details\n\n") |
| for det in rejection_details: |
| f.write(f"- **{det.get('filter')}**: `{det.get('original','')}` → `{det.get('correction','')}` (sample: {det.get('sample_id','')})\n") |
|
|
| print(f" Generated: {json_path}") |
| print(f" Generated: {md_path}") |
| print(f" Diffs: {total_diffs}, Accepted: {accepted}, Rejected: {total_rejected}") |
| return report |
|
|
|
|
| |
| |
| |
|
|
| def generate_fn_classification(records, p10_results): |
| """Task 2: Classify every grammar FN into a root cause category.""" |
| print("\n[Task 2] Generating FN root cause classification...") |
|
|
| |
| grammar_fns = [] |
| if p10_results: |
| for r in p10_results.get('results', []): |
| if r.get('dataset') == 'grammar' and r.get('pipeline_verdict') == 'FN': |
| grammar_fns.append(r) |
|
|
| |
| tel_by_id = {r['sample_id']: r for r in records} |
|
|
| classifications = [] |
| category_counts = {} |
|
|
| for fn in grammar_fns: |
| sid = fn['id'] |
| tel = tel_by_id.get(sid, {}) |
| events = tel.get('telemetry_events', []) |
|
|
| |
| classification = 'UNKNOWN' |
| evidence = '' |
|
|
| |
| raw_outputs = [e for e in events if e.get('event') == 'grammar_raw_output'] |
| diffs = [e for e in events if e.get('event') == 'grammar_diff'] |
| rejections = [e for e in events if e.get('event') == 'filter_reject'] |
| accepted = [e for e in events if e.get('event') == 'patch_accepted'] |
|
|
| if not raw_outputs: |
| classification = 'MODEL_FAILURE' |
| evidence = 'No grammar raw output event found' |
| elif not diffs: |
| |
| raw = raw_outputs[0] if raw_outputs else {} |
| if raw.get('input', '')[:100] == raw.get('output', '')[:100]: |
| classification = 'MODEL_FAILURE' |
| evidence = 'Grammar model returned input unchanged' |
| else: |
| classification = 'DIFF_EXTRACTION_FAILURE' |
| evidence = f"Model changed text but get_word_diffs() found 0 diffs" |
| elif rejections and not accepted: |
| |
| filter_names = [r.get('filter', '') for r in rejections] |
| if any(f == 'StageLocker' for f in filter_names): |
| classification = 'STAGELOCKER_FAILURE' |
| evidence = f"Rejected by StageLocker: {[r.get('original','') for r in rejections if r.get('filter')=='StageLocker']}" |
| else: |
| classification = 'FILTER_FAILURE' |
| evidence = f"Rejected by: {', '.join(set(filter_names))}" |
| elif accepted: |
| |
| classification = 'PATCH_FAILURE' |
| evidence = f"Grammar patch accepted but final output doesn't match expected" |
| else: |
| classification = 'UNKNOWN' |
| evidence = f"Events: {len(events)} total, {len(diffs)} diffs, {len(rejections)} rejections" |
|
|
| classifications.append({ |
| 'sample_id': sid, |
| 'input': fn.get('input', '')[:100], |
| 'expected': fn.get('expected', '')[:100], |
| 'pipeline_output': fn.get('pipeline_output', '')[:100], |
| 'classification': classification, |
| 'evidence': evidence, |
| 'filter_rejections': [r.get('filter', '') for r in rejections], |
| 'accepted_patches': len(accepted), |
| }) |
| category_counts[classification] = category_counts.get(classification, 0) + 1 |
|
|
| |
| json_path = os.path.join(REPORTS_DIR, 'grammar_fn_analysis.json') |
| with open(json_path, 'w', encoding='utf-8') as f: |
| json.dump({ |
| 'total_grammar_fn': len(grammar_fns), |
| 'category_counts': dict(sorted(category_counts.items(), key=lambda x: -x[1])), |
| 'classifications': classifications, |
| }, f, indent=2, ensure_ascii=False) |
|
|
| |
| md_path = os.path.join(REPORTS_DIR, 'grammar_fn_analysis.md') |
| with open(md_path, 'w', encoding='utf-8') as f: |
| f.write("# Grammar FN Root Cause Analysis\n\n") |
| f.write(f"**Total Grammar FN: {len(grammar_fns)}**\n\n") |
| f.write("## By Category\n\n") |
| f.write("| Category | Count | % |\n") |
| f.write("|---|---|---|\n") |
| for cat, cnt in sorted(category_counts.items(), key=lambda x: -x[1]): |
| pct = cnt / len(grammar_fns) * 100 if grammar_fns else 0 |
| f.write(f"| {cat} | {cnt} | {pct:.0f}% |\n") |
| f.write("\n## Detail\n\n") |
| for c in classifications: |
| f.write(f"### {c['sample_id']} — {c['classification']}\n") |
| f.write(f"- **Input**: `{c['input']}`\n") |
| f.write(f"- **Expected**: `{c['expected']}`\n") |
| f.write(f"- **Pipeline output**: `{c['pipeline_output']}`\n") |
| f.write(f"- **Evidence**: {c['evidence']}\n") |
| if c['filter_rejections']: |
| f.write(f"- **Filters**: {', '.join(c['filter_rejections'])}\n") |
| f.write("\n") |
|
|
| print(f" Generated: {json_path}") |
| print(f" Generated: {md_path}") |
| print(f" Total FN: {len(grammar_fns)}, Categories: {category_counts}") |
| return classifications |
|
|
|
|
| |
| |
| |
|
|
| def generate_g028_trace(records, p10_results): |
| """Task 3: Full lifecycle trace of G028.""" |
| print("\n[Task 3] Generating G028 deep investigation...") |
|
|
| |
| g028_tel = None |
| for r in records: |
| if r.get('sample_id') == 'G028': |
| g028_tel = r |
| break |
|
|
| |
| g028_p10 = None |
| if p10_results: |
| for r in p10_results.get('results', []): |
| if r.get('id') == 'G028': |
| g028_p10 = r |
| break |
|
|
| md_path = os.path.join(REPORTS_DIR, 'G028_root_cause.md') |
| with open(md_path, 'w', encoding='utf-8') as f: |
| f.write("# G028 Root Cause Investigation\n\n") |
|
|
| if not g028_tel: |
| f.write("> [!CAUTION]\n> G028 not found in telemetry data.\n\n") |
| print(" WARNING: G028 not found in telemetry") |
| return |
|
|
| f.write("## Input\n\n") |
| f.write(f"```\n{g028_tel.get('input', 'N/A')}\n```\n\n") |
|
|
| f.write("## Expected Output\n\n") |
| f.write(f"```\n{g028_tel.get('expected', 'N/A')}\n```\n\n") |
|
|
| f.write("## Pipeline Output\n\n") |
| f.write(f"```\n{g028_tel.get('pipeline_output', 'N/A')}\n```\n\n") |
|
|
| f.write(f"## Pass/Fail: {'✅ PASS' if g028_tel.get('passed') else '❌ FAIL'}\n\n") |
|
|
| |
| events = g028_tel.get('telemetry_events', []) |
| f.write("## Telemetry Events (in order)\n\n") |
| f.write("| # | Event | Details |\n") |
| f.write("|---|---|---|\n") |
| for i, evt in enumerate(events): |
| event_type = evt.get('event', '') |
| if event_type == 'grammar_raw_output': |
| f.write(f"| {i+1} | grammar_raw_output | input=`{evt.get('input','')[:60]}` output=`{evt.get('output','')[:60]}` |\n") |
| elif event_type == 'grammar_diff': |
| f.write(f"| {i+1} | grammar_diff | `{evt.get('original','')}` → `{evt.get('correction','')}` [{evt.get('start')}-{evt.get('end')}] |\n") |
| elif event_type == 'filter_reject': |
| f.write(f"| {i+1} | **REJECT** | **{evt.get('filter','')}**: `{evt.get('original','')}` → `{evt.get('correction','')}` |\n") |
| elif event_type == 'patch_accepted': |
| f.write(f"| {i+1} | patch_accepted | `{evt.get('original','')}` → `{evt.get('correction','')}` [{evt.get('start')}-{evt.get('end')}] |\n") |
| else: |
| f.write(f"| {i+1} | {event_type} | {json.dumps(evt, ensure_ascii=False)[:80]} |\n") |
|
|
| |
| if g028_p10: |
| f.write("\n## Phase 10 Benchmark Data\n\n") |
| f.write(f"- **Verdict**: {g028_p10.get('pipeline_verdict')}\n") |
| f.write(f"- **Root cause stage**: {g028_p10.get('root_cause_stage', 'N/A')}\n") |
| f.write(f"- **Root cause detail**: {g028_p10.get('root_cause_detail', 'N/A')}\n") |
| f.write(f"- **Suggestions**: {len(g028_p10.get('pipeline_suggestions', []))}\n") |
| for s in g028_p10.get('pipeline_suggestions', []): |
| f.write(f" - [{s.get('type')}] `{s.get('original','')}` → `{s.get('correction','')}` (conf={s.get('confidence',0)})\n") |
|
|
| |
| f.write("\n## Root Cause Determination\n\n") |
| raw_outputs = [e for e in events if e.get('event') == 'grammar_raw_output'] |
| diffs = [e for e in events if e.get('event') == 'grammar_diff'] |
| rejects = [e for e in events if e.get('event') == 'filter_reject'] |
| accepts = [e for e in events if e.get('event') == 'patch_accepted'] |
|
|
| if not raw_outputs: |
| f.write("**ROOT CAUSE: No grammar output** — Grammar model did not run or returned empty.\n") |
| elif raw_outputs: |
| raw = raw_outputs[0] |
| inp = raw.get('input', '') |
| out = raw.get('output', '') |
| if inp[:80] == out[:80]: |
| f.write("**ROOT CAUSE: MODEL_FAILURE** — Grammar model returned input unchanged. The model did not detect the error.\n") |
| elif not diffs: |
| f.write("**ROOT CAUSE: DIFF_EXTRACTION_FAILURE** — Model changed text but `get_word_diffs()` failed to extract diffs.\n") |
| f.write(f"\n- Model input: `{inp[:100]}`\n") |
| f.write(f"- Model output: `{out[:100]}`\n") |
| elif rejects and not accepts: |
| filters = set(r.get('filter', '') for r in rejects) |
| f.write(f"**ROOT CAUSE: FILTER_FAILURE** — Grammar model produced the correct fix but filters rejected it.\n") |
| f.write(f"\n- Rejected by: {', '.join(filters)}\n") |
| for r in rejects: |
| f.write(f"- `{r.get('original','')}` → `{r.get('correction','')}` (filter: {r.get('filter','')})\n") |
| elif accepts: |
| f.write("**ROOT CAUSE: PATCH_FAILURE or REBUILD_FAILURE** — Grammar patch was accepted but final output doesn't match expected.\n") |
| f.write("\nPossible causes:\n") |
| f.write("1. OffsetMapper corrupted patch coordinates during rebuild\n") |
| f.write("2. PatchSet conflict resolution dropped the patch\n") |
| f.write("3. Rebuild logic (accepted diffs → safe_grammar) lost the change\n") |
| else: |
| f.write("**ROOT CAUSE: UNKNOWN** — Insufficient telemetry data.\n") |
|
|
| print(f" Generated: {md_path}") |
|
|
|
|
| |
| |
| |
|
|
| def generate_rejection_matrix(records, p10_results): |
| """Task 4: Measure filter quality — correct vs incorrect rejections.""" |
| print("\n[Task 4] Generating rejection matrix...") |
|
|
| |
| p10_by_id = {} |
| if p10_results: |
| for r in p10_results.get('results', []): |
| p10_by_id[r['id']] = r |
|
|
| |
| filter_stats = {} |
|
|
| for rec in records: |
| sid = rec.get('sample_id', '') |
| p10 = p10_by_id.get(sid, {}) |
| expected_unchanged = (p10.get('expected', '') == p10.get('input', '')) |
| is_fp = p10.get('pipeline_verdict') == 'FP' |
| is_fn = p10.get('pipeline_verdict') == 'FN' |
|
|
| for evt in rec.get('telemetry_events', []): |
| if evt.get('event') != 'filter_reject': |
| continue |
|
|
| f_name = evt.get('filter', 'Unknown') |
| if f_name not in filter_stats: |
| filter_stats[f_name] = {'total': 0, 'correct': 0, 'incorrect': 0, 'details': []} |
|
|
| filter_stats[f_name]['total'] += 1 |
|
|
| |
| |
| |
| |
| if expected_unchanged: |
| |
| filter_stats[f_name]['correct'] += 1 |
| elif is_fn: |
| |
| |
| orig = evt.get('original', '') |
| corr = evt.get('correction', '') |
| expected = p10.get('expected', '') |
| if corr in expected and orig not in expected: |
| filter_stats[f_name]['incorrect'] += 1 |
| filter_stats[f_name]['details'].append({ |
| 'sample_id': sid, |
| 'original': orig, |
| 'correction': corr, |
| 'expected': expected[:80], |
| }) |
| else: |
| filter_stats[f_name]['correct'] += 1 |
| else: |
| |
| filter_stats[f_name]['correct'] += 1 |
|
|
| |
| md_path = os.path.join(REPORTS_DIR, 'rejection_matrix.md') |
| with open(md_path, 'w', encoding='utf-8') as f: |
| f.write("# Rejection Matrix\n\n") |
| f.write("> For every rejected grammar correction, determine whether rejection was correct or incorrect.\n\n") |
| f.write("| Filter | Total Rejections | Correct | Incorrect | Precision |\n") |
| f.write("|---|---|---|---|---|\n") |
| for f_name, stats in sorted(filter_stats.items(), key=lambda x: -x[1]['total']): |
| prec = stats['correct'] / stats['total'] * 100 if stats['total'] > 0 else 0 |
| f.write(f"| {f_name} | {stats['total']} | {stats['correct']} | {stats['incorrect']} | {prec:.0f}% |\n") |
|
|
| total_all = sum(s['total'] for s in filter_stats.values()) |
| correct_all = sum(s['correct'] for s in filter_stats.values()) |
| incorrect_all = sum(s['incorrect'] for s in filter_stats.values()) |
| prec_all = correct_all / total_all * 100 if total_all > 0 else 0 |
| f.write(f"| **TOTAL** | **{total_all}** | **{correct_all}** | **{incorrect_all}** | **{prec_all:.0f}%** |\n") |
|
|
| |
| f.write("\n## Incorrect Rejections (Valid Corrections Blocked)\n\n") |
| has_incorrect = False |
| for f_name, stats in sorted(filter_stats.items(), key=lambda x: -x[1]['incorrect']): |
| for det in stats['details']: |
| has_incorrect = True |
| f.write(f"- **{f_name}** ({det['sample_id']}): `{det['original']}` → `{det['correction']}` (expected: `{det['expected']}`)\n") |
| if not has_incorrect: |
| f.write("None detected — all rejections appear correct.\n") |
|
|
| print(f" Generated: {md_path}") |
| print(f" Total rejections: {total_all}, Correct: {correct_all}, Incorrect: {incorrect_all}") |
|
|
|
|
| |
| |
| |
|
|
| def generate_stagelocker_audit(records, p10_results): |
| """Task 6: Audit StageLocker lock/block behavior.""" |
| print("\n[Task 6] Generating StageLocker audit...") |
|
|
| stagelocker_blocks = [] |
| total_locks = 0 |
|
|
| for rec in records: |
| for evt in rec.get('telemetry_events', []): |
| if evt.get('event') == 'filter_reject' and evt.get('filter') == 'StageLocker': |
| stagelocker_blocks.append({ |
| 'sample_id': rec.get('sample_id', ''), |
| 'dataset': rec.get('dataset', ''), |
| 'original': evt.get('original', ''), |
| 'correction': evt.get('correction', ''), |
| }) |
| if evt.get('event') == 'patch_accepted': |
| total_locks += 1 |
|
|
| |
| grammar_blocks = len(stagelocker_blocks) |
| |
|
|
| md_path = os.path.join(REPORTS_DIR, 'stagelocker_audit.md') |
| with open(md_path, 'w', encoding='utf-8') as f: |
| f.write("# StageLocker Audit\n\n") |
| f.write("## Statistics\n\n") |
| f.write(f"| Metric | Value |\n") |
| f.write(f"|---|---|\n") |
| f.write(f"| Total locks created (est.) | {total_locks} |\n") |
| f.write(f"| Grammar blocks | {grammar_blocks} |\n") |
| f.write(f"\n## Grammar Blocks Detail\n\n") |
| if stagelocker_blocks: |
| f.write("| Sample | Original | Correction |\n") |
| f.write("|---|---|---|\n") |
| for b in stagelocker_blocks: |
| f.write(f"| {b['sample_id']} | `{b['original']}` | `{b['correction']}` |\n") |
| else: |
| f.write("No StageLocker blocks detected in grammar stage.\n") |
|
|
| print(f" Generated: {md_path}") |
| print(f" Total locks: {total_locks}, Grammar blocks: {grammar_blocks}") |
|
|
|
|
| |
| |
| |
|
|
| def generate_patchset_audit(records, p10_results): |
| """Task 7: Audit PatchSet conflict resolution.""" |
| print("\n[Task 7] Generating PatchSet audit...") |
|
|
| total_patches = 0 |
| total_conflicts = 0 |
| ownership_by_stage = {} |
|
|
| if p10_results: |
| for r in p10_results.get('results', []): |
| suggestions = r.get('pipeline_suggestions', []) |
| for s in suggestions: |
| stage = s.get('type', 'unknown') |
| ownership_by_stage[stage] = ownership_by_stage.get(stage, 0) + 1 |
| total_patches += 1 |
|
|
| |
| if p10_results: |
| for r in p10_results.get('results', []): |
| suggestions = r.get('pipeline_suggestions', []) |
| for i, s1 in enumerate(suggestions): |
| for j, s2 in enumerate(suggestions): |
| if i >= j: |
| continue |
| if s1.get('start', 0) < s2.get('end', 0) and s1.get('end', 0) > s2.get('start', 0): |
| if s1.get('type') != s2.get('type'): |
| total_conflicts += 1 |
|
|
| md_path = os.path.join(REPORTS_DIR, 'patchset_audit.md') |
| with open(md_path, 'w', encoding='utf-8') as f: |
| f.write("# PatchSet Audit\n\n") |
| f.write("## Statistics\n\n") |
| f.write(f"| Metric | Value |\n") |
| f.write(f"|---|---|\n") |
| f.write(f"| Total patches generated | {total_patches} |\n") |
| f.write(f"| Total cross-stage conflicts | {total_conflicts} |\n") |
| f.write(f"\n## Patch Ownership by Stage\n\n") |
| f.write("| Stage | Patches |\n") |
| f.write("|---|---|\n") |
| for stage, count in sorted(ownership_by_stage.items(), key=lambda x: -x[1]): |
| f.write(f"| {stage} | {count} |\n") |
|
|
| print(f" Generated: {md_path}") |
| print(f" Total patches: {total_patches}, Conflicts: {total_conflicts}") |
|
|
|
|
| |
| |
| |
|
|
| def main(): |
| print("Phase 11 Analysis Suite") |
| print("=" * 60) |
|
|
| records = load_telemetry() |
| p10_results = load_phase10_results() |
|
|
| print(f"Loaded {len(records)} telemetry records") |
| if p10_results: |
| print(f"Loaded phase10 results: {len(p10_results.get('results', []))} samples") |
|
|
| |
| tel_report = generate_filter_telemetry(records) |
| fn_class = generate_fn_classification(records, p10_results) |
| generate_g028_trace(records, p10_results) |
| generate_rejection_matrix(records, p10_results) |
| generate_stagelocker_audit(records, p10_results) |
| generate_patchset_audit(records, p10_results) |
|
|
| print(f"\n{'='*60}") |
| print(f"All reports generated in: {REPORTS_DIR}") |
| print(f"{'='*60}") |
|
|
|
|
| if __name__ == '__main__': |
| main() |
|
|