8f4a188f788ca9cef60cec28481ebd4fa781d134
56 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8f4a188f78 |
Event-row parser: verify against the provided 2023 R2 client; fix latent multi-row bug
Used the provided stock client as an oracle to verify the event read path. The capture-event harness returns 50 real events, and the instrument-grpc-nonstream rewrite captured the exact GetNextEventQueryResultBuffer.result buffer (63,192 bytes, version 0x0B=11, rowCount 50 = 25 Alarm.Set + 25 Alarm.Clear). Feeding that real buffer through HistorianEventRowProtocol.Parse exposed a latent parser bug. The real buffer layout is: version(2) + rowCount(4) + headerField(4, =0x1E) followed by MARKERLESS rows (rowFormat(2)=7 + filetime(8) + 8x u16 slots + compact-ascii type + propCount + props). The parser wrongly treated the one-time 0x1E field as a per-row marker and re-consumed [marker+format] for every row, so it decoded only the FIRST row of any multi-row buffer and stopped. This is not gRPC-specific: the captured WCF v9 buffer has the identical 0900 <rowCount> 1E000000 0700 header, so the shipped WCF event read had the same latent multi-row truncation. Fix: read a 10-byte buffer header (skip the 0x1E field once) and parse markerless rows; accept container version 9 (WCF) and 11 (gRPC), mirroring the interface-version gate that accepts History 11 and 12. Verified: the real 50-row buffer now decodes to exactly 50 events, ending cleanly at end-of-buffer (Parse_RealStockClientCapture_DecodesAllEvents, gated on HISTORIAN_EVENT_CAPTURE_NDJSON so it skips without the gitignored capture), plus a synthetic v11 golden test. 328 offline tests pass. The parse path is now verified against the provided client's real event data on both transports; the only remaining gap for gRPC events is the server delivering rows to our connection (the documented retrieval-server-gate). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
b0388e7a40 |
gRPC events: disprove transport hypothesis (native HTTP/2 also returns zero rows)
Tested grpc-event-query-capture.md's leading next-session hypothesis — that the native client's Grpc.Core HTTP/2 transport (vs our Grpc.Net.Client + GrpcWebHandler gRPC-Web) is why event reads return zero rows. Added HistorianGrpcChannelFactory .CreateHttp2 (plain HTTP/2 over SocketsHttpHandler, no gRPC-Web wrap) and an HISTORIAN_GRPC_EVENT_HTTP2 switch on the event orchestrator (event path only; reads stay gRPC-Web). Live side-by-side against the event-bearing 2023 R2 server, everything else held constant: the full v8 chain (ExchangeKey auth, CM_EVENT RegisterTags/EnsureTags=True, StartEventQuery with a valid handle) runs end-to-end over BOTH native HTTP/2 and gRPC-Web, and the server returns the byte-identical version-11 rowCount-0 terminal (0B00000000001E000000) on both transports. Transport choice makes no difference — the leading hypothesis is disproven and the zero-row scoping sits above the gRPC transport layer. Also confirmed the native capture-event harness queries a 30-day historical window (returns 50 rows), so the native read is connection-scoped historical retrieval, not a live subscription. CreateHttp2 + the env switch + the EventChannelMode diagnostic are retained for further connection-level probing. 44 offline tests pass; orchestrator stays on the no-row throw. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
0921e21bdb |
feat(grpc-events): handle-capture cycle — event-row gap proven NOT a client payload issue
Extends the instrument-grpc rewrite to log string (strHandle) + uint (uiHandle / queryRequestType) params, not just byte[], and captures our SDK's live v8 openParameters for a byte-diff against the native. Result of the exhaustive comparison (all live-confirmed via the opt-in EventReadDiagnostic test): - StartEventQuery request: byte-identical to the native (v6 layout) - v8 OpenConnection openParameters: byte-identical to the native (302B) once ClientNodeName matches — every control byte/ConnectionType/token/ShardId - handle usage identical: ExchangeKey->contextKey, registration->storage GUID (strHandle), query->client uint (uiHandle); handles valid (RTag/EnsT=True) - queryRequestType=3, registration order, gzip metadata header — all match - window has events (native returns 50 now); eventCount not it Every observable client-side byte matches the native, yet the server scopes 0 events to our connection. The event RPCs succeed over our transport and return a valid EMPTY result (not a transport error), so this is a connection/server-level difference (session affinity tied to the native Grpc.Core HTTP/2 connection or a connection identity used to scope events) — invisible to and unfixable by client payload matching. Needs server-side insight, not more wire RE. Added opt-in diagnostics (RegistrationDiag, LastResultBufferHex, LastEventOpenRequestHex). 326/326 offline; gated test still pins the no-row throw. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
32ae301050 |
feat(grpc-events): native-match event registration + skip ValidateClientCredential; diagnostics
Continues closing the event-row gap after the v8 ExchangeKey/RC4 auth breakthrough. - HistorianGrpcHandshake: the v8 EVENT path skips StorageService.ValidateClientCredential (the native event connection authenticates purely via ExchangeKey + the RC4 token; running the Negotiate loop establishes a different session scope). - HistorianGrpcEventOrchestrator.RegisterCmEventTag: simplified to the exact native gRPC event sequence (UpdateClientStatus -> RegisterTags -> EnsureTags -> GetHistorianInfo -> GetSystemParameter x7), dropping the 2020-WCF-era cross-service GetV probes and params-before-register that the gRPC event flow does not use. eventCount 5 -> 100. - Opt-in diagnostics (RegistrationDiag, LastResultBufferHex/LastErrorBufferHex; gated EventReadDiagnostic test) for the continued investigation. STATUS: auth + StartEventQuery + registration all succeed live (RTag/EnsT=True, valid query handle), but GetNext returns version-11 rowCount-0 while the native returns 50 for a BYTE-IDENTICAL request. Every observable wire element matches the native. The remaining unknown is the string/uint HANDLE field VALUES the native uses per event RPC — the instrument-grpc capture logged only byte[] params, not the handle fields. Next: extend the IL rewrite to log strHandle/uiHandle/queryRequestType, re-capture, and match. 326/326 offline; gated test still pins the no-row throw. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
6d0f5c4b8f |
feat(grpc-events): implement aahCryptV2 token — v8 ExchangeKey auth now passes live
Implements the reverse-engineered v8 credential token in pure managed code and
wires the full event-connection auth chain. Live result: the v8 OpenConnection
now AUTHENTICATES against the 2023 R2 server (past the 132/171 AuthenticationFailed
wall) — the crypto is solved.
- HistorianNativeHandshake.DeriveExchangeKeyClientKey: client key = SHA256(ECDH
shared secret) via ECDiffieHellman.DeriveKeyFromHash(SHA256), matching the native
ECDiffieHellmanCng{Hash,SHA256}.DeriveKeyMaterial.
- BuildExchangeKeyCredentialToken + Rc4: token = RC4(password-UTF16LE, key=MD5(clientKey)).
Reproduces a live-captured token EXACTLY (verified offline) — the native
HistorianCrypto.NRC4_V2.aahCryptV2 scheme (MD5-keyed RC4). Pure managed; nothing
AVEVA shipped. RC4 pinned by the standard test vector.
- OpenSession(eventConnection:true): ExchangeKey -> derive client key -> token ->
v8 OpenConnection with ConnectionType=Event + the token. Orchestrator re-armed.
- HistorianAddTagsProtocol.SerializeCmEventEnsureTagsGrpc: the 86-byte native gRPC
CM_EVENT EnsureTags (8-byte header + ...2f27 event-type GUID), replacing the
2020 WCF 83-byte CTagMetadata on the gRPC event registration.
Goldens: RC4 standard vector + token construction. 326/326 offline.
KNOWN REMAINING: the event query still returns zero rows (GetNext yields a 10-byte
zero-row buffer). Auth + StartEventQuery succeed; the query-layer detail (vs the
native row-returning capture) is the last step. Gated test still pins the no-row
throw; opt-in diagnostic (HISTORIAN_GRPC_EVENT_DIAG) surfaces the journey.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
|
||
|
|
d67f6f5e96 |
feat(grpc-events): ExchangeKey ECDH (Path B) — clears the v8 client-key check
Implements HistoryService.ExchangeKey as a pure-managed P-256 ECDH key exchange and wires it ahead of the v8 Event OpenConnection. - HistorianNativeHandshake.BuildExchangeKeyClientHello / DeriveExchangeKeySecret: .NET ECDiffieHellman (nistP256); wire format "ECK1" + u32(32) + X(32) + Y(32), decoded from the live capture. No native AVEVA dependency. - HistorianGrpcHandshake.OpenSession(eventConnection: true): runs ExchangeKey on the context-key handle before the v8 OpenConnection. - Guardrail HistorianGrpcHandshakeRoutingTests scoped to the token-loop closure: still pins that the Negotiate token loop routes to ValidateClientCredential (not ExchangeKey), while allowing the legitimate ExchangeKey call in OpenSession. Live result: ExchangeKey succeeds (server accepts our public key) and the v8 OpenConnection error advances from 132/34 "Failed to get client key" to 132/171 AuthenticationFailed — the ECDH cleared the client-key layer. The remaining blocker is the 26-byte v8 credential token, which must be derived from the ECDH shared secret (token KDF, not yet recovered). Orchestrator stays on v6 (set eventConnection: true to re-arm once the KDF lands). 323/323 offline. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
0b1e9d0a7f |
feat(grpc-events): v8 OpenConnection serializer + native error decode (Path A disproven)
Builds the native 2023 R2 version-8 OpenConnection format, which (unlike v6) carries a ConnectionType byte (Event vs Process) — required because the 2023 R2 server returns event rows only on an Event connection. - HistorianOpen2Protocol.SerializeNativeOpenConnectionVersion8: reproduces the 302-byte v8 layout decoded from a live capture (version 8, markers, client-key GUID, username HString, length-prefixed credential token, ClientType / ConnectionType / flag / constant word / compact metadata / two empty strings; the tail reuses WriteClientCommonInfo). Golden-tested (Version8EventSerializerReproducesCapturedNativeStructure). - HistorianNativeHandshake.BuildEventOpenConnectionVersion8Request: ConnectionType= Event, zeroed credential token (mirroring how v6 zeros its credential block and relies on the separate ValidateClientCredential handshake). - HistorianGrpcHandshake.OpenSession: optional eventConnection switch; the OpenConnection failure path now decodes the native error (type/code/ASCII). Path A (reuse ValidateClientCredential + zeroed token) was live-tested and DISPROVEN: the server parses the v8 buffer but rejects it at the auth check with native 132/34 "EstablishConnection Failed to get client key" — the v8 path looks up the client key in the registry HistoryService.ExchangeKey (ECDH) populates, not the one ValidateClientCredential does. The event orchestrator is therefore reverted to the v6 session (gated test still pins the no-row throw). The v8 serializer/builder are retained for Path B (implement ExchangeKey). 323/323 offline tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
dbb5c99c53 |
feat(grpc-events): v6 StartEventQuery request + capture-event harness scenario
Captured the stock 2023 R2 client doing a gRPC event read (50 rows flowed) to resolve the open "gRPC event ROW retrieval returns zero rows" item. Two captured differences from our SDK's path; this lands the first (necessary) one plus the capture tooling. - HistorianEventQueryProtocol.CreateStartEventQueryAttempts: add a `version` parameter (default 5 = the 2020 WCF format, unchanged). The gRPC event orchestrator now opts into version 6 — the leading `06` plus a 5-byte trailing zero pad — which is the envelope the stock 2023 R2 client sends. The two buffers are otherwise byte-identical (filter block, UTC string, metadata namespace). Golden test Version6EmptyFilterMatchesCapturedGrpcEnvelope pins it. - Grpc2023CaptureHarness: new `capture-event` scenario drives HistorianAccess over an Event-type gRPC connection (CreateEventQuery -> EventQueryArgs -> StartQuery -> MoveNext) so the wide-net instrument-grpc-nonstream rewrite dumps StartEventQuery.requestBuffer + the row result. Hostname defaults sanitized to HISTORIAN_GRPC_HOST / "localhost" (removed hardcoded server name). NECESSARY BUT NOT SUFFICIENT: live validation shows v6 alone does not make rows flow — the read also requires an Event-type connection, which our SDK's v6 Open2 format cannot express (see the companion docs commit). The gated ReadEventsAsync_OverGrpc_* test correctly still pins the no-row throw. 322/322 offline tests pass; WCF event path unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
73f66cbf27 |
docs(handoff): re-characterize gRPC event-row gate — capture-gated, not server-gated
Live SQL ground truth (user-authorized one-time read via SOCKS->SQL relay) disproves the gate on the open gRPC event-row item. The live 2023 R2 server IS event-bearing — Runtime.dbo.Events holds 19,356 rows in the last 30 days (90,944 in 365) — yet the empty-filter gRPC event query still returns zero rows and long-polls to the deadline over that same window. So GetNextEventQueryResultBuffer returning nothing is NOT "no events on the server"; the empty-filter request shape (filter / namespace / event-tag registration) doesn't match existing rows. The remaining work is a fresh native gRPC event-query capture of the stock client, not access to a different server. - handoff.md: rewrite open-item #1 with the SQL numbers + capture-gated framing; update the "to move any item" summary to match. - HistorianGrpcIntegrationTests: correct the event-read test comment (drop the false "idle dev box holds no events" rationale; document 19k-events-yet-zero-rows). No behavior change (test edit is comment-only). Sanitization scan clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
2bd86e4e83 |
probe(grpc): DeleteTagExtendedProperties multiplexed-channel — disproven
Adds an internal RE probe (HistorianGrpcTagWriteOrchestrator. ProbeDeleteTagExtendedPropertiesAsync) testing whether gRPC's single shared channel lifts the WCF per-connection working-set wall that blocks DelTep. Live result (2023 R2, History iface 12): both GetTgByNm + GetTepByNm primes succeed on the one shared channel, yet DelTep is still rejected (native code=1) and the property survives. So the working set is populated by the native client's in-process registration state, not the wire session — neither WCF's per-service channels nor gRPC's shared channel reproduce it. DelTep stays server-blocked on BOTH transports and remains unshipped. Gated negative test DeleteTagExtendedProperties_OverGrpc_ProbeMultiplexedChannel pins this (primes succeed, delete rejected, prop survives) and flips if a future server/registration lifts the wall. Comment in HistorianClient records the probe. 321 offline tests pass; live test passes bounded at ~11s. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
8984dac1ed |
test(grpc): multi-group ext-prop golden + ConnStatus + tightened write read-back
- WcfTagExtendedPropertyProtocolTests: add a multi-group golden test mirroring the live capture (one group per property + uint16 flags trailer) that the old parser failed; correct the synthetic builder to the uint16-flags trailer. - HistorianGrpcIntegrationTests: add GetConnectionStatusAsync_OverGrpc_ReportsConnected (plan #5); tighten the write-lifecycle read-back to a hard assert now that the parser is fixed; make sandbox cleanup generous best-effort (rename is async + the browse view is eventually consistent, so a hard absence assert was racy). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
274466c050 |
test(grpc): live-verify gRPC writes + pin bounded ReadEvents behavior
- TagWriteLifecycle_OverGrpc_*: live-verified the gRPC tag-config write surface (EnsureTags create, AddTagExtendedProperties, StartJob rename, DeleteTags) against a self-cleaning synthetic sandbox tag. Hardened for the live server: pre-clean both names for a clean slate, retry the async StartJob rename (transiently rejectable right after create), tolerate the known extended-property read-back parser gap (value marker 0x01 vs compact-string 0x09 — a read-side gap, not a write failure), and assert no litter remains after cleanup (TagExistsAsync). Two consecutive clean passes. - ReadEventsAsync_OverGrpc_*: pins the current bounded reality — StartEventQuery succeeds but GetNextEventQueryResultBuffer long-polls on no data, so the bounded read throws ProtocolEvidenceMissingException on the idle dev box (no hang). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
0780cec9a7 |
test(grpc): live + gated coverage for the gRPC config ops
- GetRuntimeParameterAsync_OverGrpc_ReturnsValue (live) - GetTagExtendedPropertiesAsync_OverGrpc_DoesNotThrow (live; empty for system tags) - ExecuteSqlCommandAsync_OverGrpc_IsServerWalled (live; pins the captured wall) - TagWriteLifecycle_OverGrpc_CreatesAddsPropRenamesDeletes — DESTRUCTIVE, gated on HISTORIAN_GRPC_WRITE_SANDBOX_TAG; self-cleaning create->addprop->verify->rename->delete Full gRPC live suite 19/19 green against a real 2023 R2 server (write lifecycle skips without the sandbox tag); 317 offline green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
6d8a7d48f8 |
tests(grpc): live-verify aggregate + at-time over gRPC at WCF parity
The gRPC integration suite was missing live coverage for ReadAggregateAsync and ReadAtTimeAsync, the two tooled gRPC reads the WCF suite already exercises. Add them so the full tooled gRPC surface is live-tested like WCF. - ReadAggregateAsync_OverGrpc_ReturnsTimeWeightedAverageRows - ReadAggregateAsync_OverGrpc_AcceptsRetrievalMode (Min/MaxWithTime, BestFit) - ReadAtTimeAsync_OverGrpc_ReturnsRequestedTimestamps The aggregate tests self-calibrate their window from a real raw sample (SeedAggregateWindowAsync): the interpolating modes (TimeWeightedAverage / Min/MaxWithTime) do a slow bounding-value scan and return empty when the window has no raw data, so a fixed "last N hours" window blows the per-call deadline against an idle server. Anchoring the window where data actually exists keeps the scan cheap and returns rows on idle or live servers alike. Adds an optional HISTORIAN_GRPC_TIMEOUT knob (per-call deadline override) for slow links. Full tooled gRPC surface now live-green: 15/15 gRPC integration tests pass against a real 2023 R2 server; 313 offline tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
53a9c87114 |
R4.3: measured idle-state GetStoreForwardStatusAsync over gRPC
Route GetStoreForwardStatusAsync to a gRPC path that actually contacts the server (StatusService.GetHistorianConsoleStatus) instead of synthesizing an all-false result blind. On a reachable/normal server it returns the not-storing baseline but MEASURED; when the server is unreachable or the console-status call fails it reports ErrorOccurred with the underlying error (the old synthesis never contacted the server). The active-SF buffer magnitude (Storing/Pending/DataStored) stays false because it lives behind the D2 storage-engine console wall. Non-gRPC transports keep the synthesized fallback. Live-verified against the 2023 R2 server; gated integration test GetStoreForwardStatusAsync_OverGrpc_ReturnsMeasuredIdleState added. README operation table updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
60b3673f01 |
M4 R4.4: client-side multi-historian redundancy
Adds AVEVA.Historian.Client.Redundancy — HistorianRedundantClient orchestrates N single-historian members (IHistorianMember; default HistorianClientMember over HistorianClient) as one logical client. Pure client-side, no server-side redundancy protocol, no RE. - Reads fail over to the next member in priority order. Streaming reads only fail over BEFORE the first row is observed; a mid-stream failure propagates (failing over mid-stream would risk duplicated/skipped rows). - Writes fan out: WriteFanout AllMembers | PreferredOnly, with All | Any ack policy, returning a per-member HistorianRedundantWriteResult. - Per-member health: FailureThreshold demotes a failing member out of the preferred pool; a background watchdog (PeriodicTimer) + CheckHealthAsync re-probe and restore recovered members. GetStatus() snapshot + ActiveMember. - Composes with R4.1: back a member's writes with a HistorianStoreForwardWriter so a down member buffers and replays on recovery — the pragmatic client-side equivalent of native ReSyncTags. 14 unit tests (no server): failover order, mid-stream no-failover, all-fail aggregation, probe-any-up, fan-out ack policies, PreferredOnly, soft reject, health demotion + CheckHealthAsync restore, watchdog recovery. Full suite 307 green. Roadmap R4.4 marked shipped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
dd2aec3b8b |
M4 R4.1: pragmatic store-and-forward durable outbox
Adds AVEVA.Historian.Client.StoreForward — a client-side store-and-forward layer over the historian write surface (AddHistoricalValuesAsync / SendEventAsync). Producers enqueue writes; the writer persists them and replays on reconnect so a transient disconnect never drops data. This is the roadmap's recommended pragmatic outbox, NOT a bit-faithful reimplementation of AVEVA's native SF cache (that stays deferred) — pure managed, no RE. - HistorianOutboxEntry / HistorianOutboxEntryKind: buffered-write envelope - IHistorianOutboxStore + InMemoryHistorianOutboxStore (tests) + FileHistorianOutboxStore (crash-durable: atomic temp+move JSON per entry, FIFO by filename sequence that resumes past on-disk max, corrupt-file quarantine). OutboxJson normalizes event object? properties off JsonElement. - IHistorianWriteSink + HistorianClientWriteSink (HistorianClient-backed) - HistorianStoreForwardWriter: enqueue, single-flight FIFO FlushAsync with head-of-line blocking, optional MaxDeliveryAttempts dead-lettering, DropOldest/Reject overflow policy, background drain loop (retry on reconnect), GetStatusAsync snapshot mirroring server SF Pending/Storing/ErrorOccurred. 12 unit tests (no server): durability-across-restart, reconnect-drain, FIFO order/head-of-line, dead-letter, overflow policies, background auto-drain. Full suite 293 green. Roadmap R4.1 marked shipped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
d1e96f48de |
M3 R3.2: AddHistoricalValuesAsync supports Double + Int (Int2/Int4/UInt4)
Extended the historical-write serializer from Float-only to all five analog types EnsureTagAsync supports. Captured each type's "ON" buffer live from the native client (sandbox tag per type, written + captured + deleted): - The 4-byte value descriptor (C0 10 01 00) is CONSTANT across types — it does not encode the type. - The value is u32(0) + native-width value, width by the tag's declared type: Float->float32, Double->double64, Int2->int16, Int4->int32, UInt4->uint32. HistorianHistoricalWriteProtocol.SerializeAddStreamValuesBuffer now takes the HistorianDataType and encodes accordingly (unsupported types throw ProtocolEvidenceMissingException). The orchestrator resolves the type from the tag-info NativeDataTypeDescriptor via MapDataType. Harness capture-write gained --data-type. Golden-tested against all five live captures + the gated write/read-back test validated each type end-to-end through the pure-managed SDK; 281 unit tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
aa36e58d58 |
M3 R3.2 SHIPPED: AddHistoricalValuesAsync — historical backfill writes over gRPC (live-validated)
Public HistorianClient.AddHistoricalValuesAsync(tag, values) inserts non-streamed original
(backfill) values for an existing tag over the 2023 R2 gRPC front door. The pure-managed SDK
wrote a value and read it back live (gated test AddHistoricalValuesAsync_OverGrpc_WritesAndReadsBack
PASSED against the real server).
- HistorianGrpcHistoricalWriteOrchestrator: write-enabled (0x401) session ->
RetrievalService.GetTagInfosFromName (resolves the per-tag GUID = the tag-info TypeId, and
registers the tag on the session) -> HistoryService.AddStreamValues("ON" buffer) per sample.
- HistorianHistoricalValue (public record: TimestampUtc, Value, OpcQuality=192).
- gRPC-only: non-RemoteGrpc transports throw ProtocolEvidenceMissingException (the 2020 WCF
non-streamed write is architecturally blocked, D2).
- Float value encoding only (the captured type); other types rejected by the serializer.
275 unit tests pass; the new gated live write/read-back test is green against the 2023 R2 server.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
|
||
|
|
85f0c2f0fa |
M3 R3.2: HistorianHistoricalWriteProtocol — the "ON" AddStreamValues serializer (golden-validated)
Managed serializer for the historical-write "ON" buffer, byte-for-byte matching the live capture (WcfHistoricalWriteProtocolTests golden-tests the exact 56-byte native buffer). Layout: "ON"(0x4E4F) + count + lengths + 16B tag GUID + sample FILETIME + u16 quality(192) + 4B descriptor + received FILETIME + value. Value encoding (Float, captured): an 8-byte slot = u32(0) + float32(value) — the 4-byte float in the high dword, NOT a double. The 16B tag GUID is the per-tag GUID the SDK already parses as ParseTagInfoRecord's "typeId" (confirmed: it appears at offset 8 of GetTagInfosFromName's response = where typeId is read, and in EnsureTags' response + the "ON" buffer). Only the Float encoding is captured; other types rejected until captured. Next: gRPC orchestrator (write-enabled session -> EnsureTags -> resolve tag GUID -> AddStreamValues) + public AddHistoricalValuesAsync + live write/read-back. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
57b9506d01 |
M3 R3.1: OpenStorageConnection is a dead end (error 85); precondition is front-door RegisterTags
Live-probed StorageService.OpenStorageConnection against the 2023 R2 server over a
write-enabled (0x401) session. Every attempt — sweeping ConnectionMode (0x401/0x402/0x1),
StorageSessionId-in (Open2-GUID / empty), and FreeDiskSpace — returns the IDENTICAL native
error type=4 code=85 ("session not registered"), so it's a structural refusal, not a bad
field value.
Decode (two corroborating facts):
- Error 85 is the same code the event read returns before RegisterTags2 (see
HistorianWcfEventOrchestrator) — a generic "session not registered for this op".
- The 2023 R2 decompile shows OpenStorageConnection lives on a SEPARATE GrpcStorageClient
(the storage engine's SF/snapshot channel, own port + service identity); HistorianAccess
drives non-streamed writes through the native C++ HistorianClient, never this op.
So the roadmap's mapped "missing console session" step was wrong. The real non-streamed-write
precondition is the front-door HistoryService.RegisterTags (RTag2-family) for the target tag —
which is exactly why the R3.1 batch failed at AddNonStreamValues (no tag registered ->
StoreNonStreamValues had no route). Matches the original 2020-WCF D2 hypothesis.
Remaining (both need a native gRPC capture; do not guess bytes): the regular-tag RegisterTags
btTagInfos (only CM_EVENT's tag-GUID form is known) and the AddNonStreamValues btInput.
- HistorianGrpcStorageConnectionProbe + grpc-open-storage-connection CLI (opens nothing
persistent; CloseStorageConnection on success)
- corrected revision-write-path.md §R3.1 follow-up + hcal-roadmap R3.1/R3.2 rows
- gated regression test pinning the error-85 refusal
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
|
||
|
|
23798db1ef |
M3 probe: non-streamed write transaction reachable over 2023 R2 gRPC (Begin/End live-verified)
The D2 storage-engine-pipe wall is WCF-transport-specific. On the 2023 R2 gRPC front door, TransactionService is a first-class service AND the gateway to the storage engine, so the Open2 storage-session GUID (uppercase) is accepted directly as strHandle with no legacy pipe. Live-verified against the real 2023 R2 server over a write-enabled (0x401) gRPC session: AddNonStreamValuesBegin returns a real strTransactionId, and AddNonStreamValuesEnd(bCommit=false) discards it cleanly (no data written). On 2020 WCF the same op returns UnknownClient(51) for every handle + priming chain. - HistorianGrpcRevisionProbe + grpc-revision-probe CLI command + gated test NonStreamedWriteTransaction_OverGrpc_BeginsAndDiscards (live pass). - HistorianGrpcHandshake.OpenSession gains an optional connectionMode param (default read-only 0x402; pass 0x401 for write-enabled) — non-breaking. - Docs: revision-write-path.md "the wall is gone" section; roadmap M3 section, R3.1-R3.3 rows, one-glance table, and status note updated honestly. Not yet shipped: the AddNonStreamValues btInput VTQ buffer is uncaptured (never guess wire bytes), so no value-commit is implemented. Scope is non-streamed ORIGINAL backfill; revision EDITS (R4.2) remain pipe-only even on gRPC. 272 unit tests pass; sanitization scan clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
04ea0b9a1f |
R1.3 GetServerTimeZoneAsync over gRPC (live-verified); R1.4 bounded out on gRPC
Live-probed both R1.3 and R1.4 against a real 2023 R2 server over the gRPC StatusService; implemented the one that carries an evidence-backed value. R1.3 GetServerTimeZoneAsync — SHIPPED: - StatusService.GetSystemTimeZoneName(uiHandle) returns the real server zone over RemoteGrpc (the 2020 WCF op is a client-side stub returning empty). - HistorianGrpcStatusClient.GetSystemTimeZoneNameAsync -> dialect routing -> public HistorianClient.GetServerTimeZoneAsync. Non-gRPC transports fail closed with ProtocolEvidenceMissingException (no empty-string lie). - Golden message-shape unit test + non-gRPC guardrail unit test + gated live test. 271 unit tests pass. R1.4 GetHistorianInfoAsync (EventStorageMode) — bounded out on gRPC too: - gRPC GetHistorianInfo is the same named-value query as 2020 WCF (only HistorianVersion resolves); EventStorageMode + 7 variants fail on both GetHistorianInfo and GetSystemParameter. The 518-byte struct is filled by a native vtable+648 HCAL call, not the gRPC op (per the 2023 R2 decompile), so the field is never on the wire. Not shipped on any transport. Closes the roadmap's open "build against a live 2023 R2 server" caveat. Also correct the stale M3 roadmap section: D2 already proved Transaction.AddNonStreamValues* rides the storage-engine pipe (STransactPipeClient2 -> aaStorageEngine), not WCF — same wall as R4.2 — so M3-over-WCF is blocked, not "the path that is NOT the gated cache push". Docs: hcal-roadmap.md, wcf-historian-info.md, wcf-status-localhost.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
25aff409dc | Merge re/grpc-2023r2-handshake: M0 gRPC parity (probe/system-param/metadata/browse) + handshake fix | ||
|
|
d23722ea73 |
Merge re/r1.10-rename-tags: RenameTagsAsync via History StartJob
# Conflicts: # docs/plans/hcal-capability-matrix.md # docs/plans/hcal-roadmap.md # src/AVEVA.Historian.Client/Wcf/HistorianWcfTagWriteOrchestrator.cs # tests/AVEVA.Historian.Client.Tests/HistorianClientIntegrationTests.cs # tools/AVEVA.Historian.NativeTraceHarness/Program.cs |
||
|
|
4de222c950 |
Merge re/r1.4-gethi-finding: R1.1 ExecuteSqlCommand + R1.4 GetHistorianInfo (bounded)
# Conflicts: # docs/plans/hcal-roadmap.md # src/AVEVA.Historian.Client/HistorianClient.cs # src/AVEVA.Historian.Client/Protocol/Historian2020ProtocolDialect.cs # tests/AVEVA.Historian.Client.Tests/HistorianClientIntegrationTests.cs # tools/AVEVA.Historian.NativeTraceHarness/Program.cs |
||
|
|
85ff1b48df |
R0.1 browse over gRPC SHIPPED — QueryTag cracked, M0 gRPC parity complete
Wires HistorianClient.BrowseTagNamesAsync over gRPC (Transport==RemoteGrpc) via
Grpc/HistorianGrpcTagClient.BrowseTagNamesAsync: StartTagQuery(OData) -> paged
QueryTag -> EndTagQuery. Live-verified against a real 2023 R2 server (returns Sys* tags).
QueryTag packet-id recovered WITHOUT native disassembly: a .rdata packet-descriptor
table in aahClientManaged.dll lists {0x6751,1}=StartTagQuery immediately followed by
{0x6752,1}=QueryTag (found via pefile byte-scan of .rdata), confirmed live.
Wire format (live-verified):
- request btRequest = u16 0x6752 + u16 version(1) + u16 queryType(1=names) + u32 startIndex + u32 count
- response btResonse = u32 count + per-name(u32 charCount + UTF-16LE) + trailer (NextIndex/metadata, ignored)
- new HistorianTagQueryProtocol.ParseTagNameQueryPage tolerates the trailer
- GlobToODataFilter translates the SDK glob filter to OData (Pre*->startswith, *suf->endswith,
*mid*->contains, exact->eq); the 2023 R2 metadata-server parses filters as OData.
Replaces the earlier RE probe helpers with the shipped browse path. Adds golden-byte
(BuildQueryTagRequest) + 8 glob-translation unit tests + gated live browse test.
226 unit tests pass; 5/5 live gRPC tests pass (read, probe, system-param, metadata, browse).
Milestone 0 (full gRPC parity) is complete.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
|
||
|
|
26ef5e5645 |
R0.1 browse probe: StartTagQuery over gRPC takes an OData filter (live)
Probes the 2023 R2 gRPC browse path and records the finding. The front door does NOT hit the 2020 WCF metadata-server-pipe wall. - RetrievalService.StartTagQuery is cracked: the server (CMdServer::StartActiveTagnamesQuery over \.\pipe\aahMetadataServer\console) parses the filter as OData. startswith()/ contains()/eq/empty succeed and return the 8-byte (queryHandle, tagCount); SQL-LIKE "%" and glob "*" fail with "ODataFilter: bad token". Live: 220 Sys* tags counted. - QueryTag (paging) remains: every guessed btRequest returns a constant native error type 4 / code 72 (content-independent) -> framing needs a native capture, not guessing. Adds RE probe helpers Grpc/HistorianGrpcTagClient.ProbeStartTagQuery + ProbeTagQuerySequence, a gated StartTagQuery_OverGrpc_AcceptsODataFilter test, and the finding doc docs/reverse-engineering/grpc-tag-query-odata.md. Browse is not yet wired (QueryTag open). 217 unit tests pass; 5/5 live gRPC tests pass. No tag names/identities committed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
0e19adae68 |
gRPC M0 R0.2: tag metadata over gRPC (GetTagInfosFromName, live-verified)
Routes HistorianClient.GetTagMetadataAsync over gRPC when Transport==RemoteGrpc,
via the new Grpc/HistorianGrpcTagClient calling RetrievalService.GetTagInfosFromName
(the plural string-handle metadata op).
- String handle = the Open2 storage-session GUID formatted uppercase (the format
that resolves the native string-handle path); threaded out of the shared handshake
via a new HistorianGrpcHandshake.Session { ClientHandle, StorageSessionId, StringHandle }.
- Request btTagNames = uint count + per-name(uint charCount + UTF-16LE) — golden-byte
unit-tested (BuildTagNamesBuffer).
- Response btTagInfos = uint count + CTagMetadata records — decoded by the existing
HistorianTagQueryProtocol.ParseGetTagInfoResponse; data type via the shared MapDataType.
The 2020 WCF string-handle wall does NOT apply on the gRPC front door, as the
string-handle-wall RE note predicted. LIVE-VERIFIED against a real 2023 R2 server:
GetTagMetadataAsync returns the requested tag with a valid decoded data type.
216 unit tests pass. Captured framing confirmed live then discarded; no tag names
or identities committed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
|
||
|
|
c4b8d0dde4 |
gRPC M0: probe (R0.4, live-verified) + system-param (R0.3) + shared handshake
Roadmap docs/plans/hcal-roadmap.md, milestone M0 (gRPC parity for the DONE surface). Now unblocked for live verification by a reachable 2023 R2 server. - R0.4 Probe over gRPC: new HistorianGrpcProbe calls History/Retrieval/Status GetInterfaceVersion (unauthenticated). ProbeAsync routes over gRPC when Transport==RemoteGrpc. LIVE-VERIFIED against a real 2023 R2 server — needs no credentials (runs before the auth loop), so it works despite the auth blocker. - R0.3 System parameter over gRPC: new HistorianGrpcStatusClient calls StatusService.GetSystemParameter over the authenticated session; routed in the dialect. Built + unit-tested (request/response field mapping pinned). Live-verification pending an auth fix (see below). - Extracted the proven auth handshake from HistorianGrpcReadOrchestrator into shared Grpc/HistorianGrpcHandshake (reused by read + status + future browse/metadata). Repointed the IL structural guardrail test to it. - Diagnostics: round-failure now decodes the native server error + hex/ASCII preview (HistorianNativeHandshake.DescribeError). This surfaced the live auth blocker as SEC_E_LOGON_DENIED (0x8009030C) at NTLM round 1 — framing is correct, the credential did not validate. Probable cause: stale file password or NAM-domain NTLM restriction (Kerberos/RDP works, NTLM denied; no SPN path over the tunnel). 216 unit tests pass; live gRPC probe passes. Sanitization scan clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
22e9c5e5f8 |
gRPC 2023 R2: fix auth handshake op routing + accept History v12
First live-verified gRPC read against a real 2023 R2 Historian. The handshake previously failed at round 0 (cred-independent) because the SSPI/Negotiate token loop was routed to HistoryService.ExchangeKey. ExchangeKey is a separate key-exchange/cert-path op, not the Negotiate loop — the token loop belongs on StorageService.ValidateClientCredential, which kept the 2020 inBuff/outBuff token framing the SDK's WrapValidateClientCredentialToken/TryRead helpers already build. Captured + diffed against the recovered 2023 R2 protobuf contract and the decompiled stock client; routing the loop to ValidateClientCredential completes the full chain (ValidateClientCredential x N -> OpenConnection -> StartQuery -> GetNextQueryResultBuffer) and returns rows. - HistorianGrpcReadOrchestrator: token loop now calls StorageService.ValidateClientCredential(Handle, InBuff); corrected the op-map doc comment (was asserting the wrong ExchangeKey mapping). - HistorianServerVersionGate: accept History interface version 12 alongside 11. Live server reports History=12, Retrieval=4, Storage=4; the buffers are byte-identical (a live read returns rows), so 12 is buffer-compatible. Retrieval stays pinned at 4 (matches). New AcceptedVersions() supports multi-version gates. - New HistorianGrpcHandshakeRoutingTests: IL-level structural guardrail that disassembles the orchestrator (incl. lambda closures) and asserts the handshake invokes ValidateClientCredential and never ExchangeKey — fails if the regression returns. - Updated gate tests + CLAUDE.md gRPC op-map. 240 unit tests pass on the full stack; 210 on this branch's base. The byte payloads remain the proven 2020 protocol. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
c1b1b3d23b |
R1.11 DelTep capture + R1.3/R1.4/R1.12/R1.13 bounded out
DelTep (extended-property delete) — wire format captured + serializer golden-proven, but live delete is server-blocked and NOT exposed publicly: - Captured the DelTep inBuff via a cross-session trick (harness add-tep gains --tep-skip-add + read-for-sync before --tep-delete; Capture-DeleteTagExtended Properties.ps1 / decode-del-tep-capture.py). Layout = same group framing as AddTEx but property-name-only (no 0x43 value) + 0x00 group trailer. - SerializeDeleteRequest + 4 golden tests pin the server-accepted buffer. - A decisive experiment shows SDK-added properties ARE deletable (the native client read-syncs and deletes one), so SDK-add is complete; the SDK's own DelTep is rejected by CHistStorage::DeleteTagExtendedProperties even with byte-identical inBuff, matching mode/handle, GetTgByNm+GetTepByNm prime, open channel, and 60s retries. Root cause: the native multiplexes services over one connection (per-connection working set); the SDK's per-service WCF channels don't reproduce it. Kept as documented-but-blocked internal orchestrator path; no public HistorianClient delete API. Bounded out with evidence (no code; docs + roadmap + probe): - R1.12 localized-property write — no op on 2020 (mirror of R1.6); no *LocalizedPropert*/TagLocalized* symbol in any current/*.dll. - R1.13 non-analog tag create — GATED; native AddTag rejects every non-analog type client-side (ValidationFailed, before any WCF op): SingleByteString, DoubleByteString, Int1 all fail, Float works. No Discrete type in the native enum, no TagType setter. No wire request to capture. - R1.3 timezone + R1.4 EventStorageMode — re-confirmed 2023R2/gRPC-only from the Runtime DB schema (no timezone param, no EventStorageMode anywhere) and a parameter-op probe (GetSystemParameter + GETRP return null/throw for every candidate; only HistorianVersion works). 238 unit tests pass; full solution builds with 0 warnings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
08b950caee |
R1.11 AddTagExtendedPropertiesAsync: extended-property write via AddTEx
Adds user-defined extended properties to an existing tag via the 2020 WCF
AddTEx (AddTagExtendedProperties) op. Write-enabled connection + uppercase
storage-session GUID handle; reuses the write orchestrator open/priming chain.
The AddTEx inBuff is the exact inverse of the R1.5 GetTepByNm read-response
framing, so the serializer mirrors the read parser:
uint32 groupCount + 0x01(group) + [0x09+u16+ASCII tag] + uint32 propCount
+ per prop{ 0x02 + [0x09+u16+ASCII name] + 0x43 VT_BSTR + u16 payloadLen
+ u16 charCount + UTF-16 value } + 0x01(group trailer) + 0x00(terminator).
The trailing 0x00 is required — without it inBuff is one byte short and the
server throws SErrorException in CHistStorage::AddTagExtendedProperties. The
golden fixture pins the clean inBuff the live server accepted (dumped via
AVEVA_HISTORIAN_TEP_DUMP); read-back verified via R1.5. String (0x43) values only.
Delete (DelTep) is deferred: the native DeleteTagExtendedPropertiesByName does a
client-side sync check and returns err 229 for a just-added property, so the
DelTep request never reaches the wire and its inBuff can't be captured yet.
Shipped: HistorianClient.AddTagExtendedPropertiesAsync/AddTagExtendedPropertyAsync;
HistorianTagExtendedPropertyProtocol.SerializeAddRequest; orchestrator path;
golden WcfTagExtendedPropertyWriteProtocolTests (4); gated live write/read-back test;
native-harness `add-tep` scenario + Capture-AddTagExtendedProperties.ps1 +
decode-add-tep-capture.py. Doc: wcf-add-tag-extended-properties.md. 233 tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
|
||
|
|
bc353df8c4 |
R1.10 RenameTagsAsync: async tag rename via History StartJob (StJb)
Tag rename has no dedicated WCF op — the (old,new) name batch rides the generic History StartJob (StJb) job buffer; the server returns a job id and applies renames asynchronously. Handle is the uppercase storage-session GUID, Open2 in write mode; reuses the write orchestrator's open+priming chain. jobBuffer layout (decoded + server-validated): byte[7] zero prefix + uint32 pairCount + per pair (uint32 oldCharCount + UTF-16 oldName + uint32 newCharCount + UTF-16 newName), order (old,new). The raw instrument capture mangles the final byte with MDAS chunk markers (the R1.1 lesson), so the golden fixture pins the CLEAN byte[] the SDK handed the channel (dumped via AVEVA_HISTORIAN_RENAME_DUMP) — the exact buffer the live server accepted and renamed with. Gated server-side by the AllowRenameTags system parameter (default 0): when disabled the native client rejects pre-wire (err 132); the managed SDK surfaces it as StartJob=false -> Accepted=false. Enabling needs a Historian config reload, not just a storage-engine restart. Shipped: HistorianClient.RenameTagAsync/RenameTagsAsync -> HistorianTagRenameResult; HistorianTagRenameProtocol; orchestrator RenameTags/SendStartJobRename; golden WcfTagRenameProtocolTests (4, pins server-accepted buffer); gated live test RenameTagsAsync_AgainstLocalHistorian_RenamesSandboxTag (passed end-to-end). Native-harness `rename` scenario + Capture-RenameTags.ps1 + decode-rename-capture.py. Doc: docs/reverse-engineering/wcf-rename-tags.md. 213 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
fbd839077b |
R1.4 GetHistorianInfo: bounded out on 2020 WCF (named-value-only, no struct)
Captured the native HistorianAccess.GetHistorianInfo(out HistorianInfo, out err) and decoded the wire: over 2020 WCF, GETHI is a named-value query whose only working key is "HistorianVersion" (response ~30 bytes = the version string). Probed 7 storage-mode key names -> all ok=False/err. The 518-byte HISTORIAN_INFO struct + EventStorageMode@514 is the 2023R2 HCAL-native/gRPC model (confirmed from the decompiled 2023R2 source); on 2020 the native client derives the mode outside the WCF wire. Version is already exposed (ProbeAsync/GetRuntimeParameterAsync), so no hollow GetHistorianInfoAsync is shipped (same disposition as R1.3 timezone). This completes the reachable 2020-WCF M1 read surface; remaining M1 = config writes (gated on explicit request) or gRPC/2023R2-only items. RE aids kept: harness `historian-info` scenario, Capture-HistorianInfo.ps1, decode-historian-info-capture.py, and StringHandleProbeDiagnosticTests .GETHI_CandidateInfoNames (asserts the named-value-only finding; gated). Docs: wcf-historian-info.md (new) + roadmap/matrix/wall-doc updates. 230 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
1a539882d0 |
R1.1 ExecuteSqlCommandAsync (ExeC + GetR, NRBF DataTable, no BinaryFormatter)
Ship SQL command execution over the 2020 WCF aa/Retr/ExeC + aa/Retr/GetR ops: HistorianClient.ExecuteSqlCommandAsync(sql) -> HistorianSqlResult (columns + typed rows). String-handle ops reached with the Open2 storage-session GUID formatted uppercase (the handle format that unlocked GETRP/GETHI). Chain: Retr.GetV prime -> ExeC(handle, sql, option=0, ref queryHandle) -> GetR loop. Key gotcha captured: GetR returns FALSE even on success -- the byte stream is in pResultBuff regardless; false just signals the final page. So the orchestrator consumes the buffer first, then stops on a false result / empty page. GetR's pResultBuff is an NRBF-serialized System.Data.DataTable (SerializationFormat.Xml: members XmlSchema (XSD) + XmlDiffGram (rows)). BinaryFormatter is removed from .NET 10, so the stream is decoded read-only with the System.Formats.Nrbf package (NrbfDecoder) + XDocument -- no BinaryFormatter, no code execution. Values are typed per the XSD type, falling back to string. Adds: HistorianSqlResult / HistorianSqlColumn / HistorianSqlExecuteOption models, HistorianSqlResultProtocol (NRBF + diffgram parser), HistorianWcfSqlClient (ExeC/GetR orchestration with an AVEVA_HISTORIAN_SQL_DUMP diagnostic), dialect + public API. Golden WcfSqlResultProtocolTests pinned to the real clean GetR stream for the benign "SELECT 1 AS ProbeValue" (no sensitive data); gated live tests (single cell + multi-column/multi-row/NULL). Doc: wcf-exec-sql.md; roadmap R1.1 DONE; wall doc + memory updated (incl. the QTB-server-side nuance). 229 tests green. Note: a raw instrument-wcf capture corrupts a large pResultBuff with MDAS transport chunk markers (0x9F); the clean contract-level byte[] is dumped via the AVEVA_HISTORIAN_SQL_DUMP env var for the golden fixture. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
108220c36b |
R1.5 GetTagExtendedPropertiesAsync (GetTepByNm) + R1.6 closed (no op)
Ship tag extended-property reads over the 2020 WCF aa/Retr/GetTepByNm op: HistorianClient.GetTagExtendedPropertiesAsync(tag) -> name/value pairs. String-handle op reached with the Open2 storage-session GUID formatted uppercase (same format that unlocked GETRP/GETHI/ExeC). Routed via the name-based native path (GetTagExtendedPropertiesByName, server-fetch flag), not the index-based TagQuery path. Evidence-backed findings from the capture: - GetTepByNm (and GetTgByNm) succeed with the uppercase handle -- further validates the resolved string-handle wall. - QTB (StartTagQuery) does NOT punch through: captured uppercase, it still fails server-side (CMdServer::StartActiveTagnamesQuery over the aahMetadataServer pipe) -- a metadata-server blocker, not handle format. - R1.6 (localized properties) has NO distinct op (only error-message/UI-text localization in the managed client); collapses into R1.5. Closed, not throwing. Wire format (golden-pinned, synthetic bytes -- no dev tag names committed): - request tagNames = uint count + per-name(uint charCount + UTF-16) - response = uint tagCount + per-tag(marker + compact-ASCII name + uint propCount + per-prop(marker + compact-ASCII name + 0x43 VT_BSTR value) + trailer); sequence-paged. Adds: HistorianTagExtendedProperty model, HistorianTagExtendedPropertyProtocol (codec), HistorianWcfTagExtendedPropertyClient (orchestration), dialect + public API; golden WcfTagExtendedPropertyProtocolTests (4) + gated live test (HISTORIAN_TEP_TAG). Tooling: Capture-TagExtendedProperties.ps1, decode-tag-properties-capture.py, harness tag-extended-properties scenario. Docs: wcf-tag-extended-properties.md; roadmap R1.5 DONE / R1.6 collapsed; wall doc + memory updated with the QTB-server-side nuance. 228 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
4da5287d01 |
R1.2 GetRuntimeParameter + string-handle wall RESOLVED (handle-format bug)
Execute HCAL roadmap R1.2 (GetRuntimeParameterAsync) end-to-end, and in doing so
discover that the "string-handle wall" blocking R1.1/R1.4/R1.5/R1.6 was a handle
FORMAT bug, not a missing native session/filter registration.
R1.2 (shipped, live-verified):
- Captured native GetRuntimeParameter -> WCF op aa/Stat/GETRP (string-handle op,
GETHI's shape), via scripts/Capture-RuntimeParam.ps1 + instrument-wcf-{write,read}message.
- HistorianRuntimeParameterProtocol serializes pRequestBuff (54 67 01 00 + uint
nameCount + per-name uint charCount + UTF-16) and parses pResponseBuff (version +
uint resultCount + CRetVariant 0x43 VT_BSTR + uint16 len + uint16 charCount + UTF-16).
- IStatusServiceContract2.GetRuntimeParameter (GETRP) op; HistorianWcfStatusClient
passes the Open2 storage-session GUID as the string handle, UPPERCASE.
- Public HistorianClient.GetRuntimeParameterAsync(name) via the dialect.
- Golden WcfRuntimeParameterProtocolTests + gated live test; returns HistorianVersion.
String-handle wall RESOLVED (proven, public APIs deferred):
- The Open2 storage GUID works as the string handle when sent UPPERCASE
(ToString("D").ToUpperInvariant()); earlier "blocked" probes used lowercase.
- Live-probed GETHI (R1.4) -> returns data; ExeC (R1.1) -> Retr.GetV prime -> ExeC ->
GetR returns a BinaryFormatter-serialized .NET DataTable. Gated
StringHandleProbeDiagnosticTests + scripts/Capture-ExecSql.ps1 + exec-sql harness scenario.
- Docs flipped: wcf-string-handle-wall.md RESOLVED banner; roadmap R1.1/R1.4 reachable,
R1.5/R1.6 likely; wcf-status-localhost.md GETRP section.
- R1.1/R1.4 public APIs NOT shipped: ExeC needs a GetR paging loop + a BinaryFormatter-
stream parser (BinaryFormatter is removed from .NET 10); GETHI full-info struct needs
its own capture.
223 unit tests pass; gated live tests green against the local 2020 Historian.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
|
||
|
|
6d470eab4a |
R1.7: server-side event filters — ReadEventsAsync(HistorianEventFilter), live-honored
Roadmap M1 R1.7. Filters are set on the native EventQuery object via AddEventFilter(property, HistorianComparisionType, value) — NOT EventQueryArgs (time/count/order only). Found via a new harness --dump-type-members command. Captured the native filtered StartEventQuery pRequestBuff (Capture-EventFilter.ps1 + harness --event-filter knob) and diffed Equal(0) vs Contains(12) to isolate the operator field. Filter block (decoded byte-for-byte): ushort 0 + uint filterCount + uint condCount + uint nameLen + name(UTF-16) + uint 1 + ushort op + uint 1 + value(0x09-LEN-0x00 compact-ASCII) + byte 0 The filter is REAL, not inert (unlike the analog-summary knobs): a non-matching predicate returns 0 events; Type=Equal=User.Write returns only User.Write events. Verified live via both the native harness and the SDK. - HistorianClient.ReadEventsAsync(start, end, HistorianEventFilter, ct) overload - HistorianEventFilter + HistorianEventComparison (18 ops, ordinals = native) - Filter encoding in HistorianEventQueryProtocol (empty-filter path unchanged) - Golden-byte tests (block match, op field, empty-filter regression) + gated live test Single string-valued predicate only; multi-filter (OR) / multi-condition (AND via AddEventFilterCondition) framing is partially captured and not shipped. 216 unit tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
f1e23a3a02 |
M2: implement SendEventAsync — event-send rides WCF AddS2, not the storage pipe
Roadmap Milestone 2 (event sending). Capture disproved the assumption that event delivery uses the non-WCF storage-engine pipe (which would block it like revision writes): a native AddStreamedValue(HistorianEvent) leaves over WCF as AddS2 (IHistoryServiceContract2.AddStreamValues2). CM_EVENT is a built-in registered tag, so the 129 TagNotFoundInCache gate that blocks AddS2 for user tags does not apply. - R2.1: NativeTraceHarness "event-send" scenario + Capture-EventSend.ps1; two captures diffed to separate constant framing from value-dependent fields. - R2.2: HistorianEventWriteProtocol serializes the AddS2 pBuf (storage sample buffer wrapping the event VTQ) — golden-byte tested. Decoded "OS" sig + length fields + CM_EVENT tag id + EventTime/ReceivedTime FILETIMEs + Opc 192 + 0x118D descriptor + event Id + Namespace + EventType + version 5 + typed property bag. - R2.3/R2.4: HistorianWcfEventOrchestrator.SendEventAsync (Open2 event-mode 0x501 -> reuse CM_EVENT RTag2/EnsT2 -> AddStreamValues2) + HistorianClient.SendEventAsync. - R2.5: gated live test; server accepts the AddS2 (success, empty error buffer). Server requires delivered byte[].Length == declared packet length (uint32@0x04); the native relies on the MDAS encoder adding a pad byte, so the SDK emits an explicit trailing 0x00 (else AddS2 rejects with "CValuStream buffer size vs packet length mismatch"). Original events only (RevisionVersion=0) with string properties; other property types + revision/update/delete throw ProtocolEvidenceMissingException. Caveat (documented): accepted events are not persisted on the local dev box; the native client behaves identically (event ingestion pipeline inactive) — not an SDK gap. 212 unit tests pass; 16/16 event tests pass live. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
7d5aeaeb06 |
Strengthen live event-read test: assert well-formed parsed events
ReadEventsAsync verified to return real, parsed events against the local 2020 server (e.g. User.Write with 18 properties) — the row parser (HistorianEventRowProtocol v9) is wired and works. The prior test only asserted NotNull with a stale "row format not yet decoded" comment. - Renamed to ReadEventsAsync_AgainstLocalHistorian_ReturnsWellFormedEvents. - Widened the window to 30 days (robust against a quiet recent window). - Asserts NotEmpty + per-event well-formedness (non-empty Type, non-null Properties, EventTimeUtc within the queried window) — matching the ReadRawAsync test's NotEmpty style. - Documents the known limitation: enumeration stops at the first benign `type=4 code=85` soft-terminal, so this verifies parsing correctness rather than exhaustive retrieval (draining all rows needs the code-85 decode, a capture task). Passes live (1 event over 30 days). Non-live unit suite unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
fa9cde3e2f |
CW-1: reusable capture -> sanitize -> golden-fixture pipeline
Adds the highest-leverage reverse-engineering primitive from the roadmap: one path to turn a live operation buffer into a committable golden fixture. Unblocks every capture-tier item (R0.5, R1.x, R2.1). - ProtocolCaptureSanitizer: redacts identity-bearing values (host, tag, user, machine) from a native buffer in BOTH ASCII and UTF-16LE, overwriting in place with an 'X' fill so length and every field offset are preserved (keeps the fixture useful for byte-layout RE). ASCII-letter matching is case-insensitive; secrets < 3 chars are skipped to avoid collision corruption. AssertNoSecretsRemain is a fail-closed safety net that refuses to emit if any value survives. - ProtocolFixtureWriter: serializes a capture to fixtures/protocol/<op>/<name>.json with sanitized hex, length, SHA-256 of the sanitized bytes, and a scrub report. Timestamps are passed in (deterministic / testable). - capture-tag-info CLI command: captures a live GetTagInfoFromName response and writes the fixture. The same native bytes ride inside 2023 R2 gRPC GetTagInfosFromName, so the fixture is transport-agnostic. - 11 unit tests for the sanitizer/writer (test project now references the RE tool). - First real fixture: get-tag-info/analog-*.json — a 98-byte Int4 CTagMetadata buffer captured live from the local Historian 2020 server, tag name redacted, verified to contain no identity (descriptor 03 c3 00 31 = Int4, as documented). 180 non-live unit tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6b892b69ba |
R0.6: fail-closed server interface-version gate at connect
Turns the previously-discarded GetInterfaceVersion result into a connect-time version pin. The native buffers carried in the WCF/MDAS body (and in the 2023 R2 gRPC bytes fields) are framed per native interface version; parsing them against an unexpected version risks silent misinterpretation, so we throw rather than best-effort parse. - HistorianServerVersionGate + HistorianServiceInterface: evidence-based supported versions discovered from a live Historian 2020 server (product 20.0.000) via the wcf-probe command — History=11, Retrieval=4, Transaction=2. Status' GetInterfaceVersion returns 0, so Status is reachability-only. - HistorianClientOptions.VerifyServerInterfaceVersion (default true) — bypass knob for bringing up a server whose reported integers aren't yet captured (e.g. a 2023 R2 gRPC endpoint carrying the same proven 2020 buffers). - Wired into both transports' connect paths: WCF history (auth-chain helper) + retrieval (read orchestrator), and gRPC history + retrieval. - Mismatch throws ProtocolEvidenceMissingException naming reported/expected version and the bypass knob. 10 new unit tests (198 total green). Verified the gate does not regress the proven WCF read path: a live read against the local 2020 server reaches past the gate (Retr=4 matches) — the only live failures are a pre-existing environmental read timeout (OperationCanceledException), identical with and without this change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
1e9a87fce9 |
Add 2023 R2 gRPC transport (RemoteGrpc) reusing native byte payloads
Stands up HistorianTransport.RemoteGrpc end-to-end for the read path, built on the recovered 2023 R2 gRPC contract (gRPC-Web/HTTP-1.1, port 32565, gzip). The opaque protobuf `bytes` fields carry the SAME native binary payloads as the 2020 WCF/MDAS path, so the proven serializers and parsers are reused unchanged. - Grpc/Protos/*.proto: 6 protoc-validated contracts recovered from embedded FileDescriptors (authoritative, not guessed). - Grpc/HistorianGrpcChannelFactory: GrpcWebHandler/HTTP-1.1 channel, ResolvePort/ResolveAddress, optional TLS + gzip. - Grpc/HistorianGrpcReadOrchestrator: mirrors the WCF read chain over gRPC; auth uses HistoryService.ExchangeKey (the gRPC ValCl op). - Wcf/HistorianNativeHandshake: transport-agnostic Open2 request builder + SSPI/Negotiate token loop + response decode, shared by WCF and gRPC. - Op map (2020 -> gRPC): ValCl->ExchangeKey, Open2->OpenConnection, StartQuery2->StartQuery, GetNextQueryResultBuffer2->GetNextQueryResultBuffer. - HistorianClientOptions: DefaultGrpcPort=32565, GrpcUseTls. - csproj: Google.Protobuf, Grpc.Net.Client(.Web), Grpc.Tools codegen. Not yet live-verified against a 2023 R2 server: ExchangeKey is the first thing to revisit if a live server rejects the handshake; the inner byte payloads are the proven 2020 protocol. Gated live test via HISTORIAN_GRPC_HOST. 188 unit tests green; build clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6b385441c1 |
D2 follow-up: RTag2 doesn't cascade client identity to Trx
Tested hypothesis (1) from the plan: add RTag2(CM_EVENT tag id) to the priming chain before AddNonStreamValuesBegin2. Result: - RTag2 itself succeeds: returns 25-byte response (01000000000100000001EE39C30EDCDC010100000000000000), no error buffer. - But AddNonStreamValuesBegin2 still fails with the same 04 33 00 00 00 (UnknownClient = 51) for all four handle formats. So RTag2 on /Hist isn't the cross-service registration trigger we need for /Trx. Plan doc updated with the result + next-session ordered probes (try IStorageServiceContract, then IL walk CClientCommon, then server-side decompile as last resort). Probe orchestrator now also performs the RTag2 step so the test gives one-shot diagnostic visibility of both calls. 178/178 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
b40e6948e2 |
D2 (new path): SDK-direct WCF revision orchestrator + probe
Implemented HistorianWcfRevisionOrchestrator that talks WCF directly to /Trx, bypassing the native wrapper entirely. Probes AddNonStreamValuesBegin2 against the live local Historian and surfaces what the server returns. Internal-only API; no public surface added — the path isn't viable yet. Findings (live test against localhost): - ✅ The wire path is reachable. After moving from V1 (uint handle, no errorBuffer) to V2 (string handle GUID, out errorBuffer), the server recognizes the call (no ContractFilter mismatch, no exception). - ✅ Server processes the call and returns a structured 5-byte error buffer: 04 33 00 00 00 = type 4 (CustomError) + code 51 (UnknownClient). - ❌ Tried four handle formats (contextKey upper/lower, storageSessionId upper, ClientHandle as decimal string) — all return the same UnknownClient. - ❌ Adding the full priming chain (Stat.GetV ×2, Stat.GETHI ×2, UpdC3, 6× Stat.GetSystemParameter, AllowRenameTags, Trx.GetV, Stat.GetV, Retr.GetV) — same result. ITransactionServiceContract2 has no Validate/Register/Open op of its own. The client-with-Trx registration must happen via some cross- service side effect we haven't isolated. Important takeaway: the wire-format mismatch is solved (contract method names + parameter shapes match what the server expects). The remaining gap is a single missing initialization step. Documented in docs/plans/revision-write-path.md as concrete next-session steps. 178/178 tests pass (one new probe test added). Probe is gated on HISTORIAN_HOST=localhost. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
f4709ff143 |
Speculative-items sweep: IntegralDivisor, cert tests, D3/D1/D2 findings
Plan: docs/plans/speculative-items-sweep.md (also covers parallelism + findings). Implemented: - C3: HistorianTagDefinition.IntegralDivisor (default 1.0). Wire bytes flip per the captured native serializer; live probe shows the server stores IntegralDivisor on EngineeringUnit (shared) rather than per-tag, so the value is accepted on the wire but doesn't visibly persist for the test EU. Documented in the property's doc-comment. - E: HistorianWcfCertOptionTests (5 tests) covering AllowUntrustedServer- Certificate validator installation + ServerDnsIdentity propagation through CreateEndpointAddress and CreateBindingPair. Investigated + documented (deferred): - D3: Discrete/String/Int1/Int8/UInt8 EnsT2 root cause — server-side ValidationFailed: "Transaction validation failed". Native AddTag's validator rejects non-analog types; not a wire-format issue. To unlock, need to capture a working native flow via a different code path (likely SMC's tag-import path or AddTagExtendedProperties carrying type-specific metadata). Defer until a customer asks. - D1: AddTagExtendedProperties feasibility — managed surface confirmed (ArchestrA.HistorianAccess.AddTagExtendedProperties + WCF op AddTagExtendedPropertyGroups). Cost estimated at 1-2 days of focused RE work due to CTagExtendedPropertyGroup payload complexity. Defer. - D2: AddRevisionValuesBegin/Value/End — sub-plan written at docs/plans/revision-write-path.md with 5-step capture sequence, workstream estimates, and risk register. Implementation deferred. 177/177 tests pass (was 172; +5 cert tests + 1 IntegralDivisor unit test, harness probe results not committed). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
7e4d713eb3 |
Cross-platform NegotiateAuthentication; StorageType field; docs polish
HistorianSspiClient rewritten on top of System.Net.Security.NegotiateAuthentication
in place of P/Invoke into secur32.dll's InitializeSecurityContextW. The class
keeps the same Next() / Dispose() / two-constructor surface so callers don't
change. RequiredProtectionLevel=EncryptAndSign + AllowedImpersonationLevel=
Identification produces a request-flag set equivalent to the captured native
0x2081C / 0x81C bitmasks (still preserved as constants for the existing unit
tests). Removes the only Windows P/Invoke in the production SDK; the
[SupportedOSPlatform("windows")] gating elsewhere stays in place pending a
separate sweep.
HistorianStorageType (Cyclic = 1, Delta = 2):
Captured 2026-05-04 via --write-storage-type on the harness. Delta differs
from Cyclic in three places — header byte 10 (0x02 -> 0x06), flag-block
byte 1 (0x01 -> 0x02), and 4 zero bytes inserted after StorageRate before
the FILETIME. Server persists Tag.StorageType=1/2 accordingly. Plumbed
through HistorianTagDefinition.StorageType + serializer + orchestrator + 2
new tests (golden bytes + live SQL persistence verification).
Docs polish:
CLAUDE.md no longer claims "no P/Invoke" (HistorianSspiClient is the one
allowed P/Invoke surface); updated test count to 169+; AGENTS.md Required
SDK Surface and Repository Layout brought up to date with the live state
including the write surface; handoff.md "not a git working tree" obsolete
note removed.
171/171 tests pass with the NegotiateAuthentication replacement (was 169;
+2 new tests for StorageType).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
5ce62a5900 |
Wire ApplyScaling, StorageRate; close out write-commands plan
ApplyScaling (HistorianTagDefinition.ApplyScaling): The EnsT2 trailer's second byte controls server-side scaling — `FE 00` mirrors MinRaw to MinEU and sets AnalogTag.Scaling=0; `FE 01` persists distinct MinRaw/MaxRaw and sets Scaling=1. Decoded by toggling set_ApplyScaling on the native harness and capturing the wire bytes for both values with identical inputs. The earlier docs claimed EnsureTagAsync needed a follow-up "UpdateTags" call; the WCF surface has no such operation — toggling that one byte is the whole fix. StorageRate (HistorianTagDefinition.StorageRateMs): Serializer accepts a non-default rate, validated empirically against the live server which only accepts quantized values (1000/5000/10000/60000/300000 ms). EnsureTagAsync upsert semantics: Second call on the same tag name with different fields succeeds and updates Description, MinEU, MaxEU, MinRaw, MaxRaw, Scaling in place (verified by direct SQL inspection in a live test). Plan + doc closeout: write-commands-reverse-engineering.md rewritten as a current-state plan with three workstreams (A doc closeout / B idempotency / C1 StorageRate) and a parallelism table; prior phase notes preserved as appendix. handoff.md, implementation-status.md, wcf-contract-evidence.md, README.md updated to remove "writes are out of scope" / non-existent UpdateTags references and document the actual EnsT2 wire format including the `FE xx` trailer. Reverse-engineering harness gains --write-apply-scaling and a SQL post-check that prints the persisted AnalogTag bounds so future RE sessions can verify wire→DB causality without leaving the harness. 169/169 tests pass (was 165; +4 new tests covering ApplyScaling, StorageRate golden bytes, StorageRate live persistence, and EnsureTagAsync upsert semantics). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
f32fd57874 |
Remove dead dialect methods; unblock explicit-creds tag-metadata path
Two cleanups from the post-EnsureTagAsync punch list — both isolated, no protocol discovery required. #89 dead code in Historian2020ProtocolDialect: - BrowseTagNamesAsync and GetTagMetadataAsync on the dialect both threw ProtocolEvidenceMissingException, but HistorianClient routes those calls directly to HistorianWcfTagClient — the dialect overrides were never reached. Removed both methods. ReadBlocksAsync stays (it's a deliberate guardrailed entry on the public surface). #90 explicit-creds tag-metadata path: - HistorianWcfTagClient.WcfRetrievalSession.ValidateSupportedAuth threw ProtocolEvidenceMissingException whenever IntegratedSecurity=false AND UserName/Password were supplied. But the surrounding code already wires those creds through ApplyWindowsCredential -> factory.Credentials.Windows.ClientCredential — the validator was just being conservative about an untested combination. - Inverted the check: now only rejects the no-auth-at-all combination (IntegratedSecurity=false + no UserName + no Password). The other three valid auth shapes pass through to WCF. Tests: 161 -> 163 (+2). New unit test verifies the no-auth case still throws; new gated live integration test GetTagMetadataAsync_ExplicitCredentials_AgainstLocalHistorian exercises the explicit-creds path when HISTORIAN_USER+HISTORIAN_PASSWORD are set, skips cleanly otherwise. CLAUDE.md updated: removed the two now-resolved entries from "Remaining gaps"; explicit-creds line refined to note the live-verification env-var requirement. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |