diff --git a/.github/workflows/aot-smoke.yaml b/.github/workflows/aot-smoke.yaml
new file mode 100644
index 0000000..9f0d2bc
--- /dev/null
+++ b/.github/workflows/aot-smoke.yaml
@@ -0,0 +1,53 @@
+name: AOT Smoke
+
+# Native-AOT publish-and-run smoke consumer (#132).
+#
+# Publishes a tiny console consumer with PublishAot and runs the native binary,
+# so the build fails if the library stops compiling or running under native AOT
+# (e.g. the async-enumerable pipeline path stops being trimmer-safe). This is the
+# *runtime* half of AOT verification: it exercises TestExtractor / TestTransformer
+# / TestLoader end-to-end in a trimmed, natively-compiled binary.
+#
+# windows-latest ships the MSVC "Desktop development with C++" workload that the
+# native linker requires, so no extra toolchain install is needed.
+#
+# Runs on every PR so AOT regressions block before merge.
+
+on:
+ pull_request:
+ push:
+ branches: [main]
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: aot-smoke-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ aot-smoke:
+ name: "AOT publish + run"
+ runs-on: windows-latest
+ timeout-minutes: 20
+ steps:
+ - name: Checkout code
+ 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: Publish (native AOT, win-x64)
+ run: dotnet publish aot-smoke/Wolfgang.Etl.TestKit.AotSmoke/Wolfgang.Etl.TestKit.AotSmoke.csproj -c Release -r win-x64
+
+ - name: Run native binary
+ shell: bash
+ run: |
+ EXE="aot-smoke/Wolfgang.Etl.TestKit.AotSmoke/bin/Release/net10.0/win-x64/publish/Wolfgang.Etl.TestKit.AotSmoke.exe"
+ echo "Running: $EXE"
+ "$EXE"
diff --git a/aot-smoke/.editorconfig b/aot-smoke/.editorconfig
new file mode 100644
index 0000000..df8728a
--- /dev/null
+++ b/aot-smoke/.editorconfig
@@ -0,0 +1,14 @@
+# Isolation stub — companion to Directory.Build.props/.targets in this folder.
+#
+# .editorconfig is discovered by walking UP the directory tree, independently of
+# MSBuild's Directory.Build.props isolation, so without this the repo-root
+# .editorconfig's analyzer policy (CA2007 etc.) would still apply to the
+# throwaway AOT-smoke consumer. `root = true` stops that walk-up so the fixture
+# is fully exempt from repo lint — it is a publish-and-run smoke fixture, not
+# shipped code.
+root = true
+
+[*.cs]
+# The consumer is a console app with no synchronization context, so ConfigureAwait
+# is irrelevant; silence CA2007 explicitly in case SDK defaults ever enable it.
+dotnet_diagnostic.CA2007.severity = none
diff --git a/aot-smoke/Directory.Build.props b/aot-smoke/Directory.Build.props
new file mode 100644
index 0000000..7f8c619
--- /dev/null
+++ b/aot-smoke/Directory.Build.props
@@ -0,0 +1,9 @@
+
+
+
diff --git a/aot-smoke/Directory.Build.targets b/aot-smoke/Directory.Build.targets
new file mode 100644
index 0000000..2e60f34
--- /dev/null
+++ b/aot-smoke/Directory.Build.targets
@@ -0,0 +1,3 @@
+
+
+
diff --git a/aot-smoke/Wolfgang.Etl.TestKit.AotSmoke/Program.cs b/aot-smoke/Wolfgang.Etl.TestKit.AotSmoke/Program.cs
new file mode 100644
index 0000000..de48da9
--- /dev/null
+++ b/aot-smoke/Wolfgang.Etl.TestKit.AotSmoke/Program.cs
@@ -0,0 +1,60 @@
+// Native-AOT smoke consumer for Wolfgang.Etl.TestKit (#132).
+//
+// Exercises the public surface after native-AOT publish and returns a non-zero
+// exit code if any invariant is violated, so the CI job fails if the library
+// stops being AOT-safe at publish/run time. Kept deliberately simple — this is a
+// smoke test, not a behavioural test suite (those live in the unit-test project).
+
+using Wolfgang.Etl.TestKit;
+
+static int Fail(string message)
+{
+ Console.Error.WriteLine($"AOT smoke FAILED: {message}");
+ return 1;
+}
+
+// Extractor: yields the seeded items through the async-enumerable pipeline.
+var extractor = new TestExtractor(new[] { 1, 2, 3 });
+var extracted = new List();
+await foreach (var item in extractor.ExtractAsync())
+{
+ extracted.Add(item);
+}
+if (extracted.Count != 3)
+{
+ return Fail($"TestExtractor yielded {extracted.Count} items, expected 3");
+}
+
+// Transformer: pass-through over the extracted items.
+var transformer = new TestTransformer();
+var transformed = new List();
+await foreach (var item in transformer.TransformAsync(ToAsync(extracted)))
+{
+ transformed.Add(item);
+}
+if (transformed.Count != extracted.Count)
+{
+ return Fail($"TestTransformer yielded {transformed.Count} items, expected {extracted.Count}");
+}
+
+// Loader: collects everything the pipeline pushed.
+var loader = new TestLoader(collectItems: true);
+await loader.LoadAsync(ToAsync(transformed));
+var collected = loader.GetCollectedItems();
+if (collected is null || collected.Count != transformed.Count)
+{
+ return Fail($"TestLoader collected {(collected is null ? "null" : collected.Count.ToString())} items, expected {transformed.Count}");
+}
+
+Console.WriteLine($"AOT smoke OK: extracted {extracted.Count}, transformed {transformed.Count}, loaded {collected.Count}");
+return 0;
+
+static async IAsyncEnumerable ToAsync(IEnumerable items)
+{
+ foreach (var item in items)
+ {
+ yield return item;
+ }
+
+ await Task.CompletedTask;
+}
diff --git a/aot-smoke/Wolfgang.Etl.TestKit.AotSmoke/Wolfgang.Etl.TestKit.AotSmoke.csproj b/aot-smoke/Wolfgang.Etl.TestKit.AotSmoke/Wolfgang.Etl.TestKit.AotSmoke.csproj
new file mode 100644
index 0000000..c0d7014
--- /dev/null
+++ b/aot-smoke/Wolfgang.Etl.TestKit.AotSmoke/Wolfgang.Etl.TestKit.AotSmoke.csproj
@@ -0,0 +1,29 @@
+
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ latest
+ true
+ false
+ false
+
+ true
+
+
+
+
+
+
+