Skip to content

Commit 8f24f84

Browse files
author
fer
committed
refactor: split metrics.py into modular architecture (Phase 1, Task 4)
Intent: Improve maintainability by dividing large 2,146-line metrics module Operators involved: Code organization (no runtime operators affected) Affected invariants: #10 (Domain Neutrality - modular organization) Key changes: - Split src/tnfr/operators/metrics.py (2,146 lines) into 5 modules: * metrics_core.py: Shared utilities and helpers (89 lines) * metrics_basic.py: emission, reception, coherence, dissonance (500 lines) * metrics_network.py: coupling, resonance, silence + 4 helpers (693 lines) * metrics_structural.py: expansion, contraction, self_organization, mutation, transition, recursivity + helper (1,089 lines) * metrics.py: Facade for backward compatibility (61 lines) - Created scripts/split_metrics.py for automated refactoring - Preserved all public APIs (100% backward compatible) - Exported private helpers (_compute_*, _detect_*) for test compatibility Module organization: - Core: get_node_attr() + alias system + constants - Basic: Single-node structural operators (4 functions) - Network: Multi-node coupling/propagation operators (3 functions + 4 helpers) - Structural: Complex transformation operators (6 functions + 1 helper) - Facade: Re-exports all + U6 experimental telemetry Testing validation: - test_operator_enhancements.py: 23/23 passed ✅ - test_sha_extended_metrics.py: 33/33 passed ✅ - test_thol_metrics.py: Passed ✅ - test_nul_densification.py: Passed ✅ - test_mutation_metrics_comprehensive.py: Passed ✅ - Total: 99+ tests verified Benefits: - Improved navigation: Find operator metrics by category - Reduced cognitive load: ~400-700 lines per module vs 2,146 - Better testability: Can test modules independently - Clearer dependencies: Explicit imports between modules - Maintainability: Changes isolated to relevant module Expected risks/dissonances: None - all tests passing, APIs preserved Metrics before/after: - Src files: 255 → 259 (+4 new modules, 1 facade) - Test files: 492 (unchanged) - Health: 100/100 → 100/100 (maintained) ✅ - Imports: All backward compatible - Line distribution: 2,146 → 89+500+693+1,089+61 = 2,432 total (includes docstrings, __all__, imports in each module) Backup files: - metrics.py.backup: Pre-split copy - metrics.py.old: Renamed original after split TNFR Canonicity: Strengthens Invariant #10 (modular organization) without changing structural behavior. Task completion: Phase 1, Task 4 (4h, medium risk) ✅
1 parent 0f5918f commit 8f24f84

File tree

6 files changed

+2577
-2121
lines changed

6 files changed

+2577
-2121
lines changed

scripts/split_metrics.py

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
"""Script to split metrics.py into modular files.
2+
3+
This script divides src/tnfr/operators/metrics.py into 4 specialized modules
4+
plus a facade for backward compatibility.
5+
"""
6+
7+
# Read the original file
8+
with open("src/tnfr/operators/metrics.py", "r", encoding="utf-8") as f:
9+
lines = f.readlines()
10+
11+
# Define line ranges for each module (1-indexed, inclusive)
12+
ranges = {
13+
"metrics_basic.py": (73, 572), # emission, reception, coherence, dissonance
14+
"metrics_network.py": (573, 1093), # coupling, resonance, silence + helpers
15+
"metrics_structural.py": (1094, 2146), # expansion, contraction, self_org, mutation, transition, recursivity
16+
}
17+
18+
# Extract core imports and utilities (lines 1-72)
19+
header_lines = lines[0:72]
20+
21+
# Helper functions that need to be in network module (before silence_metrics)
22+
network_helpers_start = 861 # _compute_epi_variance
23+
network_helpers_end = 999 # _estimate_time_to_collapse
24+
25+
# Helper for transition (before transition_metrics)
26+
transition_helper_line = 2058 # _detect_regime_from_state
27+
28+
# Create each module
29+
for module_name, (start, end) in ranges.items():
30+
module_lines = []
31+
32+
# Add header
33+
module_lines.append(f'"""Operator metrics: {module_name.replace("metrics_", "").replace(".py", "")} operators."""\n')
34+
module_lines.append("\n")
35+
module_lines.append("from __future__ import annotations\n")
36+
module_lines.append("\n")
37+
module_lines.append("from typing import Any\n")
38+
module_lines.append("\n")
39+
module_lines.append("from .metrics_core import (\n")
40+
module_lines.append(" get_node_attr as _get_node_attr,\n")
41+
module_lines.append(" ALIAS_D2EPI,\n")
42+
module_lines.append(" ALIAS_DNFR,\n")
43+
module_lines.append(" ALIAS_EPI,\n")
44+
module_lines.append(" ALIAS_THETA,\n")
45+
module_lines.append(" ALIAS_VF,\n")
46+
module_lines.append(" HAS_EMISSION_TIMESTAMP_ALIAS as _HAS_EMISSION_TIMESTAMP_ALIAS,\n")
47+
module_lines.append(" EMISSION_TIMESTAMP_TUPLE as _ALIAS_EMISSION_TIMESTAMP_TUPLE,\n")
48+
module_lines.append(")\n")
49+
module_lines.append("from ..alias import get_attr_str\n")
50+
module_lines.append("\n")
51+
module_lines.append("\n")
52+
53+
# Add network helpers if this is metrics_network.py
54+
if module_name == "metrics_network.py":
55+
module_lines.extend(lines[network_helpers_start-1:network_helpers_end])
56+
module_lines.append("\n\n")
57+
58+
# Add transition helper if this is metrics_structural.py
59+
if module_name == "metrics_structural.py":
60+
# Add helper before transition_metrics
61+
module_lines.extend(lines[transition_helper_line-1:transition_helper_line+32]) # _detect_regime_from_state
62+
module_lines.append("\n\n")
63+
64+
# Add main function content
65+
module_lines.extend(lines[start-1:end])
66+
67+
# Remove U6 imports from structural module (already in main facade)
68+
if module_name == "metrics_structural.py":
69+
# Remove last ~16 lines (U6 fallback imports)
70+
module_lines = [l for l in module_lines if "metrics_u6" not in l and "measure_tau_relax" not in l and "measure_nonlinear" not in l and "compute_bifurcation_index" not in l]
71+
72+
# Write module
73+
with open(f"src/tnfr/operators/{module_name}", "w", encoding="utf-8") as f:
74+
f.writelines(module_lines)
75+
76+
print(f"✅ Created {module_name}")
77+
78+
# Create facade metrics.py
79+
facade_lines = []
80+
facade_lines.append('"""Operator metrics facade for backward compatibility."""\n')
81+
facade_lines.append("\n")
82+
facade_lines.append("from .metrics_basic import (\n")
83+
facade_lines.append(" emission_metrics,\n")
84+
facade_lines.append(" reception_metrics,\n")
85+
facade_lines.append(" coherence_metrics,\n")
86+
facade_lines.append(" dissonance_metrics,\n")
87+
facade_lines.append(")\n")
88+
facade_lines.append("from .metrics_network import (\n")
89+
facade_lines.append(" coupling_metrics,\n")
90+
facade_lines.append(" resonance_metrics,\n")
91+
facade_lines.append(" silence_metrics,\n")
92+
facade_lines.append(")\n")
93+
facade_lines.append("from .metrics_structural import (\n")
94+
facade_lines.append(" expansion_metrics,\n")
95+
facade_lines.append(" contraction_metrics,\n")
96+
facade_lines.append(" self_organization_metrics,\n")
97+
facade_lines.append(" mutation_metrics,\n")
98+
facade_lines.append(" transition_metrics,\n")
99+
facade_lines.append(" recursivity_metrics,\n")
100+
facade_lines.append(")\n")
101+
facade_lines.append("\n")
102+
facade_lines.append("# U6 experimental telemetry\n")
103+
facade_lines.append("try:\n")
104+
facade_lines.append(" from .metrics_u6 import (\n")
105+
facade_lines.append(" measure_tau_relax_observed,\n")
106+
facade_lines.append(" measure_nonlinear_accumulation,\n")
107+
facade_lines.append(" compute_bifurcation_index,\n")
108+
facade_lines.append(" )\n")
109+
facade_lines.append("except Exception:\n")
110+
facade_lines.append(" from typing import Any\n")
111+
facade_lines.append(" def measure_tau_relax_observed(*args: Any, **kwargs: Any) -> dict[str, Any]:\n")
112+
facade_lines.append(' return {"error": "metrics_u6 missing", "metric_type": "u6_relaxation_time"}\n')
113+
facade_lines.append(" def measure_nonlinear_accumulation(*args: Any, **kwargs: Any) -> dict[str, Any]:\n")
114+
facade_lines.append(' return {"error": "metrics_u6 missing", "metric_type": "u6_nonlinear_accumulation"}\n')
115+
facade_lines.append(" def compute_bifurcation_index(*args: Any, **kwargs: Any) -> dict[str, Any]:\n")
116+
facade_lines.append(' return {"error": "metrics_u6 missing", "metric_type": "u6_bifurcation_index"}\n')
117+
facade_lines.append("\n")
118+
facade_lines.append("__all__ = [\n")
119+
facade_lines.append(' "emission_metrics",\n')
120+
facade_lines.append(' "reception_metrics",\n')
121+
facade_lines.append(' "coherence_metrics",\n')
122+
facade_lines.append(' "dissonance_metrics",\n')
123+
facade_lines.append(' "coupling_metrics",\n')
124+
facade_lines.append(' "resonance_metrics",\n')
125+
facade_lines.append(' "silence_metrics",\n')
126+
facade_lines.append(' "expansion_metrics",\n')
127+
facade_lines.append(' "contraction_metrics",\n')
128+
facade_lines.append(' "self_organization_metrics",\n')
129+
facade_lines.append(' "mutation_metrics",\n')
130+
facade_lines.append(' "transition_metrics",\n')
131+
facade_lines.append(' "recursivity_metrics",\n')
132+
facade_lines.append(' "measure_tau_relax_observed",\n')
133+
facade_lines.append(' "measure_nonlinear_accumulation",\n')
134+
facade_lines.append(' "compute_bifurcation_index",\n')
135+
facade_lines.append("]\n")
136+
137+
# Rename original to .old
138+
import os
139+
os.rename("src/tnfr/operators/metrics.py", "src/tnfr/operators/metrics.py.old")
140+
141+
# Write facade
142+
with open("src/tnfr/operators/metrics.py", "w", encoding="utf-8") as f:
143+
f.writelines(facade_lines)
144+
145+
print("✅ Created metrics.py facade")
146+
print("✅ Renamed original to metrics.py.old")
147+
print("\n✨ Split complete! Run tests to verify.")

0 commit comments

Comments
 (0)