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