///|
/// A self-contained release review whose report can be recomputed from its
/// complete immutable snapshots. Replay verifies content agreement, not who
/// produced the bundle or whether its snapshots represent a real dataset.
pub struct ReviewBundle {
priv before : DatasetIndex
priv after : DatasetIndex
priv report : ReleaseCheck
priv serialized : String
}
///|
fn bundle_snapshot_text(
index : DatasetIndex,
location : String,
) -> String raise TraceError {
let text = index.to_manifest_json()
if text.length() > 2097152 {
reject(
"bundle_snapshot_limit", location, "Embedded snapshot exceeds 2,097,152 UTF-16 code units; split it before creating a review bundle.",
)
}
text
}
///|
// Components come exclusively from the canonical serializers. Check the
// combined output size before concatenating repeated report evidence.
fn bundle_text(
before : String,
after : String,
expected : String,
policy : ReleasePolicy,
) -> String raise TraceError {
let pieces = [
"{\"after\":",
after,
",\"before\":",
before,
",\"expected\":",
expected,
",\"format\":\"bids-metadata-trace/review-bundle\",\"policy\":",
Json::string(release_policy_name(policy)).stringify(),
",\"version\":1}",
]
let mut length = 0
for piece in pieces {
if piece.length() > 16777216 - length {
reject(
"bundle_limit", "", "Complete review bundle exceeds 16,777,216 UTF-16 code units; split the snapshots before creating it.",
)
}
length += piece.length()
}
pieces.join("")
}
///|
/// Capture both complete snapshots and a policy-specific release check. An
/// indeterminate check is retained with all diagnostics, never made into pass.
pub fn ReviewBundle::create(
before : DatasetIndex,
after : DatasetIndex,
policy : ReleasePolicy,
) -> ReviewBundle raise TraceError {
let before_text = bundle_snapshot_text(before, "/before")
let after_text = bundle_snapshot_text(after, "/after")
let report = before.compare(after).release_check(policy)
let serialized = bundle_text(
before_text,
after_text,
report.to_json(),
policy,
)
{ before, after, report, serialized, }
}
///|
/// Strictly decode a v1 bundle and recompute its complete expected report.
/// Duplicate keys, unknown controls, unsupported versions, and stale evidence
/// are errors. Numeric source tokens are compared exactly, without float math.
pub fn ReviewBundle::from_json(text : String) -> ReviewBundle raise TraceError {
guard parse_source(
"",
text,
container_limit=80,
text_limit=16777216,
)
is Object(fields) else {
reject(
"invalid_review_bundle", "", "Expected a review bundle object with format, version, before, after, policy, and expected fields.",
)
return ReviewBundle::create(
DatasetIndex::from_manifest([], []),
DatasetIndex::from_manifest([], []),
AllChanges,
)
}
for key in sorted_keys(fields) {
if !["format", "version", "before", "after", "policy", "expected"].contains(
key,
) {
reject(
"unknown_bundle_field",
"/" + key,
"Unknown review bundle control field; remove it explicitly or use a supported bundle version.",
)
}
}
guard fields.get("format") is Some(String(format)) &&
format == "bids-metadata-trace/review-bundle" else {
reject(
"invalid_review_bundle", "/format", "Expected format bids-metadata-trace/review-bundle.",
)
return ReviewBundle::create(
DatasetIndex::from_manifest([], []),
DatasetIndex::from_manifest([], []),
AllChanges,
)
}
guard fields.get("version") is Some(version) && canonical_json(version) == "1" else {
reject(
"unsupported_bundle_version", "/version", "Only review bundle version 1 is supported; regenerate with this version before replaying.",
)
return ReviewBundle::create(
DatasetIndex::from_manifest([], []),
DatasetIndex::from_manifest([], []),
AllChanges,
)
}
guard fields.get("policy") is Some(String(policy_text)) else {
reject(
"invalid_review_bundle", "/policy", "Expected policy all_changes or effective_values.",
)
return ReviewBundle::create(
DatasetIndex::from_manifest([], []),
DatasetIndex::from_manifest([], []),
AllChanges,
)
}
let policy = match policy_text {
"all_changes" => ReleasePolicy::AllChanges
"effective_values" => EffectiveValues
_ => {
reject(
"invalid_review_bundle", "/policy", "Expected policy all_changes or effective_values.",
)
AllChanges
}
}
guard fields.get("before") is Some(Object(before)) &&
fields.get("after") is Some(Object(after)) &&
fields.get("expected") is Some(Object(expected)) else {
reject(
"invalid_review_bundle", "", "Both snapshots and the complete expected release report must be JSON objects.",
)
return ReviewBundle::create(
DatasetIndex::from_manifest([], []),
DatasetIndex::from_manifest([], []),
AllChanges,
)
}
let bundle = ReviewBundle::create(
DatasetIndex::from_json(canonical_json(Json::object(before))),
DatasetIndex::from_json(canonical_json(Json::object(after))),
policy,
)
if canonical_json(Json::object(expected)) != bundle.report.to_json() {
reject(
"replay_mismatch", "/expected", "Expected report does not match these snapshots and policy; regenerate the complete bundle from the intended inputs.",
)
}
bundle
}
///|
/// Deterministic complete v1 document; creation has already checked its size.
pub fn ReviewBundle::to_json(self : ReviewBundle) -> String {
self.serialized
}
///|
pub fn ReviewBundle::before(self : ReviewBundle) -> DatasetIndex {
self.before
}
///|
pub fn ReviewBundle::after(self : ReviewBundle) -> DatasetIndex {
self.after
}
///|
/// The recomputed check, including policy-ignored changes and diagnostics.
pub fn ReviewBundle::report(self : ReviewBundle) -> ReleaseCheck {
self.report
}