///|
/// A deterministic repair operation that can be audited before application.
pub enum RepairRule {
SortChanges
RemoveUnknownSignals
ClampNegativeTime
ClampPastEnd
RemoveDuplicateDeclarations
NormalizeValues
DropZeroWidthSignals
} derive(Debug, Eq)
///|
/// Severity assigned to a repair finding.
pub enum RepairFindingLevel {
Informational
Corrected
Rejected
Fatal
} derive(Debug, Eq)
///|
/// One repair finding with source and resulting state.
pub struct RepairFinding {
rule : RepairRule
level : RepairFindingLevel
signal_id : String?
time : Int64?
message : String
} derive(Debug)
///|
/// Repair policy for imported waveform data.
pub struct RepairOptions {
rules : Array[RepairRule]
preserve_unknown_values : Bool
end_time : Int64?
deduplicate_changes : Bool
} derive(Debug)
///|
/// Repaired trace and an audit trail of every material decision.
pub struct RepairResult {
trace : Trace
findings : Array[RepairFinding]
changed : Bool
rejected_changes : Int
repaired_changes : Int
} derive(Debug)
///|
/// List every supported repair rule.
pub fn repair_rule_catalog() -> Array[RepairRule] {
[
SortChanges,
RemoveUnknownSignals,
ClampNegativeTime,
ClampPastEnd,
RemoveDuplicateDeclarations,
NormalizeValues,
DropZeroWidthSignals,
]
}
///|
/// List every finding level.
pub fn repair_finding_level_catalog() -> Array[RepairFindingLevel] {
[Informational, Corrected, Rejected, Fatal]
}
///|
/// Create a conservative repair policy for parser output.
pub fn default_repair_options() -> RepairOptions {
{
rules: [SortChanges, RemoveUnknownSignals, NormalizeValues],
preserve_unknown_values: true,
end_time: None,
deduplicate_changes: true,
}
}
///|
/// Add one repair rule if it is not already present.
pub fn RepairOptions::with_rule(
self : RepairOptions,
rule : RepairRule,
) -> RepairOptions {
if self.rules.contains(rule) {
self
} else {
let rules = self.rules.copy()
rules.push(rule)
{ ..self, rules, }
}
}
///|
/// Set the expected trace end time used by time clamping.
pub fn RepairOptions::with_end_time(
self : RepairOptions,
end_time : Int64?,
) -> RepairOptions {
{ ..self, end_time, }
}
///|
/// Set whether unknown/X/Z values are retained.
pub fn RepairOptions::with_unknown_values(
self : RepairOptions,
preserve : Bool,
) -> RepairOptions {
{ ..self, preserve_unknown_values: preserve }
}
///|
/// Execute a repair policy and return an auditable result.
pub fn repair_trace_with_options(
trace : Trace,
options : RepairOptions,
) -> RepairResult {
let findings : Array[RepairFinding] = []
let active_ids = repair_unique_signal_ids(trace, findings, options)
let signals = repair_signals(trace.signals, findings, options)
let changes = repair_changes(trace, active_ids, findings, options)
let sorted = if options.rules.contains(SortChanges) {
repair_sort_changes(changes)
} else {
changes
}
let end_time = match options.end_time {
Some(value) => Int64::max(0L, value)
None => Int64::max(0L, trace.end_time)
}
let result_trace = {
header: trace.header,
signals,
changes: sorted,
end_time,
}
{
trace: result_trace,
repaired_changes: findings
.filter(fn(item) { item.level is Corrected })
.length(),
rejected_changes: findings
.filter(fn(item) { item.level is Rejected })
.length(),
changed: repair_trace_changed(trace, result_trace),
findings,
}
}
///|
/// Run the default repair policy.
pub fn repair_trace_default(trace : Trace) -> RepairResult {
repair_trace_with_options(trace, default_repair_options())
}
///|
/// Return whether any finding indicates a correction or rejection.
pub fn RepairResult::has_changes(self : RepairResult) -> Bool {
self.changed
}
///|
/// Return all findings for a particular rule.
pub fn RepairResult::findings_for(
self : RepairResult,
rule : RepairRule,
) -> Array[RepairFinding] {
self.findings.filter(fn(item) { item.rule == rule })
}
///|
/// Render an audit report for terminal output.
pub fn RepairResult::to_text(self : RepairResult) -> String {
let out = StringBuilder()
out.write_string("changed=" + self.changed.to_string())
out.write_string(" repaired=" + self.repaired_changes.to_string())
out.write_string(" rejected=" + self.rejected_changes.to_string() + "\n")
for finding in self.findings {
out.write_string(repair_rule_name(finding.rule))
out.write_char(' ')
out.write_string(repair_level_name(finding.level))
out.write_string(": " + finding.message + "\n")
}
out.to_string()
}
///|
/// Render repair findings as CSV.
pub fn repair_findings_csv(findings : Array[RepairFinding]) -> String {
let out = StringBuilder()
out.write_string("rule,level,signal,time,message\n")
for finding in findings {
out.write_string(repair_rule_name(finding.rule))
out.write_char(',')
out.write_string(repair_level_name(finding.level))
out.write_char(',')
out.write_string(repair_optional_text(finding.signal_id))
out.write_char(',')
out.write_string(repair_optional_time(finding.time))
out.write_char(',')
out.write_string(escape_text(finding.message))
out.write_char('\n')
}
out.to_string()
}
///|
/// Render the repair result as JSON.
pub fn RepairResult::to_json(self : RepairResult) -> String {
let findings = self.findings.map(repair_finding_json).join(",")
"{\"changed\":" +
self.changed.to_string() +
",\"repaired_changes\":" +
self.repaired_changes.to_string() +
",\"rejected_changes\":" +
self.rejected_changes.to_string() +
",\"findings\":[" +
findings +
"]}"
}
///|
/// Remove duplicate declarations and collect the surviving identifiers.
fn repair_unique_signal_ids(
trace : Trace,
findings : Array[RepairFinding],
options : RepairOptions,
) -> Array[String] {
let ids : Array[String] = []
for signal in trace.signals {
if ids.contains(signal.id) {
if options.rules.contains(RemoveDuplicateDeclarations) {
findings.push({
rule: RemoveDuplicateDeclarations,
level: Corrected,
signal_id: Some(signal.id),
time: None,
message: "duplicate signal declaration removed",
})
}
} else {
ids.push(signal.id)
}
}
ids
}
///|
/// Apply declaration-level rules.
fn repair_signals(
source : Array[Signal],
findings : Array[RepairFinding],
options : RepairOptions,
) -> Array[Signal] {
let result : Array[Signal] = []
let seen : Array[String] = []
for signal in source {
if seen.contains(signal.id) &&
options.rules.contains(RemoveDuplicateDeclarations) {
continue
}
if signal.width <= 0 && options.rules.contains(DropZeroWidthSignals) {
findings.push({
rule: DropZeroWidthSignals,
level: Rejected,
signal_id: Some(signal.id),
time: None,
message: "zero-width signal declaration removed",
})
continue
}
let normalized = if options.rules.contains(NormalizeValues) {
{
..signal,
width: Int::max(1, signal.width),
kind: repair_kind(signal.kind),
}
} else {
signal
}
result.push(normalized)
seen.push(signal.id)
}
result
}
///|
/// Apply time, identifier, value and duplicate rules to changes.
fn repair_changes(
trace : Trace,
ids : Array[String],
findings : Array[RepairFinding],
options : RepairOptions,
) -> Array[Change] {
let result : Array[Change] = []
let end_time = match options.end_time {
Some(value) => value
None => trace.end_time
}
for change in trace.changes {
if !ids.contains(change.id) && options.rules.contains(RemoveUnknownSignals) {
findings.push({
rule: RemoveUnknownSignals,
level: Rejected,
signal_id: Some(change.id),
time: Some(change.time),
message: "change references an undeclared signal",
})
continue
}
let mut time = change.time
if time < 0L && options.rules.contains(ClampNegativeTime) {
findings.push({
rule: ClampNegativeTime,
level: Corrected,
signal_id: Some(change.id),
time: Some(time),
message: "negative timestamp clamped to zero",
})
time = 0L
}
if time > end_time && options.rules.contains(ClampPastEnd) {
findings.push({
rule: ClampPastEnd,
level: Corrected,
signal_id: Some(change.id),
time: Some(time),
message: "timestamp clamped to trace end",
})
time = Int64::max(0L, end_time)
}
let value = if options.rules.contains(NormalizeValues) {
change.value.normalized()
} else {
change.value
}
if !options.preserve_unknown_values && value.is_unknown() {
findings.push({
rule: NormalizeValues,
level: Rejected,
signal_id: Some(change.id),
time: Some(time),
message: "unknown value rejected by policy",
})
continue
}
let repaired = { time, id: change.id, value }
if options.deduplicate_changes && repair_duplicate_exists(result, repaired) {
findings.push({
rule: SortChanges,
level: Rejected,
signal_id: Some(change.id),
time: Some(time),
message: "duplicate change removed",
})
} else {
result.push(repaired)
}
}
result
}
///|
/// Sort changes by timestamp while preserving declaration order at ties.
fn repair_sort_changes(changes : Array[Change]) -> Array[Change] {
let result : Array[Change] = []
for change in changes {
let mut inserted = false
for index, current in result {
if change.time < current.time {
result.insert(index, change)
inserted = true
break
}
}
if !inserted {
result.push(change)
}
}
result
}
///|
/// Detect an identical signal/time/value tuple already retained.
fn repair_duplicate_exists(changes : Array[Change], candidate : Change) -> Bool {
changes.any(fn(item) {
item.id == candidate.id &&
item.time == candidate.time &&
item.value == candidate.value
})
}
///|
/// Normalize an absent kind label without changing a valid label.
fn repair_kind(kind : String) -> String {
if kind.length() == 0 {
"wire"
} else {
kind
}
}
///|
/// Compare material trace fields for the result flag.
fn repair_trace_changed(left : Trace, right : Trace) -> Bool {
left.end_time != right.end_time ||
left.signals.length() != right.signals.length() ||
left.changes.length() != right.changes.length() ||
!traces_equal(left, right)
}
///|
/// Get a stable rule name.
pub fn repair_rule_name(rule : RepairRule) -> String {
match rule {
SortChanges => "sort-changes"
RemoveUnknownSignals => "remove-unknown-signals"
ClampNegativeTime => "clamp-negative-time"
ClampPastEnd => "clamp-past-end"
RemoveDuplicateDeclarations => "remove-duplicate-declarations"
NormalizeValues => "normalize-values"
DropZeroWidthSignals => "drop-zero-width-signals"
}
}
///|
/// Get a stable finding level name.
pub fn repair_level_name(level : RepairFindingLevel) -> String {
match level {
Informational => "info"
Corrected => "corrected"
Rejected => "rejected"
Fatal => "fatal"
}
}
///|
/// Optional text field for CSV.
fn repair_optional_text(value : String?) -> String {
match value {
Some(item) => escape_text(item)
None => ""
}
}
///|
/// Optional timestamp field for CSV.
fn repair_optional_time(value : Int64?) -> String {
match value {
Some(item) => item.to_string()
None => ""
}
}
///|
/// Encode one finding as JSON.
fn repair_finding_json(item : RepairFinding) -> String {
"{\"rule\":\"" +
repair_rule_name(item.rule) +
"\",\"level\":\"" +
repair_level_name(item.level) +
"\",\"signal\":" +
repair_optional_json_text(item.signal_id) +
",\"time\":" +
repair_optional_json_time(item.time) +
",\"message\":\"" +
escape_text(item.message) +
"\"}"
}
///|
/// Encode an optional string as JSON.
fn repair_optional_json_text(value : String?) -> String {
match value {
Some(item) => "\"" + escape_text(item) + "\""
None => "null"
}
}
///|
/// Encode an optional timestamp as JSON.
fn repair_optional_json_time(value : Int64?) -> String {
match value {
Some(item) => item.to_string()
None => "null"
}
}