15 KiB
Phase 1 Step 2 — Implementation Notes
Status
Done.
Plan reference
- Plan:
docs/PHASE-1-IMPLEMENTATION-PLAN.md - Section: "Step 2: Migrate static methods off Config"
Summary
Extracted 33 static (no-self) methods from impl Config into a new
src/config/paths.rs module and migrated every caller across the
codebase. The deprecated forwarders the plan suggested as an
intermediate step were added, used to drive the callsite migration,
and then deleted in the same step because the migration was
mechanically straightforward with ast-grep and the forwarders
became dead immediately.
What was changed
New files
src/config/paths.rs(~270 lines)- Module docstring explaining the extraction rationale and the (transitional) compatibility shim pattern.
#![allow(dead_code)]at module scope because most functions were briefly dead during the in-flight migration; kept for the duration of Step 2 and could be narrowed or removed in a later cleanup (see "Follow-up" below).- All 33 functions as free-standing
pub fns, implementations copied verbatim fromimpl Config:- Path helpers:
config_dir,local_path,cache_path,oauth_tokens_path,token_file,log_path,config_file,roles_dir,role_file,macros_dir,macro_file,env_file,rags_dir,functions_dir,functions_bin_dir,mcp_config_file,global_tools_dir,global_utils_dir,bash_prompt_utils_file,agents_data_dir,agent_data_dir,agent_config_file,agent_bin_dir,agent_rag_file,agent_functions_file,models_override_file - Listing helpers:
list_roles,list_rags,list_macros - Existence checks:
has_role,has_macro - Config loaders:
log_config,local_models_override
- Path helpers:
Modified files
Migration touched 14 source files — all of src/config/mod.rs's
internal callers, plus every external Config::method() callsite:
src/config/mod.rs— removed the 33 static-method definitions fromimpl Config, rewrote everySelf::method()internal caller to usepaths::method(), and removed thelog::LevelFilterimport that became unused afterlog_configmoved away.src/config/bridge.rs— no changes (bridge is unaffected by path migrations).src/config/macros.rs— addeduse crate::config::paths;, migrated oneConfig::macros_dir().display()call.src/config/agent.rs— addeduse crate::config::paths;, migrated 2Config::agents_data_dir()calls, 4agent_data_dircalls, 3agent_config_filecalls, 1agent_rag_filecall.src/config/request_context.rs— no changes.src/config/app_config.rs,app_state.rs— no changes.src/main.rs— addeduse crate::config::paths;, migratedConfig::log_config(),Config::list_roles(true),Config::list_rags(),Config::list_macros().src/function/mod.rs— addeduse crate::config::paths;, migrated ~25 callsites acrossConfig::config_dir,functions_dir,functions_bin_dir,global_tools_dir,agent_bin_dir,agent_data_dir,agent_functions_file,bash_prompt_utils_file. RemovedConfigfrom theuse crate::{config::{...}}block because it became unused.src/repl/mod.rs— addeduse crate::config::paths;, migratedConfig::has_role(name)andConfig::has_macro(name).src/cli/completer.rs— addeduse crate::config::paths;, migratedConfig::list_roles(true),Config::list_rags(),Config::list_macros().src/utils/logs.rs— replaceduse crate::config::Config;withuse crate::config::paths;(Config was only used forlog_path); migratedConfig::log_path()call.src/mcp/mod.rs— addeduse crate::config::paths;, migrated 3Config::mcp_config_file().display()calls.src/client/common.rs— addeduse crate::config::paths;, migratedConfig::local_models_override(). RemovedConfigfrom theconfig::{Config, GlobalConfig, Input}import because it became unused.src/client/oauth.rs— replaceduse crate::config::Config;withuse crate::config::paths;(Config was only used fortoken_file); migrated 2Config::token_filecalls.
Module registration
src/config/mod.rs— addedpub(crate) mod paths;in the module declaration block, alphabetically placed betweenmacrosandprompts.
Key decisions
1. The deprecated forwarders lived for the whole migration but not beyond
The plan said to keep #[deprecated] forwarders around while
migrating callsites module-by-module. I followed that approach but
collapsed the "migrate then delete" into a single step because the
callsite migration was almost entirely mechanical — ast-grep with
per-method patterns handled the bulk, and only a few edge cases
(Self::X inside &-expressions, multi-line format! calls)
required manual text edits. By the time all 33 methods had zero
external callers, keeping the forwarders would have just generated
dead_code warnings.
The plan also said "then remove the deprecated methods" as a distinct
phase, and that's exactly what happened — just contiguously with the
migration rather than as a separate commit. The result is the same:
no forwarders in the final tree, all callers routed through
paths::.
2. paths is a pub(crate) module, not pub
I registered the module as pub(crate) mod paths; so the functions
are available anywhere in the crate via crate::config::paths::X
but not re-exported as part of Loki's public API surface. This
matches the plan's intent — these are internal implementation
details that happen to have been static methods on Config. If
anything external needs a config path in the future, the proper
shape is probably to add it as a method on AppConfig (which goes
through Step 3's global-read migration anyway) rather than exposing
paths publicly.
3. log_config stays in paths.rs despite not being a path
log_config() returns (LevelFilter, Option<PathBuf>) — it reads
environment variables to determine the log level plus falls back to
log_path() for the file destination. Strictly speaking, it's not
a "path" function, but:
- It's a static no-
selfhelper (the reason it's in Step 2) - It's used in exactly one place (
main.rs:446) - Splitting it into its own module would add complexity for no benefit
The plan also listed it in the migration table as belonging in
paths.rs. I followed the plan.
4. #![allow(dead_code)] at module scope, not per-function
I initially scoped the allow to the whole paths.rs module because
during the mid-migration state, many functions had zero callers
temporarily. I kept it at module scope rather than narrowing to
individual functions as they became used again, because by the end
of Step 2 all 33 functions have at least one real caller and the
allow is effectively inert — but narrowing would mean tracking
which functions are used vs not in every follow-up step. Module-
level allow is set-and-forget.
This is slightly looser than ideal. See "Follow-up" below.
5. ast-grep was the primary migration tool, with manual edits for awkward cases
ast-grep --pattern 'Config::method()' and
--pattern 'Self::method()' caught ~90% of the callsites cleanly.
The remaining ~10% fell into two categories that ast-grep handled
poorly:
- Calls wrapped in
.display()or.to_string_lossy(). Some ast-grep patterns matched these, others didn't — the behavior seemed inconsistent. When a pattern found 0 matches but grep showed real matches, I switched to plain textEditfor that cluster. &Self::X()reference expressions.ast-grepappeared to not matchSelf::X()when it was the operand of a&reference, presumably because the parent node shape was different. Plain textEdithandled these without issue.
These are tooling workarounds, not architectural concerns. The
final tree has no Config::X or Self::X callers for any of the
33 migrated methods.
6. Removed Config import from three files that no longer needed it
src/function/mod.rs, src/client/common.rs, src/client/oauth.rs,
and src/utils/logs.rs all had use crate::config::Config; (or
similar) imports that became unused after every call was migrated.
I removed them. This is a minor cleanup but worth doing because:
- Clippy flags unused imports as warnings
- Leaving them in signals "this file might still need Config" which future migration steps would have to double-check
Deviations from plan
1. sync_models is not in Step 2
The plan's Step 2 table listed sync_models(url, abort) as a
migration target, but grep showed only sync_models_url(&self) -> String exists in the code. That's a &self method, so it belongs
in Step 3 (global-read methods), not Step 2.
I skipped it here and will pick it up in Step 3. The Step 2 actual count is 33 methods, not the 34 the plan's table implies.
2. Forwarders deleted contiguously, not in a separate sub-step
See Key Decision #1. The plan described a two-phase approach ("leave forwarders, migrate callers module-by-module, then remove forwarders"). I compressed this into one pass because the migration was so mechanical there was no value in the intermediate state.
Verification
Compilation
cargo check— clean, zero warnings, zero errorscargo clippy— clean
Tests
cargo test— 63 passed, 0 failed (same as Step 1 — no new tests were added because Step 2 is a pure code-move with no new behavior to test; the existing test suite verifies nothing regressed)
Manual smoke test
Not applicable — Step 2 is a pure code-move. The path computations are literally the same code at different call sites. If existing tests pass and nothing references Config's static methods anymore, there's nothing to manually verify beyond the compile.
Callsite audit
cargo check 2>&1 | grep "Config::\(config_dir\|local_path\|...\)"
Returns zero matches. Every external Config::method() callsite
for the 33 migrated methods has been converted to paths::method().
Handoff to next step
What Step 3 can rely on
Step 3 (migrate global-read methods to AppConfig) can rely on:
src/config/paths.rsexists and holds every static path helper pluslog_config,list_*,has_*, andlocal_models_override- Zero
Config::config_dir(),Config::cache_path(), etc. calls remain in the codebase - The
#[allow(dead_code)]onpaths.rsat module scope is safe to remove at any time now that all functions have callers AppConfig(from Step 0) is still fully populated and ready to receive method migrations- The bridge from Step 1 (
Config::to_app_config,to_request_context,from_parts) is unchanged and still works Configstruct has no more static methods except those that were kept because they DO take&self(vault_password_file,messages_file,sessions_dir,session_file,rag_file,state, etc.)- Deprecation forwarders are GONE — don't add them back
What Step 3 should watch for
sync_models_urlwas listed in the Step 2 plan table as static but is actually&self. It's a Step 3 target (global-read). Pick it up there.- The Step 3 target list (from
PHASE-1-IMPLEMENTATION-PLAN.md):vault_password_file,editor,sync_models_url,light_theme,render_options,print_markdown,rag_template,select_functions,select_enabled_functions,select_enabled_mcp_servers. These are all&selfmethods that only read serialized config state. - The
vault_password_filefield onAppConfigispub(crate), notpub. The accessor method onAppConfigwill need to encapsulate the same fallback logic that theConfigmethod has (seesrc/config/mod.rs— it falls back togman::config::Config::local_provider_password_file()). print_markdowndepends onrender_options. When migrating them toAppConfig, preserve the dependency chain.select_functions/select_enabled_functions/select_enabled_mcp_serverstake a&Roleparameter. Their new signatures onAppConfigwill be&self, role: &Role— make sureRoleis importable in theapp_config.rsmodule (it currently isn't).- Strategy for the Step 3 migration: same as Step 2 — create
methods on
AppConfig, add#[deprecated]forwarders onConfig, migrate callsites withast-grep, delete the forwarders. Should be quicker than Step 2 because the method count is smaller (10 vs 33) and the pattern is now well- established.
What Step 3 should NOT do
- Don't touch
paths.rs— it's complete. - Don't touch
bridge.rs— Step 3's migrations will still flow through the bridge's round-trip test correctly. - Don't try to migrate
current_model,extract_role,sysinfo, or any of theset_*methods — those are "mixed" methods listed in Step 7, not Step 3. - Don't delete
Configstruct fields yet. Step 3 only moves methods that read fields; the fields themselves still exist onConfig(and onAppConfig) in parallel until Step 10.
Files to re-read at the start of Step 3
docs/PHASE-1-IMPLEMENTATION-PLAN.md— Step 3 section (table of 10 global-read methods and their target signatures)- This notes file — specifically the "What Step 3 should watch for" section
src/config/app_config.rs— to see the currentAppConfigshape and decide where to put new methods- The current
&selfmethods onConfiginsrc/config/mod.rsthat are being migrated
Follow-up (not blocking Step 3)
1. Narrow or remove #![allow(dead_code)] on paths.rs
At Step 2's end, every function in paths.rs has real callers, so
the module-level allow could be removed without producing warnings.
I left it in because it's harmless and removes the need to add
per-function allows during mid-migration states in later steps.
Future cleanup pass can tighten this.
2. Consider renaming paths.rs if its scope grows
log_config, list_roles, list_rags, list_macros, has_role,
has_macro, and local_models_override aren't strictly "paths"
but they're close enough that extracting them into a sibling module
would be premature abstraction. If Steps 3+ add more non-path
helpers to the same module, revisit this.
3. The Config::config_dir deletion removes one access point for env vars
The config_dir() function was also the entry point for XDG-
compatible config location discovery. Nothing about that changed —
it still lives in paths::config_dir() — but if Step 4+ needs to
reference the config directory from code that doesn't yet import
paths, the import list will need updating.
References
- Phase 1 plan:
docs/PHASE-1-IMPLEMENTATION-PLAN.md - Step 1 notes:
docs/implementation/PHASE-1-STEP-1-NOTES.md - New file:
src/config/paths.rs - Modified files (module registration + callsite migration): 14
files across
src/config/,src/function/,src/repl/,src/cli/,src/main.rs,src/utils/,src/mcp/,src/client/