commit a5777fe23da4315f5b9125c10d61d0755e6631f7
parent fd5b81fd2b4f2e3a9264f628f8a1178824a20f5a
Author: triesap <tyson@radroots.org>
Date: Sun, 20 Sep 2026 12:02:24 +0000
recovery: classify cross-boundary receipt states
- Model exact frozen native and Rust transfer associations
- Distinguish lookup, pause, completion, settlement and isolated repair
- Test commit windows, repeated evidence and identity mismatches
- Verify standalone Rust and native consumers with unchanged public APIs
Diffstat:
10 files changed, 496 insertions(+), 52 deletions(-)
diff --git a/TeraFFI/provenance.json b/TeraFFI/provenance.json
@@ -22,9 +22,9 @@
"sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c"
},
{
- "bytes": 76774144,
+ "bytes": 76698520,
"path": "TeraFFI.xcframework/ios-arm64-simulator/libtera_ffi.a",
- "sha256": "c99b2a456361cff0c24d4af802452aea3c7200cf26e930d76972ed3c8c4d3b18"
+ "sha256": "52728a4e4c61f7d3811e1a358aa71908d3e925450f8ec9079221b22d9d6b03cb"
},
{
"bytes": 83913,
@@ -37,9 +37,9 @@
"sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c"
},
{
- "bytes": 76853480,
+ "bytes": 76777928,
"path": "TeraFFI.xcframework/ios-arm64/libtera_ffi.a",
- "sha256": "11bf5251738f4f16035417f53648b3ad5ccbdce3d7b59b236e0b6182d126f0df"
+ "sha256": "f9125fe01e221380b9084a2ad8336611880c901bd20c45207efb1f3d3a4035c0"
},
{
"bytes": 51571,
@@ -77,34 +77,34 @@
"sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c"
},
{
- "bytes": 22091840,
+ "bytes": 22051024,
"path": "native/aarch64-apple-darwin/libtera_ffi.dylib",
- "sha256": "d497f0eb46f56965d1fac69aa3f7acc9b3131985c74e59c1ac654e9ec5690e82"
+ "sha256": "59c25f4639fca3424166b808a0d05bf0fb8cc7e722593b2b09acfafe7cb4729c"
},
{
- "bytes": 76774144,
+ "bytes": 76698520,
"path": "native/aarch64-apple-ios-sim/libtera_ffi.a",
- "sha256": "c99b2a456361cff0c24d4af802452aea3c7200cf26e930d76972ed3c8c4d3b18"
+ "sha256": "52728a4e4c61f7d3811e1a358aa71908d3e925450f8ec9079221b22d9d6b03cb"
},
{
- "bytes": 76853480,
+ "bytes": 76777928,
"path": "native/aarch64-apple-ios/libtera_ffi.a",
- "sha256": "11bf5251738f4f16035417f53648b3ad5ccbdce3d7b59b236e0b6182d126f0df"
+ "sha256": "f9125fe01e221380b9084a2ad8336611880c901bd20c45207efb1f3d3a4035c0"
},
{
- "bytes": 106759,
+ "bytes": 107355,
"path": "source/aarch64-apple-darwin.json",
- "sha256": "ed5daaa64f4b240c1660f05f06cd1c79d4b9111cc00adb483dac58b3c43b7766"
+ "sha256": "633259fd62c85519cc0779ef722b7bfabe3aefa57d17a1c4d336bdcb44298844"
},
{
- "bytes": 106603,
+ "bytes": 107199,
"path": "source/aarch64-apple-ios-sim.json",
- "sha256": "f9444a7c2ad81cc0c02439b66ac4c603590cee3d821663e364d519442e591f9d"
+ "sha256": "c569017d267490cb59f7aff25109dd85810ef848a4d4c748905deddd809b84b3"
},
{
- "bytes": 106599,
+ "bytes": 107195,
"path": "source/aarch64-apple-ios.json",
- "sha256": "78186683a1294f9bbb714a879e060186e3a1e9788c349cb89973d20c79e0ea0d"
+ "sha256": "93ff0284eeff92eb40fd42e7db205d979a3ba76c0d9f8e735bd96f0c3682ad08"
}
],
"language": "swift",
@@ -112,7 +112,7 @@
"schema": "radroots.artifact-manifest.v2",
"source": {
"repository": "https://github.com/radrootslabs/tera",
- "tree": "207953b663237a6397cdd564c488e1491dbfde76"
+ "tree": "3e612b493a705f30a2e9ad8ae12a8e320b5f884c"
},
"source_records": {
"aarch64-apple-darwin": "source/aarch64-apple-darwin.json",
@@ -139,9 +139,9 @@
"sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c"
},
{
- "bytes": 76774144,
+ "bytes": 76698520,
"path": "Tera/Frameworks/TeraFFI.xcframework/ios-arm64-simulator/libtera_ffi.a",
- "sha256": "c99b2a456361cff0c24d4af802452aea3c7200cf26e930d76972ed3c8c4d3b18"
+ "sha256": "52728a4e4c61f7d3811e1a358aa71908d3e925450f8ec9079221b22d9d6b03cb"
},
{
"bytes": 83913,
@@ -154,9 +154,9 @@
"sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c"
},
{
- "bytes": 76853480,
+ "bytes": 76777928,
"path": "Tera/Frameworks/TeraFFI.xcframework/ios-arm64/libtera_ffi.a",
- "sha256": "11bf5251738f4f16035417f53648b3ad5ccbdce3d7b59b236e0b6182d126f0df"
+ "sha256": "f9125fe01e221380b9084a2ad8336611880c901bd20c45207efb1f3d3a4035c0"
},
{
"bytes": 663554,
@@ -174,19 +174,19 @@
"sha256": "12c54595ac3ebcb6aa22c7e968b019f67e1bc0606782ef4b71c601df6a54e764"
},
{
- "bytes": 106759,
+ "bytes": 107355,
"path": "TeraFFI/source/aarch64-apple-darwin.json",
- "sha256": "ed5daaa64f4b240c1660f05f06cd1c79d4b9111cc00adb483dac58b3c43b7766"
+ "sha256": "633259fd62c85519cc0779ef722b7bfabe3aefa57d17a1c4d336bdcb44298844"
},
{
- "bytes": 106603,
+ "bytes": 107199,
"path": "TeraFFI/source/aarch64-apple-ios-sim.json",
- "sha256": "f9444a7c2ad81cc0c02439b66ac4c603590cee3d821663e364d519442e591f9d"
+ "sha256": "c569017d267490cb59f7aff25109dd85810ef848a4d4c748905deddd809b84b3"
},
{
- "bytes": 106599,
+ "bytes": 107195,
"path": "TeraFFI/source/aarch64-apple-ios.json",
- "sha256": "78186683a1294f9bbb714a879e060186e3a1e9788c349cb89973d20c79e0ea0d"
+ "sha256": "93ff0284eeff92eb40fd42e7db205d979a3ba76c0d9f8e735bd96f0c3682ad08"
}
],
"schema": "tera.installed-native-artifacts.v1"
diff --git a/TeraFFI/source.lock b/TeraFFI/source.lock
@@ -1,7 +1,7 @@
schema = "tera.installed-source.v1"
repository = "https://github.com/radrootslabs/tera"
-source_tree = "207953b663237a6397cdd564c488e1491dbfde76"
-manifest_sha256 = "6a1a360e7a13afc89008544a964c4d747bc563c6af28f19fb2ddeea301416d0c"
+source_tree = "3e612b493a705f30a2e9ad8ae12a8e320b5f884c"
+manifest_sha256 = "e581ed9a2a599bdeaae142f5786090be6a4c0073e9297bfc056268c5f7dd47fa"
source_date_epoch = 1787871027
[foundation]
diff --git a/TeraFFI/source/aarch64-apple-darwin.json b/TeraFFI/source/aarch64-apple-darwin.json
@@ -645,10 +645,10 @@
"sha256": "dff55e46521c26f5f0ece024453b55c15f132fa94f78e2cba3d141f123d4eca0"
},
"core/crates/tera_core/src/runtime/product_surface.rs": {
- "bytes": 8364,
- "git_blob": "76c094ffe07eb95958c3559e13a6e8569ed51985",
+ "bytes": 8431,
+ "git_blob": "7fe0757ff6467b31fa087c25485e46b280b29ac6",
"mode": "100644",
- "sha256": "adcde8696fd8144f13213388f68f1c3d6cc19c31f08121f024281a99e9e05ec8"
+ "sha256": "67351a02a9d45f216212aa05e52ac0d5c36b74d7787731fb1bcfaecc69b1cec2"
},
"core/crates/tera_core/src/runtime/product_surface/authoring.rs": {
"bytes": 11282,
@@ -908,6 +908,18 @@
"mode": "100644",
"sha256": "8e0c1fedaa2ae6ceab717b5d44918be49683ac23a75a6c931fbca15013a36322"
},
+ "core/crates/tera_core/src/runtime/product_surface/recovery_reconciliation.rs": {
+ "bytes": 6212,
+ "git_blob": "63647bd304a3fd9dd51bc0930b7826eb96efe62e",
+ "mode": "100644",
+ "sha256": "655b9e8405a9a53c0e1657648a5d9d7a6a223a9d99c788c21afe5b6a8fc8242b"
+ },
+ "core/crates/tera_core/src/runtime/product_surface/recovery_reconciliation/tests.rs": {
+ "bytes": 6878,
+ "git_blob": "6d0d08c446de5fc16598de6475020d270de77aa5",
+ "mode": "100644",
+ "sha256": "dab08b5c62c89ab3f178b859c6229aef16d4cf844f6990f5c087481b773dfa85"
+ },
"core/crates/tera_core/src/runtime/product_surface/settings.rs": {
"bytes": 57049,
"git_blob": "5ffe3e45ee9d48382f5453b066eaeb166b00b407",
@@ -2271,13 +2283,13 @@
"sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d"
},
"test-fixtures/legacy-identifiers.v1.json": {
- "bytes": 151542,
- "git_blob": "854f586ecad54a22665f64d44d4843b39c66a50c",
+ "bytes": 152102,
+ "git_blob": "9801e3a668e27cf4b8e934599ae2d89b2535a9e4",
"mode": "100644",
- "sha256": "66dec6eb2296cee4c29333a58645ab3686442c34577c36bd2b8344b0bf5c32f2"
+ "sha256": "b39f1ef08ee92d472f63327675b46de673f3386d77ce93c1afe23c2f1214ee47"
}
},
"policy": "staged_inputs",
- "tree": "207953b663237a6397cdd564c488e1491dbfde76"
+ "tree": "3e612b493a705f30a2e9ad8ae12a8e320b5f884c"
}
}
diff --git a/TeraFFI/source/aarch64-apple-ios-sim.json b/TeraFFI/source/aarch64-apple-ios-sim.json
@@ -641,10 +641,10 @@
"sha256": "dff55e46521c26f5f0ece024453b55c15f132fa94f78e2cba3d141f123d4eca0"
},
"core/crates/tera_core/src/runtime/product_surface.rs": {
- "bytes": 8364,
- "git_blob": "76c094ffe07eb95958c3559e13a6e8569ed51985",
+ "bytes": 8431,
+ "git_blob": "7fe0757ff6467b31fa087c25485e46b280b29ac6",
"mode": "100644",
- "sha256": "adcde8696fd8144f13213388f68f1c3d6cc19c31f08121f024281a99e9e05ec8"
+ "sha256": "67351a02a9d45f216212aa05e52ac0d5c36b74d7787731fb1bcfaecc69b1cec2"
},
"core/crates/tera_core/src/runtime/product_surface/authoring.rs": {
"bytes": 11282,
@@ -904,6 +904,18 @@
"mode": "100644",
"sha256": "8e0c1fedaa2ae6ceab717b5d44918be49683ac23a75a6c931fbca15013a36322"
},
+ "core/crates/tera_core/src/runtime/product_surface/recovery_reconciliation.rs": {
+ "bytes": 6212,
+ "git_blob": "63647bd304a3fd9dd51bc0930b7826eb96efe62e",
+ "mode": "100644",
+ "sha256": "655b9e8405a9a53c0e1657648a5d9d7a6a223a9d99c788c21afe5b6a8fc8242b"
+ },
+ "core/crates/tera_core/src/runtime/product_surface/recovery_reconciliation/tests.rs": {
+ "bytes": 6878,
+ "git_blob": "6d0d08c446de5fc16598de6475020d270de77aa5",
+ "mode": "100644",
+ "sha256": "dab08b5c62c89ab3f178b859c6229aef16d4cf844f6990f5c087481b773dfa85"
+ },
"core/crates/tera_core/src/runtime/product_surface/settings.rs": {
"bytes": 57049,
"git_blob": "5ffe3e45ee9d48382f5453b066eaeb166b00b407",
@@ -2267,13 +2279,13 @@
"sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d"
},
"test-fixtures/legacy-identifiers.v1.json": {
- "bytes": 151542,
- "git_blob": "854f586ecad54a22665f64d44d4843b39c66a50c",
+ "bytes": 152102,
+ "git_blob": "9801e3a668e27cf4b8e934599ae2d89b2535a9e4",
"mode": "100644",
- "sha256": "66dec6eb2296cee4c29333a58645ab3686442c34577c36bd2b8344b0bf5c32f2"
+ "sha256": "b39f1ef08ee92d472f63327675b46de673f3386d77ce93c1afe23c2f1214ee47"
}
},
"policy": "staged_inputs",
- "tree": "207953b663237a6397cdd564c488e1491dbfde76"
+ "tree": "3e612b493a705f30a2e9ad8ae12a8e320b5f884c"
}
}
diff --git a/TeraFFI/source/aarch64-apple-ios.json b/TeraFFI/source/aarch64-apple-ios.json
@@ -641,10 +641,10 @@
"sha256": "dff55e46521c26f5f0ece024453b55c15f132fa94f78e2cba3d141f123d4eca0"
},
"core/crates/tera_core/src/runtime/product_surface.rs": {
- "bytes": 8364,
- "git_blob": "76c094ffe07eb95958c3559e13a6e8569ed51985",
+ "bytes": 8431,
+ "git_blob": "7fe0757ff6467b31fa087c25485e46b280b29ac6",
"mode": "100644",
- "sha256": "adcde8696fd8144f13213388f68f1c3d6cc19c31f08121f024281a99e9e05ec8"
+ "sha256": "67351a02a9d45f216212aa05e52ac0d5c36b74d7787731fb1bcfaecc69b1cec2"
},
"core/crates/tera_core/src/runtime/product_surface/authoring.rs": {
"bytes": 11282,
@@ -904,6 +904,18 @@
"mode": "100644",
"sha256": "8e0c1fedaa2ae6ceab717b5d44918be49683ac23a75a6c931fbca15013a36322"
},
+ "core/crates/tera_core/src/runtime/product_surface/recovery_reconciliation.rs": {
+ "bytes": 6212,
+ "git_blob": "63647bd304a3fd9dd51bc0930b7826eb96efe62e",
+ "mode": "100644",
+ "sha256": "655b9e8405a9a53c0e1657648a5d9d7a6a223a9d99c788c21afe5b6a8fc8242b"
+ },
+ "core/crates/tera_core/src/runtime/product_surface/recovery_reconciliation/tests.rs": {
+ "bytes": 6878,
+ "git_blob": "6d0d08c446de5fc16598de6475020d270de77aa5",
+ "mode": "100644",
+ "sha256": "dab08b5c62c89ab3f178b859c6229aef16d4cf844f6990f5c087481b773dfa85"
+ },
"core/crates/tera_core/src/runtime/product_surface/settings.rs": {
"bytes": 57049,
"git_blob": "5ffe3e45ee9d48382f5453b066eaeb166b00b407",
@@ -2267,13 +2279,13 @@
"sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d"
},
"test-fixtures/legacy-identifiers.v1.json": {
- "bytes": 151542,
- "git_blob": "854f586ecad54a22665f64d44d4843b39c66a50c",
+ "bytes": 152102,
+ "git_blob": "9801e3a668e27cf4b8e934599ae2d89b2535a9e4",
"mode": "100644",
- "sha256": "66dec6eb2296cee4c29333a58645ab3686442c34577c36bd2b8344b0bf5c32f2"
+ "sha256": "b39f1ef08ee92d472f63327675b46de673f3386d77ce93c1afe23c2f1214ee47"
}
},
"policy": "staged_inputs",
- "tree": "207953b663237a6397cdd564c488e1491dbfde76"
+ "tree": "3e612b493a705f30a2e9ad8ae12a8e320b5f884c"
}
}
diff --git a/core/crates/tera_core/src/runtime/product_surface.rs b/core/crates/tera_core/src/runtime/product_surface.rs
@@ -25,6 +25,8 @@ mod ranking;
#[cfg(feature = "mobile-social")]
pub mod recovery_inventory;
#[cfg(feature = "mobile-social")]
+pub mod recovery_reconciliation;
+#[cfg(feature = "mobile-social")]
mod settings;
#[cfg(feature = "mobile-social")]
mod submission;
diff --git a/core/crates/tera_core/src/runtime/product_surface/recovery_reconciliation.rs b/core/crates/tera_core/src/runtime/product_surface/recovery_reconciliation.rs
@@ -0,0 +1,182 @@
+//! Pure decisions over one transfer's authoritative facts. An action is a
+//! request to the existing owner, never evidence that an effect has happened.
+//! Callers re-read facts and validate the stored response/owned bytes before
+//! completion, and require durable Rust success before native settlement.
+
+use radroots_blossom::{BlobUrl, MediaType};
+use radroots_identity::PublicKey;
+use radroots_signing::SigningOperationId;
+use radroots_storage::authored_draft::AuthoredDraftId;
+
+use super::Phase1DraftError;
+
+// Match the native destination and individual header admission ceilings.
+// Use byte bounds so retained canonical text has an explicit memory ceiling.
+pub const RECOVERY_URL_MAX_BYTES: usize = 4096;
+pub const RECOVERY_MEDIA_TYPE_MAX_BYTES: usize = 8192;
+
+/// Frozen association, not the current editable draft or current settings.
+/// The canonical blob URL includes the exact hash, origin and path. The attempt
+/// is the persisted signing operation, not a newly minted recovery identifier.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct RecoveryAssociation {
+ author: PublicKey,
+ parent: AuthoredDraftId,
+ attempt: SigningOperationId,
+ canonical_url: BlobUrl,
+ media_type: MediaType,
+ byte_size: u64,
+}
+
+impl RecoveryAssociation {
+ pub fn new(
+ author: PublicKey,
+ parent: AuthoredDraftId,
+ attempt: SigningOperationId,
+ canonical_url: BlobUrl,
+ media_type: MediaType,
+ byte_size: u64,
+ ) -> Result<Self, Phase1DraftError> {
+ if byte_size == 0
+ || canonical_url.as_str().len() > RECOVERY_URL_MAX_BYTES
+ || media_type.as_str().len() > RECOVERY_MEDIA_TYPE_MAX_BYTES
+ {
+ return Err(Phase1DraftError::InvalidMedia);
+ }
+ Ok(Self {
+ author,
+ parent,
+ attempt,
+ canonical_url,
+ media_type,
+ byte_size,
+ })
+ }
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum RustCompletion {
+ Pending,
+ /// Only a validated durable journal result establishes this fact.
+ Verified,
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum RecoveryParent {
+ /// Includes absence from a displayed page. Exact lookup has not run yet.
+ Unqueried,
+ ProtectedDataUnavailable,
+ StorageUnavailable,
+ /// Only an authoritative exact lookup may establish absence.
+ Missing,
+ InvalidRecord,
+ /// Historical records without an exact attempt must retain their evidence.
+ AssociationUnconfirmed,
+ Known {
+ association: Box<RecoveryAssociation>,
+ completion: RustCompletion,
+ },
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum NativeRecoveryState {
+ Active,
+ /// The stored response is available for validation, not already trusted.
+ ReceiptAvailable,
+ Settled,
+ /// Requires authoritative native reconciliation, not a timeout or an
+ /// isolated failed callback while an OS task might still be running.
+ DefinitivelyInactive,
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum NativeRecoveryEvidence {
+ Unknown,
+ /// Conflicting identity/body evidence is retained for isolated repair.
+ Conflicting,
+ Known {
+ association: Box<RecoveryAssociation>,
+ state: NativeRecoveryState,
+ },
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum RecoveryPause {
+ ProtectedData,
+ StorageUnavailable,
+ NativeActive,
+ RetryAuthorityRequired,
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum RecoveryRepair {
+ ParentMissing,
+ InvalidParent,
+ AssociationUnconfirmed,
+ ConflictingEvidence,
+ AssociationMismatch,
+ SettlementWithoutCompletion,
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum RecoveryDecision {
+ LookupParent,
+ QueryNative,
+ Pause(RecoveryPause),
+ Complete,
+ Settle,
+ Reconciled,
+ Quarantine(RecoveryRepair),
+}
+
+/// Constant work and memory for one exact association. No clock, mutable head
+/// revision, bearer token, upload, signing, storage write or native effect.
+pub fn reconcile(parent: &RecoveryParent, native: &NativeRecoveryEvidence) -> RecoveryDecision {
+ use RecoveryDecision as D;
+ let (expected, completion) = match parent {
+ RecoveryParent::Unqueried => return D::LookupParent,
+ RecoveryParent::ProtectedDataUnavailable => {
+ return D::Pause(RecoveryPause::ProtectedData);
+ }
+ RecoveryParent::StorageUnavailable => {
+ return D::Pause(RecoveryPause::StorageUnavailable);
+ }
+ RecoveryParent::Missing => return D::Quarantine(RecoveryRepair::ParentMissing),
+ RecoveryParent::InvalidRecord => return D::Quarantine(RecoveryRepair::InvalidParent),
+ RecoveryParent::AssociationUnconfirmed => {
+ return D::Quarantine(RecoveryRepair::AssociationUnconfirmed);
+ }
+ RecoveryParent::Known {
+ association,
+ completion,
+ } => (association, completion),
+ };
+ let (observed, state) = match native {
+ NativeRecoveryEvidence::Unknown => return D::QueryNative,
+ NativeRecoveryEvidence::Conflicting => {
+ return D::Quarantine(RecoveryRepair::ConflictingEvidence);
+ }
+ NativeRecoveryEvidence::Known { association, state } => (association, state),
+ };
+ if expected != observed {
+ return D::Quarantine(RecoveryRepair::AssociationMismatch);
+ }
+ match (completion, state) {
+ (RustCompletion::Pending, NativeRecoveryState::ReceiptAvailable) => D::Complete,
+ (RustCompletion::Verified, NativeRecoveryState::ReceiptAvailable) => D::Settle,
+ (RustCompletion::Verified, NativeRecoveryState::Settled) => D::Reconciled,
+ (RustCompletion::Pending, NativeRecoveryState::Settled) => {
+ D::Quarantine(RecoveryRepair::SettlementWithoutCompletion)
+ }
+ (RustCompletion::Pending, NativeRecoveryState::Active) => {
+ D::Pause(RecoveryPause::NativeActive)
+ }
+ (RustCompletion::Pending, NativeRecoveryState::DefinitivelyInactive) => {
+ D::Pause(RecoveryPause::RetryAuthorityRequired)
+ }
+ (RustCompletion::Verified, _) => D::QueryNative,
+ }
+}
+
+#[cfg(test)]
+mod tests;
diff --git a/core/crates/tera_core/src/runtime/product_surface/recovery_reconciliation/tests.rs b/core/crates/tera_core/src/runtime/product_surface/recovery_reconciliation/tests.rs
@@ -0,0 +1,208 @@
+use super::*;
+
+fn association() -> RecoveryAssociation {
+ RecoveryAssociation::new(
+ PublicKey::from_hex("79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798")
+ .unwrap(),
+ AuthoredDraftId::new([1; 16]).unwrap(),
+ SigningOperationId::new([2; 16]).unwrap(),
+ BlobUrl::parse(&format!("https://media.example/{}", "03".repeat(32))).unwrap(),
+ MediaType::parse("image/png").unwrap(),
+ 4096,
+ )
+ .unwrap()
+}
+
+fn parent(completion: RustCompletion) -> RecoveryParent {
+ RecoveryParent::Known {
+ association: Box::new(association()),
+ completion,
+ }
+}
+
+fn native(state: NativeRecoveryState) -> NativeRecoveryEvidence {
+ NativeRecoveryEvidence::Known {
+ association: Box::new(association()),
+ state,
+ }
+}
+
+#[test]
+fn structurally_valid_text_is_bounded_before_retention_and_comparison() {
+ let prefix = format!("https://media.example/{}.", "03".repeat(32));
+ let url =
+ |length| BlobUrl::parse(&format!("{prefix}{}", "a".repeat(length - prefix.len()))).unwrap();
+ let media_type = |length| {
+ let prefix = "image/png; x=";
+ let value =
+ MediaType::parse(&format!("{prefix}{}", "a".repeat(length - prefix.len()))).unwrap();
+ assert_eq!(value.as_str().len(), length);
+ value
+ };
+ let create = |url, media_type| {
+ let a = association();
+ RecoveryAssociation::new(a.author, a.parent, a.attempt, url, media_type, a.byte_size)
+ };
+ assert!(
+ create(
+ url(RECOVERY_URL_MAX_BYTES),
+ media_type(RECOVERY_MEDIA_TYPE_MAX_BYTES)
+ )
+ .is_ok()
+ );
+ assert!(
+ create(
+ url(RECOVERY_URL_MAX_BYTES + 1),
+ media_type(RECOVERY_MEDIA_TYPE_MAX_BYTES)
+ )
+ .is_err()
+ );
+ assert!(
+ create(
+ url(RECOVERY_URL_MAX_BYTES),
+ media_type(RECOVERY_MEDIA_TYPE_MAX_BYTES + 1)
+ )
+ .is_err()
+ );
+}
+
+#[test]
+fn both_commit_windows_and_every_native_state_have_explicit_repeatable_actions() {
+ use NativeRecoveryState as N;
+ use RecoveryDecision as D;
+ use RustCompletion as R;
+ let cases = [
+ (R::Pending, N::ReceiptAvailable, D::Complete),
+ (R::Verified, N::ReceiptAvailable, D::Settle),
+ (R::Verified, N::Settled, D::Reconciled),
+ (
+ R::Pending,
+ N::Settled,
+ D::Quarantine(RecoveryRepair::SettlementWithoutCompletion),
+ ),
+ (R::Pending, N::Active, D::Pause(RecoveryPause::NativeActive)),
+ (R::Verified, N::Active, D::QueryNative),
+ (
+ R::Pending,
+ N::DefinitivelyInactive,
+ D::Pause(RecoveryPause::RetryAuthorityRequired),
+ ),
+ (R::Verified, N::DefinitivelyInactive, D::QueryNative),
+ ];
+ for (rust, state, expected) in cases {
+ let parent = parent(rust);
+ let native = native(state);
+ let original = (parent.clone(), native.clone());
+ for _ in 0..3 {
+ assert_eq!(reconcile(&parent, &native), expected);
+ assert_eq!((&parent, &native), (&original.0, &original.1));
+ }
+ }
+}
+
+#[test]
+fn page_absence_requires_exact_lookup_and_unavailable_storage_is_never_corruption() {
+ use RecoveryDecision as D;
+ let cases = [
+ (RecoveryParent::Unqueried, D::LookupParent),
+ (
+ RecoveryParent::Missing,
+ D::Quarantine(RecoveryRepair::ParentMissing),
+ ),
+ (
+ RecoveryParent::InvalidRecord,
+ D::Quarantine(RecoveryRepair::InvalidParent),
+ ),
+ (
+ RecoveryParent::AssociationUnconfirmed,
+ D::Quarantine(RecoveryRepair::AssociationUnconfirmed),
+ ),
+ (
+ RecoveryParent::ProtectedDataUnavailable,
+ D::Pause(RecoveryPause::ProtectedData),
+ ),
+ (
+ RecoveryParent::StorageUnavailable,
+ D::Pause(RecoveryPause::StorageUnavailable),
+ ),
+ ];
+ for (parent, expected) in cases {
+ for native in [
+ NativeRecoveryEvidence::Unknown,
+ NativeRecoveryEvidence::Conflicting,
+ native(NativeRecoveryState::ReceiptAvailable),
+ native(NativeRecoveryState::Settled),
+ ] {
+ assert_eq!(reconcile(&parent, &native), expected);
+ }
+ }
+ for completion in [RustCompletion::Pending, RustCompletion::Verified] {
+ assert_eq!(
+ reconcile(&parent(completion), &NativeRecoveryEvidence::Unknown),
+ D::QueryNative
+ );
+ assert_eq!(
+ reconcile(&parent(completion), &NativeRecoveryEvidence::Conflicting),
+ D::Quarantine(RecoveryRepair::ConflictingEvidence)
+ );
+ }
+}
+
+#[test]
+fn every_frozen_identity_component_must_match_before_complete_or_settle() {
+ let original = association();
+ let mut mismatches = Vec::new();
+ let mut value = original.clone();
+ value.author =
+ PublicKey::from_hex("c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5")
+ .unwrap();
+ mismatches.push(value);
+ let mut value = original.clone();
+ value.parent = AuthoredDraftId::new([4; 16]).unwrap();
+ mismatches.push(value);
+ let mut value = original.clone();
+ value.attempt = SigningOperationId::new([5; 16]).unwrap();
+ mismatches.push(value);
+ let mut value = original.clone();
+ value.canonical_url =
+ BlobUrl::parse(&format!("https://media.example/{}", "06".repeat(32))).unwrap();
+ mismatches.push(value);
+ let mut value = original.clone();
+ value.canonical_url =
+ BlobUrl::parse(&format!("https://other.example/{}", "03".repeat(32))).unwrap();
+ mismatches.push(value);
+ let mut value = original.clone();
+ value.media_type = MediaType::parse("image/jpeg").unwrap();
+ mismatches.push(value);
+ let mut value = original.clone();
+ value.byte_size += 1;
+ mismatches.push(value);
+ for association in mismatches {
+ for completion in [RustCompletion::Pending, RustCompletion::Verified] {
+ for state in [
+ NativeRecoveryState::ReceiptAvailable,
+ NativeRecoveryState::Settled,
+ ] {
+ let native = NativeRecoveryEvidence::Known {
+ association: Box::new(association.clone()),
+ state,
+ };
+ assert_eq!(
+ reconcile(&parent(completion), &native),
+ RecoveryDecision::Quarantine(RecoveryRepair::AssociationMismatch)
+ );
+ }
+ }
+ }
+ assert!(
+ RecoveryAssociation::new(
+ original.author,
+ original.parent,
+ original.attempt,
+ original.canonical_url,
+ original.media_type,
+ 0
+ )
+ .is_err()
+ );
+}
diff --git a/release/provenance.json b/release/provenance.json
@@ -2,7 +2,7 @@
"artifacts": {
"app_api_sha256": "020924097c0d7efc33128cb8fd3d3b2026d95f57c44da71880e585aff80f070b",
"ffi_api_sha256": "12c54595ac3ebcb6aa22c7e968b019f67e1bc0606782ef4b71c601df6a54e764",
- "ffi_provenance_sha256": "6a1a360e7a13afc89008544a964c4d747bc563c6af28f19fb2ddeea301416d0c",
+ "ffi_provenance_sha256": "e581ed9a2a599bdeaae142f5786090be6a4c0073e9297bfc056268c5f7dd47fa",
"info_plist_sha256": "15ef08b1cdd1096cfb9eeaf5be5bf8f814807a7ca9350bbbb47860fa72ec13ef",
"privacy_manifest_sha256": "a331d51864743ebe4e00dd22360b4a538b6b3ac26a6b3eb54094e60a36959a12",
"sbom_sha256": "3670c97ee6ffc2c2abd3249885c89bd7c20ec43fedcb66442501a2f42d74fc2a",
@@ -22,7 +22,7 @@
"lib_revision": "91006304a47ff335c6f36fbbcfcb68ecfc76f6c7",
"source_date_epoch": 1787871027,
"swift_package_lock_sha256": "29d68f2242a7725bc38381520ec196803893bf689c8e6b848647e664d6262316",
- "tera_ffi_source_tree": "207953b663237a6397cdd564c488e1491dbfde76",
+ "tera_ffi_source_tree": "3e612b493a705f30a2e9ad8ae12a8e320b5f884c",
"xcode_package_lock_sha256": "c468f2b0469438387c519e1b2de424a118dfa9145ea07dcfd2bc5cc714ec8559"
},
"version": "0.1.0-alpha"
diff --git a/test-fixtures/legacy-identifiers.v1.json b/test-fixtures/legacy-identifiers.v1.json
@@ -4489,6 +4489,10 @@
"count": 1
},
{
+ "path": "core/crates/tera_core/src/runtime/product_surface/recovery_reconciliation.rs",
+ "count": 1
+ },
+ {
"path": "core/crates/tera_core/src/runtime/product_surface/submission/capture/media.rs",
"count": 1
},
@@ -4849,6 +4853,10 @@
"count": 1
},
{
+ "path": "core/crates/tera_core/src/runtime/product_surface/recovery_reconciliation.rs",
+ "count": 1
+ },
+ {
"path": "core/crates/tera_core/src/runtime/product_surface/settings.rs",
"count": 1
},
@@ -5305,6 +5313,10 @@
"count": 6
},
{
+ "path": "core/crates/tera_core/src/runtime/product_surface/recovery_reconciliation.rs",
+ "count": 1
+ },
+ {
"path": "core/crates/tera_core/src/runtime/product_surface/submission/intent.rs",
"count": 1
},
@@ -5419,6 +5431,10 @@
"count": 1
},
{
+ "path": "core/crates/tera_core/src/runtime/product_surface/recovery_reconciliation.rs",
+ "count": 1
+ },
+ {
"path": "core/crates/tera_core/src/runtime/product_surface/settings.rs",
"count": 3
},