///|
pub struct OrderViolation {
previous_id : String
current_id : String
} derive(Debug, Eq)
///|
/// Evidence produced from a completed or partial page traversal.
pub struct PaginationAudit {
observed_count : Int
unique_count : Int
duplicate_ids : Array[String]
missing_ids : Array[String]
unexpected_ids : Array[String]
order_violations : Array[OrderViolation]
} derive(Debug, Eq)
///|
pub fn OrderViolation::previous_id(self : OrderViolation) -> String {
self.previous_id
}
///|
pub fn OrderViolation::current_id(self : OrderViolation) -> String {
self.current_id
}
///|
pub fn PaginationAudit::observed_count(self : PaginationAudit) -> Int {
self.observed_count
}
///|
pub fn PaginationAudit::unique_count(self : PaginationAudit) -> Int {
self.unique_count
}
///|
pub fn PaginationAudit::duplicate_ids(self : PaginationAudit) -> Array[String] {
self.duplicate_ids.copy()
}
///|
pub fn PaginationAudit::missing_ids(self : PaginationAudit) -> Array[String] {
self.missing_ids.copy()
}
///|
pub fn PaginationAudit::unexpected_ids(self : PaginationAudit) -> Array[String] {
self.unexpected_ids.copy()
}
///|
pub fn PaginationAudit::order_violations(
self : PaginationAudit,
) -> Array[OrderViolation] {
self.order_violations.copy()
}
///|
pub fn PaginationAudit::is_clean(self : PaginationAudit) -> Bool {
self.duplicate_ids.length() == 0 &&
self.missing_ids.length() == 0 &&
self.unexpected_ids.length() == 0 &&
self.order_violations.length() == 0
}
///|
fn string_in(items : Array[String], target : String) -> Bool {
for item in items {
if item == target {
return true
}
}
false
}
///|
fn push_unique_string(items : Array[String], value : String) -> Unit {
if !string_in(items, value) {
items.push(value)
}
}
///|
/// Audit a sequence collected from multiple pages against the records expected
/// at traversal start. This makes mutation-induced offset anomalies observable
/// in tests and production export verification.
pub fn audit_traversal(
observed : Array[PageRow],
expected : Array[PageRow],
sort : Array[SortField],
limits? : PageLimits = page_limits(),
) -> Result[PaginationAudit, PageError] {
let expected_sorted = match sort_rows(expected, sort, limits~) {
Ok(rows) => rows
Err(error) => return Err(error)
}
let expected_ids = expected_sorted.map(row => row.id)
let observed_ids : Array[String] = []
let duplicate_ids : Array[String] = []
let unexpected_ids : Array[String] = []
let order_violations : Array[OrderViolation] = []
for index = 0; index < observed.length(); index = index + 1 {
let row = observed[index]
if string_in(observed_ids, row.id) {
push_unique_string(duplicate_ids, row.id)
} else {
observed_ids.push(row.id)
}
if !string_in(expected_ids, row.id) {
push_unique_string(unexpected_ids, row.id)
}
if index > 0 {
let compared = match compare_rows(observed[index - 1], row, sort) {
Ok(value) => value
Err(error) => return Err(error)
}
if compared >= 0 {
order_violations.push({
previous_id: observed[index - 1].id,
current_id: row.id,
})
}
}
}
let missing_ids : Array[String] = []
for id in expected_ids {
if !string_in(observed_ids, id) {
missing_ids.push(id)
}
}
Ok({
observed_count: observed.length(),
unique_count: observed_ids.length(),
duplicate_ids,
missing_ids,
unexpected_ids,
order_violations,
})
}
///|
pub fn format_audit(audit : PaginationAudit) -> String {
let status = if audit.is_clean() { "PASS" } else { "FAIL" }
[
"status=" + status,
"observed=" + audit.observed_count.to_string(),
"unique=" + audit.unique_count.to_string(),
"duplicates=" + audit.duplicate_ids.join(","),
"missing=" + audit.missing_ids.join(","),
"unexpected=" + audit.unexpected_ids.join(","),
"order_violations=" + audit.order_violations.length().to_string(),
].join(" ")
}