Modularity Review

Scope: Entire ccgram codebase — fifth pass, 2026-04-16
Date: 2026-04-16 (v5 — final review of the refactoring series)

Executive Summary

ccgram is a single-process Python bot (~47k lines) that routes Telegram messages to AI coding agent CLIs running in tmux panes. Four refactoring passes have moved the overall modularity score from 4.8 to 6.3/10 (+1.5). The final pass — extracting window_query.py — was the structural breakthrough that broke through the 5.6 plateau: handler files importing SessionManager dropped from 30 to 15 (–50%), and call sites from 85 to 57 (–33%). The god-object is now split along its natural read/write boundary: window_query owns reads, session_lifecycle owns mutations, and SessionManager is retained only as the coordinator for writes, lifecycle operations, and session resolution.

The codebase has moved from "needs attention" to "healthy with known debt". No Significant or Critical issues remain. The three Minor issues — 15 handler files that still import SessionManager, 3 deferred-import cycles, and window_tick's 30 polling strategy calls — are all either at their natural floor or tolerable at low distance. The refactoring series is complete.

Five-Version Progression

Dimensionv1v2v3v4v5Net Δ
Encapsulation / Information Hiding45667+3
Cohesion55555
Coupling Discipline45556+2
Contract Stability66667+1
Testability56777+2
Volatility Alignment45556+2
Module Size Distribution66666
Dependency Direction45556+2
Overall4.85.45.65.66.3+1.5

What This Pass Changed

Metricv1 (baseline)v4 (pre-extraction)v5 (post-extraction)Total Δ
Handler files importing SessionManager303015–50%
session_manager.* call sites in handlers898557–36%
Fully decoupled handler files0014+14
Partially migrated handler files003+3

Architectural Pattern: Read/Write Boundary Split

ConcernModuleCoupling levelDepends on
Readswindow_query.pyContractwindow_state_store only
Hook-event mutationssession_lifecycle.pyContractclaude_task_state, session_manager
Poll-cycle mutationswindow_tick.pyFunctional (intentional)claude_task_state, polling_strategies
Transcript-parse mutationstranscript_reader.pyFunctional (intentional)claude_task_state
Writes + lifecyclesession.pyFunctionalFull state graph
Polling statereset_window_polling_state()Contractpolling_strategies internals

Coupling Overview

IntegrationStrengthDistanceVolatilityBalanced?
14 handler modules → window_query (read-only)ContractSame serviceHighYes — narrow read contract
15 handler modules → SessionManager (writes + lifecycle)FunctionalSame serviceHighTolerable — legitimate consumers
3 handler modules → both (partial migration)MixedSame serviceHighTolerable
hook_eventssession_lifecycle (5 facade calls)ContractSame serviceHighYes
window_tickpolling_strategies (30 calls)FunctionalSame service, low distanceHighTolerable
3 deferred-import cyclesFunctional (bidirectional)Same serviceHighNo — low priority
monitor_events.py, IdleTracker, event_reader, base.pyContractSame serviceLow/ModerateYes — design exemplars

Issue 1: 15 Handler Files Still Import SessionManager

Integration: 15 handler modules → session.py:SessionManager (57 call sites)  |  Severity: Minor — natural floor

Knowledge Leakage

The remaining 15 handler files import SessionManager because they genuinely need write, lifecycle, or query capabilities: 7 files call write methods, 4 call lifecycle methods, 4 call session resolution. These are functional coupling call sites that cannot be reduced to contract coupling without introducing artificial abstractions.

Recommended Improvement

Accept as the natural floor. The balance rule tolerates high strength at low distance. If a future pass targets this, the highest-leverage move is extracting set_window_provider (11 call sites across 7 files) into session_lifecycle.


Issue 2: Three Deferred-Import Cycles Remain

Integration: session.pysession_resolver.py, session_map.pywindow_state_store.py / thread_router.py  |  Severity: Minor

Knowledge Leakage

Three bidirectional dependency cycles survive, suppressed by deferred imports. The monitor_events.py extraction proved the fix pattern. Not blocking feature work.

Recommended Improvement

Address opportunistically. Extract shared types to dependency-free modules following the established pattern.


Issue 3: window_tick Has 30 Direct Polling Strategy Calls

Integration: window_tick.pypolling_strategies singletons  |  Severity: Minor — tolerable at low distance

Knowledge Leakage

window_tick.py is the poll cycle executor. Its 30 calls are high-strength functional coupling at very low distance. The balance rule tolerates this.

Recommended Improvement

Low priority. Group related transitions into compound methods on strategy objects if window_tick.py grows further.


Series Retrospective

Dimensionv1→v5What moved itCurrent ceiling
Encapsulation (4→7)+3window_query extraction, session_lifecycle write authority, reset_window_polling_state facade, injectable capture_fn15 legitimate SessionManager consumers
Coupling Discipline (4→6)+2Layer violation removed, cycle broken, read/write boundary split3 remaining import cycles
Testability (5→7)+2Injectable callables, monitor_events.py, window_query simplifies mockingshell_infra internal functions
Volatility Alignment (4→6)+2Mutations per trigger type, volatile handler reads now contract-coupledNatural floor
Dependency Direction (4→6)+2Inversion resolved, window_query depends only on window_state_storeclaude.py lazy tmux import
Contract Stability (6→7)+1WindowView, window_query functions, facadesProtocol-level contracts needed for 8+
Cohesion (5→5)Large files are legitimate
Module Size (6→6)Splitting adds complexity
v1 ──(+0.6)──▸ v2 ──(+0.2)──▸ v3 ──(+0.0)──▸ v4 ──(+0.7)──▸ v5
4.8            5.4            5.6            5.6            6.3

The lesson: incremental fixes converge to a local optimum; breaking through requires an architectural insight. In this case: "reads don't need the coordinator."