Files
IdeaSDK/.ideai/tickets/98/carnet.md

7.6 KiB

issueRef, version, updatedBy, updatedAt
issueRef version updatedBy updatedAt
#98 5
kind
user
1784983186248

Carnet #98 — Contrat figé (approche B2)

Cadrage ARCHITECT figé et gelé. Source de vérité pour DevBackend et QA. Ce carnet remplace la phase d'arbitrage ; les options A/B1/C sont closes (voir §2 pour le rejet motivé).


1. Cause racine affinée

Le modèle est perdu au spawn, pas à la sauvegarde (persistance vérifiée correcte bout en bout, profiles.json conserve bien opencodeProvider.model). Deux facteurs composés :

  1. Pas de bloc models pour les providers catalogue connus — opencode_provider_config_json (lifecycle.rs:2774-2844 + jumeau assistant/mod.rs:428-503) n'émet npm/baseURL/models que pour le chemin custom. Pour un provider catalogue (zai), seul provider.<id>.options.apiKey est écrit. Test fige ce manque : lifecycle.rs:4439-4440.
  2. Cache models.dev isolé et vide — IdeA passe XDG_CACHE_HOME=<run_dir>/.opencode/cache vide (lifecycle.rs:2386,2405 ; assistant/mod.rs:272,282) + HOME isolé. Or zai n'est connu d'OpenCode que via ce cache (catalogue lu côté IdeA depuis le vrai ~/.cache/opencode/models.json, provider_catalogue.rs:79-84).

→ OpenCode ne connaît plus zai, ne résout pas zai/<modèle>fallback silencieux glm-5.2 (fallback interne CLI OpenCode, absent du code IdeA).

Asymétrie confirmée : local llama.cpp et cloud custom marchent car ils émettent un bloc models (assistant/mod.rs:371-379, :457-460).

2. Approche retenue : B2 (seed best-effort du cache hôte)

Copier best-effort le cache models.dev hôte (opencode_models_cache_path()) vers <xdg_cache>/opencode/models.json dans le cache isolé, juste après le create_dir_all du cache dans apply_mcp_config (branche commune opencode/opencodeProvider), aux DEUX sites.

Rejet motivé des alternatives :

  • A (émettre bloc models pour providers connus) : rejeté car provider_catalogue.rs ne persiste que name+models et ignore npm+baseURL. Un bloc models seul laisse OpenCode incapable de joindre zai — il faudrait en plus étendre le domaine (display_name/npm/base_url) → scope bien plus large et risque régression. A n'est viable qu'en révision A-étendue, écartée pour ce lot.
  • B1 (= B sans factorisation, deux sites copiés) : rejeté pour dette hexagonale — le writer opencode_provider_config_json est déjà dupliqué lifecycle.rs vs assistant/mod.rs. Dupliquer aussi le seed amplifierait la dette et rendrait toute divergence un bug silencieux. B2 impose une factorisation unique.
  • C (hybride : pointer XDG_CACHE_HOME vers le vrai cache) : rejeté — casse l'invariant d'isolation du run (un profil pourrait lire/écrire le cache hôte ou un cache d'un autre run), et OpenCode pourrait y écrire (logs, refresh) → pollution mutuelle.

Pourquoi B2 : donne à OpenCode sa connaissance registry complète (npm, baseURL, modèles) sans toucher au domaine ni casser l'isolation écriture — c'est de la donnée publique read-only copiée dans l'espace isolé.

3. Contrat figé — ports / fichiers / invariants

3.1 Fonctions à implémenter (factorisation)

  • Rendre opencode_models_cache_path() publique — source de vérité unique du chemin hôte, ne pas re-dériver le chemin dans le seed.
  • seed_opencode_models_cache(fs, isolated_cache_dir) (fonction partagée) : lit le cache hôte via opencode_models_cache_path(), copie best-effort vers <isolated_cache_dir>/opencode/models.json. Appelée aux deux sites après le create_dir_all du cache dans apply_mcp_config (branche commune opencode/opencodeProvider).
  • seed_from_bytes(fs, dest, src_bytes) (partie pure testable) : extrait la logique d'écriture du fichier destination à partir de bytes source. C'est le seam de test unitaire.

3.2 Sites d'appel (les DEUX)

  1. crates/application/src/agent/lifecycle.rs — après create_dir_all cache dans apply_mcp_config.
  2. crates/infrastructure/src/assistant/mod.rs — après create_dir_all cache dans apply_mcp_config.

3.3 Invariants (à respecter, à tester)

  • Isolation préservée en écriture : le seed ne fait que copier une donnée read-only vers le cache isolé. Jamais de XDG_CACHE_HOME pointé vers l'hôte.
  • Best-effort : le seed n'échoue jamais le launch. Toute erreur (fichier hôte absent, IO) est tracée (warn/log) et ignorée — on retombe sur le comportement actuel (fallback glm-5.2), pas sur un crash.
  • Pas de régression : profils local (llama.cpp), custom et built-ins (anthropic/openai/openrouter) doivent continuer à fonctionner byte-identique au rendu opencode.json actuel.
  • Exclusion mutuelle #97 non touchée : le seed s'exécute sur la branche commune opencode/opencodeProvider, sans affecter la logique d'exclusion.
  • Cohérence picker↔spawn : le catalogue vu côté UI (picker) et celui seedé au spawn proviennent du même fichier hôte → l'utilisateur ne peut pas picker un modèle qu'OpenCode ne connaîtra pas au spawn.

3.4 Hors périmètre (explicitement exclu)

  • Remote SSH/WSL (le seed ne concerne que le spawn local).
  • Déduplication du rendu lifecycle↔infra au-delà du seed (la dette du writer opencode_provider_config_json reste ouverte — autre lot).
  • UI wizard (aucun changement surface).

4. Périmètre QA — 2 couches

Couche 1 — Unitaire pure (obligatoire, rapide, déterministe)

  • seed_from_bytes : assert écriture byte-identique du contenu source vers dest, gestion erreurs (fs en échec → pas de panic), idempotence.
  • Non-régression rendu opencode.json : les tests existants lifecycle.rs:4439-4440 et équivalents infra doivent rester byte-identiques pour local/custom/built-ins (le seed ne change pas le rendu config). Mettre à jour l'assert si et seulement si B2 modifie réellement le rendu — sinon la conserver telle quelle.

Couche 2 — Intégration gated (obligatoire avant fermeture du lot)

  • Spawn réel OpenCode sur profil zai + modèle X choisi.
  • Assert : OpenCode démarre sur le modèle X (pas de fallback glm-5.2).
  • Vérifier dans les background-tasks/IO qu'aucune trace glm-5.2 n'apparaît comme modèle actif.
  • Gate obligatoire à lever : OpenCode refresh/écrase-t-il le cache models.json au démarrage ? → si oui, le seed est inutile (écrasé avant lecture) et il faut escalader vers A-étendue (persistir npm/baseURL/models côté domaine). Consigner le verdict dans le rapport QA.

5. Risque résiduel — refresh models.dev par OpenCode

Risque ouvert, à trancher en QA couche 2. Si OpenCode rafraîchit/écrase <xdg_cache>/opencode/models.json au démarrage (network call ou réécriture locale), le seed B2 est potentiellement inopérant :

  • Meilleur cas : OpenCode lit le cache avant tout refresh → B2 fonctionne.
  • Cas dégradé : OpenCode refresh en premier, écrase le seed, et comme le profil est isolé (pas d'accès réseau garanti / HOME isolé), le refresh peut échouer ou produire un cache incomplet → bug persiste.
  • Plan de contournement si échec : escalader vers A-étendue (ajouter npm+base_url+models persistés côté OpenCodeProviderConfig + émettre le bloc complet au spawn). Ce plan est documenté mais hors scope B2 — il ferait l'objet d'un lot suivant si QA le confirme nécessaire.

Contexte de mise à jour

  • Cadrage figé par Main sur validation Architect (approche B2).
  • Version précédente (v1) : phase d'arbitrage A/B/C — clos.
  • Prochaine étape cycle : Git (branche) → DevBackend (implémentation 2 sites + factorisation) → QA (2 couches, gate refresh).