commit f7981583fa474afafd60ce415768659f45455d95 parent 0f592663540ea913f5c577c66739259ce15cfdca Author: triesap <tyson@radroots.org> Date: Thu, 10 Sep 2026 08:23:49 +0000 calendar: Rebuild typed timing projections safely - Version derived timing through existing storage rebuild ownership - Resume interrupted rebuilds and reject corrupt or changed sources - Preserve signed history and pending authored operation identities - Qualify exact owned artifacts and current native consumers Diffstat:
20 files changed, 1257 insertions(+), 257 deletions(-)
diff --git a/TeraFFI/provenance.json b/TeraFFI/provenance.json @@ -22,9 +22,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 71610976, + "bytes": 72341568, "path": "TeraFFI.xcframework/ios-arm64-simulator/libtera_ffi.a", - "sha256": "662657eb7099db9d8402e9009bf943d4719511354262ab4eae66cecd93f25313" + "sha256": "173a06874740c47d606b73138907215ccbaf31ddf72baaacfab7774caea00a38" }, { "bytes": 71393, @@ -37,9 +37,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 71664464, + "bytes": 72390992, "path": "TeraFFI.xcframework/ios-arm64/libtera_ffi.a", - "sha256": "ab6e2ce40e0545a091c48d5a140f73bd909a59209f8e52c3210a92ca6ff3240b" + "sha256": "aed7ff51986b5445bd73aa896bc43d1789b74ccbdfadcaaa357cc5e3fcadd855" }, { "bytes": 42544, @@ -77,34 +77,34 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 20015200, + "bytes": 20369360, "path": "native/aarch64-apple-darwin/libtera_ffi.dylib", - "sha256": "fc797c39279c817724925b118d3a8b6207c6441826f665c8e391a07210078613" + "sha256": "8b81b0c9ba4246c716647c1c525e9611ef5e87cba393c9f2191359b9c9ca6778" }, { - "bytes": 71610976, + "bytes": 72341568, "path": "native/aarch64-apple-ios-sim/libtera_ffi.a", - "sha256": "662657eb7099db9d8402e9009bf943d4719511354262ab4eae66cecd93f25313" + "sha256": "173a06874740c47d606b73138907215ccbaf31ddf72baaacfab7774caea00a38" }, { - "bytes": 71664464, + "bytes": 72390992, "path": "native/aarch64-apple-ios/libtera_ffi.a", - "sha256": "ab6e2ce40e0545a091c48d5a140f73bd909a59209f8e52c3210a92ca6ff3240b" + "sha256": "aed7ff51986b5445bd73aa896bc43d1789b74ccbdfadcaaa357cc5e3fcadd855" }, { - "bytes": 67809, + "bytes": 68702, "path": "source/aarch64-apple-darwin.json", - "sha256": "ecc80f8d4ea4c5902255fef11d1510a8f8831feb20ce5dffa8dbe4224bed3de1" + "sha256": "bd15bfbef20628525b37db7abfe1534f939df7e69492067e05fe69440476fdd0" }, { - "bytes": 67653, + "bytes": 68546, "path": "source/aarch64-apple-ios-sim.json", - "sha256": "c4763194f42dfb1ec1990c1b2d816000ef627a1f3bc1302f5ffb3520ebfb361f" + "sha256": "e7a16b34cd41576ff4ebae5e9564df84eb93125997e6d4851bf7f71422b0044b" }, { - "bytes": 67649, + "bytes": 68542, "path": "source/aarch64-apple-ios.json", - "sha256": "162b977786728a10de19c7c17ae6a115ebde1a9bcd78ea8db2cd7a9ee81cc0cd" + "sha256": "7b5dd5b4e98a99d6fcdf12cc8c94ea3939fef1a3f5820583bbcb51e388eaf88b" } ], "language": "swift", @@ -112,7 +112,7 @@ "schema": "radroots.artifact-manifest.v2", "source": { "repository": "https://github.com/radrootslabs/tera", - "tree": "d2c902b79291f9ced96cd77f989c7812bbc9c4b2" + "tree": "a2debc6a4ad04ea84e3d93b10649c3e9562de36a" }, "source_records": { "aarch64-apple-darwin": "source/aarch64-apple-darwin.json", @@ -139,9 +139,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 71610976, + "bytes": 72341568, "path": "Tera/Frameworks/TeraFFI.xcframework/ios-arm64-simulator/libtera_ffi.a", - "sha256": "662657eb7099db9d8402e9009bf943d4719511354262ab4eae66cecd93f25313" + "sha256": "173a06874740c47d606b73138907215ccbaf31ddf72baaacfab7774caea00a38" }, { "bytes": 71393, @@ -154,9 +154,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 71664464, + "bytes": 72390992, "path": "Tera/Frameworks/TeraFFI.xcframework/ios-arm64/libtera_ffi.a", - "sha256": "ab6e2ce40e0545a091c48d5a140f73bd909a59209f8e52c3210a92ca6ff3240b" + "sha256": "aed7ff51986b5445bd73aa896bc43d1789b74ccbdfadcaaa357cc5e3fcadd855" }, { "bytes": 512103, @@ -174,19 +174,19 @@ "sha256": "4cc03b03dd54db7b4ab22684ff819233444ae0a43a770ab875148892bed1e3f2" }, { - "bytes": 67809, + "bytes": 68702, "path": "TeraFFI/source/aarch64-apple-darwin.json", - "sha256": "ecc80f8d4ea4c5902255fef11d1510a8f8831feb20ce5dffa8dbe4224bed3de1" + "sha256": "bd15bfbef20628525b37db7abfe1534f939df7e69492067e05fe69440476fdd0" }, { - "bytes": 67653, + "bytes": 68546, "path": "TeraFFI/source/aarch64-apple-ios-sim.json", - "sha256": "c4763194f42dfb1ec1990c1b2d816000ef627a1f3bc1302f5ffb3520ebfb361f" + "sha256": "e7a16b34cd41576ff4ebae5e9564df84eb93125997e6d4851bf7f71422b0044b" }, { - "bytes": 67649, + "bytes": 68542, "path": "TeraFFI/source/aarch64-apple-ios.json", - "sha256": "162b977786728a10de19c7c17ae6a115ebde1a9bcd78ea8db2cd7a9ee81cc0cd" + "sha256": "7b5dd5b4e98a99d6fcdf12cc8c94ea3939fef1a3f5820583bbcb51e388eaf88b" } ], "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 = "d2c902b79291f9ced96cd77f989c7812bbc9c4b2" -manifest_sha256 = "fe8ac77f19b400cb4db0a007ac3ffc96f12db882f8eb6a9723b3b5608a43b065" +source_tree = "a2debc6a4ad04ea84e3d93b10649c3e9562de36a" +manifest_sha256 = "5dfd6f6c479bf55e985a9881826da5021080c3e198d3681cf32eb8cf2fb82fa3" source_date_epoch = 1787871027 [foundation] diff --git a/TeraFFI/source/aarch64-apple-darwin.json b/TeraFFI/source/aarch64-apple-darwin.json @@ -547,10 +547,10 @@ "sha256": "e001263dc1954463105b8668eb3cfb7b8142f2dd3d78430d96de40e0c1c1e0bc" }, "core/crates/tera_core/Cargo.toml": { - "bytes": 2426, - "git_blob": "d4e55ae9a02998a487b9eddab5fdabd25194b97c", + "bytes": 2394, + "git_blob": "de3ab08fb3fd77d73793e64a6267b38358fa28a1", "mode": "100644", - "sha256": "c50e87c9b170f42545fe7fc6efba929c89fbfdfa3d8b6501aa88d17bc8d41937" + "sha256": "691f29208fea38201e9c44f1d8862e28e8e324f1130e5d96ac26cc706bafa51a" }, "core/crates/tera_core/build.rs": { "bytes": 2056, @@ -625,10 +625,10 @@ "sha256": "c25bafe3b7228d8c9dc680ba30aa87d3e1f5fe1468484c920d29739de895d5f8" }, "core/crates/tera_core/src/runtime/mod.rs": { - "bytes": 8940, - "git_blob": "bb3aa67aff4080346d414983238187fa5b011def", + "bytes": 9046, + "git_blob": "1c16fc76d4c27cfb8136a43ff3425bd6eacf2079", "mode": "100644", - "sha256": "fb9ce7e0805a39b6723b31aafc5b6e1999f5f759606c03d4c463cab135f6e0d6" + "sha256": "8f119512dff5d8176f614bd990ff37c175bd6a847a54c2690c1920147000c48a" }, "core/crates/tera_core/src/runtime/mutation_admission.rs": { "bytes": 2148, @@ -660,6 +660,12 @@ "mode": "100644", "sha256": "fc900f2ae0802c47ad5e7e9bb19cc51692ce5454b2ffa831e8f59b2dc6e5f9c2" }, + "core/crates/tera_core/src/runtime/product_surface/calendar_v1_fixture.json": { + "bytes": 2292, + "git_blob": "397a1286f5bf761a0e513683899c69b1f511ebae", + "mode": "100644", + "sha256": "79fd90c240c67137232e6278e69f87a2159b6e078eb99123823a4fc11ce8fcc8" + }, "core/crates/tera_core/src/runtime/product_surface/context.rs": { "bytes": 14211, "git_blob": "92498807101c638cec675e0b49fde1f4ceed17a2", @@ -697,10 +703,10 @@ "sha256": "3cdca11226903c23bed81dc796338061922df620163b8f06edeb8bc278794a00" }, "core/crates/tera_core/src/runtime/product_surface/model.rs": { - "bytes": 8476, - "git_blob": "588ca0d0707f8eb4f17e705374dfb231b1b2920f", + "bytes": 8154, + "git_blob": "c5c977ac218efed07535d369858e95cc41392595", "mode": "100644", - "sha256": "b8a35018873e2d09096c1b4df8564c81f2837dcbcf1ad972e49fad1a59b524a6" + "sha256": "68b7adfb1bea5f51f8a12c70f9cea659c8a2bd6e1c93d853193fbcc1bb22877d" }, "core/crates/tera_core/src/runtime/product_surface/outbox.rs": { "bytes": 170884, @@ -715,10 +721,10 @@ "sha256": "3c048e44551d04e607597761481c24e9bbe55f6b20358047d18c8013c093fb8f" }, "core/crates/tera_core/src/runtime/product_surface/projection.rs": { - "bytes": 23489, - "git_blob": "77795e05cdf3be698f3d94b0c7e828a529aae38d", + "bytes": 23423, + "git_blob": "a0186bb29cafbaab4268af10acd5dc701cf4c228", "mode": "100644", - "sha256": "b3b7ade140749f6ec02ae7c5c2a94bb8c6b9b53a83a7b91e5945f107201a4196" + "sha256": "06923cd1020ac2609cf78f9b25b6def7a486a6d07583d25e0b80f34c7c287ec5" }, "core/crates/tera_core/src/runtime/product_surface/ranking.rs": { "bytes": 9781, @@ -733,16 +739,16 @@ "sha256": "76e3bddfc237dac63293d264d68991b489585136fe377e39dbba72aa3a1181f4" }, "core/crates/tera_core/src/runtime/product_surface/today.rs": { - "bytes": 143260, - "git_blob": "6fcffcc89b755356f65cff7abcfe57b6db005ec8", + "bytes": 144916, + "git_blob": "9d5f46ec80116b7f06e7ad700e6c9d3a8d5b0cd1", "mode": "100644", - "sha256": "b4fd53f5c264500bc9a0a4e09e2f56904e41145ef75931d64fc6f2d46bf33735" + "sha256": "b8254f384ca644d50f9065c316822fb8d67058b993d553078a9fd41830440406" }, "core/crates/tera_core/src/runtime/product_surface/today_admission_tests.rs": { - "bytes": 14686, - "git_blob": "6c35c696bcc2bf0fe4c69ce7efe93a0e820ce913", + "bytes": 14697, + "git_blob": "e234b16035b10855c8a24509160b2b9be2910e8b", "mode": "100644", - "sha256": "f5fd5da41129a2ec657e53b61b67d45ee789a4f981438f1d01bdacfac2042877" + "sha256": "fa6d019f83241eaaaae4a76e6172a2411dba74c5e7c3405eee5a9000ec8cd923" }, "core/crates/tera_core/src/runtime/product_surface/today_backfill_cursor.rs": { "bytes": 6898, @@ -756,11 +762,23 @@ "mode": "100644", "sha256": "6f5810256ca221ca66ec4056e07fc635103e0ed02da01f5a8c53161535d7eaf6" }, + "core/crates/tera_core/src/runtime/product_surface/today_calendar_migration.rs": { + "bytes": 10664, + "git_blob": "74c7bab33cc45bed2867f3b18525242fb3c38a4a", + "mode": "100644", + "sha256": "fa01f28b24461f35effe7a943a44aba8381c547b8949dd59d3b82f211124afee" + }, + "core/crates/tera_core/src/runtime/product_surface/today_calendar_migration_tests.rs": { + "bytes": 22085, + "git_blob": "14b7d497da9c5163de06f25edbaf984032873ba9", + "mode": "100644", + "sha256": "5535f00beb7ae9e59203fca46b25d6af9c926391e4843d6e324b84bf5dbcc84b" + }, "core/crates/tera_core/src/runtime/product_surface/today_calendar_tests.rs": { - "bytes": 6463, - "git_blob": "252dd511bc8b9d52b9781b7babb1b8828d86919d", + "bytes": 3058, + "git_blob": "797d66a2d6c61961f105478b679fd99bb2bb811f", "mode": "100644", - "sha256": "ff0fc470ef3e3274d57c88f078ceab88059636fb25aeeccae3c6f2a06e092eec" + "sha256": "837b68c0ab4d0cb9b59eb2cd1a7d86f29d7191f26bb79013ca7ccf60e3f83c51" }, "core/crates/tera_core/src/runtime/product_surface/today_counted_storage_tests.rs": { "bytes": 17547, @@ -817,10 +835,10 @@ "sha256": "eb611943b487bc0a48428e0c8939a51d3d2f583f791eb9a734eab186860c5aab" }, "core/crates/tera_core/src/runtime/product_surface/today_sync_tests.rs": { - "bytes": 9345, - "git_blob": "9a287720eb5af56c8ca0e008122c6c24b171d31c", + "bytes": 9396, + "git_blob": "e006513ec9cdf3f9b6d9c4b99206d5da5f2fbeaf", "mode": "100644", - "sha256": "fded0ec08844f644ea5544a2f1bef9671b5e1b6b1c47be3604db93bd2ca47e11" + "sha256": "0e842eb3f95a70e3a585ba1c816d948e65f0600580eefd26ae8ee254e5fc58e3" }, "core/crates/tera_core/src/runtime/product_surface/today_visibility_tests.rs": { "bytes": 13383, @@ -883,10 +901,10 @@ "sha256": "66c8c8d289b8bc3c89ac4f43f42ba17d24282cf58ac4b196d35d1285cc944e61" }, "core/crates/tera_ffi/src/error.rs": { - "bytes": 20215, - "git_blob": "74cd2baef3245474d56ce8033281b6be23e2b595", + "bytes": 20401, + "git_blob": "aab533ffa446c092154ad871536454fd62bfbee4", "mode": "100644", - "sha256": "2a2b0ee85005a41d30a331ef2a02f9f8ded1ee0480493307fced379452d963cb" + "sha256": "10ac830f5df81e86f1963dff96a9df7450a08afc4952c81000479b8d56e44adc" }, "core/crates/tera_ffi/src/invalidation.rs": { "bytes": 6914, @@ -1459,13 +1477,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 118536, - "git_blob": "fafa6f76026449cd0cc47f458e31dd067a94f38b", + "bytes": 120097, + "git_blob": "cd3274ed9dace12bc2a0c065630e0a5210a2e24a", "mode": "100644", - "sha256": "201494e468ffafea52652ffecab89bfa16399e0c4b6e5bed5617853be7e49fe5" + "sha256": "9f45f1964b2c53cf60fd3b536cadd628b6e67e7ab56c9edebbda917402e1507f" } }, "policy": "staged_inputs", - "tree": "d2c902b79291f9ced96cd77f989c7812bbc9c4b2" + "tree": "a2debc6a4ad04ea84e3d93b10649c3e9562de36a" } } diff --git a/TeraFFI/source/aarch64-apple-ios-sim.json b/TeraFFI/source/aarch64-apple-ios-sim.json @@ -543,10 +543,10 @@ "sha256": "e001263dc1954463105b8668eb3cfb7b8142f2dd3d78430d96de40e0c1c1e0bc" }, "core/crates/tera_core/Cargo.toml": { - "bytes": 2426, - "git_blob": "d4e55ae9a02998a487b9eddab5fdabd25194b97c", + "bytes": 2394, + "git_blob": "de3ab08fb3fd77d73793e64a6267b38358fa28a1", "mode": "100644", - "sha256": "c50e87c9b170f42545fe7fc6efba929c89fbfdfa3d8b6501aa88d17bc8d41937" + "sha256": "691f29208fea38201e9c44f1d8862e28e8e324f1130e5d96ac26cc706bafa51a" }, "core/crates/tera_core/build.rs": { "bytes": 2056, @@ -621,10 +621,10 @@ "sha256": "c25bafe3b7228d8c9dc680ba30aa87d3e1f5fe1468484c920d29739de895d5f8" }, "core/crates/tera_core/src/runtime/mod.rs": { - "bytes": 8940, - "git_blob": "bb3aa67aff4080346d414983238187fa5b011def", + "bytes": 9046, + "git_blob": "1c16fc76d4c27cfb8136a43ff3425bd6eacf2079", "mode": "100644", - "sha256": "fb9ce7e0805a39b6723b31aafc5b6e1999f5f759606c03d4c463cab135f6e0d6" + "sha256": "8f119512dff5d8176f614bd990ff37c175bd6a847a54c2690c1920147000c48a" }, "core/crates/tera_core/src/runtime/mutation_admission.rs": { "bytes": 2148, @@ -656,6 +656,12 @@ "mode": "100644", "sha256": "fc900f2ae0802c47ad5e7e9bb19cc51692ce5454b2ffa831e8f59b2dc6e5f9c2" }, + "core/crates/tera_core/src/runtime/product_surface/calendar_v1_fixture.json": { + "bytes": 2292, + "git_blob": "397a1286f5bf761a0e513683899c69b1f511ebae", + "mode": "100644", + "sha256": "79fd90c240c67137232e6278e69f87a2159b6e078eb99123823a4fc11ce8fcc8" + }, "core/crates/tera_core/src/runtime/product_surface/context.rs": { "bytes": 14211, "git_blob": "92498807101c638cec675e0b49fde1f4ceed17a2", @@ -693,10 +699,10 @@ "sha256": "3cdca11226903c23bed81dc796338061922df620163b8f06edeb8bc278794a00" }, "core/crates/tera_core/src/runtime/product_surface/model.rs": { - "bytes": 8476, - "git_blob": "588ca0d0707f8eb4f17e705374dfb231b1b2920f", + "bytes": 8154, + "git_blob": "c5c977ac218efed07535d369858e95cc41392595", "mode": "100644", - "sha256": "b8a35018873e2d09096c1b4df8564c81f2837dcbcf1ad972e49fad1a59b524a6" + "sha256": "68b7adfb1bea5f51f8a12c70f9cea659c8a2bd6e1c93d853193fbcc1bb22877d" }, "core/crates/tera_core/src/runtime/product_surface/outbox.rs": { "bytes": 170884, @@ -711,10 +717,10 @@ "sha256": "3c048e44551d04e607597761481c24e9bbe55f6b20358047d18c8013c093fb8f" }, "core/crates/tera_core/src/runtime/product_surface/projection.rs": { - "bytes": 23489, - "git_blob": "77795e05cdf3be698f3d94b0c7e828a529aae38d", + "bytes": 23423, + "git_blob": "a0186bb29cafbaab4268af10acd5dc701cf4c228", "mode": "100644", - "sha256": "b3b7ade140749f6ec02ae7c5c2a94bb8c6b9b53a83a7b91e5945f107201a4196" + "sha256": "06923cd1020ac2609cf78f9b25b6def7a486a6d07583d25e0b80f34c7c287ec5" }, "core/crates/tera_core/src/runtime/product_surface/ranking.rs": { "bytes": 9781, @@ -729,16 +735,16 @@ "sha256": "76e3bddfc237dac63293d264d68991b489585136fe377e39dbba72aa3a1181f4" }, "core/crates/tera_core/src/runtime/product_surface/today.rs": { - "bytes": 143260, - "git_blob": "6fcffcc89b755356f65cff7abcfe57b6db005ec8", + "bytes": 144916, + "git_blob": "9d5f46ec80116b7f06e7ad700e6c9d3a8d5b0cd1", "mode": "100644", - "sha256": "b4fd53f5c264500bc9a0a4e09e2f56904e41145ef75931d64fc6f2d46bf33735" + "sha256": "b8254f384ca644d50f9065c316822fb8d67058b993d553078a9fd41830440406" }, "core/crates/tera_core/src/runtime/product_surface/today_admission_tests.rs": { - "bytes": 14686, - "git_blob": "6c35c696bcc2bf0fe4c69ce7efe93a0e820ce913", + "bytes": 14697, + "git_blob": "e234b16035b10855c8a24509160b2b9be2910e8b", "mode": "100644", - "sha256": "f5fd5da41129a2ec657e53b61b67d45ee789a4f981438f1d01bdacfac2042877" + "sha256": "fa6d019f83241eaaaae4a76e6172a2411dba74c5e7c3405eee5a9000ec8cd923" }, "core/crates/tera_core/src/runtime/product_surface/today_backfill_cursor.rs": { "bytes": 6898, @@ -752,11 +758,23 @@ "mode": "100644", "sha256": "6f5810256ca221ca66ec4056e07fc635103e0ed02da01f5a8c53161535d7eaf6" }, + "core/crates/tera_core/src/runtime/product_surface/today_calendar_migration.rs": { + "bytes": 10664, + "git_blob": "74c7bab33cc45bed2867f3b18525242fb3c38a4a", + "mode": "100644", + "sha256": "fa01f28b24461f35effe7a943a44aba8381c547b8949dd59d3b82f211124afee" + }, + "core/crates/tera_core/src/runtime/product_surface/today_calendar_migration_tests.rs": { + "bytes": 22085, + "git_blob": "14b7d497da9c5163de06f25edbaf984032873ba9", + "mode": "100644", + "sha256": "5535f00beb7ae9e59203fca46b25d6af9c926391e4843d6e324b84bf5dbcc84b" + }, "core/crates/tera_core/src/runtime/product_surface/today_calendar_tests.rs": { - "bytes": 6463, - "git_blob": "252dd511bc8b9d52b9781b7babb1b8828d86919d", + "bytes": 3058, + "git_blob": "797d66a2d6c61961f105478b679fd99bb2bb811f", "mode": "100644", - "sha256": "ff0fc470ef3e3274d57c88f078ceab88059636fb25aeeccae3c6f2a06e092eec" + "sha256": "837b68c0ab4d0cb9b59eb2cd1a7d86f29d7191f26bb79013ca7ccf60e3f83c51" }, "core/crates/tera_core/src/runtime/product_surface/today_counted_storage_tests.rs": { "bytes": 17547, @@ -813,10 +831,10 @@ "sha256": "eb611943b487bc0a48428e0c8939a51d3d2f583f791eb9a734eab186860c5aab" }, "core/crates/tera_core/src/runtime/product_surface/today_sync_tests.rs": { - "bytes": 9345, - "git_blob": "9a287720eb5af56c8ca0e008122c6c24b171d31c", + "bytes": 9396, + "git_blob": "e006513ec9cdf3f9b6d9c4b99206d5da5f2fbeaf", "mode": "100644", - "sha256": "fded0ec08844f644ea5544a2f1bef9671b5e1b6b1c47be3604db93bd2ca47e11" + "sha256": "0e842eb3f95a70e3a585ba1c816d948e65f0600580eefd26ae8ee254e5fc58e3" }, "core/crates/tera_core/src/runtime/product_surface/today_visibility_tests.rs": { "bytes": 13383, @@ -879,10 +897,10 @@ "sha256": "66c8c8d289b8bc3c89ac4f43f42ba17d24282cf58ac4b196d35d1285cc944e61" }, "core/crates/tera_ffi/src/error.rs": { - "bytes": 20215, - "git_blob": "74cd2baef3245474d56ce8033281b6be23e2b595", + "bytes": 20401, + "git_blob": "aab533ffa446c092154ad871536454fd62bfbee4", "mode": "100644", - "sha256": "2a2b0ee85005a41d30a331ef2a02f9f8ded1ee0480493307fced379452d963cb" + "sha256": "10ac830f5df81e86f1963dff96a9df7450a08afc4952c81000479b8d56e44adc" }, "core/crates/tera_ffi/src/invalidation.rs": { "bytes": 6914, @@ -1455,13 +1473,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 118536, - "git_blob": "fafa6f76026449cd0cc47f458e31dd067a94f38b", + "bytes": 120097, + "git_blob": "cd3274ed9dace12bc2a0c065630e0a5210a2e24a", "mode": "100644", - "sha256": "201494e468ffafea52652ffecab89bfa16399e0c4b6e5bed5617853be7e49fe5" + "sha256": "9f45f1964b2c53cf60fd3b536cadd628b6e67e7ab56c9edebbda917402e1507f" } }, "policy": "staged_inputs", - "tree": "d2c902b79291f9ced96cd77f989c7812bbc9c4b2" + "tree": "a2debc6a4ad04ea84e3d93b10649c3e9562de36a" } } diff --git a/TeraFFI/source/aarch64-apple-ios.json b/TeraFFI/source/aarch64-apple-ios.json @@ -543,10 +543,10 @@ "sha256": "e001263dc1954463105b8668eb3cfb7b8142f2dd3d78430d96de40e0c1c1e0bc" }, "core/crates/tera_core/Cargo.toml": { - "bytes": 2426, - "git_blob": "d4e55ae9a02998a487b9eddab5fdabd25194b97c", + "bytes": 2394, + "git_blob": "de3ab08fb3fd77d73793e64a6267b38358fa28a1", "mode": "100644", - "sha256": "c50e87c9b170f42545fe7fc6efba929c89fbfdfa3d8b6501aa88d17bc8d41937" + "sha256": "691f29208fea38201e9c44f1d8862e28e8e324f1130e5d96ac26cc706bafa51a" }, "core/crates/tera_core/build.rs": { "bytes": 2056, @@ -621,10 +621,10 @@ "sha256": "c25bafe3b7228d8c9dc680ba30aa87d3e1f5fe1468484c920d29739de895d5f8" }, "core/crates/tera_core/src/runtime/mod.rs": { - "bytes": 8940, - "git_blob": "bb3aa67aff4080346d414983238187fa5b011def", + "bytes": 9046, + "git_blob": "1c16fc76d4c27cfb8136a43ff3425bd6eacf2079", "mode": "100644", - "sha256": "fb9ce7e0805a39b6723b31aafc5b6e1999f5f759606c03d4c463cab135f6e0d6" + "sha256": "8f119512dff5d8176f614bd990ff37c175bd6a847a54c2690c1920147000c48a" }, "core/crates/tera_core/src/runtime/mutation_admission.rs": { "bytes": 2148, @@ -656,6 +656,12 @@ "mode": "100644", "sha256": "fc900f2ae0802c47ad5e7e9bb19cc51692ce5454b2ffa831e8f59b2dc6e5f9c2" }, + "core/crates/tera_core/src/runtime/product_surface/calendar_v1_fixture.json": { + "bytes": 2292, + "git_blob": "397a1286f5bf761a0e513683899c69b1f511ebae", + "mode": "100644", + "sha256": "79fd90c240c67137232e6278e69f87a2159b6e078eb99123823a4fc11ce8fcc8" + }, "core/crates/tera_core/src/runtime/product_surface/context.rs": { "bytes": 14211, "git_blob": "92498807101c638cec675e0b49fde1f4ceed17a2", @@ -693,10 +699,10 @@ "sha256": "3cdca11226903c23bed81dc796338061922df620163b8f06edeb8bc278794a00" }, "core/crates/tera_core/src/runtime/product_surface/model.rs": { - "bytes": 8476, - "git_blob": "588ca0d0707f8eb4f17e705374dfb231b1b2920f", + "bytes": 8154, + "git_blob": "c5c977ac218efed07535d369858e95cc41392595", "mode": "100644", - "sha256": "b8a35018873e2d09096c1b4df8564c81f2837dcbcf1ad972e49fad1a59b524a6" + "sha256": "68b7adfb1bea5f51f8a12c70f9cea659c8a2bd6e1c93d853193fbcc1bb22877d" }, "core/crates/tera_core/src/runtime/product_surface/outbox.rs": { "bytes": 170884, @@ -711,10 +717,10 @@ "sha256": "3c048e44551d04e607597761481c24e9bbe55f6b20358047d18c8013c093fb8f" }, "core/crates/tera_core/src/runtime/product_surface/projection.rs": { - "bytes": 23489, - "git_blob": "77795e05cdf3be698f3d94b0c7e828a529aae38d", + "bytes": 23423, + "git_blob": "a0186bb29cafbaab4268af10acd5dc701cf4c228", "mode": "100644", - "sha256": "b3b7ade140749f6ec02ae7c5c2a94bb8c6b9b53a83a7b91e5945f107201a4196" + "sha256": "06923cd1020ac2609cf78f9b25b6def7a486a6d07583d25e0b80f34c7c287ec5" }, "core/crates/tera_core/src/runtime/product_surface/ranking.rs": { "bytes": 9781, @@ -729,16 +735,16 @@ "sha256": "76e3bddfc237dac63293d264d68991b489585136fe377e39dbba72aa3a1181f4" }, "core/crates/tera_core/src/runtime/product_surface/today.rs": { - "bytes": 143260, - "git_blob": "6fcffcc89b755356f65cff7abcfe57b6db005ec8", + "bytes": 144916, + "git_blob": "9d5f46ec80116b7f06e7ad700e6c9d3a8d5b0cd1", "mode": "100644", - "sha256": "b4fd53f5c264500bc9a0a4e09e2f56904e41145ef75931d64fc6f2d46bf33735" + "sha256": "b8254f384ca644d50f9065c316822fb8d67058b993d553078a9fd41830440406" }, "core/crates/tera_core/src/runtime/product_surface/today_admission_tests.rs": { - "bytes": 14686, - "git_blob": "6c35c696bcc2bf0fe4c69ce7efe93a0e820ce913", + "bytes": 14697, + "git_blob": "e234b16035b10855c8a24509160b2b9be2910e8b", "mode": "100644", - "sha256": "f5fd5da41129a2ec657e53b61b67d45ee789a4f981438f1d01bdacfac2042877" + "sha256": "fa6d019f83241eaaaae4a76e6172a2411dba74c5e7c3405eee5a9000ec8cd923" }, "core/crates/tera_core/src/runtime/product_surface/today_backfill_cursor.rs": { "bytes": 6898, @@ -752,11 +758,23 @@ "mode": "100644", "sha256": "6f5810256ca221ca66ec4056e07fc635103e0ed02da01f5a8c53161535d7eaf6" }, + "core/crates/tera_core/src/runtime/product_surface/today_calendar_migration.rs": { + "bytes": 10664, + "git_blob": "74c7bab33cc45bed2867f3b18525242fb3c38a4a", + "mode": "100644", + "sha256": "fa01f28b24461f35effe7a943a44aba8381c547b8949dd59d3b82f211124afee" + }, + "core/crates/tera_core/src/runtime/product_surface/today_calendar_migration_tests.rs": { + "bytes": 22085, + "git_blob": "14b7d497da9c5163de06f25edbaf984032873ba9", + "mode": "100644", + "sha256": "5535f00beb7ae9e59203fca46b25d6af9c926391e4843d6e324b84bf5dbcc84b" + }, "core/crates/tera_core/src/runtime/product_surface/today_calendar_tests.rs": { - "bytes": 6463, - "git_blob": "252dd511bc8b9d52b9781b7babb1b8828d86919d", + "bytes": 3058, + "git_blob": "797d66a2d6c61961f105478b679fd99bb2bb811f", "mode": "100644", - "sha256": "ff0fc470ef3e3274d57c88f078ceab88059636fb25aeeccae3c6f2a06e092eec" + "sha256": "837b68c0ab4d0cb9b59eb2cd1a7d86f29d7191f26bb79013ca7ccf60e3f83c51" }, "core/crates/tera_core/src/runtime/product_surface/today_counted_storage_tests.rs": { "bytes": 17547, @@ -813,10 +831,10 @@ "sha256": "eb611943b487bc0a48428e0c8939a51d3d2f583f791eb9a734eab186860c5aab" }, "core/crates/tera_core/src/runtime/product_surface/today_sync_tests.rs": { - "bytes": 9345, - "git_blob": "9a287720eb5af56c8ca0e008122c6c24b171d31c", + "bytes": 9396, + "git_blob": "e006513ec9cdf3f9b6d9c4b99206d5da5f2fbeaf", "mode": "100644", - "sha256": "fded0ec08844f644ea5544a2f1bef9671b5e1b6b1c47be3604db93bd2ca47e11" + "sha256": "0e842eb3f95a70e3a585ba1c816d948e65f0600580eefd26ae8ee254e5fc58e3" }, "core/crates/tera_core/src/runtime/product_surface/today_visibility_tests.rs": { "bytes": 13383, @@ -879,10 +897,10 @@ "sha256": "66c8c8d289b8bc3c89ac4f43f42ba17d24282cf58ac4b196d35d1285cc944e61" }, "core/crates/tera_ffi/src/error.rs": { - "bytes": 20215, - "git_blob": "74cd2baef3245474d56ce8033281b6be23e2b595", + "bytes": 20401, + "git_blob": "aab533ffa446c092154ad871536454fd62bfbee4", "mode": "100644", - "sha256": "2a2b0ee85005a41d30a331ef2a02f9f8ded1ee0480493307fced379452d963cb" + "sha256": "10ac830f5df81e86f1963dff96a9df7450a08afc4952c81000479b8d56e44adc" }, "core/crates/tera_ffi/src/invalidation.rs": { "bytes": 6914, @@ -1455,13 +1473,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 118536, - "git_blob": "fafa6f76026449cd0cc47f458e31dd067a94f38b", + "bytes": 120097, + "git_blob": "cd3274ed9dace12bc2a0c065630e0a5210a2e24a", "mode": "100644", - "sha256": "201494e468ffafea52652ffecab89bfa16399e0c4b6e5bed5617853be7e49fe5" + "sha256": "9f45f1964b2c53cf60fd3b536cadd628b6e67e7ab56c9edebbda917402e1507f" } }, "policy": "staged_inputs", - "tree": "d2c902b79291f9ced96cd77f989c7812bbc9c4b2" + "tree": "a2debc6a4ad04ea84e3d93b10649c3e9562de36a" } } diff --git a/core/crates/tera_core/Cargo.toml b/core/crates/tera_core/Cargo.toml @@ -24,7 +24,6 @@ mobile-social = [ "radroots_sdk/blossom", "radroots_sdk/nostr", "radroots_sdk/sync", - "dep:tokio", ] [dependencies] @@ -65,7 +64,7 @@ hex = { workspace = true } serde_json = { workspace = true } sha2 = { workspace = true } thiserror = { workspace = true } -tokio = { workspace = true, optional = true, features = [ +tokio = { workspace = true, features = [ "fs", "io-util", "rt", diff --git a/core/crates/tera_core/src/runtime/mod.rs b/core/crates/tera_core/src/runtime/mod.rs @@ -32,6 +32,7 @@ pub struct TeraRuntime { lifecycle: lifecycle::RuntimeLifecycle, pub(crate) platform_app: RwLock<Option<AppInfoPlatform>>, pub(crate) store_public_key: Option<PublicKey>, + today_projection_lock: tokio::sync::Mutex<()>, #[cfg(feature = "mobile-social")] mutations: mutation_admission::MutationAdmission, #[cfg(feature = "mobile-social")] @@ -92,6 +93,7 @@ impl TeraRuntime { lifecycle: lifecycle::RuntimeLifecycle::default(), platform_app: RwLock::new(None), store_public_key, + today_projection_lock: Default::default(), #[cfg(feature = "mobile-social")] mutations: mutation_admission::MutationAdmission::default(), #[cfg(feature = "mobile-social")] diff --git a/core/crates/tera_core/src/runtime/product_surface/calendar_v1_fixture.json b/core/crates/tera_core/src/runtime/product_surface/calendar_v1_fixture.json @@ -0,0 +1,11 @@ +{ + "cardId": "9617f75695b64dcfcdcf77f35d44cb98238af2832fb4dbe06b97140860ddd47b", + "event": "{\"content\":\"\",\"created_at\":1800000000,\"id\":\"f67b2f5785757cd8dab29e259955157085c7c8adebfc1c36912fac5a974e27d6\",\"kind\":31922,\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"sig\":\"a792621e1fc57c0608f4fc0f7092174e75b979cdeaf863bc92f99187979701a90b4b0adda823a104e9b51e4f37d48c2634421c0c9860a39bf416edd2c25b7459\",\"tags\":[[\"d\",\"harvest-day\"],[\"title\",\"Harvest day\"],[\"start\",\"2026-09-05\"],[\"end\",\"2026-09-07\"]]}", + "projection": "{\"schemaVersion\":1,\"contextId\":\"victoria\",\"contextGeneration\":1,\"storeGeneration\":[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],\"sourceEvents\":1,\"contentGeneration\":6138091618127561838,\"cards\":[{\"card\":{\"schemaVersion\":1,\"cardId\":\"9617f75695b64dcfcdcf77f35d44cb98238af2832fb4dbe06b97140860ddd47b\",\"cardType\":\"Event\",\"sourceEventId\":\"f67b2f5785757cd8dab29e259955157085c7c8adebfc1c36912fac5a974e27d6\",\"sourceAddress\":\"31922:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:harvest-day\",\"authorPubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"contractId\":\"radroots.calendar.date_event.v1\",\"title\":\"Harvest day\",\"content\":\"\",\"authoredAt\":1800000000,\"effectiveAt\":1788566400,\"eventStart\":1788566400,\"eventEnd\":1788739200,\"contextRank\":\"MissingLocalityFallback\",\"inclusionReason\":\"locality_missing_fallback\",\"media\":[],\"lifecycle\":\"Active\",\"rank\":null},\"locality\":[]}],\"profiles\":{},\"thread\":[],\"overlays\":{},\"mediaCache\":{\"schemaVersion\":1,\"configuration\":null,\"entries\":{}},\"queryScope\":[11,203,36,96,172,171,131,36,155,175,50,198,91,169,59,191,239,46,108,61,140,132,88,59,214,10,66,117,90,248,75,51],\"visibilityDigest\":[77,213,177,108,8,70,188,140,46,174,28,229,248,81,25,255,9,70,198,29,226,165,30,0,150,65,1,247,36,211,156,221]}", + "provenance": { + "producer_commit": "0f592663540ea913f5c577c66739259ce15cfdca", + "producer_test": "every_local_presentation_read_reconstructs_untyped_v1_calendar_without_rewriting_source", + "synthetic": true, + "projection_sha256": "b36d04578c610522cbbc520cdeea8e7e84c0253f3cb1aa04e7b0a27af761201f" + } +} diff --git a/core/crates/tera_core/src/runtime/product_surface/model.rs b/core/crates/tera_core/src/runtime/product_surface/model.rs @@ -161,12 +161,6 @@ pub struct ClassifiedCard { pub content: String, pub authored_at: u64, pub effective_at: u64, - // Read-only v1 hash compatibility; retired by the versioned C048 rebuild. - // New projections carry only typed calendar values and leave these empty. - #[serde(rename = "eventStart")] - pub(crate) legacy_event_start: Option<u64>, - #[serde(rename = "eventEnd")] - pub(crate) legacy_event_end: Option<u64>, #[serde(default, skip_serializing_if = "Option::is_none")] pub calendar_timing: Option<super::CalendarTiming>, #[serde(default, skip_serializing_if = "Option::is_none")] diff --git a/core/crates/tera_core/src/runtime/product_surface/projection.rs b/core/crates/tera_core/src/runtime/product_surface/projection.rs @@ -10,7 +10,7 @@ use super::{ SupportingProfile, TodayCardType, }; -const CLASSIFIED_CARD_SCHEMA_VERSION: u16 = 1; +const CLASSIFIED_CARD_SCHEMA_VERSION: u16 = 2; #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] @@ -202,8 +202,6 @@ fn card( content: event.content().to_owned(), authored_at: event.created_at_u64(), effective_at, - legacy_event_start: None, - legacy_event_end: None, calendar_timing, location, price_amount, diff --git a/core/crates/tera_core/src/runtime/product_surface/today.rs b/core/crates/tera_core/src/runtime/product_surface/today.rs @@ -47,8 +47,8 @@ use super::{Phase1MediaCachePolicy, Phase1VerifiedMediaReceipt}; use crate::runtime::TeraRuntime; const TODAY_PROJECTION_ID: &str = "radroots.today.v1"; -const TODAY_PROJECTION_DOCUMENT_SCHEMA_VERSION: u16 = 1; -const TODAY_SNAPSHOT_SCHEMA_VERSION: u16 = 2; +const TODAY_PROJECTION_DOCUMENT_SCHEMA_VERSION: u16 = 2; +const TODAY_SNAPSHOT_SCHEMA_VERSION: u16 = 3; const TODAY_PAGE_LIMIT_MAX: u16 = 100; const TODAY_SEARCH_LIMIT_MAX: u16 = 100; #[cfg(feature = "mobile-social")] @@ -57,11 +57,18 @@ const TODAY_SYNC_PAGE_LIMIT: u16 = 500; const TODAY_SYNC_MAX_PAGES: u16 = 8; #[cfg(feature = "mobile-social")] const TODAY_SYNC_KINDS: [u32; 7] = [0, 1, 5, 1111, 30_402, 31_922, 31_923]; -const PROJECTION_GENERATION_DOMAIN: &[u8] = b"radroots.today-projection.v1\0"; -const PROJECTION_CONTENT_DOMAIN: &[u8] = b"radroots.today-content-generation.v1\0"; +const PROJECTION_GENERATION_DOMAIN: &[u8] = b"radroots.today-projection.v2\0"; +const PROJECTION_CONTENT_DOMAIN: &[u8] = b"radroots.today-content-generation.v2\0"; const PROJECTION_DOCUMENT_KEY_DOMAIN: &[u8] = b"radroots.today-document-key.v1\0"; const SNAPSHOT_ID_DOMAIN: &[u8] = b"radroots.today-snapshot-id.v2\0"; +#[path = "today_calendar_migration.rs"] +mod calendar_migration; + +#[cfg(test)] +#[path = "today_calendar_migration_tests.rs"] +mod calendar_migration_tests; + #[path = "today_paging_scope.rs"] mod paging_scope; @@ -185,6 +192,8 @@ pub enum TodayError { EventNotVisible, #[error("today projection state is corrupt")] CorruptProjection, + #[error("today projection requires a different supported reader")] + UnsupportedProjectionVersion, #[error(transparent)] Cursor(#[from] CursorError), #[error(transparent)] @@ -233,6 +242,8 @@ struct TodayProjectionState { query_scope: Option<[u8; 32]>, #[serde(default, skip_serializing_if = "Option::is_none")] visibility_digest: Option<[u8; 32]>, + #[serde(default)] + quarantined_source_ids: Vec<String>, } #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] @@ -296,10 +307,13 @@ impl TeraRuntime { let requested_updated_at_unix_ms = now_unix_seconds .checked_mul(1_000) .ok_or(TodayError::InvalidRequest)?; + let _projection = self.today_projection_lock.lock().await; let storage = self .client .storage() .map_err(|_| TodayError::RuntimeUnavailable)?; + // Reject an unsupported reader before rebuilding any owner metadata. + calendar_migration::ready(storage).await?; let visibility_digest = *EventStore::rebuild_visibility(storage) .await? .digest() @@ -308,7 +322,10 @@ impl TeraRuntime { let generation = projection_generation()?; let projection_id = projection_id()?; let key = projection_document_key(context); - let prior = load_state(storage, context, generation).await?; + let prior = match load_state(storage, context, generation).await? { + Some(state) => Some(state), + None => calendar_migration::legacy_state(storage, context).await?, + }; let query_scope = paging_scope::query_scope(context, self.store_public_key)?; if update == TodayProjectionUpdate::Incremental @@ -317,12 +334,15 @@ impl TeraRuntime { && state.query_scope == Some(query_scope) && state.visibility_digest == Some(visibility_digest) && calendar_projection_ready(state) + && state.schema_version == TODAY_PROJECTION_DOCUMENT_SCHEMA_VERSION }) { let state = prior.expect("checked present"); return Ok(refresh_receipt(update, &state, false)); } + let rebuild = + calendar_migration::begin(storage, requested_updated_at_unix_ms, &event_status).await?; let visible = query_all_visible(storage).await?; let local_media = prior.as_ref().map(local_media_evidence).unwrap_or_default(); let overlays = prior @@ -376,17 +396,18 @@ impl TeraRuntime { }) .unwrap_or(0); let updated_at_unix_ms = requested_updated_at_unix_ms.max(prior_updated_at); - ProjectionStore::checkpoint( - storage, - ProjectionCheckpoint::new( - projection_id, - generation, - source_position, - event_status.raw_events(), - updated_at_unix_ms, - )?, - ) - .await?; + let checkpoint = ProjectionCheckpoint::new( + projection_id, + generation, + source_position, + event_status.raw_events(), + updated_at_unix_ms, + )?; + if let Some(ticket) = rebuild { + calendar_migration::complete(storage, &ticket, checkpoint).await?; + } else { + ProjectionStore::checkpoint(storage, checkpoint).await?; + } Ok(refresh_receipt(update, &state, changed)) } @@ -500,10 +521,15 @@ impl TeraRuntime { as_of: u64, ) -> Result<Option<TodayProjectionState>, TodayError> { let state = load_state(storage, context, projection_generation()?).await?; - if state + let needs_upgrade = state .as_ref() .is_some_and(|value| !calendar_projection_ready(value)) - { + || (state.is_none() + && (!calendar_migration::ready(storage).await? + || calendar_migration::legacy_state(storage, context) + .await? + .is_some())); + if needs_upgrade { self.phase1_refresh_today(context, as_of, TodayProjectionUpdate::Rebuild) .await?; load_state(storage, context, projection_generation()?).await @@ -1401,12 +1427,17 @@ fn project_state( overlays: BTreeMap<String, LocalAuthorOverlay>, ) -> Result<TodayProjectionState, TodayError> { let mut cards = Vec::new(); + let mut quarantined_source_ids = Vec::new(); let mut profiles = BTreeMap::new(); let mut thread = Vec::new(); for stored in visible { - let verified = verify_nip01_event(stored.event().envelope().clone()) - .map_err(|_| TodayError::CorruptProjection)?; - let admitted = admit_verified_event(verified).map_err(|_| TodayError::CorruptProjection)?; + let admitted = verify_nip01_event(stored.event().envelope().clone()) + .ok() + .and_then(|verified| admit_verified_event(verified).ok()); + let Some(admitted) = admitted else { + quarantined_source_ids.push(stored.event().id().to_hex()); + continue; + }; match &admitted { RadrootsAdmittedEvent::Profile(profile) => { profiles.insert( @@ -1455,6 +1486,7 @@ fn project_state( media_cache: Phase1MediaCacheIndex::default(), query_scope: None, visibility_digest: None, + quarantined_source_ids, }) } @@ -1765,6 +1797,9 @@ async fn load_state( context: &LocalNetwork, generation: ProjectionGeneration, ) -> Result<Option<TodayProjectionState>, TodayError> { + if !calendar_migration::ready(storage).await? { + return Ok(None); + } let document = ProjectionStore::projection_document( storage, projection_id()?, @@ -1831,7 +1866,7 @@ async fn load_snapshot( .await? .map(|snapshot| { if snapshot.generation() != generation { - return Err(TodayError::CorruptProjection); + return Err(CursorError::Stale.into()); } decode_snapshot(snapshot.value()) }) @@ -1940,7 +1975,7 @@ fn decode_snapshot(value: &[u8]) -> Result<FrozenTodaySnapshot, TodayError> { schema_version: u16, } let header: Header = decode(value)?; - if header.schema_version == 1 { + if matches!(header.schema_version, 1 | 2) { return Err(CursorError::Stale.into()); } let snapshot: FrozenTodaySnapshot = match serde_json::from_slice(value) { @@ -1980,7 +2015,11 @@ enum LegacyMediaVerificationState { } fn migrate_legacy_state(value: &[u8]) -> Result<TodayProjectionState, TodayError> { - verify_legacy_content_generation(value)?; + verify_legacy_content_generation( + value, + TODAY_PROJECTION_DOCUMENT_SCHEMA_VERSION, + PROJECTION_CONTENT_DOMAIN, + )?; let mut legacy: serde_json::Value = decode(value)?; migrate_legacy_media_values(&mut legacy)?; let object = legacy @@ -2001,7 +2040,11 @@ fn migrate_legacy_state(value: &[u8]) -> Result<TodayProjectionState, TodayError Ok(state) } -fn verify_legacy_content_generation(value: &[u8]) -> Result<(), TodayError> { +fn verify_legacy_content_generation( + value: &[u8], + schema: u16, + domain: &[u8], +) -> Result<(), TodayError> { let parsed: serde_json::Value = decode(value)?; let expected = parsed .get("contentGeneration") @@ -2011,7 +2054,7 @@ fn verify_legacy_content_generation(value: &[u8]) -> Result<(), TodayError> { if parsed .get("schemaVersion") .and_then(serde_json::Value::as_u64) - != Some(u64::from(TODAY_PROJECTION_DOCUMENT_SCHEMA_VERSION)) + != Some(u64::from(schema)) { return Err(TodayError::CorruptProjection); } @@ -2037,7 +2080,7 @@ fn verify_legacy_content_generation(value: &[u8]) -> Result<(), TodayError> { canonical.extend_from_slice(&value[..number_start]); canonical.push(b'0'); canonical.extend_from_slice(&value[number_end..]); - let digest = Sha256::digest([PROJECTION_CONTENT_DOMAIN, canonical.as_slice()].concat()); + let digest = Sha256::digest([domain, canonical.as_slice()].concat()); let observed = u64::from_be_bytes(digest[..8].try_into().expect("digest prefix")).max(1); (observed == expected) .then_some(()) @@ -2553,6 +2596,7 @@ mod tests { lifecycle: Default::default(), platform_app: RwLock::new(None), store_public_key: None, + today_projection_lock: Default::default(), mutations: Default::default(), settings_lock: tokio::sync::Mutex::new(()), identity_session: tokio::sync::RwLock::new(None), diff --git a/core/crates/tera_core/src/runtime/product_surface/today_admission_tests.rs b/core/crates/tera_core/src/runtime/product_surface/today_admission_tests.rs @@ -69,7 +69,7 @@ fn observed(event: SignedEvent) -> ObservedEvent { // Shared typestates accept caller-supplied verifier/policy implementations. // The app's public direct-ingest boundary must use its actual crypto/profile // rules before writing even if another host supplied permissive evidence. -struct PermissiveEvidence; +pub(super) struct PermissiveEvidence; impl SignatureVerifier for PermissiveEvidence { fn verify_signature( &self, diff --git a/core/crates/tera_core/src/runtime/product_surface/today_calendar_migration.rs b/core/crates/tera_core/src/runtime/product_surface/today_calendar_migration.rs @@ -0,0 +1,287 @@ +//! Application materialization coordinated by the existing storage rebuild owner. +//! Generation documents remain separate; only owner promotion exposes new state. +use super::*; +use radroots_storage::{ + event::{AdmissionStage, SourceGeneration}, + projection::{ + InvalidationReason, ProjectionHealth, ProjectionInvalidation, RawSourceDigest, + RebuildFailure, RebuildStage, RebuildTicket, RebuildTicketId, RebuildTransition, + }, +}; + +const LEGACY_GENERATION_DOMAIN: &[u8] = b"radroots.today-projection.v1\0"; +const LEGACY_CONTENT_DOMAIN: &[u8] = b"radroots.today-content-generation.v1\0"; +const SOURCE_DIGEST_DOMAIN: &[u8] = b"tera.today.calendar-rebuild-source.v1\0"; + +pub(super) fn legacy_generation() -> Result<ProjectionGeneration, TodayError> { + Ok(ProjectionGeneration::new( + Sha256::digest(LEGACY_GENERATION_DOMAIN).into(), + )?) +} + +pub(super) async fn ready(storage: &dyn radroots_storage::Storage) -> Result<bool, TodayError> { + let Some(status) = ProjectionStore::status(storage, projection_id()?).await? else { + return Ok(true); + }; + if status.generation() == projection_generation()? { + return Ok(status.health() == ProjectionHealth::Ready); + } + if status.generation() == legacy_generation()? { + return Ok(false); + } + Err(TodayError::UnsupportedProjectionVersion) +} + +/// Historical cards supply only validated local media and overlays. Their +/// timing and payload are reconstructed from canonical events, never promoted. +pub(super) async fn legacy_state( + storage: &dyn radroots_storage::Storage, + context: &LocalNetwork, +) -> Result<Option<TodayProjectionState>, TodayError> { + let document = ProjectionStore::projection_document( + storage, + projection_id()?, + legacy_generation()?, + projection_document_key(context), + ) + .await?; + let Some(document) = document else { + return Ok(None); + }; + verify_legacy_content_generation(document.value(), 1, LEGACY_CONTENT_DOMAIN)?; + let mut value: serde_json::Value = decode(document.value())?; + migrate_legacy_media_values(&mut value)?; + let object = value.as_object_mut().ok_or(TodayError::CorruptProjection)?; + object.entry("mediaCache").or_insert( + serde_json::to_value(Phase1MediaCacheIndex::default()) + .map_err(|_| TodayError::Serialization)?, + ); + if let Some(cards) = object + .get_mut("cards") + .and_then(serde_json::Value::as_array_mut) + { + for entry in cards { + let card = entry + .get_mut("card") + .and_then(serde_json::Value::as_object_mut) + .ok_or(TodayError::CorruptProjection)?; + card.remove("eventStart"); + card.remove("eventEnd"); + card.remove("calendarTiming"); + } + } + let state: TodayProjectionState = + serde_json::from_value(value).map_err(|_| TodayError::CorruptProjection)?; + if state.context_id != context.id.as_str() || state.context_generation != context.generation { + return Err(TodayError::CorruptProjection); + } + validate_media_state(&state)?; + Ok(Some(state)) +} + +#[derive(Eq, PartialEq)] +pub(super) struct SourceSnapshot { + pub(super) generation: SourceGeneration, + pub(super) high_water: Option<EventPosition>, + pub(super) digest: RawSourceDigest, +} + +impl SourceSnapshot { + fn matches(&self, ticket: &RebuildTicket) -> bool { + self.generation == ticket.source_generation() + && self.high_water == ticket.source_high_water() + && self.digest == ticket.source_digest() + } +} + +/// A bounded streaming digest detects both appended and same-count admission +/// changes. Storage continues to own all signature, replacement and deletion data. +pub(super) async fn source_snapshot( + storage: &dyn radroots_storage::Storage, +) -> Result<SourceSnapshot, TodayError> { + let mut hash = Sha256::new(); + hash.update(SOURCE_DIGEST_DOMAIN); + let mut generation = None; + let mut high_water = None; + let mut after = None; + let mut count = 0_u64; + loop { + let mut bounds = EventQueryBounds::first(radroots_storage::event::EVENT_QUERY_LIMIT_MAX)?; + if let Some(cursor) = after { + bounds = bounds.after(cursor); + } + let page = EventStore::query_raw(storage, EventQuery::all(bounds)).await?; + if generation + .replace(page.generation()) + .is_some_and(|prior| prior != page.generation()) + { + return Err(radroots_storage::Error::SourceGenerationChanged.into()); + } + hash.update(page.generation().as_bytes()); + for event in page.items() { + count = count.checked_add(1).ok_or(TodayError::InvalidRequest)?; + if count > radroots_sync::projection::PROJECTION_RAW_SOURCE_MAX_EVENTS { + return Err(TodayError::InvalidRequest); + } + hash.update(event.position().sequence().get().to_be_bytes()); + hash.update([match event.stage() { + AdmissionStage::Raw => 0, + AdmissionStage::Verified => 1, + AdmissionStage::Visible => 2, + }]); + let raw = event.event().raw_json().as_bytes(); + hash.update( + u64::try_from(raw.len()) + .map_err(|_| TodayError::InvalidRequest)? + .to_be_bytes(), + ); + hash.update(raw); + high_water = Some(event.position()); + } + after = page.next_cursor(); + if after.is_none() { + break; + } + } + Ok(SourceSnapshot { + generation: generation.ok_or(TodayError::CorruptProjection)?, + high_water, + digest: RawSourceDigest::new(hash.finalize().into()), + }) +} + +pub(super) async fn begin( + storage: &dyn radroots_storage::Storage, + now: u64, + expected: &radroots_storage::status::EventStoreStatus, +) -> Result<Option<RebuildTicket>, TodayError> { + let Some(status) = ProjectionStore::status(storage, projection_id()?).await? else { + return Ok(None); + }; + let replacement = projection_generation()?; + if status.generation() == replacement && status.health() == ProjectionHealth::Ready { + return Ok(None); + } + if status.generation() != legacy_generation()? { + return Err(TodayError::UnsupportedProjectionVersion); + } + let source = source_snapshot(storage).await?; + if source.generation != expected.generation() + || source + .high_water + .map_or(0, |position| position.sequence().get()) + != expected.raw_events() + { + return Err(radroots_storage::Error::SourceGenerationChanged.into()); + } + let now = now.max( + status + .checkpoint() + .map_or(0, ProjectionCheckpoint::updated_at_unix_ms), + ); + let mut ticket = if let Some(id) = status.active_rebuild() { + let ticket = ProjectionStore::rebuild(storage, id) + .await? + .ok_or(TodayError::CorruptProjection)?; + if ticket.invalidation().replacement_generation() != replacement + || ticket.invalidation().invalid_generation() != status.generation() + || ticket.invalidation().projection_id() != status.projection_id() + { + return Err(TodayError::UnsupportedProjectionVersion); + } + if !source.matches(&ticket) { + fail_changed_source(storage, &ticket, now).await?; + return Err(radroots_storage::Error::SourceGenerationChanged.into()); + } + ticket + } else { + let invalidation = + match ProjectionStore::invalidation(storage, projection_id()?, replacement).await? { + Some(value) if value.invalid_generation() == status.generation() => value, + Some(_) => return Err(TodayError::UnsupportedProjectionVersion), + None => ProjectionInvalidation::new( + projection_id()?, + status.generation(), + replacement, + InvalidationReason::ProjectionGenerationChanged, + now, + )?, + }; + if status.health() == ProjectionHealth::Ready { + ProjectionStore::invalidate(storage, invalidation.clone()).await?; + } else if status.health() != ProjectionHealth::Invalidated { + return Err(TodayError::CorruptProjection); + } + ProjectionStore::request_rebuild( + storage, + RebuildTicket::requested( + RebuildTicketId::new(*uuid::Uuid::new_v4().as_bytes())?, + invalidation, + source.generation, + source.high_water, + source.digest, + )?, + ) + .await? + }; + if ticket.stage() == RebuildStage::Requested { + ticket = ProjectionStore::transition_rebuild( + storage, + RebuildTransition::start( + ticket.ticket_id(), + ticket.revision(), + now.max(ticket.updated_at_unix_ms()), + ), + ) + .await?; + } + if ticket.stage() != RebuildStage::Running { + return Err(TodayError::CorruptProjection); + } + Ok(Some(ticket)) +} + +async fn fail_changed_source( + storage: &dyn radroots_storage::Storage, + ticket: &RebuildTicket, + now: u64, +) -> Result<(), TodayError> { + ProjectionStore::transition_rebuild( + storage, + RebuildTransition::fail( + ticket.ticket_id(), + ticket.revision(), + now.max(ticket.updated_at_unix_ms()), + RebuildFailure::SourceChanged, + ), + ) + .await?; + Ok(()) +} + +pub(super) async fn complete( + storage: &dyn radroots_storage::Storage, + ticket: &RebuildTicket, + checkpoint: ProjectionCheckpoint, +) -> Result<(), TodayError> { + let now = checkpoint + .updated_at_unix_ms() + .max(ticket.updated_at_unix_ms()); + if !source_snapshot(storage).await?.matches(ticket) { + fail_changed_source(storage, ticket, now).await?; + return Err(radroots_storage::Error::SourceGenerationChanged.into()); + } + match ProjectionStore::transition_rebuild( + storage, + RebuildTransition::complete(ticket.ticket_id(), ticket.revision(), now, checkpoint), + ) + .await + { + Ok(_) => Ok(()), + Err(radroots_storage::Error::SourceGenerationChanged) => { + fail_changed_source(storage, ticket, now).await?; + Err(radroots_storage::Error::SourceGenerationChanged.into()) + } + Err(error) => Err(error.into()), + } +} diff --git a/core/crates/tera_core/src/runtime/product_surface/today_calendar_migration_tests.rs b/core/crates/tera_core/src/runtime/product_surface/today_calendar_migration_tests.rs @@ -0,0 +1,648 @@ +use super::tests::{context, keys, signed, visible_admission}; +use super::*; +use crate::runtime::{ + builder::RuntimeBuilder, + store::{MobileUserStoreConfig, ProtectedDataAvailability}, +}; +use radroots_storage::projection::{ + InvalidationReason, ProjectionHealth, ProjectionInvalidation, RebuildStage, RebuildTicket, + RebuildTicketId, +}; +use std::path::Path; +const NOW: u64 = 2_000_000_000; + +fn fixture() -> serde_json::Value { + serde_json::from_str(include_str!("calendar_v1_fixture.json")).unwrap() +} + +async fn open(root: &Path) -> TeraRuntime { + let config = MobileUserStoreConfig::from_encoded( + root, + &keys().public_key().to_string(), + &"01".repeat(32), + 1_800_000_000_000, + ProtectedDataAvailability::Available, + ) + .unwrap(); + std::fs::create_dir_all(config.owner_directory()).unwrap(); + RuntimeBuilder::new(config).build().await.unwrap() +} + +async fn seed_legacy(runtime: &TeraRuntime) { + let fixture = fixture(); + let raw = fixture["event"].as_str().unwrap(); + let event = + radroots_event::SignedEvent::from_wire_verified_id(serde_json::from_str(raw).unwrap(), raw) + .unwrap(); + let storage = runtime.client.storage().unwrap(); + EventStore::admit(storage, visible_admission(event, NOW * 1_000)) + .await + .unwrap(); + let source = EventStore::status(storage).await.unwrap(); + ProjectionStore::put_projection_document( + storage, + projection_id().unwrap(), + calendar_migration::legacy_generation().unwrap(), + ProjectionDocument::new( + projection_document_key(&context(None, 1)), + fixture["projection"].as_str().unwrap().as_bytes().to_vec(), + ) + .unwrap(), + ) + .await + .unwrap(); + ProjectionStore::checkpoint( + storage, + ProjectionCheckpoint::new( + projection_id().unwrap(), + calendar_migration::legacy_generation().unwrap(), + Some(EventPosition::new( + source.generation(), + EventSequence::new(1).unwrap(), + )), + 1, + NOW * 1_000, + ) + .unwrap(), + ) + .await + .unwrap(); +} + +async fn old_bytes(runtime: &TeraRuntime) -> Vec<u8> { + ProjectionStore::projection_document( + runtime.client.storage().unwrap(), + projection_id().unwrap(), + calendar_migration::legacy_generation().unwrap(), + projection_document_key(&context(None, 1)), + ) + .await + .unwrap() + .unwrap() + .value() + .to_vec() +} + +async fn raw( + runtime: &TeraRuntime, +) -> radroots_storage::event::EventPage<radroots_storage::event::StoredRawEvent> { + EventStore::query_raw( + runtime.client.storage().unwrap(), + EventQuery::all(EventQueryBounds::first(20).unwrap()), + ) + .await + .unwrap() +} + +async fn assert_current(runtime: &TeraRuntime) { + let state = load_state( + runtime.client.storage().unwrap(), + &context(None, 1), + projection_generation().unwrap(), + ) + .await + .unwrap() + .unwrap(); + assert_eq!(state.schema_version, 2); + assert_eq!(state.cards.len(), 1); + let card = &state.cards[0].card; + assert_eq!(card.schema_version, 2); + assert_eq!(card.card_id.to_hex(), fixture()["cardId"]); + assert_eq!( + card.source_event_id, + serde_json::from_str::<serde_json::Value>(fixture()["event"].as_str().unwrap()).unwrap()["id"] + ); + let Some(CalendarTiming::DateBased(timing)) = &card.calendar_timing else { + panic!("date timing"); + }; + assert_eq!(timing.start().as_str(), "2026-09-05"); + assert_eq!(timing.end_exclusive().unwrap().as_str(), "2026-09-07"); + assert_eq!(card.effective_at, 1_800_000_000); + let encoded = String::from_utf8(encode(&state).unwrap()).unwrap(); + assert!(!encoded.contains("eventStart") && !encoded.contains("eventEnd")); + let status = + ProjectionStore::status(runtime.client.storage().unwrap(), projection_id().unwrap()) + .await + .unwrap() + .unwrap(); + assert_eq!(status.generation(), projection_generation().unwrap()); + assert_eq!(status.health(), ProjectionHealth::Ready); +} + +#[cfg(feature = "mobile-social")] +#[tokio::test] +async fn old_sqlite_calendar_migrates_on_all_readers_without_changing_pending_identity() { + use super::super::{ + CreateUpdate, Phase1AddCommand, Phase1CancellationPolicy, Phase1QueuePolicy, + Phase1RelaySatisfaction, + }; + for reader in ["page", "search", "me", "reconcile"] { + let root = tempfile::tempdir().unwrap(); + let runtime = open(root.path()).await; + seed_legacy(&runtime).await; + let original = old_bytes(&runtime).await; + let signed_before = raw(&runtime).await; + let saved = runtime + .phase1_save_draft( + [17; 16], + Phase1AddCommand::CreateUpdate( + CreateUpdate::new("Synthetic pending migration fixture").unwrap(), + ), + NOW, + vec![], + None, + NOW * 1_000, + ) + .await + .unwrap(); + let queued = runtime + .phase1_queue_draft( + [17; 16], + saved.draft().revision().get(), + Phase1QueuePolicy::new( + vec!["wss://relay.example".into()], + Phase1RelaySatisfaction::AllAccepted, + (NOW + 1000) * 1000, + Phase1CancellationPolicy::LocalCooperative, + ) + .unwrap(), + NOW * 1000 + 1, + ) + .await + .unwrap(); + assert!(queued.draft().operation_id().is_some()); + runtime.shutdown().await.unwrap(); + drop(runtime); + let runtime = open(root.path()).await; + let selected = context(None, 1); + let count = match reader { + "page" => runtime + .phase1_today_page(&selected, TodayPageRequest::first(20, NOW)) + .await + .unwrap() + .items + .len(), + "search" => runtime + .phase1_search(&selected, "Harvest", 20, NOW) + .await + .unwrap() + .len(), + "me" => runtime + .phase1_me(&selected, &keys().public_key().to_string(), NOW) + .await + .unwrap() + .cards + .len(), + "reconcile" => runtime + .phase1_today_reconcile( + &selected, + NOW, + &[fixture()["cardId"].as_str().unwrap().into()], + None, + ) + .await + .unwrap() + .items + .len(), + _ => unreachable!(), + }; + assert_eq!(count, 1, "{reader}"); + assert_current(&runtime).await; + assert_eq!(old_bytes(&runtime).await, original); + assert_eq!(raw(&runtime).await, signed_before); + assert_eq!(runtime.phase1_draft_status([17; 16]).await.unwrap(), queued); + runtime.shutdown().await.unwrap(); + drop(runtime); + let reopened = open(root.path()).await; + assert_current(&reopened).await; + assert_eq!( + reopened.phase1_draft_status([17; 16]).await.unwrap(), + queued + ); + assert_eq!(old_bytes(&reopened).await, original); + reopened.shutdown().await.unwrap(); + } +} + +#[tokio::test] +async fn every_durable_rebuild_boundary_resumes_after_sqlite_reopen() { + for boundary in ["invalidated", "requested", "running", "staged"] { + let root = tempfile::tempdir().unwrap(); + let runtime = open(root.path()).await; + seed_legacy(&runtime).await; + let storage = runtime.client.storage().unwrap(); + let source = calendar_migration::source_snapshot(storage).await.unwrap(); + let invalidation = ProjectionInvalidation::new( + projection_id().unwrap(), + calendar_migration::legacy_generation().unwrap(), + projection_generation().unwrap(), + InvalidationReason::ProjectionGenerationChanged, + NOW * 1000, + ) + .unwrap(); + ProjectionStore::invalidate(storage, invalidation.clone()) + .await + .unwrap(); + let id = RebuildTicketId::new([7; 16]).unwrap(); + if boundary != "invalidated" { + ProjectionStore::request_rebuild( + storage, + RebuildTicket::requested( + id, + invalidation, + source.generation, + source.high_water, + source.digest, + ) + .unwrap(), + ) + .await + .unwrap(); + } + if matches!(boundary, "running" | "staged") { + calendar_migration::begin( + storage, + NOW * 1000, + &EventStore::status(storage).await.unwrap(), + ) + .await + .unwrap(); + } + if boundary == "staged" { + // An unpublished partial/corrupt replacement must never be read; + // reconstruction overwrites it while retaining the old generation. + ProjectionStore::put_projection_document( + storage, + projection_id().unwrap(), + projection_generation().unwrap(), + ProjectionDocument::new( + projection_document_key(&context(None, 1)), + b"unpublished interrupted candidate".to_vec(), + ) + .unwrap(), + ) + .await + .unwrap(); + } + assert!( + load_state(storage, &context(None, 1), projection_generation().unwrap()) + .await + .unwrap() + .is_none() + ); + let original = old_bytes(&runtime).await; + let signed_before = raw(&runtime).await; + runtime.shutdown().await.unwrap(); + drop(runtime); + let runtime = open(root.path()).await; + runtime + .phase1_today_page(&context(None, 1), TodayPageRequest::first(20, NOW)) + .await + .unwrap(); + assert_current(&runtime).await; + assert_eq!(old_bytes(&runtime).await, original, "{boundary}"); + assert_eq!(raw(&runtime).await, signed_before, "{boundary}"); + if boundary != "invalidated" { + assert_eq!( + ProjectionStore::rebuild(runtime.client.storage().unwrap(), id) + .await + .unwrap() + .unwrap() + .stage(), + RebuildStage::Completed + ); + } + runtime.shutdown().await.unwrap(); + } +} + +#[tokio::test] +async fn changed_source_is_never_promoted_and_retries_from_current_history() { + use radroots_transport::{ + Target, TransportId, + source::{EventProvenance, ObservedEvent}, + }; + for same_count in [false, true] { + let root = tempfile::tempdir().unwrap(); + let runtime = open(root.path()).await; + seed_legacy(&runtime).await; + let storage = runtime.client.storage().unwrap(); + let extra = signed(1, vec![], "Synthetic source change", NOW - 1); + if same_count { + let target = Target::nostr_relay("wss://relay.example").unwrap(); + let observed = ObservedEvent::new( + extra.clone(), + EventProvenance::new(TransportId::NOSTR, target.fingerprint().clone(), NOW * 1000) + .unwrap(), + ); + EventStore::admit(storage, EventAdmission::raw(observed)) + .await + .unwrap(); + } + let ticket = calendar_migration::begin( + storage, + NOW * 1000, + &EventStore::status(storage).await.unwrap(), + ) + .await + .unwrap() + .unwrap(); + EventStore::admit(storage, visible_admission(extra, NOW * 1000)) + .await + .unwrap(); + let signed_before = raw(&runtime).await; + let rejected = runtime + .phase1_today_page(&context(None, 1), TodayPageRequest::first(20, NOW)) + .await; + assert!(matches!( + rejected, + Err(TodayError::Storage( + radroots_storage::Error::SourceGenerationChanged + )) + )); + let failed = ProjectionStore::rebuild(storage, ticket.ticket_id()) + .await + .unwrap() + .unwrap(); + assert_eq!(failed.stage(), RebuildStage::Failed); + assert_eq!( + ProjectionStore::status(storage, projection_id().unwrap()) + .await + .unwrap() + .unwrap() + .generation(), + calendar_migration::legacy_generation().unwrap() + ); + let page = runtime + .phase1_today_page(&context(None, 1), TodayPageRequest::first(20, NOW)) + .await + .unwrap(); + assert_eq!(page.items.len(), 2); + assert_eq!(raw(&runtime).await, signed_before); + assert_eq!( + old_bytes(&runtime).await, + fixture()["projection"].as_str().unwrap().as_bytes() + ); + runtime.shutdown().await.unwrap(); + } +} + +#[tokio::test] +async fn obsolete_snapshots_and_unknown_generations_fail_without_rewriting_history() { + let root = tempfile::tempdir().unwrap(); + let runtime = open(root.path()).await; + seed_legacy(&runtime).await; + let storage = runtime.client.storage().unwrap(); + let selected = context(None, 1); + let old = calendar_migration::legacy_state(storage, &selected) + .await + .unwrap() + .unwrap(); + let scope = CursorScope::new( + selected.id.clone().into(), + selected.generation, + NOW, + old.store_generation, + old.content_generation, + paging_scope::query_scope(&selected, runtime.store_public_key).unwrap(), + ) + .unwrap(); + ProjectionStore::put_projection_snapshot( + storage, + ProjectionSnapshot::new( + projection_id().unwrap(), + snapshot_id(&scope), + calendar_migration::legacy_generation().unwrap(), + NOW * 1000, + br#"{"schemaVersion":2}"#.to_vec(), + ) + .unwrap(), + ) + .await + .unwrap(); + assert!(matches!( + load_snapshot( + storage, + projection_id().unwrap(), + projection_generation().unwrap(), + &scope + ) + .await, + Err(TodayError::Cursor(CursorError::Stale)) + )); + for version in [1, 2] { + assert!(matches!( + decode_snapshot(format!("{{\"schemaVersion\":{version}}}").as_bytes()), + Err(TodayError::Cursor(CursorError::Stale)) + )); + } + assert_eq!( + old_bytes(&runtime).await, + fixture()["projection"].as_str().unwrap().as_bytes() + ); + runtime.shutdown().await.unwrap(); + let root = tempfile::tempdir().unwrap(); + let runtime = open(root.path()).await; + let storage = runtime.client.storage().unwrap(); + let future = ProjectionCheckpoint::new( + projection_id().unwrap(), + ProjectionGeneration::new([9; 32]).unwrap(), + None, + 0, + NOW * 1000, + ) + .unwrap(); + let original = ProjectionStore::checkpoint(storage, future).await.unwrap(); + assert!(matches!( + runtime + .phase1_today_page(&selected, TodayPageRequest::first(20, NOW)) + .await, + Err(TodayError::UnsupportedProjectionVersion) + )); + assert!(matches!( + runtime + .phase1_refresh_today(&selected, NOW, TodayProjectionUpdate::Rebuild) + .await, + Err(TodayError::UnsupportedProjectionVersion) + )); + assert_eq!( + ProjectionStore::status(storage, projection_id().unwrap()) + .await + .unwrap() + .unwrap(), + original + ); + runtime.shutdown().await.unwrap(); +} + +#[tokio::test] +async fn invalid_historical_visible_heads_are_quarantined_without_reviving_old_cards() { + use super::admission_tests::PermissiveEvidence; + use radroots_event::admission::RawEvent; + use radroots_transport::{ + Target, TransportId, + source::{EventProvenance, ObservedEvent}, + }; + let root = tempfile::tempdir().unwrap(); + let runtime = open(root.path()).await; + seed_legacy(&runtime).await; + let storage = runtime.client.storage().unwrap(); + let invalid = signed( + 31_922, + vec![ + vec!["d", "harvest-day"], + vec!["title", "Invalid newer head"], + vec!["start", "2026-09-05"], + ], + "", + NOW - 1, + ); + let mut wire: radroots_event::wire::Nip01EventWire = + serde_json::from_str(invalid.raw_json()).unwrap(); + wire.sig = "00".repeat(64); + let raw_json = serde_json::to_string(&wire).unwrap(); + let invalid = radroots_event::SignedEvent::from_wire_verified_id(wire, raw_json).unwrap(); + assert!(verify_nip01_event(invalid.envelope().clone()).is_err()); + let visible = RawEvent::new(invalid.envelope().clone()) + .verify_id() + .unwrap() + .verify_signature(&PermissiveEvidence) + .unwrap() + .validate_contract_for_admission("radroots.calendar.date_event.v1") + .unwrap() + .admit_with(&PermissiveEvidence) + .unwrap() + .make_visible_with(&PermissiveEvidence) + .unwrap(); + let target = Target::nostr_relay("wss://relay.example").unwrap(); + let observed = ObservedEvent::new( + invalid.clone(), + EventProvenance::new(TransportId::NOSTR, target.fingerprint().clone(), NOW * 1000).unwrap(), + ); + EventStore::admit(storage, EventAdmission::visible(observed, visible).unwrap()) + .await + .unwrap(); + let signed_before = raw(&runtime).await; + let page = runtime + .phase1_today_page(&context(None, 1), TodayPageRequest::first(20, NOW)) + .await + .unwrap(); + assert!(page.items.is_empty()); + let current = load_state(storage, &context(None, 1), projection_generation().unwrap()) + .await + .unwrap() + .unwrap(); + assert_eq!(current.quarantined_source_ids, vec![invalid.id().to_hex()]); + assert_eq!(raw(&runtime).await, signed_before); + assert_eq!( + old_bytes(&runtime).await, + fixture()["projection"].as_str().unwrap().as_bytes() + ); + runtime.shutdown().await.unwrap(); +} + +#[tokio::test] +async fn corrupt_legacy_bytes_and_source_changes_before_promotion_preserve_original_state() { + let root = tempfile::tempdir().unwrap(); + let runtime = open(root.path()).await; + seed_legacy(&runtime).await; + let storage = runtime.client.storage().unwrap(); + let mut damaged = old_bytes(&runtime).await; + let offset = damaged + .windows(b"Harvest day".len()) + .position(|part| part == b"Harvest day") + .unwrap(); + damaged[offset] = b'X'; + ProjectionStore::put_projection_document( + storage, + projection_id().unwrap(), + calendar_migration::legacy_generation().unwrap(), + ProjectionDocument::new(projection_document_key(&context(None, 1)), damaged.clone()) + .unwrap(), + ) + .await + .unwrap(); + let original_status = ProjectionStore::status(storage, projection_id().unwrap()) + .await + .unwrap(); + assert!(matches!( + runtime + .phase1_today_page(&context(None, 1), TodayPageRequest::first(20, NOW)) + .await, + Err(TodayError::CorruptProjection) + )); + assert_eq!(old_bytes(&runtime).await, damaged); + assert_eq!( + ProjectionStore::status(storage, projection_id().unwrap()) + .await + .unwrap(), + original_status + ); + runtime.shutdown().await.unwrap(); + + let root = tempfile::tempdir().unwrap(); + let runtime = open(root.path()).await; + seed_legacy(&runtime).await; + let storage = runtime.client.storage().unwrap(); + let source = EventStore::status(storage).await.unwrap(); + let ticket = calendar_migration::begin(storage, NOW * 1_000, &source) + .await + .unwrap() + .unwrap(); + ProjectionStore::put_projection_document( + storage, + projection_id().unwrap(), + projection_generation().unwrap(), + ProjectionDocument::new( + projection_document_key(&context(None, 1)), + b"unpublished candidate".to_vec(), + ) + .unwrap(), + ) + .await + .unwrap(); + EventStore::admit( + storage, + visible_admission( + signed(1, vec![], "Arrived after candidate write", NOW), + NOW * 1000, + ), + ) + .await + .unwrap(); + let checkpoint = ProjectionCheckpoint::new( + projection_id().unwrap(), + projection_generation().unwrap(), + ticket.source_high_water(), + source.raw_events(), + NOW * 1000, + ) + .unwrap(); + assert!(matches!( + calendar_migration::complete(storage, &ticket, checkpoint).await, + Err(TodayError::Storage( + radroots_storage::Error::SourceGenerationChanged + )) + )); + assert_eq!( + ProjectionStore::rebuild(storage, ticket.ticket_id()) + .await + .unwrap() + .unwrap() + .stage(), + RebuildStage::Failed + ); + assert!(!calendar_migration::ready(storage).await.unwrap()); + assert_eq!( + old_bytes(&runtime).await, + fixture()["projection"].as_str().unwrap().as_bytes() + ); + let before = raw(&runtime).await; + let page = runtime + .phase1_today_page(&context(None, 1), TodayPageRequest::first(20, NOW)) + .await + .unwrap(); + assert_eq!(page.items.len(), 2); + assert_eq!(raw(&runtime).await, before); + runtime.shutdown().await.unwrap(); +} diff --git a/core/crates/tera_core/src/runtime/product_surface/today_calendar_tests.rs b/core/crates/tera_core/src/runtime/product_surface/today_calendar_tests.rs @@ -1,4 +1,4 @@ -use super::tests::{context, ingest, keys, signed_owned}; +use super::tests::{context, ingest, signed_owned}; use super::*; use radroots_event::calendar::{ AuthoredCalendarDateEvent, AuthoredCalendarTimeEvent, CalendarDate, @@ -65,7 +65,6 @@ async fn actual_admitted_calendar_projection_keeps_dates_instants_and_zones_dist assert_eq!(timing.start().as_str(), "2026-09-05"); assert_eq!(timing.end_exclusive().unwrap().as_str(), "2026-09-07"); assert_eq!(civil_card.effective_at, AUTHORED); - assert!(civil_card.legacy_event_start.is_none() && civil_card.legacy_event_end.is_none()); let timed_card = &page .items .iter() @@ -89,93 +88,3 @@ async fn actual_admitted_calendar_projection_keeps_dates_instants_and_zones_dist assert_eq!(raw.items()[0].event(), &civil); assert_eq!(raw.items()[1].event(), &timed_event); } - -#[tokio::test] -async fn every_local_presentation_read_reconstructs_untyped_v1_calendar_without_rewriting_source() { - let runtime = TeraRuntime::test_memory().unwrap(); - let context = context(None, 1); - let event = date_event(); - ingest(&runtime, &context, event.clone(), NOW).await; - let storage = runtime.client.storage().unwrap(); - let generation = projection_generation().unwrap(); - let mut legacy = load_state(storage, &context, generation) - .await - .unwrap() - .unwrap(); - let card_id = legacy.cards[0].card.card_id; - // Reproduce the old v1 value/hash using only a synthetic historical fixture. - let old_midnight = chrono::NaiveDate::from_ymd_opt(2026, 9, 5) - .unwrap() - .and_hms_opt(0, 0, 0) - .unwrap() - .and_utc() - .timestamp() as u64; - legacy.cards[0].card.calendar_timing = None; - legacy.cards[0].card.legacy_event_start = Some(old_midnight); - legacy.cards[0].card.legacy_event_end = Some(old_midnight + 2 * 86_400); - legacy.cards[0].card.effective_at = old_midnight; - legacy.content_generation = content_generation(&legacy).unwrap(); - assert!( - !String::from_utf8(encode(&legacy).unwrap()) - .unwrap() - .contains("calendarTiming") - ); - let before = EventStore::query_raw( - storage, - EventQuery::all(EventQueryBounds::first(20).unwrap()), - ) - .await - .unwrap(); - for reader in ["page", "search", "me", "reconcile"] { - store_state(storage, &context, generation, &legacy) - .await - .unwrap(); - let selected = match reader { - "page" => { - runtime - .phase1_today_page(&context, TodayPageRequest::first(20, NOW)) - .await - .unwrap() - .items - } - "search" => runtime - .phase1_search(&context, "Harvest", 20, NOW) - .await - .unwrap() - .into_iter() - .filter_map(|result| result.card) - .collect(), - "me" => { - runtime - .phase1_me(&context, &keys().public_key().to_string(), NOW) - .await - .unwrap() - .cards - } - "reconcile" => { - runtime - .phase1_today_reconcile(&context, NOW, &[card_id.to_hex()], None) - .await - .unwrap() - .items - } - _ => unreachable!(), - }; - assert_eq!(selected.len(), 1, "{reader}"); - assert_eq!(selected[0].card.card_id, card_id); - assert_eq!(selected[0].card.source_event_id, event.id().to_hex()); - assert!(matches!( - selected[0].card.calendar_timing, - Some(CalendarTiming::DateBased(_)) - )); - assert_eq!(selected[0].card.effective_at, AUTHORED); - assert!(selected[0].card.legacy_event_start.is_none()); - let after = EventStore::query_raw( - storage, - EventQuery::all(EventQueryBounds::first(20).unwrap()), - ) - .await - .unwrap(); - assert_eq!(before, after, "{reader} changed signed history"); - } -} diff --git a/core/crates/tera_core/src/runtime/product_surface/today_sync_tests.rs b/core/crates/tera_core/src/runtime/product_surface/today_sync_tests.rs @@ -68,6 +68,7 @@ pub(super) fn runtime(source: Arc<dyn EventSource>) -> TeraRuntime { lifecycle: Default::default(), platform_app: RwLock::new(None), store_public_key: None, + today_projection_lock: Default::default(), mutations: Default::default(), settings_lock: tokio::sync::Mutex::new(()), identity_session: tokio::sync::RwLock::new(None), diff --git a/core/crates/tera_ffi/src/error.rs b/core/crates/tera_ffi/src/error.rs @@ -154,6 +154,11 @@ impl From<TodayError> for TeraAppError { TodayError::CursorPositionMissing | TodayError::Cursor(_) => { ("today_cursor_invalid", true, &["restart_pagination"][..]) } + TodayError::UnsupportedProjectionVersion => ( + "today_reader_unsupported", + false, + &["upgrade_application"][..], + ), TodayError::RuntimeUnavailable => ("today_runtime_unavailable", true, &["retry"][..]), TodayError::InboundMedia(error) => ( tera_core::error::recovery::inbound_media_code(&error), diff --git a/release/provenance.json b/release/provenance.json @@ -2,7 +2,7 @@ "artifacts": { "app_api_sha256": "020924097c0d7efc33128cb8fd3d3b2026d95f57c44da71880e585aff80f070b", "ffi_api_sha256": "4cc03b03dd54db7b4ab22684ff819233444ae0a43a770ab875148892bed1e3f2", - "ffi_provenance_sha256": "fe8ac77f19b400cb4db0a007ac3ffc96f12db882f8eb6a9723b3b5608a43b065", + "ffi_provenance_sha256": "5dfd6f6c479bf55e985a9881826da5021080c3e198d3681cf32eb8cf2fb82fa3", "info_plist_sha256": "15ef08b1cdd1096cfb9eeaf5be5bf8f814807a7ca9350bbbb47860fa72ec13ef", "privacy_manifest_sha256": "a331d51864743ebe4e00dd22360b4a538b6b3ac26a6b3eb54094e60a36959a12", "sbom_sha256": "bea2d48c06e5f7e84cdad085d81290712674b74664456f2030064d9cf94c4b33", @@ -22,7 +22,7 @@ "lib_revision": "13ac1f2135199702a07ba6aa14ab22101f17fcb0", "source_date_epoch": 1787871027, "swift_package_lock_sha256": "94ae067a374726cdaf6b4ca0a5e44663c57fcdc5334060c5ffef5e79cfbf04c0", - "tera_ffi_source_tree": "d2c902b79291f9ced96cd77f989c7812bbc9c4b2", + "tera_ffi_source_tree": "a2debc6a4ad04ea84e3d93b10649c3e9562de36a", "xcode_package_lock_sha256": "c7f41934ea25f7a287bdc4f3a6ecabbf09a3a0bdd0f5a3e58183f355ca814096" }, "version": "0.1.0-alpha" diff --git a/test-fixtures/legacy-identifiers.v1.json b/test-fixtures/legacy-identifiers.v1.json @@ -2525,8 +2525,16 @@ "count": 2 }, { + "path": "core/crates/tera_core/src/runtime/product_surface/calendar_v1_fixture.json", + "count": 1 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/projection.rs", "count": 1 + }, + { + "path": "core/crates/tera_core/src/runtime/product_surface/today_calendar_migration_tests.rs", + "count": 1 } ] }, @@ -3683,6 +3691,16 @@ "category": "persisted_data", "occurrences": [ { + "path": "core/crates/tera_core/src/runtime/product_surface/today_calendar_migration.rs", + "count": 1 + } + ] + }, + { + "identifier": "radroots.today-content-generation.v2", + "category": "persisted_data", + "occurrences": [ + { "path": "core/crates/tera_core/src/runtime/product_surface/today.rs", "count": 1 } @@ -3713,6 +3731,16 @@ "category": "persisted_data", "occurrences": [ { + "path": "core/crates/tera_core/src/runtime/product_surface/today_calendar_migration.rs", + "count": 1 + } + ] + }, + { + "identifier": "radroots.today-projection.v2", + "category": "persisted_data", + "occurrences": [ + { "path": "core/crates/tera_core/src/runtime/product_surface/today.rs", "count": 1 } @@ -4069,6 +4097,10 @@ "count": 1 }, { + "path": "core/crates/tera_core/src/runtime/product_surface/today_calendar_migration_tests.rs", + "count": 4 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/today_calendar_tests.rs", "count": 2 }, @@ -4521,6 +4553,14 @@ "count": 1 }, { + "path": "core/crates/tera_core/src/runtime/product_surface/today_calendar_migration.rs", + "count": 15 + }, + { + "path": "core/crates/tera_core/src/runtime/product_surface/today_calendar_migration_tests.rs", + "count": 5 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/today_counted_storage_tests.rs", "count": 2 }, @@ -4567,6 +4607,10 @@ "count": 1 }, { + "path": "core/crates/tera_core/src/runtime/product_surface/today_calendar_migration.rs", + "count": 1 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/today_sync.rs", "count": 1 } @@ -4609,6 +4653,10 @@ "count": 4 }, { + "path": "core/crates/tera_core/src/runtime/product_surface/today_calendar_migration_tests.rs", + "count": 2 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/today_sync.rs", "count": 1 }, diff --git a/test-fixtures/swiftlint-maintainability-baseline.v1.json b/test-fixtures/swiftlint-maintainability-baseline.v1.json @@ -1 +1 @@ -[{"text":" init(","violation":{"ruleIdentifier":"function_body_length","ruleName":"Function Body Length","location":{"character":3,"file":"Tera\/App\/TeraAppModel.swift","line":21},"reason":"Initializer body should span 50 lines or less excluding comments and whitespace: currently spans 52 lines","ruleDescription":"Function bodies should not span too many lines","severity":"error"}},{"text":"actor TeraAddMediaCoordinator: TeraAddMediaHandling {","violation":{"ruleIdentifier":"type_body_length","ruleName":"Type Body Length","location":{"character":1,"file":"Tera\/Runtime\/TeraAddMediaCoordinator.swift","line":51},"reason":"Actor body should span 250 lines or less excluding comments and whitespace: currently spans 330 lines","ruleDescription":"Type bodies should not span too many lines","severity":"error"}},{"text":"#endif","violation":{"ruleIdentifier":"file_length","ruleName":"File Length","location":{"character":1,"file":"Tera\/Runtime\/TeraAddMediaCoordinator.swift","line":458},"reason":"File should contain 400 lines or less: currently contains 458","ruleDescription":"Files should not span too many lines.","severity":"error"}},{"text":"private final class TeraGeneratedRuntimeBackend: TeraRuntimeBackend, @unchecked Sendable {","violation":{"ruleIdentifier":"type_body_length","ruleName":"Type Body Length","location":{"character":15,"file":"Tera\/Runtime\/TeraGeneratedRuntimeBackend.swift","line":111},"reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 491 lines","ruleDescription":"Type bodies should not span too many lines","severity":"error"}},{"text":"}","violation":{"ruleIdentifier":"file_length","ruleName":"File Length","location":{"character":1,"file":"Tera\/Runtime\/TeraGeneratedRuntimeBackend.swift","line":1576},"reason":"File should contain 1000 lines or less: currently contains 1576","ruleDescription":"Files should not span too many lines.","severity":"error"}},{"text":"actor TeraRuntimeClient {","violation":{"ruleIdentifier":"type_body_length","ruleName":"Type Body Length","location":{"character":1,"file":"Tera\/Runtime\/TeraRuntimeClient.swift","line":3},"reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 753 lines","ruleDescription":"Type bodies should not span too many lines","severity":"error"}},{"text":" func changes(bufferCapacity: Int = 16) async throws -> AsyncStream<TeraRuntimeChange> {","violation":{"ruleIdentifier":"function_body_length","ruleName":"Function Body Length","location":{"character":3,"file":"Tera\/Runtime\/TeraRuntimeClient.swift","line":379},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 70 lines","ruleDescription":"Function bodies should not span too many lines","severity":"error"}},{"text":"}","violation":{"ruleIdentifier":"file_length","ruleName":"File Length","location":{"character":1,"file":"Tera\/Runtime\/TeraRuntimeClient.swift","line":835},"reason":"File should contain 400 lines or less: currently contains 835","ruleDescription":"Files should not span too many lines.","severity":"error"}},{"text":"}","violation":{"ruleIdentifier":"file_length","ruleName":"File Length","location":{"character":1,"file":"Tera\/Runtime\/TeraRuntimeModels.swift","line":1175},"reason":"File should contain 1000 lines or less: currently contains 1175","ruleDescription":"Files should not span too many lines.","severity":"error"}},{"text":"final class TeraAddStore: ObservableObject {","violation":{"ruleIdentifier":"type_body_length","ruleName":"Type Body Length","location":{"character":7,"file":"Tera\/State\/TeraAddStore.swift","line":11},"reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 731 lines","ruleDescription":"Type bodies should not span too many lines","severity":"error"}},{"text":" func retract(_ card: TeraTodayCard) async {","violation":{"ruleIdentifier":"function_body_length","ruleName":"Function Body Length","location":{"character":3,"file":"Tera\/State\/TeraAddStore.swift","line":459},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 51 lines","ruleDescription":"Function bodies should not span too many lines","severity":"error"}},{"text":"}","violation":{"ruleIdentifier":"file_length","ruleName":"File Length","location":{"character":1,"file":"Tera\/State\/TeraAddStore.swift","line":796},"reason":"File should contain 400 lines or less: currently contains 796","ruleDescription":"Files should not span too many lines.","severity":"error"}},{"text":"actor TeraConfigurationStore {","violation":{"ruleIdentifier":"type_body_length","ruleName":"Type Body Length","location":{"character":1,"file":"Tera\/State\/TeraConfigurationStore.swift","line":78},"reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 468 lines","ruleDescription":"Type bodies should not span too many lines","severity":"error"}},{"text":" func load() throws -> TeraAppConfiguration {","violation":{"ruleIdentifier":"function_body_length","ruleName":"Function Body Length","location":{"character":5,"file":"Tera\/State\/TeraConfigurationStore.swift","line":148},"reason":"Function body should span 100 lines or less excluding comments and whitespace: currently spans 109 lines","ruleDescription":"Function bodies should not span too many lines","severity":"error"}},{"text":"}","violation":{"ruleIdentifier":"file_length","ruleName":"File Length","location":{"character":1,"file":"Tera\/State\/TeraConfigurationStore.swift","line":759},"reason":"File should contain 400 lines or less: currently contains 759","ruleDescription":"Files should not span too many lines.","severity":"error"}},{"text":"actor TeraSessionStore {","violation":{"ruleIdentifier":"type_body_length","ruleName":"Type Body Length","location":{"character":1,"file":"Tera\/State\/TeraSessionStore.swift","line":39},"reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 390 lines","ruleDescription":"Type bodies should not span too many lines","severity":"error"}},{"text":" private func start(acceptingReconfiguration: Bool) async -> TeraSessionPhase {","violation":{"ruleIdentifier":"cyclomatic_complexity","ruleName":"Cyclomatic Complexity","location":{"character":11,"file":"Tera\/State\/TeraSessionStore.swift","line":125},"reason":"Function should have complexity 10 or less; currently complexity is 13","ruleDescription":"Complexity of function bodies should be limited.","severity":"error"}},{"text":"}","violation":{"ruleIdentifier":"file_length","ruleName":"File Length","location":{"character":1,"file":"Tera\/State\/TeraSessionStore.swift","line":452},"reason":"File should contain 400 lines or less: currently contains 452","ruleDescription":"Files should not span too many lines.","severity":"error"}},{"text":"struct TeraAddView: View {","violation":{"ruleIdentifier":"type_body_length","ruleName":"Type Body Length","location":{"character":1,"file":"Tera\/Views\/TeraAddView.swift","line":4},"reason":"Struct body should span 350 lines or less excluding comments and whitespace: currently spans 454 lines","ruleDescription":"Type bodies should not span too many lines","severity":"error"}},{"text":"}","violation":{"ruleIdentifier":"file_length","ruleName":"File Length","location":{"character":1,"file":"Tera\/Views\/TeraAddView.swift","line":560},"reason":"File should contain 400 lines or less: currently contains 560","ruleDescription":"Files should not span too many lines.","severity":"error"}},{"text":"struct TeraSettingsView: View {","violation":{"ruleIdentifier":"type_body_length","ruleName":"Type Body Length","location":{"character":1,"file":"Tera\/Views\/TeraSupportingViews.swift","line":365},"reason":"Struct body should span 250 lines or less excluding comments and whitespace: currently spans 259 lines","ruleDescription":"Type bodies should not span too many lines","severity":"error"}},{"text":"}","violation":{"ruleIdentifier":"file_length","ruleName":"File Length","location":{"character":1,"file":"Tera\/Views\/TeraSupportingViews.swift","line":629},"reason":"File should contain 400 lines or less: currently contains 629","ruleDescription":"Files should not span too many lines.","severity":"error"}},{"text":"final class TeraAddStoreTests: XCTestCase {","violation":{"ruleIdentifier":"type_body_length","ruleName":"Type Body Length","location":{"character":7,"file":"TeraTests\/TeraAddStoreTests.swift","line":6},"reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 665 lines","ruleDescription":"Type bodies should not span too many lines","severity":"error"}},{"text":"private actor AddBackend: TeraRuntimeBackend {","violation":{"ruleIdentifier":"type_body_length","ruleName":"Type Body Length","location":{"character":9,"file":"TeraTests\/TeraAddStoreTests.swift","line":844},"reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 492 lines","ruleDescription":"Type bodies should not span too many lines","severity":"error"}},{"text":" private func makeStatus(","violation":{"ruleIdentifier":"function_parameter_count","ruleName":"Function Parameter Count","location":{"character":11,"file":"TeraTests\/TeraAddStoreTests.swift","line":1280},"reason":"Function should have 5 parameters or less: it currently has 8","ruleDescription":"Number of function parameters should be low.","severity":"error"}},{"text":"}","violation":{"ruleIdentifier":"file_length","ruleName":"File Length","location":{"character":1,"file":"TeraTests\/TeraAddStoreTests.swift","line":1431},"reason":"File should contain 1000 lines or less: currently contains 1431","ruleDescription":"Files should not span too many lines.","severity":"error"}},{"text":"final class TeraRemoteQualificationTests: XCTestCase {","violation":{"ruleIdentifier":"type_body_length","ruleName":"Type Body Length","location":{"character":7,"file":"TeraTests\/TeraRemoteQualificationTests.swift","line":7},"reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 307 lines","ruleDescription":"Type bodies should not span too many lines","severity":"error"}},{"text":" func testQualificationRejectsAmbiguousOrUnsafeHarnessValues() {","violation":{"ruleIdentifier":"function_body_length","ruleName":"Function Body Length","location":{"character":3,"file":"TeraTests\/TeraRemoteQualificationTests.swift","line":45},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 94 lines","ruleDescription":"Function bodies should not span too many lines","severity":"error"}},{"text":" func testAuthorizationEvidenceIsRedactedEphemeralAndRelaunchCleaned() throws {","violation":{"ruleIdentifier":"function_body_length","ruleName":"Function Body Length","location":{"character":3,"file":"TeraTests\/TeraRemoteQualificationTests.swift","line":212},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 53 lines","ruleDescription":"Function bodies should not span too many lines","severity":"error"}},{"text":"final class TeraRuntimeClientTests: XCTestCase {","violation":{"ruleIdentifier":"type_body_length","ruleName":"Type Body Length","location":{"character":7,"file":"TeraTests\/TeraRuntimeClientTests.swift","line":4},"reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 298 lines","ruleDescription":"Type bodies should not span too many lines","severity":"error"}},{"text":"final class TeraStateMigrationTests: XCTestCase {","violation":{"ruleIdentifier":"type_body_length","ruleName":"Type Body Length","location":{"character":7,"file":"TeraTests\/TeraStateMigrationTests.swift","line":6},"reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 580 lines","ruleDescription":"Type bodies should not span too many lines","severity":"error"}},{"text":" func testRelayValidationMatchesRuntimeProfiles() throws {","violation":{"ruleIdentifier":"function_body_length","ruleName":"Function Body Length","location":{"character":5,"file":"TeraTests\/TeraStateMigrationTests.swift","line":7},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 61 lines","ruleDescription":"Function bodies should not span too many lines","severity":"error"}},{"text":"}","violation":{"ruleIdentifier":"file_length","ruleName":"File Length","location":{"character":1,"file":"TeraTests\/TeraStateMigrationTests.swift","line":622},"reason":"File should contain 400 lines or less: currently contains 622","ruleDescription":"Files should not span too many lines.","severity":"error"}},{"text":"private actor SupportingBackend: TeraRuntimeBackend {","violation":{"ruleIdentifier":"type_body_length","ruleName":"Type Body Length","location":{"character":9,"file":"TeraTests\/TeraSupportingStoreTests.swift","line":207},"reason":"Actor body should span 250 lines or less excluding comments and whitespace: currently spans 253 lines","ruleDescription":"Type bodies should not span too many lines","severity":"error"}},{"text":"}","violation":{"ruleIdentifier":"file_length","ruleName":"File Length","location":{"character":1,"file":"TeraTests\/TeraSupportingStoreTests.swift","line":487},"reason":"File should contain 400 lines or less: currently contains 487","ruleDescription":"Files should not span too many lines.","severity":"error"}},{"text":"final class TeraRemoteQualificationUITests: XCTestCase {","violation":{"ruleIdentifier":"type_body_length","ruleName":"Type Body Length","location":{"character":7,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":4},"reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 1493 lines","ruleDescription":"Type bodies should not span too many lines","severity":"error"}},{"text":" func testLocalSocialFiveFlowScenario() throws {","violation":{"ruleIdentifier":"function_body_length","ruleName":"Function Body Length","location":{"character":3,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":62},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 73 lines","ruleDescription":"Function bodies should not span too many lines","severity":"error"}},{"text":" func testLocalSocialDeterministicPersonas() throws {","violation":{"ruleIdentifier":"function_body_length","ruleName":"Function Body Length","location":{"character":3,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":174},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 66 lines","ruleDescription":"Function bodies should not span too many lines","severity":"error"}},{"text":" func testRemoteBlossomUploadAndRecovery() throws {","violation":{"ruleIdentifier":"function_body_length","ruleName":"Function Body Length","location":{"character":3,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":247},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 54 lines","ruleDescription":"Function bodies should not span too many lines","severity":"error"}},{"text":" private func expectedAddFieldLabel(_ identifier: String, type: String) -> String {","violation":{"ruleIdentifier":"cyclomatic_complexity","ruleName":"Cyclomatic Complexity","location":{"character":11,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":573},"reason":"Function should have complexity 10 or less; currently complexity is 18","ruleDescription":"Complexity of function bodies should be limited.","severity":"error"}},{"text":" private func preparePhotoUpdate(_ app: XCUIApplication, marker: String) -> XCUIElement? {","violation":{"ruleIdentifier":"cyclomatic_complexity","ruleName":"Cyclomatic Complexity","location":{"character":11,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":767},"reason":"Function should have complexity 10 or less; currently complexity is 11","ruleDescription":"Complexity of function bodies should be limited.","severity":"error"}},{"text":" private func preparePhotoUpdate(_ app: XCUIApplication, marker: String) -> XCUIElement? {","violation":{"ruleIdentifier":"function_body_length","ruleName":"Function Body Length","location":{"character":11,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":767},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 66 lines","ruleDescription":"Function bodies should not span too many lines","severity":"error"}},{"text":" static func environment(","violation":{"ruleIdentifier":"function_body_length","ruleName":"Function Body Length","location":{"character":10,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":1736},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 67 lines","ruleDescription":"Function bodies should not span too many lines","severity":"error"}},{"text":"}","violation":{"ruleIdentifier":"file_length","ruleName":"File Length","location":{"character":1,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":2212},"reason":"File should contain 1000 lines or less: currently contains 2212","ruleDescription":"Files should not span too many lines.","severity":"error"}}] -\ No newline at end of file +[{"violation":{"reason":"Initializer body should span 50 lines or less excluding comments and whitespace: currently spans 52 lines","ruleIdentifier":"function_body_length","severity":"error","ruleDescription":"Function bodies should not span too many lines","ruleName":"Function Body Length","location":{"line":21,"file":"Tera\/App\/TeraAppModel.swift","character":3}},"text":" init("},{"violation":{"reason":"Actor body should span 250 lines or less excluding comments and whitespace: currently spans 330 lines","ruleIdentifier":"type_body_length","severity":"error","ruleDescription":"Type bodies should not span too many lines","ruleName":"Type Body Length","location":{"line":51,"file":"Tera\/Runtime\/TeraAddMediaCoordinator.swift","character":1}},"text":"actor TeraAddMediaCoordinator: TeraAddMediaHandling {"},{"violation":{"reason":"File should contain 400 lines or less: currently contains 458","ruleIdentifier":"file_length","severity":"error","ruleDescription":"Files should not span too many lines.","ruleName":"File Length","location":{"line":458,"file":"Tera\/Runtime\/TeraAddMediaCoordinator.swift","character":1}},"text":"#endif"},{"violation":{"reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 491 lines","ruleIdentifier":"type_body_length","severity":"error","ruleDescription":"Type bodies should not span too many lines","ruleName":"Type Body Length","location":{"line":111,"file":"Tera\/Runtime\/TeraGeneratedRuntimeBackend.swift","character":15}},"text":"private final class TeraGeneratedRuntimeBackend: TeraRuntimeBackend, @unchecked Sendable {"},{"violation":{"reason":"File should contain 1000 lines or less: currently contains 1576","ruleIdentifier":"file_length","severity":"error","ruleDescription":"Files should not span too many lines.","ruleName":"File Length","location":{"line":1576,"file":"Tera\/Runtime\/TeraGeneratedRuntimeBackend.swift","character":1}},"text":"}"},{"violation":{"reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 753 lines","ruleIdentifier":"type_body_length","severity":"error","ruleDescription":"Type bodies should not span too many lines","ruleName":"Type Body Length","location":{"line":3,"file":"Tera\/Runtime\/TeraRuntimeClient.swift","character":1}},"text":"actor TeraRuntimeClient {"},{"violation":{"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 70 lines","ruleIdentifier":"function_body_length","severity":"error","ruleDescription":"Function bodies should not span too many lines","ruleName":"Function Body Length","location":{"line":379,"file":"Tera\/Runtime\/TeraRuntimeClient.swift","character":3}},"text":" func changes(bufferCapacity: Int = 16) async throws -> AsyncStream<TeraRuntimeChange> {"},{"violation":{"reason":"File should contain 400 lines or less: currently contains 835","ruleIdentifier":"file_length","severity":"error","ruleDescription":"Files should not span too many lines.","ruleName":"File Length","location":{"line":835,"file":"Tera\/Runtime\/TeraRuntimeClient.swift","character":1}},"text":"}"},{"violation":{"reason":"File should contain 1000 lines or less: currently contains 1175","ruleIdentifier":"file_length","severity":"error","ruleDescription":"Files should not span too many lines.","ruleName":"File Length","location":{"line":1175,"file":"Tera\/Runtime\/TeraRuntimeModels.swift","character":1}},"text":"}"},{"violation":{"reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 731 lines","ruleIdentifier":"type_body_length","severity":"error","ruleDescription":"Type bodies should not span too many lines","ruleName":"Type Body Length","location":{"line":11,"file":"Tera\/State\/TeraAddStore.swift","character":7}},"text":"final class TeraAddStore: ObservableObject {"},{"violation":{"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 51 lines","ruleIdentifier":"function_body_length","severity":"error","ruleDescription":"Function bodies should not span too many lines","ruleName":"Function Body Length","location":{"line":459,"file":"Tera\/State\/TeraAddStore.swift","character":3}},"text":" func retract(_ card: TeraTodayCard) async {"},{"violation":{"reason":"File should contain 400 lines or less: currently contains 796","ruleIdentifier":"file_length","severity":"error","ruleDescription":"Files should not span too many lines.","ruleName":"File Length","location":{"line":796,"file":"Tera\/State\/TeraAddStore.swift","character":1}},"text":"}"},{"violation":{"reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 468 lines","ruleIdentifier":"type_body_length","severity":"error","ruleDescription":"Type bodies should not span too many lines","ruleName":"Type Body Length","location":{"line":78,"file":"Tera\/State\/TeraConfigurationStore.swift","character":1}},"text":"actor TeraConfigurationStore {"},{"violation":{"reason":"Function body should span 100 lines or less excluding comments and whitespace: currently spans 109 lines","ruleIdentifier":"function_body_length","severity":"error","ruleDescription":"Function bodies should not span too many lines","ruleName":"Function Body Length","location":{"line":148,"file":"Tera\/State\/TeraConfigurationStore.swift","character":5}},"text":" func load() throws -> TeraAppConfiguration {"},{"violation":{"reason":"File should contain 400 lines or less: currently contains 759","ruleIdentifier":"file_length","severity":"error","ruleDescription":"Files should not span too many lines.","ruleName":"File Length","location":{"line":759,"file":"Tera\/State\/TeraConfigurationStore.swift","character":1}},"text":"}"},{"violation":{"reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 390 lines","ruleIdentifier":"type_body_length","severity":"error","ruleDescription":"Type bodies should not span too many lines","ruleName":"Type Body Length","location":{"line":39,"file":"Tera\/State\/TeraSessionStore.swift","character":1}},"text":"actor TeraSessionStore {"},{"violation":{"reason":"Function should have complexity 10 or less; currently complexity is 13","ruleIdentifier":"cyclomatic_complexity","severity":"error","ruleDescription":"Complexity of function bodies should be limited.","ruleName":"Cyclomatic Complexity","location":{"line":125,"file":"Tera\/State\/TeraSessionStore.swift","character":11}},"text":" private func start(acceptingReconfiguration: Bool) async -> TeraSessionPhase {"},{"violation":{"reason":"File should contain 400 lines or less: currently contains 452","ruleIdentifier":"file_length","severity":"error","ruleDescription":"Files should not span too many lines.","ruleName":"File Length","location":{"line":452,"file":"Tera\/State\/TeraSessionStore.swift","character":1}},"text":"}"},{"violation":{"reason":"Struct body should span 350 lines or less excluding comments and whitespace: currently spans 454 lines","ruleIdentifier":"type_body_length","severity":"error","ruleDescription":"Type bodies should not span too many lines","ruleName":"Type Body Length","location":{"line":4,"file":"Tera\/Views\/TeraAddView.swift","character":1}},"text":"struct TeraAddView: View {"},{"violation":{"reason":"File should contain 400 lines or less: currently contains 560","ruleIdentifier":"file_length","severity":"error","ruleDescription":"Files should not span too many lines.","ruleName":"File Length","location":{"line":560,"file":"Tera\/Views\/TeraAddView.swift","character":1}},"text":"}"},{"violation":{"reason":"Struct body should span 250 lines or less excluding comments and whitespace: currently spans 259 lines","ruleIdentifier":"type_body_length","severity":"error","ruleDescription":"Type bodies should not span too many lines","ruleName":"Type Body Length","location":{"line":365,"file":"Tera\/Views\/TeraSupportingViews.swift","character":1}},"text":"struct TeraSettingsView: View {"},{"violation":{"reason":"File should contain 400 lines or less: currently contains 629","ruleIdentifier":"file_length","severity":"error","ruleDescription":"Files should not span too many lines.","ruleName":"File Length","location":{"line":629,"file":"Tera\/Views\/TeraSupportingViews.swift","character":1}},"text":"}"},{"violation":{"reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 665 lines","ruleIdentifier":"type_body_length","severity":"error","ruleDescription":"Type bodies should not span too many lines","ruleName":"Type Body Length","location":{"line":6,"file":"TeraTests\/TeraAddStoreTests.swift","character":7}},"text":"final class TeraAddStoreTests: XCTestCase {"},{"violation":{"reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 492 lines","ruleIdentifier":"type_body_length","severity":"error","ruleDescription":"Type bodies should not span too many lines","ruleName":"Type Body Length","location":{"line":844,"file":"TeraTests\/TeraAddStoreTests.swift","character":9}},"text":"private actor AddBackend: TeraRuntimeBackend {"},{"violation":{"reason":"Function should have 5 parameters or less: it currently has 8","ruleIdentifier":"function_parameter_count","severity":"error","ruleDescription":"Number of function parameters should be low.","ruleName":"Function Parameter Count","location":{"line":1280,"file":"TeraTests\/TeraAddStoreTests.swift","character":11}},"text":" private func makeStatus("},{"violation":{"reason":"File should contain 1000 lines or less: currently contains 1431","ruleIdentifier":"file_length","severity":"error","ruleDescription":"Files should not span too many lines.","ruleName":"File Length","location":{"line":1431,"file":"TeraTests\/TeraAddStoreTests.swift","character":1}},"text":"}"},{"violation":{"reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 307 lines","ruleIdentifier":"type_body_length","severity":"error","ruleDescription":"Type bodies should not span too many lines","ruleName":"Type Body Length","location":{"line":7,"file":"TeraTests\/TeraRemoteQualificationTests.swift","character":7}},"text":"final class TeraRemoteQualificationTests: XCTestCase {"},{"violation":{"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 94 lines","ruleIdentifier":"function_body_length","severity":"error","ruleDescription":"Function bodies should not span too many lines","ruleName":"Function Body Length","location":{"line":45,"file":"TeraTests\/TeraRemoteQualificationTests.swift","character":3}},"text":" func testQualificationRejectsAmbiguousOrUnsafeHarnessValues() {"},{"violation":{"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 53 lines","ruleIdentifier":"function_body_length","severity":"error","ruleDescription":"Function bodies should not span too many lines","ruleName":"Function Body Length","location":{"line":212,"file":"TeraTests\/TeraRemoteQualificationTests.swift","character":3}},"text":" func testAuthorizationEvidenceIsRedactedEphemeralAndRelaunchCleaned() throws {"},{"violation":{"reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 298 lines","ruleIdentifier":"type_body_length","severity":"error","ruleDescription":"Type bodies should not span too many lines","ruleName":"Type Body Length","location":{"line":4,"file":"TeraTests\/TeraRuntimeClientTests.swift","character":7}},"text":"final class TeraRuntimeClientTests: XCTestCase {"},{"violation":{"reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 580 lines","ruleIdentifier":"type_body_length","severity":"error","ruleDescription":"Type bodies should not span too many lines","ruleName":"Type Body Length","location":{"line":6,"file":"TeraTests\/TeraStateMigrationTests.swift","character":7}},"text":"final class TeraStateMigrationTests: XCTestCase {"},{"violation":{"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 61 lines","ruleIdentifier":"function_body_length","severity":"error","ruleDescription":"Function bodies should not span too many lines","ruleName":"Function Body Length","location":{"line":7,"file":"TeraTests\/TeraStateMigrationTests.swift","character":5}},"text":" func testRelayValidationMatchesRuntimeProfiles() throws {"},{"violation":{"reason":"File should contain 400 lines or less: currently contains 622","ruleIdentifier":"file_length","severity":"error","ruleDescription":"Files should not span too many lines.","ruleName":"File Length","location":{"line":622,"file":"TeraTests\/TeraStateMigrationTests.swift","character":1}},"text":"}"},{"violation":{"reason":"Actor body should span 250 lines or less excluding comments and whitespace: currently spans 253 lines","ruleIdentifier":"type_body_length","severity":"error","ruleDescription":"Type bodies should not span too many lines","ruleName":"Type Body Length","location":{"line":207,"file":"TeraTests\/TeraSupportingStoreTests.swift","character":9}},"text":"private actor SupportingBackend: TeraRuntimeBackend {"},{"violation":{"reason":"File should contain 400 lines or less: currently contains 487","ruleIdentifier":"file_length","severity":"error","ruleDescription":"Files should not span too many lines.","ruleName":"File Length","location":{"line":487,"file":"TeraTests\/TeraSupportingStoreTests.swift","character":1}},"text":"}"},{"violation":{"reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 1493 lines","ruleIdentifier":"type_body_length","severity":"error","ruleDescription":"Type bodies should not span too many lines","ruleName":"Type Body Length","location":{"line":4,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":7}},"text":"final class TeraRemoteQualificationUITests: XCTestCase {"},{"violation":{"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 73 lines","ruleIdentifier":"function_body_length","severity":"error","ruleDescription":"Function bodies should not span too many lines","ruleName":"Function Body Length","location":{"line":62,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":3}},"text":" func testLocalSocialFiveFlowScenario() throws {"},{"violation":{"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 66 lines","ruleIdentifier":"function_body_length","severity":"error","ruleDescription":"Function bodies should not span too many lines","ruleName":"Function Body Length","location":{"line":174,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":3}},"text":" func testLocalSocialDeterministicPersonas() throws {"},{"violation":{"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 54 lines","ruleIdentifier":"function_body_length","severity":"error","ruleDescription":"Function bodies should not span too many lines","ruleName":"Function Body Length","location":{"line":247,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":3}},"text":" func testRemoteBlossomUploadAndRecovery() throws {"},{"violation":{"reason":"Function should have complexity 10 or less; currently complexity is 18","ruleIdentifier":"cyclomatic_complexity","severity":"error","ruleDescription":"Complexity of function bodies should be limited.","ruleName":"Cyclomatic Complexity","location":{"line":573,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":11}},"text":" private func expectedAddFieldLabel(_ identifier: String, type: String) -> String {"},{"violation":{"reason":"Function should have complexity 10 or less; currently complexity is 11","ruleIdentifier":"cyclomatic_complexity","severity":"error","ruleDescription":"Complexity of function bodies should be limited.","ruleName":"Cyclomatic Complexity","location":{"line":767,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":11}},"text":" private func preparePhotoUpdate(_ app: XCUIApplication, marker: String) -> XCUIElement? {"},{"violation":{"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 66 lines","ruleIdentifier":"function_body_length","severity":"error","ruleDescription":"Function bodies should not span too many lines","ruleName":"Function Body Length","location":{"line":767,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":11}},"text":" private func preparePhotoUpdate(_ app: XCUIApplication, marker: String) -> XCUIElement? {"},{"violation":{"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 67 lines","ruleIdentifier":"function_body_length","severity":"error","ruleDescription":"Function bodies should not span too many lines","ruleName":"Function Body Length","location":{"line":1736,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":10}},"text":" static func environment("},{"violation":{"reason":"File should contain 1000 lines or less: currently contains 2212","ruleIdentifier":"file_length","severity":"error","ruleDescription":"Files should not span too many lines.","ruleName":"File Length","location":{"line":2212,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":1}},"text":"}"}] +\ No newline at end of file