Commit Graph

18 Commits

Author SHA1 Message Date
Joseph Doherty 8a553423ed D2: definitive conclusion — revision-write requires non-WCF storage-engine pipe
IL walk of the native wrapper:

  HistorianAccess.AddRevisionValuesBegin (private, token 0x06006175)
    -> CClientCommon.AddNonStreamValuesBegin
       -> CClient.AddNonStreamValuesBegin (8-instr overload)
          -> CClient.TransactionBegin
             -> CHistStorageConnection.StartTransaction (token 0x06001FDD)
                -> CStorageEngineConsoleClient.StartTransaction

CStorageEngineConsoleClient is built on STransactPipeClient2 +
SCrtMemFile — a shared-memory + named-pipe transport to
aaStorageEngine.exe, completely separate from WCF.

The WCF ITransactionServiceContract2.AddNonStreamValuesBegin2 op is a
server-side relay that requires a pre-existing storage-engine pipe
session for the client. Without that pipe session, the WCF relay returns
UnknownClient (51) — and there's no way to establish the pipe session
via WCF.

D2 is unimplementable as a pure-managed-WCF SDK. The native wrapper
itself depends on the C++ shared-memory channel; replicating that from
managed code would require implementing the storage-engine pipe
protocol, which is a major undertaking and out of scope.

The ITransactionServiceContract2 declaration in our contracts file
stays as documentation; no public API or orchestrator added.
HistorianWcfRevisionOrchestrator remains as an internal probe /
regression check — re-run the probe test if anyone believes the
architecture has changed.

178/178 tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 02:59:29 -04:00
Joseph Doherty 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>
2026-05-05 02:54:52 -04:00
Joseph Doherty 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>
2026-05-05 02:51:26 -04:00
Joseph Doherty b5e5f5485b D2: gate is in the C++ HistorianClient, not the managed wrapper
Direct HistorianAccess.AddNonStreamedValue (the 4-param overload that
bypasses HistorianDataValueList and goes straight to
HistorianClient.AddNonStreamedValueAsync) ALSO fails with 129
TagNotFoundInCache against SysTimeSec, even with validate=false.

So the cache check is inside the native C++ HistorianClient's
per-connection tag list — there's no managed-callable bypass.

Critical insight discovered: the SDK doesn't use the C++ HistorianClient
at all. It talks WCF directly. The cache gate that blocks the native
wrapper may not block a managed WCF client because the gate is enforced
by aahClientManaged, not by the WCF server.

This shifts the recommendation for any future D2 attempt from "wrap the
native API" (which is genuinely blocked) to "implement the wire path
directly on top of the existing ITransactionServiceContract methods and
test against the live server" (unverified but plausibly viable). The
harness can't help with that path — the wrapper itself is the blocker
we'd be bypassing.

177/177 tests still pass; harness gains --write-revision-direct flag
for further probing of the native-wrapper path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 02:34:02 -04:00
Joseph Doherty 3af8a13059 D2 (revision-write): probe SysTimeSec — same gate, narrower scope
Extended the harness with --write-revision-target-tag <name> (overrides
the value's TagKey via SQL lookup) and --write-revision-skip-validate
(passes false to AddNonStreamedValue's `validate` boolean). Added
--write-revision-commit gate so the harness validates without actually
calling SendValues by default — important when targeting system tags.

Probed SysTimeSec (wwTagKey=12, server-cache-resident system tag):
- AddNonStreamedValue: ErrorCode=TagNotFoundInCache (129) — same failure
- With validate=false: same failure (the cache check is intrinsic, not
  gated by the boolean)

Conclusion: the gate is per-(client-session, tag), not per-server-cache.
Even tags the SERVER cache knows about are rejected because the LIBRARY
maintains a separate per-connection tag list that AddNonStreamedValue
checks. That list isn't populated by knowing the wwTagKey alone — it
needs whatever mechanism (RegisterTags2 / read flow side effect / IO
server registration) that we haven't reverse-engineered.

The revision-write path remains architecturally blocked for managed
clients. Plan doc updated with the SysTimeSec finding.

177/177 tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 02:27:58 -04:00
Joseph Doherty 2feb56d52c D2 (revision-write): empirically blocked by same gate as AddS2
Drove the revision-write flow via reflection in the native trace harness
(--write-revision-values) to see whether it bypasses the AddS2
architectural blocker. It doesn't.

Findings:
- HistorianAccess.CreateHistorianDataValueList(NonStreamedOriginal) succeeds
- HistorianDataValueList.NonStreamedValuesBegin() succeeds (batchID 0->1)
- HistorianDataValueList.AddNonStreamedValue(value, validate=true, out err)
  FAILS with ErrorCode=TagNotFoundInCache (129) — same client-side
  validation gate that blocks AddS2
- AddNonStreamedValuesEnd() returns void; SendValues() returns true
  with Success because the list is empty (no value was ever added)
- No AddNonStreamValues* WCF calls reach the wire

Conclusion: the revision-write path requires the tag to be in the
library's runtime tag cache, which is only populated by configured
IO server / Application Server pipelines, not by HistorianAccess.AddTag.
This matches the architectural blocker documented for AddS2 and means
no public WriteRevisionsAsync / BeginRevisionAsync should be added to
the SDK — the path is unreachable for client-created sandbox tags.

The Wcf/Contracts/ITransactionServiceContract methods (AddNonStream-
ValuesBegin/AddNonStreamValues/AddNonStreamValuesEnd) remain declared
for completeness; no orchestrator or public surface is added.

The harness extension is preserved as a deterministic reproducer for
the blocker: re-run --write-revision-values to verify the gate any
time. docs/plans/revision-write-path.md updated with the empirical
finding plus the original plan retained as historical context.

177/177 tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 01:45:48 -04:00
Joseph Doherty 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>
2026-05-05 00:11:40 -04:00
Joseph Doherty 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>
2026-05-04 22:19:37 -04:00
Joseph Doherty 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>
2026-05-04 22:04:27 -04:00
dohertj2 200493c990 DelT investigation: wire-byte parity is necessary but not sufficient
Investigation step 1 — wire-byte parity check. Captured native DelT
sends ref input values statusSize=1 + status=null (encoded as .nil
on the wire). SDK was passing statusSize=0 + status=[] (empty array).
Updated SDK to match native input values.

Investigation step 2 — verified DelT still doesn't work standalone.
With the ref-input fix, SDK DelT now returns false (instead of the
previous true-with-no-effect). Tag continues to persist in
Runtime.dbo.Tag. So the wire-byte parity fix moved the symptom but
didn't resolve the root cause.

Investigation step 3 — discovered EnsureTagAsync is ALSO silently
broken. Byte-for-byte wire matches captured native EnsT2 (golden test
still passes), but the call returns false and does NOT create the tag
in the DB. The earlier "EnsureTagAsync round-trip test passing" was
relying on the persistent tag from the broken DelT — a false
positive.

Two distinct issues remain:
1. EnsT2 silently fails server-side (returns false; no tag created)
2. DelT returns false even with native-matching wire bytes

Test adjusted to no longer assert that EnsureTagAsync actually
creates the tag (because it currently doesn't). Test still exercises
the SDK call path to confirm it doesn't throw.

Next-session diagnostic: write a custom IClientMessageInspector for
the SDK's WCF channel that captures outgoing DelT/EnsT2 bytes to a
file. Compare byte-for-byte (offset by offset, not just per-field)
against captured native to isolate the difference.

130/130 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 08:51:24 -04:00
dohertj2 cfc8d44e3a Implement EnsureTagAsync (live-verified) + DeleteTagAsync (DelT semantics partial)
New SDK surface:
  HistorianClient.EnsureTagAsync(HistorianTagDefinition)
  HistorianClient.DeleteTagAsync(string tagName)

Plumbing:
  src/AVEVA.Historian.Client/Models/HistorianTagDefinition.cs
    Public input model — TagName/Description/EngineeringUnit/DataType/MinEU/MaxEU.
    Currently only HistorianDataType.Float is live-verified.

  src/AVEVA.Historian.Client/Wcf/HistorianTagWriteProtocol.cs
    SerializeAnalogCTagMetadata produces 146-byte payload byte-for-byte
    identical to the captured native EnsT2(Float) request.
    SerializeDeleteTagNames produces ushort 0x6751 + ushort 1 + uint count
    + per-tag (uint charCount + UTF-16 chars).

  src/AVEVA.Historian.Client/Wcf/HistorianWcfTagWriteOrchestrator.cs
    Both EnsT2 and DelT run the full Stat-priming chain captured for the
    analog flow (UpdC3 + Stat.GetV ×3 + Stat.GETHI ×2 + 7× GetSystemParameter
    + Trx.GetV + Retr.GetV).

  src/AVEVA.Historian.Client/Wcf/HistorianWcfTagClient.cs
    MapDataType extended to accept tag-origin marker 0xC7 (SDK-created tags).

Tests:
  5 golden-byte tests (HistorianTagWriteProtocolTests):
    SerializeAnalogCTagMetadata byte-for-byte match against captured 146-byte fixture
    SerializeAnalogCTagMetadata produces different bytes for different inputs
    SerializeDeleteTagNames single-tag matches captured shape
    SerializeDeleteTagNames multi-tag appends each
    SerializeDeleteTagNames empty list throws

  1 live integration test (gated by HISTORIAN_WRITE_SANDBOX_TAG):
    EnsureTagAsync_AndDeleteTagAsync_RoundTrip_AgainstLocalHistorian
    EnsureTagAsync creates the sandbox tag, GetTagMetadataAsync reads it
    back. 130/130 tests pass.

Harness improvements:
  --write-delete-after now runs DelT independently of AddStreamedValue
  outcome.
  HistorianTagStatusList constructed correctly for DeleteTags reflection
  call (previous StringCollection attempt failed with TypeMismatch).

Known DelT gap: SDK's DeleteTagAsync returns true but server-side
cascading deletion does not always complete (the row remains in
Runtime.dbo.Tag). The captured native flow's DelT removes the tag
cleanly (verified via harness --write-delete-after), so something
around the WCF DelT call is missing from our orchestrator. Documented
as known issue with SMC-based cleanup as workaround.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 08:33:21 -04:00
dohertj2 b3d22befd0 write-commands plan: AddS2 prereq is architectural - not implementable as generic client write
Three follow-up attempts to satisfy the AddS2 server-cache prereq all
failed at the same client-side gate before any AddS2 byte reached the
wire:

1. TagKey synthetic→real override. First attempt used the placeholder
   TagKey=10000000 returned by HistorianAccess.AddTag. Native
   AddStreamedValue refused with error 168 "Tag not added to server".
   Harness now ALWAYS resolves the real wwTagKey from Runtime.dbo.Tag
   after AddTag (logged as TagKeyOverride: Synthetic→RealFromSql).
   Error code shifted to 129 "Tag not found in cache" — request now
   reaches the server but the server's in-memory tag cache doesn't
   know about the new tag.

2. Server-cache settle wait. Up to 8s sleep between AddTag and
   AddStreamedValue (--write-resync-wait-seconds N). Wait period
   contains 2× UpdC3 + 2× Trx/GetV keep-alives but no server-side
   cache update — error 129 persists.

3. Fresh process / fresh connection. Skipped AddTag entirely
   (--write-skip-add-tag) and ran AddStreamedValue alone against the
   already-existing sandbox tag. New native client instance, new
   client-side cache, new server session. SAME error 129 — no AddS2
   bytes sent on wire. Capture confirms 44 records ending in Close2.

Interpretation: the Historian engine's runtime tag cache only ingests
tags from configured IOServers / Application Server data pipelines,
not from HistorianAccess.AddTag-only client flows. AddTag populates
Runtime.dbo.Tag (wwTagKey=240 was created) but doesn't register the
tag with the live cache that AddStreamedValue checks. That
registration happens server-side when an upstream data producer (an
OPC driver, AnE event subsystem, Application Server attribute store)
claims the tag.

WriteValueAsync therefore CANNOT be implemented as a generic client
API against this server architecture. The SDK's realistic writeable
surface is now narrowed to EnsureTagAsync + DeleteTagAsync only.

Harness changes:
- --write-skip-add-tag    skip the AddTag call (for fresh-cache test)
- --write-skip-add-value  skip the AddStreamedValue call (capture EnsT2 only)
- --write-resync-wait-seconds N  sleep N seconds between AddTag and
                                  AddStreamedValue (default 0)
- TagKey lookup now ALWAYS hits SQL after AddTag, not just when
  the synthetic key is 0.

Plan doc updated with full Phase 2 follow-on findings + revised
remaining work (4-item checklist focused on EnsureTagAsync/
DeleteTagAsync, plus a stretch goal of probing AddRevisionValues*
against an existing-tag).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 08:06:28 -04:00
dohertj2 b5f9a71fe7 write-commands plan: Phase 2 partial - capture EnsT2(Float) wire bytes
Per plan §1 in scope: EnsT2 for analog tags, AddS2, DelT.
Per plan §2 safety: localhost only, single sandbox tag
RetestSdkWriteSandbox, harness refuses any name not starting with
RetestSdkWrite, time-bounded writes, ReadOnly=false only when scenario
is "write".

Phase 2 actually executed:

1. tools/AVEVA.Historian.NativeTraceHarness/Program.cs extended with
   --scenario write. New args:
     --write-sandbox-tag <name>  (default RetestSdkWriteSandbox)
     --write-value <numeric>     (default 42.5)
     --write-data-type <name>    (default Float)
     --write-delete-after        (best-effort cleanup)
   Toggles ConnectionArgs.ReadOnly=false when scenario is "write" so
   the connection accepts the write attempt instead of rejecting at
   the harness boundary with error 132 "Operation is not enabled".

2. Sandbox tag RetestSdkWriteSandbox created in Runtime DB
   (wwTagKey=240, AcquisitionType=2 Manual, StorageType=1 Cyclic)
   via the harness's AddTag call. Single dedicated tag per safety §1.

3. Captured the full write-flow wire sequence at
   artifacts/reverse-engineering/instrumented-wcf-writemessage-writes/
   bothmessage-write-capture-latest.ndjson (46 records, 23 outgoing +
   23 incoming).

   The chain is identical to the event flow except:
     - EnsT2 payload is the 146-byte analog CTagMetadata instead of
       the 83-byte event one
     - NO RTag2 between Open2 and EnsT2 (events used RTag2 with
       CmEventTagId)

4. The 146-byte analog CTagMetadata layout is dumped in the plan doc
   for layout decoding. Visible fields (still being aligned against
   CTagUtil.ConvertTagMetadataToHistorianTag IL at token 0x060055CE):
     - tag name "RetestSdkWriteSandbox" (compact ASCII, len 21)
     - 16 bytes of FF (CommonArchestraEventTypeId placeholder unused
       for analog?)
     - description "SDK write-RE sandbox tag" (compact ASCII, len 24)
     - metadata provider "MDAS" (compact ASCII)
     - engineering unit "test" (compact ASCII)
     - Int64 FILETIME (date-created, year 2026)
     - uint32 0x2710 = 10000 (storage-related, possibly StorageRate)
     - double 1.0 (likely IntegralDivisor or scaling factor)
     - 5-byte trailer FE 00 01 01 01 (matches event tag's
       2F 27 01 01 01 shape)

5. AddS2 BLOCKED CLIENT-SIDE at error 168 "Tag not added to server".
   Native AddStreamedValue refuses to send because the tag isn't in
   the server's session cache, even though EnsT2 created it in the
   Runtime DB. Likely needs RTag2(analog tag GUID) prereq similar
   to the event flow's RTag2(CmEventTagId), or one of
   aahClientCommon.CHistStorage.AddTagidPairs (token 0x0600202F) or
   AddTagsWithServerTagId (token 0x06002026). AddS2 wire bytes NOT
   captured this session.

6. scripts/decode-write-capture.py — sanitized decoder for the
   capture, walks the 46 records and dumps the EnsT2 InBuff bytes
   for layout work. No identity strings; only sandbox-chosen values
   appear in output.

Phase 2 remaining work documented in the plan doc as a 5-item
checklist for the next session:
  1. Decode the AddS2 prereq (likely RTag2 with analog tag GUID).
  2. Capture AddS2 wire bytes once prereq is satisfied.
  3. Implement HistorianAddTagsProtocol.SerializeAnalog/Discrete/
     String CTagMetadata variants.
  4. Implement HistorianAddStreamValuesProtocol.Serialize.
  5. Implement public surface: EnsureTagAsync, WriteValueAsync,
     DeleteTagAsync (golden-byte + gated live integration tests).

No SDK source changed — implementation deferred until AddS2 wire
bytes are in hand.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 07:55:27 -04:00
dohertj2 e3c003d978 write-commands plan: execute Phase 1 discovery (no DB writes)
Phase 1 of docs/plans/write-commands-reverse-engineering.md is purely
discovery — static IL inspection, public-API enumeration, scope
elimination — and produces no DB writes. Phase 2 requires extending
NativeTraceHarness with a write scenario AND explicit operator approval
per the plan's safety rule §1, so it is deferred to a separate session.

Phase 1 findings recorded inline in the plan's status header:

1. §3.4 ModifyData/DeleteData eliminated — no managed wrapper exists.
   `methods` returns zero hits for EditValue, ModifyValue, EditData,
   DeleteData, ModifyData, OverwriteData. Per the plan's own §3.4
   disposition rule, this op is REST/SMC-only.

2. §4.a native serializer tokens identified for Phase 2:

   Public managed-wrapper write API in ArchestrA.HistorianAccess:
   - AddTag (0x0600619A)
   - AddStreamedValue × 3 overloads (0x0600618C/D/E)
   - AddNonStreamedValue × 2 overloads (0x0600618F/90)
   - DeleteTags (0x060061A4)
   - AddRevisionValuesBegin / AddRevisionValue / AddRevisionValuesEnd
     / AddRevisionValues (0x06006175-77, 0x0600617F)
     — covers the bulk-modify use case the eliminated ModifyData would
     have served. Worth folding into Phase 2 scope.

   Native serializers:
   - CTagUtil.ConvertTagMetadataToHistorianTag (0x060055CE) — 412 IL
     instructions; builds CTagMetadata for analog/discrete/string.
     Calls every CTagMetadata accessor we'd need to surface
     (Min/MaxRaw, Min/MaxEU, Unit, Message0/1, MaxLength,
     IntegralDivisor, DefaultTagRate, RolloverValue) plus all the
     CDataType predicates already decoded.
   - CHistoryConnectionWCF.AddStreamValuesToHistorian (0x0600404C)
     — confirms the on-wire shape matches our existing
     IHistoryServiceContract2.AddStreamValues2 declaration.

3. Chicken-and-egg resolved: the first §3.1 EnsT2 test creates the
   sandbox tag itself, so no SMC step is needed.

4. Open question §8.6 answered — the wrapper exposes both
   AddStreamedValue and AddNonStreamedValue, so the SDK should
   eventually surface both real-time and backfill write modes.

Phase 2 next steps recorded inline in the plan as a 5-item executable
checklist (extend harness with --scenario write, capture, decode
EnsT2(analog) + AddS2 bytes, implement public surface, gated live
tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 07:41:34 -04:00
dohertj2 6888b8c55a Wire SDK for remote-TCP end to end; live-verify RemoteTcpIntegrated
Executes docs/plans/tcp-connection-validation.md. Full read-only SDK
surface now works against a remote AVEVA Historian over Net.TCP with
Windows transport authentication. 124/124 tests pass; the +10 new live
integration tests in RemoteTcpIntegrationTests.cs are gated by
HISTORIAN_REMOTE_TCP_HOST + HISTORIAN_REMOTE_TCP_TAG.

Two SDK bugs found while executing the plan:

1. Historian2020ProtocolDialect.ReadRawAsync / ReadAggregateAsync /
   ReadAtTimeAsync / ReadEventsAsync had explicit
   `if (_options.Transport != HistorianTransport.LocalPipe) return Missing<T>`
   guards. These were a guardrail from before the orchestrators handled
   TCP; the orchestrators have always used CreateBindingPair(options)
   which dispatches on transport correctly. Gates removed.

2. HistorianWcfStatusClient and HistorianWcfEventOrchestrator hardcoded
   HistorianWcfBindingFactory.CreatePipeEndpointAddress for the auxiliary
   services (Stat, Trx, Retr). Worked for LocalPipe; for TCP it produced
   an EndpointAddress with scheme net.pipe attached to a TCP binding
   (channel factory rejected the URI). Worse, when only the endpoint was
   transport-aware, the binding still requested a Windows-transport-
   security upgrade that the Stat endpoint over TCP doesn't support
   (auxiliaries don't repeat the auth — the Hist session is already
   authenticated). Added two helpers:
   - HistorianWcfBindingFactory.CreateAuxiliaryEndpointAddress(options, name)
     -> net.pipe for LocalPipe, net.tcp for remote
   - HistorianWcfBindingFactory.CreateAuxiliaryBinding(options)
     -> NamedPipe for LocalPipe, plain MdasNetTcpBinding for remote
   Both call sites updated.

Live verification against the remote (probed previously in prior
sessions; reachability re-confirmed today):
- ProbeAsync over RemoteTcpIntegrated and RemoteTcpCertificate
- ReadRawAsync (8 samples returned for SysTimeSec)
- ReadAggregateAsync (TimeWeightedAverage, 1-min cycle, 10-min window)
- ReadAtTimeAsync (3 timestamps)
- BrowseTagNamesAsync (finds the test tag)
- GetTagMetadataAsync (full metadata populated)
- ReadEventsAsync (chain runs without throwing)
- GetConnectionStatusAsync (ConnectedToServer=true)
- GetSystemParameterAsync (HistorianVersion="20,0,000,000")

The default 'NT SERVICE\aahClientAccessPoint' SPN turned out to work
for the remote too — discovery workstream A (SPN-finding) was not
needed in practice.

README and the TCP plan doc updated to reflect the executed status.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 07:33:50 -04:00
dohertj2 1b31c24c8d Plan TCP connection validation (live verification of the existing remote-TCP plumbing)
docs/plans/tcp-connection-validation.md (308 lines):
  Plan to live-verify the RemoteTcpIntegrated and RemoteTcpCertificate
  transports against an actual remote AVEVA Historian. The SDK's
  HistorianWcfBindingFactory already builds all three bindings
  (CreateMdasNetTcpBinding, CreateMdasNetTcpWindowsBinding,
  CreateMdasNetTcpCertificateBinding) but only LocalPipe has been
  exercised end-to-end. Wire format is identical across transports;
  only WCF binding shape and credential negotiation differ.

  Discovery workstreams A/B/C run in parallel (SPN discovery via static
  IL + WCF probe; cert binding requirements via wcf-cert-probe; operator
  preconditions checklist). D blocks on A. Verification tracks V1-V5 also
  parallelize once V1 (ProbeAsync) confirms the transport is reachable.
  Includes risks (SPN mismatch, cert chain validation, idle disconnect,
  Open2 response delta, compression negotiation, time skew, false-positive
  empty reads), success criteria, eight open questions, and explicit
  out-of-scope items filed under the existing write-commands and
  store-forward plans.

  No code changes; no preconditions assumed met. Implementer must satisfy
  §2 preconditions (reachable remote Historian, port 32568 open, test
  account, SPN registered, etc.) before §4 discovery starts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 07:20:39 -04:00
dohertj2 6f01b83313 Plan two reverse-engineering campaigns: write commands + store/forward cache
docs/plans/write-commands-reverse-engineering.md (425 lines):
  Plan for adding WriteValueAsync (AddS2 stream values), EnsureTags2 for
  analog/discrete/string tags, and DelT for sandbox cleanup. Hard safety
  rules center on a dedicated sandbox tag gated by env var, time-bounded
  writes, SQL ground-truth verification per session, explicit rollback.
  Five-step RE workflow mirrors the read/event decode (static IL discovery
  -> instrument-wcf-writemessage capture -> instrument-wcf-readmessage
  capture -> byte/IL alignment -> managed serializer + golden-byte tests).
  Risks call out auth-chain unknowns, parameter-name-mismatch class,
  silent-success failure modes, History-vs-Storage service question.

docs/plans/store-forward-cache-reverse-engineering.md (501 lines):
  Plan for replacing the synthesized GetStoreForwardStatusAsync with a
  real implementation. Architecture investigation already partially
  answered via IL inspection during planning: ArchestrA.HistorianAccess.
  GetStoreForwardStatus (token 0x06006187) reads an in-process C struct
  via calli to mdas_GetStorageStatus, kept current by server-pushed WCF
  callbacks (IStatusServiceContract2.SetStoreForwardEvent). CSFConnection.
  GetSFPipeName indicates a separate Named Pipe sidecar exists when SF
  is configured. Five parallelizable discovery workstreams, six concrete
  RE steps with cited tokens, eight risks, eight success criteria.

Both plans deliberately produce no code changes and no captures. They
exist so the next implementer can start with full context.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 07:16:32 -04:00
dohertj2 c95824a65d Initial commit: managed .NET 10 AVEVA Historian SDK + reverse-engineering toolkit
Full read-only SDK (src/AVEVA.Historian.Client) implementing the CLAUDE.md required
surface against AVEVA Historian's binary WCF protocol — no native AVEVA runtime
dependency. All operations live-verified against a local Historian:

- ProbeAsync, ReadRawAsync, ReadAggregateAsync, ReadAtTimeAsync, ReadEventsAsync
- BrowseTagNamesAsync, GetTagMetadataAsync (17 native data-type codes mapped)
- GetConnectionStatusAsync, GetStoreForwardStatusAsync, GetSystemParameterAsync
- 108/108 unit + integration tests pass

Includes the reverse-engineering toolkit (tools/AVEVA.Historian.ReverseEngineering)
used to decode the protocol: WCF probes, IL inspection via dnlib, and IL-rewrite
instrumentation (instrument-wcf-{write,read}message etc.) plus the .NET Framework
trace harness (tools/AVEVA.Historian.NativeTraceHarness) for parity testing.

Sanitized handoff evidence under docs/reverse-engineering/. Native AVEVA binaries
(current/, aveva-install-x64/, aveva-install-x86/) are gitignored — fetch separately
from the AVEVA installer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 06:31:48 -04:00