Files
loki/docs/implementation/PHASE-1-STEP-2-NOTES.md
2026-04-10 15:45:51 -06:00

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 from impl 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

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 from impl Config, rewrote every Self::method() internal caller to use paths::method(), and removed the log::LevelFilter import that became unused after log_config moved away.
  • src/config/bridge.rs — no changes (bridge is unaffected by path migrations).
  • src/config/macros.rs — added use crate::config::paths;, migrated one Config::macros_dir().display() call.
  • src/config/agent.rs — added use crate::config::paths;, migrated 2 Config::agents_data_dir() calls, 4 agent_data_dir calls, 3 agent_config_file calls, 1 agent_rag_file call.
  • src/config/request_context.rs — no changes.
  • src/config/app_config.rs, app_state.rs — no changes.
  • src/main.rs — added use crate::config::paths;, migrated Config::log_config(), Config::list_roles(true), Config::list_rags(), Config::list_macros().
  • src/function/mod.rs — added use crate::config::paths;, migrated ~25 callsites across Config::config_dir, functions_dir, functions_bin_dir, global_tools_dir, agent_bin_dir, agent_data_dir, agent_functions_file, bash_prompt_utils_file. Removed Config from the use crate::{config::{...}} block because it became unused.
  • src/repl/mod.rs — added use crate::config::paths;, migrated Config::has_role(name) and Config::has_macro(name).
  • src/cli/completer.rs — added use crate::config::paths;, migrated Config::list_roles(true), Config::list_rags(), Config::list_macros().
  • src/utils/logs.rs — replaced use crate::config::Config; with use crate::config::paths; (Config was only used for log_path); migrated Config::log_path() call.
  • src/mcp/mod.rs — added use crate::config::paths;, migrated 3 Config::mcp_config_file().display() calls.
  • src/client/common.rs — added use crate::config::paths;, migrated Config::local_models_override(). Removed Config from the config::{Config, GlobalConfig, Input} import because it became unused.
  • src/client/oauth.rs — replaced use crate::config::Config; with use crate::config::paths; (Config was only used for token_file); migrated 2 Config::token_file calls.

Module registration

  • src/config/mod.rs — added pub(crate) mod paths; in the module declaration block, alphabetically placed between macros and prompts.

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-self helper (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:

  1. 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 text Edit for that cluster.
  2. &Self::X() reference expressions. ast-grep appeared to not match Self::X() when it was the operand of a & reference, presumably because the parent node shape was different. Plain text Edit handled 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 errors
  • cargo clippy — clean

Tests

  • cargo test63 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.rs exists and holds every static path helper plus log_config, list_*, has_*, and local_models_override
  • Zero Config::config_dir(), Config::cache_path(), etc. calls remain in the codebase
  • The #[allow(dead_code)] on paths.rs at 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
  • Config struct 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_url was 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 &self methods that only read serialized config state.
  • The vault_password_file field on AppConfig is pub(crate), not pub. The accessor method on AppConfig will need to encapsulate the same fallback logic that the Config method has (see src/config/mod.rs — it falls back to gman::config::Config::local_provider_password_file()).
  • print_markdown depends on render_options. When migrating them to AppConfig, preserve the dependency chain.
  • select_functions / select_enabled_functions / select_enabled_mcp_servers take a &Role parameter. Their new signatures on AppConfig will be &self, role: &Role — make sure Role is importable in the app_config.rs module (it currently isn't).
  • Strategy for the Step 3 migration: same as Step 2 — create methods on AppConfig, add #[deprecated] forwarders on Config, migrate callsites with ast-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 the set_* methods — those are "mixed" methods listed in Step 7, not Step 3.
  • Don't delete Config struct fields yet. Step 3 only moves methods that read fields; the fields themselves still exist on Config (and on AppConfig) 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 current AppConfig shape and decide where to put new methods
  • The current &self methods on Config in src/config/mod.rs that 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/