Files
IdeaSDK/.ideai/memory/ticket97-opencode-provider-mutual-exclusion.md

3.3 KiB

name, description, metadata
name description metadata
ticket97-opencode-provider-mutual-exclusion memory note ticket97-opencode-provider-mutual-exclusion
type
project

Ticket #97 — Exclusion mutuelle opencode / opencodeProvider (décision Architect)

Bug

Wizard first-run : profiles.json contient à la fois opencode (llamacpp) et opencodeProvider (cloud). Lecture priorise opencode → llamacpp l'emporte silencieusement.

Root cause (vérifiée dans le code)

  1. SaveOpenCodeProviderProfile::execute (crates/application/src/agent/usecases.rs:364-367) pose opencode_provider sans faire opencode = None.
  2. Invariant opencode_backend_is_consistent (crates/domain/src/profile.rs:1220) existe + testé mais jamais appelé hors tests (garde morte).
  3. Lecture priorise opencode : crates/application/src/agent/lifecycle.rs:2367 + crates/infrastructure/src/assistant/mod.rs:252.

Décisions Architect (validées)

  • Lot : un seul lot cohérent. Backend = autoritaire (invariant + garde + use case via builder + migration). Frontend = strip de la config inactive au save selon le mode (requis pour les chemins SaveProfile/ConfigureProfiles qui ne portent pas d'intention explicite). Découplés/parallélisables. DevBackend + DevFrontend + QA.
  • Frontière :
    • Domaine : builders with_opencode (profile.rs:1132) et with_opencode_provider (profile.rs:1140) doivent imposer l'exclusion mutuelle (chacun efface l'autre). Aujourd'hui ce sont des setters muets = la faille.
    • Infrastructure : FsProfileStore::save rejette tout profil violant l'invariant → AppError::Invalid (c'est ici que le prédicat enfin s'appelle). Défense en profondeur.
    • Application : use cases utilisent les builders, jamais la mutation brute de champ.
    • Refuser l'ad-hoc opencode = None par use case (DRY, 4 chemins d'écriture).
  • DTO : garder SaveOpenCodeProviderProfileRequestDto (dto.rs:1148) whole-profile (zéro cassure frontend). Le opencode parasite devient inoffensif car le use case reconstruit via builder. Output = profil normalisé, autorité pour le frontend. Corriger aussi SaveProfile (usecases.rs:269) et ConfigureProfiles (usecases.rs:460).
  • Lecture priorité : garder opencode d'abord (irrelevant post-exclusion), commenter comme fallback défensif.
  • Duplication résolution (lifecycle.rs:2367 + assistant/mod.rs:252) : dette hexagonale préexistante, NE PAS élargir ce lot. Suivre via un futur AgentProfile::effective_opencode_backend() ou générateur partagé.
  • Migration REQUISE : profils déjà corrompus sur disque portent les deux. Recovery : à la lecture (ou passe de migration), quand les deux présents, dropper opencode stale (l'intention est cloud, le bug ne venant que d'une action cloud explicite). Sans cela, #97 ne corrige que les nouveaux profils.

Sites de résolution (priorité) = exactement 2

  • crates/application/src/agent/lifecycle.rs:2367
  • crates/infrastructure/src/assistant/mod.rs:252 Autres accès .opencode scoper en mode local, non affectés : lifecycle.rs:2455-2468, model_server.rs:121-127, catalogue.rs:244-258, usecases.rs:410.

Statut

Décision Architect posée. À faire implémenter par DevBackend (domaine builders + garde store + migration + use cases) et DevFrontend (strip au save). QA : tests domaine/store + intégration cloud + scénario migration profils corrompus.