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:
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(¤t)
|| 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