Compile-Time Isolation Refactor

June 6, 2026 ยท View on GitHub

This is the active migration plan for making full-feature debug/selfdev builds faster without removing features from the developer binary.

Goal

Keep the normal debug/selfdev binary production-like, including PDF, embeddings, providers, update/selfdev tooling, and other integrations, while reducing the amount of Rust code that must be recompiled after common edits.

The target is not just "more crates". The target is a wider dependency DAG with smaller serial front-end units and cleaner invalidation boundaries.

Current diagnosis

The workspace already has many crates, but the critical path is dominated by a small number of large crates stacked linearly:

graph LR
    base["jcode-base\n~100k+ LOC"] --> appcore["jcode-app-core\n~100k LOC"]
    appcore --> tui["jcode-tui\n~100k+ LOC"]
    tui --> rootlib["jcode lib"]
    rootlib --> bin["jcode bin"]
    small["50+ smaller crates"] -. mostly parallel .-> base

From the last available Cargo timing report parsed with scripts/compile_time_probe.sh --skip-build:

  • Cargo timing wall: 16.00s
  • Known jcode serial stack span: 14.72s
  • Known jcode serial stack summed unit time: 17.36s
  • Known jcode serial stack frontend time: 11.99s

Slowest units from that timing report:

UnitTotalFrontendCodegen
jcode-app-core4.73s3.82s0.91s
jcode-base4.34s3.63s0.71s
jcode-tui4.18s3.14s1.04s
jcode bin2.34sn/an/a
jcode lib1.77s1.40s0.37s

This means the main bottleneck is rustc front-end serialization in a few mega-crates, not linker choice or third-party cold compile.

Measurement

Use the focused timing probe for each phase:

scripts/compile_time_probe.sh --json target/compile-time-probe.json
scripts/compile_time_probe.sh --touch crates/jcode-tui/src/tui/app/input.rs
scripts/compile_time_probe.sh --touch crates/jcode-app-core/src/server.rs
scripts/compile_time_probe.sh --touch crates/jcode-base/src/provider/mod.rs

For broader repeated measurements, continue using:

scripts/bench_compile.sh selfdev-jcode --runs 3 --touch <path> --json
scripts/bench_selfdev_checkpoints.sh --skip-cold --touch <path> --runs 1

Track at least:

  1. Full-feature selfdev build wall time.
  2. Cargo timing wall time.
  3. jcode-base -> jcode-app-core -> jcode-tui -> jcode lib -> jcode bin stack span.
  4. Sum of frontend time in the serial stack.
  5. Incremental rebuild after touching representative high-churn files.
  6. Static report drift from scripts/compile_isolation_report.py: LOC, inline tests, async_trait, and target-state dependency advisories.

Target architecture

graph TD
    bin["jcode binary\ntiny composition root"] --> cli["jcode-cli"]
    bin --> tui["jcode-tui"]
    bin --> server["jcode-server"]
    bin --> providers["provider leaf crates"]
    bin --> tools["tool leaf crates"]

    cli --> api["jcode-client-api / app-api"]
    tui --> api
    server --> api

    api --> protocol["protocol + view models"]
    protocol --> types["small stable type crates"]

    server --> agent["jcode-agent"]
    server --> registry["jcode-tool-registry"]
    server --> auth["jcode-auth-core"]
    server --> session["jcode-session-core"]
    server --> memory["jcode-memory-core"]

    providers --> provider_core["jcode-provider-core"]
    tools --> tool_core["jcode-tool-core"]

Rules:

  • TUI and CLI depend on client API, protocol, view models, and small type crates, not full server/provider/tool implementations.
  • Provider implementations are leaf crates. AWS/Bedrock dependencies live only in the Bedrock provider crate.
  • Tool implementations are leaf crates. Heavy tools like PDF/browser/Gmail/search are isolated behind tool-core interfaces.
  • Shared bottom crates are small and stable. Avoid putting high-churn behavior in protocol/type crates.
  • Avoid broad pub use whole_crate::* compatibility ladders in final architecture.

Migration sequence

Phase 0: measurement and guardrails

Status: started.

Deliverables:

  • scripts/compile_time_probe.sh
  • scripts/compile_isolation_report.py
  • this document
  • dependency boundary checks/advisory reports

Success criteria:

  • Every structural phase has before/after timing.
  • The timing report makes the serial stack visible.

Phase 1: widen the god-crate critical path

Split the three long-pole crates into sibling domain crates. Priority is widening the graph, not extracting more tiny type crates.

Likely first splits:

  • From jcode-base:
    • jcode-auth-core
    • jcode-session-core
    • jcode-memory-core
    • provider implementation crates, especially Bedrock/AWS as a leaf
  • From jcode-app-core:
    • jcode-server
    • jcode-agent
    • jcode-tool-registry
    • service crates for background/swarm/update/selfdev as needed
  • From jcode-tui:
    • jcode-client-api / view-model boundary first
    • then move reusable client-side state logic out of the terminal rendering crate only when it creates a real parallel unit

Success criteria:

  • Touching common TUI code no longer recompiles app-core/provider/server implementation crates.
  • Touching a provider implementation no longer recompiles TUI or broad base code.
  • Cargo timing shows multiple medium-sized Jcode crates running in parallel instead of one 4-deep mega-crate ladder.

Phase 2: kill glob re-export ladders

Current compatibility layering preserves the old monolith shape:

pub use jcode_base::*;
pub use jcode_app_core::*;
pub use jcode_tui::*;

Migration approach:

  1. Keep compatibility re-exports temporarily while moving code.
  2. Convert high-churn modules to explicit imports from leaf crates.
  3. Remove glob re-exports once downstream imports are explicit.

Success criteria:

  • New code does not rely on whole-layer prelude-style re-exports.
  • Dependency direction is visible in imports and Cargo manifests.

Phase 3: move inline tests out of hot crates

Problem:

  • Inline #[cfg(test)] modules make cargo test compile large production crates plus large test bodies as one rustc unit.

Target:

  • Integration tests or dedicated *-test-support crates for broad behavior tests.
  • Keep tiny unit tests inline only when they are genuinely local and cheap.

Success criteria:

  • Targeted tests no longer require monolithic test cfg builds for unrelated domains.

Phase 4: reduce front-end macro tax

Targets:

  • Replace async_trait with native async fn in traits where the trait is not used as dyn.
  • Keep async_trait only at object-safe plugin/interface boundaries where boxed futures are intentional.
  • Avoid adding derive-heavy types to broad shared crates unless the type is stable and necessary.

Success criteria:

  • Fewer proc-macro expansions in the hot crates.
  • No object-safety regressions.

Anti-goals

  • Do not make fast debug builds incomplete by default.
  • Do not split code into tiny crates unless the split creates a real invalidation or parallelism boundary.
  • Do not move high-churn behavior into low-level type/protocol crates.
  • Do not do a single giant rewrite. Each phase should build and be measurable.

Validation checklist per phase

Before committing a phase:

scripts/compile_time_probe.sh --skip-build
scripts/compile_isolation_report.py
scripts/check_dependency_boundaries.py
cargo check --profile selfdev -p jcode --bin jcode

For code-moving phases, also run the relevant targeted tests for the moved domain, plus one full selfdev build through the coordinated selfdev path when practical:

selfdev build target=tui