///|
/// Dependency relationships inferred from signal timing.
pub enum DependencyKind {
DataFollowsControl
ControlPrecedesData
SameTimestamp
ClockDriven
CorrelatedActivity
} derive(Debug, Eq)
///|
/// One weighted dependency hypothesis.
pub struct SignalDependency {
source_id : String
target_id : String
kind : DependencyKind
evidence_count : Int
average_lag : Double
confidence : Double
first_time : Int64?
last_time : Int64?
} derive(Debug)
///|
/// Dependency report for a trace.
pub struct DependencyReport {
dependencies : Array[SignalDependency]
signal_count : Int
observation_count : Int
threshold : Int64
} derive(Debug)
///|
/// List dependency categories.
pub fn dependency_kind_catalog() -> Array[DependencyKind] {
[
DataFollowsControl,
ControlPrecedesData,
SameTimestamp,
ClockDriven,
CorrelatedActivity,
]
}
///|
/// Infer dependencies between declared signals using nearby changes.
pub fn infer_signal_dependencies(
trace : Trace,
threshold : Int64,
) -> DependencyReport {
let limit = Int64::max(0L, threshold)
let dependencies : Array[SignalDependency] = []
let observations = trace.changes.length()
for target in trace.signals {
for source in trace.signals {
if source.id == target.id {
continue
}
let pairs = dependency_pairs(trace, source.id, target.id, limit)
if pairs.length() == 0 {
continue
}
let total = pairs.fold(init=0L, fn(sum, item) { sum + item.1 })
let average = total.to_double() / pairs.length().to_double()
let confidence = Int::min(
100,
pairs.length() * 100 / Int::max(1, trace.changes.length()),
).to_double() /
100.0
dependencies.push({
source_id: source.id,
target_id: target.id,
kind: if pairs.any(fn(item) { item.1 == 0L }) {
SameTimestamp
} else {
DataFollowsControl
},
evidence_count: pairs.length(),
average_lag: average,
confidence,
first_time: Some(pairs[0].0),
last_time: Some(pairs[pairs.length() - 1].0),
})
}
}
{
dependencies,
signal_count: trace.signals.length(),
observation_count: observations,
threshold: limit,
}
}
///|
/// Return high-confidence dependencies only.
pub fn DependencyReport::strong(
self : DependencyReport,
minimum : Double,
) -> Array[SignalDependency] {
self.dependencies.filter(fn(item) { item.confidence >= minimum })
}
///|
/// Return dependencies originating from one signal.
pub fn DependencyReport::from(
self : DependencyReport,
source_id : String,
) -> Array[SignalDependency] {
self.dependencies.filter(fn(item) { item.source_id == source_id })
}
///|
/// Return dependencies targeting one signal.
pub fn DependencyReport::to(
self : DependencyReport,
target_id : String,
) -> Array[SignalDependency] {
self.dependencies.filter(fn(item) { item.target_id == target_id })
}
///|
/// Build an adjacency map for scheduling and visualization.
pub fn dependency_adjacency(
report : DependencyReport,
) -> Array[(String, Array[String])] {
let result : Array[(String, Array[String])] = []
for item in report.dependencies {
let mut found = false
for index, group in result {
if group.0 == item.source_id {
if !group.1.contains(item.target_id) {
group.1.push(item.target_id)
}
result[index] = group
found = true
break
}
}
if !found {
result.push((item.source_id, [item.target_id]))
}
}
result
}
///|
/// Export dependencies as CSV.
pub fn dependencies_csv(items : Array[SignalDependency]) -> String {
let out = StringBuilder()
out.write_string(
"source,target,kind,evidence,average_lag,confidence,first,last\n",
)
for item in items {
out.write_string(
escape_text(item.source_id) +
"," +
escape_text(item.target_id) +
"," +
dependency_kind_name(item.kind) +
"," +
item.evidence_count.to_string() +
"," +
item.average_lag.to_string() +
"," +
item.confidence.to_string() +
"," +
dependency_optional_time(item.first_time) +
"," +
dependency_optional_time(item.last_time) +
"\n",
)
}
out.to_string()
}
///|
/// Render a dependency report summary.
pub fn DependencyReport::to_text(self : DependencyReport) -> String {
"signals=" +
self.signal_count.to_string() +
" observations=" +
self.observation_count.to_string() +
" dependencies=" +
self.dependencies.length().to_string() +
" threshold=" +
self.threshold.to_string()
}
///|
/// Render a dependency report as JSON.
pub fn DependencyReport::to_json(self : DependencyReport) -> String {
let items = self.dependencies.map(dependency_json).join(",")
"{\"signals\":" +
self.signal_count.to_string() +
",\"observations\":" +
self.observation_count.to_string() +
",\"threshold\":" +
self.threshold.to_string() +
",\"dependencies\":[" +
items +
"]}"
}
///|
/// Find target changes that follow a source change within a lag threshold.
fn dependency_pairs(
trace : Trace,
source_id : String,
target_id : String,
threshold : Int64,
) -> Array[(Int64, Int64)] {
let result : Array[(Int64, Int64)] = []
let source_times = trace.changes
.filter(fn(item) { item.id == source_id })
.map(fn(item) { item.time })
let target_times = trace.changes
.filter(fn(item) { item.id == target_id })
.map(fn(item) { item.time })
for target_time in target_times {
let mut closest : Int64? = None
for source_time in source_times {
if source_time <= target_time {
let lag = target_time - source_time
if lag <= threshold {
closest = match closest {
Some(value) => Some(Int64::min(value, lag))
None => Some(lag)
}
}
}
}
match closest {
Some(lag) => result.push((target_time, lag))
None => ()
}
}
result
}
///|
/// Name a dependency category.
pub fn dependency_kind_name(kind : DependencyKind) -> String {
match kind {
DataFollowsControl => "data-follows-control"
ControlPrecedesData => "control-precedes-data"
SameTimestamp => "same-timestamp"
ClockDriven => "clock-driven"
CorrelatedActivity => "correlated-activity"
}
}
///|
/// Encode an optional dependency timestamp.
fn dependency_optional_time(value : Int64?) -> String {
match value {
Some(item) => item.to_string()
None => ""
}
}
///|
/// Encode one dependency as JSON.
fn dependency_json(item : SignalDependency) -> String {
"{\"source\":\"" +
escape_text(item.source_id) +
"\",\"target\":\"" +
escape_text(item.target_id) +
"\",\"kind\":\"" +
dependency_kind_name(item.kind) +
"\",\"evidence\":" +
item.evidence_count.to_string() +
",\"average_lag\":" +
item.average_lag.to_string() +
",\"confidence\":" +
item.confidence.to_string() +
"}"
}