///|
/// 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)
}
}