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
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -1095,5 +1095,12 @@ MyIA.AI.Notebooks/GenAI/Texte/tools/llamasharp-bakeoff/cuda-detect-probe/
# SL-14 aifeynman run artifacts (results/, train/test splits, NN models -- regenerable, local only)
MyIA.AI.Notebooks/SymbolicAI/SymbolicLearning/_sl14_runs/

# PR #17422 — probes directory artifacts (papermill _out, _deps DLL mirror).
# `_out` n'est pas couvert par `*_output.ipynb` au-dessus (suffixe distinct).
# `_deps/` héberge la copie locale de QuikGraph.dll pour la mesure c.790 du
# workaround relatif (#r "./_deps/QuikGraph.dll") — ne pas la committer.
scripts/notebook_tools/probes/*_out.ipynb
scripts/notebook_tools/probes/_deps/

# Livecoding video pipeline (#15604) : artefacts capture/PoC -- media regenerable, local only
out/
77 changes: 77 additions & 0 deletions docs/reference/dotnet-restore-rfc-17361.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Reproduction et workaround : bug `#r nuget:` dans dotnet-interactive 1.0.617701 (po-2027)

**Issue** : #17361 — `env: restore #r nuget/#r file cassé dans dotnet-interactive sur po-2027 (les 2 builds) — re-exec .NET bloquée`

**Statut** : bug **réel mais non déterministe** — reproduit 3× le 2026-09-22 (c.760), non reproduit sur 2 re-tentatives le 2026-09-23 (c.803 cache chaud, c.807 cache froid — section *Mesures de reproduction*). Workaround identifié, fix de root cause **out-of-scope** (bug interne `Microsoft.DotNet.Interactive.PackageManagement`).

## Reproduction (3 probes livrés, 5 tentées)

**Tentées** (mesure first-hand c.760, po-2027) :

| # | Probe | Résultat |
|---|---|---|
| A | cellule unique avec `#r "nuget: IKVM, 8.15.0"` seul | ✅ restore OK |
| B | cellule 2 (post-A) avec `#r "nuget: QuikGraph, 2.5.0"` | ❌ `PackageRestoreResult..ctor ArgumentException: Must provide errors when succeeded is false` |
| C | cellule unique avec 2 `#r` consécutifs (`IKVM` puis `QuikGraph`) | ❌ même erreur |
| D | cellule unique avec `#r "file.dll"` (path local résolu via `Environment.SpecialFolder.UserProfile`) | ✅ restore OK |
| E | cellule post-D avec `#r "nuget: CsvHelper, 33.0.1"` | ❌ `ArgumentException` — `file.dll` ne réinitialise **pas** le `PackageRestoreContext` |

**Livrées dans `scripts/notebook_tools/probes/dotnet-restore-bug-17361.ipynb`** : A, D, E (3 cellules code, exécution Papermill c.760).

### Mesures de reproduction (historique)

| Cycle | Date | Conditions | Résultat |
|---|---|---|---|
| c.760 | 2026-09-22 (soir) | 3 exécutions (têtes `261d8aa709`, `03260ceae2`, run Papermill 01:42Z `exception: true`) | ❌ `ArgumentException` ×3 |
| c.803 | 2026-09-23 15:10Z | cache NuGet chaud (`csvhelper/33.0.1` présent) | ✅ restore OK |
| c.807 | 2026-09-23 17:44Z | cache NuGet **froid** (`33.0.1` purgé avant le run, re-téléchargé pendant) | ✅ restore OK |

L'hypothèse « cache chaud explique la non-reproduction » est **réfutée** par c.807 : même à cache froid, le 2ᵉ restore NuGet de la session (probe E, séquence identique A → D → E) a réussi. La précondition exacte du bug reste **inconnue**.

**Conclusion mesurée (c.760, à lire avec l'historique ci-dessus)** : le `PackageRestoreContext` interne était dans un état où `succeeded=false` était passé sans `errors` au **second restore NuGet dans une session kernel** — 3 fois sur 3 le 2026-09-22, puis plus jamais sur les re-tentatives du 2026-09-23. Pas une question de version ni de multi-cellules — la séquence « **2ᵉ restore NuGet au total** » est le déclencheur observé quand il se produit. B et C confirment (c.760) ; A isole l'état initial ; D montre le by-pass `file.dll` ; **E réfute l'hypothèse initiale** selon laquelle `file.dll` réinitialiserait le contexte (et disqualifie la workaround « mix file.dll + nuget intercalés »).

## Workaround applicable

**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
```

```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)

// Référencer une assembly locale — le `#r` est résolu au parse-time et exige un
// LITTÉRAL (ni variable ni interpolation) : la forme mesurée c.790/c.803 est le
// chemin RELATIF depuis le dossier du notebook :
// #r "./_deps/QuikGraph.dll"
// (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.

## Cause racine (out-of-scope)

Bug interne dans `Microsoft.DotNet.Interactive.PackageManagement.PackageRestoreResult..ctor` : lève `ArgumentException` si `succeeded=false` est passé avec `errors=null` ou vide. C'est un état que le code ne devrait jamais produire — probablement une race condition dans `RestoreAsync()`. Hypothèse **renforcée** par c.807 : la même séquence de commandes échouait le 2026-09-22 et réussit le 2026-09-23 (y compris à cache froid) — le facteur variable est le **chemin/timing interne du restore**, pas la séquence des `#r` ; une race sur un restore concurrent (latence réseau, résolution, écriture cache) reste l'explication la plus cohérente.

**À escalader upstream** : https://github.com/dotnet/interactive/issues (chercher `PackageRestoreResult..ctor ArgumentException`).

## Fix de root cause côté po-2027

**NON applicable localement** :
- Le pin `1.0.617701` (état cluster) ne corrige que le mono-restore, pas le multi
- Le `1.0.712001` (état-trouvé #17361) a un bug encore plus large (mono KO aussi)
- Pas d'option CLI `dotnet interactive jupyter` qui contourne

**Option à explorer** : mise à jour vers `Microsoft.DotNet.Interactive` >= 1.0.720000 si le bug y est fixé (à vérifier upstream).

## 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"`.
4. **LONG TERME** : fix upstream + bump version cluster.
96 changes: 96 additions & 0 deletions scripts/notebook_tools/extract_probes_source.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#!/usr/bin/env python3
"""Extract a clean source-only notebook from an executed probe notebook.

Usage:
python scripts/notebook_tools/extract_probes_source.py <input.ipynb> <output.source.ipynb>

Strips:
- All code-cell `outputs: []` and sets `execution_count = None`.
- Papermill metadata keys (`papermill`, `tags`, `jupyter`, `dotnet_interactive`)
on every cell, and from notebook-level `metadata`.
- Papermill error-banner markdown cells (cells containing
'An Exception was encountered' or 'papermill-error-cell').

Kept (NOT stripped — these are the notebook's own identity, not papermill artifacts):
- `metadata.kernelspec` — kernel identity (name/display_name/language). Without it,
Jupyter cannot reopen the notebook on the right kernel, and papermill cannot replay
it without `-k .net-csharp`. Adjoint finding c.44 / 2026-09-23.
- `metadata.language_info` — language hints for syntax highlighting / introspection.

Used in PR #17422 (RFC dotnet-restore-bug-17361) to keep a stable
pre-execution reference for future probes, without committing a notebook
that would be auto-flagged by pre-commit H.3.

Tells respectes:
- c.1494 strict (encoding=utf-8)
- c.builder-newline (trailing newline)
- c.gh-posting-hygiene (no -f body=@, --body-file only)
- c.1148 strict (>100 chars body, non-PAYLOAD-TRAP)
"""
import argparse
import hashlib
import json
import os
import sys


def sha256_short(path: str, n: int = 16) -> str:
return hashlib.sha256(open(path, "rb").read()).hexdigest()[:n]


def main() -> int:
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8")

ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("input", help="executed probe notebook (.ipynb)")
ap.add_argument("output", help="source-only notebook (.source.ipynb)")
args = ap.parse_args()

if not os.path.isfile(args.input):
print(f"[err] {args.input} not found", file=sys.stderr)
return 2

nb = json.load(open(args.input, encoding="utf-8"))

new_cells = []
for c in nb.get("cells", []):
src = "".join(c.get("source", []))
if c["cell_type"] == "code":
c["execution_count"] = None
c["outputs"] = []
meta = c.get("metadata", {}) or {}
for k in ("papermill", "tags", "jupyter", "dotnet_interactive"):
meta.pop(k, None)
if c["cell_type"] == "markdown":
if "An Exception was encountered" in src or "papermill-error-cell" in src:
continue
new_cells.append(c)

nb["cells"] = new_cells
nb_meta = nb.get("metadata", {}) or {}
# Only strip papermill-level artifacts. Keep `kernelspec` (kernel identity)
# and `language_info` (language hints) — see adjoint finding c.44.
for k in ("papermill",):
nb_meta.pop(k, None)

os.makedirs(os.path.dirname(os.path.abspath(args.output)), exist_ok=True)
with open(args.output, "w", encoding="utf-8") as f:
json.dump(nb, f, ensure_ascii=False, indent=1)

# Tell c.builder-newline: trailing newline
with open(args.output, "ab") as f:
f.write(b"\n")

src_sha = sha256_short(args.output)
in_sha = sha256_short(args.input)
print(f"[ok] {args.input} -> {args.output}")
print(f" input size: {os.path.getsize(args.input):>6} o, sha: {in_sha}")
print(f" output size: {os.path.getsize(args.output):>6} o, sha: {src_sha}")
print(f" distinct sha: {src_sha != in_sha}")
print(f" cells kept: {len(new_cells)}")
return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading
Loading