diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 9ce74dd..6a22806 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -34,6 +34,13 @@ jobs:
run: dotnet run --project ClaudeRevit.CliTests -c Release
- name: ModPlus adapter checks (isolated Revit contracts, no installed ModPlus)
run: dotnet run --project ClaudeRevit.ModPlusTests -c Release
+ - name: Headless bindings and call-graph checks (no vendor algorithms executed)
+ shell: pwsh
+ run: |
+ dotnet build ClaudeRevit.HeadlessTests/Fixtures/GuardFixtures.csproj -c Release
+ if ($LASTEXITCODE -ne 0) { throw "Guard fixtures failed to build" }
+ dotnet run --project ClaudeRevit.HeadlessTests -c Release -- ClaudeRevit/bin/Release/ClaudeRevit.dll ClaudeRevit.HeadlessTests/Fixtures/bin/Release/net10.0/mprHeadlessFixtures.dll ClaudeRevit/Standards/ModPlusHeadless/bindings.json
+ if ($LASTEXITCODE -ne 0) { throw "Headless checks failed" }
- name: Compile installer detection check
uses: Minionguyjpro/Inno-Setup-Action@v1.2.2
with:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index aed995d..705abcb 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -78,6 +78,14 @@ jobs:
}
Get-ChildItem "ClaudeRevit-$ver-Revit*.zip" | Format-List Name, Length
+ - name: Headless adapter checks against the packaged Revit 2027 DLL
+ shell: pwsh
+ run: |
+ dotnet build ClaudeRevit.HeadlessTests/Fixtures/GuardFixtures.csproj -c Release
+ if ($LASTEXITCODE -ne 0) { throw "Guard fixtures failed to build" }
+ dotnet run --project ClaudeRevit.HeadlessTests -c Release -- installer/payload/2027/ClaudeRevit.dll ClaudeRevit.HeadlessTests/Fixtures/bin/Release/net10.0/mprHeadlessFixtures.dll ClaudeRevit/Standards/ModPlusHeadless/bindings.json
+ if ($LASTEXITCODE -ne 0) { throw "Headless checks failed" }
+
# Refuse to silently move a published release tag: a manual dispatch may only (re)tag
# a version that doesn't exist yet, or one that already points at this very commit.
- name: Guard against retagging an existing release (manual dispatch only)
diff --git a/ClaudeRevit.HeadlessTests/ClaudeRevit.HeadlessTests.csproj b/ClaudeRevit.HeadlessTests/ClaudeRevit.HeadlessTests.csproj
new file mode 100644
index 0000000..7baf111
--- /dev/null
+++ b/ClaudeRevit.HeadlessTests/ClaudeRevit.HeadlessTests.csproj
@@ -0,0 +1,4 @@
+
+ Exenet10.0-windowstruedisabledisablefalse
+
+
diff --git a/ClaudeRevit.HeadlessTests/Fixtures/GuardFixtures.cs b/ClaudeRevit.HeadlessTests/Fixtures/GuardFixtures.cs
new file mode 100644
index 0000000..aa0d7f5
--- /dev/null
+++ b/ClaudeRevit.HeadlessTests/Fixtures/GuardFixtures.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+
+namespace System.Windows
+{
+ public static class MessageBox { public static void Show(string value) => throw new Exception("A GUI fixture must never execute."); }
+ public class Window { public virtual void Show() => throw new Exception("A GUI fixture must never execute."); }
+}
+
+namespace mprHeadlessFixtures
+{
+ public static class Algorithms
+ {
+ public static int Sum(int a, int b) => a + b;
+ public static void DirectUi() => System.Windows.MessageBox.Show("dialog");
+ private static void Indirect() => DirectUi();
+ public static void TransitiveUi() => Indirect();
+ public static object NewWindow() => new System.Windows.Window();
+ public static IEnumerable DeferredUi() { System.Windows.MessageBox.Show("dialog"); yield return 1; }
+ public static async Task AsyncOperation() { await Task.Yield(); return 1; }
+ public static object ReflectiveCall(MethodInfo method) => method.Invoke(null, null);
+ public static void DelegateCall(Action action) => action();
+ public static int VirtualCall(Base value) => value.Calculate();
+ public static int StaticFieldCall() => UiInitializer.Value;
+ public static object LateUi() => new LateUiEnumerable();
+ [DllImport("user32.dll")] public static extern bool MessageBeep(uint type);
+ }
+ public class Base { public virtual int Calculate() => 1; }
+ public class Derived : Base { public override int Calculate() { System.Windows.MessageBox.Show("dialog"); return 1; } }
+ public static class UiInitializer { public static readonly int Value; static UiInitializer() { System.Windows.MessageBox.Show("dialog"); Value = 1; } }
+ public sealed class LateUiEnumerable : System.Collections.IEnumerable
+ {
+ public System.Collections.IEnumerator GetEnumerator() => new LateUiIterator();
+ }
+ public sealed class LateUiIterator : System.Collections.IEnumerator
+ {
+ public object Current { get { System.Windows.MessageBox.Show("dialog"); return 1; } }
+ public bool MoveNext() => true;
+ public void Reset() { }
+ }
+}
diff --git a/ClaudeRevit.HeadlessTests/Fixtures/GuardFixtures.csproj b/ClaudeRevit.HeadlessTests/Fixtures/GuardFixtures.csproj
new file mode 100644
index 0000000..342ce52
--- /dev/null
+++ b/ClaudeRevit.HeadlessTests/Fixtures/GuardFixtures.csproj
@@ -0,0 +1,11 @@
+
+
+ net10.0
+ mprHeadlessFixtures
+ false
+ disable
+ disable
+ true
+
+
+
diff --git a/ClaudeRevit.HeadlessTests/Program.cs b/ClaudeRevit.HeadlessTests/Program.cs
new file mode 100644
index 0000000..baaf39d
--- /dev/null
+++ b/ClaudeRevit.HeadlessTests/Program.cs
@@ -0,0 +1,106 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.Loader;
+using System.Text.Json;
+
+internal static class ManagedTests
+{
+ private static int passed;
+ private static void Check(bool success, string description)
+ {
+ if (!success) throw new Exception("FAILED: " + description);
+ passed++;
+ }
+ public static void Main(string[] args)
+ {
+ Assembly library = AssemblyLoadContext.Default.LoadFromAssemblyPath(Path.GetFullPath(args[0]));
+ Assembly fixture = AssemblyLoadContext.Default.LoadFromAssemblyPath(Path.GetFullPath(args[1]));
+ Type guard = library.GetType("ModPlus.Headless.UiCallGuard", true);
+ object instance = Activator.CreateInstance(guard, BindingFlags.Instance | BindingFlags.NonPublic, null, new object[] { fixture }, null);
+ MethodInfo inspect = guard.GetMethod("Check", BindingFlags.Instance | BindingFlags.NonPublic);
+ Type functions = fixture.GetType("mprHeadlessFixtures.Algorithms", true);
+ Check((bool)inspect.Invoke(instance, new object[] { functions.GetMethod("Sum") }) == false, "pure function allowed");
+ Check((bool)inspect.Invoke(instance, new object[] { functions.GetMethod("Sum") }) == false, "safe cache remains valid");
+ foreach (string name in new[] { "DirectUi", "TransitiveUi", "NewWindow", "DeferredUi", "AsyncOperation", "ReflectiveCall", "MessageBeep", "DelegateCall", "VirtualCall", "StaticFieldCall" })
+ {
+ for (int attempt = 0; attempt < 2; attempt++)
+ {
+ bool rejected = false;
+ try { inspect.Invoke(instance, new object[] { functions.GetMethod(name) }); }
+ catch (TargetInvocationException e) { rejected = e.InnerException?.GetType().FullName == "ModPlus.Headless.HeadlessBindingException"; }
+ Check(rejected, name + ": rejected on attempt " + attempt);
+ }
+ }
+ object unsafeEnumerable = Activator.CreateInstance(fixture.GetType("mprHeadlessFixtures.LateUiEnumerable", true));
+ MethodInfo enumerate = guard.GetMethod("Enumerate", BindingFlags.Instance | BindingFlags.NonPublic);
+ var deferred = (IEnumerable