Closes the server-side / non-UI piece of Phase 6.4 Stream D. The Razor `IdentificationFields.razor` component for Admin-UI editing ships separately when the Admin UI pass lands (still tracked under #157 UI follow-up). Core.OpcUa additions: - IdentificationFolderBuilder — pure-function builder that materializes the OPC 40010 Machinery companion-spec Identification sub-folder per decision #139. Reads the nine nullable columns off an Equipment row: Manufacturer, Model, SerialNumber, HardwareRevision, SoftwareRevision, YearOfConstruction (short → OPC UA Int32), AssetLocation, ManufacturerUri, DeviceManualUri. Emits one AddProperty call per non-null field; skips the sub-folder entirely when all nine are null so browse trees don't carry pointless empty folders. - HasAnyFields(equipment) — cheap short-circuit so callers can decide whether to invoke Folder() at all. - FolderName constant ("Identification") + FieldNames list exposed so downstream tools / tests can cross-reference without duplicating the decision-#139 field set. ACL binding: the sub-folder + variables live under the Equipment node so Phase 6.2's PermissionTrie treats them as part of the Equipment ScopeId — no new scope level. A user with Equipment-level grant reads the Identification fields; a user without gets BadUserAccessDenied on both the Equipment node + its Identification variables. Documented in the class remarks; cross-reference update to acl-design.md is a follow-up. Tests (9 new IdentificationFolderBuilderTests): - HasAnyFields all-null false / any-non-null true. - Build all-null returns null + doesn't emit Folder. - Build fully-populated emits all 9 fields in decision #139 order. - Only non-null fields are emitted (3-of-9 case). - YearOfConstruction short widens to DriverDataType.Int32 with int value. - String values round-trip through AddProperty. - FieldNames constant matches decision #139 exactly. - FolderName is "Identification". Full solution dotnet test: 1202 passing (was 1193, +9). Pre-existing Client.CLI Subscribe flake unchanged. Production integration: the component that consumes this is the address-space-build flow that walks the live Equipment table + calls IdentificationFolderBuilder.Build(equipmentFolder, equipment) under each Equipment node. That integration is the remaining Stream D follow-up alongside the Razor UI component. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4.8 KiB
4.8 KiB