///|
/// Headless engineering project model for Isochronon E1.

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

///|
pub(all) enum ProjectAssetKind {
  ObjectContractSet
  ProtocolContract
  FieldbusProfile
  ProviderHarness
  LockwireBackend
  ReferenceRepository
} derive(Eq, Debug)

///|
pub fn ProjectAssetKind::label(self : ProjectAssetKind) -> String {
  match self {
    ObjectContractSet => "object-contract-set"
    ProtocolContract => "protocol-contract"
    FieldbusProfile => "fieldbus-profile"
    ProviderHarness => "provider-harness"
    LockwireBackend => "lockwire-backend"
    ReferenceRepository => "reference-repository"
  }
}

///|
pub(all) enum RuntimeProfileKind {
  SimNative
  SimWasm
  Replay
  FaultCampaign
  RealLinux
  RealEmbedded
} derive(Eq, Debug)

///|
pub fn RuntimeProfileKind::label(self : RuntimeProfileKind) -> String {
  match self {
    SimNative => "sim-native"
    SimWasm => "sim-wasm"
    Replay => "replay"
    FaultCampaign => "fault-campaign"
    RealLinux => "real-linux"
    RealEmbedded => "real-embedded"
  }
}

///|
pub fn RuntimeProfileKind::is_offline(self : RuntimeProfileKind) -> Bool {
  match self {
    SimNative | SimWasm | Replay | FaultCampaign => true
    RealLinux | RealEmbedded => false
  }
}

///|
pub(all) enum ClockSyncGuarantee {
  NoSync
  Causal
  Windowed
  Physical
} derive(Eq, Debug)

///|
pub fn ClockSyncGuarantee::label(self : ClockSyncGuarantee) -> String {
  match self {
    NoSync => "none"
    Causal => "causal"
    Windowed => "windowed"
    Physical => "physical"
  }
}

///|
pub(all) struct LinkCapabilityVector {
  single_initiator : Bool
  bounded_delay : Bool
  total_order_delivery : Bool
  partition_possible : Bool
  clock_sync : ClockSyncGuarantee
} derive(Eq, Debug)

///|
pub fn LinkCapabilityVector::make(
  single_initiator~ : Bool,
  bounded_delay~ : Bool,
  total_order_delivery~ : Bool,
  partition_possible~ : Bool,
  clock_sync~ : ClockSyncGuarantee,
) -> LinkCapabilityVector {
  {
    single_initiator,
    bounded_delay,
    total_order_delivery,
    partition_possible,
    clock_sync,
  }
}

///|
pub fn LinkCapabilityVector::e2_strength(self : LinkCapabilityVector) -> String {
  if self.bounded_delay && self.total_order_delivery {
    "strong-delta"
  } else {
    "logical-quantile"
  }
}

///|
pub fn LinkCapabilityVector::envelope_kind(
  self : LinkCapabilityVector,
) -> String {
  if self.bounded_delay {
    "worst-case"
  } else {
    "statistical"
  }
}

///|
pub fn LinkCapabilityVector::partition_faults_required(
  self : LinkCapabilityVector,
) -> Bool {
  self.partition_possible
}

///|
pub fn LinkCapabilityVector::absolute_time_allowed(
  self : LinkCapabilityVector,
) -> Bool {
  self.clock_sync == Physical
}

///|
pub(all) struct RuntimeProfile {
  id : String
  kind : RuntimeProfileKind
  lockwire_backend : String
  trace_route : String
  evidence_policy : String
  live_io_required : Bool
} derive(Eq, Debug)

///|
pub fn RuntimeProfile::make(
  id~ : String,
  kind~ : RuntimeProfileKind,
  lockwire_backend~ : String,
  trace_route? : String = "trace.digest",
  evidence_policy? : String = "offline",
  live_io_required? : Bool = false,
) -> RuntimeProfile {
  { id, kind, lockwire_backend, trace_route, evidence_policy, live_io_required }
}

///|
pub(all) struct DeviceNode {
  id : String
  label : String
  family : String
  provider_slot : String
  runtime_target : String
  capabilities : LinkCapabilityVector?
  parent_id : String?
} derive(Eq, Debug)

///|
pub fn DeviceNode::make(
  id~ : String,
  label~ : String,
  family~ : String,
  provider_slot~ : String,
  runtime_target~ : String,
  capabilities? : LinkCapabilityVector,
  parent_id? : String,
) -> DeviceNode {
  { id, label, family, provider_slot, runtime_target, capabilities, parent_id }
}

///|
pub(all) struct DeviceTree {
  root_id : String
  devices : Array[DeviceNode]
} derive(Eq, Debug)

///|
pub fn DeviceTree::make(
  root_id~ : String,
  devices~ : Array[DeviceNode],
) -> DeviceTree {
  { root_id, devices }
}

///|
pub fn DeviceTree::empty() -> DeviceTree {
  { root_id: "", devices: [] }
}

///|
pub fn DeviceTree::contains_device(self : DeviceTree, id : String) -> Bool {
  self.devices.any(fn(device) { device.id == id })
}

///|
pub(all) struct TaskConfig {
  id : String
  runtime_profile_id : String
  period_ns : Int64?
  priority : Int
  deadline_ns : Int64?
  clock_domain : String
} derive(Eq, Debug)

///|
pub fn TaskConfig::make(
  id~ : String,
  runtime_profile_id~ : String,
  period_ns? : Int64,
  priority? : Int = 10,
  deadline_ns? : Int64,
  clock_domain? : String = "sim",
) -> TaskConfig {
  { id, runtime_profile_id, period_ns, priority, deadline_ns, clock_domain }
}

///|
pub(all) struct ProtocolRoleBinding {
  protocol_asset_id : String
  role : String
  device_id : String
} derive(Eq, Debug)

///|
pub fn ProtocolRoleBinding::make(
  protocol_asset_id~ : String,
  role~ : String,
  device_id~ : String,
) -> ProtocolRoleBinding {
  { protocol_asset_id, role, device_id }
}

///|
pub(all) struct Application {
  id : String
  label : String
  device_id : String
  task_ids : Array[String]
  object_set_assets : Array[String]
  protocol_session_assets : Array[String]
  protocol_role_bindings : Array[ProtocolRoleBinding]
  library_assets : Array[String]
} derive(Eq, Debug)

///|
pub fn Application::make(
  id~ : String,
  label~ : String,
  device_id~ : String,
  task_ids~ : Array[String],
  object_set_assets? : Array[String] = [],
  protocol_session_assets? : Array[String] = [],
  protocol_role_bindings? : Array[ProtocolRoleBinding] = [],
  library_assets? : Array[String] = [],
) -> Application {
  {
    id,
    label,
    device_id,
    task_ids,
    object_set_assets,
    protocol_session_assets,
    protocol_role_bindings,
    library_assets,
  }
}

///|
pub(all) struct ProviderSlot {
  id : String
  provider_id : String
  package_id : String
  descriptor_path : String
  live_default_allowed : Bool
} derive(Eq, Debug)

///|
pub fn ProviderSlot::make(
  id~ : String,
  provider_id~ : String,
  package_id~ : String,
  descriptor_path~ : String,
  live_default_allowed? : Bool = false,
) -> ProviderSlot {
  { id, provider_id, package_id, descriptor_path, live_default_allowed }
}

///|
pub(all) struct ProtocolAdapterBinding {
  id : String
  application_id : String
  protocol_asset_id : String
  provider_slot_id : String
  manifest_id : String
  provider_id : String
  source_adapter_id : String
  manifest_digest : String
} derive(Eq, Debug)

///|
pub fn ProtocolAdapterBinding::make(
  id~ : String,
  application_id~ : String,
  protocol_asset_id~ : String,
  provider_slot_id~ : String,
  manifest_id~ : String,
  provider_id~ : String,
  source_adapter_id~ : String,
  manifest_digest~ : String,
) -> ProtocolAdapterBinding {
  {
    id,
    application_id,
    protocol_asset_id,
    provider_slot_id,
    manifest_id,
    provider_id,
    source_adapter_id,
    manifest_digest,
  }
}

///|
pub(all) struct ProjectAssetRef {
  id : String
  kind : ProjectAssetKind
  package_id : String
  symbol : String
  source_path : String
  provider_slot : String?
} derive(Eq, Debug)

///|
pub fn ProjectAssetRef::make(
  id~ : String,
  kind~ : ProjectAssetKind,
  package_id~ : String,
  symbol~ : String,
  source_path~ : String,
  provider_slot? : String,
) -> ProjectAssetRef {
  { id, kind, package_id, symbol, source_path, provider_slot }
}

///|
pub(all) struct LibraryRegistry {
  assets : Array[ProjectAssetRef]
  provider_slots : Array[ProviderSlot]
  protocol_adapter_bindings : Array[ProtocolAdapterBinding]
} derive(Eq, Debug)

///|
pub fn LibraryRegistry::make(
  assets~ : Array[ProjectAssetRef],
  provider_slots? : Array[ProviderSlot] = [],
  protocol_adapter_bindings? : Array[ProtocolAdapterBinding] = [],
) -> LibraryRegistry {
  { assets, provider_slots, protocol_adapter_bindings }
}

///|
pub fn LibraryRegistry::empty() -> LibraryRegistry {
  { assets: [], provider_slots: [], protocol_adapter_bindings: [] }
}

///|
pub fn LibraryRegistry::contains_asset(
  self : LibraryRegistry,
  id : String,
) -> Bool {
  self.assets.any(fn(asset) { asset.id == id })
}

///|
pub fn LibraryRegistry::contains_provider_slot(
  self : LibraryRegistry,
  id : String,
) -> Bool {
  self.provider_slots.any(fn(slot) { slot.id == id })
}

///|
pub(all) struct IsoProject {
  id : String
  name : String
  device_tree : DeviceTree
  applications : Array[Application]
  tasks : Array[TaskConfig]
  libraries : LibraryRegistry
  runtime_profiles : Array[RuntimeProfile]
} derive(Eq, Debug)

///|
pub fn IsoProject::make(
  id~ : String,
  name~ : String,
  device_tree~ : DeviceTree,
  applications~ : Array[Application],
  tasks~ : Array[TaskConfig],
  libraries~ : LibraryRegistry,
  runtime_profiles~ : Array[RuntimeProfile],
) -> IsoProject {
  { id, name, device_tree, applications, tasks, libraries, runtime_profiles }
}

///|
pub(all) struct ProjectLintIssue {
  code : String
  target : String
  message : String
} derive(Eq, Debug)

///|
pub(all) struct ProjectLintReport {
  project_id : String
  issue_count : Int
  missing_device_count : Int
  missing_application_count : Int
  missing_task_count : Int
  missing_library_count : Int
  missing_capability_count : Int
  invalid_binding_count : Int
  incompatible_capability_binding_count : Int
  unresolved_object_contract_registry_count : Int
  incompatible_object_contract_binding_count : Int
  unresolved_protocol_session_count : Int
  incompatible_protocol_edge_count : Int
  live_profile_count : Int
  issues : Array[ProjectLintIssue]
} derive(Eq, Debug)

///|
pub fn ProjectLintReport::passes(self : ProjectLintReport) -> Bool {
  self.issue_count == 0
}

///|
pub fn IsoProject::protocol_adapter_binding_declaration_issues(
  self : IsoProject,
) -> Array[ProjectLintIssue] {
  let issues : Array[ProjectLintIssue] = []
  let bindings = self.libraries.protocol_adapter_bindings
  for application in self.applications {
    for protocol_asset_id in application.protocol_session_assets {
      let matches = bindings.filter(binding => {
        binding.application_id == application.id &&
        binding.protocol_asset_id == protocol_asset_id
      })
      if matches.is_empty() {
        push_unique_issue(
          issues,
          code="missing-protocol-adapter-binding",
          target=application.id + ":" + protocol_asset_id,
          message="project protocol session must select one runtime adapter manifest",
        )
      } else if matches.length() > 1 {
        push_unique_issue(
          issues,
          code="ambiguous-protocol-adapter-binding",
          target=application.id + ":" + protocol_asset_id,
          message="project protocol session selects more than one runtime adapter manifest",
        )
      }
    }
  }
  for binding in bindings {
    let target = if binding.id.is_empty() {
      ""
    } else {
      binding.id
    }
    if binding.id.is_empty() ||
      binding.application_id.is_empty() ||
      binding.protocol_asset_id.is_empty() ||
      binding.provider_slot_id.is_empty() ||
      binding.manifest_id.is_empty() ||
      binding.provider_id.is_empty() ||
      binding.source_adapter_id.is_empty() ||
      binding.manifest_digest.is_empty() {
      push_unique_issue(
        issues,
        code="empty-protocol-adapter-binding-field",
        target~,
        message="protocol adapter binding fields must not be empty",
      )
    }
    if bindings.filter(other => other.id == binding.id).length() > 1 {
      push_unique_issue(
        issues,
        code="duplicate-protocol-adapter-binding-id",
        target~,
        message="protocol adapter binding id must be unique",
      )
    }
    let applications = self.applications.filter(application => {
      application.id == binding.application_id
    })
    if applications.length() != 1 ||
      !applications[0].protocol_session_assets.contains(
        binding.protocol_asset_id,
      ) {
      push_unique_issue(
        issues,
        code="orphan-protocol-adapter-binding",
        target~,
        message="protocol adapter binding must target one selected project protocol session",
      )
    }
    let slots = self.libraries.provider_slots.filter(slot => {
      slot.id == binding.provider_slot_id
    })
    if slots.is_empty() {
      push_unique_issue(
        issues,
        code="unknown-protocol-adapter-provider-slot",
        target~,
        message="protocol adapter binding references an unknown provider slot",
      )
    } else if slots.length() > 1 {
      push_unique_issue(
        issues,
        code="ambiguous-protocol-adapter-provider-slot",
        target~,
        message="protocol adapter binding provider slot is ambiguous",
      )
    } else if slots[0].provider_id != binding.provider_id {
      push_unique_issue(
        issues,
        code="protocol-adapter-provider-mismatch",
        target~,
        message="protocol adapter binding provider id does not match its provider slot",
      )
    }
  }
  issues
}

///|
pub fn IsoProject::lint(self : IsoProject) -> ProjectLintReport {
  let issues : Array[ProjectLintIssue] = []
  let mut missing_device_count = 0
  let mut missing_application_count = 0
  let mut missing_task_count = 0
  let mut missing_library_count = 0
  let mut missing_capability_count = 0
  let mut invalid_binding_count = 0
  let mut incompatible_capability_binding_count = 0
  if self.device_tree.root_id == "" || self.device_tree.devices.is_empty() {
    missing_device_count += 1
    push_issue(
      issues,
      code="missing-device-tree",
      target=self.id,
      message="project must declare a device tree with at least one device",
    )
  }
  if self.applications.is_empty() {
    missing_application_count += 1
    push_issue(
      issues,
      code="missing-application",
      target=self.id,
      message="project must declare at least one application",
    )
  }
  if self.tasks.is_empty() {
    missing_task_count += 1
    push_issue(
      issues,
      code="missing-task",
      target=self.id,
      message="project must declare at least one task config",
    )
  }
  if self.libraries.assets.is_empty() {
    missing_library_count += 1
    push_issue(
      issues,
      code="missing-library",
      target=self.id,
      message="project must declare at least one library asset",
    )
  }
  if self.runtime_profiles.is_empty() {
    push_issue(
      issues,
      code="missing-runtime-profile",
      target=self.id,
      message="project must declare at least one runtime profile",
    )
  }
  for device in self.device_tree.devices {
    if device.capabilities is None {
      missing_capability_count += 1
      invalid_binding_count += 1
      push_issue(
        issues,
        code="missing-capability-vector",
        target=device.id,
        message="device must explicitly declare a link capability vector",
      )
    }
    if device.provider_slot != "" &&
      !self.libraries.contains_provider_slot(device.provider_slot) {
      invalid_binding_count += 1
      push_issue(
        issues,
        code="unknown-provider-slot",
        target=device.id,
        message="device references unknown provider slot " +
          device.provider_slot,
      )
    }
  }
  for task in self.tasks {
    invalid_binding_count += lint_task(self, task, issues)
  }
  for app in self.applications {
    invalid_binding_count += lint_application(self, app, issues)
  }
  let protocol_adapter_issues = self.protocol_adapter_binding_declaration_issues()
  invalid_binding_count += protocol_adapter_issues.length()
  for issue in protocol_adapter_issues {
    issues.push(issue)
  }
  incompatible_capability_binding_count = lint_capability_bindings(self, issues)
  invalid_binding_count += incompatible_capability_binding_count
  {
    project_id: self.id,
    issue_count: issues.length(),
    missing_device_count,
    missing_application_count,
    missing_task_count,
    missing_library_count,
    missing_capability_count,
    invalid_binding_count,
    incompatible_capability_binding_count,
    unresolved_object_contract_registry_count: 0,
    incompatible_object_contract_binding_count: 0,
    unresolved_protocol_session_count: 0,
    incompatible_protocol_edge_count: 0,
    live_profile_count: self.runtime_profiles
    .filter(fn(profile) { profile.live_io_required })
    .length(),
    issues,
  }
}

///|
pub fn iso_project_fixture() -> IsoProject {
  IsoProject::make(
    id="iso.demo.axis",
    name="Isochronon demo axis project",
    device_tree=DeviceTree::make(root_id="cell-0", devices=[
      DeviceNode::make(
        id="cell-0",
        label="Demo cell",
        family="workcell",
        provider_slot="sim-provider",
        runtime_target="sim-wasm",
        capabilities=LinkCapabilityVector::make(
          single_initiator=true,
          bounded_delay=true,
          total_order_delivery=true,
          partition_possible=false,
          clock_sync=Physical,
        ),
      ),
      DeviceNode::make(
        id="axis-1",
        label="Axis 1",
        family="cia402-axis",
        provider_slot="moonecat-provider",
        runtime_target="sim-wasm",
        capabilities=LinkCapabilityVector::make(
          single_initiator=true,
          bounded_delay=true,
          total_order_delivery=true,
          partition_possible=false,
          clock_sync=Physical,
        ),
        parent_id="cell-0",
      ),
    ]),
    applications=[
      Application::make(
        id="axis-control",
        label="Axis control",
        device_id="axis-1",
        task_ids=["fast-cycle"],
        object_set_assets=["axis-object-contracts"],
        protocol_session_assets=["cia402-enable-fault-reset"],
        protocol_role_bindings=[
          ProtocolRoleBinding::make(
            protocol_asset_id="cia402-enable-fault-reset",
            role="controller",
            device_id="cell-0",
          ),
          ProtocolRoleBinding::make(
            protocol_asset_id="cia402-enable-fault-reset",
            role="drive",
            device_id="axis-1",
          ),
        ],
        library_assets=["fieldbus-core-cia402", "lockwire-runtime-substrate"],
      ),
    ],
    tasks=[
      TaskConfig::make(
        id="fast-cycle",
        runtime_profile_id="sim-wasm",
        period_ns=1_000_000L,
        priority=5,
        deadline_ns=1_000_000L,
        clock_domain="sim",
      ),
    ],
    libraries=iso_project_library_registry_fixture(),
    runtime_profiles=[
      RuntimeProfile::make(
        id="sim-wasm",
        kind=SimWasm,
        lockwire_backend="wasm-sim",
        trace_route="trace.timeline",
        evidence_policy="offline-only",
      ),
      RuntimeProfile::make(
        id="sim-native",
        kind=SimNative,
        lockwire_backend="native-sim",
        trace_route="trace.digest",
        evidence_policy="offline-only",
      ),
    ],
  )
}

///|
pub fn iso_project_library_registry_fixture() -> LibraryRegistry {
  LibraryRegistry::make(
    assets=[
      ProjectAssetRef::make(
        id="lockwire-runtime-substrate",
        kind=LockwireBackend,
        package_id="mokomoking2501/lockwire",
        symbol="BackendProfile",
        source_path="lockwire/Lockwire.mbt",
      ),
      ProjectAssetRef::make(
        id="axis-object-contracts",
        kind=ObjectContractSet,
        package_id="mokomoking2501/isocontract",
        symbol="axis_authority_contracts",
        source_path="isocontract/contract.mbt",
      ),
      ProjectAssetRef::make(
        id="cia402-enable-fault-reset",
        kind=ProtocolContract,
        package_id="mokomoking2501/concord",
        symbol="axis_authority_protocol",
        source_path="concord/Concord.mbt",
      ),
      ProjectAssetRef::make(
        id="fieldbus-core-cia402",
        kind=FieldbusProfile,
        package_id="mokomoking2501/fieldbus_core",
        symbol="cia402_object_contracts",
        source_path="fieldbus_core/FieldbusCore.mbt",
      ),
      ProjectAssetRef::make(
        id="moonecat-master-harness",
        kind=ProviderHarness,
        package_id="mokomoking2501/fieldbus_core",
        symbol="moonecat_minimal_master_harness",
        source_path="fieldbus_core/moonecat_master_harness/harness.mbt",
        provider_slot="moonecat-provider",
      ),
    ],
    provider_slots=[
      ProviderSlot::make(
        id="sim-provider",
        provider_id="lockwire-sim",
        package_id="mokomoking2501/lockwire",
        descriptor_path="lockwire/sim",
      ),
      ProviderSlot::make(
        id="moonecat-provider",
        provider_id="moonecat",
        package_id="mokomoking2501/fieldbus_core",
        descriptor_path="fieldbus_core/moonecat_master_harness",
      ),
      ProviderSlot::make(
        id="cia402-scenario-provider",
        provider_id="mokomoking2501/fieldbus_core/cia402_scenario_loop",
        package_id="mokomoking2501/fieldbus_core",
        descriptor_path="fieldbus_core/cia402_scenario_loop",
      ),
    ],
    protocol_adapter_bindings=[
      ProtocolAdapterBinding::make(
        id="axis-control-cia402-runtime-adapter",
        application_id="axis-control",
        protocol_asset_id="cia402-enable-fault-reset",
        provider_slot_id="cia402-scenario-provider",
        manifest_id="cia402-deterministic-runtime-adapter",
        provider_id="mokomoking2501/fieldbus_core/cia402_scenario_loop",
        source_adapter_id="cia402-deterministic-runner",
        manifest_digest="7748763689788634456:4831",
      ),
    ],
  )
}

///|
fn lint_task(
  project : IsoProject,
  task : TaskConfig,
  issues : Array[ProjectLintIssue],
) -> Int {
  let mut count = 0
  if !project.runtime_profiles.any(fn(profile) {
      profile.id == task.runtime_profile_id
    }) {
    count += 1
    push_issue(
      issues,
      code="unknown-runtime-profile",
      target=task.id,
      message="task references unknown runtime profile " +
        task.runtime_profile_id,
    )
  }
  if task.priority < 0 {
    count += 1
    push_issue(
      issues,
      code="invalid-task-priority",
      target=task.id,
      message="task priority must be non-negative",
    )
  }
  match task.period_ns {
    Some(value) =>
      if value <= 0L {
        count += 1
        push_issue(
          issues,
          code="invalid-task-period",
          target=task.id,
          message="task period must be positive",
        )
      }
    None => ()
  }
  match task.deadline_ns {
    Some(value) =>
      if value <= 0L {
        count += 1
        push_issue(
          issues,
          code="invalid-task-deadline",
          target=task.id,
          message="task deadline must be positive",
        )
      }
    None => ()
  }
  count
}

///|
fn lint_application(
  project : IsoProject,
  app : Application,
  issues : Array[ProjectLintIssue],
) -> Int {
  let mut count = 0
  if !project.device_tree.contains_device(app.device_id) {
    count += 1
    push_issue(
      issues,
      code="unknown-application-device",
      target=app.id,
      message="application references unknown device " + app.device_id,
    )
  }
  if app.task_ids.is_empty() {
    count += 1
    push_issue(
      issues,
      code="missing-application-task-binding",
      target=app.id,
      message="application must bind at least one task",
    )
  }
  for task_id in app.task_ids {
    if !project.tasks.any(fn(task) { task.id == task_id }) {
      count += 1
      push_issue(
        issues,
        code="unknown-task-binding",
        target=app.id,
        message="application references unknown task " + task_id,
      )
    }
  }
  count += lint_asset_refs(
    project,
    app.id,
    app.object_set_assets,
    "missing-object-set-binding",
    "unknown-object-set-asset",
    issues,
  )
  count += lint_asset_refs(
    project,
    app.id,
    app.protocol_session_assets,
    "missing-protocol-session-binding",
    "unknown-protocol-session-asset",
    issues,
  )
  count += lint_asset_refs(
    project,
    app.id,
    app.library_assets,
    "missing-library-binding",
    "unknown-library-asset",
    issues,
  )
  count
}

///|
fn lint_capability_bindings(
  project : IsoProject,
  issues : Array[ProjectLintIssue],
) -> Int {
  let mut count = 0
  for app in project.applications {
    guard unique_device_by_id(project.device_tree.devices, app.device_id)
      is Some(device) else {
      continue
    }
    guard device.capabilities is Some(capabilities) else { continue }
    for task_id in app.task_ids {
      guard unique_task_by_id(project.tasks, task_id) is Some(task) else {
        continue
      }
      match task.deadline_ns {
        Some(deadline) if deadline > 0L && !capabilities.bounded_delay => {
          count += 1
          push_issue(
            issues,
            code="hard-deadline-on-unbounded-link",
            target=app.id,
            message="task " +
              task.id +
              " has a deadline but device " +
              device.id +
              " does not declare bounded delay",
          )
        }
        _ => ()
      }
      if task.clock_domain == "physical" &&
        !capabilities.absolute_time_allowed() {
        count += 1
        push_issue(
          issues,
          code="physical-clock-without-physical-sync",
          target=app.id,
          message="task " +
            task.id +
            " requires a physical clock but device " +
            device.id +
            " does not declare physical clock sync",
        )
      }
    }
  }
  count
}

///|
fn unique_device_by_id(
  devices : ArrayView[DeviceNode],
  id : String,
) -> DeviceNode? {
  let mut found : DeviceNode? = None
  for device in devices {
    if device.id == id {
      if found is Some(_) {
        return None
      }
      found = Some(device)
    }
  }
  found
}

///|
fn unique_task_by_id(tasks : ArrayView[TaskConfig], id : String) -> TaskConfig? {
  let mut found : TaskConfig? = None
  for task in tasks {
    if task.id == id {
      if found is Some(_) {
        return None
      }
      found = Some(task)
    }
  }
  found
}

///|
fn lint_asset_refs(
  project : IsoProject,
  owner_id : String,
  asset_ids : ArrayView[String],
  missing_code : String,
  unknown_code : String,
  issues : Array[ProjectLintIssue],
) -> Int {
  let mut count = 0
  if asset_ids.is_empty() {
    count += 1
    push_issue(
      issues,
      code=missing_code,
      target=owner_id,
      message="application asset binding is empty",
    )
  }
  for asset_id in asset_ids {
    if !project.libraries.contains_asset(asset_id) {
      count += 1
      push_issue(
        issues,
        code=unknown_code,
        target=owner_id,
        message="application references unknown asset " + asset_id,
      )
    }
  }
  count
}

///|
fn push_issue(
  issues : Array[ProjectLintIssue],
  code~ : String,
  target~ : String,
  message~ : String,
) -> Unit {
  issues.push({ code, target, message })
}

///|
fn push_unique_issue(
  issues : Array[ProjectLintIssue],
  code~ : String,
  target~ : String,
  message~ : String,
) -> Unit {
  let issue = { code, target, message }
  if !issues.contains(issue) {
    issues.push(issue)
  }
}