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:
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
},