diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c413d69..dc3266c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -22,6 +22,8 @@ jobs:
run: dotnet restore ClaudeRevit\ClaudeRevit.csproj
- name: Build (Release, no deploy)
run: dotnet build ClaudeRevit\ClaudeRevit.csproj -c Release -p:SkipDeploy=true --no-restore
+ - name: Compile native acceptance harness (does not run without Revit)
+ run: dotnet build ClaudeRevit.NativeTests\ClaudeRevit.NativeTests.csproj -c Release -p:SkipDeploy=true
- name: Unit tests (pure logic, no Revit)
run: dotnet test ClaudeRevit.Tests\ClaudeRevit.Tests.csproj -c Release
- name: Pane checks (isolated WPF, no Revit or account)
diff --git a/.gitignore b/.gitignore
index 7999f3e..33515a3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,3 +27,7 @@ Thumbs.db
*.env
appsettings.Development.json
secrets.json
+
+# Local pane verification artifacts and release scanner reports
+pane-qa/
+release-security.json
diff --git a/ClaudeRevit.NativeTests/ClaudeRevit.NativeTests.csproj b/ClaudeRevit.NativeTests/ClaudeRevit.NativeTests.csproj
new file mode 100644
index 0000000..5702338
--- /dev/null
+++ b/ClaudeRevit.NativeTests/ClaudeRevit.NativeTests.csproj
@@ -0,0 +1,14 @@
+
+
+ net10.0-windows
+ true
+ enable
+ enable
+ x64
+ 2027
+ true
+
+
+
+
+
diff --git a/ClaudeRevit.NativeTests/GeometryFixtures.cs b/ClaudeRevit.NativeTests/GeometryFixtures.cs
new file mode 100644
index 0000000..b359b7f
--- /dev/null
+++ b/ClaudeRevit.NativeTests/GeometryFixtures.cs
@@ -0,0 +1,59 @@
+using System.IO;
+using System.Reflection;
+using System.Text.Json;
+using Autodesk.Revit.DB;
+using Autodesk.Revit.DB.Structure;
+using Autodesk.Revit.UI;
+using ClaudeRevit.Tools;
+
+namespace ClaudeRevit.NativeTests;
+
+internal static class GeometryFixtures
+{
+ public static object Bore(UIApplication app,JsonElement input)
+ {
+ var family=app.Application.OpenDocumentFile(input.GetProperty("section_file").GetString()!);
+ try
+ {
+ var extrusion=new FilteredElementCollector(family).OfClass(typeof(Extrusion)).Cast().Single();
+ var method=typeof(CreateParametricSection).Assembly.GetType("ClaudeRevit.Tools.ConnectionOpeningGeometry",true)!.GetMethod("Inspect")!;
+ JsonElement Inspect(XYZ origin,double diameter)=>JsonSerializer.SerializeToElement(method.Invoke(null,new object[]{extrusion,origin,XYZ.BasisZ,diameter,.1}));
+ var good=Inspect(XYZ.Zero,200);var offset=Inspect(new XYZ(5/304.8,0,0),200);var oversize=Inspect(XYZ.Zero,280);
+ if(!good.GetProperty("Verified").GetBoolean()||Math.Abs(good.GetProperty("DiameterMm").GetDouble()-276)>.001)throw new InvalidOperationException("Native through bore was not identified: "+good);
+ if(offset.GetProperty("Verified").ValueKind==JsonValueKind.True||oversize.GetProperty("Verified").ValueKind==JsonValueKind.True)throw new InvalidOperationException("Misaligned/oversized bolt was accepted.");
+ return new{passed=true,good,offset_rejected=offset,oversize_rejected=oversize,limitation="Actual bore geometry only; no capacity or thread engagement assertion."};
+ }
+ finally{family.Close(false);}
+ }
+ public static object ReferencePlacement(UIApplication app,JsonElement input)
+ {
+ var project=app.Application.NewProjectDocument(UnitSystem.Metric);
+ try
+ {
+ FamilyInstance beam;FamilySymbol symbol;
+ using(var tx=new Transaction(project,"QA native reference framing placement"))
+ {
+ tx.Start();var level=Level.Create(project,0);
+ if(!project.LoadFamily(input.GetProperty("family_file").GetString()!,out var family))throw new InvalidOperationException("Cannot load reference framing.");
+ symbol=(FamilySymbol)project.GetElement(family.GetFamilySymbolIds().First());symbol.Activate();project.Regenerate();
+ beam=project.Create.NewFamilyInstance(Line.CreateBound(new XYZ(0,0,1000/304.8),new XYZ(6000/304.8,0,1000/304.8)),symbol,level,StructuralType.Beam);
+ tx.Commit();
+ }
+ if(beam.Category.Id.Value!=(long)BuiltInCategory.OST_StructuralFraming||beam.StructuralType!=StructuralType.Beam)throw new InvalidOperationException("Reference is not native structural framing.");
+ double Length()=>beam.get_Parameter(new Guid("b62d0a35-0f0f-432d-9d3d-e821093a7d02")).AsDouble()*304.8;
+ double Width()
+ {
+ var all=new FilteredElementCollector(project).OfClass(typeof(FamilyInstance)).Cast().Where(f=>f.Id==beam.Id||f.SuperComponent?.Id==beam.Id);
+ var points=all.SelectMany(SyntheticFixtures.Solids).SelectMany(s=>s.Faces.Cast()).SelectMany(f=>f.Triangulate().Vertices).ToArray();
+ return (points.Max(p=>p.Y)-points.Min(p=>p.Y))*304.8;
+ }
+ var originalLength=Length();var originalWidth=Width();
+ using(var tx=new Transaction(project,"QA native section edit")){tx.Start();symbol.LookupParameter("Ширина").Set(260/304.8);tx.Commit();}
+ var editedWidth=Width();if(Math.Abs(editedWidth-260)>.01||Math.Abs(editedWidth-originalWidth)<.01)throw new InvalidOperationException("Native framing width did not follow type dimension: "+editedWidth);
+ using(var tx=new Transaction(project,"QA native placement length edit")){tx.Start();((LocationCurve)beam.Location).Curve=Line.CreateBound(new XYZ(0,0,1000/304.8),new XYZ(8000/304.8,0,1000/304.8));tx.Commit();}
+ var editedLength=Length();if(Math.Abs(editedLength-originalLength-2000)>.01)throw new InvalidOperationException("Actual family placement length did not follow curve edit.");
+ return new{passed=true,category=beam.Category.Name,structural_type=beam.StructuralType.ToString(),original_length_mm=originalLength,edited_length_mm=editedLength,original_width_mm=originalWidth,edited_width_mm=editedWidth,limitation="Downloaded box beam acceptance; this does not certify every generated structural template."};
+ }
+ finally{project.Close(false);}
+ }
+}
diff --git a/ClaudeRevit.NativeTests/NativeAcceptance.cs b/ClaudeRevit.NativeTests/NativeAcceptance.cs
new file mode 100644
index 0000000..10f663b
--- /dev/null
+++ b/ClaudeRevit.NativeTests/NativeAcceptance.cs
@@ -0,0 +1,114 @@
+using System.Diagnostics;
+using System.IO;
+using System.Reflection;
+using System.Security.Cryptography;
+using System.Text.Json;
+using Autodesk.Revit.DB;
+using Autodesk.Revit.UI;
+using ClaudeRevit.Tools;
+
+namespace ClaudeRevit.NativeTests;
+
+// Loaded in a separate AssemblyLoadContext from the installed add-in. Its
+// ExternalEvent runs after the launcher's transaction ends. No OnStartup, MCP
+// server, pane, or global updater is started by this acceptance harness.
+public static class NativeAcceptance
+{
+ private static readonly Handler HandlerInstance = new();
+ private static ExternalEvent? _event;
+ private static string? _pending;
+ private static bool _running;
+ public static string Queue(string requestPath)
+ {
+ if (_running || _pending != null) throw new InvalidOperationException("Acceptance job is already queued/running.");
+ _event ??= ExternalEvent.Create(HandlerInstance);
+ _pending = Path.GetFullPath(requestPath);
+ var result = _event.Raise();
+ if (result != ExternalEventRequest.Accepted) { _pending = null; throw new InvalidOperationException("ExternalEvent: " + result); }
+ return "queued";
+ }
+ private sealed class Handler : IExternalEventHandler
+ {
+ public string GetName() => "ClaudeRevit native acceptance (disposable documents only)";
+ public void Execute(UIApplication app)
+ {
+ var requestPath = _pending; _pending = null;
+ if (requestPath == null) return;
+ _running = true;
+ var clock = Stopwatch.StartNew();
+ var output = requestPath + ".result.json";
+ var openBefore = app.Application.Documents.Cast().ToArray();
+ var activeBefore = app.ActiveUIDocument?.Document;
+ var modifiedBefore = openBefore.ToDictionary(d => d, d => d.IsModified);
+ object? result = null; string? error = null;
+ try
+ {
+ File.WriteAllText(requestPath + ".started.json", JsonSerializer.Serialize(new { utc = DateTime.UtcNow, state = "running" }));
+ var failureType = typeof(InspectFamilyFiles).Assembly.GetType("ClaudeRevit.Tools.FamilyInspectionFailures", true)!;
+ using var backgroundFailures = (IDisposable)Activator.CreateInstance(failureType, app.Application)!;
+ using var request = JsonDocument.Parse(File.ReadAllText(requestPath));
+ var root = request.RootElement;
+ var action = root.GetProperty("action").GetString();
+ if (action == "tool")
+ {
+ var typeName = root.GetProperty("type").GetString()!;
+ var type = typeof(InspectFamilyFiles).Assembly.GetType("ClaudeRevit.Tools." + typeName, true)!;
+ if (Activator.CreateInstance(type) is not IRevitTool tool) throw new InvalidOperationException("Not a native tool.");
+ // This entry point inspects references only. Editing/model tests
+ // below must create their own disposable document explicitly.
+ if (tool is not (InspectFamilyFiles or AnalyzeFamilyStructure or FlexFamily)) throw new InvalidOperationException("Reference tool is not read-only or rollback-only.");
+ var input = root.GetProperty("arguments").EnumerateObject().ToDictionary(p => p.Name, p => p.Value.Clone());
+ result = JsonSerializer.Deserialize(tool.Execute(input, app));
+ }
+ else if (action == "sections") result = SyntheticFixtures.Sections(app, root);
+ else if (action == "checkpoint") result = SyntheticFixtures.Checkpoint(app, root);
+ else if (action == "nested") result = SyntheticFixtures.Nested(app, root);
+ else if (action == "live_schedule") result = SyntheticFixtures.LiveSchedule(app, root);
+ else if (action == "benchmark_probe") result = SyntheticFixtures.BenchmarkProbe(app);
+ else if (action == "dependent_node") result = SyntheticFixtures.DependentNode(app, root);
+ else if (action == "bore") result = GeometryFixtures.Bore(app,root);
+ else if (action == "reference_placement") result = GeometryFixtures.ReferencePlacement(app,root);
+ else if (action == "schedule_forms") result = SyntheticFixtures.ScheduleForms(app,root);
+ else if (action == "template_probe")
+ {
+ var rows = new List