///|
pub(all) struct FindingChange {
code : String
path : String
previous_severity : Severity
current_severity : Severity
previous_message : String
current_message : String
} derive(Eq, Debug)
///|
pub(all) struct AuditDelta {
project_name : String
previous_score : Int
current_score : Int
previous_ready : Bool
current_ready : Bool
added : Array[Finding]
resolved : Array[Finding]
unchanged : Array[Finding]
changed : Array[FindingChange]
} derive(Eq, Debug)
///|
pub fn AuditDelta::score_change(self : AuditDelta) -> Int {
self.current_score - self.previous_score
}
///|
pub fn AuditDelta::improved(self : AuditDelta) -> Bool {
self.score_change() > 0 ||
(!self.previous_ready && self.current_ready) ||
self.resolved.length() > self.added.length()
}
///|
pub fn AuditDelta::regressed(self : AuditDelta) -> Bool {
self.score_change() < 0 ||
(self.previous_ready && !self.current_ready) ||
self.added.length() > self.resolved.length()
}
///|
pub fn AuditDelta::has_changes(self : AuditDelta) -> Bool {
!self.added.is_empty() ||
!self.resolved.is_empty() ||
!self.changed.is_empty() ||
self.previous_score != self.current_score ||
self.previous_ready != self.current_ready
}
///|
fn delta_same_identity(left : Finding, right : Finding) -> Bool {
left.code == right.code && left.path == right.path
}
///|
fn delta_same_content(left : Finding, right : Finding) -> Bool {
delta_same_identity(left, right) &&
left.severity == right.severity &&
left.message == right.message
}
///|
fn delta_find_identity(
findings : Array[Finding],
expected : Finding,
) -> Finding? {
for candidate in findings {
if delta_same_identity(candidate, expected) {
return Some(candidate)
}
}
None
}
///|
/// Compare two audit reports without depending on timestamps or filesystem
/// state. Findings are matched by stable rule code and path.
pub fn compare_reports(
previous : AuditReport,
current : AuditReport,
) -> AuditDelta {
let added = []
let resolved = []
let unchanged = []
let changed = []
for finding in current.findings {
match delta_find_identity(previous.findings, finding) {
None => added.push(finding)
Some(old) =>
if delta_same_content(old, finding) {
unchanged.push(finding)
} else {
changed.push({
code: finding.code,
path: finding.path,
previous_severity: old.severity,
current_severity: finding.severity,
previous_message: old.message,
current_message: finding.message,
})
}
}
}
for finding in previous.findings {
if delta_find_identity(current.findings, finding) is None {
resolved.push(finding)
}
}
{
project_name: current.project.name,
previous_score: previous.score,
current_score: current.score,
previous_ready: previous.is_ready(),
current_ready: current.is_ready(),
added,
resolved,
unchanged,
changed,
}
}
///|
fn delta_status(delta : AuditDelta) -> String {
if delta.improved() && !delta.regressed() {
"IMPROVED"
} else if delta.regressed() && !delta.improved() {
"REGRESSED"
} else if delta.has_changes() {
"CHANGED"
} else {
"UNCHANGED"
}
}
///|
fn delta_signed(value : Int) -> String {
if value > 0 {
"+\{value}"
} else {
value.to_string()
}
}
///|
pub fn render_delta_text(delta : AuditDelta) -> String {
let out = StringBuilder::new()
out.write_string("MoonSeal audit delta: \{delta.project_name}\n")
out.write_string(
"Status: \{delta_status(delta)} | Score: \{delta.previous_score} -> \{delta.current_score} (\{delta_signed(delta.score_change())})\n",
)
out.write_string(
"Findings: +\{delta.added.length()} added | -\{delta.resolved.length()} resolved | ~\{delta.changed.length()} changed | =\{delta.unchanged.length()} unchanged\n",
)
if delta.previous_ready != delta.current_ready {
out.write_string(
"Readiness: \{if delta.previous_ready { "READY" } else { "BLOCKED" }} -> \{if delta.current_ready { "READY" } else { "BLOCKED" }}\n",
)
}
if !delta.added.is_empty() {
out.write_string("\nAdded findings:\n")
for finding in delta.added {
out.write_string(
"+ \{finding.severity.label()} \{finding.code} [\{finding.path}]: \{finding.message}\n",
)
}
}
if !delta.resolved.is_empty() {
out.write_string("\nResolved findings:\n")
for finding in delta.resolved {
out.write_string(
"- \{finding.severity.label()} \{finding.code} [\{finding.path}]: \{finding.message}\n",
)
}
}
if !delta.changed.is_empty() {
out.write_string("\nChanged findings:\n")
for change in delta.changed {
out.write_string(
"~ \{change.code} [\{change.path}]: \{change.previous_severity.label()} -> \{change.current_severity.label()}\n",
)
}
}
out.to_string()
}
///|
fn delta_finding_json(finding : Finding) -> Json {
{
"severity": finding.severity.label(),
"code": finding.code,
"path": finding.path,
"message": finding.message,
}
}
///|
fn delta_change_json(change : FindingChange) -> Json {
{
"code": change.code,
"path": change.path,
"previous": {
"severity": change.previous_severity.label(),
"message": change.previous_message,
},
"current": {
"severity": change.current_severity.label(),
"message": change.current_message,
},
}
}
///|
pub fn delta_json(delta : AuditDelta) -> Json {
let added : Array[Json] = delta.added.map(delta_finding_json)
let resolved : Array[Json] = delta.resolved.map(delta_finding_json)
let unchanged : Array[Json] = delta.unchanged.map(delta_finding_json)
let changed : Array[Json] = delta.changed.map(delta_change_json)
{
"project": delta.project_name,
"status": delta_status(delta),
"score": {
"previous": delta.previous_score,
"current": delta.current_score,
"change": delta.score_change(),
},
"readiness": {
"previous": delta.previous_ready,
"current": delta.current_ready,
},
"summary": {
"added": delta.added.length(),
"resolved": delta.resolved.length(),
"changed": delta.changed.length(),
"unchanged": delta.unchanged.length(),
},
"added": added,
"resolved": resolved,
"changed": changed,
"unchanged": unchanged,
}
}
///|
pub fn render_delta_json(delta : AuditDelta, indent? : Int = 2) -> String {
delta_json(delta).stringify(indent~)
}
///|
pub fn render_delta_markdown(delta : AuditDelta) -> String {
let out = StringBuilder::new()
out.write_string("# MoonSeal audit delta: `\{delta.project_name}`\n\n")
out.write_string("- Status: **\{delta_status(delta)}**\n")
out.write_string(
"- Score: \{delta.previous_score} → \{delta.current_score} (\{delta_signed(delta.score_change())})\n",
)
out.write_string(
"- Findings: **+\{delta.added.length()}** added, **-\{delta.resolved.length()}** resolved, **~\{delta.changed.length()}** changed\n",
)
if !delta.added.is_empty() {
out.write_string("\n## Added\n\n")
out.write_string("| Severity | Code | Path | Message |\n")
out.write_string("| --- | --- | --- | --- |\n")
for finding in delta.added {
out.write_string(
"| \{finding.severity.label()} | \{finding.code} | `\{finding.path}` | \{finding.message} |\n",
)
}
}
if !delta.resolved.is_empty() {
out.write_string("\n## Resolved\n\n")
out.write_string("| Severity | Code | Path | Message |\n")
out.write_string("| --- | --- | --- | --- |\n")
for finding in delta.resolved {
out.write_string(
"| \{finding.severity.label()} | \{finding.code} | `\{finding.path}` | \{finding.message} |\n",
)
}
}
if !delta.changed.is_empty() {
out.write_string("\n## Changed\n\n")
out.write_string("| Code | Path | Previous | Current |\n")
out.write_string("| --- | --- | --- | --- |\n")
for change in delta.changed {
out.write_string(
"| \{change.code} | `\{change.path}` | \{change.previous_severity.label()} | \{change.current_severity.label()} |\n",
)
}
}
out.to_string()
}