Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/tally/compatibility/compatibility-matrix.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"schema_version": 1,
"bridge_commit_sha": "be1c20cc3fd66fa1ece196505c69f26e555e4b8e",
"compatibility_surface_sha256": "c86a3689f0981f6015ddb908999701cb795bd8650330fd47d0d15414e5262986",
"compatibility_surface_sha256": "eaa41c1142c4c2652bd4fff1b748ed1ca7581fc7d5240c0478f5fc85f8baf4d8",
"claims": [
{
"claim_id": "erp9-6-6-3-windows-education-xml-one-company",
Expand Down
4 changes: 2 additions & 2 deletions docs/tally/compatibility/compatibility-surface.json
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@
},
{
"path": "src-tauri/src/agent_import.rs",
"sha256": "8bebc2497498c905f5eee783251b40b7083695ab958a82b17094159330a8e983"
"sha256": "47c1dc77fa33d7e4bcece1cb289e4a3535881391fe45f392e2dca0daa937930f"
},
{
"path": "src-tauri/src/agent_ledgers.rs",
Expand Down Expand Up @@ -866,5 +866,5 @@
"sha256": "a8ac2714fecf51947f2822c8c46d7ce2e8602c732780ff60566a7771f0836f9a"
}
],
"manifest_sha256": "c86a3689f0981f6015ddb908999701cb795bd8650330fd47d0d15414e5262986"
"manifest_sha256": "eaa41c1142c4c2652bd4fff1b748ed1ca7581fc7d5240c0478f5fc85f8baf4d8"
}
27 changes: 24 additions & 3 deletions src-tauri/src/agent_import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1645,10 +1645,23 @@ fn master_match_json(binding: &EntityBinding) -> Value {
BindingBasis::ExactName => "exact",
BindingBasis::Identifier => "identifier",
};
// A catalogue name can now hold characters the *proposal* side
// refuses -- a ledger genuinely named across two lines, say. That
// asymmetry is deliberate: the book's name is a fact, a caller's
// proposed name is input. But it means the live spelling is not
// always something the caller can send back, and the guidance below
// used to tell them to copy it regardless. Following that failed the
// whole batch on `master_name_unsafe`, because `source_entities`
// collects into one Result and refuses on the first bad name.
//
// Ask the proposal constructor rather than restating its rule, so
// the two can never disagree about what is admissible.
let importable = SourceEntity::new(binding.position, catalog_name).is_ok();
json!({
"requested": requested,
"match_state": match_state,
"exact_live_spelling": party_name(catalog_name.clone()),
"importable": importable,
})
}
BindingStatus::Ambiguous(unresolved) | BindingStatus::Unmatched(unresolved) => {
Expand Down Expand Up @@ -1711,12 +1724,20 @@ fn master_match_json(binding: &EntityBinding) -> Value {

fn master_recovery_guidance(report: &[Value]) -> String {
let mut guidance = Vec::new();
if report.iter().any(|master| {
master["match_state"] == "identifier" && master["importable"] != Value::Bool(false)
}) {
guidance
.push("For identifier-bound entries, copy exact_live_spelling from this fresh result.");
}
// Said separately, because the remedy is the opposite one: this spelling
// cannot be copied back at all, and no retry of this payload will post
// against that ledger.
if report
.iter()
.any(|master| master["match_state"] == "identifier")
.any(|master| master["importable"] == Value::Bool(false))
{
guidance
.push("For identifier-bound entries, copy exact_live_spelling from this fresh result.");
guidance.push("One matched ledger is named with a character imports do not accept, so its exact_live_spelling cannot be sent back; have an operator rename it in Tally, then run validate_masters again.");
}
if report
.iter()
Expand Down
53 changes: 53 additions & 0 deletions src-tauri/src/agent_import_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1632,6 +1632,59 @@ fn import_recovery_guidance_names_the_state_and_next_safe_read() {
assert!(guidance.contains("validate_masters again before building"));
}

/// A book may legitimately hold a ledger named across two lines. The catalogue
/// carries that name verbatim -- it is a fact about the book -- while the
/// proposal side still refuses a control character, because a caller's name is
/// input. The gap between those two correct rules is a spelling the tool can
/// report but the caller cannot send back.
///
/// Telling them to copy it anyway failed the *entire* batch: `source_entities`
/// collects into one Result and refuses on the first bad name, so one such
/// ledger takes every voucher in the request down with it.
#[test]
fn a_live_spelling_imports_cannot_accept_is_not_offered_for_copying() {
let two_line = "GAMMA 5550000002\r\nSecond Line";
let matched = one_master_match("GAMMA 5550000002", &[two_line]);

assert_eq!(
matched["match_state"], "identifier",
"the ledger still binds on its embedded identifier"
);
// Party names are wrapped in the egress marker, so read through it rather
// than off the field.
assert_eq!(
matched["exact_live_spelling"][super::super::PARTY_NAME_MARKER].as_str(),
Some(two_line),
"the book's own spelling is still reported verbatim, because it is what the book holds"
);
assert_eq!(
matched["importable"], false,
"but it is marked as one the import side will refuse"
);

let guidance = master_recovery_guidance(&[matched]);
assert!(
!guidance.contains("copy exact_live_spelling"),
"the tool must not instruct a caller to copy a spelling that fails the whole batch"
);
assert!(
guidance.contains("rename it in Tally"),
"and must say what the operator can actually do instead"
);
}

/// The ordinary identifier match is unaffected: its spelling is copyable and is
/// still offered.
#[test]
fn an_importable_live_spelling_is_still_offered_for_copying() {
let matched = one_master_match("GAMMA 5550000001", &["GAMMA (5550000001)"]);
assert_eq!(matched["match_state"], "identifier");
assert_eq!(matched["importable"], true);
let guidance = master_recovery_guidance(&[matched]);
assert!(guidance.contains("copy exact_live_spelling"));
assert!(!guidance.contains("rename it in Tally"));
}

#[tokio::test]
async fn import_bounds_distinct_ledger_names_before_tally_without_reducing_voucher_limit() {
let mut repeated = payload();
Expand Down