Styleguides: - agent_memory_dimensions.md: FileItem/ContextPreset line refs (src/models.py:510-559 / 909-937) -> src/project_files.py + src/context_presets.py (moved per module_taxonomy_refactor_20260627). - config_state_owner.md: 'file I/O primitives in src/models.py' -> src/project_manager.py (the config-load/save helpers moved). - python.md §10 NOT-exempt list: '12 per-aggregate types' -> ~19; 'dataclass types in src/models.py' -> per-system files enumeration (mma.py, project_files.py, mcp_tool_specs.py, result_types.py, personas.py, workspace_manager.py, mcp_client.py). - type_aliases.md: type-registry lookup examples updated to the per-system files (src_mma.md, src_project_files.md, etc.); the 'src/models.py: 48 dataclass field types' worked-example line is flagged as historical (pre-refactor state). Harness plan (directive_hotswap_harness_20260627/plan.md): - §17 line refs corrected: 17.1 220-237 -> 247-264; 17.2 239-250 -> 266-277; 17.3 252-272 -> 279-299; 17.4 274-299 -> 301-326; 17.5 301-311 -> 328-338; 17.6 313-323 -> 340-350; 17.7 325-327 -> 352-354; 17.9 336-409 -> 364-443. - §17 master range 216-409 -> 243-473. - §12 175-184 -> 202-211; §13 185-199 -> 212-224; §15 205-215 -> 234-241. - error_handling.md: hard rules 212-242 -> 212-264; boundary types 274-311 -> 284-365. - type_aliases.md: 40-81 -> 13-87 + 89-160 + 284-365 (the alias table + decision pattern 2.5 + boundary/anti-pattern sections).
3.9 KiB
Config I/O State Ownership
Rule: The AppController is the single source of truth for the
in-memory config (self.config) and the only authorized caller of
the file I/O primitives (the config-load/save helpers, which moved out
of src/models.py to src/project_manager.py per
module_taxonomy_refactor_20260627).
Why
- The controller owns the in-memory state. If other modules
write to
config.tomldirectly, the controller'sself.configsilently drifts from disk. Tests can corrupt the user's TOML files; users lose data without warning. - Test isolation breaks. When
models.save_config(...)is called from anywhere insrc/, tests cannot intercept the write without patching the I/O primitive. The test then couples to the file format, not the controller's behavior. - Path resolution can't be enforced. The controller respects
SLOP_CONFIGenv var at call time. Direct calls tomodels.save_configwould only respect it if the path is re-resolved (which it is in_save_config_to_disk, but only because someone remembered).
What is Forbidden in src/
models.load_config(...)(legacy public function)models.save_config(...)(legacy public function)models._load_config_from_disk(...)(private I/O primitive)models._save_config_to_disk(...)(private I/O primitive)
The only allowed call sites are inside AppController itself
(load_config() and save_config() methods).
The Public API
# In AppController:
def load_config(self) -> Dict[str, Any]:
"""Re-read the global config.toml from disk and update self.config."""
self.config = models._load_config_from_disk()
return self.config
def save_config(self) -> None:
"""Flush self.config to disk."""
models._save_config_to_disk(self.config)
Callers (including gui_2.py, commands.py, etc.) go through
the controller:
# In App class methods (gui_2.py): __getattr__ delegates to controller
self.save_config() # -> controller.save_config()
app.save_config() # -> controller.save_config() (via __getattr__)
app.load_config() # -> controller.load_config() (via __getattr__)
# In AppController:
self.save_config() # direct
self.load_config() # direct
Test Patterns
Tests should mock the controller methods, not the I/O primitives:
# CORRECT: route through the controller
with patch('src.app_controller.AppController.load_config',
return_value={'ai': {...}, 'projects': {...}}):
app = App() # controller's load_config returns the mock
with patch('src.app_controller.AppController.save_config'):
app._save_paths() # controller's save_config is a no-op
app.save_config.assert_called_once() # verify the call
# WRONG: patch the I/O primitive
with patch('src.models._save_config_to_disk'): # bypasses the controller
app._save_paths() # still hits the I/O primitive if production bypasses
The mock_app and app_instance fixtures in tests/conftest.py
follow the correct pattern: they patch
AppController.load_config and AppController.save_config to
prevent real I/O and to provide a default config.
Exceptions
The only allowed non-controller call site is the
test_models_no_top_level_tomli_w.py test, which specifically
verifies the lazy-load behavior of the I/O primitive itself
(tomli_w import timing). This test is exempt from the audit.
Enforcement
The scripts/audit_no_models_config_io.py script enforces this rule.
python scripts/audit_no_models_config_io.py— human reportpython scripts/audit_no_models_config_io.py --strict— exit 1 on violationpython scripts/audit_no_models_config_io.py --json— machine output
CI should run the --strict mode on every PR.
See Also
docs/guide_app_controller.md— the AppController's roledocs/guide_models.md— the models moduleconductor/product.md— "Modular Controller Pattern" principle