# 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.
