///|
/// Object Contract package for Isochronon W-1.

///|
pub fn package_id() -> String {
  "isocontract"
}

///|
pub(all) enum ObjectKind {
  Signal
  Command
  Query
  Event
  Lifecycle
} derive(Eq, Debug)

///|
pub fn ObjectKind::label(self : ObjectKind) -> String {
  match self {
    Signal => "Signal"
    Command => "Command"
    Query => "Query"
    Event => "Event"
    Lifecycle => "Lifecycle"
  }
}

///|
pub(all) enum Schema {
  Bool
  I8
  I32
  U16
  State
  Text
} derive(Eq, Debug)

///|
pub fn Schema::label(self : Schema) -> String {
  match self {
    Bool => "bool"
    I8 => "i8"
    I32 => "i32"
    U16 => "u16"
    State => "state"
    Text => "text"
  }
}

///|
pub(all) enum Authority {
  Any
  ReadOnly
  Role(String)
} derive(Eq, Debug)

///|
pub fn Authority::label(self : Authority) -> String {
  match self {
    Any => "any"
    ReadOnly => "read_only"
    Role(name) => name
  }
}

///|
pub fn Authority::allows(self : Authority, node_id : String) -> Bool {
  match self {
    Any => true
    ReadOnly => false
    Role(name) => node_id == name
  }
}

///|
pub(all) enum ObjectClockRequirement {
  AnyClock
  PhysicalClock
} derive(Eq, Debug)

///|
pub fn ObjectClockRequirement::label(self : ObjectClockRequirement) -> String {
  match self {
    AnyClock => "any"
    PhysicalClock => "physical"
  }
}

///|
pub(all) struct TimingContract {
  period_ns : Int64?
  max_age_ns : Int64?
  deadline_ns : Int64?
  valid_for_ns : Int64?
  clock_requirement : ObjectClockRequirement
} derive(Eq, Debug)

///|
pub fn TimingContract::make(
  period_ns? : Int64,
  max_age_ns? : Int64,
  deadline_ns? : Int64,
  valid_for_ns? : Int64,
  clock_requirement? : ObjectClockRequirement = AnyClock,
) -> TimingContract {
  { period_ns, max_age_ns, deadline_ns, valid_for_ns, clock_requirement }
}

///|
pub fn TimingContract::empty() -> TimingContract {
  TimingContract::make()
}

///|
pub(all) struct SafetyContract {
  authority : Authority
  min_payload_digest : Int?
  max_payload_digest : Int?
  timeout_action : String
} derive(Eq, Debug)

///|
pub fn SafetyContract::make(
  authority? : Authority = Any,
  min_payload_digest? : Int,
  max_payload_digest? : Int,
  timeout_action? : String = "hold_last",
) -> SafetyContract {
  { authority, min_payload_digest, max_payload_digest, timeout_action }
}

///|
pub(all) enum BindingKind {
  TraceLabel
  EtherCatPdo
  CoeSdo
  CanopenSdo
  CanopenPdo
  ZenohKey
  ModbusRegister
} derive(Eq, Debug)

///|
pub fn BindingKind::label(self : BindingKind) -> String {
  match self {
    TraceLabel => "trace_label"
    EtherCatPdo => "ethercat_pdo"
    CoeSdo => "coe_sdo"
    CanopenSdo => "canopen_sdo"
    CanopenPdo => "canopen_pdo"
    ZenohKey => "zenoh_key"
    ModbusRegister => "modbus_register"
  }
}

///|
pub(all) struct ObjectBinding {
  object_id : String
  binding_id : String
  medium_id : String
  label : String
  direction : @trace.TraceDirection?
  kind : BindingKind
  index : UInt?
  subindex : Byte?
  channel : String
  key : String
  authority : Authority
} derive(Eq, Debug)

///|
pub fn ObjectBinding::make(
  object_id~ : String,
  binding_id~ : String,
  medium_id? : String = "",
  label~ : String,
  direction? : @trace.TraceDirection,
  kind? : BindingKind = TraceLabel,
  index? : UInt,
  subindex? : Byte,
  channel? : String = "",
  key? : String = "",
  authority? : Authority = Any,
) -> ObjectBinding {
  {
    object_id,
    binding_id,
    medium_id,
    label,
    direction,
    kind,
    index,
    subindex,
    channel,
    key,
    authority,
  }
}

///|
pub fn ObjectBinding::matches(
  self : ObjectBinding,
  event : @trace.TraceEvent,
) -> Bool {
  let medium_ok = self.medium_id == "" || self.medium_id == event.medium_id
  let direction_ok = match self.direction {
    Some(direction) => direction == event.direction
    None => true
  }
  self.label == event.label && medium_ok && direction_ok
}

///|
pub(all) struct ObjectContract {
  id : String
  kind : ObjectKind
  schema : Schema
  timing : TimingContract
  safety : SafetyContract
  bindings : Array[ObjectBinding]
} derive(Eq, Debug)

///|
pub fn ObjectContract::make(
  id~ : String,
  kind~ : ObjectKind,
  schema~ : Schema,
  timing~ : TimingContract,
  safety~ : SafetyContract,
) -> ObjectContract {
  { id, kind, schema, timing, safety, bindings: [] }
}

///|
pub fn ObjectContract::with_binding(
  self : ObjectContract,
  binding : ObjectBinding,
) -> ObjectContract {
  let bindings : Array[ObjectBinding] = []
  for item in self.bindings {
    bindings.push(item)
  }
  bindings.push(binding)
  {
    id: self.id,
    kind: self.kind,
    schema: self.schema,
    timing: self.timing,
    safety: self.safety,
    bindings,
  }
}

///|
pub(all) struct ContractRegistry {
  objects : Array[ObjectContract]
} derive(Debug)

///|
pub fn ContractRegistry::new() -> ContractRegistry {
  { objects: [] }
}

///|
pub fn ContractRegistry::with_object(
  self : ContractRegistry,
  contract : ObjectContract,
) -> ContractRegistry {
  let objects : Array[ObjectContract] = []
  for item in self.objects {
    objects.push(item)
  }
  objects.push(contract)
  { objects, }
}

///|
pub fn ContractRegistry::copy(self : ContractRegistry) -> ContractRegistry {
  let objects = self.objects.map(object => {
    ..object,
    bindings: object.bindings.copy(),
  })
  { objects, }
}

///|
pub fn ContractRegistry::register(
  self : ContractRegistry,
  contract : ObjectContract,
) -> Unit {
  self.objects.push(contract)
}

///|
pub fn ContractRegistry::contains_object(
  self : ContractRegistry,
  object_id : String,
) -> Bool {
  let mut found = false
  for object in self.objects {
    if object.id == object_id {
      found = true
    }
  }
  found
}

///|
pub(all) struct ContractLintIssue {
  object_id : String?
  binding_id : String?
  message : String
} derive(Eq, Debug)

///|
pub(all) struct ContractLintReport {
  issues : Array[ContractLintIssue]
} derive(Eq, Debug)

///|
pub fn ContractLintReport::passes(self : ContractLintReport) -> Bool {
  self.issues.length() == 0
}

///|
pub fn ContractRegistry::lint(self : ContractRegistry) -> ContractLintReport {
  let issues : Array[ContractLintIssue] = []
  for i in 0.. Unit {
  push_negative_timing(issues, object.id, "period_ns", object.timing.period_ns)
  push_negative_timing(
    issues,
    object.id,
    "max_age_ns",
    object.timing.max_age_ns,
  )
  push_negative_timing(
    issues,
    object.id,
    "deadline_ns",
    object.timing.deadline_ns,
  )
  push_negative_timing(
    issues,
    object.id,
    "valid_for_ns",
    object.timing.valid_for_ns,
  )
  match object.kind {
    Command =>
      if object.timing.valid_for_ns is None && object.timing.deadline_ns is None {
        issues.push({
          object_id: Some(object.id),
          binding_id: None,
          message: "command object requires valid_for_ns or deadline_ns",
        })
      }
    Signal =>
      if object.timing.max_age_ns is None {
        issues.push({
          object_id: Some(object.id),
          binding_id: None,
          message: "signal object requires max_age_ns",
        })
      }
    Event =>
      if object.timing.max_age_ns is None && object.timing.deadline_ns is None {
        issues.push({
          object_id: Some(object.id),
          binding_id: None,
          message: "event object requires max_age_ns or deadline_ns",
        })
      }
    _ => ()
  }
}

///|
fn push_negative_timing(
  issues : Array[ContractLintIssue],
  object_id : String,
  field : String,
  value : Int64?,
) -> Unit {
  match value {
    Some(v) if v < 0L =>
      issues.push({
        object_id: Some(object_id),
        binding_id: None,
        message: field + " must be >= 0",
      })
    _ => ()
  }
}

///|
fn lint_safety(
  issues : Array[ContractLintIssue],
  object : ObjectContract,
) -> Unit {
  if object.safety.timeout_action.trim().length() == 0 {
    issues.push({
      object_id: Some(object.id),
      binding_id: None,
      message: "timeout_action must not be empty",
    })
  }
  if object.kind is Command && object.safety.authority is ReadOnly {
    issues.push({
      object_id: Some(object.id),
      binding_id: None,
      message: "command object cannot be read_only",
    })
  }
  if object.kind is Command && object.safety.authority is Any {
    issues.push({
      object_id: Some(object.id),
      binding_id: None,
      message: "command object requires explicit authority",
    })
  }
  match (object.safety.min_payload_digest, object.safety.max_payload_digest) {
    (Some(min_value), Some(max_value)) if min_value > max_value =>
      issues.push({
        object_id: Some(object.id),
        binding_id: None,
        message: "min_payload_digest exceeds max_payload_digest",
      })
    _ => ()
  }
}

///|
fn lint_binding_shape(
  issues : Array[ContractLintIssue],
  object : ObjectContract,
  binding : ObjectBinding,
) -> Unit {
  match binding.kind {
    TraceLabel => ()
    CoeSdo => {
      require_binding_index(issues, object.id, binding)
      require_binding_subindex(issues, object.id, binding)
    }
    CanopenSdo => {
      require_binding_index(issues, object.id, binding)
      require_binding_subindex(issues, object.id, binding)
      require_binding_channel(issues, object.id, binding)
    }
    EtherCatPdo | CanopenPdo => {
      require_binding_index(issues, object.id, binding)
      require_binding_subindex(issues, object.id, binding)
      require_binding_channel(issues, object.id, binding)
    }
    ZenohKey =>
      if binding.key == "" {
        issues.push({
          object_id: Some(object.id),
          binding_id: Some(binding.binding_id),
          message: "zenoh_key binding requires key",
        })
      }
    ModbusRegister => {
      require_binding_index(issues, object.id, binding)
      require_binding_channel(issues, object.id, binding)
    }
  }
}

///|
fn require_binding_index(
  issues : Array[ContractLintIssue],
  object_id : String,
  binding : ObjectBinding,
) -> Unit {
  if binding.index is None {
    issues.push({
      object_id: Some(object_id),
      binding_id: Some(binding.binding_id),
      message: binding.kind.label() + " binding requires index",
    })
  }
}

///|
fn require_binding_subindex(
  issues : Array[ContractLintIssue],
  object_id : String,
  binding : ObjectBinding,
) -> Unit {
  if binding.subindex is None {
    issues.push({
      object_id: Some(object_id),
      binding_id: Some(binding.binding_id),
      message: binding.kind.label() + " binding requires subindex",
    })
  }
}

///|
fn require_binding_channel(
  issues : Array[ContractLintIssue],
  object_id : String,
  binding : ObjectBinding,
) -> Unit {
  if binding.channel == "" {
    issues.push({
      object_id: Some(object_id),
      binding_id: Some(binding.binding_id),
      message: binding.kind.label() + " binding requires channel",
    })
  }
}

///|
fn lint_binding_object_shape(
  issues : Array[ContractLintIssue],
  object : ObjectContract,
  binding : ObjectBinding,
) -> Unit {
  match object.kind {
    Command =>
      if !(binding.direction is Some(@trace.Tx)) {
        issues.push({
          object_id: Some(object.id),
          binding_id: Some(binding.binding_id),
          message: "command binding direction must be tx",
        })
      }
    Signal =>
      if !(binding.direction is Some(@trace.Rx)) {
        issues.push({
          object_id: Some(object.id),
          binding_id: Some(binding.binding_id),
          message: "signal binding direction must be rx",
        })
      }
    Event =>
      if !(binding.direction is Some(@trace.Rx) ||
        binding.direction is Some(@trace.Fault)) {
        issues.push({
          object_id: Some(object.id),
          binding_id: Some(binding.binding_id),
          message: "event binding direction must be rx or fault",
        })
      }
    _ => ()
  }
}

///|
pub(all) enum ObjectViolationKind {
  AuthorityViolation
  TimingViolation
  PayloadRangeViolation
  TerminalFaultViolation
} derive(Eq, Debug)

///|
pub fn ObjectViolationKind::label(self : ObjectViolationKind) -> String {
  match self {
    AuthorityViolation => "authority"
    TimingViolation => "timing"
    PayloadRangeViolation => "payload-range"
    TerminalFaultViolation => "terminal-fault"
  }
}

///|
pub(all) struct ObjectViolation {
  kind : ObjectViolationKind
  object_id : String
  event_id : Int?
  reason : String
} derive(Eq, Debug)

///|
pub(all) struct ObjectMonitorReport {
  checked_events : Int
  violations : Array[ObjectViolation]
} derive(Eq, Debug)

///|
pub fn ObjectMonitorReport::passes(self : ObjectMonitorReport) -> Bool {
  self.violations.length() == 0
}

///|
pub(all) struct ObjectTimeoutActionObservation {
  object_id : String
  trigger_event_id : Int
  action : String
} derive(Eq, Debug)

///|
pub fn ObjectTimeoutActionObservation::make(
  object_id~ : String,
  trigger_event_id~ : Int,
  action~ : String,
) -> ObjectTimeoutActionObservation {
  { object_id, trigger_event_id, action }
}

///|
pub(all) enum ObjectTimeoutActionStatus {
  TimeoutActionMatched
  TimeoutActionMissing
  TimeoutActionMismatch
  TimeoutActionDuplicate
  TimeoutActionOrphan
} derive(Eq, Debug)

///|
pub fn ObjectTimeoutActionStatus::label(
  self : ObjectTimeoutActionStatus,
) -> String {
  match self {
    TimeoutActionMatched => "matched"
    TimeoutActionMissing => "missing"
    TimeoutActionMismatch => "mismatch"
    TimeoutActionDuplicate => "duplicate"
    TimeoutActionOrphan => "orphan"
  }
}

///|
pub(all) struct ObjectTimeoutActionEvaluation {
  object_id : String
  trigger_event_id : Int
  expected_action : String
  observed_action : String
  observation_count : Int
  status : ObjectTimeoutActionStatus
} derive(Eq, Debug)

///|
pub(all) struct ObjectMonitorAssessment {
  object_report : ObjectMonitorReport
  timeout_action_observation_count : Int
  timeout_action_evaluations : Array[ObjectTimeoutActionEvaluation]
  timeout_action_matched_count : Int
  timeout_action_missing_count : Int
  timeout_action_mismatch_count : Int
  timeout_action_duplicate_count : Int
  timeout_action_orphan_count : Int
} derive(Eq, Debug)

///|
pub fn ObjectMonitorAssessment::passes(self : ObjectMonitorAssessment) -> Bool {
  self.object_report.passes() &&
  self.timeout_action_missing_count == 0 &&
  self.timeout_action_mismatch_count == 0 &&
  self.timeout_action_duplicate_count == 0 &&
  self.timeout_action_orphan_count == 0
}

///|
pub(all) struct ObjectMonitor {
  registry : ContractRegistry
} derive(Debug)

///|
pub fn ObjectMonitor::new(registry~ : ContractRegistry) -> ObjectMonitor {
  { registry, }
}

///|
pub fn ObjectMonitor::check_trace(
  self : ObjectMonitor,
  log : @trace.TraceLog,
) -> ObjectMonitorReport {
  let violations : Array[ObjectViolation] = []
  let mut checked_events = 0
  let mut terminal_fault_seen = false
  for event in log.events {
    if event.direction == @trace.Fault {
      terminal_fault_seen = true
    }
    for object in self.registry.objects {
      for binding in object.bindings {
        if binding.matches(event) {
          checked_events += 1
          check_authority(violations, object, binding, event)
          check_timing(violations, object, event)
          check_payload_digest_range(violations, object, event)
          if object.kind is Command && terminal_fault_seen {
            violations.push({
              kind: TerminalFaultViolation,
              object_id: object.id,
              event_id: Some(event.event_id),
              reason: "command observed after terminal fault",
            })
          }
        }
      }
    }
  }
  { checked_events, violations }
}

///|
pub fn ObjectMonitor::assess_trace(
  self : ObjectMonitor,
  log : @trace.TraceLog,
  timeout_actions : ArrayView[ObjectTimeoutActionObservation],
) -> ObjectMonitorAssessment {
  let object_report = self.check_trace(log)
  let evaluations = evaluate_timeout_actions(
    self.registry,
    object_report.violations,
    timeout_actions,
  )
  {
    object_report,
    timeout_action_observation_count: timeout_actions.length(),
    timeout_action_evaluations: evaluations,
    timeout_action_matched_count: count_timeout_action_status(
      evaluations,
      TimeoutActionMatched,
    ),
    timeout_action_missing_count: count_timeout_action_status(
      evaluations,
      TimeoutActionMissing,
    ),
    timeout_action_mismatch_count: count_timeout_action_status(
      evaluations,
      TimeoutActionMismatch,
    ),
    timeout_action_duplicate_count: count_timeout_action_status(
      evaluations,
      TimeoutActionDuplicate,
    ),
    timeout_action_orphan_count: count_timeout_action_status(
      evaluations,
      TimeoutActionOrphan,
    ),
  }
}

///|
fn check_authority(
  violations : Array[ObjectViolation],
  object : ObjectContract,
  binding : ObjectBinding,
  event : @trace.TraceEvent,
) -> Unit {
  if object.kind is Command {
    let authority = match binding.authority {
      Any => object.safety.authority
      other => other
    }
    if !authority.allows(event.node_id) {
      violations.push({
        kind: AuthorityViolation,
        object_id: object.id,
        event_id: Some(event.event_id),
        reason: "authority denied for node " + event.node_id,
      })
    }
  }
}

///|
fn check_timing(
  violations : Array[ObjectViolation],
  object : ObjectContract,
  event : @trace.TraceEvent,
) -> Unit {
  let age_ns = event_age_ns(event)
  match object.timing.max_age_ns {
    Some(limit) if age_ns > limit =>
      violations.push({
        kind: TimingViolation,
        object_id: object.id,
        event_id: Some(event.event_id),
        reason: "max_age_ns exceeded",
      })
    _ => ()
  }
  match object.timing.valid_for_ns {
    Some(limit) if age_ns > limit =>
      violations.push({
        kind: TimingViolation,
        object_id: object.id,
        event_id: Some(event.event_id),
        reason: "valid_for_ns exceeded",
      })
    _ => ()
  }
  match object.timing.deadline_ns {
    Some(deadline) if age_ns > deadline =>
      violations.push({
        kind: TimingViolation,
        object_id: object.id,
        event_id: Some(event.event_id),
        reason: "deadline_ns exceeded",
      })
    _ => ()
  }
}

///|
fn check_payload_digest_range(
  violations : Array[ObjectViolation],
  object : ObjectContract,
  event : @trace.TraceEvent,
) -> Unit {
  match event.payload_digest {
    Some(digest) => {
      match object.safety.min_payload_digest {
        Some(min_value) if digest < min_value =>
          violations.push({
            kind: PayloadRangeViolation,
            object_id: object.id,
            event_id: Some(event.event_id),
            reason: "payload digest below range",
          })
        _ => ()
      }
      match object.safety.max_payload_digest {
        Some(max_value) if digest > max_value =>
          violations.push({
            kind: PayloadRangeViolation,
            object_id: object.id,
            event_id: Some(event.event_id),
            reason: "payload digest above range",
          })
        _ => ()
      }
    }
    None => ()
  }
}

///|
fn event_age_ns(event : @trace.TraceEvent) -> Int64 {
  let age = event.raw_ns - event.vtime.ns()
  if age < 0L {
    0L
  } else {
    age
  }
}

///|
fn evaluate_timeout_actions(
  registry : ContractRegistry,
  violations : ArrayView[ObjectViolation],
  observations : ArrayView[ObjectTimeoutActionObservation],
) -> Array[ObjectTimeoutActionEvaluation] {
  let evaluations : Array[ObjectTimeoutActionEvaluation] = []
  let required_objects : Array[String] = []
  let required_events : Array[Int] = []
  for violation in violations {
    guard violation.kind == TimingViolation else { continue }
    guard violation.event_id is Some(event_id) else { continue }
    if !object_event_pair_exists(
        required_objects,
        required_events,
        violation.object_id,
        event_id,
      ) {
      required_objects.push(violation.object_id)
      required_events.push(event_id)
    }
  }
  for i in 0.. 1 {
      TimeoutActionDuplicate
    } else if observed == expected {
      TimeoutActionMatched
    } else {
      TimeoutActionMismatch
    }
    evaluations.push({
      object_id,
      trigger_event_id: event_id,
      expected_action: expected,
      observed_action: observed,
      observation_count: matches.length(),
      status,
    })
  }
  let processed_objects : Array[String] = []
  let processed_events : Array[Int] = []
  for observation in observations {
    if object_event_pair_exists(
        required_objects,
        required_events,
        observation.object_id,
        observation.trigger_event_id,
      ) ||
      object_event_pair_exists(
        processed_objects,
        processed_events,
        observation.object_id,
        observation.trigger_event_id,
      ) {
      continue
    }
    let matches = timeout_action_observations_for(
      observations,
      observation.object_id,
      observation.trigger_event_id,
    )
    evaluations.push({
      object_id: observation.object_id,
      trigger_event_id: observation.trigger_event_id,
      expected_action: "",
      observed_action: observation.action,
      observation_count: matches.length(),
      status: if matches.length() > 1 {
        TimeoutActionDuplicate
      } else {
        TimeoutActionOrphan
      },
    })
    processed_objects.push(observation.object_id)
    processed_events.push(observation.trigger_event_id)
  }
  evaluations
}

///|
fn object_event_pair_exists(
  object_ids : ArrayView[String],
  event_ids : ArrayView[Int],
  object_id : String,
  event_id : Int,
) -> Bool {
  for i in 0.. Array[ObjectTimeoutActionObservation] {
  observations.filter(fn(observation) {
    observation.object_id == object_id &&
    observation.trigger_event_id == event_id
  })
}

///|
fn contract_timeout_action(
  registry : ContractRegistry,
  object_id : String,
) -> String {
  for object in registry.objects {
    if object.id == object_id {
      return object.safety.timeout_action
    }
  }
  ""
}

///|
fn count_timeout_action_status(
  evaluations : ArrayView[ObjectTimeoutActionEvaluation],
  status : ObjectTimeoutActionStatus,
) -> Int {
  evaluations.filter(fn(evaluation) { evaluation.status == status }).length()
}

///|
pub fn axis_authority_contracts() -> ContractRegistry {
  let controller = Role("controller")
  let actual_position = ObjectContract::make(
    id="axis.actual_position",
    kind=Signal,
    schema=I32,
    timing=TimingContract::make(period_ns=1_000_000L, max_age_ns=2_000_000L),
    safety=SafetyContract::make(authority=ReadOnly),
  ).with_binding(
    ObjectBinding::make(
      object_id="axis.actual_position",
      binding_id="trace.axis.actual_position",
      medium_id="trace",
      label="axis.actual_position.signal",
      direction=@trace.Rx,
    ),
  )
  let target_position = ObjectContract::make(
    id="axis.target_position",
    kind=Command,
    schema=I32,
    timing=TimingContract::make(max_age_ns=1_000_000L, valid_for_ns=1_000_000L),
    safety=SafetyContract::make(
      authority=controller,
      min_payload_digest=0,
      max_payload_digest=1_000_000,
      timeout_action="hold_last",
    ),
  ).with_binding(
    ObjectBinding::make(
      object_id="axis.target_position",
      binding_id="trace.axis.target_position",
      medium_id="trace",
      label="axis.target_position.command",
      direction=@trace.Tx,
      authority=controller,
    ),
  )
  let statusword = ObjectContract::make(
    id="axis.statusword",
    kind=Signal,
    schema=U16,
    timing=TimingContract::make(period_ns=1_000_000L, max_age_ns=2_000_000L),
    safety=SafetyContract::make(authority=ReadOnly),
  ).with_binding(
    ObjectBinding::make(
      object_id="axis.statusword",
      binding_id="trace.axis.statusword",
      medium_id="trace",
      label="axis.statusword.ack",
      direction=@trace.Rx,
    ),
  )
  let lifecycle = ObjectContract::make(
      id="axis.lifecycle",
      kind=Lifecycle,
      schema=State,
      timing=TimingContract::make(max_age_ns=5_000_000L),
      safety=SafetyContract::make(authority=controller),
    )
    .with_binding(
      ObjectBinding::make(
        object_id="axis.lifecycle",
        binding_id="trace.axis.lifecycle.enable",
        medium_id="trace",
        label="axis.lifecycle.enable",
        direction=@trace.Tx,
        authority=controller,
      ),
    )
    .with_binding(
      ObjectBinding::make(
        object_id="axis.lifecycle",
        binding_id="trace.axis.lifecycle.fault",
        medium_id="trace",
        label="axis.lifecycle.fault",
        direction=@trace.Fault,
      ),
    )
  ContractRegistry::new()
  .with_object(actual_position)
  .with_object(target_position)
  .with_object(statusword)
  .with_object(lifecycle)
}