diff --git a/CLAUDE.md b/CLAUDE.md index e4331e2..869be19 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,8 +20,9 @@ Writes (added 2026-05-04 by explicit user request — do not extend further with - `EnsureTagAsync` for analog types: Float, Double, Int2, Int4, UInt4 (live-verified end-to-end). Other types (SingleByteString/DoubleByteString/Int1/Int8/UInt8) fail at native AddTag — likely require a different path and are intentionally not supported. `MinEU`/`MaxEU`/`MinRaw`/`MaxRaw` all round-trip into the DB. By default `ApplyScaling=false` and the server mirrors MinRaw→MinEU and sets `AnalogTag.Scaling=0`; set `ApplyScaling=true` on the definition to persist distinct raw bounds with `AnalogTag.Scaling=1`. The wire encoding is the trailer's second byte (`FE 00` vs `FE 01`). - `DeleteTagAsync` +- `AddHistoricalValuesAsync` (added 2026-06-21 by explicit user request — M3 historical/backfill writes). **gRPC-only** (`HistorianTransport.RemoteGrpc`); non-gRPC transports throw `ProtocolEvidenceMissingException`. Reverse-engineered by capturing the native 2023 R2 client: the historical write rides `HistoryService.AddStreamValues` with an "ON" storage-sample buffer (`HistorianHistoricalWriteProtocol`, golden-tested), NOT the TransactionService `AddNonStreamValues` path the static decompile suggested. Orchestrator (`HistorianGrpcHistoricalWriteOrchestrator`): write-enabled session → `GetTagInfosFromName` (resolves the per-tag GUID = the tag-info `TypeId`) → `AddStreamValues`. Tag must pre-exist (`EnsureTagAsync`). Float value encoding only (the captured type; value = `u32(0) + float32` in an 8-byte slot). Live-validated end-to-end (write + read-back) against the 2023 R2 server. The D2/`AddS2` cache gate (err 129) does NOT block the primed 2023 R2 client. See `docs/plans/revision-write-path.md` §"R3.1 CAPTURED". -`AddS2` (write samples) is architecturally blocked — server cache only ingests from configured IOServers/ApplicationServer pipelines. Do not add write-samples support. +`AddS2` (streaming process-sample writes for user tags) remains architecturally blocked — the server cache only ingests from configured IOServers/ApplicationServer pipelines. Do not add streaming write-samples support. (`AddHistoricalValuesAsync` is the distinct *non-streamed original/backfill* path and is supported.) Methods without protocol evidence currently throw `ProtocolEvidenceMissingException` from `Historian2020ProtocolDialect`. Do not stub fake behavior — leave them throwing until evidence supports an implementation. diff --git a/docs/plans/hcal-roadmap.md b/docs/plans/hcal-roadmap.md index f06788d..58c8ffc 100644 --- a/docs/plans/hcal-roadmap.md +++ b/docs/plans/hcal-roadmap.md @@ -255,8 +255,8 @@ byte-correct `AddS2` (✅). Appears-and-reads-back is environment-gated on event | ID | Work | gRPC op | Status | |---|---|---|---| | R3.1 | Decode non-streamed VTQ packet | `History.AddStreamValues` ("ON" buffer) + `EnsureTags` | ✅ **CAPTURED + VALIDATED 2026-06-21.** Drove the native 2023 R2 client through a committed historical write (sandbox tag) with the IL-rewritten gRPC client dumping every `byte[]`; the value **read back over gRPC**. The path is **NOT** `AddNonStreamValues`/TransactionService — it's **`HistoryService.AddStreamValues`** with an **"ON" storage-sample buffer** (AddS2 "OS" family) + `EnsureTags`. Buffer decoded: `"ON"(0x4E4F) + u16 count + u32 totalLen + u16 payloadLen + 16B tag GUID + FILETIME + u16 quality + u32 type + FILETIME + 8B double`. D2 cache gate does NOT block the primed 2023 R2 client. See [`revision-write-path.md`](revision-write-path.md) §"R3.1 CAPTURED". | -| R3.2 | `AddHistoricalValuesAsync` | `History.AddStreamValues` ("ON") + `EnsureTags` | 🟡 **buffers captured + validated**; remaining: build the managed "ON" serializer in `src/` (adapt `HistorianEventWriteProtocol` "OS"), resolve tag GUID, reuse `EnsureTagAsync` CTagMetadata, wire `AddStreamValues` over the gRPC orchestrator, golden-test, then ship | -| R3.3 | Ingest-permission validation | confirm the target accepts original-data insert (distinct from `AddS2` cache wall) | ✅ **distinct on gRPC** — Begin succeeded against a real write-enabled session (the WCF/native cache gate does not apply here) | +| R3.2 | `AddHistoricalValuesAsync` | `History.AddStreamValues` ("ON") + `EnsureTags` | ✅ **SHIPPED + LIVE-VALIDATED 2026-06-21.** `HistorianClient.AddHistoricalValuesAsync(tag, values)` over `RemoteGrpc`: write-enabled session → `GetTagInfosFromName` (resolves the per-tag GUID = tag-info `TypeId`) → `HistoryService.AddStreamValues` ("ON" buffer, golden-tested). The pure-managed SDK wrote a value and read it back live. Float values only (captured type); gRPC-only (non-gRPC throws). | +| R3.3 | Ingest-permission validation | confirm the target accepts original-data insert (distinct from `AddS2` cache wall) | ✅ **confirmed** — the D2/AddS2 cache gate (err 129) does NOT block the primed 2023 R2 client; the historical write commits and reads back | **Acceptance:** historical points inserted and read back. **WCF path closed (D2).** gRPC path: **transaction lifecycle proven (Begin/End live) + full sequence mapped**; the remaining insert is a @@ -330,5 +330,5 @@ event-send). M3/M4 as demand dictates. | M0 gRPC parity + capture tooling | foundation | M | unblocks everything, Windows-free | ✅ **done** | | M1 cheap surface | TRIVIAL/BOUNDED | M–L | most remaining read/config | ✅ **done** (reachable surface; rest bounded out) | | M2 event send | CAPTURE | S–M | headline write capability | ✅ **done** | -| M3 historical writes | BOUNDED | M | backfill | 🟡 **wire path CAPTURED + VALIDATED (2026-06-21)** — native historical write = `HistoryService.AddStreamValues` ("ON" buffer) + `EnsureTags` (NOT AddNonStreamValues/Transaction; NOT OpenStorageConnection). Committed write read back over gRPC. Remaining: build the "ON" serializer in `src/` + ship `AddHistoricalValuesAsync`. WCF still blocked (D2) | +| M3 historical writes | BOUNDED | M | backfill | ✅ **SHIPPED + LIVE-VALIDATED (2026-06-21)** — `AddHistoricalValuesAsync` over gRPC = `HistoryService.AddStreamValues` ("ON" buffer) + tag-GUID resolve. Pure-managed SDK write read back live. Float-only (captured type). WCF still blocked (D2) | | M4 SF / revisions / redundancy | HARD | L×N | parity completeness | defer (R4.2 = same pipe wall) | diff --git a/docs/plans/revision-write-path.md b/docs/plans/revision-write-path.md index 0dac986..089104e 100644 --- a/docs/plans/revision-write-path.md +++ b/docs/plans/revision-write-path.md @@ -244,10 +244,13 @@ the server had assigned the tag key; (b) the value is keyed by a **16-byte tag G `HistorianTagMetadata.Key`); (c) batch lifecycle is `NonStreamedValuesBegin → AddNonStreamedValue → SendValues → AddNonStreamedValuesEnd` (End-before-Send returns err 160 InvalidBatchId). -**Remaining to ship `AddHistoricalValuesAsync`:** build the managed "ON" `AddStreamValues` serializer in -`src/` (adapt `HistorianEventWriteProtocol`'s "OS" builder), resolve the tag GUID, reuse the existing -`EnsureTagAsync` CTagMetadata, wire `HistoryService.AddStreamValues` over the gRPC orchestrator, golden-test -the buffer, then a real write + read-back on a sandbox tag. Capture artifacts (gitignored): +**SHIPPED 2026-06-21 — `AddHistoricalValuesAsync`.** `HistorianClient.AddHistoricalValuesAsync(tag, values)` +over `RemoteGrpc`: `HistorianGrpcHistoricalWriteOrchestrator` opens a write-enabled session → +`GetTagInfosFromName` (resolves the per-tag GUID = the tag-info record's `TypeId`) → +`HistoryService.AddStreamValues` ("ON" buffer from `HistorianHistoricalWriteProtocol`, golden-tested) per +sample. The pure-managed SDK wrote a value and read it back live (gated test +`AddHistoricalValuesAsync_OverGrpc_WritesAndReadsBack`). Float value encoding only (the captured type); +gRPC-only. Capture artifacts (gitignored): `artifacts/reverse-engineering/grpc-nonstream-capture/captureB4.ndjson`. ---