# v3 Batch 3 — UNS reference-only Equipment + `{{equip}}` reference-relative resolution **Branch:** `v3/batch3-uns-rework` (from master after Batch 2 merges) **Design authority:** v3 design §"UNS Equipment — reference-only", §"Scripting — paths and `{{equip}}` under v3", §"Error handling / edge cases" (effective-name collision, rename-warning scan). **End state:** Equipment holds `UnsTagReference` rows instead of authored tags; scripts resolve `{{equip}}/` through references; every collision/unresolved case is a clear deploy error. Address space still dark for values (Batch 4), so verification is authoring + deploy-gate level. ## Work packages ### Wave A (3 agents, parallel) **B3-WP1 — Equipment Tags tab → reference list + raw-tag picker** - `Components/Pages/Uns/EquipmentPage.razor` Tags tab: replace the Batch-1-stubbed authored-tag flow with a `UnsTagReference` list — columns: effective name, raw path, inherited datatype/access (read-only, from the raw tag), display-name override, remove. `TagModal.razor`'s driver-binding path is deleted (VirtualTag/ScriptedAlarm modals untouched). - **"+ Add reference"** opens a raw-tree picker modal reusing Batch 2's `RawTree` component in picker mode (multi-select checkboxes on Tag leaves; Device/TagGroup select-all), **scoped to the equipment's cluster** (cross-cluster structurally impossible — the service query is cluster-filtered, not just the UI). - `ImportEquipmentModal.razor` + `Uns/EquipmentInput.cs` + `UnsTreeService.ImportEquipmentAsync`: drop the `DriverInstanceId` column (equipment no longer carries a driver). - Service mutations in `UnsTreeService` (+ `IUnsTreeService`): add/remove references, set override — each enforcing WP2's uniqueness check and returning readable errors. **B3-WP2 — effective-name uniqueness (authoring + deploy gate)** - Rule: within an equipment, effective name (`DisplayNameOverride` else raw `Name`) is unique across **references, VirtualTags, and ScriptedAlarms**. - Authoring-time: service-level check in every mutation that can collide (add reference, set override, add/rename VirtualTag or ScriptedAlarm). - Deploy-time: `DraftValidator` gains the same rule over the draft snapshot — this is what catches **rename-induced** collisions (a raw rename the authoring check never saw); the error names both colliding sources and the equipment. - `DraftSnapshotFactory` extended to load `UnsTagReference` + raw-tag names into the snapshot. - Unit tests: authoring rejection, deploy rejection post-rename, override-vs-VT clash, case-sensitivity pinned (ordinal, matching NodeId semantics). **B3-WP3 — rename-warning script scan** - Extend Batch 2's rename-warning seam in `RawTreeService`: on rename of a tag or any ancestor, compute affected RawPaths (prefix scan) and warn when a tag is (a) historized without a `historianTagname` override (history forks), (b) UNS-referenced (names the equipment), (c) matched by a **substring scan of `Script` bodies for the old RawPath** (service-level, tolerates false positives — decided in the design; no new bookkeeping). `{{equip}}`-relative refs need no scan (they resolve through references; the deploy gate catches breakage). - UI: warning list in the rename confirm dialog; rename proceeds on confirm (Kepware-equivalent, deliberate). ### Wave B (1 agent — single integration point) **B3-WP4 — `{{equip}}` reference-relative resolution** - `src/Core/ZB.MOM.WW.OtOpcUa.Commons/Types/EquipmentScriptPaths.cs`: **delete `DeriveEquipmentBase`** (shared first-dot prefix of child FullNames — cannot survive reference-only equipment). `SubstituteEquipmentToken` is replaced by **per-reference resolution**: `{{equip}}/` → look up the equipment's `UnsTagReference` rows by effective name → substitute the backing RawPath. Token syntax note: today's derivation used a **dot** joint (`{{equip}}.X`); v3's is slash-shaped (`{{equip}}/`) per the design — update `ExtractDependencyRefs` / `ExtractAlarmDependencyRefs` and the `EquipToken` handling accordingly, and pin the new syntax in tests. - The **same two compose seams** substitute as today: `OpcUaServer/AddressSpaceComposer.cs` (~line 210 branch) and `Runtime/Drivers/DeploymentArtifact.cs` (`SubstituteEquipmentToken` call, line ~563) — both now receive the equipment's reference map instead of a derived base prefix. - **Unresolved `` = deploy-time validation error** (in `DraftValidator`, where the reference map + script sources are both in the snapshot), naming the script, the equipment, and the missing ref name — replacing today's silent null-base no-substitution. Alarm `{TagPath}` message tokens follow the same rule. - Monaco/editor parity: `AdminUI/ScriptAnalysis/ScriptAnalysisService.cs` — the `{{equip}}` completion branch (lines ~176–182) now completes **reference effective names** for the equipment context; the diagnostics branch (~280–284) flags unresolved refs identically to the deploy gate (editor accepts ⇔ publish accepts — the established invariant). `IScriptTagCatalog` gains the per-equipment reference list; absolute-path completions list RawPaths (already moved in Batch 1's catalog rework — verify, don't assume). - Tests: substitution unit tests (resolved, unresolved→error, override-named ref, alarm-token), composer/artifact parity tests, ScriptAnalysis completion/diagnostic tests. ## Wave schedule | Wave | Packages | Agents | Parallel-safe because | |---|---|---|---| | A | WP1 ∥ WP2 ∥ WP3 | 3 | WP1 owns Razor + service mutation methods; WP2 owns validator/snapshot + service *check* helpers (shared file `UnsTreeService.cs` — WP1 owns it, WP2 delivers its check as a separate injectable `EffectiveNameGuard` consumed by WP1); WP3 owns `RawTreeService` warning code | | B | WP4 | 1 | Touches Commons + composer + artifact + validator + ScriptAnalysis — single integration agent, rebased on Wave A | ## Batch verification gate (live `/run` on docker-dev) Rebuild **both** centrals; fixtures up; Batch 2's raw tags present (re-author or re-seed). 1. Equipment page: "+ Add reference" picker is cluster-scoped (raw tags of another cluster absent), multi-select pulls 3+ tags at once; rows show raw path + inherited type/access; set a `DisplayNameOverride` → effective name updates. 2. Collision: add a reference whose effective name equals an existing VirtualTag → **authoring rejected** with both sources named. Then force a rename-induced clash (rename the raw tag in `/raw` so two references collide) → authoring allowed at rename, **deploy blocked** by `DraftValidator` naming both. 3. `{{equip}}`: author a VirtualTag script using `ctx.GetTag("{{equip}}/")` — Monaco completes the reference names and shows no diagnostic; deploy succeeds. Misspell the ref → Monaco diagnostic AND deploy error agree (same message family). 4. Rename warning: rename a raw tag that is historized + referenced + named in a script literal → confirm dialog lists all three warnings; renaming an unrelated tag warns nothing. 5. ImportEquipmentModal: import runs without the driver column. Deploys via `POST /api/deployments` (`X-Api-Key`). Values still dark — assert nothing about live data. Record evidence in the PR.