diff --git a/docs/plans/evidence/2026-11-13-historian-700-vd03/gate700-vd03-fea7de0a.json.gz b/docs/plans/evidence/2026-11-13-historian-700-vd03/gate700-vd03-fea7de0a.json.gz
new file mode 100644
index 000000000..7ee8f40c0
Binary files /dev/null and b/docs/plans/evidence/2026-11-13-historian-700-vd03/gate700-vd03-fea7de0a.json.gz differ
diff --git a/docs/plans/evidence/2026-11-13-historian-700-vd03/probe/Gate700Probe.csproj b/docs/plans/evidence/2026-11-13-historian-700-vd03/probe/Gate700Probe.csproj
new file mode 100644
index 000000000..896bd24d3
--- /dev/null
+++ b/docs/plans/evidence/2026-11-13-historian-700-vd03/probe/Gate700Probe.csproj
@@ -0,0 +1,14 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ false
+ false
+
+
+
+
+
diff --git a/docs/plans/evidence/2026-11-13-historian-700-vd03/probe/Program.cs b/docs/plans/evidence/2026-11-13-historian-700-vd03/probe/Program.cs
new file mode 100644
index 000000000..b8122fc2e
--- /dev/null
+++ b/docs/plans/evidence/2026-11-13-historian-700-vd03/probe/Program.cs
@@ -0,0 +1,202 @@
+// Read-only #700 probe against a deployed OtOpcUa endpoint (lmxopcua#700).
+// Sends only Browse/Read/HistoryRead. Credential: env OTOPCUA_PROBE_APIKEY (otua__), never printed.
+// Usage: dotnet run --
+using System.Globalization;
+using System.Text.Json;
+using Opc.Ua;
+using Opc.Ua.Client;
+
+var endpointUrl = args.Length > 0 ? args[0] : "opc.tcp://wonder-app-vd03.zmr.zimmer.com:4840/OtOpcUa";
+var outPath = args.Length > 1 ? args[1] : "gate700-vd03.json";
+// "focas" mode (args[2]): anonymous session; reads ServiceLevel and every Variable under the FOCAS driver's raw subtree.
+var focasMode = args.Length > 2 && args[2] == "focas";
+var key = focasMode ? null : Environment.GetEnvironmentVariable("OTOPCUA_PROBE_APIKEY") ?? throw new InvalidOperationException("OTOPCUA_PROBE_APIKEY not set");
+var ct = CancellationToken.None;
+var telemetry = DefaultTelemetry.Create(_ => { });
+var pki = Path.Combine(Path.GetTempPath(), $"gate700-probe-pki-{Guid.NewGuid():N}");
+var appConfig = new ApplicationConfiguration(telemetry)
+{
+ ApplicationName = "OtOpcUa.Gate700Probe",
+ ApplicationUri = $"urn:OtOpcUa.Gate700Probe.{Guid.NewGuid():N}",
+ ApplicationType = ApplicationType.Client,
+ SecurityConfiguration = new SecurityConfiguration
+ {
+ ApplicationCertificate = new CertificateIdentifier { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(pki, "own") },
+ TrustedIssuerCertificates = new CertificateTrustList { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(pki, "issuer") },
+ TrustedPeerCertificates = new CertificateTrustList { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(pki, "trusted") },
+ RejectedCertificateStore = new CertificateTrustList { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(pki, "rejected") },
+ AutoAcceptUntrustedCertificates = true,
+ },
+ ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 120_000 },
+};
+await appConfig.ValidateAsync(ApplicationType.Client, ct);
+var endpoint = await CoreClientUtils.SelectEndpointAsync(appConfig, endpointUrl, false, telemetry, ct);
+using var session = await new DefaultSessionFactory(telemetry).CreateAsync(appConfig,
+ new ConfiguredEndpoint(null, endpoint, EndpointConfiguration.Create(appConfig)), false, false, "Gate700Probe", 120_000,
+ key is null ? new UserIdentity() : new UserIdentity("apikey", System.Text.Encoding.UTF8.GetBytes(key)), default, ct);
+
+if (focasMode)
+{
+ var ns = (ushort)session.NamespaceUris.GetIndex("https://zb.com/otopcua/raw");
+ var slv = await session.ReadAsync(null, 0, TimestampsToReturn.Neither, new[] { new ReadValueId { NodeId = VariableIds.Server_ServiceLevel, AttributeId = Attributes.Value } }, ct);
+ Console.WriteLine($"ServiceLevel={Fmt(Clr(slv.Results.ToList()[0].WrappedValue))}");
+ var vars = new List(); var queue = new Queue(new[] { new NodeId("focas-z-34184", ns) });
+ while (queue.Count > 0)
+ {
+ var b = await session.BrowseAsync(null, null, 0, new[] { new BrowseDescription { NodeId = queue.Dequeue(), BrowseDirection = BrowseDirection.Forward,
+ ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences, IncludeSubtypes = true, ResultMask = (uint)BrowseResultMask.All } }, ct);
+ foreach (var r in b.Results.ToList()[0].References.ToList())
+ {
+ var id = ExpandedNodeId.ToNodeId(r.NodeId, session.NamespaceUris);
+ if (r.NodeClass == NodeClass.Variable) vars.Add(id); else if (r.NodeClass == NodeClass.Object) queue.Enqueue(id);
+ }
+ }
+ var rv = await session.ReadAsync(null, 0, TimestampsToReturn.Both, vars.Select(v => new ReadValueId { NodeId = v, AttributeId = Attributes.Value }).ToArray(), ct);
+ var res = rv.Results.ToList();
+ var byStatus = res.GroupBy(r => SName(r.StatusCode)).Select(g => $"{g.Key}={g.Count()}");
+ Console.WriteLine($"FOCAS raw variables={vars.Count}: {string.Join(", ", byStatus)}");
+ for (var i = 0; i < vars.Count; i++) if (res[i].StatusCode.Code != 0) Console.WriteLine($" {vars[i].Identifier} {SName(res[i].StatusCode)}");
+ await session.CloseAsync(ct);
+ return 0;
+}
+
+var report = new Dictionary();
+var cases = new List>();
+report["endpoint"] = endpointUrl;
+report["securityMode"] = endpoint.SecurityMode.ToString();
+report["runUtc"] = DateTime.UtcNow.ToString("o");
+
+var rawNs = session.NamespaceUris.GetIndex("https://zb.com/otopcua/raw");
+NodeId N(string tag) => new($"gate700-hist/Device1/{tag}", (ushort)rawNs);
+var wtc1 = N("wtc1-float"); var sec = N("systimesec-byte"); var pulse = N("syspulse-byte"); var missing = N("missing-double");
+
+// ServiceLevel + server build
+var sl = await session.ReadAsync(null, 0, TimestampsToReturn.Neither, new[] {
+ new ReadValueId { NodeId = VariableIds.Server_ServiceLevel, AttributeId = Attributes.Value },
+ new ReadValueId { NodeId = VariableIds.Server_ServerStatus_BuildInfo_SoftwareVersion, AttributeId = Attributes.Value },
+ new ReadValueId { NodeId = VariableIds.Server_ServerStatus_BuildInfo_BuildNumber, AttributeId = Attributes.Value } }, ct);
+var slr = sl.Results.ToList();
+report["serviceLevel"] = Fmt(Clr(slr[0].WrappedValue));
+report["softwareVersion"] = Fmt(Clr(slr[1].WrappedValue));
+report["buildNumber"] = Fmt(Clr(slr[2].WrappedValue));
+
+// Node attributes
+var attrs = new Dictionary();
+foreach (var (name, node) in new[] { ("wtc1-float", wtc1), ("systimesec-byte", sec), ("syspulse-byte", pulse), ("missing-double", missing) })
+{
+ var ids = new (string Name, uint Id)[] { ("Historizing", Attributes.Historizing), ("AccessLevel", Attributes.AccessLevel),
+ ("UserAccessLevel", Attributes.UserAccessLevel), ("DataType", Attributes.DataType) };
+ var r = await session.ReadAsync(null, 0, TimestampsToReturn.Neither, ids.Select(a => new ReadValueId { NodeId = node, AttributeId = a.Id }).ToArray(), ct);
+ var rr = r.Results.ToList();
+ attrs[name] = ids.Select((a, i) => (a.Name, V: rr[i].StatusCode.Code == 0 ? Fmt(Clr(rr[i].WrappedValue)) : SName(rr[i].StatusCode)))
+ .ToDictionary(x => x.Name, x => (object?)x.V);
+}
+report["nodeAttributes"] = attrs;
+
+// Aggregates advertised
+var aggBrowse = await session.BrowseAsync(null, null, 0, new[] { new BrowseDescription {
+ NodeId = ObjectIds.HistoryServerCapabilities_AggregateFunctions, BrowseDirection = BrowseDirection.Forward,
+ ReferenceTypeId = ReferenceTypeIds.Organizes, IncludeSubtypes = true, ResultMask = (uint)BrowseResultMask.All } }, ct);
+report["aggregateFunctions"] = string.Join(",", aggBrowse.Results.ToList()[0].References.ToList().Select(r => r.BrowseName.Name).OrderBy(n => n, StringComparer.Ordinal));
+
+DateTime U(string s) => DateTime.Parse(s, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
+
+// ---- cases
+await Raw("missing raw", missing, U("2026-11-11T14:00:00Z"), U("2026-11-11T14:01:00Z"), 0);
+await Processed("missing Average 20s", missing, U("2026-11-11T14:00:00Z"), U("2026-11-11T14:01:00Z"), 20_000, ObjectIds.AggregateFunction_Average);
+await Processed("missing TimeAverage 20s", missing, U("2026-11-11T14:00:00Z"), U("2026-11-11T14:01:00Z"), 20_000, ObjectIds.AggregateFunction_TimeAverage);
+await AtTime("missing AtTime", missing, U("2026-11-11T14:00:00Z"), U("2026-11-11T14:00:30Z"));
+
+await Raw("SysTimeSec raw 10s", sec, U("2026-11-11T14:00:00Z"), U("2026-11-11T14:00:10Z"), 0);
+await AtTime("SysTimeSec AtTime stored+mid", sec, U("2026-11-11T14:00:00Z"), U("2026-11-11T14:00:01.5Z"));
+foreach (var (n, a) in new[] { ("Average", ObjectIds.AggregateFunction_Average), ("Minimum", ObjectIds.AggregateFunction_Minimum),
+ ("Maximum", ObjectIds.AggregateFunction_Maximum), ("Count", ObjectIds.AggregateFunction_Count), ("Start", ObjectIds.AggregateFunction_Start),
+ ("End", ObjectIds.AggregateFunction_End), ("TimeAverage", ObjectIds.AggregateFunction_TimeAverage), ("Total", ObjectIds.AggregateFunction_Total),
+ ("TimeAverage2", ObjectIds.AggregateFunction_TimeAverage2) })
+ await Processed($"SysTimeSec {n} 20s", sec, U("2026-11-11T14:00:00Z"), U("2026-11-11T14:01:00Z"), 20_000, a);
+await Processed("SysTimeSec Average 20s reverse", sec, U("2026-11-11T14:01:00Z"), U("2026-11-11T14:00:00Z"), 20_000, ObjectIds.AggregateFunction_Average);
+await Modified("SysTimeSec ReadModified", sec, U("2026-11-11T14:00:00Z"), U("2026-11-11T14:01:00Z"));
+
+await Raw("SysPulse raw 3min", pulse, U("2026-11-11T14:00:00Z"), U("2026-11-11T14:03:00Z"), 0);
+await AtTime("SysPulse AtTime", pulse, U("2026-11-11T14:00:30Z"), U("2026-11-11T14:01:30Z"));
+await Processed("SysPulse Average 60s", pulse, U("2026-11-11T14:00:00Z"), U("2026-11-11T14:03:00Z"), 60_000, ObjectIds.AggregateFunction_Average);
+
+await Raw("WTC1 raw 10s", wtc1, U("2026-11-11T14:00:00Z"), U("2026-11-11T14:00:10Z"), 0);
+await AtTime("WTC1 AtTime", wtc1, U("2026-11-11T14:00:00Z"), U("2026-11-11T14:00:01Z"));
+await Processed("WTC1 Average 60s over the NULL outage", wtc1, U("2026-11-10T11:31:00Z"), U("2026-11-10T11:40:00Z"), 60_000, ObjectIds.AggregateFunction_Average);
+await Processed("WTC1 TimeAverage 60s", wtc1, U("2026-11-10T11:31:00Z"), U("2026-11-10T11:40:00Z"), 60_000, ObjectIds.AggregateFunction_TimeAverage);
+await Raw("WTC1 end-only latest 3 before 11:38 (stopped tag reverse), first page", wtc1, DateTime.MinValue, U("2026-11-10T11:38:00Z"), 3, maxPages: 1);
+await Raw("WTC1 end-only latest 3 before 11:38, 100 pages", wtc1, DateTime.MinValue, U("2026-11-10T11:38:00Z"), 3);
+await Raw("WTC1 reverse raw 11:32:40 -> 11:32:20", wtc1, U("2026-11-10T11:32:40Z"), U("2026-11-10T11:32:20Z"), 0);
+
+var now = DateTime.UtcNow;
+await AtTime("WTC1 future AtTime +30m/+1h/+2h", wtc1, now.AddMinutes(30), now.AddHours(1), now.AddHours(2));
+await AtTime("SysTimeSec future AtTime +30m/+1h/+2h", sec, now.AddMinutes(30), now.AddHours(1), now.AddHours(2));
+
+report["cases"] = cases;
+await File.WriteAllTextAsync(outPath, JsonSerializer.Serialize(report, new JsonSerializerOptions { WriteIndented = true }), ct);
+await session.CloseAsync(ct);
+foreach (var c in cases) Console.WriteLine($"{c["case"],-58} {c["status"],-34} rows={c["rows"]} types={c["types"]} {c["ms"]}ms");
+try { Directory.Delete(pki, true); } catch { }
+return 0;
+
+// ---------------------------------------------------------------- helpers
+async Task Raw(string name, NodeId node, DateTime start, DateTime end, uint max, int maxPages = 100)
+{
+ var d = new ReadRawModifiedDetails { StartTime = start, EndTime = end, NumValuesPerNode = max, IsReadModified = false, ReturnBounds = false };
+ await Run(name, node, d, null, maxPages);
+}
+async Task Modified(string name, NodeId node, DateTime start, DateTime end)
+{
+ var d = new ReadRawModifiedDetails { StartTime = start, EndTime = end, NumValuesPerNode = 0, IsReadModified = true };
+ await Run(name, node, d);
+}
+async Task Processed(string name, NodeId node, DateTime start, DateTime end, double interval, NodeId agg)
+{
+ var d = new ReadProcessedDetails { StartTime = start, EndTime = end, ProcessingInterval = interval,
+ AggregateType = new List { agg }, AggregateConfiguration = new AggregateConfiguration { UseServerCapabilitiesDefaults = true } };
+ await Run(name, node, d);
+}
+async Task AtTime(string name, NodeId node, params DateTime[] times)
+{
+ var d = new ReadAtTimeDetails { ReqTimes = times.Select(t => (DateTimeUtc)t).ToList(), UseSimpleBounds = true };
+ await Run(name, node, d, times);
+}
+async Task Run(string name, NodeId node, IEncodeable details, DateTime[]? requested = null, int maxPages = 100)
+{
+ var sw = System.Diagnostics.Stopwatch.StartNew();
+ var rows = new List>();
+ var types = new SortedSet(StringComparer.Ordinal);
+ ByteString cp = default; string status; var pages = 0;
+ do
+ {
+ var resp = await session.HistoryReadAsync(null, new ExtensionObject(details), TimestampsToReturn.Both, false,
+ new HistoryReadValueId[] { new() { NodeId = node, ContinuationPoint = cp } }, ct);
+ var res = resp.Results.ToList()[0];
+ status = SName(res.StatusCode); cp = res.ContinuationPoint; pages++;
+ if (!res.HistoryData.IsNull && ExtensionObject.ToEncodeable(res.HistoryData) is HistoryData hd)
+ foreach (var dv in hd.DataValues.ToList())
+ {
+ var v = Clr(dv.WrappedValue);
+ if (v is not null) types.Add(v.GetType().Name);
+ rows.Add(new() { ["v"] = Fmt(v), ["t"] = v?.GetType().Name, ["s"] = $"0x{dv.StatusCode.Code:X8}",
+ ["src"] = ((DateTime)dv.SourceTimestamp).ToString("o") });
+ }
+ if (status.StartsWith("Bad", StringComparison.Ordinal)) break;
+ } while (!cp.IsEmpty && pages < maxPages);
+ sw.Stop();
+ var leftOpen = !cp.IsEmpty;
+ if (leftOpen) // release the server-side continuation point
+ await session.HistoryReadAsync(null, new ExtensionObject(details), TimestampsToReturn.Both, true,
+ new HistoryReadValueId[] { new() { NodeId = node, ContinuationPoint = cp } }, ct);
+ var c = new Dictionary { ["case"] = name, ["node"] = node.ToString(), ["status"] = status, ["pages"] = pages,
+ ["rows"] = rows.Count, ["types"] = string.Join("|", types), ["ms"] = sw.ElapsedMilliseconds, ["continuationLeft"] = leftOpen, ["data"] = rows };
+ if (requested is not null) c["requested"] = requested.Select(t => t.ToString("o")).ToList();
+ cases.Add(c);
+}
+static object? Clr(Variant value) => value.AsBoxedObject(Variant.BoxingBehavior.Legacy) switch
+{
+ ByteString b => b.IsNull ? null : b.ToArray(), DateTimeUtc t => (DateTime)t, Uuid id => (Guid)id, var o => o,
+};
+static string? Fmt(object? v) => v switch { null => null, DateTime d => d.ToString("o"), IFormattable f => f.ToString(null, CultureInfo.InvariantCulture), _ => v.ToString() };
+static string SName(StatusCode code) => code.SymbolicId is { Length: > 0 } s ? s : $"0x{code.Code:X8}";
diff --git a/docs/plans/evidence/2026-11-13-historian-700-vd03/vd03-endpoint-gate.md b/docs/plans/evidence/2026-11-13-historian-700-vd03/vd03-endpoint-gate.md
new file mode 100644
index 000000000..ac5078a38
--- /dev/null
+++ b/docs/plans/evidence/2026-11-13-historian-700-vd03/vd03-endpoint-gate.md
@@ -0,0 +1,130 @@
+# #700: HistoryRead through the deployed DEV endpoint (wonder-app-vd03, 2026-11-13)
+
+**Scope.** The one gap [#700](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/700) still had after the in-process gate
+([`../2026-11-12-historian-694-700-746/aveva-gate.md`](../2026-11-12-historian-694-700-746/aveva-gate.md)): the same reads
+through a **deployed** Host's OPC UA endpoint. Owner-approved config deploy on DEV vd03 only. Everything ran over HTTP
+(AdminUI), SMB (logs, binaries) and OPC UA from the Mac; no WinRM.
+
+**Result.** Every check that the deployed build and the no-write rule allow passed, and the wire matched the in-process
+gate. Two acceptance items could not be run here, and why is the main finding:
+binding a history node to an existing AVEVA tag as **Int32 or Boolean** would make OtOpcUa call the gateway's
+**`EnsureTags`**, an upsert that rewrites the AVEVA tag (see [Binding](#binding-and-the-no-write-rule)). The integer case
+ran on a **Byte** node instead. Boolean was not run here; it stays covered by the in-process gate only.
+
+## Versions
+
+| | |
+|---|---|
+| OtOpcUa on vd03 | **`fea7de0a`** (`OtOpcUa.Host.dll` informational version `1.0.0+fea7de0aefd9…`, read over SMB). SDK server 2.0.1, HistorianGateway Client 1.0.0. This predates `53978ca6` (native TimeAverage/Total on by default), so on vd03 the pair is still opt-in and off. |
+| HistorianGateway on vd03 | **`ea9f21c`** (`ZB.MOM.WW.HistorianGateway.Server.dll` `1.0.0+ea9f21c850fb…`), `ServerHistorian` → `http://localhost:5221`. |
+| Historian | AVEVA 2023 R2 on wonder-sql-vd03 (via the gateway). |
+| Client | [`probe/`](probe/) (SDK 2.0.1 client, `None` security, UserName `apikey` with a temporary key). Sends only Browse, Read and HistoryRead. |
+
+## Binding and the no-write rule
+
+A node is history-readable only when it is an authored **raw tag** with `isHistorized: true`; `historianTagname` overrides
+the AVEVA tag name (`AddressSpaceComposer` → `AddressSpaceApplier.MaterialiseRawSubtree`). Discovered nodes and virtual
+tags do not register history.
+
+Every **added** historized raw tag also goes to `AddressSpaceApplier.ProvisionHistorizedTags` → `GatewayTagProvisioner` →
+gateway **`EnsureTags`**, which is `Tags.AddAsync` on the historian: create **or update**. Nothing turns this off except
+`ServerHistorian:Enabled=false`, which also turns off the reads. vd03's gateway key (`OtOpcUa-wonder`, HG `/apikeys`)
+carries `historian:tags:write`. So an `Int32` node on `SysTimeSec` would re-ensure that tag as Int4 with OtOpcUa's
+description and storage rate, and a `Boolean` node on `SysPulse` would re-ensure a discrete tag as an Int2 analog.
+
+The applier skips a tag whose `DataType` string does not parse as `DriverDataType` (Debug log, no request). The AdminUI's
+and node manager's type names are OPC UA names, so **`Float`, `Double`, `Byte` and `SByte` never provision** (filed as a
+bug: [#898](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/898)). This gate used only those types. That makes the
+gate zero-write by construction, and the gateway's own counters confirm it (below).
+
+Other write paths: `ContinuousHistorization:Enabled=false` and `AlarmHistorian:Enabled=false` in vd03's
+`appsettings.json`, with no environment override (SMB read). So no recorder and no alarm sink were running.
+
+## Temporary configuration
+
+Authored in the AdminUI (`/scripts`, `/raw`, with the four tags imported from a CSV). Nothing under the FOCAS driver was touched.
+
+| Object | Value |
+|---|---|
+| Script | `SC-cdff9670531a` `gate700-const`, `return 0;` |
+| Driver | `gate700-hist`, type **Calculation** (no I/O, no endpoint), auto-created device `Device1` |
+| Tags (all Read, `scriptId` above, `changeTriggered:false`, `timerIntervalMs:3600000`, `isHistorized:true`) | `wtc1-float` **Float** → `ZIPSEN.WTC1`; `systimesec-byte` **Byte** → `SysTimeSec`; `syspulse-byte` **Byte** → `SysPulse`; `missing-double` **Double** → `OtOpcUa.Gate700.DoesNotExist` |
+
+## Deployments
+
+| | Deployment | Revision | |
+|---|---|---|---|
+| Before | `9adc5590` | `fa9a2e28c1fd…` | In sync, Applied (2026-11-12 11:06Z) |
+| Temporary | `a63e262f` | `c4f8d8dfbb61…` | 05:34:04Z, Applied. Log: `applied plan (kind="PureAdd", added=7 …)`, `raw subtree materialised (… tags=4 … failed=0)`. **No** `historian provisioning completed` line, so nothing was dispatched. |
+| Cleanup 1 | `246c6c45` | `e9f687b7c403…` | Tags, device, driver and script removed: `PureRemove, removed=6`. The revision still differed because **the artifact carries the `ApiKeys` table** (hashed secrets only) and the temporary key still existed. |
+| After | **`dc0af731`** | **`fa9a2e28c1fd…`** | After the key was deleted: the **same revision hash as before**, so the configuration is byte-identical to the original. In sync, Applied 05:40:55Z. |
+
+## Results
+
+Probe JSON: [`gate700-vd03-fea7de0a.json.gz`](gate700-vd03-fea7de0a.json.gz) (sha256 of the JSON `172cb3b9c1dd…`), run 2026-11-13 ~05:36Z.
+ServiceLevel 250 throughout. All four nodes: `Historizing=true`, `AccessLevel=5` (CurrentRead | HistoryRead).
+DataType `i=10` (Float), `i=3` (Byte), `i=3`, `i=11` (Double). Advertised aggregates: Average, Count, Delta, End, Maximum,
+MaximumActualTime, Minimum, MinimumActualTime, Range, Start (10, so no TimeAverage/Total on this build).
+
+| Case | Result |
+|---|---|
+| Missing tag Raw | `GoodNoData`, 0 rows |
+| Missing tag Average (3 × 20 s) | 3 buckets `BadNoData \| Calculated` (`0x809B0401`), null |
+| Missing tag AtTime (2 times) | 2 × `BadNoData` (`0x809B0000`) |
+| `SysTimeSec` Raw 14:00:00–:10 (2026-11-11) | 10 rows 0..9, typed **Byte** (node DataType) |
+| `SysTimeSec` AtTime :00 / :01.5 | 0 Good; 2 `Interpolated` (`0x00000402`), Byte |
+| `SysTimeSec` Average / Min / Max / Count / Start / End, 20 s | 9.5/29.5/49.5; 0/20/40; 19/39/59; 20/20/20 (Int32); 0/20/40; 19/39/59 (End stamped at the sample). Processed values Double, as in-process. |
+| `SysTimeSec` Average reverse (14:01 → 14:00) | 3 buckets newest first, stamped with the later edge |
+| `SysPulse` (discrete) on a Byte node | Raw 0/1/0 each minute (Byte); AtTime :30 → 1 Interpolated (a linear midpoint on an integer node, see note 2); Average 0/1/0 |
+| `ZIPSEN.WTC1` Raw / AtTime | 5 rows typed **Single**; AtTime Interpolated, Single |
+| `ZIPSEN.WTC1` Average 60 s, 11:31–11:40 (2026-11-10, platform stop) | 11:31 Good 68.09; 11:32 Uncertain (`0x40A40401`); 11:33–11:37 `BadNoData \| Calculated`; 11:38 Good; 11:39 `BadNoData`. Same shape as in-process. |
+| Stopped-tag reverse, end-only latest 3 before 11:38 | First page: the NULL at 11:32:36.073 (`0x80000000`), 11:32:33.620, 11:32:31.619, newest first, **96 ms**. Following the continuation points for 100 pages (300 rows) took 5.1 s. Bounded reverse 11:32:40 → 11:32:20: 8 rows newest first. |
+| Future AtTime (+30 min / +1 h / +2 h) | 3 rows in request order, Uncertain (`0x40A40402`) holds of the last value, Single and Byte |
+| TimeAverage / Total / TimeAverage2 | `BadAggregateNotSupported` on every node: the expected refusal on `fea7de0a` with `ServerHistorian:Gateway:EnableNativeTimeWeighted` off. The native pair is served from `53978ca6` on (#694) and is not deployed on vd03. |
+| ReadModified | `BadHistoryOperationUnsupported` |
+
+**Not run on the deployed endpoint** (each is covered by the in-process gate only):
+- **Boolean node DataType**: needs an `EnsureTags` on an existing AVEVA tag ([#899](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/899)).
+- **Int32 node DataType**: same reason. Integer typing was shown on Byte instead.
+- **Transport outage → `BadNoCommunication`**: needs vd03's gateway stopped.
+- **Strict ceiling and aggregate cooldown**: vd03's gateway is not strict, and changing its read limits needs a restart.
+
+## Historian-write check
+
+HistorianGateway `/metrics` (`historian_gateway_grpc_requests_total`, counters since the gateway's 2026-11-12 12:04 start):
+
+| Snapshot | Methods seen |
+|---|---|
+| Before (05:27Z) | `HistorianStatus/Probe` only |
+| After the temporary deploy (05:34Z) | `Probe` only, so the deploy sent no `EnsureTags` |
+| After both probe runs and cleanup (05:41Z) | `Probe` 364, `HistorianTags/GetTagMetadata` 5, `HistorianRead/ReadRaw` 271. No `EnsureTags`, `AddTagExtendedProperties`, `WriteLiveValues`, `AddHistoricalValues` or `SendEvent`. |
+
+AtTime and the processed reads reach the gateway as `ReadRaw` (`GatewayAtTimeFromRaw`, raw-evaluated aggregates), and the
+missing tag as `GetTagMetadata`.
+
+## Cleanup
+
+- Temporary tags, device, driver and script deleted; redeployed to revision `fa9a2e28c1fd…` (the original).
+- Temporary OPC UA key `gate700-temp-probe` (`cef71ff646a1cf7c`) deleted. Negative control: the same token then gets
+ `BadIdentityTokenRejected`. The token was never printed or written to disk.
+- After cleanup: `/healthz`, `/health/ready`, `/health/active` 200; `/fleet` Applied; ServiceLevel 250. FOCAS raw subtree
+ (anonymous read): 22 variables, 21 Good, `Program/BlockCount` `BadNoData` (an unreported program field, `26b512ad`).
+- **No history probe was kept on DEV.** It caused no writes, but it would start writing as soon as either of two things
+ happened: someone changed a type to Int32 or Boolean, or #898 was fixed so that `Float` provisions. Either way it would
+ re-ensure `ZIPSEN.WTC1` / `SysTimeSec` / `SysPulse`.
+
+## Notes
+
+1. The SDK logs `The domain 'opc.tcp://wonder-app-vd03.zmr.zimmer.com:4840/OtOpcUa' is not listed in the server
+ certificate` for every client that uses the FQDN. This is pre-existing; the client still connects.
+2. An integer node interpolates AtTime linearly, and the value is converted to the node type. On a Byte node over a
+ discrete tag this produces a 1 at the midpoint of a 0 → 1 step. The in-process gate's Boolean node did not do this:
+ it held `false`. Binding a discrete tag to an integer node is this gate's workaround, not a supported pattern.
+
+## Reproduce
+
+```bash
+cd docs/plans/evidence/2026-11-13-historian-700-vd03/probe
+OTOPCUA_PROBE_APIKEY= dotnet run -c Release -- opc.tcp://:4840/OtOpcUa out.json
+dotnet run -c Release -- opc.tcp://:4840/OtOpcUa /dev/null focas # anonymous ServiceLevel + FOCAS raw read
+```