diff --git a/.github/sourcelink/README.md b/.github/sourcelink/README.md
new file mode 100644
index 00000000..546a8fe3
--- /dev/null
+++ b/.github/sourcelink/README.md
@@ -0,0 +1,50 @@
+# SourceLink step-into verification
+
+Fixtures for [`sourcelink-stepinto.yaml`](../workflows/sourcelink-stepinto.yaml),
+which proves the *debugger* half of SourceLink end-to-end: that pressing **F11**
+in a consumer steps into the library's **real source** (resolved via the
+SourceLink map in the PDB), not a decompiled placeholder.
+
+This complements [`sourcelink.yaml`](../workflows/sourcelink.yaml), which proves
+every document in the PDB resolves to real GitHub content via
+`dotnet sourcelink test`. Together they cover the full chain F11 depends on:
+SourceLink map in the PDB → source-file resolution → GitHub raw-URL fetch.
+
+## How it works
+
+1. Build [`consumer/`](consumer/) with `-c Debug -p:ContinuousIntegrationBuild=true`.
+ Its `ProjectReference` compiles the library with the **same SourceLink map a
+ released package ships** (deterministic `/_/…` source roots that map to GitHub
+ raw URLs), and Debug/non-optimized codegen so the step-into target isn't
+ reordered away.
+2. Run [`verify_stepinto.py`](verify_stepinto.py) — it drives
+ [netcoredbg](https://github.com/Samsung/netcoredbg) over its MI interface to
+ break in the consumer, step into `Report`'s constructor, and assert the
+ resulting frame is the library's SourceLink-mapped source file with symbols
+ loaded. If SourceLink or the sequence points were broken the step would land
+ with no source and the script exits non-zero.
+
+## Why a ProjectReference, not the packed NuGet
+
+netcoredbg is the only CI-scriptable .NET debugger, and it does **not** reliably
+pair a *package*-sourced assembly with its symbol-package (`.snupkg`) PDB — it
+reports `symbols-loaded=0` and cannot step into it (verified during development).
+A `ProjectReference` built with `ContinuousIntegrationBuild=true` produces a
+**byte-identical SourceLink PDB**, so the SourceLink map under test is the same
+one that ships; only the delivery path differs. The packed-package symbol/URL
+resolution is covered by `sourcelink.yaml`.
+
+## Files
+
+- `consumer/StepIntoConsumer.csproj` / `Program.cs` — the fixture consumer. The
+ break line is marked `STEP_INTO_TARGET`.
+- `Directory.Build.props` / `.targets` — empty isolation stubs so the consumer
+ does **not** inherit the repo's analyzers / BannedSymbols / multi-TFM policy.
+- `verify_stepinto.py` — the debugger driver (exit 0 = step-into resolved real
+ source).
+
+## Scope
+
+Scheduled + manual, not a PR gate — debugger automation is heavier and slightly
+less deterministic than a unit test, and SourceLink's raw URLs resolve only
+against a pushed commit.
diff --git a/.github/sourcelink/consumer/Directory.Build.props b/.github/sourcelink/consumer/Directory.Build.props
new file mode 100644
index 00000000..cc81238d
--- /dev/null
+++ b/.github/sourcelink/consumer/Directory.Build.props
@@ -0,0 +1,3 @@
+
+
+
diff --git a/.github/sourcelink/consumer/Directory.Build.targets b/.github/sourcelink/consumer/Directory.Build.targets
new file mode 100644
index 00000000..cb60c727
--- /dev/null
+++ b/.github/sourcelink/consumer/Directory.Build.targets
@@ -0,0 +1,3 @@
+
+
+
diff --git a/.github/sourcelink/consumer/Program.cs b/.github/sourcelink/consumer/Program.cs
new file mode 100644
index 00000000..a5401cea
--- /dev/null
+++ b/.github/sourcelink/consumer/Program.cs
@@ -0,0 +1,12 @@
+using System;
+using Wolfgang.Etl.Abstractions;
+
+// End-to-end SourceLink "step into" fixture. The debugger sets a breakpoint on
+// the marked line below and issues a step-into (the F11 a consumer
+// would press). If SourceLink is intact the debugger resolves the library's real
+// source (from GitHub) at the constructor below, instead of a decompiled
+// placeholder. Report's constructor is a plain, non-async method with a guard
+// clause, which makes it a clean and stable step-into target.
+
+var report = new Report(42); // STEP_INTO_TARGET
+Console.WriteLine(report.CurrentItemCount);
diff --git a/.github/sourcelink/consumer/StepIntoConsumer.csproj b/.github/sourcelink/consumer/StepIntoConsumer.csproj
new file mode 100644
index 00000000..6b8144dc
--- /dev/null
+++ b/.github/sourcelink/consumer/StepIntoConsumer.csproj
@@ -0,0 +1,29 @@
+
+
+
+
+ Exe
+ net10.0
+ enable
+ disable
+
+ false
+ portable
+
+ ../../../src/Wolfgang.Etl.Abstractions/Wolfgang.Etl.Abstractions.csproj
+
+
+
+
+
+
+
diff --git a/.github/sourcelink/verify_stepinto.py b/.github/sourcelink/verify_stepinto.py
new file mode 100644
index 00000000..ab76c5bb
--- /dev/null
+++ b/.github/sourcelink/verify_stepinto.py
@@ -0,0 +1,115 @@
+#!/usr/bin/env python3
+"""Drive netcoredbg to prove SourceLink "step into" (F11) works end-to-end.
+
+Sets a breakpoint in the consumer, runs to it, issues a step-into, and inspects
+the resulting stack frame. Succeeds (exit 0) only if the step landed in the
+expected library source file with the library's symbols loaded — i.e. a debugger
+resolves real library source, not a decompiled placeholder. If SourceLink or the
+symbol package were broken, the step would land with no source mapping and this
+fails.
+
+Usage:
+ verify_stepinto.py
+"""
+import subprocess
+import sys
+import re
+import threading
+import queue
+import time
+
+if len(sys.argv) != 5:
+ print(__doc__)
+ sys.exit(2)
+
+debugger, consumer_dll, break_spec, expected_src = sys.argv[1:5]
+
+proc = subprocess.Popen(
+ [debugger, "--interpreter=mi", "--", "dotnet", consumer_dll],
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ text=True,
+ bufsize=1,
+)
+
+events: "queue.Queue[str]" = queue.Queue()
+
+
+def _reader():
+ for line in proc.stdout:
+ events.put(line.rstrip("\n"))
+ events.put(None)
+
+
+threading.Thread(target=_reader, daemon=True).start()
+
+
+def send(command):
+ print(">>>", command, flush=True)
+ proc.stdin.write(command + "\n")
+ proc.stdin.flush()
+
+
+def wait_for(needles, timeout=60):
+ deadline = time.time() + timeout
+ while time.time() < deadline:
+ try:
+ line = events.get(timeout=max(0.1, deadline - time.time()))
+ except queue.Empty:
+ return None
+ if line is None:
+ return None
+ print("dbg>", line, flush=True)
+ if any(n in line for n in needles):
+ return line
+ return None
+
+
+def fail(reason):
+ print("RESULT=FAIL reason=" + reason, flush=True)
+ try:
+ send("-gdb-exit")
+ except Exception:
+ pass
+ sys.exit(1)
+
+
+send("-break-insert " + break_spec)
+wait_for(["^done", "^error"], 15)
+send("-exec-run")
+
+# netcoredbg halts at the managed entry point first; continue to the breakpoint.
+stop = wait_for(["*stopped"], 60)
+if stop and 'reason="entry-point-hit"' in stop:
+ send("-exec-continue")
+ stop = wait_for(["*stopped"], 60)
+
+if not stop or "breakpoint-hit" not in stop:
+ fail("breakpoint-not-hit")
+
+# Step into (F11). Retry a couple of times in case the first step stays on the
+# call line before descending into the callee.
+resolved = None
+for _ in range(4):
+ send("-exec-step")
+ stop = wait_for(["*stopped"], 30)
+ if not stop:
+ break
+ match = re.search(r'frame=\{[^}]*?file="([^"]+)"[^}]*?fullname="([^"]+)"', stop)
+ if match and match.group(1).endswith(expected_src):
+ resolved = (match.group(1), match.group(2), stop)
+ break
+
+if not resolved:
+ fail("did-not-step-into-" + expected_src)
+
+send("-gdb-exit")
+
+source_file, fullname, stop = resolved
+line_match = re.search(r'line="(\d+)"', stop)
+print("RESOLVED_FILE=" + source_file, flush=True)
+print("RESOLVED_FULLNAME=" + fullname, flush=True)
+print("RESOLVED_LINE=" + (line_match.group(1) if line_match else "?"), flush=True)
+print("RESULT=PASS", flush=True)
+sys.exit(0)
diff --git a/.github/workflows/sourcelink-stepinto.yaml b/.github/workflows/sourcelink-stepinto.yaml
new file mode 100644
index 00000000..b4b61668
--- /dev/null
+++ b/.github/workflows/sourcelink-stepinto.yaml
@@ -0,0 +1,64 @@
+name: SourceLink Step-Into
+
+# Proves the DEBUGGER half of SourceLink end-to-end (#214): that pressing F11 in a
+# consumer steps into the library's real source (resolved via the SourceLink map in
+# the PDB), not a decompiled placeholder. sourcelink.yaml already proves every PDB
+# document resolves to real GitHub content; this drives an actual debugger step-into.
+#
+# See .github/sourcelink/README.md. Scheduled + manual, NOT a PR gate — debugger
+# automation is heavier/less deterministic than a unit test, and SourceLink's raw
+# URLs resolve only against a pushed commit.
+
+on:
+ schedule:
+ - cron: '0 4 * * 2' # weekly Tuesday 04:00 UTC
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+env:
+ NETCOREDBG_VERSION: '3.1.2-1054'
+ CONSUMER: .github/sourcelink/consumer
+ EXPECTED_SRC: Report.cs
+
+jobs:
+ step-into:
+ name: F11 step-into resolves library source
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
+ with:
+ persist-credentials: false
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6
+ with:
+ dotnet-version: '10.0.x'
+
+ - name: Install netcoredbg
+ run: |
+ set -euo pipefail
+ url="https://github.com/Samsung/netcoredbg/releases/download/${NETCOREDBG_VERSION}/netcoredbg-linux-amd64.tar.gz"
+ curl -sSfL -o netcoredbg.tar.gz "$url"
+ tar -xzf netcoredbg.tar.gz
+ echo "NETCOREDBG=$PWD/netcoredbg/netcoredbg" >> "$GITHUB_ENV"
+
+ - name: Build the step-into fixture (Debug, deterministic SourceLink PDB)
+ # Debug + ContinuousIntegrationBuild=true: the referenced library compiles
+ # with the same SourceLink map a released package ships, non-optimized so the
+ # step-into target is not reordered/inlined.
+ run: >
+ dotnet build "$CONSUMER/StepIntoConsumer.csproj"
+ -c Debug -p:ContinuousIntegrationBuild=true
+
+ - name: Drive netcoredbg step-into
+ run: |
+ set -euo pipefail
+ bpline=$(grep -n STEP_INTO_TARGET "$CONSUMER/Program.cs" | head -1 | cut -d: -f1)
+ dll="$CONSUMER/bin/Debug/net10.0/StepIntoConsumer.dll"
+ echo "Breakpoint at Program.cs:$bpline — expecting step-into to land in $EXPECTED_SRC"
+ python3 -u .github/sourcelink/verify_stepinto.py \
+ "$NETCOREDBG" "$dll" "Program.cs:$bpline" "$EXPECTED_SRC"