# Format Decisions Why the schemas repo looks the way it does. Each decision is open for the schemas-repo owner team to revisit. ## D1 — JSON Schema (Draft 2020-12) as the authoring format **Alternatives considered**: Protobuf (`.proto`), YAML, custom DSL. **Choice**: JSON Schema. **Why**: - Idiomatic for .NET 10 (System.Text.Json + JsonSchema.Net) — OtOpcUa reads templates with no extra dependencies - Idiomatic for CI tooling — every CI runner can `jq` and validate JSON Schema without extra toolchain (`ajv`, `jsonschema`, etc.) - Best authoring experience: text format, mergeable in git, structured diffing, IDE autocomplete via the `$schema` reference - Validation at multiple layers: operator-visible Admin UI errors in OtOpcUa, schemas-repo CI gates, downstream consumer runtime validation - Protobuf is better for *wire* serialization (size, speed, generated code) but worse for *authoring* (binary, requires `.proto` compiler, poor merge story in git) - Where wire-format efficiency matters (Redpanda events), we code-generate Protobuf from the JSON Schema source. One-way derivation is simpler than bidirectional sync. ## D2 — Per-class versioning, semver **Alternatives considered**: whole-repo versioning, no versioning. **Choice**: each class file has its own `version` field (semver); the repo also tags overall releases. **Why**: - Different classes evolve at different rates (FANUC CNC may stabilize while Modbus PLC catalog grows) - Consumers can pin per-class for fine-grained compatibility (e.g. `fanuc-cnc@0.1.0` + `modbus-plc@0.3.2`) - Repo-level tags exist to bundle a known-good combination for consumers that want one anchor ## D3 — Strict additive policy on minor bumps **Why**: removes ambiguity. If I see `class@1.3.0`, I know its signal set is a strict superset of `class@1.0.0` (and `class@1.x.y` for any earlier x.y). Breaking changes only happen at major-version boundaries. ## D4 — `_default` reserved as placeholder for unused UNS levels Imported from `lmxopcua/docs/v2/plan.md` decision #108. **Why**: some sites have no Area-level distinction (single-building sites). Rather than letting the UNS path have inconsistent depth across sites, we mandate 5 levels always with `_default` as the placeholder. Downstream consumers can rely on path depth. ## D5 — Tag names use PascalCase or snake_case (class's choice), NOT UNS-segment regex **Why**: UNS path segments (Enterprise/Site/Area/Line/Equipment) are infrastructure-level identifiers — they go on the wire of every browse, every URI, every dashboard filter. The regex (`^[a-z0-9-]{1,32}$`) reflects that constraint. Signal names (level 6) are vocabulary-level identifiers — they live inside an equipment node. Keeping them in PascalCase or snake_case (e.g. `RunState`, `actual_feedrate`) is more readable for operators looking at OPC UA browse output, and matches OPC UA SDK conventions which expect identifier-style names rather than URL-safe slugs. ## D6 — `stateModel` is informational, not authoritative **Why**: state derivation lives at Layer 3 (System Platform / Ignition). Placing the derivation rules in the schemas repo would create dual sources of truth (and the schemas-repo version would inevitably drift). Instead, the class template lists which states the class supports + an informational note about what the rough mapping looks like; Layer 3 owns the actual derivation logic. ## D7 — No per-equipment overrides at this layer **Why**: per-equipment config (which specific CNC has which program, etc.) is OtOpcUa's central config DB concern. Mixing per-instance config with per-class definitions in this repo would muddy the separation and cause the repo to grow with deployment-specific data instead of staying small + reusable. ## D8 — `applicability.drivers` lists OtOpcUa drivers explicitly **Why**: the schemas repo is OT-side-focused. The OtOpcUa driver enumeration is the closest thing to a canonical "how do you get raw data from this equipment" vocabulary that exists across the org. If a future class is populated by a non-OtOpcUa source, the field becomes optional or extends. For now, listing OtOpcUa driver IDs makes the consumer-side validation (per `lmxopcua/docs/v2/plan.md` decision #111 — driver type ↔ namespace kind) trivial. ## D9 — `_base` class with `extends` inheritance for cross-machine metadata **Why**: every machine in the estate, regardless of vendor or protocol, exposes a common metadata core — identity (Manufacturer, Model, SerialNumber, plus the OtOpcUa five-identifier set EquipmentUuid/EquipmentId/MachineCode/ZTag/SAPID), connection state, alarm summary, optional production context (work order, recipe, operator). Repeating these in every class template would invite drift: one class would forget HighestActiveAlarmSeverity, another would name SerialNumber differently, etc. Solution: a single `_base` class (in `classes/_base.json`) declares the common set; every other class `extends: "_base"` and inherits the signals, alarms, and stateModel. The child class adds its specifics (axes for a CNC, registers for a Modbus device, etc.) and can override individual base entries by `name` if needed. References for the `_base` content: - **OPC UA Companion Spec OPC 40010 (Machinery)** — for the Identification component (Manufacturer, Model, ProductInstanceUri, SerialNumber, HardwareRevision, SoftwareRevision, YearOfConstruction, ManufacturerUri, DeviceManual, AssetLocation) and MachineryOperationMode enum (Auto, Manual, Maintenance, Service, Setup, Other) - **OPC UA Part 9 (Alarms & Conditions)** — for the alarm summary fields (HasActiveAlarms, ActiveAlarmCount, HighestActiveAlarmSeverity) - **3-year-plan handoff §"Canonical Model Integration"** — for the canonical state vocabulary (Running / Idle / Faulted / Starved / Blocked) declared in `_base.stateModel` - **ISO 22400 (Manufacturing KPIs)** — for the lifetime counter fields (TotalRunSeconds, TotalCycles) that feed Availability + Performance KPIs at Layer 3 Convention: `_` prefix on the classId marks an abstract base class — clients should not assign `_base` directly to any equipment via `Equipment.EquipmentClassRef` (it has no specifics; it's only meant to be extended). ## D10 — Signal `category` drives OPC UA exposure pattern, not just dashboard filtering **Why**: aligning with OPC 40010 Machinery's Identification + Status + (machine-specific) folder pattern. The `category` field on each signal (`Identity`, `Status`, `Process`, `Position`, `Velocity`, `Counter`, `Alarm`, `Diagnostic`, etc.) tells the OtOpcUa NodeManager which sub-folder to place the signal in under the equipment node: | Category | OPC UA folder | Value source | |----------|---------------|--------------| | `Identity` | `Identification` | Static, from `Equipment` row in central config DB (operator-set) — except where the driver can read it dynamically (e.g. FANUC `SeriesNumber` from `cnc_sysinfo()`) | | `Status` | `Status` | Dynamic, from driver | | `Diagnostic` | `Diagnostic` | Dynamic, from driver or from OtOpcUa runtime | | `Alarm` | `Alarms` (synthetic — full OPC UA Part 9 alarm subscription is separate) | Dynamic | | `Process`, `Position`, `Velocity`, `Acceleration`, `Temperature`, `Pressure`, `Flow`, `Counter`, `Setpoint`, `Command` | (machine-specific folders by category) | Dynamic, from driver | | `Other` | `Misc` | Dynamic | This means the same template field describes both consumer behavior (filter / categorize) and OtOpcUa exposure (which folder, which value-source pattern). Avoids a separate "where does this go in OPC UA" annotation.