field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

commit 0f592663540ea913f5c577c66739259ce15cfdca
parent d81684032d11bdc8b89b8613b6129873d1800e9b
Author: triesap <tyson@radroots.org>
Date:   Thu, 10 Sep 2026 04:32:08 +0000

calendar: Distinguish civil dates from instants

- Reuse validated shared civil dates and source timezone values
- Preserve exclusive timing semantics without fabricating epochs
- Reconstruct legacy presentation from unchanged signed history
- Qualify exact owned artifacts and current native consumers

Diffstat:
MTeraFFI/provenance.json | 54+++++++++++++++++++++++++++---------------------------
MTeraFFI/source.lock | 4++--
MTeraFFI/source/aarch64-apple-darwin.json | 74++++++++++++++++++++++++++++++++++++++++++++++----------------------------
MTeraFFI/source/aarch64-apple-ios-sim.json | 74++++++++++++++++++++++++++++++++++++++++++++++----------------------------
MTeraFFI/source/aarch64-apple-ios.json | 74++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Mcore/crates/tera_core/Cargo.toml | 1+
Mcore/crates/tera_core/src/runtime/product_surface.rs | 2++
Acore/crates/tera_core/src/runtime/product_surface/calendar_timing.rs | 178+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/crates/tera_core/src/runtime/product_surface/calendar_timing_tests.rs | 140+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcore/crates/tera_core/src/runtime/product_surface/model.rs | 10++++++++--
Mcore/crates/tera_core/src/runtime/product_surface/projection.rs | 48+++++++++++++++++++++---------------------------
Mcore/crates/tera_core/src/runtime/product_surface/ranking.rs | 58++++++++++++++++++++++++++++++++++++++++++++--------------
Mcore/crates/tera_core/src/runtime/product_surface/today.rs | 84++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Acore/crates/tera_core/src/runtime/product_surface/today_calendar_tests.rs | 181+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcore/crates/tera_core/src/runtime/product_surface/today_reconciliation.rs | 3++-
Mcore/crates/tera_ffi/src/dto.rs | 29+++++++++++++++++++----------
Mrelease/provenance.json | 4++--
Mtest-fixtures/legacy-identifiers.v1.json | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mtest-fixtures/swiftlint-maintainability-baseline.v1.json | 4++--
19 files changed, 892 insertions(+), 186 deletions(-)

diff --git a/TeraFFI/provenance.json b/TeraFFI/provenance.json @@ -22,9 +22,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 71483336, + "bytes": 71610976, "path": "TeraFFI.xcframework/ios-arm64-simulator/libtera_ffi.a", - "sha256": "df300d70e6f32b68f5ac4366b3342a692c8d8a3ba243965d9c9ad60634d897ff" + "sha256": "662657eb7099db9d8402e9009bf943d4719511354262ab4eae66cecd93f25313" }, { "bytes": 71393, @@ -37,9 +37,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 71534912, + "bytes": 71664464, "path": "TeraFFI.xcframework/ios-arm64/libtera_ffi.a", - "sha256": "b6a0d44f19783376dd493b50bdc06ed61baf76c9e7a9b47b730a55198c5d22ae" + "sha256": "ab6e2ce40e0545a091c48d5a140f73bd909a59209f8e52c3210a92ca6ff3240b" }, { "bytes": 42544, @@ -77,34 +77,34 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 19948832, + "bytes": 20015200, "path": "native/aarch64-apple-darwin/libtera_ffi.dylib", - "sha256": "eb9be3233d95cfce09cc7e64a84e7a5b9d83d10ae1132efb57263c02165441e3" + "sha256": "fc797c39279c817724925b118d3a8b6207c6441826f665c8e391a07210078613" }, { - "bytes": 71483336, + "bytes": 71610976, "path": "native/aarch64-apple-ios-sim/libtera_ffi.a", - "sha256": "df300d70e6f32b68f5ac4366b3342a692c8d8a3ba243965d9c9ad60634d897ff" + "sha256": "662657eb7099db9d8402e9009bf943d4719511354262ab4eae66cecd93f25313" }, { - "bytes": 71534912, + "bytes": 71664464, "path": "native/aarch64-apple-ios/libtera_ffi.a", - "sha256": "b6a0d44f19783376dd493b50bdc06ed61baf76c9e7a9b47b730a55198c5d22ae" + "sha256": "ab6e2ce40e0545a091c48d5a140f73bd909a59209f8e52c3210a92ca6ff3240b" }, { - "bytes": 66937, + "bytes": 67809, "path": "source/aarch64-apple-darwin.json", - "sha256": "06721ef698bd79864cc5a690d0bd027ea66cd6079b739b834a79c143abf65170" + "sha256": "ecc80f8d4ea4c5902255fef11d1510a8f8831feb20ce5dffa8dbe4224bed3de1" }, { - "bytes": 66781, + "bytes": 67653, "path": "source/aarch64-apple-ios-sim.json", - "sha256": "e26e968883e963e27ec3dbe1b077d9d85f818a894951bd643902ea6c81e11219" + "sha256": "c4763194f42dfb1ec1990c1b2d816000ef627a1f3bc1302f5ffb3520ebfb361f" }, { - "bytes": 66777, + "bytes": 67649, "path": "source/aarch64-apple-ios.json", - "sha256": "9c651b6591d8d0caf19970ed2fddb9eb53132abc749a310e99896b570458386d" + "sha256": "162b977786728a10de19c7c17ae6a115ebde1a9bcd78ea8db2cd7a9ee81cc0cd" } ], "language": "swift", @@ -112,7 +112,7 @@ "schema": "radroots.artifact-manifest.v2", "source": { "repository": "https://github.com/radrootslabs/tera", - "tree": "703c94b59618f9ca1dd16c17df8fb07fb7b860bd" + "tree": "d2c902b79291f9ced96cd77f989c7812bbc9c4b2" }, "source_records": { "aarch64-apple-darwin": "source/aarch64-apple-darwin.json", @@ -139,9 +139,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 71483336, + "bytes": 71610976, "path": "Tera/Frameworks/TeraFFI.xcframework/ios-arm64-simulator/libtera_ffi.a", - "sha256": "df300d70e6f32b68f5ac4366b3342a692c8d8a3ba243965d9c9ad60634d897ff" + "sha256": "662657eb7099db9d8402e9009bf943d4719511354262ab4eae66cecd93f25313" }, { "bytes": 71393, @@ -154,9 +154,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 71534912, + "bytes": 71664464, "path": "Tera/Frameworks/TeraFFI.xcframework/ios-arm64/libtera_ffi.a", - "sha256": "b6a0d44f19783376dd493b50bdc06ed61baf76c9e7a9b47b730a55198c5d22ae" + "sha256": "ab6e2ce40e0545a091c48d5a140f73bd909a59209f8e52c3210a92ca6ff3240b" }, { "bytes": 512103, @@ -174,19 +174,19 @@ "sha256": "4cc03b03dd54db7b4ab22684ff819233444ae0a43a770ab875148892bed1e3f2" }, { - "bytes": 66937, + "bytes": 67809, "path": "TeraFFI/source/aarch64-apple-darwin.json", - "sha256": "06721ef698bd79864cc5a690d0bd027ea66cd6079b739b834a79c143abf65170" + "sha256": "ecc80f8d4ea4c5902255fef11d1510a8f8831feb20ce5dffa8dbe4224bed3de1" }, { - "bytes": 66781, + "bytes": 67653, "path": "TeraFFI/source/aarch64-apple-ios-sim.json", - "sha256": "e26e968883e963e27ec3dbe1b077d9d85f818a894951bd643902ea6c81e11219" + "sha256": "c4763194f42dfb1ec1990c1b2d816000ef627a1f3bc1302f5ffb3520ebfb361f" }, { - "bytes": 66777, + "bytes": 67649, "path": "TeraFFI/source/aarch64-apple-ios.json", - "sha256": "9c651b6591d8d0caf19970ed2fddb9eb53132abc749a310e99896b570458386d" + "sha256": "162b977786728a10de19c7c17ae6a115ebde1a9bcd78ea8db2cd7a9ee81cc0cd" } ], "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 = "703c94b59618f9ca1dd16c17df8fb07fb7b860bd" -manifest_sha256 = "f174e6196e6b8ffdadaabcf52c6d697ee7ba9e3acc55ff265cd928c1131d4010" +source_tree = "d2c902b79291f9ced96cd77f989c7812bbc9c4b2" +manifest_sha256 = "fe8ac77f19b400cb4db0a007ac3ffc96f12db882f8eb6a9723b3b5608a43b065" 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": 2415, - "git_blob": "cd499c9ebdff484def4f4c876b0188c0258eb815", + "bytes": 2426, + "git_blob": "d4e55ae9a02998a487b9eddab5fdabd25194b97c", "mode": "100644", - "sha256": "a5eaa1711202b506810837b639a26ed12f7486eccdb13f8b4504400ecb986a5d" + "sha256": "c50e87c9b170f42545fe7fc6efba929c89fbfdfa3d8b6501aa88d17bc8d41937" }, "core/crates/tera_core/build.rs": { "bytes": 2056, @@ -637,10 +637,10 @@ "sha256": "dff55e46521c26f5f0ece024453b55c15f132fa94f78e2cba3d141f123d4eca0" }, "core/crates/tera_core/src/runtime/product_surface.rs": { - "bytes": 6170, - "git_blob": "15b5322dc8003f52c2d2104071f9bce8e4f043e7", + "bytes": 6289, + "git_blob": "0c849dea1365bff736101b523e3875bd98d99f21", "mode": "100644", - "sha256": "21087052466e4e5664961b5f0af8c36f01baa5c0c4c5f7b57a3a9520350c0525" + "sha256": "6c7259ac3a865be2581ecc5d97a13f4c40731f1818b2ed57baa9b670126aa05b" }, "core/crates/tera_core/src/runtime/product_surface/authoring.rs": { "bytes": 11282, @@ -648,6 +648,18 @@ "mode": "100644", "sha256": "7fc683ca39eb27d52dcdd26bcdd9ade96e36a0b1f83c2135b3a1ce48515f988e" }, + "core/crates/tera_core/src/runtime/product_surface/calendar_timing.rs": { + "bytes": 5939, + "git_blob": "2e46833ae5301d9d5d33e941ae12a43860bed792", + "mode": "100644", + "sha256": "cf1ef2350953eaaad3323efcf810f889debcf9058a1f9d5db13253ba58e5f20a" + }, + "core/crates/tera_core/src/runtime/product_surface/calendar_timing_tests.rs": { + "bytes": 5367, + "git_blob": "998031509c595b7d99988952591add322f33a410", + "mode": "100644", + "sha256": "fc900f2ae0802c47ad5e7e9bb19cc51692ce5454b2ffa831e8f59b2dc6e5f9c2" + }, "core/crates/tera_core/src/runtime/product_surface/context.rs": { "bytes": 14211, "git_blob": "92498807101c638cec675e0b49fde1f4ceed17a2", @@ -685,10 +697,10 @@ "sha256": "3cdca11226903c23bed81dc796338061922df620163b8f06edeb8bc278794a00" }, "core/crates/tera_core/src/runtime/product_surface/model.rs": { - "bytes": 8101, - "git_blob": "2b99368604301dca7e7611821195680a42fa7597", + "bytes": 8476, + "git_blob": "588ca0d0707f8eb4f17e705374dfb231b1b2920f", "mode": "100644", - "sha256": "d22a98945384d7de1ec0d66f19fcaf7b6a9befc0e1cf1778727b7ed0280d945c" + "sha256": "b8a35018873e2d09096c1b4df8564c81f2837dcbcf1ad972e49fad1a59b524a6" }, "core/crates/tera_core/src/runtime/product_surface/outbox.rs": { "bytes": 170884, @@ -703,16 +715,16 @@ "sha256": "3c048e44551d04e607597761481c24e9bbe55f6b20358047d18c8013c093fb8f" }, "core/crates/tera_core/src/runtime/product_surface/projection.rs": { - "bytes": 23469, - "git_blob": "204fad344f268b66828d6112e544b3199f27c552", + "bytes": 23489, + "git_blob": "77795e05cdf3be698f3d94b0c7e828a529aae38d", "mode": "100644", - "sha256": "9b3eb39e9bbf9a22d92c14be5433ee97356fcfecb4ff77c66f49ecef243889ac" + "sha256": "b3b7ade140749f6ec02ae7c5c2a94bb8c6b9b53a83a7b91e5945f107201a4196" }, "core/crates/tera_core/src/runtime/product_surface/ranking.rs": { - "bytes": 8707, - "git_blob": "a216d127d747e0b1e423b48532995c3307674734", + "bytes": 9781, + "git_blob": "7f80393291a6889e27a7505e5d9d11ce9c6c75ea", "mode": "100644", - "sha256": "84cab243db775c801c87d0e9bb69a225af73d4283feb48d0ec3310ff46794642" + "sha256": "7b45e6c24302509856f7d4ea8e9e9a02a157e002ca099882e5288e855e89f5f7" }, "core/crates/tera_core/src/runtime/product_surface/settings.rs": { "bytes": 57107, @@ -721,10 +733,10 @@ "sha256": "76e3bddfc237dac63293d264d68991b489585136fe377e39dbba72aa3a1181f4" }, "core/crates/tera_core/src/runtime/product_surface/today.rs": { - "bytes": 140918, - "git_blob": "c8be6532fdb641bb5d0056cad8e07bd7ed6ae278", + "bytes": 143260, + "git_blob": "6fcffcc89b755356f65cff7abcfe57b6db005ec8", "mode": "100644", - "sha256": "1c76aafbd3d7ed373579a8658d47f9208059d2578db466e11d4148da663d4e99" + "sha256": "b4fd53f5c264500bc9a0a4e09e2f56904e41145ef75931d64fc6f2d46bf33735" }, "core/crates/tera_core/src/runtime/product_surface/today_admission_tests.rs": { "bytes": 14686, @@ -744,6 +756,12 @@ "mode": "100644", "sha256": "6f5810256ca221ca66ec4056e07fc635103e0ed02da01f5a8c53161535d7eaf6" }, + "core/crates/tera_core/src/runtime/product_surface/today_calendar_tests.rs": { + "bytes": 6463, + "git_blob": "252dd511bc8b9d52b9781b7babb1b8828d86919d", + "mode": "100644", + "sha256": "ff0fc470ef3e3274d57c88f078ceab88059636fb25aeeccae3c6f2a06e092eec" + }, "core/crates/tera_core/src/runtime/product_surface/today_counted_storage_tests.rs": { "bytes": 17547, "git_blob": "de61bd9c5fefa9cabf4818d84f9498564a6e5790", @@ -763,10 +781,10 @@ "sha256": "1158622c28cca9b6843b48199f4eb6d286a01cc15d7530e11b0fa25a2e00b93c" }, "core/crates/tera_core/src/runtime/product_surface/today_reconciliation.rs": { - "bytes": 2036, - "git_blob": "a85b9bc3a43885415147fef2d9b9a57338ed7427", + "bytes": 2048, + "git_blob": "2d584a10b520f5000705ed4516c28385af4457e1", "mode": "100644", - "sha256": "2000088c53e5e34b95d88e6de1615631a3ecb2dabf36e831f504dd0791afc5de" + "sha256": "f8c1f6572e1d996960800e56c8dd77aebd0ca6ed1018f4675646e76dee6bd381" }, "core/crates/tera_core/src/runtime/product_surface/today_reconciliation_tests.rs": { "bytes": 3424, @@ -853,10 +871,10 @@ "sha256": "f96248270139fbd131bf5290bf97560b7e12140d76e4fe77a071f92d6e76e451" }, "core/crates/tera_ffi/src/dto.rs": { - "bytes": 107309, - "git_blob": "09dedc5a8342b0d95e01c2de1fd8afcf05d37e07", + "bytes": 107758, + "git_blob": "f8c746adf993d7d8fa675332bf34fd261a165f47", "mode": "100644", - "sha256": "59fa33f8931f4ee672ce5a14d7ad22beff8f9ae61e29300e70b2f1e9ab72d6c0" + "sha256": "e2168374d97811d33b1df0f8410126eefdd3fa161b26342702062c6edde0d167" }, "core/crates/tera_ffi/src/dto/today_sync.rs": { "bytes": 4837, @@ -1441,13 +1459,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 116926, - "git_blob": "c3a54831006843505ce452a194ab72352dbac12a", + "bytes": 118536, + "git_blob": "fafa6f76026449cd0cc47f458e31dd067a94f38b", "mode": "100644", - "sha256": "20f5bc3bdcfaaa8eb6816543849e75107c498a52e45399f3f84314e5ecf6c8f0" + "sha256": "201494e468ffafea52652ffecab89bfa16399e0c4b6e5bed5617853be7e49fe5" } }, "policy": "staged_inputs", - "tree": "703c94b59618f9ca1dd16c17df8fb07fb7b860bd" + "tree": "d2c902b79291f9ced96cd77f989c7812bbc9c4b2" } } 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": 2415, - "git_blob": "cd499c9ebdff484def4f4c876b0188c0258eb815", + "bytes": 2426, + "git_blob": "d4e55ae9a02998a487b9eddab5fdabd25194b97c", "mode": "100644", - "sha256": "a5eaa1711202b506810837b639a26ed12f7486eccdb13f8b4504400ecb986a5d" + "sha256": "c50e87c9b170f42545fe7fc6efba929c89fbfdfa3d8b6501aa88d17bc8d41937" }, "core/crates/tera_core/build.rs": { "bytes": 2056, @@ -633,10 +633,10 @@ "sha256": "dff55e46521c26f5f0ece024453b55c15f132fa94f78e2cba3d141f123d4eca0" }, "core/crates/tera_core/src/runtime/product_surface.rs": { - "bytes": 6170, - "git_blob": "15b5322dc8003f52c2d2104071f9bce8e4f043e7", + "bytes": 6289, + "git_blob": "0c849dea1365bff736101b523e3875bd98d99f21", "mode": "100644", - "sha256": "21087052466e4e5664961b5f0af8c36f01baa5c0c4c5f7b57a3a9520350c0525" + "sha256": "6c7259ac3a865be2581ecc5d97a13f4c40731f1818b2ed57baa9b670126aa05b" }, "core/crates/tera_core/src/runtime/product_surface/authoring.rs": { "bytes": 11282, @@ -644,6 +644,18 @@ "mode": "100644", "sha256": "7fc683ca39eb27d52dcdd26bcdd9ade96e36a0b1f83c2135b3a1ce48515f988e" }, + "core/crates/tera_core/src/runtime/product_surface/calendar_timing.rs": { + "bytes": 5939, + "git_blob": "2e46833ae5301d9d5d33e941ae12a43860bed792", + "mode": "100644", + "sha256": "cf1ef2350953eaaad3323efcf810f889debcf9058a1f9d5db13253ba58e5f20a" + }, + "core/crates/tera_core/src/runtime/product_surface/calendar_timing_tests.rs": { + "bytes": 5367, + "git_blob": "998031509c595b7d99988952591add322f33a410", + "mode": "100644", + "sha256": "fc900f2ae0802c47ad5e7e9bb19cc51692ce5454b2ffa831e8f59b2dc6e5f9c2" + }, "core/crates/tera_core/src/runtime/product_surface/context.rs": { "bytes": 14211, "git_blob": "92498807101c638cec675e0b49fde1f4ceed17a2", @@ -681,10 +693,10 @@ "sha256": "3cdca11226903c23bed81dc796338061922df620163b8f06edeb8bc278794a00" }, "core/crates/tera_core/src/runtime/product_surface/model.rs": { - "bytes": 8101, - "git_blob": "2b99368604301dca7e7611821195680a42fa7597", + "bytes": 8476, + "git_blob": "588ca0d0707f8eb4f17e705374dfb231b1b2920f", "mode": "100644", - "sha256": "d22a98945384d7de1ec0d66f19fcaf7b6a9befc0e1cf1778727b7ed0280d945c" + "sha256": "b8a35018873e2d09096c1b4df8564c81f2837dcbcf1ad972e49fad1a59b524a6" }, "core/crates/tera_core/src/runtime/product_surface/outbox.rs": { "bytes": 170884, @@ -699,16 +711,16 @@ "sha256": "3c048e44551d04e607597761481c24e9bbe55f6b20358047d18c8013c093fb8f" }, "core/crates/tera_core/src/runtime/product_surface/projection.rs": { - "bytes": 23469, - "git_blob": "204fad344f268b66828d6112e544b3199f27c552", + "bytes": 23489, + "git_blob": "77795e05cdf3be698f3d94b0c7e828a529aae38d", "mode": "100644", - "sha256": "9b3eb39e9bbf9a22d92c14be5433ee97356fcfecb4ff77c66f49ecef243889ac" + "sha256": "b3b7ade140749f6ec02ae7c5c2a94bb8c6b9b53a83a7b91e5945f107201a4196" }, "core/crates/tera_core/src/runtime/product_surface/ranking.rs": { - "bytes": 8707, - "git_blob": "a216d127d747e0b1e423b48532995c3307674734", + "bytes": 9781, + "git_blob": "7f80393291a6889e27a7505e5d9d11ce9c6c75ea", "mode": "100644", - "sha256": "84cab243db775c801c87d0e9bb69a225af73d4283feb48d0ec3310ff46794642" + "sha256": "7b45e6c24302509856f7d4ea8e9e9a02a157e002ca099882e5288e855e89f5f7" }, "core/crates/tera_core/src/runtime/product_surface/settings.rs": { "bytes": 57107, @@ -717,10 +729,10 @@ "sha256": "76e3bddfc237dac63293d264d68991b489585136fe377e39dbba72aa3a1181f4" }, "core/crates/tera_core/src/runtime/product_surface/today.rs": { - "bytes": 140918, - "git_blob": "c8be6532fdb641bb5d0056cad8e07bd7ed6ae278", + "bytes": 143260, + "git_blob": "6fcffcc89b755356f65cff7abcfe57b6db005ec8", "mode": "100644", - "sha256": "1c76aafbd3d7ed373579a8658d47f9208059d2578db466e11d4148da663d4e99" + "sha256": "b4fd53f5c264500bc9a0a4e09e2f56904e41145ef75931d64fc6f2d46bf33735" }, "core/crates/tera_core/src/runtime/product_surface/today_admission_tests.rs": { "bytes": 14686, @@ -740,6 +752,12 @@ "mode": "100644", "sha256": "6f5810256ca221ca66ec4056e07fc635103e0ed02da01f5a8c53161535d7eaf6" }, + "core/crates/tera_core/src/runtime/product_surface/today_calendar_tests.rs": { + "bytes": 6463, + "git_blob": "252dd511bc8b9d52b9781b7babb1b8828d86919d", + "mode": "100644", + "sha256": "ff0fc470ef3e3274d57c88f078ceab88059636fb25aeeccae3c6f2a06e092eec" + }, "core/crates/tera_core/src/runtime/product_surface/today_counted_storage_tests.rs": { "bytes": 17547, "git_blob": "de61bd9c5fefa9cabf4818d84f9498564a6e5790", @@ -759,10 +777,10 @@ "sha256": "1158622c28cca9b6843b48199f4eb6d286a01cc15d7530e11b0fa25a2e00b93c" }, "core/crates/tera_core/src/runtime/product_surface/today_reconciliation.rs": { - "bytes": 2036, - "git_blob": "a85b9bc3a43885415147fef2d9b9a57338ed7427", + "bytes": 2048, + "git_blob": "2d584a10b520f5000705ed4516c28385af4457e1", "mode": "100644", - "sha256": "2000088c53e5e34b95d88e6de1615631a3ecb2dabf36e831f504dd0791afc5de" + "sha256": "f8c1f6572e1d996960800e56c8dd77aebd0ca6ed1018f4675646e76dee6bd381" }, "core/crates/tera_core/src/runtime/product_surface/today_reconciliation_tests.rs": { "bytes": 3424, @@ -849,10 +867,10 @@ "sha256": "f96248270139fbd131bf5290bf97560b7e12140d76e4fe77a071f92d6e76e451" }, "core/crates/tera_ffi/src/dto.rs": { - "bytes": 107309, - "git_blob": "09dedc5a8342b0d95e01c2de1fd8afcf05d37e07", + "bytes": 107758, + "git_blob": "f8c746adf993d7d8fa675332bf34fd261a165f47", "mode": "100644", - "sha256": "59fa33f8931f4ee672ce5a14d7ad22beff8f9ae61e29300e70b2f1e9ab72d6c0" + "sha256": "e2168374d97811d33b1df0f8410126eefdd3fa161b26342702062c6edde0d167" }, "core/crates/tera_ffi/src/dto/today_sync.rs": { "bytes": 4837, @@ -1437,13 +1455,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 116926, - "git_blob": "c3a54831006843505ce452a194ab72352dbac12a", + "bytes": 118536, + "git_blob": "fafa6f76026449cd0cc47f458e31dd067a94f38b", "mode": "100644", - "sha256": "20f5bc3bdcfaaa8eb6816543849e75107c498a52e45399f3f84314e5ecf6c8f0" + "sha256": "201494e468ffafea52652ffecab89bfa16399e0c4b6e5bed5617853be7e49fe5" } }, "policy": "staged_inputs", - "tree": "703c94b59618f9ca1dd16c17df8fb07fb7b860bd" + "tree": "d2c902b79291f9ced96cd77f989c7812bbc9c4b2" } } 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": 2415, - "git_blob": "cd499c9ebdff484def4f4c876b0188c0258eb815", + "bytes": 2426, + "git_blob": "d4e55ae9a02998a487b9eddab5fdabd25194b97c", "mode": "100644", - "sha256": "a5eaa1711202b506810837b639a26ed12f7486eccdb13f8b4504400ecb986a5d" + "sha256": "c50e87c9b170f42545fe7fc6efba929c89fbfdfa3d8b6501aa88d17bc8d41937" }, "core/crates/tera_core/build.rs": { "bytes": 2056, @@ -633,10 +633,10 @@ "sha256": "dff55e46521c26f5f0ece024453b55c15f132fa94f78e2cba3d141f123d4eca0" }, "core/crates/tera_core/src/runtime/product_surface.rs": { - "bytes": 6170, - "git_blob": "15b5322dc8003f52c2d2104071f9bce8e4f043e7", + "bytes": 6289, + "git_blob": "0c849dea1365bff736101b523e3875bd98d99f21", "mode": "100644", - "sha256": "21087052466e4e5664961b5f0af8c36f01baa5c0c4c5f7b57a3a9520350c0525" + "sha256": "6c7259ac3a865be2581ecc5d97a13f4c40731f1818b2ed57baa9b670126aa05b" }, "core/crates/tera_core/src/runtime/product_surface/authoring.rs": { "bytes": 11282, @@ -644,6 +644,18 @@ "mode": "100644", "sha256": "7fc683ca39eb27d52dcdd26bcdd9ade96e36a0b1f83c2135b3a1ce48515f988e" }, + "core/crates/tera_core/src/runtime/product_surface/calendar_timing.rs": { + "bytes": 5939, + "git_blob": "2e46833ae5301d9d5d33e941ae12a43860bed792", + "mode": "100644", + "sha256": "cf1ef2350953eaaad3323efcf810f889debcf9058a1f9d5db13253ba58e5f20a" + }, + "core/crates/tera_core/src/runtime/product_surface/calendar_timing_tests.rs": { + "bytes": 5367, + "git_blob": "998031509c595b7d99988952591add322f33a410", + "mode": "100644", + "sha256": "fc900f2ae0802c47ad5e7e9bb19cc51692ce5454b2ffa831e8f59b2dc6e5f9c2" + }, "core/crates/tera_core/src/runtime/product_surface/context.rs": { "bytes": 14211, "git_blob": "92498807101c638cec675e0b49fde1f4ceed17a2", @@ -681,10 +693,10 @@ "sha256": "3cdca11226903c23bed81dc796338061922df620163b8f06edeb8bc278794a00" }, "core/crates/tera_core/src/runtime/product_surface/model.rs": { - "bytes": 8101, - "git_blob": "2b99368604301dca7e7611821195680a42fa7597", + "bytes": 8476, + "git_blob": "588ca0d0707f8eb4f17e705374dfb231b1b2920f", "mode": "100644", - "sha256": "d22a98945384d7de1ec0d66f19fcaf7b6a9befc0e1cf1778727b7ed0280d945c" + "sha256": "b8a35018873e2d09096c1b4df8564c81f2837dcbcf1ad972e49fad1a59b524a6" }, "core/crates/tera_core/src/runtime/product_surface/outbox.rs": { "bytes": 170884, @@ -699,16 +711,16 @@ "sha256": "3c048e44551d04e607597761481c24e9bbe55f6b20358047d18c8013c093fb8f" }, "core/crates/tera_core/src/runtime/product_surface/projection.rs": { - "bytes": 23469, - "git_blob": "204fad344f268b66828d6112e544b3199f27c552", + "bytes": 23489, + "git_blob": "77795e05cdf3be698f3d94b0c7e828a529aae38d", "mode": "100644", - "sha256": "9b3eb39e9bbf9a22d92c14be5433ee97356fcfecb4ff77c66f49ecef243889ac" + "sha256": "b3b7ade140749f6ec02ae7c5c2a94bb8c6b9b53a83a7b91e5945f107201a4196" }, "core/crates/tera_core/src/runtime/product_surface/ranking.rs": { - "bytes": 8707, - "git_blob": "a216d127d747e0b1e423b48532995c3307674734", + "bytes": 9781, + "git_blob": "7f80393291a6889e27a7505e5d9d11ce9c6c75ea", "mode": "100644", - "sha256": "84cab243db775c801c87d0e9bb69a225af73d4283feb48d0ec3310ff46794642" + "sha256": "7b45e6c24302509856f7d4ea8e9e9a02a157e002ca099882e5288e855e89f5f7" }, "core/crates/tera_core/src/runtime/product_surface/settings.rs": { "bytes": 57107, @@ -717,10 +729,10 @@ "sha256": "76e3bddfc237dac63293d264d68991b489585136fe377e39dbba72aa3a1181f4" }, "core/crates/tera_core/src/runtime/product_surface/today.rs": { - "bytes": 140918, - "git_blob": "c8be6532fdb641bb5d0056cad8e07bd7ed6ae278", + "bytes": 143260, + "git_blob": "6fcffcc89b755356f65cff7abcfe57b6db005ec8", "mode": "100644", - "sha256": "1c76aafbd3d7ed373579a8658d47f9208059d2578db466e11d4148da663d4e99" + "sha256": "b4fd53f5c264500bc9a0a4e09e2f56904e41145ef75931d64fc6f2d46bf33735" }, "core/crates/tera_core/src/runtime/product_surface/today_admission_tests.rs": { "bytes": 14686, @@ -740,6 +752,12 @@ "mode": "100644", "sha256": "6f5810256ca221ca66ec4056e07fc635103e0ed02da01f5a8c53161535d7eaf6" }, + "core/crates/tera_core/src/runtime/product_surface/today_calendar_tests.rs": { + "bytes": 6463, + "git_blob": "252dd511bc8b9d52b9781b7babb1b8828d86919d", + "mode": "100644", + "sha256": "ff0fc470ef3e3274d57c88f078ceab88059636fb25aeeccae3c6f2a06e092eec" + }, "core/crates/tera_core/src/runtime/product_surface/today_counted_storage_tests.rs": { "bytes": 17547, "git_blob": "de61bd9c5fefa9cabf4818d84f9498564a6e5790", @@ -759,10 +777,10 @@ "sha256": "1158622c28cca9b6843b48199f4eb6d286a01cc15d7530e11b0fa25a2e00b93c" }, "core/crates/tera_core/src/runtime/product_surface/today_reconciliation.rs": { - "bytes": 2036, - "git_blob": "a85b9bc3a43885415147fef2d9b9a57338ed7427", + "bytes": 2048, + "git_blob": "2d584a10b520f5000705ed4516c28385af4457e1", "mode": "100644", - "sha256": "2000088c53e5e34b95d88e6de1615631a3ecb2dabf36e831f504dd0791afc5de" + "sha256": "f8c1f6572e1d996960800e56c8dd77aebd0ca6ed1018f4675646e76dee6bd381" }, "core/crates/tera_core/src/runtime/product_surface/today_reconciliation_tests.rs": { "bytes": 3424, @@ -849,10 +867,10 @@ "sha256": "f96248270139fbd131bf5290bf97560b7e12140d76e4fe77a071f92d6e76e451" }, "core/crates/tera_ffi/src/dto.rs": { - "bytes": 107309, - "git_blob": "09dedc5a8342b0d95e01c2de1fd8afcf05d37e07", + "bytes": 107758, + "git_blob": "f8c746adf993d7d8fa675332bf34fd261a165f47", "mode": "100644", - "sha256": "59fa33f8931f4ee672ce5a14d7ad22beff8f9ae61e29300e70b2f1e9ab72d6c0" + "sha256": "e2168374d97811d33b1df0f8410126eefdd3fa161b26342702062c6edde0d167" }, "core/crates/tera_ffi/src/dto/today_sync.rs": { "bytes": 4837, @@ -1437,13 +1455,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 116926, - "git_blob": "c3a54831006843505ce452a194ab72352dbac12a", + "bytes": 118536, + "git_blob": "fafa6f76026449cd0cc47f458e31dd067a94f38b", "mode": "100644", - "sha256": "20f5bc3bdcfaaa8eb6816543849e75107c498a52e45399f3f84314e5ecf6c8f0" + "sha256": "201494e468ffafea52652ffecab89bfa16399e0c4b6e5bed5617853be7e49fe5" } }, "policy": "staged_inputs", - "tree": "703c94b59618f9ca1dd16c17df8fb07fb7b860bd" + "tree": "d2c902b79291f9ced96cd77f989c7812bbc9c4b2" } } diff --git a/core/crates/tera_core/Cargo.toml b/core/crates/tera_core/Cargo.toml @@ -39,6 +39,7 @@ radroots_blossom = { workspace = true, default-features = false, features = [ radroots_sdk = { workspace = true, features = ["sqlite"] } radroots_event = { workspace = true, default-features = false, features = [ "std", + "serde", ] } radroots_event_codec = { workspace = true, default-features = false, features = [ "json", diff --git a/core/crates/tera_core/src/runtime/product_surface.rs b/core/crates/tera_core/src/runtime/product_surface.rs @@ -5,6 +5,7 @@ //! composition remain in the runtime slices that consume these types. mod authoring; +mod calendar_timing; mod context; mod cursor; mod identity; @@ -23,6 +24,7 @@ pub use authoring::{ CreateAsk, CreateEvent, CreateFoodAvailability, CreatePhotoUpdate, CreateUpdate, Phase1AddCommand, Phase1ReplacementPolicy, phase1_retraction_plan, }; +pub use calendar_timing::{CalendarTiming, CalendarTimingError, DateBasedTiming, TimeBasedTiming}; pub use context::{ ContextAdmission, ContextRank, LocalNetwork, LocalNetworkAdmission, LocalNetworkError, LocalNetworkRelayPolicy, LocalityEvidence, diff --git a/core/crates/tera_core/src/runtime/product_surface/calendar_timing.rs b/core/crates/tera_core/src/runtime/product_surface/calendar_timing.rs @@ -0,0 +1,178 @@ +//! Calendar projection values. Signed wire parsing remains owned by Lib. +use radroots_event::calendar::{CalendarDate, IanaTimeZoneId}; +use radroots_event_codec::{ + admission::RadrootsAdmittedEvent, + decode::calendar::{ + admit_radroots_calendar_date_event, admit_radroots_calendar_time_event, + parse_nip52_calendar_date_event, parse_nip52_calendar_time_event, + }, +}; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)] +pub enum CalendarTimingError { + #[error("calendar end must be after its start")] + InvalidRange, + #[error("calendar source does not match its admitted contract")] + InvalidSource, +} + +/// A civil interval. No timezone or fabricated instant is part of this value. +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DateBasedTiming { + start: CalendarDate, + end_exclusive: Option<CalendarDate>, +} + +impl DateBasedTiming { + pub fn new( + start: CalendarDate, + end_exclusive: Option<CalendarDate>, + ) -> Result<Self, CalendarTimingError> { + if end_exclusive.as_ref().is_some_and(|end| end <= &start) { + return Err(CalendarTimingError::InvalidRange); + } + Ok(Self { + start, + end_exclusive, + }) + } + pub fn start(&self) -> &CalendarDate { + &self.start + } + pub fn end_exclusive(&self) -> Option<&CalendarDate> { + self.end_exclusive.as_ref() + } +} + +impl<'de> Deserialize<'de> for DateBasedTiming { + fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> { + #[derive(Deserialize)] + #[serde(deny_unknown_fields, rename_all = "camelCase")] + struct Wire { + start: CalendarDate, + end_exclusive: Option<CalendarDate>, + } + let wire = Wire::deserialize(deserializer)?; + Self::new(wire.start, wire.end_exclusive).map_err(serde::de::Error::custom) + } +} + +/// Exact signed instants and their optional source zones. The full u64 wire +/// domain is retained; presentation support is a separate fallible boundary. +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TimeBasedTiming { + start: u64, + end_exclusive: Option<u64>, + start_tzid: Option<IanaTimeZoneId>, + end_tzid: Option<IanaTimeZoneId>, +} + +impl TimeBasedTiming { + pub fn new( + start: u64, + end_exclusive: Option<u64>, + start_tzid: Option<IanaTimeZoneId>, + end_tzid: Option<IanaTimeZoneId>, + ) -> Result<Self, CalendarTimingError> { + if end_exclusive.is_some_and(|end| end <= start) { + return Err(CalendarTimingError::InvalidRange); + } + Ok(Self { + start, + end_exclusive, + start_tzid, + end_tzid, + }) + } + pub const fn start(&self) -> u64 { + self.start + } + pub const fn end_exclusive(&self) -> Option<u64> { + self.end_exclusive + } + pub fn start_tzid(&self) -> Option<&IanaTimeZoneId> { + self.start_tzid.as_ref() + } + pub fn end_tzid(&self) -> Option<&IanaTimeZoneId> { + self.end_tzid.as_ref() + } +} + +impl<'de> Deserialize<'de> for TimeBasedTiming { + fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> { + #[derive(Deserialize)] + #[serde(deny_unknown_fields, rename_all = "camelCase")] + struct Wire { + start: u64, + end_exclusive: Option<u64>, + start_tzid: Option<IanaTimeZoneId>, + end_tzid: Option<IanaTimeZoneId>, + } + let wire = Wire::deserialize(deserializer)?; + Self::new( + wire.start, + wire.end_exclusive, + wire.start_tzid, + wire.end_tzid, + ) + .map_err(serde::de::Error::custom) + } +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(tag = "kind")] +pub enum CalendarTiming { + DateBased(DateBasedTiming), + TimeBased(TimeBasedTiming), +} + +impl CalendarTiming { + pub(crate) fn from_admitted( + admitted: &RadrootsAdmittedEvent, + ) -> Result<Option<Self>, CalendarTimingError> { + if !matches!( + admitted.contract_id(), + "radroots.calendar.date_event.v1" | "radroots.calendar.time_event.v1" + ) { + return Ok(None); + } + let event = admitted.event(); + let tags = event.tags_as_vec(); + match admitted.contract_id() { + "radroots.calendar.date_event.v1" => { + let parsed = + parse_nip52_calendar_date_event(event.kind_u32(), &tags, event.content()) + .map_err(|_| CalendarTimingError::InvalidSource)?; + let value = admit_radroots_calendar_date_event(parsed) + .map_err(|_| CalendarTimingError::InvalidSource)?; + Ok(Some(Self::DateBased(DateBasedTiming::new( + value.parsed().start().clone(), + value.parsed().end().cloned(), + )?))) + } + "radroots.calendar.time_event.v1" => { + let parsed = + parse_nip52_calendar_time_event(event.kind_u32(), &tags, event.content()) + .map_err(|_| CalendarTimingError::InvalidSource)?; + let value = admit_radroots_calendar_time_event(parsed) + .map_err(|_| CalendarTimingError::InvalidSource)?; + let parsed = value.parsed(); + Ok(Some(Self::TimeBased(TimeBasedTiming::new( + parsed.start(), + parsed.end(), + parsed.start_tzid().cloned(), + parsed.end_tzid().cloned(), + )?))) + } + _ => Ok(None), + } + } +} + +#[cfg(test)] +#[path = "calendar_timing_tests.rs"] +mod tests; diff --git a/core/crates/tera_core/src/runtime/product_surface/calendar_timing_tests.rs b/core/crates/tera_core/src/runtime/product_surface/calendar_timing_tests.rs @@ -0,0 +1,140 @@ +use super::*; + +fn date(value: &str) -> CalendarDate { + CalendarDate::parse(value).unwrap() +} + +#[test] +fn civil_dates_keep_gregorian_invariants_and_exclusive_ends() { + for value in ["0001-01-01", "2000-02-29", "2028-02-29", "9999-12-31"] { + let timing = CalendarTiming::DateBased(DateBasedTiming::new(date(value), None).unwrap()); + let json = serde_json::to_string(&timing).unwrap(); + assert_eq!( + json, + format!(r#"{{"kind":"DateBased","start":"{value}","endExclusive":null}}"#) + ); + assert_eq!( + serde_json::from_str::<CalendarTiming>(&json).unwrap(), + timing + ); + } + for invalid in [ + "0000-01-01", + "1900-02-29", + "2026-02-29", + "2026-04-31", + "2026-13-01", + "2026-9-05", + "2026-09-05Z", + ] { + assert!( + serde_json::from_value::<CalendarTiming>(serde_json::json!({ + "kind": "DateBased", "start": invalid, "endExclusive": null, + })) + .is_err() + ); + } + for end in ["2026-09-05", "2026-09-04"] { + assert!(DateBasedTiming::new(date("2026-09-05"), Some(date(end))).is_err()); + assert!( + serde_json::from_value::<CalendarTiming>(serde_json::json!({ + "kind": "DateBased", "start": "2026-09-05", "endExclusive": end, + })) + .is_err() + ); + } + let single = DateBasedTiming::new(date("2026-09-05"), Some(date("2026-09-06"))).unwrap(); + assert_eq!(single.start().as_str(), "2026-09-05"); + assert_eq!(single.end_exclusive().unwrap().as_str(), "2026-09-06"); + let multiple = DateBasedTiming::new(date("2028-02-28"), Some(date("2028-03-01"))).unwrap(); + assert_eq!(multiple.end_exclusive().unwrap().as_str(), "2028-03-01"); +} + +#[test] +fn timing_serialization_cannot_mix_civil_dates_instants_or_zones() { + for value in [ + serde_json::json!({"kind":"DateBased", "start":1800000000}), + serde_json::json!({"kind":"DateBased", "start":"2026-09-05", "startTzid":"America/Vancouver"}), + serde_json::json!({"kind":"TimeBased", "start":"2026-09-05"}), + serde_json::json!({"kind":"TimeBased", "start":-1}), + serde_json::json!({"kind":"TimeBased", "start":1.5}), + serde_json::json!({"kind":"Unsupported", "start":1}), + serde_json::json!({"kind":"TimeBased", "start":1, "endExclusive":1}), + serde_json::json!({"kind":"TimeBased", "start":1, "startTzid":"not/a-zone"}), + ] { + assert!(serde_json::from_value::<CalendarTiming>(value).is_err()); + } +} + +#[test] +fn timed_values_preserve_exact_wire_integer_domain_and_source_zones() { + for start in [0, 1, i64::MAX as u64, u64::MAX] { + let timing = + CalendarTiming::TimeBased(TimeBasedTiming::new(start, None, None, None).unwrap()); + let encoded = serde_json::to_string(&timing).unwrap(); + assert_eq!( + serde_json::from_str::<CalendarTiming>(&encoded).unwrap(), + timing + ); + } + let start_zone = IanaTimeZoneId::parse("America/Vancouver").unwrap(); + let end_zone = IanaTimeZoneId::parse("Europe/Paris").unwrap(); + let value = TimeBasedTiming::new( + 1_800_000_000, + Some(1_800_003_600), + Some(start_zone), + Some(end_zone), + ) + .unwrap(); + assert_eq!(value.start(), 1_800_000_000); + assert_eq!(value.end_exclusive(), Some(1_800_003_600)); + assert_eq!(value.start_tzid().unwrap().as_str(), "America/Vancouver"); + assert_eq!(value.end_tzid().unwrap().as_str(), "Europe/Paris"); + let timing = CalendarTiming::TimeBased(value); + let encoded = serde_json::to_string(&timing).unwrap(); + assert_eq!( + serde_json::from_str::<CalendarTiming>(&encoded).unwrap(), + timing + ); + assert!(TimeBasedTiming::new(u64::MAX, Some(u64::MAX), None, None).is_err()); + assert!( + serde_json::from_str::<CalendarTiming>( + r#"{"kind":"TimeBased","start":18446744073709551616}"# + ) + .is_err() + ); +} + +#[test] +fn date_relevance_uses_explicit_civil_context_and_exclusive_end() { + use crate::runtime::product_surface::{ + CardId, ContextRank, TimeRelevance, TodayCardType, TodayRank, TodayRankInput, + }; + for (start, end, as_of_date, expected) in [ + ("2026-09-05", None, "2026-09-05", 4), + ("2026-09-05", None, "2026-09-06", 0), + ("2026-09-05", Some("2026-09-07"), "2026-09-06", 4), + ("2026-09-05", Some("2026-09-07"), "2026-09-07", 0), + ("2026-09-12", None, "2026-09-05", 3), + ("2026-09-13", None, "2026-09-05", 2), + ("2028-02-28", Some("2028-03-01"), "2028-02-29", 4), + ("2028-02-28", Some("2028-03-01"), "2028-03-01", 0), + ] { + let rank = TodayRank::derive(TodayRankInput { + card_type: TodayCardType::Event, + context_rank: ContextRank::LocalityMatch, + as_of: 1, + effective_at: 1, + time: TimeRelevance::DateBased { + event: DateBasedTiming::new(date(start), end.map(date)).unwrap(), + as_of_date: date(as_of_date), + }, + card_id: CardId::parse(&"a".repeat(64)).unwrap(), + }) + .unwrap(); + assert_eq!( + rank.time_relevance_rank, expected, + "{start}/{end:?}/{as_of_date}" + ); + } +} 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,8 +161,14 @@ pub struct ClassifiedCard { pub content: String, pub authored_at: u64, pub effective_at: u64, - pub event_start: Option<u64>, - pub event_end: Option<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")] pub location: Option<String>, #[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 @@ -5,9 +5,9 @@ use radroots_event_codec::{ use serde::{Deserialize, Serialize}; use super::{ - CardId, CardLifecycleState, CardSourceIdentity, ClassifiedCard, ContextAdmission, - LocalNetworkAdmission, MediaReference, Phase1StructuralMediaReference, SupportingProfile, - TodayCardType, + CalendarTiming, CardId, CardLifecycleState, CardSourceIdentity, ClassifiedCard, + ContextAdmission, LocalNetworkAdmission, MediaReference, Phase1StructuralMediaReference, + SupportingProfile, TodayCardType, }; const CLASSIFIED_CARD_SCHEMA_VERSION: u16 = 1; @@ -174,19 +174,21 @@ fn card( let food_status = matches!(card_type, TodayCardType::FoodAvailability) .then(|| tag_value(&tags, &["status"])) .flatten(); - let (effective_at, event_start, event_end) = match card_type { - TodayCardType::Event => { - let start = tag_time(&tags, "start").unwrap_or_else(|| event.created_at_u64()); - (start, Some(start), tag_time(&tags, "end")) + let calendar_timing = match CalendarTiming::from_admitted(admitted) { + Ok(value) => value, + Err(_) => { + return ProductEventClassification::Excluded(ProductEventExclusion::UnsupportedProfile); } - TodayCardType::FoodAvailability => ( - tag_time(&tags, "published_at").unwrap_or_else(|| event.created_at_u64()), - None, - None, - ), - TodayCardType::Update | TodayCardType::PhotoUpdate | TodayCardType::Ask => { - (event.created_at_u64(), None, None) + }; + let effective_at = match &calendar_timing { + Some(CalendarTiming::TimeBased(timing)) => timing.start(), + // A civil date has no instant. Preserve its source publication instant + // for the secondary order; date relevance is compared as civil dates. + Some(CalendarTiming::DateBased(_)) => event.created_at_u64(), + None if card_type == TodayCardType::FoodAvailability => { + tag_time(&tags, "published_at").unwrap_or_else(|| event.created_at_u64()) } + None => event.created_at_u64(), }; ProductEventClassification::Card(Box::new(ClassifiedCard { schema_version: CLASSIFIED_CARD_SCHEMA_VERSION, @@ -200,8 +202,9 @@ fn card( content: event.content().to_owned(), authored_at: event.created_at_u64(), effective_at, - event_start, - event_end, + legacy_event_start: None, + legacy_event_end: None, + calendar_timing, location, price_amount, price_currency, @@ -234,16 +237,7 @@ fn tag_values(tags: &[Vec<String>], name: &str) -> Option<Vec<String>> { } fn tag_time(tags: &[Vec<String>], name: &str) -> Option<u64> { - let value = tag_value(tags, &[name])?; - value.parse().ok().or_else(|| { - chrono::NaiveDate::parse_from_str(&value, "%Y-%m-%d") - .ok()? - .and_hms_opt(0, 0, 0)? - .and_utc() - .timestamp() - .try_into() - .ok() - }) + tag_value(tags, &[name])?.parse().ok() } fn post_media( @@ -531,7 +525,7 @@ mod tests { &[vec!["start".to_owned(), "2026-08-13".to_owned()]], "start" ) - .is_some() + .is_none() ); assert_eq!(blossom_digest("not-a-url"), None); diff --git a/core/crates/tera_core/src/runtime/product_surface/ranking.rs b/core/crates/tera_core/src/runtime/product_surface/ranking.rs @@ -4,7 +4,8 @@ use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use thiserror::Error; -use super::{CardId, ContextRank, TodayCardType}; +use super::{CardId, ContextRank, DateBasedTiming, TodayCardType}; +use radroots_event::calendar::CalendarDate; pub const TODAY_RANK_SCHEMA_VERSION: u16 = 1; pub const TODAY_RANK_ALGORITHM_VERSION: u16 = 1; @@ -12,14 +13,23 @@ const RANK_DIGEST_DOMAIN: &[u8] = b"radroots.today-rank.v1\0"; const UPCOMING_EVENT_WINDOW_SECONDS: u64 = 7 * 24 * 60 * 60; /// Exact time inputs used by the deliberately small Phase 1 ranking function. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] pub enum TimeRelevance { Published, - Event { start: u64, end: Option<u64> }, - FoodAvailability { active: bool }, + TimeBased { + start: u64, + end: Option<u64>, + }, + DateBased { + event: DateBasedTiming, + as_of_date: CalendarDate, + }, + FoodAvailability { + active: bool, + }, } -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct TodayRankInput { pub card_type: TodayCardType, pub context_rank: ContextRank, @@ -35,6 +45,8 @@ pub enum RankError { MismatchedTimeProfile, #[error("event end must be later than its start")] InvalidEventRange, + #[error("calendar relevance requires a supported civil date")] + InvalidDateContext, } /// Lexicographic Today order key. @@ -54,7 +66,7 @@ pub struct TodayRank { impl TodayRank { pub fn derive(input: TodayRankInput) -> Result<Self, RankError> { - let time_relevance_rank = time_relevance_rank(input)?; + let time_relevance_rank = time_relevance_rank(&input)?; Ok(Self { schema_version: TODAY_RANK_SCHEMA_VERSION, algorithm_version: TODAY_RANK_ALGORITHM_VERSION, @@ -101,23 +113,41 @@ impl PartialOrd for TodayRank { } } -fn time_relevance_rank(input: TodayRankInput) -> Result<u8, RankError> { - match (input.card_type, input.time) { +fn time_relevance_rank(input: &TodayRankInput) -> Result<u8, RankError> { + match (input.card_type, &input.time) { ( TodayCardType::Update | TodayCardType::PhotoUpdate | TodayCardType::Ask, TimeRelevance::Published, ) => Ok(1), (TodayCardType::FoodAvailability, TimeRelevance::FoodAvailability { active }) => { - Ok(if active { 3 } else { 0 }) + Ok(if *active { 3 } else { 0 }) } - (TodayCardType::Event, TimeRelevance::Event { start, end }) => { - if end.is_some_and(|end| end <= start) { + (TodayCardType::Event, TimeRelevance::DateBased { event, as_of_date }) => { + if event.end_exclusive().is_some_and(|end| as_of_date >= end) + || (event.end_exclusive().is_none() && as_of_date > event.start()) + { + return Ok(0); + } + if as_of_date >= event.start() { + return Ok(4); + } + let parse = |date: &CalendarDate| { + chrono::NaiveDate::parse_from_str(date.as_str(), "%Y-%m-%d") + .map_err(|_| RankError::InvalidDateContext) + }; + let distance = parse(event.start())? + .signed_duration_since(parse(as_of_date)?) + .num_days(); + Ok(if distance <= 7 { 3 } else { 2 }) + } + (TodayCardType::Event, TimeRelevance::TimeBased { start, end }) => { + if end.is_some_and(|end| end <= *start) { return Err(RankError::InvalidEventRange); } if end.is_some_and(|end| input.as_of >= end) { return Ok(0); } - if input.as_of >= start { + if input.as_of >= *start { return Ok(4); } if start.saturating_sub(input.as_of) <= UPCOMING_EVENT_WINDOW_SECONDS { @@ -185,7 +215,7 @@ mod tests { assert_eq!( TodayRank::derive(input( TodayCardType::Event, - TimeRelevance::Event { start, end } + TimeRelevance::TimeBased { start, end } )) .expect("event") .time_relevance_rank, @@ -264,7 +294,7 @@ mod tests { assert_eq!( TodayRank::derive(input( TodayCardType::Event, - TimeRelevance::Event { + TimeRelevance::TimeBased { start: 10, end: Some(10), } diff --git a/core/crates/tera_core/src/runtime/product_surface/today.rs b/core/crates/tera_core/src/runtime/product_surface/today.rs @@ -33,10 +33,10 @@ use radroots_sync::ingest::{AdmissionDecision, AdmissionPolicy, ContractFailureD #[cfg(feature = "mobile-social")] use super::Phase1LocalMediaArtifact; use super::{ - CardId, CardLifecycleState, ClassifiedCard, CursorError, CursorScope, LocalAuthorOverlay, - LocalNetwork, LocalityEvidence, MeSnapshot, MediaReference, Phase1InboundMediaError, - Phase1InboundMediaFailure, Phase1InboundMediaPending, Phase1InboundMediaState, - Phase1MediaArtifactId, Phase1MediaCacheIndex, Phase1MediaCacheStatus, + CalendarTiming, CardId, CardLifecycleState, ClassifiedCard, CursorError, CursorScope, + LocalAuthorOverlay, LocalNetwork, LocalityEvidence, MeSnapshot, MediaReference, + Phase1InboundMediaError, Phase1InboundMediaFailure, Phase1InboundMediaPending, + Phase1InboundMediaState, Phase1MediaArtifactId, Phase1MediaCacheIndex, Phase1MediaCacheStatus, Phase1MediaConfigurationFingerprint, Phase1StructuralMediaReference, ProductEventClassification, ProfileSummary, SearchResult, SearchResultType, SupportingProfile, ThreadEntry, ThreadReference, TimeRelevance, TodayCard, TodayCardType, TodayCursor, @@ -78,6 +78,10 @@ mod reconciliation_tests; mod performance_tests; #[cfg(test)] +#[path = "today_calendar_tests.rs"] +mod calendar_tests; + +#[cfg(test)] #[path = "today_scope_tests.rs"] mod scope_tests; @@ -312,6 +316,7 @@ impl TeraRuntime { state.source_events == event_status.raw_events() && state.query_scope == Some(query_scope) && state.visibility_digest == Some(visibility_digest) + && calendar_projection_ready(state) }) { let state = prior.expect("checked present"); @@ -437,6 +442,7 @@ impl TeraRuntime { if current.store_generation != scope.store_generation || current.query_scope != Some(scope.query_scope) || current.visibility_digest.is_none() + || !calendar_projection_ready(&current) || current.content_generation != scope.projection_generation { return Err(CursorError::Stale.into()); @@ -451,7 +457,8 @@ impl TeraRuntime { let state = match load_state(storage, context, algorithm_generation).await? { Some(state) if state.query_scope == Some(query_scope) - && state.visibility_digest.is_some() => + && state.visibility_digest.is_some() + && calendar_projection_ready(&state) => { state } @@ -486,6 +493,25 @@ impl TeraRuntime { page_from_snapshot(snapshot, scope, after, request.limit) } + async fn calendar_state_for_read( + &self, + storage: &dyn radroots_storage::Storage, + context: &LocalNetwork, + as_of: u64, + ) -> Result<Option<TodayProjectionState>, TodayError> { + let state = load_state(storage, context, projection_generation()?).await?; + if state + .as_ref() + .is_some_and(|value| !calendar_projection_ready(value)) + { + self.phase1_refresh_today(context, as_of, TodayProjectionUpdate::Rebuild) + .await?; + load_state(storage, context, projection_generation()?).await + } else { + Ok(state) + } + } + /// Searches the current local projection using Today visibility and context rules. pub async fn phase1_search( &self, @@ -506,7 +532,8 @@ impl TeraRuntime { .client .storage() .map_err(|_| TodayError::RuntimeUnavailable)?; - let state = load_state(storage, context, projection_generation()?) + let state = self + .calendar_state_for_read(storage, context, as_of) .await? .ok_or(TodayError::ProjectionMissing)?; let cards = ranked_cards(&state, context, as_of)?; @@ -581,7 +608,8 @@ impl TeraRuntime { .client .storage() .map_err(|_| TodayError::RuntimeUnavailable)?; - let state = load_state(storage, context, projection_generation()?) + let state = self + .calendar_state_for_read(storage, context, as_of) .await? .ok_or(TodayError::ProjectionMissing)?; let cards = ranked_cards(&state, context, as_of)? @@ -1555,6 +1583,12 @@ fn locality_evidence(selected: Option<&str>, locality: &[LocalityTag]) -> Locali } } +fn calendar_projection_ready(state: &TodayProjectionState) -> bool { + state.cards.iter().all(|entry| { + entry.card.card_type != TodayCardType::Event || entry.card.calendar_timing.is_some() + }) +} + fn ranked_cards( state: &TodayProjectionState, context: &LocalNetwork, @@ -1589,9 +1623,32 @@ fn selected_ranked_cards( TodayCardType::FoodAvailability => TimeRelevance::FoodAvailability { active: card.lifecycle == CardLifecycleState::Active, }, - TodayCardType::Event => TimeRelevance::Event { - start: card.event_start.unwrap_or(card.effective_at), - end: card.event_end, + TodayCardType::Event => match card + .calendar_timing + .as_ref() + .ok_or(TodayError::CorruptProjection)? + { + CalendarTiming::TimeBased(timing) => TimeRelevance::TimeBased { + start: timing.start(), + end: timing.end_exclusive(), + }, + CalendarTiming::DateBased(event) => { + // The existing instant-only request has an explicit UTC + // relevance context. C052 replaces this bridge with the + // frozen viewer IANA context; no event date becomes an epoch. + let instant = i64::try_from(as_of) + .ok() + .and_then(|value| chrono::DateTime::from_timestamp(value, 0)) + .ok_or(TodayError::InvalidRequest)?; + let as_of_date = radroots_event::calendar::CalendarDate::parse( + &instant.date_naive().to_string(), + ) + .map_err(|_| TodayError::InvalidRequest)?; + TimeRelevance::DateBased { + event: event.clone(), + as_of_date, + } + } }, }; let rank = TodayRank::derive(TodayRankInput { @@ -2233,7 +2290,7 @@ mod tests { ) } - fn signed_owned( + pub(super) fn signed_owned( kind: u32, tags: Vec<Vec<String>>, content: &str, @@ -3865,8 +3922,9 @@ mod tests { let mut event_state = state.clone(); event_state.cards[0].card.card_type = TodayCardType::Event; - event_state.cards[0].card.event_start = Some(100); - event_state.cards[0].card.event_end = Some(200); + event_state.cards[0].card.calendar_timing = Some(CalendarTiming::TimeBased( + super::super::TimeBasedTiming::new(100, Some(200), None, None).unwrap(), + )); event_state.cards[0].card.effective_at = 100; assert_eq!( ranked_cards(&event_state, &context, 150).expect("live event")[0] 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 @@ -0,0 +1,181 @@ +use super::tests::{context, ingest, keys, signed_owned}; +use super::*; +use radroots_event::calendar::{ + AuthoredCalendarDateEvent, AuthoredCalendarTimeEvent, CalendarDate, +}; +use radroots_event_codec::encode::calendar::{ + calendar_date_event_build_tags, calendar_time_event_build_tags, +}; + +const AUTHORED: u64 = 1_800_000_000; +const NOW: u64 = 2_000_000_000; + +fn date_event() -> radroots_event::SignedEvent { + let event = AuthoredCalendarDateEvent::new( + "harvest-day", + "Harvest day", + CalendarDate::parse("2026-09-05").unwrap(), + ) + .unwrap() + .with_end(CalendarDate::parse("2026-09-07").unwrap()) + .unwrap(); + signed_owned( + 31_922, + calendar_date_event_build_tags(&event).unwrap(), + "", + AUTHORED, + ) +} + +#[tokio::test] +async fn actual_admitted_calendar_projection_keeps_dates_instants_and_zones_distinct() { + let runtime = TeraRuntime::test_memory().unwrap(); + let context = context(None, 1); + let civil = date_event(); + ingest(&runtime, &context, civil.clone(), NOW).await; + let timed = AuthoredCalendarTimeEvent::new("shift", "Shift", AUTHORED + 100) + .unwrap() + .with_end(AUTHORED + 200) + .unwrap() + .with_start_tzid("America/Vancouver") + .unwrap() + .with_end_tzid("Europe/Paris") + .unwrap(); + let timed_event = signed_owned( + 31_923, + calendar_time_event_build_tags(&timed).unwrap(), + "", + AUTHORED + 1, + ); + ingest(&runtime, &context, timed_event.clone(), NOW).await; + let page = runtime + .phase1_today_page(&context, TodayPageRequest::first(20, NOW)) + .await + .unwrap(); + assert_eq!(page.items.len(), 2); + let civil_card = &page + .items + .iter() + .find(|item| item.card.source_event_id == civil.id().to_hex()) + .unwrap() + .card; + let Some(CalendarTiming::DateBased(timing)) = &civil_card.calendar_timing else { + panic!("civil timing") + }; + 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() + .find(|item| item.card.source_event_id == timed_event.id().to_hex()) + .unwrap() + .card; + let Some(CalendarTiming::TimeBased(timing)) = &timed_card.calendar_timing else { + panic!("timed timing") + }; + assert_eq!(timing.start(), AUTHORED + 100); + assert_eq!(timing.end_exclusive(), Some(AUTHORED + 200)); + assert_eq!(timing.start_tzid().unwrap().as_str(), "America/Vancouver"); + assert_eq!(timing.end_tzid().unwrap().as_str(), "Europe/Paris"); + assert_eq!(timed_card.effective_at, AUTHORED + 100); + let raw = EventStore::query_raw( + runtime.client.storage().unwrap(), + EventQuery::all(EventQueryBounds::first(20).unwrap()), + ) + .await + .unwrap(); + 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_reconciliation.rs b/core/crates/tera_core/src/runtime/product_surface/today_reconciliation.rs @@ -35,7 +35,8 @@ impl TeraRuntime { .client .storage() .map_err(|_| TodayError::RuntimeUnavailable)?; - let state = load_state(storage, context, projection_generation()?) + let state = self + .calendar_state_for_read(storage, context, as_of) .await? .ok_or(TodayError::ProjectionMissing)?; if state.query_scope != Some(query_scope) diff --git a/core/crates/tera_ffi/src/dto.rs b/core/crates/tera_ffi/src/dto.rs @@ -18,14 +18,15 @@ use tera_core::runtime::{ app_info::AppInfoPlatform, info::{AppInfo, RuntimeBuildInfo, RuntimeInfo}, product_surface::{ - AddCommandType, CardLifecycleState, CreateAsk, CreateEvent, CreateFoodAvailability, - CreatePhotoUpdate, CreateUpdate, LocalNetwork, LocalNetworkRelayPolicy, MeSnapshot, - MediaReference, Phase1AddCommand, Phase1CancellationPolicy, Phase1DraftEventTiming, - Phase1DraftFormSnapshot, Phase1DraftKind, Phase1DraftMediaSnapshot, Phase1DraftStatus, - Phase1InboundMediaState, Phase1MediaPrerequisite, Phase1MediaStage, Phase1OutboxState, - Phase1QueuePolicy, Phase1RelaySatisfaction, Phase1UploadIntent, ProfileSummary, - SearchResult, SearchResultType, SupportingProfile, ThreadEntry, TodayCard, TodayCardType, - TodayPage, TodayProjectionUpdate, TodayRefreshReceipt, + AddCommandType, CalendarTiming, CardLifecycleState, CreateAsk, CreateEvent, + CreateFoodAvailability, CreatePhotoUpdate, CreateUpdate, LocalNetwork, + LocalNetworkRelayPolicy, MeSnapshot, MediaReference, Phase1AddCommand, + Phase1CancellationPolicy, Phase1DraftEventTiming, Phase1DraftFormSnapshot, Phase1DraftKind, + Phase1DraftMediaSnapshot, Phase1DraftStatus, Phase1InboundMediaState, + Phase1MediaPrerequisite, Phase1MediaStage, Phase1OutboxState, Phase1QueuePolicy, + Phase1RelaySatisfaction, Phase1UploadIntent, ProfileSummary, SearchResult, + SearchResultType, SupportingProfile, ThreadEntry, TodayCard, TodayCardType, TodayPage, + TodayProjectionUpdate, TodayRefreshReceipt, }, sdk::{ SdkBlossomConfigurationRecord, SdkBlossomEvidenceRecord, SdkCapabilityRecord, @@ -701,6 +702,14 @@ pub struct FfiTodayCardRecord { impl From<TodayCard> for FfiTodayCardRecord { fn from(value: TodayCard) -> Self { let card = value.card; + // Temporary instant-only generated boundary, replaced in C049. Civil + // dates are never represented as midnight-UTC timestamps here. + let (event_start, event_end) = match &card.calendar_timing { + Some(CalendarTiming::TimeBased(timing)) => { + (Some(timing.start()), timing.end_exclusive()) + } + Some(CalendarTiming::DateBased(_)) | None => (None, None), + }; Self { schema_version: MOBILE_FFI_SCHEMA_VERSION, card_id: card.card_id.to_hex(), @@ -713,8 +722,8 @@ impl From<TodayCard> for FfiTodayCardRecord { content: card.content, authored_at_unix_s: card.authored_at, effective_at_unix_s: card.effective_at, - event_start_unix_s: card.event_start, - event_end_unix_s: card.event_end, + event_start_unix_s: event_start, + event_end_unix_s: event_end, location: card.location, price_amount: card.price_amount, price_currency: card.price_currency, 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": "f174e6196e6b8ffdadaabcf52c6d697ee7ba9e3acc55ff265cd928c1131d4010", + "ffi_provenance_sha256": "fe8ac77f19b400cb4db0a007ac3ffc96f12db882f8eb6a9723b3b5608a43b065", "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": "703c94b59618f9ca1dd16c17df8fb07fb7b860bd", + "tera_ffi_source_tree": "d2c902b79291f9ced96cd77f989c7812bbc9c4b2", "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 @@ -129,6 +129,10 @@ "category": "shared_foundation_api", "occurrences": [ { + "path": "core/crates/tera_core/src/runtime/product_surface/calendar_timing.rs", + "count": 2 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/projection.rs", "count": 14 }, @@ -1731,6 +1735,26 @@ ] }, { + "identifier": "admit_radroots_calendar_date_event", + "category": "shared_foundation_api", + "occurrences": [ + { + "path": "core/crates/tera_core/src/runtime/product_surface/calendar_timing.rs", + "count": 2 + } + ] + }, + { + "identifier": "admit_radroots_calendar_time_event", + "category": "shared_foundation_api", + "occurrences": [ + { + "path": "core/crates/tera_core/src/runtime/product_surface/calendar_timing.rs", + "count": 2 + } + ] + }, + { "identifier": "radroots", "category": "public_namespace", "occurrences": [ @@ -2497,6 +2521,10 @@ "category": "shared_foundation_api", "occurrences": [ { + "path": "core/crates/tera_core/src/runtime/product_surface/calendar_timing.rs", + "count": 2 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/projection.rs", "count": 1 } @@ -2507,6 +2535,10 @@ "category": "shared_foundation_api", "occurrences": [ { + "path": "core/crates/tera_core/src/runtime/product_surface/calendar_timing.rs", + "count": 2 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/projection.rs", "count": 1 } @@ -4001,6 +4033,10 @@ "count": 2 }, { + "path": "core/crates/tera_core/src/runtime/product_surface/calendar_timing.rs", + "count": 1 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/identity.rs", "count": 1 }, @@ -4013,12 +4049,16 @@ "count": 2 }, { + "path": "core/crates/tera_core/src/runtime/product_surface/ranking.rs", + "count": 1 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/settings.rs", "count": 1 }, { "path": "core/crates/tera_core/src/runtime/product_surface/today.rs", - "count": 6 + "count": 7 }, { "path": "core/crates/tera_core/src/runtime/product_surface/today_admission_tests.rs", @@ -4029,6 +4069,10 @@ "count": 1 }, { + "path": "core/crates/tera_core/src/runtime/product_surface/today_calendar_tests.rs", + "count": 2 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/today_visibility_tests.rs", "count": 1 }, @@ -4067,6 +4111,10 @@ "count": 1 }, { + "path": "core/crates/tera_core/src/runtime/product_surface/calendar_timing.rs", + "count": 1 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/outbox.rs", "count": 2 }, @@ -4083,6 +4131,10 @@ "count": 4 }, { + "path": "core/crates/tera_core/src/runtime/product_surface/today_calendar_tests.rs", + "count": 1 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/today_visibility_tests.rs", "count": 2 } @@ -4458,7 +4510,7 @@ }, { "path": "core/crates/tera_core/src/runtime/product_surface/today.rs", - "count": 11 + "count": 12 }, { "path": "core/crates/tera_core/src/runtime/product_surface/today_admission_tests.rs", diff --git a/test-fixtures/swiftlint-maintainability-baseline.v1.json b/test-fixtures/swiftlint-maintainability-baseline.v1.json @@ -1 +1 @@ -[{"violation":{"ruleName":"Function Body Length","severity":"error","location":{"file":"Tera\/App\/TeraAppModel.swift","line":21,"character":3},"reason":"Initializer body should span 50 lines or less excluding comments and whitespace: currently spans 52 lines","ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines"},"text":" init("},{"violation":{"ruleName":"Type Body Length","severity":"error","location":{"file":"Tera\/Runtime\/TeraAddMediaCoordinator.swift","line":51,"character":1},"reason":"Actor body should span 250 lines or less excluding comments and whitespace: currently spans 330 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"actor TeraAddMediaCoordinator: TeraAddMediaHandling {"},{"violation":{"ruleName":"File Length","severity":"error","location":{"file":"Tera\/Runtime\/TeraAddMediaCoordinator.swift","line":458,"character":1},"reason":"File should contain 400 lines or less: currently contains 458","ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines."},"text":"#endif"},{"violation":{"ruleName":"Type Body Length","severity":"error","location":{"file":"Tera\/Runtime\/TeraGeneratedRuntimeBackend.swift","line":111,"character":15},"reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 491 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"private final class TeraGeneratedRuntimeBackend: TeraRuntimeBackend, @unchecked Sendable {"},{"violation":{"ruleName":"File Length","severity":"error","location":{"file":"Tera\/Runtime\/TeraGeneratedRuntimeBackend.swift","line":1576,"character":1},"reason":"File should contain 1000 lines or less: currently contains 1576","ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleName":"Type Body Length","severity":"error","location":{"file":"Tera\/Runtime\/TeraRuntimeClient.swift","line":3,"character":1},"reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 753 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"actor TeraRuntimeClient {"},{"violation":{"ruleName":"Function Body Length","severity":"error","location":{"file":"Tera\/Runtime\/TeraRuntimeClient.swift","line":379,"character":3},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 70 lines","ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines"},"text":" func changes(bufferCapacity: Int = 16) async throws -> AsyncStream<TeraRuntimeChange> {"},{"violation":{"ruleName":"File Length","severity":"error","location":{"file":"Tera\/Runtime\/TeraRuntimeClient.swift","line":835,"character":1},"reason":"File should contain 400 lines or less: currently contains 835","ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleName":"File Length","severity":"error","location":{"file":"Tera\/Runtime\/TeraRuntimeModels.swift","line":1175,"character":1},"reason":"File should contain 1000 lines or less: currently contains 1175","ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleName":"Type Body Length","severity":"error","location":{"file":"Tera\/State\/TeraAddStore.swift","line":11,"character":7},"reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 731 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"final class TeraAddStore: ObservableObject {"},{"violation":{"ruleName":"Function Body Length","severity":"error","location":{"file":"Tera\/State\/TeraAddStore.swift","line":459,"character":3},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 51 lines","ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines"},"text":" func retract(_ card: TeraTodayCard) async {"},{"violation":{"ruleName":"File Length","severity":"error","location":{"file":"Tera\/State\/TeraAddStore.swift","line":796,"character":1},"reason":"File should contain 400 lines or less: currently contains 796","ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleName":"Type Body Length","severity":"error","location":{"file":"Tera\/State\/TeraConfigurationStore.swift","line":78,"character":1},"reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 468 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"actor TeraConfigurationStore {"},{"violation":{"ruleName":"Function Body Length","severity":"error","location":{"file":"Tera\/State\/TeraConfigurationStore.swift","line":148,"character":5},"reason":"Function body should span 100 lines or less excluding comments and whitespace: currently spans 109 lines","ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines"},"text":" func load() throws -> TeraAppConfiguration {"},{"violation":{"ruleName":"File Length","severity":"error","location":{"file":"Tera\/State\/TeraConfigurationStore.swift","line":759,"character":1},"reason":"File should contain 400 lines or less: currently contains 759","ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleName":"Type Body Length","severity":"error","location":{"file":"Tera\/State\/TeraSessionStore.swift","line":39,"character":1},"reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 390 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"actor TeraSessionStore {"},{"violation":{"ruleName":"Cyclomatic Complexity","severity":"error","location":{"file":"Tera\/State\/TeraSessionStore.swift","line":125,"character":11},"reason":"Function should have complexity 10 or less; currently complexity is 13","ruleIdentifier":"cyclomatic_complexity","ruleDescription":"Complexity of function bodies should be limited."},"text":" private func start(acceptingReconfiguration: Bool) async -> TeraSessionPhase {"},{"violation":{"ruleName":"File Length","severity":"error","location":{"file":"Tera\/State\/TeraSessionStore.swift","line":452,"character":1},"reason":"File should contain 400 lines or less: currently contains 452","ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleName":"Type Body Length","severity":"error","location":{"file":"Tera\/Views\/TeraAddView.swift","line":4,"character":1},"reason":"Struct body should span 350 lines or less excluding comments and whitespace: currently spans 454 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"struct TeraAddView: View {"},{"violation":{"ruleName":"File Length","severity":"error","location":{"file":"Tera\/Views\/TeraAddView.swift","line":560,"character":1},"reason":"File should contain 400 lines or less: currently contains 560","ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleName":"Type Body Length","severity":"error","location":{"file":"Tera\/Views\/TeraSupportingViews.swift","line":365,"character":1},"reason":"Struct body should span 250 lines or less excluding comments and whitespace: currently spans 259 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"struct TeraSettingsView: View {"},{"violation":{"ruleName":"File Length","severity":"error","location":{"file":"Tera\/Views\/TeraSupportingViews.swift","line":629,"character":1},"reason":"File should contain 400 lines or less: currently contains 629","ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleName":"Type Body Length","severity":"error","location":{"file":"TeraTests\/TeraAddStoreTests.swift","line":6,"character":7},"reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 665 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"final class TeraAddStoreTests: XCTestCase {"},{"violation":{"ruleName":"Type Body Length","severity":"error","location":{"file":"TeraTests\/TeraAddStoreTests.swift","line":844,"character":9},"reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 492 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"private actor AddBackend: TeraRuntimeBackend {"},{"violation":{"ruleName":"Function Parameter Count","severity":"error","location":{"file":"TeraTests\/TeraAddStoreTests.swift","line":1280,"character":11},"reason":"Function should have 5 parameters or less: it currently has 8","ruleIdentifier":"function_parameter_count","ruleDescription":"Number of function parameters should be low."},"text":" private func makeStatus("},{"violation":{"ruleName":"File Length","severity":"error","location":{"file":"TeraTests\/TeraAddStoreTests.swift","line":1431,"character":1},"reason":"File should contain 1000 lines or less: currently contains 1431","ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleName":"Type Body Length","severity":"error","location":{"file":"TeraTests\/TeraRemoteQualificationTests.swift","line":7,"character":7},"reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 307 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"final class TeraRemoteQualificationTests: XCTestCase {"},{"violation":{"ruleName":"Function Body Length","severity":"error","location":{"file":"TeraTests\/TeraRemoteQualificationTests.swift","line":45,"character":3},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 94 lines","ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines"},"text":" func testQualificationRejectsAmbiguousOrUnsafeHarnessValues() {"},{"violation":{"ruleName":"Function Body Length","severity":"error","location":{"file":"TeraTests\/TeraRemoteQualificationTests.swift","line":212,"character":3},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 53 lines","ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines"},"text":" func testAuthorizationEvidenceIsRedactedEphemeralAndRelaunchCleaned() throws {"},{"violation":{"ruleName":"Type Body Length","severity":"error","location":{"file":"TeraTests\/TeraRuntimeClientTests.swift","line":4,"character":7},"reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 298 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"final class TeraRuntimeClientTests: XCTestCase {"},{"violation":{"ruleName":"Type Body Length","severity":"error","location":{"file":"TeraTests\/TeraStateMigrationTests.swift","line":6,"character":7},"reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 580 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"final class TeraStateMigrationTests: XCTestCase {"},{"violation":{"ruleName":"Function Body Length","severity":"error","location":{"file":"TeraTests\/TeraStateMigrationTests.swift","line":7,"character":5},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 61 lines","ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines"},"text":" func testRelayValidationMatchesRuntimeProfiles() throws {"},{"violation":{"ruleName":"File Length","severity":"error","location":{"file":"TeraTests\/TeraStateMigrationTests.swift","line":622,"character":1},"reason":"File should contain 400 lines or less: currently contains 622","ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleName":"Type Body Length","severity":"error","location":{"file":"TeraTests\/TeraSupportingStoreTests.swift","line":207,"character":9},"reason":"Actor body should span 250 lines or less excluding comments and whitespace: currently spans 253 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"private actor SupportingBackend: TeraRuntimeBackend {"},{"violation":{"ruleName":"File Length","severity":"error","location":{"file":"TeraTests\/TeraSupportingStoreTests.swift","line":487,"character":1},"reason":"File should contain 400 lines or less: currently contains 487","ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleName":"Type Body Length","severity":"error","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":4,"character":7},"reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 1493 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"final class TeraRemoteQualificationUITests: XCTestCase {"},{"violation":{"ruleName":"Function Body Length","severity":"error","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":62,"character":3},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 73 lines","ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines"},"text":" func testLocalSocialFiveFlowScenario() throws {"},{"violation":{"ruleName":"Function Body Length","severity":"error","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":174,"character":3},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 66 lines","ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines"},"text":" func testLocalSocialDeterministicPersonas() throws {"},{"violation":{"ruleName":"Function Body Length","severity":"error","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":247,"character":3},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 54 lines","ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines"},"text":" func testRemoteBlossomUploadAndRecovery() throws {"},{"violation":{"ruleName":"Cyclomatic Complexity","severity":"error","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":573,"character":11},"reason":"Function should have complexity 10 or less; currently complexity is 18","ruleIdentifier":"cyclomatic_complexity","ruleDescription":"Complexity of function bodies should be limited."},"text":" private func expectedAddFieldLabel(_ identifier: String, type: String) -> String {"},{"violation":{"ruleName":"Cyclomatic Complexity","severity":"error","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":767,"character":11},"reason":"Function should have complexity 10 or less; currently complexity is 11","ruleIdentifier":"cyclomatic_complexity","ruleDescription":"Complexity of function bodies should be limited."},"text":" private func preparePhotoUpdate(_ app: XCUIApplication, marker: String) -> XCUIElement? {"},{"violation":{"ruleName":"Function Body Length","severity":"error","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":767,"character":11},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 66 lines","ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines"},"text":" private func preparePhotoUpdate(_ app: XCUIApplication, marker: String) -> XCUIElement? {"},{"violation":{"ruleName":"Function Body Length","severity":"error","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":1736,"character":10},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 67 lines","ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines"},"text":" static func environment("},{"violation":{"ruleName":"File Length","severity":"error","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":2212,"character":1},"reason":"File should contain 1000 lines or less: currently contains 2212","ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines."},"text":"}"}] -\ No newline at end of file +[{"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