6.8 KiB
v3 Batch 3 — UNS reference-only Equipment + {{equip}}/<RefName> reference-relative resolution
Implements docs/plans/2026-07-15-v3-batch3-uns-rework-plan.md (WP1–WP4), executed via the
/v3-batch 3 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 5-item live gate on
docker-dev.
What landed
- Contracts —
IEffectiveNameGuard(authoring-time effective-name uniqueness seam; WP2 implements, WP1 consumes). - Wave A (3 parallel agents):
- WP1 — equipment Tags tab →
UnsTagReferencelist (effective name / RawPath / inherited DataType+AccessLevel / display-name override); newAddReferenceModalreusingRawTreein opt-inPickerMode(multi-select tag leaves + device/tag-group "Select all tags below"), cluster-scoped (structural + server-enforcedtag.cluster == equipment.cluster);UnsTreeServicereference CRUD; consumesIEffectiveNameGuardin all colliding mutations;ImportEquipmentModal/EquipmentInputdroppedDriverInstanceId; deleted the retired equipmentTagModal. - WP2 —
EffectiveNameGuard(injectable, ordinal) + DI registration; verified the deploy-gateUnsEffectiveNameCollisionrule (already computes ref effective name as override-else-current-raw-name, so it catches rename-induced collisions; ordinal; names both sources + equipment). - WP3 —
RawTreeService.BuildRenameWarningsAsync: refined historized warning (only tags without ahistorianTagnameoverride) + UNS-referenced + new substring scan of everyScript.SourceCodefor affected tags' OLD RawPaths (recomputed pre-save viaRawPathResolver, ordinal).
- WP1 — equipment Tags tab →
- Wave B (1 integration agent):
- WP4 —
{{equip}}/<RefName>reference-relative resolution.EquipmentScriptPaths.DeriveEquipmentBasedeleted; slash joint replaces the dot joint; new sharedEquipmentReferenceMap(equipmentId → effectiveName → RawPath) built identically at both compose seams (AddressSpaceComposer+DeploymentArtifact, byte-parity). Unresolved-ref deploy error (EquipReferenceUnresolved) covering VT scripts, SA predicates, and SA message-template tokens; authoring guard (ValidateEquipTokenAsync, the Wave-A M1 fix); Monaco{{equip}}/completion of reference effective names + unresolved diagnostic (OTSCRIPT_EQUIPREF),EquipmentIdthreaded request→model→JS.
- WP4 —
Address space stays dark (values light up in Batch 4). Full solution builds 0 errors; Commons 309, Configuration 121, AdminUI 659, OpcUaServer 335/4-skip, Runtime 361/42-skip (the skips are the Batch-1 dark-address-space tests Batch 4 un-skips).
Reviewer findings fixed in-branch
- Wave A — no HIGH. Verified: the register-AND-consume guard seam (prod DI injects the real guard; the
NoOp fallback only wins under
newin tests; guard invoked in all 6 colliding mutations); cluster scoping structural + server-enforced; Razor defaultRawTreebyte-unchanged; WP3 pre-save RawPath recomputation. L1 (misleading concurrency comment) fixed. - Wave B — no HIGH; byte-parity + production wiring confirmed. M1 fixed: the editor⇔authoring⇔deploy
invariant now holds on the ScriptedAlarm surface too (SA predicate + message-template
{{equip}}refs validated at authoring, matching the deploy gate). L3 fixed: parity test hardened (unresolved-ref-intact- folder/tag-group RawPath ancestry). L1 already closed at the write path (empty override normalized→null).
- Deferred (documented follow-ups): absolute-path Monaco completion still projects the raw leaf name, not
the RawPath (out of
{{equip}}scope;{{equip}}/completion works) — reworkScriptTagCatalogto RawPaths in Batch 4; a broken raw-topology-chain reference is deploy-accepted but compose-dropped (low reachability); message-template{{equip}}/Xis gate-validated but rendered/substituted only in Batch 4.
Live /run gate (docker-dev :9200, both central nodes rebuilt on Batch-3 code)
Substrate: an Area→Line→Equipment seeded in MAIN, plus a SITE-A raw tag so cross-cluster exclusion is
demonstrable. (Hand-seeded equipment surfaced the Batch-1 EquipmentIdNotDerived invariant — the canonical
EQ-<hash> id was applied before the gate.)
- Reference picker + list — "+ Add reference" header "The tree is scoped to the equipment's cluster";
only the Main cluster root shows (SITE-A/B absent — the seeded
SiteAOnlytag unreachable); lazy expansion; a device "Select all tags below" pulled tags across a collapsed group → 4 references in one commit. Rows show effective name / RawPath (incl. deepopcua1/plc/OpcPlc/Telemetry/Basic/…ancestry) / DataType / Access / override. Set override "MainPressure" → effective name updatedHR200 → MainPressure, RawPath unchanged. - Collision — authoring: setting a reference override to an existing VirtualTag's name → red banner
"effective name 'GateVt' already used by VirtualTag 'VT-gatevt' in equipment '…'" (not persisted).
rename-induced: renamed a raw tag so two references collide (allowed at rename) → deploy 422
[UnsEffectiveNameCollision] 2 UNS signals collide on effective name 'ImportedTag' … reference '…', reference '…'. {{equip}}(editor ⇔ deploy) — resolving{{equip}}/MainPressuredeploys 202; misspelled{{equip}}/MainPresuredeploys 422[EquipReferenceUnresolved] … has no reference named 'MainPresure'. Monaco: red squiggle + hover "'{{equip}}/MainPresure' does not resolve — … no reference named 'MainPresure' … (OTSCRIPT_EQUIPREF)"; typing{{equip}}/completes the four reference effective names (AlternatingBoolean, ImportedTag, MainPressure, RandomSignedInt32).- Rename warning — renaming a driver whose beneath-it tag is historized-without-override + UNS-referenced
- named in a script literal → "Renamed — with warnings" listing all three (historized fork,
UNS-referenced by 'gateequip', "2 scripts ('cval','gatevt') reference tags … by their raw paths"); renaming
an unrelated driver → no warning dialog. (Note: direct tag rename flows through
UpdateTagAsyncwhich has no warnings surface — warnings fire on container renames affecting descendants; documented follow-up if per-tag-rename warnings are wanted.)
- named in a script literal → "Renamed — with warnings" listing all three (historized fork,
UNS-referenced by 'gateequip', "2 scripts ('cval','gatevt') reference tags … by their raw paths"); renaming
an unrelated driver → no warning dialog. (Note: direct tag rename flows through
- Import without driver column — the Import equipment CSV modal columns are
Name, MachineCode, UnsLineId(+ optionalZTag, SAPID, Manufacturer, Model); noDriverInstanceId.
Docs
docs/Uns.md Tags section rewritten to the reference-only model; docs/ScriptEditor.md updated (WP4) to the
slash/reference {{equip}} semantics; CLAUDE.md gains a v3 Batch 3 paragraph.