From 60f7a945c013c38adc2b1c06752729bd7929d26e Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 11 Sep 2026 21:20:52 +0000 Subject: [PATCH 01/13] Add a caller-supplied derived-payload erasure so lawful deletion is expressible without treating ListingGone as a delete. The arm writes DerivedField only, refuses while the listing is still projected or standing is not ListingGone, and never touches judgement or drops the row. Co-authored-by: Cursor --- dag/gunbc/review_sheet_projection.dag | 228 +++++++++++++++--- .../review_sheet_converge_witness_test.dag | 153 ++++++++++++ 2 files changed, 348 insertions(+), 33 deletions(-) diff --git a/dag/gunbc/review_sheet_projection.dag b/dag/gunbc/review_sheet_projection.dag index 7e67f1932ad..9a11bf9a24a 100644 --- a/dag/gunbc/review_sheet_projection.dag +++ b/dag/gunbc/review_sheet_projection.dag @@ -46,6 +46,21 @@ type ReviewSheetProjection { rows: List } +// Derived payload erasure is caller-supplied, never implicit in ListingGone: a listing +// ending and a person exercising a right are unrelated facts. It writes DerivedField only, +// never OperatorEntered, never a silent row drop; standing stays ListingGone. Located +// writes or a located refuse; quiet success is refused. Judgement has no deletion arm. +// identity_column in the intended binding is an eBay item id: a stub that keeps that key +// still retains eBay data under a broad MAD reading. Identity split is deferred until +// exemption wording puts the item id in scope. +// Blanking DerivedField is a real meaning loss for human re-audit: title and price are why +// they wrote buy, and a labelled stub does not carry that. The buyer loop still has +// identity plus the operator cell and can refuse to re-ask. We gave something up; we did +// not never have it. +type DerivedPayloadErasure { + listing_id: NonEmptyStr +} + type ObservedSheetRow { data_index: Int cells: List @@ -319,6 +334,69 @@ fn observed_identity_key_rows(sheet: ObservedSheet, identity_index: Int) -> List |> map(raw => KeyedRow { row_key: raw, value: true }) } +fn erasure_key_rows(erasures: List) -> List> { + map(erasures, e => KeyedRow { row_key: e.listing_id as String, value: e }) +} + +fn listing_has_erasure(erasure_by_id: Map, listing_id: NonEmptyStr) -> Bool { + match erasure_by_id.lookup(listing_id as String) { + Present { value: _ } => true + Absent => false + } +} + +fn column_is_listing_standing(column: ReviewColumn) -> Bool { + match column.kind { + ListingStanding => true + OperatorEntered => false + DerivedField { source_field: _ } => false + } +} + +fn read_row_listing_standing( + schema: ReviewSheetSchema, + header_by_name: Map, + row: ObservedSheetRow, +) -> ListingStandingRead? { + match schema.columns |> filter(c => column_is_listing_standing(column: c)) |> first { + Absent => none + Present { value: col } => + match header_by_name.lookup(col.name as String) { + Absent => none + Present { value: idx } => Present { value: read_listing_standing(cell: cell_at(cells: row.cells, index: idx)) } + } + } +} + +fn erasure_still_projected_reason(listing_id: NonEmptyStr) -> NonEmptyStr { + concat("derived payload erasure refused while listing still in projection listing=", listing_id as String) as NonEmptyStr +} + +fn erasure_requires_gone_reason(listing_id: NonEmptyStr) -> NonEmptyStr { + concat("derived payload erasure requires ListingGone listing=", listing_id as String) as NonEmptyStr +} + +fn erasure_not_on_sheet_reason(listing_id: NonEmptyStr) -> NonEmptyStr { + concat("derived payload erasure listing not on sheet listing=", listing_id as String) as NonEmptyStr +} + +fn erasure_missing_standing_column_reason(listing_id: NonEmptyStr) -> NonEmptyStr { + concat("derived payload erasure missing ListingStanding column listing=", listing_id as String) as NonEmptyStr +} + +fn first_erasure_not_on_sheet( + erasures: List, + observed_by_id: Map, +) -> NonEmptyStr? { + erasures + |> filter(e => match observed_by_id.lookup(e.listing_id as String) { + Present { value: _ } => false + Absent => true + }) + |> map(e => e.listing_id) + |> first +} + fn derived_by_column(row: ProjectionRow) -> Map { fold(row.derived, init: empty_map(), f: fn(acc, cell) { map_insert(acc, cell.column as String, cell.value) @@ -439,6 +517,14 @@ fn decide_derived_field_cell(observed: String, desired: String) -> CellDecision } } +fn decide_derived_erasure_cell(observed: String) -> CellDecision { + if observed == "" { + CellSkip + } else { + CellWrite { value: "" } + } +} + fn classification_from_cell_decision( plan: ReviewSheetWritePlan, listing_id: NonEmptyStr, @@ -490,6 +576,7 @@ fn classify_existing_row( row: ObservedSheetRow, rows_by_listing: Map, identity_index: Int, + erasure_by_id: Map, plan: ReviewSheetWritePlan, ) -> ReviewSheetClassification { let listing_raw = cell_at(cells: row.cells, index: identity_index) @@ -512,6 +599,7 @@ fn classify_existing_row( listing_id: listing_id, in_projection: true, derived: derived_by_column(row: prow), + erasure_by_id: erasure_by_id, plan: plan, ) Absent => @@ -522,6 +610,7 @@ fn classify_existing_row( listing_id: listing_id, in_projection: false, derived: empty_map(), + erasure_by_id: erasure_by_id, plan: plan, ) } @@ -535,6 +624,7 @@ fn fold_columns_for_row( listing_id: NonEmptyStr, in_projection: Bool, derived: Map, + erasure_by_id: Map, plan: ReviewSheetWritePlan, ) -> ReviewSheetClassification { fold(schema.columns, init: ReviewSheetNeedsWrite { plan: plan }, f: fn(acc, col) { @@ -562,31 +652,64 @@ fn fold_columns_for_row( ) OperatorEntered => ReviewSheetNeedsWrite { plan: p } DerivedField { source_field: _ } => - if in_projection { - match derived.lookup(col.name as String) { - Absent => - ReviewSheetUnreadable { - reason: omitted_derived_column_reason(listing_id: listing_id, column: col.name) - } - Present { value: desired } => - classification_from_cell_decision( - plan: p, - listing_id: listing_id, - column: col.name, - column_index: idx, - data_index: row.data_index, - decision: decide_derived_field_cell(observed: observed, desired: desired), - ) + if listing_has_erasure(erasure_by_id: erasure_by_id, listing_id: listing_id) { + if in_projection { + ReviewSheetUnreadable { reason: erasure_still_projected_reason(listing_id: listing_id) } + } else { + match read_row_listing_standing(schema: schema, header_by_name: header_by_name, row: row) { + Absent => + ReviewSheetUnreadable { + reason: erasure_missing_standing_column_reason(listing_id: listing_id) + } + Present { value: standing_read } => + match standing_read { + StandingKnown { state: standing_state } => + if listing_standing_eq(a: standing_state, b: StandingGone) { + classification_from_cell_decision( + plan: p, + listing_id: listing_id, + column: col.name, + column_index: idx, + data_index: row.data_index, + decision: decide_derived_erasure_cell(observed: observed), + ) + } else { + ReviewSheetUnreadable { reason: erasure_requires_gone_reason(listing_id: listing_id) } + } + StandingBlank => + ReviewSheetUnreadable { reason: erasure_requires_gone_reason(listing_id: listing_id) } + StandingForeign { raw: _ } => + ReviewSheetUnreadable { reason: erasure_requires_gone_reason(listing_id: listing_id) } + } + } } } else { - classification_from_cell_decision( - plan: p, - listing_id: listing_id, - column: col.name, - column_index: idx, - data_index: row.data_index, - decision: decide_derived_field_cell(observed: observed, desired: observed), - ) + if in_projection { + match derived.lookup(col.name as String) { + Absent => + ReviewSheetUnreadable { + reason: omitted_derived_column_reason(listing_id: listing_id, column: col.name) + } + Present { value: desired } => + classification_from_cell_decision( + plan: p, + listing_id: listing_id, + column: col.name, + column_index: idx, + data_index: row.data_index, + decision: decide_derived_field_cell(observed: observed, desired: desired), + ) + } + } else { + classification_from_cell_decision( + plan: p, + listing_id: listing_id, + column: col.name, + column_index: idx, + data_index: row.data_index, + decision: decide_derived_field_cell(observed: observed, desired: observed), + ) + } } } } @@ -855,6 +978,14 @@ fn append_listing_row( } fn classify_review_sheet(observed: ObservedSheet, projection: ReviewSheetProjection) -> ReviewSheetClassification { + classify_review_sheet_with_erasure(observed: observed, projection: projection, erasures: []) +} + +fn classify_review_sheet_with_erasure( + observed: ObservedSheet, + projection: ReviewSheetProjection, + erasures: List, +) -> ReviewSheetClassification { match keyed_roster_build(incomings: header_key_rows(headers: observed.headers), key_eq: review_sheet_key_eq) { KeyedRosterBuildDuplicateKey { key: d, first: _, duplicate: _ } => ReviewSheetUnreadable { reason: concat("duplicate header ", d) as NonEmptyStr } @@ -880,14 +1011,26 @@ fn classify_review_sheet(observed: ObservedSheet, projection: ReviewSheetProject ) { KeyedRosterBuildDuplicateKey { key: d, first: _, duplicate: _ } => ReviewSheetUnreadable { reason: concat("duplicate sheet listing ", d) as NonEmptyStr } - KeyedRosterBuilt { rows: obs_rows } => finish_classify( - observed: observed, - projection: projection, - header_by_name: header_by_name, - identity_index: identity_index, - rows_by_listing: keyed_string_map(rows: proj_rows), - observed_by_id: keyed_string_map(rows: obs_rows), - ) + KeyedRosterBuilt { rows: obs_rows } => + match keyed_roster_build( + incomings: erasure_key_rows(erasures: erasures), + key_eq: review_sheet_key_eq, + ) { + KeyedRosterBuildDuplicateKey { key: d, first: _, duplicate: _ } => + ReviewSheetUnreadable { + reason: concat("duplicate derived payload erasure listing ", d) as NonEmptyStr + } + KeyedRosterBuilt { rows: erasure_rows } => finish_classify( + observed: observed, + projection: projection, + header_by_name: header_by_name, + identity_index: identity_index, + rows_by_listing: keyed_string_map(rows: proj_rows), + observed_by_id: keyed_string_map(rows: obs_rows), + erasures: erasures, + erasure_by_id: keyed_string_map(rows: erasure_rows), + ) + } } } } @@ -903,6 +1046,8 @@ fn finish_classify( identity_index: Int, rows_by_listing: Map, observed_by_id: Map, + erasures: List, + erasure_by_id: Map, ) -> ReviewSheetClassification { let after_existing = fold(observed.rows, init: ReviewSheetNeedsWrite { plan: empty_write_plan() }, f: fn(acc, row) { match acc { @@ -916,6 +1061,7 @@ fn finish_classify( row: row, rows_by_listing: rows_by_listing, identity_index: identity_index, + erasure_by_id: erasure_by_id, plan: p, ) } @@ -924,7 +1070,11 @@ fn finish_classify( ReviewSheetConflict { conflict: c } => ReviewSheetConflict { conflict: c } ReviewSheetUnreadable { reason: r } => ReviewSheetUnreadable { reason: r } ReviewSheetConverged => ReviewSheetConverged - ReviewSheetNeedsWrite { plan: p } => { + ReviewSheetNeedsWrite { plan: p } => + match first_erasure_not_on_sheet(erasures: erasures, observed_by_id: observed_by_id) { + Present { value: missing } => + ReviewSheetUnreadable { reason: erasure_not_on_sheet_reason(listing_id: missing) } + Absent => { let start_row = (observed.rows |> count) + 2 let column_by_name = columns_by_name(schema: projection.schema) let missing = projection.rows |> filter(r => match observed_by_id.lookup(r.listing_id as String) { @@ -966,7 +1116,8 @@ fn finish_classify( ReviewSheetUnreadable { reason: r } => ReviewSheetUnreadable { reason: r } ReviewSheetConverged => ReviewSheetConverged } - } + } + } } } @@ -1053,3 +1204,14 @@ data buyer_review_judgement_read_frontier: Disposition = Scaffold { field: WholeDeclaration } } + +// Live ensure still calls classify_review_sheet (empty erasures). A named later change binds +// classify_review_sheet_with_erasure when a caller supplies DerivedPayloadErasure rows. +data derived_payload_erasure_frontier: Disposition = Scaffold { + dissolves_to: RealizationDispatch, + bind: DeclarationRef { + module_path: "gunbc.review_sheet_projection", + decl_name: "classify_review_sheet_with_erasure", + field: WholeDeclaration + } +} diff --git a/dag/test/claim/review_sheet_converge_witness_test.dag b/dag/test/claim/review_sheet_converge_witness_test.dag index a01d666c638..240d2540ff4 100644 --- a/dag/test/claim/review_sheet_converge_witness_test.dag +++ b/dag/test/claim/review_sheet_converge_witness_test.dag @@ -26,6 +26,9 @@ import gunbc.review_sheet_projection { write_plan_covers_column_letter, a1_row_span_includes_letter, classify_review_sheet, + classify_review_sheet_with_erasure, + DerivedPayloadErasure, + derived_payload_erasure_frontier, conflict_reason, append_listing_row, columns_by_name, @@ -185,6 +188,14 @@ fn w_standing_observed() -> String { listing_standing_label(state: StandingObserved) } +fn w_standing_gone() -> String { + listing_standing_label(state: StandingGone) +} + +fn w_erasure(listing_id: NonEmptyStr) -> DerivedPayloadErasure { + DerivedPayloadErasure { listing_id: listing_id } +} + fn w_value_range(rows: List>) -> ValueRange { ValueRange { range: "Review!A1:D", @@ -347,6 +358,140 @@ test fn listing_gone_with_judgement_is_retained_and_marked() -> Bool { } } +test fn derived_payload_erasure_blanks_derived_keeps_judgement_and_listing_gone() -> Bool { + let projection = w_projection(rows: []) + let observed = w_observed( + headers: w_headers(), + rows: [["L1", "alpha", w_standing_gone(), "buy"]], + ) + match classify_review_sheet_with_erasure( + observed: observed, + projection: projection, + erasures: [w_erasure(listing_id: "L1" as NonEmptyStr)], + ) { + ReviewSheetNeedsWrite { plan: p } => + write_plan_preserves_operator_columns(schema: w_schema(), plan: p) + && !write_plan_touches_column(plan: p, column: "judgement" as NonEmptyStr) + && !write_plan_touches_column(plan: p, column: "standing" as NonEmptyStr) + && (p.appends |> count) == 0 + && match judgement_for_listing( + observed: observed, + identity_column: "listing_id" as NonEmptyStr, + judgement_column: "judgement" as NonEmptyStr, + listing_id: "L1" as NonEmptyStr, + ) { + JudgementEntered { value: v } => (v as String) == "buy" + JudgementBlank => false + JudgementUnreadable { reason: _ } => false + } + && fold(p.patches, init: false, f: fn(acc, w) { + acc || (w.listing_id == ("L1" as NonEmptyStr) + && w.column == ("title" as NonEmptyStr) + && w.value == "") + }) + ReviewSheetConverged => false + ReviewSheetConflict { conflict: _ } => false + ReviewSheetUnreadable { reason: _ } => false + } +} + +test fn derived_payload_erasure_refuses_while_listing_still_projected() -> Bool { + let projection = w_projection(rows: [w_row(listing_id: "L1" as NonEmptyStr, title: "alpha")]) + let observed = w_observed( + headers: w_headers(), + rows: [["L1", "alpha", w_standing_observed(), "buy"]], + ) + match classify_review_sheet_with_erasure( + observed: observed, + projection: projection, + erasures: [w_erasure(listing_id: "L1" as NonEmptyStr)], + ) { + ReviewSheetUnreadable { reason: r } => + string_contains(s: r as String, pattern: "still in projection") + && string_contains(s: r as String, pattern: "listing=L1") + ReviewSheetNeedsWrite { plan: _ } => false + ReviewSheetConverged => false + ReviewSheetConflict { conflict: _ } => false + } +} + +test fn derived_payload_erasure_refuses_until_standing_is_listing_gone() -> Bool { + let projection = w_projection(rows: []) + let observed = w_observed( + headers: w_headers(), + rows: [["L1", "alpha", w_standing_observed(), "buy"]], + ) + match classify_review_sheet_with_erasure( + observed: observed, + projection: projection, + erasures: [w_erasure(listing_id: "L1" as NonEmptyStr)], + ) { + ReviewSheetUnreadable { reason: r } => + string_contains(s: r as String, pattern: "requires ListingGone") + && string_contains(s: r as String, pattern: "listing=L1") + ReviewSheetNeedsWrite { plan: _ } => false + ReviewSheetConverged => false + ReviewSheetConflict { conflict: _ } => false + } +} + +test fn derived_payload_erasure_refuses_listing_not_on_sheet() -> Bool { + let projection = w_projection(rows: []) + let observed = w_observed(headers: w_headers(), rows: []) + match classify_review_sheet_with_erasure( + observed: observed, + projection: projection, + erasures: [w_erasure(listing_id: "L1" as NonEmptyStr)], + ) { + ReviewSheetUnreadable { reason: r } => + string_contains(s: r as String, pattern: "not on sheet") + && string_contains(s: r as String, pattern: "listing=L1") + ReviewSheetNeedsWrite { plan: _ } => false + ReviewSheetConverged => false + ReviewSheetConflict { conflict: _ } => false + } +} + +test fn derived_payload_erasure_already_empty_derived_is_converged() -> Bool { + let projection = w_projection(rows: []) + let observed = w_observed( + headers: w_headers(), + rows: [["L1", "", w_standing_gone(), "buy"]], + ) + match classify_review_sheet_with_erasure( + observed: observed, + projection: projection, + erasures: [w_erasure(listing_id: "L1" as NonEmptyStr)], + ) { + ReviewSheetConverged => true + ReviewSheetNeedsWrite { plan: _ } => false + ReviewSheetConflict { conflict: _ } => false + ReviewSheetUnreadable { reason: _ } => false + } +} + +test fn derived_payload_erasure_duplicate_listing_is_unreadable() -> Bool { + let projection = w_projection(rows: []) + let observed = w_observed( + headers: w_headers(), + rows: [["L1", "alpha", w_standing_gone(), "buy"]], + ) + match classify_review_sheet_with_erasure( + observed: observed, + projection: projection, + erasures: [ + w_erasure(listing_id: "L1" as NonEmptyStr), + w_erasure(listing_id: "L1" as NonEmptyStr), + ], + ) { + ReviewSheetUnreadable { reason: r } => + string_contains(s: r as String, pattern: "duplicate derived payload erasure listing L1") + ReviewSheetNeedsWrite { plan: _ } => false + ReviewSheetConverged => false + ReviewSheetConflict { conflict: _ } => false + } +} + test fn readback_apply_qualifies_once_on_the_value_range_fold() -> Bool { let projection = w_projection(rows: [w_row(listing_id: "L1" as NonEmptyStr, title: "Widget")]) let range = w_value_range(rows: [["L1", "", w_standing_observed(), ""]]) @@ -889,6 +1034,14 @@ test fn live_transport_frontiers_are_scaffold_not_terminal() -> Bool { Scaffold { dissolves_to: SubstrateMandatoryTag, bind: _ } => false Terminal { reason: _ } => false } + && match derived_payload_erasure_frontier { + Scaffold { dissolves_to: RealizationDispatch, bind: b } => + (b.module_path as String) == "gunbc.review_sheet_projection" + && (b.decl_name as String) == "classify_review_sheet_with_erasure" + Scaffold { dissolves_to: SingleAuthority, bind: _ } => false + Scaffold { dissolves_to: SubstrateMandatoryTag, bind: _ } => false + Terminal { reason: _ } => false + } && sheet_a1_range_wire( range: review_sheet_get_values_request( tab: "Review" as NonEmptyStr, From 99d246a54d70965fa9d574225b7ec253add03b59 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 11 Sep 2026 21:48:32 +0000 Subject: [PATCH 02/13] Authorize derived-payload erasure once per listing, and drop the circular scaffold. Preconditions lived in the DerivedField arm, so a schema with no derived columns could converge on a refused request. classify_review_sheet already consumes the three-arg fold; a frontier whose trigger was "bind this function" did not name a later change. Co-authored-by: Cursor --- dag/gunbc/review_sheet_projection.dag | 140 ++++++++++++------ .../review_sheet_converge_witness_test.dag | 62 ++++++-- 2 files changed, 148 insertions(+), 54 deletions(-) diff --git a/dag/gunbc/review_sheet_projection.dag b/dag/gunbc/review_sheet_projection.dag index 9a11bf9a24a..a14a9d213ba 100644 --- a/dag/gunbc/review_sheet_projection.dag +++ b/dag/gunbc/review_sheet_projection.dag @@ -61,6 +61,11 @@ type DerivedPayloadErasure { listing_id: NonEmptyStr } +type DerivedPayloadErasureDecision + = ErasureNotRequested + | ErasureAuthorized + | ErasureRefused { reason: NonEmptyStr } + type ObservedSheetRow { data_index: Int cells: List @@ -397,6 +402,40 @@ fn first_erasure_not_on_sheet( |> first } +fn authorize_derived_payload_erasure( + listing_id: NonEmptyStr, + in_projection: Bool, + schema: ReviewSheetSchema, + header_by_name: Map, + row: ObservedSheetRow, + erasure_by_id: Map, +) -> DerivedPayloadErasureDecision { + if listing_has_erasure(erasure_by_id: erasure_by_id, listing_id: listing_id) { + if in_projection { + ErasureRefused { reason: erasure_still_projected_reason(listing_id: listing_id) } + } else { + match read_row_listing_standing(schema: schema, header_by_name: header_by_name, row: row) { + Absent => ErasureRefused { reason: erasure_missing_standing_column_reason(listing_id: listing_id) } + Present { value: standing_read } => + match standing_read { + StandingKnown { state: standing_state } => + if listing_standing_eq(a: standing_state, b: StandingGone) { + ErasureAuthorized + } else { + ErasureRefused { reason: erasure_requires_gone_reason(listing_id: listing_id) } + } + StandingBlank => + ErasureRefused { reason: erasure_requires_gone_reason(listing_id: listing_id) } + StandingForeign { raw: _ } => + ErasureRefused { reason: erasure_requires_gone_reason(listing_id: listing_id) } + } + } + } + } else { + ErasureNotRequested + } +} + fn derived_by_column(row: ProjectionRow) -> Map { fold(row.derived, init: empty_map(), f: fn(acc, cell) { map_insert(acc, cell.column as String, cell.value) @@ -592,7 +631,7 @@ fn classify_existing_row( let listing_id = listing_raw as NonEmptyStr match rows_by_listing.lookup(listing_raw) { Present { value: prow } => - fold_columns_for_row( + classify_existing_row_after_erasure_auth( schema: schema, header_by_name: header_by_name, row: row, @@ -603,7 +642,7 @@ fn classify_existing_row( plan: plan, ) Absent => - fold_columns_for_row( + classify_existing_row_after_erasure_auth( schema: schema, header_by_name: header_by_name, row: row, @@ -617,7 +656,7 @@ fn classify_existing_row( } } -fn fold_columns_for_row( +fn classify_existing_row_after_erasure_auth( schema: ReviewSheetSchema, header_by_name: Map, row: ObservedSheetRow, @@ -626,6 +665,50 @@ fn fold_columns_for_row( derived: Map, erasure_by_id: Map, plan: ReviewSheetWritePlan, +) -> ReviewSheetClassification { + match authorize_derived_payload_erasure( + listing_id: listing_id, + in_projection: in_projection, + schema: schema, + header_by_name: header_by_name, + row: row, + erasure_by_id: erasure_by_id, + ) { + ErasureRefused { reason: r } => ReviewSheetUnreadable { reason: r } + ErasureAuthorized => + fold_columns_for_row( + schema: schema, + header_by_name: header_by_name, + row: row, + listing_id: listing_id, + in_projection: in_projection, + derived: derived, + erase_derived: true, + plan: plan, + ) + ErasureNotRequested => + fold_columns_for_row( + schema: schema, + header_by_name: header_by_name, + row: row, + listing_id: listing_id, + in_projection: in_projection, + derived: derived, + erase_derived: false, + plan: plan, + ) + } +} + +fn fold_columns_for_row( + schema: ReviewSheetSchema, + header_by_name: Map, + row: ObservedSheetRow, + listing_id: NonEmptyStr, + in_projection: Bool, + derived: Map, + erase_derived: Bool, + plan: ReviewSheetWritePlan, ) -> ReviewSheetClassification { fold(schema.columns, init: ReviewSheetNeedsWrite { plan: plan }, f: fn(acc, col) { match acc { @@ -652,37 +735,15 @@ fn fold_columns_for_row( ) OperatorEntered => ReviewSheetNeedsWrite { plan: p } DerivedField { source_field: _ } => - if listing_has_erasure(erasure_by_id: erasure_by_id, listing_id: listing_id) { - if in_projection { - ReviewSheetUnreadable { reason: erasure_still_projected_reason(listing_id: listing_id) } - } else { - match read_row_listing_standing(schema: schema, header_by_name: header_by_name, row: row) { - Absent => - ReviewSheetUnreadable { - reason: erasure_missing_standing_column_reason(listing_id: listing_id) - } - Present { value: standing_read } => - match standing_read { - StandingKnown { state: standing_state } => - if listing_standing_eq(a: standing_state, b: StandingGone) { - classification_from_cell_decision( - plan: p, - listing_id: listing_id, - column: col.name, - column_index: idx, - data_index: row.data_index, - decision: decide_derived_erasure_cell(observed: observed), - ) - } else { - ReviewSheetUnreadable { reason: erasure_requires_gone_reason(listing_id: listing_id) } - } - StandingBlank => - ReviewSheetUnreadable { reason: erasure_requires_gone_reason(listing_id: listing_id) } - StandingForeign { raw: _ } => - ReviewSheetUnreadable { reason: erasure_requires_gone_reason(listing_id: listing_id) } - } - } - } + if erase_derived { + classification_from_cell_decision( + plan: p, + listing_id: listing_id, + column: col.name, + column_index: idx, + data_index: row.data_index, + decision: decide_derived_erasure_cell(observed: observed), + ) } else { if in_projection { match derived.lookup(col.name as String) { @@ -1204,14 +1265,3 @@ data buyer_review_judgement_read_frontier: Disposition = Scaffold { field: WholeDeclaration } } - -// Live ensure still calls classify_review_sheet (empty erasures). A named later change binds -// classify_review_sheet_with_erasure when a caller supplies DerivedPayloadErasure rows. -data derived_payload_erasure_frontier: Disposition = Scaffold { - dissolves_to: RealizationDispatch, - bind: DeclarationRef { - module_path: "gunbc.review_sheet_projection", - decl_name: "classify_review_sheet_with_erasure", - field: WholeDeclaration - } -} diff --git a/dag/test/claim/review_sheet_converge_witness_test.dag b/dag/test/claim/review_sheet_converge_witness_test.dag index 240d2540ff4..8c95accabc7 100644 --- a/dag/test/claim/review_sheet_converge_witness_test.dag +++ b/dag/test/claim/review_sheet_converge_witness_test.dag @@ -28,7 +28,6 @@ import gunbc.review_sheet_projection { classify_review_sheet, classify_review_sheet_with_erasure, DerivedPayloadErasure, - derived_payload_erasure_frontier, conflict_reason, append_listing_row, columns_by_name, @@ -162,6 +161,16 @@ fn w_schema() -> ReviewSheetSchema { } } +fn w_schema_no_derived() -> ReviewSheetSchema { + ReviewSheetSchema { + identity_column: "listing_id" as NonEmptyStr, + columns: [ + ReviewColumn { name: "standing" as NonEmptyStr, kind: ListingStanding }, + ReviewColumn { name: "judgement" as NonEmptyStr, kind: OperatorEntered }, + ] + } +} + fn w_projection(rows: List) -> ReviewSheetProjection { ReviewSheetProjection { schema: w_schema(), rows: rows } } @@ -435,6 +444,49 @@ test fn derived_payload_erasure_refuses_until_standing_is_listing_gone() -> Bool } } +test fn derived_payload_erasure_refuses_without_derived_columns_when_standing_is_not_gone() -> Bool { + let projection = ReviewSheetProjection { schema: w_schema_no_derived(), rows: [] } + let observed = w_observed( + headers: ["listing_id", "standing", "judgement"], + rows: [["L1", w_standing_observed(), "buy"]], + ) + match classify_review_sheet_with_erasure( + observed: observed, + projection: projection, + erasures: [w_erasure(listing_id: "L1" as NonEmptyStr)], + ) { + ReviewSheetUnreadable { reason: r } => + string_contains(s: r as String, pattern: "requires ListingGone") + && string_contains(s: r as String, pattern: "listing=L1") + ReviewSheetNeedsWrite { plan: _ } => false + ReviewSheetConverged => false + ReviewSheetConflict { conflict: _ } => false + } +} + +test fn derived_payload_erasure_refuses_without_derived_columns_while_still_projected() -> Bool { + let projection = ReviewSheetProjection { + schema: w_schema_no_derived(), + rows: [ProjectionRow { listing_id: "L1" as NonEmptyStr, derived: [] }], + } + let observed = w_observed( + headers: ["listing_id", "standing", "judgement"], + rows: [["L1", w_standing_observed(), "buy"]], + ) + match classify_review_sheet_with_erasure( + observed: observed, + projection: projection, + erasures: [w_erasure(listing_id: "L1" as NonEmptyStr)], + ) { + ReviewSheetUnreadable { reason: r } => + string_contains(s: r as String, pattern: "still in projection") + && string_contains(s: r as String, pattern: "listing=L1") + ReviewSheetNeedsWrite { plan: _ } => false + ReviewSheetConverged => false + ReviewSheetConflict { conflict: _ } => false + } +} + test fn derived_payload_erasure_refuses_listing_not_on_sheet() -> Bool { let projection = w_projection(rows: []) let observed = w_observed(headers: w_headers(), rows: []) @@ -1034,14 +1086,6 @@ test fn live_transport_frontiers_are_scaffold_not_terminal() -> Bool { Scaffold { dissolves_to: SubstrateMandatoryTag, bind: _ } => false Terminal { reason: _ } => false } - && match derived_payload_erasure_frontier { - Scaffold { dissolves_to: RealizationDispatch, bind: b } => - (b.module_path as String) == "gunbc.review_sheet_projection" - && (b.decl_name as String) == "classify_review_sheet_with_erasure" - Scaffold { dissolves_to: SingleAuthority, bind: _ } => false - Scaffold { dissolves_to: SubstrateMandatoryTag, bind: _ } => false - Terminal { reason: _ } => false - } && sheet_a1_range_wire( range: review_sheet_get_values_request( tab: "Review" as NonEmptyStr, From ee8a8fa90471d3b023e556a843af043fc0099068 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 11 Sep 2026 23:21:39 +0000 Subject: [PATCH 03/13] Thread caller-supplied erasures through review_sheet_ensure and locate refuses at the identity cell. The door was only reachable from tests and a zero-erasure wrapper. Ensure is the operator path; empty erasures remain the no-erasure pass. Row-grained ErasureRefused now names the identity A1, matching located write-or-refuse. Co-authored-by: Cursor --- dag/gunbc/review_sheet_projection.dag | 30 +++++++- dag/gunbc/review_sheet_sheets.dag | 17 +++-- .../review_sheet_converge_witness_test.dag | 69 ++++++++++++++++--- 3 files changed, 100 insertions(+), 16 deletions(-) diff --git a/dag/gunbc/review_sheet_projection.dag b/dag/gunbc/review_sheet_projection.dag index a14a9d213ba..c99450dfe78 100644 --- a/dag/gunbc/review_sheet_projection.dag +++ b/dag/gunbc/review_sheet_projection.dag @@ -436,6 +436,26 @@ fn authorize_derived_payload_erasure( } } +fn erasure_refuse_at_identity_cell( + identity_index: Int, + data_index: Int, + reason: NonEmptyStr, +) -> ReviewSheetClassification { + match a1_cell(column_index: identity_index, sheet_row: sheet_row_number(data_index: data_index)) { + Absent => + ReviewSheetUnreadable { + reason: concat( + reason as String, + concat(" at unrepresentable A1 column index ", to_string(identity_index)), + ) as NonEmptyStr + } + Present { value: a1 } => + ReviewSheetUnreadable { + reason: concat(reason as String, concat(" cell=", a1_cell_wire(cell: a1))) as NonEmptyStr + } + } +} + fn derived_by_column(row: ProjectionRow) -> Map { fold(row.derived, init: empty_map(), f: fn(acc, cell) { map_insert(acc, cell.column as String, cell.value) @@ -636,6 +656,7 @@ fn classify_existing_row( header_by_name: header_by_name, row: row, listing_id: listing_id, + identity_index: identity_index, in_projection: true, derived: derived_by_column(row: prow), erasure_by_id: erasure_by_id, @@ -647,6 +668,7 @@ fn classify_existing_row( header_by_name: header_by_name, row: row, listing_id: listing_id, + identity_index: identity_index, in_projection: false, derived: empty_map(), erasure_by_id: erasure_by_id, @@ -661,6 +683,7 @@ fn classify_existing_row_after_erasure_auth( header_by_name: Map, row: ObservedSheetRow, listing_id: NonEmptyStr, + identity_index: Int, in_projection: Bool, derived: Map, erasure_by_id: Map, @@ -674,7 +697,12 @@ fn classify_existing_row_after_erasure_auth( row: row, erasure_by_id: erasure_by_id, ) { - ErasureRefused { reason: r } => ReviewSheetUnreadable { reason: r } + ErasureRefused { reason: r } => + erasure_refuse_at_identity_cell( + identity_index: identity_index, + data_index: row.data_index, + reason: r, + ) ErasureAuthorized => fold_columns_for_row( schema: schema, diff --git a/dag/gunbc/review_sheet_sheets.dag b/dag/gunbc/review_sheet_sheets.dag index 2293dcf4fa6..f365ef5ad28 100644 --- a/dag/gunbc/review_sheet_sheets.dag +++ b/dag/gunbc/review_sheet_sheets.dag @@ -23,7 +23,8 @@ import gunbc.review_sheet_projection { ReviewSheetUnreadable, DerivedCellWrite, AppendListingRow, - classify_review_sheet, + classify_review_sheet_with_erasure, + DerivedPayloadErasure, conflict_reason, a1_cell_eq, a1_cell_wire, @@ -385,6 +386,7 @@ fn review_sheet_ensure( requested: SheetA1Range, range: ValueRange, projection: ReviewSheetProjection, + erasures: List, ) -> UpsertDecision { match requested.end { A1EndClosed { cell: _ } => @@ -413,7 +415,11 @@ fn review_sheet_ensure( ) as NonEmptyStr } } else { - let classified = classify_review_sheet(observed: observed, projection: projection) + let classified = classify_review_sheet_with_erasure( + observed: observed, + projection: projection, + erasures: erasures, + ) ensure_decide( policy: policy, observation: EnsureObserved { evidence: observed }, @@ -438,9 +444,10 @@ fn review_sheet_ensure( // sunny-carp-475 binds a live sheets.Spreadsheets GetValues and BatchUpdateValues adapter onto // review_sheet_ensure (policy is the caller's EnsurePolicy, never a literal in this module). -// That same adapter is the named later consumer of sheets_spreadsheets_oauth_scope (writes) -// and sheets_spreadsheets_readonly_oauth_scope (get-only). sheets_api_base is already the -// service endpoint. Quota numbers are enrolled via sheets_rate_limits, not a citation grep. +// review_sheet_ensure takes caller-supplied DerivedPayloadErasure rows; empty means no erasure +// this pass. That same adapter is the named later consumer of sheets_spreadsheets_oauth_scope +// (writes) and sheets_spreadsheets_readonly_oauth_scope (get-only). sheets_api_base is already +// the service endpoint. Quota numbers are enrolled via sheets_rate_limits, not a citation grep. data live_sheets_transport_frontier: Disposition = Scaffold { dissolves_to: RealizationDispatch, bind: DeclarationRef { diff --git a/dag/test/claim/review_sheet_converge_witness_test.dag b/dag/test/claim/review_sheet_converge_witness_test.dag index 8c95accabc7..1f9b66ee2e7 100644 --- a/dag/test/claim/review_sheet_converge_witness_test.dag +++ b/dag/test/claim/review_sheet_converge_witness_test.dag @@ -418,6 +418,7 @@ test fn derived_payload_erasure_refuses_while_listing_still_projected() -> Bool ReviewSheetUnreadable { reason: r } => string_contains(s: r as String, pattern: "still in projection") && string_contains(s: r as String, pattern: "listing=L1") + && string_contains(s: r as String, pattern: "cell=A2") ReviewSheetNeedsWrite { plan: _ } => false ReviewSheetConverged => false ReviewSheetConflict { conflict: _ } => false @@ -438,6 +439,7 @@ test fn derived_payload_erasure_refuses_until_standing_is_listing_gone() -> Bool ReviewSheetUnreadable { reason: r } => string_contains(s: r as String, pattern: "requires ListingGone") && string_contains(s: r as String, pattern: "listing=L1") + && string_contains(s: r as String, pattern: "cell=A2") ReviewSheetNeedsWrite { plan: _ } => false ReviewSheetConverged => false ReviewSheetConflict { conflict: _ } => false @@ -458,6 +460,7 @@ test fn derived_payload_erasure_refuses_without_derived_columns_when_standing_is ReviewSheetUnreadable { reason: r } => string_contains(s: r as String, pattern: "requires ListingGone") && string_contains(s: r as String, pattern: "listing=L1") + && string_contains(s: r as String, pattern: "cell=A2") ReviewSheetNeedsWrite { plan: _ } => false ReviewSheetConverged => false ReviewSheetConflict { conflict: _ } => false @@ -481,6 +484,7 @@ test fn derived_payload_erasure_refuses_without_derived_columns_while_still_proj ReviewSheetUnreadable { reason: r } => string_contains(s: r as String, pattern: "still in projection") && string_contains(s: r as String, pattern: "listing=L1") + && string_contains(s: r as String, pattern: "cell=A2") ReviewSheetNeedsWrite { plan: _ } => false ReviewSheetConverged => false ReviewSheetConflict { conflict: _ } => false @@ -544,10 +548,52 @@ test fn derived_payload_erasure_duplicate_listing_is_unreadable() -> Bool { } } +test fn ensure_with_erasure_applies_blank_derived() -> Bool { + let projection = w_projection(rows: []) + let range = w_value_range(rows: [["L1", "alpha", w_standing_gone(), "buy"]]) + match review_sheet_ensure( + policy: EnsureMayApply, + requested: w_open_request(tab: "Review" as NonEmptyStr), + range: range, + projection: projection, + erasures: [w_erasure(listing_id: "L1" as NonEmptyStr)], + ) { + Apply { plan: bound } => + write_plan_preserves_operator_columns(schema: w_schema(), plan: bound.plan) + && !write_plan_touches_column(plan: bound.plan, column: "judgement" as NonEmptyStr) + && fold(bound.plan.patches, init: false, f: fn(acc, w) { + acc || (w.listing_id == ("L1" as NonEmptyStr) + && w.column == ("title" as NonEmptyStr) + && w.value == "") + }) + Noop => false + Refuse { reason: _ } => false + } +} + +test fn ensure_with_erasure_refuses_while_still_projected() -> Bool { + let projection = w_projection(rows: [w_row(listing_id: "L1" as NonEmptyStr, title: "alpha")]) + let range = w_value_range(rows: [["L1", "alpha", w_standing_observed(), "buy"]]) + match review_sheet_ensure( + policy: EnsureMayApply, + requested: w_open_request(tab: "Review" as NonEmptyStr), + range: range, + projection: projection, + erasures: [w_erasure(listing_id: "L1" as NonEmptyStr)], + ) { + Refuse { reason: r } => + string_contains(s: r as String, pattern: "still in projection") + && string_contains(s: r as String, pattern: "listing=L1") + && string_contains(s: r as String, pattern: "cell=A2") + Noop => false + Apply { plan: _ } => false + } +} + test fn readback_apply_qualifies_once_on_the_value_range_fold() -> Bool { let projection = w_projection(rows: [w_row(listing_id: "L1" as NonEmptyStr, title: "Widget")]) let range = w_value_range(rows: [["L1", "", w_standing_observed(), ""]]) - match review_sheet_ensure(policy: EnsureMayApply, requested: w_open_request(tab: "Review" as NonEmptyStr), range: range, projection: projection) { + match review_sheet_ensure(policy: EnsureMayApply, requested: w_open_request(tab: "Review" as NonEmptyStr), range: range, projection: projection, erasures: []) { Apply { plan: bound } => (bound.tab as String) == "Review" && match bound.plan.patches |> first { @@ -574,7 +620,7 @@ test fn ensure_write_ranges_use_the_readback_tab() -> Bool { majorDimension: Present { value: "ROWS" }, values: Present { value: concat([w_headers()], [["L1", "", w_standing_observed(), ""]]) }, } - match review_sheet_ensure(policy: EnsureMayApply, requested: w_open_request(tab: "OtherTab" as NonEmptyStr), range: range, projection: projection) { + match review_sheet_ensure(policy: EnsureMayApply, requested: w_open_request(tab: "OtherTab" as NonEmptyStr), range: range, projection: projection, erasures: []) { Apply { plan: bound } => (bound.tab as String) == "OtherTab" && match value_ranges_from_write_plan(bound: bound) |> first { @@ -591,9 +637,9 @@ test fn ensure_write_ranges_use_the_readback_tab() -> Bool { test fn ensure_over_unchanged_readback_is_noop() -> Bool { let projection = w_projection(rows: [w_row(listing_id: "L1" as NonEmptyStr, title: "alpha")]) let range = w_value_range(rows: [["L1", "alpha", w_standing_observed(), "buy"]]) - match review_sheet_ensure(policy: EnsureMayApply, requested: w_open_request(tab: "Review" as NonEmptyStr), range: range, projection: projection) { + match review_sheet_ensure(policy: EnsureMayApply, requested: w_open_request(tab: "Review" as NonEmptyStr), range: range, projection: projection, erasures: []) { Noop => - match review_sheet_ensure(policy: EnsureObserveOnly, requested: w_open_request(tab: "Review" as NonEmptyStr), range: range, projection: projection) { + match review_sheet_ensure(policy: EnsureObserveOnly, requested: w_open_request(tab: "Review" as NonEmptyStr), range: range, projection: projection, erasures: []) { Noop => true Apply { plan: _ } => false Refuse { reason: _ } => false @@ -606,7 +652,7 @@ test fn ensure_over_unchanged_readback_is_noop() -> Bool { test fn observe_only_does_not_emit_a_write_plan() -> Bool { let projection = w_projection(rows: [w_row(listing_id: "L1" as NonEmptyStr, title: "Widget")]) let range = w_value_range(rows: [["L1", "", w_standing_observed(), ""]]) - match review_sheet_ensure(policy: EnsureObserveOnly, requested: w_open_request(tab: "Review" as NonEmptyStr), range: range, projection: projection) { + match review_sheet_ensure(policy: EnsureObserveOnly, requested: w_open_request(tab: "Review" as NonEmptyStr), range: range, projection: projection, erasures: []) { Refuse { reason: r } => string_contains(s: r as String, pattern: "derived region differs") Noop => false Apply { plan: _ } => false @@ -620,7 +666,7 @@ test fn column_major_readback_refuses() -> Bool { majorDimension: Present { value: "COLUMNS" }, values: Present { value: concat([w_headers()], [["L1", "alpha", w_standing_observed(), ""]]) }, } - match review_sheet_ensure(policy: EnsureMayApply, requested: w_open_request(tab: "Review" as NonEmptyStr), range: range, projection: projection) { + match review_sheet_ensure(policy: EnsureMayApply, requested: w_open_request(tab: "Review" as NonEmptyStr), range: range, projection: projection, erasures: []) { Refuse { reason: r } => string_contains(s: r as String, pattern: "column-major") Noop => false Apply { plan: _ } => false @@ -634,7 +680,7 @@ test fn foreign_major_dimension_refuses() -> Bool { majorDimension: Present { value: "DIAGONAL" }, values: Present { value: concat([w_headers()], [["L1", "alpha", w_standing_observed(), ""]]) }, } - match review_sheet_ensure(policy: EnsureMayApply, requested: w_open_request(tab: "Review" as NonEmptyStr), range: range, projection: projection) { + match review_sheet_ensure(policy: EnsureMayApply, requested: w_open_request(tab: "Review" as NonEmptyStr), range: range, projection: projection, erasures: []) { Refuse { reason: r } => string_contains(s: r as String, pattern: "foreign majorDimension") Noop => false Apply { plan: _ } => false @@ -654,7 +700,7 @@ test fn value_range_readback_is_identity_keyed_not_positional() -> Bool { match observed_read { ReadbackUnreadable { reason: _ } => false ReadbackObserved { observed: observed, tab: _ } => - match review_sheet_ensure(policy: EnsureMayApply, requested: w_open_request(tab: "Review" as NonEmptyStr), range: range, projection: projection) { + match review_sheet_ensure(policy: EnsureMayApply, requested: w_open_request(tab: "Review" as NonEmptyStr), range: range, projection: projection, erasures: []) { Noop => match judgement_for_listing( observed: observed, @@ -679,7 +725,7 @@ test fn readback_not_a1_anchored_refuses() -> Bool { majorDimension: Present { value: "ROWS" }, values: Present { value: concat([w_headers()], [["L1", "alpha", w_standing_observed(), ""]]) }, } - match review_sheet_ensure(policy: EnsureMayApply, requested: w_open_request(tab: "Review" as NonEmptyStr), range: range, projection: projection) { + match review_sheet_ensure(policy: EnsureMayApply, requested: w_open_request(tab: "Review" as NonEmptyStr), range: range, projection: projection, erasures: []) { Refuse { reason: r } => string_contains(s: r as String, pattern: "not A1-anchored") Noop => false Apply { plan: _ } => false @@ -693,7 +739,7 @@ test fn unqualified_readback_range_refuses() -> Bool { majorDimension: Present { value: "ROWS" }, values: Present { value: concat([w_headers()], [["L1", "alpha", w_standing_observed(), ""]]) }, } - match review_sheet_ensure(policy: EnsureMayApply, requested: w_open_request(tab: "Review" as NonEmptyStr), range: range, projection: projection) { + match review_sheet_ensure(policy: EnsureMayApply, requested: w_open_request(tab: "Review" as NonEmptyStr), range: range, projection: projection, erasures: []) { Refuse { reason: r } => string_contains(s: r as String, pattern: "unqualified A1 range") Noop => false Apply { plan: _ } => false @@ -717,6 +763,7 @@ test fn bounded_get_values_request_refuses() -> Bool { }, range: range, projection: projection, + erasures: [], ) { Refuse { reason: r } => string_contains(s: r as String, pattern: "bounded GetValues request") @@ -740,6 +787,7 @@ test fn column_bounded_get_values_request_refuses() -> Bool { }, range: range, projection: projection, + erasures: [], ) { Refuse { reason: r } => string_contains(s: r as String, pattern: "column-bounded GetValues request") @@ -763,6 +811,7 @@ test fn grid_resolved_response_range_is_not_a_truncated_request() -> Bool { ), range: range, projection: projection, + erasures: [], ) { Apply { plan: bound } => (bound.tab as String) == "Review" From 1c73dadbc7576a2a9e58ca1e9fc9e957a29b0372 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 12 Sep 2026 03:45:19 +0000 Subject: [PATCH 04/13] Refuse to restore derived payload once ListingGone and empty derived cells record an erasure. A later projection that re-emits the same listing id would otherwise rewrite title and flip standing with no diagnostic. Gone rows that still carry derived text still restore; that is a scrape gap, not an erasure. Co-authored-by: Cursor --- dag/gunbc/review_sheet_projection.dag | 86 ++++++++++++++++--- .../review_sheet_converge_witness_test.dag | 41 +++++++++ 2 files changed, 117 insertions(+), 10 deletions(-) diff --git a/dag/gunbc/review_sheet_projection.dag b/dag/gunbc/review_sheet_projection.dag index c99450dfe78..cb9099ffd54 100644 --- a/dag/gunbc/review_sheet_projection.dag +++ b/dag/gunbc/review_sheet_projection.dag @@ -57,6 +57,9 @@ type ReviewSheetProjection { // they wrote buy, and a labelled stub does not carry that. The buyer loop still has // identity plus the operator cell and can refuse to re-ask. We gave something up; we did // not never have it. +// Durability: ListingGone plus empty DerivedField cells is the record that an erasure +// occurred. A later projection that would rewrite those cells or flip standing back to +// Observed refuses; it does not restore the payload the caller asked to have erased. type DerivedPayloadErasure { listing_id: NonEmptyStr } @@ -456,6 +459,56 @@ fn erasure_refuse_at_identity_cell( } } +fn column_is_derived_field(column: ReviewColumn) -> Bool { + match column.kind { + DerivedField { source_field: _ } => true + OperatorEntered => false + ListingStanding => false + } +} + +fn derived_field_cells_are_empty( + schema: ReviewSheetSchema, + header_by_name: Map, + row: ObservedSheetRow, +) -> Bool { + let derived_cols = schema.columns |> filter(c => column_is_derived_field(column: c)) + if (derived_cols |> count) == 0 { + false + } else { + all(derived_cols, c => match header_by_name.lookup(c.name as String) { + Absent => false + Present { value: idx } => cell_at(cells: row.cells, index: idx) == "" + }) + } +} + +fn row_holds_derived_payload_erasure( + schema: ReviewSheetSchema, + header_by_name: Map, + row: ObservedSheetRow, +) -> Bool { + match read_row_listing_standing(schema: schema, header_by_name: header_by_name, row: row) { + Absent => false + Present { value: standing_read } => + match standing_read { + StandingKnown { state: standing_state } => + listing_standing_eq(a: standing_state, b: StandingGone) + && derived_field_cells_are_empty( + schema: schema, + header_by_name: header_by_name, + row: row, + ) + StandingBlank => false + StandingForeign { raw: _ } => false + } + } +} + +fn restore_after_erasure_reason(listing_id: NonEmptyStr) -> NonEmptyStr { + concat("cannot restore derived payload after erasure listing=", listing_id as String) as NonEmptyStr +} + fn derived_by_column(row: ProjectionRow) -> Map { fold(row.derived, init: empty_map(), f: fn(acc, cell) { map_insert(acc, cell.column as String, cell.value) @@ -715,16 +768,29 @@ fn classify_existing_row_after_erasure_auth( plan: plan, ) ErasureNotRequested => - fold_columns_for_row( - schema: schema, - header_by_name: header_by_name, - row: row, - listing_id: listing_id, - in_projection: in_projection, - derived: derived, - erase_derived: false, - plan: plan, - ) + if in_projection + && row_holds_derived_payload_erasure( + schema: schema, + header_by_name: header_by_name, + row: row, + ) { + erasure_refuse_at_identity_cell( + identity_index: identity_index, + data_index: row.data_index, + reason: restore_after_erasure_reason(listing_id: listing_id), + ) + } else { + fold_columns_for_row( + schema: schema, + header_by_name: header_by_name, + row: row, + listing_id: listing_id, + in_projection: in_projection, + derived: derived, + erase_derived: false, + plan: plan, + ) + } } } diff --git a/dag/test/claim/review_sheet_converge_witness_test.dag b/dag/test/claim/review_sheet_converge_witness_test.dag index 1f9b66ee2e7..17f082c1b6c 100644 --- a/dag/test/claim/review_sheet_converge_witness_test.dag +++ b/dag/test/claim/review_sheet_converge_witness_test.dag @@ -590,6 +590,47 @@ test fn ensure_with_erasure_refuses_while_still_projected() -> Bool { } } +test fn erased_derived_is_not_restored_when_the_listing_returns_to_the_projection() -> Bool { + let projection = w_projection(rows: [w_row(listing_id: "L1" as NonEmptyStr, title: "alpha")]) + let observed = w_observed( + headers: w_headers(), + rows: [["L1", "", w_standing_gone(), "buy"]], + ) + match classify_review_sheet_with_erasure( + observed: observed, + projection: projection, + erasures: [], + ) { + ReviewSheetUnreadable { reason: r } => + string_contains(s: r as String, pattern: "cannot restore derived payload after erasure") + && string_contains(s: r as String, pattern: "listing=L1") + && string_contains(s: r as String, pattern: "cell=A2") + ReviewSheetNeedsWrite { plan: _ } => false + ReviewSheetConverged => false + ReviewSheetConflict { conflict: _ } => false + } +} + +test fn gone_listing_with_derived_payload_still_restores_when_it_returns() -> Bool { + let projection = w_projection(rows: [w_row(listing_id: "L1" as NonEmptyStr, title: "alpha")]) + let observed = w_observed( + headers: w_headers(), + rows: [["L1", "alpha", w_standing_gone(), "buy"]], + ) + match classify_review_sheet(observed: observed, projection: projection) { + ReviewSheetNeedsWrite { plan: p } => + write_plan_preserves_operator_columns(schema: w_schema(), plan: p) + && fold(p.patches, init: false, f: fn(acc, w) { + acc || (w.listing_id == ("L1" as NonEmptyStr) + && w.column == ("standing" as NonEmptyStr) + && w.value == listing_standing_label(state: StandingObserved)) + }) + ReviewSheetConverged => false + ReviewSheetConflict { conflict: _ } => false + ReviewSheetUnreadable { reason: _ } => false + } +} + test fn readback_apply_qualifies_once_on_the_value_range_fold() -> Bool { let projection = w_projection(rows: [w_row(listing_id: "L1" as NonEmptyStr, title: "Widget")]) let range = w_value_range(rows: [["L1", "", w_standing_observed(), ""]]) From eb96c54d906e15891b8a8474af7c1b7640e483fd Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 12 Sep 2026 04:53:03 +0000 Subject: [PATCH 05/13] Refuse erasure when the schema has no DerivedField, and stop inferring durability. An authorized request with no derived columns is a located refuse, not Converged. Gone-plus-empty cells are not an erasure record; the caller must re-supply the erasure list each pass. Co-authored-by: Cursor --- dag/gunbc/review_sheet_projection.dag | 112 ++++++------------ .../review_sheet_converge_witness_test.dag | 42 +++---- 2 files changed, 56 insertions(+), 98 deletions(-) diff --git a/dag/gunbc/review_sheet_projection.dag b/dag/gunbc/review_sheet_projection.dag index cb9099ffd54..8525584674a 100644 --- a/dag/gunbc/review_sheet_projection.dag +++ b/dag/gunbc/review_sheet_projection.dag @@ -57,9 +57,11 @@ type ReviewSheetProjection { // they wrote buy, and a labelled stub does not carry that. The buyer loop still has // identity plus the operator cell and can refuse to re-ask. We gave something up; we did // not never have it. -// Durability: ListingGone plus empty DerivedField cells is the record that an erasure -// occurred. A later projection that would rewrite those cells or flip standing back to -// Observed refuses; it does not restore the payload the caller asked to have erased. +// Durability is the caller re-supplying DerivedPayloadErasure each pass. ListingGone +// plus empty DerivedField cells is not a record: a gone listing whose derived payload +// was legitimately empty looks the same. MAD against restore holds only while the +// erasure list still names the listing (still-projected refuse). An empty erasure list +// does not infer a prior erasure. type DerivedPayloadErasure { listing_id: NonEmptyStr } @@ -392,6 +394,21 @@ fn erasure_missing_standing_column_reason(listing_id: NonEmptyStr) -> NonEmptySt concat("derived payload erasure missing ListingStanding column listing=", listing_id as String) as NonEmptyStr } +fn erasure_has_no_derived_field_reason(listing_id: NonEmptyStr) -> NonEmptyStr { + concat("derived payload erasure has no DerivedField column listing=", listing_id as String) as NonEmptyStr +} + +fn schema_has_derived_field(schema: ReviewSheetSchema) -> Bool { + match schema.columns |> filter(c => match c.kind { + DerivedField { source_field: _ } => true + OperatorEntered => false + ListingStanding => false + }) |> first { + Present { value: _ } => true + Absent => false + } +} + fn first_erasure_not_on_sheet( erasures: List, observed_by_id: Map, @@ -423,7 +440,11 @@ fn authorize_derived_payload_erasure( match standing_read { StandingKnown { state: standing_state } => if listing_standing_eq(a: standing_state, b: StandingGone) { - ErasureAuthorized + if schema_has_derived_field(schema: schema) { + ErasureAuthorized + } else { + ErasureRefused { reason: erasure_has_no_derived_field_reason(listing_id: listing_id) } + } } else { ErasureRefused { reason: erasure_requires_gone_reason(listing_id: listing_id) } } @@ -459,56 +480,6 @@ fn erasure_refuse_at_identity_cell( } } -fn column_is_derived_field(column: ReviewColumn) -> Bool { - match column.kind { - DerivedField { source_field: _ } => true - OperatorEntered => false - ListingStanding => false - } -} - -fn derived_field_cells_are_empty( - schema: ReviewSheetSchema, - header_by_name: Map, - row: ObservedSheetRow, -) -> Bool { - let derived_cols = schema.columns |> filter(c => column_is_derived_field(column: c)) - if (derived_cols |> count) == 0 { - false - } else { - all(derived_cols, c => match header_by_name.lookup(c.name as String) { - Absent => false - Present { value: idx } => cell_at(cells: row.cells, index: idx) == "" - }) - } -} - -fn row_holds_derived_payload_erasure( - schema: ReviewSheetSchema, - header_by_name: Map, - row: ObservedSheetRow, -) -> Bool { - match read_row_listing_standing(schema: schema, header_by_name: header_by_name, row: row) { - Absent => false - Present { value: standing_read } => - match standing_read { - StandingKnown { state: standing_state } => - listing_standing_eq(a: standing_state, b: StandingGone) - && derived_field_cells_are_empty( - schema: schema, - header_by_name: header_by_name, - row: row, - ) - StandingBlank => false - StandingForeign { raw: _ } => false - } - } -} - -fn restore_after_erasure_reason(listing_id: NonEmptyStr) -> NonEmptyStr { - concat("cannot restore derived payload after erasure listing=", listing_id as String) as NonEmptyStr -} - fn derived_by_column(row: ProjectionRow) -> Map { fold(row.derived, init: empty_map(), f: fn(acc, cell) { map_insert(acc, cell.column as String, cell.value) @@ -768,29 +739,16 @@ fn classify_existing_row_after_erasure_auth( plan: plan, ) ErasureNotRequested => - if in_projection - && row_holds_derived_payload_erasure( - schema: schema, - header_by_name: header_by_name, - row: row, - ) { - erasure_refuse_at_identity_cell( - identity_index: identity_index, - data_index: row.data_index, - reason: restore_after_erasure_reason(listing_id: listing_id), - ) - } else { - fold_columns_for_row( - schema: schema, - header_by_name: header_by_name, - row: row, - listing_id: listing_id, - in_projection: in_projection, - derived: derived, - erase_derived: false, - plan: plan, - ) - } + fold_columns_for_row( + schema: schema, + header_by_name: header_by_name, + row: row, + listing_id: listing_id, + in_projection: in_projection, + derived: derived, + erase_derived: false, + plan: plan, + ) } } diff --git a/dag/test/claim/review_sheet_converge_witness_test.dag b/dag/test/claim/review_sheet_converge_witness_test.dag index 17f082c1b6c..27435f1d46e 100644 --- a/dag/test/claim/review_sheet_converge_witness_test.dag +++ b/dag/test/claim/review_sheet_converge_witness_test.dag @@ -491,6 +491,27 @@ test fn derived_payload_erasure_refuses_without_derived_columns_while_still_proj } } +test fn derived_payload_erasure_refuses_without_derived_columns_when_gone_and_authorized_otherwise() -> Bool { + let projection = ReviewSheetProjection { schema: w_schema_no_derived(), rows: [] } + let observed = w_observed( + headers: ["listing_id", "standing", "judgement"], + rows: [["L1", w_standing_gone(), "buy"]], + ) + match classify_review_sheet_with_erasure( + observed: observed, + projection: projection, + erasures: [w_erasure(listing_id: "L1" as NonEmptyStr)], + ) { + ReviewSheetUnreadable { reason: r } => + string_contains(s: r as String, pattern: "has no DerivedField column") + && string_contains(s: r as String, pattern: "listing=L1") + && string_contains(s: r as String, pattern: "cell=A2") + ReviewSheetNeedsWrite { plan: _ } => false + ReviewSheetConverged => false + ReviewSheetConflict { conflict: _ } => false + } +} + test fn derived_payload_erasure_refuses_listing_not_on_sheet() -> Bool { let projection = w_projection(rows: []) let observed = w_observed(headers: w_headers(), rows: []) @@ -590,27 +611,6 @@ test fn ensure_with_erasure_refuses_while_still_projected() -> Bool { } } -test fn erased_derived_is_not_restored_when_the_listing_returns_to_the_projection() -> Bool { - let projection = w_projection(rows: [w_row(listing_id: "L1" as NonEmptyStr, title: "alpha")]) - let observed = w_observed( - headers: w_headers(), - rows: [["L1", "", w_standing_gone(), "buy"]], - ) - match classify_review_sheet_with_erasure( - observed: observed, - projection: projection, - erasures: [], - ) { - ReviewSheetUnreadable { reason: r } => - string_contains(s: r as String, pattern: "cannot restore derived payload after erasure") - && string_contains(s: r as String, pattern: "listing=L1") - && string_contains(s: r as String, pattern: "cell=A2") - ReviewSheetNeedsWrite { plan: _ } => false - ReviewSheetConverged => false - ReviewSheetConflict { conflict: _ } => false - } -} - test fn gone_listing_with_derived_payload_still_restores_when_it_returns() -> Bool { let projection = w_projection(rows: [w_row(listing_id: "L1" as NonEmptyStr, title: "alpha")]) let observed = w_observed( From 464d749a777cce4b9d262903155ec683daf759a9 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 12 Sep 2026 05:50:06 +0000 Subject: [PATCH 06/13] Record applied derived-payload erasure in a mark cell the next pass can read. Empty derived cells are not an erasure record. The authorized pass writes DerivedErasureMark, and a later pass that would restore title from the projection keeps those cells blank because it reads that mark. Co-authored-by: Cursor --- dag/gunbc/review_sheet_projection.dag | 181 ++++++++++++++++-- .../review_sheet_converge_witness_test.dag | 99 +++++++++- 2 files changed, 256 insertions(+), 24 deletions(-) diff --git a/dag/gunbc/review_sheet_projection.dag b/dag/gunbc/review_sheet_projection.dag index 8525584674a..6dd96a8ae22 100644 --- a/dag/gunbc/review_sheet_projection.dag +++ b/dag/gunbc/review_sheet_projection.dag @@ -20,6 +20,7 @@ type ReviewColumnKind = DerivedField { source_field: NonEmptyStr } | OperatorEntered | ListingStanding + | DerivedErasureMark type ReviewColumn { name: NonEmptyStr @@ -47,9 +48,10 @@ type ReviewSheetProjection { } // Derived payload erasure is caller-supplied, never implicit in ListingGone: a listing -// ending and a person exercising a right are unrelated facts. It writes DerivedField only, -// never OperatorEntered, never a silent row drop; standing stays ListingGone. Located -// writes or a located refuse; quiet success is refused. Judgement has no deletion arm. +// ending and a person exercising a right are unrelated facts. It writes DerivedField only +// plus the DerivedErasureMark cell, never OperatorEntered, never a silent row drop; +// standing stays ListingGone on the apply pass. Located writes or a located refuse; +// quiet success is refused. Judgement has no deletion arm. // identity_column in the intended binding is an eBay item id: a stub that keeps that key // still retains eBay data under a broad MAD reading. Identity split is deferred until // exemption wording puts the item id in scope. @@ -57,11 +59,9 @@ type ReviewSheetProjection { // they wrote buy, and a labelled stub does not carry that. The buyer loop still has // identity plus the operator cell and can refuse to re-ask. We gave something up; we did // not never have it. -// Durability is the caller re-supplying DerivedPayloadErasure each pass. ListingGone -// plus empty DerivedField cells is not a record: a gone listing whose derived payload -// was legitimately empty looks the same. MAD against restore holds only while the -// erasure list still names the listing (still-projected refuse). An empty erasure list -// does not infer a prior erasure. +// Durability is the DerivedErasureMark cell written on the authorized pass. A later pass +// that would restore derived payload reads that mark, not empty DerivedField cells and +// not a comment telling the caller to re-supply the list. type DerivedPayloadErasure { listing_id: NonEmptyStr } @@ -176,6 +176,47 @@ fn read_listing_standing(cell: String) -> ListingStandingRead { } } +type DerivedErasureMarkState + = ErasureUnmarked + | ErasureApplied + +type DerivedErasureMarkRead + = ErasureMarkKnown { state: DerivedErasureMarkState } + | ErasureMarkBlank + | ErasureMarkForeign { raw: String } + +fn derived_erasure_mark_label(state: DerivedErasureMarkState) -> String { + match state { + ErasureUnmarked => "" + ErasureApplied => "ErasureApplied" + } +} + +fn derived_erasure_mark_eq(a: DerivedErasureMarkState, b: DerivedErasureMarkState) -> Bool { + match a { + ErasureUnmarked => match b { + ErasureUnmarked => true + ErasureApplied => false + } + ErasureApplied => match b { + ErasureApplied => true + ErasureUnmarked => false + } + } +} + +fn read_derived_erasure_mark(cell: String) -> DerivedErasureMarkRead { + if cell == "" { + ErasureMarkBlank + } else { + if cell == derived_erasure_mark_label(state: ErasureApplied) { + ErasureMarkKnown { state: ErasureApplied } + } else { + ErasureMarkForeign { raw: cell } + } + } +} + data a1_column_letters: List = [ "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" @@ -360,6 +401,16 @@ fn column_is_listing_standing(column: ReviewColumn) -> Bool { ListingStanding => true OperatorEntered => false DerivedField { source_field: _ } => false + DerivedErasureMark => false + } +} + +fn column_is_erasure_mark(column: ReviewColumn) -> Bool { + match column.kind { + DerivedErasureMark => true + ListingStanding => false + OperatorEntered => false + DerivedField { source_field: _ } => false } } @@ -378,6 +429,38 @@ fn read_row_listing_standing( } } +fn read_row_erasure_mark( + schema: ReviewSheetSchema, + header_by_name: Map, + row: ObservedSheetRow, +) -> DerivedErasureMarkRead? { + match schema.columns |> filter(c => column_is_erasure_mark(column: c)) |> first { + Absent => none + Present { value: col } => + match header_by_name.lookup(col.name as String) { + Absent => none + Present { value: idx } => Present { value: read_derived_erasure_mark(cell: cell_at(cells: row.cells, index: idx)) } + } + } +} + +fn row_erasure_mark_is_applied( + schema: ReviewSheetSchema, + header_by_name: Map, + row: ObservedSheetRow, +) -> Bool { + match read_row_erasure_mark(schema: schema, header_by_name: header_by_name, row: row) { + Absent => false + Present { value: mark_read } => + match mark_read { + ErasureMarkKnown { state: mark_state } => + derived_erasure_mark_eq(a: mark_state, b: ErasureApplied) + ErasureMarkBlank => false + ErasureMarkForeign { raw: _ } => false + } + } +} + fn erasure_still_projected_reason(listing_id: NonEmptyStr) -> NonEmptyStr { concat("derived payload erasure refused while listing still in projection listing=", listing_id as String) as NonEmptyStr } @@ -398,17 +481,29 @@ fn erasure_has_no_derived_field_reason(listing_id: NonEmptyStr) -> NonEmptyStr { concat("derived payload erasure has no DerivedField column listing=", listing_id as String) as NonEmptyStr } +fn erasure_has_no_erasure_mark_reason(listing_id: NonEmptyStr) -> NonEmptyStr { + concat("derived payload erasure has no DerivedErasureMark column listing=", listing_id as String) as NonEmptyStr +} + fn schema_has_derived_field(schema: ReviewSheetSchema) -> Bool { match schema.columns |> filter(c => match c.kind { DerivedField { source_field: _ } => true OperatorEntered => false ListingStanding => false + DerivedErasureMark => false }) |> first { Present { value: _ } => true Absent => false } } +fn schema_has_erasure_mark(schema: ReviewSheetSchema) -> Bool { + match schema.columns |> filter(c => column_is_erasure_mark(column: c)) |> first { + Present { value: _ } => true + Absent => false + } +} + fn first_erasure_not_on_sheet( erasures: List, observed_by_id: Map, @@ -441,7 +536,11 @@ fn authorize_derived_payload_erasure( StandingKnown { state: standing_state } => if listing_standing_eq(a: standing_state, b: StandingGone) { if schema_has_derived_field(schema: schema) { - ErasureAuthorized + if schema_has_erasure_mark(schema: schema) { + ErasureAuthorized + } else { + ErasureRefused { reason: erasure_has_no_erasure_mark_reason(listing_id: listing_id) } + } } else { ErasureRefused { reason: erasure_has_no_derived_field_reason(listing_id: listing_id) } } @@ -501,6 +600,7 @@ fn first_omitted_derived_column(row: ProjectionRow, schema: ReviewSheetSchema) - DerivedField { source_field: _ } => true OperatorEntered => false ListingStanding => false + DerivedErasureMark => false }) |> filter(c => match derived.lookup(c.name as String) { Absent => true @@ -552,7 +652,7 @@ fn write_plan_touches_column(plan: ReviewSheetWritePlan, column: NonEmptyStr) -> } fn operator_column_names(schema: ReviewSheetSchema) -> List { - schema.columns |> filter(c => match c.kind { OperatorEntered => true DerivedField { source_field: _ } => false ListingStanding => false }) |> map(c => c.name) + schema.columns |> filter(c => match c.kind { OperatorEntered => true DerivedField { source_field: _ } => false ListingStanding => false DerivedErasureMark => false }) |> map(c => c.name) } fn write_plan_covers_column_letter(plan: ReviewSheetWritePlan, letter: String) -> Bool { @@ -608,6 +708,41 @@ fn decide_derived_erasure_cell(observed: String) -> CellDecision { } } +fn decide_erasure_mark_cell(observed: String, desired: DerivedErasureMarkState) -> CellDecision { + match read_derived_erasure_mark(cell: observed) { + ErasureMarkForeign { raw: _ } => CellConflict + ErasureMarkBlank => + if derived_erasure_mark_eq(a: desired, b: ErasureUnmarked) { + CellSkip + } else { + CellWrite { value: derived_erasure_mark_label(state: desired) } + } + ErasureMarkKnown { state: observed_state } => + if derived_erasure_mark_eq(a: observed_state, b: desired) { + CellSkip + } else { + CellWrite { value: derived_erasure_mark_label(state: desired) } + } + } +} + +fn desired_erasure_mark(erase_derived: Bool, observed: String) -> DerivedErasureMarkState { + if erase_derived { + ErasureApplied + } else { + match read_derived_erasure_mark(cell: observed) { + ErasureMarkKnown { state: mark_state } => + if derived_erasure_mark_eq(a: mark_state, b: ErasureApplied) { + ErasureApplied + } else { + ErasureUnmarked + } + ErasureMarkBlank => ErasureUnmarked + ErasureMarkForeign { raw: _ } => ErasureUnmarked + } + } +} + fn classification_from_cell_decision( plan: ReviewSheetWritePlan, listing_id: NonEmptyStr, @@ -746,7 +881,11 @@ fn classify_existing_row_after_erasure_auth( listing_id: listing_id, in_projection: in_projection, derived: derived, - erase_derived: false, + erase_derived: row_erasure_mark_is_applied( + schema: schema, + header_by_name: header_by_name, + row: row, + ), plan: plan, ) } @@ -786,6 +925,18 @@ fn fold_columns_for_row( ), ) OperatorEntered => ReviewSheetNeedsWrite { plan: p } + DerivedErasureMark => + classification_from_cell_decision( + plan: p, + listing_id: listing_id, + column: col.name, + column_index: idx, + data_index: row.data_index, + decision: decide_erasure_mark_cell( + observed: observed, + desired: desired_erasure_mark(erase_derived: erase_derived, observed: observed), + ), + ) DerivedField { source_field: _ } => if erase_derived { classification_from_cell_decision( @@ -831,16 +982,11 @@ fn fold_columns_for_row( }) } -fn columns_by_name(schema: ReviewSheetSchema) -> Map { - fold(schema.columns, init: empty_map(), f: fn(acc, c) { - map_insert(acc, c.name as String, c) - }) -} - fn append_cell_for_column(column: ReviewColumn, derived: Map) -> String? { match column.kind { ListingStanding => Present { value: listing_standing_label(state: StandingObserved) } OperatorEntered => none + DerivedErasureMark => Present { value: "" } DerivedField { source_field: _ } => match derived.lookup(column.name as String) { Present { value: v } => Present { value: v } @@ -971,6 +1117,7 @@ fn operator_overwrite_reason( OperatorEntered => true DerivedField { source_field: _ } => false ListingStanding => false + DerivedErasureMark => false }) |> filter(c => match header_by_name.lookup(c.name as String) { Absent => false diff --git a/dag/test/claim/review_sheet_converge_witness_test.dag b/dag/test/claim/review_sheet_converge_witness_test.dag index 27435f1d46e..16723c65516 100644 --- a/dag/test/claim/review_sheet_converge_witness_test.dag +++ b/dag/test/claim/review_sheet_converge_witness_test.dag @@ -9,6 +9,7 @@ import gunbc.review_sheet_projection { DerivedField, OperatorEntered, ListingStanding, + DerivedErasureMark, ReviewSheetSchema, ReviewSheetProjection, ProjectionRow, @@ -37,6 +38,8 @@ import gunbc.review_sheet_projection { JudgementEntered, JudgementUnreadable, listing_standing_label, + derived_erasure_mark_label, + ErasureApplied, read_listing_standing, StandingObserved, StandingGone, @@ -157,6 +160,7 @@ fn w_schema() -> ReviewSheetSchema { ReviewColumn { name: "title" as NonEmptyStr, kind: DerivedField { source_field: "title" as NonEmptyStr } }, ReviewColumn { name: "standing" as NonEmptyStr, kind: ListingStanding }, ReviewColumn { name: "judgement" as NonEmptyStr, kind: OperatorEntered }, + ReviewColumn { name: "erasure" as NonEmptyStr, kind: DerivedErasureMark }, ] } } @@ -189,8 +193,19 @@ fn w_observed(headers: List, rows: List>) -> ObservedSheet } } +fn w_schema_without_erasure_mark() -> ReviewSheetSchema { + ReviewSheetSchema { + identity_column: "listing_id" as NonEmptyStr, + columns: [ + ReviewColumn { name: "title" as NonEmptyStr, kind: DerivedField { source_field: "title" as NonEmptyStr } }, + ReviewColumn { name: "standing" as NonEmptyStr, kind: ListingStanding }, + ReviewColumn { name: "judgement" as NonEmptyStr, kind: OperatorEntered }, + ] + } +} + fn w_headers() -> List { - ["listing_id", "title", "standing", "judgement"] + ["listing_id", "title", "standing", "judgement", "erasure"] } fn w_standing_observed() -> String { @@ -201,13 +216,17 @@ fn w_standing_gone() -> String { listing_standing_label(state: StandingGone) } +fn w_erasure_applied() -> String { + derived_erasure_mark_label(state: ErasureApplied) +} + fn w_erasure(listing_id: NonEmptyStr) -> DerivedPayloadErasure { DerivedPayloadErasure { listing_id: listing_id } } fn w_value_range(rows: List>) -> ValueRange { ValueRange { - range: "Review!A1:D", + range: "Review!A1:E", majorDimension: Present { value: "ROWS" }, values: Present { value: concat([w_headers()], rows) }, } @@ -398,6 +417,11 @@ test fn derived_payload_erasure_blanks_derived_keeps_judgement_and_listing_gone( && w.column == ("title" as NonEmptyStr) && w.value == "") }) + && fold(p.patches, init: false, f: fn(acc, w) { + acc || (w.listing_id == ("L1" as NonEmptyStr) + && w.column == ("erasure" as NonEmptyStr) + && w.value == w_erasure_applied()) + }) ReviewSheetConverged => false ReviewSheetConflict { conflict: _ } => false ReviewSheetUnreadable { reason: _ } => false @@ -533,7 +557,7 @@ test fn derived_payload_erasure_already_empty_derived_is_converged() -> Bool { let projection = w_projection(rows: []) let observed = w_observed( headers: w_headers(), - rows: [["L1", "", w_standing_gone(), "buy"]], + rows: [["L1", "", w_standing_gone(), "buy", w_erasure_applied()]], ) match classify_review_sheet_with_erasure( observed: observed, @@ -587,6 +611,11 @@ test fn ensure_with_erasure_applies_blank_derived() -> Bool { && w.column == ("title" as NonEmptyStr) && w.value == "") }) + && fold(bound.plan.patches, init: false, f: fn(acc, w) { + acc || (w.listing_id == ("L1" as NonEmptyStr) + && w.column == ("erasure" as NonEmptyStr) + && w.value == w_erasure_applied()) + }) Noop => false Refuse { reason: _ } => false } @@ -625,12 +654,68 @@ test fn gone_listing_with_derived_payload_still_restores_when_it_returns() -> Bo && w.column == ("standing" as NonEmptyStr) && w.value == listing_standing_label(state: StandingObserved)) }) + && !fold(p.patches, init: false, f: fn(acc, w) { + acc || (w.listing_id == ("L1" as NonEmptyStr) + && w.column == ("title" as NonEmptyStr) + && w.value == "") + }) + ReviewSheetConverged => false + ReviewSheetConflict { conflict: _ } => false + ReviewSheetUnreadable { reason: _ } => false + } +} + +test fn applied_erasure_mark_does_not_restore_derived_when_the_listing_returns() -> Bool { + let projection = w_projection(rows: [w_row(listing_id: "L1" as NonEmptyStr, title: "alpha")]) + let observed = w_observed( + headers: w_headers(), + rows: [["L1", "", w_standing_gone(), "buy", w_erasure_applied()]], + ) + match classify_review_sheet_with_erasure( + observed: observed, + projection: projection, + erasures: [], + ) { + ReviewSheetNeedsWrite { plan: p } => + write_plan_preserves_operator_columns(schema: w_schema(), plan: p) + && fold(p.patches, init: false, f: fn(acc, w) { + acc || (w.listing_id == ("L1" as NonEmptyStr) + && w.column == ("standing" as NonEmptyStr) + && w.value == listing_standing_label(state: StandingObserved)) + }) + && !fold(p.patches, init: false, f: fn(acc, w) { + acc || (w.listing_id == ("L1" as NonEmptyStr) + && w.column == ("title" as NonEmptyStr) + && w.value == "alpha") + }) + && !write_plan_touches_column(plan: p, column: "erasure" as NonEmptyStr) ReviewSheetConverged => false ReviewSheetConflict { conflict: _ } => false ReviewSheetUnreadable { reason: _ } => false } } +test fn derived_payload_erasure_refuses_without_erasure_mark_column() -> Bool { + let projection = ReviewSheetProjection { schema: w_schema_without_erasure_mark(), rows: [] } + let observed = w_observed( + headers: ["listing_id", "title", "standing", "judgement"], + rows: [["L1", "alpha", w_standing_gone(), "buy"]], + ) + match classify_review_sheet_with_erasure( + observed: observed, + projection: projection, + erasures: [w_erasure(listing_id: "L1" as NonEmptyStr)], + ) { + ReviewSheetUnreadable { reason: r } => + string_contains(s: r as String, pattern: "has no DerivedErasureMark column") + && string_contains(s: r as String, pattern: "listing=L1") + && string_contains(s: r as String, pattern: "cell=A2") + ReviewSheetNeedsWrite { plan: _ } => false + ReviewSheetConverged => false + ReviewSheetConflict { conflict: _ } => false + } +} + test fn readback_apply_qualifies_once_on_the_value_range_fold() -> Bool { let projection = w_projection(rows: [w_row(listing_id: "L1" as NonEmptyStr, title: "Widget")]) let range = w_value_range(rows: [["L1", "", w_standing_observed(), ""]]) @@ -703,7 +788,7 @@ test fn observe_only_does_not_emit_a_write_plan() -> Bool { test fn column_major_readback_refuses() -> Bool { let projection = w_projection(rows: [w_row(listing_id: "L1" as NonEmptyStr, title: "alpha")]) let range = ValueRange { - range: "Review!A1:D", + range: "Review!A1:E", majorDimension: Present { value: "COLUMNS" }, values: Present { value: concat([w_headers()], [["L1", "alpha", w_standing_observed(), ""]]) }, } @@ -717,7 +802,7 @@ test fn column_major_readback_refuses() -> Bool { test fn foreign_major_dimension_refuses() -> Bool { let projection = w_projection(rows: [w_row(listing_id: "L1" as NonEmptyStr, title: "alpha")]) let range = ValueRange { - range: "Review!A1:D", + range: "Review!A1:E", majorDimension: Present { value: "DIAGONAL" }, values: Present { value: concat([w_headers()], [["L1", "alpha", w_standing_observed(), ""]]) }, } @@ -890,7 +975,7 @@ test fn standing_is_a_sum_projected_at_emit() -> Bool { test fn duplicate_header_refuses_rather_than_last_wins() -> Bool { let projection = w_projection(rows: [w_row(listing_id: "L1" as NonEmptyStr, title: "alpha")]) let observed = w_observed( - headers: ["listing_id", "title", "standing", "judgement", "title"], + headers: ["listing_id", "title", "standing", "judgement", "erasure", "title"], rows: [["L1", "alpha", w_standing_observed(), "", "hand-edit"]], ) match classify_review_sheet(observed: observed, projection: projection) { @@ -1033,7 +1118,7 @@ test fn absent_listing_appends_a_located_row() -> Bool { test fn append_follows_observed_header_order_not_schema_order() -> Bool { let projection = w_projection(rows: [w_row(listing_id: "L2" as NonEmptyStr, title: "alpha")]) - let headers = ["listing_id", "standing", "title", "judgement"] + let headers = ["listing_id", "standing", "title", "judgement", "erasure"] let observed = w_observed(headers: headers, rows: []) match classify_review_sheet(observed: observed, projection: projection) { ReviewSheetNeedsWrite { plan: p } => From 270655b1c99b386cce942df41869a79694230c03 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 12 Sep 2026 06:22:22 +0000 Subject: [PATCH 07/13] Drop the uninhabitable unmarked mark state and one kind-selector fold. Applied is a readable label; blank is the unmarked cell. Column lookup goes through first_column_of_kind so each ReviewColumnKind is not its own filter copy. Co-authored-by: Cursor --- dag/gunbc/review_sheet_projection.dag | 212 +++++++++--------- .../review_sheet_converge_witness_test.dag | 5 +- 2 files changed, 110 insertions(+), 107 deletions(-) diff --git a/dag/gunbc/review_sheet_projection.dag b/dag/gunbc/review_sheet_projection.dag index 6dd96a8ae22..1e3163aeee3 100644 --- a/dag/gunbc/review_sheet_projection.dag +++ b/dag/gunbc/review_sheet_projection.dag @@ -176,44 +176,95 @@ fn read_listing_standing(cell: String) -> ListingStandingRead { } } -type DerivedErasureMarkState - = ErasureUnmarked - | ErasureApplied - type DerivedErasureMarkRead - = ErasureMarkKnown { state: DerivedErasureMarkState } + = ErasureMarkApplied | ErasureMarkBlank | ErasureMarkForeign { raw: String } -fn derived_erasure_mark_label(state: DerivedErasureMarkState) -> String { - match state { - ErasureUnmarked => "" - ErasureApplied => "ErasureApplied" +data derived_erasure_applied_label: String = "ErasureApplied" + +fn read_derived_erasure_mark(cell: String) -> DerivedErasureMarkRead { + if cell == "" { + ErasureMarkBlank + } else { + if cell == derived_erasure_applied_label { + ErasureMarkApplied + } else { + ErasureMarkForeign { raw: cell } + } } } -fn derived_erasure_mark_eq(a: DerivedErasureMarkState, b: DerivedErasureMarkState) -> Bool { +type ReviewColumnRole + = RoleDerivedField + | RoleOperatorEntered + | RoleListingStanding + | RoleDerivedErasureMark + +fn review_column_role(kind: ReviewColumnKind) -> ReviewColumnRole { + match kind { + DerivedField { source_field: _ } => RoleDerivedField + OperatorEntered => RoleOperatorEntered + ListingStanding => RoleListingStanding + DerivedErasureMark => RoleDerivedErasureMark + } +} + +fn review_column_role_eq(a: ReviewColumnRole, b: ReviewColumnRole) -> Bool { match a { - ErasureUnmarked => match b { - ErasureUnmarked => true - ErasureApplied => false + RoleDerivedField => match b { + RoleDerivedField => true + RoleOperatorEntered => false + RoleListingStanding => false + RoleDerivedErasureMark => false + } + RoleOperatorEntered => match b { + RoleOperatorEntered => true + RoleDerivedField => false + RoleListingStanding => false + RoleDerivedErasureMark => false + } + RoleListingStanding => match b { + RoleListingStanding => true + RoleDerivedField => false + RoleOperatorEntered => false + RoleDerivedErasureMark => false } - ErasureApplied => match b { - ErasureApplied => true - ErasureUnmarked => false + RoleDerivedErasureMark => match b { + RoleDerivedErasureMark => true + RoleDerivedField => false + RoleOperatorEntered => false + RoleListingStanding => false } } } -fn read_derived_erasure_mark(cell: String) -> DerivedErasureMarkRead { - if cell == "" { - ErasureMarkBlank - } else { - if cell == derived_erasure_mark_label(state: ErasureApplied) { - ErasureMarkKnown { state: ErasureApplied } - } else { - ErasureMarkForeign { raw: cell } - } +fn first_column_of_kind(schema: ReviewSheetSchema, role: ReviewColumnRole) -> ReviewColumn? { + schema.columns + |> filter(c => review_column_role_eq(a: review_column_role(kind: c.kind), b: role)) + |> first +} + +fn schema_has_kind(schema: ReviewSheetSchema, role: ReviewColumnRole) -> Bool { + match first_column_of_kind(schema: schema, role: role) { + Present { value: _ } => true + Absent => false + } +} + +fn observed_cell_of_kind( + schema: ReviewSheetSchema, + header_by_name: Map, + row: ObservedSheetRow, + role: ReviewColumnRole, +) -> String? { + match first_column_of_kind(schema: schema, role: role) { + Absent => none + Present { value: col } => + match header_by_name.lookup(col.name as String) { + Absent => none + Present { value: idx } => Present { value: cell_at(cells: row.cells, index: idx) } + } } } @@ -396,36 +447,19 @@ fn listing_has_erasure(erasure_by_id: Map, listin } } -fn column_is_listing_standing(column: ReviewColumn) -> Bool { - match column.kind { - ListingStanding => true - OperatorEntered => false - DerivedField { source_field: _ } => false - DerivedErasureMark => false - } -} - -fn column_is_erasure_mark(column: ReviewColumn) -> Bool { - match column.kind { - DerivedErasureMark => true - ListingStanding => false - OperatorEntered => false - DerivedField { source_field: _ } => false - } -} - fn read_row_listing_standing( schema: ReviewSheetSchema, header_by_name: Map, row: ObservedSheetRow, ) -> ListingStandingRead? { - match schema.columns |> filter(c => column_is_listing_standing(column: c)) |> first { + match observed_cell_of_kind( + schema: schema, + header_by_name: header_by_name, + row: row, + role: RoleListingStanding, + ) { Absent => none - Present { value: col } => - match header_by_name.lookup(col.name as String) { - Absent => none - Present { value: idx } => Present { value: read_listing_standing(cell: cell_at(cells: row.cells, index: idx)) } - } + Present { value: cell } => Present { value: read_listing_standing(cell: cell) } } } @@ -434,13 +468,14 @@ fn read_row_erasure_mark( header_by_name: Map, row: ObservedSheetRow, ) -> DerivedErasureMarkRead? { - match schema.columns |> filter(c => column_is_erasure_mark(column: c)) |> first { + match observed_cell_of_kind( + schema: schema, + header_by_name: header_by_name, + row: row, + role: RoleDerivedErasureMark, + ) { Absent => none - Present { value: col } => - match header_by_name.lookup(col.name as String) { - Absent => none - Present { value: idx } => Present { value: read_derived_erasure_mark(cell: cell_at(cells: row.cells, index: idx)) } - } + Present { value: cell } => Present { value: read_derived_erasure_mark(cell: cell) } } } @@ -453,8 +488,7 @@ fn row_erasure_mark_is_applied( Absent => false Present { value: mark_read } => match mark_read { - ErasureMarkKnown { state: mark_state } => - derived_erasure_mark_eq(a: mark_state, b: ErasureApplied) + ErasureMarkApplied => true ErasureMarkBlank => false ErasureMarkForeign { raw: _ } => false } @@ -486,22 +520,11 @@ fn erasure_has_no_erasure_mark_reason(listing_id: NonEmptyStr) -> NonEmptyStr { } fn schema_has_derived_field(schema: ReviewSheetSchema) -> Bool { - match schema.columns |> filter(c => match c.kind { - DerivedField { source_field: _ } => true - OperatorEntered => false - ListingStanding => false - DerivedErasureMark => false - }) |> first { - Present { value: _ } => true - Absent => false - } + schema_has_kind(schema: schema, role: RoleDerivedField) } fn schema_has_erasure_mark(schema: ReviewSheetSchema) -> Bool { - match schema.columns |> filter(c => column_is_erasure_mark(column: c)) |> first { - Present { value: _ } => true - Absent => false - } + schema_has_kind(schema: schema, role: RoleDerivedErasureMark) } fn first_erasure_not_on_sheet( @@ -596,12 +619,7 @@ fn col_and_listing(column: NonEmptyStr, listing_id: NonEmptyStr) -> String { fn first_omitted_derived_column(row: ProjectionRow, schema: ReviewSheetSchema) -> NonEmptyStr? { let derived = derived_by_column(row: row) schema.columns - |> filter(c => match c.kind { - DerivedField { source_field: _ } => true - OperatorEntered => false - ListingStanding => false - DerivedErasureMark => false - }) + |> filter(c => review_column_role_eq(a: review_column_role(kind: c.kind), b: RoleDerivedField)) |> filter(c => match derived.lookup(c.name as String) { Absent => true Present { value: _ } => false @@ -652,7 +670,9 @@ fn write_plan_touches_column(plan: ReviewSheetWritePlan, column: NonEmptyStr) -> } fn operator_column_names(schema: ReviewSheetSchema) -> List { - schema.columns |> filter(c => match c.kind { OperatorEntered => true DerivedField { source_field: _ } => false ListingStanding => false DerivedErasureMark => false }) |> map(c => c.name) + schema.columns + |> filter(c => review_column_role_eq(a: review_column_role(kind: c.kind), b: RoleOperatorEntered)) + |> map(c => c.name) } fn write_plan_covers_column_letter(plan: ReviewSheetWritePlan, letter: String) -> Bool { @@ -708,41 +728,19 @@ fn decide_derived_erasure_cell(observed: String) -> CellDecision { } } -fn decide_erasure_mark_cell(observed: String, desired: DerivedErasureMarkState) -> CellDecision { +fn decide_erasure_mark_cell(observed: String, erase_derived: Bool) -> CellDecision { match read_derived_erasure_mark(cell: observed) { ErasureMarkForeign { raw: _ } => CellConflict + ErasureMarkApplied => CellSkip ErasureMarkBlank => - if derived_erasure_mark_eq(a: desired, b: ErasureUnmarked) { - CellSkip + if erase_derived { + CellWrite { value: derived_erasure_applied_label } } else { - CellWrite { value: derived_erasure_mark_label(state: desired) } - } - ErasureMarkKnown { state: observed_state } => - if derived_erasure_mark_eq(a: observed_state, b: desired) { CellSkip - } else { - CellWrite { value: derived_erasure_mark_label(state: desired) } } } } -fn desired_erasure_mark(erase_derived: Bool, observed: String) -> DerivedErasureMarkState { - if erase_derived { - ErasureApplied - } else { - match read_derived_erasure_mark(cell: observed) { - ErasureMarkKnown { state: mark_state } => - if derived_erasure_mark_eq(a: mark_state, b: ErasureApplied) { - ErasureApplied - } else { - ErasureUnmarked - } - ErasureMarkBlank => ErasureUnmarked - ErasureMarkForeign { raw: _ } => ErasureUnmarked - } - } -} - fn classification_from_cell_decision( plan: ReviewSheetWritePlan, listing_id: NonEmptyStr, @@ -934,7 +932,7 @@ fn fold_columns_for_row( data_index: row.data_index, decision: decide_erasure_mark_cell( observed: observed, - desired: desired_erasure_mark(erase_derived: erase_derived, observed: observed), + erase_derived: erase_derived, ), ) DerivedField { source_field: _ } => @@ -984,9 +982,15 @@ fn fold_columns_for_row( fn append_cell_for_column(column: ReviewColumn, derived: Map) -> String? { match column.kind { +<<<<<<< HEAD ListingStanding => Present { value: listing_standing_label(state: StandingObserved) } OperatorEntered => none DerivedErasureMark => Present { value: "" } +======= + ListingStanding => listing_standing_label(state: StandingObserved) + OperatorEntered => "" + DerivedErasureMark => "" +>>>>>>> 609ea55a52 (Drop the uninhabitable unmarked mark state and one kind-selector fold.) DerivedField { source_field: _ } => match derived.lookup(column.name as String) { Present { value: v } => Present { value: v } diff --git a/dag/test/claim/review_sheet_converge_witness_test.dag b/dag/test/claim/review_sheet_converge_witness_test.dag index 16723c65516..9203dfab18c 100644 --- a/dag/test/claim/review_sheet_converge_witness_test.dag +++ b/dag/test/claim/review_sheet_converge_witness_test.dag @@ -38,8 +38,7 @@ import gunbc.review_sheet_projection { JudgementEntered, JudgementUnreadable, listing_standing_label, - derived_erasure_mark_label, - ErasureApplied, + derived_erasure_applied_label, read_listing_standing, StandingObserved, StandingGone, @@ -217,7 +216,7 @@ fn w_standing_gone() -> String { } fn w_erasure_applied() -> String { - derived_erasure_mark_label(state: ErasureApplied) + derived_erasure_applied_label } fn w_erasure(listing_id: NonEmptyStr) -> DerivedPayloadErasure { From 82d01b4741d8b55901acc97830ae33ea902271ac Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 12 Sep 2026 07:10:39 +0000 Subject: [PATCH 08/13] Delete ReviewColumnRole and split kind-cell absence causes. Kind selection is now a keep over ReviewColumnKind. Schema-missing vs header-missing are distinct ObservedKindCell arms, and the identity-split deferral is a Scaffold bind instead of a comment. Co-authored-by: Cursor --- dag/gunbc/review_sheet_projection.dag | 168 +++++++++++------- .../review_sheet_converge_witness_test.dag | 29 +++ 2 files changed, 130 insertions(+), 67 deletions(-) diff --git a/dag/gunbc/review_sheet_projection.dag b/dag/gunbc/review_sheet_projection.dag index 1e3163aeee3..a9e74bb4c84 100644 --- a/dag/gunbc/review_sheet_projection.dag +++ b/dag/gunbc/review_sheet_projection.dag @@ -7,7 +7,7 @@ import std.keyed_roster { KeyedRosterBuilt, KeyedRosterBuildDuplicateKey, } -import std.disposition { Disposition, Scaffold, RealizationDispatch } +import std.disposition { Disposition, Scaffold, RealizationDispatch, SingleAuthority } import std.decl_ref { DeclarationRef, WholeDeclaration } // Identity-keyed review projection. This is the shape the buyer-agent lane binds to: every row @@ -53,8 +53,7 @@ type ReviewSheetProjection { // standing stays ListingGone on the apply pass. Located writes or a located refuse; // quiet success is refused. Judgement has no deletion arm. // identity_column in the intended binding is an eBay item id: a stub that keeps that key -// still retains eBay data under a broad MAD reading. Identity split is deferred until -// exemption wording puts the item id in scope. +// still retains eBay data under a broad MAD reading. // Blanking DerivedField is a real meaning loss for human re-audit: title and price are why // they wrote buy, and a labelled stub does not carry that. The buyer loop still has // identity plus the operator cell and can refuse to re-ask. We gave something up; we did @@ -195,58 +194,55 @@ fn read_derived_erasure_mark(cell: String) -> DerivedErasureMarkRead { } } -type ReviewColumnRole - = RoleDerivedField - | RoleOperatorEntered - | RoleListingStanding - | RoleDerivedErasureMark +fn kind_is_derived_field(kind: ReviewColumnKind) -> Bool { + match kind { + DerivedField { source_field: _ } => true + OperatorEntered => false + ListingStanding => false + DerivedErasureMark => false + } +} -fn review_column_role(kind: ReviewColumnKind) -> ReviewColumnRole { +fn kind_is_operator_entered(kind: ReviewColumnKind) -> Bool { match kind { - DerivedField { source_field: _ } => RoleDerivedField - OperatorEntered => RoleOperatorEntered - ListingStanding => RoleListingStanding - DerivedErasureMark => RoleDerivedErasureMark + OperatorEntered => true + DerivedField { source_field: _ } => false + ListingStanding => false + DerivedErasureMark => false } } -fn review_column_role_eq(a: ReviewColumnRole, b: ReviewColumnRole) -> Bool { - match a { - RoleDerivedField => match b { - RoleDerivedField => true - RoleOperatorEntered => false - RoleListingStanding => false - RoleDerivedErasureMark => false - } - RoleOperatorEntered => match b { - RoleOperatorEntered => true - RoleDerivedField => false - RoleListingStanding => false - RoleDerivedErasureMark => false - } - RoleListingStanding => match b { - RoleListingStanding => true - RoleDerivedField => false - RoleOperatorEntered => false - RoleDerivedErasureMark => false - } - RoleDerivedErasureMark => match b { - RoleDerivedErasureMark => true - RoleDerivedField => false - RoleOperatorEntered => false - RoleListingStanding => false - } +fn kind_is_listing_standing(kind: ReviewColumnKind) -> Bool { + match kind { + ListingStanding => true + DerivedField { source_field: _ } => false + OperatorEntered => false + DerivedErasureMark => false } } -fn first_column_of_kind(schema: ReviewSheetSchema, role: ReviewColumnRole) -> ReviewColumn? { +fn kind_is_erasure_mark(kind: ReviewColumnKind) -> Bool { + match kind { + DerivedErasureMark => true + DerivedField { source_field: _ } => false + OperatorEntered => false + ListingStanding => false + } +} + +type ObservedKindCell + = KindCell { value: String } + | KindAbsentFromSchema + | KindColumnMissingFromHeader { column: NonEmptyStr } + +fn first_column_of_kind(schema: ReviewSheetSchema, keep: fn(ReviewColumnKind) -> Bool) -> ReviewColumn? { schema.columns - |> filter(c => review_column_role_eq(a: review_column_role(kind: c.kind), b: role)) + |> filter(c => keep(c.kind)) |> first } -fn schema_has_kind(schema: ReviewSheetSchema, role: ReviewColumnRole) -> Bool { - match first_column_of_kind(schema: schema, role: role) { +fn schema_has_kind(schema: ReviewSheetSchema, keep: fn(ReviewColumnKind) -> Bool) -> Bool { + match first_column_of_kind(schema: schema, keep: keep) { Present { value: _ } => true Absent => false } @@ -256,14 +252,14 @@ fn observed_cell_of_kind( schema: ReviewSheetSchema, header_by_name: Map, row: ObservedSheetRow, - role: ReviewColumnRole, -) -> String? { - match first_column_of_kind(schema: schema, role: role) { - Absent => none + keep: fn(ReviewColumnKind) -> Bool, +) -> ObservedKindCell { + match first_column_of_kind(schema: schema, keep: keep) { + Absent => KindAbsentFromSchema Present { value: col } => match header_by_name.lookup(col.name as String) { - Absent => none - Present { value: idx } => Present { value: cell_at(cells: row.cells, index: idx) } + Absent => KindColumnMissingFromHeader { column: col.name } + Present { value: idx } => KindCell { value: cell_at(cells: row.cells, index: idx) } } } } @@ -425,8 +421,15 @@ fn header_key_rows(headers: List) -> List> { map(headers |> enumerate, pair => KeyedRow { row_key: pair.second, value: pair.first }) } +fn review_sheet_listing_identity(listing_id: NonEmptyStr) -> NonEmptyStr { + listing_id +} + fn projection_key_rows(projection: ReviewSheetProjection) -> List> { - map(projection.rows, row => KeyedRow { row_key: row.listing_id as String, value: row }) + map(projection.rows, row => KeyedRow { + row_key: review_sheet_listing_identity(listing_id: row.listing_id) as String, + value: row, + }) } fn observed_identity_key_rows(sheet: ObservedSheet, identity_index: Int) -> List> { @@ -437,11 +440,14 @@ fn observed_identity_key_rows(sheet: ObservedSheet, identity_index: Int) -> List } fn erasure_key_rows(erasures: List) -> List> { - map(erasures, e => KeyedRow { row_key: e.listing_id as String, value: e }) + map(erasures, e => KeyedRow { + row_key: review_sheet_listing_identity(listing_id: e.listing_id) as String, + value: e, + }) } fn listing_has_erasure(erasure_by_id: Map, listing_id: NonEmptyStr) -> Bool { - match erasure_by_id.lookup(listing_id as String) { + match erasure_by_id.lookup(review_sheet_listing_identity(listing_id: listing_id) as String) { Present { value: _ } => true Absent => false } @@ -456,10 +462,11 @@ fn read_row_listing_standing( schema: schema, header_by_name: header_by_name, row: row, - role: RoleListingStanding, + keep: kind_is_listing_standing, ) { - Absent => none - Present { value: cell } => Present { value: read_listing_standing(cell: cell) } + KindAbsentFromSchema => none + KindColumnMissingFromHeader { column: _ } => none + KindCell { value: cell } => Present { value: read_listing_standing(cell: cell) } } } @@ -472,10 +479,11 @@ fn read_row_erasure_mark( schema: schema, header_by_name: header_by_name, row: row, - role: RoleDerivedErasureMark, + keep: kind_is_erasure_mark, ) { - Absent => none - Present { value: cell } => Present { value: read_derived_erasure_mark(cell: cell) } + KindAbsentFromSchema => none + KindColumnMissingFromHeader { column: _ } => none + KindCell { value: cell } => Present { value: read_derived_erasure_mark(cell: cell) } } } @@ -511,6 +519,13 @@ fn erasure_missing_standing_column_reason(listing_id: NonEmptyStr) -> NonEmptySt concat("derived payload erasure missing ListingStanding column listing=", listing_id as String) as NonEmptyStr } +fn erasure_missing_standing_header_reason(listing_id: NonEmptyStr, column: NonEmptyStr) -> NonEmptyStr { + concat( + "derived payload erasure missing header ", + concat(column as String, concat(" listing=", listing_id as String)), + ) as NonEmptyStr +} + fn erasure_has_no_derived_field_reason(listing_id: NonEmptyStr) -> NonEmptyStr { concat("derived payload erasure has no DerivedField column listing=", listing_id as String) as NonEmptyStr } @@ -520,11 +535,11 @@ fn erasure_has_no_erasure_mark_reason(listing_id: NonEmptyStr) -> NonEmptyStr { } fn schema_has_derived_field(schema: ReviewSheetSchema) -> Bool { - schema_has_kind(schema: schema, role: RoleDerivedField) + schema_has_kind(schema: schema, keep: kind_is_derived_field) } fn schema_has_erasure_mark(schema: ReviewSheetSchema) -> Bool { - schema_has_kind(schema: schema, role: RoleDerivedErasureMark) + schema_has_kind(schema: schema, keep: kind_is_erasure_mark) } fn first_erasure_not_on_sheet( @@ -532,7 +547,7 @@ fn first_erasure_not_on_sheet( observed_by_id: Map, ) -> NonEmptyStr? { erasures - |> filter(e => match observed_by_id.lookup(e.listing_id as String) { + |> filter(e => match observed_by_id.lookup(review_sheet_listing_identity(listing_id: e.listing_id) as String) { Present { value: _ } => false Absent => true }) @@ -552,10 +567,20 @@ fn authorize_derived_payload_erasure( if in_projection { ErasureRefused { reason: erasure_still_projected_reason(listing_id: listing_id) } } else { - match read_row_listing_standing(schema: schema, header_by_name: header_by_name, row: row) { - Absent => ErasureRefused { reason: erasure_missing_standing_column_reason(listing_id: listing_id) } - Present { value: standing_read } => - match standing_read { + match observed_cell_of_kind( + schema: schema, + header_by_name: header_by_name, + row: row, + keep: kind_is_listing_standing, + ) { + KindAbsentFromSchema => + ErasureRefused { reason: erasure_missing_standing_column_reason(listing_id: listing_id) } + KindColumnMissingFromHeader { column: n } => + ErasureRefused { + reason: erasure_missing_standing_header_reason(listing_id: listing_id, column: n) + } + KindCell { value: cell } => + match read_listing_standing(cell: cell) { StandingKnown { state: standing_state } => if listing_standing_eq(a: standing_state, b: StandingGone) { if schema_has_derived_field(schema: schema) { @@ -619,7 +644,7 @@ fn col_and_listing(column: NonEmptyStr, listing_id: NonEmptyStr) -> String { fn first_omitted_derived_column(row: ProjectionRow, schema: ReviewSheetSchema) -> NonEmptyStr? { let derived = derived_by_column(row: row) schema.columns - |> filter(c => review_column_role_eq(a: review_column_role(kind: c.kind), b: RoleDerivedField)) + |> filter(c => kind_is_derived_field(kind: c.kind)) |> filter(c => match derived.lookup(c.name as String) { Absent => true Present { value: _ } => false @@ -671,7 +696,7 @@ fn write_plan_touches_column(plan: ReviewSheetWritePlan, column: NonEmptyStr) -> fn operator_column_names(schema: ReviewSheetSchema) -> List { schema.columns - |> filter(c => review_column_role_eq(a: review_column_role(kind: c.kind), b: RoleOperatorEntered)) + |> filter(c => kind_is_operator_entered(kind: c.kind)) |> map(c => c.name) } @@ -1468,3 +1493,12 @@ data buyer_review_judgement_read_frontier: Disposition = Scaffold { field: WholeDeclaration } } + +data buyer_review_identity_split_frontier: Disposition = Scaffold { + dissolves_to: SingleAuthority, + bind: DeclarationRef { + module_path: "gunbc.review_sheet_projection", + decl_name: "review_sheet_listing_identity", + field: WholeDeclaration + } +} diff --git a/dag/test/claim/review_sheet_converge_witness_test.dag b/dag/test/claim/review_sheet_converge_witness_test.dag index 9203dfab18c..2fed5187e5f 100644 --- a/dag/test/claim/review_sheet_converge_witness_test.dag +++ b/dag/test/claim/review_sheet_converge_witness_test.dag @@ -34,6 +34,7 @@ import gunbc.review_sheet_projection { columns_by_name, judgement_for_listing, buyer_review_judgement_read_frontier, + buyer_review_identity_split_frontier, JudgementBlank, JudgementEntered, JudgementUnreadable, @@ -715,6 +716,26 @@ test fn derived_payload_erasure_refuses_without_erasure_mark_column() -> Bool { } } +test fn derived_payload_erasure_refuses_when_standing_header_is_missing() -> Bool { + let projection = w_projection(rows: []) + let observed = w_observed( + headers: ["listing_id", "title", "judgement", "erasure"], + rows: [["L1", "alpha", "buy", ""]], + ) + match classify_review_sheet_with_erasure( + observed: observed, + projection: projection, + erasures: [w_erasure(listing_id: "L1" as NonEmptyStr)], + ) { + ReviewSheetUnreadable { reason: r } => + string_contains(s: r as String, pattern: "missing header standing") + && !string_contains(s: r as String, pattern: "missing ListingStanding column") + ReviewSheetNeedsWrite { plan: _ } => false + ReviewSheetConverged => false + ReviewSheetConflict { conflict: _ } => false + } +} + test fn readback_apply_qualifies_once_on_the_value_range_fold() -> Bool { let projection = w_projection(rows: [w_row(listing_id: "L1" as NonEmptyStr, title: "Widget")]) let range = w_value_range(rows: [["L1", "", w_standing_observed(), ""]]) @@ -1252,6 +1273,14 @@ test fn live_transport_frontiers_are_scaffold_not_terminal() -> Bool { Scaffold { dissolves_to: SubstrateMandatoryTag, bind: _ } => false Terminal { reason: _ } => false } + && match buyer_review_identity_split_frontier { + Scaffold { dissolves_to: SingleAuthority, bind: b } => + (b.module_path as String) == "gunbc.review_sheet_projection" + && (b.decl_name as String) == "review_sheet_listing_identity" + Scaffold { dissolves_to: RealizationDispatch, bind: _ } => false + Scaffold { dissolves_to: SubstrateMandatoryTag, bind: _ } => false + Terminal { reason: _ } => false + } && match buyer_review_judgement_read_frontier { Scaffold { dissolves_to: RealizationDispatch, bind: b } => (b.module_path as String) == "gunbc.review_sheet_projection" From e4fc211d90a60d6985dddbca33d6c4cc88090046 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 12 Sep 2026 07:35:16 +0000 Subject: [PATCH 09/13] Select review columns by typed accessors, not kind predicates. Kind membership is exhaustive match that yields a column or a refuse arm, so the Bool keep helpers (and the keep-parameter fold) are gone. Co-authored-by: Cursor --- dag/gunbc/review_sheet_projection.dag | 161 ++++++++++++-------------- 1 file changed, 77 insertions(+), 84 deletions(-) diff --git a/dag/gunbc/review_sheet_projection.dag b/dag/gunbc/review_sheet_projection.dag index a9e74bb4c84..60de1208a6e 100644 --- a/dag/gunbc/review_sheet_projection.dag +++ b/dag/gunbc/review_sheet_projection.dag @@ -194,67 +194,57 @@ fn read_derived_erasure_mark(cell: String) -> DerivedErasureMarkRead { } } -fn kind_is_derived_field(kind: ReviewColumnKind) -> Bool { - match kind { - DerivedField { source_field: _ } => true - OperatorEntered => false - ListingStanding => false - DerivedErasureMark => false - } -} - -fn kind_is_operator_entered(kind: ReviewColumnKind) -> Bool { - match kind { - OperatorEntered => true - DerivedField { source_field: _ } => false - ListingStanding => false - DerivedErasureMark => false - } -} +type ObservedKindCell + = KindCell { value: String } + | KindAbsentFromSchema + | KindColumnMissingFromHeader { column: NonEmptyStr } -fn kind_is_listing_standing(kind: ReviewColumnKind) -> Bool { - match kind { - ListingStanding => true - DerivedField { source_field: _ } => false - OperatorEntered => false - DerivedErasureMark => false +fn first_present_column(acc: ReviewColumn?, next: ReviewColumn?) -> ReviewColumn? { + match acc { + Present { value: found } => Present { value: found } + Absent => next } } -fn kind_is_erasure_mark(kind: ReviewColumnKind) -> Bool { - match kind { - DerivedErasureMark => true - DerivedField { source_field: _ } => false - OperatorEntered => false - ListingStanding => false - } +fn first_derived_field_column(schema: ReviewSheetSchema) -> ReviewColumn? { + fold(schema.columns, init: none, f: fn(acc, col) { + first_present_column(acc: acc, next: match col.kind { + DerivedField { source_field: _ } => Present { value: col } + OperatorEntered => none + ListingStanding => none + DerivedErasureMark => none + }) + }) } -type ObservedKindCell - = KindCell { value: String } - | KindAbsentFromSchema - | KindColumnMissingFromHeader { column: NonEmptyStr } - -fn first_column_of_kind(schema: ReviewSheetSchema, keep: fn(ReviewColumnKind) -> Bool) -> ReviewColumn? { - schema.columns - |> filter(c => keep(c.kind)) - |> first +fn first_listing_standing_column(schema: ReviewSheetSchema) -> ReviewColumn? { + fold(schema.columns, init: none, f: fn(acc, col) { + first_present_column(acc: acc, next: match col.kind { + ListingStanding => Present { value: col } + DerivedField { source_field: _ } => none + OperatorEntered => none + DerivedErasureMark => none + }) + }) } -fn schema_has_kind(schema: ReviewSheetSchema, keep: fn(ReviewColumnKind) -> Bool) -> Bool { - match first_column_of_kind(schema: schema, keep: keep) { - Present { value: _ } => true - Absent => false - } +fn first_erasure_mark_column(schema: ReviewSheetSchema) -> ReviewColumn? { + fold(schema.columns, init: none, f: fn(acc, col) { + first_present_column(acc: acc, next: match col.kind { + DerivedErasureMark => Present { value: col } + DerivedField { source_field: _ } => none + OperatorEntered => none + ListingStanding => none + }) + }) } -fn observed_cell_of_kind( - schema: ReviewSheetSchema, +fn observed_cell_for_column( header_by_name: Map, row: ObservedSheetRow, - keep: fn(ReviewColumnKind) -> Bool, + column: ReviewColumn?, ) -> ObservedKindCell { - match first_column_of_kind(schema: schema, keep: keep) { + match column { Absent => KindAbsentFromSchema Present { value: col } => match header_by_name.lookup(col.name as String) { @@ -458,11 +448,10 @@ fn read_row_listing_standing( header_by_name: Map, row: ObservedSheetRow, ) -> ListingStandingRead? { - match observed_cell_of_kind( - schema: schema, + match observed_cell_for_column( header_by_name: header_by_name, row: row, - keep: kind_is_listing_standing, + column: first_listing_standing_column(schema: schema), ) { KindAbsentFromSchema => none KindColumnMissingFromHeader { column: _ } => none @@ -475,11 +464,10 @@ fn read_row_erasure_mark( header_by_name: Map, row: ObservedSheetRow, ) -> DerivedErasureMarkRead? { - match observed_cell_of_kind( - schema: schema, + match observed_cell_for_column( header_by_name: header_by_name, row: row, - keep: kind_is_erasure_mark, + column: first_erasure_mark_column(schema: schema), ) { KindAbsentFromSchema => none KindColumnMissingFromHeader { column: _ } => none @@ -534,14 +522,6 @@ fn erasure_has_no_erasure_mark_reason(listing_id: NonEmptyStr) -> NonEmptyStr { concat("derived payload erasure has no DerivedErasureMark column listing=", listing_id as String) as NonEmptyStr } -fn schema_has_derived_field(schema: ReviewSheetSchema) -> Bool { - schema_has_kind(schema: schema, keep: kind_is_derived_field) -} - -fn schema_has_erasure_mark(schema: ReviewSheetSchema) -> Bool { - schema_has_kind(schema: schema, keep: kind_is_erasure_mark) -} - fn first_erasure_not_on_sheet( erasures: List, observed_by_id: Map, @@ -567,11 +547,10 @@ fn authorize_derived_payload_erasure( if in_projection { ErasureRefused { reason: erasure_still_projected_reason(listing_id: listing_id) } } else { - match observed_cell_of_kind( - schema: schema, + match observed_cell_for_column( header_by_name: header_by_name, row: row, - keep: kind_is_listing_standing, + column: first_listing_standing_column(schema: schema), ) { KindAbsentFromSchema => ErasureRefused { reason: erasure_missing_standing_column_reason(listing_id: listing_id) } @@ -583,14 +562,15 @@ fn authorize_derived_payload_erasure( match read_listing_standing(cell: cell) { StandingKnown { state: standing_state } => if listing_standing_eq(a: standing_state, b: StandingGone) { - if schema_has_derived_field(schema: schema) { - if schema_has_erasure_mark(schema: schema) { - ErasureAuthorized - } else { - ErasureRefused { reason: erasure_has_no_erasure_mark_reason(listing_id: listing_id) } - } - } else { - ErasureRefused { reason: erasure_has_no_derived_field_reason(listing_id: listing_id) } + match first_derived_field_column(schema: schema) { + Absent => + ErasureRefused { reason: erasure_has_no_derived_field_reason(listing_id: listing_id) } + Present { value: _ } => + match first_erasure_mark_column(schema: schema) { + Absent => + ErasureRefused { reason: erasure_has_no_erasure_mark_reason(listing_id: listing_id) } + Present { value: _ } => ErasureAuthorized + } } } else { ErasureRefused { reason: erasure_requires_gone_reason(listing_id: listing_id) } @@ -643,14 +623,22 @@ fn col_and_listing(column: NonEmptyStr, listing_id: NonEmptyStr) -> String { fn first_omitted_derived_column(row: ProjectionRow, schema: ReviewSheetSchema) -> NonEmptyStr? { let derived = derived_by_column(row: row) - schema.columns - |> filter(c => kind_is_derived_field(kind: c.kind)) - |> filter(c => match derived.lookup(c.name as String) { - Absent => true - Present { value: _ } => false - }) - |> map(c => c.name) - |> first + fold(schema.columns, init: none, f: fn(acc, col) { + match acc { + Present { value: found } => Present { value: found } + Absent => + match col.kind { + DerivedField { source_field: _ } => + match derived.lookup(col.name as String) { + Absent => Present { value: col.name } + Present { value: _ } => none + } + OperatorEntered => none + ListingStanding => none + DerivedErasureMark => none + } + } + }) } fn missing_schema_header(schema: ReviewSheetSchema, header_by_name: Map) -> NonEmptyStr? { @@ -695,9 +683,14 @@ fn write_plan_touches_column(plan: ReviewSheetWritePlan, column: NonEmptyStr) -> } fn operator_column_names(schema: ReviewSheetSchema) -> List { - schema.columns - |> filter(c => kind_is_operator_entered(kind: c.kind)) - |> map(c => c.name) + fold(schema.columns, init: [], f: fn(acc, col) { + match col.kind { + OperatorEntered => concat(acc, [col.name]) + DerivedField { source_field: _ } => acc + ListingStanding => acc + DerivedErasureMark => acc + } + }) } fn write_plan_covers_column_letter(plan: ReviewSheetWritePlan, letter: String) -> Bool { From 8ed04e88ee34b12564403b2d31b3490f53ced2fc Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 12 Sep 2026 08:06:27 +0000 Subject: [PATCH 10/13] Restore linear operator-column names and drop the identity nickname. Operator names are filter then map again; listing identity is listing_id, not a no-op scaffold bind. Co-authored-by: Cursor --- dag/gunbc/review_sheet_projection.dag | 45 ++++++------------- .../review_sheet_converge_witness_test.dag | 9 ---- 2 files changed, 13 insertions(+), 41 deletions(-) diff --git a/dag/gunbc/review_sheet_projection.dag b/dag/gunbc/review_sheet_projection.dag index 60de1208a6e..7680cde7f8f 100644 --- a/dag/gunbc/review_sheet_projection.dag +++ b/dag/gunbc/review_sheet_projection.dag @@ -7,7 +7,7 @@ import std.keyed_roster { KeyedRosterBuilt, KeyedRosterBuildDuplicateKey, } -import std.disposition { Disposition, Scaffold, RealizationDispatch, SingleAuthority } +import std.disposition { Disposition, Scaffold, RealizationDispatch } import std.decl_ref { DeclarationRef, WholeDeclaration } // Identity-keyed review projection. This is the shape the buyer-agent lane binds to: every row @@ -411,15 +411,8 @@ fn header_key_rows(headers: List) -> List> { map(headers |> enumerate, pair => KeyedRow { row_key: pair.second, value: pair.first }) } -fn review_sheet_listing_identity(listing_id: NonEmptyStr) -> NonEmptyStr { - listing_id -} - fn projection_key_rows(projection: ReviewSheetProjection) -> List> { - map(projection.rows, row => KeyedRow { - row_key: review_sheet_listing_identity(listing_id: row.listing_id) as String, - value: row, - }) + map(projection.rows, row => KeyedRow { row_key: row.listing_id as String, value: row }) } fn observed_identity_key_rows(sheet: ObservedSheet, identity_index: Int) -> List> { @@ -430,14 +423,11 @@ fn observed_identity_key_rows(sheet: ObservedSheet, identity_index: Int) -> List } fn erasure_key_rows(erasures: List) -> List> { - map(erasures, e => KeyedRow { - row_key: review_sheet_listing_identity(listing_id: e.listing_id) as String, - value: e, - }) + map(erasures, e => KeyedRow { row_key: e.listing_id as String, value: e }) } fn listing_has_erasure(erasure_by_id: Map, listing_id: NonEmptyStr) -> Bool { - match erasure_by_id.lookup(review_sheet_listing_identity(listing_id: listing_id) as String) { + match erasure_by_id.lookup(listing_id as String) { Present { value: _ } => true Absent => false } @@ -527,7 +517,7 @@ fn first_erasure_not_on_sheet( observed_by_id: Map, ) -> NonEmptyStr? { erasures - |> filter(e => match observed_by_id.lookup(review_sheet_listing_identity(listing_id: e.listing_id) as String) { + |> filter(e => match observed_by_id.lookup(e.listing_id as String) { Present { value: _ } => false Absent => true }) @@ -683,14 +673,14 @@ fn write_plan_touches_column(plan: ReviewSheetWritePlan, column: NonEmptyStr) -> } fn operator_column_names(schema: ReviewSheetSchema) -> List { - fold(schema.columns, init: [], f: fn(acc, col) { - match col.kind { - OperatorEntered => concat(acc, [col.name]) - DerivedField { source_field: _ } => acc - ListingStanding => acc - DerivedErasureMark => acc - } - }) + schema.columns + |> filter(c => match c.kind { + OperatorEntered => true + DerivedField { source_field: _ } => false + ListingStanding => false + DerivedErasureMark => false + }) + |> map(c => c.name) } fn write_plan_covers_column_letter(plan: ReviewSheetWritePlan, letter: String) -> Bool { @@ -1486,12 +1476,3 @@ data buyer_review_judgement_read_frontier: Disposition = Scaffold { field: WholeDeclaration } } - -data buyer_review_identity_split_frontier: Disposition = Scaffold { - dissolves_to: SingleAuthority, - bind: DeclarationRef { - module_path: "gunbc.review_sheet_projection", - decl_name: "review_sheet_listing_identity", - field: WholeDeclaration - } -} diff --git a/dag/test/claim/review_sheet_converge_witness_test.dag b/dag/test/claim/review_sheet_converge_witness_test.dag index 2fed5187e5f..b15bea2de9a 100644 --- a/dag/test/claim/review_sheet_converge_witness_test.dag +++ b/dag/test/claim/review_sheet_converge_witness_test.dag @@ -34,7 +34,6 @@ import gunbc.review_sheet_projection { columns_by_name, judgement_for_listing, buyer_review_judgement_read_frontier, - buyer_review_identity_split_frontier, JudgementBlank, JudgementEntered, JudgementUnreadable, @@ -1273,14 +1272,6 @@ test fn live_transport_frontiers_are_scaffold_not_terminal() -> Bool { Scaffold { dissolves_to: SubstrateMandatoryTag, bind: _ } => false Terminal { reason: _ } => false } - && match buyer_review_identity_split_frontier { - Scaffold { dissolves_to: SingleAuthority, bind: b } => - (b.module_path as String) == "gunbc.review_sheet_projection" - && (b.decl_name as String) == "review_sheet_listing_identity" - Scaffold { dissolves_to: RealizationDispatch, bind: _ } => false - Scaffold { dissolves_to: SubstrateMandatoryTag, bind: _ } => false - Terminal { reason: _ } => false - } && match buyer_review_judgement_read_frontier { Scaffold { dissolves_to: RealizationDispatch, bind: b } => (b.module_path as String) == "gunbc.review_sheet_projection" From 71da79bd72008238b87eaee3b07bcf041540087d Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 12 Sep 2026 08:37:43 +0000 Subject: [PATCH 11/13] Keep one classify_review_sheet and one first-column fold. Erasures are a parameter of classify, not a second name. Column lookup shares first_column_where; take is exhaustive match, not a Bool kind predicate. Co-authored-by: Cursor --- dag/gunbc/review_sheet_projection.dag | 104 +++++++++--------- dag/gunbc/review_sheet_sheets.dag | 4 +- .../review_sheet_converge_witness_test.dag | 55 +++++---- 3 files changed, 78 insertions(+), 85 deletions(-) diff --git a/dag/gunbc/review_sheet_projection.dag b/dag/gunbc/review_sheet_projection.dag index 7680cde7f8f..2e151febf65 100644 --- a/dag/gunbc/review_sheet_projection.dag +++ b/dag/gunbc/review_sheet_projection.dag @@ -206,36 +206,12 @@ fn first_present_column(acc: ReviewColumn?, next: ReviewColumn?) -> ReviewColumn } } -fn first_derived_field_column(schema: ReviewSheetSchema) -> ReviewColumn? { - fold(schema.columns, init: none, f: fn(acc, col) { - first_present_column(acc: acc, next: match col.kind { - DerivedField { source_field: _ } => Present { value: col } - OperatorEntered => none - ListingStanding => none - DerivedErasureMark => none - }) - }) -} - -fn first_listing_standing_column(schema: ReviewSheetSchema) -> ReviewColumn? { - fold(schema.columns, init: none, f: fn(acc, col) { - first_present_column(acc: acc, next: match col.kind { - ListingStanding => Present { value: col } - DerivedField { source_field: _ } => none - OperatorEntered => none - DerivedErasureMark => none - }) - }) -} - -fn first_erasure_mark_column(schema: ReviewSheetSchema) -> ReviewColumn? { +fn first_column_where( + schema: ReviewSheetSchema, + take: fn(ReviewColumn) -> ReviewColumn?, +) -> ReviewColumn? { fold(schema.columns, init: none, f: fn(acc, col) { - first_present_column(acc: acc, next: match col.kind { - DerivedErasureMark => Present { value: col } - DerivedField { source_field: _ } => none - OperatorEntered => none - ListingStanding => none - }) + first_present_column(acc: acc, next: take(col)) }) } @@ -441,7 +417,12 @@ fn read_row_listing_standing( match observed_cell_for_column( header_by_name: header_by_name, row: row, - column: first_listing_standing_column(schema: schema), + column: first_column_where(schema: schema, take: col => match col.kind { + ListingStanding => Present { value: col } + DerivedField { source_field: _ } => none + OperatorEntered => none + DerivedErasureMark => none + }), ) { KindAbsentFromSchema => none KindColumnMissingFromHeader { column: _ } => none @@ -457,7 +438,12 @@ fn read_row_erasure_mark( match observed_cell_for_column( header_by_name: header_by_name, row: row, - column: first_erasure_mark_column(schema: schema), + column: first_column_where(schema: schema, take: col => match col.kind { + DerivedErasureMark => Present { value: col } + DerivedField { source_field: _ } => none + OperatorEntered => none + ListingStanding => none + }), ) { KindAbsentFromSchema => none KindColumnMissingFromHeader { column: _ } => none @@ -540,7 +526,12 @@ fn authorize_derived_payload_erasure( match observed_cell_for_column( header_by_name: header_by_name, row: row, - column: first_listing_standing_column(schema: schema), + column: first_column_where(schema: schema, take: col => match col.kind { + ListingStanding => Present { value: col } + DerivedField { source_field: _ } => none + OperatorEntered => none + DerivedErasureMark => none + }), ) { KindAbsentFromSchema => ErasureRefused { reason: erasure_missing_standing_column_reason(listing_id: listing_id) } @@ -552,11 +543,21 @@ fn authorize_derived_payload_erasure( match read_listing_standing(cell: cell) { StandingKnown { state: standing_state } => if listing_standing_eq(a: standing_state, b: StandingGone) { - match first_derived_field_column(schema: schema) { + match first_column_where(schema: schema, take: col => match col.kind { + DerivedField { source_field: _ } => Present { value: col } + OperatorEntered => none + ListingStanding => none + DerivedErasureMark => none + }) { Absent => ErasureRefused { reason: erasure_has_no_derived_field_reason(listing_id: listing_id) } Present { value: _ } => - match first_erasure_mark_column(schema: schema) { + match first_column_where(schema: schema, take: col => match col.kind { + DerivedErasureMark => Present { value: col } + DerivedField { source_field: _ } => none + OperatorEntered => none + ListingStanding => none + }) { Absent => ErasureRefused { reason: erasure_has_no_erasure_mark_reason(listing_id: listing_id) } Present { value: _ } => ErasureAuthorized @@ -613,22 +614,19 @@ fn col_and_listing(column: NonEmptyStr, listing_id: NonEmptyStr) -> String { fn first_omitted_derived_column(row: ProjectionRow, schema: ReviewSheetSchema) -> NonEmptyStr? { let derived = derived_by_column(row: row) - fold(schema.columns, init: none, f: fn(acc, col) { - match acc { - Present { value: found } => Present { value: found } - Absent => - match col.kind { - DerivedField { source_field: _ } => - match derived.lookup(col.name as String) { - Absent => Present { value: col.name } - Present { value: _ } => none - } - OperatorEntered => none - ListingStanding => none - DerivedErasureMark => none - } - } - }) + match first_column_where(schema: schema, take: col => match col.kind { + DerivedField { source_field: _ } => + match derived.lookup(col.name as String) { + Absent => Present { value: col } + Present { value: _ } => none + } + OperatorEntered => none + ListingStanding => none + DerivedErasureMark => none + }) { + Absent => none + Present { value: col } => Present { value: col.name } + } } fn missing_schema_header(schema: ReviewSheetSchema, header_by_name: Map) -> NonEmptyStr? { @@ -1249,11 +1247,7 @@ fn append_listing_row( } } -fn classify_review_sheet(observed: ObservedSheet, projection: ReviewSheetProjection) -> ReviewSheetClassification { - classify_review_sheet_with_erasure(observed: observed, projection: projection, erasures: []) -} - -fn classify_review_sheet_with_erasure( +fn classify_review_sheet( observed: ObservedSheet, projection: ReviewSheetProjection, erasures: List, diff --git a/dag/gunbc/review_sheet_sheets.dag b/dag/gunbc/review_sheet_sheets.dag index f365ef5ad28..cf8e397c2a9 100644 --- a/dag/gunbc/review_sheet_sheets.dag +++ b/dag/gunbc/review_sheet_sheets.dag @@ -23,7 +23,7 @@ import gunbc.review_sheet_projection { ReviewSheetUnreadable, DerivedCellWrite, AppendListingRow, - classify_review_sheet_with_erasure, + classify_review_sheet, DerivedPayloadErasure, conflict_reason, a1_cell_eq, @@ -415,7 +415,7 @@ fn review_sheet_ensure( ) as NonEmptyStr } } else { - let classified = classify_review_sheet_with_erasure( + let classified = classify_review_sheet( observed: observed, projection: projection, erasures: erasures, diff --git a/dag/test/claim/review_sheet_converge_witness_test.dag b/dag/test/claim/review_sheet_converge_witness_test.dag index b15bea2de9a..edbd34064d0 100644 --- a/dag/test/claim/review_sheet_converge_witness_test.dag +++ b/dag/test/claim/review_sheet_converge_witness_test.dag @@ -27,7 +27,6 @@ import gunbc.review_sheet_projection { write_plan_covers_column_letter, a1_row_span_includes_letter, classify_review_sheet, - classify_review_sheet_with_erasure, DerivedPayloadErasure, conflict_reason, append_listing_row, @@ -241,9 +240,9 @@ test fn converge_twice_unchanged_is_noop() -> Bool { headers: w_headers(), rows: [["L1", "alpha", w_standing_observed(), ""]], ) - match classify_review_sheet(observed: observed, projection: projection) { + match classify_review_sheet(observed: observed, projection: projection, erasures: []) { ReviewSheetConverged => - match classify_review_sheet(observed: observed, projection: projection) { + match classify_review_sheet(observed: observed, projection: projection, erasures: []) { ReviewSheetConverged => true ReviewSheetNeedsWrite { plan: _ } => false ReviewSheetConflict { conflict: _ } => false @@ -261,7 +260,7 @@ test fn converge_preserves_populated_human_column() -> Bool { headers: w_headers(), rows: [["L1", "", w_standing_observed(), "buy"]], ) - match classify_review_sheet(observed: observed, projection: projection) { + match classify_review_sheet(observed: observed, projection: projection, erasures: []) { ReviewSheetNeedsWrite { plan: p } => write_plan_preserves_operator_columns(schema: w_schema(), plan: p) && match judgement_for_listing( @@ -287,7 +286,7 @@ test fn converge_over_hand_edited_derived_cell_refuses_and_names_the_cell() -> B headers: w_headers(), rows: [["L1", "hand-edited", w_standing_observed(), ""]], ) - match classify_review_sheet(observed: observed, projection: projection) { + match classify_review_sheet(observed: observed, projection: projection, erasures: []) { ReviewSheetConflict { conflict: c } => string_contains(s: conflict_reason(conflict: c) as String, pattern: "listing=L1") && string_contains(s: conflict_reason(conflict: c) as String, pattern: "column=title") @@ -320,9 +319,9 @@ test fn resorted_sheet_does_not_move_a_judgement() -> Bool { ["L1", "alpha", w_standing_observed(), "buy"], ], ) - match classify_review_sheet(observed: sorted, projection: projection) { + match classify_review_sheet(observed: sorted, projection: projection, erasures: []) { ReviewSheetConverged => - match classify_review_sheet(observed: resorted, projection: projection) { + match classify_review_sheet(observed: resorted, projection: projection, erasures: []) { ReviewSheetConverged => match judgement_for_listing( observed: resorted, @@ -360,7 +359,7 @@ test fn listing_gone_with_judgement_is_retained_and_marked() -> Bool { headers: w_headers(), rows: [["L1", "alpha", w_standing_observed(), "buy"]], ) - match classify_review_sheet(observed: observed, projection: projection) { + match classify_review_sheet(observed: observed, projection: projection, erasures: []) { ReviewSheetNeedsWrite { plan: p } => write_plan_preserves_operator_columns(schema: w_schema(), plan: p) && (p.appends |> count) == 0 @@ -391,7 +390,7 @@ test fn derived_payload_erasure_blanks_derived_keeps_judgement_and_listing_gone( headers: w_headers(), rows: [["L1", "alpha", w_standing_gone(), "buy"]], ) - match classify_review_sheet_with_erasure( + match classify_review_sheet( observed: observed, projection: projection, erasures: [w_erasure(listing_id: "L1" as NonEmptyStr)], @@ -433,7 +432,7 @@ test fn derived_payload_erasure_refuses_while_listing_still_projected() -> Bool headers: w_headers(), rows: [["L1", "alpha", w_standing_observed(), "buy"]], ) - match classify_review_sheet_with_erasure( + match classify_review_sheet( observed: observed, projection: projection, erasures: [w_erasure(listing_id: "L1" as NonEmptyStr)], @@ -454,7 +453,7 @@ test fn derived_payload_erasure_refuses_until_standing_is_listing_gone() -> Bool headers: w_headers(), rows: [["L1", "alpha", w_standing_observed(), "buy"]], ) - match classify_review_sheet_with_erasure( + match classify_review_sheet( observed: observed, projection: projection, erasures: [w_erasure(listing_id: "L1" as NonEmptyStr)], @@ -475,7 +474,7 @@ test fn derived_payload_erasure_refuses_without_derived_columns_when_standing_is headers: ["listing_id", "standing", "judgement"], rows: [["L1", w_standing_observed(), "buy"]], ) - match classify_review_sheet_with_erasure( + match classify_review_sheet( observed: observed, projection: projection, erasures: [w_erasure(listing_id: "L1" as NonEmptyStr)], @@ -499,7 +498,7 @@ test fn derived_payload_erasure_refuses_without_derived_columns_while_still_proj headers: ["listing_id", "standing", "judgement"], rows: [["L1", w_standing_observed(), "buy"]], ) - match classify_review_sheet_with_erasure( + match classify_review_sheet( observed: observed, projection: projection, erasures: [w_erasure(listing_id: "L1" as NonEmptyStr)], @@ -520,7 +519,7 @@ test fn derived_payload_erasure_refuses_without_derived_columns_when_gone_and_au headers: ["listing_id", "standing", "judgement"], rows: [["L1", w_standing_gone(), "buy"]], ) - match classify_review_sheet_with_erasure( + match classify_review_sheet( observed: observed, projection: projection, erasures: [w_erasure(listing_id: "L1" as NonEmptyStr)], @@ -538,7 +537,7 @@ test fn derived_payload_erasure_refuses_without_derived_columns_when_gone_and_au test fn derived_payload_erasure_refuses_listing_not_on_sheet() -> Bool { let projection = w_projection(rows: []) let observed = w_observed(headers: w_headers(), rows: []) - match classify_review_sheet_with_erasure( + match classify_review_sheet( observed: observed, projection: projection, erasures: [w_erasure(listing_id: "L1" as NonEmptyStr)], @@ -558,7 +557,7 @@ test fn derived_payload_erasure_already_empty_derived_is_converged() -> Bool { headers: w_headers(), rows: [["L1", "", w_standing_gone(), "buy", w_erasure_applied()]], ) - match classify_review_sheet_with_erasure( + match classify_review_sheet( observed: observed, projection: projection, erasures: [w_erasure(listing_id: "L1" as NonEmptyStr)], @@ -576,7 +575,7 @@ test fn derived_payload_erasure_duplicate_listing_is_unreadable() -> Bool { headers: w_headers(), rows: [["L1", "alpha", w_standing_gone(), "buy"]], ) - match classify_review_sheet_with_erasure( + match classify_review_sheet( observed: observed, projection: projection, erasures: [ @@ -645,7 +644,7 @@ test fn gone_listing_with_derived_payload_still_restores_when_it_returns() -> Bo headers: w_headers(), rows: [["L1", "alpha", w_standing_gone(), "buy"]], ) - match classify_review_sheet(observed: observed, projection: projection) { + match classify_review_sheet(observed: observed, projection: projection, erasures: []) { ReviewSheetNeedsWrite { plan: p } => write_plan_preserves_operator_columns(schema: w_schema(), plan: p) && fold(p.patches, init: false, f: fn(acc, w) { @@ -670,7 +669,7 @@ test fn applied_erasure_mark_does_not_restore_derived_when_the_listing_returns() headers: w_headers(), rows: [["L1", "", w_standing_gone(), "buy", w_erasure_applied()]], ) - match classify_review_sheet_with_erasure( + match classify_review_sheet( observed: observed, projection: projection, erasures: [], @@ -700,7 +699,7 @@ test fn derived_payload_erasure_refuses_without_erasure_mark_column() -> Bool { headers: ["listing_id", "title", "standing", "judgement"], rows: [["L1", "alpha", w_standing_gone(), "buy"]], ) - match classify_review_sheet_with_erasure( + match classify_review_sheet( observed: observed, projection: projection, erasures: [w_erasure(listing_id: "L1" as NonEmptyStr)], @@ -721,7 +720,7 @@ test fn derived_payload_erasure_refuses_when_standing_header_is_missing() -> Boo headers: ["listing_id", "title", "judgement", "erasure"], rows: [["L1", "alpha", "buy", ""]], ) - match classify_review_sheet_with_erasure( + match classify_review_sheet( observed: observed, projection: projection, erasures: [w_erasure(listing_id: "L1" as NonEmptyStr)], @@ -997,7 +996,7 @@ test fn duplicate_header_refuses_rather_than_last_wins() -> Bool { headers: ["listing_id", "title", "standing", "judgement", "erasure", "title"], rows: [["L1", "alpha", w_standing_observed(), "", "hand-edit"]], ) - match classify_review_sheet(observed: observed, projection: projection) { + match classify_review_sheet(observed: observed, projection: projection, erasures: []) { ReviewSheetUnreadable { reason: r } => string_contains(s: r as String, pattern: "duplicate header title") && !string_contains(s: r as String, pattern: "derived cell conflict") @@ -1013,7 +1012,7 @@ test fn foreign_standing_cell_refuses_and_names_the_cell() -> Bool { headers: w_headers(), rows: [["L1", "alpha", "Obsered", ""]], ) - match classify_review_sheet(observed: observed, projection: projection) { + match classify_review_sheet(observed: observed, projection: projection, erasures: []) { ReviewSheetConflict { conflict: c } => string_contains(s: conflict_reason(conflict: c) as String, pattern: "listing=L1") && string_contains(s: conflict_reason(conflict: c) as String, pattern: "column=standing") @@ -1064,7 +1063,7 @@ test fn a_twenty_seventh_column_refuses_rather_than_writing_a_fake_a1() -> Bool headers: concat(["listing_id"], a1_column_letters), rows: [concat(["L1"], map(a1_column_letters, n => ""))], ) - match classify_review_sheet(observed: observed, projection: projection) { + match classify_review_sheet(observed: observed, projection: projection, erasures: []) { ReviewSheetUnreadable { reason: r } => string_contains(s: r as String, pattern: "unrepresentable A1 column index 26") ReviewSheetConverged => false ReviewSheetNeedsWrite { plan: _ } => false @@ -1075,7 +1074,7 @@ test fn a_twenty_seventh_column_refuses_rather_than_writing_a_fake_a1() -> Bool test fn absent_listing_appends_a_located_row() -> Bool { let projection = w_projection(rows: [w_row(listing_id: "L2" as NonEmptyStr, title: "beta")]) let observed = w_observed(headers: w_headers(), rows: []) - match classify_review_sheet(observed: observed, projection: projection) { + match classify_review_sheet(observed: observed, projection: projection, erasures: []) { ReviewSheetNeedsWrite { plan: p } => write_plan_preserves_operator_columns(schema: w_schema(), plan: p) && !write_plan_touches_column(plan: p, column: "judgement" as NonEmptyStr) @@ -1139,7 +1138,7 @@ test fn append_follows_observed_header_order_not_schema_order() -> Bool { let projection = w_projection(rows: [w_row(listing_id: "L2" as NonEmptyStr, title: "alpha")]) let headers = ["listing_id", "standing", "title", "judgement", "erasure"] let observed = w_observed(headers: headers, rows: []) - match classify_review_sheet(observed: observed, projection: projection) { + match classify_review_sheet(observed: observed, projection: projection, erasures: []) { ReviewSheetNeedsWrite { plan: p } => write_plan_preserves_operator_columns(schema: w_schema(), plan: p) && !write_plan_touches_column(plan: p, column: "judgement" as NonEmptyStr) @@ -1234,7 +1233,7 @@ test fn omitted_derived_column_on_blank_sheet_refuses_rather_than_noop() -> Bool headers: w_headers(), rows: [["L1", "", w_standing_observed(), ""]], ) - match classify_review_sheet(observed: observed, projection: projection) { + match classify_review_sheet(observed: observed, projection: projection, erasures: []) { ReviewSheetUnreadable { reason: r } => string_contains(s: r as String, pattern: "projection omitted derived column title") && string_contains(s: r as String, pattern: "listing=L1") @@ -1253,7 +1252,7 @@ test fn omitted_derived_column_on_populated_sheet_is_not_a_conflict() -> Bool { headers: w_headers(), rows: [["L1", "alpha", w_standing_observed(), ""]], ) - match classify_review_sheet(observed: observed, projection: projection) { + match classify_review_sheet(observed: observed, projection: projection, erasures: []) { ReviewSheetUnreadable { reason: r } => string_contains(s: r as String, pattern: "projection omitted derived column title") && !string_contains(s: r as String, pattern: "derived cell conflict") From 11e1dc5c72e1dc1e24e145370c24c1a595327289 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 12 Sep 2026 09:39:14 +0000 Subject: [PATCH 12/13] Delete unused read_row_listing_standing. Authorize already matches ObservedKindCell for schema-absent vs header-missing; a ListingStandingRead? wrapper had no call site and collapsed those causes. Co-authored-by: Cursor --- dag/gunbc/review_sheet_projection.dag | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/dag/gunbc/review_sheet_projection.dag b/dag/gunbc/review_sheet_projection.dag index 2e151febf65..310a0941372 100644 --- a/dag/gunbc/review_sheet_projection.dag +++ b/dag/gunbc/review_sheet_projection.dag @@ -409,27 +409,6 @@ fn listing_has_erasure(erasure_by_id: Map, listin } } -fn read_row_listing_standing( - schema: ReviewSheetSchema, - header_by_name: Map, - row: ObservedSheetRow, -) -> ListingStandingRead? { - match observed_cell_for_column( - header_by_name: header_by_name, - row: row, - column: first_column_where(schema: schema, take: col => match col.kind { - ListingStanding => Present { value: col } - DerivedField { source_field: _ } => none - OperatorEntered => none - DerivedErasureMark => none - }), - ) { - KindAbsentFromSchema => none - KindColumnMissingFromHeader { column: _ } => none - KindCell { value: cell } => Present { value: read_listing_standing(cell: cell) } - } -} - fn read_row_erasure_mark( schema: ReviewSheetSchema, header_by_name: Map, From 5ad8f74737520b1ff1dc3e0e248450122d228720 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 12 Sep 2026 09:47:43 +0000 Subject: [PATCH 13/13] Finish rebase: keep String? append cells and restore columns_by_name. Conflict markers were left in append_cell_for_column; keep parent skip of operator columns and the door's blank DerivedErasureMark, and restore columns_by_name so classify/append still resolve. Co-authored-by: Cursor --- dag/gunbc/review_sheet_projection.dag | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dag/gunbc/review_sheet_projection.dag b/dag/gunbc/review_sheet_projection.dag index 310a0941372..2c75821bed7 100644 --- a/dag/gunbc/review_sheet_projection.dag +++ b/dag/gunbc/review_sheet_projection.dag @@ -965,17 +965,17 @@ fn fold_columns_for_row( }) } +fn columns_by_name(schema: ReviewSheetSchema) -> Map { + fold(schema.columns, init: empty_map(), f: fn(acc, c) { + map_insert(acc, c.name as String, c) + }) +} + fn append_cell_for_column(column: ReviewColumn, derived: Map) -> String? { match column.kind { -<<<<<<< HEAD ListingStanding => Present { value: listing_standing_label(state: StandingObserved) } OperatorEntered => none DerivedErasureMark => Present { value: "" } -======= - ListingStanding => listing_standing_label(state: StandingObserved) - OperatorEntered => "" - DerivedErasureMark => "" ->>>>>>> 609ea55a52 (Drop the uninhabitable unmarked mark state and one kind-selector fold.) DerivedField { source_field: _ } => match derived.lookup(column.name as String) { Present { value: v } => Present { value: v }