# 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}}/<RefName>` 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}}/<RefName>` → 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}}/<RefName>`) 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 `<RefName>` = 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}}/<RefName>")` —
   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.
