Research – Issue #313: Core → Utils Layer Violation
Date: 2026-01-27
Owner: Claude (research agent)
Phase: Research
Issue: #313 Layer violation: core → utils (18 imports, skips Layer 2)
Goal
Document all core → utils imports, categorize by fix strategy, and provide a migration plan to eliminate the layer violation where core (Layer 1) imports directly from utils (Layer 3), skipping Layer 2 (tools/indexing/lsp).
Architecture Reference
ui (Layer 0)
↓ only
core (Layer 1)
↓ only
tools | indexing | lsp (Layer 2)
↓ only
utils (Layer 3)
Foundation (exempt): types, configuration, constants
Findings
Current Import Analysis
Total: 24 imports across 13 files
| Core File |
Utils Module |
Imports |
Count |
core/system_paths.py:5-11 |
utils.system.paths |
check_for_updates, delete_session_file, get_project_id |
3 |
core/agents/resume/sanitize_debug.py:11 |
utils.messaging |
_get_attr, _get_parts, get_tool_call_ids, get_tool_return_ids |
4 |
core/agents/resume/summary.py:19 |
utils.messaging |
estimate_tokens |
1 |
core/agents/resume/prune.py:11 |
utils.messaging |
estimate_tokens |
1 |
core/agents/resume/sanitize.py:20 |
utils.messaging |
_get_attr, _get_parts, find_dangling_tool_calls, to_canonical_list |
4 |
core/agents/agent_components/agent_config.py:24-25 |
utils.config, utils.limits |
load_config, get_max_tokens |
2 |
core/agents/agent_components/orchestrator/tool_dispatcher.py:108,149 |
utils.parsing |
parse_args, parse_tool_calls_from_text, has_potential_tool_call |
3 |
core/agents/main.py:31 |
utils.ui |
DotDict |
1 |
core/file_filter.py:5 |
utils.ui.file_filter |
FileFilter |
1 |
core/formatting.py:5,8 |
utils.formatting |
truncate_diagnostic_message, MAX_DIAGNOSTIC_MESSAGE_LENGTH |
2 |
core/user_configuration.py:6-10 |
utils.config |
load_config_with_defaults, save_config, UserConfigStateManager |
3 |
core/state.py:19,98,185,291,337 |
utils.messaging, utils.config, utils.system.paths |
estimate_tokens, get_content, load_config_with_defaults, get_session_storage_dir |
4 |
core/messaging.py:7 |
utils.messaging |
get_content |
1 |
Utils Modules Dependency Analysis
| Utils Module |
Dependencies |
Layer Assessment |
utils/system/paths.py |
configuration.settings, constants |
Foundation-eligible |
utils/messaging/ |
types.canonical only |
Foundation-eligible (type adapters) |
utils/config/ |
configuration.settings, exceptions, types |
Foundation-eligible |
utils/limits.py |
constants, utils.config |
Foundation-eligible (after config moves) |
utils/parsing/ |
constants, exceptions, types |
Tool-specific (Layer 2) |
utils/ui/helpers.py |
None |
Inline candidate |
utils/ui/file_filter.py |
constants, utils.system.ignore_patterns |
Infrastructure |
utils/formatting.py |
None |
Inline candidate |
Existing Facade Pattern
Core already has facade files that re-export from utils:
core/messaging.py → re-exports get_content
core/formatting.py → re-exports truncate_diagnostic_message
core/user_configuration.py → re-exports config functions
core/system_paths.py → re-exports path utilities
core/file_filter.py → re-exports FileFilter
Pattern uses underscore-prefixed imports:
from tunacode.utils.messaging import get_content as _get_content
Key Patterns / Solutions Found
Fix Strategy Categories
1. Move to Foundation (configuration/) - 4 modules
utils/system/paths.py → configuration/paths.py
utils/config/user_configuration.py → configuration/user_config.py
utils/limits.py → configuration/limits.py
utils/system/ignore_patterns.py → configuration/ignore_patterns.py
2. Move to Foundation (types/adapters/) - 1 module
utils/messaging/ → types/adapters/ (message format conversion)
3. Re-export through Layer 2 (tools/) - 1 module
utils/parsing/ → tools/parsing/ (tool-specific parsing logic)
4. Inline into Core - 2 modules
utils/ui/helpers.py (DotDict, 14 lines) → inline into core/agents/main.py
utils/formatting.py (6-line function) → inline into core/formatting.py
5. Move to Infrastructure - 1 module
utils/ui/file_filter.py → new infrastructure/ package
Migration Order
To avoid circular dependencies, migrate in this order:
Phase 1 - Inline tiny modules (no dependencies)
- Inline
utils/formatting.py into core/formatting.py
- Inline
DotDict from utils/ui/helpers.py into core/agents/main.py
Phase 2 - Foundation layer (configuration/)
- Move
utils/system/ignore_patterns.py → configuration/ignore_patterns.py
- Move
utils/config/user_configuration.py → configuration/user_config.py
- Move
utils/limits.py → configuration/limits.py
- Move
utils/system/paths.py → configuration/paths.py
Phase 3 - Foundation layer (types/adapters/)
- Move
utils/messaging/ → types/adapters/
Phase 4 - Infrastructure
- Create
infrastructure/ package
- Move
utils/ui/file_filter.py → infrastructure/file_filter.py
Phase 5 - Tool layer re-export
- Move
utils/parsing/ → tools/parsing/
- Update
core/agents/.../tool_dispatcher.py to import from tools.parsing
Phase 6 - Cleanup
- Delete empty
utils/ directories
- Update all imports across codebase
- Verify with grimp/import-linter
Knowledge Gaps
- Cross-layer import counts: Need to verify tools/ also imports from some utils modules (limits, ignore_patterns)
- Test coverage: Need to verify tests don't have direct utils imports
- UI layer: Need to verify UI doesn't import directly from utils (should go through core facades)
Acceptance Criteria (from issue)
Impact Assessment
| Impact Area |
Scope |
Risk |
| Messaging (estimate_tokens, get_content) |
6 files |
Medium - hot path for token counting |
| Config (load_config, limits) |
4 files |
Low - straightforward move |
| Paths (session storage) |
2 files |
Low - infrastructure |
| Parsing (tool dispatch) |
1 file |
Medium - complex multi-strategy parser |
| UI helpers (DotDict, FileFilter) |
2 files |
Low - simple utilities |
| Formatting |
1 file |
Low - 6-line inline |
References
File Inventory
Files to Modify (core/ - update imports)
src/tunacode/core/system_paths.py
src/tunacode/core/agents/resume/sanitize_debug.py
src/tunacode/core/agents/resume/summary.py
src/tunacode/core/agents/resume/prune.py
src/tunacode/core/agents/resume/sanitize.py
src/tunacode/core/agents/agent_components/agent_config.py
src/tunacode/core/agents/agent_components/orchestrator/tool_dispatcher.py
src/tunacode/core/agents/main.py
src/tunacode/core/file_filter.py
src/tunacode/core/formatting.py
src/tunacode/core/user_configuration.py
src/tunacode/core/state.py
src/tunacode/core/messaging.py
Files to Move/Create
| Source |
Destination |
src/tunacode/utils/system/paths.py |
src/tunacode/configuration/paths.py |
src/tunacode/utils/config/user_configuration.py |
src/tunacode/configuration/user_config.py |
src/tunacode/utils/limits.py |
src/tunacode/configuration/limits.py |
src/tunacode/utils/system/ignore_patterns.py |
src/tunacode/configuration/ignore_patterns.py |
src/tunacode/utils/messaging/ |
src/tunacode/types/adapters/ |
src/tunacode/utils/parsing/ |
src/tunacode/tools/parsing/ |
src/tunacode/utils/ui/file_filter.py |
src/tunacode/infrastructure/file_filter.py |
(inline) utils/formatting.py |
→ core/formatting.py |
(inline) utils/ui/helpers.py DotDict |
→ core/agents/main.py |
Files to Delete (after migration)
src/tunacode/utils/formatting.py
src/tunacode/utils/ui/helpers.py (if only contains DotDict)
- Empty
utils/ subdirectories
1---2name: 513-2026-01-27-11-53-37-issue-313-core-utils-layer-violation3description: Research – Issue #313: Core → Utils Layer Violation4---5# Research – Issue #313: Core → Utils Layer Violation67**Date:** 2026-01-278**Owner:** Claude (research agent)9**Phase:** Research10**Issue:** [#313 Layer violation: core → utils (18 imports, skips Layer 2)](https://github.com/alchemiststudiosDOTai/tunacode/issues/313)1112## Goal1314Document all `core → utils` imports, categorize by fix strategy, and provide a migration plan to eliminate the layer violation where core (Layer 1) imports directly from utils (Layer 3), skipping Layer 2 (tools/indexing/lsp).1516## Architecture Reference1718```19ui (Layer 0)20 ↓ only21core (Layer 1)22 ↓ only23tools | indexing | lsp (Layer 2)24 ↓ only25utils (Layer 3)2627Foundation (exempt): types, configuration, constants28```2930## Findings3132### Current Import Analysis3334**Total:** 24 imports across 13 files3536| Core File | Utils Module | Imports | Count |37|-----------|-------------|---------|-------|38| `core/system_paths.py:5-11` | `utils.system.paths` | `check_for_updates`, `delete_session_file`, `get_project_id` | 3 |39| `core/agents/resume/sanitize_debug.py:11` | `utils.messaging` | `_get_attr`, `_get_parts`, `get_tool_call_ids`, `get_tool_return_ids` | 4 |40| `core/agents/resume/summary.py:19` | `utils.messaging` | `estimate_tokens` | 1 |41| `core/agents/resume/prune.py:11` | `utils.messaging` | `estimate_tokens` | 1 |42| `core/agents/resume/sanitize.py:20` | `utils.messaging` | `_get_attr`, `_get_parts`, `find_dangling_tool_calls`, `to_canonical_list` | 4 |43| `core/agents/agent_components/agent_config.py:24-25` | `utils.config`, `utils.limits` | `load_config`, `get_max_tokens` | 2 |44| `core/agents/agent_components/orchestrator/tool_dispatcher.py:108,149` | `utils.parsing` | `parse_args`, `parse_tool_calls_from_text`, `has_potential_tool_call` | 3 |45| `core/agents/main.py:31` | `utils.ui` | `DotDict` | 1 |46| `core/file_filter.py:5` | `utils.ui.file_filter` | `FileFilter` | 1 |47| `core/formatting.py:5,8` | `utils.formatting` | `truncate_diagnostic_message`, `MAX_DIAGNOSTIC_MESSAGE_LENGTH` | 2 |48| `core/user_configuration.py:6-10` | `utils.config` | `load_config_with_defaults`, `save_config`, `UserConfigStateManager` | 3 |49| `core/state.py:19,98,185,291,337` | `utils.messaging`, `utils.config`, `utils.system.paths` | `estimate_tokens`, `get_content`, `load_config_with_defaults`, `get_session_storage_dir` | 4 |50| `core/messaging.py:7` | `utils.messaging` | `get_content` | 1 |5152### Utils Modules Dependency Analysis5354| Utils Module | Dependencies | Layer Assessment |55|-------------|-------------|------------------|56| `utils/system/paths.py` | `configuration.settings`, `constants` | Foundation-eligible |57| `utils/messaging/` | `types.canonical` only | Foundation-eligible (type adapters) |58| `utils/config/` | `configuration.settings`, `exceptions`, `types` | Foundation-eligible |59| `utils/limits.py` | `constants`, `utils.config` | Foundation-eligible (after config moves) |60| `utils/parsing/` | `constants`, `exceptions`, `types` | Tool-specific (Layer 2) |61| `utils/ui/helpers.py` | None | Inline candidate |62| `utils/ui/file_filter.py` | `constants`, `utils.system.ignore_patterns` | Infrastructure |63| `utils/formatting.py` | None | Inline candidate |6465### Existing Facade Pattern6667Core already has facade files that re-export from utils:6869- `core/messaging.py` → re-exports `get_content`70- `core/formatting.py` → re-exports `truncate_diagnostic_message`71- `core/user_configuration.py` → re-exports config functions72- `core/system_paths.py` → re-exports path utilities73- `core/file_filter.py` → re-exports `FileFilter`7475Pattern uses underscore-prefixed imports:76```python77from tunacode.utils.messaging import get_content as _get_content78```7980## Key Patterns / Solutions Found8182### Fix Strategy Categories8384**1. Move to Foundation (`configuration/`)** - 4 modules85- `utils/system/paths.py` → `configuration/paths.py`86- `utils/config/user_configuration.py` → `configuration/user_config.py`87- `utils/limits.py` → `configuration/limits.py`88- `utils/system/ignore_patterns.py` → `configuration/ignore_patterns.py`8990**2. Move to Foundation (`types/adapters/`)** - 1 module91- `utils/messaging/` → `types/adapters/` (message format conversion)9293**3. Re-export through Layer 2 (`tools/`)** - 1 module94- `utils/parsing/` → `tools/parsing/` (tool-specific parsing logic)9596**4. Inline into Core** - 2 modules97- `utils/ui/helpers.py` (DotDict, 14 lines) → inline into `core/agents/main.py`98- `utils/formatting.py` (6-line function) → inline into `core/formatting.py`99100**5. Move to Infrastructure** - 1 module101- `utils/ui/file_filter.py` → new `infrastructure/` package102103### Migration Order104105To avoid circular dependencies, migrate in this order:1061071. **Phase 1 - Inline tiny modules (no dependencies)**108 - Inline `utils/formatting.py` into `core/formatting.py`109 - Inline `DotDict` from `utils/ui/helpers.py` into `core/agents/main.py`1101112. **Phase 2 - Foundation layer (configuration/)**112 - Move `utils/system/ignore_patterns.py` → `configuration/ignore_patterns.py`113 - Move `utils/config/user_configuration.py` → `configuration/user_config.py`114 - Move `utils/limits.py` → `configuration/limits.py`115 - Move `utils/system/paths.py` → `configuration/paths.py`1161173. **Phase 3 - Foundation layer (types/adapters/)**118 - Move `utils/messaging/` → `types/adapters/`1191204. **Phase 4 - Infrastructure**121 - Create `infrastructure/` package122 - Move `utils/ui/file_filter.py` → `infrastructure/file_filter.py`1231245. **Phase 5 - Tool layer re-export**125 - Move `utils/parsing/` → `tools/parsing/`126 - Update `core/agents/.../tool_dispatcher.py` to import from `tools.parsing`1271286. **Phase 6 - Cleanup**129 - Delete empty `utils/` directories130 - Update all imports across codebase131 - Verify with grimp/import-linter132133## Knowledge Gaps134135- **Cross-layer import counts**: Need to verify tools/ also imports from some utils modules (limits, ignore_patterns)136- **Test coverage**: Need to verify tests don't have direct utils imports137- **UI layer**: Need to verify UI doesn't import directly from utils (should go through core facades)138139## Acceptance Criteria (from issue)140141- [ ] Zero direct imports from `tunacode.utils` in `tunacode.core`142- [ ] Utils accessed through `tools`/`indexing`/`lsp` layer OR moved to foundation143- [ ] `import-linter` passes with layer config144- [ ] No new functionality added (pure refactor)145- [ ] Tests pass146147## Impact Assessment148149| Impact Area | Scope | Risk |150|-------------|-------|------|151| Messaging (estimate_tokens, get_content) | 6 files | Medium - hot path for token counting |152| Config (load_config, limits) | 4 files | Low - straightforward move |153| Paths (session storage) | 2 files | Low - infrastructure |154| Parsing (tool dispatch) | 1 file | Medium - complex multi-strategy parser |155| UI helpers (DotDict, FileFilter) | 2 files | Low - simple utilities |156| Formatting | 1 file | Low - 6-line inline |157158## References159160- Issue: https://github.com/alchemiststudiosDOTai/tunacode/issues/313161- Architecture: `docs/architecture/layers_html.html`162- Dependency map: `docs/architecture/DEPENDENCY_MAP.md`163- Related issues: #311 (core→types), #312 (core→configuration), #314 (Layer 2 lateral), #315 (lsp→configuration)164165## File Inventory166167### Files to Modify (core/ - update imports)1681691. `src/tunacode/core/system_paths.py`1702. `src/tunacode/core/agents/resume/sanitize_debug.py`1713. `src/tunacode/core/agents/resume/summary.py`1724. `src/tunacode/core/agents/resume/prune.py`1735. `src/tunacode/core/agents/resume/sanitize.py`1746. `src/tunacode/core/agents/agent_components/agent_config.py`1757. `src/tunacode/core/agents/agent_components/orchestrator/tool_dispatcher.py`1768. `src/tunacode/core/agents/main.py`1779. `src/tunacode/core/file_filter.py`17810. `src/tunacode/core/formatting.py`17911. `src/tunacode/core/user_configuration.py`18012. `src/tunacode/core/state.py`18113. `src/tunacode/core/messaging.py`182183### Files to Move/Create184185| Source | Destination |186|--------|-------------|187| `src/tunacode/utils/system/paths.py` | `src/tunacode/configuration/paths.py` |188| `src/tunacode/utils/config/user_configuration.py` | `src/tunacode/configuration/user_config.py` |189| `src/tunacode/utils/limits.py` | `src/tunacode/configuration/limits.py` |190| `src/tunacode/utils/system/ignore_patterns.py` | `src/tunacode/configuration/ignore_patterns.py` |191| `src/tunacode/utils/messaging/` | `src/tunacode/types/adapters/` |192| `src/tunacode/utils/parsing/` | `src/tunacode/tools/parsing/` |193| `src/tunacode/utils/ui/file_filter.py` | `src/tunacode/infrastructure/file_filter.py` |194| (inline) `utils/formatting.py` | → `core/formatting.py` |195| (inline) `utils/ui/helpers.py` DotDict | → `core/agents/main.py` |196197### Files to Delete (after migration)198199- `src/tunacode/utils/formatting.py`200- `src/tunacode/utils/ui/helpers.py` (if only contains DotDict)201- Empty `utils/` subdirectories