Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ADR-015: File-size threshold + modular split policy

Date: 2026-05-25 Status: Accepted Author: Rômulo Febasi


Context

src/mcp/server.rs crossed 870 lines during the v0.3 sequence (tool args + tool handlers + resource enumeration + prompt rendering

  • freshness enrichment + federation helpers, all in one file). The trajectory is clear: every new MCP feature lands here, and the file keeps growing. Beyond a certain point the file stops being readable: code review costs explode, regressions multiply, new contributors spend hours mapping responsibilities, and the temptation to “just add one more if” wins over proper layering.

The project already has clear architectural principles (BR-1.4 source-of-truth, BR-6.1 atomicity, the edge/core/data layering in ARCHITECTURE.md). What it lacks is an operational rule that triggers refactor before a file becomes a problem, not after.

Decision

Adopt a two-tier size threshold for source files:

TierLinesAction
Yellow500-800A reviewer must call out the file in the next PR that touches it. Refactor proposal welcome but not forced.
Red800+A refactor issue must be opened and linked from any PR that adds further code to that file. New code in a red file blocks merge unless it (a) already removes lines, or (b) is part of the refactor itself.

The threshold is heuristic, not dogmatic, a 600-line file of schemars-derived argument structs is fine; a 600-line file of braided business logic is not. Reviewers apply judgement informed by cohesion, not just line count.

Refactor playbook

When a file enters the red tier, the refactor follows this order:

  1. Inventory. List the responsibilities the file currently carries. One bullet per concern (e.g. for mcp/server.rs: tool arg shapes, tool handlers, resource list, resource read, prompt list, prompt render, freshness enrichment, federation routing, blocking-task helper).
  2. Group. Cluster the bullets into 3-5 cohesive modules. Each module exports a public surface that the others depend on; the inter-module graph must be a tree, not a cycle.
  3. Open an issue + ADR in the same milestone. The issue lists the new module tree; the ADR records the architectural intent so the next maintainer knows why the split landed where it did.
  4. Land in one commit per module where possible, smallest first. Each commit must pass cargo clippy --all-targets -- -D warnings and cargo test. No big-bang.
  5. Document. Update docs/ARCHITECTURE.md (module tree) and docs/CONTRIBUTING.md (file-size policy section) so the rule propagates beyond this ADR.

Architectural principles to respect

  • Single Responsibility per module: a file does one thing well.
  • Dependency Inversion at boundaries: handlers receive &AppDeps, never construct the concrete ServerHandler.
  • Use-case isolation: each MCP tool / each CLI command / each core operation lives in a file you can read without scrolling.
  • Edge / core / data layering remains the spine (ARCHITECTURE.md §1). Modular splits land within a layer; never across.

Consequences

  • + New code lands in small, reviewable files. Cohesion stays high without anyone having to remember the rule mid-PR.
  • + Onboarding gets cheaper: the module tree maps directly to the feature list.
  • + The “red tier blocks merge” rule prevents the death-by-1000- ifs failure mode that big files trend toward.
  • − More files to navigate. Mitigated by clear naming and the module tree in ARCHITECTURE.md.
  • − Some refactors will touch a lot of files at once. Mitigated by the “smallest first” landing order: bisect stays meaningful.
  • − The tiers are heuristic and will need revisiting. ADR supersedes itself when the project’s median file size shifts.

Immediate follow-up

A v0.4 issue tracks the mcp/server.rs split as the first beneficiary of this policy. Other current candidates (audit-listing in cli/audit.rs, the federation fan-out helpers) sit in yellow tier and stay as-is until the next PR touches them.

Playbook: what to do when the tier moves

The four-row patterns covered so far. Add a row when a new shape shows up.

SymptomPatternReference
A subcommand dispatcher (syn at, syn audit, syn ws, …) crosses 500 lines because the verbs keep growingConvert to a mod.rs + one file per verb under cli/<name>/. Helpers shared by ≥ 2 verbs go in a sibling <helpers>.rs. Helpers shared by 1 verb live with their caller.cli/at/ (SYN-152), cli/audit/ (SYN-153)
A macro-bound impl block (rmcp #[tool_router], #[tool_handler]) blows the budget because each new tool has to land in the same implDispatcher pattern: keep the #[tool] shim thin (3–15 lines, parse args → call core → wrap result); move all logic into core::<facet>::* modules outside the macro’s reach. When the impl ServerHandler block also has to leave the file, use #[tool_router(vis = "pub(crate)")] so its companion macro in a sibling module can still see the generated method.mcp/server.rs + core/mcp/* (SYN-151)
A use case grows because it gained an optional async branch (progress, cancellation, sampling)The async/transport-bound work stays in the shim at the transport boundary (it imports rmcp / tokio / signal types). The pure business work moves to core::<facet>::* returning Result<serde_json::Value> or a small Synapse type.core::mcp::{reindex,federated_search,replay_proposal} (SYN-151)
The yellow-tier file has tests that exercise an internal helperThe tests stay next to the helper they exercise (keep test files small too: same tiers apply). When the helper moves, the test moves with it.cli/at/tree.rs host of count_delta_between_two_commits (SYN-152)

Enforcement (SYN-154)

A CI job file-size (in .github/workflows/ci.yml) and the local .githooks/pre-push hook both run scripts/check-file-sizes.sh. Yellow tier emits a GitHub Actions warning annotation; red tier emits an error annotation and exits non-zero so the PR cannot merge until the file is split.

The lint exists because a human-only enforcement of this ADR was silently violated within two weeks of being written: SYN-150 reduced mcp/server.rs from 1380 → ~550, and by v0.4 ship two new MCP features had brought it back to 844. Automation is the fix that survives the next contributor.

Alternatives considered

  • Hard line-count gate enforced by CI (e.g. tokei + fail). Rejected: too rigid; punishes legitimate cases (long arg-struct files) and misses cohesion-based bloat.
  • No policy, refactor when it hurts. Rejected; that is the status quo and it just produced the 870-line mcp/server.rs. The threshold exists precisely so the conversation happens before pain.
  • Refactor on every PR that touches the file. Rejected: too expensive; the yellow tier is the right calibration: visible but not blocking.

References

  • src/mcp/server.rs: the file whose growth triggered this ADR.
  • docs/ARCHITECTURE.md §Module tree: primary documentation site for the resulting splits.
  • docs/CONTRIBUTING.md §File-size policy: contributor-facing summary of the same rule.