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 601aaa13d10c5486716b2a42e1301a1953ff5a65
parent 47a7ee7dfe288847982c54d6468df8bd02238bd4
Author: triesap <tyson@radroots.org>
Date:   Mon, 21 Sep 2026 23:05:00 +0000

availability: preserve exact shared announcement semantics

- Translate complete locale decimal entries before capture
- Preserve raw partial drafts and immutable retry inputs
- Verify shared price, quantity, unit and sold-state semantics
- Qualify exact native artifacts and distinct announcements

Diffstat:
MPackage.swift | 2++
MREADME.md | 11+++++++++++
MTera.xcodeproj/project.pbxproj | 4++++
ATera/Runtime/TeraFoodDecimalEntry.swift | 40++++++++++++++++++++++++++++++++++++++++
ATera/State/TeraAddSelectionActions.swift | 11+++++++++++
MTera/State/TeraAddStore.swift | 22++++++++++------------
MTeraFFI/provenance.json | 26+++++++++++++-------------
MTeraFFI/source.lock | 4++--
MTeraFFI/source/aarch64-apple-darwin.json | 20+++++++++++++-------
MTeraFFI/source/aarch64-apple-ios-sim.json | 20+++++++++++++-------
MTeraFFI/source/aarch64-apple-ios.json | 20+++++++++++++-------
ATeraTests/TeraFoodDecimalEntryTests.swift | 136+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/crates/tera_core/src/runtime/product_surface/submission/capture/food_tests.rs | 221+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcore/crates/tera_core/src/runtime/product_surface/submission/capture/tests.rs | 3+++
Mproject.yml | 1+
Mrelease/provenance.json | 6+++---
Mtest-fixtures/legacy-identifiers.v1.json | 30++++++++++++++++++++++++++++++
Mtest-fixtures/swiftlint-maintainability-baseline.v1.json | 2+-
18 files changed, 527 insertions(+), 52 deletions(-)

diff --git a/Package.swift b/Package.swift @@ -89,6 +89,7 @@ let package = Package( "Runtime/TeraCalendarPresentation.swift", "Runtime/TeraCalendarEditing.swift", "Runtime/TeraCivilDateInput.swift", + "Runtime/TeraFoodDecimalEntry.swift", "Runtime/TeraCivilDateFormat.swift", "Runtime/TeraCheckedTime.swift", "Runtime/TeraComposerModels.swift", @@ -145,6 +146,7 @@ let package = Package( "State/TeraSubmissionStore.swift", "State/TeraSubmissionStopControl.swift", "State/TeraAddStore.swift", + "State/TeraAddSelectionActions.swift", "State/TeraAddAffordances.swift", "State/TeraRevisionPreparation.swift", "State/TeraAddCardIntents.swift", diff --git a/README.md b/README.md @@ -299,6 +299,17 @@ policy; inspecting or acting on a revision does not replace composer editing. Meeting an Any relay policy does not imply completion at every saved destination, and even a fully accepted retraction remains a request, not proof of erasure. +Food Availability remains a public announcement. Shared constructors validate +exact decimal strings, matching quantity/price units, currency shape and +active/sold status. A three-letter currency is not a verified financial asset; +bags and bunches carry no inferred conversion. The native form preserves partial +draft text. A new Submit or revision capture translates complete locale decimal entries into exact +canonical strings before capturing the saved revision, without floating-point +conversion. Canonical dots remain valid after a locale change; grouping, mixed +separators, signs, exponents and incomplete entries are not guessed. Translation +does not alter an existing submission or retry. A new intentional announcement +retains its own identifier and operation even when its visible details match. + Retraction requires the exact signed original in the current account's store. Rust verifies its signature, author, event kind, address and product card identity before capture and again before signing, local admission and delivery. A native diff --git a/Tera.xcodeproj/project.pbxproj b/Tera.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 08A44543B171791AF858C517 /* TeraAddSignerFixture.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1CC48A95774B931BE8D1E4A /* TeraAddSignerFixture.swift */; }; 093A6AC6C9B724E465507004 /* TeraPublicationEvidenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B954B7006CC5904C98138C6 /* TeraPublicationEvidenceTests.swift */; }; 095FBECA7268AA594F614EFA /* TeraScopeBackend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ED6D3E35C3235B6068E1902 /* TeraScopeBackend.swift */; }; + 116EF45DA06343BA3FA697AD /* TeraFoodDecimalEntryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 547AD45DC8C58FC4AA0D073D /* TeraFoodDecimalEntryTests.swift */; }; 1381A92874CF80925EEC06F5 /* TeraComposerMediaOwnershipTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE713B98919CB7692E3104A9 /* TeraComposerMediaOwnershipTests.swift */; }; 141327BC6BAE2CFA2C62C584 /* TeraTodayReconciliationTaskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EB21C33D6173E4FED05FDA0 /* TeraTodayReconciliationTaskTests.swift */; }; 17805D95512D7144DAC59D85 /* TeraTodayRenderingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7283D1C8F6E90A236BC84AF6 /* TeraTodayRenderingTests.swift */; }; @@ -189,6 +190,7 @@ 5238B5F236C392C82CEF9838 /* TeraRootShellUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraRootShellUITests.swift; sourceTree = "<group>"; }; 53CB51FEF4CE772320B3264C /* TeraTodayCacheFirstTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraTodayCacheFirstTests.swift; sourceTree = "<group>"; }; 545A0222D8C5B735130C8A12 /* TeraSubmissionAdmissionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraSubmissionAdmissionTests.swift; sourceTree = "<group>"; }; + 547AD45DC8C58FC4AA0D073D /* TeraFoodDecimalEntryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraFoodDecimalEntryTests.swift; sourceTree = "<group>"; }; 56A65AF934F7D75487094C79 /* TeraComposerSubmissionCaptureTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraComposerSubmissionCaptureTests.swift; sourceTree = "<group>"; }; 56F7B42264CD896F76189F33 /* TeraPublicationRetryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraPublicationRetryTests.swift; sourceTree = "<group>"; }; 57CA176D0A7566A4ADD2CEA4 /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; }; @@ -373,6 +375,7 @@ B310FD8CCA9F9C2DA19BE8CF /* TeraEditingReplacementFFITests.swift */, 0F97ECE43BB7028B5A497720 /* TeraEditingReplacementTests.swift */, 93A3358AFC23C567DB9CB02F /* TeraErrorRecoveryTests.swift */, + 547AD45DC8C58FC4AA0D073D /* TeraFoodDecimalEntryTests.swift */, 99681A076A08E385942344F7 /* TeraForegroundSubmissionTests.swift */, 217E44A3221756EC904702D6 /* TeraLateSigningTests.swift */, C890BDE885536BC2AB215393 /* TeraLifecycleTests.swift */, @@ -690,6 +693,7 @@ 888B178679E012BB053FE31B /* TeraEditingReplacementFFITests.swift in Sources */, 7B8C7C130DC15AF0266F2544 /* TeraEditingReplacementTests.swift in Sources */, D918A664D9A4B9859A1A6503 /* TeraErrorRecoveryTests.swift in Sources */, + 116EF45DA06343BA3FA697AD /* TeraFoodDecimalEntryTests.swift in Sources */, 9A7E808CB591126300D3A118 /* TeraForegroundSubmissionTests.swift in Sources */, F13090A3350CAE9CA3FFF3F3 /* TeraLateSigningTests.swift in Sources */, 671EF50DB4EFBC25AABF26DC /* TeraLifecycleTests.swift in Sources */, diff --git a/Tera/Runtime/TeraFoodDecimalEntry.swift b/Tera/Runtime/TeraFoodDecimalEntry.swift @@ -0,0 +1,40 @@ +import Foundation + +/// Native entry translation only. Shared Rust constructors still validate the +/// canonical amount, digit budget, price/quantity rules, currency and unit. +enum TeraFoodDecimalEntry { + static func canonicalOrRaw(_ raw: String?, locale: Locale) -> String? { + guard let raw, !raw.isEmpty else { return raw } + let separator = locale.decimalSeparator ?? "." + // A canonical dot remains usable after a draft moves between locales. + guard separator == "." || !raw.contains(separator) || !raw.contains(".") else { return raw } + let text = raw.replacingOccurrences(of: separator, with: ".") + let parts = text.split(separator: ".", omittingEmptySubsequences: false) + guard parts.count <= 2, let first = parts.first, !first.isEmpty, + parts.allSatisfy({ !$0.isEmpty }) else { return raw } + var digits: [String] = [] + for part in parts { + var translated = "" + for character in part { + guard character.unicodeScalars.count == 1, + character.unicodeScalars.first?.properties.generalCategory == .decimalNumber, + let value = character.wholeNumberValue else { return raw } + translated += String(value) + } + digits.append(translated) + } + let integer = String(digits[0].drop(while: { $0 == "0" })) + let whole = integer.isEmpty ? "0" : integer + guard digits.count == 2 else { return whole } + let fraction = String(digits[1].reversed().drop(while: { $0 == "0" }).reversed()) + return fraction.isEmpty ? whole : whole + "." + fraction + } + + static func form(_ input: TeraAddForm, locale: Locale) -> TeraAddForm { + guard input.commandType == .createFoodAvailability else { return input } + var result = input + result.priceAmount = canonicalOrRaw(input.priceAmount, locale: locale) + result.quantity = canonicalOrRaw(input.quantity, locale: locale) + return result + } +} diff --git a/Tera/State/TeraAddSelectionActions.swift b/Tera/State/TeraAddSelectionActions.swift @@ -0,0 +1,11 @@ +extension TeraAddStore { + func retry(_ draft: TeraDraftStatus? = nil) async { + guard let draft = draft ?? activeDraft else { return } + await retry(id: draft.id) + } + + func cancel(_ draft: TeraDraftStatus? = nil) async { + guard let draft = draft ?? activeDraft, draft.state.canCancel else { return } + await cancel(id: draft.id) + } +} diff --git a/Tera/State/TeraAddStore.swift b/Tera/State/TeraAddStore.swift @@ -235,7 +235,10 @@ final class TeraAddStore: ObservableObject { form.media[index].alt = alt } - func save() async { + func save(locale: Locale = .current) async { + if canSave, !protection.isWorking, revisionTarget != nil, isFormEditable { + form = TeraFoodDecimalEntry.form(form, locale: locale) + } await perform { requestedGeneration in if self.revisionTarget != nil { _ = try await self.saveCurrentForm(generation: requestedGeneration) @@ -248,7 +251,12 @@ final class TeraAddStore: ObservableObject { } } - func submit() async { + func submit(locale: Locale = .current) async { + // Only a new editable request may translate entry text. Existing capture, + // retry, revision operation and signed bytes retain their original inputs. + if canSubmit, !protection.isWorking, isFormEditable, !submissions.hasAction, activeDraft?.isRevision != true { + form = TeraFoodDecimalEntry.form(form, locale: locale) + } if activeDraft == nil, revisionTarget == nil { guard canSubmit else { return } await submissions.submit(form: form) @@ -274,11 +282,6 @@ final class TeraAddStore: ObservableObject { } } - func retry(_ draft: TeraDraftStatus? = nil) async { - guard let draft = draft ?? activeDraft else { return } - await retry(id: draft.id) - } - func retry(id: String) async { await replaceEditing { store, requestedGeneration in var current = try await store.runtimeClient.draftStatus(id: id) @@ -322,11 +325,6 @@ final class TeraAddStore: ObservableObject { } } - func cancel(_ draft: TeraDraftStatus? = nil) async { - guard let draft = draft ?? activeDraft, draft.state.canCancel else { return } - await cancel(id: draft.id) - } - func cancel(id: String) async { await replaceEditing { store, requestedGeneration in let current = try await store.runtimeClient.draftStatus(id: id) diff --git a/TeraFFI/provenance.json b/TeraFFI/provenance.json @@ -92,19 +92,19 @@ "sha256": "192ea19311867527593d7a757e115e45f315d1475822ec6d52c80cdba946671c" }, { - "bytes": 124608, + "bytes": 124909, "path": "source/aarch64-apple-darwin.json", - "sha256": "203767e8e334c6648618917ff2f6468dd285711cbdf1bf59ea1c7aa573d27357" + "sha256": "c2e7fe0b741d080a7d0bc1884fc117dac1685611dfbbfb9779d0c6c6c2ec1dbe" }, { - "bytes": 124452, + "bytes": 124753, "path": "source/aarch64-apple-ios-sim.json", - "sha256": "06a5e96fdac823083eedb2341c46b4f84de3e66fc80da395789d0176b8340f2b" + "sha256": "587570f13b5e72cc6ff06707ea0c6b65d775abef3f2d7bc5b252f93c006f57cf" }, { - "bytes": 124448, + "bytes": 124749, "path": "source/aarch64-apple-ios.json", - "sha256": "4ec8cc644372b9716ecf75f1c16843dcb704788e4ba827df78288aa61c726e70" + "sha256": "1e1affc3fdb43ea9846ade6bb30f0d93906d0a5aa2da4981a6e6eea10c384f89" } ], "language": "swift", @@ -112,7 +112,7 @@ "schema": "radroots.artifact-manifest.v2", "source": { "repository": "https://github.com/radrootslabs/tera", - "tree": "555ee9dfbfd65fcdb930d41fdb4f3145b2f2f431" + "tree": "9967f6d127127390a09bf27fba74d83965af0c6e" }, "source_records": { "aarch64-apple-darwin": "source/aarch64-apple-darwin.json", @@ -174,19 +174,19 @@ "sha256": "81a943ef98405676b4f65932a4a8d4bdb4011923f65fa2e3496fcb58e1d68be8" }, { - "bytes": 124608, + "bytes": 124909, "path": "TeraFFI/source/aarch64-apple-darwin.json", - "sha256": "203767e8e334c6648618917ff2f6468dd285711cbdf1bf59ea1c7aa573d27357" + "sha256": "c2e7fe0b741d080a7d0bc1884fc117dac1685611dfbbfb9779d0c6c6c2ec1dbe" }, { - "bytes": 124452, + "bytes": 124753, "path": "TeraFFI/source/aarch64-apple-ios-sim.json", - "sha256": "06a5e96fdac823083eedb2341c46b4f84de3e66fc80da395789d0176b8340f2b" + "sha256": "587570f13b5e72cc6ff06707ea0c6b65d775abef3f2d7bc5b252f93c006f57cf" }, { - "bytes": 124448, + "bytes": 124749, "path": "TeraFFI/source/aarch64-apple-ios.json", - "sha256": "4ec8cc644372b9716ecf75f1c16843dcb704788e4ba827df78288aa61c726e70" + "sha256": "1e1affc3fdb43ea9846ade6bb30f0d93906d0a5aa2da4981a6e6eea10c384f89" } ], "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 = "555ee9dfbfd65fcdb930d41fdb4f3145b2f2f431" -manifest_sha256 = "04d36e07e071305a5a665f64b836565fb58a4a4bf8e018087881f74badb2227d" +source_tree = "9967f6d127127390a09bf27fba74d83965af0c6e" +manifest_sha256 = "6ae0f0683a383c1c8042ad24378efcd09d602318c436b62fb2e45807cb1f59ef" source_date_epoch = 1787871027 [foundation] diff --git a/TeraFFI/source/aarch64-apple-darwin.json b/TeraFFI/source/aarch64-apple-darwin.json @@ -1196,6 +1196,12 @@ "mode": "100644", "sha256": "b851cd2b84566f35e5d3116e40f744c2d373586a4ab7b601fd1aa95e0e6654af" }, + "core/crates/tera_core/src/runtime/product_surface/submission/capture/food_tests.rs": { + "bytes": 7984, + "git_blob": "b8ccafaa6d3155766eb8a114c7ae6eba85faef63", + "mode": "100644", + "sha256": "71abfd4669ca5df3dd264ae2880216bf75f6debdd72deef3b08e5f7e7b360208" + }, "core/crates/tera_core/src/runtime/product_surface/submission/capture/form.rs": { "bytes": 5358, "git_blob": "4fe474f78b22cbcf81a08b16665d82d91b45fcdc", @@ -1221,10 +1227,10 @@ "sha256": "6b7c433b52df37ea50499bf86a9046a8ae47bad6122dfa92712595b3e3d9fe1d" }, "core/crates/tera_core/src/runtime/product_surface/submission/capture/tests.rs": { - "bytes": 8518, - "git_blob": "472f9d0e22d0a320cbd9146c862e0bbba3580aa5", + "bytes": 8561, + "git_blob": "05b9a529303088bc0b3e903ea5eec3750958d36d", "mode": "100644", - "sha256": "6c7481eb4a17a3064ec7672d4a1971f811f98ef8111068b6c2496ff9bbc202e7" + "sha256": "76c7d5cb5f8bee41d6465c1d2d6765b3235a302cd26d456ab486a9e42aea6296" }, "core/crates/tera_core/src/runtime/product_surface/submission/commit.rs": { "bytes": 9360, @@ -2637,13 +2643,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 168574, - "git_blob": "4efe38933170b055e6e6a79f8fc719243f24ab5e", + "bytes": 169590, + "git_blob": "7ce5cb28f8c823f3874e8c9f417f8999f83854ec", "mode": "100644", - "sha256": "48c0cb55ae2695e065ac47ff290c84fed0913e017363764bb3fcd9106f88d396" + "sha256": "40a5be69ead4952c15ff4041339e3296e1dd8a775137dbfafb644d4699574403" } }, "policy": "staged_inputs", - "tree": "555ee9dfbfd65fcdb930d41fdb4f3145b2f2f431" + "tree": "9967f6d127127390a09bf27fba74d83965af0c6e" } } diff --git a/TeraFFI/source/aarch64-apple-ios-sim.json b/TeraFFI/source/aarch64-apple-ios-sim.json @@ -1192,6 +1192,12 @@ "mode": "100644", "sha256": "b851cd2b84566f35e5d3116e40f744c2d373586a4ab7b601fd1aa95e0e6654af" }, + "core/crates/tera_core/src/runtime/product_surface/submission/capture/food_tests.rs": { + "bytes": 7984, + "git_blob": "b8ccafaa6d3155766eb8a114c7ae6eba85faef63", + "mode": "100644", + "sha256": "71abfd4669ca5df3dd264ae2880216bf75f6debdd72deef3b08e5f7e7b360208" + }, "core/crates/tera_core/src/runtime/product_surface/submission/capture/form.rs": { "bytes": 5358, "git_blob": "4fe474f78b22cbcf81a08b16665d82d91b45fcdc", @@ -1217,10 +1223,10 @@ "sha256": "6b7c433b52df37ea50499bf86a9046a8ae47bad6122dfa92712595b3e3d9fe1d" }, "core/crates/tera_core/src/runtime/product_surface/submission/capture/tests.rs": { - "bytes": 8518, - "git_blob": "472f9d0e22d0a320cbd9146c862e0bbba3580aa5", + "bytes": 8561, + "git_blob": "05b9a529303088bc0b3e903ea5eec3750958d36d", "mode": "100644", - "sha256": "6c7481eb4a17a3064ec7672d4a1971f811f98ef8111068b6c2496ff9bbc202e7" + "sha256": "76c7d5cb5f8bee41d6465c1d2d6765b3235a302cd26d456ab486a9e42aea6296" }, "core/crates/tera_core/src/runtime/product_surface/submission/commit.rs": { "bytes": 9360, @@ -2633,13 +2639,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 168574, - "git_blob": "4efe38933170b055e6e6a79f8fc719243f24ab5e", + "bytes": 169590, + "git_blob": "7ce5cb28f8c823f3874e8c9f417f8999f83854ec", "mode": "100644", - "sha256": "48c0cb55ae2695e065ac47ff290c84fed0913e017363764bb3fcd9106f88d396" + "sha256": "40a5be69ead4952c15ff4041339e3296e1dd8a775137dbfafb644d4699574403" } }, "policy": "staged_inputs", - "tree": "555ee9dfbfd65fcdb930d41fdb4f3145b2f2f431" + "tree": "9967f6d127127390a09bf27fba74d83965af0c6e" } } diff --git a/TeraFFI/source/aarch64-apple-ios.json b/TeraFFI/source/aarch64-apple-ios.json @@ -1192,6 +1192,12 @@ "mode": "100644", "sha256": "b851cd2b84566f35e5d3116e40f744c2d373586a4ab7b601fd1aa95e0e6654af" }, + "core/crates/tera_core/src/runtime/product_surface/submission/capture/food_tests.rs": { + "bytes": 7984, + "git_blob": "b8ccafaa6d3155766eb8a114c7ae6eba85faef63", + "mode": "100644", + "sha256": "71abfd4669ca5df3dd264ae2880216bf75f6debdd72deef3b08e5f7e7b360208" + }, "core/crates/tera_core/src/runtime/product_surface/submission/capture/form.rs": { "bytes": 5358, "git_blob": "4fe474f78b22cbcf81a08b16665d82d91b45fcdc", @@ -1217,10 +1223,10 @@ "sha256": "6b7c433b52df37ea50499bf86a9046a8ae47bad6122dfa92712595b3e3d9fe1d" }, "core/crates/tera_core/src/runtime/product_surface/submission/capture/tests.rs": { - "bytes": 8518, - "git_blob": "472f9d0e22d0a320cbd9146c862e0bbba3580aa5", + "bytes": 8561, + "git_blob": "05b9a529303088bc0b3e903ea5eec3750958d36d", "mode": "100644", - "sha256": "6c7481eb4a17a3064ec7672d4a1971f811f98ef8111068b6c2496ff9bbc202e7" + "sha256": "76c7d5cb5f8bee41d6465c1d2d6765b3235a302cd26d456ab486a9e42aea6296" }, "core/crates/tera_core/src/runtime/product_surface/submission/commit.rs": { "bytes": 9360, @@ -2633,13 +2639,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 168574, - "git_blob": "4efe38933170b055e6e6a79f8fc719243f24ab5e", + "bytes": 169590, + "git_blob": "7ce5cb28f8c823f3874e8c9f417f8999f83854ec", "mode": "100644", - "sha256": "48c0cb55ae2695e065ac47ff290c84fed0913e017363764bb3fcd9106f88d396" + "sha256": "40a5be69ead4952c15ff4041339e3296e1dd8a775137dbfafb644d4699574403" } }, "policy": "staged_inputs", - "tree": "555ee9dfbfd65fcdb930d41fdb4f3145b2f2f431" + "tree": "9967f6d127127390a09bf27fba74d83965af0c6e" } } diff --git a/TeraTests/TeraFoodDecimalEntryTests.swift b/TeraTests/TeraFoodDecimalEntryTests.swift @@ -0,0 +1,136 @@ +import Foundation +@testable import TeraApp +import XCTest + +final class TeraFoodDecimalEntryTests: XCTestCase { + func testExactLocaleTranslationNeverRoundsOrInfersUnits() { + for (locale, raw, expected) in [ + ("fr_FR", "0004,500", "4.5"), ("de_DE", "0,000000000000000000000000001", "0.000000000000000000000000001"), + ("en_US", "12345678901234567890.12345678", "12345678901234567890.12345678"), + ("en_US", "000.000", "0"), ("ar_EG", "٤٫٥٠", "4.5"), ("fa_IR", "۱۲٫۵۰", "12.5"), + ("fr_FR", "4.5", "4.5"), ("en_US", "9.9900", "9.99"), + ] { + XCTAssertEqual(TeraFoodDecimalEntry.canonicalOrRaw(raw, locale: Locale(identifier: locale)), expected, raw) + } + } + + func testIncompleteOrAmbiguousEntriesStayUnchangedForSharedValidation() { + for raw in ["", "-", "1,", ",5", "1.000,5", "1,000.5", "1,2,3", "1 000", "1\u{202F}000", "1e3", "+1", " 1 ", "⅕", "²"] { + XCTAssertEqual(TeraFoodDecimalEntry.canonicalOrRaw(raw, locale: Locale(identifier: "fr_FR")), raw) + } + XCTAssertNil(TeraFoodDecimalEntry.canonicalOrRaw(nil, locale: Locale(identifier: "en_US"))) + XCTAssertEqual(TeraFoodDecimalEntry.canonicalOrRaw("1,5", locale: Locale(identifier: "en_US")), "1,5") + } + + func testOnlyFoodAmountsAreTranslatedAndContractFieldsArePreserved() { + var form = TeraAddForm.empty(.createFoodAvailability) + form.priceAmount = "4,50" + form.quantity = "0002,00" + form.unit = "bag" + form.currency = "ZZZ" + form.foodStatus = "sold" + var expected = form + expected.priceAmount = "4.5" + expected.quantity = "2" + XCTAssertEqual(TeraFoodDecimalEntry.form(form, locale: Locale(identifier: "fr_FR")), expected) + form.commandType = .createUpdate + XCTAssertEqual(TeraFoodDecimalEntry.form(form, locale: Locale(identifier: "fr_FR")), form) + } +} + +extension TeraAddStoreTests { + @MainActor + func testAutomaticRevisionPreservationKeepsRawEntryAndItsRetryInterlock() async throws { + let backend = AddBackend() + let client = try await Self.startedClient(backend) + let store = TeraAddStore(runtimeClient: client, initialType: .createFoodAvailability) + await store.configure(snapshot: backend.snapshot()) + await store.start() + configure(store, type: .createFoodAvailability) + let original = try await backend.saveAddIntent(input: TeraAddRuntimeInput(form: store.form, media: []), + existingDraftID: nil, expectedRevision: nil) + await store.retractAndRevise(Self.card(localOperationID: original.id)) + store.updateForm(\.priceAmount, "4.50") + await backend.failNextRevisionReceipt() + store.newDraft(type: .createUpdate) + await TeraScopeFixtures.eventually { !store.protection.isWorking } + XCTAssertTrue(store.protection.failed) + XCTAssertTrue(store.revisionPreparation.isCaptured) + XCTAssertEqual(store.form.priceAmount, "4.50") + store.protection.retry() + await TeraScopeFixtures.eventually { !store.protection.isWorking } + XCTAssertFalse(store.protection.failed) + XCTAssertEqual(store.form.commandType, .createUpdate) + let retained = await backend.values + XCTAssertEqual(retained.values.first(where: \.isRevision)?.form?.priceAmount, "4.50") + let count = await backend.revisionPlanCount() + XCTAssertEqual(count, 1) + store.stop() + _ = try await client.stop() + } + + @MainActor + func testFoodRevisionSaveTranslatesOnceAndLostReceiptRetainsExactAmounts() async throws { + let backend = AddBackend() + let client = try await Self.startedClient(backend) + let store = TeraAddStore(runtimeClient: client, initialType: .createFoodAvailability) + await store.configure(snapshot: backend.snapshot()) + await store.start() + configure(store, type: .createFoodAvailability) + let original = try await backend.saveAddIntent(input: TeraAddRuntimeInput(form: store.form, media: []), + existingDraftID: nil, expectedRevision: nil) + await store.retractAndRevise(Self.card(localOperationID: original.id)) + store.updateForm(\.priceAmount, "4,50") + store.updateForm(\.quantity, "002,00") + await backend.failNextRevisionReceipt() + await store.save(locale: Locale(identifier: "fr_FR")) + XCTAssertTrue(store.revisionPreparation.isCaptured) + XCTAssertEqual(store.form.priceAmount, "4.5") + XCTAssertEqual(store.form.quantity, "2") + store.updateForm(\.priceAmount, "9,00") + await store.save(locale: Locale(identifier: "en_US")) + XCTAssertTrue(store.activeDraft?.isRevision == true) + XCTAssertEqual(store.activeDraft?.form?.priceAmount, "4.5") + XCTAssertEqual(store.activeDraft?.form?.quantity, "2") + let count = await backend.revisionPlanCount() + XCTAssertEqual(count, 1) + store.stop() + _ = try await client.stop() + } + + @MainActor + func testFoodDraftStaysRawUntilNewSubmitAndRetryRetainsCapturedAmounts() async throws { + let backend = AddBackend() + let client = try await Self.startedClient(backend) + let store = TeraAddStore(runtimeClient: client, initialType: .createFoodAvailability) + await store.configure(snapshot: backend.snapshot()) + await store.start() + configure(store, type: .createFoodAvailability) + store.updateForm(\.priceAmount, "4,50") + store.updateForm(\.quantity, "002,00") + await store.save() + XCTAssertEqual(store.savedComposer?.form.priceAmount, "4,50") + XCTAssertEqual(store.form.quantity, "002,00") + await store.submit(locale: Locale(identifier: "fr_FR")) + let captured = try XCTUnwrap(store.submissions.status?.captured) + XCTAssertEqual(captured.form.priceAmount, "4.5") + XCTAssertEqual(captured.form.quantity, "2") + XCTAssertEqual(captured.form.editingValue, store.form) + store.updateForm(\.priceAmount, "9,00") + await store.submit(locale: Locale(identifier: "en_US")) + XCTAssertEqual(store.submissions.status?.captured, captured) + XCTAssertEqual(store.form.priceAmount, "9,00") + store.newDraft(type: .createFoodAvailability) + await TeraScopeFixtures.eventually { !store.protection.isWorking } + configure(store, type: .createFoodAvailability) + store.updateForm(\.priceAmount, "4,50") + store.updateForm(\.quantity, "002,00") + await store.submit(locale: Locale(identifier: "fr_FR")) + let next = try XCTUnwrap(store.submissions.status?.captured) + XCTAssertNotEqual(next.id, captured.id) + XCTAssertNotEqual(next.form.identifier, captured.form.identifier) + XCTAssertEqual(next.form.priceAmount, captured.form.priceAmount) + store.stop() + _ = try await client.stop() + } +} diff --git a/core/crates/tera_core/src/runtime/product_surface/submission/capture/food_tests.rs b/core/crates/tera_core/src/runtime/product_surface/submission/capture/food_tests.rs @@ -0,0 +1,221 @@ +use super::{reservation, *}; +use crate::runtime::product_surface::{ + CardLifecycleState, ContextAdmission, ContextRank, LocalNetworkAdmission, + ProductEventClassification, classify_admitted_event, +}; +use nostr::secp256k1::{Keypair, Message, SECP256K1}; +use radroots_event::{Event, envelope::EventEnvelopeParts, wire::compute_canonical_nip01_event_id}; +use radroots_event_codec::{ + admission::{RadrootsAdmittedEvent, admit_verified_event}, + authoring::AuthoredEventPlan, + verify::verify_nip01_event, +}; + +fn food() -> ComposerFormInput { + let mut value = input(AddCommandType::CreateFoodAvailability); + value.identifier = Some("carrots".into()); + value.title = Some("Carrots".into()); + value.summary = Some("Fresh".into()); + value.location = Some("Victoria".into()); + value.price_amount = Some("12345678901234567890.12345678".into()); + value.currency = Some("ZZZ".into()); // Shape is not a finance registry assertion. + value.unit = Some("bunch".into()); + value.quantity = Some("0.000000000000000000000000001".into()); + value.food_status = Some("sold".into()); + value +} + +fn capture(value: ComposerFormInput) -> Result<CapturedSubmission, SubmissionCaptureError> { + CapturedSubmission::capture( + reservation(value), + policy("wss://relay.example"), + None, + vec![], + ) +} + +fn signed( + plan: &AuthoredEventPlan, + tags: Vec<Vec<String>>, +) -> radroots_event_codec::verify::RadrootsSignatureVerifiedEvent { + let keys = nostr::Keys::parse(&format!("{:064x}", 1)).unwrap(); + let author = plan.author().to_hex(); + let id = compute_canonical_nip01_event_id( + &author, + plan.created_at(), + plan.body().kind(), + &tags, + plan.body().content(), + ) + .unwrap(); + let signature = SECP256K1.sign_schnorr_no_aux_rand( + &Message::from_digest(*id.as_bytes()), + &Keypair::from_secret_key(SECP256K1, keys.secret_key()), + ); + verify_nip01_event( + Event::new(EventEnvelopeParts { + id: id.to_hex(), + author, + created_at: plan.created_at(), + kind: plan.body().kind(), + tags, + content: plan.body().content().into(), + sig: signature.to_string(), + }) + .unwrap(), + ) + .unwrap() +} + +#[test] +fn food_exact_capture_shared_admission_and_sold_projection_round_trip() { + let input = food(); + let captured = capture(input.clone()).unwrap(); + assert_eq!(captured.reservation().captured().form().input(), &input); + let plan = captured.plan(); + let admitted = admit_verified_event(signed(plan, plan.body().tags().to_vec())).unwrap(); + let RadrootsAdmittedEvent::FoodAvailability(event) = &admitted else { + panic!("food contract required") + }; + assert_eq!( + event.projection().price().amount(), + input.price_amount.as_deref().unwrap() + ); + assert_eq!(event.projection().price().currency().as_str(), "ZZZ"); + assert_eq!( + event.projection().quantity().unwrap().amount(), + input.quantity.as_deref().unwrap() + ); + let ProductEventClassification::Card(card) = classify_admitted_event( + &admitted, + LocalNetworkAdmission::Included(ContextAdmission { + rank: ContextRank::MissingLocalityFallback, + reason: "locality_missing_fallback", + }), + ) else { + panic!("food card required") + }; + assert_eq!(card.price_amount, input.price_amount); + assert_eq!(card.quantity, input.quantity); + assert_eq!(card.price_currency.as_deref(), Some("ZZZ")); + assert_eq!(card.price_unit.as_deref(), Some("bunch")); + assert_eq!(card.food_status.as_deref(), Some("sold")); + assert_eq!(card.lifecycle, CardLifecycleState::Sold); +} + +#[test] +fn food_invalid_decimals_currency_unit_status_and_zero_quantity_fail_shared_capture() { + for amount in [ + "", + "01", + "1.0", + "1,5", + "1e2", + "-1", + "+1", + ".5", + "1.", + "12345678901234567890123456789", + ] { + let mut value = food(); + value.price_amount = Some(amount.into()); + assert!(capture(value).is_err(), "{amount}"); + } + for (field, invalid) in [ + ("quantity", "0"), + ("quantity", "1.00"), + ("currency", "cad"), + ("currency", "EURO"), + ("unit", "crate"), + ("status", "reserved"), + ] { + let mut value = food(); + match field { + "quantity" => value.quantity = Some(invalid.into()), + "currency" => value.currency = Some(invalid.into()), + "unit" => value.unit = Some(invalid.into()), + "status" => value.food_status = Some(invalid.into()), + _ => unreachable!(), + } + assert!(capture(value).is_err(), "{field}={invalid}"); + } + let mut free = food(); + free.price_amount = Some("0".into()); + free.quantity = None; + free.food_status = None; + let captured = capture(free).unwrap(); + let admitted = admit_verified_event(signed( + captured.plan(), + captured.plan().body().tags().to_vec(), + )) + .unwrap(); + let RadrootsAdmittedEvent::FoodAvailability(event) = admitted else { + panic!("food required") + }; + assert_eq!(event.projection().price().amount(), "0"); + assert!(event.projection().quantity().is_none()); + assert_eq!(event.projection().status().as_str(), "active"); +} + +#[test] +fn food_signed_unit_mismatch_is_rejected_before_typed_projection() { + let captured = capture(food()).unwrap(); + let mut tags = captured.plan().body().tags().to_vec(); + let quantity = tags + .iter_mut() + .find(|tag| tag.first().map(String::as_str) == Some("radroots:quantity")) + .unwrap(); + assert_eq!(quantity[2], "bunch"); + quantity[2] = "bag".into(); + assert!(admit_verified_event(signed(captured.plan(), tags)).is_err()); +} + +#[tokio::test] +async fn food_identical_announcements_keep_distinct_intent_and_coordinate_identity() { + use super::super::super::repository::SubmissionRepository; + use crate::runtime::product_surface::{ + ComposerId, ComposerRevision, SubmissionCommandId, SubmissionReservationRequest, + }; + let client = radroots_sdk::ClientBuilder::memory_default() + .build() + .unwrap(); + let store = client.storage().unwrap(); + let repo = SubmissionRepository { store }; + let mut receipts = Vec::new(); + let mut event_ids = Vec::new(); + for number in [1, 2] { + let request = SubmissionReservationRequest::new( + SubmissionCommandId::new([number; 16]).unwrap(), + scope(AUTHOR, "nearby"), + ComposerId::new([number; 16]).unwrap(), + ComposerRevision::INITIAL, + ); + let mut value = food(); + value.identifier = Some(format!("announcement-{number}")); + let source = ComposerStorageRecord::initial( + request.composer_id(), + request.scope().clone(), + ComposerEditSequence::INITIAL, + ComposerPartialForm::new(value).unwrap(), + NOW, + ) + .unwrap(); + store + .append_authored_draft(source.into_stored(), None) + .await + .unwrap(); + let reservation = repo.reserve(&request, Some(NOW)).await.unwrap(); + let captured = + CapturedSubmission::capture(reservation, policy("wss://relay.example"), None, vec![]) + .unwrap(); + event_ids.push(captured.plan().expected_event_id().to_hex()); + let receipt = repo.commit(&captured).await.unwrap(); + let replay = repo.commit(&captured).await.unwrap(); + assert_eq!(replay.operation_id(), receipt.operation_id()); + assert!(replay.is_replay()); + receipts.push(receipt); + } + assert_ne!(receipts[0].intent_id(), receipts[1].intent_id()); + assert_ne!(receipts[0].operation_id(), receipts[1].operation_id()); + assert_ne!(event_ids[0], event_ids[1]); +} 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 @@ -9,6 +9,9 @@ use radroots_sdk::transport::BlossomConfig; #[path = "runtime_tests.rs"] mod runtime_tests; +#[path = "food_tests.rs"] +mod food_tests; + fn reservation(input: ComposerFormInput) -> SubmissionReservationReceipt { let request = request(); let source = ComposerStorageRecord::initial( diff --git a/project.yml b/project.yml @@ -70,6 +70,7 @@ targets: - path: TeraTests/TeraPublicationRetryTests.swift - path: TeraTests/TeraCoordinateAdmissionTests.swift - path: TeraTests/TeraRevisionPreparationTests.swift + - path: TeraTests/TeraFoodDecimalEntryTests.swift - path: TeraTests/TeraRevisionDetailTests.swift - path: TeraTests/TeraRevisionTestBackend.swift - path: TeraTests/TeraStoppedUploadTests.swift diff --git a/release/provenance.json b/release/provenance.json @@ -2,11 +2,11 @@ "artifacts": { "app_api_sha256": "020924097c0d7efc33128cb8fd3d3b2026d95f57c44da71880e585aff80f070b", "ffi_api_sha256": "81a943ef98405676b4f65932a4a8d4bdb4011923f65fa2e3496fcb58e1d68be8", - "ffi_provenance_sha256": "04d36e07e071305a5a665f64b836565fb58a4a4bf8e018087881f74badb2227d", + "ffi_provenance_sha256": "6ae0f0683a383c1c8042ad24378efcd09d602318c436b62fb2e45807cb1f59ef", "info_plist_sha256": "15ef08b1cdd1096cfb9eeaf5be5bf8f814807a7ca9350bbbb47860fa72ec13ef", "privacy_manifest_sha256": "a331d51864743ebe4e00dd22360b4a538b6b3ac26a6b3eb54094e60a36959a12", "sbom_sha256": "bfc57ed1f259dad8dcff7ec061b0889942a27bfdc9f9f01ebb631e25b715267b", - "xcode_project_sha256": "6a4bc06482f0202bd55b6d054eff3094315cf1f3fbd4ac11ecaf8a7169af3578" + "xcode_project_sha256": "83c8c22a4f8eaf0f1f944073e67593af38c89fbdbb0bd27278ff1c128715c7fd" }, "disposition": "unsigned", "platforms": [ @@ -22,7 +22,7 @@ "lib_revision": "e6f296604363e5293d6abc6e18bf5eaea2177b9f", "source_date_epoch": 1787871027, "swift_package_lock_sha256": "a7e31d77d31ecbc1e0e5a78f2681e3fb0c6f367af0edf8b4ceef0af973a78681", - "tera_ffi_source_tree": "555ee9dfbfd65fcdb930d41fdb4f3145b2f2f431", + "tera_ffi_source_tree": "9967f6d127127390a09bf27fba74d83965af0c6e", "xcode_package_lock_sha256": "d98a614a38dce7b55c2925dee51c2d4bf8dcfc4e31779d0c5fe8561c45d6e3ba" }, "version": "0.1.0-alpha" diff --git a/test-fixtures/legacy-identifiers.v1.json b/test-fixtures/legacy-identifiers.v1.json @@ -147,6 +147,10 @@ "count": 19 }, { + "path": "core/crates/tera_core/src/runtime/product_surface/submission/capture/food_tests.rs", + "count": 3 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/today.rs", "count": 11 } @@ -2473,6 +2477,10 @@ "count": 7 }, { + "path": "core/crates/tera_core/src/runtime/product_surface/submission/capture/food_tests.rs", + "count": 1 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/today.rs", "count": 3 }, @@ -4927,6 +4935,10 @@ "count": 1 }, { + "path": "core/crates/tera_core/src/runtime/product_surface/submission/capture/food_tests.rs", + "count": 1 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/submission/capture/form.rs", "count": 2 }, @@ -5069,6 +5081,10 @@ "count": 1 }, { + "path": "core/crates/tera_core/src/runtime/product_surface/submission/capture/food_tests.rs", + "count": 2 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/submission/intent.rs", "count": 1 }, @@ -5527,6 +5543,10 @@ "count": 1 }, { + "path": "core/crates/tera_core/src/runtime/product_surface/submission/capture/food_tests.rs", + "count": 1 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/submission/capture/media.rs", "count": 1 }, @@ -6315,6 +6335,16 @@ "count": 2 } ] + }, + { + "identifier": "RadrootsSignatureVerifiedEvent", + "category": "shared_foundation_api", + "occurrences": [ + { + "path": "core/crates/tera_core/src/runtime/product_surface/submission/capture/food_tests.rs", + "count": 1 + } + ] } ] } diff --git a/test-fixtures/swiftlint-maintainability-baseline.v1.json b/test-fixtures/swiftlint-maintainability-baseline.v1.json @@ -1 +1 @@ -[{"violation":{"ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","ruleName":"Function Body Length","reason":"Initializer body should span 50 lines or less excluding comments and whitespace: currently spans 52 lines","location":{"line":21,"character":3,"file":"Tera/App/TeraAppModel.swift"},"severity":"error"},"text":" init("},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 373 lines","location":{"line":31,"character":7,"file":"Tera/Runtime/TeraGeneratedRuntimeBackend.swift"},"severity":"error"},"text":"final class TeraGeneratedRuntimeBackend: TeraRuntimeBackend, @unchecked Sendable {"},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length","reason":"File should contain 1000 lines or less: currently contains 1089","location":{"line":1089,"character":1,"file":"Tera/Runtime/TeraGeneratedRuntimeBackend.swift"},"severity":"error"},"text":"}"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 675 lines","location":{"line":3,"character":1,"file":"Tera/Runtime/TeraRuntimeClient.swift"},"severity":"error"},"text":"actor TeraRuntimeClient {"},{"violation":{"ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","ruleName":"Function Body Length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 70 lines","location":{"line":353,"character":3,"file":"Tera/Runtime/TeraRuntimeClient.swift"},"severity":"error"},"text":" func changes(bufferCapacity: Int = 16) async throws -> AsyncStream<TeraRuntimeChange> {"},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length","reason":"File should contain 400 lines or less: currently contains 749","location":{"line":749,"character":1,"file":"Tera/Runtime/TeraRuntimeClient.swift"},"severity":"error"},"text":"}"},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length","reason":"File should contain 1000 lines or less: currently contains 1085","location":{"line":1085,"character":1,"file":"Tera/Runtime/TeraRuntimeModels.swift"},"severity":"error"},"text":"}"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 493 lines","location":{"line":4,"character":7,"file":"Tera/State/TeraAddStore.swift"},"severity":"error"},"text":"final class TeraAddStore: ObservableObject {"},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length","reason":"File should contain 400 lines or less: currently contains 704","location":{"line":704,"character":1,"file":"Tera/State/TeraAddStore.swift"},"severity":"error"},"text":"}"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 468 lines","location":{"line":78,"character":1,"file":"Tera/State/TeraConfigurationStore.swift"},"severity":"error"},"text":"actor TeraConfigurationStore {"},{"violation":{"ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","ruleName":"Function Body Length","reason":"Function body should span 100 lines or less excluding comments and whitespace: currently spans 109 lines","location":{"line":148,"character":5,"file":"Tera/State/TeraConfigurationStore.swift"},"severity":"error"},"text":" func load() throws -> TeraAppConfiguration {"},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length","reason":"File should contain 400 lines or less: currently contains 759","location":{"line":759,"character":1,"file":"Tera/State/TeraConfigurationStore.swift"},"severity":"error"},"text":"}"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 390 lines","location":{"line":39,"character":1,"file":"Tera/State/TeraSessionStore.swift"},"severity":"error"},"text":"actor TeraSessionStore {"},{"violation":{"ruleDescription":"Complexity of function bodies should be limited.","ruleIdentifier":"cyclomatic_complexity","ruleName":"Cyclomatic Complexity","reason":"Function should have complexity 10 or less; currently complexity is 13","location":{"line":125,"character":11,"file":"Tera/State/TeraSessionStore.swift"},"severity":"error"},"text":" private func start(acceptingReconfiguration: Bool) async -> TeraSessionPhase {"},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length","reason":"File should contain 400 lines or less: currently contains 452","location":{"line":452,"character":1,"file":"Tera/State/TeraSessionStore.swift"},"severity":"error"},"text":"}"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Struct body should span 250 lines or less excluding comments and whitespace: currently spans 342 lines","location":{"line":4,"character":1,"file":"Tera/Views/TeraAddView.swift"},"severity":"error"},"text":"struct TeraAddView: View {"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Struct body should span 250 lines or less excluding comments and whitespace: currently spans 259 lines","location":{"line":365,"character":1,"file":"Tera/Views/TeraSupportingViews.swift"},"severity":"error"},"text":"struct TeraSettingsView: View {"},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length","reason":"File should contain 400 lines or less: currently contains 629","location":{"line":629,"character":1,"file":"Tera/Views/TeraSupportingViews.swift"},"severity":"error"},"text":"}"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 607 lines","location":{"line":6,"character":7,"file":"TeraTests/TeraAddStoreTests.swift"},"severity":"error"},"text":"final class TeraAddStoreTests: XCTestCase {"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 394 lines","location":{"line":682,"character":1,"file":"TeraTests/TeraAddStoreTests.swift"},"severity":"error"},"text":"actor AddBackend: TeraRuntimeBackend {"},{"violation":{"ruleDescription":"Number of function parameters should be low.","ruleIdentifier":"function_parameter_count","ruleName":"Function Parameter Count","reason":"Function should have 5 parameters or less: it currently has 8","location":{"line":1020,"character":3,"file":"TeraTests/TeraAddStoreTests.swift"},"severity":"error"},"text":" func makeStatus("},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length","reason":"File should contain 1000 lines or less: currently contains 1232","location":{"line":1232,"character":1,"file":"TeraTests/TeraAddStoreTests.swift"},"severity":"error"},"text":"}"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 307 lines","location":{"line":7,"character":7,"file":"TeraTests/TeraRemoteQualificationTests.swift"},"severity":"error"},"text":"final class TeraRemoteQualificationTests: XCTestCase {"},{"violation":{"ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","ruleName":"Function Body Length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 94 lines","location":{"line":45,"character":3,"file":"TeraTests/TeraRemoteQualificationTests.swift"},"severity":"error"},"text":" func testQualificationRejectsAmbiguousOrUnsafeHarnessValues() {"},{"violation":{"ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","ruleName":"Function Body Length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 53 lines","location":{"line":212,"character":3,"file":"TeraTests/TeraRemoteQualificationTests.swift"},"severity":"error"},"text":" func testAuthorizationEvidenceIsRedactedEphemeralAndRelaunchCleaned() throws {"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 298 lines","location":{"line":4,"character":7,"file":"TeraTests/TeraRuntimeClientTests.swift"},"severity":"error"},"text":"final class TeraRuntimeClientTests: XCTestCase {"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 580 lines","location":{"line":6,"character":7,"file":"TeraTests/TeraStateMigrationTests.swift"},"severity":"error"},"text":"final class TeraStateMigrationTests: XCTestCase {"},{"violation":{"ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","ruleName":"Function Body Length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 61 lines","location":{"line":7,"character":5,"file":"TeraTests/TeraStateMigrationTests.swift"},"severity":"error"},"text":" func testRelayValidationMatchesRuntimeProfiles() throws {"},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length","reason":"File should contain 400 lines or less: currently contains 622","location":{"line":622,"character":1,"file":"TeraTests/TeraStateMigrationTests.swift"},"severity":"error"},"text":"}"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Actor body should span 250 lines or less excluding comments and whitespace: currently spans 252 lines","location":{"line":207,"character":9,"file":"TeraTests/TeraSupportingStoreTests.swift"},"severity":"error"},"text":"private actor SupportingBackend: TeraRuntimeBackend {"},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length","reason":"File should contain 400 lines or less: currently contains 486","location":{"line":486,"character":1,"file":"TeraTests/TeraSupportingStoreTests.swift"},"severity":"error"},"text":"}"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 1365 lines","location":{"line":4,"character":7,"file":"TeraUITests/TeraRemoteQualificationUITests.swift"},"severity":"error"},"text":"final class TeraRemoteQualificationUITests: XCTestCase {"},{"violation":{"ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","ruleName":"Function Body Length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 65 lines","location":{"line":62,"character":3,"file":"TeraUITests/TeraRemoteQualificationUITests.swift"},"severity":"error"},"text":" func testLocalSocialFiveFlowScenario() throws {"},{"violation":{"ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","ruleName":"Function Body Length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 66 lines","location":{"line":166,"character":3,"file":"TeraUITests/TeraRemoteQualificationUITests.swift"},"severity":"error"},"text":" func testLocalSocialDeterministicPersonas() throws {"},{"violation":{"ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","ruleName":"Function Body Length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 54 lines","location":{"line":239,"character":3,"file":"TeraUITests/TeraRemoteQualificationUITests.swift"},"severity":"error"},"text":" func testRemoteBlossomUploadAndRecovery() throws {"},{"violation":{"ruleDescription":"Complexity of function bodies should be limited.","ruleIdentifier":"cyclomatic_complexity","ruleName":"Cyclomatic Complexity","reason":"Function should have complexity 10 or less; currently complexity is 18","location":{"line":566,"character":11,"file":"TeraUITests/TeraRemoteQualificationUITests.swift"},"severity":"error"},"text":" private func expectedAddFieldLabel(_ identifier: String, type: String) -> String {"},{"violation":{"ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","ruleName":"Function Body Length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 67 lines","location":{"line":1601,"character":10,"file":"TeraUITests/TeraRemoteQualificationUITests.swift"},"severity":"error"},"text":" static func environment("},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length","reason":"File should contain 1000 lines or less: currently contains 2077","location":{"line":2077,"character":1,"file":"TeraUITests/TeraRemoteQualificationUITests.swift"},"severity":"error"},"text":"}"}] +[{"violation":{"ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","ruleName":"Function Body Length","reason":"Initializer body should span 50 lines or less excluding comments and whitespace: currently spans 52 lines","location":{"line":21,"character":3,"file":"Tera/App/TeraAppModel.swift"},"severity":"error"},"text":" init("},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 373 lines","location":{"line":31,"character":7,"file":"Tera/Runtime/TeraGeneratedRuntimeBackend.swift"},"severity":"error"},"text":"final class TeraGeneratedRuntimeBackend: TeraRuntimeBackend, @unchecked Sendable {"},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length","reason":"File should contain 1000 lines or less: currently contains 1089","location":{"line":1089,"character":1,"file":"Tera/Runtime/TeraGeneratedRuntimeBackend.swift"},"severity":"error"},"text":"}"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 675 lines","location":{"line":3,"character":1,"file":"Tera/Runtime/TeraRuntimeClient.swift"},"severity":"error"},"text":"actor TeraRuntimeClient {"},{"violation":{"ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","ruleName":"Function Body Length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 70 lines","location":{"line":353,"character":3,"file":"Tera/Runtime/TeraRuntimeClient.swift"},"severity":"error"},"text":" func changes(bufferCapacity: Int = 16) async throws -> AsyncStream<TeraRuntimeChange> {"},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length","reason":"File should contain 400 lines or less: currently contains 749","location":{"line":749,"character":1,"file":"Tera/Runtime/TeraRuntimeClient.swift"},"severity":"error"},"text":"}"},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length","reason":"File should contain 1000 lines or less: currently contains 1085","location":{"line":1085,"character":1,"file":"Tera/Runtime/TeraRuntimeModels.swift"},"severity":"error"},"text":"}"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 491 lines","location":{"line":4,"character":7,"file":"Tera/State/TeraAddStore.swift"},"severity":"error"},"text":"final class TeraAddStore: ObservableObject {"},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length","reason":"File should contain 400 lines or less: currently contains 702","location":{"line":702,"character":1,"file":"Tera/State/TeraAddStore.swift"},"severity":"error"},"text":"}"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 468 lines","location":{"line":78,"character":1,"file":"Tera/State/TeraConfigurationStore.swift"},"severity":"error"},"text":"actor TeraConfigurationStore {"},{"violation":{"ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","ruleName":"Function Body Length","reason":"Function body should span 100 lines or less excluding comments and whitespace: currently spans 109 lines","location":{"line":148,"character":5,"file":"Tera/State/TeraConfigurationStore.swift"},"severity":"error"},"text":" func load() throws -> TeraAppConfiguration {"},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length","reason":"File should contain 400 lines or less: currently contains 759","location":{"line":759,"character":1,"file":"Tera/State/TeraConfigurationStore.swift"},"severity":"error"},"text":"}"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 390 lines","location":{"line":39,"character":1,"file":"Tera/State/TeraSessionStore.swift"},"severity":"error"},"text":"actor TeraSessionStore {"},{"violation":{"ruleDescription":"Complexity of function bodies should be limited.","ruleIdentifier":"cyclomatic_complexity","ruleName":"Cyclomatic Complexity","reason":"Function should have complexity 10 or less; currently complexity is 13","location":{"line":125,"character":11,"file":"Tera/State/TeraSessionStore.swift"},"severity":"error"},"text":" private func start(acceptingReconfiguration: Bool) async -> TeraSessionPhase {"},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length","reason":"File should contain 400 lines or less: currently contains 452","location":{"line":452,"character":1,"file":"Tera/State/TeraSessionStore.swift"},"severity":"error"},"text":"}"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Struct body should span 250 lines or less excluding comments and whitespace: currently spans 342 lines","location":{"line":4,"character":1,"file":"Tera/Views/TeraAddView.swift"},"severity":"error"},"text":"struct TeraAddView: View {"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Struct body should span 250 lines or less excluding comments and whitespace: currently spans 259 lines","location":{"line":365,"character":1,"file":"Tera/Views/TeraSupportingViews.swift"},"severity":"error"},"text":"struct TeraSettingsView: View {"},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length","reason":"File should contain 400 lines or less: currently contains 629","location":{"line":629,"character":1,"file":"Tera/Views/TeraSupportingViews.swift"},"severity":"error"},"text":"}"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 607 lines","location":{"line":6,"character":7,"file":"TeraTests/TeraAddStoreTests.swift"},"severity":"error"},"text":"final class TeraAddStoreTests: XCTestCase {"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 394 lines","location":{"line":682,"character":1,"file":"TeraTests/TeraAddStoreTests.swift"},"severity":"error"},"text":"actor AddBackend: TeraRuntimeBackend {"},{"violation":{"ruleDescription":"Number of function parameters should be low.","ruleIdentifier":"function_parameter_count","ruleName":"Function Parameter Count","reason":"Function should have 5 parameters or less: it currently has 8","location":{"line":1020,"character":3,"file":"TeraTests/TeraAddStoreTests.swift"},"severity":"error"},"text":" func makeStatus("},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length","reason":"File should contain 1000 lines or less: currently contains 1232","location":{"line":1232,"character":1,"file":"TeraTests/TeraAddStoreTests.swift"},"severity":"error"},"text":"}"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 307 lines","location":{"line":7,"character":7,"file":"TeraTests/TeraRemoteQualificationTests.swift"},"severity":"error"},"text":"final class TeraRemoteQualificationTests: XCTestCase {"},{"violation":{"ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","ruleName":"Function Body Length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 94 lines","location":{"line":45,"character":3,"file":"TeraTests/TeraRemoteQualificationTests.swift"},"severity":"error"},"text":" func testQualificationRejectsAmbiguousOrUnsafeHarnessValues() {"},{"violation":{"ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","ruleName":"Function Body Length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 53 lines","location":{"line":212,"character":3,"file":"TeraTests/TeraRemoteQualificationTests.swift"},"severity":"error"},"text":" func testAuthorizationEvidenceIsRedactedEphemeralAndRelaunchCleaned() throws {"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 298 lines","location":{"line":4,"character":7,"file":"TeraTests/TeraRuntimeClientTests.swift"},"severity":"error"},"text":"final class TeraRuntimeClientTests: XCTestCase {"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 580 lines","location":{"line":6,"character":7,"file":"TeraTests/TeraStateMigrationTests.swift"},"severity":"error"},"text":"final class TeraStateMigrationTests: XCTestCase {"},{"violation":{"ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","ruleName":"Function Body Length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 61 lines","location":{"line":7,"character":5,"file":"TeraTests/TeraStateMigrationTests.swift"},"severity":"error"},"text":" func testRelayValidationMatchesRuntimeProfiles() throws {"},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length","reason":"File should contain 400 lines or less: currently contains 622","location":{"line":622,"character":1,"file":"TeraTests/TeraStateMigrationTests.swift"},"severity":"error"},"text":"}"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Actor body should span 250 lines or less excluding comments and whitespace: currently spans 252 lines","location":{"line":207,"character":9,"file":"TeraTests/TeraSupportingStoreTests.swift"},"severity":"error"},"text":"private actor SupportingBackend: TeraRuntimeBackend {"},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length","reason":"File should contain 400 lines or less: currently contains 486","location":{"line":486,"character":1,"file":"TeraTests/TeraSupportingStoreTests.swift"},"severity":"error"},"text":"}"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 1365 lines","location":{"line":4,"character":7,"file":"TeraUITests/TeraRemoteQualificationUITests.swift"},"severity":"error"},"text":"final class TeraRemoteQualificationUITests: XCTestCase {"},{"violation":{"ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","ruleName":"Function Body Length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 65 lines","location":{"line":62,"character":3,"file":"TeraUITests/TeraRemoteQualificationUITests.swift"},"severity":"error"},"text":" func testLocalSocialFiveFlowScenario() throws {"},{"violation":{"ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","ruleName":"Function Body Length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 66 lines","location":{"line":166,"character":3,"file":"TeraUITests/TeraRemoteQualificationUITests.swift"},"severity":"error"},"text":" func testLocalSocialDeterministicPersonas() throws {"},{"violation":{"ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","ruleName":"Function Body Length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 54 lines","location":{"line":239,"character":3,"file":"TeraUITests/TeraRemoteQualificationUITests.swift"},"severity":"error"},"text":" func testRemoteBlossomUploadAndRecovery() throws {"},{"violation":{"ruleDescription":"Complexity of function bodies should be limited.","ruleIdentifier":"cyclomatic_complexity","ruleName":"Cyclomatic Complexity","reason":"Function should have complexity 10 or less; currently complexity is 18","location":{"line":566,"character":11,"file":"TeraUITests/TeraRemoteQualificationUITests.swift"},"severity":"error"},"text":" private func expectedAddFieldLabel(_ identifier: String, type: String) -> String {"},{"violation":{"ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","ruleName":"Function Body Length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 67 lines","location":{"line":1601,"character":10,"file":"TeraUITests/TeraRemoteQualificationUITests.swift"},"severity":"error"},"text":" static func environment("},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length","reason":"File should contain 1000 lines or less: currently contains 2077","location":{"line":2077,"character":1,"file":"TeraUITests/TeraRemoteQualificationUITests.swift"},"severity":"error"},"text":"}"}]