Skip to content

Commit 50fa164

Browse files
saltmachineclaude
andcommitted
fix: show key value in refusals and add verdict line
In key mode, refusal examples now show the key value (e.g., key "B") instead of a record number. All refusal bodies now begin with "Cannot produce a verdict." so the output is self-contained even without the RVL ERROR banner. Closes #8, closes #9 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 39fcd44 commit 50fa164

File tree

12 files changed

+112
-29
lines changed

12 files changed

+112
-29
lines changed

src/numeric/missingness.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ pub fn build_missingness_refusal(
1717
record: error.row_id,
1818
column: error.column,
1919
value: error.present_value,
20+
key_value: None,
2021
};
2122
RefusalDetail::with_default_next(kind, paths)
2223
}

src/orchestrator.rs

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ struct RefusalContext<'a> {
8585
struct RowRef {
8686
old_record: u64,
8787
new_record: u64,
88+
key: Option<Vec<u8>>,
8889
}
8990

9091
impl RowRef {
@@ -389,6 +390,7 @@ fn run_diff(
389390
RowRef {
390391
old_record: row.old.record_number,
391392
new_record: row.new.record_number,
393+
key: Some(row.key.clone()),
392394
},
393395
row.old.fields.as_slice(),
394396
row.new.fields.as_slice(),
@@ -416,6 +418,7 @@ fn run_diff(
416418
RowRef {
417419
old_record: record,
418420
new_record: record,
421+
key: None,
419422
},
420423
old_row.as_slice(),
421424
new_row.as_slice(),
@@ -960,13 +963,15 @@ fn map_column_error(err: ColumnTypingError<RowRef>, paths: RerunPaths<'_>) -> Re
960963
ColumnSide::Old => FileSide::Old,
961964
ColumnSide::New => FileSide::New,
962965
};
966+
let key_value = detail.row_id.key.clone();
963967
RefusalPayload::with_default_next(
964968
RefusalCode::MixedTypes,
965969
RefusalKind::MixedTypes {
966970
file,
967971
record: detail.row_id.record_for(detail.side),
968972
column: detail.column,
969973
value: detail.value,
974+
key_value,
970975
},
971976
paths,
972977
)
@@ -980,13 +985,15 @@ fn map_column_error(err: ColumnTypingError<RowRef>, paths: RerunPaths<'_>) -> Re
980985
ColumnSide::Old => FileSide::Old,
981986
ColumnSide::New => FileSide::New,
982987
};
988+
let key_value = detail.row_id.key.clone();
983989
RefusalPayload::with_default_next(
984990
RefusalCode::Missingness,
985991
RefusalKind::Missingness {
986992
file,
987993
record: detail.row_id.record_for(present_side),
988994
column: detail.column,
989995
value: detail.present_value,
996+
key_value,
990997
},
991998
paths,
992999
)
@@ -1574,24 +1581,38 @@ fn refusal_detail_json(detail: &RefusalDetail) -> Value {
15741581
record,
15751582
column,
15761583
value,
1577-
} => json!({
1578-
"file": file.as_str(),
1579-
"record": record,
1580-
"column": encode_identifier_json(column),
1581-
"value": encode_identifier_json(value),
1582-
}),
1584+
key_value,
1585+
} => {
1586+
let mut obj = json!({
1587+
"file": file.as_str(),
1588+
"record": record,
1589+
"column": encode_identifier_json(column),
1590+
"value": encode_identifier_json(value),
1591+
});
1592+
if let Some(key) = key_value {
1593+
obj["key"] = json!(encode_identifier_json(key));
1594+
}
1595+
obj
1596+
}
15831597
RefusalKind::NoNumeric => json!({}),
15841598
RefusalKind::Missingness {
15851599
file,
15861600
record,
15871601
column,
15881602
value,
1589-
} => json!({
1590-
"file": file.as_str(),
1591-
"record": record,
1592-
"column": encode_identifier_json(column),
1593-
"value": encode_identifier_json(value),
1594-
}),
1603+
key_value,
1604+
} => {
1605+
let mut obj = json!({
1606+
"file": file.as_str(),
1607+
"record": record,
1608+
"column": encode_identifier_json(column),
1609+
"value": encode_identifier_json(value),
1610+
});
1611+
if let Some(key) = key_value {
1612+
obj["key"] = json!(encode_identifier_json(key));
1613+
}
1614+
obj
1615+
}
15951616
RefusalKind::Diffuse {
15961617
top_k_coverage,
15971618
threshold,

src/output/human/refusal.rs

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ pub struct RefusalBody<'a> {
1414
}
1515

1616
pub fn render_refusal_body(ctx: &RefusalBody<'_>) -> Vec<String> {
17-
let mut lines = Vec::with_capacity(3);
17+
let mut lines = Vec::with_capacity(4);
18+
lines.push("Cannot produce a verdict.".to_string());
1819
lines.push(format!("Reason ({}): {}.", ctx.code, ctx.code.reason()));
1920
lines.push(render_example_line(ctx.detail, ctx.old_name, ctx.new_name));
2021
lines.push(format!("Next: {}", ctx.detail.next));
@@ -148,29 +149,45 @@ fn render_example_line(detail: &RefusalDetail, old_name: &str, new_name: &str) -
148149
record,
149150
column,
150151
value,
152+
key_value,
151153
} => {
152-
let file = file_label(*file, old_name, new_name);
153154
let column = render_identifier_human(column);
154155
let value = render_identifier_human(value);
155-
format!(
156-
"Example: {file} data record {} column \"{column}\" has non-numeric value \"{value}\".",
157-
format_count_u64(*record)
158-
)
156+
if let Some(key) = key_value {
157+
let key = render_identifier_human(key);
158+
format!(
159+
"Example: key \"{key}\" column \"{column}\" has non-numeric value \"{value}\"."
160+
)
161+
} else {
162+
let file = file_label(*file, old_name, new_name);
163+
format!(
164+
"Example: {file} data record {} column \"{column}\" has non-numeric value \"{value}\".",
165+
format_count_u64(*record)
166+
)
167+
}
159168
}
160169
RefusalKind::NoNumeric => "Example: no numeric columns in common.".to_string(),
161170
RefusalKind::Missingness {
162171
file,
163172
record,
164173
column,
165174
value,
175+
key_value,
166176
} => {
167-
let file = file_label(*file, old_name, new_name);
168177
let column = render_identifier_human(column);
169178
let value = render_identifier_human(value);
170-
format!(
171-
"Example: {file} data record {} column \"{column}\" has numeric value \"{value}\" while the other side is missing.",
172-
format_count_u64(*record)
173-
)
179+
if let Some(key) = key_value {
180+
let key = render_identifier_human(key);
181+
format!(
182+
"Example: key \"{key}\" column \"{column}\" has numeric value \"{value}\" while the other side is missing."
183+
)
184+
} else {
185+
let file = file_label(*file, old_name, new_name);
186+
format!(
187+
"Example: {file} data record {} column \"{column}\" has numeric value \"{value}\" while the other side is missing.",
188+
format_count_u64(*record)
189+
)
190+
}
174191
}
175192
RefusalKind::Diffuse {
176193
top_k_coverage,
@@ -253,12 +270,13 @@ mod tests {
253270
new_name: "new.csv",
254271
};
255272
let lines = render_refusal_body(&ctx);
256-
assert_eq!(lines[0], "Reason (E_KEY_DUP): duplicate key values.");
273+
assert_eq!(lines[0], "Cannot produce a verdict.");
274+
assert_eq!(lines[1], "Reason (E_KEY_DUP): duplicate key values.");
257275
assert_eq!(
258-
lines[1],
276+
lines[2],
259277
"Example: old.csv data record 184 duplicates key \"A123\"."
260278
);
261-
assert!(lines[2].starts_with("Next:"));
279+
assert!(lines[3].starts_with("Next:"));
262280
}
263281

264282
#[test]
@@ -277,11 +295,12 @@ mod tests {
277295
new_name: "new.csv",
278296
};
279297
let lines = render_refusal_body(&ctx);
298+
assert_eq!(lines[0], "Cannot produce a verdict.");
280299
assert_eq!(
281-
lines[0],
300+
lines[1],
282301
"Reason (E_DIFFUSE): diffuse change below coverage threshold."
283302
);
284-
assert_eq!(lines[1], "Example: top_k_coverage=80.0% threshold=95.0%.");
303+
assert_eq!(lines[2], "Example: top_k_coverage=80.0% threshold=95.0%.");
285304
}
286305

287306
#[test]
@@ -306,7 +325,7 @@ mod tests {
306325
};
307326
let lines = render_refusal_body(&ctx);
308327
assert_eq!(
309-
lines[1],
328+
lines[2],
310329
"Example: old.csv delimiter ambiguous among [comma, tab]."
311330
);
312331
}

src/refusal/details.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,13 +102,15 @@ pub enum RefusalKind {
102102
record: u64,
103103
column: Vec<u8>,
104104
value: Vec<u8>,
105+
key_value: Option<Vec<u8>>,
105106
},
106107
NoNumeric,
107108
Missingness {
108109
file: FileSide,
109110
record: u64,
110111
column: Vec<u8>,
111112
value: Vec<u8>,
113+
key_value: Option<Vec<u8>>,
112114
},
113115
Diffuse {
114116
top_k_coverage: f64,

tests/fixtures/regression/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,14 @@ Each case has expected human and JSON outputs checked by `tests/regression.rs`.
1111
- `basic.human.txt`
1212
- `basic.json`
1313

14+
## missingness_key
15+
- Files: `missingness_key_old.csv`, `missingness_key_new.csv`
16+
- Mode: key (`--key id`)
17+
- Change: old has numeric value, new has missing value — triggers `E_MISSINGNESS`
18+
- Verifies key value (not record number) appears in the refusal example
19+
- Expected outputs:
20+
- `missingness_key.human.txt`
21+
- `missingness_key.json`
22+
1423
Notes
1524
- Paths in the golden outputs are relative (e.g., `tests/fixtures/regression/basic_old.csv`) to keep tests stable across machines.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
RVL ERROR (E_MISSINGNESS)
2+
3+
Compared: missingness_key_old.csv -> missingness_key_new.csv
4+
Alignment: key=id
5+
Dialect(old): delimiter=, quote=" escape=none
6+
Dialect(new): delimiter=, quote=" escape=none
7+
Settings: threshold=95.0% tolerance=1e-9
8+
9+
Cannot produce a verdict.
10+
Reason (E_MISSINGNESS): missing value vs numeric value.
11+
Example: key "B" column "amount" has numeric value "200.75" while the other side is missing.
12+
Next: fill missing values or remove the column, then rerun
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"version":"rvl.v0","outcome":"REFUSAL","files":{"old":"tests/fixtures/regression/missingness_key_old.csv","new":"tests/fixtures/regression/missingness_key_new.csv"},"alignment":{"mode":"key","key_column":"u8:id"},"dialect":{"old":{"delimiter":",","quote":"\"","escape":null},"new":{"delimiter":",","quote":"\"","escape":null}},"threshold":0.95,"tolerance":1e-9,"counts":{"rows_old":null,"rows_new":null,"rows_aligned":null,"columns_old":null,"columns_new":null,"columns_common":null,"columns_old_only":null,"columns_new_only":null,"numeric_columns":null,"numeric_cells_checked":null,"numeric_cells_changed":null},"metrics":{"total_change":null,"max_abs_delta":null,"top_k_coverage":null},"limits":{"max_contributors":25},"contributors":[],"refusal":{"code":"E_MISSINGNESS","message":"missing value vs numeric value","detail":{"column":"u8:amount","file":"old","key":"u8:B","record":2,"value":"u8:200.75"}}}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
id,amount
2+
A,100.50
3+
B,
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
id,amount
2+
A,100.50
3+
B,200.75

tests/fixtures/regression/no_numeric.human.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Dialect(old): delimiter=, quote=" escape=none
66
Dialect(new): delimiter=, quote=" escape=none
77
Settings: threshold=95.0% tolerance=1e-9
88

9+
Cannot produce a verdict.
910
Reason (E_NO_NUMERIC): no numeric columns in common.
1011
Example: no numeric columns in common.
1112
Next: ensure common numeric columns exist (or adjust inputs) and rerun

0 commit comments

Comments
 (0)