# v3 Batch 2 — `/raw` project-tree AdminUI + `Calculation` driver Implements `docs/plans/2026-07-15-v3-batch2-raw-ui-calculation-plan.md` (Track 0 + WP1–WP8), executed via the `/v3-batch 2` coordinator: contracts-first fan-out, worktree-isolated Opus agents per wave, a code-reviewer pass + green build/test after each wave, then the non-negotiable 7-item live gate on docker-dev. ## What landed - **Track 0** — reusable `ContextMenu` (right-click + `⋯`, keyboard/focus); RFC-4180 `CsvParser`/`CsvWriter`; `DriverTypeNames` constants + a reflection-discovery guard; `CalculationEvaluator` core (VirtualTag-evaluator parity). - **Wave A** — `IRawTreeService`/`RawTreeService` (lazy tree + mutations, integrity guards, rename-warnings); `/raw` page + lazy `RawTree` with per-node context menus. - **Wave B** — driver→embeddable-form refactor + `DriverConfigModal`/`DeviceModal` (endpoint→`DeviceConfig`, Test-connect via merged config); manual tag entry + tag edit modal + `Calculation` tag editor; CSV import (staged review grid) / export; all wired into the tree (name/confirm/driver-type dialogs + post-mutation refresh). - **Wave C** — browse re-target (two-tier gate, merged-config feed, commit → raw tags with address-field mapping + folder mirroring); `Calculation` driver (`IDependencyConsumer` + mux adapter, timer + change triggers, `DraftValidator` scriptId + cycle gates); `DriverTypeNames` rewire + retirement of the routed driver flow. **Address space stays dark** (values light up in Batch 4). Full solution builds 0 errors; `AdminUI.Tests` 638 passed / 3 skipped, `Runtime.Tests` 357 passed, driver + guard suites green. ## Reviewer findings fixed in-branch - **Wave B H1** — driver forms still serialized the endpoint into `DriverConfig`; stripped the endpoint keys (Modbus/S7/OpcUaClient) so `DeviceConfig` is the sole source (a 2nd device on a single-Host driver no longer reverts to `127.0.0.1`). - **Wave C HIGH** — `CalculationDriver.ReinitializeAsync` ignored the new config, so calc tag/script edits were silently inert after a redeploy and the re-register loop was decorative. Now rebuilds the tag/dependency table on reinit and re-registers deps after the delta applies (`DeltaApplied` notification, race-free). New redeploy test proves it. - Wave-A/B/0 mediums (auto-expand, friendly create-race/delete failures, discovery-driven guard, CSV comment). Deferred (documented): pre-existing Modbus/S7 form `TimeSpan`↔`*Ms` DTO mismatch; CSV alarm-subfield/flag-precedence edge cases; OpcUaClient endpoint precedence; unused `LoadMergedProbeConfigAsync`; refresh collapses sibling expansion. ## Live `/run` gate (docker-dev `:9200`, both central nodes rebuilt) 1. **Authoring + Test-connect** — `/raw` → New Modbus driver `gate-modbus` (form body renders, `PLC family`/`MELSEC` enum dropdowns correct) → Device1 endpoint `10.100.0.35:5020` in `DeviceConfig` → **Test connect OK · 94 ms** against the live Modbus fixture; SQL confirms endpoint in `DeviceConfig` (`{"Host":"10.100.0.35","Port":5020,"UnitId":1}`). 2. **Historian-tagname deploy block** — a historized tag with a 260-char effective tagname → `POST /api/deployments` **422** `[HistorianTagnameTooLong] tag 'TAG-gatelong' … is 260 chars (max 255)`; shortened override → **202 Accepted**. 3. **CSV round-trip + bad-enum** — export produces the exact column dictionary (`…,Region,Address,ModbusDataType,ByteOrder,…`); importing 1 valid + 1 bad-enum row → review grid "2 rows, 1 valid, 1 invalid", row-2 error *"Column 'ModbusDataType': 'NOTATYPE' is not a valid ModbusDataType (expected one of: Bool, Int16, UInt16, …)"*, **Commit disabled** (all-or-nothing; SQL confirms nothing committed); a valid-only import → Commit → `ImportedTag` lands with typed columns assembled into `TagConfig`. 4. **Browse-commit** — OpcUaClient device → opc-plc (`opc.tcp://10.100.0.35:50000`): two-tier gate enabled browse, merged config connected, real tree walked (NetworkSet/…/OpcPlc); multi-selected 2 leaves with "create matching tag-groups" → raw tags land under the mirrored `Basic` group with DataType + the browse ref in the `nodeId` address field (`nsu=…;s=AlternatingBoolean`), not an identity key. 5. **Calculation deploy + cycle gate** — a calc tag reading a Modbus RawPath deploys **202**; a 2-cycle (`calc1/Engine/A`↔`calc1/Engine/B`) → **422** `[CalculationDependencyCycle] … (members: calc1/Engine/A, calc1/Engine/B)`; clean redeploy 202, no crash loops (both central nodes stable; only a deploy-reject WRN in the log). 6. **Rename warning** — renaming `gate-modbus`→`gate-modbus-renamed` fired the warning modal *"1 historized tag beneath this node will change its RawPath (historian tagname)…"*. 7. **Retired routes** — `/clusters/{id}/drivers`, `…/new`, `…/new/{slug}`, `…/{id}` all return **404**; `/raw` + `/uns` still 200. ## Docs `docs/Raw.md` (new — the `/raw` authoring tree + endpoint split + CSV + Calculation + retirement note); `CLAUDE.md` AdminUI section updated; the Calculation mini-design remains the driver's authority.