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 19ccb99f20af7a2abfadce61862fcc54f1af61ad
parent 8f60150b66eb3dda84fbcc8702bf7297bc0a5ad8
Author: triesap <tyson@radroots.org>
Date:   Thu, 10 Sep 2026 09:22:58 +0000

ios: Render civil calendar dates directly

- Format locale-aware civil components without timezone conversion
- Preserve exclusive ranges and distinct timed display and event zones
- Align feed and detail accessibility with visible calendar text
- Verify native semantics screenshots and current standalone consumers

Diffstat:
MPackage.swift | 3+++
MTera.xcodeproj/project.pbxproj | 4++++
ATera/Runtime/TeraCalendarPresentation.swift | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MTera/Runtime/TeraCalendarTiming.swift | 8--------
ATera/Runtime/TeraCivilDateFormat.swift | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MTera/Runtime/TeraRuntimeModels.swift | 20--------------------
ATera/Views/TeraCalendarMetadata.swift | 20++++++++++++++++++++
MTera/Views/TeraTodayCardPresentation.swift | 7+++++--
MTera/Views/TeraTodayView.swift | 18++++--------------
ATeraTests/TeraCalendarPresentationTests.swift | 145+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MTeraTests/TeraScopeFixtures.swift | 8++++++--
Mproject.yml | 1+
Mrelease/provenance.json | 2+-
Mtest-fixtures/maintainability-baseline.v1.json | 2+-
Mtest-fixtures/swiftlint-maintainability-baseline.v1.json | 4++--
15 files changed, 368 insertions(+), 50 deletions(-)

diff --git a/Package.swift b/Package.swift @@ -58,6 +58,8 @@ let package = Package( "Runtime/TeraAddMediaCoordinator.swift", "Runtime/TeraBackgroundUploadRequest.swift", "Runtime/TeraCalendarTiming.swift", + "Runtime/TeraCalendarPresentation.swift", + "Runtime/TeraCivilDateFormat.swift", "Runtime/TeraCheckedTime.swift", "Runtime/TeraErrorRecovery.swift", "Runtime/TeraGeneratedCalendarCards.swift", @@ -111,6 +113,7 @@ let package = Package( "Views/TeraTodayCurrentDetail.swift", "Views/TeraTodayDetailView.swift", "Views/TeraTodayCardPresentation.swift", + "Views/TeraCalendarMetadata.swift", "Views/TeraTodayStatusView.swift", "Views/TeraTodayPagingStatusView.swift", "Views/RuntimeStatusView.swift", diff --git a/Tera.xcodeproj/project.pbxproj b/Tera.xcodeproj/project.pbxproj @@ -55,6 +55,7 @@ B4AE86BE0A9F7FBE0DEBD9AC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = ADFD21D28C5F9E58B70E76CA /* Localizable.strings */; }; B64A836C5E84924D28CB5D47 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AB8A30A75EFD65EFD938D248 /* Assets.xcassets */; }; BB71614D2A6017C071A39BED /* TeraMutationAdmissionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F322CF98F8BEB908B93215 /* TeraMutationAdmissionTests.swift */; }; + BF0BF2D7F46217C4EBCA570C /* TeraCalendarPresentationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFC5639BDCD2A5C1C27FE38 /* TeraCalendarPresentationTests.swift */; }; C0C7D6F0CBCE262C348D3C3E /* TeraTodayReconciliationFFITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F65B01F6F5F7E125148EC4FD /* TeraTodayReconciliationFFITests.swift */; }; C219B5F4E24188E1116BF45B /* TeraRuntimeClientFixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88ED360C84532DA7088BFC6E /* TeraRuntimeClientFixtures.swift */; }; C3177E827FE62639579C3EF5 /* TeraRemoteQualificationUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32CBDAE974FB8D7408108E69 /* TeraRemoteQualificationUITests.swift */; }; @@ -115,6 +116,7 @@ 57CA176D0A7566A4ADD2CEA4 /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; }; 5854504D0CAD5ABC29302292 /* TeraTodaySyncFixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraTodaySyncFixtures.swift; sourceTree = "<group>"; }; 58E711687008866790042792 /* TeraBindingPortabilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraBindingPortabilityTests.swift; sourceTree = "<group>"; }; + 5CFC5639BDCD2A5C1C27FE38 /* TeraCalendarPresentationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraCalendarPresentationTests.swift; sourceTree = "<group>"; }; 5EB21C33D6173E4FED05FDA0 /* TeraTodayReconciliationTaskTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraTodayReconciliationTaskTests.swift; sourceTree = "<group>"; }; 61B6F0B16BFD8F046E9C163F /* TeraMediaStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraMediaStoreTests.swift; sourceTree = "<group>"; }; 61D8C0835D5A1D5C36D80020 /* TeraBackgroundUploadFixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraBackgroundUploadFixtures.swift; sourceTree = "<group>"; }; @@ -230,6 +232,7 @@ 61D8C0835D5A1D5C36D80020 /* TeraBackgroundUploadFixtures.swift */, 58E711687008866790042792 /* TeraBindingPortabilityTests.swift */, 3B808E67E85CD36E49C38B6F /* TeraCalendarFFICompatibilityTests.swift */, + 5CFC5639BDCD2A5C1C27FE38 /* TeraCalendarPresentationTests.swift */, 36AF0D194520F0F02EAD6B50 /* TeraCalendarTimingTests.swift */, 2A506546B0EC1792240C36F7 /* TeraClockTests.swift */, 93A3358AFC23C567DB9CB02F /* TeraErrorRecoveryTests.swift */, @@ -493,6 +496,7 @@ 6F53FF88A29F8DF93F074EB4 /* TeraBackgroundUploadFixtures.swift in Sources */, FB585785DC906CB2AE4AE535 /* TeraBindingPortabilityTests.swift in Sources */, 772A9B1AD8DE1D2E0969F285 /* TeraCalendarFFICompatibilityTests.swift in Sources */, + BF0BF2D7F46217C4EBCA570C /* TeraCalendarPresentationTests.swift in Sources */, 9309D7175535546A0C878F75 /* TeraCalendarTimingTests.swift in Sources */, 70417FB635F06CC9829FB38C /* TeraClockTests.swift in Sources */, D918A664D9A4B9859A1A6503 /* TeraErrorRecoveryTests.swift in Sources */, diff --git a/Tera/Runtime/TeraCalendarPresentation.swift b/Tera/Runtime/TeraCalendarPresentation.swift @@ -0,0 +1,79 @@ +import Foundation + +struct TeraCalendarPresentation { + var locale: Locale = .current + var timeZone: TimeZone = .current + + func summary(_ timing: TeraCalendarTiming) -> String { + switch timing { + case let .dateBased(start, end): + let format = TeraCivilDateFormat(locale: locale) + guard let end else { return "All day, \(format.string(start))" } + guard end > start, let last = end.previousDay else { return "Date unavailable" } + let range = last == start ? format.string(start) : "\(format.string(start)) – \(format.string(last))" + return "All day, \(range)" + case let .timeBased(start, end, startZone, endZone): + guard end == nil || end.map({ $0 > start }) == true else { return "Date unavailable" } + var parts = [instant(start, zone: timeZone, label: "Starts")] + if let end { + parts.append(instant(end, zone: timeZone, label: "Ends")) + } + parts.append("Display time zone: \(timeZone.identifier)") + if let startZone { + parts.append(sourceInstant(start, zoneID: startZone, label: "Event start")) + } + if let end, let endZone { + parts.append(sourceInstant(end, zoneID: endZone, label: "Event end")) + } + return parts.joined(separator: "; ") + } + } + + private func sourceInstant(_ seconds: UInt64, zoneID: String, label: String) -> String { + guard zoneID.utf8.count <= 255, let zone = TimeZone(identifier: zoneID) else { return "Date unavailable" } + return "\(instant(seconds, zone: zone, label: label)) (\(zoneID))" + } + + private func instant(_ seconds: UInt64, zone: TimeZone, label: String) -> String { + guard let date = TeraCalendarTiming.presentationInstant(seconds) else { return "Date unavailable" } + var calendar = Calendar(identifier: .gregorian) + calendar.timeZone = zone + guard (1 ... 9999).contains(calendar.component(.year, from: date)) else { return "Date unavailable" } + let format = DateFormatter() + format.locale = locale + format.calendar = calendar + format.timeZone = zone + format.dateStyle = .medium + format.timeStyle = .long + return "\(label): \(format.string(from: date))" + } +} + +extension TeraTodayCard { + var accessibilitySummary: String { + accessibilitySummary(locale: .current, timeZone: .current) + } + + func accessibilitySummary(locale: Locale, timeZone: TimeZone) -> String { + var parts = [type.label, "by \(authorName)"] + if let title { + parts.append(title) + } + if !content.isEmpty { + parts.append(content) + } + if let calendarTiming { + parts.append(TeraCalendarPresentation(locale: locale, timeZone: timeZone).summary(calendarTiming)) + } + if let priceSummary { + parts.append(priceSummary) + } + if lifecycle != .active { + parts.append(lifecycle.rawValue) + } + if let localOperationState { + parts.append(localOperationState) + } + return parts.joined(separator: ", ") + } +} diff --git a/Tera/Runtime/TeraCalendarTiming.swift b/Tera/Runtime/TeraCalendarTiming.swift @@ -51,14 +51,6 @@ enum TeraCalendarTiming: Sendable, Equatable, Hashable { guard seconds <= 253_402_300_799 else { return nil } return Date(timeIntervalSince1970: TimeInterval(seconds)) } - - var summary: String { - switch self { - case let .dateBased(start, _): start.canonical - case let .timeBased(start, _, _, _): - Self.presentationInstant(start)?.formatted(date: .abbreviated, time: .shortened) ?? "Date unavailable" - } - } } extension FfiCivilDate { diff --git a/Tera/Runtime/TeraCivilDateFormat.swift b/Tera/Runtime/TeraCivilDateFormat.swift @@ -0,0 +1,97 @@ +import Foundation + +/// Locale field order and symbols applied directly to Gregorian components. +/// No civil date is represented by an instant, even temporarily. +struct TeraCivilDateFormat { + let locale: Locale + + func string(_ value: TeraCivilDate) -> String { + guard let pattern = DateFormatter.dateFormat(fromTemplate: "yMMMd", options: 0, locale: locale), + pattern.utf8.count <= 128 + else { return value.canonical } + let symbols = DateFormatter() + symbols.locale = locale + symbols.calendar = Calendar(identifier: .gregorian) + return render(pattern, value: value, symbols: symbols) ?? value.canonical + } + + private func render(_ pattern: String, value: TeraCivilDate, symbols: DateFormatter) -> String? { + let characters = Array(pattern) + var index = 0 + var quoted = false + var output = "" + while index < characters.count { + let character = characters[index] + if character == "'" { + if index + 1 < characters.count, characters[index + 1] == "'" { + output.append("'") + index += 2 + } else { + quoted.toggle() + index += 1 + } + } else if !quoted, character.isASCII, character.isLetter { + let start = index + while index < characters.count, characters[index] == character { + index += 1 + } + guard let field = field(character, count: index - start, value: value, symbols: symbols) else { return nil } + output += field + } else { + output.append(character) + index += 1 + } + } + return !quoted && output.utf8.count <= 512 ? output : nil + } + + private func field(_ field: Character, count: Int, value: TeraCivilDate, symbols: DateFormatter) -> String? { + switch field { + case "y": number(Int(value.year), minimumDigits: count == 2 ? 1 : min(count, 4)) + case "d": number(Int(value.day), minimumDigits: min(count, 2)) + case "M", "L": month(Int(value.month), count: count, standalone: field == "L", symbols: symbols) + case "G": symbols.eraSymbols.last + default: nil + } + } + + private func month(_ month: Int, count: Int, standalone: Bool, symbols: DateFormatter) -> String? { + if count <= 2 { + return number(month, minimumDigits: count) + } + let names: [String] = switch count { + case 3: standalone ? symbols.shortStandaloneMonthSymbols : symbols.shortMonthSymbols + case 4: standalone ? symbols.standaloneMonthSymbols : symbols.monthSymbols + default: standalone ? symbols.veryShortStandaloneMonthSymbols : symbols.veryShortMonthSymbols + } + return names.indices.contains(month - 1) ? names[month - 1] : nil + } + + private func number(_ value: Int, minimumDigits: Int) -> String? { + let formatter = NumberFormatter() + formatter.locale = locale + formatter.numberStyle = .decimal + formatter.usesGroupingSeparator = false + formatter.minimumIntegerDigits = minimumDigits + formatter.maximumFractionDigits = 0 + return formatter.string(from: NSNumber(value: value)) + } +} + +extension TeraCivilDate { + var previousDay: Self? { + if day > 1 { + return try? Self(year: year, month: month, day: day - 1) + } + guard year > 1 || month > 1 else { return nil } + let previousYear = month == 1 ? year - 1 : year + let previousMonth = month == 1 ? 12 : month - 1 + // At most four validated component candidates; no timezone arithmetic. + for previousDay: UInt8 in stride(from: 31, through: 28, by: -1) { + if let value = try? Self(year: previousYear, month: previousMonth, day: previousDay) { + return value + } + } + return nil + } +} diff --git a/Tera/Runtime/TeraRuntimeModels.swift b/Tera/Runtime/TeraRuntimeModels.swift @@ -646,26 +646,6 @@ struct TeraTodayCard: Sendable, Equatable, Hashable, Identifiable { return "\(priceAmount) \(priceCurrency)/\(priceUnit)" } - var accessibilitySummary: String { - var parts = [type.label, "by \(authorName)"] - if let title { - parts.append(title) - } - if !content.isEmpty { - parts.append(content) - } - if let priceSummary { - parts.append(priceSummary) - } - if lifecycle != .active { - parts.append(lifecycle.rawValue) - } - if let localOperationState { - parts.append(localOperationState) - } - return parts.joined(separator: ", ") - } - var retractionTargetKind: UInt32? { switch type { case .update, .photoUpdate, .ask: diff --git a/Tera/Views/TeraCalendarMetadata.swift b/Tera/Views/TeraCalendarMetadata.swift @@ -0,0 +1,20 @@ +import SwiftUI + +struct TeraCalendarMetadata: View { + let card: TeraTodayCard + let presentation: TeraTodayCardPresentation + @Environment(\.locale) private var locale + @Environment(\.timeZone) private var timeZone + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + if let timing = card.calendarTiming { + Label(TeraCalendarPresentation(locale: locale, timeZone: timeZone).summary(timing), systemImage: "calendar") + } + if let location = card.location { + Label(presentation.label(location), systemImage: "mappin.and.ellipse") + } + } + .font(.subheadline) + } +} diff --git a/Tera/Views/TeraTodayCardPresentation.swift b/Tera/Views/TeraTodayCardPresentation.swift @@ -22,15 +22,18 @@ enum TeraTodayCardPresentation { self == .detail ? values : Array(values.prefix(Self.thumbnails)) } - func accessibility(_ card: TeraTodayCard) -> String { + func accessibility(_ card: TeraTodayCard, locale: Locale = .current, timeZone: TimeZone = .current) -> String { if self == .detail { - return card.accessibilitySummary + return card.accessibilitySummary(locale: locale, timeZone: timeZone) } 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 timing = card.calendarTiming { + parts.append(TeraCalendarPresentation(locale: locale, timeZone: timeZone).summary(timing)) + } if let price = card.priceSummary { parts.append(label(price)) } diff --git a/Tera/Views/TeraTodayView.swift b/Tera/Views/TeraTodayView.swift @@ -190,6 +190,8 @@ struct TeraTodayView: View { } struct TeraTodayCardView: View { + @Environment(\.locale) private var locale + @Environment(\.timeZone) private var timeZone let card: TeraTodayCard let context: TeraLocalNetwork? @ObservedObject var mediaStore: TeraMediaStore @@ -219,7 +221,7 @@ struct TeraTodayCardView: View { } if card.type == .event { - eventMetadata + TeraCalendarMetadata(card: card, presentation: presentation) } if card.type == .foodAvailability { foodMetadata @@ -248,19 +250,7 @@ struct TeraTodayCardView: View { } .padding(.vertical, 8) .accessibilityElement(children: .combine) - .accessibilityLabel(presentation.accessibility(card)) - } - - private var eventMetadata: some View { - VStack(alignment: .leading, spacing: 4) { - if let timing = card.calendarTiming { - Label(timing.summary, systemImage: "calendar") - } - if let location = card.location { - Label(presentation.label(location), systemImage: "mappin.and.ellipse") - } - } - .font(.subheadline) + .accessibilityLabel(presentation.accessibility(card, locale: locale, timeZone: timeZone)) } private var foodMetadata: some View { diff --git a/TeraTests/TeraCalendarPresentationTests.swift b/TeraTests/TeraCalendarPresentationTests.swift @@ -0,0 +1,145 @@ +import Foundation +import SwiftUI +@testable import TeraApp +import XCTest + +final class TeraCalendarPresentationTests: XCTestCase { + private let locale = Locale(identifier: "en_US") + + func testSeptemberFifthNeverMovesAcrossDeviceTimeZones() throws { + let timing = try civil(2026, 9, 5) + for name in ["America/Vancouver", "UTC", "Pacific/Kiritimati", "Pacific/Pago_Pago", "Asia/Kathmandu"] { + let zone = try XCTUnwrap(TimeZone(identifier: name)) + XCTAssertEqual(TeraCalendarPresentation(locale: locale, timeZone: zone).summary(timing), "All day, Sep 5, 2026") + } + } + + func testCivilLocaleOrderMonthNamesAndNumeralsUseTheSameGregorianDate() throws { + let value = try TeraCivilDate(year: 2026, month: 9, day: 5) + for (identifier, expected) in [("en_US", "Sep 5, 2026"), ("en_GB", "5 Sep 2026"), + ("fr_FR", "5 sept. 2026"), ("ja_JP", "2026年9月5日")] + { + XCTAssertEqual(TeraCivilDateFormat(locale: Locale(identifier: identifier)).string(value), expected, identifier) + } + let arabic = TeraCivilDateFormat(locale: Locale(identifier: "ar_EG")).string(value) + XCTAssertTrue(arabic.contains("٥")) + XCTAssertTrue(arabic.contains("٢٠٢٦")) + XCTAssertEqual(value.canonical, "2026-09-05") + } + + func testExclusiveCivilEndsDisplayOnlyIncludedLeapAndYearBoundaryDays() throws { + let format = TeraCalendarPresentation(locale: locale) + XCTAssertEqual(try format.summary(civil(2024, 2, 29, end: date(2024, 3, 1))), "All day, Feb 29, 2024") + XCTAssertEqual(try format.summary(civil(2024, 2, 28, end: date(2024, 3, 1))), "All day, Feb 28, 2024 – Feb 29, 2024") + XCTAssertEqual(try format.summary(civil(2026, 9, 5, end: date(2026, 9, 7))), "All day, Sep 5, 2026 – Sep 6, 2026") + XCTAssertEqual(try format.summary(civil(2025, 12, 31, end: date(2026, 1, 2))), "All day, Dec 31, 2025 – Jan 1, 2026") + XCTAssertEqual(try format.summary(civil(2026, 9, 5, end: date(2026, 9, 5))), "Date unavailable") + } + + func testCivilPredecessorHandlesGregorianCenturyAndSupportedExtremes() throws { + for (year, last) in [(UInt16(1900), UInt8(28)), (2000, 29), (2100, 28), (2400, 29)] { + XCTAssertEqual(try TeraCivilDate(year: year, month: 3, day: 1).previousDay, + try TeraCivilDate(year: year, month: 2, day: last)) + } + XCTAssertNil(try TeraCivilDate(year: 1, month: 1, day: 1).previousDay) + XCTAssertEqual(try TeraCivilDate(year: 9999, month: 12, day: 31).previousDay, + try TeraCivilDate(year: 9999, month: 12, day: 30)) + XCTAssertTrue(try TeraCalendarPresentation(locale: locale).summary(civil(9999, 12, 31)).contains("9999")) + } + + func testTimedDisplayRetainsExactInstantsAndDistinctSourceZones() throws { + let timing = TeraCalendarTiming.timeBased(startUnixSeconds: 0, endExclusiveUnixSeconds: 3600, + startTimezoneID: "America/Vancouver", endTimezoneID: "Europe/Paris") + let zone = try XCTUnwrap(TimeZone(identifier: "UTC")) + let text = TeraCalendarPresentation(locale: locale, timeZone: zone).summary(timing) + XCTAssertTrue(text.contains("Starts: Jan 1, 1970")) + XCTAssertTrue(text.contains("Ends: Jan 1, 1970")) + XCTAssertTrue(text.contains("Display time zone: \(zone.identifier)")) + XCTAssertTrue(text.contains("Event start: Dec 31, 1969")) + XCTAssertTrue(text.contains("(America/Vancouver)")) + XCTAssertTrue(text.contains("Event end: Jan 1, 1970")) + XCTAssertTrue(text.contains("(Europe/Paris)")) + XCTAssertFalse(text.contains("All day")) + XCTAssertEqual(timing, .timeBased(startUnixSeconds: 0, endExclusiveUnixSeconds: 3600, + startTimezoneID: "America/Vancouver", endTimezoneID: "Europe/Paris")) + } + + func testFullUnsignedWireValuesNeverBecomeRoundedOrInventedDisplayDates() { + for seconds: UInt64 in [253_402_300_800, (1 << 53) + 1, .max] { + let timing = TeraCalendarTiming.timeBased(startUnixSeconds: seconds, endExclusiveUnixSeconds: nil, + startTimezoneID: nil, endTimezoneID: nil) + let text = TeraCalendarPresentation(locale: locale).summary(timing) + XCTAssertTrue(text.contains("Date unavailable")) + XCTAssertFalse(text.contains("1970")) + XCTAssertFalse(text.contains("Event start")) + } + } + + func testRepeatedWallTimesRetainDaylightSavingOffsetsAndLocalYearBounds() throws { + let zone = try XCTUnwrap(TimeZone(identifier: "America/Vancouver")) + let format = TeraCalendarPresentation(locale: locale, timeZone: zone) + // Use a historical overlap: current tzdata keeps Vancouver on PDT in late 2026. + let timing = TeraCalendarTiming.timeBased(startUnixSeconds: 1_730_622_600, endExclusiveUnixSeconds: 1_730_626_200, + startTimezoneID: nil, endTimezoneID: nil) + let text = format.summary(timing) + XCTAssertTrue(text.contains("PDT"), text) + XCTAssertTrue(text.contains("PST"), text) + let east = try XCTUnwrap(TimeZone(identifier: "Pacific/Kiritimati")) + let upper = TeraCalendarTiming.timeBased(startUnixSeconds: 253_402_300_799, endExclusiveUnixSeconds: nil, + startTimezoneID: nil, endTimezoneID: nil) + XCTAssertTrue(TeraCalendarPresentation(locale: locale, timeZone: east).summary(upper).contains("Date unavailable")) + } + + func testFeedAndDetailAccessibilityIncludeTheIdenticalVisibleTiming() throws { + let zone = try XCTUnwrap(TimeZone(identifier: "America/Vancouver")) + for timing in try [civil(2026, 9, 5, end: date(2026, 9, 7)), + .timeBased(startUnixSeconds: 0, endExclusiveUnixSeconds: 3600, + startTimezoneID: "Europe/Paris", endTimezoneID: nil)] + { + let card = TeraScopeFixtures.card("Calendar example", timing: timing) + let visible = TeraCalendarPresentation(locale: locale, timeZone: zone).summary(timing) + for presentation: TeraTodayCardPresentation in [.feed, .detail] { + XCTAssertTrue(presentation.accessibility(card, locale: locale, timeZone: zone).contains(visible)) + } + XCTAssertTrue(card.accessibilitySummary(locale: locale, timeZone: zone).contains(visible)) + } + } + + @MainActor + func testProductionCalendarCardScreenshotsRetainRangeAndAccessibleText() async throws { + let client = try await TeraScopeFixtures.client(TeraScopeBackend()) + let store = TeraMediaStore(runtimeClient: client) + let zone = try XCTUnwrap(TimeZone(identifier: "America/Vancouver")) + let card = try TeraScopeFixtures.card("Community harvest", timing: civil(2026, 9, 5, end: date(2026, 9, 7))) + for (name, presentation, size) in [("feed", TeraTodayCardPresentation.feed, DynamicTypeSize.large), + ("detail-accessible", .detail, .accessibility3)] + { + XCTAssertTrue(presentation.accessibility(card, locale: locale, timeZone: zone) + .contains("All day, Sep 5, 2026 – Sep 6, 2026")) + let content = TeraTodayCardView(card: card, context: nil, mediaStore: store, presentation: presentation) + .environment(\.locale, locale).environment(\.timeZone, zone) + .environment(\.dynamicTypeSize, size).environment(\.colorScheme, .light) + .padding(16).frame(width: 390).background(.white) + let renderer = ImageRenderer(content: content) + renderer.scale = 2 + let image = try XCTUnwrap(renderer.uiImage) + XCTAssertEqual(image.size.width, 390) + XCTAssertGreaterThan(image.size.height, 100) + let attachment = XCTAttachment(image: image) + attachment.name = "c050-calendar-\(name)" + attachment.lifetime = .keepAlways + add(attachment) + } + _ = try await client.stop() + } + + private func civil(_ year: UInt16, _ month: UInt8, _ day: UInt8, + end: TeraCivilDate? = nil) throws -> TeraCalendarTiming + { + try .dateBased(start: date(year, month, day), endExclusive: end) + } + + private func date(_ year: UInt16, _ month: UInt8, _ day: UInt8) throws -> TeraCivilDate { + try TeraCivilDate(year: year, month: month, day: day) + } +} diff --git a/TeraTests/TeraScopeFixtures.swift b/TeraTests/TeraScopeFixtures.swift @@ -35,11 +35,15 @@ enum TeraScopeFixtures { } static func card(_ id: String) -> TeraTodayCard { + card(id, timing: nil) + } + + static func card(_ id: String, timing: TeraCalendarTiming?) -> TeraTodayCard { TeraTodayCard( - id: id, type: .update, sourceEventID: id, sourceAddress: nil, + id: id, type: timing == nil ? .update : .event, sourceEventID: id, sourceAddress: nil, authorPublicKey: String(repeating: "a", count: 64), contractID: "test.update", title: nil, content: id, authoredAtUnixSeconds: 1, effectiveAtUnixSeconds: 1, - calendarTiming: nil, location: nil, + calendarTiming: timing, location: nil, priceAmount: nil, priceCurrency: nil, priceUnit: nil, quantity: nil, foodSummary: nil, foodPublishedAtUnixSeconds: nil, foodStatus: nil, contextRank: 1, inclusionReason: "local", media: [], lifecycle: .active, rankDigest: nil, authorProfile: nil, thread: [], diff --git a/project.yml b/project.yml @@ -99,6 +99,7 @@ targets: - path: TeraTests/TeraClockTests.swift - path: TeraTests/TeraCalendarFFICompatibilityTests.swift - path: TeraTests/TeraCalendarTimingTests.swift + - path: TeraTests/TeraCalendarPresentationTests.swift - path: TeraTests/TeraRemoteQualificationTests.swift - path: TeraTests/TeraUserMessagesTests.swift settings: diff --git a/release/provenance.json b/release/provenance.json @@ -6,7 +6,7 @@ "info_plist_sha256": "15ef08b1cdd1096cfb9eeaf5be5bf8f814807a7ca9350bbbb47860fa72ec13ef", "privacy_manifest_sha256": "a331d51864743ebe4e00dd22360b4a538b6b3ac26a6b3eb54094e60a36959a12", "sbom_sha256": "bea2d48c06e5f7e84cdad085d81290712674b74664456f2030064d9cf94c4b33", - "xcode_project_sha256": "1e6634c28e0380672b89b8469afe4bbdc3eb1811b9aee0a88837ef4509017530" + "xcode_project_sha256": "c1c42bc8deec2d46e8047d39c4278beb882410891f0d8221689dd3236c65028c" }, "disposition": "unsigned", "platforms": [ diff --git a/test-fixtures/maintainability-baseline.v1.json b/test-fixtures/maintainability-baseline.v1.json @@ -10,7 +10,7 @@ "swift_file_exception": [ {"path": "Tera/Runtime/TeraGeneratedRuntimeBackend.swift", "maximum_lines": 1531}, {"path": "Tera/Runtime/TeraRuntimeClient.swift", "maximum_lines": 835}, - {"path": "Tera/Runtime/TeraRuntimeModels.swift", "maximum_lines": 1175}, + {"path": "Tera/Runtime/TeraRuntimeModels.swift", "maximum_lines": 1154}, {"path": "Tera/State/TeraAddStore.swift", "maximum_lines": 810}, {"path": "Tera/State/TeraConfigurationStore.swift", "maximum_lines": 759}, {"path": "Tera/Views/TeraSupportingViews.swift", "maximum_lines": 629}, diff --git a/test-fixtures/swiftlint-maintainability-baseline.v1.json b/test-fixtures/swiftlint-maintainability-baseline.v1.json @@ -1 +1 @@ -[{"violation":{"location":{"file":"Tera\/App\/TeraAppModel.swift","character":3,"line":21},"ruleName":"Function Body Length","reason":"Initializer body should span 50 lines or less excluding comments and whitespace: currently spans 52 lines","ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","severity":"error"},"text":" init("},{"violation":{"location":{"file":"Tera\/Runtime\/TeraAddMediaCoordinator.swift","character":1,"line":51},"ruleName":"Type Body Length","reason":"Actor body should span 250 lines or less excluding comments and whitespace: currently spans 330 lines","ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","severity":"error"},"text":"actor TeraAddMediaCoordinator: TeraAddMediaHandling {"},{"violation":{"location":{"file":"Tera\/Runtime\/TeraAddMediaCoordinator.swift","character":1,"line":458},"ruleName":"File Length","reason":"File should contain 400 lines or less: currently contains 458","ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","severity":"error"},"text":"#endif"},{"violation":{"location":{"file":"Tera\/Runtime\/TeraGeneratedRuntimeBackend.swift","character":15,"line":111},"ruleName":"Type Body Length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 491 lines","ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","severity":"error"},"text":"private final class TeraGeneratedRuntimeBackend: TeraRuntimeBackend, @unchecked Sendable {"},{"violation":{"location":{"file":"Tera\/Runtime\/TeraGeneratedRuntimeBackend.swift","character":1,"line":1531},"ruleName":"File Length","reason":"File should contain 1000 lines or less: currently contains 1531","ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","severity":"error"},"text":"}"},{"violation":{"location":{"file":"Tera\/Runtime\/TeraRuntimeClient.swift","character":1,"line":3},"ruleName":"Type Body Length","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 753 lines","ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","severity":"error"},"text":"actor TeraRuntimeClient {"},{"violation":{"location":{"file":"Tera\/Runtime\/TeraRuntimeClient.swift","character":3,"line":379},"ruleName":"Function Body Length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 70 lines","ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","severity":"error"},"text":" func changes(bufferCapacity: Int = 16) async throws -> AsyncStream<TeraRuntimeChange> {"},{"violation":{"location":{"file":"Tera\/Runtime\/TeraRuntimeClient.swift","character":1,"line":835},"ruleName":"File Length","reason":"File should contain 400 lines or less: currently contains 835","ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","severity":"error"},"text":"}"},{"violation":{"location":{"file":"Tera\/Runtime\/TeraRuntimeModels.swift","character":1,"line":1174},"ruleName":"File Length","reason":"File should contain 1000 lines or less: currently contains 1174","ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","severity":"error"},"text":"}"},{"violation":{"location":{"file":"Tera\/State\/TeraAddStore.swift","character":7,"line":11},"ruleName":"Type Body Length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 731 lines","ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","severity":"error"},"text":"final class TeraAddStore: ObservableObject {"},{"violation":{"location":{"file":"Tera\/State\/TeraAddStore.swift","character":3,"line":459},"ruleName":"Function Body Length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 51 lines","ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","severity":"error"},"text":" func retract(_ card: TeraTodayCard) async {"},{"violation":{"location":{"file":"Tera\/State\/TeraAddStore.swift","character":1,"line":796},"ruleName":"File Length","reason":"File should contain 400 lines or less: currently contains 796","ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","severity":"error"},"text":"}"},{"violation":{"location":{"file":"Tera\/State\/TeraConfigurationStore.swift","character":1,"line":78},"ruleName":"Type Body Length","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 468 lines","ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","severity":"error"},"text":"actor TeraConfigurationStore {"},{"violation":{"location":{"file":"Tera\/State\/TeraConfigurationStore.swift","character":5,"line":148},"ruleName":"Function Body Length","reason":"Function body should span 100 lines or less excluding comments and whitespace: currently spans 109 lines","ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","severity":"error"},"text":" func load() throws -> TeraAppConfiguration {"},{"violation":{"location":{"file":"Tera\/State\/TeraConfigurationStore.swift","character":1,"line":759},"ruleName":"File Length","reason":"File should contain 400 lines or less: currently contains 759","ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","severity":"error"},"text":"}"},{"violation":{"location":{"file":"Tera\/State\/TeraSessionStore.swift","character":1,"line":39},"ruleName":"Type Body Length","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 390 lines","ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","severity":"error"},"text":"actor TeraSessionStore {"},{"violation":{"location":{"file":"Tera\/State\/TeraSessionStore.swift","character":11,"line":125},"ruleName":"Cyclomatic Complexity","reason":"Function should have complexity 10 or less; currently complexity is 13","ruleDescription":"Complexity of function bodies should be limited.","ruleIdentifier":"cyclomatic_complexity","severity":"error"},"text":" private func start(acceptingReconfiguration: Bool) async -> TeraSessionPhase {"},{"violation":{"location":{"file":"Tera\/State\/TeraSessionStore.swift","character":1,"line":452},"ruleName":"File Length","reason":"File should contain 400 lines or less: currently contains 452","ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","severity":"error"},"text":"}"},{"violation":{"location":{"file":"Tera\/Views\/TeraAddView.swift","character":1,"line":4},"ruleName":"Type Body Length","reason":"Struct body should span 350 lines or less excluding comments and whitespace: currently spans 454 lines","ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","severity":"error"},"text":"struct TeraAddView: View {"},{"violation":{"location":{"file":"Tera\/Views\/TeraAddView.swift","character":1,"line":560},"ruleName":"File Length","reason":"File should contain 400 lines or less: currently contains 560","ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","severity":"error"},"text":"}"},{"violation":{"location":{"file":"Tera\/Views\/TeraSupportingViews.swift","character":1,"line":365},"ruleName":"Type Body Length","reason":"Struct body should span 250 lines or less excluding comments and whitespace: currently spans 259 lines","ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","severity":"error"},"text":"struct TeraSettingsView: View {"},{"violation":{"location":{"file":"Tera\/Views\/TeraSupportingViews.swift","character":1,"line":629},"ruleName":"File Length","reason":"File should contain 400 lines or less: currently contains 629","ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","severity":"error"},"text":"}"},{"violation":{"location":{"file":"TeraTests\/TeraAddStoreTests.swift","character":7,"line":6},"ruleName":"Type Body Length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 664 lines","ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","severity":"error"},"text":"final class TeraAddStoreTests: XCTestCase {"},{"violation":{"location":{"file":"TeraTests\/TeraAddStoreTests.swift","character":9,"line":843},"ruleName":"Type Body Length","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 492 lines","ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","severity":"error"},"text":"private actor AddBackend: TeraRuntimeBackend {"},{"violation":{"location":{"file":"TeraTests\/TeraAddStoreTests.swift","character":11,"line":1279},"ruleName":"Function Parameter Count","reason":"Function should have 5 parameters or less: it currently has 8","ruleDescription":"Number of function parameters should be low.","ruleIdentifier":"function_parameter_count","severity":"error"},"text":" private func makeStatus("},{"violation":{"location":{"file":"TeraTests\/TeraAddStoreTests.swift","character":1,"line":1430},"ruleName":"File Length","reason":"File should contain 1000 lines or less: currently contains 1430","ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","severity":"error"},"text":"}"},{"violation":{"location":{"file":"TeraTests\/TeraRemoteQualificationTests.swift","character":7,"line":7},"ruleName":"Type Body Length","reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 307 lines","ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","severity":"error"},"text":"final class TeraRemoteQualificationTests: XCTestCase {"},{"violation":{"location":{"file":"TeraTests\/TeraRemoteQualificationTests.swift","character":3,"line":45},"ruleName":"Function Body Length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 94 lines","ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","severity":"error"},"text":" func testQualificationRejectsAmbiguousOrUnsafeHarnessValues() {"},{"violation":{"location":{"file":"TeraTests\/TeraRemoteQualificationTests.swift","character":3,"line":212},"ruleName":"Function Body Length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 53 lines","ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","severity":"error"},"text":" func testAuthorizationEvidenceIsRedactedEphemeralAndRelaunchCleaned() throws {"},{"violation":{"location":{"file":"TeraTests\/TeraRuntimeClientTests.swift","character":7,"line":4},"ruleName":"Type Body Length","reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 298 lines","ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","severity":"error"},"text":"final class TeraRuntimeClientTests: XCTestCase {"},{"violation":{"location":{"file":"TeraTests\/TeraStateMigrationTests.swift","character":7,"line":6},"ruleName":"Type Body Length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 580 lines","ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","severity":"error"},"text":"final class TeraStateMigrationTests: XCTestCase {"},{"violation":{"location":{"file":"TeraTests\/TeraStateMigrationTests.swift","character":5,"line":7},"ruleName":"Function Body Length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 61 lines","ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","severity":"error"},"text":" func testRelayValidationMatchesRuntimeProfiles() throws {"},{"violation":{"location":{"file":"TeraTests\/TeraStateMigrationTests.swift","character":1,"line":622},"ruleName":"File Length","reason":"File should contain 400 lines or less: currently contains 622","ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","severity":"error"},"text":"}"},{"violation":{"location":{"file":"TeraTests\/TeraSupportingStoreTests.swift","character":9,"line":207},"ruleName":"Type Body Length","reason":"Actor body should span 250 lines or less excluding comments and whitespace: currently spans 252 lines","ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","severity":"error"},"text":"private actor SupportingBackend: TeraRuntimeBackend {"},{"violation":{"location":{"file":"TeraTests\/TeraSupportingStoreTests.swift","character":1,"line":486},"ruleName":"File Length","reason":"File should contain 400 lines or less: currently contains 486","ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","severity":"error"},"text":"}"},{"violation":{"location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":7,"line":4},"ruleName":"Type Body Length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 1493 lines","ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","severity":"error"},"text":"final class TeraRemoteQualificationUITests: XCTestCase {"},{"violation":{"location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":3,"line":62},"ruleName":"Function Body Length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 73 lines","ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","severity":"error"},"text":" func testLocalSocialFiveFlowScenario() throws {"},{"violation":{"location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":3,"line":174},"ruleName":"Function Body Length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 66 lines","ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","severity":"error"},"text":" func testLocalSocialDeterministicPersonas() throws {"},{"violation":{"location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":3,"line":247},"ruleName":"Function Body Length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 54 lines","ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","severity":"error"},"text":" func testRemoteBlossomUploadAndRecovery() throws {"},{"violation":{"location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":11,"line":573},"ruleName":"Cyclomatic Complexity","reason":"Function should have complexity 10 or less; currently complexity is 18","ruleDescription":"Complexity of function bodies should be limited.","ruleIdentifier":"cyclomatic_complexity","severity":"error"},"text":" private func expectedAddFieldLabel(_ identifier: String, type: String) -> String {"},{"violation":{"location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":11,"line":767},"ruleName":"Cyclomatic Complexity","reason":"Function should have complexity 10 or less; currently complexity is 11","ruleDescription":"Complexity of function bodies should be limited.","ruleIdentifier":"cyclomatic_complexity","severity":"error"},"text":" private func preparePhotoUpdate(_ app: XCUIApplication, marker: String) -> XCUIElement? {"},{"violation":{"location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":11,"line":767},"ruleName":"Function Body Length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 66 lines","ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","severity":"error"},"text":" private func preparePhotoUpdate(_ app: XCUIApplication, marker: String) -> XCUIElement? {"},{"violation":{"location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":10,"line":1736},"ruleName":"Function Body Length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 67 lines","ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","severity":"error"},"text":" static func environment("},{"violation":{"location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":1,"line":2212},"ruleName":"File Length","reason":"File should contain 1000 lines or less: currently contains 2212","ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","severity":"error"},"text":"}"}] -\ No newline at end of file +[{"violation":{"ruleIdentifier":"function_body_length","location":{"file":"Tera\/App\/TeraAppModel.swift","character":3,"line":21},"severity":"error","reason":"Initializer body should span 50 lines or less excluding comments and whitespace: currently spans 52 lines","ruleName":"Function Body Length","ruleDescription":"Function bodies should not span too many lines"},"text":" init("},{"violation":{"ruleIdentifier":"type_body_length","location":{"file":"Tera\/Runtime\/TeraAddMediaCoordinator.swift","character":1,"line":51},"severity":"error","reason":"Actor body should span 250 lines or less excluding comments and whitespace: currently spans 330 lines","ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"},"text":"actor TeraAddMediaCoordinator: TeraAddMediaHandling {"},{"violation":{"ruleIdentifier":"file_length","location":{"file":"Tera\/Runtime\/TeraAddMediaCoordinator.swift","character":1,"line":458},"severity":"error","reason":"File should contain 400 lines or less: currently contains 458","ruleName":"File Length","ruleDescription":"Files should not span too many lines."},"text":"#endif"},{"violation":{"ruleIdentifier":"type_body_length","location":{"file":"Tera\/Runtime\/TeraGeneratedRuntimeBackend.swift","character":15,"line":111},"severity":"error","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 491 lines","ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"},"text":"private final class TeraGeneratedRuntimeBackend: TeraRuntimeBackend, @unchecked Sendable {"},{"violation":{"ruleIdentifier":"file_length","location":{"file":"Tera\/Runtime\/TeraGeneratedRuntimeBackend.swift","character":1,"line":1531},"severity":"error","reason":"File should contain 1000 lines or less: currently contains 1531","ruleName":"File Length","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleIdentifier":"type_body_length","location":{"file":"Tera\/Runtime\/TeraRuntimeClient.swift","character":1,"line":3},"severity":"error","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 753 lines","ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"},"text":"actor TeraRuntimeClient {"},{"violation":{"ruleIdentifier":"function_body_length","location":{"file":"Tera\/Runtime\/TeraRuntimeClient.swift","character":3,"line":379},"severity":"error","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 70 lines","ruleName":"Function Body Length","ruleDescription":"Function bodies should not span too many lines"},"text":" func changes(bufferCapacity: Int = 16) async throws -> AsyncStream<TeraRuntimeChange> {"},{"violation":{"ruleIdentifier":"file_length","location":{"file":"Tera\/Runtime\/TeraRuntimeClient.swift","character":1,"line":835},"severity":"error","reason":"File should contain 400 lines or less: currently contains 835","ruleName":"File Length","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleIdentifier":"file_length","location":{"file":"Tera\/Runtime\/TeraRuntimeModels.swift","character":1,"line":1154},"severity":"error","reason":"File should contain 1000 lines or less: currently contains 1154","ruleName":"File Length","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleIdentifier":"type_body_length","location":{"file":"Tera\/State\/TeraAddStore.swift","character":7,"line":11},"severity":"error","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 731 lines","ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"},"text":"final class TeraAddStore: ObservableObject {"},{"violation":{"ruleIdentifier":"function_body_length","location":{"file":"Tera\/State\/TeraAddStore.swift","character":3,"line":459},"severity":"error","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 51 lines","ruleName":"Function Body Length","ruleDescription":"Function bodies should not span too many lines"},"text":" func retract(_ card: TeraTodayCard) async {"},{"violation":{"ruleIdentifier":"file_length","location":{"file":"Tera\/State\/TeraAddStore.swift","character":1,"line":796},"severity":"error","reason":"File should contain 400 lines or less: currently contains 796","ruleName":"File Length","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleIdentifier":"type_body_length","location":{"file":"Tera\/State\/TeraConfigurationStore.swift","character":1,"line":78},"severity":"error","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 468 lines","ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"},"text":"actor TeraConfigurationStore {"},{"violation":{"ruleIdentifier":"function_body_length","location":{"file":"Tera\/State\/TeraConfigurationStore.swift","character":5,"line":148},"severity":"error","reason":"Function body should span 100 lines or less excluding comments and whitespace: currently spans 109 lines","ruleName":"Function Body Length","ruleDescription":"Function bodies should not span too many lines"},"text":" func load() throws -> TeraAppConfiguration {"},{"violation":{"ruleIdentifier":"file_length","location":{"file":"Tera\/State\/TeraConfigurationStore.swift","character":1,"line":759},"severity":"error","reason":"File should contain 400 lines or less: currently contains 759","ruleName":"File Length","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleIdentifier":"type_body_length","location":{"file":"Tera\/State\/TeraSessionStore.swift","character":1,"line":39},"severity":"error","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 390 lines","ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"},"text":"actor TeraSessionStore {"},{"violation":{"ruleIdentifier":"cyclomatic_complexity","location":{"file":"Tera\/State\/TeraSessionStore.swift","character":11,"line":125},"severity":"error","reason":"Function should have complexity 10 or less; currently complexity is 13","ruleName":"Cyclomatic Complexity","ruleDescription":"Complexity of function bodies should be limited."},"text":" private func start(acceptingReconfiguration: Bool) async -> TeraSessionPhase {"},{"violation":{"ruleIdentifier":"file_length","location":{"file":"Tera\/State\/TeraSessionStore.swift","character":1,"line":452},"severity":"error","reason":"File should contain 400 lines or less: currently contains 452","ruleName":"File Length","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleIdentifier":"type_body_length","location":{"file":"Tera\/Views\/TeraAddView.swift","character":1,"line":4},"severity":"error","reason":"Struct body should span 350 lines or less excluding comments and whitespace: currently spans 454 lines","ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"},"text":"struct TeraAddView: View {"},{"violation":{"ruleIdentifier":"file_length","location":{"file":"Tera\/Views\/TeraAddView.swift","character":1,"line":560},"severity":"error","reason":"File should contain 400 lines or less: currently contains 560","ruleName":"File Length","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleIdentifier":"type_body_length","location":{"file":"Tera\/Views\/TeraSupportingViews.swift","character":1,"line":365},"severity":"error","reason":"Struct body should span 250 lines or less excluding comments and whitespace: currently spans 259 lines","ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"},"text":"struct TeraSettingsView: View {"},{"violation":{"ruleIdentifier":"file_length","location":{"file":"Tera\/Views\/TeraSupportingViews.swift","character":1,"line":629},"severity":"error","reason":"File should contain 400 lines or less: currently contains 629","ruleName":"File Length","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleIdentifier":"type_body_length","location":{"file":"TeraTests\/TeraAddStoreTests.swift","character":7,"line":6},"severity":"error","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 664 lines","ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"},"text":"final class TeraAddStoreTests: XCTestCase {"},{"violation":{"ruleIdentifier":"type_body_length","location":{"file":"TeraTests\/TeraAddStoreTests.swift","character":9,"line":843},"severity":"error","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 492 lines","ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"},"text":"private actor AddBackend: TeraRuntimeBackend {"},{"violation":{"ruleIdentifier":"function_parameter_count","location":{"file":"TeraTests\/TeraAddStoreTests.swift","character":11,"line":1279},"severity":"error","reason":"Function should have 5 parameters or less: it currently has 8","ruleName":"Function Parameter Count","ruleDescription":"Number of function parameters should be low."},"text":" private func makeStatus("},{"violation":{"ruleIdentifier":"file_length","location":{"file":"TeraTests\/TeraAddStoreTests.swift","character":1,"line":1430},"severity":"error","reason":"File should contain 1000 lines or less: currently contains 1430","ruleName":"File Length","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleIdentifier":"type_body_length","location":{"file":"TeraTests\/TeraRemoteQualificationTests.swift","character":7,"line":7},"severity":"error","reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 307 lines","ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"},"text":"final class TeraRemoteQualificationTests: XCTestCase {"},{"violation":{"ruleIdentifier":"function_body_length","location":{"file":"TeraTests\/TeraRemoteQualificationTests.swift","character":3,"line":45},"severity":"error","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 94 lines","ruleName":"Function Body Length","ruleDescription":"Function bodies should not span too many lines"},"text":" func testQualificationRejectsAmbiguousOrUnsafeHarnessValues() {"},{"violation":{"ruleIdentifier":"function_body_length","location":{"file":"TeraTests\/TeraRemoteQualificationTests.swift","character":3,"line":212},"severity":"error","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 53 lines","ruleName":"Function Body Length","ruleDescription":"Function bodies should not span too many lines"},"text":" func testAuthorizationEvidenceIsRedactedEphemeralAndRelaunchCleaned() throws {"},{"violation":{"ruleIdentifier":"type_body_length","location":{"file":"TeraTests\/TeraRuntimeClientTests.swift","character":7,"line":4},"severity":"error","reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 298 lines","ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"},"text":"final class TeraRuntimeClientTests: XCTestCase {"},{"violation":{"ruleIdentifier":"type_body_length","location":{"file":"TeraTests\/TeraStateMigrationTests.swift","character":7,"line":6},"severity":"error","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 580 lines","ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"},"text":"final class TeraStateMigrationTests: XCTestCase {"},{"violation":{"ruleIdentifier":"function_body_length","location":{"file":"TeraTests\/TeraStateMigrationTests.swift","character":5,"line":7},"severity":"error","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 61 lines","ruleName":"Function Body Length","ruleDescription":"Function bodies should not span too many lines"},"text":" func testRelayValidationMatchesRuntimeProfiles() throws {"},{"violation":{"ruleIdentifier":"file_length","location":{"file":"TeraTests\/TeraStateMigrationTests.swift","character":1,"line":622},"severity":"error","reason":"File should contain 400 lines or less: currently contains 622","ruleName":"File Length","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleIdentifier":"type_body_length","location":{"file":"TeraTests\/TeraSupportingStoreTests.swift","character":9,"line":207},"severity":"error","reason":"Actor body should span 250 lines or less excluding comments and whitespace: currently spans 252 lines","ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"},"text":"private actor SupportingBackend: TeraRuntimeBackend {"},{"violation":{"ruleIdentifier":"file_length","location":{"file":"TeraTests\/TeraSupportingStoreTests.swift","character":1,"line":486},"severity":"error","reason":"File should contain 400 lines or less: currently contains 486","ruleName":"File Length","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleIdentifier":"type_body_length","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":7,"line":4},"severity":"error","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 1493 lines","ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"},"text":"final class TeraRemoteQualificationUITests: XCTestCase {"},{"violation":{"ruleIdentifier":"function_body_length","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":3,"line":62},"severity":"error","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 73 lines","ruleName":"Function Body Length","ruleDescription":"Function bodies should not span too many lines"},"text":" func testLocalSocialFiveFlowScenario() throws {"},{"violation":{"ruleIdentifier":"function_body_length","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":3,"line":174},"severity":"error","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 66 lines","ruleName":"Function Body Length","ruleDescription":"Function bodies should not span too many lines"},"text":" func testLocalSocialDeterministicPersonas() throws {"},{"violation":{"ruleIdentifier":"function_body_length","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":3,"line":247},"severity":"error","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 54 lines","ruleName":"Function Body Length","ruleDescription":"Function bodies should not span too many lines"},"text":" func testRemoteBlossomUploadAndRecovery() throws {"},{"violation":{"ruleIdentifier":"cyclomatic_complexity","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":11,"line":573},"severity":"error","reason":"Function should have complexity 10 or less; currently complexity is 18","ruleName":"Cyclomatic Complexity","ruleDescription":"Complexity of function bodies should be limited."},"text":" private func expectedAddFieldLabel(_ identifier: String, type: String) -> String {"},{"violation":{"ruleIdentifier":"cyclomatic_complexity","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":11,"line":767},"severity":"error","reason":"Function should have complexity 10 or less; currently complexity is 11","ruleName":"Cyclomatic Complexity","ruleDescription":"Complexity of function bodies should be limited."},"text":" private func preparePhotoUpdate(_ app: XCUIApplication, marker: String) -> XCUIElement? {"},{"violation":{"ruleIdentifier":"function_body_length","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":11,"line":767},"severity":"error","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 66 lines","ruleName":"Function Body Length","ruleDescription":"Function bodies should not span too many lines"},"text":" private func preparePhotoUpdate(_ app: XCUIApplication, marker: String) -> XCUIElement? {"},{"violation":{"ruleIdentifier":"function_body_length","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":10,"line":1736},"severity":"error","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 67 lines","ruleName":"Function Body Length","ruleDescription":"Function bodies should not span too many lines"},"text":" static func environment("},{"violation":{"ruleIdentifier":"file_length","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":1,"line":2212},"severity":"error","reason":"File should contain 1000 lines or less: currently contains 2212","ruleName":"File Length","ruleDescription":"Files should not span too many lines."},"text":"}"}] +\ No newline at end of file