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 4f28e5ccc9d72c4385b26a9e07ca198f156dcc79
parent 3d06a47a962371eaad325894d41ec943f85bd686
Author: triesap <tyson@radroots.org>
Date:   Thu, 10 Sep 2026 13:08:15 +0000

compose: Model bounded incomplete drafts

- Define validated composer identity, stable scope and checked edit order
- Preserve bounded raw partial forms separately from strict publication
- Test round-trip bounds, malformed input, overflow and redacted diagnostics
- Regenerate exact native artifacts and qualify standalone consumers

Diffstat:
MTeraFFI/provenance.json | 54+++++++++++++++++++++++++++---------------------------
MTeraFFI/source.lock | 4++--
MTeraFFI/source/aarch64-apple-darwin.json | 38+++++++++++++++++++++++++++++++-------
MTeraFFI/source/aarch64-apple-ios-sim.json | 38+++++++++++++++++++++++++++++++-------
MTeraFFI/source/aarch64-apple-ios.json | 38+++++++++++++++++++++++++++++++-------
Mcore/crates/tera_core/src/runtime/product_surface.rs | 8++++++++
Acore/crates/tera_core/src/runtime/product_surface/composer.rs | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/crates/tera_core/src/runtime/product_surface/composer/form.rs | 199+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/crates/tera_core/src/runtime/product_surface/composer/identity.rs | 142+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/crates/tera_core/src/runtime/product_surface/composer/tests.rs | 302++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mrelease/provenance.json | 4++--
Mtest-fixtures/legacy-identifiers.v1.json | 24++++++++++++++++++++++++
12 files changed, 870 insertions(+), 52 deletions(-)

diff --git a/TeraFFI/provenance.json b/TeraFFI/provenance.json @@ -22,9 +22,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 74073560, + "bytes": 74158312, "path": "TeraFFI.xcframework/ios-arm64-simulator/libtera_ffi.a", - "sha256": "75c234fc38deb7256251cc36402729ea2515386c0aca25ab8eaaefe5e7baa194" + "sha256": "bd6bf366fb6a9aa1d16f248863152bca91987e6f86d0a8d4263e35b1080eafa4" }, { "bytes": 71509, @@ -37,9 +37,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 74142448, + "bytes": 74223136, "path": "TeraFFI.xcframework/ios-arm64/libtera_ffi.a", - "sha256": "0f64982e961c1bb822d25509205ba9502d0735e44f07a38722cd6ef9da27c63a" + "sha256": "47e6b3fc1a8a538709300a3a0f6dcb006c0de13493a611616c76a542836aac55" }, { "bytes": 42544, @@ -77,34 +77,34 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 20612720, + "bytes": 20614064, "path": "native/aarch64-apple-darwin/libtera_ffi.dylib", - "sha256": "1f7116dbfdd62e63b26c14c91dc6d89b80cc5326786966f2302229a811f82234" + "sha256": "86167d0c906cb9fa1804834de4f96bd1966d302d2c80cdb3bb5ab765471c6edb" }, { - "bytes": 74073560, + "bytes": 74158312, "path": "native/aarch64-apple-ios-sim/libtera_ffi.a", - "sha256": "75c234fc38deb7256251cc36402729ea2515386c0aca25ab8eaaefe5e7baa194" + "sha256": "bd6bf366fb6a9aa1d16f248863152bca91987e6f86d0a8d4263e35b1080eafa4" }, { - "bytes": 74142448, + "bytes": 74223136, "path": "native/aarch64-apple-ios/libtera_ffi.a", - "sha256": "0f64982e961c1bb822d25509205ba9502d0735e44f07a38722cd6ef9da27c63a" + "sha256": "47e6b3fc1a8a538709300a3a0f6dcb006c0de13493a611616c76a542836aac55" }, { - "bytes": 72396, + "bytes": 73537, "path": "source/aarch64-apple-darwin.json", - "sha256": "159bea54b2f7daed1ac323b40199a4fcadb71724699b168e6a8cd7f9240ba60d" + "sha256": "0298803e4efc991b0bba6f68b5297b6d299c99d5e8935d5ca261b4f6648b7e05" }, { - "bytes": 72240, + "bytes": 73381, "path": "source/aarch64-apple-ios-sim.json", - "sha256": "a3df3829d9753fe3d9467f9c1487afb1b42c658a915244caa2d358b30d1707ad" + "sha256": "f425a738ea4a720c6665aaa08ed6f128fcf9df0abd6cda871f760f8ae1cf8bf5" }, { - "bytes": 72236, + "bytes": 73377, "path": "source/aarch64-apple-ios.json", - "sha256": "825952afc2b82cae2d750f3717a0ebbd265897d9049c62eee9435becb7d0e7cc" + "sha256": "9d05336e0586128305b4a4cd5183acdc1ed84805ea8e5603e21b567e6cb87e7f" } ], "language": "swift", @@ -112,7 +112,7 @@ "schema": "radroots.artifact-manifest.v2", "source": { "repository": "https://github.com/radrootslabs/tera", - "tree": "84401a0c284d46897210f6ef8eee4da2732c8d41" + "tree": "628534dfa9763f18b3ebba50dc25b1adb71801ba" }, "source_records": { "aarch64-apple-darwin": "source/aarch64-apple-darwin.json", @@ -139,9 +139,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 74073560, + "bytes": 74158312, "path": "Tera/Frameworks/TeraFFI.xcframework/ios-arm64-simulator/libtera_ffi.a", - "sha256": "75c234fc38deb7256251cc36402729ea2515386c0aca25ab8eaaefe5e7baa194" + "sha256": "bd6bf366fb6a9aa1d16f248863152bca91987e6f86d0a8d4263e35b1080eafa4" }, { "bytes": 71509, @@ -154,9 +154,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 74142448, + "bytes": 74223136, "path": "Tera/Frameworks/TeraFFI.xcframework/ios-arm64/libtera_ffi.a", - "sha256": "0f64982e961c1bb822d25509205ba9502d0735e44f07a38722cd6ef9da27c63a" + "sha256": "47e6b3fc1a8a538709300a3a0f6dcb006c0de13493a611616c76a542836aac55" }, { "bytes": 521598, @@ -174,19 +174,19 @@ "sha256": "e2e025ee9a29182970f52f53593796afd154cdfb09aba197474577103dcb4ad6" }, { - "bytes": 72396, + "bytes": 73537, "path": "TeraFFI/source/aarch64-apple-darwin.json", - "sha256": "159bea54b2f7daed1ac323b40199a4fcadb71724699b168e6a8cd7f9240ba60d" + "sha256": "0298803e4efc991b0bba6f68b5297b6d299c99d5e8935d5ca261b4f6648b7e05" }, { - "bytes": 72240, + "bytes": 73381, "path": "TeraFFI/source/aarch64-apple-ios-sim.json", - "sha256": "a3df3829d9753fe3d9467f9c1487afb1b42c658a915244caa2d358b30d1707ad" + "sha256": "f425a738ea4a720c6665aaa08ed6f128fcf9df0abd6cda871f760f8ae1cf8bf5" }, { - "bytes": 72236, + "bytes": 73377, "path": "TeraFFI/source/aarch64-apple-ios.json", - "sha256": "825952afc2b82cae2d750f3717a0ebbd265897d9049c62eee9435becb7d0e7cc" + "sha256": "9d05336e0586128305b4a4cd5183acdc1ed84805ea8e5603e21b567e6cb87e7f" } ], "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 = "84401a0c284d46897210f6ef8eee4da2732c8d41" -manifest_sha256 = "893a6c04c88868f70d9d911262cbb06c4f6a433e56a703388fb7344678a64530" +source_tree = "628534dfa9763f18b3ebba50dc25b1adb71801ba" +manifest_sha256 = "47abd7034be7df2e7f58cf6404dbcff14ce75599b38b999f24064f64b05965d5" 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": 6420, - "git_blob": "05d1ab510c3ce5739a828447e1714912131a3ab7", + "bytes": 6792, + "git_blob": "346a7cd12368feae85595c80719caf52ec6bc417", "mode": "100644", - "sha256": "f1e285b8cf81b14dd80ee79c249423533acf9a1fe38a38bb6da1391057e295bf" + "sha256": "e386e570092c204602b26cfd7a515179c32502e3855be91547c1471b5eed43ac" }, "core/crates/tera_core/src/runtime/product_surface/authoring.rs": { "bytes": 11282, @@ -680,6 +680,30 @@ "mode": "100644", "sha256": "3ec65d1cb2d94740396137d03920b212afb74692dba11729e4ce5bb3c5a2a789" }, + "core/crates/tera_core/src/runtime/product_surface/composer.rs": { + "bytes": 1925, + "git_blob": "18d40988e6a1045444e7ff3a205aa045d8b113d0", + "mode": "100644", + "sha256": "f8fc65558d1b3c7a20edff9a5bdecf47d6bdca64eab8a7a6faa9075e7b7b8cdd" + }, + "core/crates/tera_core/src/runtime/product_surface/composer/form.rs": { + "bytes": 6734, + "git_blob": "193ab6e5d32a17fab99048862c1c46ef6d9085e3", + "mode": "100644", + "sha256": "0c69e3cc7674807537ad1f29667e474a3483e14160859da728c2db477a5ab7b1" + }, + "core/crates/tera_core/src/runtime/product_surface/composer/identity.rs": { + "bytes": 4004, + "git_blob": "f212f48aedc7612cf4cf7441f115e51e52b26c6f", + "mode": "100644", + "sha256": "3dc9c2fc93fa3f368de6717dbe85406c240a71f05e7d94784fc4f2553b53897b" + }, + "core/crates/tera_core/src/runtime/product_surface/composer/tests.rs": { + "bytes": 11869, + "git_blob": "fdb5caa3b8172923577ea860eb42fdf913754cc9", + "mode": "100644", + "sha256": "77a596cc8ae3814a072904a2c25251454b6149c595c19646514e3561f2049bb5" + }, "core/crates/tera_core/src/runtime/product_surface/context.rs": { "bytes": 14211, "git_blob": "92498807101c638cec675e0b49fde1f4ceed17a2", @@ -1557,13 +1581,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 121056, - "git_blob": "2b03b6159e5f7f21e740ea5a88eb9ec6281259b7", + "bytes": 121846, + "git_blob": "6d5c4b58152735f4655422275bcb8873af1efb05", "mode": "100644", - "sha256": "5c36fc9b2210b480ff61aec17de3046ee23f2c1a567139767a6c1d551396313a" + "sha256": "5b3c4b76223c1e56cce542a7fc15f91ba97547f2f1912646e5c323989861a844" } }, "policy": "staged_inputs", - "tree": "84401a0c284d46897210f6ef8eee4da2732c8d41" + "tree": "628534dfa9763f18b3ebba50dc25b1adb71801ba" } } 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": 6420, - "git_blob": "05d1ab510c3ce5739a828447e1714912131a3ab7", + "bytes": 6792, + "git_blob": "346a7cd12368feae85595c80719caf52ec6bc417", "mode": "100644", - "sha256": "f1e285b8cf81b14dd80ee79c249423533acf9a1fe38a38bb6da1391057e295bf" + "sha256": "e386e570092c204602b26cfd7a515179c32502e3855be91547c1471b5eed43ac" }, "core/crates/tera_core/src/runtime/product_surface/authoring.rs": { "bytes": 11282, @@ -676,6 +676,30 @@ "mode": "100644", "sha256": "3ec65d1cb2d94740396137d03920b212afb74692dba11729e4ce5bb3c5a2a789" }, + "core/crates/tera_core/src/runtime/product_surface/composer.rs": { + "bytes": 1925, + "git_blob": "18d40988e6a1045444e7ff3a205aa045d8b113d0", + "mode": "100644", + "sha256": "f8fc65558d1b3c7a20edff9a5bdecf47d6bdca64eab8a7a6faa9075e7b7b8cdd" + }, + "core/crates/tera_core/src/runtime/product_surface/composer/form.rs": { + "bytes": 6734, + "git_blob": "193ab6e5d32a17fab99048862c1c46ef6d9085e3", + "mode": "100644", + "sha256": "0c69e3cc7674807537ad1f29667e474a3483e14160859da728c2db477a5ab7b1" + }, + "core/crates/tera_core/src/runtime/product_surface/composer/identity.rs": { + "bytes": 4004, + "git_blob": "f212f48aedc7612cf4cf7441f115e51e52b26c6f", + "mode": "100644", + "sha256": "3dc9c2fc93fa3f368de6717dbe85406c240a71f05e7d94784fc4f2553b53897b" + }, + "core/crates/tera_core/src/runtime/product_surface/composer/tests.rs": { + "bytes": 11869, + "git_blob": "fdb5caa3b8172923577ea860eb42fdf913754cc9", + "mode": "100644", + "sha256": "77a596cc8ae3814a072904a2c25251454b6149c595c19646514e3561f2049bb5" + }, "core/crates/tera_core/src/runtime/product_surface/context.rs": { "bytes": 14211, "git_blob": "92498807101c638cec675e0b49fde1f4ceed17a2", @@ -1553,13 +1577,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 121056, - "git_blob": "2b03b6159e5f7f21e740ea5a88eb9ec6281259b7", + "bytes": 121846, + "git_blob": "6d5c4b58152735f4655422275bcb8873af1efb05", "mode": "100644", - "sha256": "5c36fc9b2210b480ff61aec17de3046ee23f2c1a567139767a6c1d551396313a" + "sha256": "5b3c4b76223c1e56cce542a7fc15f91ba97547f2f1912646e5c323989861a844" } }, "policy": "staged_inputs", - "tree": "84401a0c284d46897210f6ef8eee4da2732c8d41" + "tree": "628534dfa9763f18b3ebba50dc25b1adb71801ba" } } 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": 6420, - "git_blob": "05d1ab510c3ce5739a828447e1714912131a3ab7", + "bytes": 6792, + "git_blob": "346a7cd12368feae85595c80719caf52ec6bc417", "mode": "100644", - "sha256": "f1e285b8cf81b14dd80ee79c249423533acf9a1fe38a38bb6da1391057e295bf" + "sha256": "e386e570092c204602b26cfd7a515179c32502e3855be91547c1471b5eed43ac" }, "core/crates/tera_core/src/runtime/product_surface/authoring.rs": { "bytes": 11282, @@ -676,6 +676,30 @@ "mode": "100644", "sha256": "3ec65d1cb2d94740396137d03920b212afb74692dba11729e4ce5bb3c5a2a789" }, + "core/crates/tera_core/src/runtime/product_surface/composer.rs": { + "bytes": 1925, + "git_blob": "18d40988e6a1045444e7ff3a205aa045d8b113d0", + "mode": "100644", + "sha256": "f8fc65558d1b3c7a20edff9a5bdecf47d6bdca64eab8a7a6faa9075e7b7b8cdd" + }, + "core/crates/tera_core/src/runtime/product_surface/composer/form.rs": { + "bytes": 6734, + "git_blob": "193ab6e5d32a17fab99048862c1c46ef6d9085e3", + "mode": "100644", + "sha256": "0c69e3cc7674807537ad1f29667e474a3483e14160859da728c2db477a5ab7b1" + }, + "core/crates/tera_core/src/runtime/product_surface/composer/identity.rs": { + "bytes": 4004, + "git_blob": "f212f48aedc7612cf4cf7441f115e51e52b26c6f", + "mode": "100644", + "sha256": "3dc9c2fc93fa3f368de6717dbe85406c240a71f05e7d94784fc4f2553b53897b" + }, + "core/crates/tera_core/src/runtime/product_surface/composer/tests.rs": { + "bytes": 11869, + "git_blob": "fdb5caa3b8172923577ea860eb42fdf913754cc9", + "mode": "100644", + "sha256": "77a596cc8ae3814a072904a2c25251454b6149c595c19646514e3561f2049bb5" + }, "core/crates/tera_core/src/runtime/product_surface/context.rs": { "bytes": 14211, "git_blob": "92498807101c638cec675e0b49fde1f4ceed17a2", @@ -1553,13 +1577,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 121056, - "git_blob": "2b03b6159e5f7f21e740ea5a88eb9ec6281259b7", + "bytes": 121846, + "git_blob": "6d5c4b58152735f4655422275bcb8873af1efb05", "mode": "100644", - "sha256": "5c36fc9b2210b480ff61aec17de3046ee23f2c1a567139767a6c1d551396313a" + "sha256": "5b3c4b76223c1e56cce542a7fc15f91ba97547f2f1912646e5c323989861a844" } }, "policy": "staged_inputs", - "tree": "84401a0c284d46897210f6ef8eee4da2732c8d41" + "tree": "628534dfa9763f18b3ebba50dc25b1adb71801ba" } } diff --git a/core/crates/tera_core/src/runtime/product_surface.rs b/core/crates/tera_core/src/runtime/product_surface.rs @@ -6,6 +6,8 @@ mod authoring; mod calendar_timing; +#[cfg(feature = "mobile-social")] +mod composer; mod context; mod cursor; mod identity; @@ -29,6 +31,12 @@ pub use authoring::{ Phase1AddCommand, Phase1ReplacementPolicy, phase1_retraction_plan, }; pub use calendar_timing::{CalendarTiming, CalendarTimingError, DateBasedTiming, TimeBasedTiming}; +#[cfg(feature = "mobile-social")] +pub use composer::{ + COMPOSER_CONTENT_MAX_BYTES, COMPOSER_FORM_MAX_BYTES, COMPOSER_MEDIA_MAX, + COMPOSER_TEXT_MAX_BYTES, ComposerDraft, ComposerEditSequence, ComposerError, ComposerFormInput, + ComposerId, ComposerMediaInput, ComposerPartialForm, ComposerRevision, ComposerScope, +}; pub use context::{ ContextAdmission, ContextRank, LocalNetwork, LocalNetworkAdmission, LocalNetworkError, LocalNetworkRelayPolicy, LocalityEvidence, diff --git a/core/crates/tera_core/src/runtime/product_surface/composer.rs b/core/crates/tera_core/src/runtime/product_surface/composer.rs @@ -0,0 +1,71 @@ +//! Bounded local editing values. These types carry no signing or delivery authority. + +mod form; +mod identity; + +pub use form::{ + COMPOSER_CONTENT_MAX_BYTES, COMPOSER_FORM_MAX_BYTES, COMPOSER_MEDIA_MAX, + COMPOSER_TEXT_MAX_BYTES, ComposerFormInput, ComposerMediaInput, ComposerPartialForm, +}; +pub use identity::{ComposerEditSequence, ComposerId, ComposerRevision, ComposerScope}; + +#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)] +pub enum ComposerError { + #[error("invalid composer identity")] + InvalidIdentity, + #[error("invalid composer revision")] + InvalidRevision, + #[error("invalid composer edit sequence")] + InvalidEditSequence, + #[error("composer form exceeds its bounds or has invalid metadata")] + InvalidForm, + #[error("unsupported composer form representation")] + InvalidRepresentation, +} + +/// A local editing revision, distinct from a strict authored plan or sealed intent. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ComposerDraft { + id: ComposerId, + revision: ComposerRevision, + scope: ComposerScope, + edit_sequence: ComposerEditSequence, + form: ComposerPartialForm, +} + +impl ComposerDraft { + pub fn new( + id: ComposerId, + revision: ComposerRevision, + scope: ComposerScope, + edit_sequence: ComposerEditSequence, + form: ComposerPartialForm, + ) -> Self { + Self { + id, + revision, + scope, + edit_sequence, + form, + } + } + + pub const fn id(&self) -> ComposerId { + self.id + } + pub const fn revision(&self) -> ComposerRevision { + self.revision + } + pub fn scope(&self) -> &ComposerScope { + &self.scope + } + pub const fn edit_sequence(&self) -> ComposerEditSequence { + self.edit_sequence + } + pub fn form(&self) -> &ComposerPartialForm { + &self.form + } +} + +#[cfg(test)] +mod tests; diff --git a/core/crates/tera_core/src/runtime/product_surface/composer/form.rs b/core/crates/tera_core/src/runtime/product_surface/composer/form.rs @@ -0,0 +1,199 @@ +use radroots_blossom::{MediaType, Sha256}; +use serde::{Deserialize, Serialize}; + +use super::ComposerError; +use crate::runtime::product_surface::{AddCommandType, Phase1DraftEventTiming}; + +pub const COMPOSER_CONTENT_MAX_BYTES: usize = 65_535; +pub const COMPOSER_TEXT_MAX_BYTES: usize = 1_024; +pub const COMPOSER_MEDIA_MAX: usize = 20; +/// Bounds decoding before JSON allocation and stays below the shared draft payload cap. +pub const COMPOSER_FORM_MAX_BYTES: usize = 1024 * 1024; +const MEDIA_REFERENCE_MAX_BYTES: usize = 256; +const MEDIA_FILE_MAX_BYTES: u64 = 10 * 1024 * 1024; + +/// Untrusted local media metadata. It proves neither byte ownership nor upload readiness. +#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ComposerMediaInput { + pub opaque_reference: String, + pub sha256: String, + pub media_type: String, + pub byte_size: u64, + pub width: u32, + pub height: u32, + pub alt: String, + pub prepared_at_unix_s: u64, +} + +impl std::fmt::Debug for ComposerMediaInput { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("ComposerMediaInput") + .finish_non_exhaustive() + } +} + +impl ComposerMediaInput { + fn validate(&self) -> Result<(), ComposerError> { + let reference = &self.opaque_reference; + if reference.len() <= "media:".len() + || reference.len() > MEDIA_REFERENCE_MAX_BYTES + || !reference.starts_with("media:") + || !reference["media:".len()..].bytes().all(|byte| { + byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'-' | b'_') + }) + || self.sha256.len() != 64 + || self.media_type.len() > 128 + || self.byte_size == 0 + || self.byte_size > MEDIA_FILE_MAX_BYTES + || self.alt.len() > COMPOSER_TEXT_MAX_BYTES + || self.prepared_at_unix_s == 0 + || self.prepared_at_unix_s.checked_mul(1000).is_none() + { + return Err(ComposerError::InvalidForm); + } + Sha256::from_hex(&self.sha256).map_err(|_| ComposerError::InvalidForm)?; + MediaType::parse(&self.media_type).map_err(|_| ComposerError::InvalidForm)?; + radroots_sdk::transport::BlossomImageDimensions::new(self.width, self.height) + .map_err(|_| ComposerError::InvalidForm)?; + Ok(()) + } +} + +/// Untrusted editing input. Validate it before retaining it as a composer form. +#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ComposerFormInput { + pub command_type: AddCommandType, + pub content: String, + pub identifier: Option<String>, + pub title: Option<String>, + pub summary: Option<String>, + pub location: Option<String>, + pub event_timing: Option<Phase1DraftEventTiming>, + pub event_start_date: Option<String>, + pub event_end_date: Option<String>, + pub event_start_unix_s: Option<u64>, + pub event_end_unix_s: Option<u64>, + pub event_timezone: Option<String>, + pub price_amount: Option<String>, + pub currency: Option<String>, + pub unit: Option<String>, + pub quantity: Option<String>, + pub food_published_at_unix_s: Option<u64>, + pub food_status: Option<String>, + pub media: Vec<ComposerMediaInput>, +} + +impl std::fmt::Debug for ComposerFormInput { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("ComposerFormInput") + .field("command_type", &self.command_type) + .field("media_count", &self.media.len()) + .finish_non_exhaustive() + } +} + +impl ComposerFormInput { + pub fn empty(command_type: AddCommandType) -> Self { + Self { + command_type, + content: String::new(), + identifier: None, + title: None, + summary: None, + location: None, + event_timing: None, + event_start_date: None, + event_end_date: None, + event_start_unix_s: None, + event_end_unix_s: None, + event_timezone: None, + price_amount: None, + currency: None, + unit: None, + quantity: None, + food_published_at_unix_s: None, + food_status: None, + media: Vec::new(), + } + } + + fn validate(&self) -> Result<(), ComposerError> { + if self.content.len() > COMPOSER_CONTENT_MAX_BYTES || self.media.len() > COMPOSER_MEDIA_MAX + { + return Err(ComposerError::InvalidForm); + } + for text in [ + &self.identifier, + &self.title, + &self.summary, + &self.location, + &self.event_start_date, + &self.event_end_date, + &self.event_timezone, + &self.price_amount, + &self.currency, + &self.unit, + &self.quantity, + &self.food_status, + ] + .into_iter() + .flatten() + { + if text.len() > COMPOSER_TEXT_MAX_BYTES { + return Err(ComposerError::InvalidForm); + } + } + for media in &self.media { + media.validate()?; + } + Ok(()) + } +} + +/// Immutable validated editing data, deliberately not a publishable event. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(try_from = "ComposerFormInput", into = "ComposerFormInput")] +pub struct ComposerPartialForm(ComposerFormInput); + +impl ComposerPartialForm { + pub fn new(input: ComposerFormInput) -> Result<Self, ComposerError> { + input.validate()?; + Ok(Self(input)) + } + + pub fn input(&self) -> &ComposerFormInput { + &self.0 + } + + pub fn from_json(bytes: &[u8]) -> Result<Self, ComposerError> { + if bytes.is_empty() || bytes.len() > COMPOSER_FORM_MAX_BYTES { + return Err(ComposerError::InvalidRepresentation); + } + serde_json::from_slice(bytes).map_err(|_| ComposerError::InvalidRepresentation) + } + + pub fn to_json(&self) -> Result<Vec<u8>, ComposerError> { + let bytes = serde_json::to_vec(self).map_err(|_| ComposerError::InvalidRepresentation)?; + if bytes.len() > COMPOSER_FORM_MAX_BYTES { + return Err(ComposerError::InvalidRepresentation); + } + Ok(bytes) + } +} + +impl TryFrom<ComposerFormInput> for ComposerPartialForm { + type Error = ComposerError; + fn try_from(value: ComposerFormInput) -> Result<Self, Self::Error> { + Self::new(value) + } +} + +impl From<ComposerPartialForm> for ComposerFormInput { + fn from(value: ComposerPartialForm) -> Self { + value.0 + } +} diff --git a/core/crates/tera_core/src/runtime/product_surface/composer/identity.rs b/core/crates/tera_core/src/runtime/product_surface/composer/identity.rs @@ -0,0 +1,142 @@ +use radroots_identity::PublicKey; +use radroots_storage::authored_draft::{AuthoredDraftId, AuthoredDraftRevision}; +use serde::{Deserialize, Serialize}; + +use super::ComposerError; +use crate::runtime::product_surface::LocalNetworkId; + +/// Persistent editing identity. It is never an authored operation or command ID. +#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +#[serde(try_from = "[u8; 16]", into = "[u8; 16]")] +pub struct ComposerId(AuthoredDraftId); + +impl ComposerId { + pub fn new(bytes: [u8; 16]) -> Result<Self, ComposerError> { + AuthoredDraftId::new(bytes) + .map(Self) + .map_err(|_| ComposerError::InvalidIdentity) + } + + pub const fn as_bytes(&self) -> &[u8; 16] { + self.0.as_bytes() + } +} + +impl TryFrom<[u8; 16]> for ComposerId { + type Error = ComposerError; + fn try_from(value: [u8; 16]) -> Result<Self, Self::Error> { + Self::new(value) + } +} + +impl From<ComposerId> for [u8; 16] { + fn from(value: ComposerId) -> Self { + *value.as_bytes() + } +} + +/// Revision representable by the actual shared SQLite owner, with checked increment. +#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)] +#[serde(try_from = "u64", into = "u64")] +pub struct ComposerRevision(AuthoredDraftRevision); + +impl ComposerRevision { + pub const INITIAL: Self = Self(AuthoredDraftRevision::INITIAL); + pub const MAX: u64 = i64::MAX as u64; + + pub fn new(value: u64) -> Result<Self, ComposerError> { + if value > Self::MAX { + return Err(ComposerError::InvalidRevision); + } + AuthoredDraftRevision::new(value) + .map(Self) + .map_err(|_| ComposerError::InvalidRevision) + } + + pub const fn get(self) -> u64 { + self.0.get() + } + + pub fn next(self) -> Result<Self, ComposerError> { + self.0 + .next() + .map_err(|_| ComposerError::InvalidRevision) + .and_then(|value| Self::new(value.get())) + } +} + +impl TryFrom<u64> for ComposerRevision { + type Error = ComposerError; + fn try_from(value: u64) -> Result<Self, Self::Error> { + Self::new(value) + } +} + +impl From<ComposerRevision> for u64 { + fn from(value: ComposerRevision) -> Self { + value.get() + } +} + +/// Client editing order, independent of durable revision and transient generation. +#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)] +#[serde(try_from = "u64", into = "u64")] +pub struct ComposerEditSequence(core::num::NonZeroU64); + +impl ComposerEditSequence { + pub const INITIAL: Self = Self(core::num::NonZeroU64::MIN); + + pub fn new(value: u64) -> Result<Self, ComposerError> { + core::num::NonZeroU64::new(value) + .map(Self) + .ok_or(ComposerError::InvalidEditSequence) + } + + pub const fn get(self) -> u64 { + self.0.get() + } + + pub fn next(self) -> Result<Self, ComposerError> { + self.get() + .checked_add(1) + .ok_or(ComposerError::InvalidEditSequence) + .and_then(Self::new) + } +} + +impl TryFrom<u64> for ComposerEditSequence { + type Error = ComposerError; + fn try_from(value: u64) -> Result<Self, Self::Error> { + Self::new(value) + } +} + +impl From<ComposerEditSequence> for u64 { + fn from(value: ComposerEditSequence) -> Self { + value.get() + } +} + +/// Stable local account/context. Session generations and mutable relay lists are absent. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ComposerScope { + author: PublicKey, + local_network: LocalNetworkId, +} + +impl ComposerScope { + pub fn new(author: PublicKey, local_network: LocalNetworkId) -> Self { + Self { + author, + local_network, + } + } + + pub const fn author(&self) -> PublicKey { + self.author + } + pub fn local_network(&self) -> &LocalNetworkId { + &self.local_network + } +} diff --git a/core/crates/tera_core/src/runtime/product_surface/composer/tests.rs b/core/crates/tera_core/src/runtime/product_surface/composer/tests.rs @@ -0,0 +1,302 @@ +use super::*; +use crate::runtime::product_surface::{ + AddCommandType, CANONICAL_ADD_COMMAND_TYPES, CreateAsk, CreatePhotoUpdate, CreateUpdate, + LocalNetworkId, Phase1DraftEventTiming, +}; +use radroots_identity::PublicKey; + +fn scope() -> ComposerScope { + ComposerScope::new( + PublicKey::from_hex("79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798") + .unwrap(), + LocalNetworkId::new("nearby".into()).unwrap(), + ) +} + +fn media() -> ComposerMediaInput { + ComposerMediaInput { + opaque_reference: "media:editing-fixture".into(), + sha256: "a".repeat(64), + media_type: "image/png".into(), + byte_size: 512, + width: 4, + height: 4, + alt: String::new(), + prepared_at_unix_s: 1_800_000_000, + } +} + +#[test] +fn all_five_families_accept_empty_and_incomplete_local_editing() { + for family in CANONICAL_ADD_COMMAND_TYPES { + let input = ComposerFormInput::empty(family); + let empty = ComposerPartialForm::new(input.clone()).unwrap(); + assert_eq!(empty.input(), &input); + let mut partial = input; + partial.price_amount = Some("-".into()); + partial.quantity = Some("1.".into()); + partial.currency = Some("C".into()); + partial.event_start_date = Some("2026-09-".into()); + partial.event_end_date = Some("2".into()); + partial.event_timezone = Some("America/".into()); + partial.media.push(media()); + let form = ComposerPartialForm::new(partial.clone()).unwrap(); + assert_eq!(form.input(), &partial); + assert_eq!( + ComposerPartialForm::from_json(&form.to_json().unwrap()).unwrap(), + form + ); + } +} + +#[test] +fn raw_editing_bytes_and_inactive_fields_survive_round_trip_exactly() { + let mut input = ComposerFormInput::empty(AddCommandType::CreateEvent); + input.content = " café\n\tunfinished\r\n".into(); + input.summary = Some("\n".into()); + input.price_amount = Some("01,20.".into()); + input.quantity = Some(" 2 ".into()); + input.event_timing = Some(Phase1DraftEventTiming::AllDay); + input.event_start_date = Some("2026-02-3".into()); + input.event_start_unix_s = Some(0); + input.event_end_unix_s = Some(u64::MAX); + input.food_published_at_unix_s = Some(u64::MAX); + input.media.push(media()); + input.media[0].alt = " unfinishe\n".into(); + let form = ComposerPartialForm::new(input.clone()).unwrap(); + let encoded = form.to_json().unwrap(); + let restored = ComposerPartialForm::from_json(&encoded).unwrap(); + assert_eq!(restored.input(), &input); + assert_eq!(restored.to_json().unwrap(), encoded); +} + +#[test] +fn exact_maxima_and_worst_case_json_escaping_stay_within_the_aggregate_bound() { + let mut input = ComposerFormInput::empty(AddCommandType::CreatePhotoUpdate); + input.content = "\0".repeat(COMPOSER_CONTENT_MAX_BYTES); + for field in [ + &mut input.identifier, + &mut input.title, + &mut input.summary, + &mut input.location, + &mut input.event_start_date, + &mut input.event_end_date, + &mut input.event_timezone, + &mut input.price_amount, + &mut input.currency, + &mut input.unit, + &mut input.quantity, + &mut input.food_status, + ] { + *field = Some("\0".repeat(COMPOSER_TEXT_MAX_BYTES)); + } + let mut reference = media(); + reference.opaque_reference = "media:".to_owned() + &"a".repeat(250); + reference.alt = "\0".repeat(COMPOSER_TEXT_MAX_BYTES); + input.media = vec![reference; COMPOSER_MEDIA_MAX]; + let form = ComposerPartialForm::new(input).unwrap(); + let bytes = form.to_json().unwrap(); + assert!(bytes.len() <= COMPOSER_FORM_MAX_BYTES); + assert_eq!(ComposerPartialForm::from_json(&bytes).unwrap(), form); +} + +#[test] +fn utf8_byte_limits_and_excess_media_reject_before_retaining_a_form() { + let mut input = ComposerFormInput::empty(AddCommandType::CreateUpdate); + input.content = "é".repeat(COMPOSER_CONTENT_MAX_BYTES / 2) + "a"; + assert!(ComposerPartialForm::new(input.clone()).is_ok()); + input.content.push('a'); + assert_eq!( + ComposerPartialForm::new(input), + Err(ComposerError::InvalidForm) + ); + let mut input = ComposerFormInput::empty(AddCommandType::CreateFoodAvailability); + input.quantity = Some("é".repeat(COMPOSER_TEXT_MAX_BYTES / 2)); + assert!(ComposerPartialForm::new(input.clone()).is_ok()); + input.quantity.as_mut().unwrap().push('a'); + assert!(ComposerPartialForm::new(input).is_err()); + let mut input = ComposerFormInput::empty(AddCommandType::CreatePhotoUpdate); + input.media = vec![media(); COMPOSER_MEDIA_MAX + 1]; + assert!(ComposerPartialForm::new(input).is_err()); +} + +#[test] +fn invalid_media_metadata_is_not_confused_with_missing_alt_text() { + let mut input = ComposerFormInput::empty(AddCommandType::CreatePhotoUpdate); + input.media.push(media()); + assert!(ComposerPartialForm::new(input.clone()).is_ok()); + let changes: &[fn(&mut ComposerMediaInput)] = &[ + |m| m.opaque_reference = "file:/private/image.png".into(), + |m| m.opaque_reference = "media:".into(), + |m| m.opaque_reference = "media:".to_owned() + &"a".repeat(251), + |m| m.opaque_reference = "media:UPPER".into(), + |m| m.sha256 = "g".repeat(64), + |m| m.media_type = "invalid media type".into(), + |m| m.media_type = "a".repeat(129), + |m| m.byte_size = 0, + |m| m.byte_size = 10 * 1024 * 1024 + 1, + |m| m.width = 0, + |m| m.height = u32::MAX, + |m| m.alt = "é".repeat(513), + |m| m.prepared_at_unix_s = 0, + |m| m.prepared_at_unix_s = u64::MAX, + ]; + for change in changes { + let mut invalid = input.clone(); + change(&mut invalid.media[0]); + assert_eq!( + ComposerPartialForm::new(invalid), + Err(ComposerError::InvalidForm) + ); + } +} + +#[test] +fn wire_decoding_cannot_bypass_bounds_or_add_unknown_fields() { + let mut input = ComposerFormInput::empty(AddCommandType::CreateUpdate); + input.content = "x".repeat(COMPOSER_CONTENT_MAX_BYTES + 1); + let invalid = serde_json::to_vec(&input).unwrap(); + assert!(serde_json::from_slice::<ComposerPartialForm>(&invalid).is_err()); + assert!(ComposerPartialForm::from_json(&invalid).is_err()); + assert!(ComposerPartialForm::from_json(&[]).is_err()); + assert!(ComposerPartialForm::from_json(b"{").is_err()); + assert!(ComposerPartialForm::from_json(&vec![b' '; COMPOSER_FORM_MAX_BYTES + 1]).is_err()); + let mut input = ComposerFormInput::empty(AddCommandType::CreatePhotoUpdate); + input.media.push(media()); + let value = serde_json::to_value(input).unwrap(); + for field in ["schema_version", "signing_key", "operation_id"] { + let mut unknown = value.clone(); + unknown[field] = serde_json::json!(1); + assert!(serde_json::from_value::<ComposerPartialForm>(unknown).is_err()); + } + let mut nested = value; + nested["media"][0]["authorization_header"] = serde_json::json!("fixture"); + assert!(serde_json::from_value::<ComposerPartialForm>(nested).is_err()); +} + +#[test] +fn persistent_identity_and_scope_use_validated_scalars_without_generation() { + let id = ComposerId::new([1; 16]).unwrap(); + assert_eq!(id.as_bytes(), &[1; 16]); + assert!(ComposerId::new([0; 16]).is_err()); + assert!(serde_json::from_value::<ComposerId>(serde_json::json!([0; 16].to_vec())).is_err()); + assert_eq!( + serde_json::from_value::<ComposerId>(serde_json::to_value(id).unwrap()).unwrap(), + id + ); + let selected = scope(); + let wire = serde_json::to_value(&selected).unwrap(); + assert_eq!( + serde_json::from_value::<ComposerScope>(wire.clone()).unwrap(), + selected + ); + assert_eq!(selected.local_network().as_str(), "nearby"); + assert_ne!( + selected, + ComposerScope::new( + selected.author(), + LocalNetworkId::new("other".into()).unwrap() + ) + ); + for (field, value) in [ + ("author", serde_json::json!("00".repeat(32))), + ("local_network", serde_json::json!(" nearby")), + ("generation", serde_json::json!(2)), + ] { + let mut invalid = wire.clone(); + invalid[field] = value; + assert!(serde_json::from_value::<ComposerScope>(invalid).is_err()); + } +} + +#[test] +fn revision_and_edit_sequence_are_distinct_checked_monotonic_values() { + assert_eq!(ComposerRevision::INITIAL.next().unwrap().get(), 2); + assert_eq!(ComposerEditSequence::INITIAL.next().unwrap().get(), 2); + for value in [0, ComposerRevision::MAX + 1, u64::MAX] { + assert!(ComposerRevision::new(value).is_err()); + assert!(serde_json::from_value::<ComposerRevision>(serde_json::json!(value)).is_err()); + } + assert!( + ComposerRevision::new(ComposerRevision::MAX) + .unwrap() + .next() + .is_err() + ); + assert!(ComposerEditSequence::new(0).is_err()); + assert!(serde_json::from_str::<ComposerEditSequence>("0").is_err()); + let max_edit = ComposerEditSequence::new(u64::MAX).unwrap(); + assert!(max_edit.next().is_err()); + assert_eq!( + serde_json::from_value::<ComposerEditSequence>(serde_json::to_value(max_edit).unwrap()) + .unwrap(), + max_edit + ); + let form = + ComposerPartialForm::new(ComposerFormInput::empty(AddCommandType::CreateAsk)).unwrap(); + let draft = ComposerDraft::new( + ComposerId::new([1; 16]).unwrap(), + ComposerRevision::INITIAL, + scope(), + max_edit, + form.clone(), + ); + assert_eq!(draft.revision(), ComposerRevision::INITIAL); + assert_eq!(draft.edit_sequence(), max_edit); + assert_eq!(draft.form(), &form); + assert_eq!(draft.scope(), &scope()); + assert_eq!(draft.id().as_bytes(), &[1; 16]); +} + +#[test] +fn incomplete_editing_does_not_weaken_strict_publication_constructors() { + use radroots_event::{ + calendar::{AuthoredCalendarTimeEvent, CalendarDate}, + food::availability::{FoodCurrency, FoodPrice, FoodUnit}, + }; + let mut input = ComposerFormInput::empty(AddCommandType::CreateFoodAvailability); + input.price_amount = Some("-".into()); + input.event_start_date = Some("2026-09-".into()); + let form = ComposerPartialForm::new(input).unwrap(); + assert!(CreateUpdate::new(&form.input().content).is_err()); + assert!(CreateAsk::new(&form.input().content, vec![]).is_err()); + assert!(CreatePhotoUpdate::new("unfinished photo", vec![]).is_err()); + assert!(CalendarDate::parse(form.input().event_start_date.as_deref().unwrap()).is_err()); + assert!(AuthoredCalendarTimeEvent::new("", "", 0).is_err()); + assert!( + FoodPrice::new( + form.input().price_amount.as_deref().unwrap(), + FoodCurrency::parse("CAD").unwrap(), + FoodUnit::Pound + ) + .is_err() + ); +} + +#[test] +fn debug_and_errors_do_not_expose_editing_content_or_local_references() { + let mut input = ComposerFormInput::empty(AddCommandType::CreatePhotoUpdate); + input.content = "PRIVATE_EDITING_MARKER".into(); + input.media.push(media()); + input.media[0].alt = "PRIVATE_ALT_MARKER".into(); + let form = ComposerPartialForm::new(input).unwrap(); + let draft = ComposerDraft::new( + ComposerId::new([1; 16]).unwrap(), + ComposerRevision::INITIAL, + scope(), + ComposerEditSequence::INITIAL, + form.clone(), + ); + for debug in [ + format!("{form:?}"), + format!("{draft:?}"), + format!("{:?}", form.input().media[0]), + ] { + assert!(!debug.contains("PRIVATE_")); + assert!(!debug.contains("media:editing-fixture")); + } + assert_eq!( + ComposerError::InvalidForm.to_string(), + "composer form exceeds its bounds or has invalid metadata" + ); +} diff --git a/release/provenance.json b/release/provenance.json @@ -2,7 +2,7 @@ "artifacts": { "app_api_sha256": "020924097c0d7efc33128cb8fd3d3b2026d95f57c44da71880e585aff80f070b", "ffi_api_sha256": "e2e025ee9a29182970f52f53593796afd154cdfb09aba197474577103dcb4ad6", - "ffi_provenance_sha256": "893a6c04c88868f70d9d911262cbb06c4f6a433e56a703388fb7344678a64530", + "ffi_provenance_sha256": "47abd7034be7df2e7f58cf6404dbcff14ce75599b38b999f24064f64b05965d5", "info_plist_sha256": "15ef08b1cdd1096cfb9eeaf5be5bf8f814807a7ca9350bbbb47860fa72ec13ef", "privacy_manifest_sha256": "a331d51864743ebe4e00dd22360b4a538b6b3ac26a6b3eb54094e60a36959a12", "sbom_sha256": "61e3d99a2c7f051a91d6a38b3b1f7974fb4c56da3a1c9758f5ad06fc66f1d856", @@ -22,7 +22,7 @@ "lib_revision": "363b9be18831be2fd6ef505d6154fb878ed8efd7", "source_date_epoch": 1787871027, "swift_package_lock_sha256": "94ae067a374726cdaf6b4ca0a5e44663c57fcdc5334060c5ffef5e79cfbf04c0", - "tera_ffi_source_tree": "84401a0c284d46897210f6ef8eee4da2732c8d41", + "tera_ffi_source_tree": "628534dfa9763f18b3ebba50dc25b1adb71801ba", "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 @@ -3971,6 +3971,10 @@ "count": 1 }, { + "path": "core/crates/tera_core/src/runtime/product_surface/composer/form.rs", + "count": 1 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/media.rs", "count": 1 }, @@ -4061,6 +4065,10 @@ "count": 2 }, { + "path": "core/crates/tera_core/src/runtime/product_surface/composer/tests.rs", + "count": 1 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/identity.rs", "count": 1 }, @@ -4227,6 +4235,14 @@ "count": 1 }, { + "path": "core/crates/tera_core/src/runtime/product_surface/composer/identity.rs", + "count": 1 + }, + { + "path": "core/crates/tera_core/src/runtime/product_surface/composer/tests.rs", + "count": 1 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/outbox.rs", "count": 1 }, @@ -4425,6 +4441,10 @@ "count": 8 }, { + "path": "core/crates/tera_core/src/runtime/product_surface/composer/form.rs", + "count": 1 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/outbox.rs", "count": 43 }, @@ -4557,6 +4577,10 @@ "count": 1 }, { + "path": "core/crates/tera_core/src/runtime/product_surface/composer/identity.rs", + "count": 1 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/outbox.rs", "count": 5 },