diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 39371efa..7b40e20d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -207,7 +207,12 @@ jobs:
python-version: "3.13"
- uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
with:
- dotnet-version: "8.0.x"
+ # 8.0.x runs the pinned net8.0 extractor/probe; 9.0.x is only needed to BUILD the
+ # deliberately-incompatible net9 wrapper fixture in the step 11 Tier B suite (which
+ # then proves it is refused WRAPPER_RUNTIME_UNSUPPORTED under the fixed net8 probe).
+ dotnet-version: |
+ 8.0.x
+ 9.0.x
- name: Extract OwnIR facts from sample C#
run: |
dotnet run --project frontend/roslyn/OwnSharp.Extractor -- \
@@ -260,6 +265,10 @@ jobs:
env:
OWN_TIERB_REQUIRED: "1"
run: python tests/test_verify_delta_tierb.py
+ - name: S2 step 11 verified-target-wrapper gate (Tier B, full public CLI)
+ env:
+ OWN_TIERB_REQUIRED: "1"
+ run: python tests/test_verify_target_tierb.py
- name: Check facts through the core
run: |
out=$(python -m ownlang ownir "$RUNNER_TEMP/facts.json" || true)
diff --git a/frontend/roslyn/OwnSharp.WeakTargetProbe/OwnSharp.WeakTargetProbe.csproj b/frontend/roslyn/OwnSharp.WeakTargetProbe/OwnSharp.WeakTargetProbe.csproj
new file mode 100644
index 00000000..93f934d3
--- /dev/null
+++ b/frontend/roslyn/OwnSharp.WeakTargetProbe/OwnSharp.WeakTargetProbe.csproj
@@ -0,0 +1,24 @@
+
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+ OwnSharp.WeakTargetProbe
+ true
+
+
+
+
+
+
+
diff --git a/frontend/roslyn/OwnSharp.WeakTargetProbe/Program.cs b/frontend/roslyn/OwnSharp.WeakTargetProbe/Program.cs
new file mode 100644
index 00000000..b90d2916
--- /dev/null
+++ b/frontend/roslyn/OwnSharp.WeakTargetProbe/Program.cs
@@ -0,0 +1,760 @@
+// S2 Step 11 — OwnSharp.WeakTargetProbe. See the .csproj for the two-mode contract.
+using System.ComponentModel;
+using System.Reflection;
+using System.Reflection.Metadata;
+using System.Reflection.PortableExecutable;
+using System.Runtime.CompilerServices;
+using System.Runtime.Loader;
+using System.Text;
+using System.Text.Json;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Text;
+
+internal static class Program
+{
+ private static int Main(string[] args)
+ {
+ try
+ {
+ if (args.Length >= 1 && args[0] == "bind") return BindMode.Run(args);
+ if (args.Length >= 1 && args[0] == "probe") return ProbeMode.Run(args);
+ Console.Error.WriteLine("weak-target-probe: usage: (bind|probe) ...");
+ return 2;
+ }
+ catch (Exception e)
+ {
+ Console.Error.WriteLine($"weak-target-probe: internal error ({e.GetType().Name}: {e.Message})");
+ return 2;
+ }
+ }
+
+ // --- deterministic canonical JSON (sorted keys, compact, trailing LF) for a restricted
+ // value domain (bool / non-negative long / printable-ASCII string / array / object) so the
+ // bytes byte-match Python json.dumps(sort_keys=True, separators=(",",":"), ensure_ascii=False).
+ internal static void WriteCanonical(string path, object value)
+ {
+ var sb = new StringBuilder();
+ Emit(sb, value);
+ sb.Append('\n');
+ File.WriteAllBytes(path, Encoding.UTF8.GetBytes(sb.ToString()));
+ }
+
+ private static void Emit(StringBuilder sb, object? v)
+ {
+ switch (v)
+ {
+ case null: sb.Append("null"); break;
+ case bool b: sb.Append(b ? "true" : "false"); break;
+ case int i: sb.Append(i.ToString(System.Globalization.CultureInfo.InvariantCulture)); break;
+ case long l: sb.Append(l.ToString(System.Globalization.CultureInfo.InvariantCulture)); break;
+ case string s: EmitString(sb, s); break;
+ case IReadOnlyList