field_ios

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

commit ec2a4e753c96750b9eb9f9dd60403e4748e80b9b
parent c6abf2d2e36ad9877bf9619b598aadded0232705
Author: triesap <tyson@radroots.org>
Date:   Sat, 12 Sep 2026 14:46:56 +0000

publication: Seal captured composer intent

- Validate historical composer revisions through shared authoring constructors.
- Freeze author, media prerequisites, and destination policy before effects.
- Verify replay, strict failures, account isolation, and media policy drift.
- Refresh exact native provenance and preserve the no-default test lane.

Diffstat:
MTeraFFI/provenance.json | 54+++++++++++++++++++++++++++---------------------------
MTeraFFI/source.lock | 4++--
MTeraFFI/source/aarch64-apple-darwin.json | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
MTeraFFI/source/aarch64-apple-ios-sim.json | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
MTeraFFI/source/aarch64-apple-ios.json | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Mcore/crates/tera_core/src/runtime/product_surface.rs | 6+++---
Mcore/crates/tera_core/src/runtime/product_surface/outbox.rs | 5++++-
Mcore/crates/tera_core/src/runtime/product_surface/submission.rs | 2++
Acore/crates/tera_core/src/runtime/product_surface/submission/capture.rs | 155+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/crates/tera_core/src/runtime/product_surface/submission/capture/form.rs | 133+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/crates/tera_core/src/runtime/product_surface/submission/capture/form/calendar.rs | 85+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/crates/tera_core/src/runtime/product_surface/submission/capture/media.rs | 90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/crates/tera_core/src/runtime/product_surface/submission/capture/runtime_tests.rs | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/crates/tera_core/src/runtime/product_surface/submission/capture/tests.rs | 315+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcore/crates/tera_core/src/runtime/product_surface/today.rs | 4++++
Mcore/crates/tera_core/src/runtime/product_surface/today_admission_tests.rs | 43++-----------------------------------------
Mcore/crates/tera_core/src/runtime/product_surface/today_calendar_migration_tests.rs | 2+-
Acore/crates/tera_core/src/runtime/product_surface/today_permissive_evidence_tests.rs | 38++++++++++++++++++++++++++++++++++++++
Mrelease/provenance.json | 4++--
Mtest-fixtures/legacy-identifiers.v1.json | 42+++++++++++++++++++++++++++++++++++++++++-
20 files changed, 1189 insertions(+), 144 deletions(-)

diff --git a/TeraFFI/provenance.json b/TeraFFI/provenance.json @@ -22,9 +22,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 74726648, + "bytes": 74831168, "path": "TeraFFI.xcframework/ios-arm64-simulator/libtera_ffi.a", - "sha256": "72438f74ff908c2ba7600a688a1f181deaed5a39904e658dd775719cea6b6fff" + "sha256": "14eff7d216e69a6489fcbb47eda412ff8e06cecfce000e4380df3034bed940f0" }, { "bytes": 75137, @@ -37,9 +37,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 74787784, + "bytes": 74891696, "path": "TeraFFI.xcframework/ios-arm64/libtera_ffi.a", - "sha256": "9238c523a73d5df2c41a2bcd76606ff42fe3f458b36188e9f81701895055c0ad" + "sha256": "aa03ceb4b82422068786b0bb300b803f42308ee901f180dfff67eaf2e935036b" }, { "bytes": 45118, @@ -77,34 +77,34 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 20973168, + "bytes": 20960128, "path": "native/aarch64-apple-darwin/libtera_ffi.dylib", - "sha256": "84acecefc965da8bf22d0fad42f22f66b98d5c4ba873ec95440f774c79f3d56e" + "sha256": "14ae8e95fb9e54c1a29e6db68530b17ad771a70901efe6376c18f96e0b6e00fc" }, { - "bytes": 74726648, + "bytes": 74831168, "path": "native/aarch64-apple-ios-sim/libtera_ffi.a", - "sha256": "72438f74ff908c2ba7600a688a1f181deaed5a39904e658dd775719cea6b6fff" + "sha256": "14eff7d216e69a6489fcbb47eda412ff8e06cecfce000e4380df3034bed940f0" }, { - "bytes": 74787784, + "bytes": 74891696, "path": "native/aarch64-apple-ios/libtera_ffi.a", - "sha256": "9238c523a73d5df2c41a2bcd76606ff42fe3f458b36188e9f81701895055c0ad" + "sha256": "aa03ceb4b82422068786b0bb300b803f42308ee901f180dfff67eaf2e935036b" }, { - "bytes": 82598, + "bytes": 84686, "path": "source/aarch64-apple-darwin.json", - "sha256": "5e02a9adf87e45e5193108b0d8c26497ae783adf94d19be42727cbd6295b8aa4" + "sha256": "a36bf93e1dd5fe02c94a372858a6ae95781315822099b9acdc74bbb8c6c283f8" }, { - "bytes": 82442, + "bytes": 84530, "path": "source/aarch64-apple-ios-sim.json", - "sha256": "c52969823b5673a6a11bd1f8f1b87e490259042fff475cfae31e7396dc5a772c" + "sha256": "01c66e19801f7a103de539d85192a608293da74ddc986a5388f819d6a38ec239" }, { - "bytes": 82438, + "bytes": 84526, "path": "source/aarch64-apple-ios.json", - "sha256": "a02fd8ea3155846b3ebc69e8ddcdce51c436a27e3a60d3945ff0101a67bb456f" + "sha256": "3a74308521321d281658c7d2ba5be947e06b790f7eb5e3dad77b8544f5412da0" } ], "language": "swift", @@ -112,7 +112,7 @@ "schema": "radroots.artifact-manifest.v2", "source": { "repository": "https://github.com/radrootslabs/tera", - "tree": "285367d2870ff7b21a26fb854919e0c6ede69df3" + "tree": "fd8f5f318e898c71e22b21daf727bfe936daf9b4" }, "source_records": { "aarch64-apple-darwin": "source/aarch64-apple-darwin.json", @@ -139,9 +139,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 74726648, + "bytes": 74831168, "path": "Tera/Frameworks/TeraFFI.xcframework/ios-arm64-simulator/libtera_ffi.a", - "sha256": "72438f74ff908c2ba7600a688a1f181deaed5a39904e658dd775719cea6b6fff" + "sha256": "14eff7d216e69a6489fcbb47eda412ff8e06cecfce000e4380df3034bed940f0" }, { "bytes": 75137, @@ -154,9 +154,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 74787784, + "bytes": 74891696, "path": "Tera/Frameworks/TeraFFI.xcframework/ios-arm64/libtera_ffi.a", - "sha256": "9238c523a73d5df2c41a2bcd76606ff42fe3f458b36188e9f81701895055c0ad" + "sha256": "aa03ceb4b82422068786b0bb300b803f42308ee901f180dfff67eaf2e935036b" }, { "bytes": 592248, @@ -174,19 +174,19 @@ "sha256": "fa1cc3ee4d2ed28a8ff535e598de1b45dd2129a3260186f74c6b3d1a14069f83" }, { - "bytes": 82598, + "bytes": 84686, "path": "TeraFFI/source/aarch64-apple-darwin.json", - "sha256": "5e02a9adf87e45e5193108b0d8c26497ae783adf94d19be42727cbd6295b8aa4" + "sha256": "a36bf93e1dd5fe02c94a372858a6ae95781315822099b9acdc74bbb8c6c283f8" }, { - "bytes": 82442, + "bytes": 84530, "path": "TeraFFI/source/aarch64-apple-ios-sim.json", - "sha256": "c52969823b5673a6a11bd1f8f1b87e490259042fff475cfae31e7396dc5a772c" + "sha256": "01c66e19801f7a103de539d85192a608293da74ddc986a5388f819d6a38ec239" }, { - "bytes": 82438, + "bytes": 84526, "path": "TeraFFI/source/aarch64-apple-ios.json", - "sha256": "a02fd8ea3155846b3ebc69e8ddcdce51c436a27e3a60d3945ff0101a67bb456f" + "sha256": "3a74308521321d281658c7d2ba5be947e06b790f7eb5e3dad77b8544f5412da0" } ], "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 = "285367d2870ff7b21a26fb854919e0c6ede69df3" -manifest_sha256 = "b1597f11822e313199cc40688c9c47460c3a868c5dae8833b3adf35d4d00b1ed" +source_tree = "fd8f5f318e898c71e22b21daf727bfe936daf9b4" +manifest_sha256 = "131bda77cd11b832f6ed8831c41b5720af9913c779828a478d285b929450c880" source_date_epoch = 1787871027 [foundation] diff --git a/TeraFFI/source/aarch64-apple-darwin.json b/TeraFFI/source/aarch64-apple-darwin.json @@ -645,10 +645,10 @@ "sha256": "dff55e46521c26f5f0ece024453b55c15f132fa94f78e2cba3d141f123d4eca0" }, "core/crates/tera_core/src/runtime/product_surface.rs": { - "bytes": 7543, - "git_blob": "eb602a4d6f63d5e1f00d7759a49249679419446d", + "bytes": 7587, + "git_blob": "82c90eeb8d419354a09cf30eb60015a55820fc9f", "mode": "100644", - "sha256": "3de72c743fb5a1f9e46e76823a2edc1efcae319f5fb00aceb54a97091ee41c8e" + "sha256": "28070e6a0a31bc68d47e31b9cef38f3231ada80a0dd8c3496155fe95592e8321" }, "core/crates/tera_core/src/runtime/product_surface/authoring.rs": { "bytes": 11282, @@ -801,10 +801,10 @@ "sha256": "f5d9c1bac647bf745600c2edefffa1f25172221ac9d4913b81b2b5a8fcc8b590" }, "core/crates/tera_core/src/runtime/product_surface/outbox.rs": { - "bytes": 171873, - "git_blob": "73215066c73b2703420eb4e49229deb76e161bdd", + "bytes": 171907, + "git_blob": "dcabc95b601845caad80eafc1f43df2b47ea65c6", "mode": "100644", - "sha256": "c903ed3b20b8c8644cad4c4a1c2e5e3e7064d85ef1474590710391f4cfcd84f7" + "sha256": "dd9df1773f92e8d8291e6a270e65c8083cf94bb77ef2c35c6c6c63396567df13" }, "core/crates/tera_core/src/runtime/product_surface/outbox/inventory.rs": { "bytes": 8642, @@ -843,10 +843,46 @@ "sha256": "76e3bddfc237dac63293d264d68991b489585136fe377e39dbba72aa3a1181f4" }, "core/crates/tera_core/src/runtime/product_surface/submission.rs": { - "bytes": 4393, - "git_blob": "e49caf1ce80aebdba522284d8903130df49e4cd2", + "bytes": 4469, + "git_blob": "fd3bf4814c7652da421ffb58e132417adc260458", "mode": "100644", - "sha256": "0f1f9d5252a5c1795e4bd1a799660fd96d067f2dc55dac37163dd9f0eac8748d" + "sha256": "10dba026fab9f325e16b53548ef6c36142b6b787dab1bb4992841d9d4e0e04dd" + }, + "core/crates/tera_core/src/runtime/product_surface/submission/capture.rs": { + "bytes": 5479, + "git_blob": "252f6e268ae4285c1c642e263c6eb740f28b2158", + "mode": "100644", + "sha256": "47d4de618b1789fee9b03bef65b47603b0f1d549509b484a9fab2c7f3259a9d9" + }, + "core/crates/tera_core/src/runtime/product_surface/submission/capture/form.rs": { + "bytes": 5358, + "git_blob": "4fe474f78b22cbcf81a08b16665d82d91b45fcdc", + "mode": "100644", + "sha256": "2e2b9d17ef0e04f5fabf9a27490ce1cb92caae4200d9a7fb541e4352c02049dd" + }, + "core/crates/tera_core/src/runtime/product_surface/submission/capture/form/calendar.rs": { + "bytes": 3769, + "git_blob": "3942ea369bd64dacc5f1f9ed69de5c7679ddd409", + "mode": "100644", + "sha256": "dbd1a4ca1caba9f4914b00f2224790b5307ac0f97b4b2d427ab7d576c4545fb2" + }, + "core/crates/tera_core/src/runtime/product_surface/submission/capture/media.rs": { + "bytes": 3560, + "git_blob": "d137fdabc31a02322ea9250022d151f1fc5a7ca0", + "mode": "100644", + "sha256": "727b75bcfe09b3e6c4a6413ea86aebb0c2ce5f5256de8d49941de6cbc83eb10a" + }, + "core/crates/tera_core/src/runtime/product_surface/submission/capture/runtime_tests.rs": { + "bytes": 3275, + "git_blob": "af0f97ad2c7074d5de7cba28eb41d8ed40848c56", + "mode": "100644", + "sha256": "6b7c433b52df37ea50499bf86a9046a8ae47bad6122dfa92712595b3e3d9fe1d" + }, + "core/crates/tera_core/src/runtime/product_surface/submission/capture/tests.rs": { + "bytes": 9885, + "git_blob": "81c609cc27a574ebed30bebf47c545a75fed539d", + "mode": "100644", + "sha256": "10cee00311f24141584103be7b664e9d4fe2fd2e7a2008d59ca69c445e598aae" }, "core/crates/tera_core/src/runtime/product_surface/submission/fault_store.rs": { "bytes": 3438, @@ -891,16 +927,16 @@ "sha256": "ec786993280bd6ed83d63d71d7913a1684a6cc41c0a1eaefd02059ab92e50d40" }, "core/crates/tera_core/src/runtime/product_surface/today.rs": { - "bytes": 140913, - "git_blob": "cdbd64d61b33c2e35b4adcbe41834125286caa78", + "bytes": 140999, + "git_blob": "674cfcce505e37e271c14c313aa6e1c93dc45956", "mode": "100644", - "sha256": "4ef107bd6e4dd7870fd262dea9efebcc19ed7e6b262a9baa34ebdf8894d2720c" + "sha256": "31ab557d528858e913fc62f41b80bfd00c39cb1e57e169d81c51f345645a998e" }, "core/crates/tera_core/src/runtime/product_surface/today_admission_tests.rs": { - "bytes": 14718, - "git_blob": "04317c6d326af1e591e78a246315f2049fe613ee", + "bytes": 13548, + "git_blob": "49290a9de69cda0fb30590006d65c8fdbc4e6e50", "mode": "100644", - "sha256": "3949dd625ef559b1b922f7d1661907c7451037f973ad0950b627c5c69a4ba985" + "sha256": "454f5647199ec62eca8ef6d36b9b2c13ec1b56c8ddd2ba00197ffc7bedc86c36" }, "core/crates/tera_core/src/runtime/product_surface/today_backfill_cursor.rs": { "bytes": 6898, @@ -921,10 +957,10 @@ "sha256": "fa01f28b24461f35effe7a943a44aba8381c547b8949dd59d3b82f211124afee" }, "core/crates/tera_core/src/runtime/product_surface/today_calendar_migration_tests.rs": { - "bytes": 22282, - "git_blob": "f261aac3545344485ea70af5d4cf3509d7d53697", + "bytes": 22286, + "git_blob": "bb66620255076230165b59f507331704aa41cba7", "mode": "100644", - "sha256": "8ba0bb6ce192a3c1dbb873937d821e36a7fa908eefe0ffa27e84190be4276daa" + "sha256": "4af6ed960986c6d606d07c0589958b52b131c44a334e6a0a762aa5a7e502d2c6" }, "core/crates/tera_core/src/runtime/product_surface/today_calendar_tests.rs": { "bytes": 3065, @@ -956,6 +992,12 @@ "mode": "100644", "sha256": "28491691c60f9ab41eba1e4537ba0689b2f240af2a9a1d49c0ab5caabd05f43b" }, + "core/crates/tera_core/src/runtime/product_surface/today_permissive_evidence_tests.rs": { + "bytes": 1242, + "git_blob": "0976b78a67ef56dfe19fc422dea89c39540ae85f", + "mode": "100644", + "sha256": "82066604130534748981481842ce5948a495454d96f761dcefa1779fa19faa17" + }, "core/crates/tera_core/src/runtime/product_surface/today_reconciliation.rs": { "bytes": 2252, "git_blob": "0871eff4fec4e3f656b17770b1062bf6b9a1fac7", @@ -1773,13 +1815,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 126998, - "git_blob": "ec3baa34ed2b5435a684c5e1a67088a9fc1d5d7c", + "bytes": 128410, + "git_blob": "5dc7e9d79eb9e3e8509f5683d0fc513d7c08a6e5", "mode": "100644", - "sha256": "abb909b89241735ab749bf19ad5c7f603612ae4f282daccd036ebfc2927023b3" + "sha256": "ce1ba5a2231b01b7d3e2244633a00f52d15d97a424e9f688e784f3a4c699f383" } }, "policy": "staged_inputs", - "tree": "285367d2870ff7b21a26fb854919e0c6ede69df3" + "tree": "fd8f5f318e898c71e22b21daf727bfe936daf9b4" } } diff --git a/TeraFFI/source/aarch64-apple-ios-sim.json b/TeraFFI/source/aarch64-apple-ios-sim.json @@ -641,10 +641,10 @@ "sha256": "dff55e46521c26f5f0ece024453b55c15f132fa94f78e2cba3d141f123d4eca0" }, "core/crates/tera_core/src/runtime/product_surface.rs": { - "bytes": 7543, - "git_blob": "eb602a4d6f63d5e1f00d7759a49249679419446d", + "bytes": 7587, + "git_blob": "82c90eeb8d419354a09cf30eb60015a55820fc9f", "mode": "100644", - "sha256": "3de72c743fb5a1f9e46e76823a2edc1efcae319f5fb00aceb54a97091ee41c8e" + "sha256": "28070e6a0a31bc68d47e31b9cef38f3231ada80a0dd8c3496155fe95592e8321" }, "core/crates/tera_core/src/runtime/product_surface/authoring.rs": { "bytes": 11282, @@ -797,10 +797,10 @@ "sha256": "f5d9c1bac647bf745600c2edefffa1f25172221ac9d4913b81b2b5a8fcc8b590" }, "core/crates/tera_core/src/runtime/product_surface/outbox.rs": { - "bytes": 171873, - "git_blob": "73215066c73b2703420eb4e49229deb76e161bdd", + "bytes": 171907, + "git_blob": "dcabc95b601845caad80eafc1f43df2b47ea65c6", "mode": "100644", - "sha256": "c903ed3b20b8c8644cad4c4a1c2e5e3e7064d85ef1474590710391f4cfcd84f7" + "sha256": "dd9df1773f92e8d8291e6a270e65c8083cf94bb77ef2c35c6c6c63396567df13" }, "core/crates/tera_core/src/runtime/product_surface/outbox/inventory.rs": { "bytes": 8642, @@ -839,10 +839,46 @@ "sha256": "76e3bddfc237dac63293d264d68991b489585136fe377e39dbba72aa3a1181f4" }, "core/crates/tera_core/src/runtime/product_surface/submission.rs": { - "bytes": 4393, - "git_blob": "e49caf1ce80aebdba522284d8903130df49e4cd2", + "bytes": 4469, + "git_blob": "fd3bf4814c7652da421ffb58e132417adc260458", "mode": "100644", - "sha256": "0f1f9d5252a5c1795e4bd1a799660fd96d067f2dc55dac37163dd9f0eac8748d" + "sha256": "10dba026fab9f325e16b53548ef6c36142b6b787dab1bb4992841d9d4e0e04dd" + }, + "core/crates/tera_core/src/runtime/product_surface/submission/capture.rs": { + "bytes": 5479, + "git_blob": "252f6e268ae4285c1c642e263c6eb740f28b2158", + "mode": "100644", + "sha256": "47d4de618b1789fee9b03bef65b47603b0f1d549509b484a9fab2c7f3259a9d9" + }, + "core/crates/tera_core/src/runtime/product_surface/submission/capture/form.rs": { + "bytes": 5358, + "git_blob": "4fe474f78b22cbcf81a08b16665d82d91b45fcdc", + "mode": "100644", + "sha256": "2e2b9d17ef0e04f5fabf9a27490ce1cb92caae4200d9a7fb541e4352c02049dd" + }, + "core/crates/tera_core/src/runtime/product_surface/submission/capture/form/calendar.rs": { + "bytes": 3769, + "git_blob": "3942ea369bd64dacc5f1f9ed69de5c7679ddd409", + "mode": "100644", + "sha256": "dbd1a4ca1caba9f4914b00f2224790b5307ac0f97b4b2d427ab7d576c4545fb2" + }, + "core/crates/tera_core/src/runtime/product_surface/submission/capture/media.rs": { + "bytes": 3560, + "git_blob": "d137fdabc31a02322ea9250022d151f1fc5a7ca0", + "mode": "100644", + "sha256": "727b75bcfe09b3e6c4a6413ea86aebb0c2ce5f5256de8d49941de6cbc83eb10a" + }, + "core/crates/tera_core/src/runtime/product_surface/submission/capture/runtime_tests.rs": { + "bytes": 3275, + "git_blob": "af0f97ad2c7074d5de7cba28eb41d8ed40848c56", + "mode": "100644", + "sha256": "6b7c433b52df37ea50499bf86a9046a8ae47bad6122dfa92712595b3e3d9fe1d" + }, + "core/crates/tera_core/src/runtime/product_surface/submission/capture/tests.rs": { + "bytes": 9885, + "git_blob": "81c609cc27a574ebed30bebf47c545a75fed539d", + "mode": "100644", + "sha256": "10cee00311f24141584103be7b664e9d4fe2fd2e7a2008d59ca69c445e598aae" }, "core/crates/tera_core/src/runtime/product_surface/submission/fault_store.rs": { "bytes": 3438, @@ -887,16 +923,16 @@ "sha256": "ec786993280bd6ed83d63d71d7913a1684a6cc41c0a1eaefd02059ab92e50d40" }, "core/crates/tera_core/src/runtime/product_surface/today.rs": { - "bytes": 140913, - "git_blob": "cdbd64d61b33c2e35b4adcbe41834125286caa78", + "bytes": 140999, + "git_blob": "674cfcce505e37e271c14c313aa6e1c93dc45956", "mode": "100644", - "sha256": "4ef107bd6e4dd7870fd262dea9efebcc19ed7e6b262a9baa34ebdf8894d2720c" + "sha256": "31ab557d528858e913fc62f41b80bfd00c39cb1e57e169d81c51f345645a998e" }, "core/crates/tera_core/src/runtime/product_surface/today_admission_tests.rs": { - "bytes": 14718, - "git_blob": "04317c6d326af1e591e78a246315f2049fe613ee", + "bytes": 13548, + "git_blob": "49290a9de69cda0fb30590006d65c8fdbc4e6e50", "mode": "100644", - "sha256": "3949dd625ef559b1b922f7d1661907c7451037f973ad0950b627c5c69a4ba985" + "sha256": "454f5647199ec62eca8ef6d36b9b2c13ec1b56c8ddd2ba00197ffc7bedc86c36" }, "core/crates/tera_core/src/runtime/product_surface/today_backfill_cursor.rs": { "bytes": 6898, @@ -917,10 +953,10 @@ "sha256": "fa01f28b24461f35effe7a943a44aba8381c547b8949dd59d3b82f211124afee" }, "core/crates/tera_core/src/runtime/product_surface/today_calendar_migration_tests.rs": { - "bytes": 22282, - "git_blob": "f261aac3545344485ea70af5d4cf3509d7d53697", + "bytes": 22286, + "git_blob": "bb66620255076230165b59f507331704aa41cba7", "mode": "100644", - "sha256": "8ba0bb6ce192a3c1dbb873937d821e36a7fa908eefe0ffa27e84190be4276daa" + "sha256": "4af6ed960986c6d606d07c0589958b52b131c44a334e6a0a762aa5a7e502d2c6" }, "core/crates/tera_core/src/runtime/product_surface/today_calendar_tests.rs": { "bytes": 3065, @@ -952,6 +988,12 @@ "mode": "100644", "sha256": "28491691c60f9ab41eba1e4537ba0689b2f240af2a9a1d49c0ab5caabd05f43b" }, + "core/crates/tera_core/src/runtime/product_surface/today_permissive_evidence_tests.rs": { + "bytes": 1242, + "git_blob": "0976b78a67ef56dfe19fc422dea89c39540ae85f", + "mode": "100644", + "sha256": "82066604130534748981481842ce5948a495454d96f761dcefa1779fa19faa17" + }, "core/crates/tera_core/src/runtime/product_surface/today_reconciliation.rs": { "bytes": 2252, "git_blob": "0871eff4fec4e3f656b17770b1062bf6b9a1fac7", @@ -1769,13 +1811,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 126998, - "git_blob": "ec3baa34ed2b5435a684c5e1a67088a9fc1d5d7c", + "bytes": 128410, + "git_blob": "5dc7e9d79eb9e3e8509f5683d0fc513d7c08a6e5", "mode": "100644", - "sha256": "abb909b89241735ab749bf19ad5c7f603612ae4f282daccd036ebfc2927023b3" + "sha256": "ce1ba5a2231b01b7d3e2244633a00f52d15d97a424e9f688e784f3a4c699f383" } }, "policy": "staged_inputs", - "tree": "285367d2870ff7b21a26fb854919e0c6ede69df3" + "tree": "fd8f5f318e898c71e22b21daf727bfe936daf9b4" } } diff --git a/TeraFFI/source/aarch64-apple-ios.json b/TeraFFI/source/aarch64-apple-ios.json @@ -641,10 +641,10 @@ "sha256": "dff55e46521c26f5f0ece024453b55c15f132fa94f78e2cba3d141f123d4eca0" }, "core/crates/tera_core/src/runtime/product_surface.rs": { - "bytes": 7543, - "git_blob": "eb602a4d6f63d5e1f00d7759a49249679419446d", + "bytes": 7587, + "git_blob": "82c90eeb8d419354a09cf30eb60015a55820fc9f", "mode": "100644", - "sha256": "3de72c743fb5a1f9e46e76823a2edc1efcae319f5fb00aceb54a97091ee41c8e" + "sha256": "28070e6a0a31bc68d47e31b9cef38f3231ada80a0dd8c3496155fe95592e8321" }, "core/crates/tera_core/src/runtime/product_surface/authoring.rs": { "bytes": 11282, @@ -797,10 +797,10 @@ "sha256": "f5d9c1bac647bf745600c2edefffa1f25172221ac9d4913b81b2b5a8fcc8b590" }, "core/crates/tera_core/src/runtime/product_surface/outbox.rs": { - "bytes": 171873, - "git_blob": "73215066c73b2703420eb4e49229deb76e161bdd", + "bytes": 171907, + "git_blob": "dcabc95b601845caad80eafc1f43df2b47ea65c6", "mode": "100644", - "sha256": "c903ed3b20b8c8644cad4c4a1c2e5e3e7064d85ef1474590710391f4cfcd84f7" + "sha256": "dd9df1773f92e8d8291e6a270e65c8083cf94bb77ef2c35c6c6c63396567df13" }, "core/crates/tera_core/src/runtime/product_surface/outbox/inventory.rs": { "bytes": 8642, @@ -839,10 +839,46 @@ "sha256": "76e3bddfc237dac63293d264d68991b489585136fe377e39dbba72aa3a1181f4" }, "core/crates/tera_core/src/runtime/product_surface/submission.rs": { - "bytes": 4393, - "git_blob": "e49caf1ce80aebdba522284d8903130df49e4cd2", + "bytes": 4469, + "git_blob": "fd3bf4814c7652da421ffb58e132417adc260458", "mode": "100644", - "sha256": "0f1f9d5252a5c1795e4bd1a799660fd96d067f2dc55dac37163dd9f0eac8748d" + "sha256": "10dba026fab9f325e16b53548ef6c36142b6b787dab1bb4992841d9d4e0e04dd" + }, + "core/crates/tera_core/src/runtime/product_surface/submission/capture.rs": { + "bytes": 5479, + "git_blob": "252f6e268ae4285c1c642e263c6eb740f28b2158", + "mode": "100644", + "sha256": "47d4de618b1789fee9b03bef65b47603b0f1d549509b484a9fab2c7f3259a9d9" + }, + "core/crates/tera_core/src/runtime/product_surface/submission/capture/form.rs": { + "bytes": 5358, + "git_blob": "4fe474f78b22cbcf81a08b16665d82d91b45fcdc", + "mode": "100644", + "sha256": "2e2b9d17ef0e04f5fabf9a27490ce1cb92caae4200d9a7fb541e4352c02049dd" + }, + "core/crates/tera_core/src/runtime/product_surface/submission/capture/form/calendar.rs": { + "bytes": 3769, + "git_blob": "3942ea369bd64dacc5f1f9ed69de5c7679ddd409", + "mode": "100644", + "sha256": "dbd1a4ca1caba9f4914b00f2224790b5307ac0f97b4b2d427ab7d576c4545fb2" + }, + "core/crates/tera_core/src/runtime/product_surface/submission/capture/media.rs": { + "bytes": 3560, + "git_blob": "d137fdabc31a02322ea9250022d151f1fc5a7ca0", + "mode": "100644", + "sha256": "727b75bcfe09b3e6c4a6413ea86aebb0c2ce5f5256de8d49941de6cbc83eb10a" + }, + "core/crates/tera_core/src/runtime/product_surface/submission/capture/runtime_tests.rs": { + "bytes": 3275, + "git_blob": "af0f97ad2c7074d5de7cba28eb41d8ed40848c56", + "mode": "100644", + "sha256": "6b7c433b52df37ea50499bf86a9046a8ae47bad6122dfa92712595b3e3d9fe1d" + }, + "core/crates/tera_core/src/runtime/product_surface/submission/capture/tests.rs": { + "bytes": 9885, + "git_blob": "81c609cc27a574ebed30bebf47c545a75fed539d", + "mode": "100644", + "sha256": "10cee00311f24141584103be7b664e9d4fe2fd2e7a2008d59ca69c445e598aae" }, "core/crates/tera_core/src/runtime/product_surface/submission/fault_store.rs": { "bytes": 3438, @@ -887,16 +923,16 @@ "sha256": "ec786993280bd6ed83d63d71d7913a1684a6cc41c0a1eaefd02059ab92e50d40" }, "core/crates/tera_core/src/runtime/product_surface/today.rs": { - "bytes": 140913, - "git_blob": "cdbd64d61b33c2e35b4adcbe41834125286caa78", + "bytes": 140999, + "git_blob": "674cfcce505e37e271c14c313aa6e1c93dc45956", "mode": "100644", - "sha256": "4ef107bd6e4dd7870fd262dea9efebcc19ed7e6b262a9baa34ebdf8894d2720c" + "sha256": "31ab557d528858e913fc62f41b80bfd00c39cb1e57e169d81c51f345645a998e" }, "core/crates/tera_core/src/runtime/product_surface/today_admission_tests.rs": { - "bytes": 14718, - "git_blob": "04317c6d326af1e591e78a246315f2049fe613ee", + "bytes": 13548, + "git_blob": "49290a9de69cda0fb30590006d65c8fdbc4e6e50", "mode": "100644", - "sha256": "3949dd625ef559b1b922f7d1661907c7451037f973ad0950b627c5c69a4ba985" + "sha256": "454f5647199ec62eca8ef6d36b9b2c13ec1b56c8ddd2ba00197ffc7bedc86c36" }, "core/crates/tera_core/src/runtime/product_surface/today_backfill_cursor.rs": { "bytes": 6898, @@ -917,10 +953,10 @@ "sha256": "fa01f28b24461f35effe7a943a44aba8381c547b8949dd59d3b82f211124afee" }, "core/crates/tera_core/src/runtime/product_surface/today_calendar_migration_tests.rs": { - "bytes": 22282, - "git_blob": "f261aac3545344485ea70af5d4cf3509d7d53697", + "bytes": 22286, + "git_blob": "bb66620255076230165b59f507331704aa41cba7", "mode": "100644", - "sha256": "8ba0bb6ce192a3c1dbb873937d821e36a7fa908eefe0ffa27e84190be4276daa" + "sha256": "4af6ed960986c6d606d07c0589958b52b131c44a334e6a0a762aa5a7e502d2c6" }, "core/crates/tera_core/src/runtime/product_surface/today_calendar_tests.rs": { "bytes": 3065, @@ -952,6 +988,12 @@ "mode": "100644", "sha256": "28491691c60f9ab41eba1e4537ba0689b2f240af2a9a1d49c0ab5caabd05f43b" }, + "core/crates/tera_core/src/runtime/product_surface/today_permissive_evidence_tests.rs": { + "bytes": 1242, + "git_blob": "0976b78a67ef56dfe19fc422dea89c39540ae85f", + "mode": "100644", + "sha256": "82066604130534748981481842ce5948a495454d96f761dcefa1779fa19faa17" + }, "core/crates/tera_core/src/runtime/product_surface/today_reconciliation.rs": { "bytes": 2252, "git_blob": "0871eff4fec4e3f656b17770b1062bf6b9a1fac7", @@ -1769,13 +1811,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 126998, - "git_blob": "ec3baa34ed2b5435a684c5e1a67088a9fc1d5d7c", + "bytes": 128410, + "git_blob": "5dc7e9d79eb9e3e8509f5683d0fc513d7c08a6e5", "mode": "100644", - "sha256": "abb909b89241735ab749bf19ad5c7f603612ae4f282daccd036ebfc2927023b3" + "sha256": "ce1ba5a2231b01b7d3e2244633a00f52d15d97a424e9f688e784f3a4c699f383" } }, "policy": "staged_inputs", - "tree": "285367d2870ff7b21a26fb854919e0c6ede69df3" + "tree": "fd8f5f318e898c71e22b21daf727bfe936daf9b4" } } diff --git a/core/crates/tera_core/src/runtime/product_surface.rs b/core/crates/tera_core/src/runtime/product_surface.rs @@ -90,10 +90,10 @@ pub use settings::{ }; #[cfg(feature = "mobile-social")] pub use submission::{ - SUBMISSION_RESERVATION_MAX_BYTES, SUBMISSION_RESERVATION_PAYLOAD_SCHEMA, + CapturedSubmission, SUBMISSION_RESERVATION_MAX_BYTES, SUBMISSION_RESERVATION_PAYLOAD_SCHEMA, SUBMISSION_RESERVATION_SCHEMA_SHA256, SUBMISSION_RESERVATION_SCHEMA_VERSION, - SubmissionCommandId, SubmissionReservationError, SubmissionReservationReceipt, - SubmissionReservationRequest, + SubmissionCaptureError, SubmissionCommandId, SubmissionReservationError, + SubmissionReservationReceipt, SubmissionReservationRequest, }; #[cfg(feature = "mobile-social")] pub use today::{ diff --git a/core/crates/tera_core/src/runtime/product_surface/outbox.rs b/core/crates/tera_core/src/runtime/product_surface/outbox.rs @@ -1456,7 +1456,10 @@ impl TeraRuntime { .await } - fn active_queue_policy(&self, now_unix_ms: u64) -> Result<Phase1QueuePolicy, Phase1DraftError> { + pub(super) fn active_queue_policy( + &self, + now_unix_ms: u64, + ) -> Result<Phase1QueuePolicy, Phase1DraftError> { let report = self .client .nostr_status() diff --git a/core/crates/tera_core/src/runtime/product_surface/submission.rs b/core/crates/tera_core/src/runtime/product_surface/submission.rs @@ -1,8 +1,10 @@ //! Stable user-action reservations over the existing authored draft journal. //! Reservation does not authorize signing, delivery or strict publication. +mod capture; mod record; mod repository; +pub use capture::{CapturedSubmission, SubmissionCaptureError}; #[cfg(test)] mod test_support; diff --git a/core/crates/tera_core/src/runtime/product_surface/submission/capture.rs b/core/crates/tera_core/src/runtime/product_surface/submission/capture.rs @@ -0,0 +1,155 @@ +//! Strict captured input; durable operation installation belongs to the submission transaction. + +use std::sync::Arc; + +use radroots_event_codec::authoring::AuthoredEventPlan; +use radroots_sdk::transport::{BlossomConfigFingerprint, BlossomSlot}; + +use super::{ + SubmissionReservationError, SubmissionReservationReceipt, SubmissionReservationRequest, +}; +use crate::{ + TeraRuntime, + runtime::product_surface::{ + ComposerPersistenceError, Phase1AddCommand, Phase1MediaPrerequisite, Phase1QueuePolicy, + }, +}; + +mod form; +mod media; +#[cfg(test)] +mod tests; + +#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)] +pub enum SubmissionCaptureError { + #[error(transparent)] + Reservation(#[from] SubmissionReservationError), + #[error("invalid publication input: {0}")] + InvalidInput(&'static str), + #[error("publication policy is unavailable")] + PolicyUnavailable, +} + +/// An owned immutable plan and its complete source binding. This is not a commit receipt. +/// It contains no secret, signer, transport handle or live form reference. +#[derive(Clone)] +pub struct CapturedSubmission { + reservation: SubmissionReservationReceipt, + command: Phase1AddCommand, + plan: AuthoredEventPlan, + media: Vec<Phase1MediaPrerequisite>, + media_policy: Option<BlossomConfigFingerprint>, + policy: Phase1QueuePolicy, +} + +impl std::fmt::Debug for CapturedSubmission { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("CapturedSubmission") + .finish_non_exhaustive() + } +} + +impl CapturedSubmission { + pub fn reservation(&self) -> &SubmissionReservationReceipt { + &self.reservation + } + pub fn command(&self) -> &Phase1AddCommand { + &self.command + } + pub fn plan(&self) -> &AuthoredEventPlan { + &self.plan + } + pub fn media(&self) -> &[Phase1MediaPrerequisite] { + &self.media + } + pub fn policy(&self) -> &Phase1QueuePolicy { + &self.policy + } + pub fn media_policy(&self) -> Option<BlossomConfigFingerprint> { + self.media_policy + } + + /// Compares semantic request identity, excluding the transient replay observation. + pub fn same_request(&self, other: &Self) -> bool { + self.reservation.request() == other.reservation.request() + && self.reservation.captured() == other.reservation.captured() + && self.reservation.reserved_at_unix_ms() == other.reservation.reserved_at_unix_ms() + && self.command == other.command + && self.media == other.media + && self.media_policy == other.media_policy + && self.policy == other.policy + && self.plan == other.plan + } + + fn capture( + reservation: SubmissionReservationReceipt, + policy: Phase1QueuePolicy, + blossom: Option<&BlossomSlot>, + bytes: Vec<Arc<[u8]>>, + ) -> Result<Self, SubmissionCaptureError> { + let input = reservation.captured().form().input(); + let media_policy = if input.media.is_empty() { + None + } else { + Some( + blossom + .and_then(BlossomSlot::config_fingerprint) + .ok_or(SubmissionCaptureError::PolicyUnavailable)?, + ) + }; + let prepared = media::prepare(&input.media, blossom, media_policy, bytes)?; + let command = form::command(input, &prepared, reservation.reserved_at_unix_ms() / 1000)?; + let plan = command + .authored_plan( + reservation.reserved_at_unix_ms() / 1000, + hex::encode(reservation.request().scope().author().as_bytes()), + ) + .map_err(|_| SubmissionCaptureError::InvalidInput("invalid_plan"))?; + let media = prepared + .iter() + .map(|item| { + Phase1MediaPrerequisite::new(item.input.opaque_reference.clone(), &item.descriptor) + .map_err(|_| SubmissionCaptureError::InvalidInput("invalid_media_reference")) + }) + .collect::<Result<_, _>>()?; + Ok(Self { + reservation, + command, + plan, + media, + media_policy, + policy, + }) + } +} + +impl TeraRuntime { + /// Validates the reserved historical revision and freezes current target policy. + /// Media bytes must match the captured metadata in order; no upload or signer is invoked. + pub async fn submission_capture( + &self, + request: &SubmissionReservationRequest, + media_bytes: Vec<Arc<[u8]>>, + ) -> Result<CapturedSubmission, SubmissionCaptureError> { + let _command = self + .lifecycle + .enter() + .map_err(SubmissionReservationError::from)?; + if self.store_public_key != Some(request.scope().author()) { + return Err(SubmissionReservationError::Source( + ComposerPersistenceError::ScopeMismatch, + ) + .into()); + } + let reservation = self.submission_reserve(request).await?; + let policy = self + .active_queue_policy(reservation.reserved_at_unix_ms()) + .map_err(|_| SubmissionCaptureError::PolicyUnavailable)?; + let blossom = self + .client + .blossom() + .map_err(|_| SubmissionCaptureError::PolicyUnavailable)?; + CapturedSubmission::capture(reservation, policy, blossom, media_bytes) + } +} diff --git a/core/crates/tera_core/src/runtime/product_surface/submission/capture/form.rs b/core/crates/tera_core/src/runtime/product_surface/submission/capture/form.rs @@ -0,0 +1,133 @@ +use super::{SubmissionCaptureError as E, media::Prepared}; +use crate::runtime::product_surface::{ + AddCommandType, ComposerFormInput, CreateAsk, CreateFoodAvailability, CreatePhotoUpdate, + CreateUpdate, Phase1AddCommand, +}; +use radroots_event::food::availability::{ + FoodAvailabilityDetails, FoodAvailabilityDetailsParts, FoodAvailabilityImage, + FoodAvailabilityStatus, FoodContent, FoodCurrency, FoodIdentifier, FoodImageDimensions, + FoodPrice, FoodPublishedAt, FoodQuantity, FoodText, FoodUnit, +}; + +mod calendar; + +pub(super) fn command( + input: &ComposerFormInput, + media: &[Prepared], + time: u64, +) -> Result<Phase1AddCommand, E> { + Ok(match input.command_type { + AddCommandType::CreateUpdate => { + if !media.is_empty() { + return Err(E::InvalidInput("media_not_allowed")); + } + Phase1AddCommand::CreateUpdate( + CreateUpdate::new(input.content.clone()) + .map_err(|_| E::InvalidInput("invalid_update"))?, + ) + } + AddCommandType::CreatePhotoUpdate => Phase1AddCommand::CreatePhotoUpdate( + CreatePhotoUpdate::new(content(&input.content, media)?, post_images(media)?) + .map_err(|_| E::InvalidInput("invalid_photo_update"))?, + ), + AddCommandType::CreateAsk => Phase1AddCommand::CreateAsk( + CreateAsk::new(content(&input.content, media)?, post_images(media)?) + .map_err(|_| E::InvalidInput("invalid_ask"))?, + ), + AddCommandType::CreateEvent => { + Phase1AddCommand::CreateEvent(calendar::command(input, media)?) + } + AddCommandType::CreateFoodAvailability => { + Phase1AddCommand::CreateFoodAvailability(food(input, media, time)?) + } + }) +} + +fn post_images(media: &[Prepared]) -> Result<Vec<radroots_event::post::AuthoredPostImage>, E> { + media.iter().map(Prepared::post_image).collect() +} + +fn content(input: &str, media: &[Prepared]) -> Result<String, E> { + if input.trim().is_empty() { + return Err(E::InvalidInput("content_required")); + } + let mut content = input.to_owned(); + for item in media { + let url = item.descriptor.url().as_str(); + match content.match_indices(url).count() { + 0 => { + if !content.ends_with('\n') { + content.push('\n'); + } + content.push_str(url); + } + 1 => {} + _ => return Err(E::InvalidInput("duplicate_media_reference")), + } + } + Ok(content) +} + +fn required<'a>(value: Option<&'a str>, code: &'static str) -> Result<&'a str, E> { + value.ok_or(E::InvalidInput(code)) +} + +fn food( + input: &ComposerFormInput, + media: &[Prepared], + time: u64, +) -> Result<CreateFoodAvailability, E> { + let unit = FoodUnit::parse(required(input.unit.as_deref(), "food_unit_required")?) + .map_err(|_| E::InvalidInput("invalid_food_unit"))?; + let images = media + .iter() + .map(|item| { + Ok(FoodAvailabilityImage::new( + item.image()?, + FoodImageDimensions::new(item.input.width, item.input.height) + .map_err(|_| E::InvalidInput("invalid_image_dimensions"))?, + )) + }) + .collect::<Result<Vec<_>, E>>()?; + let details = FoodAvailabilityDetails::new(FoodAvailabilityDetailsParts { + content: FoodContent::new(input.content.clone()) + .map_err(|_| E::InvalidInput("invalid_food_content"))?, + identifier: FoodIdentifier::parse(required( + input.identifier.as_deref(), + "food_identifier_required", + )?) + .map_err(|_| E::InvalidInput("invalid_food_identifier"))?, + title: FoodText::new(required(input.title.as_deref(), "food_title_required")?) + .map_err(|_| E::InvalidInput("invalid_food_title"))?, + summary: FoodText::new(required(input.summary.as_deref(), "food_summary_required")?) + .map_err(|_| E::InvalidInput("invalid_food_summary"))?, + published_at: FoodPublishedAt::new(input.food_published_at_unix_s.unwrap_or(time)) + .map_err(|_| E::InvalidInput("invalid_food_published_at"))?, + location: FoodText::new(required( + input.location.as_deref(), + "food_location_required", + )?) + .map_err(|_| E::InvalidInput("invalid_food_location"))?, + price: FoodPrice::new( + required(input.price_amount.as_deref(), "food_price_required")?, + FoodCurrency::parse(required( + input.currency.as_deref(), + "food_currency_required", + )?) + .map_err(|_| E::InvalidInput("invalid_food_currency"))?, + unit, + ) + .map_err(|_| E::InvalidInput("invalid_food_price"))?, + quantity: input + .quantity + .as_deref() + .map(|value| FoodQuantity::new(value, unit)) + .transpose() + .map_err(|_| E::InvalidInput("invalid_food_quantity"))?, + status: FoodAvailabilityStatus::parse(input.food_status.as_deref().unwrap_or("active")) + .map_err(|_| E::InvalidInput("invalid_food_status"))?, + images, + }) + .map_err(|_| E::InvalidInput("invalid_food_availability"))?; + Ok(CreateFoodAvailability::new(details)) +} diff --git a/core/crates/tera_core/src/runtime/product_surface/submission/capture/form/calendar.rs b/core/crates/tera_core/src/runtime/product_surface/submission/capture/form/calendar.rs @@ -0,0 +1,85 @@ +use super::{E, Prepared, required}; +use crate::runtime::product_surface::{ComposerFormInput, CreateEvent, Phase1DraftEventTiming}; +use radroots_event::calendar::{ + AuthoredCalendarDateEvent, AuthoredCalendarTimeEvent, CalendarDate, +}; + +pub(super) fn command(input: &ComposerFormInput, media: &[Prepared]) -> Result<CreateEvent, E> { + if media.len() > 1 { + return Err(E::InvalidInput("event_image_limit")); + } + let identifier = required(input.identifier.as_deref(), "event_identifier_required")?; + let title = required(input.title.as_deref(), "event_title_required")?; + match input + .event_timing + .ok_or(E::InvalidInput("event_timing_required"))? + { + Phase1DraftEventTiming::AllDay => { + let start = CalendarDate::parse(required( + input.event_start_date.as_deref(), + "event_start_date_required", + )?) + .map_err(|_| E::InvalidInput("invalid_event_start_date"))?; + let mut event = AuthoredCalendarDateEvent::new(identifier, title, start) + .map_err(|_| E::InvalidInput("invalid_event"))?; + if let Some(end) = input.event_end_date.as_deref() { + event = event + .with_end( + CalendarDate::parse(end) + .map_err(|_| E::InvalidInput("invalid_event_end_date"))?, + ) + .map_err(|_| E::InvalidInput("invalid_event_range"))?; + } + if !input.content.is_empty() { + event = event + .with_description(input.content.clone()) + .map_err(|_| E::InvalidInput("invalid_event_description"))?; + } + if let Some(location) = &input.location { + event = event + .with_locations(vec![location.clone()]) + .map_err(|_| E::InvalidInput("invalid_event_location"))?; + } + if let Some(image) = media.first() { + event = event + .with_image(image.image()?) + .map_err(|_| E::InvalidInput("invalid_event_image"))?; + } + Ok(CreateEvent::date(event)) + } + Phase1DraftEventTiming::Timed => { + let start = input + .event_start_unix_s + .filter(|value| *value != 0) + .ok_or(E::InvalidInput("event_start_required"))?; + let mut event = AuthoredCalendarTimeEvent::new(identifier, title, start) + .map_err(|_| E::InvalidInput("invalid_event"))?; + if let Some(end) = input.event_end_unix_s { + event = event + .with_end(end) + .map_err(|_| E::InvalidInput("invalid_event_range"))?; + } + if let Some(zone) = &input.event_timezone { + event = event + .with_start_tzid(zone) + .map_err(|_| E::InvalidInput("invalid_event_timezone"))?; + } + if !input.content.is_empty() { + event = event + .with_description(input.content.clone()) + .map_err(|_| E::InvalidInput("invalid_event_description"))?; + } + if let Some(location) = &input.location { + event = event + .with_locations(vec![location.clone()]) + .map_err(|_| E::InvalidInput("invalid_event_location"))?; + } + if let Some(image) = media.first() { + event = event + .with_image(image.image()?) + .map_err(|_| E::InvalidInput("invalid_event_image"))?; + } + Ok(CreateEvent::time(event)) + } + } +} diff --git a/core/crates/tera_core/src/runtime/product_surface/submission/capture/media.rs b/core/crates/tera_core/src/runtime/product_surface/submission/capture/media.rs @@ -0,0 +1,90 @@ +use super::SubmissionCaptureError as E; +use crate::runtime::product_surface::ComposerMediaInput; +use radroots_blossom::{BlobDescriptor, ByteVerifiedDescriptor, MediaType, Sha256}; +use radroots_event::{ + media::AuthoredImage, + post::{AuthoredPostImage, PostImageDimensions}, +}; +use radroots_sdk::transport::{ + BlossomConfigFingerprint, BlossomImageDimensions, BlossomSlot, BlossomUploadRequest, +}; +use std::sync::Arc; + +pub(super) struct Prepared { + pub input: ComposerMediaInput, + pub descriptor: ByteVerifiedDescriptor, +} + +impl Prepared { + pub fn image(&self) -> Result<AuthoredImage, E> { + AuthoredImage::try_from_verified_descriptor(self.descriptor.clone()) + .map_err(|_| E::InvalidInput("invalid_image_media")) + } + pub fn post_image(&self) -> Result<AuthoredPostImage, E> { + AuthoredPostImage::new( + self.image()?, + PostImageDimensions::new(self.input.width, self.input.height) + .map_err(|_| E::InvalidInput("invalid_image_dimensions"))?, + self.input.alt.clone(), + ) + .map_err(|_| E::InvalidInput("invalid_image")) + } +} + +pub(super) fn prepare( + inputs: &[ComposerMediaInput], + blossom: Option<&BlossomSlot>, + expected_policy: Option<BlossomConfigFingerprint>, + bytes: Vec<Arc<[u8]>>, +) -> Result<Vec<Prepared>, E> { + if inputs.len() != bytes.len() { + return Err(E::InvalidInput("media_count_mismatch")); + } + inputs + .iter() + .zip(bytes) + .map(|(input, bytes)| { + let blossom = blossom.ok_or(E::InvalidInput("blossom_not_configured"))?; + let media_type = MediaType::parse(&input.media_type) + .map_err(|_| E::InvalidInput("invalid_media_type"))?; + let hash = Sha256::from_hex(&input.sha256) + .map_err(|_| E::InvalidInput("invalid_media_digest"))?; + if input.byte_size != bytes.len() as u64 || hash != Sha256::digest(&bytes) { + return Err(E::InvalidInput("media_verification_failed")); + } + let dimensions = BlossomImageDimensions::new(input.width, input.height) + .map_err(|_| E::InvalidInput("invalid_image_dimensions"))?; + let verified_at = input + .prepared_at_unix_s + .checked_mul(1000) + .ok_or(E::InvalidInput("invalid_media_time"))?; + let request = BlossomUploadRequest::new( + Arc::clone(&bytes), + media_type.clone(), + dimensions, + verified_at, + ) + .map_err(|_| E::InvalidInput("media_verification_failed"))?; + let transaction = blossom + .prepare_upload(request) + .map_err(|_| E::InvalidInput("invalid_media_policy"))?; + if Some(transaction.config_fingerprint()) != expected_policy { + return Err(E::InvalidInput("media_policy_changed")); + } + let descriptor = BlobDescriptor::new( + transaction.expected_url().clone(), + hash, + input.byte_size, + media_type.clone(), + input.prepared_at_unix_s, + ) + .and_then(BlobDescriptor::approve_reference) + .and_then(|descriptor| descriptor.verify_bytes(&bytes, &media_type)) + .map_err(|_| E::InvalidInput("media_verification_failed"))?; + Ok(Prepared { + input: input.clone(), + descriptor, + }) + }) + .collect() +} diff --git a/core/crates/tera_core/src/runtime/product_surface/submission/capture/runtime_tests.rs b/core/crates/tera_core/src/runtime/product_surface/submission/capture/runtime_tests.rs @@ -0,0 +1,93 @@ +use super::*; +use crate::runtime::{ + builder::RuntimeBuilder, + store::{MobileUserStoreConfig, ProtectedDataAvailability}, +}; + +#[tokio::test] +async fn durable_source_survives_strict_failure_edits_restart_and_account_switch() { + let root = tempfile::tempdir().unwrap(); + let build = |author: &str, generation: u8| { + let config = MobileUserStoreConfig::from_encoded( + root.path(), + author, + &hex::encode([generation; 32]), + NOW, + ProtectedDataAvailability::Available, + ) + .unwrap(); + std::fs::create_dir_all(config.owner_directory()).unwrap(); + RuntimeBuilder::new(config).build() + }; + let runtime = build(AUTHOR, 1).await.unwrap(); + let request = request(); + let blank = + ComposerPartialForm::new(ComposerFormInput::empty(AddCommandType::CreateUpdate)).unwrap(); + runtime + .composer_create( + request.scope(), + request.composer_id(), + ComposerEditSequence::INITIAL, + blank, + ) + .await + .unwrap(); + let before = runtime + .composer_load(request.scope(), request.composer_id()) + .await + .unwrap(); + assert!(matches!( + runtime.submission_capture(&request, vec![]).await, + Err(SubmissionCaptureError::InvalidInput("invalid_update")) + )); + assert_eq!( + runtime + .composer_load(request.scope(), request.composer_id()) + .await + .unwrap(), + before + ); + let saved = runtime + .composer_save( + request.scope(), + request.composer_id(), + request.expected_revision(), + ComposerEditSequence::new(2).unwrap(), + ComposerPartialForm::new(input(AddCommandType::CreateUpdate)).unwrap(), + ) + .await + .unwrap(); + let mut request = SubmissionReservationRequest::new( + super::super::super::SubmissionCommandId::generate().unwrap(), + request.scope().clone(), + request.composer_id(), + saved.draft().revision(), + ); + let captured = runtime.submission_capture(&request, vec![]).await.unwrap(); + runtime + .composer_save( + request.scope(), + request.composer_id(), + request.expected_revision(), + ComposerEditSequence::new(3).unwrap(), + ComposerPartialForm::new(input(AddCommandType::CreateAsk)).unwrap(), + ) + .await + .unwrap(); + let replay = runtime.submission_capture(&request, vec![]).await.unwrap(); + assert!(captured.same_request(&replay)); + runtime.shutdown().await.unwrap(); + drop(runtime); + // A new host session retains the durable store generation. + let runtime = build(AUTHOR, 1).await.unwrap(); + assert!(captured.same_request(&runtime.submission_capture(&request, vec![]).await.unwrap())); + request.scope = scope(OTHER, "nearby"); + assert!(matches!( + runtime.submission_capture(&request, vec![]).await, + Err(SubmissionCaptureError::Reservation( + SubmissionReservationError::Source(ComposerPersistenceError::ScopeMismatch) + )) + )); + assert_eq!(captured.plan().author().to_string(), AUTHOR); + runtime.shutdown().await.unwrap(); +} diff --git a/core/crates/tera_core/src/runtime/product_surface/submission/capture/tests.rs b/core/crates/tera_core/src/runtime/product_surface/submission/capture/tests.rs @@ -0,0 +1,315 @@ +use super::super::test_support::*; +use super::*; +use crate::runtime::product_surface::{ + AddCommandType, ComposerDraft, ComposerEditSequence, ComposerFormInput, ComposerMediaInput, + ComposerPartialForm, Phase1CancellationPolicy, Phase1DraftEventTiming, Phase1RelaySatisfaction, +}; +use radroots_sdk::transport::{ + BlossomConfig, BlossomEndpointAuthority, BlossomHostKind, BlossomProfile, +}; + +#[path = "runtime_tests.rs"] +mod runtime_tests; + +fn reservation(input: ComposerFormInput) -> SubmissionReservationReceipt { + let request = request(); + SubmissionReservationReceipt { + reservation_id: super::super::record::reservation_id(&request).unwrap(), + captured: ComposerDraft::new( + request.composer_id(), + request.expected_revision(), + request.scope().clone(), + ComposerEditSequence::INITIAL, + ComposerPartialForm::new(input).unwrap(), + ), + request, + reserved_at_unix_ms: NOW, + replayed: false, + } +} + +fn policy(relay: &str) -> Phase1QueuePolicy { + Phase1QueuePolicy::new( + vec![relay.into()], + Phase1RelaySatisfaction::AllAccepted, + NOW + 1000, + Phase1CancellationPolicy::LocalCooperative, + ) + .unwrap() +} + +fn blossom() -> BlossomSlot { + let slot = BlossomSlot::new(); + slot.configure(BlossomConfig::from_profile( + BlossomProfile::new( + BlossomHostKind::Simulator, + BlossomEndpointAuthority::LoopbackDevelopment, + "http://127.0.0.1:3000", + std::iter::empty::<&str>(), + ) + .unwrap(), + )) + .unwrap(); + slot +} + +fn input(kind: AddCommandType) -> ComposerFormInput { + let mut input = ComposerFormInput::empty(kind); + input.content = "PRIVATE harvest café".into(); + input +} + +fn photo() -> (ComposerMediaInput, Arc<[u8]>) { + let mut bytes = b"\x89PNG\r\n\x1a\n\0\0\0\rIHDR".to_vec(); + bytes.extend_from_slice(&2u32.to_be_bytes()); + bytes.extend_from_slice(&2u32.to_be_bytes()); + ( + ComposerMediaInput { + opaque_reference: "media:harvest".into(), + sha256: radroots_blossom::Sha256::digest(&bytes).to_hex(), + media_type: "image/png".into(), + byte_size: bytes.len() as u64, + width: 2, + height: 2, + alt: "Harvest".into(), + prepared_at_unix_s: NOW / 1000, + }, + bytes.into(), + ) +} + +#[test] +fn all_five_families_capture_shared_plans_and_both_calendar_profiles() { + let mut cases = vec![ + input(AddCommandType::CreateUpdate), + input(AddCommandType::CreateAsk), + ]; + for timing in [ + Phase1DraftEventTiming::AllDay, + Phase1DraftEventTiming::Timed, + ] { + let mut event = input(AddCommandType::CreateEvent); + event.identifier = Some("harvest".into()); + event.title = Some("Harvest".into()); + event.event_timing = Some(timing); + event.event_start_date = Some("2026-09-12".into()); + event.event_end_date = Some("2026-09-13".into()); + event.event_start_unix_s = Some(NOW / 1000); + event.event_end_unix_s = Some(NOW / 1000 + 3600); + event.event_timezone = Some("America/Vancouver".into()); + cases.push(event); + } + let mut food = input(AddCommandType::CreateFoodAvailability); + food.identifier = Some("carrots".into()); + food.title = Some("Carrots".into()); + food.summary = Some("Fresh".into()); + food.location = Some("Victoria".into()); + food.price_amount = Some("4.5".into()); + food.currency = Some("CAD".into()); + food.unit = Some("bunch".into()); + food.quantity = Some("12".into()); + cases.push(food); + cases.push(input(AddCommandType::CreatePhotoUpdate)); + let slot = blossom(); + for mut input in cases { + let bytes = if input.command_type == AddCommandType::CreatePhotoUpdate { + let (media, bytes) = photo(); + input.media.push(media); + vec![bytes] + } else { + vec![] + }; + let kind = input.command_type; + let captured = CapturedSubmission::capture( + reservation(input), + policy("wss://relay.example"), + Some(&slot), + bytes, + ) + .unwrap(); + assert_eq!(captured.command().command_type(), kind); + assert_eq!(captured.plan().author().to_string(), AUTHOR); + assert_eq!(captured.plan().created_at(), NOW / 1000); + assert!(!format!("{captured:?}").contains("PRIVATE")); + } +} + +#[test] +fn captured_values_and_semantic_equality_survive_edits_and_replay_observation() { + let mut live = input(AddCommandType::CreateUpdate); + let original = reservation(live.clone()); + let captured = CapturedSubmission::capture( + original.clone(), + policy("wss://relay.example"), + None, + vec![], + ) + .unwrap(); + live.content = "later content".into(); + let mut replay = original.clone(); + replay.replayed = true; + let equal = + CapturedSubmission::capture(replay, policy("wss://relay.example"), None, vec![]).unwrap(); + assert!(captured.same_request(&equal)); + assert_eq!( + captured.reservation().captured().form().input().content, + "PRIVATE harvest café" + ); + let changed = CapturedSubmission::capture( + reservation(live), + policy("wss://relay.example"), + None, + vec![], + ) + .unwrap(); + assert!(!captured.same_request(&changed)); + let changed_policy = CapturedSubmission::capture( + original.clone(), + policy("wss://other.example"), + None, + vec![], + ) + .unwrap(); + assert!(!captured.same_request(&changed_policy)); + let mut other = original; + other.request.scope = scope(OTHER, "nearby"); + other.captured = ComposerDraft::new( + other.captured.id(), + other.captured.revision(), + other.request.scope.clone(), + other.captured.edit_sequence(), + other.captured.form().clone(), + ); + let changed_author = + CapturedSubmission::capture(other, policy("wss://relay.example"), None, vec![]).unwrap(); + assert!(!captured.same_request(&changed_author)); + assert_eq!(captured.plan().author().to_string(), AUTHOR); +} + +#[test] +fn strict_incomplete_fields_fail_without_changing_the_captured_form() { + for kind in [ + AddCommandType::CreateUpdate, + AddCommandType::CreatePhotoUpdate, + AddCommandType::CreateAsk, + AddCommandType::CreateEvent, + AddCommandType::CreateFoodAvailability, + ] { + let reservation = reservation(ComposerFormInput::empty(kind)); + let before = reservation.captured().clone(); + assert!( + CapturedSubmission::capture( + reservation.clone(), + policy("wss://relay.example"), + None, + vec![] + ) + .is_err() + ); + assert_eq!(reservation.captured(), &before); + } +} + +#[test] +fn captured_media_requires_exact_bytes_dimensions_count_and_current_target() { + let slot = blossom(); + let (media, bytes) = photo(); + let mut input = input(AddCommandType::CreatePhotoUpdate); + input.media.push(media); + let source = reservation(input.clone()); + assert!( + CapturedSubmission::capture( + source.clone(), + policy("wss://relay.example"), + Some(&slot), + vec![] + ) + .is_err() + ); + assert!( + CapturedSubmission::capture( + source.clone(), + policy("wss://relay.example"), + None, + vec![Arc::clone(&bytes)] + ) + .is_err() + ); + assert!( + CapturedSubmission::capture( + source.clone(), + policy("wss://relay.example"), + Some(&slot), + vec![Arc::from(&b"wrong"[..])] + ) + .is_err() + ); + input.media[0].width = 3; + assert!( + CapturedSubmission::capture( + reservation(input), + policy("wss://relay.example"), + Some(&slot), + vec![Arc::clone(&bytes)] + ) + .is_err() + ); + let captured = CapturedSubmission::capture( + source, + policy("wss://relay.example"), + Some(&slot), + vec![bytes], + ) + .unwrap(); + assert!( + captured.media()[0] + .url() + .starts_with("http://127.0.0.1:3000/") + ); + assert_eq!( + captured.media()[0].stage(), + crate::runtime::product_surface::Phase1MediaStage::Pending + ); +} + +#[test] +fn media_policy_changes_conflict_even_when_blob_url_and_bytes_are_unchanged() { + let slot = blossom(); + let (media, bytes) = photo(); + let mut input = input(AddCommandType::CreatePhotoUpdate); + input.media.push(media); + let source = reservation(input); + let first = CapturedSubmission::capture( + source.clone(), + policy("wss://relay.example"), + Some(&slot), + vec![Arc::clone(&bytes)], + ) + .unwrap(); + let original_policy = first.media_policy(); + slot.configure( + BlossomConfig::from_profile(slot.profile().unwrap()) + .with_limits(1024 * 1024, 8192, 1) + .unwrap(), + ) + .unwrap(); + let changed = CapturedSubmission::capture( + source.clone(), + policy("wss://relay.example"), + Some(&slot), + vec![Arc::clone(&bytes)], + ) + .unwrap(); + assert_eq!(first.media()[0].url(), changed.media()[0].url()); + assert!(!first.same_request(&changed)); + assert_ne!(first.media_policy(), changed.media_policy()); + assert!(matches!( + media::prepare( + &source.captured().form().input().media, + Some(&slot), + original_policy, + vec![bytes] + ), + Err(SubmissionCaptureError::InvalidInput("media_policy_changed")) + )); +} diff --git a/core/crates/tera_core/src/runtime/product_surface/today.rs b/core/crates/tera_core/src/runtime/product_surface/today.rs @@ -70,6 +70,10 @@ mod calendar_wire_tests; mod calendar_migration; #[cfg(test)] +#[path = "today_permissive_evidence_tests.rs"] +mod permissive_evidence; + +#[cfg(test)] #[path = "today_calendar_migration_tests.rs"] mod calendar_migration_tests; diff --git a/core/crates/tera_core/src/runtime/product_surface/today_admission_tests.rs b/core/crates/tera_core/src/runtime/product_surface/today_admission_tests.rs @@ -1,10 +1,8 @@ +use super::permissive_evidence::PermissiveEvidence; use super::sync_tests::runtime; use super::tests::{context, signed}; use super::*; -use radroots_event::{ - SignedEvent, - admission::{RawEvent, SignatureVerifier, VisibilityPolicy}, -}; +use radroots_event::{SignedEvent, admission::RawEvent}; use radroots_event_codec::verify::Nip01SignatureVerifier; use radroots_transport::{ Error, EventSource, FetchPage, FetchRequest, SourceStatus, Target, TransportId, @@ -66,43 +64,6 @@ fn observed(event: SignedEvent) -> ObservedEvent { ) } -// Shared typestates accept caller-supplied verifier/policy implementations. -// The app's public direct-ingest boundary must use its actual crypto/profile -// rules before writing even if another host supplied permissive evidence. -pub(super) struct PermissiveEvidence; -impl SignatureVerifier for PermissiveEvidence { - fn verify_signature( - &self, - _: &radroots_event::envelope::EventEnvelope, - ) -> Result<(), radroots_event::admission::Error> { - Ok(()) - } -} -impl radroots_event::admission::AdmissionPolicy for PermissiveEvidence { - type Error = std::convert::Infallible; - fn policy_id(&self) -> &'static str { - "tera.test.permissive-admission" - } - fn admit( - &self, - _: &radroots_event::admission::ContractValidatedEvent, - ) -> Result<(), Self::Error> { - Ok(()) - } -} -impl VisibilityPolicy for PermissiveEvidence { - type Error = std::convert::Infallible; - fn policy_id(&self) -> &'static str { - "tera.test.permissive-visibility" - } - fn make_visible( - &self, - _: &radroots_event::admission::AdmittedEvent, - ) -> Result<(), Self::Error> { - Ok(()) - } -} - #[tokio::test] async fn direct_visible_ingest_cannot_poison_storage_with_permissive_signature_evidence() { let runtime = TeraRuntime::test_memory().unwrap(); diff --git a/core/crates/tera_core/src/runtime/product_surface/today_calendar_migration_tests.rs b/core/crates/tera_core/src/runtime/product_surface/today_calendar_migration_tests.rs @@ -479,7 +479,7 @@ async fn obsolete_snapshots_and_unknown_generations_fail_without_rewriting_histo #[tokio::test] async fn invalid_historical_visible_heads_are_quarantined_without_reviving_old_cards() { - use super::admission_tests::PermissiveEvidence; + use super::permissive_evidence::PermissiveEvidence; use radroots_event::admission::RawEvent; use radroots_transport::{ Target, TransportId, diff --git a/core/crates/tera_core/src/runtime/product_surface/today_permissive_evidence_tests.rs b/core/crates/tera_core/src/runtime/product_surface/today_permissive_evidence_tests.rs @@ -0,0 +1,38 @@ +use radroots_event::admission::{SignatureVerifier, VisibilityPolicy}; + +// Shared typestates accept caller-supplied verifier/policy implementations. +// The app's public direct-ingest boundary must use its actual crypto/profile +// rules before writing even if another host supplied permissive evidence. +pub(super) struct PermissiveEvidence; +impl SignatureVerifier for PermissiveEvidence { + fn verify_signature( + &self, + _: &radroots_event::envelope::EventEnvelope, + ) -> Result<(), radroots_event::admission::Error> { + Ok(()) + } +} +impl radroots_event::admission::AdmissionPolicy for PermissiveEvidence { + type Error = std::convert::Infallible; + fn policy_id(&self) -> &'static str { + "tera.test.permissive-admission" + } + fn admit( + &self, + _: &radroots_event::admission::ContractValidatedEvent, + ) -> Result<(), Self::Error> { + Ok(()) + } +} +impl VisibilityPolicy for PermissiveEvidence { + type Error = std::convert::Infallible; + fn policy_id(&self) -> &'static str { + "tera.test.permissive-visibility" + } + fn make_visible( + &self, + _: &radroots_event::admission::AdmittedEvent, + ) -> Result<(), Self::Error> { + Ok(()) + } +} diff --git a/release/provenance.json b/release/provenance.json @@ -2,7 +2,7 @@ "artifacts": { "app_api_sha256": "020924097c0d7efc33128cb8fd3d3b2026d95f57c44da71880e585aff80f070b", "ffi_api_sha256": "fa1cc3ee4d2ed28a8ff535e598de1b45dd2129a3260186f74c6b3d1a14069f83", - "ffi_provenance_sha256": "b1597f11822e313199cc40688c9c47460c3a868c5dae8833b3adf35d4d00b1ed", + "ffi_provenance_sha256": "131bda77cd11b832f6ed8831c41b5720af9913c779828a478d285b929450c880", "info_plist_sha256": "15ef08b1cdd1096cfb9eeaf5be5bf8f814807a7ca9350bbbb47860fa72ec13ef", "privacy_manifest_sha256": "a331d51864743ebe4e00dd22360b4a538b6b3ac26a6b3eb54094e60a36959a12", "sbom_sha256": "374a98323bf66c55bd2f3ec28027d3068e8dccb4eeae1dbd070f0ac95fec9510", @@ -22,7 +22,7 @@ "lib_revision": "d3332ff1c68245a232f093434ee35fe2a342ed38", "source_date_epoch": 1787871027, "swift_package_lock_sha256": "94ae067a374726cdaf6b4ca0a5e44663c57fcdc5334060c5ffef5e79cfbf04c0", - "tera_ffi_source_tree": "285367d2870ff7b21a26fb854919e0c6ede69df3", + "tera_ffi_source_tree": "fd8f5f318e898c71e22b21daf727bfe936daf9b4", "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 @@ -4067,6 +4067,14 @@ "count": 2 }, { + "path": "core/crates/tera_core/src/runtime/product_surface/submission/capture/media.rs", + "count": 1 + }, + { + "path": "core/crates/tera_core/src/runtime/product_surface/submission/capture/tests.rs", + "count": 1 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/today.rs", "count": 2 }, @@ -4169,12 +4177,24 @@ "count": 1 }, { + "path": "core/crates/tera_core/src/runtime/product_surface/submission/capture/form.rs", + "count": 2 + }, + { + "path": "core/crates/tera_core/src/runtime/product_surface/submission/capture/form/calendar.rs", + "count": 1 + }, + { + "path": "core/crates/tera_core/src/runtime/product_surface/submission/capture/media.rs", + "count": 1 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/today.rs", "count": 6 }, { "path": "core/crates/tera_core/src/runtime/product_surface/today_admission_tests.rs", - "count": 6 + "count": 1 }, { "path": "core/crates/tera_core/src/runtime/product_surface/today_backfill_tests.rs", @@ -4189,6 +4209,10 @@ "count": 2 }, { + "path": "core/crates/tera_core/src/runtime/product_surface/today_permissive_evidence_tests.rs", + "count": 6 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/today_viewer_calendar_tests.rs", "count": 1 }, @@ -4259,6 +4283,10 @@ "count": 4 }, { + "path": "core/crates/tera_core/src/runtime/product_surface/submission/capture.rs", + "count": 1 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/today.rs", "count": 2 }, @@ -4561,6 +4589,18 @@ "count": 23 }, { + "path": "core/crates/tera_core/src/runtime/product_surface/submission/capture.rs", + "count": 1 + }, + { + "path": "core/crates/tera_core/src/runtime/product_surface/submission/capture/media.rs", + "count": 1 + }, + { + "path": "core/crates/tera_core/src/runtime/product_surface/submission/capture/tests.rs", + "count": 1 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/submission/repository_tests.rs", "count": 5 },