You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
processbehavior/analysis_result.py is 1572 LoC with a single AnalysisResult class carrying 46 methods plus a FocusedAnalysisResult subclass. The file has accumulated multiple unrelated responsibilities — tabular formatting, signal-detection orchestration, stratification, repr, dict protocol, plotting delegates — and is starting to show the same accretion pattern as the plotter's tick subsystem before its recent extraction.
Preemptive refactor: no specific recurring bug yet, but two recent independent edits both landed in focus() within 48 hours (the focused-stratum tick-label bug + the reset_index follow-up), and chart_table() carries a # noqa: C901 complexity suppression — the same hidden-debt signal that preceded the plotter rewrite. Pre-release status means decomposing now is cheap.
Public API is unchanged. Every existing method (chart_table, detect_signals, focus, plot, etc.) remains callable on AnalysisResult exactly as today. Only the implementation moves to focused modules; methods become thin delegates.
Goal
Split analysis_result.py into 4 focused modules so each has one responsibility:
build_chart_table() + build_signals_table() as pure functions
~250
result_signals.py (NEW)
detect_signals_for_result() as a pure orchestrator
~180
focused_result.py (NEW)
focus_on(result, stratum) + FocusedAnalysisResult class
~280
Net effect: same total LoC, distributed across single-responsibility modules. The four module entry points are pure-ish functions taking result as their first argument and returning the same types as today — directly unit-testable without instantiating the full class.
Non-Goals
No public API changes. result.chart_table(...), result.detect_signals(...), result.focus(...) work identically.
No methodology changes. Chart math, signal detection rules, statistics — untouched.
No removal of the dict protocol (__getitem__, keys, etc.) in this PR. Keep for backward compat; that's a separate decision.
The refactored AnalysisResult keeps every method on its public surface. Method bodies become 1-3 line delegates:
# in analysis_result.pyclassAnalysisResult:
defchart_table(self, chart=None, include_signal_col=True, signal_symbols=True):
from .result_tabularimportbuild_chart_tablereturnbuild_chart_table(self, chart=chart,
include_signal_col=include_signal_col,
signal_symbols=signal_symbols)
defdetect_signals(self, chart=None, rules=None, config=None, **kwargs):
from .result_signalsimportdetect_signals_for_resultreturndetect_signals_for_result(self, chart=chart, rules=rules,
config=config, **kwargs)
deffocus(self, stratum: str) ->AnalysisResult:
from .focused_resultimportfocus_onreturnfocus_on(self, stratum)
The lazy imports (inside the method body) keep analysis_result free of new top-level coupling and avoid any risk of a circular import with focused_result.py (which itself imports AnalysisResult for the subclass).
File changes — detailed
NEW: processbehavior/result_tabular.py (~250 LoC)
Move from analysis_result.py:707-925:
build_chart_table(result, *, chart, include_signal_col, signal_symbols) — pure function version of the current chart_table body (currently analysis_result.py:707-885). The # noqa: C901 goes away once the function is split into:
_extract_chart_type(chart_name) moves with it as a module-level private helper.
NEW: processbehavior/focused_result.py (~280 LoC)
Move from analysis_result.py:412-555 + 1442-1572:
focus_on(result, stratum) — pure function version of the focus() body. Includes the per-stratum boundary unpacking + reset_index from the recent Bug A fix.
class FocusedAnalysisResult(AnalysisResult) — moved verbatim. Backward-compat re-export at bottom of analysis_result.py:
from .focused_resultimportFocusedAnalysisResult# noqa: E402, F401
Move existing test_focus.py::TestFocusLaneBoundaries here alongside new tests for the extracted focus_on function. ~12 tests total. Keep test_focus.py for the public-API surface tests.
UNCHANGED tests
Every existing test using the public API continues to pass without modification:
tests/test_focus.py (19 tests) — result.focus(...) still works
tests/test_chart_parsing.py — result.chart_table(...) still works
Recent precedent: x-axis layout extraction in processbehavior/plotting/x_axis_layout.py (commits 754d2fd, 98513a6, 65c83c5, ff800f0) followed exactly this delegate-and-extract pattern with property tests at the layout level
Context
processbehavior/analysis_result.pyis 1572 LoC with a singleAnalysisResultclass carrying 46 methods plus aFocusedAnalysisResultsubclass. The file has accumulated multiple unrelated responsibilities — tabular formatting, signal-detection orchestration, stratification, repr, dict protocol, plotting delegates — and is starting to show the same accretion pattern as the plotter's tick subsystem before its recent extraction.Preemptive refactor: no specific recurring bug yet, but two recent independent edits both landed in
focus()within 48 hours (the focused-stratum tick-label bug + thereset_indexfollow-up), andchart_table()carries a# noqa: C901complexity suppression — the same hidden-debt signal that preceded the plotter rewrite. Pre-release status means decomposing now is cheap.Public API is unchanged. Every existing method (
chart_table,detect_signals,focus,plot, etc.) remains callable onAnalysisResultexactly as today. Only the implementation moves to focused modules; methods become thin delegates.Goal
Split
analysis_result.pyinto 4 focused modules so each has one responsibility:analysis_result.pyresult_tabular.py(NEW)build_chart_table()+build_signals_table()as pure functionsresult_signals.py(NEW)detect_signals_for_result()as a pure orchestratorfocused_result.py(NEW)focus_on(result, stratum)+FocusedAnalysisResultclassNet effect: same total LoC, distributed across single-responsibility modules. The four module entry points are pure-ish functions taking
resultas their first argument and returning the same types as today — directly unit-testable without instantiating the full class.Non-Goals
result.chart_table(...),result.detect_signals(...),result.focus(...)work identically.__getitem__,keys, etc.) in this PR. Keep for backward compat; that's a separate decision.mypy/lint cleanup beyond what the move requires — that's tracked in Tier 3: Maintenance polish — analysis.py refactor, vocabulary, mypy, coverage #82.Architecture
The refactored
AnalysisResultkeeps every method on its public surface. Method bodies become 1-3 line delegates:The lazy imports (inside the method body) keep
analysis_resultfree of new top-level coupling and avoid any risk of a circular import withfocused_result.py(which itself importsAnalysisResultfor the subclass).File changes — detailed
NEW:
processbehavior/result_tabular.py(~250 LoC)Move from
analysis_result.py:707-925:build_chart_table(result, *, chart, include_signal_col, signal_symbols)— pure function version of the currentchart_tablebody (currentlyanalysis_result.py:707-885). The# noqa: C901goes away once the function is split into:_resolve_value_col(result, chart_data)— value-column inference (currentlyanalysis_result.py:771-810)_extract_signal_column(chart_data, signal_symbols)— signal-symbol mappingbuild_chart_table(...)— orchestrator that calls bothbuild_signals_table(result, *, chart)— pure function version ofget_signals(currentlyanalysis_result.py:887-925).NEW:
processbehavior/result_signals.py(~180 LoC)Move from
analysis_result.py:1054-1198+1200-1225:detect_signals_for_result(result, *, chart, rules, config, **kwargs)— pure function version. Internal split:_build_config(rules, config, kwargs)— config construction_detect_for_chart(detector, result, chart_name, config)— per-chart detectiondetect_signals_for_result(...)— orchestrator_extract_chart_type(chart_name)moves with it as a module-level private helper.NEW:
processbehavior/focused_result.py(~280 LoC)Move from
analysis_result.py:412-555+1442-1572:focus_on(result, stratum)— pure function version of thefocus()body. Includes the per-stratum boundary unpacking +reset_indexfrom the recent Bug A fix.class FocusedAnalysisResult(AnalysisResult)— moved verbatim. Backward-compat re-export at bottom ofanalysis_result.py:MODIFIED:
processbehavior/analysis_result.py(1572 → ~700 LoC)Kept on
AnalysisResultclass:__init__,_build_summary,_is_stratifiedresiduals,effects,interactions,summary,has_residuals,has_effects,has_interactions,all_charts,strata,is_stratified_resolve_chart_name,get_chart,get_statistics,get_residual,list_strata,iter_charts__getitem__,__contains__,keys,values,items,__len__,__iter__,get__repr__,__str__plot,plot_residuals,plot_effects,reportto_excelchart_table,get_signals,detect_signals,focus(each becomes 1-3 lines)Removed (moved):
chart_table(707-885) →result_tabular.pyget_signals(887-925) →result_tabular.pydetect_signals(1054-1198) →result_signals.py_extract_chart_type(1200-1225) →result_signals.pyfocus(412-555) →focused_result.pyFocusedAnalysisResultclass (1442-1572) →focused_result.py(with re-export shim)NEW:
tests/test_result_tabular.pyUnit tests (~10):
chart=Noneuses first chartChartNotAvailableErrorspec.response_var, then known statistic columns, then first non-meta columnbeyond_limitsvaluesinclude_signal_col=Falseomits signal columnNEW:
tests/test_result_signals.pyUnit tests (~8):
chart=Nonereturns dict of all chartschartreturns singleSignalResultrules='standard'/'extended'/['rule_1']/RuleSet(...)all build correct configChartNotAvailableErrormetadataraisesProcessBehaviorErrorNEW:
tests/test_focused_result.pyMove existing
test_focus.py::TestFocusLaneBoundarieshere alongside new tests for the extractedfocus_onfunction. ~12 tests total. Keeptest_focus.pyfor the public-API surface tests.UNCHANGED tests
Every existing test using the public API continues to pass without modification:
tests/test_focus.py(19 tests) —result.focus(...)still workstests/test_chart_parsing.py—result.chart_table(...)still workstests/test_plotting*.py—result.plot(...)unchangedtests/test_analysis_*.py— accessors unchangedtests/test_signals*.py—result.detect_signals(...)unchangedMigration phases
Each phase commits independently with full pytest green.
Phase 1 —
result_tabular.py+ testsprocessbehavior/result_tabular.pywithbuild_chart_table,build_signals_table(verbatim move withresultas first arg)tests/test_result_tabular.pyanalysis_result.pyyet;pytest tests/test_result_tabular.pypassesrefactor(result): extract chart_table + signals_table into result_tabularPhase 2 — delegate
chart_table/get_signalsAnalysisResult.chart_tableandAnalysisResult.get_signalsto 1-3 line delegatesanalysis_result.pyrefactor(result): delegate chart_table / get_signals to result_tabularPhase 3 —
result_signals.py+ delegatedetect_signalsprocessbehavior/result_signals.pywithdetect_signals_for_result+_extract_chart_typetests/test_result_signals.pyAnalysisResult.detect_signalsto a delegate; delete old body +_extract_chart_typerefactor(result): extract detect_signals into result_signalsPhase 4 —
focused_result.py+ delegatefocusprocessbehavior/focused_result.pywithfocus_onandFocusedAnalysisResultanalysis_result.pyAnalysisResult.focusto a delegate; delete old body and inline classTestFocusLaneBoundariestotests/test_focused_result.pyrefactor(result): extract focus + FocusedAnalysisResult into focused_resultPhase 5 — verification
Invariants to Preserve
result.<method>(...)calls work identically with the same return types.FocusedAnalysisResultimportable fromanalysis_resultvia re-export shim.validation/e2e_bishop_report.py,e2e_T20_report.py) pass byte-for-byte.Risk Areas / Edge Cases
focused_resultimportsAnalysisResult;analysis_result.focuslazy-importsfocused_resultif TYPE_CHECKING:for type hints. Pattern already used by_build_summaryFocusedAnalysisResult— qualified class name changesanalysis_result.pykeepsanalysis_result.FocusedAnalysisResultresolvableresult._extract_chart_typeAnalysisResult.chart_tableEstimate
After-refactor improvement opportunities (out of scope here)
build_chart_tablecan drop the# noqa: C901once the inner helpers are extracted_extract_chart_typecan lose the "common chart type mapping" + prefix loop (25 LoC for what's a 2-linename.split('_')[0])__getitem__,keys, etc.) — documented "for backward compatibility" — could be deprecated in a future PRRelated
analysis.py,study.py,plotter.py,synthetic.py)processbehavior/plotting/x_axis_layout.py(commits754d2fd,98513a6,65c83c5,ff800f0) followed exactly this delegate-and-extract pattern with property tests at the layout level