Skip to content

Epic: Codebase maintainability audit — structural refactors for long-term health #498

@dollspace-gay

Description

@dollspace-gay

Context

PR #482 (hub lock ownership refactor) demonstrated the pattern: identify code that lives in module A but actually manages state owned by module B, then move it to B with a clean entry point. That refactor was small (+118/−106) but significantly improved the hub system's maintainability.

This audit applies that same lens across the entire codebase (102k lines, 153 files). The goal is not to rewrite — it's to find every place where the code does the right thing but in a harder-to-maintain way, and plan incremental refactors.

Findings are grouped by severity and area. Each section is a potential sub-issue.


1. Stringly-Typed Domain Values (Pervasive)

Impact: Runtime bugs that should be compile-time errors. Every match on a string status/priority is a silent breakage vector.

Locations:

  • models.rsstatus: String, priority: String on Issue struct
  • trust_model.rsmodel: String ("local-only", "multi-tenant", "public-api")
  • issue_file.rs — comment kind: String validated against KNOWN_COMMENT_KINDS list
  • findings.rs / issue_filing.rsseverity: String throughout
  • token_usage.rs — model names matched with .contains() on lowercase
  • lock_check.rsread_auto_steal_config() accepts bool/number/string for same field

Fix: Extract enums: Status { Open, Closed }, Priority { Critical, High, Medium, Low }, CommentKind { Plan, Decision, Observation, ... }, Severity { Critical, High, Medium, Low, Info }, TrustModel { LocalOnly, MultiTenant, PublicApi }. Derive serde with #[serde(rename_all = "kebab-case")] for backward-compatible serialization.


2. N+1 Query Patterns (Production Risk)

Impact: Linear DB round-trips per issue. Slow at scale, invisible until list views hit 100+ issues.

2a. server/handlers/issues.rs:156-164list_issues()

For each issue in the result set, calls db.get_labels(id) and db.get_blockers(id) separately.
100 issues = 200 queries. Should be 2 batch queries returning HashMap<i64, Vec<_>>.

2b. server/handlers/issues.rs:246-282get_issue()

Calls 6 separate queries per issue detail view: labels, comments, blockers, blocking, subissues, milestone.
Fix: db.get_issue_detail(id) returning a hydrated struct in 1-2 queries using JOINs.

2c. Silent error swallowing

Same handlers use .unwrap_or_default() on all those queries — if the DB is corrupted or locked, the user gets empty data with a 200 status, not a 500 error.


3. main.rs Command Dispatch (2,782 lines)

Impact: Every new command requires edits in 3+ places. Hidden aliases duplicate argument definitions verbatim.

3a. Clap definition duplication

Commands::Create (lines 299-323) and IssueCommands::Create (lines 465-489) have identical field definitions — title, description, priority, template, label, work, defer_id, parent. Same for Quick, List, Show, Close. ~10 hidden aliases × ~8 fields each.

3b. Per-arm resource initialization

find_crosslink_dir() called ~50 times, get_db() ~45 times across match arms. No lazy caching, no shared initialization.

3c. Swarm nested match (lines 2625-2757)

29 SwarmCommands arms, every one follows the same pattern: extract crosslink_dir, call commands::swarm::<fn>(). Should be a single commands::swarm::dispatch(action, &crosslink_dir, json, quiet) call.

3d. Inline business logic

  • Heartbeat (lines 2504-2520): loads agent config, pushes heartbeat — should be in daemon::heartbeat()
  • Kickoff (lines 2586-2611): hardcodes default KickoffCommands::Launch fields that are already defined as clap defaults
  • Config (lines 2570-2576): bare vs. subcommand branching belongs in the config module

3e. JSON output branching

List, Search, Show commands all have if json { run_json() } else { run() } — repeated 3 times in dispatch_issue(). Should be handled at the command module level.

3f. External repo branching

List, Search, Show all copy-paste the same if let Some(repo) = repo { external_issues::... } else { local::... } pattern.

3g. Inconsistent param naming

Half the command enums use action: FooCommands, the other half use command: FooCommands. No rule for which.


4. init.rs Kitchen Sink (2,969 lines)

Impact: Changing Python detection requires reading through TUI rendering code. Five unrelated concerns in one file.

Distinct concerns that should be separate modules:

  1. Python toolchain detection (lines 26-210): detect_python_prefix(), cpitd_is_installed(), install_cpitd() — used by hooks/daemon too, not just init
  2. Git/signing configuration (lines 212-286): setup_driver_signing() — security setup orthogonal to project init
  3. TUI walkthrough (lines 956-1556, ~600 lines): InitWalkthroughApp, draw_init_walkthrough(), run_tui_walkthrough() — rendering logic that belongs in tui/
  4. Filesystem merging (lines 365-545): write_root_gitignore(), write_mcp_json_merged(), write_settings_json_merged() — JSON merge utilities reused by config.rs
  5. The run() function (lines 1598-2969, 1,370 lines): orchestrator that should delegate to the above

Proposed structure:

commands/init/
  mod.rs          (~300 lines, orchestration)
  python_env.rs   (~150 lines)
  signing.rs      (~80 lines)
  filesystem.rs   (~300 lines)
  tests.rs
tui/init_wizard.rs (~600 lines)

5. signing.rs Monolith (2,285 lines)

Impact: Five unrelated concerns in one file. Platform-specific branches make testing hard.

5a. Mixed concerns

  • SSH key generation + Unix chmod + Windows icacls (lines 48-153, 105-line function)
  • Git worktree detection (lines 246-281) — belongs in git-ops module
  • AllowedSigners lifecycle (lines 423-556) — file I/O + CRUD + validation
  • SSH signature pipeline (lines 566-762) — sign, verify, parse multi-format output
  • Platform helpers (lines 764-807) — home_dir_fallback(), make_temp_dir()

5b. Repeated subprocess pattern

Command::new("ssh-keygen") appears 4+ times, each with its own output checking and error context. No shared check_command_output() helper.

5c. AllowedSigners::parse() uses eprintln!() for validation errors

Can't suppress in silent mode, can't test for specific warnings. Should return Result or use tracing::warn!.

5d. generate_agent_key() — 105 lines

Platform branches for Unix/Windows permissions are 57 lines of the function. Extract set_key_permissions().


6. seam.rs Pipeline (2,149 lines)

Impact: Eight algorithmic concerns in one file. Hardcoded thresholds, Rust-specific detection mixed with language-agnostic collection.

6a. Eight concerns that should be separate modules

  1. File system traversal + collection (lines 127-193)
  2. Rust module boundary detection (lines 195-367) — only works for Rust
  3. Directory-based fallback partitioning (lines 372-401)
  4. Coverage validation + deduplication (lines 408-444)
  5. Git coupling analysis (lines 453-519) — subprocess + output parsing
  6. Graph-based partition merging with inlined union-find (lines 523-615)
  7. Size-based partition adjustment (lines 622-691)
  8. Partition count trimming (lines 763-803)

6b. Hardcoded thresholds with no configuration

MAX_PARTITION_LINES = 2000, MIN_PARTITION_LINES = 200, GIT_LOG_DEPTH = 200, COUPLING_THRESHOLD = 3.

6c. Inconsistent line counting

ensure_complete_coverage() creates _uncategorized partition with line_count = 0. split_partition() approximates per-file counts. Root path not threaded to all helpers.

6d. Label format is an implicit contract

Partitions use :: for modules, /a//b for splits, + for merges — no type safety.


7. TUI Duplication (8,718 lines across 7 files)

Impact: Adding a new list-based tab requires reimplementing navigation, search, detail scroll, and table rendering from scratch.

7a. Duplicated list rendering (~5 instances)

All tabs use identical vertical layout: header + content + footer. Table column construction, highlight styling, and status row formatting are copy-pasted.

7b. Duplicated input handlers (~21 occurrences of navigation pattern)

Down/j → selected + 1, Up/k → selected - 1 appears in every tab. Search input handling duplicated between issues_tab and knowledge_tab. Detail scroll duplicated in 4 tabs.

Fix: Extract ListNavigationHandler and SearchHandler traits.

7c. Hardcoded colors scattered

Color::Indexed(236) (highlight bg) appears 5 times. Priority/status colors duplicated.

7d. Inconsistent state management

IssuesTab blocks UI thread on DB load. AgentsTab uses background thread + mpsc. ConfigTab uses mixed approach. No shared BackgroundLoader<T>.

7e. TUI code in commands/

init.rs ~600 lines, config.rs ~300 lines, kickoff/wizard.rs 1,193 lines of TUI rendering in command modules.


8. compaction.rs Monolith (3,508 lines)

8a. apply() — duplicated event reduction (lines 318-518)

13 of 14 event types follow identical pattern: check exists → mutate → update timestamp → track change. Label/relation/dependency add/remove are near-identical pairs.

8b. compact() — too many responsibilities (lines 144-279)

Mixes: lock acquisition, watermark reading, event collection, sorting, state reset, reduction loop, materialization, skew detection, checkpoint writing.


9. Duplicated Code Across Modules

9a. Test setup boilerplate — 19 files

setup_test_db() copy-pasted into 19 command test modules.

9b. Jaccard similarity — 2 implementations

findings.rs and issue_filing.rs both implement Jaccard with different thresholds (0.5 vs 0.7).

9c. String truncation — 3 implementations

utils.rs::truncate(), utils.rs::truncate_slug(), kickoff/helpers.rs::slugify_with_max().

9d. JSON file read-or-empty — 3+ implementations

init.rs, config.rs, knowledge/operations.rs all implement the same pattern.

9e. Dynamic SQL construction — 3 instances

db/issues.rs, db/token_usage.rs (×2) build WHERE clauses with manual string concatenation. Extract QueryBuilder.


10. hydration.rs Complexity

10a. hydrate_to_sqlite() does too much (lines 74-359)

Clears data, deduplicates, hydrates 7 entity types, preserves SQLite-only issues, toggles FK constraints — all in one function.

10b. Three V1/V2 layout paths

read_locks(), read_locks_v2(), read_locks_auto() each reimplement error handling. Layout version not cached.

10c. Deduplication gaps

Groups by display_id but doesn't deduplicate by UUID.


11. shared_writer Brittleness

11a. emit_compact_push() — silent partial staging

If cache_dir/issues/ doesn't exist, staging silently does nothing.

11b. mutations.rsCell<i64> for closure capture

If closure never executes (error path), display_id.get() is 0 with no guard.

11c. hydrate_with_retry() — undifferentiated retry

No distinction between transient and permanent errors. Single retry, no backoff.


12. Sync Layer Risks

12a. Lock retry without jitter

Multiple agents retry simultaneously after push conflict → thundering herd.

12b. PID-based stale lock detection

PID wrapping: recycled PID could cause incorrect lock auto-removal.

12c. 30-second polling timeout

300 iterations at 100ms = effective spin-wait under contention.


13. Business Logic in Wrong Layers

13a. Graph traversal in DB layer

db/relations.rs — DFS cycle detection in same file as SQL queries. Should be utils::graph.

13b. Server handler does post-filtering

handlers/issues.rs — search returns all issues, handler filters in memory. Should be in DB.

13c. Relation normalization inconsistency

add_relation() normalizes (smaller ID first), get_related_issues() queries both directions.


14. Remaining Single-File Modules

  • knowledge/operations.rs (1,637 lines) → split into basic/sync/search
  • kickoff/launch.rs (706 lines) → split into worktree/git/container/agent
  • kickoff/monitor.rs (884 lines) → split into status/logs/report
  • kickoff/helpers.rs (697 lines) → distribute to appropriate utility modules

15. Minor Issues

  • external.rs: manual timeout loop for git_ls_remote_ok() instead of timeout crate
  • daemon.rs: no SIGTERM signal handling
  • clock_skew.rs: re-runs git log every detection cycle, no caching
  • Magic numbers: similarity thresholds (0.5, 0.7), stale timeouts (30, 60), severity mappings — hardcoded without rationale
  • tui/issues_tab.rs:1109 has private copy of format_issue_id() from utils.rs

Suggested Execution Order

Phase 1 — Quick wins (low risk, high value)

  • Extract setup_test_db() to shared test helper (19 files)
  • Consolidate truncation/slugification (3 impls → 1)
  • Extract JSON read-or-empty utility
  • Deduplicate Jaccard similarity
  • Introduce Status, Priority, CommentKind enums

Phase 2 — Structural refactors (medium risk)

  • Break init.rs into module directory
  • Move TUI code out of commands/ (init, config, kickoff wizards)
  • Split signing.rs into signing/ module directory
  • Split seam.rs into seam/ module directory
  • Refactor main.rs dispatch (delegate nested matches, deduplicate aliases)

Phase 3 — Performance & correctness

  • Fix N+1 queries in handlers/issues.rs (batch labels/blockers)
  • Add db.get_issue_detail() hydrated query
  • Replace .unwrap_or_default() with proper error propagation in handlers
  • Extract QueryBuilder for dynamic SQL
  • Add jitter to lock retry loops

Phase 4 — Deep refactors (higher risk)

  • Refactor compaction.rs apply() to declarative field operations
  • Split hydrate_to_sqlite() into per-entity functions
  • Unify V1/V2 layout code paths
  • Extract graph algorithms from db/relations.rs
  • Split knowledge/operations.rs, kickoff/launch.rs, kickoff/monitor.rs

This audit was performed on the develop branch at commit 83a59c14. Findings reflect the codebase as of 2026-03-21.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions