///|
/// A validated, replayable JUnit archive. Storage and gate policy are opaque.
struct Evidence {
ledger : @ledger.Ledger
deny_recovered : Bool
}
///|
pub fn Evidence::summary(self : Evidence) -> @ledger.Summary {
self.ledger.summary()
}
///|
pub fn Evidence::source(self : Evidence, report_id : String) -> String? {
self.ledger.source(report_id)
}
///|
/// Independent inventory/document copies, including accepted empty reports.
pub fn Evidence::snapshot(self : Evidence) -> @ledger.Snapshot {
self.ledger.snapshot()
}
///|
pub fn Evidence::exit_code(self : Evidence) -> Int32 {
self.summary().exit_code(deny_recovered=self.deny_recovered)
}
///|
pub fn Evidence::deny_recovered(self : Evidence) -> Bool {
self.deny_recovered
}
///|
/// Archive the full inventory, exact original XML and recovery policy. Custom
/// projections that do not match the supported JUnit parser are refused.
pub fn encode_evidence(
ledger : @ledger.Ledger,
deny_recovered~ : Bool = false,
limits~ : @ledger.Limits = @ledger.Limits::default(),
) -> String raise @ledger.InputError {
limits.validate()
let snapshot = ledger.snapshot()
let checked = @ledger.Ledger::new(snapshot.plan, limits~)
let documents : Array[Json] = []
for document in snapshot.documents {
let parsed = @junit.parse(
document.report.source,
report_id=document.binding.id,
limits~,
)
if parsed != document.report {
raise bad(
"ProjectionMismatch",
"/documents/" + document.binding.id,
"Stored observations differ from the original JUnit source; refusing to discard a custom projection",
)
}
checked.ingest(document)
let fields = binding_fields(document.binding)
fields["xml"] = Json::string(document.report.source)
documents.push(Json::object(fields))
}
let result = Json::object({
"schema": Json::number(1),
"kind": Json::string("moontestledger/evidence"),
"deny_recovered": Json::boolean(deny_recovered),
"plan": plan_json(snapshot.plan),
"documents": Json::array(documents),
}).stringify(indent=2)
if result.length() > limits.max_source_units {
raise bad(
"SourceLimit", "$", "Encoded archive exceeds the configured JSON input budget",
)
}
result
}
///|
/// Strictly replay all stored reports against the stored plan. Missing
/// artifacts stay incomplete; unknown fields and malformed XML are rejected.
pub fn decode_evidence(
source : String,
limits~ : @ledger.Limits = @ledger.Limits::default(),
) -> Evidence raise @ledger.InputError {
let fields = object(strict_json(source, limits), "$")
keys(fields, ["schema", "kind", "deny_recovered", "plan", "documents"], "$")
schema(fields)
if string_field(fields, "kind", "$") != "moontestledger/evidence" {
raise bad(
"ArchiveKind", "$/kind", "Expected a MoonTestLedger evidence archive",
)
}
let deny_recovered = match required(fields, "deny_recovered", "$") {
True => true
False => false
_ =>
raise bad(
"ExpectedBoolean", "$/deny_recovered", "Expected a boolean gate policy",
)
}
let plan = decode_plan(required(fields, "plan", "$").stringify(), limits~)
let ledger = @ledger.Ledger::new(plan, limits~)
let items = array(required(fields, "documents", "$"), "$/documents")
if items.length() > limits.max_reports {
raise bad("ReportLimit", "$/documents", "Archive document budget exceeded")
}
let ids : Set[String] = Set([])
for i, item in items {
let path = "$/documents/\{i}"
let item = object(item, path)
keys(
item,
["id", "run_id", "revision", "environment", "shard", "attempt", "xml"],
path,
)
let id = string_field(item, "id", path)
if ids.contains(id) {
raise bad(
"DuplicateReport", path, "Archive report identity appears more than once",
)
}
ids.add(id)
let binding : @ledger.ReportBinding = {
id,
run_id: string_field(item, "run_id", path),
revision: string_field(item, "revision", path),
environment: string_field(item, "environment", path),
shard: string_field(item, "shard", path),
attempt: positive_integer(item, "attempt", path),
}
let report = @junit.parse(
string_field(item, "xml", path),
report_id=id,
limits~,
)
ledger.ingest({ binding, report, })
}
{ ledger, deny_recovered, }
}
///|
pub fn Evidence::encode(
self : Evidence,
limits~ : @ledger.Limits = @ledger.Limits::default(),
) -> String raise @ledger.InputError {
encode_evidence(self.ledger, deny_recovered=self.deny_recovered, limits~)
}