///|
/// A replayable evidence case derived from one generated counterexample.
pub(all) struct WitnessCase {
id : String
code : String
direction : String
path : String
payload : String
accepted_by : String
rejected_by : String
reason : String
assertion : String
} derive(Eq, Debug)
///|
/// A deterministic pack of negative compatibility evidence.
pub(all) struct WitnessPack {
contract_name : String
old_version : String
new_version : String
mode : CompatibilityMode
cases : Array[WitnessCase]
} derive(Eq, Debug)
///|
/// Build a replayable evidence pack from all generated witnesses in a report.
pub fn AnalysisReport::to_witness_pack(self : AnalysisReport) -> WitnessPack {
let cases : Array[WitnessCase] = []
let mut ordinal = 1
for change in self.changes {
guard change.witness is Some(witness) else { continue }
let id = make_witness_case_id(ordinal, change.code, change.path)
cases.push({
id,
code: change.code,
direction: change.direction,
path: change.path,
payload: witness.payload,
accepted_by: witness.accepted_by,
rejected_by: witness.rejected_by,
reason: witness.reason,
assertion: replay_assertion(witness),
})
ordinal = ordinal + 1
}
{
contract_name: self.contract_name,
old_version: self.old_version,
new_version: self.new_version,
mode: self.mode,
cases,
}
}
///|
/// Render the witness pack as JSON for archival or review systems.
pub fn WitnessPack::to_json(self : WitnessPack) -> String {
let cases = self.cases.map(witness_case_to_json)
"{" +
"\"schemaVersion\":\"1.0\"," +
"\"kind\":\"evowitness-witness-pack\"," +
"\"contract\":" +
json_string(self.contract_name) +
"," +
"\"oldVersion\":" +
json_string(self.old_version) +
"," +
"\"newVersion\":" +
json_string(self.new_version) +
"," +
"\"mode\":" +
json_string(self.mode.render()) +
"," +
"\"caseCount\":" +
self.cases.length().to_string() +
"," +
"\"cases\":[" +
cases.join(",") +
"]}"
}
///|
/// Render one witness per line so CI can turn each case into a separate test.
pub fn WitnessPack::to_jsonl(self : WitnessPack) -> String {
self.cases.map(witness_case_to_json).join("\n")
}
///|
/// Render a human-readable evidence pack centered on replay assertions.
pub fn WitnessPack::to_markdown(self : WitnessPack) -> String {
let out = StringBuilder()
out.write_string("# EvoWitness witness pack\n\n")
out.write_string(
"- Contract: `" + markdown_escape(self.contract_name) + "`\n",
)
out.write_string(
"- Versions: `" +
markdown_escape(self.old_version) +
"` -> `" +
markdown_escape(self.new_version) +
"`\n",
)
out.write_string("- Mode: `" + self.mode.render() + "`\n")
out.write_string("- Cases: " + self.cases.length().to_string() + "\n\n")
if self.cases.is_empty() {
out.write_string("No witness cases were generated.\n")
return out.to_string()
}
out.write_string("| Id | Direction | Code | Path | Assertion |\n")
out.write_string("|---|---|---|---|---|\n")
for case in self.cases {
out.write_string("| `" + case.id + "` | ")
out.write_string(markdown_escape(case.direction) + " | `")
out.write_string(case.code + "` | `" + markdown_escape(case.path) + "` | ")
out.write_string(markdown_escape(case.assertion) + " |\n")
}
for case in self.cases {
out.write_string("\n## " + case.id + "\n\n")
out.write_string("Replay assertion: " + case.assertion + "\n\n")
out.write_string("Payload:\n\n```json\n")
out.write_string(case.payload + "\n```\n\n")
out.write_string("Reason: " + case.reason + "\n")
}
out.to_string()
}
///|
pub fn AnalysisReport::to_witness_json(self : AnalysisReport) -> String {
self.to_witness_pack().to_json()
}
///|
pub fn AnalysisReport::to_witness_jsonl(self : AnalysisReport) -> String {
self.to_witness_pack().to_jsonl()
}
///|
pub fn AnalysisReport::to_witness_markdown(self : AnalysisReport) -> String {
self.to_witness_pack().to_markdown()
}
///|
fn witness_case_to_json(case : WitnessCase) -> String {
"{" +
"\"schemaVersion\":\"1.0\"," +
"\"kind\":\"evowitness-witness-case\"," +
"\"id\":" +
json_string(case.id) +
"," +
"\"code\":" +
json_string(case.code) +
"," +
"\"direction\":" +
json_string(case.direction) +
"," +
"\"path\":" +
json_string(case.path) +
"," +
"\"payload\":" +
json_string(case.payload) +
"," +
"\"acceptedBy\":" +
json_string(case.accepted_by) +
"," +
"\"rejectedBy\":" +
json_string(case.rejected_by) +
"," +
"\"reason\":" +
json_string(case.reason) +
"," +
"\"assertion\":" +
json_string(case.assertion) +
"}"
}
///|
fn replay_assertion(witness : Witness) -> String {
"accept under " +
witness.accepted_by +
" and reject under " +
witness.rejected_by
}
///|
fn make_witness_case_id(ordinal : Int, code : String, path : String) -> String {
"EW-" + pad3(ordinal) + "-" + pack_slug(code + "-" + path)
}
///|
fn pad3(value : Int) -> String {
if value < 10 {
"00" + value.to_string()
} else if value < 100 {
"0" + value.to_string()
} else {
value.to_string()
}
}
///|
fn pack_slug(value : String) -> String {
let out = StringBuilder()
let mut previous_dash = false
for char in value.to_lower() {
if char.is_ascii_alphabetic() || char.is_ascii_digit() {
out.write_char(char)
previous_dash = false
} else if !previous_dash && !out.is_empty() {
out.write_char('-')
previous_dash = true
}
}
let result = out.to_string()
if result.has_suffix("-") {
result[:result.length() - 1].to_owned()
} else if result.is_empty() {
"case"
} else {
result
}
}