Private
Public Access
0
0
Files
manual_slop/conductor/code_styleguides/config_state_owner.md
T
ed 8b7b8b96c7 docs(styleguides+harness-plan): fix stale models.py refs + harness plan line drift
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).
2026-07-02 19:31:32 -04:00

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

  1. The controller owns the in-memory state. If other modules write to config.toml directly, the controller's self.config silently drifts from disk. Tests can corrupt the user's TOML files; users lose data without warning.
  2. Test isolation breaks. When models.save_config(...) is called from anywhere in src/, tests cannot intercept the write without patching the I/O primitive. The test then couples to the file format, not the controller's behavior.
  3. Path resolution can't be enforced. The controller respects SLOP_CONFIG env var at call time. Direct calls to models.save_config would 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 report
  • python scripts/audit_no_models_config_io.py --strict — exit 1 on violation
  • python 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 role
  • docs/guide_models.md — the models module
  • conductor/product.md — "Modular Controller Pattern" principle