diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ecb0e10..42223e1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -148,14 +148,9 @@ jobs: working-directory: src-tauri run: >- cargo test --locked - -p bridge-tally-canonical -p bridge-tally-core - -p bridge-tally-incremental - -p bridge-tally-observability -p bridge-tally-protocol - -p bridge-tally-runtime -p bridge-tally-transport - -p bridge-tally-write -p tally-protocol-simulator - name: Test portable Tally qualification tools working-directory: tools @@ -165,11 +160,6 @@ jobs: run: >- cargo test --locked -p bridge-tally-protocol --features jsonex-parser,jsonex-request-builder,india-tax-observation-parser,bills-payments-observation-parser,bills-native-outstandings-probe - - name: Test sealed canary payload capsule - working-directory: src-tauri - run: >- - cargo test --locked -p bridge-tally-write - --features fixture-canary-dispatch-seam - name: Test isolated native outstandings qualification path working-directory: tools run: | @@ -180,14 +170,9 @@ jobs: working-directory: src-tauri run: >- cargo clippy --locked - -p bridge-tally-canonical -p bridge-tally-core - -p bridge-tally-incremental - -p bridge-tally-observability -p bridge-tally-protocol - -p bridge-tally-runtime -p bridge-tally-transport - -p bridge-tally-write -p tally-protocol-simulator --all-targets -- -D warnings - name: Lint portable Tally qualification tools @@ -199,12 +184,6 @@ jobs: cargo clippy --locked -p bridge-tally-protocol --features jsonex-parser,jsonex-request-builder,india-tax-observation-parser,bills-payments-observation-parser,bills-native-outstandings-probe --all-targets -- -D warnings - - name: Lint sealed canary payload capsule - working-directory: src-tauri - run: >- - cargo clippy --locked -p bridge-tally-write - --features fixture-canary-dispatch-seam - -- -D warnings - name: Lint isolated native outstandings qualification path working-directory: tools run: | diff --git a/THIRD_PARTY_LICENSES_RUST.txt b/THIRD_PARTY_LICENSES_RUST.txt index 2797cdf..98314dd 100644 --- a/THIRD_PARTY_LICENSES_RUST.txt +++ b/THIRD_PARTY_LICENSES_RUST.txt @@ -431,10 +431,6 @@ futures-io 0.3.34 License: MIT OR Apache-2.0 Source: https://github.com/rust-lang/futures-rs -futures-macro 0.3.34 -License: MIT OR Apache-2.0 -Source: https://github.com/rust-lang/futures-rs - futures-sink 0.3.34 License: MIT OR Apache-2.0 Source: https://github.com/rust-lang/futures-rs @@ -4335,7 +4331,6 @@ Apache License 2.0 - futures-core 0.3.34 - futures-executor 0.3.34 - futures-io 0.3.34 -- futures-macro 0.3.34 - futures-sink 0.3.34 - futures-task 0.3.34 - futures-util 0.3.34 diff --git a/docs/tally/compatibility/compatibility-matrix.json b/docs/tally/compatibility/compatibility-matrix.json index 4075f45..2c76339 100644 --- a/docs/tally/compatibility/compatibility-matrix.json +++ b/docs/tally/compatibility/compatibility-matrix.json @@ -1,7 +1,7 @@ { "schema_version": 1, "bridge_commit_sha": "be1c20cc3fd66fa1ece196505c69f26e555e4b8e", - "compatibility_surface_sha256": "8c07bb0159cb594a62e5c62d8741a822b01a5655889b9ceec1a939b345b1267c", + "compatibility_surface_sha256": "af8d0ba7d679001bd44a954ce90f9b164188a146af44efe55d74c472d29d18e5", "claims": [ { "claim_id": "erp9-6-6-3-windows-education-xml-one-company", diff --git a/docs/tally/compatibility/compatibility-surface.json b/docs/tally/compatibility/compatibility-surface.json index de5c4cb..3bee61a 100644 --- a/docs/tally/compatibility/compatibility-surface.json +++ b/docs/tally/compatibility/compatibility-surface.json @@ -3,7 +3,7 @@ "files": [ { "path": ".github/workflows/ci.yml", - "sha256": "61c283554b1e95eac6368a1aaeb6fd6691cfce9c6060aa93bb2949d281f36bad" + "sha256": "309fe37d7038a4a1eb6197edf32f652af50ded2017a55d55466e577b878b05ed" }, { "path": ".github/workflows/dependency-security.yml", @@ -95,23 +95,11 @@ }, { "path": "src-tauri/Cargo.lock", - "sha256": "628573c923c0d164eb9e707908b42c10d2550e6c389945079814838df140222b" + "sha256": "e1f2c3c2d2cc09a5f78a22874bd32942e93a935d54db51001ca4568fe4ab6fc7" }, { "path": "src-tauri/Cargo.toml", - "sha256": "7169cc1c3f6f75304daca1eea5b3d57237d88ad2575a3debc4acaf57cce23e09" - }, - { - "path": "src-tauri/crates/bridge-tally-canonical/Cargo.toml", - "sha256": "8750257055c37d81d9b93aea43c6902bd5a609a88eb5acd143752a88e8229631" - }, - { - "path": "src-tauri/crates/bridge-tally-canonical/src/lib.rs", - "sha256": "0bd5c939487e7f873286d65014dbbac804d046730ee0ac6722cdaac06ed675b6" - }, - { - "path": "src-tauri/crates/bridge-tally-canonical/tests/canonicalization.rs", - "sha256": "c09ae5ccb8489070a4024618f0cc6b7f13345e1147f187302e0af8a3df430c9a" + "sha256": "00843c54594b173fe5524d4236f0a9842773eb06d2efe5da88c81089ff8ac8f1" }, { "path": "src-tauri/crates/bridge-tally-core/Cargo.toml", @@ -129,14 +117,6 @@ "path": "src-tauri/crates/bridge-tally-core/src/pack_models.rs", "sha256": "f8362c877483f1782be00bbc23d0e6e12749858c65cff0a31fd16689c3bcf353" }, - { - "path": "src-tauri/crates/bridge-tally-observability/Cargo.toml", - "sha256": "21567544f89c83b6f291f9a483199c02c3a55f9fcb31e00beb27ae945032633f" - }, - { - "path": "src-tauri/crates/bridge-tally-observability/src/lib.rs", - "sha256": "6e861415d3872f9c1a62db386623077a798e689b7fd9ebf3d150d0ee514572e2" - }, { "path": "src-tauri/crates/bridge-tally-primitives/Cargo.toml", "sha256": "b4fd800d51bf1b816ede684d8b4dd44a8b47e301ba0347537441ff7738bbc228" @@ -201,14 +181,6 @@ "path": "src-tauri/crates/bridge-tally-protocol/tests/stream_text_decoder.rs", "sha256": "62cb2bf28a63eff3dc70870d084262e8c762206cd917c786a17463a1b6b1e627" }, - { - "path": "src-tauri/crates/bridge-tally-runtime/Cargo.toml", - "sha256": "bf89ba14640d8bd05dffbdf79d719676e9aa881657c144efe0f105f34cabad87" - }, - { - "path": "src-tauri/crates/bridge-tally-runtime/src/lib.rs", - "sha256": "61624920f0386c827305d4a11d43269c6a9a2366265b402c61a7a72fd68c11f7" - }, { "path": "src-tauri/crates/bridge-tally-transport/Cargo.toml", "sha256": "099ce34a52d4f3719c75dd053ef3e3c5bd8d2db1fd8587d43c1afe9ffa4d74ca" @@ -221,18 +193,6 @@ "path": "src-tauri/crates/bridge-tally-transport/tests/http_transport.rs", "sha256": "ddb4fc07f036e96a53eb2a32b76cf09fc54e8c95e1417748d227a605a724a5c5" }, - { - "path": "src-tauri/crates/bridge-tally-write/Cargo.toml", - "sha256": "f40eac110a9ef6df2279001db4878c42b4c6795a48613c9ccd513a782506d1c9" - }, - { - "path": "src-tauri/crates/bridge-tally-write/src/lib.rs", - "sha256": "bb8ffe97121a500da966a37c6c9a2816905dccca5ff4301bbed2810727f3ee5b" - }, - { - "path": "src-tauri/crates/bridge-tally-write/tests/qualification.rs", - "sha256": "6fce830053f833b3e52c209fbd1bbcc130fbd98a8af83851bb35a069cea2d27c" - }, { "path": "src-tauri/crates/tally-protocol-simulator/Cargo.toml", "sha256": "45e0bb723e924938811bd058d91ee8a34cb12ca8edc2ddca84ca3a500618f6f0" @@ -331,7 +291,7 @@ }, { "path": "src-tauri/src/db/tally_incremental.rs", - "sha256": "bac2c859de102cf1e558f669dee0445a9406e95ee9923b6f52bd498ee768e73b" + "sha256": "ebd981e558a99084138d46bf83bee54e28b640d60b49be9e546e793e523db2bf" }, { "path": "src-tauri/src/db/tally_mirror.rs", @@ -357,26 +317,6 @@ "path": "src-tauri/src/sync/snapshot.rs", "sha256": "e5acb891420e568678b9f20e9adabe2fa247deb4569c2648b19d4b50480f55f7" }, - { - "path": "src-tauri/src/tally/canary_dispatch_admission.rs", - "sha256": "033840d0a4e9f28e9fb8e566f8efff010ba6a4d8dce20626fb912444588a0f9b" - }, - { - "path": "src-tauri/src/tally/canary_preflight.rs", - "sha256": "df2146cdda1a48263d56ae285437e08d34d335be3166016bf0244be0cc8790c6" - }, - { - "path": "src-tauri/src/tally/canary_preflight_preparation.rs", - "sha256": "c822ab91a45e22b19814ae7005db8e314d402117e63a7b9a3ff0a423fa46a9f9" - }, - { - "path": "src-tauri/src/tally/canary_preflight_read_coordinator.rs", - "sha256": "9f79823ab1a2483b2679a1514555b76ed48c6b25af130bdc7e9b12a31c924715" - }, - { - "path": "src-tauri/src/tally/canary_runtime_dispatch_coordinator.rs", - "sha256": "b5f29ece3da67ab2d7a6bd2b6ad95c6823abf69720a9c0e03335ed01898423da" - }, { "path": "src-tauri/src/tally/capability_packs.rs", "sha256": "92ba5d666a677ff164f95f140937705c238b967a9c8214f7e7101ed8ac2ebc7f" @@ -413,10 +353,6 @@ "path": "src-tauri/src/tally/validators.rs", "sha256": "40976622598bdbfe2567f3fd70adee9ff78f5cd9c7fbc43e1fbac0640ef1523f" }, - { - "path": "src-tauri/src/tally/write_sandbox.rs", - "sha256": "2fab7dc59374b38b0a562200332aeaca9c1d39105e329ec5c4505dfebd6d90fe" - }, { "path": "src-tauri/src/tally/xml_parser.rs", "sha256": "654938f3207822cd9889b8d81cd53684ca43c856b727fbabc09aadde6f9e82eb" @@ -494,5 +430,5 @@ "sha256": "ef1d33e90da527faa9735469ea5040c9258fbab9e4c761f2473d79e7b3dbd0c4" } ], - "manifest_sha256": "8c07bb0159cb594a62e5c62d8741a822b01a5655889b9ceec1a939b345b1267c" + "manifest_sha256": "af8d0ba7d679001bd44a954ce90f9b164188a146af44efe55d74c472d29d18e5" } diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index a0a0389..ad037c5 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -409,11 +409,8 @@ version = "0.2.0" dependencies = [ "anyhow", "async-trait", - "bridge-tally-canonical", "bridge-tally-core", - "bridge-tally-incremental", "bridge-tally-protocol", - "bridge-tally-runtime", "bridge-tally-transport", "chrono", "cryptoki", @@ -444,15 +441,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "bridge-tally-canonical" -version = "0.1.0" -dependencies = [ - "bridge-tally-core", - "bridge-tally-protocol", - "sha2 0.11.0", -] - [[package]] name = "bridge-tally-core" version = "0.1.0" @@ -466,23 +454,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "bridge-tally-incremental" -version = "0.1.0" -dependencies = [ - "bridge-tally-core", - "serde", -] - -[[package]] -name = "bridge-tally-observability" -version = "0.1.0" -dependencies = [ - "serde", - "serde_json", - "sha2 0.11.0", -] - [[package]] name = "bridge-tally-primitives" version = "0.1.0" @@ -506,15 +477,6 @@ dependencies = [ "tally-protocol-simulator", ] -[[package]] -name = "bridge-tally-runtime" -version = "0.1.0" -dependencies = [ - "bridge-tally-observability", - "tokio", - "tokio-util", -] - [[package]] name = "bridge-tally-transport" version = "0.1.0" @@ -528,19 +490,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "bridge-tally-write" -version = "0.1.0" -dependencies = [ - "bridge-tally-core", - "bridge-tally-protocol", - "bridge-tally-transport", - "serde", - "serde_json", - "sha2 0.11.0", - "thiserror 2.0.20", -] - [[package]] name = "brotli" version = "8.0.4" @@ -5262,7 +5211,6 @@ dependencies = [ "bytes", "futures-core", "futures-sink", - "futures-util", "libc", "pin-project-lite", "tokio", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index ff15f22..934cd24 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -26,15 +26,10 @@ live-calibration-harness = ["voucher-scan"] [workspace] members = [ ".", - "crates/bridge-tally-canonical", "crates/bridge-tally-core", - "crates/bridge-tally-incremental", - "crates/bridge-tally-observability", "crates/bridge-tally-primitives", "crates/bridge-tally-protocol", - "crates/bridge-tally-runtime", "crates/bridge-tally-transport", - "crates/bridge-tally-write", "crates/tally-protocol-simulator", ] resolver = "2" @@ -62,10 +57,7 @@ tauri-build = { version = "2", features = [] } anyhow = "1" async-trait = "0.1" bridge-tally-core = { path = "crates/bridge-tally-core" } -bridge-tally-canonical = { path = "crates/bridge-tally-canonical" } -bridge-tally-incremental = { path = "crates/bridge-tally-incremental" } bridge-tally-protocol = { path = "crates/bridge-tally-protocol" } -bridge-tally-runtime = { path = "crates/bridge-tally-runtime" } bridge-tally-transport = { path = "crates/bridge-tally-transport" } chrono = { version = "0.4", features = ["serde"] } cryptoki = "0.12" diff --git a/src-tauri/crates/bridge-tally-canonical/Cargo.toml b/src-tauri/crates/bridge-tally-canonical/Cargo.toml deleted file mode 100644 index f8d1138..0000000 --- a/src-tauri/crates/bridge-tally-canonical/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "bridge-tally-canonical" -version = "0.1.0" -description = "Portable fail-closed canonicalization for Bridge Tally exports" -license = "Apache-2.0" -repository = "https://github.com/lamemustafa/bridge" -publish = false -edition = "2021" -rust-version = "1.96" - -[dependencies] -bridge-tally-core = { path = "../bridge-tally-core" } -bridge-tally-protocol = { path = "../bridge-tally-protocol" } -sha2 = "0.11" diff --git a/src-tauri/crates/bridge-tally-canonical/src/lib.rs b/src-tauri/crates/bridge-tally-canonical/src/lib.rs deleted file mode 100644 index bb5d450..0000000 --- a/src-tauri/crates/bridge-tally-canonical/src/lib.rs +++ /dev/null @@ -1,528 +0,0 @@ -//! Portable, deterministic conversion from strict Tally export records to Bridge canonical packs. -//! -//! This crate deliberately has no HTTP, database, OpenSSL, or Tauri dependency so the complete -//! identity and reference-binding boundary remains executable on every supported development host. - -use bridge_tally_core::{ - source_count_scope_fingerprint, CanonicalPackWindow, CanonicalText, CoreAccountingBatch, - ExactDecimal, GroupRecord, LedgerEntryPolarity, LedgerEntryRecord, LedgerRecord, - ObservedSourceIdentities, PackBatch, RawSourceSha256, RequestContext, SourceAlterId, - SourceCountScope, SourceCountScopeDescriptor, SourceIdentityKind, SourceRecordEvidence, - SourceRecordId, SourceReportedCountEvidence, TallyDate, TallyError, VoucherRecord, - VoucherTypeRecord, -}; -use bridge_tally_protocol::{ - ParsedExport, ParsedSourceIdentityKind, ParsedSourceRecord, TallyLedger, TallyNamedMaster, - TallyVoucher, -}; -use sha2::{Digest, Sha256}; -use std::collections::BTreeMap; - -/// Converts the four exact core-accounting exports into one reference-complete canonical window. -/// Any missing/ambiguous identity, mutable-name collision, or unresolved relationship fails closed. -pub fn build_core_window( - context: &RequestContext, - groups: ParsedExport>, - ledgers: ParsedExport>, - voucher_types: ParsedExport>, - vouchers: ParsedExport>, -) -> Result { - let requested_from = TallyDate::parse(context.window.from_yyyymmdd.clone()) - .map_err(|_| invalid_data("requested_window_invalid"))?; - let requested_to = TallyDate::parse(context.window.to_yyyymmdd.clone()) - .map_err(|_| invalid_data("requested_window_invalid"))?; - if requested_from.as_str() > requested_to.as_str() { - return Err(invalid_data("requested_window_invalid")); - } - let group_count = required_source_count(&groups, "group_source_count_missing")?; - let ledger_count = required_source_count(&ledgers, "ledger_source_count_missing")?; - let voucher_type_count = - required_source_count(&voucher_types, "voucher_type_source_count_missing")?; - let voucher_count = required_source_count(&vouchers, "voucher_source_count_missing")?; - validate_selected_voucher_window( - context.window.from_yyyymmdd.as_str(), - context.window.to_yyyymmdd.as_str(), - &vouchers, - )?; - let mut batch = CoreAccountingBatch::default(); - let mut record_evidence = Vec::new(); - - let group_ids_by_name = unique_source_ids_by_name( - &groups.records, - |record| &record.name, - "group_identity_missing", - "group_name_missing", - "group_name_duplicate", - )?; - for source in groups.records { - let source_id = required_source_id(&source, "group_identity_missing")?; - let evidence = source_evidence("group", source_id.clone(), &source)?; - let name = required_text(&source.record.name, "group_name_missing")?; - let parent_source_id = resolve_group_parent( - source.record.parent.as_deref(), - &group_ids_by_name, - "group_parent_missing", - )?; - batch.groups.push(GroupRecord { - source_id, - name, - parent_source_id, - }); - record_evidence.push(evidence); - } - - let ledger_ids_by_name = unique_source_ids_by_name( - &ledgers.records, - |record| &record.name, - "ledger_identity_missing", - "ledger_name_missing", - "ledger_name_duplicate", - )?; - for source in ledgers.records { - let source_id = required_source_id(&source, "ledger_identity_missing")?; - let evidence = source_evidence("ledger", source_id.clone(), &source)?; - let name = required_text(&source.record.name, "ledger_name_missing")?; - let parent_source_id = resolve_optional_reference( - source.record.parent.as_deref(), - &group_ids_by_name, - "ledger_parent_group_missing", - )?; - let opening_balance = source - .record - .opening_balance - .as_deref() - .filter(|value| !value.trim().is_empty()) - .map(|value| ExactDecimal::parse(value.to_string())) - .transpose()?; - batch.ledgers.push(LedgerRecord { - source_id, - name, - parent_source_id, - opening_balance, - }); - record_evidence.push(evidence); - } - - let voucher_type_ids_by_name = unique_source_ids_by_name( - &voucher_types.records, - |record| &record.name, - "voucher_type_identity_missing", - "voucher_type_name_missing", - "voucher_type_name_duplicate", - )?; - for source in voucher_types.records { - let source_id = required_source_id(&source, "voucher_type_identity_missing")?; - let evidence = source_evidence("voucher_type", source_id.clone(), &source)?; - let name = required_text(&source.record.name, "voucher_type_name_missing")?; - batch - .voucher_types - .push(VoucherTypeRecord { source_id, name }); - record_evidence.push(evidence); - } - - for source in vouchers.records { - let voucher_source_id = required_source_id(&source, "voucher_identity_missing")?; - let voucher_evidence = source_evidence("voucher", voucher_source_id.clone(), &source)?; - let voucher_type_name = source - .record - .voucher_type - .as_deref() - .ok_or_else(|| invalid_data("voucher_type_missing"))?; - let voucher_type_source_id = resolve_required_reference( - voucher_type_name, - &voucher_type_ids_by_name, - "voucher_type_reference_missing", - )?; - let date_yyyymmdd = required_text( - source - .record - .date - .as_deref() - .ok_or_else(|| invalid_data("voucher_date_missing"))?, - "voucher_date_missing", - )?; - let voucher_date = TallyDate::parse(date_yyyymmdd.clone()) - .map_err(|_| invalid_data("voucher_date_invalid"))?; - if voucher_date.as_str() < requested_from.as_str() - || voucher_date.as_str() > requested_to.as_str() - { - return Err(invalid_data("voucher_date_outside_requested_window")); - } - let voucher_number = source - .record - .voucher_number - .as_deref() - .filter(|value| !value.trim().is_empty()) - .map(|value| required_text(value, "voucher_number_invalid")) - .transpose()?; - let cancelled = source - .record - .cancelled - .ok_or_else(|| invalid_data("voucher_cancelled_missing"))?; - let optional = source - .record - .optional - .ok_or_else(|| invalid_data("voucher_optional_missing"))?; - - for entry in &source.record.ledger_entries { - let ledger_source_id = resolve_required_reference( - &entry.ledger_name, - &ledger_ids_by_name, - "voucher_ledger_reference_missing", - )?; - let entry_source_id = derived_ledger_entry_id( - &context.company.identity.company_guid, - &source, - entry.entry_index, - &entry.raw_source_sha256, - )?; - batch.ledger_entries.push(LedgerEntryRecord { - source_id: entry_source_id.clone(), - voucher_source_id: voucher_source_id.clone(), - ledger_source_id, - amount: ExactDecimal::parse(entry.amount.clone())?, - polarity: if entry.is_deemed_positive { - LedgerEntryPolarity::Debit - } else { - LedgerEntryPolarity::Credit - }, - }); - record_evidence.push(SourceRecordEvidence { - object_type: CanonicalText::parse("ledger_entry")?, - source_id: SourceRecordId::parse(entry_source_id)?, - identity_kind: SourceIdentityKind::Fallback, - observed_identities: ObservedSourceIdentities::default(), - // Hash of the exact decoded XML row fragment, not the HTTP transport bytes. - raw_source_sha256: RawSourceSha256::parse(entry.raw_source_sha256.clone())?, - alter_id: None, - }); - } - - batch.vouchers.push(VoucherRecord { - source_id: voucher_source_id, - date_yyyymmdd, - voucher_type_source_id, - voucher_number, - cancelled, - optional, - }); - record_evidence.push(voucher_evidence); - } - - let source_counts = vec![ - count_evidence(context, "group", group_count, SourceCountScope::Complete)?, - count_evidence(context, "ledger", ledger_count, SourceCountScope::Complete)?, - count_evidence( - context, - "voucher_type", - voucher_type_count, - SourceCountScope::Complete, - )?, - count_evidence(context, "voucher", voucher_count, SourceCountScope::Window)?, - ]; - let window = CanonicalPackWindow { - batch: PackBatch::CoreAccounting(batch), - source_counts: Some(source_counts), - record_evidence: Some(record_evidence), - }; - window.validate_source_count_evidence()?; - window.validate_record_evidence_binding()?; - Ok(window) -} - -/// Validates the exact selected voucher profile without canonicalising or retaining book data. -/// A successful zero-row response proves only execution of the selected profile, not emptiness or -/// source completeness. -pub fn validate_selected_voucher_window( - from_yyyymmdd: &str, - to_yyyymmdd: &str, - vouchers: &ParsedExport>, -) -> Result<(), TallyError> { - let requested_from = TallyDate::parse(from_yyyymmdd.to_string()) - .map_err(|_| invalid_data("requested_window_invalid"))?; - let requested_to = TallyDate::parse(to_yyyymmdd.to_string()) - .map_err(|_| invalid_data("requested_window_invalid"))?; - if requested_from.as_str() > requested_to.as_str() { - return Err(invalid_data("requested_window_invalid")); - } - for source in &vouchers.records { - let source_id = required_source_id(source, "voucher_identity_missing")?; - source_evidence("voucher", source_id, source)?; - required_text( - source - .record - .voucher_type - .as_deref() - .ok_or_else(|| invalid_data("voucher_type_missing"))?, - "voucher_type_missing", - )?; - let date = required_text( - source - .record - .date - .as_deref() - .ok_or_else(|| invalid_data("voucher_date_missing"))?, - "voucher_date_missing", - )?; - let voucher_date = - TallyDate::parse(date).map_err(|_| invalid_data("voucher_date_invalid"))?; - if voucher_date.as_str() < requested_from.as_str() - || voucher_date.as_str() > requested_to.as_str() - { - return Err(invalid_data("voucher_date_outside_requested_window")); - } - source - .record - .cancelled - .ok_or_else(|| invalid_data("voucher_cancelled_missing"))?; - source - .record - .optional - .ok_or_else(|| invalid_data("voucher_optional_missing"))?; - source - .record - .voucher_number - .as_deref() - .map(|value| required_text(value, "voucher_number_invalid")) - .transpose()?; - source - .record - .party_ledger_name - .as_deref() - .map(|value| required_text(value, "voucher_party_ledger_name_invalid")) - .transpose()?; - let declared_entries = source - .record - .ledger_entry_count - .ok_or_else(|| invalid_data("voucher_ledger_entry_count_missing"))?; - if declared_entries != source.record.ledger_entries.len() as u64 { - return Err(invalid_data("voucher_ledger_entry_count_mismatch")); - } - let mut entry_indices = std::collections::BTreeSet::new(); - for entry in &source.record.ledger_entries { - if entry.entry_index == 0 || !entry_indices.insert(entry.entry_index) { - return Err(invalid_data("voucher_ledger_entry_index_invalid")); - } - required_text(&entry.ledger_name, "voucher_ledger_name_invalid")?; - ExactDecimal::parse(entry.amount.clone())?; - RawSourceSha256::parse(entry.raw_source_sha256.clone())?; - } - } - Ok(()) -} - -fn required_source_count( - export: &ParsedExport, - code: &'static str, -) -> Result { - export - .evidence - .source_record_count - .ok_or_else(|| protocol_error(code)) -} - -fn unique_source_ids_by_name( - records: &[ParsedSourceRecord], - name: F, - missing_identity_code: &'static str, - invalid_name_code: &'static str, - duplicate_name_code: &'static str, -) -> Result, TallyError> -where - F: Fn(&T) -> &str, -{ - let mut ids = BTreeMap::new(); - for source in records { - let source_id = required_source_id(source, missing_identity_code)?; - let canonical_name = required_text(name(&source.record), invalid_name_code)?; - if ids.insert(canonical_name, source_id).is_some() { - return Err(invalid_data(duplicate_name_code)); - } - } - Ok(ids) -} - -fn resolve_optional_reference( - value: Option<&str>, - ids_by_name: &BTreeMap, - missing_code: &'static str, -) -> Result, TallyError> { - value - .filter(|value| !value.trim().is_empty()) - .map(|value| resolve_required_reference(value, ids_by_name, missing_code)) - .transpose() -} - -fn resolve_group_parent( - value: Option<&str>, - ids_by_name: &BTreeMap, - missing_code: &'static str, -) -> Result, TallyError> { - let Some(value) = value.filter(|value| !value.trim().is_empty()) else { - return Ok(None); - }; - // `Primary` is Tally's reserved top-level classification, not one of the exported Group - // masters. Preserve the canonical tree root as `None`; every other named parent must resolve. - if value.trim().eq_ignore_ascii_case("primary") { - return Ok(None); - } - resolve_required_reference(value, ids_by_name, missing_code).map(Some) -} - -fn resolve_required_reference( - value: &str, - ids_by_name: &BTreeMap, - missing_code: &'static str, -) -> Result { - let name = required_text(value, missing_code)?; - ids_by_name - .get(&name) - .cloned() - .ok_or_else(|| invalid_data(missing_code)) -} - -fn count_evidence( - context: &RequestContext, - object_type: &str, - count: u64, - scope: SourceCountScope, -) -> Result { - let object_type = CanonicalText::parse(object_type)?; - let descriptor = SourceCountScopeDescriptor { - source_identity: context.company.identity.clone(), - pack: context.pack, - pack_schema_version: context.schema_version, - object_type: object_type.clone(), - query_profile: context.query_profile.clone(), - filters_sha256: context.filters_sha256.clone(), - window: (scope == SourceCountScope::Window).then(|| context.window.clone()), - }; - Ok(SourceReportedCountEvidence { - object_type, - query_profile: context.query_profile.clone(), - source_scope_fingerprint: source_count_scope_fingerprint(&descriptor, scope)?, - source_count_scope: scope, - source_reported_count: count, - }) -} - -fn source_evidence( - object_type: &str, - source_id: String, - source: &ParsedSourceRecord, -) -> Result { - let identity_kind = match source.identity_kind { - Some(ParsedSourceIdentityKind::Guid) => SourceIdentityKind::Guid, - Some(ParsedSourceIdentityKind::RemoteId) => SourceIdentityKind::RemoteId, - Some(ParsedSourceIdentityKind::MasterId) => SourceIdentityKind::MasterId, - None => return Err(invalid_data("source_identity_kind_missing")), - }; - Ok(SourceRecordEvidence { - object_type: CanonicalText::parse(object_type)?, - source_id: SourceRecordId::parse(source_id)?, - identity_kind, - observed_identities: ObservedSourceIdentities { - guid: source - .identities - .guid - .clone() - .map(SourceRecordId::parse) - .transpose()?, - remote_id: source - .identities - .remote_id - .clone() - .map(SourceRecordId::parse) - .transpose()?, - master_id: source - .identities - .master_id - .clone() - .map(SourceRecordId::parse) - .transpose()?, - }, - raw_source_sha256: RawSourceSha256::parse(source.raw_source_sha256.clone())?, - alter_id: source - .alter_id - .clone() - .map(SourceAlterId::parse) - .transpose()?, - }) -} - -fn required_source_id( - source: &ParsedSourceRecord, - code: &'static str, -) -> Result { - source - .source_id - .clone() - .filter(|value| !value.trim().is_empty()) - .ok_or_else(|| invalid_data(code)) -} - -fn required_text(value: &str, code: &'static str) -> Result { - CanonicalText::parse(value.to_string()) - .map(|value| value.as_str().to_string()) - .map_err(|_| invalid_data(code)) -} - -fn derived_ledger_entry_id( - company_guid: &str, - voucher: &ParsedSourceRecord, - entry_index: u64, - entry_fragment_sha256: &str, -) -> Result { - let identity_kind = voucher - .identity_kind - .ok_or_else(|| invalid_data("voucher_identity_kind_missing"))?; - let source_id = required_source_id(voucher, "voucher_identity_missing")?; - RawSourceSha256::parse(entry_fragment_sha256.to_string())?; - - let mut digest = Sha256::new(); - digest.update(b"bridge-tally-ledger-entry-derived-id-v1\0"); - hash_field(&mut digest, company_guid.as_bytes()); - hash_field(&mut digest, parsed_identity_kind_code(identity_kind)); - hash_field(&mut digest, source_id.as_bytes()); - hash_field(&mut digest, &entry_index.to_be_bytes()); - hash_field(&mut digest, entry_fragment_sha256.as_bytes()); - Ok(format!( - "bridge-derived:ledger-entry:v1:{}", - hex_lower(&digest.finalize()) - )) -} - -fn parsed_identity_kind_code(kind: ParsedSourceIdentityKind) -> &'static [u8] { - match kind { - ParsedSourceIdentityKind::Guid => b"guid", - ParsedSourceIdentityKind::RemoteId => b"remote_id", - ParsedSourceIdentityKind::MasterId => b"master_id", - } -} - -fn hash_field(digest: &mut Sha256, value: &[u8]) { - digest.update((value.len() as u64).to_be_bytes()); - digest.update(value); -} - -fn hex_lower(bytes: &[u8]) -> String { - const HEX: &[u8; 16] = b"0123456789abcdef"; - let mut output = String::with_capacity(bytes.len() * 2); - for byte in bytes { - output.push(HEX[(byte >> 4) as usize] as char); - output.push(HEX[(byte & 0x0f) as usize] as char); - } - output -} - -fn invalid_data(code: &'static str) -> TallyError { - TallyError::InvalidData { - code: code.to_string(), - } -} - -fn protocol_error(code: &'static str) -> TallyError { - TallyError::Protocol { - code: code.to_string(), - } -} diff --git a/src-tauri/crates/bridge-tally-canonical/tests/canonicalization.rs b/src-tauri/crates/bridge-tally-canonical/tests/canonicalization.rs deleted file mode 100644 index 4e8d0f2..0000000 --- a/src-tauri/crates/bridge-tally-canonical/tests/canonicalization.rs +++ /dev/null @@ -1,278 +0,0 @@ -use bridge_tally_canonical::{build_core_window, validate_selected_voucher_window}; -use bridge_tally_core::{ - CanonicalPackWindow, CanonicalText, CapabilityPackId, CompanyRef, LedgerEntryPolarity, - ObservedSourceIdentities, PackBatch, PackSchemaVersion, ReadWindow, RequestContext, - SourceIdentity, SourceIdentityKind, TallyError, -}; -use bridge_tally_protocol::{ - parse_group_source_records_with_evidence, parse_ledger_source_records_with_evidence, - parse_voucher_source_records_with_evidence, parse_voucher_type_source_records_with_evidence, - ParsedExport, ParsedSourceRecord, TallyLedger, TallyNamedMaster, TallyVoucher, - BRIDGE_GROUP_EXPORT_SCHEMA, BRIDGE_LEDGER_EXPORT_SCHEMA, BRIDGE_VOUCHER_EXPORT_SCHEMA, - BRIDGE_VOUCHER_TYPE_EXPORT_SCHEMA, -}; - -fn context() -> RequestContext { - RequestContext { - run_id: "synthetic-run".to_string(), - company: CompanyRef { - identity: SourceIdentity { - bridge_source_lineage: "synthetic-lineage".to_string(), - company_guid: "synthetic-company-guid".to_string(), - observed_fingerprint: "synthetic-observation".to_string(), - }, - display_name: "BRIDGE SYNTHETIC BOOK".to_string(), - }, - pack: CapabilityPackId::CoreAccounting, - schema_version: PackSchemaVersion { major: 1, minor: 0 }, - window: ReadWindow { - from_yyyymmdd: "20260701".to_string(), - to_yyyymmdd: "20260731".to_string(), - }, - query_profile: CanonicalText::parse("core_accounting_v1").unwrap(), - filters_sha256: CanonicalText::parse("0".repeat(64)).unwrap(), - } -} - -fn groups() -> ParsedExport> { - parse_group_source_records_with_evidence(&format!( - r#"
1
Primary
"#, - BRIDGE_GROUP_EXPORT_SCHEMA - )) - .unwrap() -} - -fn ledgers_and_vouchers( - cash_name: &str, - entry_ledger_name: &str, -) -> ( - ParsedExport>, - ParsedExport>, -) { - let ledgers = parse_ledger_source_records_with_evidence(&format!( - r#"
1
Assets0Assets0
"#, - BRIDGE_LEDGER_EXPORT_SCHEMA, cash_name - )) - .unwrap(); - let vouchers = parse_voucher_source_records_with_evidence(&format!( - r#"
1
20260714ReceiptSYN-1NoNo21{}-100.00Yes2Sales100.00No
"#, - BRIDGE_VOUCHER_EXPORT_SCHEMA, entry_ledger_name - )) - .unwrap(); - (ledgers, vouchers) -} - -fn voucher_types() -> ParsedExport> { - parse_voucher_type_source_records_with_evidence(&format!( - r#"
1
Receipt
"#, - BRIDGE_VOUCHER_TYPE_EXPORT_SCHEMA - )) - .unwrap() -} - -fn valid_window() -> CanonicalPackWindow { - let (ledgers, vouchers) = ledgers_and_vouchers("Cash", "Cash"); - build_core_window(&context(), groups(), ledgers, voucher_types(), vouchers).unwrap() -} - -#[test] -fn canonicalizes_all_core_records_with_exact_reference_and_provenance_binding() { - let window = valid_window(); - window.validate_record_evidence_binding().unwrap(); - let PackBatch::CoreAccounting(batch) = &window.batch else { - panic!("wrong pack") - }; - assert_eq!( - ( - batch.groups.len(), - batch.ledgers.len(), - batch.voucher_types.len() - ), - (1, 2, 1) - ); - assert_eq!((batch.vouchers.len(), batch.ledger_entries.len()), (1, 2)); - assert_eq!( - batch.ledgers[0].parent_source_id.as_deref(), - Some("group-guid") - ); - assert_eq!(batch.groups[0].parent_source_id, None); - assert_eq!( - batch.vouchers[0].voucher_type_source_id, - "voucher-type-guid" - ); - assert_eq!(batch.ledger_entries[0].ledger_source_id, "ledger-cash"); - assert_eq!(batch.ledger_entries[0].voucher_source_id, "voucher-guid"); - assert_eq!(batch.ledger_entries[0].polarity, LedgerEntryPolarity::Debit); - assert_eq!( - batch.ledger_entries[1].polarity, - LedgerEntryPolarity::Credit - ); - assert!(batch.ledger_entries[0] - .source_id - .starts_with("bridge-derived:ledger-entry:v1:")); - assert_eq!(window.source_counts.as_ref().unwrap().len(), 4); - assert!(window - .source_counts - .as_ref() - .unwrap() - .iter() - .all(|evidence| evidence.object_type.as_str() != "ledger_entry")); - assert_eq!(window.record_evidence.as_ref().unwrap().len(), 7); - - let voucher_evidence = window - .record_evidence - .as_ref() - .unwrap() - .iter() - .find(|evidence| evidence.object_type.as_str() == "voucher") - .unwrap(); - assert_eq!(voucher_evidence.identity_kind, SourceIdentityKind::Guid); - assert_eq!( - voucher_evidence - .observed_identities - .remote_id - .as_ref() - .unwrap() - .as_str(), - "voucher-remote" - ); - assert_eq!( - voucher_evidence - .observed_identities - .master_id - .as_ref() - .unwrap() - .as_str(), - "9" - ); -} - -#[test] -fn nested_entry_totals_remain_local_and_are_never_claimed_as_source_reported() { - let window = valid_window(); - let PackBatch::CoreAccounting(batch) = &window.batch else { - panic!("wrong pack") - }; - - assert_eq!(batch.ledger_entries.len(), 2); - assert_eq!( - window - .record_evidence - .as_ref() - .unwrap() - .iter() - .filter(|evidence| evidence.object_type.as_str() == "ledger_entry") - .count(), - 2 - ); - assert!(window - .source_counts - .as_ref() - .unwrap() - .iter() - .all(|evidence| evidence.object_type.as_str() != "ledger_entry")); -} - -#[test] -fn derived_entry_ids_are_deterministic_but_never_claim_native_identity() { - fn entry_ids(window: &CanonicalPackWindow) -> Vec { - let PackBatch::CoreAccounting(batch) = &window.batch else { - panic!("wrong pack") - }; - batch - .ledger_entries - .iter() - .map(|entry| entry.source_id.clone()) - .collect() - } - let first = valid_window(); - let second = valid_window(); - assert_eq!(entry_ids(&first), entry_ids(&second)); - let entry_evidence = first - .record_evidence - .as_ref() - .unwrap() - .iter() - .filter(|evidence| evidence.object_type.as_str() == "ledger_entry") - .collect::>(); - assert_eq!(entry_evidence.len(), 2); - assert!(entry_evidence.iter().all(|evidence| { - evidence.identity_kind == SourceIdentityKind::Fallback - && evidence.observed_identities == ObservedSourceIdentities::default() - })); -} - -#[test] -fn unresolved_mutable_name_reference_fails_closed() { - let (ledgers, vouchers) = ledgers_and_vouchers("Cash", "Missing Ledger"); - let error = - build_core_window(&context(), groups(), ledgers, voucher_types(), vouchers).unwrap_err(); - assert!(matches!( - error, - TallyError::InvalidData { code } - if code == "voucher_ledger_reference_missing" - )); -} - -#[test] -fn duplicate_mutable_names_fail_closed_even_when_native_ids_differ() { - let (ledgers, vouchers) = ledgers_and_vouchers("Sales", "Sales"); - let error = - build_core_window(&context(), groups(), ledgers, voucher_types(), vouchers).unwrap_err(); - assert!(matches!( - error, - TallyError::InvalidData { code } if code == "ledger_name_duplicate" - )); -} - -#[test] -fn invalid_or_out_of_window_voucher_dates_fail_before_canonical_state_exists() { - for (date, expected_code) in [ - ("20260230", "voucher_date_invalid"), - ("20260630", "voucher_date_outside_requested_window"), - ("20260801", "voucher_date_outside_requested_window"), - ] { - let (ledgers, mut vouchers) = ledgers_and_vouchers("Cash", "Cash"); - vouchers.records[0].record.date = Some(date.to_string()); - let error = build_core_window(&context(), groups(), ledgers, voucher_types(), vouchers) - .unwrap_err(); - assert!(matches!( - error, - TallyError::InvalidData { code } if code == expected_code - )); - } -} - -#[test] -fn invalid_requested_window_fails_before_source_rows_are_canonicalized() { - for (from, to) in [("20260230", "20260731"), ("20260801", "20260731")] { - let mut request = context(); - request.window.from_yyyymmdd = from.to_string(); - request.window.to_yyyymmdd = to.to_string(); - let (ledgers, vouchers) = ledgers_and_vouchers("Cash", "Cash"); - let error = - build_core_window(&request, groups(), ledgers, voucher_types(), vouchers).unwrap_err(); - assert!(matches!( - error, - TallyError::InvalidData { code } if code == "requested_window_invalid" - )); - } -} - -#[test] -fn selected_voucher_qualification_rejects_noncanonical_records_and_entries() { - let (_, vouchers) = ledgers_and_vouchers("Cash", "Cash"); - validate_selected_voucher_window("20260701", "20260731", &vouchers).unwrap(); - - let mut invalid_amount = vouchers.clone(); - invalid_amount.records[0].record.ledger_entries[0].amount = "not-an-amount".to_string(); - assert!(validate_selected_voucher_window("20260701", "20260731", &invalid_amount).is_err()); - - let mut invalid_name = vouchers.clone(); - invalid_name.records[0].record.ledger_entries[0].ledger_name = " x ".to_string(); - assert!(validate_selected_voucher_window("20260701", "20260731", &invalid_name).is_err()); - - let mut invalid_alter_id = vouchers; - invalid_alter_id.records[0].alter_id = Some("contains whitespace".to_string()); - assert!(validate_selected_voucher_window("20260701", "20260731", &invalid_alter_id).is_err()); -} diff --git a/src-tauri/crates/bridge-tally-incremental/Cargo.toml b/src-tauri/crates/bridge-tally-incremental/Cargo.toml deleted file mode 100644 index c711cb1..0000000 --- a/src-tauri/crates/bridge-tally-incremental/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "bridge-tally-incremental" -version = "0.1.0" -edition = "2021" -rust-version = "1.96" -license = "Apache-2.0" -repository = "https://github.com/lamemustafa/bridge" -publish = false -description = "Portable fail-closed incremental policy for Bridge Tally sync" - -[dependencies] -bridge-tally-core = { path = "../bridge-tally-core" } -serde = { version = "1", features = ["derive"] } diff --git a/src-tauri/crates/bridge-tally-incremental/src/lib.rs b/src-tauri/crates/bridge-tally-incremental/src/lib.rs deleted file mode 100644 index 0907cc8..0000000 --- a/src-tauri/crates/bridge-tally-incremental/src/lib.rs +++ /dev/null @@ -1,1121 +0,0 @@ -//! Portable, fail-closed incremental-sync policy for Bridge Tally integrations. -//! -//! This crate performs deterministic policy calculations; it does not authenticate -//! database or protocol evidence. The production runtime must obtain capability and -//! checkpoint facts through its sealed repository verifier before calling `plan_sync`. - -use bridge_tally_core::{ - CapabilityPackId, CapabilityState, EvidenceConfidence, PackSchemaVersion, TransportId, - VerificationState, -}; -use serde::{Deserialize, Serialize}; -use std::collections::{BTreeMap, BTreeSet}; - -/// Every dimension that can change the meaning or ordering of a Tally change -/// identifier. Checkpoints are reusable only under exact equality. -#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] -pub struct IncrementalScope { - /// Stable Bridge lineage for the Tally origin; never a raw endpoint. - pub source_lineage: String, - pub company_guid: String, - pub company_fingerprint: String, - pub object_type: String, - pub capability_profile_version: u16, - pub product: String, - pub release: String, - pub mode: String, - pub transport: TransportId, - pub pack: CapabilityPackId, - pub pack_schema_version: PackSchemaVersion, - /// Stable name of the exact query and canonical mapping. - pub query_profile: String, - /// Canonical lowercase SHA-256 of every filter that changes feed membership. - pub filters_sha256: String, - /// Versioned date/overlap-window policy; never inferred from a cursor alone. - pub date_window_policy: String, -} - -impl IncrementalScope { - pub fn is_exact(&self) -> bool { - self.capability_profile_version > 0 - && [ - self.source_lineage.as_str(), - self.company_guid.as_str(), - self.company_fingerprint.as_str(), - self.object_type.as_str(), - self.product.as_str(), - self.release.as_str(), - self.mode.as_str(), - self.query_profile.as_str(), - self.date_window_policy.as_str(), - ] - .into_iter() - .all(|value| { - !value.trim().is_empty() - && value.len() <= 512 - && !value.chars().any(char::is_control) - }) - && self.filters_sha256.len() == 64 - && self - .filters_sha256 - .bytes() - .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum ChangeIdentifierSemantics { - /// Verified monotonic identifier scoped to one exact Tally object type. - MonotonicPerObject, - Unknown, -} - -#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] -pub struct IncrementalCapabilityObservation { - pub scope: IncrementalScope, - pub state: CapabilityState, - pub confidence: EvidenceConfidence, - pub identifier_semantics: ChangeIdentifierSemantics, - /// True only after the exact query profile was observed to support an - /// inclusive lower bound, which makes overlap reads safe. - pub inclusive_lower_bound_observed: bool, - /// True only when the exact response contract exposes a source high - /// watermark independently of the maximum identifier in returned rows. - pub explicit_source_high_watermark_observed: bool, -} - -impl IncrementalCapabilityObservation { - fn proves_incremental_for(&self, scope: &IncrementalScope) -> bool { - self.scope == *scope - && scope.is_exact() - && self.state == CapabilityState::Supported - && self.confidence == EvidenceConfidence::Observed - && self.identifier_semantics == ChangeIdentifierSemantics::MonotonicPerObject - && self.inclusive_lower_bound_observed - && self.explicit_source_high_watermark_observed - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] -pub struct IncrementalCheckpoint { - pub scope: IncrementalScope, - pub high_watermark: u64, - pub established_by_verified_full_snapshot: bool, - pub established_by_proof_sha256: String, - pub last_transition_proof_sha256: String, - pub last_identity_sweep_unix_ms: i64, - pub invalidated_reason: Option, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] -pub struct IncrementalPolicy { - /// Number of identifiers re-read before the checkpoint. Inclusive queries - /// therefore start at `checkpoint - overlap`, saturating at zero. - pub overlap_identifiers: u64, - pub identity_sweep_interval_ms: i64, -} - -impl Default for IncrementalPolicy { - fn default() -> Self { - Self { - overlap_identifiers: 128, - identity_sweep_interval_ms: 24 * 60 * 60 * 1_000, - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum FullSnapshotReason { - ScopeIncomplete, - CapabilityNotObserved, - CapabilityUnsupported, - CapabilityNotObservedAtRuntime, - CapabilityScopeDrift, - NoVerifiedCheckpoint, - CheckpointScopeDrift, - CheckpointInvalidated, - InvalidPolicy, - ReceiptScopeMismatch, - SourceHighWatermarkMissing, - InvalidProofReceipt, -} - -impl FullSnapshotReason { - pub const fn safe_warning_code(self) -> &'static str { - match self { - Self::ScopeIncomplete => "incremental_scope_incomplete_full_snapshot_required", - Self::CapabilityNotObserved => "incremental_capability_unknown_full_snapshot_required", - Self::CapabilityUnsupported => { - "incremental_capability_unsupported_full_snapshot_required" - } - Self::CapabilityNotObservedAtRuntime => { - "incremental_capability_not_observed_full_snapshot_required" - } - Self::CapabilityScopeDrift => { - "incremental_capability_scope_drift_full_snapshot_required" - } - Self::NoVerifiedCheckpoint => "verified_full_snapshot_checkpoint_required", - Self::CheckpointScopeDrift => { - "incremental_checkpoint_scope_drift_full_snapshot_required" - } - Self::CheckpointInvalidated => "incremental_checkpoint_invalid_full_snapshot_required", - Self::InvalidPolicy => "incremental_policy_invalid_full_snapshot_required", - Self::ReceiptScopeMismatch => { - "incremental_receipt_scope_mismatch_full_snapshot_required" - } - Self::SourceHighWatermarkMissing => { - "incremental_source_high_watermark_missing_full_snapshot_required" - } - Self::InvalidProofReceipt => "incremental_proof_receipt_invalid_full_snapshot_required", - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct VerifiedFullSnapshotReceipt { - scope: IncrementalScope, - verification: VerificationState, - proof_sha256: String, - observed_source_high_watermark: Option, - completed_at_unix_ms: i64, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct VerifiedIncrementalReceipt { - scope: IncrementalScope, - checkpoint_before: u64, - verification: VerificationState, - proof_sha256: String, - observed_source_high_watermark: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum SyncPlan { - FullSnapshot { - reason: FullSnapshotReason, - warning_code: &'static str, - }, - Incremental { - from_change_id_inclusive: u64, - checkpoint_before: u64, - identity_sweep_required: bool, - }, -} - -pub fn plan_sync( - policy: IncrementalPolicy, - scope: &IncrementalScope, - capability: Option<&IncrementalCapabilityObservation>, - checkpoint: Option<&IncrementalCheckpoint>, - now_unix_ms: i64, -) -> SyncPlan { - let full = |reason: FullSnapshotReason| SyncPlan::FullSnapshot { - reason, - warning_code: reason.safe_warning_code(), - }; - - if policy.identity_sweep_interval_ms <= 0 { - return full(FullSnapshotReason::InvalidPolicy); - } - if !scope.is_exact() { - return full(FullSnapshotReason::ScopeIncomplete); - } - - let Some(capability) = capability else { - return full(FullSnapshotReason::CapabilityNotObserved); - }; - if capability.scope != *scope { - return full(FullSnapshotReason::CapabilityScopeDrift); - } - if capability.state == CapabilityState::Unsupported { - return full(FullSnapshotReason::CapabilityUnsupported); - } - if !capability.proves_incremental_for(scope) { - return full(FullSnapshotReason::CapabilityNotObservedAtRuntime); - } - - let Some(checkpoint) = checkpoint else { - return full(FullSnapshotReason::NoVerifiedCheckpoint); - }; - if checkpoint.scope != *scope { - return full(FullSnapshotReason::CheckpointScopeDrift); - } - if checkpoint.invalidated_reason.is_some() { - return full(FullSnapshotReason::CheckpointInvalidated); - } - if !checkpoint.established_by_verified_full_snapshot { - return full(FullSnapshotReason::NoVerifiedCheckpoint); - } - if !is_lower_sha256(&checkpoint.established_by_proof_sha256) - || !is_lower_sha256(&checkpoint.last_transition_proof_sha256) - { - return full(FullSnapshotReason::InvalidProofReceipt); - } - - SyncPlan::Incremental { - from_change_id_inclusive: checkpoint - .high_watermark - .saturating_sub(policy.overlap_identifiers), - checkpoint_before: checkpoint.high_watermark, - identity_sweep_required: identity_sweep_due( - checkpoint.last_identity_sweep_unix_ms, - now_unix_ms, - policy.identity_sweep_interval_ms, - ), - } -} - -pub fn establish_checkpoint_from_full_snapshot( - scope: IncrementalScope, - capability: &IncrementalCapabilityObservation, - receipt: VerifiedFullSnapshotReceipt, -) -> Result { - if receipt.scope != scope { - return Err(FullSnapshotReason::ReceiptScopeMismatch); - } - if receipt.verification != VerificationState::Verified { - return Err(FullSnapshotReason::NoVerifiedCheckpoint); - } - if !is_lower_sha256(&receipt.proof_sha256) || receipt.completed_at_unix_ms <= 0 { - return Err(FullSnapshotReason::InvalidProofReceipt); - } - let observed_high_watermark = receipt - .observed_source_high_watermark - .ok_or(FullSnapshotReason::SourceHighWatermarkMissing)?; - if !capability.proves_incremental_for(&scope) { - return Err(FullSnapshotReason::CapabilityNotObservedAtRuntime); - } - Ok(IncrementalCheckpoint { - scope, - high_watermark: observed_high_watermark, - established_by_verified_full_snapshot: true, - established_by_proof_sha256: receipt.proof_sha256.clone(), - last_transition_proof_sha256: receipt.proof_sha256, - last_identity_sweep_unix_ms: receipt.completed_at_unix_ms, - invalidated_reason: None, - }) -} - -fn identity_sweep_due(last_sweep_unix_ms: i64, now_unix_ms: i64, interval_ms: i64) -> bool { - now_unix_ms < last_sweep_unix_ms - || now_unix_ms.saturating_sub(last_sweep_unix_ms) >= interval_ms -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum CheckpointInvalidationReason { - IdentifierRegressedOrReset, - SourceHighWatermarkMissing, - IncrementalResponseUnverified, - ReceiptScopeMismatch, - InvalidProofReceipt, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum CheckpointTransition { - Advanced { - from: u64, - to: u64, - }, - Unchanged { - at: u64, - }, - Invalidated { - at: u64, - reason: CheckpointInvalidationReason, - }, -} - -/// Advance only from a fully verified incremental response and an explicit -/// source high-watermark. Record IDs inside the overlap are intentionally not -/// used to detect regression. -pub fn apply_incremental_high_watermark( - checkpoint: &mut IncrementalCheckpoint, - receipt: &VerifiedIncrementalReceipt, -) -> CheckpointTransition { - let invalidate = |checkpoint: &mut IncrementalCheckpoint, - reason: CheckpointInvalidationReason| { - checkpoint.invalidated_reason = Some(reason); - CheckpointTransition::Invalidated { - at: checkpoint.high_watermark, - reason, - } - }; - - if let Some(reason) = checkpoint.invalidated_reason { - return CheckpointTransition::Invalidated { - at: checkpoint.high_watermark, - reason, - }; - } - - if receipt.scope != checkpoint.scope || receipt.checkpoint_before != checkpoint.high_watermark { - return invalidate( - checkpoint, - CheckpointInvalidationReason::ReceiptScopeMismatch, - ); - } - if !is_lower_sha256(&receipt.proof_sha256) { - return invalidate( - checkpoint, - CheckpointInvalidationReason::InvalidProofReceipt, - ); - } - if receipt.verification != VerificationState::Verified { - return invalidate( - checkpoint, - CheckpointInvalidationReason::IncrementalResponseUnverified, - ); - } - let Some(observed) = receipt.observed_source_high_watermark else { - return invalidate( - checkpoint, - CheckpointInvalidationReason::SourceHighWatermarkMissing, - ); - }; - if observed < checkpoint.high_watermark { - return invalidate( - checkpoint, - CheckpointInvalidationReason::IdentifierRegressedOrReset, - ); - } - if observed == checkpoint.high_watermark { - checkpoint.last_transition_proof_sha256 = receipt.proof_sha256.clone(); - return CheckpointTransition::Unchanged { at: observed }; - } - - let from = checkpoint.high_watermark; - checkpoint.high_watermark = observed; - checkpoint.last_transition_proof_sha256 = receipt.proof_sha256.clone(); - CheckpointTransition::Advanced { from, to: observed } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct CanonicalChange { - pub stable_identity: String, - pub change_id: u64, - pub canonical_sha256: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] -pub struct AmbiguousChange { - pub change_id: u64, - pub reason: ChangeRejectionReason, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum ChangeRejectionReason { - InvalidIdentity, - InvalidCanonicalSha256, - ConflictingPayload, -} - -/// Deduplicate overlap records deterministically. The newest change identifier -/// wins; identical replayed records collapse; conflicting payloads at the same -/// identity and change identifier fail closed. -pub fn deduplicate_changes( - changes: impl IntoIterator, -) -> Result, AmbiguousChange> { - let mut deduplicated = BTreeMap::::new(); - for change in changes { - if !valid_identity(&change.stable_identity) { - return Err(AmbiguousChange { - change_id: change.change_id, - reason: ChangeRejectionReason::InvalidIdentity, - }); - } - if !is_lower_sha256(&change.canonical_sha256) { - return Err(AmbiguousChange { - change_id: change.change_id, - reason: ChangeRejectionReason::InvalidCanonicalSha256, - }); - } - match deduplicated.get(&change.stable_identity) { - None => { - deduplicated.insert(change.stable_identity.clone(), change); - } - Some(existing) if change.change_id > existing.change_id => { - deduplicated.insert(change.stable_identity.clone(), change); - } - Some(existing) if change.change_id < existing.change_id => {} - Some(existing) if change.canonical_sha256 == existing.canonical_sha256 => {} - Some(_) => { - return Err(AmbiguousChange { - change_id: change.change_id, - reason: ChangeRejectionReason::ConflictingPayload, - }); - } - } - } - Ok(deduplicated.into_values().collect()) -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct DeletionCapabilityObservation { - scope: IncrementalScope, - rule_id: String, - state: CapabilityState, - confidence: EvidenceConfidence, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ExplicitTombstone { - pub stable_identity: String, - pub rule_id: String, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct IncrementalReconciliation { - pub upserted_identities: BTreeSet, - pub deleted_identities: BTreeSet, - /// Existing records not mentioned by the feed remain present. This set is - /// explicit so callers cannot accidentally interpret absence as deletion. - pub retained_absent_identities: BTreeSet, - pub rejected_tombstones: BTreeSet, -} - -#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum IncrementalReconciliationError { - InvalidIdentity, - InvalidDeletionRule, - AmbiguousChange { - change_id: u64, - reason: ChangeRejectionReason, - }, - ConflictingAction, -} - -pub fn reconcile_incremental( - scope: &IncrementalScope, - existing_identities: &BTreeSet, - changes: &[CanonicalChange], - tombstones: &[ExplicitTombstone], - deletion_capability: Option<&DeletionCapabilityObservation>, -) -> Result { - for identity in existing_identities { - if !valid_identity(identity) { - return Err(IncrementalReconciliationError::InvalidIdentity); - } - } - let deduplicated = deduplicate_changes(changes.iter().cloned()).map_err(|error| { - IncrementalReconciliationError::AmbiguousChange { - change_id: error.change_id, - reason: error.reason, - } - })?; - let upserted_identities = deduplicated - .iter() - .map(|change| change.stable_identity.clone()) - .collect::>(); - - let mut deleted_identities = BTreeSet::new(); - let mut rejected_tombstones = BTreeSet::new(); - for tombstone in tombstones { - if !valid_identity(&tombstone.stable_identity) { - return Err(IncrementalReconciliationError::InvalidIdentity); - } - if !valid_rule_id(&tombstone.rule_id) { - return Err(IncrementalReconciliationError::InvalidDeletionRule); - } - let proven = deletion_capability.is_some_and(|capability| { - capability.scope == *scope - && capability.state == CapabilityState::Supported - && capability.confidence == EvidenceConfidence::Observed - && !capability.rule_id.trim().is_empty() - && capability.rule_id == tombstone.rule_id - }); - if proven { - deleted_identities.insert(tombstone.stable_identity.clone()); - } else { - rejected_tombstones.insert(tombstone.stable_identity.clone()); - } - } - if upserted_identities - .intersection(&deleted_identities) - .next() - .is_some() - { - return Err(IncrementalReconciliationError::ConflictingAction); - } - - let retained_absent_identities = existing_identities - .difference(&upserted_identities) - .filter(|identity| !deleted_identities.contains(*identity)) - .cloned() - .collect(); - - Ok(IncrementalReconciliation { - upserted_identities, - deleted_identities, - retained_absent_identities, - rejected_tombstones, - }) -} - -fn valid_identity(value: &str) -> bool { - !value.trim().is_empty() && value.len() <= 512 && !value.chars().any(char::is_control) -} - -fn valid_rule_id(value: &str) -> bool { - !value.is_empty() - && value.len() <= 128 - && value - .bytes() - .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.')) -} - -fn is_lower_sha256(value: &str) -> bool { - value.len() == 64 - && value - .bytes() - .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn scope(release: &str, query_profile: &str) -> IncrementalScope { - IncrementalScope { - source_lineage: "source-lineage".to_string(), - company_guid: "company-guid".to_string(), - company_fingerprint: "company-fingerprint".to_string(), - object_type: "voucher".to_string(), - capability_profile_version: 1, - product: "tally_prime".to_string(), - release: release.to_string(), - mode: "education".to_string(), - transport: TransportId::XmlHttp, - pack: CapabilityPackId::CoreAccounting, - pack_schema_version: PackSchemaVersion { major: 1, minor: 0 }, - query_profile: query_profile.to_string(), - filters_sha256: "a".repeat(64), - date_window_policy: "change_id_overlap_v1".to_string(), - } - } - - fn observed_capability(scope: IncrementalScope) -> IncrementalCapabilityObservation { - IncrementalCapabilityObservation { - scope, - state: CapabilityState::Supported, - confidence: EvidenceConfidence::Observed, - identifier_semantics: ChangeIdentifierSemantics::MonotonicPerObject, - inclusive_lower_bound_observed: true, - explicit_source_high_watermark_observed: true, - } - } - - fn checkpoint(scope: IncrementalScope, high_watermark: u64) -> IncrementalCheckpoint { - IncrementalCheckpoint { - scope, - high_watermark, - established_by_verified_full_snapshot: true, - established_by_proof_sha256: "c".repeat(64), - last_transition_proof_sha256: "c".repeat(64), - last_identity_sweep_unix_ms: 1_000, - invalidated_reason: None, - } - } - - fn full_receipt( - scope: IncrementalScope, - verification: VerificationState, - high_watermark: Option, - ) -> VerifiedFullSnapshotReceipt { - VerifiedFullSnapshotReceipt { - scope, - verification, - proof_sha256: "d".repeat(64), - observed_source_high_watermark: high_watermark, - completed_at_unix_ms: 1_000, - } - } - - #[test] - fn overlap_start_saturates_and_never_exceeds_checkpoint() { - let scope = scope("7.0", "voucher-v1"); - let capability = observed_capability(scope.clone()); - for checkpoint_value in 0..=256 { - for overlap in [0, 1, 7, 128, u64::MAX] { - let plan = plan_sync( - IncrementalPolicy { - overlap_identifiers: overlap, - identity_sweep_interval_ms: 10_000, - }, - &scope, - Some(&capability), - Some(&checkpoint(scope.clone(), checkpoint_value)), - 1_001, - ); - let SyncPlan::Incremental { - from_change_id_inclusive, - checkpoint_before, - .. - } = plan - else { - panic!("observed exact capability should permit incremental sync"); - }; - assert_eq!(checkpoint_before, checkpoint_value); - assert_eq!( - from_change_id_inclusive, - checkpoint_value.saturating_sub(overlap) - ); - assert!(from_change_id_inclusive <= checkpoint_before); - } - } - } - - #[test] - fn any_scope_drift_forces_honest_full_snapshot_fallback() { - let original = scope("7.0", "voucher-v1"); - let capability = observed_capability(original.clone()); - let checkpoint = checkpoint(original.clone(), 42); - let mut object_changed = original.clone(); - object_changed.object_type = "ledger".to_string(); - let mut profile_changed = original.clone(); - profile_changed.capability_profile_version = 2; - let mut transport_changed = original.clone(); - transport_changed.transport = TransportId::JsonEx; - let mut schema_changed = original.clone(); - schema_changed.pack_schema_version.minor = 1; - let mut company_changed = original.clone(); - company_changed.company_guid = "different-company".to_string(); - let mut pack_changed = original.clone(); - pack_changed.pack = CapabilityPackId::BillsAndPayments; - let mut lineage_changed = original.clone(); - lineage_changed.source_lineage = "different-source-lineage".to_string(); - let mut filters_changed = original.clone(); - filters_changed.filters_sha256 = "b".repeat(64); - let mut window_policy_changed = original.clone(); - window_policy_changed.date_window_policy = "different-policy".to_string(); - for changed in [ - scope("7.1", "voucher-v1"), - scope("7.0", "voucher-v2"), - object_changed, - profile_changed, - transport_changed, - schema_changed, - company_changed, - pack_changed, - lineage_changed, - filters_changed, - window_policy_changed, - ] { - let plan = plan_sync( - IncrementalPolicy::default(), - &changed, - Some(&capability), - Some(&checkpoint), - 2_000, - ); - assert!(matches!( - plan, - SyncPlan::FullSnapshot { - reason: FullSnapshotReason::CapabilityScopeDrift, - .. - } - )); - } - } - - #[test] - fn documented_or_inferred_capability_is_not_treated_as_observed() { - let scope = scope("7.0", "voucher-v1"); - for confidence in [ - EvidenceConfidence::Documented, - EvidenceConfidence::Inferred, - EvidenceConfidence::Unknown, - ] { - let mut capability = observed_capability(scope.clone()); - capability.confidence = confidence; - assert!(matches!( - plan_sync( - IncrementalPolicy::default(), - &scope, - Some(&capability), - Some(&checkpoint(scope.clone(), 42)), - 2_000, - ), - SyncPlan::FullSnapshot { - reason: FullSnapshotReason::CapabilityNotObservedAtRuntime, - .. - } - )); - } - for missing_protocol_fact in [ - { - let mut capability = observed_capability(scope.clone()); - capability.inclusive_lower_bound_observed = false; - capability - }, - { - let mut capability = observed_capability(scope.clone()); - capability.explicit_source_high_watermark_observed = false; - capability - }, - ] { - assert!(matches!( - plan_sync( - IncrementalPolicy::default(), - &scope, - Some(&missing_protocol_fact), - Some(&checkpoint(scope.clone(), 42)), - 2_000, - ), - SyncPlan::FullSnapshot { - reason: FullSnapshotReason::CapabilityNotObservedAtRuntime, - .. - } - )); - } - } - - #[test] - fn malformed_filter_hash_or_missing_profile_dimension_forces_full_snapshot() { - let valid = scope("7.0", "voucher-v1"); - let mut invalid_hash = valid.clone(); - invalid_hash.filters_sha256 = "not-a-sha256".to_string(); - let mut missing_profile = valid.clone(); - missing_profile.capability_profile_version = 0; - let mut missing_window_policy = valid; - missing_window_policy.date_window_policy.clear(); - for invalid in [invalid_hash, missing_profile, missing_window_policy] { - assert!(matches!( - plan_sync(IncrementalPolicy::default(), &invalid, None, None, 2_000), - SyncPlan::FullSnapshot { - reason: FullSnapshotReason::ScopeIncomplete, - .. - } - )); - } - } - - #[test] - fn periodic_identity_sweep_is_due_at_interval_and_on_clock_regression() { - let scope = scope("7.0", "voucher-v1"); - let capability = observed_capability(scope.clone()); - let checkpoint = checkpoint(scope.clone(), 42); - for (now, expected) in [(999, true), (1_000, false), (10_999, false), (11_000, true)] { - let SyncPlan::Incremental { - identity_sweep_required, - .. - } = plan_sync( - IncrementalPolicy { - overlap_identifiers: 5, - identity_sweep_interval_ms: 10_000, - }, - &scope, - Some(&capability), - Some(&checkpoint), - now, - ) - else { - panic!("valid incremental plan"); - }; - assert_eq!(identity_sweep_required, expected, "now={now}"); - } - } - - #[test] - fn identifier_regression_or_missing_proof_invalidates_checkpoint() { - for observed in [Some(41), None] { - let scope = scope("7.0", "voucher-v1"); - let mut checkpoint = checkpoint(scope.clone(), 42); - let receipt = VerifiedIncrementalReceipt { - scope, - checkpoint_before: 42, - verification: VerificationState::Verified, - proof_sha256: "e".repeat(64), - observed_source_high_watermark: observed, - }; - let transition = apply_incremental_high_watermark(&mut checkpoint, &receipt); - assert!(matches!( - transition, - CheckpointTransition::Invalidated { .. } - )); - assert!(checkpoint.invalidated_reason.is_some()); - } - } - - #[test] - fn overlap_deduplication_is_idempotent_and_order_independent() { - let input = vec![ - CanonicalChange { - stable_identity: "a".to_string(), - change_id: 10, - canonical_sha256: "1".repeat(64), - }, - CanonicalChange { - stable_identity: "b".to_string(), - change_id: 11, - canonical_sha256: "2".repeat(64), - }, - CanonicalChange { - stable_identity: "a".to_string(), - change_id: 12, - canonical_sha256: "3".repeat(64), - }, - CanonicalChange { - stable_identity: "b".to_string(), - change_id: 11, - canonical_sha256: "2".repeat(64), - }, - ]; - let mut reversed = input.clone(); - reversed.reverse(); - - let first = deduplicate_changes(input).expect("unambiguous records"); - let second = deduplicate_changes(reversed).expect("unambiguous records"); - assert_eq!(first, second); - assert_eq!(deduplicate_changes(first.clone()).unwrap(), first); - assert_eq!(first[0].canonical_sha256, "3".repeat(64)); - } - - #[test] - fn same_change_id_with_different_content_fails_closed() { - let result = deduplicate_changes([ - CanonicalChange { - stable_identity: "voucher-1".to_string(), - change_id: 7, - canonical_sha256: "a".repeat(64), - }, - CanonicalChange { - stable_identity: "voucher-1".to_string(), - change_id: 7, - canonical_sha256: "b".repeat(64), - }, - ]); - assert_eq!( - result, - Err(AmbiguousChange { - change_id: 7, - reason: ChangeRejectionReason::ConflictingPayload, - }) - ); - } - - #[test] - fn absence_never_deletes_and_only_proven_tombstones_are_accepted() { - let scope = scope("7.0", "voucher-v1"); - let existing = ["unchanged", "edited", "deleted"] - .into_iter() - .map(str::to_string) - .collect(); - let changes = [CanonicalChange { - stable_identity: "edited".to_string(), - change_id: 44, - canonical_sha256: "a".repeat(64), - }]; - let tombstones = [ExplicitTombstone { - stable_identity: "deleted".to_string(), - rule_id: "explicit-deleted-collection-v1".to_string(), - }]; - - let without_proof = - reconcile_incremental(&scope, &existing, &changes, &tombstones, None).unwrap(); - assert!(without_proof.deleted_identities.is_empty()); - assert_eq!( - without_proof.retained_absent_identities, - ["deleted", "unchanged"] - .into_iter() - .map(str::to_string) - .collect() - ); - - let deletion_capability = DeletionCapabilityObservation { - scope: scope.clone(), - rule_id: "explicit-deleted-collection-v1".to_string(), - state: CapabilityState::Supported, - confidence: EvidenceConfidence::Observed, - }; - let proven = reconcile_incremental( - &scope, - &existing, - &changes, - &tombstones, - Some(&deletion_capability), - ) - .unwrap(); - assert_eq!( - proven.deleted_identities, - ["deleted"].into_iter().map(str::to_string).collect() - ); - assert_eq!( - proven.retained_absent_identities, - ["unchanged"].into_iter().map(str::to_string).collect() - ); - } - - #[test] - fn only_verified_full_snapshot_can_establish_incremental_checkpoint() { - let scope = scope("7.0", "voucher-v1"); - let capability = observed_capability(scope.clone()); - for verification in [VerificationState::Partial, VerificationState::Unverified] { - assert_eq!( - establish_checkpoint_from_full_snapshot( - scope.clone(), - &capability, - full_receipt(scope.clone(), verification, Some(42)), - ), - Err(FullSnapshotReason::NoVerifiedCheckpoint) - ); - } - assert_eq!( - establish_checkpoint_from_full_snapshot( - scope.clone(), - &capability, - full_receipt(scope, VerificationState::Verified, Some(42)), - ) - .unwrap() - .high_watermark, - 42 - ); - } - - #[test] - fn receipt_scope_or_checkpoint_drift_invalidates_authority() { - let expected_scope = scope("7.0", "voucher-v1"); - let capability = observed_capability(expected_scope.clone()); - let different_scope = scope("7.1", "voucher-v1"); - assert_eq!( - establish_checkpoint_from_full_snapshot( - expected_scope.clone(), - &capability, - full_receipt( - different_scope.clone(), - VerificationState::Verified, - Some(42) - ), - ), - Err(FullSnapshotReason::ReceiptScopeMismatch) - ); - - let mut checkpoint = checkpoint(expected_scope, 42); - let transition = apply_incremental_high_watermark( - &mut checkpoint, - &VerifiedIncrementalReceipt { - scope: different_scope, - checkpoint_before: 41, - verification: VerificationState::Verified, - proof_sha256: "f".repeat(64), - observed_source_high_watermark: Some(43), - }, - ); - assert_eq!( - transition, - CheckpointTransition::Invalidated { - at: 42, - reason: CheckpointInvalidationReason::ReceiptScopeMismatch, - } - ); - } - - #[test] - fn one_identity_cannot_be_upserted_and_deleted_in_the_same_delta() { - let scope = scope("7.0", "voucher-v1"); - let identity = "same-identity"; - let changes = [CanonicalChange { - stable_identity: identity.to_string(), - change_id: 44, - canonical_sha256: "a".repeat(64), - }]; - let tombstones = [ExplicitTombstone { - stable_identity: identity.to_string(), - rule_id: "deleted_v1".to_string(), - }]; - let deletion_capability = DeletionCapabilityObservation { - scope: scope.clone(), - rule_id: "deleted_v1".to_string(), - state: CapabilityState::Supported, - confidence: EvidenceConfidence::Observed, - }; - assert_eq!( - reconcile_incremental( - &scope, - &BTreeSet::new(), - &changes, - &tombstones, - Some(&deletion_capability), - ), - Err(IncrementalReconciliationError::ConflictingAction) - ); - } - - #[test] - fn reconciliation_cannot_bypass_overlap_deduplication() { - let scope = scope("7.0", "voucher-v1"); - let existing = BTreeSet::new(); - let conflicting = [ - CanonicalChange { - stable_identity: "voucher-1".to_string(), - change_id: 7, - canonical_sha256: "a".repeat(64), - }, - CanonicalChange { - stable_identity: "voucher-1".to_string(), - change_id: 7, - canonical_sha256: "b".repeat(64), - }, - ]; - assert_eq!( - reconcile_incremental(&scope, &existing, &conflicting, &[], None), - Err(IncrementalReconciliationError::AmbiguousChange { - change_id: 7, - reason: ChangeRejectionReason::ConflictingPayload, - }) - ); - - let ordered_overlap = [ - CanonicalChange { - stable_identity: "voucher-1".to_string(), - change_id: 9, - canonical_sha256: "c".repeat(64), - }, - CanonicalChange { - stable_identity: "voucher-1".to_string(), - change_id: 8, - canonical_sha256: "b".repeat(64), - }, - ]; - let result = reconcile_incremental(&scope, &existing, &ordered_overlap, &[], None) - .expect("newest unambiguous overlap record wins"); - assert_eq!( - result.upserted_identities, - ["voucher-1"].into_iter().map(str::to_string).collect() - ); - } - - #[test] - fn invalidated_checkpoint_is_terminal_for_later_receipts() { - let scope = scope("7.0", "voucher-v1"); - let mut checkpoint = checkpoint(scope.clone(), 42); - checkpoint.invalidated_reason = - Some(CheckpointInvalidationReason::IdentifierRegressedOrReset); - let prior_proof = checkpoint.last_transition_proof_sha256.clone(); - let transition = apply_incremental_high_watermark( - &mut checkpoint, - &VerifiedIncrementalReceipt { - scope, - checkpoint_before: 42, - verification: VerificationState::Verified, - proof_sha256: "f".repeat(64), - observed_source_high_watermark: Some(43), - }, - ); - assert_eq!( - transition, - CheckpointTransition::Invalidated { - at: 42, - reason: CheckpointInvalidationReason::IdentifierRegressedOrReset, - } - ); - assert_eq!(checkpoint.high_watermark, 42); - assert_eq!(checkpoint.last_transition_proof_sha256, prior_proof); - } -} diff --git a/src-tauri/crates/bridge-tally-observability/Cargo.toml b/src-tauri/crates/bridge-tally-observability/Cargo.toml deleted file mode 100644 index a7d84af..0000000 --- a/src-tauri/crates/bridge-tally-observability/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "bridge-tally-observability" -version = "0.1.0" -description = "Portable bounded and privacy-reduced Tally telemetry contract for Bridge" -license = "Apache-2.0" -repository = "https://github.com/lamemustafa/bridge" -publish = false -edition = "2021" -rust-version = "1.96" - -[dependencies] -serde = { version = "1", features = ["derive"] } -serde_json = "1" -sha2 = "0.11" diff --git a/src-tauri/crates/bridge-tally-observability/src/lib.rs b/src-tauri/crates/bridge-tally-observability/src/lib.rs deleted file mode 100644 index 1b38019..0000000 --- a/src-tauri/crates/bridge-tally-observability/src/lib.rs +++ /dev/null @@ -1,851 +0,0 @@ -//! Fixed-cardinality, local-only Tally transport observations. -//! -//! This crate deliberately has no runtime, HTTP, database, logging, tracing, -//! persistence, system-metrics, or exporter dependency. Its preview is a -//! privacy-reduced operational aid, not Proof of Sync or performance support. - -use std::{fmt, sync::Mutex, time::Duration}; - -use serde::Serialize; -use sha2::{Digest, Sha256}; - -pub const PREVIEW_SCHEMA: &str = "bridge.tally.telemetry-preview/2"; -pub const MAX_SERIALIZED_PREVIEW_BYTES: usize = 64 * 1024; -pub const LATENCY_UPPER_BOUNDS_MICROS: [u64; 8] = [ - 1_000, 5_000, 25_000, 100_000, 500_000, 2_000_000, 10_000_000, 30_000_000, -]; -pub const RESPONSE_BYTE_UPPER_BOUNDS: [u64; 6] = [ - 0, - 1_024, - 64 * 1_024, - 1_024 * 1_024, - 8 * 1_024 * 1_024, - 32 * 1_024 * 1_024, -]; - -const LATENCY_BUCKETS: usize = LATENCY_UPPER_BOUNDS_MICROS.len() + 1; -const BYTE_BUCKETS: usize = RESPONSE_BYTE_UPPER_BOUNDS.len() + 1; -const QUEUE_OUTCOMES: usize = QueueOutcome::ALL.len(); -const RESPONSE_OUTCOMES: usize = ResponseOutcome::ALL.len(); -const CIRCUIT_REJECT_REASONS: usize = CircuitRejectReason::ALL.len(); -const REQUEST_CLASSES: usize = RequestClass::ALL.len(); -const QUEUE_CELLS: usize = REQUEST_CLASSES * QUEUE_OUTCOMES * LATENCY_BUCKETS; -const RESPONSE_LATENCY_CELLS: usize = REQUEST_CLASSES * RESPONSE_OUTCOMES * LATENCY_BUCKETS; -const RESPONSE_BYTE_CELLS: usize = REQUEST_CLASSES * RESPONSE_OUTCOMES * BYTE_BUCKETS; -const RESPONSE_BYTE_UNAVAILABLE_CELLS: usize = REQUEST_CLASSES * RESPONSE_OUTCOMES; -const CIRCUIT_REJECTION_CELLS: usize = REQUEST_CLASSES * CIRCUIT_REJECT_REASONS; -pub const FIXED_HISTOGRAM_CELL_COUNT: usize = QUEUE_CELLS - + RESPONSE_LATENCY_CELLS - + RESPONSE_BYTE_CELLS - + RESPONSE_BYTE_UNAVAILABLE_CELLS - + CIRCUIT_REJECTION_CELLS; -const EXPORT_HASH_DOMAIN: &[u8] = b"bridge.tally.telemetry-preview-payload/2\0"; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum RequestClass { - Status, - Capability, - CompanyList, - MasterExport, - VoucherExport, - ReportExport, - Import, - OtherRead, -} - -impl RequestClass { - pub const ALL: [Self; 8] = [ - Self::Status, - Self::Capability, - Self::CompanyList, - Self::MasterExport, - Self::VoucherExport, - Self::ReportExport, - Self::Import, - Self::OtherRead, - ]; - - const fn index(self) -> usize { - self as usize - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum QueueOutcome { - Acquired, - Deadline, - Cancelled, -} - -impl QueueOutcome { - pub const ALL: [Self; 3] = [Self::Acquired, Self::Deadline, Self::Cancelled]; - - const fn index(self) -> usize { - self as usize - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum ResponseOutcome { - Success, - Cancelled, - Timeout, - Transport, - HttpStatus, - SizeLimit, - Decode, - Application, - Parse, - Validation, -} - -impl ResponseOutcome { - pub const ALL: [Self; 10] = [ - Self::Success, - Self::Cancelled, - Self::Timeout, - Self::Transport, - Self::HttpStatus, - Self::SizeLimit, - Self::Decode, - Self::Application, - Self::Parse, - Self::Validation, - ]; - - const fn index(self) -> usize { - self as usize - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum CircuitRejectReason { - Cooldown, - HalfOpenProbeInFlight, -} - -impl CircuitRejectReason { - pub const ALL: [Self; 2] = [Self::Cooldown, Self::HalfOpenProbeInFlight]; - - const fn index(self) -> usize { - self as usize - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum BodyBytesObservation { - Observed(u64), - Unavailable, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum AttemptObservation { - CircuitRejected { - class: RequestClass, - reason: CircuitRejectReason, - }, - QueueDeadline { - class: RequestClass, - queue_wait: Duration, - }, - QueueCancelled { - class: RequestClass, - queue_wait: Duration, - }, - Response { - class: RequestClass, - queue_wait: Duration, - outcome: ResponseOutcome, - /// Custom Bridge pipeline duration from send start through bounded - /// body read and decode completion. This is not an OTel HTTP duration. - response_pipeline_elapsed: Duration, - /// Bytes consumed before the terminal outcome, including partial - /// failed bodies. This is not an OTel HTTP response-body-size metric. - observed_body_bytes: BodyBytesObservation, - }, -} - -/// A sink accepts one caller-supplied terminal attempt and no dynamic labels -/// or text. It aggregates observations; it does not authenticate provenance or -/// detect duplicate calls. -pub trait ObservationSink { - fn record_attempt(&self, observation: AttemptObservation); -} - -#[derive(Clone)] -struct AggregateState { - queue_latency: [u64; QUEUE_CELLS], - response_latency: [u64; RESPONSE_LATENCY_CELLS], - response_bytes: [u64; RESPONSE_BYTE_CELLS], - response_bytes_unavailable: [u64; RESPONSE_BYTE_UNAVAILABLE_CELLS], - circuit_rejections: [u64; CIRCUIT_REJECTION_CELLS], - saturated_cell_increments: u64, -} - -impl Default for AggregateState { - fn default() -> Self { - Self { - queue_latency: [0; QUEUE_CELLS], - response_latency: [0; RESPONSE_LATENCY_CELLS], - response_bytes: [0; RESPONSE_BYTE_CELLS], - response_bytes_unavailable: [0; RESPONSE_BYTE_UNAVAILABLE_CELLS], - circuit_rejections: [0; CIRCUIT_REJECTION_CELLS], - saturated_cell_increments: 0, - } - } -} - -/// Fixed-memory coherent aggregation. Poison recovery retains observations; -/// measurement failure never changes a Tally operation result. -#[derive(Default)] -pub struct TelemetryCollector { - state: Mutex, -} - -impl fmt::Debug for TelemetryCollector { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter - .debug_struct("TelemetryCollector") - .field("fixed_histogram_cell_count", &FIXED_HISTOGRAM_CELL_COUNT) - .finish_non_exhaustive() - } -} - -impl TelemetryCollector { - pub fn new() -> Self { - Self::default() - } - - pub fn preview_v2(&self) -> TallyTelemetryPreviewV2 { - let state = self.snapshot(); - let rows = RequestClass::ALL.map(|class| build_row(&state, class)); - TallyTelemetryPreviewV2 { - schema: PREVIEW_SCHEMA, - schema_version: 2, - privacy_profile: "fixed_dimensions_bucketed_values_v1", - collection_scope: "unstamped_collector_instance_lifetime", - snapshot_consistency: "coherent_mutex_snapshot", - observation_provenance: "caller_supplied_not_authenticated", - collection_completeness: "not_established", - lifecycle_consistency: "one_terminal_observation_per_attempt_duplicates_not_detected", - standards_mapping: "custom_lossy_summary_not_an_opentelemetry_histogram", - integrity_claim: "checksum_only", - authenticity_claim: "none", - collector_has_network_exporter: false, - establishes_performance_support: false, - rows_are_taxonomy_not_capability: true, - fixed_histogram_cell_count: FIXED_HISTOGRAM_CELL_COUNT as u16, - latency_upper_bounds_micros: LATENCY_UPPER_BOUNDS_MICROS, - response_byte_upper_bounds: RESPONSE_BYTE_UPPER_BOUNDS, - saturated_cell_increments: count_bucket(state.saturated_cell_increments), - rows, - } - } - - pub fn privacy_reduced_export_v2( - &self, - ) -> Result { - let preview = self.preview_v2(); - let json = - serde_json::to_string(&preview).map_err(|_| TelemetryExportError::Serialization)?; - if json.len() > MAX_SERIALIZED_PREVIEW_BYTES { - return Err(TelemetryExportError::PreviewTooLarge); - } - let payload_sha256 = hash_payload(json.as_bytes()); - Ok(PrivacyReducedTelemetryExport { - json, - payload_sha256, - }) - } - - fn snapshot(&self) -> AggregateState { - self.state - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) - .clone() - } - - fn increment(state: &mut AggregateState, cell: &mut u64) { - if *cell == u64::MAX { - state.saturated_cell_increments = state.saturated_cell_increments.saturating_add(1); - } else { - *cell += 1; - } - } -} - -impl ObservationSink for TelemetryCollector { - fn record_attempt(&self, observation: AttemptObservation) { - let mut state = self - .state - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - match observation { - AttemptObservation::CircuitRejected { class, reason } => { - let index = class.index() * CIRCUIT_REJECT_REASONS + reason.index(); - let mut cell = state.circuit_rejections[index]; - Self::increment(&mut state, &mut cell); - state.circuit_rejections[index] = cell; - } - AttemptObservation::QueueDeadline { class, queue_wait } => { - increment_queue(&mut state, class, QueueOutcome::Deadline, queue_wait); - } - AttemptObservation::QueueCancelled { class, queue_wait } => { - increment_queue(&mut state, class, QueueOutcome::Cancelled, queue_wait); - } - AttemptObservation::Response { - class, - queue_wait, - outcome, - response_pipeline_elapsed, - observed_body_bytes, - } => { - increment_queue(&mut state, class, QueueOutcome::Acquired, queue_wait); - let latency = latency_bucket(response_pipeline_elapsed); - let series = class.index() * RESPONSE_OUTCOMES + outcome.index(); - let latency_index = series * LATENCY_BUCKETS + latency; - let mut latency_cell = state.response_latency[latency_index]; - Self::increment(&mut state, &mut latency_cell); - state.response_latency[latency_index] = latency_cell; - match observed_body_bytes { - BodyBytesObservation::Observed(bytes) => { - let byte_index = series * BYTE_BUCKETS + byte_bucket(bytes); - let mut byte_cell = state.response_bytes[byte_index]; - Self::increment(&mut state, &mut byte_cell); - state.response_bytes[byte_index] = byte_cell; - } - BodyBytesObservation::Unavailable => { - let mut cell = state.response_bytes_unavailable[series]; - Self::increment(&mut state, &mut cell); - state.response_bytes_unavailable[series] = cell; - } - } - } - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum CountBucket { - Zero, - One, - TwoToFive, - SixToTwenty, - TwentyOneToHundred, - HundredOneToThousand, - OverThousand, -} - -#[derive(Debug, Clone, Serialize)] -pub struct TallyTelemetryPreviewV2 { - schema: &'static str, - schema_version: u16, - privacy_profile: &'static str, - collection_scope: &'static str, - snapshot_consistency: &'static str, - observation_provenance: &'static str, - collection_completeness: &'static str, - lifecycle_consistency: &'static str, - standards_mapping: &'static str, - integrity_claim: &'static str, - authenticity_claim: &'static str, - collector_has_network_exporter: bool, - establishes_performance_support: bool, - rows_are_taxonomy_not_capability: bool, - fixed_histogram_cell_count: u16, - latency_upper_bounds_micros: [u64; 8], - response_byte_upper_bounds: [u64; 6], - saturated_cell_increments: CountBucket, - rows: [OperationTelemetryRow; 8], -} - -impl TallyTelemetryPreviewV2 { - pub fn rows(&self) -> &[OperationTelemetryRow; 8] { - &self.rows - } - - pub const fn establishes_performance_support(&self) -> bool { - self.establishes_performance_support - } -} - -#[derive(Debug, Clone, Serialize)] -pub struct OperationTelemetryRow { - request_class: RequestClass, - circuit_rejections: [CircuitTelemetryRow; 2], - queue: [QueueTelemetryRow; 3], - response: [ResponseTelemetryRow; 10], -} - -impl OperationTelemetryRow { - pub const fn request_class(&self) -> RequestClass { - self.request_class - } -} - -#[derive(Debug, Clone, Serialize)] -struct QueueTelemetryRow { - outcome: QueueOutcome, - latency_buckets: [CountBucket; LATENCY_BUCKETS], -} - -#[derive(Debug, Clone, Serialize)] -struct ResponseTelemetryRow { - outcome: ResponseOutcome, - latency_buckets: [CountBucket; LATENCY_BUCKETS], - bytes_received_buckets: [CountBucket; BYTE_BUCKETS], - bytes_measurement_unavailable: CountBucket, -} - -#[derive(Debug, Clone, Serialize)] -struct CircuitTelemetryRow { - reason: CircuitRejectReason, - count: CountBucket, -} - -#[derive(Clone)] -pub struct PrivacyReducedTelemetryExport { - json: String, - payload_sha256: String, -} - -impl fmt::Debug for PrivacyReducedTelemetryExport { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter - .debug_struct("PrivacyReducedTelemetryExport") - .field("json_bytes", &self.json.len()) - .field("payload_sha256", &self.payload_sha256) - .finish() - } -} - -impl PrivacyReducedTelemetryExport { - pub fn json(&self) -> &str { - &self.json - } - - pub fn payload_sha256(&self) -> &str { - &self.payload_sha256 - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum TelemetryExportError { - Serialization, - PreviewTooLarge, -} - -impl TelemetryExportError { - pub const fn safe_code(self) -> &'static str { - match self { - Self::Serialization => "tally_telemetry_serialization_failed", - Self::PreviewTooLarge => "tally_telemetry_preview_too_large", - } - } -} - -impl fmt::Display for TelemetryExportError { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str(self.safe_code()) - } -} - -impl std::error::Error for TelemetryExportError {} - -fn build_row(state: &AggregateState, class: RequestClass) -> OperationTelemetryRow { - let circuit_rejections = CircuitRejectReason::ALL.map(|reason| { - let index = class.index() * CIRCUIT_REJECT_REASONS + reason.index(); - CircuitTelemetryRow { - reason, - count: count_bucket(state.circuit_rejections[index]), - } - }); - let queue = QueueOutcome::ALL.map(|outcome| { - let base = (class.index() * QUEUE_OUTCOMES + outcome.index()) * LATENCY_BUCKETS; - QueueTelemetryRow { - outcome, - latency_buckets: std::array::from_fn(|offset| { - count_bucket(state.queue_latency[base + offset]) - }), - } - }); - let response = ResponseOutcome::ALL.map(|outcome| { - let series = class.index() * RESPONSE_OUTCOMES + outcome.index(); - let latency_base = series * LATENCY_BUCKETS; - let byte_base = series * BYTE_BUCKETS; - ResponseTelemetryRow { - outcome, - latency_buckets: std::array::from_fn(|offset| { - count_bucket(state.response_latency[latency_base + offset]) - }), - bytes_received_buckets: std::array::from_fn(|offset| { - count_bucket(state.response_bytes[byte_base + offset]) - }), - bytes_measurement_unavailable: count_bucket(state.response_bytes_unavailable[series]), - } - }); - OperationTelemetryRow { - request_class: class, - circuit_rejections, - queue, - response, - } -} - -fn increment_queue( - state: &mut AggregateState, - class: RequestClass, - outcome: QueueOutcome, - elapsed: Duration, -) { - let bucket = latency_bucket(elapsed); - let index = (class.index() * QUEUE_OUTCOMES + outcome.index()) * LATENCY_BUCKETS + bucket; - let mut cell = state.queue_latency[index]; - TelemetryCollector::increment(state, &mut cell); - state.queue_latency[index] = cell; -} - -fn latency_bucket(duration: Duration) -> usize { - let micros = u64::try_from(duration.as_micros()).unwrap_or(u64::MAX); - LATENCY_UPPER_BOUNDS_MICROS - .iter() - .position(|upper| micros <= *upper) - .unwrap_or(LATENCY_BUCKETS - 1) -} - -fn byte_bucket(bytes: u64) -> usize { - RESPONSE_BYTE_UPPER_BOUNDS - .iter() - .position(|upper| bytes <= *upper) - .unwrap_or(BYTE_BUCKETS - 1) -} - -fn count_bucket(value: u64) -> CountBucket { - match value { - 0 => CountBucket::Zero, - 1 => CountBucket::One, - 2..=5 => CountBucket::TwoToFive, - 6..=20 => CountBucket::SixToTwenty, - 21..=100 => CountBucket::TwentyOneToHundred, - 101..=1_000 => CountBucket::HundredOneToThousand, - _ => CountBucket::OverThousand, - } -} - -fn hash_payload(payload: &[u8]) -> String { - let mut hasher = Sha256::new(); - hasher.update(EXPORT_HASH_DOMAIN); - hasher.update(payload); - let digest = hasher.finalize(); - let mut encoded = String::with_capacity(digest.len() * 2); - for byte in digest { - use std::fmt::Write as _; - let _ = write!(encoded, "{byte:02x}"); - } - encoded -} - -#[cfg(test)] -mod tests { - use super::*; - use std::{sync::Arc, thread}; - - #[test] - fn histogram_boundaries_are_inclusive_and_queue_semantics_are_explicit() { - let collector = TelemetryCollector::new(); - for micros in [1_000, 1_001, 30_000_000, 30_000_001] { - collector.record_attempt(AttemptObservation::Response { - class: RequestClass::Status, - queue_wait: Duration::from_micros(micros), - outcome: ResponseOutcome::Success, - response_pipeline_elapsed: Duration::ZERO, - observed_body_bytes: BodyBytesObservation::Observed(0), - }); - } - for bytes in [ - 0, - 1, - 1_024, - 1_025, - 32 * 1_024 * 1_024, - 32 * 1_024 * 1_024 + 1, - ] { - collector.record_attempt(AttemptObservation::Response { - class: RequestClass::VoucherExport, - queue_wait: Duration::ZERO, - outcome: ResponseOutcome::SizeLimit, - response_pipeline_elapsed: Duration::from_millis(10), - observed_body_bytes: BodyBytesObservation::Observed(bytes), - }); - } - let preview = collector.preview_v2(); - assert_eq!(preview.rows().len(), RequestClass::ALL.len()); - assert!(!preview.establishes_performance_support()); - let state = collector.snapshot(); - let queue_base = (RequestClass::Status.index() * QUEUE_OUTCOMES - + QueueOutcome::Acquired.index()) - * LATENCY_BUCKETS; - assert_eq!( - &state.queue_latency[queue_base..queue_base + LATENCY_BUCKETS], - &[1, 1, 0, 0, 0, 0, 0, 1, 1] - ); - let response_series = RequestClass::VoucherExport.index() * RESPONSE_OUTCOMES - + ResponseOutcome::SizeLimit.index(); - let latency_base = response_series * LATENCY_BUCKETS; - assert_eq!(state.response_latency[latency_base + 2], 6); - assert_eq!( - &state.response_bytes - [response_series * BYTE_BUCKETS..response_series * BYTE_BUCKETS + BYTE_BUCKETS], - &[1, 2, 1, 0, 0, 1, 1] - ); - let json = collector.privacy_reduced_export_v2().unwrap().json; - assert!(json.contains("\"unstamped_collector_instance_lifetime\"")); - assert!(json.contains("\"coherent_mutex_snapshot\"")); - assert!(!json.contains("post_request_spacing")); - } - - #[test] - fn cardinality_and_export_size_remain_fixed_after_many_observations() { - let collector = TelemetryCollector::new(); - for index in 0..100_000_u64 { - let class = RequestClass::ALL[index as usize % RequestClass::ALL.len()]; - collector.record_attempt(AttemptObservation::Response { - class, - queue_wait: Duration::from_micros(index), - outcome: ResponseOutcome::Success, - response_pipeline_elapsed: Duration::from_micros(index), - observed_body_bytes: BodyBytesObservation::Observed(index), - }); - } - let export = collector - .privacy_reduced_export_v2() - .expect("bounded export"); - assert_eq!(collector.preview_v2().rows().len(), RequestClass::ALL.len()); - assert!(export.json().len() <= MAX_SERIALIZED_PREVIEW_BYTES); - assert_eq!(FIXED_HISTOGRAM_CELL_COUNT, 1_592); - } - - #[test] - fn preview_has_no_input_surface_for_sensitive_or_high_cardinality_values() { - let collector = TelemetryCollector::new(); - collector.record_attempt(AttemptObservation::Response { - class: RequestClass::CompanyList, - queue_wait: Duration::from_millis(2), - outcome: ResponseOutcome::Decode, - response_pipeline_elapsed: Duration::from_millis(7), - observed_body_bytes: BodyBytesObservation::Observed(777), - }); - let export = collector.privacy_reduced_export_v2().unwrap(); - let debug = format!("{collector:?} {export:?}"); - for forbidden in [ - "BRIDGE SECRET COMPANY", - "27ABCDE1234F1Z5", - "ABCDE1234F", - "", - "127.0.0.1:9000", - "developer-home-path-sentinel", - "request-secret-id", - ] { - assert!(!export.json().contains(forbidden)); - assert!(!debug.contains(forbidden)); - } - assert!(!export.json().contains("company_guid")); - assert!(!export.json().contains("endpoint")); - assert!(!export.json().contains("timestamp")); - assert!(!export.json().contains("payload")); - } - - #[test] - fn preview_is_coherent_under_concurrent_recording_and_repeatable_when_idle() { - use std::sync::{ - atomic::{AtomicUsize, Ordering}, - Barrier, - }; - - let collector = Arc::new(TelemetryCollector::new()); - let start = Arc::new(Barrier::new(9)); - let active = Arc::new(AtomicUsize::new(8)); - let threads = (0..8) - .map(|_| { - let collector = Arc::clone(&collector); - let start = Arc::clone(&start); - let active = Arc::clone(&active); - thread::spawn(move || { - start.wait(); - for index in 0..20_000 { - collector.record_attempt(AttemptObservation::Response { - class: RequestClass::MasterExport, - queue_wait: Duration::from_millis(1), - outcome: ResponseOutcome::Success, - response_pipeline_elapsed: Duration::from_millis(5), - observed_body_bytes: BodyBytesObservation::Observed(1_024), - }); - if index % 100 == 0 { - thread::yield_now(); - } - } - active.fetch_sub(1, Ordering::Release); - }) - }) - .collect::>(); - start.wait(); - let response_series = RequestClass::MasterExport.index() * RESPONSE_OUTCOMES - + ResponseOutcome::Success.index(); - let mut concurrent_snapshots = 0_u64; - while active.load(Ordering::Acquire) > 0 { - let state = collector.snapshot(); - let latency_total = state.response_latency - [response_series * LATENCY_BUCKETS..(response_series + 1) * LATENCY_BUCKETS] - .iter() - .sum::(); - let byte_total = state.response_bytes - [response_series * BYTE_BUCKETS..(response_series + 1) * BYTE_BUCKETS] - .iter() - .sum::(); - assert_eq!(latency_total, byte_total); - assert_eq!(collector.preview_v2().rows().len(), RequestClass::ALL.len()); - concurrent_snapshots += 1; - thread::yield_now(); - } - for thread in threads { - thread.join().expect("observation thread"); - } - assert!(concurrent_snapshots > 0); - let first = collector.privacy_reduced_export_v2().unwrap(); - let second = collector.privacy_reduced_export_v2().unwrap(); - assert_eq!(first.json(), second.json()); - assert_eq!(first.payload_sha256(), second.payload_sha256()); - } - - #[test] - fn saturation_never_wraps_and_is_disclosed() { - let collector = TelemetryCollector::new(); - { - let mut state = collector.state.lock().unwrap(); - state.queue_latency[0] = u64::MAX; - } - collector.record_attempt(AttemptObservation::Response { - class: RequestClass::Status, - queue_wait: Duration::ZERO, - outcome: ResponseOutcome::Success, - response_pipeline_elapsed: Duration::ZERO, - observed_body_bytes: BodyBytesObservation::Observed(0), - }); - let state = collector.snapshot(); - assert_eq!(state.queue_latency[0], u64::MAX); - assert_eq!(state.saturated_cell_increments, 1); - assert!(collector - .privacy_reduced_export_v2() - .unwrap() - .json() - .contains("\"saturated_cell_increments\":\"one\"")); - } - - #[test] - fn longest_serialized_count_bucket_still_fits_the_reviewed_preview_ceiling() { - let collector = TelemetryCollector::new(); - { - let mut state = collector.state.lock().unwrap(); - state.queue_latency.fill(101); - state.response_latency.fill(101); - state.response_bytes.fill(101); - state.response_bytes_unavailable.fill(101); - state.circuit_rejections.fill(101); - state.saturated_cell_increments = 101; - } - let export = collector - .privacy_reduced_export_v2() - .expect("worst textual bucket export"); - assert!(export.json().len() <= MAX_SERIALIZED_PREVIEW_BYTES); - assert_eq!(collector.preview_v2().rows().len(), RequestClass::ALL.len()); - } - - #[test] - fn schema_v2_taxonomy_bounds_and_zero_preview_bytes_are_golden() { - assert_eq!(PREVIEW_SCHEMA, "bridge.tally.telemetry-preview/2"); - assert_eq!( - LATENCY_UPPER_BOUNDS_MICROS, - [1_000, 5_000, 25_000, 100_000, 500_000, 2_000_000, 10_000_000, 30_000_000,] - ); - assert_eq!( - RESPONSE_BYTE_UPPER_BOUNDS, - [0, 1_024, 65_536, 1_048_576, 8_388_608, 33_554_432] - ); - assert_eq!(QueueOutcome::ALL.len(), 3); - assert_eq!(ResponseOutcome::ALL.len(), 10); - assert_eq!(CircuitRejectReason::ALL.len(), 2); - assert_eq!(FIXED_HISTOGRAM_CELL_COUNT, 1_592); - let export = TelemetryCollector::new() - .privacy_reduced_export_v2() - .expect("golden zero preview"); - assert_eq!( - export.payload_sha256(), - "013e4b52577f9b89c22a31c203ec8940a783b3ada13e3f9d5e508b79a92ed37a" - ); - } - - #[test] - fn terminal_attempt_shape_keeps_queue_failures_out_of_response_histograms() { - let collector = TelemetryCollector::new(); - collector.record_attempt(AttemptObservation::QueueDeadline { - class: RequestClass::Capability, - queue_wait: Duration::from_secs(30), - }); - collector.record_attempt(AttemptObservation::QueueCancelled { - class: RequestClass::Capability, - queue_wait: Duration::from_millis(5), - }); - collector.record_attempt(AttemptObservation::Response { - class: RequestClass::Capability, - queue_wait: Duration::from_millis(1), - outcome: ResponseOutcome::Timeout, - response_pipeline_elapsed: Duration::from_secs(10), - observed_body_bytes: BodyBytesObservation::Unavailable, - }); - - let state = collector.snapshot(); - let queue_base = RequestClass::Capability.index() * QUEUE_OUTCOMES * LATENCY_BUCKETS; - let queue_total = state.queue_latency - [queue_base..queue_base + QUEUE_OUTCOMES * LATENCY_BUCKETS] - .iter() - .sum::(); - let response_base = RequestClass::Capability.index() * RESPONSE_OUTCOMES * LATENCY_BUCKETS; - let response_total = state.response_latency - [response_base..response_base + RESPONSE_OUTCOMES * LATENCY_BUCKETS] - .iter() - .sum::(); - assert_eq!(queue_total, 3); - assert_eq!(response_total, 1); - } - - #[test] - fn circuit_rejections_and_unavailable_byte_measurement_are_explicit() { - let collector = TelemetryCollector::new(); - collector.record_attempt(AttemptObservation::CircuitRejected { - class: RequestClass::CompanyList, - reason: CircuitRejectReason::Cooldown, - }); - collector.record_attempt(AttemptObservation::Response { - class: RequestClass::CompanyList, - queue_wait: Duration::ZERO, - outcome: ResponseOutcome::Application, - response_pipeline_elapsed: Duration::from_millis(2), - observed_body_bytes: BodyBytesObservation::Unavailable, - }); - let state = collector.snapshot(); - let circuit = RequestClass::CompanyList.index() * CIRCUIT_REJECT_REASONS - + CircuitRejectReason::Cooldown.index(); - let response = RequestClass::CompanyList.index() * RESPONSE_OUTCOMES - + ResponseOutcome::Application.index(); - assert_eq!(state.circuit_rejections[circuit], 1); - assert_eq!(state.response_bytes_unavailable[response], 1); - let json = collector.privacy_reduced_export_v2().unwrap().json; - assert!(json.contains("\"cooldown\"")); - assert!(json.contains("\"bytes_measurement_unavailable\":\"one\"")); - } -} diff --git a/src-tauri/crates/bridge-tally-runtime/Cargo.toml b/src-tauri/crates/bridge-tally-runtime/Cargo.toml deleted file mode 100644 index beccb70..0000000 --- a/src-tauri/crates/bridge-tally-runtime/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "bridge-tally-runtime" -version = "0.1.0" -description = "Portable read-only endpoint execution control plane for Bridge Tally" -license = "Apache-2.0" -repository = "https://github.com/lamemustafa/bridge" -publish = false -edition = "2021" -rust-version = "1.96" - -[dependencies] -bridge-tally-observability = { path = "../bridge-tally-observability" } -tokio = { version = "1", features = ["macros", "sync", "time"] } -tokio-util = { version = "0.7", features = ["rt"] } - -[dev-dependencies] -tokio = { version = "1", features = ["macros", "rt-multi-thread"] } diff --git a/src-tauri/crates/bridge-tally-runtime/README.md b/src-tauri/crates/bridge-tally-runtime/README.md deleted file mode 100644 index 0c8ce1a..0000000 --- a/src-tauri/crates/bridge-tally-runtime/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# Bridge Tally portable runtime - -This crate owns the read-side endpoint execution control plane independently of -Tauri, SQLCipher, and the native application. It provides per-endpoint -serialization, queue deadlines, cancellation, request spacing, circuit -admission, deterministic bounded transient-read retry, and fixed-cardinality -privacy-reduced observations. - -The public operation enum contains no import or write variant. The generic -closure is an integration seam rather than proof about the closure's behavior; -native dependency and source checks must continue to ensure that write paths do -not call the read retry API. diff --git a/src-tauri/crates/bridge-tally-runtime/src/lib.rs b/src-tauri/crates/bridge-tally-runtime/src/lib.rs deleted file mode 100644 index 85a57d4..0000000 --- a/src-tauri/crates/bridge-tally-runtime/src/lib.rs +++ /dev/null @@ -1,1241 +0,0 @@ -//! Portable execution control for read-only Tally operations. -//! -//! This crate authenticates no endpoint and establishes no accounting or -//! support claim. It controls local execution and emits only fixed-cardinality, -//! privacy-reduced observations. - -use std::{ - collections::{hash_map::DefaultHasher, HashMap}, - fmt, - future::Future, - hash::{Hash, Hasher}, - sync::{Arc, Mutex}, - time::{Duration, Instant, SystemTime, UNIX_EPOCH}, -}; - -use bridge_tally_observability::{ - AttemptObservation, ObservationSink, RequestClass, ResponseOutcome, TelemetryCollector, -}; -pub use bridge_tally_observability::{BodyBytesObservation, CircuitRejectReason}; -use tokio::sync::{Mutex as AsyncMutex, MutexGuard}; -use tokio_util::sync::CancellationToken; - -const MAX_ENDPOINT_IDENTITY_BYTES: usize = 512; -const MAX_QUEUE_DEADLINE: Duration = Duration::from_secs(120); -const MAX_REQUEST_SPACING: Duration = Duration::from_secs(10); -const MAX_CIRCUIT_COOLDOWN: Duration = Duration::from_secs(10 * 60); -const MAX_RETRY_DELAY: Duration = Duration::from_secs(60); -pub const TELEMETRY_PREVIEW_SCHEMA: &str = bridge_tally_observability::PREVIEW_SCHEMA; - -#[derive(Clone, PartialEq, Eq, Hash)] -pub struct EndpointIdentity(String); - -impl EndpointIdentity { - pub fn new(value: impl Into) -> Result { - let value = value.into(); - if value.is_empty() - || value.len() > MAX_ENDPOINT_IDENTITY_BYTES - || value.trim() != value - || value.chars().any(char::is_control) - { - return Err(RuntimeConfigurationError::EndpointIdentityInvalid); - } - Ok(Self(value)) - } - - fn private_value(&self) -> &str { - &self.0 - } -} - -impl fmt::Debug for EndpointIdentity { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str("EndpointIdentity([redacted])") - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ReadOperation { - Status, - Capability, - CompanyList, - MasterExport, - VoucherExport, - ReportExport, - OtherRead, -} - -impl ReadOperation { - pub const fn request_class(self) -> RequestClass { - match self { - Self::Status => RequestClass::Status, - Self::Capability => RequestClass::Capability, - Self::CompanyList => RequestClass::CompanyList, - Self::MasterExport => RequestClass::MasterExport, - Self::VoucherExport => RequestClass::VoucherExport, - Self::ReportExport => RequestClass::ReportExport, - Self::OtherRead => RequestClass::OtherRead, - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ReadFailureClass { - Connection, - RequestTimeout, - RequestFailed, - HttpServer, - RateLimited, - HttpClient, - SizeLimit, - Decode, - Application, - Parse, - Validation, - CompanyMismatch, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum EndpointCircuitState { - Closed, - Open, - HalfOpen, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct EndpointRuntimeSnapshot { - pub consecutive_failures: u32, - pub circuit_state: EndpointCircuitState, - pub circuit_retry_after_unix_ms: Option, - pub half_open_probe_in_flight: bool, - pub last_failure_unix_ms: Option, -} - -impl ReadFailureClass { - pub const fn retryable(self) -> bool { - matches!( - self, - Self::Connection - | Self::RequestTimeout - | Self::RequestFailed - | Self::HttpServer - | Self::RateLimited - ) - } - - const fn response_outcome(self) -> ResponseOutcome { - match self { - Self::Connection | Self::RequestFailed => ResponseOutcome::Transport, - Self::RequestTimeout => ResponseOutcome::Timeout, - Self::HttpServer | Self::RateLimited | Self::HttpClient => ResponseOutcome::HttpStatus, - Self::SizeLimit => ResponseOutcome::SizeLimit, - Self::Decode => ResponseOutcome::Decode, - Self::Application => ResponseOutcome::Application, - Self::Parse => ResponseOutcome::Parse, - Self::Validation | Self::CompanyMismatch => ResponseOutcome::Validation, - } - } - - const fn circuit_outcome(self) -> CircuitOutcome { - match self { - Self::Connection - | Self::RequestTimeout - | Self::RequestFailed - | Self::HttpServer - | Self::RateLimited => CircuitOutcome::TransportFailure, - Self::HttpClient - | Self::SizeLimit - | Self::Decode - | Self::Application - | Self::Parse - | Self::Validation - | Self::CompanyMismatch => CircuitOutcome::ApplicationRejected, - } - } -} - -pub enum ReadAttempt { - Success { - value: T, - observed_body_bytes: BodyBytesObservation, - }, - Failure { - error: E, - class: ReadFailureClass, - observed_body_bytes: BodyBytesObservation, - }, -} - -impl ReadAttempt { - fn observation(&self) -> (ResponseOutcome, BodyBytesObservation) { - match self { - Self::Success { - observed_body_bytes, - .. - } => (ResponseOutcome::Success, *observed_body_bytes), - Self::Failure { - class, - observed_body_bytes, - .. - } => (class.response_outcome(), *observed_body_bytes), - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct ReadRetryPolicy { - maximum_attempts: u8, - base_delay: Duration, - maximum_delay: Duration, - jitter_percent: u8, -} - -impl ReadRetryPolicy { - pub const SINGLE_ATTEMPT: Self = Self { - maximum_attempts: 1, - base_delay: Duration::ZERO, - maximum_delay: Duration::ZERO, - jitter_percent: 0, - }; - - pub fn transient_default() -> Self { - Self { - maximum_attempts: 3, - base_delay: Duration::from_millis(250), - maximum_delay: Duration::from_secs(2), - jitter_percent: 20, - } - } - - pub fn new( - maximum_attempts: u8, - base_delay: Duration, - maximum_delay: Duration, - jitter_percent: u8, - ) -> Result { - if maximum_attempts == 0 - || maximum_attempts > 5 - || base_delay > maximum_delay - || maximum_delay > MAX_RETRY_DELAY - || jitter_percent > 25 - { - return Err(RuntimeConfigurationError::RetryPolicyInvalid); - } - Ok(Self { - maximum_attempts, - base_delay, - maximum_delay, - jitter_percent, - }) - } - - pub const fn maximum_attempts(self) -> u8 { - self.maximum_attempts - } - - fn delay_after_failure(self, completed_attempt: u8, entropy: u64) -> Duration { - if self.base_delay.is_zero() || completed_attempt >= self.maximum_attempts { - return Duration::ZERO; - } - let exponent = u32::from(completed_attempt.saturating_sub(1)); - let multiplier = 1_u32.checked_shl(exponent).unwrap_or(u32::MAX); - let base = self - .base_delay - .saturating_mul(multiplier) - .min(self.maximum_delay); - let jitter_ceiling = base - .as_millis() - .saturating_mul(u128::from(self.jitter_percent)) - / 100; - let jitter = if jitter_ceiling == 0 { - 0 - } else { - u128::from(entropy) % (jitter_ceiling + 1) - }; - base.saturating_add(Duration::from_millis( - u64::try_from(jitter).unwrap_or(u64::MAX), - )) - .min(MAX_RETRY_DELAY) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct RuntimePolicy { - pub queue_deadline: Duration, - pub request_spacing: Duration, - pub circuit_failure_threshold: u32, - pub circuit_cooldown: Duration, - pub maximum_endpoint_sessions: usize, -} - -impl Default for RuntimePolicy { - fn default() -> Self { - Self { - queue_deadline: Duration::from_secs(30), - request_spacing: Duration::from_millis(500), - circuit_failure_threshold: 3, - circuit_cooldown: Duration::from_secs(10), - maximum_endpoint_sessions: 32, - } - } -} - -impl RuntimePolicy { - fn validate(self) -> Result { - if self.queue_deadline.is_zero() - || self.queue_deadline > MAX_QUEUE_DEADLINE - || self.request_spacing > MAX_REQUEST_SPACING - || self.circuit_failure_threshold == 0 - || self.circuit_failure_threshold > 100 - || self.circuit_cooldown.is_zero() - || self.circuit_cooldown > MAX_CIRCUIT_COOLDOWN - || self.maximum_endpoint_sessions == 0 - || self.maximum_endpoint_sessions > 128 - { - return Err(RuntimeConfigurationError::RuntimePolicyInvalid); - } - Ok(self) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum RuntimeConfigurationError { - EndpointIdentityInvalid, - RetryPolicyInvalid, - RuntimePolicyInvalid, -} - -impl RuntimeConfigurationError { - pub const fn safe_code(self) -> &'static str { - match self { - Self::EndpointIdentityInvalid => "endpoint_identity_invalid", - Self::RetryPolicyInvalid => "read_retry_policy_invalid", - Self::RuntimePolicyInvalid => "endpoint_runtime_policy_invalid", - } - } -} - -impl fmt::Display for RuntimeConfigurationError { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str(self.safe_code()) - } -} - -impl std::error::Error for RuntimeConfigurationError {} - -#[derive(Debug)] -pub enum ReadExecutionError { - QueueDeadline, - Cancelled, - CircuitRejected { - reason: CircuitRejectReason, - retry_after_unix_ms: Option, - }, - EndpointSessionLimit, - Attempt(E), -} - -impl ReadExecutionError { - pub const fn safe_code(&self) -> &'static str { - match self { - Self::QueueDeadline => "endpoint_queue_deadline_exceeded", - Self::Cancelled => "read_request_cancelled", - Self::CircuitRejected { - reason: CircuitRejectReason::Cooldown, - .. - } => "endpoint_circuit_cooldown", - Self::CircuitRejected { - reason: CircuitRejectReason::HalfOpenProbeInFlight, - .. - } => "endpoint_half_open_probe_in_flight", - Self::EndpointSessionLimit => "endpoint_session_limit_in_use", - Self::Attempt(_) => "read_attempt_failed", - } - } - - pub fn into_attempt_error(self) -> Option { - match self { - Self::Attempt(error) => Some(error), - _ => None, - } - } -} - -struct GateState { - next_request_not_before: Option, -} - -struct SpacingGuard<'a> { - state: MutexGuard<'a, GateState>, - spacing: Duration, -} - -impl Drop for SpacingGuard<'_> { - fn drop(&mut self) { - self.state.next_request_not_before = Some(Instant::now() + self.spacing); - } -} - -#[derive(Default)] -struct CircuitState { - consecutive_failures: u32, - last_failure_unix_ms: Option, - half_open_probe_in_flight: bool, -} - -struct CircuitBreaker { - state: Mutex, - threshold: u32, - cooldown: Duration, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum CircuitOutcome { - TransportSuccess, - TransportFailure, - ApplicationRejected, - Cancelled, -} - -struct CircuitPermit<'a> { - circuit: &'a CircuitBreaker, - half_open: bool, - completed: bool, -} - -impl CircuitBreaker { - fn admit( - &self, - now_unix_ms: i64, - ) -> Result, (CircuitRejectReason, Option)> { - let mut state = self - .state - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - if state.consecutive_failures < self.threshold { - return Ok(CircuitPermit { - circuit: self, - half_open: false, - completed: false, - }); - } - let retry_after = state - .last_failure_unix_ms - .unwrap_or(now_unix_ms) - .saturating_add(duration_millis_i64(self.cooldown)); - if now_unix_ms < retry_after { - return Err((CircuitRejectReason::Cooldown, Some(retry_after))); - } - if state.half_open_probe_in_flight { - return Err((CircuitRejectReason::HalfOpenProbeInFlight, None)); - } - state.half_open_probe_in_flight = true; - Ok(CircuitPermit { - circuit: self, - half_open: true, - completed: false, - }) - } - - fn record(&self, outcome: CircuitOutcome, now_unix_ms: i64) { - let mut state = self - .state - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - state.half_open_probe_in_flight = false; - match outcome { - CircuitOutcome::TransportSuccess => { - state.consecutive_failures = 0; - } - CircuitOutcome::TransportFailure => { - state.consecutive_failures = state.consecutive_failures.saturating_add(1); - state.last_failure_unix_ms = Some(now_unix_ms); - } - CircuitOutcome::ApplicationRejected | CircuitOutcome::Cancelled => {} - } - } - - fn release_half_open(&self) { - let mut state = self - .state - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - state.half_open_probe_in_flight = false; - } - - fn snapshot(&self, now_unix_ms: i64) -> EndpointRuntimeSnapshot { - let state = self - .state - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - let retry_after = state - .last_failure_unix_ms - .map(|last_failure| last_failure.saturating_add(duration_millis_i64(self.cooldown))); - let circuit_state = if state.consecutive_failures < self.threshold { - EndpointCircuitState::Closed - } else if retry_after.is_some_and(|deadline| now_unix_ms < deadline) { - EndpointCircuitState::Open - } else { - EndpointCircuitState::HalfOpen - }; - EndpointRuntimeSnapshot { - consecutive_failures: state.consecutive_failures, - circuit_state, - circuit_retry_after_unix_ms: match circuit_state { - EndpointCircuitState::Open => retry_after, - EndpointCircuitState::Closed | EndpointCircuitState::HalfOpen => None, - }, - half_open_probe_in_flight: state.half_open_probe_in_flight, - last_failure_unix_ms: state.last_failure_unix_ms, - } - } -} - -impl CircuitPermit<'_> { - fn complete(mut self, outcome: CircuitOutcome, now_unix_ms: i64) { - self.circuit.record(outcome, now_unix_ms); - self.completed = true; - } -} - -impl Drop for CircuitPermit<'_> { - fn drop(&mut self) { - if self.half_open && !self.completed { - self.circuit.release_half_open(); - } - } -} - -struct EndpointSession { - gate: AsyncMutex, - circuit: CircuitBreaker, - sequence: std::sync::atomic::AtomicU64, -} - -impl EndpointSession { - fn new(policy: RuntimePolicy) -> Self { - Self { - gate: AsyncMutex::new(GateState { - next_request_not_before: None, - }), - circuit: CircuitBreaker { - state: Mutex::new(CircuitState::default()), - threshold: policy.circuit_failure_threshold, - cooldown: policy.circuit_cooldown, - }, - sequence: std::sync::atomic::AtomicU64::new(0), - } - } -} - -struct SessionSlot { - session: Arc, - last_used: Instant, -} - -#[derive(Clone)] -pub struct PortableReadRuntime { - sessions: Arc>>, - collector: Arc, - policy: RuntimePolicy, -} - -impl fmt::Debug for PortableReadRuntime { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter - .debug_struct("PortableReadRuntime") - .field("policy", &self.policy) - .finish_non_exhaustive() - } -} - -impl Default for PortableReadRuntime { - fn default() -> Self { - Self::new(RuntimePolicy::default()).expect("default runtime policy is valid") - } -} - -impl PortableReadRuntime { - pub fn new(policy: RuntimePolicy) -> Result { - Self::with_collector(policy, Arc::new(TelemetryCollector::new())) - } - - pub fn with_collector( - policy: RuntimePolicy, - collector: Arc, - ) -> Result { - Ok(Self { - sessions: Arc::new(Mutex::new(HashMap::new())), - collector, - policy: policy.validate()?, - }) - } - - pub fn collector(&self) -> Arc { - Arc::clone(&self.collector) - } - - pub fn endpoint_snapshot( - &self, - endpoint: &EndpointIdentity, - ) -> Option { - let sessions = self - .sessions - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - sessions - .get(endpoint) - .map(|slot| slot.session.circuit.snapshot(now_unix_ms())) - } - - pub async fn execute_read( - &self, - endpoint: EndpointIdentity, - operation: ReadOperation, - retry: ReadRetryPolicy, - cancellation: CancellationToken, - mut request: F, - ) -> Result> - where - F: FnMut(u8) -> Fut, - Fut: Future>, - { - let session = self - .session(&endpoint) - .map_err(|()| ReadExecutionError::EndpointSessionLimit)?; - let sequence = session - .sequence - .fetch_add(1, std::sync::atomic::Ordering::Relaxed) - .saturating_add(1); - let class = operation.request_class(); - let mut attempt_number = 1_u8; - loop { - let attempt = match self - .run_queued(&session, class, &cancellation, || request(attempt_number)) - .await - { - Ok(attempt) => attempt, - Err(error) => return Err(error), - }; - match attempt { - ReadAttempt::Success { value, .. } => return Ok(value), - ReadAttempt::Failure { error, class, .. } => { - if !class.retryable() || attempt_number >= retry.maximum_attempts { - return Err(ReadExecutionError::Attempt(error)); - } - let entropy = retry_entropy(&endpoint, sequence, attempt_number); - let delay = retry.delay_after_failure(attempt_number, entropy); - attempt_number = attempt_number.saturating_add(1); - if !delay.is_zero() { - tokio::select! { - _ = cancellation.cancelled() => return Err(ReadExecutionError::Cancelled), - _ = tokio::time::sleep(delay) => {} - } - } - } - } - } - } - - fn session(&self, endpoint: &EndpointIdentity) -> Result, ()> { - let mut sessions = self - .sessions - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - if let Some(slot) = sessions.get_mut(endpoint) { - slot.last_used = Instant::now(); - return Ok(Arc::clone(&slot.session)); - } - if sessions.len() >= self.policy.maximum_endpoint_sessions { - let oldest = sessions - .iter() - .filter(|(_, slot)| Arc::strong_count(&slot.session) == 1) - .min_by_key(|(_, slot)| slot.last_used) - .map(|(identity, _)| identity.clone()); - if let Some(identity) = oldest { - sessions.remove(&identity); - } else { - return Err(()); - } - } - let session = Arc::new(EndpointSession::new(self.policy)); - sessions.insert( - endpoint.clone(), - SessionSlot { - session: Arc::clone(&session), - last_used: Instant::now(), - }, - ); - Ok(session) - } - - async fn run_queued( - &self, - session: &EndpointSession, - class: RequestClass, - cancellation: &CancellationToken, - request: F, - ) -> Result, ReadExecutionError> - where - F: FnOnce() -> Fut, - Fut: Future>, - { - let queued_at = Instant::now(); - let state = tokio::select! { - _ = cancellation.cancelled() => { - self.collector.record_attempt(AttemptObservation::QueueCancelled { - class, - queue_wait: queued_at.elapsed(), - }); - return Err(ReadExecutionError::Cancelled); - } - result = tokio::time::timeout(self.policy.queue_deadline, session.gate.lock()) => { - match result { - Ok(state) => state, - Err(_) => { - self.collector.record_attempt(AttemptObservation::QueueDeadline { - class, - queue_wait: queued_at.elapsed(), - }); - return Err(ReadExecutionError::QueueDeadline); - } - } - } - }; - let queue_wait = queued_at.elapsed(); - if let Some(spacing_wait) = state - .next_request_not_before - .and_then(|not_before| not_before.checked_duration_since(Instant::now())) - { - let remaining = self - .policy - .queue_deadline - .checked_sub(queued_at.elapsed()) - .ok_or_else(|| { - self.collector - .record_attempt(AttemptObservation::QueueDeadline { class, queue_wait }); - ReadExecutionError::QueueDeadline - })?; - let wait = spacing_wait.min(remaining); - tokio::select! { - _ = cancellation.cancelled() => { - self.collector.record_attempt(AttemptObservation::QueueCancelled { - class, - queue_wait, - }); - return Err(ReadExecutionError::Cancelled); - } - _ = tokio::time::sleep(wait) => {} - } - if wait < spacing_wait { - self.collector - .record_attempt(AttemptObservation::QueueDeadline { class, queue_wait }); - return Err(ReadExecutionError::QueueDeadline); - } - } - let permit = match session.circuit.admit(now_unix_ms()) { - Ok(permit) => permit, - Err((reason, retry_after_unix_ms)) => { - self.collector - .record_attempt(AttemptObservation::CircuitRejected { class, reason }); - return Err(ReadExecutionError::CircuitRejected { - reason, - retry_after_unix_ms, - }); - } - }; - let _guard = SpacingGuard { - state, - spacing: self.policy.request_spacing, - }; - let response_started = Instant::now(); - let attempt = tokio::select! { - _ = cancellation.cancelled() => { - self.collector.record_attempt(AttemptObservation::Response { - class, - queue_wait, - outcome: ResponseOutcome::Cancelled, - response_pipeline_elapsed: response_started.elapsed(), - observed_body_bytes: BodyBytesObservation::Unavailable, - }); - permit.complete(CircuitOutcome::Cancelled, now_unix_ms()); - return Err(ReadExecutionError::Cancelled); - } - attempt = request() => attempt, - }; - let (outcome, observed_body_bytes) = attempt.observation(); - self.collector.record_attempt(AttemptObservation::Response { - class, - queue_wait, - outcome, - response_pipeline_elapsed: response_started.elapsed(), - observed_body_bytes, - }); - let circuit_outcome = match &attempt { - ReadAttempt::Success { .. } => CircuitOutcome::TransportSuccess, - ReadAttempt::Failure { class, .. } => class.circuit_outcome(), - }; - permit.complete(circuit_outcome, now_unix_ms()); - Ok(attempt) - } -} - -fn now_unix_ms() -> i64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .ok() - .and_then(|duration| i64::try_from(duration.as_millis()).ok()) - .unwrap_or(i64::MAX) -} - -fn duration_millis_i64(duration: Duration) -> i64 { - i64::try_from(duration.as_millis()).unwrap_or(i64::MAX) -} - -fn retry_entropy(endpoint: &EndpointIdentity, sequence: u64, attempt: u8) -> u64 { - let mut hasher = DefaultHasher::new(); - endpoint.private_value().hash(&mut hasher); - sequence.hash(&mut hasher); - attempt.hash(&mut hasher); - hasher.finish() -} - -#[cfg(test)] -mod tests { - use super::*; - use std::sync::atomic::{AtomicUsize, Ordering}; - - fn test_runtime(spacing: Duration, threshold: u32) -> PortableReadRuntime { - PortableReadRuntime::new(RuntimePolicy { - queue_deadline: Duration::from_secs(1), - request_spacing: spacing, - circuit_failure_threshold: threshold, - circuit_cooldown: Duration::from_millis(50), - maximum_endpoint_sessions: 4, - }) - .unwrap() - } - - fn endpoint(value: &str) -> EndpointIdentity { - EndpointIdentity::new(value).unwrap() - } - - #[tokio::test] - async fn same_endpoint_serializes_while_distinct_endpoints_are_independent() { - let runtime = test_runtime(Duration::ZERO, 3); - let in_flight = Arc::new(AtomicUsize::new(0)); - let same_max = Arc::new(AtomicUsize::new(0)); - let run = |runtime: PortableReadRuntime, - endpoint: EndpointIdentity, - in_flight: Arc, - maximum: Arc| async move { - runtime - .execute_read( - endpoint, - ReadOperation::CompanyList, - ReadRetryPolicy::SINGLE_ATTEMPT, - CancellationToken::new(), - |_| { - let in_flight = Arc::clone(&in_flight); - let maximum = Arc::clone(&maximum); - async move { - let active = in_flight.fetch_add(1, Ordering::SeqCst) + 1; - maximum.fetch_max(active, Ordering::SeqCst); - tokio::time::sleep(Duration::from_millis(20)).await; - in_flight.fetch_sub(1, Ordering::SeqCst); - ReadAttempt::<_, ()>::Success { - value: (), - observed_body_bytes: BodyBytesObservation::Observed(10), - } - } - }, - ) - .await - }; - let (first, second) = tokio::join!( - run( - runtime.clone(), - endpoint("loopback-a"), - Arc::clone(&in_flight), - Arc::clone(&same_max) - ), - run( - runtime.clone(), - endpoint("loopback-a"), - Arc::clone(&in_flight), - Arc::clone(&same_max) - ) - ); - first.unwrap(); - second.unwrap(); - assert_eq!(same_max.load(Ordering::SeqCst), 1); - - let distinct_max = Arc::new(AtomicUsize::new(0)); - let (first, second) = tokio::join!( - run( - runtime.clone(), - endpoint("loopback-a"), - Arc::clone(&in_flight), - Arc::clone(&distinct_max) - ), - run( - runtime, - endpoint("loopback-b"), - in_flight, - Arc::clone(&distinct_max) - ) - ); - first.unwrap(); - second.unwrap(); - assert_eq!(distinct_max.load(Ordering::SeqCst), 2); - } - - #[tokio::test] - async fn transient_reads_retry_exactly_but_validation_never_retries() { - let runtime = test_runtime(Duration::ZERO, 100); - let attempts = Arc::new(AtomicUsize::new(0)); - let observed = Arc::clone(&attempts); - let result = runtime - .execute_read( - endpoint("retry-endpoint"), - ReadOperation::VoucherExport, - ReadRetryPolicy::new(3, Duration::ZERO, Duration::ZERO, 0).unwrap(), - CancellationToken::new(), - move |_| { - let observed = Arc::clone(&observed); - async move { - let attempt = observed.fetch_add(1, Ordering::SeqCst) + 1; - if attempt < 3 { - ReadAttempt::Failure { - error: "transient", - class: ReadFailureClass::RequestTimeout, - observed_body_bytes: BodyBytesObservation::Unavailable, - } - } else { - ReadAttempt::Success { - value: "ok", - observed_body_bytes: BodyBytesObservation::Observed(12), - } - } - } - }, - ) - .await - .unwrap(); - assert_eq!(result, "ok"); - assert_eq!(attempts.load(Ordering::SeqCst), 3); - - let attempts = Arc::new(AtomicUsize::new(0)); - let observed = Arc::clone(&attempts); - let error = runtime - .execute_read( - endpoint("validation-endpoint"), - ReadOperation::MasterExport, - ReadRetryPolicy::new(3, Duration::ZERO, Duration::ZERO, 0).unwrap(), - CancellationToken::new(), - move |_| { - let observed = Arc::clone(&observed); - async move { - observed.fetch_add(1, Ordering::SeqCst); - ReadAttempt::<(), _>::Failure { - error: "company_mismatch", - class: ReadFailureClass::CompanyMismatch, - observed_body_bytes: BodyBytesObservation::Observed(100), - } - } - }, - ) - .await - .unwrap_err(); - assert!(matches!( - error, - ReadExecutionError::Attempt("company_mismatch") - )); - assert_eq!(attempts.load(Ordering::SeqCst), 1); - } - - #[tokio::test] - async fn cancellation_is_terminal_and_preserves_follow_up_spacing() { - let spacing = Duration::from_millis(60); - let runtime = test_runtime(spacing, 3); - let cancellation = CancellationToken::new(); - let cancel = cancellation.clone(); - let running = { - let runtime = runtime.clone(); - tokio::spawn(async move { - runtime - .execute_read( - endpoint("cancel-endpoint"), - ReadOperation::ReportExport, - ReadRetryPolicy::SINGLE_ATTEMPT, - cancellation, - |_| async { std::future::pending::>().await }, - ) - .await - }) - }; - tokio::time::sleep(Duration::from_millis(10)).await; - cancel.cancel(); - assert!(matches!( - running.await.unwrap(), - Err(ReadExecutionError::Cancelled) - )); - let started = Instant::now(); - runtime - .execute_read( - endpoint("cancel-endpoint"), - ReadOperation::ReportExport, - ReadRetryPolicy::SINGLE_ATTEMPT, - CancellationToken::new(), - |_| async { - ReadAttempt::<_, ()>::Success { - value: (), - observed_body_bytes: BodyBytesObservation::Observed(0), - } - }, - ) - .await - .unwrap(); - assert!(started.elapsed() >= spacing.saturating_sub(Duration::from_millis(10))); - } - - #[tokio::test] - async fn circuit_cooldown_and_single_half_open_probe_are_enforced() { - let runtime = test_runtime(Duration::ZERO, 1); - let fail = || async { - ReadAttempt::<(), _>::Failure { - error: "offline", - class: ReadFailureClass::Connection, - observed_body_bytes: BodyBytesObservation::Unavailable, - } - }; - let _ = runtime - .execute_read( - endpoint("circuit-endpoint"), - ReadOperation::CompanyList, - ReadRetryPolicy::SINGLE_ATTEMPT, - CancellationToken::new(), - |_| fail(), - ) - .await; - let rejected = runtime - .execute_read( - endpoint("circuit-endpoint"), - ReadOperation::CompanyList, - ReadRetryPolicy::SINGLE_ATTEMPT, - CancellationToken::new(), - |_| fail(), - ) - .await - .unwrap_err(); - assert!(matches!( - rejected, - ReadExecutionError::CircuitRejected { - reason: CircuitRejectReason::Cooldown, - .. - } - )); - tokio::time::sleep(Duration::from_millis(60)).await; - let entered = Arc::new(tokio::sync::Notify::new()); - let release = Arc::new(tokio::sync::Notify::new()); - let first = { - let runtime = runtime.clone(); - let entered = Arc::clone(&entered); - let release = Arc::clone(&release); - tokio::spawn(async move { - runtime - .execute_read( - endpoint("circuit-endpoint"), - ReadOperation::CompanyList, - ReadRetryPolicy::SINGLE_ATTEMPT, - CancellationToken::new(), - |_| { - let entered = Arc::clone(&entered); - let release = Arc::clone(&release); - async move { - entered.notify_one(); - release.notified().await; - ReadAttempt::<_, ()>::Success { - value: (), - observed_body_bytes: BodyBytesObservation::Observed(1), - } - } - }, - ) - .await - }) - }; - entered.notified().await; - let second = { - let runtime = runtime.clone(); - tokio::spawn(async move { - runtime - .execute_read( - endpoint("circuit-endpoint"), - ReadOperation::CompanyList, - ReadRetryPolicy::SINGLE_ATTEMPT, - CancellationToken::new(), - |_| async { - ReadAttempt::<_, ()>::Success { - value: (), - observed_body_bytes: BodyBytesObservation::Observed(1), - } - }, - ) - .await - }) - }; - tokio::task::yield_now().await; - assert!( - !second.is_finished(), - "a second probe must remain serialized" - ); - release.notify_one(); - first.await.unwrap().unwrap(); - second.await.unwrap().unwrap(); - } - - #[tokio::test] - async fn queued_request_cannot_use_a_stale_closed_circuit_admission() { - let runtime = test_runtime(Duration::ZERO, 1); - let entered = Arc::new(tokio::sync::Notify::new()); - let release = Arc::new(tokio::sync::Notify::new()); - let first = { - let runtime = runtime.clone(); - let entered = Arc::clone(&entered); - let release = Arc::clone(&release); - tokio::spawn(async move { - runtime - .execute_read( - endpoint("stale-admission-endpoint"), - ReadOperation::CompanyList, - ReadRetryPolicy::SINGLE_ATTEMPT, - CancellationToken::new(), - |_| { - let entered = Arc::clone(&entered); - let release = Arc::clone(&release); - async move { - entered.notify_one(); - release.notified().await; - ReadAttempt::<(), _>::Failure { - error: "offline", - class: ReadFailureClass::Connection, - observed_body_bytes: BodyBytesObservation::Unavailable, - } - } - }, - ) - .await - }) - }; - entered.notified().await; - let executed = Arc::new(AtomicUsize::new(0)); - let second = { - let runtime = runtime.clone(); - let executed = Arc::clone(&executed); - tokio::spawn(async move { - runtime - .execute_read( - endpoint("stale-admission-endpoint"), - ReadOperation::CompanyList, - ReadRetryPolicy::SINGLE_ATTEMPT, - CancellationToken::new(), - move |_| { - let executed = Arc::clone(&executed); - async move { - executed.fetch_add(1, Ordering::SeqCst); - ReadAttempt::<_, ()>::Success { - value: (), - observed_body_bytes: BodyBytesObservation::Observed(1), - } - } - }, - ) - .await - }) - }; - tokio::task::yield_now().await; - release.notify_one(); - assert!(matches!( - first.await.unwrap(), - Err(ReadExecutionError::Attempt("offline")) - )); - assert!(matches!( - second.await.unwrap(), - Err(ReadExecutionError::CircuitRejected { - reason: CircuitRejectReason::Cooldown, - .. - }) - )); - assert_eq!(executed.load(Ordering::SeqCst), 0); - } - - #[test] - fn circuit_breaker_rejects_a_concurrent_half_open_permit() { - let breaker = CircuitBreaker { - state: Mutex::new(CircuitState { - consecutive_failures: 1, - last_failure_unix_ms: Some(now_unix_ms().saturating_sub(100)), - half_open_probe_in_flight: false, - }), - threshold: 1, - cooldown: Duration::from_millis(50), - }; - let permit = breaker - .admit(now_unix_ms()) - .expect("first half-open permit"); - assert!(matches!( - breaker.admit(now_unix_ms()), - Err((CircuitRejectReason::HalfOpenProbeInFlight, None)) - )); - drop(permit); - assert!(breaker.admit(now_unix_ms()).is_ok()); - } - - #[tokio::test] - async fn deterministic_runtime_sequence_retries_server_failure_then_succeeds() { - let runtime = test_runtime(Duration::ZERO, 3); - let attempts = Arc::new(AtomicUsize::new(0)); - let observed_attempts = Arc::clone(&attempts); - let xml = runtime - .execute_read( - endpoint("sequence-endpoint"), - ReadOperation::ReportExport, - ReadRetryPolicy::new(2, Duration::ZERO, Duration::ZERO, 0).unwrap(), - CancellationToken::new(), - move |_| { - let observed_attempts = Arc::clone(&observed_attempts); - async move { - if observed_attempts.fetch_add(1, Ordering::SeqCst) == 0 { - ReadAttempt::Failure { - error: "http_server_failure", - class: ReadFailureClass::HttpServer, - observed_body_bytes: BodyBytesObservation::Observed(64), - } - } else { - ReadAttempt::Success { - value: "1", - observed_body_bytes: BodyBytesObservation::Observed(18), - } - } - } - }, - ) - .await - .expect("second deterministic response succeeds"); - assert!(xml.contains("1")); - assert_eq!(attempts.load(Ordering::SeqCst), 2); - } - - #[test] - fn operation_and_retry_surface_are_read_only_and_redacted() { - assert_eq!(ReadOperation::Status.request_class(), RequestClass::Status); - assert!(ReadFailureClass::HttpServer.retryable()); - assert!(!ReadFailureClass::Application.retryable()); - assert!(format!("{:?}", endpoint("sensitive-loopback")).contains("[redacted]")); - assert_eq!( - ReadRetryPolicy::new(0, Duration::ZERO, Duration::ZERO, 0), - Err(RuntimeConfigurationError::RetryPolicyInvalid) - ); - } -} diff --git a/src-tauri/crates/bridge-tally-write/Cargo.toml b/src-tauri/crates/bridge-tally-write/Cargo.toml deleted file mode 100644 index afd5f40..0000000 --- a/src-tauri/crates/bridge-tally-write/Cargo.toml +++ /dev/null @@ -1,31 +0,0 @@ -[package] -name = "bridge-tally-write" -version = "0.1.0" -description = "Portable, network-free qualification contract for controlled Tally ledger writes" -license = "Apache-2.0" -repository = "https://github.com/lamemustafa/bridge" -publish = false -edition = "2021" -rust-version = "1.96" - -[features] -# This feature is intentionally disabled by Bridge. Enabling it only exposes -# the sealed, one-use in-memory payload capsule; it does not add HTTP or any -# Tally transport. A later reviewed coordinator must opt in explicitly. -fixture-canary-dispatch-seam = [] -# This opt-in adds sealed receipt/error types used by Bridge's private runtime -# coordinator. This portable crate still has no HTTP client, send function, -# generic XML callback, or arbitrary payload API. -fixture-canary-runtime-dispatch = [ - "fixture-canary-dispatch-seam", - "dep:bridge-tally-transport", -] - -[dependencies] -bridge-tally-core = { path = "../bridge-tally-core" } -bridge-tally-protocol = { path = "../bridge-tally-protocol" } -bridge-tally-transport = { path = "../bridge-tally-transport", optional = true } -serde = { version = "1", features = ["derive"] } -serde_json = "1" -sha2 = "0.11" -thiserror = "2" diff --git a/src-tauri/crates/bridge-tally-write/src/lib.rs b/src-tauri/crates/bridge-tally-write/src/lib.rs deleted file mode 100644 index 36945d5..0000000 --- a/src-tauri/crates/bridge-tally-write/src/lib.rs +++ /dev/null @@ -1,1712 +0,0 @@ -//! Portable, network-free qualification for controlled Tally ledger writes. -//! -//! This crate cannot dispatch HTTP. It binds a deterministic import preview to -//! typed intent, strict preflight state, parser-derived import evidence, and a -//! company-bound readback before it can return an exact verdict. - -use std::collections::{BTreeMap, BTreeSet, HashMap}; - -use bridge_tally_core::ExactDecimal; -use bridge_tally_protocol::{ - parse_import_evidence, parse_ledger_write_readback_with_evidence, ParsedImportEvidence, - TallyImportApplicationStatus, TallyImportResult, BRIDGE_LEDGER_WRITE_READBACK_SCHEMA, -}; -#[cfg(feature = "fixture-canary-runtime-dispatch")] -use bridge_tally_transport::TallyTransportError; -use serde::Serialize; -use sha2::{Digest, Sha256}; -use thiserror::Error; - -pub const MAX_LEDGER_WRITE_BATCH: usize = 10; -pub const LEDGER_WRITE_PROJECTION: &str = "bridge.tally.ledger-write-state/1"; -pub const LEDGER_READBACK_PROFILE: &str = BRIDGE_LEDGER_WRITE_READBACK_SCHEMA; -pub const FIXTURE_CANARY_MAPPING_VERSION: &str = "bridge.fixture-canary/v1"; -pub const FIXTURE_CANARY_LEDGER_NAME: &str = "BRIDGE-CANARY-LEDGER-V1"; - -const FIXTURE_CANARY_REMOTE_ID: &str = "bridge-fixture-canary-ledger-v1"; -const FIXTURE_CANARY_PARENT: &str = "Indirect Expenses"; -const FIXTURE_CANARY_OPENING_BALANCE: &str = "0"; - -macro_rules! digest_type { - ($name:ident) => { - #[derive(Clone, PartialEq, Eq)] - pub struct $name(String); - - impl $name { - pub fn as_hex(&self) -> &str { - &self.0 - } - } - - impl std::fmt::Debug for $name { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str(concat!(stringify!($name), "()")) - } - } - }; -} - -digest_type!(WirePayloadDigest); -digest_type!(IntendedStateDigest); -digest_type!(ImportResponseDigest); -digest_type!(ReadbackStateDigest); -digest_type!(IdentityCoverageDigest); -digest_type!(LineErrorDigest); -digest_type!(ApprovalEvidenceDigest); -digest_type!(IdentityQueryDigest); - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum LedgerOperation { - Create, - Alter, -} - -impl LedgerOperation { - fn tally_action(self) -> &'static str { - match self { - Self::Create => "Create", - Self::Alter => "Alter", - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] -pub struct LedgerState { - name: String, - parent: Option, - party_gstin: Option, - opening_balance: Option, -} - -impl LedgerState { - pub fn new( - name: impl Into, - parent: Option, - party_gstin: Option, - opening_balance: Option, - ) -> Result { - let state = Self { - name: name.into(), - parent, - party_gstin, - opening_balance, - }; - validate_value(&state.name, "ledger_name")?; - for (value, field) in [ - (state.parent.as_deref(), "parent"), - (state.party_gstin.as_deref(), "party_gstin"), - (state.opening_balance.as_deref(), "opening_balance"), - ] { - if let Some(value) = value { - validate_value(value, field)?; - } - } - if let Some(value) = state.party_gstin.as_deref() { - validate_gstin(value)?; - } - if let Some(value) = state.opening_balance.as_deref() { - ExactDecimal::parse(value.to_owned()) - .map_err(|_| QualificationError::InvalidField("opening_balance"))?; - } - Ok(state) - } - - pub fn name(&self) -> &str { - &self.name - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] -pub struct SourceLineage { - system: String, - record_id: String, - version: String, -} - -impl SourceLineage { - pub fn new( - system: impl Into, - record_id: impl Into, - version: impl Into, - ) -> Result { - let lineage = Self { - system: system.into(), - record_id: record_id.into(), - version: version.into(), - }; - validate_value(&lineage.system, "source_system")?; - validate_value(&lineage.record_id, "source_record_id")?; - validate_value(&lineage.version, "source_version")?; - Ok(lineage) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct LedgerMutation { - operation: LedgerOperation, - remote_id: String, - before: Option, - after: LedgerState, - source_lineage: SourceLineage, -} - -impl LedgerMutation { - pub fn create( - remote_id: impl Into, - after: LedgerState, - source_lineage: SourceLineage, - ) -> Result { - if after.parent.is_none() { - return Err(QualificationError::CreateParentRequired); - } - Self::new( - LedgerOperation::Create, - remote_id.into(), - None, - after, - source_lineage, - ) - } - - pub fn alter( - remote_id: impl Into, - before: LedgerState, - after: LedgerState, - source_lineage: SourceLineage, - ) -> Result { - if before == after { - return Err(QualificationError::NoOpMutation); - } - for (before, after, field) in [ - (before.parent.as_ref(), after.parent.as_ref(), "parent"), - ( - before.party_gstin.as_ref(), - after.party_gstin.as_ref(), - "party_gstin", - ), - ( - before.opening_balance.as_ref(), - after.opening_balance.as_ref(), - "opening_balance", - ), - ] { - if before.is_some() && after.is_none() { - return Err(QualificationError::UnsupportedFieldClear(field)); - } - } - Self::new( - LedgerOperation::Alter, - remote_id.into(), - Some(before), - after, - source_lineage, - ) - } - - fn new( - operation: LedgerOperation, - remote_id: String, - before: Option, - after: LedgerState, - source_lineage: SourceLineage, - ) -> Result { - validate_value(&remote_id, "remote_id")?; - Ok(Self { - operation, - remote_id, - before, - after, - source_lineage, - }) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SyntheticCompany { - name: String, - guid: String, -} - -impl SyntheticCompany { - pub fn new( - name: impl Into, - guid: impl Into, - ) -> Result { - let company = Self { - name: name.into(), - guid: guid.into(), - }; - validate_value(&company.name, "company_name")?; - validate_value(&company.guid, "company_guid")?; - Ok(company) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum WriteCapability { - Observed, - Documented, - Unknown, - Unsupported, -} - -#[derive(Clone)] -pub struct WriteAuthorizationRequest { - pub explicit_opt_in: bool, - pub synthetic_company_confirmed: bool, - pub company_guid: String, - pub capability: WriteCapability, - pub backup_guidance_acknowledged: bool, - pub approval_evidence_sha256: String, - pub approved_wire_sha256: String, - pub approved_intended_state_sha256: String, - pub approved_identity_query_sha256: String, - pub idempotency_key: String, - pub outbox_id: String, - pub mapping_version: String, -} - -impl std::fmt::Debug for WriteAuthorizationRequest { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter - .debug_struct("WriteAuthorizationRequest") - .field("explicit_opt_in", &self.explicit_opt_in) - .field( - "synthetic_company_confirmed", - &self.synthetic_company_confirmed, - ) - .field("company_guid", &"") - .field("capability", &self.capability) - .field( - "backup_guidance_acknowledged", - &self.backup_guidance_acknowledged, - ) - .field("approval_evidence_sha256", &"") - .field("approved_wire_sha256", &"") - .field("approved_intended_state_sha256", &"") - .field("approved_identity_query_sha256", &"") - .field("idempotency_key", &"") - .field("outbox_id", &"") - .field("mapping_version", &self.mapping_version) - .finish() - } -} - -#[derive(Clone)] -pub struct WriteAuthorization { - company_guid: String, - approval_evidence_sha256: String, - idempotency_key: String, - outbox_id: String, - mapping_version: String, - approved_wire_sha256: String, - approved_intended_state_sha256: String, - approved_identity_query_sha256: String, -} - -/// The only authority that can prepare the initial fixture canary. -/// -/// Unlike `WriteAuthorizationRequest`, it deliberately has no capability -/// field: the canary establishes the first observed write capability. It is -/// still unusable without an external durable reservation and exact preview -/// commitments, both of which are checked by the application coordinator -/// before any future transport is introduced. -#[derive(Clone)] -pub struct FixtureCanaryAuthorizationRequest { - pub explicit_opt_in: bool, - pub synthetic_company_confirmed: bool, - pub company_guid: String, - pub backup_guidance_acknowledged: bool, - pub review_commitment_sha256: String, - pub reservation_id: String, - pub reservation_payload_sha256: String, - pub approved_wire_sha256: String, - pub approved_intended_state_sha256: String, - pub approved_identity_query_sha256: String, - pub idempotency_key: String, -} - -impl std::fmt::Debug for FixtureCanaryAuthorizationRequest { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter - .debug_struct("FixtureCanaryAuthorizationRequest") - .field("explicit_opt_in", &self.explicit_opt_in) - .field( - "synthetic_company_confirmed", - &self.synthetic_company_confirmed, - ) - .field("company_guid", &"") - .field( - "backup_guidance_acknowledged", - &self.backup_guidance_acknowledged, - ) - .field("review_commitment_sha256", &"") - .field("reservation_id", &"") - .field("reservation_payload_sha256", &"") - .field("approved_wire_sha256", &"") - .field("approved_intended_state_sha256", &"") - .field("approved_identity_query_sha256", &"") - .field("idempotency_key", &"") - .finish() - } -} - -#[derive(Clone)] -pub struct FixtureCanaryAuthorization { - authorization: WriteAuthorization, - reservation_id: String, - reservation_payload_sha256: String, - review_commitment_sha256: String, -} - -impl std::fmt::Debug for FixtureCanaryAuthorization { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter - .debug_struct("FixtureCanaryAuthorization") - .field("reservation_id", &"") - .field("reservation_payload_sha256", &"") - .field("review_commitment_sha256", &"") - .finish() - } -} - -impl FixtureCanaryAuthorization { - pub fn reservation_id(&self) -> &str { - &self.reservation_id - } - - pub fn reservation_payload_sha256(&self) -> &str { - &self.reservation_payload_sha256 - } - - pub fn review_commitment_sha256(&self) -> &str { - &self.review_commitment_sha256 - } -} - -impl std::fmt::Debug for WriteAuthorization { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter - .debug_struct("WriteAuthorization") - .field("company_guid", &"") - .field("approval_evidence_sha256", &"") - .field("idempotency_key", &"") - .field("outbox_id", &"") - .field("mapping_version", &self.mapping_version) - .finish() - } -} - -pub fn authorize_synthetic_write( - request: WriteAuthorizationRequest, -) -> Result { - if !request.explicit_opt_in { - return Err(QualificationError::ExplicitOptInRequired); - } - if !request.synthetic_company_confirmed { - return Err(QualificationError::SyntheticCompanyRequired); - } - if request.capability != WriteCapability::Observed { - return Err(QualificationError::ObservedCapabilityRequired); - } - if !request.backup_guidance_acknowledged { - return Err(QualificationError::BackupAcknowledgementRequired); - } - validate_value(&request.company_guid, "company_guid")?; - validate_sha256( - &request.approval_evidence_sha256, - "approval_evidence_sha256", - )?; - validate_sha256(&request.approved_wire_sha256, "approved_wire_sha256")?; - validate_sha256( - &request.approved_intended_state_sha256, - "approved_intended_state_sha256", - )?; - validate_sha256( - &request.approved_identity_query_sha256, - "approved_identity_query_sha256", - )?; - validate_value(&request.idempotency_key, "idempotency_key")?; - validate_value(&request.outbox_id, "outbox_id")?; - validate_value(&request.mapping_version, "mapping_version")?; - Ok(WriteAuthorization { - company_guid: request.company_guid, - approval_evidence_sha256: request.approval_evidence_sha256, - idempotency_key: request.idempotency_key, - outbox_id: request.outbox_id, - mapping_version: request.mapping_version, - approved_wire_sha256: request.approved_wire_sha256, - approved_intended_state_sha256: request.approved_intended_state_sha256, - approved_identity_query_sha256: request.approved_identity_query_sha256, - }) -} - -/// Builds the distinct authority for one fixture-defined canary. No caller can -/// supply a ledger mutation, mapping version, or observed write capability. -pub fn authorize_fixture_canary( - request: FixtureCanaryAuthorizationRequest, -) -> Result { - if !request.explicit_opt_in { - return Err(QualificationError::ExplicitOptInRequired); - } - if !request.synthetic_company_confirmed { - return Err(QualificationError::SyntheticCompanyRequired); - } - if !request.backup_guidance_acknowledged { - return Err(QualificationError::BackupAcknowledgementRequired); - } - validate_value(&request.company_guid, "company_guid")?; - validate_sha256( - &request.review_commitment_sha256, - "review_commitment_sha256", - )?; - validate_value(&request.reservation_id, "reservation_id")?; - validate_sha256( - &request.reservation_payload_sha256, - "reservation_payload_sha256", - )?; - validate_sha256(&request.approved_wire_sha256, "approved_wire_sha256")?; - validate_sha256( - &request.approved_intended_state_sha256, - "approved_intended_state_sha256", - )?; - validate_sha256( - &request.approved_identity_query_sha256, - "approved_identity_query_sha256", - )?; - validate_value(&request.idempotency_key, "idempotency_key")?; - Ok(FixtureCanaryAuthorization { - authorization: WriteAuthorization { - company_guid: request.company_guid, - approval_evidence_sha256: request.review_commitment_sha256.clone(), - idempotency_key: request.idempotency_key, - outbox_id: format!("fixture-canary:{}", request.reservation_id), - mapping_version: FIXTURE_CANARY_MAPPING_VERSION.to_owned(), - approved_wire_sha256: request.approved_wire_sha256, - approved_intended_state_sha256: request.approved_intended_state_sha256, - approved_identity_query_sha256: request.approved_identity_query_sha256, - }, - reservation_id: request.reservation_id, - reservation_payload_sha256: request.reservation_payload_sha256, - review_commitment_sha256: request.review_commitment_sha256, - }) -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum ReservationState { - Reserved, - Sent, - AwaitingReadback, - OutcomeUnknown, - Terminal, -} - -#[derive(Debug, Clone)] -struct Reservation { - wire_digest: WirePayloadDigest, - outbox_id: String, - state: ReservationState, -} - -#[derive(Debug, Default)] -pub struct IdempotencyRegistry { - reservations: HashMap, -} - -impl IdempotencyRegistry { - fn reserve( - &mut self, - authorization: &WriteAuthorization, - wire_digest: &WirePayloadDigest, - ) -> Result<(), QualificationError> { - if let Some(existing) = self.reservations.get(&authorization.idempotency_key) { - if existing.wire_digest != *wire_digest || existing.outbox_id != authorization.outbox_id - { - return Err(QualificationError::IdempotencyConflict); - } - return Err(QualificationError::DuplicateSubmission); - } - self.reservations.insert( - authorization.idempotency_key.clone(), - Reservation { - wire_digest: wire_digest.clone(), - outbox_id: authorization.outbox_id.clone(), - state: ReservationState::Reserved, - }, - ); - Ok(()) - } - - fn transition( - &mut self, - key: &str, - expected: ReservationState, - next: ReservationState, - ) -> Result<(), QualificationError> { - let reservation = self - .reservations - .get_mut(key) - .ok_or(QualificationError::MissingIdempotencyReservation)?; - if reservation.state != expected { - return Err(QualificationError::DuplicateSubmission); - } - reservation.state = next; - Ok(()) - } -} - -#[derive(Clone)] -pub struct PreparedLedgerImport { - company: SyntheticCompany, - mutations: Vec, - wire_digest: WirePayloadDigest, - intended_state_digest: IntendedStateDigest, - expected_before: BTreeMap, - expected_after: BTreeMap, - authorization: WriteAuthorization, - approval_evidence_digest: ApprovalEvidenceDigest, - identity_query_digest: IdentityQueryDigest, -} - -/// A fixed fixture canary preview that deliberately has no progression into -/// the generic import lifecycle. A separately reviewed coordinator may derive -/// sealed, non-dispatchable preflight evidence from an exact readback, but it -/// can never obtain a transport, receipt, or generic qualified-import state. -pub struct PreparedFixtureCanary { - prepared: PreparedLedgerImport, -} - -/// An opaque, non-dispatchable capability capsule for the fixed fixture-canary -/// payload commitment. -/// -/// This is deliberately feature-gated and unavailable to Bridge's normal -/// build. It carries no transport, endpoint, retry policy, or persistence -/// hook, and it does not expose the XML. A separately reviewed runtime -/// coordinator must bind this capsule to a durable dispatch claim before it -/// introduces the one-send operation. -#[cfg(feature = "fixture-canary-dispatch-seam")] -#[allow(dead_code)] // Read only by Bridge's private in-crate runtime coordinator. -pub struct SealedFixtureCanaryDispatch { - pub(crate) prepared: PreparedLedgerImport, - pub(crate) wire_xml: String, - pub(crate) wire_digest: WirePayloadDigest, -} - -/// A one-send canary receipt that remains sealed until it is correlated with -/// the exact closed readback profile. It never exposes the Tally response. -#[cfg(feature = "fixture-canary-runtime-dispatch")] -pub struct SealedFixtureCanaryReceipt { - pub(crate) prepared: PreparedLedgerImport, - pub(crate) receipt_xml: String, - pub(crate) wire_digest: WirePayloadDigest, -} - -/// Transport failure for the closed synthetic-canary path. A failure consumes -/// the dispatch capsule; callers must treat the outcome as unknown and must -/// not retry the import. -#[cfg(feature = "fixture-canary-runtime-dispatch")] -#[derive(Debug, thiserror::Error)] -pub enum FixtureCanaryDispatchError { - #[error("sealed fixture-canary transport failed")] - Transport(#[source] TallyTransportError), - #[error("sealed fixture-canary runtime is unavailable")] - RuntimeUnavailable, -} - -#[cfg(feature = "fixture-canary-dispatch-seam")] -impl std::fmt::Debug for SealedFixtureCanaryDispatch { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter - .debug_struct("SealedFixtureCanaryDispatch") - .field("wire_digest", &self.wire_digest) - .field("payload", &"[redacted]") - .field("transport", &"absent") - .finish() - } -} - -#[cfg(feature = "fixture-canary-dispatch-seam")] -impl SealedFixtureCanaryDispatch { - pub fn wire_digest(&self) -> &WirePayloadDigest { - &self.wire_digest - } -} - -#[cfg(feature = "fixture-canary-runtime-dispatch")] -impl std::fmt::Debug for SealedFixtureCanaryReceipt { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter - .debug_struct("SealedFixtureCanaryReceipt") - .field("wire_digest", &self.wire_digest) - .field("receipt", &"[redacted]") - .finish() - } -} - -#[cfg(feature = "fixture-canary-runtime-dispatch")] -impl SealedFixtureCanaryReceipt { - /// Rejects a malformed import response before any follow-up readback. The - /// parsed counters remain sealed and are checked again with the readback - /// when deriving the final digest-only observation. - pub fn validate_receipt(&self) -> Result<(), QualificationError> { - parse_import_receipt(&self.receipt_xml).map(|_| ()) - } - - /// Validates the sealed receipt and a caller-owned closed readback, then - /// returns only digest evidence. Neither raw XML document can escape. - pub fn observe_with_readback( - self, - readback_xml: &str, - ) -> Result { - observe_fixture_canary_post_dispatch( - &PreparedFixtureCanary { - prepared: self.prepared, - }, - &self.receipt_xml, - readback_xml, - ) - } -} - -impl std::fmt::Debug for PreparedFixtureCanary { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter - .debug_struct("PreparedFixtureCanary") - .field("wire_digest", self.prepared.wire_digest()) - .field( - "intended_state_digest", - self.prepared.intended_state_digest(), - ) - .field( - "identity_query_digest", - self.prepared.identity_query_digest(), - ) - .field("dispatch_eligible", &false) - .finish() - } -} - -impl PreparedFixtureCanary { - pub fn wire_digest(&self) -> &WirePayloadDigest { - self.prepared.wire_digest() - } - - pub fn intended_state_digest(&self) -> &IntendedStateDigest { - self.prepared.intended_state_digest() - } - - pub fn identity_query_digest(&self) -> &IdentityQueryDigest { - self.prepared.identity_query_digest() - } - - pub const fn dispatch_eligible(&self) -> bool { - false - } - - /// Seals the immutable fixture payload commitment only in the separately - /// opted-in dispatch-seam build. The normal Bridge build cannot name this - /// type or invoke this method. - #[cfg(feature = "fixture-canary-dispatch-seam")] - pub fn seal_for_dispatch(self) -> Result { - let prepared = self.prepared; - let wire_xml = build_import_xml(&prepared.company, &prepared.mutations); - let wire_digest = WirePayloadDigest(domain_digest( - b"bridge.tally.ledger-import-wire/1\0", - wire_xml.as_bytes(), - )); - if wire_digest != prepared.wire_digest { - return Err(QualificationError::FixtureCanaryPayloadMismatch); - } - Ok(SealedFixtureCanaryDispatch { - wire_digest: prepared.wire_digest.clone(), - prepared, - wire_xml, - }) - } -} - -/// Digest-only proof that the fixed fixture canary is absent before a possible -/// write. It is not a dispatch authorization and cannot enter the generic -/// import lifecycle. -#[derive(Clone)] -pub struct FixtureCanaryPreflightEvidence { - readback_state_digest: ReadbackStateDigest, - identity_coverage_digest: IdentityCoverageDigest, -} - -impl std::fmt::Debug for FixtureCanaryPreflightEvidence { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter - .debug_struct("FixtureCanaryPreflightEvidence") - .field("readback_state_digest", &self.readback_state_digest) - .field("identity_coverage_digest", &self.identity_coverage_digest) - .field("dispatch_eligible", &false) - .finish() - } -} - -impl FixtureCanaryPreflightEvidence { - pub fn readback_state_digest(&self) -> &ReadbackStateDigest { - &self.readback_state_digest - } - - pub fn identity_coverage_digest(&self) -> &IdentityCoverageDigest { - &self.identity_coverage_digest - } - - pub const fn dispatch_eligible(&self) -> bool { - false - } -} - -/// Digest-only semantic observation for the fixed fixture canary. Receipt and -/// readback alone cannot correlate an import to a durable dispatch claim, so -/// this is explicitly not capability evidence or an exact-applied proof. -#[derive(Clone)] -pub struct FixtureCanaryPostDispatchObservation { - import_response_digest: ImportResponseDigest, - readback_state_digest: ReadbackStateDigest, - identity_coverage_digest: IdentityCoverageDigest, -} - -impl FixtureCanaryPostDispatchObservation { - pub fn import_response_digest(&self) -> &ImportResponseDigest { - &self.import_response_digest - } - - pub fn readback_state_digest(&self) -> &ReadbackStateDigest { - &self.readback_state_digest - } - - pub fn identity_coverage_digest(&self) -> &IdentityCoverageDigest { - &self.identity_coverage_digest - } - - pub const fn dispatch_eligible(&self) -> bool { - false - } - - pub const fn capability_observed(&self) -> bool { - false - } -} - -/// Validates a sealed fixture-canary readback and derives only its digest -/// evidence. The caller must keep the XML sealed; this function neither -/// exposes it nor returns any write-capable object. -pub fn verify_fixture_canary_preflight( - prepared: &PreparedFixtureCanary, - readback_xml: &str, -) -> Result { - let observed = parse_readback(&prepared.prepared, readback_xml)?; - if observed.projections != prepared.prepared.expected_before { - return Err(QualificationError::PreflightMismatch); - } - Ok(FixtureCanaryPreflightEvidence { - readback_state_digest: observed.state_digest, - identity_coverage_digest: observed.coverage_digest, - }) -} - -/// Parses an exact-looking receipt/readback pair for later correlation. Both -/// XML inputs stay caller-owned; only digests are returned. A durable sealed -/// coordinator must bind this observation to one dispatch claim before it can -/// record a capability or final verdict. -pub fn observe_fixture_canary_post_dispatch( - prepared: &PreparedFixtureCanary, - receipt_xml: &str, - readback_xml: &str, -) -> Result { - let receipt = parse_import_receipt(receipt_xml)?; - let observed = parse_readback(&prepared.prepared, readback_xml)?; - let exact_after = observed.projections == prepared.prepared.expected_after; - let exact_counters = receipt.is_clean_for(1, 0, 0); - if !exact_after || !exact_counters { - return Err(QualificationError::PostDispatchMismatch); - } - Ok(FixtureCanaryPostDispatchObservation { - import_response_digest: receipt.response_digest, - readback_state_digest: observed.state_digest, - identity_coverage_digest: observed.coverage_digest, - }) -} - -impl std::fmt::Debug for PreparedLedgerImport { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter - .debug_struct("PreparedLedgerImport") - .field("mutation_count", &self.mutations.len()) - .field("wire_digest", &self.wire_digest) - .field("intended_state_digest", &self.intended_state_digest) - .field("approval_evidence_digest", &self.approval_evidence_digest) - .field("identity_query_digest", &self.identity_query_digest) - .field("dispatch_eligible", &false) - .finish() - } -} - -impl PreparedLedgerImport { - pub fn wire_digest(&self) -> &WirePayloadDigest { - &self.wire_digest - } - - pub fn intended_state_digest(&self) -> &IntendedStateDigest { - &self.intended_state_digest - } - - pub fn approval_evidence_digest(&self) -> &ApprovalEvidenceDigest { - &self.approval_evidence_digest - } - - pub fn identity_query_digest(&self) -> &IdentityQueryDigest { - &self.identity_query_digest - } - - pub const fn dispatch_eligible(&self) -> bool { - false - } - - pub fn qualify_preflight( - self, - readback_xml: &str, - ) -> Result { - let observed = parse_readback(&self, readback_xml)?; - if observed.projections != self.expected_before { - return Err(QualificationError::PreflightMismatch); - } - Ok(QualifiedLedgerImport { - prepared: self, - before_readback_digest: observed.state_digest, - before_coverage_digest: observed.coverage_digest, - }) - } -} - -#[derive(Clone)] -pub struct QualifiedLedgerImport { - prepared: PreparedLedgerImport, - before_readback_digest: ReadbackStateDigest, - before_coverage_digest: IdentityCoverageDigest, -} - -impl std::fmt::Debug for QualifiedLedgerImport { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter - .debug_struct("QualifiedLedgerImport") - .field("prepared", &self.prepared) - .field("before_readback_digest", &self.before_readback_digest) - .field("before_coverage_digest", &self.before_coverage_digest) - .finish() - } -} - -impl QualifiedLedgerImport { - pub fn identity_query_digest(&self) -> &IdentityQueryDigest { - &self.prepared.identity_query_digest - } - - pub fn record_dispatch_attempt( - self, - request_id: impl Into, - registry: &mut IdempotencyRegistry, - ) -> Result { - let request_id = request_id.into(); - validate_value(&request_id, "request_id")?; - registry.transition( - &self.prepared.authorization.idempotency_key, - ReservationState::Reserved, - ReservationState::Sent, - )?; - Ok(SentLedgerImport { - qualified: self, - request_id, - }) - } -} - -#[derive(Clone)] -pub struct SentLedgerImport { - qualified: QualifiedLedgerImport, - request_id: String, -} - -impl SentLedgerImport { - pub fn request_id(&self) -> &str { - &self.request_id - } - - pub fn identity_query_digest(&self) -> &IdentityQueryDigest { - self.qualified.identity_query_digest() - } - - pub fn record_import_receipt( - self, - response_xml: &str, - registry: &mut IdempotencyRegistry, - ) -> Result { - let receipt = parse_import_receipt(response_xml)?; - registry.transition( - &self.qualified.prepared.authorization.idempotency_key, - ReservationState::Sent, - ReservationState::AwaitingReadback, - )?; - Ok(AwaitingLedgerReadback { - sent: self, - receipt, - }) - } - - pub fn record_outcome_unknown( - self, - registry: &mut IdempotencyRegistry, - ) -> Result { - registry.transition( - &self.qualified.prepared.authorization.idempotency_key, - ReservationState::Sent, - ReservationState::OutcomeUnknown, - )?; - Ok(OutcomeUnknownLedgerImport { sent: self }) - } -} - -#[derive(Clone)] -pub struct AwaitingLedgerReadback { - sent: SentLedgerImport, - receipt: ParsedImportReceipt, -} - -impl AwaitingLedgerReadback { - pub fn identity_query_digest(&self) -> &IdentityQueryDigest { - self.sent.identity_query_digest() - } - - pub fn verify_readback( - self, - readback_xml: &str, - registry: &mut IdempotencyRegistry, - ) -> Result { - let prepared = &self.sent.qualified.prepared; - let observed = parse_readback(prepared, readback_xml)?; - let exact_after = observed.projections == prepared.expected_after; - let exact_before = observed.projections == prepared.expected_before; - let expected_created = prepared - .mutations - .iter() - .filter(|mutation| mutation.operation == LedgerOperation::Create) - .count() as u64; - let expected_altered = prepared.mutations.len() as u64 - expected_created; - let exact_counters = self - .receipt - .is_clean_for(expected_created, expected_altered, 0); - let zero_counters = self.receipt.counters.created == 0 - && self.receipt.counters.altered == 0 - && self.receipt.counters.deleted == 0; - let outcome = if exact_after && exact_counters { - WriteOutcome::ExactApplied - } else if exact_before && zero_counters { - WriteOutcome::ExactNotApplied - } else { - WriteOutcome::Mismatch - }; - registry.transition( - &prepared.authorization.idempotency_key, - ReservationState::AwaitingReadback, - ReservationState::Terminal, - )?; - Ok(verdict(prepared, Some(&self.receipt), observed, outcome)) - } -} - -#[derive(Clone)] -pub struct OutcomeUnknownLedgerImport { - sent: SentLedgerImport, -} - -impl OutcomeUnknownLedgerImport { - pub fn identity_query_digest(&self) -> &IdentityQueryDigest { - self.sent.identity_query_digest() - } - - /// A readback may add commitments for investigation, but without a parsed - /// import receipt the terminal write outcome remains unknown. - pub fn observe_readback( - &self, - readback_xml: &str, - ) -> Result { - let prepared = &self.sent.qualified.prepared; - let observed = parse_readback(prepared, readback_xml)?; - Ok(verdict( - prepared, - None, - observed, - WriteOutcome::OutcomeUnknown, - )) - } -} - -fn verdict( - prepared: &PreparedLedgerImport, - receipt: Option<&ParsedImportReceipt>, - observed: ParsedReadback, - outcome: WriteOutcome, -) -> DerivedWriteVerdict { - DerivedWriteVerdict { - outcome, - wire_digest: prepared.wire_digest.clone(), - intended_state_digest: prepared.intended_state_digest.clone(), - import_response_digest: receipt.map(|value| value.response_digest.clone()), - readback_state_digest: observed.state_digest, - identity_coverage_digest: observed.coverage_digest, - auto_retry_allowed: false, - } -} - -#[derive(Clone)] -pub struct ParsedImportReceipt { - application_status: TallyImportApplicationStatus, - counters: TallyImportResult, - exceptions_were_reported: bool, - response_digest: ImportResponseDigest, - line_error_digests: Vec, -} - -impl std::fmt::Debug for ParsedImportReceipt { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter - .debug_struct("ParsedImportReceipt") - .field("application_status", &self.application_status) - .field("counters", &self.counters) - .field("exceptions_were_reported", &self.exceptions_were_reported) - .field("response_digest", &self.response_digest) - .field("line_error_count", &self.line_error_digests.len()) - .finish() - } -} - -impl ParsedImportReceipt { - pub fn application_status(&self) -> TallyImportApplicationStatus { - self.application_status - } - - pub fn counters(&self) -> &TallyImportResult { - &self.counters - } - - pub fn exceptions_were_reported(&self) -> bool { - self.exceptions_were_reported - } - - pub fn response_digest(&self) -> &ImportResponseDigest { - &self.response_digest - } - - pub fn line_error_digests(&self) -> &[LineErrorDigest] { - &self.line_error_digests - } - - fn is_clean_for( - &self, - expected_created: u64, - expected_altered: u64, - expected_deleted: u64, - ) -> bool { - self.application_status != TallyImportApplicationStatus::Failure - && self.counters.is_clean_success_for( - expected_created, - expected_altered, - expected_deleted, - ) - } -} - -pub fn parse_import_receipt(xml: &str) -> Result { - let evidence = - parse_import_evidence(xml).map_err(|_| QualificationError::InvalidImportReceipt)?; - Ok(receipt_from_evidence(evidence)) -} - -fn receipt_from_evidence(evidence: ParsedImportEvidence) -> ParsedImportReceipt { - ParsedImportReceipt { - application_status: evidence.application_status(), - counters: evidence.counters().clone(), - exceptions_were_reported: evidence.exceptions_were_reported(), - response_digest: ImportResponseDigest(evidence.response_sha256().to_owned()), - line_error_digests: evidence - .line_error_sha256() - .iter() - .cloned() - .map(LineErrorDigest) - .collect(), - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum WriteOutcome { - ExactApplied, - ExactNotApplied, - Mismatch, - OutcomeUnknown, -} - -#[derive(Clone)] -pub struct DerivedWriteVerdict { - outcome: WriteOutcome, - wire_digest: WirePayloadDigest, - intended_state_digest: IntendedStateDigest, - import_response_digest: Option, - readback_state_digest: ReadbackStateDigest, - identity_coverage_digest: IdentityCoverageDigest, - auto_retry_allowed: bool, -} - -impl std::fmt::Debug for DerivedWriteVerdict { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter - .debug_struct("DerivedWriteVerdict") - .field("outcome", &self.outcome) - .field("wire_digest", &self.wire_digest) - .field("intended_state_digest", &self.intended_state_digest) - .field("import_response_digest", &self.import_response_digest) - .field("readback_state_digest", &self.readback_state_digest) - .field("identity_coverage_digest", &self.identity_coverage_digest) - .field("auto_retry_allowed", &self.auto_retry_allowed) - .finish() - } -} - -impl DerivedWriteVerdict { - pub fn outcome(&self) -> WriteOutcome { - self.outcome - } - - pub const fn auto_retry_allowed(&self) -> bool { - self.auto_retry_allowed - } -} - -#[derive(Debug, Error, Clone, PartialEq, Eq)] -pub enum QualificationError { - #[error("explicit operator opt-in is required")] - ExplicitOptInRequired, - #[error("controlled writes require an explicitly confirmed synthetic company")] - SyntheticCompanyRequired, - #[error("controlled writes require an observed capability")] - ObservedCapabilityRequired, - #[error("backup guidance acknowledgement is required")] - BackupAcknowledgementRequired, - #[error("approval evidence does not bind the exact preview commitments")] - ApprovalMismatch, - #[error("fixture canary payload does not match its approved commitment")] - FixtureCanaryPayloadMismatch, - #[error("controlled ledger write batch must contain between one and ten items")] - InvalidBatchSize, - #[error("controlled ledger write input is invalid: {0}")] - InvalidField(&'static str), - #[error("controlled ledger write contains a duplicate remote identity")] - DuplicateIdentity, - #[error("idempotency key is already bound to another write")] - IdempotencyConflict, - #[error("duplicate write submission is blocked")] - DuplicateSubmission, - #[error("idempotency reservation is missing")] - MissingIdempotencyReservation, - #[error("controlled ledger alter must change at least one verified field")] - NoOpMutation, - #[error("controlled ledger create requires an explicit parent")] - CreateParentRequired, - #[error("clearing controlled ledger field is not yet qualified: {0}")] - UnsupportedFieldClear(&'static str), - #[error("preflight readback did not exactly match the declared before state")] - PreflightMismatch, - #[error("post-dispatch receipt or readback did not exactly apply the fixture canary")] - PostDispatchMismatch, - #[error("Tally import receipt was invalid")] - InvalidImportReceipt, - #[error("Tally ledger readback was invalid or outside the expected company/profile")] - InvalidReadback, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] -struct Projection { - contract: &'static str, - company_guid: String, - profile: &'static str, - operation: LedgerOperation, - remote_id: String, - source_lineage: SourceLineage, - mapping_version: String, - state: LedgerState, -} - -#[derive(Serialize)] -struct MutationIntentPreimage<'a> { - contract: &'static str, - before: &'a BTreeMap, - after: &'a BTreeMap, -} - -#[derive(Debug)] -struct ParsedReadback { - projections: BTreeMap, - state_digest: ReadbackStateDigest, - coverage_digest: IdentityCoverageDigest, -} - -#[derive(Clone, Debug)] -pub struct LedgerImportPreview { - wire_digest: WirePayloadDigest, - intended_state_digest: IntendedStateDigest, - identity_query_digest: IdentityQueryDigest, -} - -impl LedgerImportPreview { - pub fn wire_digest(&self) -> &WirePayloadDigest { - &self.wire_digest - } - - pub fn intended_state_digest(&self) -> &IntendedStateDigest { - &self.intended_state_digest - } - - pub fn identity_query_digest(&self) -> &IdentityQueryDigest { - &self.identity_query_digest - } -} - -pub fn preview_ledger_import( - company: &SyntheticCompany, - mutations: &[LedgerMutation], - mapping_version: &str, -) -> Result { - validate_value(mapping_version, "mapping_version")?; - if mutations.is_empty() || mutations.len() > MAX_LEDGER_WRITE_BATCH { - return Err(QualificationError::InvalidBatchSize); - } - let identities = mutations - .iter() - .map(|mutation| mutation.remote_id.clone()) - .collect::>(); - if identities.len() != mutations.len() { - return Err(QualificationError::DuplicateIdentity); - } - let expected_after = mutations - .iter() - .map(|mutation| { - ( - mutation.remote_id.clone(), - projection(company, mutation, mutation.after.clone(), mapping_version), - ) - }) - .collect::>(); - let expected_before = mutations - .iter() - .filter_map(|mutation| { - mutation.before.as_ref().map(|before| { - ( - mutation.remote_id.clone(), - projection(company, mutation, before.clone(), mapping_version), - ) - }) - }) - .collect::>(); - let wire_bytes = build_import_xml(company, mutations).into_bytes(); - let intended_bytes = serde_json::to_vec(&MutationIntentPreimage { - contract: "bridge.tally.ledger-mutation-intent/1", - before: &expected_before, - after: &expected_after, - }) - .expect("versioned ledger projections are always serializable"); - let identity_bytes = - serde_json::to_vec(&identities).expect("identity query sets are always serializable"); - Ok(LedgerImportPreview { - wire_digest: WirePayloadDigest(domain_digest( - b"bridge.tally.ledger-import-wire/1\0", - &wire_bytes, - )), - intended_state_digest: IntendedStateDigest(domain_digest( - b"bridge.tally.ledger-intended-state/1\0", - &intended_bytes, - )), - identity_query_digest: IdentityQueryDigest(domain_digest( - b"bridge.tally.ledger-readback-query-identities/1\0", - &identity_bytes, - )), - }) -} - -pub fn prepare_ledger_import( - company: SyntheticCompany, - mutations: Vec, - authorization: WriteAuthorization, - registry: &mut IdempotencyRegistry, -) -> Result { - if mutations.is_empty() || mutations.len() > MAX_LEDGER_WRITE_BATCH { - return Err(QualificationError::InvalidBatchSize); - } - if authorization.company_guid != company.guid { - return Err(QualificationError::SyntheticCompanyRequired); - } - let mut identities = BTreeSet::new(); - let mut expected_before = BTreeMap::new(); - let mut expected_after = BTreeMap::new(); - for mutation in &mutations { - if !identities.insert(mutation.remote_id.clone()) { - return Err(QualificationError::DuplicateIdentity); - } - if let Some(before) = &mutation.before { - expected_before.insert( - mutation.remote_id.clone(), - projection( - &company, - mutation, - before.clone(), - &authorization.mapping_version, - ), - ); - } - expected_after.insert( - mutation.remote_id.clone(), - projection( - &company, - mutation, - mutation.after.clone(), - &authorization.mapping_version, - ), - ); - } - let wire_bytes = build_import_xml(&company, &mutations).into_bytes(); - let wire_digest = WirePayloadDigest(domain_digest( - b"bridge.tally.ledger-import-wire/1\0", - &wire_bytes, - )); - let intended_bytes = serde_json::to_vec(&MutationIntentPreimage { - contract: "bridge.tally.ledger-mutation-intent/1", - before: &expected_before, - after: &expected_after, - }) - .expect("versioned ledger projections are always serializable"); - let intended_state_digest = IntendedStateDigest(domain_digest( - b"bridge.tally.ledger-intended-state/1\0", - &intended_bytes, - )); - let identity_bytes = - serde_json::to_vec(&identities).expect("identity query sets are always serializable"); - let identity_query_digest = IdentityQueryDigest(domain_digest( - b"bridge.tally.ledger-readback-query-identities/1\0", - &identity_bytes, - )); - if authorization.approved_wire_sha256 != wire_digest.as_hex() - || authorization.approved_intended_state_sha256 != intended_state_digest.as_hex() - || authorization.approved_identity_query_sha256 != identity_query_digest.as_hex() - { - return Err(QualificationError::ApprovalMismatch); - } - let approval_evidence_digest = - ApprovalEvidenceDigest(authorization.approval_evidence_sha256.clone()); - registry.reserve(&authorization, &wire_digest)?; - Ok(PreparedLedgerImport { - company, - mutations, - wire_digest, - intended_state_digest, - expected_before, - expected_after, - authorization, - approval_evidence_digest, - identity_query_digest, - }) -} - -/// Returns the one immutable ledger create used to qualify a disposable -/// fixture. The canary deliberately carries no GSTIN and a zero balance. -pub fn fixture_canary_ledger_mutation() -> Result { - LedgerMutation::create( - FIXTURE_CANARY_REMOTE_ID, - LedgerState::new( - FIXTURE_CANARY_LEDGER_NAME, - Some(FIXTURE_CANARY_PARENT.to_owned()), - None, - Some(FIXTURE_CANARY_OPENING_BALANCE.to_owned()), - )?, - SourceLineage::new( - "bridge-fixture-canary", - FIXTURE_CANARY_REMOTE_ID, - FIXTURE_CANARY_MAPPING_VERSION, - )?, - ) -} - -/// Prepares exactly one fixed, non-dispatchable fixture-canary preview. It has -/// no caller-supplied mutation or mapping escape hatch, and its returned type -/// intentionally cannot progress into the generic import lifecycle. -pub fn prepare_fixture_canary_ledger_import( - company: SyntheticCompany, - authorization: FixtureCanaryAuthorization, - registry: &mut IdempotencyRegistry, -) -> Result { - let prepared = prepare_ledger_import( - company, - vec![fixture_canary_ledger_mutation()?], - authorization.authorization, - registry, - )?; - Ok(PreparedFixtureCanary { prepared }) -} - -fn projection( - company: &SyntheticCompany, - mutation: &LedgerMutation, - state: LedgerState, - mapping_version: &str, -) -> Projection { - Projection { - contract: LEDGER_WRITE_PROJECTION, - company_guid: company.guid.clone(), - profile: LEDGER_READBACK_PROFILE, - operation: mutation.operation, - remote_id: mutation.remote_id.clone(), - source_lineage: mutation.source_lineage.clone(), - mapping_version: mapping_version.to_owned(), - state, - } -} - -fn parse_readback( - prepared: &PreparedLedgerImport, - xml: &str, -) -> Result { - let parsed = parse_ledger_write_readback_with_evidence(xml) - .map_err(|_| QualificationError::InvalidReadback)?; - let context = parsed - .evidence - .company_context - .as_ref() - .ok_or(QualificationError::InvalidReadback)?; - if context.guid.as_deref() != Some(prepared.company.guid.as_str()) - || context.query_identity_set_sha256.as_deref() - != Some(prepared.identity_query_digest.as_hex()) - || parsed.evidence.schema.as_deref() != Some(LEDGER_READBACK_PROFILE) - || parsed.evidence.object_type.as_deref() != Some("LEDGER") - || parsed.evidence.source_record_count != Some(parsed.records.len() as u64) - || !parsed.evidence.duplicate_identities.is_empty() - { - return Err(QualificationError::InvalidReadback); - } - - let by_id: BTreeMap<&str, &LedgerMutation> = prepared - .mutations - .iter() - .map(|mutation| (mutation.remote_id.as_str(), mutation)) - .collect(); - let mut projections = BTreeMap::new(); - for record in parsed.records { - let remote_id = record - .identities - .remote_id - .ok_or(QualificationError::InvalidReadback)?; - let mutation = by_id - .get(remote_id.as_str()) - .ok_or(QualificationError::InvalidReadback)?; - if projections.contains_key(&remote_id) { - return Err(QualificationError::InvalidReadback); - } - let state = LedgerState::new( - record.record.name, - record.record.parent, - record.record.party_gstin, - record.record.opening_balance, - )?; - projections.insert( - remote_id.clone(), - projection( - &prepared.company, - mutation, - state, - &prepared.authorization.mapping_version, - ), - ); - } - let state_bytes = serde_json::to_vec(&projections) - .expect("versioned ledger projections are always serializable"); - let identities = projections.keys().cloned().collect::>(); - let coverage_bytes = - serde_json::to_vec(&identities).expect("identity coverage is always serializable"); - Ok(ParsedReadback { - projections, - state_digest: ReadbackStateDigest(domain_digest( - b"bridge.tally.ledger-readback-state/1\0", - &state_bytes, - )), - coverage_digest: IdentityCoverageDigest(domain_digest( - b"bridge.tally.ledger-identity-coverage/1\0", - &coverage_bytes, - )), - }) -} - -fn build_import_xml(company: &SyntheticCompany, mutations: &[LedgerMutation]) -> String { - let mut xml = String::from( - "
1ImportDataAll Masters
", - ); - push_xml_text(&mut xml, &company.name); - xml.push_str(""); - for mutation in mutations { - xml.push_str(""); - if mutation.operation == LedgerOperation::Create - || mutation - .before - .as_ref() - .is_some_and(|before| before.name != mutation.after.name) - { - push_element(&mut xml, "NAME", &mutation.after.name); - } - if let Some(value) = &mutation.after.parent { - push_element(&mut xml, "PARENT", value); - } - if let Some(value) = &mutation.after.party_gstin { - push_element(&mut xml, "PARTYGSTIN", value); - } - if let Some(value) = &mutation.after.opening_balance { - push_element(&mut xml, "OPENINGBALANCE", value); - } - xml.push_str(""); - } - xml.push_str("
"); - xml -} - -fn push_element(xml: &mut String, name: &str, value: &str) { - xml.push('<'); - xml.push_str(name); - xml.push('>'); - push_xml_text(xml, value); - xml.push_str("'); -} - -fn push_xml_text(xml: &mut String, value: &str) { - for character in value.chars() { - match character { - '&' => xml.push_str("&"), - '<' => xml.push_str("<"), - '>' => xml.push_str(">"), - _ => xml.push(character), - } - } -} - -fn push_xml_attribute(xml: &mut String, value: &str) { - for character in value.chars() { - match character { - '&' => xml.push_str("&"), - '<' => xml.push_str("<"), - '>' => xml.push_str(">"), - '\"' => xml.push_str("""), - '\'' => xml.push_str("'"), - _ => xml.push(character), - } - } -} - -fn validate_value(value: &str, field: &'static str) -> Result<(), QualificationError> { - if value.trim().is_empty() - || value.len() > 255 - || value.chars().any(|character| character.is_control()) - { - return Err(QualificationError::InvalidField(field)); - } - Ok(()) -} - -fn validate_sha256(value: &str, field: &'static str) -> Result<(), QualificationError> { - if value.len() != 64 || !value.bytes().all(|byte| byte.is_ascii_hexdigit()) { - return Err(QualificationError::InvalidField(field)); - } - Ok(()) -} - -fn validate_gstin(value: &str) -> Result<(), QualificationError> { - let bytes = value.as_bytes(); - let valid = bytes.len() == 15 - && bytes[0..2].iter().all(u8::is_ascii_digit) - && bytes[2..7].iter().all(u8::is_ascii_uppercase) - && bytes[7..11].iter().all(u8::is_ascii_digit) - && bytes[11].is_ascii_uppercase() - && bytes[12].is_ascii_alphanumeric() - && bytes[13] == b'Z' - && bytes[14].is_ascii_alphanumeric(); - if !valid { - return Err(QualificationError::InvalidField("party_gstin")); - } - Ok(()) -} - -fn domain_digest(domain: &[u8], value: &[u8]) -> String { - let mut digest = Sha256::new(); - digest.update(domain); - digest.update(value); - let mut encoded = String::with_capacity(64); - for byte in digest.finalize() { - use std::fmt::Write as _; - write!(&mut encoded, "{byte:02x}").expect("writing to a String cannot fail"); - } - encoded -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn private_wire_builder_escapes_text_and_attributes() { - let company = SyntheticCompany::new("BRIDGE & BOOK", "company-guid").unwrap(); - let mutation = LedgerMutation::create( - "remote-\"<&", - LedgerState::new( - "LEDGER <&", - Some("PARENT <&".to_owned()), - None, - Some("0".to_owned()), - ) - .unwrap(), - SourceLineage::new("synthetic", "record", "v1").unwrap(), - ) - .unwrap(); - let xml = build_import_xml(&company, &[mutation]); - assert!(xml.contains("BRIDGE & BOOK")); - assert!(xml.contains("remote-"<&")); - assert!(xml - .contains("NAME=\"LEDGER <&\" ACTION=\"Create\">LEDGER <&")); - assert!(xml.contains("PARENT <&")); - } - - #[test] - fn private_wire_builder_uses_existing_name_to_select_a_rename() { - let company = SyntheticCompany::new("BRIDGE SYNTHETIC BOOK", "company-guid").unwrap(); - let mutation = LedgerMutation::alter( - "bridge-remote-id", - LedgerState::new("OLD LEDGER", None, None, None).unwrap(), - LedgerState::new("NEW LEDGER", None, None, None).unwrap(), - SourceLineage::new("synthetic", "record", "v1").unwrap(), - ) - .unwrap(); - - let xml = build_import_xml(&company, &[mutation]); - - assert!(xml.contains("NAME=\"OLD LEDGER\" ACTION=\"Alter\">NEW LEDGER")); - assert!(!xml.contains("NAME=\"NEW LEDGER\" ACTION=\"Alter\"")); - } -} diff --git a/src-tauri/crates/bridge-tally-write/tests/qualification.rs b/src-tauri/crates/bridge-tally-write/tests/qualification.rs deleted file mode 100644 index 6d780ba..0000000 --- a/src-tauri/crates/bridge-tally-write/tests/qualification.rs +++ /dev/null @@ -1,743 +0,0 @@ -use bridge_tally_write::{ - authorize_fixture_canary, authorize_synthetic_write, fixture_canary_ledger_mutation, - observe_fixture_canary_post_dispatch, prepare_fixture_canary_ledger_import, - prepare_ledger_import, preview_ledger_import, verify_fixture_canary_preflight, - FixtureCanaryAuthorization, FixtureCanaryAuthorizationRequest, IdempotencyRegistry, - LedgerMutation, LedgerState, PreparedLedgerImport, QualificationError, SourceLineage, - SyntheticCompany, WriteAuthorizationRequest, WriteCapability, WriteOutcome, - FIXTURE_CANARY_MAPPING_VERSION, MAX_LEDGER_WRITE_BATCH, -}; - -#[cfg(feature = "fixture-canary-dispatch-seam")] -use bridge_tally_write::SealedFixtureCanaryDispatch; - -const COMPANY_GUID: &str = "00000000-0000-4000-8000-000000000001"; -const REMOTE_ID: &str = "bridge-synthetic-ledger-001"; -const HASH: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; -const PROFILE: &str = "bridge.tally.ledger-write-readback/1"; - -fn company() -> SyntheticCompany { - SyntheticCompany::new("BRIDGE SYNTHETIC BOOK", COMPANY_GUID).unwrap() -} - -fn lineage(index: usize) -> SourceLineage { - SourceLineage::new("synthetic-source", format!("record-{index}"), "version-1").unwrap() -} - -fn state(name: &str, balance: &str) -> LedgerState { - LedgerState::new( - name, - Some("BRIDGE SYNTHETIC GROUP".to_owned()), - Some("29ABCDE1234F1Z5".to_owned()), - Some(balance.to_owned()), - ) - .unwrap() -} - -fn create(remote_id: impl Into, after: LedgerState, index: usize) -> LedgerMutation { - LedgerMutation::create(remote_id, after, lineage(index)).unwrap() -} - -fn alter(remote_id: impl Into, before: LedgerState, after: LedgerState) -> LedgerMutation { - LedgerMutation::alter(remote_id, before, after, lineage(1)).unwrap() -} - -fn authorization( - key: &str, - company: &SyntheticCompany, - mutations: &[LedgerMutation], -) -> bridge_tally_write::WriteAuthorization { - let preview = preview_ledger_import(company, mutations, "mapping-v1").unwrap(); - authorize_synthetic_write(WriteAuthorizationRequest { - explicit_opt_in: true, - synthetic_company_confirmed: true, - company_guid: COMPANY_GUID.to_owned(), - capability: WriteCapability::Observed, - backup_guidance_acknowledged: true, - approval_evidence_sha256: HASH.to_owned(), - approved_wire_sha256: preview.wire_digest().as_hex().to_owned(), - approved_intended_state_sha256: preview.intended_state_digest().as_hex().to_owned(), - approved_identity_query_sha256: preview.identity_query_digest().as_hex().to_owned(), - idempotency_key: key.to_owned(), - outbox_id: format!("outbox-{key}"), - mapping_version: "mapping-v1".to_owned(), - }) - .unwrap() -} - -fn prepare( - key: &str, - mutations: Vec, - registry: &mut IdempotencyRegistry, -) -> Result { - let company = company(); - let authorization = authorization(key, &company, &mutations); - prepare_ledger_import(company, mutations, authorization, registry) -} - -fn fixture_authorization( - company: &SyntheticCompany, - reservation_id: &str, - idempotency_key: &str, -) -> FixtureCanaryAuthorization { - let mutation = fixture_canary_ledger_mutation().unwrap(); - let preview = - preview_ledger_import(company, &[mutation], FIXTURE_CANARY_MAPPING_VERSION).unwrap(); - authorize_fixture_canary(FixtureCanaryAuthorizationRequest { - explicit_opt_in: true, - synthetic_company_confirmed: true, - company_guid: COMPANY_GUID.to_owned(), - backup_guidance_acknowledged: true, - review_commitment_sha256: HASH.to_owned(), - reservation_id: reservation_id.to_owned(), - reservation_payload_sha256: HASH.to_owned(), - approved_wire_sha256: preview.wire_digest().as_hex().to_owned(), - approved_intended_state_sha256: preview.intended_state_digest().as_hex().to_owned(), - approved_identity_query_sha256: preview.identity_query_digest().as_hex().to_owned(), - idempotency_key: idempotency_key.to_owned(), - }) - .unwrap() -} - -fn export( - company_guid: &str, - schema: &str, - query_digest: &str, - ledgers: &str, - count: usize, -) -> String { - format!( - r#"
1
{ledgers}
"# - ) -} - -fn ledger(remote_id: &str, name: &str, balance: &str) -> String { - format!( - r#"BRIDGE SYNTHETIC GROUP29ABCDE1234F1Z5{balance}"# - ) -} - -fn receipt(created: u64, altered: u64, deleted: u64) -> String { - format!( - "{created}{altered}{deleted}0000" - ) -} - -#[test] -fn authorization_gates_are_mandatory() { - let base = WriteAuthorizationRequest { - explicit_opt_in: true, - synthetic_company_confirmed: true, - company_guid: COMPANY_GUID.to_owned(), - capability: WriteCapability::Observed, - backup_guidance_acknowledged: true, - approval_evidence_sha256: HASH.to_owned(), - approved_wire_sha256: HASH.to_owned(), - approved_intended_state_sha256: HASH.to_owned(), - approved_identity_query_sha256: HASH.to_owned(), - idempotency_key: "key".to_owned(), - outbox_id: "outbox".to_owned(), - mapping_version: "mapping-v1".to_owned(), - }; - let mut request = base.clone(); - request.explicit_opt_in = false; - assert_eq!( - authorize_synthetic_write(request).unwrap_err(), - QualificationError::ExplicitOptInRequired - ); - let mut request = base.clone(); - request.synthetic_company_confirmed = false; - assert_eq!( - authorize_synthetic_write(request).unwrap_err(), - QualificationError::SyntheticCompanyRequired - ); - let mut request = base.clone(); - request.capability = WriteCapability::Documented; - assert_eq!( - authorize_synthetic_write(request).unwrap_err(), - QualificationError::ObservedCapabilityRequired - ); - let mut request = base; - request.backup_guidance_acknowledged = false; - assert_eq!( - authorize_synthetic_write(request).unwrap_err(), - QualificationError::BackupAcknowledgementRequired - ); -} - -#[test] -fn authorization_requests_redact_fixture_and_commitment_values_in_debug_output() { - let synthetic_company = company(); - let generic_mutation = create(REMOTE_ID, state("BRIDGE LEDGER", "10.00"), 1); - let generic_preview = - preview_ledger_import(&synthetic_company, &[generic_mutation], "mapping-v1") - .expect("preview generic authorization request"); - let generic = WriteAuthorizationRequest { - explicit_opt_in: true, - synthetic_company_confirmed: true, - company_guid: COMPANY_GUID.to_owned(), - capability: WriteCapability::Observed, - backup_guidance_acknowledged: true, - approval_evidence_sha256: HASH.to_owned(), - approved_wire_sha256: generic_preview.wire_digest().as_hex().to_owned(), - approved_intended_state_sha256: generic_preview.intended_state_digest().as_hex().to_owned(), - approved_identity_query_sha256: generic_preview.identity_query_digest().as_hex().to_owned(), - idempotency_key: "fixture-sensitive-key".to_owned(), - outbox_id: "fixture-sensitive-outbox".to_owned(), - mapping_version: "mapping-v1".to_owned(), - }; - let fixture = FixtureCanaryAuthorizationRequest { - explicit_opt_in: true, - synthetic_company_confirmed: true, - company_guid: COMPANY_GUID.to_owned(), - backup_guidance_acknowledged: true, - review_commitment_sha256: HASH.to_owned(), - reservation_id: "fixture-sensitive-reservation".to_owned(), - reservation_payload_sha256: HASH.to_owned(), - approved_wire_sha256: HASH.to_owned(), - approved_intended_state_sha256: HASH.to_owned(), - approved_identity_query_sha256: HASH.to_owned(), - idempotency_key: "fixture-sensitive-key".to_owned(), - }; - let generic_debug = format!("{generic:?}"); - let fixture_debug = format!("{fixture:?}"); - for secret in [ - COMPANY_GUID, - HASH, - "fixture-sensitive-key", - "fixture-sensitive-outbox", - "fixture-sensitive-reservation", - ] { - assert!(!generic_debug.contains(secret)); - assert!(!fixture_debug.contains(secret)); - } -} - -#[test] -fn fixture_canary_is_fixed_reservation_bound_and_dispatch_ineligible() { - let synthetic_company = company(); - let first = fixture_canary_ledger_mutation().expect("construct fixed canary"); - let second = fixture_canary_ledger_mutation().expect("construct fixed canary replay"); - let first_preview = - preview_ledger_import(&synthetic_company, &[first], FIXTURE_CANARY_MAPPING_VERSION) - .unwrap(); - let second_preview = preview_ledger_import( - &synthetic_company, - &[second], - FIXTURE_CANARY_MAPPING_VERSION, - ) - .unwrap(); - assert_eq!( - first_preview.wire_digest().as_hex(), - second_preview.wire_digest().as_hex() - ); - assert_eq!( - first_preview.intended_state_digest().as_hex(), - second_preview.intended_state_digest().as_hex() - ); - - let authorization = - fixture_authorization(&synthetic_company, "fixture-reservation-1", "fixture-key-1"); - assert_eq!(authorization.reservation_id(), "fixture-reservation-1"); - assert_eq!(authorization.reservation_payload_sha256(), HASH); - assert_eq!(authorization.review_commitment_sha256(), HASH); - let mut registry = IdempotencyRegistry::default(); - let prepared = - prepare_fixture_canary_ledger_import(synthetic_company, authorization, &mut registry) - .expect("prepare exact fixture canary"); - assert!(!prepared.dispatch_eligible()); - let query = prepared.identity_query_digest().as_hex().to_owned(); - let evidence = - verify_fixture_canary_preflight(&prepared, &export(COMPANY_GUID, PROFILE, &query, "", 0)) - .expect("derive sealed absence evidence"); - assert!(!evidence.dispatch_eligible()); - let after = export( - COMPANY_GUID, - PROFILE, - &query, - r#"Indirect Expenses0"#, - 1, - ); - let verdict = observe_fixture_canary_post_dispatch(&prepared, &receipt(1, 0, 0), &after) - .expect("derive sealed exact-applied evidence"); - assert!(!verdict.dispatch_eligible()); - assert!(!verdict.capability_observed()); - assert!(matches!( - observe_fixture_canary_post_dispatch(&prepared, &receipt(0, 0, 0), &after), - Err(QualificationError::PostDispatchMismatch) - )); - - let mut mismatched_request = FixtureCanaryAuthorizationRequest { - explicit_opt_in: true, - synthetic_company_confirmed: true, - company_guid: COMPANY_GUID.to_owned(), - backup_guidance_acknowledged: true, - review_commitment_sha256: HASH.to_owned(), - reservation_id: "fixture-reservation-2".to_owned(), - reservation_payload_sha256: HASH.to_owned(), - approved_wire_sha256: HASH.to_owned(), - approved_intended_state_sha256: HASH.to_owned(), - approved_identity_query_sha256: HASH.to_owned(), - idempotency_key: "fixture-key-2".to_owned(), - }; - assert_eq!( - prepare_fixture_canary_ledger_import( - company(), - authorize_fixture_canary(mismatched_request.clone()).unwrap(), - &mut IdempotencyRegistry::default(), - ) - .unwrap_err(), - QualificationError::ApprovalMismatch - ); - mismatched_request.explicit_opt_in = false; - assert_eq!( - authorize_fixture_canary(mismatched_request).unwrap_err(), - QualificationError::ExplicitOptInRequired - ); -} - -#[cfg(feature = "fixture-canary-dispatch-seam")] -#[test] -fn sealed_fixture_dispatch_capability_is_explicit_and_redacted() { - let synthetic_company = company(); - let authorization = fixture_authorization( - &synthetic_company, - "fixture-dispatch-seam-reservation", - "fixture-dispatch-seam-idempotency", - ); - let prepared = prepare_fixture_canary_ledger_import( - synthetic_company, - authorization, - &mut IdempotencyRegistry::default(), - ) - .expect("prepare fixed fixture canary"); - - let approved_wire_digest = prepared.wire_digest().clone(); - let capsule: SealedFixtureCanaryDispatch = prepared - .seal_for_dispatch() - .expect("seal exact fixed fixture canary payload"); - assert_eq!(capsule.wire_digest(), &approved_wire_digest); - let debug = format!("{capsule:?}"); - assert!(debug.contains("[redacted]")); - assert!(debug.contains("absent")); - assert!(!debug.contains("BRIDGE-CANARY-LEDGER-V1")); - - assert_eq!(capsule.wire_digest(), &approved_wire_digest); -} - -#[test] -fn approval_must_bind_the_exact_preview_commitments() { - let company = company(); - let mutations = vec![create(REMOTE_ID, state("BRIDGE LEDGER", "10.00"), 1)]; - let preview = preview_ledger_import(&company, &mutations, "mapping-v1").unwrap(); - let authorization = authorize_synthetic_write(WriteAuthorizationRequest { - explicit_opt_in: true, - synthetic_company_confirmed: true, - company_guid: COMPANY_GUID.to_owned(), - capability: WriteCapability::Observed, - backup_guidance_acknowledged: true, - approval_evidence_sha256: HASH.to_owned(), - approved_wire_sha256: HASH.to_owned(), - approved_intended_state_sha256: preview.intended_state_digest().as_hex().to_owned(), - approved_identity_query_sha256: preview.identity_query_digest().as_hex().to_owned(), - idempotency_key: "key-mismatched-approval".to_owned(), - outbox_id: "outbox-mismatched-approval".to_owned(), - mapping_version: "mapping-v1".to_owned(), - }) - .unwrap(); - assert_eq!( - prepare_ledger_import( - company, - mutations, - authorization, - &mut IdempotencyRegistry::default(), - ) - .unwrap_err(), - QualificationError::ApprovalMismatch - ); -} - -#[test] -fn alter_approval_binds_the_exact_declared_before_state() { - let company = company(); - let approved = vec![alter( - REMOTE_ID, - state("BRIDGE LEDGER", "10.00"), - state("BRIDGE LEDGER", "20.00"), - )]; - let authorization = authorization("key-before-binding", &company, &approved); - let changed_before = vec![alter( - REMOTE_ID, - state("BRIDGE LEDGER", "11.00"), - state("BRIDGE LEDGER", "20.00"), - )]; - assert_eq!( - prepare_ledger_import( - company, - changed_before, - authorization, - &mut IdempotencyRegistry::default(), - ) - .unwrap_err(), - QualificationError::ApprovalMismatch - ); -} - -#[test] -fn prepared_import_is_deterministic_and_bytes_are_not_publicly_exposed() { - let mutation = create( - "bridge-&-identity", - LedgerState::new( - "BRIDGE ", - Some("BRIDGE SYNTHETIC GROUP".to_owned()), - None, - Some("0".to_owned()), - ) - .unwrap(), - 1, - ); - let mut first_registry = IdempotencyRegistry::default(); - let first = prepare("key-1", vec![mutation.clone()], &mut first_registry).unwrap(); - let mut second_registry = IdempotencyRegistry::default(); - let second = prepare("key-1", vec![mutation], &mut second_registry).unwrap(); - assert!(!first.dispatch_eligible()); - assert_eq!(first.wire_digest().as_hex(), second.wire_digest().as_hex()); - assert_ne!( - first.wire_digest().as_hex(), - first.intended_state_digest().as_hex() - ); - let query = first.identity_query_digest().as_hex().to_owned(); - first - .qualify_preflight(&export(COMPANY_GUID, PROFILE, &query, "", 0)) - .unwrap(); -} - -#[test] -fn exact_company_bound_create_requires_lifecycle_and_exact_counters() { - let mut registry = IdempotencyRegistry::default(); - let prepared = prepare( - "key-applied", - vec![create(REMOTE_ID, state("BRIDGE LEDGER", "10.00"), 1)], - &mut registry, - ) - .unwrap(); - let query = prepared.identity_query_digest().as_hex().to_owned(); - let sent = prepared - .qualify_preflight(&export(COMPANY_GUID, PROFILE, &query, "", 0)) - .unwrap() - .record_dispatch_attempt("request-1", &mut registry) - .unwrap(); - let awaiting = sent - .record_import_receipt(&receipt(1, 0, 0), &mut registry) - .unwrap(); - let after = ledger(REMOTE_ID, "BRIDGE LEDGER", "10.00"); - let verdict = awaiting - .verify_readback( - &export(COMPANY_GUID, PROFILE, &query, &after, 1), - &mut registry, - ) - .unwrap(); - assert_eq!(verdict.outcome(), WriteOutcome::ExactApplied); - assert!(!verdict.auto_retry_allowed()); -} - -#[test] -fn contradictory_operation_counters_cannot_verify() { - for (key, counters) in [ - ("wrong-alter", receipt(0, 1, 0)), - ("wrong-delete", receipt(0, 0, 1)), - ] { - let mut registry = IdempotencyRegistry::default(); - let prepared = prepare( - key, - vec![create(REMOTE_ID, state("BRIDGE LEDGER", "10.00"), 1)], - &mut registry, - ) - .unwrap(); - let query = prepared.identity_query_digest().as_hex().to_owned(); - let awaiting = prepared - .qualify_preflight(&export(COMPANY_GUID, PROFILE, &query, "", 0)) - .unwrap() - .record_dispatch_attempt(format!("request-{key}"), &mut registry) - .unwrap() - .record_import_receipt(&counters, &mut registry) - .unwrap(); - let after = ledger(REMOTE_ID, "BRIDGE LEDGER", "10.00"); - assert_eq!( - awaiting - .verify_readback( - &export(COMPANY_GUID, PROFILE, &query, &after, 1), - &mut registry, - ) - .unwrap() - .outcome(), - WriteOutcome::Mismatch - ); - } -} - -#[test] -fn clean_counters_plus_unchanged_alter_state_cannot_verify() { - let mut registry = IdempotencyRegistry::default(); - let old = ledger(REMOTE_ID, "BRIDGE LEDGER", "10.00"); - let prepared = prepare( - "key-stale", - vec![alter( - REMOTE_ID, - state("BRIDGE LEDGER", "10.00"), - state("BRIDGE LEDGER", "20.00"), - )], - &mut registry, - ) - .unwrap(); - let query = prepared.identity_query_digest().as_hex().to_owned(); - let awaiting = prepared - .qualify_preflight(&export(COMPANY_GUID, PROFILE, &query, &old, 1)) - .unwrap() - .record_dispatch_attempt("request-stale", &mut registry) - .unwrap() - .record_import_receipt(&receipt(0, 1, 0), &mut registry) - .unwrap(); - assert_eq!( - awaiting - .verify_readback( - &export(COMPANY_GUID, PROFILE, &query, &old, 1), - &mut registry, - ) - .unwrap() - .outcome(), - WriteOutcome::Mismatch - ); -} - -#[test] -fn lost_response_remains_outcome_unknown_even_with_exact_readback() { - let mut registry = IdempotencyRegistry::default(); - let prepared = prepare( - "key-unknown", - vec![create(REMOTE_ID, state("BRIDGE LEDGER", "10.00"), 1)], - &mut registry, - ) - .unwrap(); - let query = prepared.identity_query_digest().as_hex().to_owned(); - let unknown = prepared - .qualify_preflight(&export(COMPANY_GUID, PROFILE, &query, "", 0)) - .unwrap() - .record_dispatch_attempt("request-unknown", &mut registry) - .unwrap() - .record_outcome_unknown(&mut registry) - .unwrap(); - let after = ledger(REMOTE_ID, "BRIDGE LEDGER", "10.00"); - assert_eq!( - unknown - .observe_readback(&export(COMPANY_GUID, PROFILE, &query, &after, 1)) - .unwrap() - .outcome(), - WriteOutcome::OutcomeUnknown - ); - assert_eq!( - unknown - .observe_readback(&export(COMPANY_GUID, PROFILE, &query, "", 0)) - .unwrap() - .outcome(), - WriteOutcome::OutcomeUnknown - ); -} - -#[test] -fn parsed_zero_mutation_receipt_can_prove_exact_not_applied() { - let mut registry = IdempotencyRegistry::default(); - let prepared = prepare( - "key-not-applied", - vec![create(REMOTE_ID, state("BRIDGE LEDGER", "10.00"), 1)], - &mut registry, - ) - .unwrap(); - let query = prepared.identity_query_digest().as_hex().to_owned(); - let awaiting = prepared - .qualify_preflight(&export(COMPANY_GUID, PROFILE, &query, "", 0)) - .unwrap() - .record_dispatch_attempt("request-not-applied", &mut registry) - .unwrap() - .record_import_receipt(&receipt(0, 0, 0), &mut registry) - .unwrap(); - assert_eq!( - awaiting - .verify_readback(&export(COMPANY_GUID, PROFILE, &query, "", 0), &mut registry,) - .unwrap() - .outcome(), - WriteOutcome::ExactNotApplied - ); -} - -#[test] -fn wrong_company_profile_identity_or_field_fails_closed_or_mismatches() { - let mut registry = IdempotencyRegistry::default(); - let prepared = prepare( - "key-scope", - vec![create(REMOTE_ID, state("BRIDGE LEDGER", "10.00"), 1)], - &mut registry, - ) - .unwrap(); - let query = prepared.identity_query_digest().as_hex().to_owned(); - assert_eq!( - prepared - .clone() - .qualify_preflight(&export( - "00000000-0000-4000-8000-000000000099", - PROFILE, - &query, - "", - 0, - )) - .unwrap_err(), - QualificationError::InvalidReadback - ); - assert_eq!( - prepared - .clone() - .qualify_preflight(&export(COMPANY_GUID, PROFILE, HASH, "", 0)) - .unwrap_err(), - QualificationError::InvalidReadback - ); - assert_eq!( - prepared - .clone() - .qualify_preflight(&export( - COMPANY_GUID, - "bridge.tally.ledgers/1", - &query, - "", - 0, - )) - .unwrap_err(), - QualificationError::InvalidReadback - ); - let sent = prepared - .qualify_preflight(&export(COMPANY_GUID, PROFILE, &query, "", 0)) - .unwrap() - .record_dispatch_attempt("request-scope", &mut registry) - .unwrap(); - let awaiting = sent - .record_import_receipt(&receipt(1, 0, 0), &mut registry) - .unwrap(); - let changed = ledger(REMOTE_ID, "BRIDGE LEDGER", "10.01"); - assert_eq!( - awaiting - .verify_readback( - &export(COMPANY_GUID, PROFILE, &query, &changed, 1), - &mut registry, - ) - .unwrap() - .outcome(), - WriteOutcome::Mismatch - ); -} - -#[test] -fn invalid_decimal_gstin_limits_duplicates_noops_and_replays_are_rejected() { - assert_eq!( - LedgerState::new("BRIDGE", None, None, Some("NaN".to_owned())).unwrap_err(), - QualificationError::InvalidField("opening_balance") - ); - assert_eq!( - LedgerState::new("BRIDGE", None, Some("not-a-gstin".to_owned()), None).unwrap_err(), - QualificationError::InvalidField("party_gstin") - ); - let parentless = LedgerState::new("BRIDGE", None, None, None) - .expect("a parentless state remains usable as an alter snapshot"); - assert_eq!( - LedgerMutation::create(REMOTE_ID, parentless, lineage(1)).unwrap_err(), - QualificationError::CreateParentRequired - ); - let unchanged = state("BRIDGE", "0"); - assert_eq!( - LedgerMutation::alter(REMOTE_ID, unchanged.clone(), unchanged.clone(), lineage(1),) - .unwrap_err(), - QualificationError::NoOpMutation - ); - for (before, after, field) in [ - ( - LedgerState::new( - "BRIDGE", - Some("BRIDGE SYNTHETIC GROUP".to_owned()), - None, - None, - ) - .unwrap(), - LedgerState::new("BRIDGE", None, None, None).unwrap(), - "parent", - ), - ( - LedgerState::new("BRIDGE", None, Some("29ABCDE1234F1Z5".to_owned()), None).unwrap(), - LedgerState::new("BRIDGE", None, None, None).unwrap(), - "party_gstin", - ), - ( - LedgerState::new("BRIDGE", None, None, Some("0".to_owned())).unwrap(), - LedgerState::new("BRIDGE", None, None, None).unwrap(), - "opening_balance", - ), - ] { - assert_eq!( - LedgerMutation::alter(REMOTE_ID, before, after, lineage(1)).unwrap_err(), - QualificationError::UnsupportedFieldClear(field) - ); - } - let duplicate = create(REMOTE_ID, unchanged.clone(), 1); - let mut registry = IdempotencyRegistry::default(); - assert_eq!( - preview_ledger_import(&company(), &[duplicate.clone(), duplicate], "mapping-v1",) - .unwrap_err(), - QualificationError::DuplicateIdentity - ); - let too_many: Vec<_> = (0..=MAX_LEDGER_WRITE_BATCH) - .map(|index| create(format!("bridge-id-{index}"), unchanged.clone(), index)) - .collect(); - assert_eq!( - preview_ledger_import(&company(), &too_many, "mapping-v1").unwrap_err(), - QualificationError::InvalidBatchSize - ); - let mutation = create(REMOTE_ID, unchanged, 2); - prepare("key-replay", vec![mutation.clone()], &mut registry).unwrap(); - assert_eq!( - prepare("key-replay", vec![mutation], &mut registry).unwrap_err(), - QualificationError::DuplicateSubmission - ); -} - -#[test] -fn line_error_evidence_is_derived_and_never_debugs_raw_text() { - let first = bridge_tally_write::parse_import_receipt( - "00110PRIVATE-SYNTHETIC-SENTINEL-A", - ) - .unwrap(); - let second = bridge_tally_write::parse_import_receipt( - "00110PRIVATE-SYNTHETIC-SENTINEL-B", - ) - .unwrap(); - assert_ne!( - first.line_error_digests()[0].as_hex(), - second.line_error_digests()[0].as_hex() - ); - let debug = format!("{first:?}"); - assert!(!debug.contains("PRIVATE-SYNTHETIC")); - assert!(!debug.contains("SENTINEL")); -} - -#[test] -fn documented_direct_failure_receipt_retains_redacted_evidence_without_becoming_clean() { - let receipt = bridge_tally_write::parse_import_receipt( - "
0
000010PRIVATE-SYNTHETIC-FAILURE
", - ) - .expect("documented failure receipt remains auditable"); - assert_eq!( - receipt.application_status(), - bridge_tally_protocol::TallyImportApplicationStatus::Failure - ); - assert_eq!(receipt.counters().errors, 1); - assert_eq!(receipt.line_error_digests().len(), 1); - assert!(!receipt.exceptions_were_reported()); - let debug = format!("{receipt:?}"); - assert!(!debug.contains("PRIVATE-SYNTHETIC-FAILURE")); -} diff --git a/src-tauri/src/db/tally_incremental.rs b/src-tauri/src/db/tally_incremental.rs index 6545a71..abae891 100644 --- a/src-tauri/src/db/tally_incremental.rs +++ b/src-tauri/src/db/tally_incremental.rs @@ -1,40 +1,21 @@ -use bridge_tally_core::{CapabilityPackId, CapabilityState, EvidenceConfidence, TransportId}; -use bridge_tally_incremental::{ - plan_sync, ChangeIdentifierSemantics, IncrementalCapabilityObservation, IncrementalCheckpoint, - IncrementalPolicy, IncrementalScope, SyncPlan, -}; -use sha2::{Digest, Sha256}; use sqlx::Row; -use uuid::Uuid; use crate::db::tally_mirror::{MirrorError, TallyMirrorRepository}; -use crate::sync::snapshot::{SnapshotPhase, SqliteSnapshotStateStore}; -use crate::tally::company_source_identity; -/// Sealed authority receipt. Only the future protocol verifier in this module may construct one; -/// callers cannot turn a generic pack passport into observed incremental evidence. -#[derive(Debug, Clone)] -pub struct VerifiedIncrementalCanaryReceipt { - company_id: String, - capability_snapshot_id: String, - canary_contract_version: u16, - response_sha256: String, - observation: IncrementalCapabilityObservation, - observed_at_unix_ms: i64, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct StoredIncrementalCapability { - pub id: String, - pub observation: IncrementalCapabilityObservation, - pub observed_at_unix_ms: i64, -} - -#[derive(Debug, Clone, serde::Serialize)] -pub struct IncrementalReadiness { - pub scope_sha256: String, - pub plan: SyncPlan, -} +// This module previously also carried a full incremental-sync capability/checkpoint evidence +// pipeline (`IncrementalScope`, `plan_sync`, checkpoint establishment and invalidation, overlap +// deduplication, and their `tally_mirror` persistence methods), backed by the former +// `bridge-tally-incremental` crate. Folding that crate in and narrowing its visibility away from +// `pub` (2026-08) made the whole pipeline visible to `dead_code` for the first time, and it turned +// out to have zero callers anywhere in the binary outside its own tests: `incremental_readiness` +// is never invoked by any Tauri command (the only live entry point, +// `incremental_foundation_evidence` below, is a read-only count query that does not need it), and +// nothing ever calls `save_incremental_capability_observation` to populate the evidence it would +// read. It was deleted rather than kept behind `#[allow(dead_code)]`: it had no caller anywhere, +// including tests that exercised it for reasons other than testing it, so it met the "no caller +// anywhere" bar for removal rather than the "load-bearing but not yet wired" bar for keeping it. +// If incremental sync is revived, reintroduce the policy module fresh against the live call site +// that will actually invoke it, rather than resurrecting unreachable code. #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] pub struct IncrementalFoundationEvidence { @@ -46,31 +27,6 @@ pub struct IncrementalFoundationEvidence { pub fallback_warning_code: &'static str, } -#[derive(Debug, serde::Serialize)] -struct EstablishmentReceiptHashInput { - verifier_contract_version: i64, - receipt_id: String, - scope_sha256: String, - capability_observation_id: String, - proof_id: String, - proof_sha256: String, - batch_id: String, - snapshot_plan_sha256: String, - source_response_sha256: String, - coverage_manifest_sha256: String, - source_high_watermark_decimal: String, - max_observed_alter_id_decimal: Option, - source_record_count: i64, - accepted_record_count: i64, - deduplicated_record_count: i64, - numeric_alter_id_count: i64, - rejected_record_count: i64, - duplicate_identity_count: i64, - missing_identity_count: i64, - out_of_scope_record_count: i64, - created_at_unix_ms: i64, -} - impl TallyMirrorRepository { /// Read-only, count-only operator evidence. It cannot authorize or start an incremental read. pub async fn incremental_foundation_evidence( @@ -126,465 +82,15 @@ impl TallyMirrorRepository { fallback_warning_code: "incremental_execution_disabled_full_snapshot_required", }) } - - /// Persist only exact-profile evidence. This is intentionally not exposed as a Tauri command: - /// the future object-specific canary owns authority to call it. - pub async fn save_incremental_capability_observation( - &self, - receipt: VerifiedIncrementalCanaryReceipt, - ) -> Result { - validate_scope(&receipt.observation.scope)?; - if receipt.company_id.trim().is_empty() - || receipt.capability_snapshot_id.trim().is_empty() - || receipt.canary_contract_version == 0 - || !is_lower_sha256(&receipt.response_sha256) - || receipt.observed_at_unix_ms <= 0 - { - return Err(MirrorError::InvalidInput("incremental_authority")); - } - let pin = self.snapshot_source_pin(&receipt.company_id).await?; - let expected_lineage = format!("tally_xml_http:{}", pin.canonical_origin); - let expected_identity = company_source_identity(&expected_lineage, &pin.company_guid); - if receipt.observation.scope.source_lineage != expected_identity.bridge_source_lineage - || !receipt - .observation - .scope - .company_guid - .eq_ignore_ascii_case(&expected_identity.company_guid) - || receipt.observation.scope.company_fingerprint - != expected_identity.observed_fingerprint - { - return Err(MirrorError::InvalidInput("incremental_company_scope")); - } - if !self - .capability_snapshot_matches_plan( - &receipt.capability_snapshot_id, - &receipt.company_id, - receipt.observation.scope.capability_profile_version, - &receipt.observation.scope.product, - Some(&receipt.observation.scope.release), - Some(&receipt.observation.scope.mode), - ) - .await? - { - return Err(MirrorError::InvalidInput("incremental_capability_profile")); - } - - let scope_json = serde_json::to_string(&receipt.observation.scope)?; - let scope_sha256 = incremental_scope_sha256(&receipt.observation.scope)?; - let id = Uuid::new_v4().to_string(); - sqlx::query( - "INSERT INTO tally_incremental_capability_observations(\ - id, scope_sha256, scope_json, capability_snapshot_id, company_id, \ - verifier_contract_version, response_sha256, capability_state, confidence, \ - identifier_semantics, inclusive_lower_bound_observed, \ - explicit_source_high_watermark_observed, observed_at_unix_ms\ - ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)", - ) - .bind(&id) - .bind(scope_sha256) - .bind(scope_json) - .bind(receipt.capability_snapshot_id) - .bind(receipt.company_id) - .bind(i64::from(receipt.canary_contract_version)) - .bind(receipt.response_sha256) - .bind(capability_state_code(receipt.observation.state)) - .bind(confidence_code(receipt.observation.confidence)) - .bind(identifier_semantics_code( - receipt.observation.identifier_semantics, - )) - .bind(i64::from( - receipt.observation.inclusive_lower_bound_observed, - )) - .bind(i64::from( - receipt.observation.explicit_source_high_watermark_observed, - )) - .bind(receipt.observed_at_unix_ms) - .execute(&self.pool) - .await?; - Ok(StoredIncrementalCapability { - id, - observation: receipt.observation, - observed_at_unix_ms: receipt.observed_at_unix_ms, - }) - } - - pub async fn load_incremental_capability( - &self, - scope: &IncrementalScope, - ) -> Result, MirrorError> { - validate_scope(scope)?; - let row = sqlx::query( - "SELECT id, scope_json, capability_state, confidence, identifier_semantics, \ - inclusive_lower_bound_observed, explicit_source_high_watermark_observed, \ - observed_at_unix_ms \ - FROM tally_incremental_capability_observations WHERE scope_sha256 = ?1 \ - ORDER BY observed_at_unix_ms DESC, id DESC LIMIT 1", - ) - .bind(incremental_scope_sha256(scope)?) - .fetch_optional(&self.pool) - .await?; - let stored = row.map(decode_capability_row).transpose()?; - if stored - .as_ref() - .is_some_and(|stored| stored.observation.scope != *scope) - { - return Err(MirrorError::VerificationInvariant); - } - Ok(stored) - } - - pub async fn load_incremental_checkpoint( - &self, - scope: &IncrementalScope, - ) -> Result, MirrorError> { - validate_scope(scope)?; - let scope_sha256 = incremental_scope_sha256(scope)?; - let head_count = sqlx::query_scalar::<_, i64>( - "SELECT COUNT(*) FROM tally_incremental_checkpoint_heads WHERE scope_sha256 = ?1", - ) - .bind(&scope_sha256) - .fetch_one(&self.pool) - .await?; - if head_count == 0 { - return Ok(None); - } - if head_count != 1 { - return Err(MirrorError::VerificationInvariant); - } - let row = sqlx::query( - "SELECT head.scope_sha256, head.scope_json, head.high_watermark_decimal, \ - head.generation, head.state, head.established_at_unix_ms, \ - receipt.id AS receipt_id, receipt.scope_sha256 AS receipt_scope_sha256, \ - receipt.scope_json AS receipt_scope_json, \ - receipt.capability_observation_id, receipt.proof_id, receipt.proof_sha256, \ - receipt.batch_id, receipt.snapshot_plan_sha256, \ - receipt.source_response_sha256, receipt.coverage_manifest_sha256, \ - receipt.source_high_watermark_decimal, receipt.max_observed_alter_id_decimal, \ - receipt.source_record_count, receipt.accepted_record_count, \ - receipt.deduplicated_record_count, receipt.numeric_alter_id_count, \ - receipt.rejected_record_count, receipt.duplicate_identity_count, \ - receipt.missing_identity_count, receipt.out_of_scope_record_count, \ - receipt.verifier_contract_version AS receipt_verifier_contract_version, \ - receipt.receipt_sha256, receipt.created_at_unix_ms, proof.run_id, \ - CASE WHEN \ - head.establishment_receipt_id = receipt.id AND \ - head.scope_sha256 = receipt.scope_sha256 AND \ - head.scope_json = receipt.scope_json AND \ - head.high_watermark_decimal = receipt.source_high_watermark_decimal AND \ - head.generation = 1 AND head.state = 'active' AND \ - head.established_at_unix_ms = receipt.created_at_unix_ms AND \ - capability.id = receipt.capability_observation_id AND \ - capability.scope_sha256 = receipt.scope_sha256 AND \ - capability.scope_json = receipt.scope_json AND \ - capability.company_id = proof.company_id AND \ - capability.capability_snapshot_id = proof.capability_snapshot_id AND \ - capability.verifier_contract_version > 0 AND \ - length(capability.response_sha256) = 64 AND \ - capability.response_sha256 NOT GLOB '*[^0-9a-f]*' AND \ - capability.capability_state = 'supported' AND capability.confidence = 'observed' AND \ - capability.identifier_semantics = 'monotonic_per_object' AND \ - capability.inclusive_lower_bound_observed = 1 AND \ - capability.explicit_source_high_watermark_observed = 1 AND \ - proof.id = receipt.proof_id AND proof.entry_sha256 = receipt.proof_sha256 AND \ - proof.batch_id = receipt.batch_id AND proof.outcome = 'completed' AND \ - proof.verification_state = 'verified' AND proof.completed_at_unix_ms IS NOT NULL AND \ - proof.snapshot_sha256 IS NOT NULL AND proof.gap_codes_json = '[]' AND \ - proof.warning_codes_json = '[]' AND proof.rejected_records = 0 AND \ - proof.accepted_records = receipt.accepted_record_count AND \ - batch.id = receipt.batch_id AND batch.run_id = proof.run_id AND \ - batch.capability_snapshot_id = proof.capability_snapshot_id AND \ - batch.company_id = proof.company_id AND batch.pack_id = proof.pack_id AND \ - batch.pack_id = json_extract(receipt.scope_json, '$.pack') AND \ - batch.pack_schema_major = json_extract(receipt.scope_json, '$.pack_schema_version.major') AND \ - batch.pack_schema_minor = json_extract(receipt.scope_json, '$.pack_schema_version.minor') AND \ - batch.source_transport = json_extract(receipt.scope_json, '$.transport') AND \ - batch.source_release = json_extract(receipt.scope_json, '$.release') AND \ - batch.state = 'verified' AND batch.snapshot_sha256 = proof.snapshot_sha256 AND \ - batch.accepted_records = receipt.accepted_record_count AND batch.rejected_records = 0 AND \ - snapshot.id = proof.capability_snapshot_id AND \ - snapshot.profile_version = json_extract(receipt.scope_json, '$.capability_profile_version') AND \ - snapshot.product = json_extract(receipt.scope_json, '$.product') AND \ - snapshot.release = json_extract(receipt.scope_json, '$.release') AND \ - snapshot.mode = json_extract(receipt.scope_json, '$.mode') AND \ - snapshot.mode_confidence = 'observed' AND company.id = proof.company_id AND \ - company.endpoint_id = snapshot.endpoint_id AND \ - company.company_guid = json_extract(receipt.scope_json, '$.company_guid') COLLATE NOCASE AND \ - company.identity_confidence = 'observed' AND durable.run_id = proof.run_id AND \ - durable.row_sha256 IS NOT NULL AND durable.plan_sha256 = receipt.snapshot_plan_sha256 AND \ - json_extract(durable.state_json, '$.plan_sha256') = receipt.snapshot_plan_sha256 AND \ - json_extract(durable.state_json, '$.batch_id') = receipt.batch_id AND \ - json_extract(durable.state_json, '$.progress.phase') = 'completed' AND \ - json_extract(durable.state_json, '$.commit_receipt.proof_id') = receipt.proof_id AND \ - json_extract(durable.state_json, '$.commit_receipt.proof_sha256') = receipt.proof_sha256 AND \ - EXISTS (SELECT 1 FROM json_each(durable.state_json, '$.plan.windows') AS window \ - WHERE json_extract(window.value, '$.query_profile') = json_extract(receipt.scope_json, '$.query_profile') \ - AND json_extract(window.value, '$.filters_sha256') = json_extract(receipt.scope_json, '$.filters_sha256')) \ - THEN 1 ELSE 0 END AS relationally_valid \ - FROM tally_incremental_checkpoint_heads AS head \ - JOIN tally_incremental_establishment_receipts AS receipt \ - ON receipt.id = head.establishment_receipt_id \ - JOIN tally_incremental_capability_observations AS capability \ - ON capability.id = receipt.capability_observation_id \ - JOIN tally_proof_ledger AS proof ON proof.id = receipt.proof_id \ - JOIN tally_observation_batches AS batch ON batch.id = receipt.batch_id \ - JOIN tally_capability_snapshots AS snapshot ON snapshot.id = proof.capability_snapshot_id \ - JOIN tally_companies AS company ON company.id = proof.company_id \ - JOIN tally_snapshot_run_states AS durable ON durable.run_id = proof.run_id \ - WHERE head.scope_sha256 = ?1", - ) - .bind(&scope_sha256) - .fetch_optional(&self.pool) - .await? - .ok_or(MirrorError::VerificationInvariant)?; - if row.try_get::("relationally_valid")? != 1 { - return Err(MirrorError::VerificationInvariant); - } - let stored_scope: IncrementalScope = - serde_json::from_str(&row.try_get::("scope_json")?)?; - let receipt_scope: IncrementalScope = - serde_json::from_str(&row.try_get::("receipt_scope_json")?)?; - if stored_scope != *scope - || receipt_scope != *scope - || row.try_get::("scope_sha256")? != scope_sha256 - || row.try_get::("receipt_scope_sha256")? != scope_sha256 - { - return Err(MirrorError::VerificationInvariant); - } - let receipt = decode_establishment_receipt_hash_input(&row)?; - if sha256_establishment_receipt(&receipt)? != row.try_get::("receipt_sha256")? { - return Err(MirrorError::VerificationInvariant); - } - let batch_id: String = row.try_get("batch_id")?; - let run_id: String = row.try_get("run_id")?; - let proof_id: String = row.try_get("proof_id")?; - let proof_sha256: String = row.try_get("proof_sha256")?; - let durable = SqliteSnapshotStateStore::new(self.pool_clone()) - .load_by_run_id(&run_id) - .await - .map_err(|_| MirrorError::VerificationInvariant)? - .ok_or(MirrorError::VerificationInvariant)?; - let durable_receipt = durable - .commit_receipt - .as_ref() - .ok_or(MirrorError::VerificationInvariant)?; - if !durable.row_integrity_bound - || durable.progress.phase != SnapshotPhase::Completed - || durable.plan_sha256 != row.try_get::("snapshot_plan_sha256")? - || durable.batch_id.as_deref() != Some(batch_id.as_str()) - || durable_receipt.proof_id.as_deref() != Some(proof_id.as_str()) - || durable_receipt.proof_sha256.as_deref() != Some(proof_sha256.as_str()) - || !durable_receipt.checkpoint_advanced - { - return Err(MirrorError::VerificationInvariant); - } - let generic_proof = self - .historical_commit_receipt_for_batch(&batch_id, &run_id) - .await?; - if generic_proof.proof_id != proof_id || generic_proof.proof_sha256 != proof_sha256 { - return Err(MirrorError::VerificationInvariant); - } - let high_watermark_decimal: String = row.try_get("high_watermark_decimal")?; - let high_watermark = high_watermark_decimal - .parse::() - .map_err(|_| MirrorError::VerificationInvariant)?; - Ok(Some(IncrementalCheckpoint { - scope: stored_scope, - high_watermark, - established_by_verified_full_snapshot: true, - established_by_proof_sha256: proof_sha256.clone(), - last_transition_proof_sha256: proof_sha256, - last_identity_sweep_unix_ms: row.try_get("established_at_unix_ms")?, - invalidated_reason: None, - })) - } - - /// Current runtime entry point: it can prove eligibility, but it never starts a delta read. - /// Missing evidence produces the portable policy's explicit full-snapshot warning. - pub async fn incremental_readiness( - &self, - scope: &IncrementalScope, - policy: IncrementalPolicy, - now_unix_ms: i64, - ) -> Result { - let capability = self.load_incremental_capability(scope).await?; - let checkpoint = self.load_incremental_checkpoint(scope).await?; - Ok(IncrementalReadiness { - scope_sha256: incremental_scope_sha256(scope)?, - plan: plan_sync( - policy, - scope, - capability.as_ref().map(|stored| &stored.observation), - checkpoint.as_ref(), - now_unix_ms, - ), - }) - } -} - -pub fn incremental_scope_sha256(scope: &IncrementalScope) -> Result { - validate_scope(scope)?; - let bytes = serde_json::to_vec(scope)?; - let mut digest = Sha256::new(); - digest.update(b"bridge-tally-incremental-scope-v1\0"); - digest.update(bytes); - Ok(hex_digest(digest.finalize())) -} - -fn decode_establishment_receipt_hash_input( - row: &sqlx::sqlite::SqliteRow, -) -> Result { - Ok(EstablishmentReceiptHashInput { - verifier_contract_version: row.try_get("receipt_verifier_contract_version")?, - receipt_id: row.try_get("receipt_id")?, - scope_sha256: row.try_get("receipt_scope_sha256")?, - capability_observation_id: row.try_get("capability_observation_id")?, - proof_id: row.try_get("proof_id")?, - proof_sha256: row.try_get("proof_sha256")?, - batch_id: row.try_get("batch_id")?, - snapshot_plan_sha256: row.try_get("snapshot_plan_sha256")?, - source_response_sha256: row.try_get("source_response_sha256")?, - coverage_manifest_sha256: row.try_get("coverage_manifest_sha256")?, - source_high_watermark_decimal: row.try_get("source_high_watermark_decimal")?, - max_observed_alter_id_decimal: row.try_get("max_observed_alter_id_decimal")?, - source_record_count: row.try_get("source_record_count")?, - accepted_record_count: row.try_get("accepted_record_count")?, - deduplicated_record_count: row.try_get("deduplicated_record_count")?, - numeric_alter_id_count: row.try_get("numeric_alter_id_count")?, - rejected_record_count: row.try_get("rejected_record_count")?, - duplicate_identity_count: row.try_get("duplicate_identity_count")?, - missing_identity_count: row.try_get("missing_identity_count")?, - out_of_scope_record_count: row.try_get("out_of_scope_record_count")?, - created_at_unix_ms: row.try_get("created_at_unix_ms")?, - }) -} - -fn sha256_establishment_receipt( - receipt: &EstablishmentReceiptHashInput, -) -> Result { - let mut digest = Sha256::new(); - digest.update(b"bridge-tally-incremental-establishment-v1\0"); - digest.update(serde_json::to_vec(receipt)?); - Ok(hex_digest(digest.finalize())) -} - -fn hex_digest(bytes: impl AsRef<[u8]>) -> String { - bytes - .as_ref() - .iter() - .map(|byte| format!("{byte:02x}")) - .collect() -} - -fn validate_scope(scope: &IncrementalScope) -> Result<(), MirrorError> { - if !scope.is_exact() - || scope.pack != CapabilityPackId::CoreAccounting - || scope.transport != TransportId::XmlHttp - { - return Err(MirrorError::InvalidInput("incremental_scope")); - } - Ok(()) -} - -fn is_lower_sha256(value: &str) -> bool { - value.len() == 64 - && value - .bytes() - .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) -} - -fn decode_capability_row( - row: sqlx::sqlite::SqliteRow, -) -> Result { - let scope: IncrementalScope = serde_json::from_str(&row.try_get::("scope_json")?)?; - validate_scope(&scope)?; - Ok(StoredIncrementalCapability { - id: row.try_get("id")?, - observation: IncrementalCapabilityObservation { - scope, - state: parse_capability_state(&row.try_get::("capability_state")?)?, - confidence: parse_confidence(&row.try_get::("confidence")?)?, - identifier_semantics: parse_identifier_semantics( - &row.try_get::("identifier_semantics")?, - )?, - inclusive_lower_bound_observed: row - .try_get::("inclusive_lower_bound_observed")? - == 1, - explicit_source_high_watermark_observed: row - .try_get::("explicit_source_high_watermark_observed")? - == 1, - }, - observed_at_unix_ms: row.try_get("observed_at_unix_ms")?, - }) -} - -fn capability_state_code(value: CapabilityState) -> &'static str { - match value { - CapabilityState::Supported => "supported", - CapabilityState::Unsupported => "unsupported", - CapabilityState::Unknown => "unknown", - CapabilityState::NotConfigured => "not_configured", - } -} - -fn parse_capability_state(value: &str) -> Result { - match value { - "supported" => Ok(CapabilityState::Supported), - "unsupported" => Ok(CapabilityState::Unsupported), - "unknown" => Ok(CapabilityState::Unknown), - "not_configured" => Ok(CapabilityState::NotConfigured), - _ => Err(MirrorError::VerificationInvariant), - } -} - -fn confidence_code(value: EvidenceConfidence) -> &'static str { - match value { - EvidenceConfidence::Documented => "documented", - EvidenceConfidence::Observed => "observed", - EvidenceConfidence::Inferred => "inferred", - EvidenceConfidence::Unknown => "unknown", - } -} - -fn parse_confidence(value: &str) -> Result { - match value { - "documented" => Ok(EvidenceConfidence::Documented), - "observed" => Ok(EvidenceConfidence::Observed), - "inferred" => Ok(EvidenceConfidence::Inferred), - "unknown" => Ok(EvidenceConfidence::Unknown), - _ => Err(MirrorError::VerificationInvariant), - } -} - -fn identifier_semantics_code(value: ChangeIdentifierSemantics) -> &'static str { - match value { - ChangeIdentifierSemantics::MonotonicPerObject => "monotonic_per_object", - ChangeIdentifierSemantics::Unknown => "unknown", - } -} - -fn parse_identifier_semantics(value: &str) -> Result { - match value { - "monotonic_per_object" => Ok(ChangeIdentifierSemantics::MonotonicPerObject), - "unknown" => Ok(ChangeIdentifierSemantics::Unknown), - _ => Err(MirrorError::VerificationInvariant), - } } #[cfg(test)] mod tests { - use bridge_tally_core::{CapabilityPackId, PackSchemaVersion, TransportId}; use sqlx::sqlite::SqlitePoolOptions; use super::*; - use crate::db::tally_mirror::{ - BeginBatchInput, CapabilityItemInput, CapabilityKind, CapabilitySnapshotInput, - CapabilityState as MirrorCapabilityState, CompanyInput, Confidence, RunOutcome, - SourceIdentityInput, VerificationState as MirrorVerificationState, - }; - use crate::sync::reconciliation::{CommitBatchInput, CommitBatchParts}; - async fn setup() -> (TallyMirrorRepository, String, String, IncrementalScope) { + async fn empty_repository() -> TallyMirrorRepository { let pool = SqlitePoolOptions::new() .max_connections(1) .after_connect(|connection, _| { @@ -603,298 +109,36 @@ mod tests { .migrate() .await .expect("migrate synthetic mirror"); - let snapshot = repository - .save_capability_snapshot(CapabilitySnapshotInput { - canonical_origin: "http://127.0.0.1:9000".to_string(), - observed_at_unix_ms: 1_000, - profile_version: 1, - product: "TallyPrime".to_string(), - release: Some("7.0".to_string()), - mode: Some("Education".to_string()), - mode_confidence: Confidence::Observed, - items: vec![ - CapabilityItemInput { - kind: CapabilityKind::Transport, - key: "xml_http".to_string(), - state: MirrorCapabilityState::Supported, - confidence: Confidence::Observed, - safe_reason_code: None, - }, - CapabilityItemInput { - kind: CapabilityKind::Pack, - key: "core_accounting".to_string(), - state: MirrorCapabilityState::Supported, - confidence: Confidence::Observed, - safe_reason_code: None, - }, - ], - }) - .await - .expect("persist synthetic capability profile"); - let company_guid = "synthetic-company-guid"; - let company = repository - .upsert_company(CompanyInput { - endpoint_id: snapshot.endpoint_id, - display_name: "Synthetic Company".to_string(), - identity: SourceIdentityInput { - guid: Some(company_guid.to_string()), - confidence: Some(Confidence::Observed), - ..Default::default() - }, - observed_at_unix_ms: 1_000, - }) - .await - .expect("persist synthetic company"); - let lineage = "tally_xml_http:http://127.0.0.1:9000"; - let identity = company_source_identity(lineage, company_guid); - let scope = IncrementalScope { - source_lineage: identity.bridge_source_lineage, - company_guid: identity.company_guid, - company_fingerprint: identity.observed_fingerprint, - object_type: "voucher".to_string(), - capability_profile_version: 1, - product: "TallyPrime".to_string(), - release: "7.0".to_string(), - mode: "Education".to_string(), - transport: TransportId::XmlHttp, - pack: CapabilityPackId::CoreAccounting, - pack_schema_version: PackSchemaVersion { major: 1, minor: 0 }, - query_profile: "core_voucher_incremental_v1".to_string(), - filters_sha256: "a".repeat(64), - date_window_policy: "change_id_overlap_v1".to_string(), - }; - (repository, snapshot.id, company.id, scope) + repository } #[tokio::test] - async fn exact_observation_is_immutable_and_still_requires_verified_full_checkpoint() { - let (repository, snapshot_id, company_id, scope) = setup().await; - let generic_only = repository - .incremental_readiness(&scope, IncrementalPolicy::default(), 1_500) - .await - .expect("generic capability passport is inspectable"); - assert!(matches!( - generic_only.plan, - SyncPlan::FullSnapshot { - reason: bridge_tally_incremental::FullSnapshotReason::CapabilityNotObserved, - .. - } - )); - let stored = repository - .save_incremental_capability_observation(VerifiedIncrementalCanaryReceipt { - company_id: company_id.clone(), - capability_snapshot_id: snapshot_id, - canary_contract_version: 1, - response_sha256: "1".repeat(64), - observation: IncrementalCapabilityObservation { - scope: scope.clone(), - state: CapabilityState::Supported, - confidence: EvidenceConfidence::Observed, - identifier_semantics: ChangeIdentifierSemantics::MonotonicPerObject, - inclusive_lower_bound_observed: true, - explicit_source_high_watermark_observed: true, - }, - observed_at_unix_ms: 2_000, - }) - .await - .expect("persist exact synthetic incremental evidence"); - let loaded = repository - .load_incremental_capability(&scope) - .await - .expect("load exact capability") - .expect("capability exists"); - assert_eq!(loaded, stored); - let foundation = repository - .incremental_foundation_evidence(&company_id) - .await - .expect("read count-only incremental evidence"); - assert!(!foundation.execution_enabled); - assert_eq!(foundation.affirmative_exact_capability_receipts, 1); - assert_eq!(foundation.establishment_receipts, 0); - assert_eq!(foundation.active_checkpoint_heads, 0); - assert_eq!(foundation.state, "verified_establishment_missing"); - let readiness = repository - .incremental_readiness(&scope, IncrementalPolicy::default(), 3_000) - .await - .expect("plan readiness"); + async fn foundation_evidence_rejects_invalid_company_id_and_is_honest_about_absent_evidence() { + let repository = empty_repository().await; + assert!(matches!( - readiness.plan, - SyncPlan::FullSnapshot { - reason: bridge_tally_incremental::FullSnapshotReason::NoVerifiedCheckpoint, - .. - } + repository.incremental_foundation_evidence("").await, + Err(MirrorError::InvalidInput("company_id")) )); - let mutation = sqlx::query( - "UPDATE tally_incremental_capability_observations SET confidence = 'unknown' \ - WHERE id = ?1", - ) - .bind(stored.id) - .execute(&repository.pool) - .await; - assert!(mutation.is_err(), "capability evidence must be immutable"); - } - - #[tokio::test] - async fn company_or_filter_scope_drift_cannot_reuse_incremental_authority() { - let (repository, snapshot_id, company_id, scope) = setup().await; - let observation = |scope| IncrementalCapabilityObservation { - scope, - state: CapabilityState::Supported, - confidence: EvidenceConfidence::Observed, - identifier_semantics: ChangeIdentifierSemantics::MonotonicPerObject, - inclusive_lower_bound_observed: true, - explicit_source_high_watermark_observed: true, - }; - let original = repository - .save_incremental_capability_observation(VerifiedIncrementalCanaryReceipt { - company_id: company_id.clone(), - capability_snapshot_id: snapshot_id.clone(), - canary_contract_version: 1, - response_sha256: "1".repeat(64), - observation: observation(scope.clone()), - observed_at_unix_ms: 2_000, - }) - .await - .expect("persist original exact scope"); - let mut changed_filter = scope.clone(); - changed_filter.filters_sha256 = "b".repeat(64); - let changed = repository - .save_incremental_capability_observation(VerifiedIncrementalCanaryReceipt { - company_id: company_id.clone(), - capability_snapshot_id: snapshot_id.clone(), - canary_contract_version: 1, - response_sha256: "2".repeat(64), - observation: observation(changed_filter), - observed_at_unix_ms: 2_001, - }) - .await - .expect("a new exact filter scope gets separate evidence"); - assert_ne!( - incremental_scope_sha256(&original.observation.scope).unwrap(), - incremental_scope_sha256(&changed.observation.scope).unwrap() - ); - - let mut wrong_company = scope; - wrong_company.company_guid = "different-guid".to_string(); assert!(matches!( repository - .save_incremental_capability_observation(VerifiedIncrementalCanaryReceipt { - company_id, - capability_snapshot_id: snapshot_id, - canary_contract_version: 1, - response_sha256: "3".repeat(64), - observation: observation(wrong_company), - observed_at_unix_ms: 2_001, - }) + .incremental_foundation_evidence(&"x".repeat(129)) .await, - Err(MirrorError::InvalidInput("incremental_company_scope")) + Err(MirrorError::InvalidInput("company_id")) )); - } - - #[tokio::test] - async fn generic_pack_proof_cannot_self_attest_incremental_establishment() { - let (repository, snapshot_id, company_id, scope) = setup().await; - let capability = repository - .save_incremental_capability_observation(VerifiedIncrementalCanaryReceipt { - company_id: company_id.clone(), - capability_snapshot_id: snapshot_id.clone(), - canary_contract_version: 1, - response_sha256: "1".repeat(64), - observation: IncrementalCapabilityObservation { - scope: scope.clone(), - state: CapabilityState::Supported, - confidence: EvidenceConfidence::Observed, - identifier_semantics: ChangeIdentifierSemantics::MonotonicPerObject, - inclusive_lower_bound_observed: true, - explicit_source_high_watermark_observed: true, - }, - observed_at_unix_ms: 2_000, - }) - .await - .expect("persist sealed canary evidence"); - let batch_id = repository - .begin_batch(BeginBatchInput { - run_id: "incremental-foundation-proof".to_string(), - capability_snapshot_id: snapshot_id, - company_id: company_id.clone(), - pack_id: "core_accounting".to_string(), - pack_schema_major: 1, - pack_schema_minor: 0, - source_transport: "xml_http".to_string(), - source_release: Some("7.0".to_string()), - requested_from_yyyymmdd: None, - requested_to_yyyymmdd: None, - started_at_unix_ms: 2_100, - }) - .await - .expect("begin synthetic verified full snapshot"); - let proof = repository - .commit_batch(CommitBatchInput::test_only(CommitBatchParts { - batch_id: batch_id.clone(), - proof_contract_version: 1, - outcome: RunOutcome::Completed, - verification: MirrorVerificationState::Verified, - completed_at_unix_ms: 3_000, - record_counts_sha256: None, - snapshot_sha256: Some("2".repeat(64)), - expected_checkpoint_before: None, - checkpoint_after: Some("42".to_string()), - freshness_target_seconds: 60, - gap_codes: vec![], - warning_codes: vec![], - })) - .await - .expect("commit proof-bound full snapshot"); - let scope_sha256 = incremental_scope_sha256(&scope).unwrap(); - let scope_json = serde_json::to_string(&scope).unwrap(); - for watermark in ["01", "18446744073709551616", "42"] { - let insert = sqlx::query( - "INSERT INTO tally_incremental_establishment_receipts(\ - id, scope_sha256, scope_json, capability_observation_id, proof_id, \ - proof_sha256, batch_id, snapshot_plan_sha256, source_response_sha256, \ - coverage_manifest_sha256, source_high_watermark_decimal, \ - max_observed_alter_id_decimal, source_record_count, accepted_record_count, \ - deduplicated_record_count, numeric_alter_id_count, rejected_record_count, \ - duplicate_identity_count, missing_identity_count, out_of_scope_record_count, \ - verifier_contract_version, receipt_sha256, created_at_unix_ms\ - ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, NULL, \ - 0, 0, 0, 0, 0, 0, 0, 0, 1, ?12, 3000)", - ) - .bind(Uuid::new_v4().to_string()) - .bind(&scope_sha256) - .bind(&scope_json) - .bind(&capability.id) - .bind(&proof.proof_id) - .bind(&proof.proof_sha256) - .bind(&batch_id) - .bind("3".repeat(64)) - .bind("4".repeat(64)) - .bind("5".repeat(64)) - .bind(watermark) - .bind("6".repeat(64)) - .execute(&repository.pool) - .await; - assert!( - insert.is_err(), - "generic proof and caller hashes must never establish authority" - ); - } - assert!(repository - .load_incremental_checkpoint(&scope) + let foundation = repository + .incremental_foundation_evidence("no-such-company") .await - .expect("missing authority falls back safely") - .is_none()); - assert!(sqlx::query( - "INSERT INTO tally_incremental_checkpoint_heads(\ - scope_sha256, scope_json, establishment_receipt_id, high_watermark_decimal, \ - generation, state, established_at_unix_ms\ - ) VALUES (?1, ?2, 'missing-receipt', '42', 1, 'active', 3000)", - ) - .bind(scope_sha256) - .bind(scope_json) - .execute(&repository.pool) - .await - .is_err()); + .expect("read count-only evidence for an unknown company"); + assert!(!foundation.execution_enabled); + assert_eq!(foundation.affirmative_exact_capability_receipts, 0); + assert_eq!(foundation.establishment_receipts, 0); + assert_eq!(foundation.active_checkpoint_heads, 0); + assert_eq!(foundation.state, "exact_capability_not_observed"); + assert_eq!( + foundation.fallback_warning_code, + "incremental_execution_disabled_full_snapshot_required" + ); } } diff --git a/src-tauri/src/tally/canary_dispatch_admission.rs b/src-tauri/src/tally/canary_dispatch_admission.rs deleted file mode 100644 index ec6e3a8..0000000 --- a/src-tauri/src/tally/canary_dispatch_admission.rs +++ /dev/null @@ -1,122 +0,0 @@ -//! Disabled application-level admission for the sealed fixture-canary runtime. -//! -//! This module is deliberately not a Tauri command and contains no endpoint, -//! runtime, transport, import, payload-construction, persistence, or retry -//! capability. It only checks that an already prepared synthetic fixture is -//! tied to active, exact durable preflight evidence and an active reviewed -//! fixture enrollment. A future dispatch boundary must remain separately -//! reviewed and revalidate every condition immediately before any send. - -use crate::{ - db::tally_mirror::{ - ActiveWriteCanaryPreflightEvidenceInput, TallyMirrorRepository, - WriteCanaryPreflightEvidenceRef, WriteFixtureEnrollmentStatus, - }, - tally::{ - canary_preflight::{ - verify_sealed_canary_preflight_evidence, SealedCanaryPreflightEvidenceGateRequest, - }, - write_sandbox::PreparedFixtureCanary, - }, -}; -use anyhow::{bail, Result}; -use bridge_tally_protocol::xml_read_profiles::{ - ValidatedCanaryLedgerName, ValidatedIdentityQuerySha256, -}; - -/// Local-only inputs for the disabled admission seam. The caller supplies -/// immutable evidence previously committed by the sealed preflight; this type -/// deliberately has no Tally configuration, company display data, XML, or -/// dispatch/payload material. -pub(crate) struct SealedCanaryRuntimeAdmissionRequest { - pub company_id: String, - pub ledger_name: ValidatedCanaryLedgerName, - pub identity_query_sha256: ValidatedIdentityQuerySha256, - pub evidence: ActiveWriteCanaryPreflightEvidenceInput, -} - -/// Read-only admission evidence. This is not a dispatch token and cannot be -/// used to construct, send, repeat, or record a Tally import. -pub(crate) struct SealedCanaryRuntimeAdmission { - pub preflight_evidence: WriteCanaryPreflightEvidenceRef, -} - -fn has_active_reviewed_fixture_enrollment(status: &WriteFixtureEnrollmentStatus) -> bool { - status.fixture_state == "active" && status.candidate_gate == "enrolled" -} - -/// Confirms active reviewed enrollment and exact durable preflight evidence -/// for the already prepared synthetic fixture. This performs database reads -/// only; the future runtime-dispatch path must repeat its own final checks -/// before it is allowed to contact Tally. -pub(crate) async fn admit_sealed_canary_runtime_dispatch( - repository: &TallyMirrorRepository, - request: SealedCanaryRuntimeAdmissionRequest, - prepared: &PreparedFixtureCanary, -) -> Result { - if request.company_id != request.evidence.binding.company_id { - bail!("sealed_canary_runtime_admission_company_mismatch"); - } - - let enrollment = repository - .write_fixture_enrollment_status(&request.company_id) - .await?; - if !has_active_reviewed_fixture_enrollment(&enrollment) { - bail!("sealed_canary_runtime_admission_not_enrolled"); - } - - let evidence_id = request.evidence.evidence_id.clone(); - let attempt_id = request.evidence.attempt_id.clone(); - let preflight_evidence = verify_sealed_canary_preflight_evidence( - repository, - SealedCanaryPreflightEvidenceGateRequest { - ledger_name: request.ledger_name, - identity_query_sha256: request.identity_query_sha256, - evidence: request.evidence, - }, - prepared, - ) - .await?; - if preflight_evidence.id != evidence_id || preflight_evidence.attempt_id != attempt_id { - bail!("sealed_canary_runtime_admission_evidence_mismatch"); - } - - Ok(SealedCanaryRuntimeAdmission { preflight_evidence }) -} -#[cfg(test)] -mod tests { - use super::has_active_reviewed_fixture_enrollment; - use crate::db::tally_mirror::WriteFixtureEnrollmentStatus; - - fn status( - fixture_state: &'static str, - candidate_gate: &'static str, - ) -> WriteFixtureEnrollmentStatus { - WriteFixtureEnrollmentStatus { - fixture_state, - enrolled_at_unix_ms: None, - revoked_at_unix_ms: None, - candidate_gate, - write_capability: "unknown", - } - } - - #[test] - fn admission_requires_an_exact_active_reviewed_fixture_enrollment() { - assert!(has_active_reviewed_fixture_enrollment(&status( - "active", "enrolled" - ))); - assert!(!has_active_reviewed_fixture_enrollment(&status( - "not_enrolled", - "not_enrolled" - ))); - assert!(!has_active_reviewed_fixture_enrollment(&status( - "revoked", - "not_enrolled" - ))); - assert!(!has_active_reviewed_fixture_enrollment(&status( - "active", - "not_enrolled" - ))); - } -} diff --git a/src-tauri/src/tally/canary_preflight.rs b/src-tauri/src/tally/canary_preflight.rs deleted file mode 100644 index c162d79..0000000 --- a/src-tauri/src/tally/canary_preflight.rs +++ /dev/null @@ -1,377 +0,0 @@ -//! Sealed orchestration for the first synthetic write canary. -//! -//! Ordinary builds stop after digest-only preflight evidence. The separately -//! disabled runtime feature adds one closed, one-send coordinator with no UI -//! command, generic payload API, or retry behavior. - -use crate::tally::write_sandbox::{ - verify_fixture_canary_preflight, FixtureCanaryPostDispatchObservation, - FixtureCanaryPreflightEvidence, PreparedFixtureCanary, FIXTURE_CANARY_LEDGER_NAME, -}; -use anyhow::{bail, Result}; -use bridge_tally_protocol::xml_read_profiles::{ - ValidatedCanaryLedgerName, ValidatedCompanyName, ValidatedIdentityQuerySha256, -}; -use chrono::Utc; -use sha2::{Digest, Sha256}; - -#[cfg(feature = "fixture-canary-runtime-dispatch")] -use crate::db::tally_mirror::ActiveWriteCanaryDispatchAttemptInput; - -use crate::{ - db::tally_mirror::{ - ActiveWriteCanaryPayloadBindingInput, ActiveWriteCanaryPreflightEvidenceInput, - BeginWriteCanaryDispatchInput, BeginWriteCanaryPreflightInput, TallyMirrorRepository, - WriteCanaryDispatchAttemptRef, WriteCanaryFinalVerdictInput, WriteCanaryFinalVerdictRef, - WriteCanaryPreflightEvidenceInput, WriteCanaryPreflightEvidenceRef, - }, - tally::{connection::canonical_loopback_origin, TallyConfig, TallyRuntime}, -}; - -fn sealed_target_binding_sha256( - config: &TallyConfig, - expected_company_guid: &str, -) -> Result<(String, String)> { - let endpoint = canonical_loopback_origin(config)?; - let endpoint_sha256 = sha256_hex(endpoint.as_bytes()); - let company_sha256 = sha256_hex(expected_company_guid.to_ascii_lowercase().as_bytes()); - Ok((endpoint_sha256, company_sha256)) -} - -fn sha256_hex(value: &[u8]) -> String { - Sha256::digest(value) - .iter() - .map(|byte| format!("{byte:02x}")) - .collect() -} - -/// Every value required for one sealed, serial preflight read. This stays -/// crate-private until a separately reviewed command layer exposes it. -pub(crate) struct SealedCanaryPreflightRequest { - pub config: TallyConfig, - pub company: ValidatedCompanyName, - pub ledger_name: ValidatedCanaryLedgerName, - pub identity_query_sha256: ValidatedIdentityQuerySha256, - pub expected_company_guid: String, - pub binding: BeginWriteCanaryPreflightInput, -} - -/// Exact, digest-only evidence required before a future canary import may be -/// considered. It intentionally contains no Tally configuration, transport, -/// import payload, retry policy, or dispatch capability. -pub(crate) struct SealedCanaryPreflightEvidenceGateRequest { - pub ledger_name: ValidatedCanaryLedgerName, - pub identity_query_sha256: ValidatedIdentityQuerySha256, - pub evidence: ActiveWriteCanaryPreflightEvidenceInput, -} - -/// The private handoff from an exact preflight read to the feature-gated -/// runtime sequence. It retains the complete durable evidence commitment so a -/// later local admission can verify it exactly; it has no import payload, -/// transport handle, or raw Tally response. -pub(crate) struct SealedCanaryPreflightCompletion { - pub evidence: WriteCanaryPreflightEvidenceRef, - pub active_evidence: ActiveWriteCanaryPreflightEvidenceInput, -} - -/// The final local, no-send claim before a future import coordinator is even -/// considered. This remains deliberately transport-free. -pub(crate) struct SealedCanaryDispatchClaimRequest { - pub ledger_name: ValidatedCanaryLedgerName, - pub identity_query_sha256: ValidatedIdentityQuerySha256, - pub dispatch: BeginWriteCanaryDispatchInput, -} - -/// A final local, digest-only record request. The caller supplies only the -/// durable commitments plus an already parsed semantic observation; it has no -/// Tally configuration, raw XML, payload, or transport capability. -pub(crate) struct SealedCanaryFinalVerdictRequest { - pub ledger_name: ValidatedCanaryLedgerName, - pub identity_query_sha256: ValidatedIdentityQuerySha256, - pub verdict: WriteCanaryFinalVerdictInput, -} - -/// All inputs needed for the closed, one-send synthetic canary runtime. This -/// is crate-private and feature-gated; there is intentionally no Tauri command -/// or UI route that can invoke it in this change. -#[cfg(feature = "fixture-canary-runtime-dispatch")] -pub(crate) struct SealedCanaryRuntimeDispatchRequest { - pub config: TallyConfig, - pub company: ValidatedCompanyName, - pub ledger_name: ValidatedCanaryLedgerName, - pub identity_query_sha256: ValidatedIdentityQuerySha256, - pub expected_company_guid: String, - pub dispatch: BeginWriteCanaryDispatchInput, -} - -/// Terminal truth classification for the one-send sealed runtime. Errors before -/// its durable dispatch claim prove that no import was sent; after that claim a -/// final verdict may be absent even if Tally received the one permitted import. -#[cfg(feature = "fixture-canary-runtime-dispatch")] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum SealedCanaryRuntimeDispatchError { - PreDispatch, - OutcomeUnknown, -} - -/// Claims the durable preflight slot, performs exactly one sealed read, and -/// persists only the resulting digests. Any failure leaves the claim consumed -/// and cannot progress to a write. -pub(crate) async fn run_sealed_canary_preflight( - repository: &TallyMirrorRepository, - runtime: &TallyRuntime, - request: SealedCanaryPreflightRequest, - prepared: &PreparedFixtureCanary, -) -> Result { - let binding = request.binding.binding.clone(); - if binding.wire_sha256 != prepared.wire_digest().as_hex() - || binding.intended_state_sha256 != prepared.intended_state_digest().as_hex() - || binding.identity_query_sha256 != prepared.identity_query_digest().as_hex() - || request.identity_query_sha256.as_str() != prepared.identity_query_digest().as_hex() - || request.ledger_name.as_str() != FIXTURE_CANARY_LEDGER_NAME - { - bail!("sealed_canary_preflight_binding_mismatch"); - } - - let attempt = repository - .begin_write_canary_preflight(request.binding) - .await?; - let (canonical_endpoint_sha256, company_identity_sha256) = - sealed_target_binding_sha256(&request.config, &request.expected_company_guid)?; - let readback = runtime - .fetch_ledger_canary_readback( - request.config, - request.company, - request.ledger_name, - request.identity_query_sha256, - request.expected_company_guid, - ) - .await?; - let evidence: FixtureCanaryPreflightEvidence = - verify_fixture_canary_preflight(prepared, readback.as_xml())?; - let active_evidence = ActiveWriteCanaryPreflightEvidenceInput { - binding, - attempt_id: attempt.id.clone(), - evidence_id: String::new(), - readback_state_sha256: evidence.readback_state_digest().as_hex().to_owned(), - identity_coverage_sha256: evidence.identity_coverage_digest().as_hex().to_owned(), - canonical_endpoint_sha256, - company_identity_sha256, - }; - let persisted = repository - .record_write_canary_preflight_evidence(WriteCanaryPreflightEvidenceInput { - attempt_id: attempt.id, - readback_state_sha256: active_evidence.readback_state_sha256.clone(), - identity_coverage_sha256: active_evidence.identity_coverage_sha256.clone(), - canonical_endpoint_sha256: active_evidence.canonical_endpoint_sha256.clone(), - company_identity_sha256: active_evidence.company_identity_sha256.clone(), - verified_at_unix_ms: Utc::now().timestamp_millis(), - }) - .await?; - Ok(SealedCanaryPreflightCompletion { - active_evidence: ActiveWriteCanaryPreflightEvidenceInput { - evidence_id: persisted.id.clone(), - ..active_evidence - }, - evidence: persisted, - }) -} - -/// Rechecks that the supplied immutable preflight evidence is still bound to -/// the exact prepared fixture canary and active enrollment. This is a -/// read-only gate for a future separately reviewed import coordinator; it -/// cannot contact Tally or construct an import request. -pub(crate) async fn verify_sealed_canary_preflight_evidence( - repository: &TallyMirrorRepository, - request: SealedCanaryPreflightEvidenceGateRequest, - prepared: &PreparedFixtureCanary, -) -> Result { - let binding: &ActiveWriteCanaryPayloadBindingInput = &request.evidence.binding; - if binding.wire_sha256 != prepared.wire_digest().as_hex() - || binding.intended_state_sha256 != prepared.intended_state_digest().as_hex() - || binding.identity_query_sha256 != prepared.identity_query_digest().as_hex() - || request.identity_query_sha256.as_str() != prepared.identity_query_digest().as_hex() - || request.ledger_name.as_str() != FIXTURE_CANARY_LEDGER_NAME - { - bail!("sealed_canary_preflight_evidence_binding_mismatch"); - } - - Ok(repository - .active_write_canary_preflight_evidence(request.evidence) - .await?) -} - -/// Consumes one immutable, evidence-gated dispatch claim. It has no Tally -/// configuration or payload and therefore cannot create a request or write. -pub(crate) async fn claim_sealed_canary_dispatch( - repository: &TallyMirrorRepository, - request: SealedCanaryDispatchClaimRequest, - prepared: &PreparedFixtureCanary, -) -> Result { - let binding = &request.dispatch.evidence.binding; - if binding.wire_sha256 != prepared.wire_digest().as_hex() - || binding.intended_state_sha256 != prepared.intended_state_digest().as_hex() - || binding.identity_query_sha256 != prepared.identity_query_digest().as_hex() - || request.identity_query_sha256.as_str() != prepared.identity_query_digest().as_hex() - || request.ledger_name.as_str() != FIXTURE_CANARY_LEDGER_NAME - { - bail!("sealed_canary_dispatch_claim_binding_mismatch"); - } - Ok(repository - .begin_write_canary_dispatch_attempt(request.dispatch) - .await?) -} - -/// Validates the target commitment, claims the one durable dispatch slot, then -/// acquires an exclusive lease and revalidates the pinned GUID immediately -/// before the sole import. That lease remains held through the final readback, -/// so no Bridge read can interleave between identity verification and import. -/// There is no retry or resend. Errors through the claim are pre-dispatch; -/// every later failure is an unknown outcome. -#[cfg(feature = "fixture-canary-runtime-dispatch")] -pub(crate) async fn run_sealed_canary_runtime_dispatch( - repository: &TallyMirrorRepository, - runtime: &TallyRuntime, - request: SealedCanaryRuntimeDispatchRequest, - prepared: PreparedFixtureCanary, -) -> std::result::Result { - let (canonical_endpoint_sha256, company_identity_sha256) = - sealed_target_binding_sha256(&request.config, &request.expected_company_guid) - .map_err(|_| SealedCanaryRuntimeDispatchError::PreDispatch)?; - if request.dispatch.evidence.canonical_endpoint_sha256 != canonical_endpoint_sha256 - || request.dispatch.evidence.company_identity_sha256 != company_identity_sha256 - { - return Err(SealedCanaryRuntimeDispatchError::PreDispatch); - } - let dispatch_claim = claim_sealed_canary_dispatch( - repository, - SealedCanaryDispatchClaimRequest { - ledger_name: request.ledger_name.clone(), - identity_query_sha256: request.identity_query_sha256.clone(), - dispatch: request.dispatch.clone(), - }, - &prepared, - ) - .await - .map_err(|_| SealedCanaryRuntimeDispatchError::PreDispatch)?; - - let dispatch_lease = runtime - .begin_verified_canary_dispatch( - request.config.clone(), - request.company.clone(), - request.ledger_name.clone(), - request.identity_query_sha256.clone(), - request.expected_company_guid.clone(), - ) - .await - .map_err(|_| SealedCanaryRuntimeDispatchError::OutcomeUnknown)?; - let receipt = runtime - .dispatch_fixture_canary_once_under_dispatch( - &dispatch_lease, - &request.config, - prepared - .seal_for_dispatch() - .map_err(|_| SealedCanaryRuntimeDispatchError::OutcomeUnknown)?, - ) - .await - .map_err(|_| SealedCanaryRuntimeDispatchError::OutcomeUnknown)?; - receipt - .validate_receipt() - .map_err(|_| SealedCanaryRuntimeDispatchError::OutcomeUnknown)?; - let readback = runtime - .fetch_ledger_canary_readback_under_dispatch( - &dispatch_lease, - request.config, - request.company, - request.ledger_name, - request.identity_query_sha256, - request.expected_company_guid, - ) - .await - .map_err(|_| SealedCanaryRuntimeDispatchError::OutcomeUnknown)?; - let observation = receipt - .observe_with_readback(readback.as_xml()) - .map_err(|_| SealedCanaryRuntimeDispatchError::OutcomeUnknown)?; - let verdict = WriteCanaryFinalVerdictInput { - dispatch: ActiveWriteCanaryDispatchAttemptInput { - evidence: request.dispatch.evidence, - dispatch_attempt_id: dispatch_claim.id, - claimed_at_unix_ms: dispatch_claim.claimed_at_unix_ms, - }, - import_response_sha256: observation.import_response_digest().as_hex().to_owned(), - readback_state_sha256: observation.readback_state_digest().as_hex().to_owned(), - identity_coverage_sha256: observation.identity_coverage_digest().as_hex().to_owned(), - recorded_at_unix_ms: Utc::now().timestamp_millis(), - }; - repository - .record_write_canary_final_verdict(verdict) - .await - .map_err(|_| SealedCanaryRuntimeDispatchError::OutcomeUnknown) -} - -/// Correlates an exact portable observation to one durable dispatch claim and -/// stores only its digests. This coordinator cannot make a Tally request: a -/// separately reviewed dispatch path must create the observation first. -pub(crate) async fn record_sealed_canary_final_verdict( - repository: &TallyMirrorRepository, - request: SealedCanaryFinalVerdictRequest, - prepared: &PreparedFixtureCanary, - observation: &FixtureCanaryPostDispatchObservation, -) -> Result { - let binding = &request.verdict.dispatch.evidence.binding; - if binding.wire_sha256 != prepared.wire_digest().as_hex() - || binding.intended_state_sha256 != prepared.intended_state_digest().as_hex() - || binding.identity_query_sha256 != prepared.identity_query_digest().as_hex() - || request.identity_query_sha256.as_str() != prepared.identity_query_digest().as_hex() - || request.ledger_name.as_str() != FIXTURE_CANARY_LEDGER_NAME - || request.verdict.import_response_sha256 != observation.import_response_digest().as_hex() - || request.verdict.readback_state_sha256 != observation.readback_state_digest().as_hex() - || request.verdict.identity_coverage_sha256 - != observation.identity_coverage_digest().as_hex() - { - bail!("sealed_canary_final_verdict_binding_mismatch"); - } - Ok(repository - .record_write_canary_final_verdict(request.verdict) - .await?) -} - -#[cfg(test)] -mod tests { - use super::sealed_target_binding_sha256; - use crate::tally::TallyConfig; - - #[test] - fn sealed_target_binding_is_canonical_and_target_specific() { - let loopback = TallyConfig { - host: "127.0.0.1".to_string(), - port: 9000, - }; - let localhost = TallyConfig { - host: "localhost".to_string(), - port: 9000, - }; - let different_port = TallyConfig { - host: "127.0.0.1".to_string(), - port: 9001, - }; - - let binding = sealed_target_binding_sha256(&loopback, "fixture-company-guid") - .expect("hash synthetic target binding"); - assert_eq!( - binding, - sealed_target_binding_sha256(&localhost, "FIXTURE-COMPANY-GUID") - .expect("canonical aliases and GUID case share one binding") - ); - assert_ne!( - binding, - sealed_target_binding_sha256(&different_port, "fixture-company-guid") - .expect("a changed endpoint must not share authority") - ); - assert_ne!( - binding, - sealed_target_binding_sha256(&loopback, "another-fixture-company-guid") - .expect("a changed company must not share authority") - ); - } -} diff --git a/src-tauri/src/tally/canary_preflight_preparation.rs b/src-tauri/src/tally/canary_preflight_preparation.rs deleted file mode 100644 index b5e0b6a..0000000 --- a/src-tauri/src/tally/canary_preflight_preparation.rs +++ /dev/null @@ -1,287 +0,0 @@ -//! Private, no-network preparation of the fixed synthetic fixture canary. -//! -//! This module has no Tauri command, runtime, transport, endpoint, retry, XML -//! accessor, or configuration input. It reserves then binds the one fixed -//! canary locally; a future readback or dispatch boundary must be reviewed -//! separately before it can use the resulting durable commitment. - -use crate::{ - db::tally_mirror::{ - ActiveWriteCanaryPayloadBindingInput, SnapshotSourcePin, TallyMirrorRepository, - WriteCanaryPayloadBindingInput, WriteCanaryReservationInput, WriteCanaryReservationRef, - }, - tally::write_sandbox::{ - authorize_fixture_canary, fixture_canary_ledger_mutation, - prepare_fixture_canary_ledger_import, preview_ledger_import, - FixtureCanaryAuthorizationRequest, IdempotencyRegistry, PreparedFixtureCanary, - SyntheticCompany, FIXTURE_CANARY_LEDGER_NAME, FIXTURE_CANARY_MAPPING_VERSION, - }, -}; -use anyhow::{bail, Result}; -use bridge_tally_protocol::xml_read_profiles::{ - ValidatedCanaryLedgerName, ValidatedCompanyName, ValidatedIdentityQuerySha256, -}; -use chrono::Utc; - -/// Operator assertions required before any durable canary preparation work. -/// -/// This is crate-private so no UI or command can turn it into a generic import -/// request. The eventual command boundary must construct it from a separately -/// reviewed, explicit user interaction. -pub(crate) struct PrepareSealedCanaryPreflightRequest { - pub company_id: String, - pub review_commitment_sha256: String, - pub explicit_opt_in: bool, - pub synthetic_company_confirmed: bool, - pub backup_guidance_acknowledged: bool, -} - -/// A private, non-serializable preparation result. The opaque prepared capsule -/// intentionally has no transport or import API in this build. -pub(crate) struct PreparedSealedCanaryPreflight { - pub canonical_origin: String, - pub company: ValidatedCompanyName, - pub expected_company_guid: String, - pub ledger_name: ValidatedCanaryLedgerName, - pub identity_query_sha256: ValidatedIdentityQuerySha256, - pub binding: ActiveWriteCanaryPayloadBindingInput, - pub prepared: PreparedFixtureCanary, -} - -/// Every source-derived value that can reject preparation is validated before -/// the irreversible one-time reservation is consumed. -struct ValidatedFixedCanary { - canonical_origin: String, - company: SyntheticCompany, - company_name: ValidatedCompanyName, - expected_company_guid: String, - ledger_name: ValidatedCanaryLedgerName, - identity_query_sha256: ValidatedIdentityQuerySha256, - wire_sha256: String, - intended_state_sha256: String, -} - -fn validate_operator_assertions(request: &PrepareSealedCanaryPreflightRequest) -> Result<()> { - if !request.explicit_opt_in { - bail!("sealed_canary_preflight_explicit_opt_in_required"); - } - if !request.synthetic_company_confirmed { - bail!("sealed_canary_preflight_synthetic_company_required"); - } - if !request.backup_guidance_acknowledged { - bail!("sealed_canary_preflight_backup_acknowledgement_required"); - } - if request.review_commitment_sha256.len() != 64 - || !request - .review_commitment_sha256 - .bytes() - .all(|byte| byte.is_ascii_hexdigit()) - { - bail!("sealed_canary_preflight_review_commitment_invalid"); - } - Ok(()) -} - -fn validate_fixed_canary(pin: &SnapshotSourcePin) -> Result { - let company = SyntheticCompany::new(pin.display_name.clone(), pin.company_guid.clone())?; - let mutation = fixture_canary_ledger_mutation()?; - let preview = preview_ledger_import(&company, &[mutation], FIXTURE_CANARY_MAPPING_VERSION)?; - Ok(ValidatedFixedCanary { - canonical_origin: pin.canonical_origin.clone(), - company, - company_name: ValidatedCompanyName::new(pin.display_name.clone())?, - expected_company_guid: pin.company_guid.clone(), - ledger_name: ValidatedCanaryLedgerName::new(FIXTURE_CANARY_LEDGER_NAME)?, - identity_query_sha256: ValidatedIdentityQuerySha256::new( - preview.identity_query_digest().as_hex(), - )?, - wire_sha256: preview.wire_digest().as_hex().to_owned(), - intended_state_sha256: preview.intended_state_digest().as_hex().to_owned(), - }) -} - -fn source_pin_matches_origin(pin: &SnapshotSourcePin, expected_canonical_origin: &str) -> bool { - pin.canonical_origin == expected_canonical_origin -} -fn materialize_fixed_preflight( - request: &PrepareSealedCanaryPreflightRequest, - fixed_canary: ValidatedFixedCanary, - reservation: &WriteCanaryReservationRef, -) -> Result { - let authorization = authorize_fixture_canary(FixtureCanaryAuthorizationRequest { - explicit_opt_in: request.explicit_opt_in, - synthetic_company_confirmed: request.synthetic_company_confirmed, - company_guid: fixed_canary.expected_company_guid.clone(), - backup_guidance_acknowledged: request.backup_guidance_acknowledged, - review_commitment_sha256: request.review_commitment_sha256.clone(), - reservation_id: reservation.id.clone(), - reservation_payload_sha256: reservation.reservation_payload_sha256.clone(), - approved_wire_sha256: fixed_canary.wire_sha256, - approved_intended_state_sha256: fixed_canary.intended_state_sha256, - approved_identity_query_sha256: fixed_canary.identity_query_sha256.as_str().to_owned(), - idempotency_key: format!("fixture-canary:{}", reservation.id), - })?; - let prepared = prepare_fixture_canary_ledger_import( - fixed_canary.company, - authorization, - &mut IdempotencyRegistry::default(), - )?; - let binding = ActiveWriteCanaryPayloadBindingInput { - company_id: request.company_id.clone(), - review_commitment_sha256: request.review_commitment_sha256.clone(), - reservation_id: reservation.id.clone(), - reservation_payload_sha256: reservation.reservation_payload_sha256.clone(), - wire_sha256: prepared.wire_digest().as_hex().to_owned(), - intended_state_sha256: prepared.intended_state_digest().as_hex().to_owned(), - identity_query_sha256: prepared.identity_query_digest().as_hex().to_owned(), - }; - - Ok(PreparedSealedCanaryPreflight { - canonical_origin: fixed_canary.canonical_origin, - company: fixed_canary.company_name, - expected_company_guid: fixed_canary.expected_company_guid, - ledger_name: fixed_canary.ledger_name, - identity_query_sha256: fixed_canary.identity_query_sha256, - binding, - prepared, - }) -} - -/// Reserves and binds the fixed canary for a previously enrolled synthetic -/// fixture. It performs no Tally interaction: both repository calls are local -/// durable-state operations, and the returned capsule remains private. -pub(crate) async fn prepare_sealed_canary_preflight( - repository: &TallyMirrorRepository, - request: PrepareSealedCanaryPreflightRequest, - expected_canonical_origin: &str, -) -> Result { - validate_operator_assertions(&request)?; - let pin = repository.snapshot_source_pin(&request.company_id).await?; - if pin.company_id != request.company_id { - bail!("sealed_canary_preflight_persisted_company_mismatch"); - } - if !source_pin_matches_origin(&pin, expected_canonical_origin) { - bail!("sealed_canary_preflight_origin_mismatch"); - } - // Validate every caller- and source-dependent fixed payload input before - // consuming the fixture's irreversible one-time reservation. - let fixed_canary = validate_fixed_canary(&pin)?; - let reservation = repository - .reserve_write_canary(WriteCanaryReservationInput { - company_id: request.company_id.clone(), - review_commitment_sha256: request.review_commitment_sha256.clone(), - reserved_at_unix_ms: Utc::now().timestamp_millis(), - }) - .await?; - let preparation = materialize_fixed_preflight(&request, fixed_canary, &reservation)?; - repository - .bind_write_canary_payload(WriteCanaryPayloadBindingInput { - company_id: preparation.binding.company_id.clone(), - review_commitment_sha256: preparation.binding.review_commitment_sha256.clone(), - reservation_id: preparation.binding.reservation_id.clone(), - reservation_payload_sha256: preparation.binding.reservation_payload_sha256.clone(), - wire_sha256: preparation.binding.wire_sha256.clone(), - intended_state_sha256: preparation.binding.intended_state_sha256.clone(), - identity_query_sha256: preparation.binding.identity_query_sha256.clone(), - bound_at_unix_ms: Utc::now().timestamp_millis(), - }) - .await?; - - Ok(preparation) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn request() -> PrepareSealedCanaryPreflightRequest { - PrepareSealedCanaryPreflightRequest { - company_id: "synthetic-company".to_owned(), - review_commitment_sha256: "a".repeat(64), - explicit_opt_in: true, - synthetic_company_confirmed: true, - backup_guidance_acknowledged: true, - } - } - - fn pin(company_id: String) -> SnapshotSourcePin { - SnapshotSourcePin { - company_id, - endpoint_id: "synthetic-endpoint".to_owned(), - canonical_origin: "synthetic-origin".to_owned(), - display_name: "Synthetic Fixture Company".to_owned(), - company_guid: "synthetic-company-guid".to_owned(), - } - } - - fn reservation() -> WriteCanaryReservationRef { - WriteCanaryReservationRef { - id: "synthetic-reservation".to_owned(), - enrollment_id: "synthetic-enrollment".to_owned(), - reservation_payload_sha256: "b".repeat(64), - reserved_at_unix_ms: 1_000, - } - } - - #[test] - fn preparation_derives_binding_only_from_the_fixed_canary() { - let request = request(); - let pin = pin(request.company_id.clone()); - let fixed_canary = validate_fixed_canary(&pin).expect("fixed source validates"); - let preparation = materialize_fixed_preflight(&request, fixed_canary, &reservation()) - .expect("fixed synthetic fixture must prepare"); - - assert_eq!(preparation.canonical_origin, pin.canonical_origin); - assert_eq!(preparation.company.as_str(), pin.display_name); - assert_eq!(preparation.expected_company_guid, pin.company_guid); - assert_eq!(preparation.ledger_name.as_str(), FIXTURE_CANARY_LEDGER_NAME); - assert_eq!( - preparation.identity_query_sha256.as_str(), - preparation.prepared.identity_query_digest().as_hex(), - ); - assert_eq!(preparation.binding.company_id, request.company_id); - assert_eq!( - preparation.binding.review_commitment_sha256, - request.review_commitment_sha256, - ); - assert_eq!(preparation.binding.reservation_id, reservation().id); - assert_eq!( - preparation.binding.reservation_payload_sha256, - reservation().reservation_payload_sha256, - ); - assert_eq!( - preparation.binding.wire_sha256, - preparation.prepared.wire_digest().as_hex(), - ); - assert_eq!( - preparation.binding.intended_state_sha256, - preparation.prepared.intended_state_digest().as_hex(), - ); - } - - #[test] - fn source_values_that_cannot_materialize_fail_before_reservation() { - let request = request(); - let mut oversized_name = pin(request.company_id.clone()); - oversized_name.display_name = "n".repeat(256); - assert!(validate_fixed_canary(&oversized_name).is_err()); - - let mut oversized_guid = pin(request.company_id); - oversized_guid.company_guid = "g".repeat(256); - assert!(validate_fixed_canary(&oversized_guid).is_err()); - } - - #[test] - fn invalid_review_commitment_fails_before_reservation() { - let mut request = request(); - request.review_commitment_sha256 = "not-a-sha256".to_owned(); - assert!(validate_operator_assertions(&request).is_err()); - } - - #[test] - fn source_pin_origin_must_match_before_reservation() { - let source = pin("synthetic-company".to_owned()); - assert!(source_pin_matches_origin(&source, &source.canonical_origin)); - assert!(!source_pin_matches_origin(&source, "http://127.0.0.1:9001")); - } -} diff --git a/src-tauri/src/tally/canary_preflight_read_coordinator.rs b/src-tauri/src/tally/canary_preflight_read_coordinator.rs deleted file mode 100644 index da9af6b..0000000 --- a/src-tauri/src/tally/canary_preflight_read_coordinator.rs +++ /dev/null @@ -1,112 +0,0 @@ -//! Private coordinator for the one sealed synthetic-canary preflight read. -//! -//! This has no public Tauri command, UI route, import path, retry, or -//! dispatch capability. It binds an already prepared fixed canary to the -//! existing one-time durable preflight-read claim; no write can occur here. - -use crate::{ - db::tally_mirror::{ - BeginWriteCanaryPreflightInput, TallyMirrorRepository, WriteCanaryPreflightEvidenceRef, - }, - tally::{ - canary_preflight::{run_sealed_canary_preflight, SealedCanaryPreflightRequest}, - canary_preflight_preparation::PreparedSealedCanaryPreflight, - connection::canonical_loopback_origin, - TallyConfig, TallyRuntime, - }, -}; -use anyhow::Result; -use chrono::Utc; - -/// The exact local preparation plus validated loopback configuration required to -/// consume the sealed preflight-read claim. The preparation is non-cloneable -/// in practice because its opaque capsule moves into this request. -pub(crate) struct SealedCanaryPreflightReadCoordinatorRequest { - pub config: TallyConfig, - pub preparation: PreparedSealedCanaryPreflight, -} - -/// Digest-only metadata for a completed exact readback. No XML, payload, -/// target, response, or dispatch authority crosses this boundary. -pub(crate) struct SealedCanaryPreflightReadCoordinatorResult { - pub evidence_id: String, - pub verified_at_unix_ms: i64, -} - -fn validate_preflight_config(config: &TallyConfig) -> Result<()> { - canonical_loopback_origin(config).map(|_| ()) -} -fn digest_only_result( - evidence: WriteCanaryPreflightEvidenceRef, -) -> SealedCanaryPreflightReadCoordinatorResult { - SealedCanaryPreflightReadCoordinatorResult { - evidence_id: evidence.id, - verified_at_unix_ms: evidence.verified_at_unix_ms, - } -} - -/// Claims the one durable preflight-read slot and delegates to the sealed -/// readback routine. Any error after the claim is terminal for this fixture; -/// this coordinator deliberately provides neither retry nor dispatch access. -pub(crate) async fn run_prepared_sealed_canary_preflight_read( - repository: &TallyMirrorRepository, - runtime: &TallyRuntime, - request: SealedCanaryPreflightReadCoordinatorRequest, -) -> Result { - // Reject malformed or non-loopback configuration before consuming the - // irreversible one-time preflight claim. - validate_preflight_config(&request.config)?; - let PreparedSealedCanaryPreflight { - company, - expected_company_guid, - ledger_name, - identity_query_sha256, - binding, - prepared, - .. - } = request.preparation; - let evidence = run_sealed_canary_preflight( - repository, - runtime, - SealedCanaryPreflightRequest { - config: request.config, - company, - ledger_name, - identity_query_sha256, - expected_company_guid, - binding: BeginWriteCanaryPreflightInput { - binding, - started_at_unix_ms: Utc::now().timestamp_millis(), - }, - }, - &prepared, - ) - .await?; - Ok(digest_only_result(evidence.evidence)) -} - -#[cfg(test)] -mod tests { - use super::{digest_only_result, validate_preflight_config}; - use crate::db::tally_mirror::WriteCanaryPreflightEvidenceRef; - use crate::tally::TallyConfig; - - #[test] - fn malformed_loopback_configuration_fails_before_preflight_claim() { - assert!(validate_preflight_config(&TallyConfig { - host: "example.invalid".to_owned(), - port: 9000, - }) - .is_err()); - } - #[test] - fn coordinator_result_contains_only_evidence_metadata() { - let result = digest_only_result(WriteCanaryPreflightEvidenceRef { - id: "synthetic-preflight-evidence".to_owned(), - attempt_id: "synthetic-preflight-attempt".to_owned(), - verified_at_unix_ms: 1_000, - }); - assert_eq!(result.evidence_id, "synthetic-preflight-evidence"); - assert_eq!(result.verified_at_unix_ms, 1_000); - } -} diff --git a/src-tauri/src/tally/canary_runtime_dispatch_coordinator.rs b/src-tauri/src/tally/canary_runtime_dispatch_coordinator.rs deleted file mode 100644 index 6e72b75..0000000 --- a/src-tauri/src/tally/canary_runtime_dispatch_coordinator.rs +++ /dev/null @@ -1,231 +0,0 @@ -//! Runtime-feature-only coordinator for the sealed synthetic fixture canary. -//! -//! This is not a Tauri command or UI route. It remains unavailable in default -//! builds and accepts no generic payload or endpoint. When a future reviewed -//! command layer explicitly enables the non-default runtime feature, this -//! coordinator rechecks local admission and delegates exactly once to the -//! sealed runtime. It returns digest-only final-verdict metadata. - -use crate::{ - db::tally_mirror::{ - BeginWriteCanaryDispatchInput, BeginWriteCanaryPreflightInput, TallyMirrorRepository, - WriteCanaryFinalVerdictRef, - }, - tally::{ - canary_dispatch_admission::{ - admit_sealed_canary_runtime_dispatch, SealedCanaryRuntimeAdmissionRequest, - }, - canary_preflight::{ - run_sealed_canary_preflight, run_sealed_canary_runtime_dispatch, - SealedCanaryPreflightRequest, SealedCanaryRuntimeDispatchRequest, - }, - canary_preflight_preparation::{ - prepare_sealed_canary_preflight, PrepareSealedCanaryPreflightRequest, - PreparedSealedCanaryPreflight, - }, - connection::canonical_loopback_origin, - write_sandbox::PreparedFixtureCanary, - TallyConfig, TallyRuntime, - }, -}; -use anyhow::Result as AnyhowResult; -use bridge_tally_protocol::xml_read_profiles::{ - ValidatedCanaryLedgerName, ValidatedCompanyName, ValidatedIdentityQuerySha256, -}; -use chrono::Utc; - -/// Exact terminal classification for the runtime command boundary. A failure -/// before the durable dispatch claim cannot have written to Tally; any failure -/// after that claim is an unknown outcome because one import may have reached -/// Tally without a recorded final verdict. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum SealedCanaryRuntimeSequenceError { - PreDispatch, - OutcomeUnknown, -} - -/// Exact, sealed inputs for the only runtime-capable coordinator. It has no -/// raw XML fields, generic request body, retry settings, or caller-selected -/// dispatch operation. -pub(crate) struct SealedCanaryRuntimeCoordinatorRequest { - pub company_id: String, - pub config: TallyConfig, - pub company: ValidatedCompanyName, - pub ledger_name: ValidatedCanaryLedgerName, - pub identity_query_sha256: ValidatedIdentityQuerySha256, - pub expected_company_guid: String, - pub dispatch: BeginWriteCanaryDispatchInput, - pub prepared: PreparedFixtureCanary, -} - -/// The only source-derived input accepted by the feature-gated end-to-end -/// coordinator. The fixed canary is created inside the coordinator from the -/// enrolled local company pin; callers cannot supply XML, payload fields, -/// evidence digests, an endpoint override, a retry policy, or dispatch data. -pub(crate) struct SealedCanaryRuntimeSequenceRequest { - pub config: TallyConfig, - pub preparation: PrepareSealedCanaryPreflightRequest, -} - -/// Safe result of an already completed sealed dispatch. It contains neither -/// a Tally response nor a readback, payload, target, or digest value. -pub(crate) struct SealedCanaryRuntimeCoordinatorResult { - pub final_verdict_id: String, - pub recorded_at_unix_ms: i64, -} - -fn digest_only_result(verdict: WriteCanaryFinalVerdictRef) -> SealedCanaryRuntimeCoordinatorResult { - SealedCanaryRuntimeCoordinatorResult { - final_verdict_id: verdict.id, - recorded_at_unix_ms: verdict.recorded_at_unix_ms, - } -} - -fn validate_runtime_sequence_config(config: &TallyConfig) -> AnyhowResult<()> { - canonical_loopback_origin(config).map(|_| ()) -} - -fn into_preflight_request( - config: TallyConfig, - preparation: &PreparedSealedCanaryPreflight, -) -> SealedCanaryPreflightRequest { - SealedCanaryPreflightRequest { - config, - company: preparation.company.clone(), - ledger_name: preparation.ledger_name.clone(), - identity_query_sha256: preparation.identity_query_sha256.clone(), - expected_company_guid: preparation.expected_company_guid.clone(), - binding: BeginWriteCanaryPreflightInput { - binding: preparation.binding.clone(), - started_at_unix_ms: Utc::now().timestamp_millis(), - }, - } -} - -/// Runs the closed synthetic-canary sequence after its non-default feature has -/// been explicitly enabled by a future reviewed command boundary. It validates -/// the loopback endpoint before reserving anything, prepares the fixed canary, -/// performs the exact one-time preflight read, rechecks admission, and then -/// delegates once to the sealed runtime. It never retries a read or import. -/// -/// This remains crate-private and is not a Tauri command or UI route. -pub(crate) async fn run_sealed_canary_runtime_sequence( - repository: &TallyMirrorRepository, - runtime: &TallyRuntime, - request: SealedCanaryRuntimeSequenceRequest, -) -> std::result::Result { - let canonical_origin = canonical_loopback_origin(&request.config) - .map_err(|_| SealedCanaryRuntimeSequenceError::PreDispatch)?; - let preparation = - prepare_sealed_canary_preflight(repository, request.preparation, &canonical_origin) - .await - .map_err(|_| SealedCanaryRuntimeSequenceError::PreDispatch)?; - if preparation.canonical_origin != canonical_origin { - return Err(SealedCanaryRuntimeSequenceError::PreDispatch); - } - let preflight = run_sealed_canary_preflight( - repository, - runtime, - into_preflight_request(request.config.clone(), &preparation), - &preparation.prepared, - ) - .await - .map_err(|_| SealedCanaryRuntimeSequenceError::PreDispatch)?; - run_admitted_sealed_canary_runtime_dispatch( - repository, - runtime, - SealedCanaryRuntimeCoordinatorRequest { - company_id: preparation.binding.company_id, - config: request.config, - company: preparation.company, - ledger_name: preparation.ledger_name, - identity_query_sha256: preparation.identity_query_sha256, - expected_company_guid: preparation.expected_company_guid, - dispatch: BeginWriteCanaryDispatchInput { - evidence: preflight.active_evidence, - claimed_at_unix_ms: Utc::now().timestamp_millis(), - }, - prepared: preparation.prepared, - }, - ) - .await -} - -/// Rechecks local admission immediately before calling the sealed one-send -/// runtime. The sealed runtime owns the durable claim, single import, exact -/// readback, and digest-only verdict record; this coordinator adds no retry or -/// recovery send behavior. -pub(crate) async fn run_admitted_sealed_canary_runtime_dispatch( - repository: &TallyMirrorRepository, - runtime: &TallyRuntime, - request: SealedCanaryRuntimeCoordinatorRequest, -) -> std::result::Result { - let admission = admit_sealed_canary_runtime_dispatch( - repository, - SealedCanaryRuntimeAdmissionRequest { - company_id: request.company_id, - ledger_name: request.ledger_name.clone(), - identity_query_sha256: request.identity_query_sha256.clone(), - evidence: request.dispatch.evidence.clone(), - }, - &request.prepared, - ) - .await - .map_err(|_| SealedCanaryRuntimeSequenceError::PreDispatch)?; - if admission.preflight_evidence.id != request.dispatch.evidence.evidence_id - || admission.preflight_evidence.attempt_id != request.dispatch.evidence.attempt_id - { - return Err(SealedCanaryRuntimeSequenceError::PreDispatch); - } - - let verdict = run_sealed_canary_runtime_dispatch( - repository, - runtime, - SealedCanaryRuntimeDispatchRequest { - config: request.config, - company: request.company, - ledger_name: request.ledger_name, - identity_query_sha256: request.identity_query_sha256, - expected_company_guid: request.expected_company_guid, - dispatch: request.dispatch, - }, - request.prepared, - ) - .await - .map_err(|error| match error { - crate::tally::canary_preflight::SealedCanaryRuntimeDispatchError::PreDispatch => { - SealedCanaryRuntimeSequenceError::PreDispatch - } - crate::tally::canary_preflight::SealedCanaryRuntimeDispatchError::OutcomeUnknown => { - SealedCanaryRuntimeSequenceError::OutcomeUnknown - } - })?; - Ok(digest_only_result(verdict)) -} - -#[cfg(test)] -mod tests { - use super::{digest_only_result, validate_runtime_sequence_config}; - use crate::db::tally_mirror::WriteCanaryFinalVerdictRef; - use crate::tally::TallyConfig; - - #[test] - fn coordinator_result_contains_only_final_verdict_metadata() { - let result = digest_only_result(WriteCanaryFinalVerdictRef { - id: "synthetic-final-verdict".to_string(), - dispatch_attempt_id: "synthetic-dispatch-attempt".to_string(), - recorded_at_unix_ms: 1_000, - }); - assert_eq!(result.final_verdict_id, "synthetic-final-verdict"); - assert_eq!(result.recorded_at_unix_ms, 1_000); - } - - #[test] - fn malformed_loopback_configuration_fails_before_runtime_sequence() { - assert!(validate_runtime_sequence_config(&TallyConfig { - host: "example.invalid".to_owned(), - port: 9000, - }) - .is_err()); - } -} diff --git a/src-tauri/src/tally/incremental.rs b/src-tauri/src/tally/incremental.rs deleted file mode 100644 index 4f7177b..0000000 --- a/src-tauri/src/tally/incremental.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Native Bridge uses the same portable incremental policy exercised in the fast CI lane. - -pub use bridge_tally_incremental::*; diff --git a/src-tauri/src/tally/write_sandbox.rs b/src-tauri/src/tally/write_sandbox.rs deleted file mode 100644 index 75b40bd..0000000 --- a/src-tauri/src/tally/write_sandbox.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! Network-free controlled-write qualification. -//! -//! The implementation source is shared with the portable crate so its evidence -//! derivation can be tested without Tauri, SQLCipher, native libraries, or an -//! installed Tally. In this desktop module its sealed fields remain -//! crate-private; only the crate-private runtime coordinator can hand them to -//! the bounded loopback transport. - -#[path = "../../crates/bridge-tally-write/src/lib.rs"] -#[allow(dead_code, unused_imports)] -mod implementation; - -pub(crate) use implementation::*;