fix(backend): exclusion mutuelle des backends OpenCode (llamacpp vs cloud) (#97)

Les builders `with_opencode` / `with_opencode_provider` s'évacuent
réciproquement (dernier appel gagnant) pour honorer l'invariant
`opencode_backend_is_consistent`. Le use case SaveOpenCodeProviderProfile
route désormais via le builder au lieu de muter le champ directement — c'est
ce qui dupliquait `opencode` + `opencodeProvider` et forçait un repli sur
llamacpp même quand l'utilisateur choisissait un provider cloud.

Défense en profondeur côté store :
- `read_doc` répare les profils corrompus sur disque (drop du stale `opencode`)
- `save` refuse de persister un profil violant l'invariant via le nouveau
  variant `StoreError::Invalid` (mappé vers `AppError::Invalid`)

Tests verts : builders last-wins (domain), save rejets + read repair
(infra, profile_store 10/10).

Refs #97
This commit is contained in:
2026-07-24 19:52:34 +02:00
parent 7fee56acf5
commit 0f0a76d806
6 changed files with 173 additions and 10 deletions

View File

@ -796,6 +796,12 @@ pub enum StoreError {
/// Underlying I/O error.
#[error("store io failed: {0}")]
Io(String),
/// A persisted item violated a domain invariant — surfaced by defense-in-depth
/// guards (e.g. [`crate::profile::AgentProfile::opencode_backend_is_consistent`],
/// ticket #97). Mapped upstream to an *invalid input* error (not a generic
/// store failure) so callers can distinguish invariant violations.
#[error("invalid persisted item: {0}")]
Invalid(String),
}
/// Errors from the [`MemoryStore`].