Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -1127,6 +1127,12 @@ MyIA.AI.Notebooks/SymbolicAI/SymbolicLearning/_sl14_runs/
scripts/notebook_tools/probes/*_out.ipynb
scripts/notebook_tools/probes/_deps/

# #17361 — le helper `scripts/ci/dotnet_preload_packages.py` depose son mirror
# d'assemblies A COTE du notebook qui les reference (`#r "./_deps/X.dll"`).
# Tout dossier `_deps/`, a n'importe quelle profondeur, est un artefact
# regenerable (copie du cache NuGet) — aucun n'est suivi dans le depot.
_deps/

# Livecoding video pipeline (#15604) : artefacts capture/PoC -- media regenerable, local only
out/

Expand Down
45 changes: 37 additions & 8 deletions docs/reference/dotnet-restore-rfc-17361.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,21 @@ L'hypothèse « cache chaud explique la non-reproduction » est **réfutée** pa

**Précharger** les packages NuGet en assemblies locales (résolution + copie par helper Python), puis référencer par `#r "file.dll"` dans les notebooks :

```python
# scripts/ci/dotnet_preload_packages.py — TODO: à implémenter
# - Lit une liste de packages (ex: ikvm, quikgraph, csvhelper)
# - Pour chaque : nuget restore -> copie .dll dans .dotnet_packages/<pkg>/<ver>/
# - Émet un manifest .NET-packages.json avec paths résolus
```bash
# scripts/ci/dotnet_preload_packages.py — LIVRÉ (livrable 2)
# Lit une liste de packages, résout le cache NuGet global (ou le peuple par
# `dotnet restore` s'il est absent), copie les .dll dans `_deps/` et émet
# `_deps/.NET-packages.json` avec les lignes `#r` prêtes à coller.
python scripts/ci/dotnet_preload_packages.py QuikGraph==2.5.0 CsvHelper==33.0.1 IKVM
```

La copie est **à plat** dans `_deps/` — et non dans `.dotnet_packages/<pkg>/<ver>/`
comme l'esquissait la première rédaction de ce RFC : `#r` est résolu au parse-time
et exige un littéral relatif au notebook, dont la forme mesurée (c.790) est
`./_deps/<Dll>.dll`. Les dépendances transitives ne sont **pas** résolues — un
package qui en déclare (IKVM par exemple) doit les lister lui-même sur la ligne
de commande.

```csharp
// Dans un notebook .NET, au lieu de :
// #r "nuget: QuikGraph, 2.5.0" ← a planté au 2ᵉ restore dans la session (c.760 ; non déterministe)
Expand All @@ -52,7 +60,28 @@ L'hypothèse « cache chaud explique la non-reproduction » est **réfutée** pa
// (DLL copiée au préalable depuis le cache NuGet vers _deps/, gitignore)
```

La mesure discriminante c.760 (probe E) **réfute** l'hypothèse initiale « `file.dll` réinitialise le `PackageRestoreContext` » : un `#r "nuget:"` après un `#r "file.dll"` a levé `ArgumentException` à chacune des 3 exécutions de c.760. **Mais** la re-production a échoué sur les 2 re-tentatives du 2026-09-23 (c.803 cache chaud, c.807 cache froid) : le bug est **non déterministe**. La recommandation reste néanmoins **univoque et défensive** : **préchargement complet seul** (tous les packages NuGet en `.dll` locaux, via `./_deps/` relatif en attendant le helper `dotnet_preload_packages.py`), pas de mix `file.dll` + `nuget` dans la même session kernel — quand l'exception se produit, elle tue la cellule sans contournement runtime.
La mesure discriminante c.760 (probe E) **réfute** l'hypothèse initiale « `file.dll` réinitialise le `PackageRestoreContext` » : un `#r "nuget:"` après un `#r "file.dll"` a levé `ArgumentException` à chacune des 3 exécutions de c.760. **Mais** la re-production a échoué sur les 2 re-tentatives du 2026-09-23 (c.803 cache chaud, c.807 cache froid) : le bug est **non déterministe**. La recommandation reste néanmoins **univoque et défensive** : **préchargement complet seul** (tous les packages NuGet en `.dll` locaux, via `./_deps/` relatif et le helper `scripts/ci/dotnet_preload_packages.py`), pas de mix `file.dll` + `nuget` dans la même session kernel — quand l'exception se produit, elle tue la cellule sans contournement runtime.

## Pilot du livrable 3 : le remplacement n'est PAS équivalent (mesuré 2026-09-25)

Le livrable 3 (« convertir `#r "nuget:"` en `#r` local ») suppose que les deux formes sont interchangeables. **Elles ne le sont pas.** Mesure sur `MyIA.AI.Notebooks/Search/Part2-CSP/CSP-1-Fundamentals-CSharp.ipynb` — le notebook du dépôt le plus exposé au bug (3 restores NuGet dans une seule cellule, cellule 3 : `IKVM`, `IKVM.Image`, `IKVM.Image.runtime.win-x64`), exécuté sur po-2024 avec le kernel `.net-csharp` et le pin cluster `1.0.617701` :

| Bras | Cellule 3 (les `#r`) | Résultat | Diagnostic |
|---|---|---|---|
| **0 — original** | 3 × `#r "nuget:"` | **19/19 OK**, 0 erreur, 14,1 s | — |
| **A — converti** | 3 × `#r "./_deps/…"` (helper) | **16/19**, 3 erreurs (cellules 32, 34, 37) | `IKVM.Runtime.InternalException: Could not locate ikvm home path` |
| **C — mixte** | `IKVM` local + les 2 packages image en `nuget:` | **15/19**, 4 erreurs (cellule 3 *et* 32-37) | `IKVM.Image.targets(45,9): error MSB4036: Tâche "IkvmResolveNearestRuntimeIdentifier" introuvable` |

Le bras C est celui qui explique les deux autres : les trois `#r` ne sont pas de simples **références**, ce sont eux qui font restaurer à `Microsoft.DotNet.Interactive.PackageManagement` l'**arbre IKVM complet** (`IKVM.MSBuild` fournit la tâche MSBuild que `IKVM.Image.targets` invoque ; l'image `any/any` + `win-x64` fournit le home que `IKVM.Runtime` cherche au premier type `java.*`). Retirer ou scinder les `#r` casse ce graphe : le bras C échoue **dès la cellule 3** sur la tâche MSBuild manquante, le bras A va plus loin mais échoue au premier appel Java faute de home.

Deux conséquences pour le livrable 3, dans cet ordre :

1. **Il est réfuté pour la famille IKVM** — et cette famille est justement celle qui porte le plus de restores par cellule (Choco, Tweety, RDF.Net).
2. **`IKVM.Image` et `IKVM.Image.runtime.win-x64` ne sont pas exprimables en `#r` local du tout** : leurs dossiers `lib/<tfm>/` ne contiennent qu'un `_._`, la convention NuGet « ce TFM est compatible, mais ce package n'apporte aucune assembly ». Le helper le dit correctement (`aucune assembly dans …/lib`), ce qui n'est pas un défaut de l'outil mais la limite du remplacement.

S'y ajoute un coût de distribution, indépendant du bug : `_deps/` est **gitignore**, donc un notebook committé en `#r "./_deps/X.dll"` casse pour quiconque clone — un échec **déterministe** (« fichier introuvable ») en échange d'un bug **non déterministe**.

**Décision qui en découle** : le helper reste un **outil de réparation à la demande** — à invoquer quand l'exception se produit, sur le notebook concerné — et **non** une convention à généraliser aux notebooks pédagogiques. Le bras 0 montre au passage une 4ᵉ non-reproduction du bug : 3 restores dans une cellule, tous réussis (corrobore c.803 et c.807, après les 3 repros de c.760).

## Cause racine (out-of-scope)

Expand All @@ -72,6 +101,6 @@ Bug interne dans `Microsoft.DotNet.Interactive.PackageManagement.PackageRestoreR
## Livrables possibles (par ordre de coût)

1. **MAINTENU** : ce RFC documente le bug et la workaround pour les pairs.
2. **COURT TERME** : `scripts/ci/dotnet_preload_packages.py` (helper ~50 lignes) + convention `.net-csharp` notebooks.
3. **MOYEN TERME** : audit complet de tous les notebooks `.net-csharp` pour convertir les `#r "nuget:"` en `#r "file.dll"`.
2. **LIVRÉ (outillage)** : `scripts/ci/dotnet_preload_packages.py` + `scripts/tests/test_dotnet_preload_packages.py` (41 cas) + entrée `.gitignore` de `_deps/`. Usage retenu (cf pilot) : **réparation à la demande** sur un notebook qui a effectivement levé l'exception, **pas** une convention à généraliser.
3. **MOYEN TERME — RÉFUTÉ par la mesure** (pilot du 2026-09-25, section dédiée) : convertir tous les `.net-csharp` en `#r` local n'est pas un remplacement équivalent (les `#r "nuget:"` portent le graphe de restauration dont dépendent `IKVM.MSBuild` et le home JVM), et `_deps/` étant gitignore, la conversion rendrait le notebook déterministiquement cassé pour tout clone. **Ne pas lancer cet audit comme conversion de masse.** Le geste utile restant : convertir au cas par cas, **quand** l'exception s'est effectivement produite sur ce notebook.
4. **LONG TERME** : fix upstream + bump version cluster.
Loading
Loading