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 d81684032d11bdc8b89b8613b6129873d1800e9b
parent a9977d1ec88e74fd0943dff1b04642cd98760db0
Author: triesap <tyson@radroots.org>
Date:   Thu, 10 Sep 2026 03:58:53 +0000

today: Bound rendering and cached query work

- Replace full history generation reads with bounded owner queries
- Preserve full detail while bounding feed text and thumbnails
- Supervise raster decoding with validated cache and worker budgets
- Qualify real SQLite performance and native resource boundaries

Diffstat:
MPackage.swift | 4++++
MTera.xcodeproj/project.pbxproj | 8++++++++
ATera/State/TeraMediaPresentationLimits.swift | 46++++++++++++++++++++++++++++++++++++++++++++++
MTera/State/TeraMediaStore.swift | 219+++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
ATera/State/TeraMediaThumbnail.swift | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ATera/State/TeraMediaWorkQueue.swift | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ATera/Views/TeraTodayCardPresentation.swift | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MTera/Views/TeraTodayDetailView.swift | 2+-
MTera/Views/TeraTodayView.swift | 26++++++++++++++------------
MTeraFFI/provenance.json | 52++++++++++++++++++++++++++--------------------------
MTeraFFI/source.lock | 4++--
MTeraFFI/source/aarch64-apple-darwin.json | 32++++++++++++++++++++++----------
MTeraFFI/source/aarch64-apple-ios-sim.json | 32++++++++++++++++++++++----------
MTeraFFI/source/aarch64-apple-ios.json | 32++++++++++++++++++++++----------
ATeraTests/TeraMediaBudgetTests.swift | 181+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ATeraTests/TeraTodayRenderingTests.swift | 44++++++++++++++++++++++++++++++++++++++++++++
Mcore/crates/tera_core/src/runtime/product_surface/today.rs | 51+++++++++++++++++++++++++++++++++++++--------------
Acore/crates/tera_core/src/runtime/product_surface/today_counted_storage_tests.rs | 500+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/crates/tera_core/src/runtime/product_surface/today_performance_tests.rs | 163+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcore/crates/tera_core/src/runtime/product_surface/today_reconciliation.rs | 2+-
Mproject.yml | 2++
Mrelease/provenance.json | 6+++---
Mtest-fixtures/legacy-identifiers.v1.json | 12++++++++++++
Mtest-fixtures/swiftlint-maintainability-baseline.v1.json | 4++--
24 files changed, 1445 insertions(+), 164 deletions(-)

diff --git a/Package.swift b/Package.swift @@ -90,6 +90,9 @@ let package = Package( "State/TeraConfigurationStore.swift", "State/TeraIdentityStore.swift", "State/TeraMediaStore.swift", + "State/TeraMediaPresentationLimits.swift", + "State/TeraMediaWorkQueue.swift", + "State/TeraMediaThumbnail.swift", "State/TeraSessionStore.swift", "State/TeraSessionBootstrap.swift", "State/TeraSupportingStores.swift", @@ -105,6 +108,7 @@ let package = Package( "Views/TeraTodayView.swift", "Views/TeraTodayCurrentDetail.swift", "Views/TeraTodayDetailView.swift", + "Views/TeraTodayCardPresentation.swift", "Views/TeraTodayStatusView.swift", "Views/TeraTodayPagingStatusView.swift", "Views/RuntimeStatusView.swift", diff --git a/Tera.xcodeproj/project.pbxproj b/Tera.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 095FBECA7268AA594F614EFA /* TeraScopeBackend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ED6D3E35C3235B6068E1902 /* TeraScopeBackend.swift */; }; 141327BC6BAE2CFA2C62C584 /* TeraTodayReconciliationTaskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EB21C33D6173E4FED05FDA0 /* TeraTodayReconciliationTaskTests.swift */; }; + 17805D95512D7144DAC59D85 /* TeraTodayRenderingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7283D1C8F6E90A236BC84AF6 /* TeraTodayRenderingTests.swift */; }; 1D45B3D566A0A60063190854 /* TeraApp in Frameworks */ = {isa = PBXBuildFile; productRef = E666175A800272732A27CBEB /* TeraApp */; }; 229C9AC991E67663DA53D339 /* TeraRuntimeBoundedTaskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A4FFC0023C620B68D0EC0ED /* TeraRuntimeBoundedTaskTests.swift */; }; 2479571FA99A7962794999C4 /* TeraSupportingStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0636AEDEB50DDDD328374A9 /* TeraSupportingStoreTests.swift */; }; @@ -39,6 +40,7 @@ 827D8CC5C0488F0D5D15CE99 /* TeraMediaOwnershipFFITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2521493B0CD2B82445974E81 /* TeraMediaOwnershipFFITests.swift */; }; 8422EAEECA73BB72974D002A /* TeraTodayBackfillTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D3FEE2830E056E82AF05FD /* TeraTodayBackfillTests.swift */; }; 852FF7514FF7F682A246FB47 /* TeraStateMigrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E969BCFF7CED33A1D4A040 /* TeraStateMigrationTests.swift */; }; + 85404A5125CF3E64E6009DC1 /* TeraMediaBudgetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F596D3A9B9E236963DCE15D7 /* TeraMediaBudgetTests.swift */; }; 912C8743116D929BB6A02778 /* TeraRuntimeClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB77C087F15C2F42CD959607 /* TeraRuntimeClientTests.swift */; }; 9384AAECB22885E2D05C54A8 /* TeraScopedMediaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFBAA57ABEAA4B84C73EE8E8 /* TeraScopedMediaTests.swift */; }; 9995BC15D67E1C9C29B4A7DC /* TeraTodayContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFECCBF9A84D1D65544C6094 /* TeraTodayContextTests.swift */; }; @@ -118,6 +120,7 @@ 6967B3EAC4CF1A410B26DD1A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; }; 6B8677670E203FF68EC88983 /* TeraRootShellTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraRootShellTests.swift; sourceTree = "<group>"; }; 714B4DD70B881D29A89812C9 /* TeraTodayReconciliationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraTodayReconciliationTests.swift; sourceTree = "<group>"; }; + 7283D1C8F6E90A236BC84AF6 /* TeraTodayRenderingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraTodayRenderingTests.swift; sourceTree = "<group>"; }; 73EBD10E201ECA26DCED523F /* TeraRuntimeInvalidationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraRuntimeInvalidationTests.swift; sourceTree = "<group>"; }; 74FCF1C4A8A7137623BBE223 /* TeraOpenedMediaCloseFixture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraOpenedMediaCloseFixture.swift; sourceTree = "<group>"; }; 794D475C70BA8ED994D1CA66 /* TeraTodayStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraTodayStoreTests.swift; sourceTree = "<group>"; }; @@ -154,6 +157,7 @@ EFBAA57ABEAA4B84C73EE8E8 /* TeraScopedMediaTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraScopedMediaTests.swift; sourceTree = "<group>"; }; EFECCBF9A84D1D65544C6094 /* TeraTodayContextTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraTodayContextTests.swift; sourceTree = "<group>"; }; F0A51F9AF2E0803264E07FBB /* TeraRuntimeResourceFixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraRuntimeResourceFixtures.swift; sourceTree = "<group>"; }; + F596D3A9B9E236963DCE15D7 /* TeraMediaBudgetTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraMediaBudgetTests.swift; sourceTree = "<group>"; }; F65B01F6F5F7E125148EC4FD /* TeraTodayReconciliationFFITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraTodayReconciliationFFITests.swift; sourceTree = "<group>"; }; FB77C087F15C2F42CD959607 /* TeraRuntimeClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraRuntimeClientTests.swift; sourceTree = "<group>"; }; /* End PBXFileReference section */ @@ -227,6 +231,7 @@ 2A506546B0EC1792240C36F7 /* TeraClockTests.swift */, 93A3358AFC23C567DB9CB02F /* TeraErrorRecoveryTests.swift */, C890BDE885536BC2AB215393 /* TeraLifecycleTests.swift */, + F596D3A9B9E236963DCE15D7 /* TeraMediaBudgetTests.swift */, D3C914B349DAE2EF66658229 /* TeraMediaFileFixture.swift */, 2521493B0CD2B82445974E81 /* TeraMediaOwnershipFFITests.swift */, 61B6F0B16BFD8F046E9C163F /* TeraMediaStoreTests.swift */, @@ -267,6 +272,7 @@ F65B01F6F5F7E125148EC4FD /* TeraTodayReconciliationFFITests.swift */, 5EB21C33D6173E4FED05FDA0 /* TeraTodayReconciliationTaskTests.swift */, 714B4DD70B881D29A89812C9 /* TeraTodayReconciliationTests.swift */, + 7283D1C8F6E90A236BC84AF6 /* TeraTodayRenderingTests.swift */, 794D475C70BA8ED994D1CA66 /* TeraTodayStoreTests.swift */, 5854504D0CAD5ABC29302292 /* TeraTodaySyncFixtures.swift */, 9BC55E41FBE589010038FDA5 /* TeraTodaySyncReceiptTests.swift */, @@ -487,6 +493,7 @@ 70417FB635F06CC9829FB38C /* TeraClockTests.swift in Sources */, D918A664D9A4B9859A1A6503 /* TeraErrorRecoveryTests.swift in Sources */, 671EF50DB4EFBC25AABF26DC /* TeraLifecycleTests.swift in Sources */, + 85404A5125CF3E64E6009DC1 /* TeraMediaBudgetTests.swift in Sources */, D64AC2FDC6FE05A39114D2DB /* TeraMediaFileFixture.swift in Sources */, 827D8CC5C0488F0D5D15CE99 /* TeraMediaOwnershipFFITests.swift in Sources */, CDBE38E6B521611436F28858 /* TeraMediaStoreTests.swift in Sources */, @@ -527,6 +534,7 @@ C0C7D6F0CBCE262C348D3C3E /* TeraTodayReconciliationFFITests.swift in Sources */, 141327BC6BAE2CFA2C62C584 /* TeraTodayReconciliationTaskTests.swift in Sources */, D86A417A36A2186AB7777753 /* TeraTodayReconciliationTests.swift in Sources */, + 17805D95512D7144DAC59D85 /* TeraTodayRenderingTests.swift in Sources */, 64D49ADFD3FDD54CBA509DC5 /* TeraTodayStoreTests.swift in Sources */, E4E85524E39F13D6C76650F1 /* TeraTodaySyncFixtures.swift in Sources */, DBA4B403D65289242AFDDB07 /* TeraTodaySyncReceiptTests.swift in Sources */, diff --git a/Tera/State/TeraMediaPresentationLimits.swift b/Tera/State/TeraMediaPresentationLimits.swift @@ -0,0 +1,46 @@ +import Foundation + +/// Presentation budgets only. Eviction never mutates an authored artifact. +struct TeraMediaPresentationLimits: Sendable, Equatable { + let encodedBytes: Int + let sourcePixels: Int + let thumbnailDimension: Int + let cacheBytes: Int + let cacheEntries: Int + let workers: Int + let queuedRequests: Int + let visibilityEntries: Int + + static let standard: Self = { + guard let limits = Self(encodedBytes: 8 * 1024 * 1024, sourcePixels: 16_000_000, + thumbnailDimension: 1024, cacheBytes: 32 * 1024 * 1024, cacheEntries: 64, + workers: 2, queuedRequests: 16, visibilityEntries: 4096) + else { + preconditionFailure("Bundled media presentation limits must be valid") + } + return limits + }() + + init?(encodedBytes: Int, sourcePixels: Int, thumbnailDimension: Int, cacheBytes: Int, + cacheEntries: Int, workers: Int, queuedRequests: Int, visibilityEntries: Int) + { + guard (1 ... 8 * 1024 * 1024).contains(encodedBytes), + (1 ... 16_000_000).contains(sourcePixels), (1 ... 1024).contains(thumbnailDimension), + (1 ... 32 * 1024 * 1024).contains(cacheBytes), (1 ... 64).contains(cacheEntries), + (1 ... 2).contains(workers), (1 ... 16).contains(queuedRequests), + (1 ... 4096).contains(visibilityEntries), workers + queuedRequests <= cacheEntries else { return nil } + self.encodedBytes = encodedBytes + self.sourcePixels = sourcePixels + self.thumbnailDimension = thumbnailDimension + self.cacheBytes = cacheBytes + self.cacheEntries = cacheEntries + self.workers = workers + self.queuedRequests = queuedRequests + self.visibilityEntries = visibilityEntries + } + + func accepts(byteCount: Int, width: Int, height: Int) -> Bool { + byteCount > 0 && byteCount <= encodedBytes && width > 0 && height > 0 + && width <= sourcePixels / height + } +} diff --git a/Tera/State/TeraMediaStore.swift b/Tera/State/TeraMediaStore.swift @@ -36,34 +36,50 @@ final class TeraMediaStore: ObservableObject { let reference: String } - private struct Work { - let id: UUID - let task: Task<Void, Never> + private struct CachedImage { + let image: UIImage + let cost: Int } @Published private var states: [Key: TeraMediaPresentationState] = [:] - private let runtimeClient: TeraRuntimeClient - private var tasks: [Key: Work] = [:] + private let limits: TeraMediaPresentationLimits + private let work: TeraMediaWorkQueue + private var owners: [Key: UUID] = [:] + private var images: [Key: CachedImage] = [:] + private var recent: [Key] = [] private var revoked = Set<Key>() + private var visibilityOverflow = false + private var reauthorized = Set<Key>() private var configuration: TeraPresentationConfiguration? - init(runtimeClient: TeraRuntimeClient) { + init(runtimeClient: TeraRuntimeClient, limits: TeraMediaPresentationLimits = .standard) { self.runtimeClient = runtimeClient + self.limits = limits + work = TeraMediaWorkQueue(limits: limits) } - deinit { - for task in tasks.values { - task.task.cancel() - } + var cachedByteCount: Int { + images.values.reduce(0) { $0 + $1.cost } } - func state( - for media: TeraMediaReference, - context: TeraLocalNetwork? - ) -> TeraMediaPresentationState { + var stateCount: Int { + states.count + } + + func image(for media: TeraMediaReference, context: TeraLocalNetwork?) -> UIImage? { + guard let context else { return nil } + let key = key(media: media, context: context) + guard allowed(key) else { return nil } + touch(key) + return images[key]?.image + } + + func state(for media: TeraMediaReference, context: TeraLocalNetwork?) -> TeraMediaPresentationState { guard let context else { return .unavailable } - if let state = states[key(media: media, context: context)] { + let key = key(media: media, context: context) + guard allowed(key) else { return .unavailable } + if let state = states[key] { return state } switch media.verification { @@ -76,22 +92,14 @@ final class TeraMediaStore: ObservableObject { func load(media: TeraMediaReference, context: TeraLocalNetwork?) { guard let context else { return } let key = key(media: media, context: context) - guard !revoked.contains(key), states[key] == nil, tasks[key] == nil else { return } + guard allowed(key), states[key] == nil, owners[key] == nil else { return } switch media.verification { - case .pending: - states[key] = .pending - case .failed: - states[key] = .failed + case .pending: set(.pending, for: key) + case .failed: set(.failed, for: key) case .verified: - guard let artifactID = media.verifiedArtifactID else { - states[key] = .corrupt - return - } + guard let artifactID = media.verifiedArtifactID else { set(.corrupt, for: key); return } start(key: key, context: context) { [runtimeClient] in - try await runtimeClient.verifiedMediaArtifact( - context: context, - artifactID: artifactID - ) + try await runtimeClient.verifiedMediaArtifact(context: context, artifactID: artifactID) } case .unavailable: start(key: key, context: context) { [runtimeClient] in @@ -103,40 +111,58 @@ final class TeraMediaStore: ObservableObject { func retry(media: TeraMediaReference, context: TeraLocalNetwork?) { guard let context else { return } let key = key(media: media, context: context) - guard !revoked.contains(key) else { return } - tasks[key]?.task.cancel() - tasks[key] = nil - states[key] = nil + guard allowed(key) else { return } + cancel(key) start(key: key, context: context) { [runtimeClient] in try await runtimeClient.retrieveMedia(context: context, reference: media) } } - func reconcileVisibility( - previous: [TeraMediaReference], current: [TeraMediaReference], context: TeraLocalNetwork? - ) { + func reconcileVisibility(previous: [TeraMediaReference], current: [TeraMediaReference], + context: TeraLocalNetwork?) + { guard let context else { return } - let allowed = Set(current.map { key(media: $0, context: context) }) - for key in allowed where revoked.remove(key) != nil { - states[key] = nil + let permitted = Set(current.prefix(limits.visibilityEntries).map { key(media: $0, context: context) }) + if current.count > limits.visibilityEntries { + visibilityOverflow = true + } + reauthorized = permitted + for key in permitted where revoked.remove(key) != nil { + remove(key) } for reference in previous { let key = key(media: reference, context: context) - guard !allowed.contains(key) else { continue } - revoked.insert(key) - tasks[key]?.task.cancel() - tasks[key] = nil - states[key] = .unavailable + guard !permitted.contains(key) else { continue } + if !revoked.contains(key) { + if revoked.count < limits.visibilityEntries { + revoked.insert(key) + } else { + visibilityOverflow = true + } + } + cancel(key) + } + // Under admission pressure only the bounded, explicitly current window may + // load. Forgetting an old denial must never authorize a stale reference. + if visibilityOverflow { + for key in Array(owners.keys) where !allowed(key) { + cancel(key) + } + for key in Array(states.keys) where !allowed(key) { + remove(key) + } } } func reset() { - for task in tasks.values { - task.task.cancel() - } - tasks.removeAll(keepingCapacity: false) - states.removeAll(keepingCapacity: false) - revoked.removeAll(keepingCapacity: false) + work.cancelAll() + owners.removeAll() + states.removeAll() + images.removeAll() + recent.removeAll() + revoked.removeAll() + reauthorized.removeAll() + visibilityOverflow = false } func configure(snapshot: TeraRuntimeSnapshot) { @@ -146,46 +172,93 @@ final class TeraMediaStore: ObservableObject { configuration = updated } - private func start( - key: Key, - context: TeraLocalNetwork, - operation: @escaping @Sendable () async throws -> TeraVerifiedMediaArtifact? - ) { - states[key] = .loading + private func start(key: Key, context: TeraLocalNetwork, + operation: @escaping @Sendable () async throws -> TeraVerifiedMediaArtifact?) + { let id = UUID() - let task = Task { [weak self] in + owners[key] = id + set(.loading, for: key) + let accepted = work.submit(id: id) { [weak self, runtimeClient, limits] in do { - let artifact = try await operation() - guard let self, isCurrent(key: key, id: id) else { return } - guard let artifact else { - complete(key: key, id: id, state: .unavailable) + try Task.checkCancellation() + guard let artifact = try await operation() else { + self?.complete(key: key, id: id, state: .unavailable) return } - guard UIImage(data: artifact.bytes) != nil else { - _ = try? await runtimeClient.invalidateMediaArtifact( - context: context, artifactID: artifact.artifactID - ) - complete(key: key, id: id, state: .corrupt) - return + guard self?.isCurrent(key, id) == true else { return } + do { + let image = try await TeraMediaThumbnail.prepare(artifact, limits: limits) + guard let self, isCurrent(key, id) else { return } + let cost = artifact.bytes.count + image.bytesPerRow * image.height + guard cost <= limits.cacheBytes else { + complete(key: key, id: id, state: .unavailable) + return + } + images[key] = CachedImage(image: UIImage(cgImage: image), cost: cost) + complete(key: key, id: id, state: .ready(artifact)) + } catch TeraMediaThumbnailFailure.corrupt { + guard self?.isCurrent(key, id) == true else { return } + _ = try? await runtimeClient.invalidateMediaArtifact(context: context, artifactID: artifact.artifactID) + self?.complete(key: key, id: id, state: .corrupt) + } catch TeraMediaThumbnailFailure.resourceLimit { + self?.complete(key: key, id: id, state: .unavailable) } - complete(key: key, id: id, state: .ready(artifact)) } catch is CancellationError { self?.complete(key: key, id: id, state: nil) } catch { self?.complete(key: key, id: id, state: Self.failureState(error)) } } - tasks[key] = Work(id: id, task: task) + if !accepted { + owners[key] = nil; set(.unavailable, for: key) + } } - private func isCurrent(key: Key, id: UUID) -> Bool { - tasks[key]?.id == id && !Task.isCancelled + private func isCurrent(_ key: Key, _ id: UUID) -> Bool { + owners[key] == id && allowed(key) && !Task.isCancelled } private func complete(key: Key, id: UUID, state: TeraMediaPresentationState?) { - guard tasks[key]?.id == id else { return } - tasks[key] = nil - states[key] = Task.isCancelled ? nil : state + guard owners[key] == id else { return } + owners[key] = nil + if Task.isCancelled || !allowed(key) { + remove(key) + } else { + set(state, for: key) + } + } + + private func allowed(_ key: Key) -> Bool { + visibilityOverflow ? reauthorized.contains(key) : !revoked.contains(key) + } + + private func cancel(_ key: Key) { + if let id = owners.removeValue(forKey: key) { + work.cancel(id: id) + } + remove(key) + } + + private func remove(_ key: Key) { + states[key] = nil + images[key] = nil + recent.removeAll { $0 == key } + } + + private func touch(_ key: Key) { + guard states[key] != nil else { return } + recent.removeAll { $0 == key } + recent.append(key) + } + + private func set(_ state: TeraMediaPresentationState?, for key: Key) { + guard let state else { remove(key); return } + states[key] = state + touch(key) + while states.count > limits.cacheEntries || cachedByteCount > limits.cacheBytes { + guard let oldest = recent.first(where: { owners[$0] == nil }) else { break } + remove(oldest) + } } private func key(media: TeraMediaReference, context: TeraLocalNetwork) -> Key { diff --git a/Tera/State/TeraMediaThumbnail.swift b/Tera/State/TeraMediaThumbnail.swift @@ -0,0 +1,57 @@ +import Foundation +import ImageIO + +nonisolated enum TeraMediaThumbnailFailure: Error { + case corrupt + case resourceLimit +} + +nonisolated enum TeraMediaThumbnail { + /// The caller owns this task and its worker slot through decode completion. + static func prepare(_ artifact: TeraVerifiedMediaArtifact, + limits: TeraMediaPresentationLimits) async throws -> CGImage + { + let task = Task.detached(priority: .utility) { + try decode(artifact, limits: limits) + } + return try await withTaskCancellationHandler { + try await task.value + } onCancel: { + task.cancel() + } + } + + private static func decode(_ artifact: TeraVerifiedMediaArtifact, + limits: TeraMediaPresentationLimits) throws -> CGImage + { + assert(!Thread.isMainThread, "Raster decoding must stay off the UI thread") + try Task.checkCancellation() + guard limits.accepts(byteCount: artifact.bytes.count, width: Int(artifact.width), + height: Int(artifact.height)) else { throw TeraMediaThumbnailFailure.resourceLimit } + guard let source = CGImageSourceCreateWithData(artifact.bytes as CFData, + [kCGImageSourceShouldCache: false] as CFDictionary), + let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any], + let width = properties[kCGImagePropertyPixelWidth] as? Int, + let height = properties[kCGImagePropertyPixelHeight] as? Int, + width == Int(artifact.width), height == Int(artifact.height) + else { + throw TeraMediaThumbnailFailure.corrupt + } + guard limits.accepts(byteCount: artifact.bytes.count, width: width, height: height) else { + throw TeraMediaThumbnailFailure.resourceLimit + } + let options: [CFString: Any] = [ + kCGImageSourceCreateThumbnailFromImageAlways: true, + kCGImageSourceThumbnailMaxPixelSize: limits.thumbnailDimension, + kCGImageSourceCreateThumbnailWithTransform: true, + kCGImageSourceShouldCacheImmediately: true, + ] + guard let image = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary), + image.width <= limits.thumbnailDimension, image.height <= limits.thumbnailDimension + else { + throw TeraMediaThumbnailFailure.corrupt + } + try Task.checkCancellation() + return image + } +} diff --git a/Tera/State/TeraMediaWorkQueue.swift b/Tera/State/TeraMediaWorkQueue.swift @@ -0,0 +1,61 @@ +import Foundation + +/// Cancelled work retains its physical slot until its operation returns. +@MainActor +final class TeraMediaWorkQueue { + private struct Pending { + let id: UUID + let operation: @MainActor () async -> Void + } + + private let limits: TeraMediaPresentationLimits + private var active: [UUID: Task<Void, Never>] = [:] + private var pending: [Pending] = [] + + init(limits: TeraMediaPresentationLimits) { + self.limits = limits + } + + deinit { for task in active.values { + task.cancel() + } } + + var activeCount: Int { + active.count + } + + var queuedCount: Int { + pending.count + } + + func submit(id: UUID, operation: @escaping @MainActor () async -> Void) -> Bool { + guard active[id] == nil, !pending.contains(where: { $0.id == id }), + active.count < limits.workers || pending.count < limits.queuedRequests else { return false } + pending.append(Pending(id: id, operation: operation)) + drain() + return true + } + + func cancel(id: UUID) { + pending.removeAll { $0.id == id } + active[id]?.cancel() + } + + func cancelAll() { + pending.removeAll() + for task in active.values { + task.cancel() + } + } + + private func drain() { + while active.count < limits.workers, !pending.isEmpty { + let work = pending.removeFirst() + active[work.id] = Task { [weak self] in + await work.operation() + self?.active[work.id] = nil + self?.drain() + } + } + } +} diff --git a/Tera/Views/TeraTodayCardPresentation.swift b/Tera/Views/TeraTodayCardPresentation.swift @@ -0,0 +1,69 @@ +import Foundation + +enum TeraTodayCardPresentation { + case feed + case detail + + static let excerptCharacters = 640 + static let excerptBytes = 4096 + static let labelCharacters = 160 + static let labelBytes = 1024 + static let thumbnails = 3 + + func content(_ value: String) -> String { + self == .detail ? value : Self.excerpt(value, characters: Self.excerptCharacters, bytes: Self.excerptBytes) + } + + func label(_ value: String) -> String { + self == .detail ? value : Self.excerpt(value, characters: Self.labelCharacters, bytes: Self.labelBytes) + } + + func media(_ values: [TeraMediaReference]) -> [TeraMediaReference] { + self == .detail ? values : Array(values.prefix(Self.thumbnails)) + } + + func accessibility(_ card: TeraTodayCard) -> String { + if self == .detail { + return card.accessibilitySummary + } + var parts = [card.type.label, "by \(label(card.authorName))"] + if let title = card.title { + parts.append(label(title)) + } + parts.append(content(card.content)) + if let price = card.priceSummary { + parts.append(label(price)) + } + if card.lifecycle != .active { + parts.append(card.lifecycle.rawValue) + } + if let operation = card.localOperationState { + parts.append(label(operation)) + } + return parts.joined(separator: ", ") + } + + /// Bound bytes before walking graphemes: a single combining sequence can be + /// arbitrarily long. Drop an incomplete final cluster rather than split it. + static func excerpt(_ value: String, characters: Int, bytes: Int) -> String { + precondition(characters > 0 && bytes >= 3) + let prefix = Array(value.utf8.prefix(bytes + 1)) + let byteTruncated = prefix.count > bytes + var bounded: String + if byteTruncated { + var encoded = Array(prefix.prefix(bytes - 3)) + while String(bytes: encoded, encoding: .utf8) == nil { + encoded.removeLast() + } + bounded = String(bytes: encoded, encoding: .utf8) ?? "" + if !bounded.isEmpty { + bounded.removeLast() + } + } else { + bounded = value + } + let clusterPrefix = bounded.prefix(characters + 1) + let truncated = byteTruncated || clusterPrefix.count > characters + return truncated ? String(clusterPrefix.prefix(characters - 1)) + "…" : String(clusterPrefix) + } +} diff --git a/Tera/Views/TeraTodayDetailView.swift b/Tera/Views/TeraTodayDetailView.swift @@ -13,7 +13,7 @@ struct TeraTodayDetailView: View { var body: some View { List { Section { - TeraTodayCardView(card: card, context: context, mediaStore: mediaStore) + TeraTodayCardView(card: card, context: context, mediaStore: mediaStore, presentation: .detail) } if !card.thread.isEmpty { Section("Conversation") { diff --git a/Tera/Views/TeraTodayView.swift b/Tera/Views/TeraTodayView.swift @@ -194,10 +194,12 @@ struct TeraTodayCardView: View { let context: TeraLocalNetwork? @ObservedObject var mediaStore: TeraMediaStore + var presentation: TeraTodayCardPresentation = .feed + var body: some View { VStack(alignment: .leading, spacing: 12) { HStack(alignment: .firstTextBaseline) { - Text(card.authorName) + Text(presentation.label(card.authorName)) .font(.subheadline.weight(.semibold)) Spacer() Text(card.type.label) @@ -208,11 +210,11 @@ struct TeraTodayCardView: View { } if let title = card.title { - Text(title) + Text(presentation.label(title)) .font(.headline) } if !card.content.isEmpty { - Text(card.content) + Text(presentation.content(card.content)) .font(card.type == .ask ? .headline : .body) } @@ -223,7 +225,7 @@ struct TeraTodayCardView: View { foodMetadata } - ForEach(card.media) { media in + ForEach(presentation.media(card.media)) { media in TeraTrustedMediaView(media: media, context: context, store: mediaStore) } @@ -246,7 +248,7 @@ struct TeraTodayCardView: View { } .padding(.vertical, 8) .accessibilityElement(children: .combine) - .accessibilityLabel(card.accessibilitySummary) + .accessibilityLabel(presentation.accessibility(card)) } private var eventMetadata: some View { @@ -260,7 +262,7 @@ struct TeraTodayCardView: View { ) } if let location = card.location { - Label(location, systemImage: "mappin.and.ellipse") + Label(presentation.label(location), systemImage: "mappin.and.ellipse") } } .font(.subheadline) @@ -281,13 +283,13 @@ struct TeraTodayCardView: View { @ViewBuilder private var foodMetadataLabels: some View { if let price = card.priceSummary { - Label(price, systemImage: "tag") + Label(presentation.label(price), systemImage: "tag") } if let quantity = card.quantity, let unit = card.priceUnit { - Label("\(quantity) \(unit) available", systemImage: "basket") + Label(presentation.label("\(quantity) \(unit) available"), systemImage: "basket") } if let location = card.location { - Label(location, systemImage: "mappin.and.ellipse") + Label(presentation.label(location), systemImage: "mappin.and.ellipse") } } } @@ -314,8 +316,8 @@ struct TeraLocalMediaContent: View { let state = store.state(for: media, context: context) Group { switch state { - case let .ready(artifact): - if let image = UIImage(data: artifact.bytes) { + case .ready: + if let image = store.image(for: media, context: context) { Image(uiImage: image) .resizable() .scaledToFill() @@ -366,6 +368,6 @@ struct TeraLocalMediaContent: View { private func accessibilityLabel(for state: TeraMediaPresentationState) -> String { guard let alt = media.alt, !alt.isEmpty else { return state.accessibilityLabel } - return "\(alt). \(state.accessibilityLabel)" + return "\(TeraTodayCardPresentation.feed.label(alt)). \(state.accessibilityLabel)" } } diff --git a/TeraFFI/provenance.json b/TeraFFI/provenance.json @@ -22,9 +22,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 71475528, + "bytes": 71483336, "path": "TeraFFI.xcframework/ios-arm64-simulator/libtera_ffi.a", - "sha256": "fc75faf80679711dc0826952a756e15a55749c03b0297982fbe99c24983bea1a" + "sha256": "df300d70e6f32b68f5ac4366b3342a692c8d8a3ba243965d9c9ad60634d897ff" }, { "bytes": 71393, @@ -37,9 +37,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 71528104, + "bytes": 71534912, "path": "TeraFFI.xcframework/ios-arm64/libtera_ffi.a", - "sha256": "25c2115bfd6c7f581111c6b20bab43bdc9b514cbd82aca0c7717796393345967" + "sha256": "b6a0d44f19783376dd493b50bdc06ed61baf76c9e7a9b47b730a55198c5d22ae" }, { "bytes": 42544, @@ -79,32 +79,32 @@ { "bytes": 19948832, "path": "native/aarch64-apple-darwin/libtera_ffi.dylib", - "sha256": "18f8312d4ec4e6f45aca1b5f6698ae1cb13cb7957ce064028aff529c64e5f402" + "sha256": "eb9be3233d95cfce09cc7e64a84e7a5b9d83d10ae1132efb57263c02165441e3" }, { - "bytes": 71475528, + "bytes": 71483336, "path": "native/aarch64-apple-ios-sim/libtera_ffi.a", - "sha256": "fc75faf80679711dc0826952a756e15a55749c03b0297982fbe99c24983bea1a" + "sha256": "df300d70e6f32b68f5ac4366b3342a692c8d8a3ba243965d9c9ad60634d897ff" }, { - "bytes": 71528104, + "bytes": 71534912, "path": "native/aarch64-apple-ios/libtera_ffi.a", - "sha256": "25c2115bfd6c7f581111c6b20bab43bdc9b514cbd82aca0c7717796393345967" + "sha256": "b6a0d44f19783376dd493b50bdc06ed61baf76c9e7a9b47b730a55198c5d22ae" }, { - "bytes": 66342, + "bytes": 66937, "path": "source/aarch64-apple-darwin.json", - "sha256": "402223883665871ba2fb4a229a5483a1d52948f7e0855dca3436368683ac5f61" + "sha256": "06721ef698bd79864cc5a690d0bd027ea66cd6079b739b834a79c143abf65170" }, { - "bytes": 66186, + "bytes": 66781, "path": "source/aarch64-apple-ios-sim.json", - "sha256": "5eb6c319a529465870f04ca46f7e247c8694e5f5b4f14562d951ab58cbe26f62" + "sha256": "e26e968883e963e27ec3dbe1b077d9d85f818a894951bd643902ea6c81e11219" }, { - "bytes": 66182, + "bytes": 66777, "path": "source/aarch64-apple-ios.json", - "sha256": "764e8b804218f1bd1b7993f42a31ae621f8c3ccb32a9ce2f0adf1b363d6cc11f" + "sha256": "9c651b6591d8d0caf19970ed2fddb9eb53132abc749a310e99896b570458386d" } ], "language": "swift", @@ -112,7 +112,7 @@ "schema": "radroots.artifact-manifest.v2", "source": { "repository": "https://github.com/radrootslabs/tera", - "tree": "3ff40c5420af5cfa8f2726bf65285c69a5a0386e" + "tree": "703c94b59618f9ca1dd16c17df8fb07fb7b860bd" }, "source_records": { "aarch64-apple-darwin": "source/aarch64-apple-darwin.json", @@ -139,9 +139,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 71475528, + "bytes": 71483336, "path": "Tera/Frameworks/TeraFFI.xcframework/ios-arm64-simulator/libtera_ffi.a", - "sha256": "fc75faf80679711dc0826952a756e15a55749c03b0297982fbe99c24983bea1a" + "sha256": "df300d70e6f32b68f5ac4366b3342a692c8d8a3ba243965d9c9ad60634d897ff" }, { "bytes": 71393, @@ -154,9 +154,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 71528104, + "bytes": 71534912, "path": "Tera/Frameworks/TeraFFI.xcframework/ios-arm64/libtera_ffi.a", - "sha256": "25c2115bfd6c7f581111c6b20bab43bdc9b514cbd82aca0c7717796393345967" + "sha256": "b6a0d44f19783376dd493b50bdc06ed61baf76c9e7a9b47b730a55198c5d22ae" }, { "bytes": 512103, @@ -174,19 +174,19 @@ "sha256": "4cc03b03dd54db7b4ab22684ff819233444ae0a43a770ab875148892bed1e3f2" }, { - "bytes": 66342, + "bytes": 66937, "path": "TeraFFI/source/aarch64-apple-darwin.json", - "sha256": "402223883665871ba2fb4a229a5483a1d52948f7e0855dca3436368683ac5f61" + "sha256": "06721ef698bd79864cc5a690d0bd027ea66cd6079b739b834a79c143abf65170" }, { - "bytes": 66186, + "bytes": 66781, "path": "TeraFFI/source/aarch64-apple-ios-sim.json", - "sha256": "5eb6c319a529465870f04ca46f7e247c8694e5f5b4f14562d951ab58cbe26f62" + "sha256": "e26e968883e963e27ec3dbe1b077d9d85f818a894951bd643902ea6c81e11219" }, { - "bytes": 66182, + "bytes": 66777, "path": "TeraFFI/source/aarch64-apple-ios.json", - "sha256": "764e8b804218f1bd1b7993f42a31ae621f8c3ccb32a9ce2f0adf1b363d6cc11f" + "sha256": "9c651b6591d8d0caf19970ed2fddb9eb53132abc749a310e99896b570458386d" } ], "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 = "3ff40c5420af5cfa8f2726bf65285c69a5a0386e" -manifest_sha256 = "9061000a642f27b7ae4a4c8fb91e9d0be789ca4badedbecece5f18b0fdd3bd73" +source_tree = "703c94b59618f9ca1dd16c17df8fb07fb7b860bd" +manifest_sha256 = "f174e6196e6b8ffdadaabcf52c6d697ee7ba9e3acc55ff265cd928c1131d4010" source_date_epoch = 1787871027 [foundation] diff --git a/TeraFFI/source/aarch64-apple-darwin.json b/TeraFFI/source/aarch64-apple-darwin.json @@ -721,10 +721,10 @@ "sha256": "76e3bddfc237dac63293d264d68991b489585136fe377e39dbba72aa3a1181f4" }, "core/crates/tera_core/src/runtime/product_surface/today.rs": { - "bytes": 140031, - "git_blob": "eaf6fd526b5d5ed8bc1d85f1d365689424bb12ed", + "bytes": 140918, + "git_blob": "c8be6532fdb641bb5d0056cad8e07bd7ed6ae278", "mode": "100644", - "sha256": "6f5c6df78c457e87bc02fcf6759b241ee81bfdbb67ac4cd35b2a02bea4b350d2" + "sha256": "1c76aafbd3d7ed373579a8658d47f9208059d2578db466e11d4148da663d4e99" }, "core/crates/tera_core/src/runtime/product_surface/today_admission_tests.rs": { "bytes": 14686, @@ -744,17 +744,29 @@ "mode": "100644", "sha256": "6f5810256ca221ca66ec4056e07fc635103e0ed02da01f5a8c53161535d7eaf6" }, + "core/crates/tera_core/src/runtime/product_surface/today_counted_storage_tests.rs": { + "bytes": 17547, + "git_blob": "de61bd9c5fefa9cabf4818d84f9498564a6e5790", + "mode": "100644", + "sha256": "31590402860e49ec6a980b5822ab129207852ba59d9516bd7d23952071fe2a88" + }, "core/crates/tera_core/src/runtime/product_surface/today_paging_scope.rs": { "bytes": 1703, "git_blob": "cc1fb36406b9ad236a5653bbd59d8256dd91bd19", "mode": "100644", "sha256": "e9ea1a0ee6c52031895184cd0a2b3c526318fd521ab2bd6b1b08ef53f3c9ff66" }, + "core/crates/tera_core/src/runtime/product_surface/today_performance_tests.rs": { + "bytes": 5938, + "git_blob": "70a56d607ba4e59305e4f7488b20e509764fbee8", + "mode": "100644", + "sha256": "1158622c28cca9b6843b48199f4eb6d286a01cc15d7530e11b0fa25a2e00b93c" + }, "core/crates/tera_core/src/runtime/product_surface/today_reconciliation.rs": { - "bytes": 2055, - "git_blob": "d615a82f5e06bfcae0582bb2069b0cf168caee9d", + "bytes": 2036, + "git_blob": "a85b9bc3a43885415147fef2d9b9a57338ed7427", "mode": "100644", - "sha256": "df4e51923b6e352faf7ce6590b6e969e4853cb9d3d0a906892d0a6d50c60b51f" + "sha256": "2000088c53e5e34b95d88e6de1615631a3ecb2dabf36e831f504dd0791afc5de" }, "core/crates/tera_core/src/runtime/product_surface/today_reconciliation_tests.rs": { "bytes": 3424, @@ -1429,13 +1441,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 116498, - "git_blob": "4a0af9069c1be7f07205d40c2272d7f1d417ddf4", + "bytes": 116926, + "git_blob": "c3a54831006843505ce452a194ab72352dbac12a", "mode": "100644", - "sha256": "3db541aae6722c0ebfacdbceec33a0c8415d17b9b915d8a3a3a97217ed7f52e5" + "sha256": "20f5bc3bdcfaaa8eb6816543849e75107c498a52e45399f3f84314e5ecf6c8f0" } }, "policy": "staged_inputs", - "tree": "3ff40c5420af5cfa8f2726bf65285c69a5a0386e" + "tree": "703c94b59618f9ca1dd16c17df8fb07fb7b860bd" } } diff --git a/TeraFFI/source/aarch64-apple-ios-sim.json b/TeraFFI/source/aarch64-apple-ios-sim.json @@ -717,10 +717,10 @@ "sha256": "76e3bddfc237dac63293d264d68991b489585136fe377e39dbba72aa3a1181f4" }, "core/crates/tera_core/src/runtime/product_surface/today.rs": { - "bytes": 140031, - "git_blob": "eaf6fd526b5d5ed8bc1d85f1d365689424bb12ed", + "bytes": 140918, + "git_blob": "c8be6532fdb641bb5d0056cad8e07bd7ed6ae278", "mode": "100644", - "sha256": "6f5c6df78c457e87bc02fcf6759b241ee81bfdbb67ac4cd35b2a02bea4b350d2" + "sha256": "1c76aafbd3d7ed373579a8658d47f9208059d2578db466e11d4148da663d4e99" }, "core/crates/tera_core/src/runtime/product_surface/today_admission_tests.rs": { "bytes": 14686, @@ -740,17 +740,29 @@ "mode": "100644", "sha256": "6f5810256ca221ca66ec4056e07fc635103e0ed02da01f5a8c53161535d7eaf6" }, + "core/crates/tera_core/src/runtime/product_surface/today_counted_storage_tests.rs": { + "bytes": 17547, + "git_blob": "de61bd9c5fefa9cabf4818d84f9498564a6e5790", + "mode": "100644", + "sha256": "31590402860e49ec6a980b5822ab129207852ba59d9516bd7d23952071fe2a88" + }, "core/crates/tera_core/src/runtime/product_surface/today_paging_scope.rs": { "bytes": 1703, "git_blob": "cc1fb36406b9ad236a5653bbd59d8256dd91bd19", "mode": "100644", "sha256": "e9ea1a0ee6c52031895184cd0a2b3c526318fd521ab2bd6b1b08ef53f3c9ff66" }, + "core/crates/tera_core/src/runtime/product_surface/today_performance_tests.rs": { + "bytes": 5938, + "git_blob": "70a56d607ba4e59305e4f7488b20e509764fbee8", + "mode": "100644", + "sha256": "1158622c28cca9b6843b48199f4eb6d286a01cc15d7530e11b0fa25a2e00b93c" + }, "core/crates/tera_core/src/runtime/product_surface/today_reconciliation.rs": { - "bytes": 2055, - "git_blob": "d615a82f5e06bfcae0582bb2069b0cf168caee9d", + "bytes": 2036, + "git_blob": "a85b9bc3a43885415147fef2d9b9a57338ed7427", "mode": "100644", - "sha256": "df4e51923b6e352faf7ce6590b6e969e4853cb9d3d0a906892d0a6d50c60b51f" + "sha256": "2000088c53e5e34b95d88e6de1615631a3ecb2dabf36e831f504dd0791afc5de" }, "core/crates/tera_core/src/runtime/product_surface/today_reconciliation_tests.rs": { "bytes": 3424, @@ -1425,13 +1437,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 116498, - "git_blob": "4a0af9069c1be7f07205d40c2272d7f1d417ddf4", + "bytes": 116926, + "git_blob": "c3a54831006843505ce452a194ab72352dbac12a", "mode": "100644", - "sha256": "3db541aae6722c0ebfacdbceec33a0c8415d17b9b915d8a3a3a97217ed7f52e5" + "sha256": "20f5bc3bdcfaaa8eb6816543849e75107c498a52e45399f3f84314e5ecf6c8f0" } }, "policy": "staged_inputs", - "tree": "3ff40c5420af5cfa8f2726bf65285c69a5a0386e" + "tree": "703c94b59618f9ca1dd16c17df8fb07fb7b860bd" } } diff --git a/TeraFFI/source/aarch64-apple-ios.json b/TeraFFI/source/aarch64-apple-ios.json @@ -717,10 +717,10 @@ "sha256": "76e3bddfc237dac63293d264d68991b489585136fe377e39dbba72aa3a1181f4" }, "core/crates/tera_core/src/runtime/product_surface/today.rs": { - "bytes": 140031, - "git_blob": "eaf6fd526b5d5ed8bc1d85f1d365689424bb12ed", + "bytes": 140918, + "git_blob": "c8be6532fdb641bb5d0056cad8e07bd7ed6ae278", "mode": "100644", - "sha256": "6f5c6df78c457e87bc02fcf6759b241ee81bfdbb67ac4cd35b2a02bea4b350d2" + "sha256": "1c76aafbd3d7ed373579a8658d47f9208059d2578db466e11d4148da663d4e99" }, "core/crates/tera_core/src/runtime/product_surface/today_admission_tests.rs": { "bytes": 14686, @@ -740,17 +740,29 @@ "mode": "100644", "sha256": "6f5810256ca221ca66ec4056e07fc635103e0ed02da01f5a8c53161535d7eaf6" }, + "core/crates/tera_core/src/runtime/product_surface/today_counted_storage_tests.rs": { + "bytes": 17547, + "git_blob": "de61bd9c5fefa9cabf4818d84f9498564a6e5790", + "mode": "100644", + "sha256": "31590402860e49ec6a980b5822ab129207852ba59d9516bd7d23952071fe2a88" + }, "core/crates/tera_core/src/runtime/product_surface/today_paging_scope.rs": { "bytes": 1703, "git_blob": "cc1fb36406b9ad236a5653bbd59d8256dd91bd19", "mode": "100644", "sha256": "e9ea1a0ee6c52031895184cd0a2b3c526318fd521ab2bd6b1b08ef53f3c9ff66" }, + "core/crates/tera_core/src/runtime/product_surface/today_performance_tests.rs": { + "bytes": 5938, + "git_blob": "70a56d607ba4e59305e4f7488b20e509764fbee8", + "mode": "100644", + "sha256": "1158622c28cca9b6843b48199f4eb6d286a01cc15d7530e11b0fa25a2e00b93c" + }, "core/crates/tera_core/src/runtime/product_surface/today_reconciliation.rs": { - "bytes": 2055, - "git_blob": "d615a82f5e06bfcae0582bb2069b0cf168caee9d", + "bytes": 2036, + "git_blob": "a85b9bc3a43885415147fef2d9b9a57338ed7427", "mode": "100644", - "sha256": "df4e51923b6e352faf7ce6590b6e969e4853cb9d3d0a906892d0a6d50c60b51f" + "sha256": "2000088c53e5e34b95d88e6de1615631a3ecb2dabf36e831f504dd0791afc5de" }, "core/crates/tera_core/src/runtime/product_surface/today_reconciliation_tests.rs": { "bytes": 3424, @@ -1425,13 +1437,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 116498, - "git_blob": "4a0af9069c1be7f07205d40c2272d7f1d417ddf4", + "bytes": 116926, + "git_blob": "c3a54831006843505ce452a194ab72352dbac12a", "mode": "100644", - "sha256": "3db541aae6722c0ebfacdbceec33a0c8415d17b9b915d8a3a3a97217ed7f52e5" + "sha256": "20f5bc3bdcfaaa8eb6816543849e75107c498a52e45399f3f84314e5ecf6c8f0" } }, "policy": "staged_inputs", - "tree": "3ff40c5420af5cfa8f2726bf65285c69a5a0386e" + "tree": "703c94b59618f9ca1dd16c17df8fb07fb7b860bd" } } diff --git a/TeraTests/TeraMediaBudgetTests.swift b/TeraTests/TeraMediaBudgetTests.swift @@ -0,0 +1,181 @@ +import Foundation +@testable import TeraApp +import UIKit +import XCTest + +final class TeraMediaBudgetTests: XCTestCase { + func testStartupLimitsRejectZeroAndEveryMaximumPlusOne() throws { + let maximum = [8 * 1024 * 1024, 16_000_000, 1024, 32 * 1024 * 1024, 64, 2, 16, 4096] + func limits(_ values: [Int]) -> TeraMediaPresentationLimits? { + TeraMediaPresentationLimits(encodedBytes: values[0], sourcePixels: values[1], + thumbnailDimension: values[2], cacheBytes: values[3], cacheEntries: values[4], + workers: values[5], queuedRequests: values[6], visibilityEntries: values[7]) + } + XCTAssertEqual(limits(maximum), .standard) + for index in maximum.indices { + var invalid = maximum + invalid[index] += 1 + XCTAssertNil(limits(invalid)) + invalid[index] = 0 + XCTAssertNil(limits(invalid)) + } + let policy = try XCTUnwrap(limits(maximum)) + XCTAssertTrue(policy.accepts(byteCount: maximum[0], width: 4000, height: 4000)) + XCTAssertFalse(policy.accepts(byteCount: maximum[0] + 1, width: 4000, height: 4000)) + XCTAssertFalse(policy.accepts(byteCount: maximum[0], width: 16_000_001, height: 1)) + XCTAssertFalse(policy.accepts(byteCount: 1, width: Int.max, height: Int.max)) + } + + @MainActor + func testDecodeUsesBoundedRasterOffUIActorAndRejectsCorruption() async throws { + let renderer = UIGraphicsImageRenderer(size: CGSize(width: 2048, height: 1024), format: { + let format = UIGraphicsImageRendererFormat() + format.scale = 1 + return format + }()) + let bytes = renderer.pngData { context in + UIColor.green.setFill() + context.fill(CGRect(x: 0, y: 0, width: 2048, height: 1024)) + } + let artifact = try XCTUnwrap(TeraVerifiedMediaArtifact(artifactID: String(repeating: "a", count: 64), + bytes: bytes, byteSize: UInt64(bytes.count), mediaType: "image/png", width: 2048, height: 1024)) + let image = try await TeraMediaThumbnail.prepare(artifact, limits: .standard) + XCTAssertEqual(image.width, 1024) + XCTAssertEqual(image.height, 512) + do { + _ = try await TeraMediaThumbnail.prepare(TeraScopeFixtures.artifact("a", corrupt: true), limits: .standard) + XCTFail("Corrupt raster must fail") + } catch TeraMediaThumbnailFailure.corrupt {} + } + + func testActualDecoderRejectsEncodedAndDeclaredPixelMaximumPlusOne() async throws { + let original = try TeraScopeFixtures.artifact("a") + for (bytes, width, height) in [(Data(repeating: 0, count: 8 * 1024 * 1024 + 1), UInt32(1), UInt32(1)), + (original.bytes, UInt32(16_000_001), UInt32(1))] + { + let artifact = try XCTUnwrap(TeraVerifiedMediaArtifact(artifactID: original.artifactID, + bytes: bytes, byteSize: UInt64(bytes.count), mediaType: "image/png", width: width, height: height)) + do { + _ = try await TeraMediaThumbnail.prepare(artifact, limits: .standard) + XCTFail("Resource admission must precede raster decoding") + } catch TeraMediaThumbnailFailure.resourceLimit {} + } + } + + @MainActor + func testQueueMaxPlusOneAndCancellationKeepPhysicalWorkersBounded() async { + let queue = TeraMediaWorkQueue(limits: .standard) + let firstPause = ResourceTestPause() + let secondPause = ResourceTestPause() + let first = UUID() + var started = 0 + XCTAssertTrue(queue.submit(id: first) { started += 1; await firstPause.wait() }) + XCTAssertTrue(queue.submit(id: UUID()) { started += 1; await secondPause.wait() }) + for _ in 0 ..< 16 { + XCTAssertTrue(queue.submit(id: UUID()) { started += 1 }) + } + await firstPause.entered.wait() + await secondPause.entered.wait() + XCTAssertEqual(started, 2) + XCTAssertEqual(queue.activeCount, 2) + XCTAssertEqual(queue.queuedCount, 16) + XCTAssertFalse(queue.submit(id: UUID()) {}) + queue.cancel(id: first) + XCTAssertEqual(queue.activeCount, 2) + XCTAssertFalse(queue.submit(id: UUID()) {}) + queue.cancelAll() + XCTAssertEqual(queue.activeCount, 2) + XCTAssertEqual(queue.queuedCount, 0) + await firstPause.resume.open() + await secondPause.resume.open() + await TeraScopeFixtures.eventually { queue.activeCount == 0 } + XCTAssertEqual(started, 2) + } + + @MainActor + func testPresentationCacheEvictsOnlyTransientBytesAtCapacity() async throws { + let backend = try TeraScopeBackend() + let client = try await TeraScopeFixtures.client(backend) + let policy = try XCTUnwrap(TeraMediaPresentationLimits(encodedBytes: 1024, sourcePixels: 16_000_000, + thumbnailDimension: 1024, cacheBytes: 200, cacheEntries: 3, workers: 1, queuedRequests: 1, + visibilityEntries: 4096)) + let store = TeraMediaStore(runtimeClient: client, limits: policy) + let context = TeraLocalNetwork.defaultContext(snapshot: TeraScopeFixtures.snapshot()) + for index in 0 ..< 4 { + var reference = TeraScopeFixtures.reference() + reference = TeraMediaReference(referenceFingerprint: String(repeating: String(index), count: 64), + url: reference.url, sha256: reference.sha256, mediaType: reference.mediaType, + width: reference.width, height: reference.height, byteSize: reference.byteSize, + alt: reference.alt, verification: reference.verification) + store.load(media: reference, context: context) + await TeraScopeFixtures.eventually { store.state(for: reference, context: context) != .loading } + XCTAssertLessThanOrEqual(store.stateCount, policy.cacheEntries) + XCTAssertLessThanOrEqual(store.cachedByteCount, policy.cacheBytes) + XCTAssertGreaterThan(store.cachedByteCount, 0) + } + let invalidations = await backend.counts[.invalidate] + XCTAssertNil(invalidations) + store.reset() + XCTAssertEqual(store.cachedByteCount, 0) + _ = try await client.stop() + } + + @MainActor + func testVisibilityCapacityFailsClosedAndExplicitCurrentReferencesCanReturn() async throws { + let backend = try TeraScopeBackend() + let client = try await TeraScopeFixtures.client(backend) + let limits = try XCTUnwrap(TeraMediaPresentationLimits(encodedBytes: 1024, + sourcePixels: 16_000_000, thumbnailDimension: 1024, cacheBytes: 1024, + cacheEntries: 64, workers: 2, queuedRequests: 16, visibilityEntries: 2)) + let store = TeraMediaStore(runtimeClient: client, limits: limits) + let context = TeraLocalNetwork.defaultContext(snapshot: TeraScopeFixtures.snapshot()) + let references = (0 ..< 3).map { reference($0, verification: .unavailable) } + store.reconcileVisibility(previous: Array(references.prefix(2)), current: [], context: context) + store.reconcileVisibility(previous: [references[0]], current: [], context: context) + let artifact = try TeraScopeFixtures.artifact("a") + store.load(media: references[2], context: context) + await TeraScopeFixtures.eventually { store.state(for: references[2], context: context) == .ready(artifact) } + store.reconcileVisibility(previous: [references[2]], current: [], context: context) + for value in references { + store.load(media: value, context: context) + store.retry(media: value, context: context) + XCTAssertEqual(store.state(for: value, context: context), .unavailable) + } + let calls = await backend.counts[.media] + XCTAssertEqual(calls, 1) + store.reconcileVisibility(previous: [], current: [references[2]], context: context) + store.load(media: references[2], context: context) + await TeraScopeFixtures.eventually { store.state(for: references[2], context: context) == .ready(artifact) } + store.retry(media: references[0], context: context) + let finalCalls = await backend.counts[.media] + XCTAssertEqual(finalCalls, 2) + store.reset() + _ = try await client.stop() + } + + @MainActor + func testPendingStateEntryMaximumPlusOneIsBounded() async throws { + let backend = try TeraScopeBackend() + let client = try await TeraScopeFixtures.client(backend) + let store = TeraMediaStore(runtimeClient: client) + let context = TeraLocalNetwork.defaultContext(snapshot: TeraScopeFixtures.snapshot()) + for index in 0 ..< 64 { + store.load(media: reference(index, verification: .pending), context: context) + } + XCTAssertEqual(store.stateCount, 64) + store.load(media: reference(64, verification: .pending), context: context) + XCTAssertEqual(store.stateCount, 64) + let calls = await backend.counts[.media] + XCTAssertNil(calls) + store.reset() + _ = try await client.stop() + } + + private func reference(_ index: Int, verification: TeraMediaVerificationState) -> TeraMediaReference { + let original = TeraScopeFixtures.reference() + return TeraMediaReference(referenceFingerprint: String(format: "%064x", index), + url: original.url, sha256: original.sha256, mediaType: original.mediaType, + width: original.width, height: original.height, byteSize: original.byteSize, + alt: original.alt, verification: verification) + } +} diff --git a/TeraTests/TeraTodayRenderingTests.swift b/TeraTests/TeraTodayRenderingTests.swift @@ -0,0 +1,44 @@ +@testable import TeraApp +import XCTest + +final class TeraTodayRenderingTests: XCTestCase { + func testExcerptCharacterAndByteLimitsPreserveFullDetail() { + let feed = TeraTodayCardPresentation.feed + let detail = TeraTodayCardPresentation.detail + let maximum = String(repeating: "a", count: 640) + XCTAssertEqual(feed.content(maximum), maximum) + XCTAssertEqual(feed.content(maximum + "b").count, 640) + XCTAssertTrue(feed.content(maximum + "b").hasSuffix("…")) + for value in [String(repeating: "👨‍👩‍👧‍👦", count: 1000), + "e" + String(repeating: "\u{301}", count: 100_000), + String(repeating: "農", count: 2000), String(repeating: "👩🏽‍🌾", count: 1000)] + { + let excerpt = feed.content(value) + XCTAssertLessThanOrEqual(excerpt.utf8.count, 4096) + XCTAssertLessThanOrEqual(excerpt.count, 640) + XCTAssertFalse(excerpt.contains("�")) + XCTAssertEqual(detail.content(value), value) + } + } + + func testByteBoundaryNeverIncludesAPartialGrapheme() { + let cluster = "👩🏽‍🌾" + let value = "abc" + cluster + "end" + for bytes in 3 ... value.utf8.count { + let result = TeraTodayCardPresentation.excerpt(value, characters: 100, bytes: bytes) + XCTAssertLessThanOrEqual(result.utf8.count, bytes) + XCTAssertTrue(value.hasPrefix(String(result.dropLast())) || result == value) + XCTAssertFalse(result.contains("�")) + } + } + + func testFeedBoundsLabelsAndThumbnailsWhileDetailRetainsEveryReference() { + let label = String(repeating: "a", count: 160) + XCTAssertEqual(TeraTodayCardPresentation.feed.label(label), label) + XCTAssertEqual(TeraTodayCardPresentation.feed.label(label + "b").count, 160) + let references = Array(repeating: TeraScopeFixtures.reference(), count: 4) + XCTAssertEqual(TeraTodayCardPresentation.feed.media(Array(references.prefix(3))).count, 3) + XCTAssertEqual(TeraTodayCardPresentation.feed.media(references).count, 3) + XCTAssertEqual(TeraTodayCardPresentation.detail.media(references), references) + } +} diff --git a/core/crates/tera_core/src/runtime/product_surface/today.rs b/core/crates/tera_core/src/runtime/product_surface/today.rs @@ -74,6 +74,10 @@ pub use reconciliation::TodayReconciliation; mod reconciliation_tests; #[cfg(test)] +#[path = "today_performance_tests.rs"] +mod performance_tests; + +#[cfg(test)] #[path = "today_scope_tests.rs"] mod scope_tests; @@ -391,17 +395,14 @@ impl TeraRuntime { if request.limit == 0 || request.limit > TODAY_PAGE_LIMIT_MAX { return Err(TodayError::InvalidRequest); } - let storage = self - .client - .storage() - .map_err(|_| TodayError::RuntimeUnavailable)?; - let event_status = EventStore::status(storage).await?; - let algorithm_generation = projection_generation()?; - let projection_id = projection_id()?; + // Decode and validate all caller-owned scope before any storage I/O. + let decoded_scope = request + .cursor + .as_deref() + .map(TodayCursor::scope) + .transpose()?; let query_scope = paging_scope::query_scope(context, self.store_public_key)?; - - let (scope, snapshot, after) = if let Some(cursor) = request.cursor.as_deref() { - let scope = TodayCursor::scope(cursor)?; + if let Some(scope) = &decoded_scope { if scope.context_id != context.id || scope.context_generation != context.generation || scope.query_scope != query_scope @@ -411,7 +412,19 @@ impl TeraRuntime { if request.as_of.is_some_and(|as_of| as_of != scope.as_of) { return Err(CursorError::SnapshotMismatch.into()); } - if scope.store_generation != *event_status.generation().as_bytes() { + } else if request.as_of.is_none_or(|value| value == 0) { + return Err(TodayError::InvalidRequest); + } + let storage = self + .client + .storage() + .map_err(|_| TodayError::RuntimeUnavailable)?; + let store_generation = current_store_generation(storage).await?; + let algorithm_generation = projection_generation()?; + let projection_id = projection_id()?; + let (scope, snapshot, after) = if let Some(cursor) = request.cursor.as_deref() { + let scope = decoded_scope.expect("cursor presence was decoded above"); + if scope.store_generation != store_generation { return Err(CursorError::Stale.into()); } let position = TodayCursor::decode(cursor, &scope)?; @@ -453,8 +466,7 @@ impl TeraRuntime { .ok_or(TodayError::ProjectionMissing)? } }; - if state.store_generation != *event_status.generation().as_bytes() - || state.query_scope != Some(query_scope) + if state.store_generation != store_generation || state.query_scope != Some(query_scope) { return Err(CursorError::Stale.into()); } @@ -1322,6 +1334,17 @@ fn refresh_thread_profiles(state: &mut TodayProjectionState) { } } +// A bounded owner query authenticates the source generation without invoking +// full visibility/status reduction. The backend may read one look-ahead row. +async fn current_store_generation(storage: &dyn EventStore) -> Result<[u8; 32], TodayError> { + Ok( + *EventStore::query_raw(storage, EventQuery::all(EventQueryBounds::first(1)?)) + .await? + .generation() + .as_bytes(), + ) +} + async fn query_all_visible( storage: &dyn radroots_storage::Storage, ) -> Result<Vec<radroots_storage::event::StoredVisibleEvent>, TodayError> { @@ -2248,7 +2271,7 @@ mod tests { SignedEvent::from_wire_verified_id(wire, raw).expect("signed event") } - fn visible_admission(event: SignedEvent, observed_at: u64) -> EventAdmission { + pub(super) fn visible_admission(event: SignedEvent, observed_at: u64) -> EventAdmission { let selected = admit_verified_event( verify_nip01_event(event.envelope().clone()).expect("codec verification"), ) diff --git a/core/crates/tera_core/src/runtime/product_surface/today_counted_storage_tests.rs b/core/crates/tera_core/src/runtime/product_surface/today_counted_storage_tests.rs @@ -0,0 +1,500 @@ +// Test-only forwarding adapter. Every operation reaches the real SDK-owned SQLite +// store. Counts describe SPI calls and returned rows, not disabled SQL telemetry. +use radroots_storage::{ + Error, atomic::*, authored::*, authored_atomic::*, authored_delivery::*, authored_draft::*, + backup::*, event::*, journal::*, outbox::*, private_artifact::*, projection::*, status::*, +}; +use std::sync::{Arc, Mutex}; + +#[derive(Default)] +pub(super) struct QueryCounts(Mutex<std::collections::BTreeMap<&'static str, u64>>); +impl QueryCounts { + fn add(&self, key: &'static str, amount: u64) { + *self.0.lock().unwrap().entry(key).or_default() += amount; + } + pub(super) fn take(&self) -> std::collections::BTreeMap<&'static str, u64> { + std::mem::take(&mut self.0.lock().unwrap()) + } +} +pub(super) struct CountedStorage { + pub(super) client: radroots_sdk::Client, + pub(super) counts: Arc<QueryCounts>, +} +impl CountedStorage { + fn storage(&self) -> &dyn radroots_storage::Storage { + self.client.storage().unwrap() + } +} + +impl EventStore for CountedStorage { + fn status(&self) -> BoxFuture<'_, Result<EventStoreStatus, Error>> { + Box::pin(async move { + self.counts.add("event.status", 1); + EventStore::status(self.storage()).await + }) + } + fn admit(&self, admission: EventAdmission) -> BoxFuture<'_, Result<AdmissionReceipt, Error>> { + Box::pin(async move { + self.counts.add("event.admit", 1); + EventStore::admit(self.storage(), admission).await + }) + } + fn query_raw( + &self, + query: EventQuery, + ) -> BoxFuture<'_, Result<EventPage<StoredRawEvent>, Error>> { + Box::pin(async move { + self.counts.add("event.query_raw", 1); + let result = EventStore::query_raw(self.storage(), query).await; + if let Ok(page) = &result { + self.counts.add("event.rows", page.items().len() as u64); + } + result + }) + } + fn query_verified( + &self, + query: EventQuery, + ) -> BoxFuture<'_, Result<EventPage<StoredVerifiedEvent>, Error>> { + Box::pin(async move { + self.counts.add("event.query_verified", 1); + let result = EventStore::query_verified(self.storage(), query).await; + if let Ok(page) = &result { + self.counts.add("event.rows", page.items().len() as u64); + } + result + }) + } + fn query_visible( + &self, + query: EventQuery, + ) -> BoxFuture<'_, Result<EventPage<StoredVisibleEvent>, Error>> { + Box::pin(async move { + self.counts.add("event.query_visible", 1); + let result = EventStore::query_visible(self.storage(), query).await; + if let Ok(page) = &result { + self.counts.add("event.rows", page.items().len() as u64); + } + result + }) + } + fn rebuild_visibility(&self) -> BoxFuture<'_, Result<VisibilitySnapshot, Error>> { + Box::pin(async move { + self.counts.add("event.rebuild_visibility", 1); + EventStore::rebuild_visibility(self.storage()).await + }) + } + fn query_provenance( + &self, + event_id: EventId, + bounds: EventQueryBounds, + ) -> BoxFuture<'_, Result<EventPage<StoredEventProvenance>, Error>> { + Box::pin(async move { + self.counts.add("event.query_provenance", 1); + EventStore::query_provenance(self.storage(), event_id, bounds).await + }) + } +} + +impl ProjectionStore for CountedStorage { + fn status( + &self, + projection_id: ProjectionId, + ) -> BoxFuture<'_, Result<Option<ProjectionStatus>, Error>> { + Box::pin(async move { + self.counts.add("projection.status", 1); + ProjectionStore::status(self.storage(), projection_id).await + }) + } + fn checkpoint( + &self, + checkpoint: ProjectionCheckpoint, + ) -> BoxFuture<'_, Result<ProjectionStatus, Error>> { + Box::pin(async move { + self.counts.add("projection.checkpoint", 1); + ProjectionStore::checkpoint(self.storage(), checkpoint).await + }) + } + fn invalidate( + &self, + invalidation: ProjectionInvalidation, + ) -> BoxFuture<'_, Result<ProjectionStatus, Error>> { + Box::pin(async move { + self.counts.add("projection.invalidate", 1); + ProjectionStore::invalidate(self.storage(), invalidation).await + }) + } + fn invalidation( + &self, + projection_id: ProjectionId, + replacement_generation: ProjectionGeneration, + ) -> BoxFuture<'_, Result<Option<ProjectionInvalidation>, Error>> { + Box::pin(async move { + self.counts.add("projection.invalidation", 1); + ProjectionStore::invalidation(self.storage(), projection_id, replacement_generation) + .await + }) + } + fn request_rebuild( + &self, + ticket: RebuildTicket, + ) -> BoxFuture<'_, Result<RebuildTicket, Error>> { + Box::pin(async move { + self.counts.add("projection.request_rebuild", 1); + ProjectionStore::request_rebuild(self.storage(), ticket).await + }) + } + fn rebuild( + &self, + ticket_id: RebuildTicketId, + ) -> BoxFuture<'_, Result<Option<RebuildTicket>, Error>> { + Box::pin(async move { + self.counts.add("projection.rebuild", 1); + ProjectionStore::rebuild(self.storage(), ticket_id).await + }) + } + fn transition_rebuild( + &self, + transition: RebuildTransition, + ) -> BoxFuture<'_, Result<RebuildTicket, Error>> { + Box::pin(async move { + self.counts.add("projection.transition_rebuild", 1); + ProjectionStore::transition_rebuild(self.storage(), transition).await + }) + } + fn event_index_manifest( + &self, + generation: ProjectionGeneration, + ) -> BoxFuture<'_, Result<Option<EventIndexManifest>, Error>> { + Box::pin(async move { + self.counts.add("projection.event_index_manifest", 1); + ProjectionStore::event_index_manifest(self.storage(), generation).await + }) + } + fn put_event_index_manifest( + &self, + manifest: EventIndexManifest, + ) -> BoxFuture<'_, Result<(), Error>> { + Box::pin(async move { + self.counts.add("projection.put_event_index_manifest", 1); + ProjectionStore::put_event_index_manifest(self.storage(), manifest).await + }) + } + fn event_index_checkpoint( + &self, + generation: ProjectionGeneration, + ) -> BoxFuture<'_, Result<Option<EventIndexCheckpoint>, Error>> { + Box::pin(async move { + self.counts.add("projection.event_index_checkpoint", 1); + ProjectionStore::event_index_checkpoint(self.storage(), generation).await + }) + } + fn put_event_index_checkpoint( + &self, + checkpoint: EventIndexCheckpoint, + ) -> BoxFuture<'_, Result<(), Error>> { + Box::pin(async move { + self.counts.add("projection.put_event_index_checkpoint", 1); + ProjectionStore::put_event_index_checkpoint(self.storage(), checkpoint).await + }) + } + fn put_projection_document( + &self, + projection_id: ProjectionId, + generation: ProjectionGeneration, + document: ProjectionDocument, + ) -> BoxFuture<'_, Result<(), Error>> { + Box::pin(async move { + self.counts.add("projection.put_projection_document", 1); + ProjectionStore::put_projection_document( + self.storage(), + projection_id, + generation, + document, + ) + .await + }) + } + fn projection_document( + &self, + projection_id: ProjectionId, + generation: ProjectionGeneration, + key: String, + ) -> BoxFuture<'_, Result<Option<ProjectionDocument>, Error>> { + Box::pin(async move { + self.counts.add("projection.projection_document", 1); + ProjectionStore::projection_document(self.storage(), projection_id, generation, key) + .await + }) + } + fn put_projection_snapshot( + &self, + snapshot: ProjectionSnapshot, + ) -> BoxFuture<'_, Result<(), Error>> { + Box::pin(async move { + self.counts.add("projection.put_projection_snapshot", 1); + ProjectionStore::put_projection_snapshot(self.storage(), snapshot).await + }) + } + fn projection_snapshot( + &self, + projection_id: ProjectionId, + snapshot_id: [u8; 32], + ) -> BoxFuture<'_, Result<Option<ProjectionSnapshot>, Error>> { + Box::pin(async move { + self.counts.add("projection.projection_snapshot", 1); + ProjectionStore::projection_snapshot(self.storage(), projection_id, snapshot_id).await + }) + } +} + +impl Journal for CountedStorage { + fn prepare(&self, operation: PrepareOperation) -> BoxFuture<'_, Result<PrepareReceipt, Error>> { + Journal::prepare(self.storage(), operation) + } + fn operation( + &self, + instance_id: OperationInstanceId, + ) -> BoxFuture<'_, Result<Option<OperationRecord>, Error>> { + Journal::operation(self.storage(), instance_id) + } + fn by_idempotency_key( + &self, + operation_id: OperationId, + idempotency_key: IdempotencyKey, + ) -> BoxFuture<'_, Result<Option<OperationRecord>, Error>> { + Journal::by_idempotency_key(self.storage(), operation_id, idempotency_key) + } + fn transition( + &self, + transition: JournalTransition, + ) -> BoxFuture<'_, Result<OperationRecord, Error>> { + Journal::transition(self.storage(), transition) + } + fn recoverable(&self, limit: u16) -> BoxFuture<'_, Result<Vec<OperationRecord>, Error>> { + Journal::recoverable(self.storage(), limit) + } +} + +impl Outbox for CountedStorage { + fn enqueue(&self, item: EnqueueOutboxItem) -> BoxFuture<'_, Result<EnqueueReceipt, Error>> { + Outbox::enqueue(self.storage(), item) + } + fn item(&self, item_id: OutboxItemId) -> BoxFuture<'_, Result<Option<OutboxRecord>, Error>> { + Outbox::item(self.storage(), item_id) + } + fn claim( + &self, + request: ClaimOutboxItems, + ) -> BoxFuture<'_, Result<Vec<ClaimedOutboxItem>, Error>> { + Outbox::claim(self.storage(), request) + } + fn record_attempt( + &self, + evidence: DeliveryAttemptEvidence, + ) -> BoxFuture<'_, Result<OutboxRecord, Error>> { + Outbox::record_attempt(self.storage(), evidence) + } + fn release( + &self, + item_id: OutboxItemId, + lease_id: LeaseId, + expected_revision: OutboxRevision, + released_at_unix_ms: u64, + retry_not_before_unix_ms: Option<u64>, + ) -> BoxFuture<'_, Result<OutboxRecord, Error>> { + Outbox::release( + self.storage(), + item_id, + lease_id, + expected_revision, + released_at_unix_ms, + retry_not_before_unix_ms, + ) + } + fn status(&self) -> BoxFuture<'_, Result<OutboxStatus, Error>> { + Outbox::status(self.storage()) + } +} + +impl PrivateArtifactStore for CountedStorage { + fn put_metadata( + &self, + metadata: PrivateArtifactMetadata, + ) -> BoxFuture<'_, Result<PrivateArtifactMetadata, Error>> { + PrivateArtifactStore::put_metadata(self.storage(), metadata) + } + fn metadata( + &self, + artifact_id: PrivateArtifactId, + ) -> BoxFuture<'_, Result<Option<PrivateArtifactMetadata>, Error>> { + PrivateArtifactStore::metadata(self.storage(), artifact_id) + } + fn reseal_metadata( + &self, + request: PrivateArtifactResealRequest, + ) -> BoxFuture<'_, Result<PrivateArtifactResealReceipt, Error>> { + PrivateArtifactStore::reseal_metadata(self.storage(), request) + } + fn mark_expired( + &self, + artifact_id: PrivateArtifactId, + expected_revision: PrivateArtifactRevision, + at_unix_ms: u64, + ) -> BoxFuture<'_, Result<PrivateArtifactMetadata, Error>> { + PrivateArtifactStore::mark_expired( + self.storage(), + artifact_id, + expected_revision, + at_unix_ms, + ) + } + fn tombstone( + &self, + artifact_id: PrivateArtifactId, + expected_revision: PrivateArtifactRevision, + at_unix_ms: u64, + reason: DeletionReason, + ) -> BoxFuture<'_, Result<PrivateArtifactMetadata, Error>> { + PrivateArtifactStore::tombstone( + self.storage(), + artifact_id, + expected_revision, + at_unix_ms, + reason, + ) + } + fn expired( + &self, + at_unix_ms: u64, + limit: u16, + ) -> BoxFuture<'_, Result<Vec<PrivateArtifactMetadata>, Error>> { + PrivateArtifactStore::expired(self.storage(), at_unix_ms, limit) + } + fn status(&self) -> BoxFuture<'_, Result<PrivateArtifactStatus, Error>> { + PrivateArtifactStore::status(self.storage()) + } +} + +impl StorageReliability for CountedStorage { + fn begin_backup(&self, plan: BackupPlan) -> BoxFuture<'_, Result<BackupOperation, Error>> { + StorageReliability::begin_backup(self.storage(), plan) + } + fn transition_backup( + &self, + backup_id: BackupId, + expected_revision: ReliabilityRevision, + transition: BackupTransition, + at_unix_ms: u64, + ) -> BoxFuture<'_, Result<BackupOperation, Error>> { + StorageReliability::transition_backup( + self.storage(), + backup_id, + expected_revision, + transition, + at_unix_ms, + ) + } + fn begin_restore(&self, plan: RestorePlan) -> BoxFuture<'_, Result<RestoreOperation, Error>> { + StorageReliability::begin_restore(self.storage(), plan) + } + fn transition_restore( + &self, + backup_id: BackupId, + expected_revision: ReliabilityRevision, + transition: RestoreTransition, + at_unix_ms: u64, + ) -> BoxFuture<'_, Result<RestoreOperation, Error>> { + StorageReliability::transition_restore( + self.storage(), + backup_id, + expected_revision, + transition, + at_unix_ms, + ) + } + fn integrity(&self) -> BoxFuture<'_, Result<IntegrityStatus, Error>> { + StorageReliability::integrity(self.storage()) + } + fn status(&self) -> BoxFuture<'_, Result<StorageStatus, Error>> { + StorageReliability::status(self.storage()) + } + fn close(&self) -> BoxFuture<'_, Result<StorageStatus, Error>> { + StorageReliability::close(self.storage()) + } +} + +impl AtomicStorage for CountedStorage { + fn commit(&self, request: AtomicCommit) -> BoxFuture<'_, Result<AtomicCommitReceipt, Error>> { + AtomicStorage::commit(self.storage(), request) + } + fn receipt( + &self, + commit_id: AtomicCommitId, + ) -> BoxFuture<'_, Result<Option<AtomicCommitReceipt>, Error>> { + AtomicStorage::receipt(self.storage(), commit_id) + } +} + +impl AuthoredAtomicStorage for CountedStorage { + fn execute_authored( + &self, + command: AuthoredAtomicCommand, + ) -> BoxFuture<'_, Result<AuthoredAtomicReceipt, Error>> { + AuthoredAtomicStorage::execute_authored(self.storage(), command) + } + fn authored_receipt( + &self, + commit_id: AtomicCommitId, + ) -> BoxFuture<'_, Result<Option<AuthoredAtomicReceipt>, Error>> { + AuthoredAtomicStorage::authored_receipt(self.storage(), commit_id) + } + fn authored_operation( + &self, + operation_id: OperationInstanceId, + ) -> BoxFuture<'_, Result<Option<AuthoredOperation>, Error>> { + AuthoredAtomicStorage::authored_operation(self.storage(), operation_id) + } + fn authored_artifact( + &self, + artifact_id: AuthoredArtifactId, + ) -> BoxFuture<'_, Result<Option<AuthoredArtifact>, Error>> { + AuthoredAtomicStorage::authored_artifact(self.storage(), artifact_id) + } + fn authored_delivery_plan( + &self, + plan_id: AuthoredDeliveryPlanId, + ) -> BoxFuture<'_, Result<Option<AuthoredDeliveryPlan>, Error>> { + AuthoredAtomicStorage::authored_delivery_plan(self.storage(), plan_id) + } +} + +impl AuthoredDraftStore for CountedStorage { + fn append_authored_draft( + &self, + draft: AuthoredDraft, + expected_head: Option<AuthoredDraftRevision>, + ) -> BoxFuture<'_, Result<DraftAppendReceipt, Error>> { + AuthoredDraftStore::append_authored_draft(self.storage(), draft, expected_head) + } + fn authored_draft_head( + &self, + draft_id: AuthoredDraftId, + ) -> BoxFuture<'_, Result<Option<AuthoredDraft>, Error>> { + AuthoredDraftStore::authored_draft_head(self.storage(), draft_id) + } + fn authored_draft_revision( + &self, + draft_id: AuthoredDraftId, + revision: AuthoredDraftRevision, + ) -> BoxFuture<'_, Result<Option<AuthoredDraft>, Error>> { + AuthoredDraftStore::authored_draft_revision(self.storage(), draft_id, revision) + } + fn authored_draft_heads( + &self, + author: [u8; 32], + limit: u16, + ) -> BoxFuture<'_, Result<Vec<AuthoredDraft>, Error>> { + AuthoredDraftStore::authored_draft_heads(self.storage(), author, limit) + } +} diff --git a/core/crates/tera_core/src/runtime/product_surface/today_performance_tests.rs b/core/crates/tera_core/src/runtime/product_surface/today_performance_tests.rs @@ -0,0 +1,163 @@ +use super::tests::{context, keys, signed, visible_admission}; +use super::*; +use crate::runtime::{ + builder::RuntimeBuilder, + store::{MobileUserStoreConfig, ProtectedDataAvailability}, +}; +use std::{sync::Arc, time::Instant}; +#[path = "today_counted_storage_tests.rs"] +mod counted_storage; +use counted_storage::{CountedStorage, QueryCounts}; +const EVENT_COUNT: u64 = 10_000; +const NOW: u64 = 2_000_000_000; + +#[tokio::test] +async fn cached_sqlite_pages_do_not_decode_ten_thousand_source_events() { + let (_root, runtime, counts) = counted_sqlite().await; + let selected = context(None, 1); + for index in 0..EVENT_COUNT { + let event = signed(1, vec![], "Synthetic bounded query fixture", NOW - index); + EventStore::admit( + runtime.client.storage().unwrap(), + visible_admission(event, NOW * 1_000), + ) + .await + .unwrap(); + } + counts.take(); + let refresh = Instant::now(); + runtime + .phase1_refresh_today(&selected, NOW, TodayProjectionUpdate::Rebuild) + .await + .unwrap(); + let rebuild_ms = refresh.elapsed().as_secs_f64() * 1_000.0; + let rebuild_calls = counts.take(); + let samples = async { + let mut samples = Vec::new(); + for _ in 0..if cfg!(debug_assertions) { 5 } else { 20 } { + counts.take(); + let start = Instant::now(); + let first = runtime + .phase1_today_page(&selected, TodayPageRequest::first(20, NOW)) + .await + .unwrap(); + assert_eq!(first.items.len(), 20); + let calls = counts.take(); + samples.push(("first", start.elapsed().as_secs_f64() * 1_000.0, calls)); + counts.take(); + let start = Instant::now(); + let next = runtime + .phase1_today_page( + &selected, + TodayPageRequest::after(20, first.next_cursor.unwrap()), + ) + .await + .unwrap(); + assert_eq!(next.items.len(), 20); + assert_ne!(first.items[0].card.card_id, next.items[0].card.card_id); + let calls = counts.take(); + samples.push(("after", start.elapsed().as_secs_f64() * 1_000.0, calls)); + } + samples + } + .await; + println!( + "{}", + serde_json::json!({ + "scenario": "tera_10000_sqlite_cached_pages", + "events": EVENT_COUNT, "page_size": 20, "rebuild_ms": rebuild_ms, + "profile": if cfg!(debug_assertions) { "debug" } else { "release" }, + "samples": samples, "rebuild_calls": rebuild_calls, + "count_boundary": "actual storage SPI calls and returned event rows; SQL logging remains disabled", + "physical_device_qualification": false + }) + ); + runtime.shutdown().await.unwrap(); + assert!(samples.iter().all(|(_, _, calls)| { + calls + .get("projection.projection_document") + .copied() + .unwrap_or(0) + > 0 + })); + assert!( + samples.iter().all( + |(_, _, calls)| calls.get("event.status").copied().unwrap_or(0) == 0 + && calls.get("event.query_visible").copied().unwrap_or(0) == 0 + && calls.get("event.rows").copied().unwrap_or(0) <= 1 + ), + "Cached pages must not read full event history for generation validation" + ); +} + +async fn counted_sqlite() -> (tempfile::TempDir, TeraRuntime, Arc<QueryCounts>) { + let root = tempfile::tempdir().unwrap(); + let store = MobileUserStoreConfig::from_encoded( + root.path(), + &keys().public_key().to_string(), + &"03".repeat(32), + 1_800_000_000_000, + ProtectedDataAvailability::Available, + ) + .unwrap(); + std::fs::create_dir_all(store.owner_directory()).unwrap(); + let mut runtime = RuntimeBuilder::new(store).build().await.unwrap(); + let counts = Arc::new(QueryCounts::default()); + runtime.client = radroots_sdk::ClientBuilder::new() + .storage(Arc::new(CountedStorage { + client: runtime.client.clone(), + counts: counts.clone(), + })) + .build() + .unwrap(); + (root, runtime, counts) +} + +#[tokio::test] +async fn invalid_page_boundaries_reject_before_any_storage_query() { + let (_root, runtime, counts) = counted_sqlite().await; + let selected = context(None, 1); + let mut wrong = selected.clone(); + wrong.generation += 1; + let scope = CursorScope::new( + wrong.id.clone().into(), + wrong.generation, + NOW, + [3; 32], + 1, + paging_scope::query_scope(&wrong, runtime.store_public_key).unwrap(), + ) + .unwrap(); + // Malformed and oversized opaque inputs, invalid limit/as-of, and a valid + // cursor for a different authenticated context all precede storage work. + let position = TodayCursorPosition { + rank: TodayRank::derive(TodayRankInput { + card_id: CardId::parse(&"ab".repeat(32)).unwrap(), + context_rank: super::super::ContextRank::MissingLocalityFallback, + card_type: TodayCardType::Update, + time: TimeRelevance::Published, + effective_at: NOW, + as_of: NOW, + }) + .unwrap(), + }; + let wrong_cursor = TodayCursor::encode(&scope, position) + .unwrap() + .as_str() + .to_owned(); + for request in [ + TodayPageRequest::after(20, "bad".into()), + TodayPageRequest::after(20, "x".repeat(100_000)), + TodayPageRequest::first(0, NOW), + TodayPageRequest::first(20, 0), + TodayPageRequest::after(20, wrong_cursor), + ] { + counts.take(); + assert!(runtime.phase1_today_page(&selected, request).await.is_err()); + assert!( + counts.take().is_empty(), + "Invalid scope must not touch the store" + ); + } + runtime.shutdown().await.unwrap(); +} diff --git a/core/crates/tera_core/src/runtime/product_surface/today_reconciliation.rs b/core/crates/tera_core/src/runtime/product_surface/today_reconciliation.rs @@ -41,7 +41,7 @@ impl TeraRuntime { if state.query_scope != Some(query_scope) || state.visibility_digest.is_none() || expected_generation.is_some_and(|value| value != state.content_generation) - || state.store_generation != *EventStore::status(storage).await?.generation().as_bytes() + || state.store_generation != current_store_generation(storage).await? { return Err(CursorError::Stale.into()); } diff --git a/project.yml b/project.yml @@ -84,6 +84,8 @@ targets: - path: TeraTests/TeraStoreRecoveryTests.swift - path: TeraTests/TeraAddObservationStartupTests.swift - path: TeraTests/TeraMediaStoreTests.swift + - path: TeraTests/TeraMediaBudgetTests.swift + - path: TeraTests/TeraTodayRenderingTests.swift - path: TeraTests/TeraMediaFileFixture.swift - path: TeraTests/TeraMediaOwnershipFFITests.swift - path: TeraTests/TeraBindingPortabilityTests.swift diff --git a/release/provenance.json b/release/provenance.json @@ -2,11 +2,11 @@ "artifacts": { "app_api_sha256": "020924097c0d7efc33128cb8fd3d3b2026d95f57c44da71880e585aff80f070b", "ffi_api_sha256": "4cc03b03dd54db7b4ab22684ff819233444ae0a43a770ab875148892bed1e3f2", - "ffi_provenance_sha256": "9061000a642f27b7ae4a4c8fb91e9d0be789ca4badedbecece5f18b0fdd3bd73", + "ffi_provenance_sha256": "f174e6196e6b8ffdadaabcf52c6d697ee7ba9e3acc55ff265cd928c1131d4010", "info_plist_sha256": "15ef08b1cdd1096cfb9eeaf5be5bf8f814807a7ca9350bbbb47860fa72ec13ef", "privacy_manifest_sha256": "a331d51864743ebe4e00dd22360b4a538b6b3ac26a6b3eb54094e60a36959a12", "sbom_sha256": "bea2d48c06e5f7e84cdad085d81290712674b74664456f2030064d9cf94c4b33", - "xcode_project_sha256": "3e09b28fc0437d3bacdc5d62f6758f31f98fb6be34fcac8c182a8f1a3a0b4922" + "xcode_project_sha256": "edb5658749d389fee06d27a37451c2bff6851bb32b00572553e11b0fa340566d" }, "disposition": "unsigned", "platforms": [ @@ -22,7 +22,7 @@ "lib_revision": "13ac1f2135199702a07ba6aa14ab22101f17fcb0", "source_date_epoch": 1787871027, "swift_package_lock_sha256": "94ae067a374726cdaf6b4ca0a5e44663c57fcdc5334060c5ffef5e79cfbf04c0", - "tera_ffi_source_tree": "3ff40c5420af5cfa8f2726bf65285c69a5a0386e", + "tera_ffi_source_tree": "703c94b59618f9ca1dd16c17df8fb07fb7b860bd", "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 @@ -4337,6 +4337,14 @@ "count": 4 }, { + "path": "core/crates/tera_core/src/runtime/product_surface/today_counted_storage_tests.rs", + "count": 1 + }, + { + "path": "core/crates/tera_core/src/runtime/product_surface/today_performance_tests.rs", + "count": 1 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/today_sync_tests.rs", "count": 2 }, @@ -4461,6 +4469,10 @@ "count": 1 }, { + "path": "core/crates/tera_core/src/runtime/product_surface/today_counted_storage_tests.rs", + "count": 2 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/today_sync.rs", "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":{"ruleName":"Function Body Length","severity":"error","location":{"character":3,"line":21,"file":"Tera\/App\/TeraAppModel.swift"},"ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines","reason":"Initializer body should span 50 lines or less excluding comments and whitespace: currently spans 52 lines"}},{"text":"actor TeraAddMediaCoordinator: TeraAddMediaHandling {","violation":{"ruleName":"Type Body Length","severity":"error","location":{"character":1,"line":51,"file":"Tera\/Runtime\/TeraAddMediaCoordinator.swift"},"ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines","reason":"Actor body should span 250 lines or less excluding comments and whitespace: currently spans 330 lines"}},{"text":"#endif","violation":{"ruleName":"File Length","severity":"error","location":{"character":1,"line":458,"file":"Tera\/Runtime\/TeraAddMediaCoordinator.swift"},"ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines.","reason":"File should contain 400 lines or less: currently contains 458"}},{"text":"private final class TeraGeneratedRuntimeBackend: TeraRuntimeBackend, @unchecked Sendable {","violation":{"ruleName":"Type Body Length","severity":"error","location":{"character":15,"line":111,"file":"Tera\/Runtime\/TeraGeneratedRuntimeBackend.swift"},"ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 491 lines"}},{"text":"}","violation":{"ruleName":"File Length","severity":"error","location":{"character":1,"line":1576,"file":"Tera\/Runtime\/TeraGeneratedRuntimeBackend.swift"},"ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines.","reason":"File should contain 1000 lines or less: currently contains 1576"}},{"text":"actor TeraRuntimeClient {","violation":{"ruleName":"Type Body Length","severity":"error","location":{"character":1,"line":3,"file":"Tera\/Runtime\/TeraRuntimeClient.swift"},"ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 753 lines"}},{"text":" func changes(bufferCapacity: Int = 16) async throws -> AsyncStream<TeraRuntimeChange> {","violation":{"ruleName":"Function Body Length","severity":"error","location":{"character":3,"line":379,"file":"Tera\/Runtime\/TeraRuntimeClient.swift"},"ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 70 lines"}},{"text":"}","violation":{"ruleName":"File Length","severity":"error","location":{"character":1,"line":835,"file":"Tera\/Runtime\/TeraRuntimeClient.swift"},"ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines.","reason":"File should contain 400 lines or less: currently contains 835"}},{"text":"}","violation":{"ruleName":"File Length","severity":"error","location":{"character":1,"line":1175,"file":"Tera\/Runtime\/TeraRuntimeModels.swift"},"ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines.","reason":"File should contain 1000 lines or less: currently contains 1175"}},{"text":"final class TeraAddStore: ObservableObject {","violation":{"ruleName":"Type Body Length","severity":"error","location":{"character":7,"line":11,"file":"Tera\/State\/TeraAddStore.swift"},"ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 731 lines"}},{"text":" func retract(_ card: TeraTodayCard) async {","violation":{"ruleName":"Function Body Length","severity":"error","location":{"character":3,"line":459,"file":"Tera\/State\/TeraAddStore.swift"},"ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 51 lines"}},{"text":"}","violation":{"ruleName":"File Length","severity":"error","location":{"character":1,"line":796,"file":"Tera\/State\/TeraAddStore.swift"},"ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines.","reason":"File should contain 400 lines or less: currently contains 796"}},{"text":"actor TeraConfigurationStore {","violation":{"ruleName":"Type Body Length","severity":"error","location":{"character":1,"line":78,"file":"Tera\/State\/TeraConfigurationStore.swift"},"ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 468 lines"}},{"text":" func load() throws -> TeraAppConfiguration {","violation":{"ruleName":"Function Body Length","severity":"error","location":{"character":5,"line":148,"file":"Tera\/State\/TeraConfigurationStore.swift"},"ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines","reason":"Function body should span 100 lines or less excluding comments and whitespace: currently spans 109 lines"}},{"text":"}","violation":{"ruleName":"File Length","severity":"error","location":{"character":1,"line":759,"file":"Tera\/State\/TeraConfigurationStore.swift"},"ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines.","reason":"File should contain 400 lines or less: currently contains 759"}},{"text":"actor TeraSessionStore {","violation":{"ruleName":"Type Body Length","severity":"error","location":{"character":1,"line":39,"file":"Tera\/State\/TeraSessionStore.swift"},"ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 390 lines"}},{"text":" private func start(acceptingReconfiguration: Bool) async -> TeraSessionPhase {","violation":{"ruleName":"Cyclomatic Complexity","severity":"error","location":{"character":11,"line":125,"file":"Tera\/State\/TeraSessionStore.swift"},"ruleIdentifier":"cyclomatic_complexity","ruleDescription":"Complexity of function bodies should be limited.","reason":"Function should have complexity 10 or less; currently complexity is 13"}},{"text":"}","violation":{"ruleName":"File Length","severity":"error","location":{"character":1,"line":452,"file":"Tera\/State\/TeraSessionStore.swift"},"ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines.","reason":"File should contain 400 lines or less: currently contains 452"}},{"text":"struct TeraAddView: View {","violation":{"ruleName":"Type Body Length","severity":"error","location":{"character":1,"line":4,"file":"Tera\/Views\/TeraAddView.swift"},"ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines","reason":"Struct body should span 350 lines or less excluding comments and whitespace: currently spans 454 lines"}},{"text":"}","violation":{"ruleName":"File Length","severity":"error","location":{"character":1,"line":560,"file":"Tera\/Views\/TeraAddView.swift"},"ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines.","reason":"File should contain 400 lines or less: currently contains 560"}},{"text":"struct TeraSettingsView: View {","violation":{"ruleName":"Type Body Length","severity":"error","location":{"character":1,"line":365,"file":"Tera\/Views\/TeraSupportingViews.swift"},"ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines","reason":"Struct body should span 250 lines or less excluding comments and whitespace: currently spans 259 lines"}},{"text":"}","violation":{"ruleName":"File Length","severity":"error","location":{"character":1,"line":629,"file":"Tera\/Views\/TeraSupportingViews.swift"},"ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines.","reason":"File should contain 400 lines or less: currently contains 629"}},{"text":"final class TeraAddStoreTests: XCTestCase {","violation":{"ruleName":"Type Body Length","severity":"error","location":{"character":7,"line":6,"file":"TeraTests\/TeraAddStoreTests.swift"},"ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 665 lines"}},{"text":"private actor AddBackend: TeraRuntimeBackend {","violation":{"ruleName":"Type Body Length","severity":"error","location":{"character":9,"line":844,"file":"TeraTests\/TeraAddStoreTests.swift"},"ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 492 lines"}},{"text":" private func makeStatus(","violation":{"ruleName":"Function Parameter Count","severity":"error","location":{"character":11,"line":1280,"file":"TeraTests\/TeraAddStoreTests.swift"},"ruleIdentifier":"function_parameter_count","ruleDescription":"Number of function parameters should be low.","reason":"Function should have 5 parameters or less: it currently has 8"}},{"text":"}","violation":{"ruleName":"File Length","severity":"error","location":{"character":1,"line":1431,"file":"TeraTests\/TeraAddStoreTests.swift"},"ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines.","reason":"File should contain 1000 lines or less: currently contains 1431"}},{"text":"final class TeraRemoteQualificationTests: XCTestCase {","violation":{"ruleName":"Type Body Length","severity":"error","location":{"character":7,"line":7,"file":"TeraTests\/TeraRemoteQualificationTests.swift"},"ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines","reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 307 lines"}},{"text":" func testQualificationRejectsAmbiguousOrUnsafeHarnessValues() {","violation":{"ruleName":"Function Body Length","severity":"error","location":{"character":3,"line":45,"file":"TeraTests\/TeraRemoteQualificationTests.swift"},"ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 94 lines"}},{"text":" func testAuthorizationEvidenceIsRedactedEphemeralAndRelaunchCleaned() throws {","violation":{"ruleName":"Function Body Length","severity":"error","location":{"character":3,"line":212,"file":"TeraTests\/TeraRemoteQualificationTests.swift"},"ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 53 lines"}},{"text":"final class TeraRuntimeClientTests: XCTestCase {","violation":{"ruleName":"Type Body Length","severity":"error","location":{"character":7,"line":4,"file":"TeraTests\/TeraRuntimeClientTests.swift"},"ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines","reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 298 lines"}},{"text":"final class TeraStateMigrationTests: XCTestCase {","violation":{"ruleName":"Type Body Length","severity":"error","location":{"character":7,"line":6,"file":"TeraTests\/TeraStateMigrationTests.swift"},"ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 580 lines"}},{"text":" func testRelayValidationMatchesRuntimeProfiles() throws {","violation":{"ruleName":"Function Body Length","severity":"error","location":{"character":5,"line":7,"file":"TeraTests\/TeraStateMigrationTests.swift"},"ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 61 lines"}},{"text":"}","violation":{"ruleName":"File Length","severity":"error","location":{"character":1,"line":622,"file":"TeraTests\/TeraStateMigrationTests.swift"},"ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines.","reason":"File should contain 400 lines or less: currently contains 622"}},{"text":"private actor SupportingBackend: TeraRuntimeBackend {","violation":{"ruleName":"Type Body Length","severity":"error","location":{"character":9,"line":207,"file":"TeraTests\/TeraSupportingStoreTests.swift"},"ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines","reason":"Actor body should span 250 lines or less excluding comments and whitespace: currently spans 253 lines"}},{"text":"}","violation":{"ruleName":"File Length","severity":"error","location":{"character":1,"line":487,"file":"TeraTests\/TeraSupportingStoreTests.swift"},"ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines.","reason":"File should contain 400 lines or less: currently contains 487"}},{"text":"final class TeraRemoteQualificationUITests: XCTestCase {","violation":{"ruleName":"Type Body Length","severity":"error","location":{"character":7,"line":4,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift"},"ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 1493 lines"}},{"text":" func testLocalSocialFiveFlowScenario() throws {","violation":{"ruleName":"Function Body Length","severity":"error","location":{"character":3,"line":62,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift"},"ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 73 lines"}},{"text":" func testLocalSocialDeterministicPersonas() throws {","violation":{"ruleName":"Function Body Length","severity":"error","location":{"character":3,"line":174,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift"},"ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 66 lines"}},{"text":" func testRemoteBlossomUploadAndRecovery() throws {","violation":{"ruleName":"Function Body Length","severity":"error","location":{"character":3,"line":247,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift"},"ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines","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":{"ruleName":"Cyclomatic Complexity","severity":"error","location":{"character":11,"line":573,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift"},"ruleIdentifier":"cyclomatic_complexity","ruleDescription":"Complexity of function bodies should be limited.","reason":"Function should have complexity 10 or less; currently complexity is 18"}},{"text":" private func preparePhotoUpdate(_ app: XCUIApplication, marker: String) -> XCUIElement? {","violation":{"ruleName":"Cyclomatic Complexity","severity":"error","location":{"character":11,"line":767,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift"},"ruleIdentifier":"cyclomatic_complexity","ruleDescription":"Complexity of function bodies should be limited.","reason":"Function should have complexity 10 or less; currently complexity is 11"}},{"text":" private func preparePhotoUpdate(_ app: XCUIApplication, marker: String) -> XCUIElement? {","violation":{"ruleName":"Function Body Length","severity":"error","location":{"character":11,"line":767,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift"},"ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 66 lines"}},{"text":" static func environment(","violation":{"ruleName":"Function Body Length","severity":"error","location":{"character":10,"line":1736,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift"},"ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 67 lines"}},{"text":"}","violation":{"ruleName":"File Length","severity":"error","location":{"character":1,"line":2212,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift"},"ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines.","reason":"File should contain 1000 lines or less: currently contains 2212"}}] -\ No newline at end of file +[{"violation":{"ruleName":"Function Body Length","severity":"error","location":{"file":"Tera\/App\/TeraAppModel.swift","line":21,"character":3},"reason":"Initializer body should span 50 lines or less excluding comments and whitespace: currently spans 52 lines","ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines"},"text":" init("},{"violation":{"ruleName":"Type Body Length","severity":"error","location":{"file":"Tera\/Runtime\/TeraAddMediaCoordinator.swift","line":51,"character":1},"reason":"Actor body should span 250 lines or less excluding comments and whitespace: currently spans 330 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"actor TeraAddMediaCoordinator: TeraAddMediaHandling {"},{"violation":{"ruleName":"File Length","severity":"error","location":{"file":"Tera\/Runtime\/TeraAddMediaCoordinator.swift","line":458,"character":1},"reason":"File should contain 400 lines or less: currently contains 458","ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines."},"text":"#endif"},{"violation":{"ruleName":"Type Body Length","severity":"error","location":{"file":"Tera\/Runtime\/TeraGeneratedRuntimeBackend.swift","line":111,"character":15},"reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 491 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"private final class TeraGeneratedRuntimeBackend: TeraRuntimeBackend, @unchecked Sendable {"},{"violation":{"ruleName":"File Length","severity":"error","location":{"file":"Tera\/Runtime\/TeraGeneratedRuntimeBackend.swift","line":1576,"character":1},"reason":"File should contain 1000 lines or less: currently contains 1576","ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleName":"Type Body Length","severity":"error","location":{"file":"Tera\/Runtime\/TeraRuntimeClient.swift","line":3,"character":1},"reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 753 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"actor TeraRuntimeClient {"},{"violation":{"ruleName":"Function Body Length","severity":"error","location":{"file":"Tera\/Runtime\/TeraRuntimeClient.swift","line":379,"character":3},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 70 lines","ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines"},"text":" func changes(bufferCapacity: Int = 16) async throws -> AsyncStream<TeraRuntimeChange> {"},{"violation":{"ruleName":"File Length","severity":"error","location":{"file":"Tera\/Runtime\/TeraRuntimeClient.swift","line":835,"character":1},"reason":"File should contain 400 lines or less: currently contains 835","ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleName":"File Length","severity":"error","location":{"file":"Tera\/Runtime\/TeraRuntimeModels.swift","line":1175,"character":1},"reason":"File should contain 1000 lines or less: currently contains 1175","ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleName":"Type Body Length","severity":"error","location":{"file":"Tera\/State\/TeraAddStore.swift","line":11,"character":7},"reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 731 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"final class TeraAddStore: ObservableObject {"},{"violation":{"ruleName":"Function Body Length","severity":"error","location":{"file":"Tera\/State\/TeraAddStore.swift","line":459,"character":3},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 51 lines","ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines"},"text":" func retract(_ card: TeraTodayCard) async {"},{"violation":{"ruleName":"File Length","severity":"error","location":{"file":"Tera\/State\/TeraAddStore.swift","line":796,"character":1},"reason":"File should contain 400 lines or less: currently contains 796","ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleName":"Type Body Length","severity":"error","location":{"file":"Tera\/State\/TeraConfigurationStore.swift","line":78,"character":1},"reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 468 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"actor TeraConfigurationStore {"},{"violation":{"ruleName":"Function Body Length","severity":"error","location":{"file":"Tera\/State\/TeraConfigurationStore.swift","line":148,"character":5},"reason":"Function body should span 100 lines or less excluding comments and whitespace: currently spans 109 lines","ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines"},"text":" func load() throws -> TeraAppConfiguration {"},{"violation":{"ruleName":"File Length","severity":"error","location":{"file":"Tera\/State\/TeraConfigurationStore.swift","line":759,"character":1},"reason":"File should contain 400 lines or less: currently contains 759","ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleName":"Type Body Length","severity":"error","location":{"file":"Tera\/State\/TeraSessionStore.swift","line":39,"character":1},"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"},"text":"actor TeraSessionStore {"},{"violation":{"ruleName":"Cyclomatic Complexity","severity":"error","location":{"file":"Tera\/State\/TeraSessionStore.swift","line":125,"character":11},"reason":"Function should have complexity 10 or less; currently complexity is 13","ruleIdentifier":"cyclomatic_complexity","ruleDescription":"Complexity of function bodies should be limited."},"text":" private func start(acceptingReconfiguration: Bool) async -> TeraSessionPhase {"},{"violation":{"ruleName":"File Length","severity":"error","location":{"file":"Tera\/State\/TeraSessionStore.swift","line":452,"character":1},"reason":"File should contain 400 lines or less: currently contains 452","ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleName":"Type Body Length","severity":"error","location":{"file":"Tera\/Views\/TeraAddView.swift","line":4,"character":1},"reason":"Struct body should span 350 lines or less excluding comments and whitespace: currently spans 454 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"struct TeraAddView: View {"},{"violation":{"ruleName":"File Length","severity":"error","location":{"file":"Tera\/Views\/TeraAddView.swift","line":560,"character":1},"reason":"File should contain 400 lines or less: currently contains 560","ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleName":"Type Body Length","severity":"error","location":{"file":"Tera\/Views\/TeraSupportingViews.swift","line":365,"character":1},"reason":"Struct body should span 250 lines or less excluding comments and whitespace: currently spans 259 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"struct TeraSettingsView: View {"},{"violation":{"ruleName":"File Length","severity":"error","location":{"file":"Tera\/Views\/TeraSupportingViews.swift","line":629,"character":1},"reason":"File should contain 400 lines or less: currently contains 629","ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleName":"Type Body Length","severity":"error","location":{"file":"TeraTests\/TeraAddStoreTests.swift","line":6,"character":7},"reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 665 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"final class TeraAddStoreTests: XCTestCase {"},{"violation":{"ruleName":"Type Body Length","severity":"error","location":{"file":"TeraTests\/TeraAddStoreTests.swift","line":844,"character":9},"reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 492 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"private actor AddBackend: TeraRuntimeBackend {"},{"violation":{"ruleName":"Function Parameter Count","severity":"error","location":{"file":"TeraTests\/TeraAddStoreTests.swift","line":1280,"character":11},"reason":"Function should have 5 parameters or less: it currently has 8","ruleIdentifier":"function_parameter_count","ruleDescription":"Number of function parameters should be low."},"text":" private func makeStatus("},{"violation":{"ruleName":"File Length","severity":"error","location":{"file":"TeraTests\/TeraAddStoreTests.swift","line":1431,"character":1},"reason":"File should contain 1000 lines or less: currently contains 1431","ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleName":"Type Body Length","severity":"error","location":{"file":"TeraTests\/TeraRemoteQualificationTests.swift","line":7,"character":7},"reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 307 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"final class TeraRemoteQualificationTests: XCTestCase {"},{"violation":{"ruleName":"Function Body Length","severity":"error","location":{"file":"TeraTests\/TeraRemoteQualificationTests.swift","line":45,"character":3},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 94 lines","ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines"},"text":" func testQualificationRejectsAmbiguousOrUnsafeHarnessValues() {"},{"violation":{"ruleName":"Function Body Length","severity":"error","location":{"file":"TeraTests\/TeraRemoteQualificationTests.swift","line":212,"character":3},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 53 lines","ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines"},"text":" func testAuthorizationEvidenceIsRedactedEphemeralAndRelaunchCleaned() throws {"},{"violation":{"ruleName":"Type Body Length","severity":"error","location":{"file":"TeraTests\/TeraRuntimeClientTests.swift","line":4,"character":7},"reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 298 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"final class TeraRuntimeClientTests: XCTestCase {"},{"violation":{"ruleName":"Type Body Length","severity":"error","location":{"file":"TeraTests\/TeraStateMigrationTests.swift","line":6,"character":7},"reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 580 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"final class TeraStateMigrationTests: XCTestCase {"},{"violation":{"ruleName":"Function Body Length","severity":"error","location":{"file":"TeraTests\/TeraStateMigrationTests.swift","line":7,"character":5},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 61 lines","ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines"},"text":" func testRelayValidationMatchesRuntimeProfiles() throws {"},{"violation":{"ruleName":"File Length","severity":"error","location":{"file":"TeraTests\/TeraStateMigrationTests.swift","line":622,"character":1},"reason":"File should contain 400 lines or less: currently contains 622","ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleName":"Type Body Length","severity":"error","location":{"file":"TeraTests\/TeraSupportingStoreTests.swift","line":207,"character":9},"reason":"Actor body should span 250 lines or less excluding comments and whitespace: currently spans 253 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"private actor SupportingBackend: TeraRuntimeBackend {"},{"violation":{"ruleName":"File Length","severity":"error","location":{"file":"TeraTests\/TeraSupportingStoreTests.swift","line":487,"character":1},"reason":"File should contain 400 lines or less: currently contains 487","ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleName":"Type Body Length","severity":"error","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":4,"character":7},"reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 1493 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"final class TeraRemoteQualificationUITests: XCTestCase {"},{"violation":{"ruleName":"Function Body Length","severity":"error","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":62,"character":3},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 73 lines","ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines"},"text":" func testLocalSocialFiveFlowScenario() throws {"},{"violation":{"ruleName":"Function Body Length","severity":"error","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","ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines"},"text":" func testLocalSocialDeterministicPersonas() throws {"},{"violation":{"ruleName":"Function Body Length","severity":"error","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":247,"character":3},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 54 lines","ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines"},"text":" func testRemoteBlossomUploadAndRecovery() throws {"},{"violation":{"ruleName":"Cyclomatic Complexity","severity":"error","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":573,"character":11},"reason":"Function should have complexity 10 or less; currently complexity is 18","ruleIdentifier":"cyclomatic_complexity","ruleDescription":"Complexity of function bodies should be limited."},"text":" private func expectedAddFieldLabel(_ identifier: String, type: String) -> String {"},{"violation":{"ruleName":"Cyclomatic Complexity","severity":"error","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":767,"character":11},"reason":"Function should have complexity 10 or less; currently complexity is 11","ruleIdentifier":"cyclomatic_complexity","ruleDescription":"Complexity of function bodies should be limited."},"text":" private func preparePhotoUpdate(_ app: XCUIApplication, marker: String) -> XCUIElement? {"},{"violation":{"ruleName":"Function Body Length","severity":"error","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":767,"character":11},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 66 lines","ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines"},"text":" private func preparePhotoUpdate(_ app: XCUIApplication, marker: String) -> XCUIElement? {"},{"violation":{"ruleName":"Function Body Length","severity":"error","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":1736,"character":10},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 67 lines","ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines"},"text":" static func environment("},{"violation":{"ruleName":"File Length","severity":"error","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":2212,"character":1},"reason":"File should contain 1000 lines or less: currently contains 2212","ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines."},"text":"}"}] +\ No newline at end of file