Repository navigation
feat(probas,#19310): CausalBridges-00 — Pearl Ladder introductive (Pli 1 Origami causal) #10775
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Bare cross-dir #load gate | |
| # Purpose | |
| # ------- | |
| # Per-PR gate for .NET Interactive cells that carry a BARE `#load "X.cs"` | |
| # directive (bare filename, no path separator) whose `.cs` source is NOT | |
| # present in the notebook's own directory. Such a `#load` resolves ONLY when | |
| # the kernel's cwd already happens to contain the file; in a clean headless | |
| # re-exec from the notebook's directory it fails with CS1504 (source file not | |
| # found), the helper's `Formatter.Register` never runs, and the chart cell | |
| # renders BLANK. This is the SOURCE-CLASS root cause upstream of the empty | |
| # SVG output caught by `svg-empty-display-gate.yml` (#6971): that gate catches | |
| # the SYMPTOM (empty `outputs`), this one catches the CAUSE (the unresolvable | |
| # `#load`) at the source line, before exec. | |
| # | |
| # Root cause | |
| # ---------- | |
| # The SVG inline rollout (#6927) makes dozens of .NET notebooks converge on | |
| # the shared `SvgChartHelper.cs`, which lives in ONE place | |
| # (`MyIA.AI.Notebooks/Probas/Infer/`, 0 copies elsewhere). The | |
| # `dotnet-interactive` kernel resolves `#load "X"` RELATIVE to the notebook's | |
| # directory (its real cwd at headless exec). A notebook in a DIFFERENT family | |
| # (e.g. `GameTheory/`) that loads the helper via a bare | |
| # `#load "SvgChartHelper.cs"` therefore cannot resolve it from its own | |
| # directory — it only rendered because the kernel happened to run from a cwd | |
| # that found the file. Same defect class as the DecInfer portability batch | |
| # (10+ merged PRs), but CROSS-FAMILY (the `.cs` lives in a different folder | |
| # than the notebook). | |
| # | |
| # Why this is NOT redundant with svg-empty-display-gate.yml | |
| # --------------------------------------------------------- | |
| # `svg-empty-display-gate.yml` inspects the OUTPUT (a cell with a chart | |
| # `display()` and `outputs == []`). It only fires AFTER a bad exec has been | |
| # committed, and only if the notebook was executed with the wrong cwd. This | |
| # gate inspects the SOURCE `#load` directive and fires DETERMINISTICALLY on | |
| # the unresolvable-bare-load pattern regardless of whether the notebook was | |
| # executed — catching the defect at its cause, one step earlier in the | |
| # pipeline. The two gates are COMPLEMENTARY: source-cause vs output-symptom. | |
| # | |
| # This gate runs scripts/notebook_tools/detect_bare_cross_dir_load.py in | |
| # --check mode on every notebook changed by the PR and FAILs if any code cell | |
| # carries a bare `#load "X.cs"` whose `.cs` is not in the notebook's directory. | |
| # The detector is deterministic and zero-FP: | |
| # - Explicit-path loads (`#load "../Probas/Infer/SvgChartHelper.cs"`, any | |
| # load whose target contains `/` or `\`) are NOT flagged — they carry a | |
| # human's resolution intent; the recommended fix form has separators and | |
| # passes by construction. | |
| # - Co-located bare loads (`#load "Foo.cs"` where `Foo.cs` IS in the | |
| # notebook's directory) are NOT flagged — they resolve in headless exec. | |
| # - Non-source targets (`#r "nuget:..."`, `.dll`) are NOT `#load` of a | |
| # source file and are out of scope. | |
| # | |
| # Absolute (not regression) semantics: on `main` the only residual bare | |
| # cross-dir loads are the 4 GameTheory notebooks (4c/8c/15c/17) being drained | |
| # by #7052 / #7049 (issue #7050). This gate is DIFF-SCOPED — it only checks | |
| # notebooks changed by the PR — so it does not fail unrelated PRs on the | |
| # pre-existing baseline; it prevents NEW bare cross-dir loads and enforces the | |
| # fix on the 4 residuals as they are touched. | |
| # | |
| # Fix (design-gate tranched by ai-01, issue #7050) | |
| # ------------------------------------------------ | |
| # Replace the bare `#load "X.cs"` with an EXPLICIT relative path that reaches | |
| # the source file from the notebook's directory, e.g. | |
| # `#load "../Probas/Infer/SvgChartHelper.cs"` (the single SvgChartHelper.cs | |
| # lives in MyIA.AI.Notebooks/Probas/Infer/). Rejected alternatives: copying | |
| # the helper next to each notebook (4 divergent copies, DRY violation); | |
| # a centralised refactor (over-engineering for 4 call sites). Source-only fix | |
| # (the `#load` line only; execution_count/outputs unchanged — run a separate | |
| # verification re-exec with the notebook dir as cwd and confirm 0 errors, do | |
| # NOT commit it). Stop&Repair (secrets-hygiene rule 6): fix the cause + | |
| # re-execute, never scrub the output by hand. | |
| # | |
| # See EPIC #3801 (SOTA/portability), rollout #6927 (SVG inline fleet-wide), | |
| # issue #7050 (design-gate + class closure), sibling gate | |
| # svg-empty-display-gate.yml (#6971). | |
| on: | |
| pull_request: | |
| branches: [ main ] | |
| paths: | |
| - 'MyIA.AI.Notebooks/**/*.ipynb' | |
| - 'scripts/notebook_tools/detect_bare_cross_dir_load.py' | |
| - '.github/workflows/bare-cross-dir-load-gate.yml' | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: bare-cross-dir-load-gate-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| bare-cross-dir-load: | |
| name: "No bare cross-dir #load in changed notebooks" | |
| # Route vers la jambe Linux auto-hebergee (#13378 tranche 5, decision ai-01 | |
| # 2026-09-02) : garde Python pur, sans secret. La garde same-repo ci-dessous | |
| # saute les PRs de fork (pr_gate.py compte `skipped` comme OK) : aucun code de | |
| # fork n'atteint le runner. Retour arriere = revert de la PR. | |
| runs-on: [self-hosted, coursia-ephemeral, coursia-linux] | |
| if: github.event.pull_request.head.repo.full_name == null || github.event.pull_request.head.repo.full_name == github.repository | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| filter: blob:none | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Gate changed notebooks | |
| run: | | |
| set -uo pipefail | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| echo "::notice title=Bare cross-dir #load gate::Manual dispatch — nothing to diff." | |
| exit 0 | |
| fi | |
| BASE="origin/${{ github.base_ref }}" | |
| CHANGED=$(git diff --name-only "$BASE"...HEAD -- 'MyIA.AI.Notebooks/**/*.ipynb' || true) | |
| if [ -z "$CHANGED" ]; then | |
| echo "::notice title=Bare cross-dir #load gate::No notebooks changed — clean." | |
| exit 0 | |
| fi | |
| echo "## Bare cross-dir #load gate" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| fail=0 | |
| checked=0 | |
| while IFS= read -r nb; do | |
| [ -z "$nb" ] && continue | |
| # Skip notebooks deleted by the PR. | |
| [ -f "$nb" ] || continue | |
| checked=$((checked + 1)) | |
| # `&& rc=0 || rc=$?` is load-bearing under `bash -e`: a bare `out=$(cmd)` | |
| # that exits non-zero aborts the step before `rc=$?` is read, making the | |
| # rc=1/rc=2 branches below dead code and the failure anonymous (#8884 motif). | |
| # NOT `|| true` (that makes $? read 0 from `true` and permanently disarms the gate). | |
| out=$(python scripts/notebook_tools/detect_bare_cross_dir_load.py "$nb" --check 2>&1) && rc=0 || rc=$? | |
| if [ "$rc" -eq 1 ]; then | |
| echo "::error title=Bare cross-dir #load::$nb carries a bare \`#load \"X.cs\"\` whose source is not in the notebook's directory (breaks headless re-exec, CS1504; renders BLANK)." | |
| echo "<details><summary><code>$nb</code></summary>" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo '```' >> "$GITHUB_STEP_SUMMARY" | |
| echo "$out" >> "$GITHUB_STEP_SUMMARY" | |
| echo '```' >> "$GITHUB_STEP_SUMMARY" | |
| echo "</details>" >> "$GITHUB_STEP_SUMMARY" | |
| fail=1 | |
| elif [ "$rc" -eq 2 ]; then | |
| echo "::warning title=Bare cross-dir #load gate::$nb could not be read (rc=2); see detector log." | |
| fi | |
| done <<< "$CHANGED" | |
| if [ "$fail" -ne 0 ]; then | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "FIX: replace the bare \`#load \"X.cs\"\` with an EXPLICIT relative path that reaches the source file from the notebook's directory (e.g. \`#load \"../Probas/Infer/SvgChartHelper.cs\"\` — the single SvgChartHelper.cs lives in MyIA.AI.Notebooks/Probas/Infer/). Design-gate tranched by ai-01 (issue #7050): explicit relative path is canonical (copying the helper = divergent copies / DRY violation; centralised refactor = over-engineering, both rejected). Source-only fix (the \`#load\` line; execution_count/outputs unchanged — run a separate verification re-exec with the notebook dir as cwd, confirm 0 errors, do NOT commit it). Stop&Repair: fix the cause + re-execute, never scrub the output by hand (secrets-hygiene rule 6)." >> "$GITHUB_STEP_SUMMARY" | |
| exit 1 | |
| fi | |
| echo "::notice title=Bare cross-dir #load gate::Checked $checked changed notebook(s); no bare cross-dir #load." |