7.6 KiB
issueRef, version, updatedBy, updatedAt
| issueRef | version | updatedBy | updatedAt | ||
|---|---|---|---|---|---|
| #98 | 5 |
|
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 :
- Pas de bloc
modelspour les providers catalogue connus —opencode_provider_config_json(lifecycle.rs:2774-2844+ jumeauassistant/mod.rs:428-503) n'émetnpm/baseURL/modelsque pour le chemincustom. Pour un provider catalogue (zai), seulprovider.<id>.options.apiKeyest écrit. Test fige ce manque :lifecycle.rs:4439-4440. - Cache models.dev isolé et vide — IdeA passe
XDG_CACHE_HOME=<run_dir>/.opencode/cachevide (lifecycle.rs:2386,2405;assistant/mod.rs:272,282) +HOMEisolé. Orzain'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
modelspour providers connus) : rejeté carprovider_catalogue.rsne persiste quename+modelset ignorenpm+baseURL. Un blocmodelsseul laisse OpenCode incapable de joindrezai— 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_jsonest déjà dupliquélifecycle.rsvsassistant/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_HOMEvers 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 viaopencode_models_cache_path(), copie best-effort vers<isolated_cache_dir>/opencode/models.json. Appelée aux deux sites après lecreate_dir_alldu cache dansapply_mcp_config(branche communeopencode/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)
crates/application/src/agent/lifecycle.rs— aprèscreate_dir_allcache dansapply_mcp_config.crates/infrastructure/src/assistant/mod.rs— aprèscreate_dir_allcache dansapply_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_HOMEpointé 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.jsonactuel. - 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_jsonreste 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 versdest, gestion erreurs (fs en échec → pas de panic), idempotence.- Non-régression rendu
opencode.json: les tests existantslifecycle.rs:4439-4440et é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.2n'apparaît comme modèle actif. - Gate obligatoire à lever : OpenCode refresh/écrase-t-il le cache
models.jsonau 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+modelspersisté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).