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 d35da2725d134faef0cf18a616a622686ab02608
parent 49e3c5a8c9eb3da8fa8e215c1ae0747bf89befe0
Author: triesap <tyson@radroots.org>
Date:   Wed,  9 Sep 2026 15:41:43 +0000

today: separate content availability from refresh

- Keep cached and empty content independent of refresh and local read failures.
- Track projection freshness from existing receipts without relay completeness claims.
- Preserve scoped cancellation and expose actual outcomes through native accessibility.
- Verify the presentation matrix, pagination recovery and standalone product gates.

Diffstat:
MPackage.swift | 2++
MTera.xcodeproj/project.pbxproj | 4++++
ATera/State/TeraTodayPresentation.swift | 132+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MTera/State/TeraTodayStore.swift | 90++++++++++++++++++++++++++++++++++++++++---------------------------------------
ATera/Views/TeraTodayStatusView.swift | 31+++++++++++++++++++++++++++++++
MTera/Views/TeraTodayView.swift | 59++++++++++++++++++++---------------------------------------
MTeraFFI/provenance.json | 14+++++++-------
MTeraFFI/source.lock | 4++--
MTeraFFI/source/aarch64-apple-darwin.json | 8++++----
MTeraFFI/source/aarch64-apple-ios-sim.json | 8++++----
MTeraFFI/source/aarch64-apple-ios.json | 8++++----
MTeraTests/TeraErrorRecoveryTests.swift | 4++--
MTeraTests/TeraScopedStoreTests.swift | 4++--
MTeraTests/TeraStoreRecoveryTests.swift | 2+-
ATeraTests/TeraTodayPresentationTests.swift | 190+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MTeraTests/TeraTodayStoreTests.swift | 10++++++----
Mproject.yml | 1+
Mrelease/provenance.json | 6+++---
Mtest-fixtures/legacy-identifiers.v1.json | 12+-----------
Mtest-fixtures/swiftlint-maintainability-baseline.v1.json | 4++--
20 files changed, 464 insertions(+), 129 deletions(-)

diff --git a/Package.swift b/Package.swift @@ -85,9 +85,11 @@ let package = Package( "State/TeraSupportingStores.swift", "State/TeraSettingsStore.swift", "State/TeraTodayStore.swift", + "State/TeraTodayPresentation.swift", "Views/TeraAddView.swift", "Views/TeraSupportingViews.swift", "Views/TeraTodayView.swift", + "Views/TeraTodayStatusView.swift", "Views/RuntimeStatusView.swift", ], resources: [ diff --git a/Tera.xcodeproj/project.pbxproj b/Tera.xcodeproj/project.pbxproj @@ -40,6 +40,7 @@ 9AD968F0B37C64B5AF8D9A68 /* TeraRootShellTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B8677670E203FF68EC88983 /* TeraRootShellTests.swift */; }; 9B37084901EC52EB44969304 /* TeraRuntimeResourceLifecycleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 226F5C2370522AAFAD9D1C3D /* TeraRuntimeResourceLifecycleTests.swift */; }; A1AD2A380F09994786889AF1 /* TeraAddStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F22CFABCF27A28408F68D25 /* TeraAddStoreTests.swift */; }; + AA7C3FC1F46B6723A39B1D09 /* TeraTodayPresentationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99BE248D1450D76476255626 /* TeraTodayPresentationTests.swift */; }; AC33144E09CB0384C4FB090C /* TeraObservationCoalescingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21DD785439F415278214D746 /* TeraObservationCoalescingTests.swift */; }; B1B83B8673660E0EC109F74C /* TeraStateTestFixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAD45A3134A16704CBE4B1A5 /* TeraStateTestFixtures.swift */; }; B2DAAE01D16E1FAC35816DED /* RadrootsKit in Frameworks */ = {isa = PBXBuildFile; productRef = 1A9F6E32DE0C3728D4DB8E60 /* RadrootsKit */; }; @@ -111,6 +112,7 @@ 93A3358AFC23C567DB9CB02F /* TeraErrorRecoveryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraErrorRecoveryTests.swift; sourceTree = "<group>"; }; 98E969BCFF7CED33A1D4A040 /* TeraStateMigrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraStateMigrationTests.swift; sourceTree = "<group>"; }; 999BDCEF144A19F35D048D15 /* TeraTests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = TeraTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 99BE248D1450D76476255626 /* TeraTodayPresentationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraTodayPresentationTests.swift; sourceTree = "<group>"; }; 9B10B1F3C7FCD9A72F04FC00 /* TeraOpenedMediaTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraOpenedMediaTests.swift; sourceTree = "<group>"; }; 9ED6D3E35C3235B6068E1902 /* TeraScopeBackend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraScopeBackend.swift; sourceTree = "<group>"; }; 9F22CFABCF27A28408F68D25 /* TeraAddStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraAddStoreTests.swift; sourceTree = "<group>"; }; @@ -233,6 +235,7 @@ BAD45A3134A16704CBE4B1A5 /* TeraStateTestFixtures.swift */, BD39C5F3460EEF38B98D78B4 /* TeraStoreRecoveryTests.swift */, C0636AEDEB50DDDD328374A9 /* TeraSupportingStoreTests.swift */, + 99BE248D1450D76476255626 /* TeraTodayPresentationTests.swift */, 794D475C70BA8ED994D1CA66 /* TeraTodayStoreTests.swift */, 46DDEE781FDD92A292FD847B /* TeraUserMessagesTests.swift */, ); @@ -481,6 +484,7 @@ B1B83B8673660E0EC109F74C /* TeraStateTestFixtures.swift in Sources */, 30EDD3EB0C09D864FC1DD3D4 /* TeraStoreRecoveryTests.swift in Sources */, 2479571FA99A7962794999C4 /* TeraSupportingStoreTests.swift in Sources */, + AA7C3FC1F46B6723A39B1D09 /* TeraTodayPresentationTests.swift in Sources */, 64D49ADFD3FDD54CBA509DC5 /* TeraTodayStoreTests.swift in Sources */, 4734A842C366608EC1BD31C5 /* TeraUserMessagesTests.swift in Sources */, ); diff --git a/Tera/State/TeraTodayPresentation.swift b/Tera/State/TeraTodayPresentation.swift @@ -0,0 +1,132 @@ +import Foundation + +enum TeraTodayContentAvailability: Sendable, Equatable { + case notLoaded + case empty + case available +} + +enum TeraTodayRefreshState: Sendable, Equatable { + case idle + case refreshing + case completed + case failed(TeraTodayFailure) +} + +enum TeraTodayFreshness: Sendable, Equatable { + case unconfirmed + /// A local projection receipt does not prove complete relay history. + case refreshed(contentGeneration: UInt64) +} + +enum TeraTodayFailure: Sendable, Equatable { + case offline(message: String) + case failed(message: String) + + init(_ error: Error) { + let message = TeraUserMessages.text(for: error, fallback: .todayUnavailable) + if TeraRuntimeFailure.from(error)?.recovery.disposition == .networkUnavailable { + self = .offline(message: message) + } else { + self = .failed(message: message) + } + } + + var message: String { + switch self { + case let .offline(message), let .failed(message): message + } + } + + var systemImage: String { + switch self { + case .offline: "wifi.slash" + case .failed: "exclamationmark.triangle" + } + } +} + +struct TeraTodayPresentation: Sendable, Equatable { + private(set) var content: TeraTodayContentAvailability = .notLoaded + private(set) var refresh: TeraTodayRefreshState = .idle + private(set) var freshness: TeraTodayFreshness = .unconfirmed + private(set) var readFailure: TeraTodayFailure? + private(set) var isReading = false + + mutating func beginReload(refreshProjection: Bool) { + readFailure = nil + isReading = false + freshness = .unconfirmed + if refreshProjection { + refresh = .refreshing + } else if refresh == .refreshing { + refresh = .idle + } + } + + mutating func refreshCompleted() { + refresh = .completed + } + + mutating func refreshFailed(_ error: Error) { + refresh = .failed(TeraTodayFailure(error)) + } + + mutating func beginRead() { + isReading = true + } + + mutating func finishReading() { + isReading = false + } + + mutating func acceptPage(count: Int, receipt: TeraTodayRefreshReceipt? = nil) { + content = count == 0 ? .empty : .available + readFailure = nil + isReading = false + if let receipt { + freshness = .refreshed(contentGeneration: receipt.contentGeneration) + } + } + + mutating func failRead(_ failure: TeraTodayFailure) { + readFailure = failure + isReading = false + freshness = .unconfirmed + } + + mutating func stop() { + isReading = false + if refresh == .refreshing { + refresh = .idle + } + } + + var freshnessMessage: String? { + guard content != .notLoaded else { return nil } + switch freshness { + case .unconfirmed: return "Showing saved posts." + case .refreshed: return "Saved posts checked." + } + } + + var accessibilityStatus: String { + var messages: [String] = [] + if refresh == .refreshing { + messages.append("Checking for updates.") + } + if isReading { + messages.append("Reading saved posts.") + } + if case let .failed(failure) = refresh { + messages.append("Refresh failed. \(failure.message)") + } + if let readFailure { + messages.append("Saved posts could not be read. \(readFailure.message)") + } + if let freshnessMessage { + messages.append(freshnessMessage) + } + return messages.joined(separator: " ") + } +} diff --git a/Tera/State/TeraTodayStore.swift b/Tera/State/TeraTodayStore.swift @@ -1,20 +1,11 @@ import Foundation -enum TeraTodayLoadState: Sendable, Equatable { - case idle - case loading - case loaded - case empty - case offline(message: String) - case failed(message: String) -} - @MainActor final class TeraTodayStore: ObservableObject { @Published private(set) var contexts: [TeraLocalNetwork] @Published private(set) var selectedContextID: String? @Published private(set) var cards: [TeraTodayCard] = [] - @Published private(set) var state: TeraTodayLoadState = .idle + @Published private(set) var presentation = TeraTodayPresentation() @Published private(set) var isLoadingNextPage = false @Published private(set) var observationState: TeraRuntimeObservationState = .inactive @@ -95,6 +86,7 @@ final class TeraTodayStore: ObservableObject { reloadTask?.cancel() reloadTask = nil isLoadingNextPage = false + presentation.stop() } private func invalidatePresentation() { @@ -105,7 +97,7 @@ final class TeraTodayStore: ObservableObject { frozenAsOfUnixSeconds = nil nextCursor = nil isLoadingNextPage = false - state = .idle + presentation = TeraTodayPresentation() if observation.isActive { observation.stop() startObservation() @@ -144,33 +136,56 @@ final class TeraTodayStore: ObservableObject { ) async { guard let context = selectedContext else { cards = [] - state = .failed(message: "Choose a local network to load Today.") + presentation = TeraTodayPresentation() + presentation.failRead(.failed(message: "Choose a local network to load Today.")) return } requestGeneration = requestGeneration.invalidated() let generation = requestGeneration + defer { + if generation == requestGeneration { + presentation.stop() + } + } frozenAsOfUnixSeconds = nil nextCursor = nil isLoadingNextPage = false - if cards.isEmpty { - state = .loading + presentation.beginReload(refreshProjection: refreshProjection) + var receipt: TeraTodayRefreshReceipt? + if refreshProjection { + receipt = await refresh(context: context, update: update, generation: generation) } + guard generation == requestGeneration, generation.isActive, !Task.isCancelled else { return } + await readFirstPage(context: context, generation: generation, receipt: receipt) + } - var refreshFailure: Error? - if refreshProjection { - do { - _ = try await runtimeClient.refreshToday( - context: context, - nowUnixSeconds: clock.unixSeconds(), - update: update - ) - } catch { - refreshFailure = error - } + private func refresh( + context: TeraLocalNetwork, update: TeraTodayProjectionUpdate, generation: TeraSessionGeneration + ) async -> TeraTodayRefreshReceipt? { + do { + let receipt = try await runtimeClient.refreshToday( + context: context, nowUnixSeconds: clock.unixSeconds(), update: update + ) + guard generation == requestGeneration, generation.isActive, !Task.isCancelled else { return nil } + presentation.refreshCompleted() + return receipt + } catch { + guard generation == requestGeneration, generation.isActive, !Task.isCancelled else { return nil } + presentation.refreshFailed(error) + return nil } + } - guard generation == requestGeneration, generation.isActive, !Task.isCancelled else { return } + private func readFirstPage( + context: TeraLocalNetwork, generation: TeraSessionGeneration, receipt: TeraTodayRefreshReceipt? + ) async { + presentation.beginRead() + defer { + if generation == requestGeneration { + presentation.finishReading() + } + } do { let asOf = try clock.unixSeconds() let page = try await runtimeClient.todayPage( @@ -184,10 +199,10 @@ final class TeraTodayStore: ObservableObject { frozenAsOfUnixSeconds = page.asOfUnixSeconds nextCursor = page.nextCursor cards = Self.unique(page.items) - state = refreshFailure.map(Self.failureState) ?? (cards.isEmpty ? .empty : .loaded) + presentation.acceptPage(count: cards.count, receipt: receipt) } catch { guard generation == requestGeneration, generation.isActive, !Task.isCancelled else { return } - state = Self.failureState(error) + presentation.failRead(TeraTodayFailure(error)) } } @@ -212,21 +227,16 @@ final class TeraTodayStore: ObservableObject { ) guard generation == requestGeneration, generation.isActive, !Task.isCancelled else { return } guard frozenAsOfUnixSeconds == nil || frozenAsOfUnixSeconds == page.asOfUnixSeconds else { - state = .failed(message: "Today changed while loading. Refresh to continue.") + presentation.failRead(.failed(message: "Today changed while loading. Refresh to continue.")) return } frozenAsOfUnixSeconds = page.asOfUnixSeconds nextCursor = page.nextCursor cards = Self.unique(cards + page.items) - switch state { - case .offline, .failed: - break - default: - state = cards.isEmpty ? .empty : .loaded - } + presentation.acceptPage(count: cards.count) } catch { guard generation == requestGeneration, generation.isActive, !Task.isCancelled else { return } - state = Self.failureState(error) + presentation.failRead(TeraTodayFailure(error)) } } @@ -255,12 +265,4 @@ final class TeraTodayStore: ObservableObject { var identifiers = Set<String>() return cards.filter { identifiers.insert($0.id).inserted } } - - static func failureState(_ error: Error) -> TeraTodayLoadState { - let message = TeraUserMessages.text(for: error, fallback: .todayUnavailable) - if TeraRuntimeFailure.from(error)?.recovery.disposition == .networkUnavailable { - return .offline(message: message) - } - return .failed(message: message) - } } diff --git a/Tera/Views/TeraTodayStatusView.swift b/Tera/Views/TeraTodayStatusView.swift @@ -0,0 +1,31 @@ +import SwiftUI + +struct TeraTodayStatusView: View { + let presentation: TeraTodayPresentation + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + if presentation.refresh == .refreshing { + ProgressView("Checking for updates…") + } + if presentation.isReading { + ProgressView("Reading saved posts…") + } + if case let .failed(failure) = presentation.refresh { + Label("Refresh failed. \(failure.message)", systemImage: failure.systemImage) + } + if let failure = presentation.readFailure { + Label("Saved posts could not be read. \(failure.message)", systemImage: failure.systemImage) + } + if let message = presentation.freshnessMessage { + Text(message) + } + } + .font(.footnote) + .foregroundStyle(.secondary) + .accessibilityElement(children: .ignore) + .accessibilityLabel("Today status") + .accessibilityValue(presentation.accessibilityStatus) + .accessibilityIdentifier("radroots.today.status") + } +} diff --git a/Tera/Views/TeraTodayView.swift b/Tera/Views/TeraTodayView.swift @@ -17,28 +17,12 @@ struct TeraTodayView: View { var body: some View { Group { - switch store.state { - case .idle where store.cards.isEmpty, - .loading where store.cards.isEmpty: - ProgressView("Loading Today…") - .accessibilityIdentifier("radroots.today.loading") + switch store.presentation.content { + case .notLoaded: + initialContent case .empty: emptyView - case let .failed(message) where store.cards.isEmpty: - unavailableView( - title: "Today is unavailable", - message: message, - systemImage: "exclamationmark.triangle" - ) - .accessibilityIdentifier("radroots.today.error") - case let .offline(message) where store.cards.isEmpty: - unavailableView( - title: "Network unavailable", - message: message, - systemImage: "wifi.slash" - ) - .accessibilityIdentifier("radroots.today.offline") - default: + case .available: feed } } @@ -73,9 +57,21 @@ struct TeraTodayView: View { .task { await store.start() } } + @ViewBuilder + private var initialContent: some View { + if let failure = store.presentation.readFailure { + unavailableView(failure) + .accessibilityIdentifier("radroots.today.error") + } else { + TeraTodayStatusView(presentation: store.presentation) + .accessibilityIdentifier("radroots.today.loading") + } + } + private var emptyView: some View { ScrollView { VStack(spacing: 16) { + TeraTodayStatusView(presentation: store.presentation) Image(systemName: "leaf") .font(.largeTitle) .foregroundStyle(.secondary) @@ -99,11 +95,7 @@ struct TeraTodayView: View { private var feed: some View { List { - if case let .offline(message) = store.state { - statusBanner(message: message, systemImage: "wifi.slash") - } else if case let .failed(message) = store.state { - statusBanner(message: message, systemImage: "exclamationmark.triangle") - } + TeraTodayStatusView(presentation: store.presentation) ForEach(store.cards) { card in NavigationLink(value: card) { @@ -184,26 +176,15 @@ struct TeraTodayView: View { } } - private func unavailableView( - title: String, - message: String, - systemImage: String - ) -> some View { + private func unavailableView(_ failure: TeraTodayFailure) -> some View { ContentUnavailableView { - Label(title, systemImage: systemImage) + Label("Today is unavailable", systemImage: failure.systemImage) } description: { - Text(message) + TeraTodayStatusView(presentation: store.presentation) } actions: { Button("Try again") { Task { await store.reload() } } } } - - private func statusBanner(message: String, systemImage: String) -> some View { - Label(message, systemImage: systemImage) - .font(.footnote) - .foregroundStyle(.secondary) - .accessibilityIdentifier("radroots.today.status") - } } private struct TeraContextPicker: View { diff --git a/TeraFFI/provenance.json b/TeraFFI/provenance.json @@ -94,17 +94,17 @@ { "bytes": 61732, "path": "source/aarch64-apple-darwin.json", - "sha256": "84ee6f7e6a892b2118be20f451a2bbb6b90e92a48a2e676127f8e75380761600" + "sha256": "60f5dd2de192cae21c908a57bab5f020b07303bfd967a7862fdd94bb3356237d" }, { "bytes": 61576, "path": "source/aarch64-apple-ios-sim.json", - "sha256": "3d5fac7931493190aaf9d8c6fac27e0b23ba3a90cabc70b62fc492f80d21fe5e" + "sha256": "3f1f0c4b9cd588994ff448ccf2d9114e026328ac16f0662972fcb09859a1896d" }, { "bytes": 61572, "path": "source/aarch64-apple-ios.json", - "sha256": "01120be5946961acb0184a1c643b1a3f28a49d358188781633271ed29381d4b1" + "sha256": "57bbefdc3a973e442f50b7c17269e9ab789a4e76e531dcc22ca7ea930b2b3ccc" } ], "language": "swift", @@ -112,7 +112,7 @@ "schema": "radroots.artifact-manifest.v2", "source": { "repository": "https://github.com/radrootslabs/tera", - "tree": "6f16b615f8c48f08138dba1fc369d91cc9fa9c54" + "tree": "783f5e1a06525924cc58ddb9f5e577f586703553" }, "source_records": { "aarch64-apple-darwin": "source/aarch64-apple-darwin.json", @@ -176,17 +176,17 @@ { "bytes": 61732, "path": "TeraFFI/source/aarch64-apple-darwin.json", - "sha256": "84ee6f7e6a892b2118be20f451a2bbb6b90e92a48a2e676127f8e75380761600" + "sha256": "60f5dd2de192cae21c908a57bab5f020b07303bfd967a7862fdd94bb3356237d" }, { "bytes": 61576, "path": "TeraFFI/source/aarch64-apple-ios-sim.json", - "sha256": "3d5fac7931493190aaf9d8c6fac27e0b23ba3a90cabc70b62fc492f80d21fe5e" + "sha256": "3f1f0c4b9cd588994ff448ccf2d9114e026328ac16f0662972fcb09859a1896d" }, { "bytes": 61572, "path": "TeraFFI/source/aarch64-apple-ios.json", - "sha256": "01120be5946961acb0184a1c643b1a3f28a49d358188781633271ed29381d4b1" + "sha256": "57bbefdc3a973e442f50b7c17269e9ab789a4e76e531dcc22ca7ea930b2b3ccc" } ], "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 = "6f16b615f8c48f08138dba1fc369d91cc9fa9c54" -manifest_sha256 = "b1ec0407b61e560048d3f33ec6b1cb8819b4c7830bf1fb35e08688c45792fbbd" +source_tree = "783f5e1a06525924cc58ddb9f5e577f586703553" +manifest_sha256 = "3b77261735a12783b64b05d33c730b5999bbef11115d3368b4e3f5f3bc0130a1" source_date_epoch = 1787871027 [foundation] diff --git a/TeraFFI/source/aarch64-apple-darwin.json b/TeraFFI/source/aarch64-apple-darwin.json @@ -1333,13 +1333,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 113480, - "git_blob": "bbe6a3c6766726354da3067d0b26ff2ae10f7dd9", + "bytes": 113263, + "git_blob": "c173fdb56956eb47bc9b7b1ae650775031428682", "mode": "100644", - "sha256": "a99d49b2c0c6e074f27e6ff04d6193a877abde3b689c442a260f8a093d10775b" + "sha256": "a316e2fc020f7ba47229e084e5b890c1dd239fbc68735c9117297f629e978389" } }, "policy": "staged_inputs", - "tree": "6f16b615f8c48f08138dba1fc369d91cc9fa9c54" + "tree": "783f5e1a06525924cc58ddb9f5e577f586703553" } } diff --git a/TeraFFI/source/aarch64-apple-ios-sim.json b/TeraFFI/source/aarch64-apple-ios-sim.json @@ -1329,13 +1329,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 113480, - "git_blob": "bbe6a3c6766726354da3067d0b26ff2ae10f7dd9", + "bytes": 113263, + "git_blob": "c173fdb56956eb47bc9b7b1ae650775031428682", "mode": "100644", - "sha256": "a99d49b2c0c6e074f27e6ff04d6193a877abde3b689c442a260f8a093d10775b" + "sha256": "a316e2fc020f7ba47229e084e5b890c1dd239fbc68735c9117297f629e978389" } }, "policy": "staged_inputs", - "tree": "6f16b615f8c48f08138dba1fc369d91cc9fa9c54" + "tree": "783f5e1a06525924cc58ddb9f5e577f586703553" } } diff --git a/TeraFFI/source/aarch64-apple-ios.json b/TeraFFI/source/aarch64-apple-ios.json @@ -1329,13 +1329,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 113480, - "git_blob": "bbe6a3c6766726354da3067d0b26ff2ae10f7dd9", + "bytes": 113263, + "git_blob": "c173fdb56956eb47bc9b7b1ae650775031428682", "mode": "100644", - "sha256": "a99d49b2c0c6e074f27e6ff04d6193a877abde3b689c442a260f8a093d10775b" + "sha256": "a316e2fc020f7ba47229e084e5b890c1dd239fbc68735c9117297f629e978389" } }, "policy": "staged_inputs", - "tree": "6f16b615f8c48f08138dba1fc369d91cc9fa9c54" + "tree": "783f5e1a06525924cc58ddb9f5e577f586703553" } } diff --git a/TeraTests/TeraErrorRecoveryTests.swift b/TeraTests/TeraErrorRecoveryTests.swift @@ -28,14 +28,14 @@ final class TeraErrorRecoveryTests: XCTestCase { let wrapped = TeraRuntimeClientError.today(failure) XCTAssertEqual(TeraUserMessages.key(for: wrapped, fallback: .todayUnavailable), key, code) XCTAssertEqual( - TeraTodayStore.failureState(wrapped), + TeraTodayFailure(wrapped), .failed(message: TeraUserMessages.text(key)), code ) XCTAssertEqual(TeraMediaStore.failureState(wrapped), .failed, code) } let offline = makeFailure(code: "today_relay_offline") XCTAssertEqual( - TeraTodayStore.failureState(offline), + TeraTodayFailure(offline), .offline(message: TeraUserMessages.text(.networkUnavailable)) ) XCTAssertEqual( diff --git a/TeraTests/TeraScopedStoreTests.swift b/TeraTests/TeraScopedStoreTests.swift @@ -20,7 +20,7 @@ final class TeraScopedStoreTests: XCTestCase { await pause.resume.open() await old.value XCTAssertEqual(store.cards.map(\.id), ["wss://second.example"]) - XCTAssertEqual(store.state, .loaded) + XCTAssertEqual(store.presentation.content, .available) store.stop() _ = try await client.stop() } @@ -41,7 +41,7 @@ final class TeraScopedStoreTests: XCTestCase { let after = await backend.counts[.page] XCTAssertEqual(after, count) XCTAssertEqual(store.cards.map(\.id), ["wss://second.example"]) - XCTAssertEqual(store.state, .loaded) + XCTAssertEqual(store.presentation.content, .available) store.stop() _ = try await client.stop() } diff --git a/TeraTests/TeraStoreRecoveryTests.swift b/TeraTests/TeraStoreRecoveryTests.swift @@ -83,7 +83,7 @@ final class TeraStoreRecoveryTests: XCTestCase { XCTAssertEqual(paused, before, "Only one observer query may execute at a time") await first.resume.open() await TeraScopeFixtures.eventually { store.cards.first?.id == "revision-256" } - XCTAssertEqual(store.state, .loaded) + XCTAssertEqual(store.presentation.content, .available) store.stop() _ = try await client.stop() } diff --git a/TeraTests/TeraTodayPresentationTests.swift b/TeraTests/TeraTodayPresentationTests.swift @@ -0,0 +1,190 @@ +@testable import TeraApp +import XCTest + +@MainActor +final class TeraTodayPresentationTests: XCTestCase { + func testContentAvailabilityIsIndependentOfRunningSuccessfulAndFailedRefresh() async throws { + for cached in [nil, [], [TeraScopeFixtures.card("cached")]] as [[TeraTodayCard]?] { + for fails in [false, true] { + try await assertRefresh(cached: cached, fails: fails) + } + } + } + + func testReadFailureRetainsCachedContentAndDoesNotInventAnEmptyResult() async throws { + for cached in [false, true] { + let backend = try TeraScopeBackend() + let (client, store) = try await makeStore(backend) + if cached { + await store.reload(refreshProjection: false) + } + let expected = store.presentation.content + let pause = await backend.pause(.page, fails: true) + let task = Task { await store.reload() } + await pause.entered.wait() + XCTAssertEqual(store.presentation.refresh, .completed) + XCTAssertEqual(store.presentation.content, expected) + XCTAssertTrue(store.presentation.isReading) + XCTAssertEqual(store.presentation.freshness, .unconfirmed) + await pause.resume.open() + await task.value + XCTAssertEqual(store.presentation.content, expected) + XCTAssertNotNil(store.presentation.readFailure) + XCTAssertEqual(store.presentation.refresh, .completed) + XCTAssertEqual(store.presentation.freshness, .unconfirmed) + XCTAssertFalse(store.presentation.isReading) + XCTAssertTrue(store.presentation.accessibilityStatus.contains("Saved posts could not be read.")) + XCTAssertFalse(store.presentation.accessibilityStatus.contains("Refresh failed.")) + XCTAssertEqual(store.cards.isEmpty, !cached) + _ = try await client.stop() + } + } + + func testCanceledRefreshStopsActivityWithoutDiscardingCachedContent() async throws { + let backend = try TeraScopeBackend() + let (client, store) = try await makeStore(backend) + await store.reload(refreshProjection: false) + let cards = store.cards + let pause = await backend.pause(.refresh) + let task = Task { await store.reload() } + await pause.entered.wait() + task.cancel() + await task.value + XCTAssertEqual(store.cards, cards) + XCTAssertEqual(store.presentation.content, .available) + XCTAssertEqual(store.presentation.refresh, .idle) + XCTAssertEqual(store.presentation.freshness, .unconfirmed) + XCTAssertFalse(store.presentation.isReading) + XCTAssertFalse(store.presentation.accessibilityStatus.contains("Checking for updates.")) + await pause.resume.open() + _ = try await client.stop() + } + + func testOldReadCancellationCannotClearTheNewRefreshActivity() async throws { + let backend = try TeraScopeBackend() + let (client, store) = try await makeStore(backend) + await store.reload(refreshProjection: false) + let reading = await backend.pause(.page) + let old = Task { await store.reload(refreshProjection: false) } + await reading.entered.wait() + let refreshing = await backend.pause(.refresh) + let current = Task { await store.reload() } + await refreshing.entered.wait() + old.cancel() + await old.value + XCTAssertEqual(store.presentation.refresh, .refreshing) + XCTAssertEqual(store.presentation.content, .available) + XCTAssertFalse(store.presentation.isReading) + XCTAssertTrue(store.presentation.accessibilityStatus.contains("Checking for updates.")) + await reading.resume.open() + await refreshing.resume.open() + await current.value + XCTAssertEqual(store.presentation.refresh, .completed) + XCTAssertEqual(store.presentation.freshness, .refreshed(contentGeneration: 1)) + _ = try await client.stop() + } + + func testMissingContextDoesNotClaimAnEmptyReadOrSuccessfulRefresh() async throws { + let backend = try TeraScopeBackend() + let client = try await TeraScopeFixtures.client(backend) + let store = TeraTodayStore(runtimeClient: client) + await store.reload() + XCTAssertEqual(store.presentation.content, .notLoaded) + XCTAssertEqual(store.presentation.refresh, .idle) + XCTAssertEqual(store.presentation.freshness, .unconfirmed) + XCTAssertNotNil(store.presentation.readFailure) + let counts = await backend.counts + XCTAssertNil(counts[.refresh]) + XCTAssertNil(counts[.page]) + _ = try await client.stop() + } + + func testPaginationReadFailureAndRecoveryPreserveTheRefreshOutcome() async throws { + let backend = try TeraScopeBackend() + await backend.setPage(page([TeraScopeFixtures.card("one")], next: "two")) + await backend.setPage(page([TeraScopeFixtures.card("two")]), cursor: "two") + let (client, store) = try await makeStore(backend) + await store.reload() + let pause = await backend.pause(.page, fails: true) + let task = Task { await store.loadNextPage() } + await pause.entered.wait() + await pause.resume.open() + await task.value + XCTAssertEqual(store.cards.map(\.id), ["one"]) + XCTAssertEqual(store.presentation.content, .available) + XCTAssertEqual(store.presentation.refresh, .completed) + XCTAssertNotNil(store.presentation.readFailure) + XCTAssertEqual(store.presentation.freshness, .unconfirmed) + await store.loadNextPage() + XCTAssertEqual(store.cards.map(\.id), ["one", "two"]) + XCTAssertNil(store.presentation.readFailure) + XCTAssertEqual(store.presentation.refresh, .completed) + _ = try await client.stop() + } + + func testFailedRefreshAndFailedLocalReadHaveIndependentAccessibleOutcomes() async throws { + let backend = try TeraScopeBackend() + let (client, store) = try await makeStore(backend) + let refreshing = await backend.pause(.refresh, fails: true) + let reading = await backend.pause(.page, fails: true) + let task = Task { await store.reload() } + await refreshing.entered.wait() + await refreshing.resume.open() + await reading.entered.wait() + XCTAssertTrue(store.presentation.isReading) + await reading.resume.open() + await task.value + XCTAssertEqual(store.presentation.content, .notLoaded) + XCTAssertNotNil(store.presentation.readFailure) + XCTAssertTrue(store.presentation.accessibilityStatus.contains("Refresh failed.")) + XCTAssertTrue(store.presentation.accessibilityStatus.contains("Saved posts could not be read.")) + XCTAssertFalse(store.presentation.accessibilityStatus.contains("Reading saved posts.")) + _ = try await client.stop() + } + + private func assertRefresh(cached: [TeraTodayCard]?, fails: Bool) async throws { + let backend = try TeraScopeBackend() + await backend.setPage(page(cached ?? [])) + let (client, store) = try await makeStore(backend) + if cached != nil { + await store.reload(refreshProjection: false) + } + let expected = store.presentation.content + let pause = await backend.pause(.refresh, fails: fails) + let task = Task { await store.reload() } + await pause.entered.wait() + XCTAssertEqual(store.presentation.content, expected) + XCTAssertEqual(store.presentation.refresh, .refreshing) + XCTAssertEqual(store.presentation.freshness, .unconfirmed) + XCTAssertFalse(store.presentation.isReading) + XCTAssertTrue(store.presentation.accessibilityStatus.contains("Checking for updates.")) + XCTAssertEqual(store.cards, cached ?? []) + await pause.resume.open() + await task.value + XCTAssertEqual(store.presentation.content, (cached ?? []).isEmpty ? .empty : .available) + XCTAssertNil(store.presentation.readFailure) + XCTAssertFalse(store.presentation.isReading) + if fails { + XCTAssertEqual(store.presentation.refresh, .failed(TeraTodayFailure(TeraScopeFixtures.failure()))) + XCTAssertEqual(store.presentation.freshness, .unconfirmed) + XCTAssertTrue(store.presentation.accessibilityStatus.contains("Refresh failed.")) + XCTAssertTrue(store.presentation.accessibilityStatus.contains("Showing saved posts.")) + } else { + XCTAssertEqual(store.presentation.refresh, .completed) + XCTAssertEqual(store.presentation.freshness, .refreshed(contentGeneration: 1)) + XCTAssertEqual(store.presentation.accessibilityStatus, "Saved posts checked.") + } + _ = try await client.stop() + } + + private func makeStore(_ backend: TeraScopeBackend) async throws -> (TeraRuntimeClient, TeraTodayStore) { + let client = try await TeraScopeFixtures.client(backend) + let store = TeraTodayStore(runtimeClient: client, clock: .fixed(unixSeconds: 1)) + store.configure(snapshot: TeraScopeFixtures.snapshot()) + return (client, store) + } + + private func page(_ cards: [TeraTodayCard], next: String? = nil) -> TeraTodayPage { + TeraTodayPage(asOfUnixSeconds: 1, items: cards, nextCursor: next) + } +} diff --git a/TeraTests/TeraTodayStoreTests.swift b/TeraTests/TeraTodayStoreTests.swift @@ -35,7 +35,7 @@ final class TeraTodayStoreTests: XCTestCase { await store.loadNextPage() XCTAssertEqual(store.cards.map(\.id), ["a", "b"]) - XCTAssertEqual(store.state, .loaded) + XCTAssertEqual(store.presentation.content, .available) XCTAssertFalse(store.canLoadNextPage) _ = try await client.stop() } @@ -151,7 +151,7 @@ final class TeraTodayStoreTests: XCTestCase { } @MainActor - private func assertCachedRefreshFailure(code: String, expected: TeraTodayLoadState) async throws { + private func assertCachedRefreshFailure(code: String, expected: TeraTodayFailure) async throws { let context = makeContext(id: "offline", label: "Offline") let cached = makeCard(id: "cached", type: .update) let failure = TeraRuntimeFailure( @@ -189,10 +189,12 @@ final class TeraTodayStoreTests: XCTestCase { await store.reload() XCTAssertEqual(store.cards.map(\.id), ["cached"]) - XCTAssertEqual(store.state, expected) + XCTAssertEqual(store.presentation.content, .available) + XCTAssertEqual(store.presentation.refresh, .failed(expected)) await store.loadNextPage() XCTAssertEqual(store.cards.map(\.id), ["cached", "cached-next"]) - XCTAssertEqual(store.state, expected) + XCTAssertEqual(store.presentation.content, .available) + XCTAssertEqual(store.presentation.refresh, .failed(expected)) _ = try await client.stop() } diff --git a/project.yml b/project.yml @@ -61,6 +61,7 @@ targets: - path: TeraTests/TeraRootShellTests.swift - path: TeraTests/TeraStateMigrationTests.swift - path: TeraTests/TeraTodayStoreTests.swift + - path: TeraTests/TeraTodayPresentationTests.swift - path: TeraTests/TeraAddSchemaFixtures.swift - path: TeraTests/TeraScopeFixtures.swift - path: TeraTests/TeraScopeBackend.swift diff --git a/release/provenance.json b/release/provenance.json @@ -2,11 +2,11 @@ "artifacts": { "app_api_sha256": "020924097c0d7efc33128cb8fd3d3b2026d95f57c44da71880e585aff80f070b", "ffi_api_sha256": "ab87eb6e3d4512acc3986120c38988f8e7559ac1781de76d9808e0bae338d1de", - "ffi_provenance_sha256": "b1ec0407b61e560048d3f33ec6b1cb8819b4c7830bf1fb35e08688c45792fbbd", + "ffi_provenance_sha256": "3b77261735a12783b64b05d33c730b5999bbef11115d3368b4e3f5f3bc0130a1", "info_plist_sha256": "15ef08b1cdd1096cfb9eeaf5be5bf8f814807a7ca9350bbbb47860fa72ec13ef", "privacy_manifest_sha256": "a331d51864743ebe4e00dd22360b4a538b6b3ac26a6b3eb54094e60a36959a12", "sbom_sha256": "6b5ce897d5273290febc8b831663e12dea759cb0084f182ba7949e1c593fc410", - "xcode_project_sha256": "4e668fefb0f9c1ce1a0cfff65c96360cbddc935cb45ceeb405c7507613bc35e0" + "xcode_project_sha256": "c62334b1d43ba931d096a9a9895affc3e722b266191a52beda11e894f0f9318f" }, "disposition": "unsigned", "platforms": [ @@ -22,7 +22,7 @@ "lib_revision": "ad17b7d3455a7147cfa303d976fc5c70c3a4c0cb", "source_date_epoch": 1787871027, "swift_package_lock_sha256": "94ae067a374726cdaf6b4ca0a5e44663c57fcdc5334060c5ffef5e79cfbf04c0", - "tera_ffi_source_tree": "6f16b615f8c48f08138dba1fc369d91cc9fa9c54", + "tera_ffi_source_tree": "783f5e1a06525924cc58ddb9f5e577f586703553", "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 @@ -3765,16 +3765,6 @@ ] }, { - "identifier": "radroots.today.offline", - "category": "native_ui_contract", - "occurrences": [ - { - "path": "Tera/Views/TeraTodayView.swift", - "count": 1 - } - ] - }, - { "identifier": "radroots.today.refresh", "category": "native_ui_contract", "occurrences": [ @@ -3837,7 +3827,7 @@ "category": "native_ui_contract", "occurrences": [ { - "path": "Tera/Views/TeraTodayView.swift", + "path": "Tera/Views/TeraTodayStatusView.swift", "count": 1 } ] diff --git a/test-fixtures/swiftlint-maintainability-baseline.v1.json b/test-fixtures/swiftlint-maintainability-baseline.v1.json @@ -1 +1 @@ -[{"text":" init(","violation":{"ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","reason":"Initializer body should span 50 lines or less excluding comments and whitespace: currently spans 52 lines","severity":"error","location":{"line":83,"character":3,"file":"Tera\/App\/TeraAppModel.swift"},"ruleName":"Function Body Length"}},{"text":"actor TeraAddMediaCoordinator: TeraAddMediaHandling {","violation":{"ruleDescription":"Type bodies should not span too many lines","severity":"error","location":{"line":51,"file":"Tera\/Runtime\/TeraAddMediaCoordinator.swift","character":1},"reason":"Actor body should span 250 lines or less excluding comments and whitespace: currently spans 330 lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length"}},{"text":"#endif","violation":{"ruleName":"File Length","ruleIdentifier":"file_length","severity":"error","location":{"character":1,"file":"Tera\/Runtime\/TeraAddMediaCoordinator.swift","line":458},"reason":"File should contain 400 lines or less: currently contains 458","ruleDescription":"Files should not span too many lines."}},{"text":"private final class TeraGeneratedRuntimeBackend: TeraRuntimeBackend, @unchecked Sendable {","violation":{"severity":"error","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 545 lines","ruleDescription":"Type bodies should not span too many lines","location":{"file":"Tera\/Runtime\/TeraGeneratedRuntimeBackend.swift","line":111,"character":15},"ruleIdentifier":"type_body_length","ruleName":"Type Body Length"}},{"text":"}","violation":{"reason":"File should contain 1000 lines or less: currently contains 1640","ruleName":"File Length","ruleDescription":"Files should not span too many lines.","severity":"error","ruleIdentifier":"file_length","location":{"line":1640,"character":1,"file":"Tera\/Runtime\/TeraGeneratedRuntimeBackend.swift"}}},{"text":"actor TeraRuntimeClient {","violation":{"ruleDescription":"Type bodies should not span too many lines","severity":"error","ruleIdentifier":"type_body_length","location":{"file":"Tera\/Runtime\/TeraRuntimeClient.swift","line":291,"character":1},"reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 787 lines","ruleName":"Type Body Length"}},{"text":" func changes(bufferCapacity: Int = 16) async throws -> AsyncStream<TeraRuntimeChange> {","violation":{"ruleIdentifier":"function_body_length","severity":"error","location":{"character":3,"file":"Tera\/Runtime\/TeraRuntimeClient.swift","line":703},"ruleDescription":"Function bodies should not span too many lines","ruleName":"Function Body Length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 70 lines"}},{"text":"}","violation":{"reason":"File should contain 1000 lines or less: currently contains 1159","location":{"character":1,"line":1159,"file":"Tera\/Runtime\/TeraRuntimeClient.swift"},"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length","severity":"error"}},{"text":"}","violation":{"location":{"character":1,"line":1210,"file":"Tera\/Runtime\/TeraRuntimeModels.swift"},"ruleDescription":"Files should not span too many lines.","reason":"File should contain 1000 lines or less: currently contains 1210","severity":"error","ruleIdentifier":"file_length","ruleName":"File Length"}},{"text":"final class TeraAddStore: ObservableObject {","violation":{"reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 731 lines","ruleDescription":"Type bodies should not span too many lines","ruleName":"Type Body Length","location":{"character":7,"file":"Tera\/State\/TeraAddStore.swift","line":11},"severity":"error","ruleIdentifier":"type_body_length"}},{"text":" func retract(_ card: TeraTodayCard) async {","violation":{"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 51 lines","location":{"line":459,"character":3,"file":"Tera\/State\/TeraAddStore.swift"},"ruleDescription":"Function bodies should not span too many lines","ruleName":"Function Body Length","ruleIdentifier":"function_body_length","severity":"error"}},{"text":"}","violation":{"ruleDescription":"Files should not span too many lines.","ruleName":"File Length","reason":"File should contain 400 lines or less: currently contains 796","severity":"error","ruleIdentifier":"file_length","location":{"line":796,"character":1,"file":"Tera\/State\/TeraAddStore.swift"}}},{"text":"actor TeraConfigurationStore {","violation":{"ruleDescription":"Type bodies should not span too many lines","ruleName":"Type Body Length","location":{"line":78,"file":"Tera\/State\/TeraConfigurationStore.swift","character":1},"reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 468 lines","ruleIdentifier":"type_body_length","severity":"error"}},{"text":" func load() throws -> TeraAppConfiguration {","violation":{"severity":"error","ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines","location":{"line":148,"file":"Tera\/State\/TeraConfigurationStore.swift","character":5},"reason":"Function body should span 100 lines or less excluding comments and whitespace: currently spans 109 lines","ruleName":"Function Body Length"}},{"text":"}","violation":{"severity":"error","ruleIdentifier":"file_length","reason":"File should contain 400 lines or less: currently contains 759","ruleDescription":"Files should not span too many lines.","location":{"file":"Tera\/State\/TeraConfigurationStore.swift","character":1,"line":759},"ruleName":"File Length"}},{"text":"actor TeraSessionStore {","violation":{"location":{"character":1,"file":"Tera\/State\/TeraSessionStore.swift","line":39},"reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 390 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines","ruleName":"Type Body Length","severity":"error"}},{"text":" private func start(acceptingReconfiguration: Bool) async -> TeraSessionPhase {","violation":{"ruleName":"Cyclomatic Complexity","location":{"file":"Tera\/State\/TeraSessionStore.swift","character":11,"line":125},"ruleIdentifier":"cyclomatic_complexity","ruleDescription":"Complexity of function bodies should be limited.","severity":"error","reason":"Function should have complexity 10 or less; currently complexity is 13"}},{"text":"}","violation":{"severity":"error","location":{"line":452,"character":1,"file":"Tera\/State\/TeraSessionStore.swift"},"ruleIdentifier":"file_length","ruleName":"File Length","reason":"File should contain 400 lines or less: currently contains 452","ruleDescription":"Files should not span too many lines."}},{"text":"struct TeraAddView: View {","violation":{"ruleName":"Type Body Length","reason":"Struct body should span 350 lines or less excluding comments and whitespace: currently spans 454 lines","location":{"file":"Tera\/Views\/TeraAddView.swift","line":4,"character":1},"severity":"error","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"}},{"text":"}","violation":{"ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines.","location":{"line":560,"character":1,"file":"Tera\/Views\/TeraAddView.swift"},"severity":"error","ruleName":"File Length","reason":"File should contain 400 lines or less: currently contains 560"}},{"text":"struct TeraSettingsView: View {","violation":{"location":{"file":"Tera\/Views\/TeraSupportingViews.swift","line":365,"character":1},"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","severity":"error","ruleName":"Type Body Length","reason":"Struct body should span 250 lines or less excluding comments and whitespace: currently spans 259 lines"}},{"text":"}","violation":{"location":{"character":1,"file":"Tera\/Views\/TeraSupportingViews.swift","line":629},"ruleName":"File Length","reason":"File should contain 400 lines or less: currently contains 629","severity":"error","ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines."}},{"text":"}","violation":{"location":{"file":"Tera\/Views\/TeraTodayView.swift","character":1,"line":493},"reason":"File should contain 400 lines or less: currently contains 493","ruleDescription":"Files should not span too many lines.","ruleName":"File Length","ruleIdentifier":"file_length","severity":"error"}},{"text":"final class TeraAddStoreTests: XCTestCase {","violation":{"reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 665 lines","location":{"file":"TeraTests\/TeraAddStoreTests.swift","character":7,"line":6},"ruleName":"Type Body Length","severity":"error","ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length"}},{"text":"private actor AddBackend: TeraRuntimeBackend {","violation":{"ruleIdentifier":"type_body_length","ruleName":"Type Body Length","severity":"error","ruleDescription":"Type bodies should not span too many lines","location":{"character":9,"file":"TeraTests\/TeraAddStoreTests.swift","line":844},"reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 492 lines"}},{"text":" private func makeStatus(","violation":{"location":{"line":1280,"file":"TeraTests\/TeraAddStoreTests.swift","character":11},"ruleIdentifier":"function_parameter_count","severity":"error","reason":"Function should have 5 parameters or less: it currently has 8","ruleDescription":"Number of function parameters should be low.","ruleName":"Function Parameter Count"}},{"text":"}","violation":{"location":{"character":1,"file":"TeraTests\/TeraAddStoreTests.swift","line":1431},"ruleDescription":"Files should not span too many lines.","ruleName":"File Length","ruleIdentifier":"file_length","reason":"File should contain 1000 lines or less: currently contains 1431","severity":"error"}},{"text":"final class TeraRemoteQualificationTests: XCTestCase {","violation":{"ruleName":"Type Body Length","severity":"error","location":{"line":7,"file":"TeraTests\/TeraRemoteQualificationTests.swift","character":7},"reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 307 lines","ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length"}},{"text":" func testQualificationRejectsAmbiguousOrUnsafeHarnessValues() {","violation":{"severity":"error","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 94 lines","location":{"character":3,"file":"TeraTests\/TeraRemoteQualificationTests.swift","line":45},"ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines","ruleName":"Function Body Length"}},{"text":" func testAuthorizationEvidenceIsRedactedEphemeralAndRelaunchCleaned() throws {","violation":{"location":{"file":"TeraTests\/TeraRemoteQualificationTests.swift","character":3,"line":212},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 53 lines","ruleName":"Function Body Length","ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines","severity":"error"}},{"text":"final class TeraRuntimeClientTests: XCTestCase {","violation":{"location":{"character":7,"line":4,"file":"TeraTests\/TeraRuntimeClientTests.swift"},"ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines","severity":"error","reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 298 lines","ruleName":"Type Body Length"}},{"text":"final class TeraStateMigrationTests: XCTestCase {","violation":{"ruleName":"Type Body Length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 580 lines","location":{"character":7,"file":"TeraTests\/TeraStateMigrationTests.swift","line":6},"ruleDescription":"Type bodies should not span too many lines","severity":"error","ruleIdentifier":"type_body_length"}},{"text":" func testRelayValidationMatchesRuntimeProfiles() throws {","violation":{"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 61 lines","ruleIdentifier":"function_body_length","ruleName":"Function Body Length","severity":"error","location":{"line":7,"file":"TeraTests\/TeraStateMigrationTests.swift","character":5},"ruleDescription":"Function bodies should not span too many lines"}},{"text":"}","violation":{"ruleDescription":"Files should not span too many lines.","severity":"error","ruleName":"File Length","ruleIdentifier":"file_length","location":{"line":622,"file":"TeraTests\/TeraStateMigrationTests.swift","character":1},"reason":"File should contain 400 lines or less: currently contains 622"}},{"text":"private actor SupportingBackend: TeraRuntimeBackend {","violation":{"reason":"Actor body should span 250 lines or less excluding comments and whitespace: currently spans 253 lines","ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","severity":"error","ruleName":"Type Body Length","location":{"line":207,"file":"TeraTests\/TeraSupportingStoreTests.swift","character":9}}},{"text":"}","violation":{"ruleName":"File Length","ruleDescription":"Files should not span too many lines.","location":{"file":"TeraTests\/TeraSupportingStoreTests.swift","line":487,"character":1},"ruleIdentifier":"file_length","reason":"File should contain 400 lines or less: currently contains 487","severity":"error"}},{"text":"final class TeraRemoteQualificationUITests: XCTestCase {","violation":{"reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 1493 lines","ruleIdentifier":"type_body_length","location":{"line":4,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":7},"severity":"error","ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"}},{"text":" func testLocalSocialFiveFlowScenario() throws {","violation":{"ruleDescription":"Function bodies should not span too many lines","location":{"character":3,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":62},"ruleName":"Function Body Length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 73 lines","ruleIdentifier":"function_body_length","severity":"error"}},{"text":" func testLocalSocialDeterministicPersonas() throws {","violation":{"severity":"error","ruleIdentifier":"function_body_length","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":174,"character":3},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 66 lines","ruleDescription":"Function bodies should not span too many lines","ruleName":"Function Body Length"}},{"text":" func testRemoteBlossomUploadAndRecovery() throws {","violation":{"ruleDescription":"Function bodies should not span too many lines","ruleName":"Function Body Length","severity":"error","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":247,"character":3},"ruleIdentifier":"function_body_length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 54 lines"}},{"text":" private func expectedAddFieldLabel(_ identifier: String, type: String) -> String {","violation":{"severity":"error","location":{"line":573,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":11},"reason":"Function should have complexity 10 or less; currently complexity is 18","ruleName":"Cyclomatic Complexity","ruleIdentifier":"cyclomatic_complexity","ruleDescription":"Complexity of function bodies should be limited."}},{"text":" private func preparePhotoUpdate(_ app: XCUIApplication, marker: String) -> XCUIElement? {","violation":{"ruleIdentifier":"cyclomatic_complexity","ruleName":"Cyclomatic Complexity","location":{"character":11,"line":767,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift"},"severity":"error","reason":"Function should have complexity 10 or less; currently complexity is 11","ruleDescription":"Complexity of function bodies should be limited."}},{"text":" private func preparePhotoUpdate(_ app: XCUIApplication, marker: String) -> XCUIElement? {","violation":{"ruleIdentifier":"function_body_length","ruleName":"Function Body Length","location":{"line":767,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":11},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 66 lines","severity":"error","ruleDescription":"Function bodies should not span too many lines"}},{"text":" static func environment(","violation":{"ruleIdentifier":"function_body_length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 67 lines","severity":"error","ruleDescription":"Function bodies should not span too many lines","location":{"line":1736,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":10},"ruleName":"Function Body Length"}},{"text":"}","violation":{"ruleName":"File Length","location":{"line":2212,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":1},"reason":"File should contain 1000 lines or less: currently contains 2212","ruleDescription":"Files should not span too many lines.","severity":"error","ruleIdentifier":"file_length"}}] -\ No newline at end of file +[{"text":" init(","violation":{"severity":"error","ruleIdentifier":"function_body_length","reason":"Initializer body should span 50 lines or less excluding comments and whitespace: currently spans 52 lines","location":{"file":"Tera\/App\/TeraAppModel.swift","character":3,"line":83},"ruleName":"Function Body Length","ruleDescription":"Function bodies should not span too many lines"}},{"text":"actor TeraAddMediaCoordinator: TeraAddMediaHandling {","violation":{"severity":"error","ruleIdentifier":"type_body_length","reason":"Actor body should span 250 lines or less excluding comments and whitespace: currently spans 330 lines","location":{"file":"Tera\/Runtime\/TeraAddMediaCoordinator.swift","character":1,"line":51},"ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"}},{"text":"#endif","violation":{"severity":"error","ruleIdentifier":"file_length","reason":"File should contain 400 lines or less: currently contains 458","location":{"file":"Tera\/Runtime\/TeraAddMediaCoordinator.swift","character":1,"line":458},"ruleName":"File Length","ruleDescription":"Files should not span too many lines."}},{"text":"private final class TeraGeneratedRuntimeBackend: TeraRuntimeBackend, @unchecked Sendable {","violation":{"severity":"error","ruleIdentifier":"type_body_length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 545 lines","location":{"file":"Tera\/Runtime\/TeraGeneratedRuntimeBackend.swift","character":15,"line":111},"ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"}},{"text":"}","violation":{"severity":"error","ruleIdentifier":"file_length","reason":"File should contain 1000 lines or less: currently contains 1640","location":{"file":"Tera\/Runtime\/TeraGeneratedRuntimeBackend.swift","character":1,"line":1640},"ruleName":"File Length","ruleDescription":"Files should not span too many lines."}},{"text":"actor TeraRuntimeClient {","violation":{"severity":"error","ruleIdentifier":"type_body_length","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 787 lines","location":{"file":"Tera\/Runtime\/TeraRuntimeClient.swift","character":1,"line":291},"ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"}},{"text":" func changes(bufferCapacity: Int = 16) async throws -> AsyncStream<TeraRuntimeChange> {","violation":{"severity":"error","ruleIdentifier":"function_body_length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 70 lines","location":{"file":"Tera\/Runtime\/TeraRuntimeClient.swift","character":3,"line":703},"ruleName":"Function Body Length","ruleDescription":"Function bodies should not span too many lines"}},{"text":"}","violation":{"severity":"error","ruleIdentifier":"file_length","reason":"File should contain 1000 lines or less: currently contains 1159","location":{"file":"Tera\/Runtime\/TeraRuntimeClient.swift","character":1,"line":1159},"ruleName":"File Length","ruleDescription":"Files should not span too many lines."}},{"text":"}","violation":{"severity":"error","ruleIdentifier":"file_length","reason":"File should contain 1000 lines or less: currently contains 1210","location":{"file":"Tera\/Runtime\/TeraRuntimeModels.swift","character":1,"line":1210},"ruleName":"File Length","ruleDescription":"Files should not span too many lines."}},{"text":"final class TeraAddStore: ObservableObject {","violation":{"severity":"error","ruleIdentifier":"type_body_length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 731 lines","location":{"file":"Tera\/State\/TeraAddStore.swift","character":7,"line":11},"ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"}},{"text":" func retract(_ card: TeraTodayCard) async {","violation":{"severity":"error","ruleIdentifier":"function_body_length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 51 lines","location":{"file":"Tera\/State\/TeraAddStore.swift","character":3,"line":459},"ruleName":"Function Body Length","ruleDescription":"Function bodies should not span too many lines"}},{"text":"}","violation":{"severity":"error","ruleIdentifier":"file_length","reason":"File should contain 400 lines or less: currently contains 796","location":{"file":"Tera\/State\/TeraAddStore.swift","character":1,"line":796},"ruleName":"File Length","ruleDescription":"Files should not span too many lines."}},{"text":"actor TeraConfigurationStore {","violation":{"severity":"error","ruleIdentifier":"type_body_length","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 468 lines","location":{"file":"Tera\/State\/TeraConfigurationStore.swift","character":1,"line":78},"ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"}},{"text":" func load() throws -> TeraAppConfiguration {","violation":{"severity":"error","ruleIdentifier":"function_body_length","reason":"Function body should span 100 lines or less excluding comments and whitespace: currently spans 109 lines","location":{"file":"Tera\/State\/TeraConfigurationStore.swift","character":5,"line":148},"ruleName":"Function Body Length","ruleDescription":"Function bodies should not span too many lines"}},{"text":"}","violation":{"severity":"error","ruleIdentifier":"file_length","reason":"File should contain 400 lines or less: currently contains 759","location":{"file":"Tera\/State\/TeraConfigurationStore.swift","character":1,"line":759},"ruleName":"File Length","ruleDescription":"Files should not span too many lines."}},{"text":"actor TeraSessionStore {","violation":{"severity":"error","ruleIdentifier":"type_body_length","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 390 lines","location":{"file":"Tera\/State\/TeraSessionStore.swift","character":1,"line":39},"ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"}},{"text":" private func start(acceptingReconfiguration: Bool) async -> TeraSessionPhase {","violation":{"severity":"error","ruleIdentifier":"cyclomatic_complexity","reason":"Function should have complexity 10 or less; currently complexity is 13","location":{"file":"Tera\/State\/TeraSessionStore.swift","character":11,"line":125},"ruleName":"Cyclomatic Complexity","ruleDescription":"Complexity of function bodies should be limited."}},{"text":"}","violation":{"severity":"error","ruleIdentifier":"file_length","reason":"File should contain 400 lines or less: currently contains 452","location":{"file":"Tera\/State\/TeraSessionStore.swift","character":1,"line":452},"ruleName":"File Length","ruleDescription":"Files should not span too many lines."}},{"text":"struct TeraAddView: View {","violation":{"severity":"error","ruleIdentifier":"type_body_length","reason":"Struct body should span 350 lines or less excluding comments and whitespace: currently spans 454 lines","location":{"file":"Tera\/Views\/TeraAddView.swift","character":1,"line":4},"ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"}},{"text":"}","violation":{"severity":"error","ruleIdentifier":"file_length","reason":"File should contain 400 lines or less: currently contains 560","location":{"file":"Tera\/Views\/TeraAddView.swift","character":1,"line":560},"ruleName":"File Length","ruleDescription":"Files should not span too many lines."}},{"text":"struct TeraSettingsView: View {","violation":{"severity":"error","ruleIdentifier":"type_body_length","reason":"Struct body should span 250 lines or less excluding comments and whitespace: currently spans 259 lines","location":{"file":"Tera\/Views\/TeraSupportingViews.swift","character":1,"line":365},"ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"}},{"text":"}","violation":{"severity":"error","ruleIdentifier":"file_length","reason":"File should contain 400 lines or less: currently contains 629","location":{"file":"Tera\/Views\/TeraSupportingViews.swift","character":1,"line":629},"ruleName":"File Length","ruleDescription":"Files should not span too many lines."}},{"text":"}","violation":{"severity":"error","ruleIdentifier":"file_length","reason":"File should contain 400 lines or less: currently contains 474","location":{"file":"Tera\/Views\/TeraTodayView.swift","character":1,"line":474},"ruleName":"File Length","ruleDescription":"Files should not span too many lines."}},{"text":"final class TeraAddStoreTests: XCTestCase {","violation":{"severity":"error","ruleIdentifier":"type_body_length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 665 lines","location":{"file":"TeraTests\/TeraAddStoreTests.swift","character":7,"line":6},"ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"}},{"text":"private actor AddBackend: TeraRuntimeBackend {","violation":{"severity":"error","ruleIdentifier":"type_body_length","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 492 lines","location":{"file":"TeraTests\/TeraAddStoreTests.swift","character":9,"line":844},"ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"}},{"text":" private func makeStatus(","violation":{"severity":"error","ruleIdentifier":"function_parameter_count","reason":"Function should have 5 parameters or less: it currently has 8","location":{"file":"TeraTests\/TeraAddStoreTests.swift","character":11,"line":1280},"ruleName":"Function Parameter Count","ruleDescription":"Number of function parameters should be low."}},{"text":"}","violation":{"severity":"error","ruleIdentifier":"file_length","reason":"File should contain 1000 lines or less: currently contains 1431","location":{"file":"TeraTests\/TeraAddStoreTests.swift","character":1,"line":1431},"ruleName":"File Length","ruleDescription":"Files should not span too many lines."}},{"text":"final class TeraRemoteQualificationTests: XCTestCase {","violation":{"severity":"error","ruleIdentifier":"type_body_length","reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 307 lines","location":{"file":"TeraTests\/TeraRemoteQualificationTests.swift","character":7,"line":7},"ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"}},{"text":" func testQualificationRejectsAmbiguousOrUnsafeHarnessValues() {","violation":{"severity":"error","ruleIdentifier":"function_body_length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 94 lines","location":{"file":"TeraTests\/TeraRemoteQualificationTests.swift","character":3,"line":45},"ruleName":"Function Body Length","ruleDescription":"Function bodies should not span too many lines"}},{"text":" func testAuthorizationEvidenceIsRedactedEphemeralAndRelaunchCleaned() throws {","violation":{"severity":"error","ruleIdentifier":"function_body_length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 53 lines","location":{"file":"TeraTests\/TeraRemoteQualificationTests.swift","character":3,"line":212},"ruleName":"Function Body Length","ruleDescription":"Function bodies should not span too many lines"}},{"text":"final class TeraRuntimeClientTests: XCTestCase {","violation":{"severity":"error","ruleIdentifier":"type_body_length","reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 298 lines","location":{"file":"TeraTests\/TeraRuntimeClientTests.swift","character":7,"line":4},"ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"}},{"text":"final class TeraStateMigrationTests: XCTestCase {","violation":{"severity":"error","ruleIdentifier":"type_body_length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 580 lines","location":{"file":"TeraTests\/TeraStateMigrationTests.swift","character":7,"line":6},"ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"}},{"text":" func testRelayValidationMatchesRuntimeProfiles() throws {","violation":{"severity":"error","ruleIdentifier":"function_body_length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 61 lines","location":{"file":"TeraTests\/TeraStateMigrationTests.swift","character":5,"line":7},"ruleName":"Function Body Length","ruleDescription":"Function bodies should not span too many lines"}},{"text":"}","violation":{"severity":"error","ruleIdentifier":"file_length","reason":"File should contain 400 lines or less: currently contains 622","location":{"file":"TeraTests\/TeraStateMigrationTests.swift","character":1,"line":622},"ruleName":"File Length","ruleDescription":"Files should not span too many lines."}},{"text":"private actor SupportingBackend: TeraRuntimeBackend {","violation":{"severity":"error","ruleIdentifier":"type_body_length","reason":"Actor body should span 250 lines or less excluding comments and whitespace: currently spans 253 lines","location":{"file":"TeraTests\/TeraSupportingStoreTests.swift","character":9,"line":207},"ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"}},{"text":"}","violation":{"severity":"error","ruleIdentifier":"file_length","reason":"File should contain 400 lines or less: currently contains 487","location":{"file":"TeraTests\/TeraSupportingStoreTests.swift","character":1,"line":487},"ruleName":"File Length","ruleDescription":"Files should not span too many lines."}},{"text":"final class TeraRemoteQualificationUITests: XCTestCase {","violation":{"severity":"error","ruleIdentifier":"type_body_length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 1493 lines","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":7,"line":4},"ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"}},{"text":" func testLocalSocialFiveFlowScenario() throws {","violation":{"severity":"error","ruleIdentifier":"function_body_length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 73 lines","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":3,"line":62},"ruleName":"Function Body Length","ruleDescription":"Function bodies should not span too many lines"}},{"text":" func testLocalSocialDeterministicPersonas() throws {","violation":{"severity":"error","ruleIdentifier":"function_body_length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 66 lines","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":3,"line":174},"ruleName":"Function Body Length","ruleDescription":"Function bodies should not span too many lines"}},{"text":" func testRemoteBlossomUploadAndRecovery() throws {","violation":{"severity":"error","ruleIdentifier":"function_body_length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 54 lines","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":3,"line":247},"ruleName":"Function Body Length","ruleDescription":"Function bodies should not span too many lines"}},{"text":" private func expectedAddFieldLabel(_ identifier: String, type: String) -> String {","violation":{"severity":"error","ruleIdentifier":"cyclomatic_complexity","reason":"Function should have complexity 10 or less; currently complexity is 18","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":11,"line":573},"ruleName":"Cyclomatic Complexity","ruleDescription":"Complexity of function bodies should be limited."}},{"text":" private func preparePhotoUpdate(_ app: XCUIApplication, marker: String) -> XCUIElement? {","violation":{"severity":"error","ruleIdentifier":"cyclomatic_complexity","reason":"Function should have complexity 10 or less; currently complexity is 11","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":11,"line":767},"ruleName":"Cyclomatic Complexity","ruleDescription":"Complexity of function bodies should be limited."}},{"text":" private func preparePhotoUpdate(_ app: XCUIApplication, marker: String) -> XCUIElement? {","violation":{"severity":"error","ruleIdentifier":"function_body_length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 66 lines","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":11,"line":767},"ruleName":"Function Body Length","ruleDescription":"Function bodies should not span too many lines"}},{"text":" static func environment(","violation":{"severity":"error","ruleIdentifier":"function_body_length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 67 lines","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":10,"line":1736},"ruleName":"Function Body Length","ruleDescription":"Function bodies should not span too many lines"}},{"text":"}","violation":{"severity":"error","ruleIdentifier":"file_length","reason":"File should contain 1000 lines or less: currently contains 2212","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":1,"line":2212},"ruleName":"File Length","ruleDescription":"Files should not span too many lines."}}] +\ No newline at end of file