diff --git a/src/v2/lens/inert_carrier.dag b/src/v2/lens/inert_carrier.dag index 6b0cdb5b09c..96b4e62ad09 100644 --- a/src/v2/lens/inert_carrier.dag +++ b/src/v2/lens/inert_carrier.dag @@ -38,6 +38,7 @@ fn inert_carrier_rostered_names() -> List { data empty_inert_carrier_names: List = [] + fn list_contains_str(xs: List, target: String) -> Bool { fold_list( xs: xs, @@ -46,30 +47,144 @@ fn list_contains_str(xs: List, target: String) -> Bool { ) } -fn count_not_in_roster(names: List, roster: List) -> Int { +// The two populations have OPPOSITE remedies, which is why they are two +// variants and not one located list. UnrosteredInertCarrier: the carrier is +// inert in the live tree and the roster does not admit it -- the repair is to +// give it a live consumer, or (if it is deliberately staged ahead of one) to +// author a roster row with a reason and a dissolution condition. +// StaleInertCarrierRow: the roster admits a carrier that is NO LONGER inert -- +// a consumer exists, so the repair is to DELETE the row. Applying either +// remedy to the other population is wrong in both directions: rostering a live +// carrier re-opens the admission the row exists to close, and deleting a row +// for an unrostered carrier removes nothing. +type InertCarrierFinding + = UnrosteredInertCarrier { carrier: String } + | StaleInertCarrierRow { carrier: String } + +type InertCarrierPopulations { + unrostered: List + stale_rows: List +} + +type InertCarrierRosterVerdict + = InertCarrierRosterClean + | InertCarrierRosterDiverged { findings: List } + +data empty_inert_carrier_findings: List = [] + + +fn names_not_in_roster(names: List, roster: List) -> List { fold_list( xs: names, - empty: 0, - cons: fn(count, name) { - if list_contains_str(xs: roster, target: name) { count } else { count + 1 } + empty: empty_inert_carrier_names, + cons: fn(acc, name) { + if list_contains_str(xs: roster, target: name) { + acc + } else { + list_snoc_item(xs: acc, item: name) + } } ) } -fn count_stale_roster(roster: List, inert_names: List) -> Int { +fn stale_roster_names(roster: List, inert_names: List) -> List { fold_list( xs: roster, - empty: 0, - cons: fn(count, entry) { - if list_contains_str(xs: inert_names, target: entry) { count } else { count + 1 } + empty: empty_inert_carrier_names, + cons: fn(acc, entry) { + if list_contains_str(xs: inert_names, target: entry) { + acc + } else { + list_snoc_item(xs: acc, item: entry) + } } ) } +fn count_not_in_roster(names: List, roster: List) -> Int { + length(xs: names_not_in_roster(names: names, roster: roster)) +} + +fn count_stale_roster(roster: List, inert_names: List) -> Int { + length(xs: stale_roster_names(roster: roster, inert_names: inert_names)) +} + +fn inert_carrier_findings(names: List, roster: List) -> List { + let unrostered = fold_list( + xs: names_not_in_roster(names: names, roster: roster), + empty: empty_inert_carrier_findings, + cons: fn(acc, name) { list_snoc_item(xs: acc, item: UnrosteredInertCarrier { carrier: name }) } + ) + fold_list( + xs: stale_roster_names(roster: roster, inert_names: names), + empty: unrostered, + cons: fn(acc, entry) { list_snoc_item(xs: acc, item: StaleInertCarrierRow { carrier: entry }) } + ) +} + +fn inert_carrier_verdict_from_findings(findings: List) -> InertCarrierRosterVerdict { + if is_empty(xs: findings) { + InertCarrierRosterClean + } else { + InertCarrierRosterDiverged { findings: findings } + } +} + +fn inert_carrier_roster_verdict(names: List, roster: List) -> InertCarrierRosterVerdict { + inert_carrier_verdict_from_findings(findings: inert_carrier_findings(names: names, roster: roster)) +} + +fn inert_carrier_live_roster_verdict() -> InertCarrierRosterVerdict { + inert_carrier_roster_verdict(names: inert_carrier_names_live(), roster: inert_carrier_rostered_names()) +} + +fn inert_carrier_verdict_findings(v: InertCarrierRosterVerdict) -> List { + match v { + InertCarrierRosterClean => empty_inert_carrier_findings + InertCarrierRosterDiverged { findings } => findings + } +} + +fn inert_carrier_verdict_is_clean(v: InertCarrierRosterVerdict) -> Bool { + is_empty(xs: inert_carrier_verdict_findings(v: v)) +} + +data empty_inert_carrier_populations: InertCarrierPopulations = InertCarrierPopulations { + unrostered: empty_inert_carrier_names, + stale_rows: empty_inert_carrier_names +} + +fn inert_carrier_population_add(acc: InertCarrierPopulations, f: InertCarrierFinding) -> InertCarrierPopulations { + match f { + UnrosteredInertCarrier { carrier } => InertCarrierPopulations { + unrostered: list_snoc_item(xs: acc.unrostered, item: carrier), + stale_rows: acc.stale_rows + } + StaleInertCarrierRow { carrier } => InertCarrierPopulations { + unrostered: acc.unrostered, + stale_rows: list_snoc_item(xs: acc.stale_rows, item: carrier) + } + } +} + +fn inert_carrier_populations(v: InertCarrierRosterVerdict) -> InertCarrierPopulations { + fold_list( + xs: inert_carrier_verdict_findings(v: v), + empty: empty_inert_carrier_populations, + cons: fn(acc, f) { inert_carrier_population_add(acc: acc, f: f) } + ) +} + +fn inert_carrier_unrostered_names(v: InertCarrierRosterVerdict) -> List { + inert_carrier_populations(v: v).unrostered +} + +fn inert_carrier_stale_row_names(v: InertCarrierRosterVerdict) -> List { + inert_carrier_populations(v: v).stale_rows +} + fn inert_carrier_clean_holds() -> Bool { - let names = inert_carrier_names_live() - count_not_in_roster(names: names, roster: inert_carrier_rostered_names()) == 0 - && count_stale_roster(roster: inert_carrier_rostered_names(), inert_names: names) == 0 + inert_carrier_verdict_is_clean(v: inert_carrier_live_roster_verdict()) } fn inert_carrier_live_count() -> Int { @@ -77,11 +192,11 @@ fn inert_carrier_live_count() -> Int { } fn inert_carrier_live_unrostered_count() -> Int { - count_not_in_roster(names: inert_carrier_names_live(), roster: inert_carrier_rostered_names()) + length(xs: inert_carrier_unrostered_names(v: inert_carrier_live_roster_verdict())) } fn inert_carrier_live_stale_roster_count() -> Int { - count_stale_roster(roster: inert_carrier_rostered_names(), inert_names: inert_carrier_names_live()) + length(xs: inert_carrier_stale_row_names(v: inert_carrier_live_roster_verdict())) } fn inert_carrier_live_declared_count() -> Int { diff --git a/src/v2/lens/inert_carrier_test.dag b/src/v2/lens/inert_carrier_test.dag index 806e851bf1b..192a904cd53 100644 --- a/src/v2/lens/inert_carrier_test.dag +++ b/src/v2/lens/inert_carrier_test.dag @@ -6,6 +6,11 @@ import v2.std.text { String } import v2.lens.inert_carrier { inert_carrier_clean_holds, inert_carrier_live_declared_count, + inert_carrier_roster_verdict, + inert_carrier_verdict_is_clean, + inert_carrier_unrostered_names, + inert_carrier_stale_row_names, + list_contains_str, count_not_in_roster, count_stale_roster, empty_inert_carrier_names @@ -16,6 +21,7 @@ data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly data roster_one: List = ["KnownEntry"] data names_with_unknown: List = ["UnknownEntry"] +data names_known_only: List = ["KnownEntry"] test fn inert_carrier_no_unrostered_or_stale() -> Bool { return inert_carrier_clean_holds() @@ -32,3 +38,35 @@ test fn count_not_in_roster_detects_unrostered() -> Bool { test fn count_stale_roster_detects_stale_entry() -> Bool { return count_stale_roster(roster: roster_one, inert_names: empty_inert_carrier_names) == 1 } + +test fn verdict_clean_when_roster_matches_live_inert_names() -> Bool { + return inert_carrier_verdict_is_clean(v: inert_carrier_roster_verdict(names: names_known_only, roster: roster_one)) +} + +test fn unrostered_population_names_the_carrier_needing_a_row() -> Bool { + let v = inert_carrier_roster_verdict(names: names_with_unknown, roster: empty_inert_carrier_names) + let unrostered = inert_carrier_unrostered_names(v: v) + return inert_carrier_verdict_is_clean(v: v) == false + && length(xs: unrostered) == 1 + && list_contains_str(xs: unrostered, target: "UnknownEntry") + && length(xs: inert_carrier_stale_row_names(v: v)) == 0 +} + +test fn stale_population_names_the_row_needing_deletion() -> Bool { + let v = inert_carrier_roster_verdict(names: empty_inert_carrier_names, roster: roster_one) + let stale = inert_carrier_stale_row_names(v: v) + return inert_carrier_verdict_is_clean(v: v) == false + && length(xs: stale) == 1 + && list_contains_str(xs: stale, target: "KnownEntry") + && length(xs: inert_carrier_unrostered_names(v: v)) == 0 +} + +test fn the_two_populations_are_reported_separately() -> Bool { + let v = inert_carrier_roster_verdict(names: names_with_unknown, roster: roster_one) + let unrostered = inert_carrier_unrostered_names(v: v) + let stale = inert_carrier_stale_row_names(v: v) + return list_contains_str(xs: unrostered, target: "UnknownEntry") + && (list_contains_str(xs: unrostered, target: "KnownEntry") == false) + && list_contains_str(xs: stale, target: "KnownEntry") + && (list_contains_str(xs: stale, target: "UnknownEntry") == false) +}