# The Raw project tree (`/raw`) v3 authors device I/O in a **Raw project tree** — a cluster-rooted, Kepware-style hierarchy that is the single surface for driver/device/tag authoring. It is the peer of the `/uns` unified-namespace page; the two OPC UA namespaces (`ns=Raw` / `ns=UNS`) light up in Batch 4. ``` Enterprise → Cluster → Folder(s) → Driver → Device → TagGroup(s) → Tag ``` Every node's identity is its **RawPath** (`////`, cluster-scoped, `/`-separated, ordinal). Names are validated as RawPath segments (no `/`, no leading/trailing whitespace) at authoring and at the deploy gate. ## The tree `/raw` (`GlobalRaw.razor` → `RawTree.razor`, backed by `IRawTreeService`) lazily loads each container level on expand (unlike `/uns`, which eager-loads). Each node has a right-click context menu **and** a `⋯` fallback (the reusable `ContextMenu` component): | Node | Actions | |---|---| | Cluster | New folder · New driver | | Folder | New folder · New driver · Rename · Delete | | Driver | Configure · New device · Enable/Disable · Rename · Delete | | Device | Configure (endpoint + **Test connect**) · New tag-group · Add tags ▸ · Delete | | TagGroup | New group · Add tags ▸ · Rename · Delete | | Tag | Edit · Delete | Deletes are blocked while a node has children (or, for a raw tag, while a `UnsTagReference` points at it — the error names the referencing equipment). Renames return non-blocking **warnings**: renaming a node changes the RawPath of every historized / UNS-referenced tag beneath it (its historian tagname / UNS projection path moves). **A rename rebinds live data on the deploy — no node restart is needed** (Gitea #489, live-gated 2026-07-31 for both a tag rename and a device rename). The mechanism is #516's respawn: a rename changes the merged `DriverConfig`, and `DriverSpawnPlan.Compute` stops and respawns the driver child on any `DriverConfig` change, so the driver rebuilds against the new RawPaths and re-subscribes. ⚠️ The flip side is that **a rename reconnects the driver** — briefly dropping every tag on it, not just the renamed one. That is the accepted cost #516 documents; it is not specific to renames. ## Endpoint → DeviceConfig (the channel/device split) v3 moves the **connection endpoint** off the driver and onto the **Device** (`DeviceConfig`), mirroring Kepware's channel/device split. The driver's `DriverConfig` holds protocol/channel settings; the device's `DeviceConfig` holds host/port/endpoint. At deploy/probe time `DriverDeviceConfigMerger` merges the device's endpoint up into the driver config, and Test connect (inside the **Device** modal) probes that merged config. Driver forms no longer serialize the endpoint keys, so `DeviceConfig` is the single source of truth. **Creating a driver auto-creates an empty `Device1` (by design, #596).** Every driver needs at least one device to hang browse and tags off, and for a single-device driver the merger treats that device's `DeviceConfig` as the endpoint, so an empty, renamable, deletable `Device1` is the lowest-friction start (`RawTreeService.CreateDriverAsync`; pinned by `RawTreeServiceWaveBTests`). It is not a placeholder to be removed later. ## Authoring tags **Add tags ▸** on a Device or TagGroup offers: - **Manual entry** — a grid of Name / DataType / AccessLevel / WriteIdempotent / PollGroup + the driver-typed `TagConfig` editor per row. - **Import/Export CSV** — a staged flow (upload → parse → **review grid** with per-row verdicts → commit) over an RFC-4180 parser. Columns: the common set (`Name, TagGroupPath, DataType, AccessLevel, WriteIdempotent, PollGroup, IsHistorized, HistorianTagname, IsArray, ArrayLength, Alarm.*, TagConfigJson`) plus the per-driver typed columns (e.g. Modbus `Region, Address, ModbusDataType, ByteOrder, …`). `TagGroupPath` auto-creates nested groups; typed columns win over `TagConfigJson` on conflict (flagged in the review grid). **Import is all-or-nothing** — any invalid row blocks the whole commit. Export round-trips the same shape. - **Browse device…** — enabled per the two-tier resolution (bespoke `IDriverBrowser` → universal `DiscoveryDriverBrowser` when the driver's `ITagDiscovery.SupportsOnlineDiscovery` is true → grayed out otherwise). Multi-selected leaves become raw `Tag` rows under the target, with the browsed reference written into the driver-typed `TagConfig` **address field** (`nodeId` / `tagPath` / …), never an identity key. An opt-in "create matching tag-groups" toggle mirrors the browse folder nesting. The browser dials a real ephemeral driver against the **merged** Driver+Device config. ## What a deploy tears down (surgical deploys, 2026-08-15) Editing a raw **tag's** attributes is no longer a fleet-wide event. A **non-alarm** raw-tag delta whose only differences are the in-place-updatable attributes — `Writable`, `IsHistorized`, `HistorianTagname`, `DataType`, `IsArray`, `ArrayLength`, `ArrayWritable` (the driver's array-write capability, #610 — see [Uns.md §Array writes](Uns.md#array-writes-610)) — and/or its set of referencing equipment is applied **in place** on the existing Raw-realm node (`ISurgicalAddressSpaceSink.UpdateTagAttributes`), so every client subscription on that tag and on the rest of the server survives. The same is true of **every** UNS tag-reference change: adding, removing or re-pointing a reference used to flip the backing raw tag to Changed (the referencing set is part of its record equality) and rebuild the whole address space for a one-variable edit. **Still a full rebuild**, deliberately: an **alarm-bearing** raw tag (a delta with `Alarm != null` on either side — only a rebuild clears and re-wires the native-alarm notifiers that fan the condition to its referencing equipment folders); an **identity** change on a tag (`TagId`, `DriverInstanceId`, `Name`, `ParentNodeId`, realm); and any change to a raw **container** — a Folder / Driver / Device / TagGroup **rename or removal** — because the surgical sink has no folder-remove. The classifier is default-closed: any field it does not explicitly whitelist, including fields added later, still routes to a rebuild, and any surgical call returning `false` or throwing ratchets back to one. **Mirror repair.** A raw-side historian or shape edit also reaches every UNS reference mirroring the tag: the reference record carries the backing tag's historian/shape, so the edit emits a coincident UNS-reference delta. Identity-preserving repairs update the mirror **in place** (its MonitoredItems survive); a re-point or effective-name change removes and recreates **that one mirror node**. See [`Uns.md`](Uns.md) and [`plans/2026-08-15-surgical-deploys.md`](plans/2026-08-15-surgical-deploys.md). A surgically added raw container/tag is **announced** (Gitea #599): `AnnounceAddedNodes` raises a `GeneralModelChangeEvent(NodeAdded)` in the **Raw** realm for each distinct existing parent of the added nodes (a parent that is itself new in the same deploy is skipped — its own parent is announced, and a re-browse there finds the whole new subtree; a new top-level folder announces itself). UNS-side adds announce in the Uns realm as before. Until #599 raw adds were materialised but never announced, so model-aware clients only saw them on their next browse — invisible while every add still forced a rebuild. Removing an equipment that holds UNS references is also surgical: the references are removed (terminal Bad, then `RemoveVariableNode`, which severs the backing raw node's inverse `Organizes` edge) **before** the equipment subtree, and the subtree removal severs the same edge for anything it reaches itself (archreview 03/P5 + 03/S20). ## The `Calculation` driver Signal-level calculated tags are ordinary raw tags bound to a `Calculation` driver (`DriverTypeNames.Calculation`), computed by C# scripts that read other tags' live values via `ctx.GetTag("")`. See `docs/plans/2026-07-15-calculation-driver-mini-design.md` for the full design. Highlights: - ⚠️ **Correction (2026-07-28): the "auto-created default `Engine` device" below never shipped.** `"Engine"` has zero occurrences in `src/`, and nothing special-cases `Calculation` at device creation — author the device like any other. Per-tag `TagConfig` is `{ "scriptId": "…", "changeTriggered": true, "timerIntervalMs": 5000 }`. - The host feeds dependency values via the new `IDependencyConsumer` capability + a `DependencyConsumerMuxAdapter` on the per-node dependency mux; calc-of-calc chains work because a calc output re-enters the mux. The driver rebuilds its tag/dependency table on every redeploy (`ReinitializeAsync`) and re-registers its refs after the delta applies. - **Deploy gates** (`DraftValidator`): `scriptId` existence, and a hard **cycle gate** (`DependencyGraph.DetectCycles` over calc→calc edges; cross-driver refs are terminal) that rejects an A→B→A oscillation loop naming the members. Compile is not hard-gated (VirtualTag parity); a runtime compile/throw/timeout lands as Bad quality + a `script-logs` entry. ## Quality: a raw tag's node when its driver stops communicating A raw tag's node — and every UNS mirror of it — goes **`BadNoCommunication` with its last value retained** when the owning driver is declared comms-lost, and clears when the driver re-publishes. It is never cleared by the server on reconnect: recovery is a value, never an assumption. The rule and the seams that implement it are stated once in [`docs/DriverLifecycle.md` §"The quality contract"](DriverLifecycle.md#the-quality-contract). Calc tags authored here inherit the same rule from their inputs — see [`docs/drivers/Calculation.md` §"Input quality"](drivers/Calculation.md#input-quality). ## Retirement note The routed `/clusters/{id}/drivers` driver-authoring flow (`DriverTypePicker`, `DriverEditRouter`, the 8 per-type `*DriverPage` shells, and the per-cluster Drivers list) is **retired** — authoring lives in `/raw`. The extracted driver/device **form bodies** live on inside the `/raw` modals. The `DriverType` dispatch maps (`TagConfigEditorMap`, `TagConfigValidator`, `EquipmentTagConfigInspector`) are keyed off the single-source-of-truth `DriverTypeNames` constants (fixing the historical `TwinCat`/`Focas` drift).