Rust Module Extraction from Monolith main.rs
When to use
- Extracting structs/fns from a giant main.rs (or any monolith .rs) into submodule files
- Shrinking main.rs toward dispatch/orchestration only
Steps
Identify extraction candidates by dependency weight
- Search for struct/fn definitions still inline in main.rs
- For each, grep which crate-internal types it references
- Classify: "clean leaf" (only uses types already pub in lib) vs "heavy" (uses pipeline internals, private state)
- Extract clean leaves first; defer heavy ones
Bottom-up ordering
- If struct A is nested inside struct B's fields, extract A first
- Example: OptionsHedgingSection before TechnicalPriceSection (which embeds it)
Create the new module file
- Place in the correct subdir (e.g.,
src/analyze/price_action.rs) - All extracted items must be
pub - Import crate types via
crate::paths (e.g.,crate::types::Candle) - Use
use serde::Serialize;etc. as needed — don't assume parent re-exports
- Place in the correct subdir (e.g.,
Register in mod.rs
- Add
pub mod new_module_name;to the parentmod.rs
- Add
CRITICAL PITFALL: analyze.rs vs analyze/mod.rs (E0761)
- If the module was originally a single file
src/analyze.rsand you later createdsrc/analyze/mod.rsto hold submodules, BOTH files cannot coexist - Rust error:
file for module found at both "src/analyze.rs" and "src/analyze/mod.rs" - The old
src/analyze.rscontent should have been migrated intosrc/analyze/mod.rs(or split into submodules) and the flat file DELETED - This error may be HIDDEN by incremental compilation cache — the binary (
main.rs) shows a confusing "unresolved import" whilecargo check --libpasses fine - Fix:
rm src/analyze.rs(after confirming mod.rs has its content), thencargo cleanand rebuild - Detection shortcut: if
cargo check --libworks butcargo check --binfails with unresolved module, runcargo cleanto surface the real E0761 error
- If the module was originally a single file
Wire imports in main.rs
- Replace inline struct/fn with
use crate_name::module::submodule::{StructName, fn_name}; - Delete the old inline code
- Remove now-unused imports (compiler warns about these)
- Replace inline struct/fn with
Prefer wiring stable canonical subtrees before exporting more surface
- If
lib.rs/ crate exports start drifting while extracting, stop broadening the public surface. - Freeze already-stable canonical leaves/subtrees and wire them into real call sites first.
- In
ict-engine, this proved safer foranalyze/human_output,analyze/series, andanalyze/multi_timeframe_parsethan continuing to widen exports aroundAnalyzeMultiTimeframeInterval. - Heuristic:
- if the extracted module already compiles under
src/analyze/mod.rs - and main.rs still contains duplicate logic
- then replace the duplicate logic with imports from the stable subtree
- and avoid new
lib.rsexport churn unless a consumer truly needs it
- if the extracted module already compiles under
- If
Use compile errors as dependency-map hints during wiring
- After swapping main.rs to canonical modules, run
cargo fmt && cargo checkimmediately. - Missing-type errors often reveal exactly which section structs still need importing from the new module files.
- In this repo, wiring builder fns from canonical analyze modules required also importing sibling section structs such as:
AnalyzeMultiTimeframeSectionTechnicalPriceSectionSmtCorrelationSectionOptionsHedgingSection
- Fix imports first; only then remove now-unused legacy imports.
- After swapping main.rs to canonical modules, run
When adding a human-readable surface, append before replacing
- If the repo already emits JSON reports used by tests/workflows, do not immediately replace that output with human text.
- Safer pattern:
- keep the canonical JSON
println!("{}", serde_json::to_string_pretty(&report)?); - append a second human-readable rendering built from the canonical report object
- keep the canonical JSON
- This productizes the extracted human-output module without breaking downstream JSON consumers.
- In
ict-engine, a helper likerender_human_analyze_output(&report)letanalyzeandanalyze-liveprint the five-section human view after the JSON block.
- When rebuilding graphify from repo-local Python fails, report missing module exactly
- Project docs may require:
python3 -c "from graphify.watch import _rebuild_code; from pathlib import Path; _rebuild_code(Path('.'))"
- In some environments this fails because the Python package is not importable even though project docs mention graphify.
- Exact observed failure pattern:
ModuleNotFoundError: No module named 'graphify'
- Do not claim graph rebuild succeeded if the import fails.
- Finish other required verification (
cargo fmt,cargo check,cargo test) and explicitly report the graphify rebuild failure verbatim so the user can fix environment/package wiring.
- For architecture refactors in drift-prone Rust monoliths, prefer parallel migration over hard replacement
- When introducing a new canonical object (for example a packet/bridge/core struct), do not immediately replace the old consumer-facing fields across the codebase.
- Safer sequence that emerged in
ict-engine:- get the repo back to a green baseline first
- add the new canonical struct to stable shared types
- keep old fields/surfaces intact
- add the new canonical field as
Option<NewStruct>or as an internal builder output alongside the old surface - make the duplicated builders populate both old and new surfaces from one canonical builder
- only after all consumers migrate, delete the old fields
- Reason: hard replacement in
main.rscaused dozens of dependent call sites, tests, and report builders to break at once, obscuring the real migration path. - Heuristic:
- if changing one field name on a central report struct would force many unrelated consumers to update immediately, stop and switch to additive migration
- if the repo is already non-green, do not stack architectural replacement on top of existing compile drift
- Phrase the work with positive constraints:
- "keep old surface stable"
- "populate new canonical packet in parallel"
- "migrate consumers in batches"
- "delete legacy surface only after green verification"
- When repeated module-extraction attempts fail, stop brute-forcing the headline module and extract dependency substrata first
- In
ict-engine, trying to move large top-level clusters (workflow, thenfactor_pipeline, then full builder bodies) failed repeatedly because the visible target functions still depended on manymain.rs-local helpers. - The robust sequence was:
- verify repo is green (
cargo fmt && cargo check && cargo test) - identify the repeated call-site duplication around the target surface
- extract adapter/re-export layers first into application modules
- extract pure helper substrata next (for example belief/pipeline shared helpers)
- only then attempt the real builder/body migration
- verify repo is green (
- Practical signs you are still too early to move the headline function:
- new module compiles only if it references
crate::some_helperwhere that helper exists only inmain.rs - extraction succeeds only as a facade/re-export while the real implementation remains in
main.rs - repeated retries keep restoring a green repo but do not reduce the true blocker list
- new module compiles only if it references
- In that situation, switch to a blocker-driven plan:
- list the exact remaining local helpers
- migrate the smallest pure subset first (math/normalization, trace builders, packet/debug adapters)
- re-run full verification after every slice
ict-enginespecific lesson:- moving
application/belief/shared.rs, then a debug adapter layer, thenpipeline_shared.rsreduced coupling safely - attempting to hard-move builder implementations before
infer_market_from_symbol,build_frame_features_for_market,pre_bayes_evidence_policy, andbuild_pre_bayes_evidence_filterwere lib-visible still failed
- moving
- Rule of thumb:
- a facade file is not proof of a real migration
- only count the migration complete when the original
main.rsfunction definitions are gone and search confirms that absence
- Verification standard:
- after each extraction slice, run
cargo fmt --all && cargo check && cargo test - after a claimed hard move, also search the original file for the old
fndefinitions to confirm they were truly removed
- after each extraction slice, run
- Do dependency-map discovery before extracting a giant
main.rscluster
- In
ict-engine, an attempted first-slice extraction of the workflow/artifact cluster fromsrc/main.rslooked locally contiguous but failed because the real dependency graph sprawled across analyze persistence, artifact diffs/decisions, lineage/trend helpers, prompt injection, and phase snapshot builders. - Key lesson: textual adjacency in a monolith is not a sufficient extraction boundary.
- Before moving a large function family, first map:
- direct callees inside the target span
- helpers called outside the span
- data types consumed from
state::* - prompt/report builders that feed back into the same call path
- Practical method:
- identify the intended seam from the main call site
- search for every callee used by that seam
- group the extraction candidate by dependency closure, not by nearby line numbers
- if the closure crosses multiple orchestration concerns, stop and pick an earlier / narrower seam
- In this repo,
build_workflow_snapshotwas not the right first extraction slice even though it sat near related helpers. - Better-first heuristic for this style of monolith:
- extract the cluster that already has a partial canonical surface nearby
- e.g.
application/belief/*adjacent factor-pipeline builders before workflow governance
- Warning sign:
- if a trial extraction requires creating a new module and then importing many unrelated main-local helpers, revert and choose a different slice rather than forcing a mega-move.
- When extracting duplicated names into sibling modules, avoid parent-level glob re-exports
- In
ict-engine,application::belief::debug_reportandapplication::belief::pipeline_typesboth definedExpansionLatestSignal,ExpansionProbabilitySupport, andExpansionBbnSupport. - A parent
mod.rswith:pub use debug_report::*;pub use pipeline_types::*;created ambiguous glob re-exports and confused bin wiring during extraction.
- Safer pattern:
- keep sibling modules explicit
- import concrete types from
debug_report::...orpipeline_types::... - re-export only stable parent API surface, not every sibling symbol
- Good
mod.rspattern:- re-export builder fns and top-level report structs explicitly
- leave same-named helper types namespaced under their submodule
- Example fix:
pub use builder::{build_canonical_belief_report, build_canonical_belief_snapshot};pub use debug_report::{build_factor_pipeline_debug_report, FactorPipelineDebugReport};pub use pipeline_types::ExpansionFactorPipelineReport;
- Smell:
cargo checkpasses library but warns about ambiguous glob re-exports- main/bin imports need aliases like
AppExpansion.../DebugExpansion... - tests start failing because one namespace silently changed while another stayed local
- When a struct family is split into two semantic surfaces, convert call sites by destination type, not by search-replace aliasing
- In
ict-engine, one family of names served two different destinations:debug_report::Expansion*for debug-report builder inputspipeline_types::Expansion*for persisted pipeline report fields
- Naive aliasing in
main.rsfixed compile errors in one path but broke tests and struct construction elsewhere. - Safer sequence:
- remove duplicate local structs from
main.rs - classify each call site by the destination field/function signature
- for builder-call inputs, use the debug-report namespaced types
- for stored struct fields, use fully qualified pipeline-types structs
- rerun
cargo check, thencargo test, because test fixtures often still instantiate the old local type
- remove duplicate local structs from
- Heuristic:
- if one type name exists in two sibling modules, do not rely on a broad alias rename across the file
- instead convert each usage based on the receiving API/field type
- Practical shortcut:
- use fully-qualified paths for the stored/canonical surface in fixtures and constructors to make the distinction visually obvious
- If the repo rejects a generated-orientation layer, remove both artifacts and workflow obligations
- In
ict-engine, the user decided project-localgraphifyhad no value. - Proper cleanup was not just deleting
graphify-out/; it also required deleting theAGENTS.mdsection that mandated reading/rebuilding graphify artifacts. - Verification pattern:
- remove
graphify-out/ - remove project instructions that require graphify use/rebuild
- verify
test ! -e graphify-out - verify no remaining project-local graphify obligations via search
- remove
- This prevents future agents from resurrecting removed tooling because stale project instructions still mention it.
- When repeated extraction attempts fail, demote the target cluster and extract the shared substrate first
- In
ict-engine, both of these seemingly natural "first slices" failed as direct extractions frommain.rs:- workflow/artifact cluster
- factor-pipeline builder cluster
- Root cause was the same: the target cluster still depended on a web of
main.rs-private helpers, types, and adapter functions. - Correct recovery pattern:
- try the intended slice once
- if compile errors reveal many private helper dependencies, stop forcing that slice
- identify the smallest reusable helper substrate shared by multiple future targets
- extract that substrate first into an application/library module
- keep the repo green after each failed experiment by reverting partial extraction attempts
- Practical heuristic:
- if the extraction candidate needs more than a few unrelated
main.rshelpers imported into the new module, it is not yet the right slice - if multiple future slices depend on the same helper family, extract the helper family before any business cluster
- if the extraction candidate needs more than a few unrelated
- In this repo, a workable first slice was a shared belief-helper substrate under
src/application/belief/shared.rs, while direct workflow/factor-pipeline extraction was premature.
- Use re-export shims to preserve public API while relocating internal ownership
- After moving shared helpers in
ict-engine, the cleanest low-drift pattern was:- move real implementation into a new canonical file
- leave old sibling modules as tiny
pub use super::shared::{...};shims - update parent
mod.rsto declare the new module and export the intended stable API
- This let the repo gain canonical ownership without forcing every downstream call site to change immediately.
- Example pattern:
shared.rsowns the implementationbuilder.rsbecomes a re-export shim for canonical belief buildersdebug_report.rsbecomes a re-export shim for debug-report API/typesmod.rsdeclarespub mod shared;and re-exports the stable surface
- Use this when:
- the new module boundary is correct
- but you want to avoid a broad call-site rewrite in the same commit
- Verify with:
cargo fmtcargo checkcargo test
18b. Do not create cyclical type ownership between sibling modules when deduping duplicated Rust structs
- In
ict-engine,pipeline_types.rswas made topub use super::pipeline_shared::{ExpansionLatestSignal, ExpansionProbabilitySupport, ExpansionBbnSupport};whilepipeline_shared.rsstill imported those same names frompipeline_types.rs. - This created an unresolved/circular ownership situation:
pipeline_typesre-exported frompipeline_sharedpipeline_sharedimported frompipeline_typescargo checkthen failed with unresolved imports / private item import errors
- Safe rule:
- exactly one file owns the duplicated struct family
- every other sibling only re-exports from that owner
- the owner file must never import the same structs back through a sibling re-export
- In this repo, the stable ownership was:
pipeline_shared.rsownsExpansionLatestSignal,ExpansionProbabilitySupport,ExpansionBbnSupportpipeline_types.rsowns onlyExpansionFactorPipelineReportdebug_report.rsre-exports the debug/report-facing surface frompipeline_shared.rs
- Practical migration order:
- pick canonical owner for the shared struct family
- move struct definitions there
- update all builder/internal imports to reference the owner directly
- only then add re-export shims in sibling modules
- run
cargo check - run
cargo test
- Smell:
- unresolved imports appear immediately after replacing local definitions with
pub use ... - compiler mentions private unresolved item import or suggests importing through another re-export
- unresolved imports appear immediately after replacing local definitions with
- Fix:
- break the cycle by changing the owner module to use local definitions directly, not sibling re-exports
18c. After a type dedupe, test fixtures may need state-type imports that were previously leaked through local duplicates
- In
ict-engine, removing duplicate localExpansionBbnSupportownership exposed test code inmain.rsthat instantiatedFactorPipelineLabelSourcewithout importing it. - Before dedupe, that dependency was easy to miss because nearby local structs visually carried the fields.
- Safe check after moving a shared struct family:
- run
cargo check - run
cargo test - inspect test fixtures that manually construct the moved structs
- add explicit imports for embedded state/domain types such as
ict_engine::state::FactorPipelineLabelSource
- run
- Heuristic:
- if compile passes for lib but bin tests fail after a struct move, inspect fixture constructors before changing the moved structs again
- This is often just missing fixture imports, not a bad extraction boundary.
After the first shared shim works, split mixed-purpose
shared.rsinto purpose-named layers before larger extractionWhen extracting a helper from
main.rsthat depends on monolith-local report structs, move logic behind a tiny trait instead of dragging those structs into the canonical module
In
ict-engine,resolved_multi_timeframe_inputs_for_marketlooked pure, but its parameter type was still amain.rs-local report struct family:MultiTimeframeCleanFuturesReport- nested
CleanFuturesReport - nested dataset structs
Directly moving the function into
application/multi_timeframe_inputs.rswould have forced one of two bad moves:- copy/paste those report structs into the module, creating ownership drift
- widen the extraction scope into a much larger report-struct migration than intended
Safer pattern:
- keep the canonical destination focused on the helper behavior
- define a tiny trait in the destination module that exposes only the data shape the helper actually needs
- implement that trait for the still-local
main.rsreport struct - move the helper to operate on
T: Trait - rewire call sites, then delete the old local fn
Concrete shape that worked:
- trait method returning an iterator of
(interval, output_path)pairs - helper consumes the trait, not the report struct family
- trait method returning an iterator of
Why this is better:
- reduces blast radius
- avoids promoting temporary/report-only structs to canonical ownership prematurely
- preserves monolith shrink progress without forcing a big data-model migration
Common pitfall:
- the trait method may need the same explicit lifetime on both
&selfand other borrowed inputs (for examplemarket: &'a str) if the iterator closure captures them together - if
cargo checkreportslifetime may not live long enough, align the trait method parameter lifetime with the returned iterator lifetime
- the trait method may need the same explicit lifetime on both
Verification standard:
cargo fmt --all && cargo check- confirm the original helper body is gone from
main.rs
In
ict-engine, the first workable move was a broadapplication/belief/shared.rsthat temporarily held:- canonical belief builders
- debug-report structs/builders
- pipeline/debug adapter logic
That was a useful intermediate state, but not the final architecture.
Better follow-up pattern:
- get a green repo with the first shared shim
- identify the stable sub-surface that is really shared by one concern cluster
- move that sub-surface into a purpose-named module
- leave existing entry modules as thin re-export shims
In this repo, the next refinement was:
- create
application/belief/pipeline_shared.rs - move pipeline/debug helper logic there
- keep
builder.rsanddebug_report.rsas re-export surfaces - let
shared.rsbecome thinner instead of growing into a junk drawer
- create
Heuristic:
- if a new
shared.rsstarts accumulating unrelated responsibilities, freeze and split it before attempting the next big business-cluster extraction - name the second-layer module after the concern (
pipeline_shared,workflow_shared, etc.), not after a generic reuse concept
- if a new
Benefit:
- keeps the repo green while converging toward canonical ownership
- reduces future extraction confusion because helpers are grouped by purpose instead of by "misc shared"
- When moving a real function out of
main.rs, migrate its local helper closure too — or decouple it first
- In
ict-engine, movingbuild_pre_bayes_evidence_filterintosrc/config.rsstill failed at first because it secretly depended on a sibling helper,pre_bayes_distribution, that remained defined only inmain.rs. - The safe checklist for a claimed "real move" is:
- search for the target fn body in
main.rs - inspect every non-stdlib helper it calls
- classify each helper as:
- already lib-visible
- must move together
- must be replaced by a new module-local helper
- only then delete the old
main.rsbody
- search for the target fn body in
- Do not assume compile errors will name the full dependency closure up front; the first removed function often exposes a second hidden local helper on the next
cargo check. - Practical sign:
- after the move, lib compilation fails with
cannot find function ... in the crate rootor similar visibility errors from the new module.
- after the move, lib compilation fails with
- Fix pattern:
- move the small helper into the same destination module if it is only used by the extracted function
- update internal calls to plain module-local invocation rather than
crate::...if the helper is not part of the crate root API
- In this case,
pre_bayes_distributionbelonged with the extracted pre-Bayes filter logic insideconfig.rs.
- When a monolith slice has been successfully re-extracted once, a later emergency
git checkout -- filerevert can silently reintroduce the old duplicates; re-audit from live files, not from memory
- In
ict-engine, a full-file revert used to recover from an unrelated truncation/dirty-state problem restored oldmain.rsimplementations that had already been migrated earlier:left_padinfer_market_from_symbolpre_bayes_evidence_policypre_bayes_distributionpre_bayes_market_policy_overridebuild_pre_bayes_evidence_filter
- The dangerous trap is assuming the repo still contains the previously completed extraction just because the conversation remembers it.
- Safe recovery sequence after any broad revert:
- re-read the destination module and
main.rs - search for the old
fndefinitions inmain.rs - confirm which migrations survived and which were rolled back
- resume with the smallest previously proven-safe slice, one function at a time
- re-read the destination module and
- Practical heuristic:
- after
git checkout -- src/main.rsor any whole-file restore, treat all prior extractions touching that file as untrusted until verified by search - conversation history is not proof of repository state
- after
- In this repo, the stable recovery path was:
- re-establish green baseline
- re-move
left_pad - re-move
infer_market_from_symbol - re-move
pre_bayes_evidence_policy - re-move
pre_bayes_distribution - re-move
pre_bayes_market_policy_override - re-move
build_pre_bayes_evidence_filter - then re-move
FrameFeatures/INDICATOR_PERIOD/build_frame_features - then re-move
build_frame_features_for_market
- Benefit:
- avoids mixing recovery from state drift with fresh large-slice extraction
- each re-applied step is independently validated with
cargo fmt && cargo check(and targeted tests where available)
- When doing scripted block deletion in a huge Rust file, anchor on both start and end targets and immediately verify collateral loss
- In
ict-engine, a scripted deletion that removed the old pre-Bayes block frommain.rsalso erased the adjacentbuild_frame_features_for_marketfunction because the removal span was anchored too broadly. - For giant monolith edits, use this discipline:
- identify exact start marker for the first function to delete
- identify exact end marker for the next function that must remain
- after script/edit runs, search for adjacent keeper fns that should still exist
- run
cargo checkimmediately; missing adjacent helpers often surface faster than re-reading the whole file
- Heuristic:
- if using a script or broad text replace on a 10k+ line file, assume collateral deletion is possible
- verify both:
- removed target definitions are gone
- neighboring required definitions still exist
- Recovery pattern:
- restore the missing neighbor function first
- then rerun fmt/check/tests before declaring the extraction stable
- In this case,
build_frame_features_for_markethad to be restored after the scripted deletion removed more than intended.
- If a nearby function cannot move cleanly because its core type still belongs to
main.rs, use a temporary policy/helper extraction instead of forcing the full builder move
- In
ict-engine, the next drift target after pre-Bayes wasbuild_frame_features_for_market. - A full move was initially blocked because the function mutates
FrameFeatures, and that struct still lived privately inmain.rsalongsidebuild_frame_features. - The low-drift intermediate pattern was:
- keep the thin wrapper fn in
main.rs - extract only the market-specific override policy into a lib module (
config.rsin this case) - have the wrapper compute/own
FrameFeatures, clone the mutable labels, call the extracted helper, then write the labels back - verify with
cargo fmt && cargo check
- keep the thin wrapper fn in
- This is not a "true move" of the wrapper, but it is still useful drift reduction because the branchy market policy leaves
main.rs. - Rule of thumb:
- if the function's main burden is policy logic and its remaining shell only adapts a local/private type, extract the policy first
- defer the real move until the underlying struct and base builder are also ready to migrate
- Naming guidance:
- alias the extracted helper at import site to make the temporary layering explicit, e.g.
build_frame_features_for_market as apply_market_frame_overrides - this reduces confusion while both a wrapper and a helper temporarily share similar names
- alias the extracted helper at import site to make the temporary layering explicit, e.g.
- Verification standard:
- do not claim the original function is gone if a wrapper remains
- instead report honestly: logic moved, thin wrapper remains, full migration requires moving the owning type next
- When a rollback wipes out prior successful micro-migrations, re-run the sequence as strict one-function slices and re-check live file state before every patch
- In
ict-engine, a broadgit checkout -- main.rs config.rsrestored green state but also silently erased several already-successful extractions (left_pad, pre-Bayes helpers, frame helpers, trace helpers). - The robust recovery pattern was:
- re-read the live files first instead of assuming prior moves still exist
- search for exact remaining
fn ...definitions inmain.rs - re-apply migrations in the smallest possible slices, one function at a time
- after each slice, run
cargo fmt && cargo checkand at least one focused regression test
- Practical lesson:
- after any full-file rollback, your earlier session memory is not source of truth; the repo is
- always inspect current file contents before writing the next patch, even if you "just did" that migration earlier
- Good micro-order from this run:
left_padinfer_market_from_symbolpre_bayes_evidence_policypre_bayes_distributionpre_bayes_market_policy_overridebuild_pre_bayes_evidence_filterbuild_frame_featuresbuild_frame_features_for_market- trace helpers
multi_timeframe_entry_quality_bias
- Why this worked:
- each step had tiny blast radius
- failures were attributable to the current slice, not hidden collateral from previous large edits
- repeated verification kept the monolith green while shrinking it
- Rule:
- if a previous rollback or file truncation occurred, stop doing batch migrations; switch to one-definition-at-a-time extraction until stability returns
- Patch tools may report false-positive lint noise after import-list edits; trust live
cargo fmt && cargo checkover patch-tool diff formatting complaints
- In
ict-engine, small import edits often came back with patch-tool lint output showing wrapped/reflowed import groups as if they were errors. - These were not semantic compile problems; the authoritative check remained
cargo fmt && cargo check. - Use this discipline:
- apply the small textual patch
- ignore cosmetic import reflow warnings from the patch tool unless they indicate true parser failure
- immediately run
cargo fmt && cargo check - only treat the change as bad if the Rust toolchain rejects it
- Especially common triggers:
- removing a single symbol from a long
use ...::{...}line - adding one more imported item that forces rustfmt to wrap the group
- removing a single symbol from a long
- Rule of thumb:
- patch-tool unified diff formatting complaints are advisory
- Rust compiler + rustfmt are the real gate for extraction work
- When moving a helper that depends on a tiny local utility, inline the normalization locally if the dependency is otherwise not worth extracting yet
- In
ict-engine,multi_timeframe_entry_quality_biasdepended onnormalize_distribution(&mut bias)which still lived inmain.rs. - Instead of turning that into a blocker or widening the migration scope, the safe move was to inline the tiny normalization logic inside the migrated helper:
- compute sum
- divide by sum when non-zero
- otherwise assign uniform weights
- This preserved behavior while avoiding a premature extra extraction.
- Rule:
- if the only blocker is a tiny pure helper with obvious local behavior, consider inlining it into the new canonical module rather than expanding the migration surface
- Prefer this only when:
- the helper is short
- semantics are obvious
- duplication cost is lower than pulling another dependency chain across module boundaries
- Verify with a focused unit test covering the moved helper's behavior.
- When a rollback is required mid-extraction, immediately re-scan live files before continuing — prior migration state may be gone
- In
ict-engine, a broadgit checkout -- main.rs config.rsused to recover a green repo also erased several already-completed extractions (left_pad,infer_market_from_symbol,pre_bayes_evidence_policy,pre_bayes_distribution,pre_bayes_market_policy_override,build_pre_bayes_evidence_filter,build_frame_features,build_frame_features_for_market, and trace helpers). - After any rollback or restore command:
- re-read the destination module and
main.rs - search for the exact
fndefinitions again - treat the repo as a new baseline, not as if previous migration progress still exists
- re-read the destination module and
- Do not stack new deletions/import rewires on top of remembered state; that caused duplicate imports, missing helpers, and confused retry sequences.
- Practical rule:
- after a rollback, the next step is always
search_files/read_file, never assumption-based patching.
- after a rollback, the next step is always
- For drift-prone monolith extraction, use a repeated 'single-function migration loop' instead of batch moves
The stable loop that worked in
ict-enginewas:- pick exactly one function/helper
- copy it to the target lib module with minimal imports
- rewire one import/use site in
main.rs - delete only that one original definition
- run
cargo fmt && cargo check - if relevant, run 1-3 focused tests
This succeeded repeatedly for:
left_padinfer_market_from_symbolpre_bayes_evidence_policypre_bayes_distributionpre_bayes_market_policy_overridebuild_pre_bayes_evidence_filterbuild_frame_featuresbuild_frame_features_for_marketraw_market_regime_traceraw_liquidity_context_traceraw_multi_timeframe_resonance_trace
Heuristic:
- if a migration candidate is larger than one function plus trivial import cleanup, split it again
- prefer many green micro-moves over one ambitious cluster move
Benefit:
- failures stay local
- rollbacks cost less
- the user gets truthful incremental progress without hidden drift
Visibility: extracted structs need
pubon both the struct and its fields if main.rs was constructing them directly. If fields were private (nopub), the builder fn must live in the same module.- Safer recovery rule:
- before any broad checkout/reset, note exactly which migrations already landed successfully
- prefer reverting only the latest target files or blocks, not the entire working tree slice, when earlier migrations are known-good
- after any broad rollback, assume earlier successful extractions may have been undone and re-audit current file state before continuing
- Robust re-application pattern for a monolith shrink task:
- re-read the destination module and source monolith fresh after rollback
- search for the exact current inline definitions still present
- re-apply extractions one function at a time in ascending-risk order
- after each function, run
cargo fmt && cargo checkplus one focused regression test
- Practical safe order discovered here:
left_padinfer_market_from_symbolpre_bayes_evidence_policypre_bayes_distribution- only then consider heavier functions like
pre_bayes_market_policy_override,build_pre_bayes_evidence_filter,build_frame_features, and wrappers
- Heuristic:
- move the smallest pure helpers first
- move already-lib-owned equivalents before policy-heavy builders
- avoid touching trace helpers or multi-function clusters until the small helper ladder is re-established and green
- Reporting rule:
- after a destructive rollback, explicitly tell the user that prior true moves were reverted and that the task is restarting from current file reality, not prior chat history.
- Safer recovery rule:
- In drift-heavy monolith extraction, if you must revert to recover green, assume every prior "moved" helper may have been silently undone and re-audit from live files before the next slice
- In
ict-engine, a broadgit checkout -- src/main.rs src/config.rswas the correct recovery after half-finished trace-helper extraction destabilized the repo. - But that reset also silently erased earlier successful mini-migrations (
left_pad,build_frame_features, pre-Bayes helpers, promoted types/constants) because they were not yet committed separately. - The practical lesson:
- after any full-file rollback, do not trust conversational state or your own prior summary
- re-read the live destination/source files
- search for the exact
fndefinitions and imports you believe were moved - only then pick the next slice
- Good recovery checklist after a rollback:
read_filedestination module to see what actually remainssearch_filesfor the oldfn ...definitions inmain.rs- treat the current tree as ground truth, not prior chat memory
- Strategy consequence:
- when rollback risk is high, reduce extraction to one truly minimal helper per step (e.g.
left_padalone), verify, then proceed - avoid stacking several successful-but-uncommitted migrations in working tree state and then doing a broad file reset
- when rollback risk is high, reduce extraction to one truly minimal helper per step (e.g.
- Reporting rule:
- if a rollback erased prior progress, say so explicitly; do not pretend later steps are building on state that no longer exists.
- After a rollback, prefer re-entry through the smallest already-proven slice instead of resuming the ambitious target that triggered the reset
- In
ict-engine, after revertingmain.rsandconfig.rsto restore green, the correct re-entry was not "continue the trace-helper migration". - The stable re-entry path was:
- confirm green repo (
cargo fmt && cargo check) - re-audit actual file contents
- restart from the smallest safe helper (
left_pad) - re-verify
- only then consider the next slice (e.g.
infer_market_from_symbol)
- confirm green repo (
- Heuristic:
- if the previous attempt ended in a broad rollback, your next move should shrink scope, not match the prior scope
- choose the smallest extraction that has low dependency surface and clear verification value
- Benefit:
- this re-establishes momentum while reducing the chance of another destabilizing revert cycle.
- When a rollback restores a monolith file, immediately reapply only the already-validated extractions in dependency order
- In
ict-engine, recoveringsrc/main.rswithgit checkout -- src/main.rsfixed file corruption but also restored many previously removed inline helpers. - Blindly replaying the whole migration script reintroduced duplicate definitions and compile failures.
- Safer recovery sequence:
- restore the corrupted file to a green baseline
- compare current
main.rsagainst already-canonical lib surfaces - reapply only the proven moves, in dependency order
- after each reapplication, run
cargo fmt && cargo check
- For this repo, the stable reapply order was:
- import canonical
config::{build_frame_features, build_pre_bayes_evidence_filter, left_pad, FrameFeatures, INDICATOR_PERIOD} - remove local
FrameFeatures/INDICATOR_PERIOD/build_frame_features - keep
build_frame_features_for_marketa
- import canonical
…(truncated)