///|
/// Audit information for a group of resilient cards.
pub(all) struct ResilientDeckAudit {
deck : String
expected_cards : Int
present_cards : Int
valid_cards : Int
data_cards : Int
parity_cards : Int
missing_data : Array[Int]
missing_parity : Array[Int]
duplicate_indices : Array[Int]
checksum_failures : Array[Int]
profile_mismatches : Array[Int]
width_failures : Array[Int]
recoverable_now : Bool
summary : String
} derive(Eq, @debug.Debug)
///|
/// Options for human-readable resilient sheet rendering.
pub(all) struct SheetLayout {
columns : Int
group_size : Int
include_header : Bool
include_audit : Bool
include_checklist : Bool
} derive(Eq, @debug.Debug)
///|
/// A short estimate for manual copy work.
pub(all) struct CopyEstimate {
cards : Int
symbols : Int
groups : Int
estimated_minutes : Int
risk_level : String
} derive(Eq, @debug.Debug)
///|
/// Return a practical default layout for printed notes.
pub fn default_sheet_layout() -> SheetLayout {
{
columns: 1,
group_size: 8,
include_header: true,
include_audit: true,
include_checklist: true,
}
}
///|
/// Audit a resilient card set without attempting to repair it.
pub fn audit_resilient_deck(
cards : Array[ResilientShardCard],
) -> ResilientDeckAudit {
guard cards.length() > 0 else {
return {
deck: "",
expected_cards: 0,
present_cards: 0,
valid_cards: 0,
data_cards: 0,
parity_cards: 0,
missing_data: [0],
missing_parity: [],
duplicate_indices: [],
checksum_failures: [],
profile_mismatches: [],
width_failures: [],
recoverable_now: false,
summary: "empty card set",
}
}
let profile = cards[0]
let total = profile.data_count + profile.parity_count
let seen : Array[Bool] = []
for _ in 0..= 0 && card.index < total {
if seen[card.index] {
duplicates.push(card.index)
} else {
seen[card.index] = true
}
}
if card.index >= 0 && card.index < profile.data_count {
data_cards = data_cards + 1
} else if card.index >= profile.data_count && card.index < total {
parity_cards = parity_cards + 1
}
if valid {
valid_cards = valid_cards + 1
}
}
let missing_data : Array[Int] = []
for i in 0.. String {
let lines : Array[String] = []
lines.push("## Resilient Deck Audit")
lines.push("")
lines.push("- deck: " + audit.deck)
lines.push("- expected cards: " + audit.expected_cards.to_string())
lines.push("- present cards: " + audit.present_cards.to_string())
lines.push("- valid cards: " + audit.valid_cards.to_string())
lines.push("- data cards: " + audit.data_cards.to_string())
lines.push("- parity cards: " + audit.parity_cards.to_string())
lines.push("- missing data: " + render_ints_inline(audit.missing_data))
lines.push("- missing parity: " + render_ints_inline(audit.missing_parity))
lines.push(
"- duplicate indices: " + render_ints_inline(audit.duplicate_indices),
)
lines.push(
"- checksum failures: " + render_ints_inline(audit.checksum_failures),
)
lines.push(
"- profile mismatches: " + render_ints_inline(audit.profile_mismatches),
)
lines.push("- width failures: " + render_ints_inline(audit.width_failures))
lines.push("- recoverable now: " + audit.recoverable_now.to_string())
lines.push("")
lines.push(audit.summary)
lines.join("\n")
}
///|
/// Render resilient cards with optional audit and checklist sections.
pub fn render_resilient_sheet_with_layout(
cards : Array[ResilientShardCard],
layout : SheetLayout,
) -> String {
let clean_layout = normalize_layout(layout)
let lines : Array[String] = []
if clean_layout.include_header {
lines.push("# resilient-shard-note")
lines.push("")
}
let grouped = render_grouped_cards(cards, clean_layout.group_size)
for line in grouped {
lines.push(line)
}
if clean_layout.include_audit {
lines.push("")
lines.push(render_resilient_audit(audit_resilient_deck(cards)))
}
if clean_layout.include_checklist {
lines.push("")
lines.push(render_recovery_checklist(cards))
}
lines.join("\n")
}
///|
/// Render cards as a Markdown table for README examples and issue comments.
pub fn render_resilient_markdown_table(
cards : Array[ResilientShardCard],
) -> String {
let lines : Array[String] = [
"| index | role | checksum | cells |", "| ---: | --- | ---: | --- |",
]
for card in cards {
lines.push(
"| " +
card.index.to_string() +
" | " +
resilient_public_role(card) +
" | " +
card.checksum.to_string() +
" | `" +
hex_cells(card.cells) +
"` |",
)
}
lines.join("\n")
}
///|
/// Render a manual recovery checklist.
pub fn render_recovery_checklist(cards : Array[ResilientShardCard]) -> String {
guard cards.length() > 0 else {
return "## Recovery Checklist\n\n- [ ] Add at least one card."
}
let audit = audit_resilient_deck(cards)
let lines : Array[String] = []
lines.push("## Recovery Checklist")
lines.push("")
lines.push("- [ ] Confirm every card starts with `RSN1|`.")
lines.push("- [ ] Confirm deck id is `" + audit.deck + "` on every card.")
lines.push(
"- [ ] Confirm missing data indices are " +
render_ints_inline(audit.missing_data) +
".",
)
lines.push(
"- [ ] Confirm checksum failures are " +
render_ints_inline(audit.checksum_failures) +
".",
)
if audit.recoverable_now {
lines.push("- [ ] Run `recover_resilient` or `recover_resilient_sheet`.")
} else {
lines.push("- [ ] Collect more cards before attempting recovery.")
}
lines.join("\n")
}
///|
/// Drop cards with selected indices. This is used by examples and tests.
pub fn drop_resilient_indices(
cards : Array[ResilientShardCard],
indices : Array[Int],
) -> Array[ResilientShardCard] {
let out : Array[ResilientShardCard] = []
for card in cards {
if !indices.contains(card.index) {
out.push(card)
}
}
out
}
///|
/// Keep only selected card indices, preserving original card order.
pub fn keep_resilient_indices(
cards : Array[ResilientShardCard],
indices : Array[Int],
) -> Array[ResilientShardCard] {
let out : Array[ResilientShardCard] = []
for card in cards {
if indices.contains(card.index) {
out.push(card)
}
}
out
}
///|
/// Create a copy of a card with one cell changed.
pub fn corrupt_resilient_cell(
card : ResilientShardCard,
offset : Int,
value : Int,
) -> ResilientShardCard {
let cells : Array[Int] = []
for cell in card.cells {
cells.push(cell)
}
if offset >= 0 && offset < cells.length() {
cells[offset] = normalize_byte(value)
}
{
deck: card.deck,
index: card.index,
data_count: card.data_count,
parity_count: card.parity_count,
width: card.width,
original_len: card.original_len,
checksum: card.checksum,
cells,
}
}
///|
/// Return a corrected copy of a card after a deliberate cell edit.
pub fn rewrite_resilient_cell(
card : ResilientShardCard,
offset : Int,
value : Int,
) -> ResilientShardCard {
let cells : Array[Int] = []
for cell in card.cells {
cells.push(cell)
}
if offset >= 0 && offset < cells.length() {
cells[offset] = normalize_byte(value)
}
resilient_public_card_from_cells(card, cells)
}
///|
/// Estimate manual copy effort for a group of cards.
pub fn estimate_copy_work(cards : Array[ResilientShardCard]) -> CopyEstimate {
let mut symbols = 0
for card in cards {
symbols = symbols + card.cells.length() * 2
}
let groups = ceil_div(symbols, 8)
let estimated_minutes = max_int(1, ceil_div(groups, 12))
{
cards: cards.length(),
symbols,
groups,
estimated_minutes,
risk_level: copy_risk_level(symbols, cards.length()),
}
}
///|
/// Render copy effort as one line for CLI output.
pub fn render_copy_estimate(estimate : CopyEstimate) -> String {
"cards=" +
estimate.cards.to_string() +
" symbols=" +
estimate.symbols.to_string() +
" groups=" +
estimate.groups.to_string() +
" estimated-minutes=" +
estimate.estimated_minutes.to_string() +
" risk=" +
estimate.risk_level
}
///|
/// Return true when all visible cards share the same profile.
pub fn resilient_profiles_match(cards : Array[ResilientShardCard]) -> Bool {
guard cards.length() > 0 else { return true }
let profile = cards[0]
for card in cards {
if !same_resilient_public_profile(profile, card) {
return false
}
}
true
}
///|
/// Count visible cards by role as `[data, parity, other]`.
pub fn count_resilient_roles(cards : Array[ResilientShardCard]) -> Array[Int] {
guard cards.length() > 0 else { return [0, 0, 0] }
let profile = cards[0]
let counts = [0, 0, 0]
for card in cards {
if card.index >= 0 && card.index < profile.data_count {
counts[0] = counts[0] + 1
} else if card.index >= profile.data_count &&
card.index < profile.data_count + profile.parity_count {
counts[1] = counts[1] + 1
} else {
counts[2] = counts[2] + 1
}
}
counts
}
///|
/// Compute a lightweight payload fingerprint for logs and examples.
pub fn payload_fingerprint(payload : Array[Int]) -> String {
let mut a = 1
let mut b = 0
for value in payload {
a = (a + normalize_byte(value)) % 65521
b = (b + a) % 65521
}
let combined = (b * 257 + a) % 65521
combined.to_string()
}
///|
/// Compare two payloads and return mismatch offsets.
pub fn payload_diff_offsets(
left : Array[Int],
right : Array[Int],
) -> Array[Int] {
let out : Array[Int] = []
let max_len = max_int(left.length(), right.length())
for i in 0.. String {
let mut min_value = 255
let mut max_value = 0
let mut zero_count = 0
for value in payload {
let byte = normalize_byte(value)
if byte < min_value {
min_value = byte
}
if byte > max_value {
max_value = byte
}
if byte == 0 {
zero_count = zero_count + 1
}
}
"len=" +
payload.length().to_string() +
" min=" +
min_value.to_string() +
" max=" +
max_value.to_string() +
" zeros=" +
zero_count.to_string() +
" fp=" +
payload_fingerprint(payload)
}
///|
/// Build a deterministic payload for demos and scenario tests.
pub fn deterministic_payload(seed : Int, len : Int) -> Array[Int] {
let clean_len = max_int(0, len)
let out : Array[Int] = []
let mut state = normalize_byte(seed)
for i in 0.. Bool {
left.deck == right.deck &&
left.data_count == right.data_count &&
left.parity_count == right.parity_count &&
left.width == right.width &&
left.original_len == right.original_len
}
///|
fn resilient_public_role(card : ResilientShardCard) -> String {
if card.index < card.data_count {
"data"
} else if card.index < card.data_count + card.parity_count {
"parity"
} else {
"unknown"
}
}
///|
fn resilient_public_card_from_cells(
source : ResilientShardCard,
cells : Array[Int],
) -> ResilientShardCard {
resilient_card(
source.deck,
source.index,
source.data_count,
source.parity_count,
source.width,
source.original_len,
cells,
)
}
///|
fn normalize_layout(layout : SheetLayout) -> SheetLayout {
{
columns: clamp(layout.columns, 1, 4),
group_size: clamp(layout.group_size, 2, 32),
include_header: layout.include_header,
include_audit: layout.include_audit,
include_checklist: layout.include_checklist,
}
}
///|
fn render_grouped_cards(
cards : Array[ResilientShardCard],
group_size : Int,
) -> Array[String] {
let out : Array[String] = []
for card in cards {
out.push(group_card_line(render_resilient_card(card), group_size))
}
out
}
///|
fn group_card_line(line : String, group_size : Int) -> String {
match line.split_once("x=") {
Some((prefix, hex_view)) => {
let hex = hex_view.to_owned()
prefix.to_owned() + "x=" + group_hex(hex, group_size)
}
None => line
}
}
///|
fn group_hex(hex : String, group_size : Int) -> String {
let parts : Array[String] = []
let mut start = 0
while start < hex.length() {
let end = min_int(start + group_size, hex.length())
parts.push(hex.sub(start~, end~).to_owned())
start = end
}
parts.join(" ")
}
///|
fn audit_summary_text(
deck : String,
present : Int,
expected : Int,
missing_data : Array[Int],
missing_parity : Array[Int],
checksum_failures : Array[Int],
recoverable_now : Bool,
) -> String {
if checksum_failures.length() > 0 {
return "deck " + deck + " has checksum failures; fix copied cards first"
}
if recoverable_now {
return "deck " +
deck +
" is recoverable with " +
present.to_string() +
"/" +
expected.to_string() +
" visible cards"
}
"deck " +
deck +
" is not recoverable yet; missing data " +
render_ints_inline(missing_data) +
", missing parity " +
render_ints_inline(missing_parity)
}
///|
fn copy_risk_level(symbols : Int, cards : Int) -> String {
if cards <= 0 {
"empty"
} else if symbols <= 64 {
"low"
} else if symbols <= 256 {
"medium"
} else {
"high"
}
}