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 67dbaa1f4893b9b21bf512fe5f719d4d980e2a82
parent 19ccb99f20af7a2abfadce61862fcc54f1af61ad
Author: triesap <tyson@radroots.org>
Date:   Thu, 10 Sep 2026 09:49:52 +0000

ios: Preserve calendar input semantics

- Edit civil components without converting date-only values to instants
- Preserve exact timed values and explicit source zone occurrence choices
- Retain unsaved fields and validate actual generated shared plans
- Qualify matching artifacts and complete current native consumers

Diffstat:
MPackage.swift | 5+++++
MTera.xcodeproj/project.pbxproj | 8++++++++
ATera/Runtime/TeraCalendarEditing.swift | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ATera/Runtime/TeraCivilDateInput.swift | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MTera/Runtime/TeraGeneratedRuntimeBackend.swift | 6+++---
MTera/State/TeraAddPresentation.swift | 23+++--------------------
MTera/Views/TeraAddView.swift | 83+------------------------------------------------------------------------------
ATera/Views/TeraCalendarComposerFields.swift | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
ATera/Views/TeraCivilDateField.swift | 40++++++++++++++++++++++++++++++++++++++++
ATera/Views/TeraTimedCalendarField.swift | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
MTeraFFI/provenance.json | 14+++++++-------
MTeraFFI/source.lock | 4++--
MTeraFFI/source/aarch64-apple-darwin.json | 8++++----
MTeraFFI/source/aarch64-apple-ios-sim.json | 8++++----
MTeraFFI/source/aarch64-apple-ios.json | 8++++----
ATeraTests/TeraCalendarComposerFFITests.swift | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ATeraTests/TeraCalendarEditingTests.swift | 108+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mproject.yml | 2++
Mrelease/provenance.json | 6+++---
Mtest-fixtures/legacy-identifiers.v1.json | 6+++---
Mtest-fixtures/swiftlint-maintainability-baseline.v1.json | 4++--
21 files changed, 534 insertions(+), 134 deletions(-)

diff --git a/Package.swift b/Package.swift @@ -59,6 +59,8 @@ let package = Package( "Runtime/TeraBackgroundUploadRequest.swift", "Runtime/TeraCalendarTiming.swift", "Runtime/TeraCalendarPresentation.swift", + "Runtime/TeraCalendarEditing.swift", + "Runtime/TeraCivilDateInput.swift", "Runtime/TeraCivilDateFormat.swift", "Runtime/TeraCheckedTime.swift", "Runtime/TeraErrorRecovery.swift", @@ -114,6 +116,9 @@ let package = Package( "Views/TeraTodayDetailView.swift", "Views/TeraTodayCardPresentation.swift", "Views/TeraCalendarMetadata.swift", + "Views/TeraCalendarComposerFields.swift", + "Views/TeraCivilDateField.swift", + "Views/TeraTimedCalendarField.swift", "Views/TeraTodayStatusView.swift", "Views/TeraTodayPagingStatusView.swift", "Views/RuntimeStatusView.swift", diff --git a/Tera.xcodeproj/project.pbxproj b/Tera.xcodeproj/project.pbxproj @@ -21,6 +21,7 @@ 31CBA09815D972CCE187009F /* TeraSessionResourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4383B870D20178E633610113 /* TeraSessionResourceTests.swift */; }; 38EF7FA8874E82B28110A232 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 6967B3EAC4CF1A410B26DD1A /* PrivacyInfo.xcprivacy */; }; 39B47EE698E9D7CC716A372F /* TeraRuntimeShutdownTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91E82E561F73A1BE6BBF09B7 /* TeraRuntimeShutdownTests.swift */; }; + 3D219E759CFB685936EB7068 /* TeraCalendarEditingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27C48B4B5442B1A404C1A328 /* TeraCalendarEditingTests.swift */; }; 4734A842C366608EC1BD31C5 /* TeraUserMessagesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46DDEE781FDD92A292FD847B /* TeraUserMessagesTests.swift */; }; 53651EBAF42E8698717CE936 /* TeraScopedStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF28A9D711806A07A4A65EA8 /* TeraScopedStoreTests.swift */; }; 5F252ABA8CA48D5E3A7A0157 /* TeraMediaVisibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E1E87F9A11E85700F38635 /* TeraMediaVisibilityTests.swift */; }; @@ -29,6 +30,7 @@ 64D49ADFD3FDD54CBA509DC5 /* TeraTodayStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 794D475C70BA8ED994D1CA66 /* TeraTodayStoreTests.swift */; }; 671EF50DB4EFBC25AABF26DC /* TeraLifecycleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C890BDE885536BC2AB215393 /* TeraLifecycleTests.swift */; }; 6B0BE8E92892EF60B8E996A5 /* TeraRuntimeResourceFixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A51F9AF2E0803264E07FBB /* TeraRuntimeResourceFixtures.swift */; }; + 6DB9A1B2B9ABD39613561459 /* TeraCalendarComposerFFITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA1A8EA600F6BDC45333520C /* TeraCalendarComposerFFITests.swift */; }; 6F53FF88A29F8DF93F074EB4 /* TeraBackgroundUploadFixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D8C0835D5A1D5C36D80020 /* TeraBackgroundUploadFixtures.swift */; }; 6FED2D2255CC702B33CDC760 /* TeraOpenedMediaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B10B1F3C7FCD9A72F04FC00 /* TeraOpenedMediaTests.swift */; }; 7035433B676A50A4801A8DFC /* TeraRuntimeInvalidationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73EBD10E201ECA26DCED523F /* TeraRuntimeInvalidationTests.swift */; }; @@ -100,6 +102,7 @@ 21DD785439F415278214D746 /* TeraObservationCoalescingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraObservationCoalescingTests.swift; sourceTree = "<group>"; }; 226F5C2370522AAFAD9D1C3D /* TeraRuntimeResourceLifecycleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraRuntimeResourceLifecycleTests.swift; sourceTree = "<group>"; }; 2521493B0CD2B82445974E81 /* TeraMediaOwnershipFFITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraMediaOwnershipFFITests.swift; sourceTree = "<group>"; }; + 27C48B4B5442B1A404C1A328 /* TeraCalendarEditingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraCalendarEditingTests.swift; sourceTree = "<group>"; }; 27D9D40699A01FFB02F30B11 /* local-social-personas.v1.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "local-social-personas.v1.json"; sourceTree = "<group>"; }; 27E1E87F9A11E85700F38635 /* TeraMediaVisibilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraMediaVisibilityTests.swift; sourceTree = "<group>"; }; 2A4FFC0023C620B68D0EC0ED /* TeraRuntimeBoundedTaskTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraRuntimeBoundedTaskTests.swift; sourceTree = "<group>"; }; @@ -163,6 +166,7 @@ F0A51F9AF2E0803264E07FBB /* TeraRuntimeResourceFixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraRuntimeResourceFixtures.swift; sourceTree = "<group>"; }; F596D3A9B9E236963DCE15D7 /* TeraMediaBudgetTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraMediaBudgetTests.swift; sourceTree = "<group>"; }; F65B01F6F5F7E125148EC4FD /* TeraTodayReconciliationFFITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraTodayReconciliationFFITests.swift; sourceTree = "<group>"; }; + FA1A8EA600F6BDC45333520C /* TeraCalendarComposerFFITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraCalendarComposerFFITests.swift; sourceTree = "<group>"; }; FB77C087F15C2F42CD959607 /* TeraRuntimeClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraRuntimeClientTests.swift; sourceTree = "<group>"; }; /* End PBXFileReference section */ @@ -231,6 +235,8 @@ 9F22CFABCF27A28408F68D25 /* TeraAddStoreTests.swift */, 61D8C0835D5A1D5C36D80020 /* TeraBackgroundUploadFixtures.swift */, 58E711687008866790042792 /* TeraBindingPortabilityTests.swift */, + FA1A8EA600F6BDC45333520C /* TeraCalendarComposerFFITests.swift */, + 27C48B4B5442B1A404C1A328 /* TeraCalendarEditingTests.swift */, 3B808E67E85CD36E49C38B6F /* TeraCalendarFFICompatibilityTests.swift */, 5CFC5639BDCD2A5C1C27FE38 /* TeraCalendarPresentationTests.swift */, 36AF0D194520F0F02EAD6B50 /* TeraCalendarTimingTests.swift */, @@ -495,6 +501,8 @@ A1AD2A380F09994786889AF1 /* TeraAddStoreTests.swift in Sources */, 6F53FF88A29F8DF93F074EB4 /* TeraBackgroundUploadFixtures.swift in Sources */, FB585785DC906CB2AE4AE535 /* TeraBindingPortabilityTests.swift in Sources */, + 6DB9A1B2B9ABD39613561459 /* TeraCalendarComposerFFITests.swift in Sources */, + 3D219E759CFB685936EB7068 /* TeraCalendarEditingTests.swift in Sources */, 772A9B1AD8DE1D2E0969F285 /* TeraCalendarFFICompatibilityTests.swift in Sources */, BF0BF2D7F46217C4EBCA570C /* TeraCalendarPresentationTests.swift in Sources */, 9309D7175535546A0C878F75 /* TeraCalendarTimingTests.swift in Sources */, diff --git a/Tera/Runtime/TeraCalendarEditing.swift b/Tera/Runtime/TeraCalendarEditing.swift @@ -0,0 +1,87 @@ +import Foundation + +enum TeraCalendarEditing { + static func civilDate(at seconds: UInt64, timeZone: TimeZone) -> TeraCivilDate? { + guard let instant = TeraCalendarTiming.presentationInstant(seconds) else { return nil } + var calendar = Calendar(identifier: .gregorian) + calendar.timeZone = timeZone + let parts = calendar.dateComponents([.year, .month, .day], from: instant) + guard let year = parts.year.flatMap(UInt16.init(exactly:)), + let month = parts.month.flatMap(UInt8.init(exactly:)), + let day = parts.day.flatMap(UInt8.init(exactly:)) + else { return nil } + return try? TeraCivilDate(year: year, month: month, day: day) + } + + static func initialize(_ form: inout TeraAddForm, now: UInt64, timeZone: TimeZone) { + form.eventTimezone = timeZone.identifier + let civil = civilDate(at: now, timeZone: timeZone) + form.eventStartDate = civil?.canonical + form.eventEndDate = civil?.nextDay?.canonical + let (start, startOverflow) = now.addingReportingOverflow(3600) + let (end, endOverflow) = start.addingReportingOverflow(3600) + if !startOverflow, TeraCalendarTiming.presentationInstant(start) != nil { + form.eventStartUnixSeconds = start + } + if !startOverflow, !endOverflow, TeraCalendarTiming.presentationInstant(end) != nil { + form.eventEndUnixSeconds = end + } + } + + static func pickerInstant(_ value: UInt64?) -> Date? { + guard let value, value > 0 else { return nil } + return TeraCalendarTiming.presentationInstant(value) + } + + static func pickerSeconds(_ value: Date) -> UInt64? { + guard let seconds = try? TeraClock.unixSeconds(from: value, requirePositive: true), + TeraCalendarTiming.presentationInstant(seconds) != nil + else { return nil } + return seconds + } +} + +/// A wall time exists only in conjunction with an explicit zone. Strict matching +/// returns no instant for a gap and both distinct instants for an overlap. +struct TeraWallTime { + let date: TeraCivilDate + var hour: Int + var minute: Int + var second: Int = 0 + + func instants(in timeZone: TimeZone) -> [UInt64] { + guard (0 ... 23).contains(hour), (0 ... 59).contains(minute), (0 ... 59).contains(second) else { return [] } + var calendar = Calendar(identifier: .gregorian) + calendar.timeZone = timeZone + let components = DateComponents(year: Int(date.year), month: Int(date.month), day: Int(date.day), + hour: hour, minute: minute, second: second) + guard let noon = calendar.date(from: DateComponents(year: Int(date.year), month: Int(date.month), + day: Int(date.day), hour: 12)), + let anchor = calendar.date(byAdding: .day, value: -1, to: noon) + else { return [] } + let candidates = [Calendar.RepeatedTimePolicy.first, .last].compactMap { policy -> UInt64? in + guard let found = calendar.nextDate(after: anchor, matching: components, matchingPolicy: .strict, + repeatedTimePolicy: policy, direction: .forward), + matches(found, components: components, calendar: calendar) + else { return nil } + return TeraCalendarEditing.pickerSeconds(found) + } + return Array(Set(candidates)).sorted() + } + + private func matches(_ instant: Date, components: DateComponents, calendar: Calendar) -> Bool { + let fields: Set<Calendar.Component> = [.year, .month, .day, .hour, .minute, .second] + let actual = calendar.dateComponents(fields, from: instant) + return fields.allSatisfy { actual.value(for: $0) == components.value(for: $0) } + } + + static func from(_ seconds: UInt64, timeZone: TimeZone) -> Self? { + guard let instant = TeraCalendarEditing.pickerInstant(seconds), + let date = TeraCalendarEditing.civilDate(at: seconds, timeZone: timeZone) + else { return nil } + var calendar = Calendar(identifier: .gregorian) + calendar.timeZone = timeZone + return Self(date: date, hour: calendar.component(.hour, from: instant), + minute: calendar.component(.minute, from: instant), second: calendar.component(.second, from: instant)) + } +} diff --git a/Tera/Runtime/TeraCivilDateInput.swift b/Tera/Runtime/TeraCivilDateInput.swift @@ -0,0 +1,57 @@ +import Foundation + +/// Partial native component entry stays separate from a validated civil date. +struct TeraCivilDateInput { + let raw: String? + + func component(_ index: Int) -> String { + let parts = raw?.split(separator: "-", omittingEmptySubsequences: false) ?? [] + return parts.indices.contains(index) ? String(parts[index]) : "" + } + + var value: TeraCivilDate? { + guard let raw, raw.utf8.count <= 64, + raw.split(separator: "-", omittingEmptySubsequences: false).count == 3, + let year = number(component(0), maximumDigits: 4), + let month = number(component(1), maximumDigits: 2), + let day = number(component(2), maximumDigits: 2), + let typedYear = UInt16(exactly: year), let typedMonth = UInt8(exactly: month), + let typedDay = UInt8(exactly: day) + else { return nil } + return try? TeraCivilDate(year: typedYear, month: typedMonth, day: typedDay) + } + + func replacing(_ index: Int, with input: String) -> String? { + guard (0 ... 2).contains(index) else { return raw } + var parts = (0 ... 2).map(component) + parts[index] = String(input.prefix(index == 0 ? 4 : 2)) + guard parts.contains(where: { !$0.isEmpty }) else { return nil } + return parts.joined(separator: "-") + } + + var canonicalOrRaw: String? { + value?.canonical ?? raw + } + + private func number(_ text: String, maximumDigits: Int) -> Int? { + guard !text.isEmpty, text.count <= maximumDigits else { return nil } + var value = 0 + for character in text { + guard let digit = character.wholeNumberValue, (0 ... 9).contains(digit) else { return nil } + value = value * 10 + digit + } + return value + } +} + +extension TeraCivilDate { + var nextDay: Self? { + if let next = try? Self(year: year, month: month, day: day + 1) { + return next + } + if let next = try? Self(year: year, month: month + 1, day: 1) { + return next + } + return try? Self(year: year + 1, month: 1, day: 1) + } +} diff --git a/Tera/Runtime/TeraGeneratedRuntimeBackend.swift b/Tera/Runtime/TeraGeneratedRuntimeBackend.swift @@ -1018,7 +1018,7 @@ extension TeraEventTiming { } extension TeraAddRuntimeInput { - fileprivate var generatedValue: FfiAddDraftInput { + var generatedValue: FfiAddDraftInput { FfiAddDraftInput( schemaVersion: 1, commandType: form.commandType.generatedValue, @@ -1028,8 +1028,8 @@ extension TeraAddRuntimeInput { summary: form.summary, location: form.location, eventTiming: form.eventTiming?.generatedValue, - eventStartDate: form.eventStartDate, - eventEndDate: form.eventEndDate, + eventStartDate: TeraCivilDateInput(raw: form.eventStartDate).canonicalOrRaw, + eventEndDate: TeraCivilDateInput(raw: form.eventEndDate).canonicalOrRaw, eventStartUnixS: form.eventStartUnixSeconds, eventEndUnixS: form.eventEndUnixSeconds, eventTimezone: form.eventTimezone, diff --git a/Tera/State/TeraAddPresentation.swift b/Tera/State/TeraAddPresentation.swift @@ -13,7 +13,8 @@ enum TeraAddPresentation { static func newForm( type: TeraAddCommandType, identifier: @Sendable () -> String, - clock: TeraClock + clock: TeraClock, + timeZone: TimeZone = .current ) -> TeraAddForm { var form = TeraAddForm.empty(type) if type == .createEvent || type == .createFoodAvailability { @@ -26,16 +27,7 @@ enum TeraAddPresentation { guard let now = try? clock.unixSeconds() else { return form } - let start = now.addingReportingOverflow(3600).overflow ? now : now + 3600 - let end = start.addingReportingOverflow(3600).overflow ? start : start + 3600 - form.eventStartUnixSeconds = start - form.eventEndUnixSeconds = end - form.eventStartDate = eventDateFormatter.string( - from: Date(timeIntervalSince1970: TimeInterval(start)) - ) - form.eventEndDate = eventDateFormatter.string( - from: Date(timeIntervalSince1970: TimeInterval(end)) - ) + TeraCalendarEditing.initialize(&form, now: now, timeZone: timeZone) } return form } @@ -47,15 +39,6 @@ enum TeraAddPresentation { } } - static let eventDateFormatter: DateFormatter = { - let formatter = DateFormatter() - formatter.calendar = Calendar(identifier: .gregorian) - formatter.locale = Locale(identifier: "en_US_POSIX") - formatter.timeZone = TimeZone(secondsFromGMT: 0) - formatter.dateFormat = "yyyy-MM-dd" - return formatter - }() - static func message(for error: Error) -> String { TeraUserMessages.text(for: error, fallback: .addOperationFailed) } diff --git a/Tera/Views/TeraAddView.swift b/Tera/Views/TeraAddView.swift @@ -174,27 +174,7 @@ struct TeraAddView: View { label: "Location (optional)", text: optional(\.location), focus: .location, identifier: "radroots.add.location" ) - Picker("When", selection: optionalTiming) { - ForEach(TeraEventTiming.allCases) { timing in - Text(timing.label).tag(Optional(timing)) - } - } - .accessibilityIdentifier("radroots.add.event_timing") - if store.form.eventTiming == .allDay { - DatePicker("Starts", selection: allDayStart, displayedComponents: .date) - .accessibilityLabel("Starts") - .accessibilityIdentifier("radroots.add.event.start") - DatePicker("Ends", selection: allDayEnd, displayedComponents: .date) - .accessibilityLabel("Ends") - .accessibilityIdentifier("radroots.add.event.end") - } else { - DatePicker("Starts", selection: timedStart) - .accessibilityLabel("Starts") - .accessibilityIdentifier("radroots.add.event.start") - DatePicker("Ends", selection: timedEnd) - .accessibilityLabel("Ends") - .accessibilityIdentifier("radroots.add.event.end") - } + TeraCalendarComposerFields(store: store) } } @@ -397,10 +377,6 @@ struct TeraAddView: View { Binding(get: { store.form.commandType }, set: { store.selectType($0) }) } - private var optionalTiming: Binding<TeraEventTiming?> { - Binding(get: { store.form.eventTiming }, set: { store.updateForm(\.eventTiming, $0) }) - } - private func optional(_ keyPath: WritableKeyPath<TeraAddForm, String?>) -> Binding<String> { Binding( get: { store.form[keyPath: keyPath] ?? "" }, @@ -415,54 +391,6 @@ struct TeraAddView: View { ) } - private var timedStart: Binding<Date> { - secondsBinding(\.eventStartUnixSeconds, defaultOffset: 3600) - } - - private var timedEnd: Binding<Date> { - secondsBinding(\.eventEndUnixSeconds, defaultOffset: 7200) - } - - private func secondsBinding( - _ keyPath: WritableKeyPath<TeraAddForm, UInt64?>, - defaultOffset: TimeInterval - ) -> Binding<Date> { - Binding( - get: { - store.form[keyPath: keyPath] - .map { Date(timeIntervalSince1970: TimeInterval($0)) } - ?? Date().addingTimeInterval(defaultOffset) - }, - set: { - guard let value = try? TeraClock.unixSeconds(from: $0, requirePositive: true) else { - return - } - store.updateForm(keyPath, value) - } - ) - } - - private var allDayStart: Binding<Date> { - dateBinding(\.eventStartDate, defaultOffset: 86400) - } - - private var allDayEnd: Binding<Date> { - dateBinding(\.eventEndDate, defaultOffset: 172_800) - } - - private func dateBinding( - _ keyPath: WritableKeyPath<TeraAddForm, String?>, - defaultOffset: TimeInterval - ) -> Binding<Date> { - Binding( - get: { - store.form[keyPath: keyPath].flatMap(Self.dateFormatter.date(from:)) - ?? Calendar.current.startOfDay(for: Date().addingTimeInterval(defaultOffset)) - }, - set: { store.updateForm(keyPath, Self.dateFormatter.string(from: $0)) } - ) - } - private var submitLabel: String { if store.activeDraft?.kind == .retraction { return "Retry retraction" @@ -482,15 +410,6 @@ struct TeraAddView: View { default: "info.circle" } } - - private static let dateFormatter: DateFormatter = { - let formatter = DateFormatter() - formatter.calendar = Calendar(identifier: .gregorian) - formatter.locale = Locale(identifier: "en_US_POSIX") - formatter.timeZone = TimeZone(secondsFromGMT: 0) - formatter.dateFormat = "yyyy-MM-dd" - return formatter - }() } struct TeraDraftsSheet: View { diff --git a/Tera/Views/TeraCalendarComposerFields.swift b/Tera/Views/TeraCalendarComposerFields.swift @@ -0,0 +1,55 @@ +import SwiftUI + +struct TeraCalendarComposerFields: View { + @ObservedObject var store: TeraAddStore + + var body: some View { + Picker("When", selection: binding(\.eventTiming)) { + ForEach(TeraEventTiming.allCases) { timing in + Text(timing.label).tag(Optional(timing)) + } + } + .accessibilityIdentifier("radroots.add.event_timing") + if store.form.eventTiming == .allDay { + TeraCivilDateField(label: "Starts", identifier: "radroots.add.event.start", raw: binding(\.eventStartDate)) + TeraCivilDateField(label: "Ends before (optional)", identifier: "radroots.add.event.end", raw: binding(\.eventEndDate)) + Text("The end date is not included. Leave it empty for a one-day event.") + .font(.caption).foregroundStyle(.secondary) + } else { + timedFields + } + } + + @ViewBuilder + private var timedFields: some View { + TextField("Event time zone (optional)", text: Binding( + get: { store.form.eventTimezone ?? "" }, + set: { store.updateForm(\.eventTimezone, $0.isEmpty ? nil : String($0.prefix(255))) } + )) + .textInputAutocapitalization(.never).autocorrectionDisabled() + .accessibilityIdentifier("tera.add.event.timezone") + if let zone = selectedTimeZone { + Text(store.form.eventTimezone == nil ? "Device display time: \(zone.identifier)" : "Event time: \(zone.identifier)") + .font(.caption).foregroundStyle(.secondary) + TeraTimedCalendarField(label: "Starts", identifier: "radroots.add.event.start", timeZone: zone, + seconds: binding(\.eventStartUnixSeconds)) + TeraTimedCalendarField(label: "Ends", identifier: "radroots.add.event.end", timeZone: zone, + seconds: binding(\.eventEndUnixSeconds)) + if store.form.eventEndUnixSeconds != nil { + Button("Remove end time") { store.updateForm(\.eventEndUnixSeconds, nil) } + } + } else { + Text("Choose a supported time zone, such as America/Vancouver.").foregroundStyle(.secondary) + } + } + + private var selectedTimeZone: TimeZone? { + guard let identifier = store.form.eventTimezone else { return .current } + guard identifier.utf8.count <= 255 else { return nil } + return TimeZone(identifier: identifier) + } + + private func binding<Value>(_ keyPath: WritableKeyPath<TeraAddForm, Value>) -> Binding<Value> { + Binding(get: { store.form[keyPath: keyPath] }, set: { store.updateForm(keyPath, $0) }) + } +} diff --git a/Tera/Views/TeraCivilDateField.swift b/Tera/Views/TeraCivilDateField.swift @@ -0,0 +1,40 @@ +import SwiftUI + +struct TeraCivilDateField: View { + let label: String + let identifier: String + @Binding var raw: String? + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + Text(label).font(.subheadline.weight(.semibold)) + ViewThatFits(in: .horizontal) { + HStack { components } + VStack(alignment: .leading) { components } + } + if raw != nil, TeraCivilDateInput(raw: raw).value == nil { + Text("Enter a valid year, month and day.").font(.caption).foregroundStyle(.secondary) + } + } + .accessibilityElement(children: .contain) + .accessibilityLabel(label) + .accessibilityIdentifier(identifier) + } + + private var components: some View { + ForEach(Array(["Year", "Month", "Day"].enumerated()), id: \.offset) { index, name in + VStack(alignment: .leading) { + Text(name).font(.caption) + TextField(name, text: Binding( + get: { TeraCivilDateInput(raw: raw).component(index) }, + set: { raw = TeraCivilDateInput(raw: raw).replacing(index, with: $0) } + )) + .keyboardType(.numberPad) + .textFieldStyle(.roundedBorder) + .accessibilityLabel("\(label), \(name)") + .accessibilityIdentifier("\(identifier).\(name.lowercased())") + } + .frame(minWidth: index == 0 ? 90 : 70) + } + } +} diff --git a/Tera/Views/TeraTimedCalendarField.swift b/Tera/Views/TeraTimedCalendarField.swift @@ -0,0 +1,54 @@ +import SwiftUI + +struct TeraTimedCalendarField: View { + let label: String + let identifier: String + let timeZone: TimeZone + @Binding var seconds: UInt64? + + var body: some View { + if let instant = TeraCalendarEditing.pickerInstant(seconds) { + DatePicker(label, selection: Binding( + get: { TeraCalendarEditing.pickerInstant(seconds) ?? instant }, + set: { + if let value = TeraCalendarEditing.pickerSeconds($0), + TeraWallTime.from(value, timeZone: timeZone)?.instants(in: timeZone).contains(value) == true + { + seconds = value + } + } + )) + .environment(\.calendar, Calendar(identifier: .gregorian)) + .environment(\.timeZone, timeZone) + .accessibilityLabel(label) + .accessibilityIdentifier(identifier) + repeatedTime + } else { + LabeledContent(label) { + Button(seconds == nil ? "Set time" : "Replace unsupported time") { + seconds = TeraCalendarEditing.pickerSeconds(Date()) + } + } + .accessibilityIdentifier(identifier) + } + } + + @ViewBuilder + private var repeatedTime: some View { + let values = seconds.flatMap { TeraWallTime.from($0, timeZone: timeZone) }?.instants(in: timeZone) ?? [] + if values.count == 2 { + Picker("\(label), repeated time", selection: $seconds) { + ForEach(Array(values.enumerated()), id: \.element) { index, value in + Text(occurrence(value, index: index)).tag(Optional(value)) + } + } + .accessibilityIdentifier("\(identifier).occurrence") + } + } + + private func occurrence(_ seconds: UInt64, index: Int) -> String { + guard let instant = TeraCalendarEditing.pickerInstant(seconds) else { return "Time unavailable" } + let zone = timeZone.abbreviation(for: instant) ?? timeZone.identifier + return "\(index == 0 ? "First" : "Second") occurrence (\(zone))" + } +} diff --git a/TeraFFI/provenance.json b/TeraFFI/provenance.json @@ -94,17 +94,17 @@ { "bytes": 69516, "path": "source/aarch64-apple-darwin.json", - "sha256": "3aec2f7bc7d9c6350c8576085edd65d1fe1256eff063bca8761af55e7e13a6ed" + "sha256": "64621b31558912669d82e388e21eec0a092028d85d055b3ef61df3d4dfcab649" }, { "bytes": 69360, "path": "source/aarch64-apple-ios-sim.json", - "sha256": "be2671a68f63ca939852f96e0171c99c0efdc555f9c91170fa4c74615740dff8" + "sha256": "84187a2447d1576ae8fbc796044db9afedfbf1543ccad29eab35120f3fbfa7a7" }, { "bytes": 69356, "path": "source/aarch64-apple-ios.json", - "sha256": "e00a5ff06ff97642c243ad4ed01cfdbd04ec6d2c3f4752c26bcfa7f731647506" + "sha256": "df67c9471bb76eaffb50fc659892c1a066a3ae42f34ee945e996a799430db1ba" } ], "language": "swift", @@ -112,7 +112,7 @@ "schema": "radroots.artifact-manifest.v2", "source": { "repository": "https://github.com/radrootslabs/tera", - "tree": "a304a7b5ce6f52f7f3b4a13a59baed9571879bed" + "tree": "114cb7bc9ce1f3ac0808c7d6a6dd4887298bceec" }, "source_records": { "aarch64-apple-darwin": "source/aarch64-apple-darwin.json", @@ -176,17 +176,17 @@ { "bytes": 69516, "path": "TeraFFI/source/aarch64-apple-darwin.json", - "sha256": "3aec2f7bc7d9c6350c8576085edd65d1fe1256eff063bca8761af55e7e13a6ed" + "sha256": "64621b31558912669d82e388e21eec0a092028d85d055b3ef61df3d4dfcab649" }, { "bytes": 69360, "path": "TeraFFI/source/aarch64-apple-ios-sim.json", - "sha256": "be2671a68f63ca939852f96e0171c99c0efdc555f9c91170fa4c74615740dff8" + "sha256": "84187a2447d1576ae8fbc796044db9afedfbf1543ccad29eab35120f3fbfa7a7" }, { "bytes": 69356, "path": "TeraFFI/source/aarch64-apple-ios.json", - "sha256": "e00a5ff06ff97642c243ad4ed01cfdbd04ec6d2c3f4752c26bcfa7f731647506" + "sha256": "df67c9471bb76eaffb50fc659892c1a066a3ae42f34ee945e996a799430db1ba" } ], "schema": "tera.installed-native-artifacts.v1" diff --git a/TeraFFI/source.lock b/TeraFFI/source.lock @@ -1,7 +1,7 @@ schema = "tera.installed-source.v1" repository = "https://github.com/radrootslabs/tera" -source_tree = "a304a7b5ce6f52f7f3b4a13a59baed9571879bed" -manifest_sha256 = "f0dc6fe46d5dff2abd7cb2b225f3449d3e8d28a8ec8179936f3b7a18c2e6592c" +source_tree = "114cb7bc9ce1f3ac0808c7d6a6dd4887298bceec" +manifest_sha256 = "f2423a84ad3adf11a7e35c4ed7b7ab3f3c5951815857bd771dd55b07683b0bed" source_date_epoch = 1787871027 [foundation] diff --git a/TeraFFI/source/aarch64-apple-darwin.json b/TeraFFI/source/aarch64-apple-darwin.json @@ -1495,13 +1495,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 120311, - "git_blob": "6459327c030fefd58023e9748a4b63cda70e41f2", + "bytes": 120356, + "git_blob": "1a703d450d7a9ba8921d015211707b654c723e41", "mode": "100644", - "sha256": "01b849b783353d4a93d420aa674004f61352595972e24163de4af0ffeaa01d13" + "sha256": "1c3c108f781ea5f8c6dc8e167180765a03c71a8391578ae981584786addf1ea0" } }, "policy": "staged_inputs", - "tree": "a304a7b5ce6f52f7f3b4a13a59baed9571879bed" + "tree": "114cb7bc9ce1f3ac0808c7d6a6dd4887298bceec" } } diff --git a/TeraFFI/source/aarch64-apple-ios-sim.json b/TeraFFI/source/aarch64-apple-ios-sim.json @@ -1491,13 +1491,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 120311, - "git_blob": "6459327c030fefd58023e9748a4b63cda70e41f2", + "bytes": 120356, + "git_blob": "1a703d450d7a9ba8921d015211707b654c723e41", "mode": "100644", - "sha256": "01b849b783353d4a93d420aa674004f61352595972e24163de4af0ffeaa01d13" + "sha256": "1c3c108f781ea5f8c6dc8e167180765a03c71a8391578ae981584786addf1ea0" } }, "policy": "staged_inputs", - "tree": "a304a7b5ce6f52f7f3b4a13a59baed9571879bed" + "tree": "114cb7bc9ce1f3ac0808c7d6a6dd4887298bceec" } } diff --git a/TeraFFI/source/aarch64-apple-ios.json b/TeraFFI/source/aarch64-apple-ios.json @@ -1491,13 +1491,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 120311, - "git_blob": "6459327c030fefd58023e9748a4b63cda70e41f2", + "bytes": 120356, + "git_blob": "1a703d450d7a9ba8921d015211707b654c723e41", "mode": "100644", - "sha256": "01b849b783353d4a93d420aa674004f61352595972e24163de4af0ffeaa01d13" + "sha256": "1c3c108f781ea5f8c6dc8e167180765a03c71a8391578ae981584786addf1ea0" } }, "policy": "staged_inputs", - "tree": "a304a7b5ce6f52f7f3b4a13a59baed9571879bed" + "tree": "114cb7bc9ce1f3ac0808c7d6a6dd4887298bceec" } } diff --git a/TeraTests/TeraCalendarComposerFFITests.swift b/TeraTests/TeraCalendarComposerFFITests.swift @@ -0,0 +1,82 @@ +import Foundation +@testable import TeraApp +import TeraKitBindings +import XCTest + +final class TeraCalendarComposerFFITests: XCTestCase { + func testNativeComposerInputsUseTheActualGeneratedSharedPlanAndReopenExactly() async throws { + let fixture = try MediaOwnershipFixture() + defer { fixture.remove() } + let first = try await fixture.runtime() + var saved = [FfiDraftStatusRecord]() + for (index, mode) in [TeraEventTiming.allDay, .timed].enumerated() { + let form = try form(mode) + let input = TeraAddRuntimeInput(form: form, media: []).generatedValue + let draft = try await first.phase1SaveDraft(draftId: String(repeating: index == 0 ? "51" : "52", count: 16), + input: input, authoredAtUnixS: 1_900_000_000, expectedRevision: nil, + persistedAtUnixMs: 1_900_000_000_000) + XCTAssertEqual(draft.form?.eventStartDate, input.eventStartDate) + XCTAssertEqual(draft.form?.eventEndDate, input.eventEndDate) + XCTAssertEqual(input.eventStartDate, "2026-09-05") + XCTAssertEqual(draft.form?.eventStartUnixS, form.eventStartUnixSeconds) + XCTAssertEqual(draft.form?.eventEndUnixS, form.eventEndUnixSeconds) + XCTAssertEqual(draft.form?.eventTimezone, form.eventTimezone) + let queued = try await first.phase1QueueDraft(draftId: draft.draftId, expectedRevision: draft.revision, + policy: FfiQueuePolicyRecord(schemaVersion: 1, relayUrls: ["wss://relay.example"], + satisfaction: .allAccepted, deliveryDeadlineUnixMs: 2_000_000_000_000, + cancellation: .localCooperative), queuedAtUnixMs: 1_900_000_000_001) + XCTAssertEqual(queued.state, .queued) + XCTAssertEqual(queued.form, draft.form) + XCTAssertNotNil(queued.operationId) + saved.append(queued) + } + _ = try await first.shutdown() + let reopened = try await fixture.runtime() + for expected in saved { + let actual = try await reopened.phase1DraftStatus(draftId: expected.draftId) + XCTAssertEqual(actual, expected) + } + _ = try await reopened.shutdown() + } + + func testActualSharedBoundaryRejectsPartialDatesExclusiveRangesAndInvalidZones() async throws { + let fixture = try MediaOwnershipFixture() + defer { fixture.remove() } + let runtime = try await fixture.runtime() + var partial = try form(.allDay) + partial.eventStartDate = "2026-09-" + var reversed = try form(.allDay) + reversed.eventEndDate = reversed.eventStartDate + var timed = try form(.timed) + timed.eventEndUnixSeconds = timed.eventStartUnixSeconds + var zone = try form(.timed) + zone.eventTimezone = "Mars/Olympus" + let codes = ["invalid_event_start_date", "invalid_event_range", "invalid_event_range", "invalid_event_timezone"] + for (index, form) in [partial, reversed, timed, zone].enumerated() { + do { + _ = try await runtime.phase1SaveDraft(draftId: String(repeating: String(index + 3), count: 32), + input: TeraAddRuntimeInput(form: form, media: []).generatedValue, + authoredAtUnixS: 1_900_000_000, expectedRevision: nil, + persistedAtUnixMs: 1_900_000_000_000) + XCTFail("Invalid calendar input must fail strict shared planning before saving a draft.") + } catch let TeraAppError.Failure(report) { + XCTAssertEqual(report.code, codes[index]) + } + } + let drafts = try await runtime.phase1DraftHeads(limit: 10) + XCTAssertTrue(drafts.isEmpty) + _ = try await runtime.shutdown() + } + + private func form(_ mode: TeraEventTiming) throws -> TeraAddForm { + let zone = try XCTUnwrap(TimeZone(identifier: "America/Vancouver")) + var form = TeraAddPresentation.newForm(type: .createEvent, identifier: { String(repeating: "a", count: 32) }, + clock: .fixed(unixSeconds: 1_788_568_200), timeZone: zone) + form.eventTiming = mode + form.title = "Synthetic harvest" + form.content = "Synthetic calendar composer" + form.eventStartDate = TeraCivilDateInput(raw: "2026-09-").replacing(2, with: "5") + form.eventEndDate = "2026-09-07" + return form + } +} diff --git a/TeraTests/TeraCalendarEditingTests.swift b/TeraTests/TeraCalendarEditingTests.swift @@ -0,0 +1,108 @@ +import Foundation +@testable import TeraApp +import XCTest + +final class TeraCalendarEditingTests: XCTestCase { + func testCivilComponentsRetainIncompleteInputAndCanonicalizeWithoutDateParsing() throws { + let partial = TeraCivilDateInput(raw: "2024--") + XCTAssertNil(partial.value) + let month = partial.replacing(1, with: "2") + XCTAssertEqual(month, "2024-2-") + let leap = TeraCivilDateInput(raw: month).replacing(2, with: "29") + XCTAssertEqual(leap, "2024-2-29") + XCTAssertEqual(TeraCivilDateInput(raw: leap).canonicalOrRaw, "2024-02-29") + XCTAssertEqual(TeraCivilDateInput(raw: leap).value, try TeraCivilDate(year: 2024, month: 2, day: 29)) + XCTAssertNil(TeraCivilDateInput(raw: "2023-02-29").value) + XCTAssertNil(TeraCivilDateInput(raw: "2026-09-05-extra").value) + let arabic = TeraCivilDateInput(raw: "٢٠٢٦-٠٩-").replacing(2, with: "٠٥") + XCTAssertEqual(arabic, "٢٠٢٦-٠٩-٠٥") + XCTAssertEqual(TeraCivilDateInput(raw: arabic).canonicalOrRaw, "2026-09-05") + XCTAssertEqual(TeraCivilDateInput(raw: "2026-09-05").replacing(0, with: "2"), "2-09-05") + XCTAssertNil(TeraCivilDateInput(raw: "--").replacing(0, with: "")) + } + + func testNewFormUsesLocalCivilTodayAndSeparateCheckedInstantDefaults() throws { + let zone = try XCTUnwrap(TimeZone(identifier: "America/Vancouver")) + let form = TeraAddPresentation.newForm(type: .createEvent, identifier: { String(repeating: "a", count: 32) }, + clock: .fixed(unixSeconds: 1_788_568_200), timeZone: zone) + XCTAssertEqual(form.eventTimezone, "America/Vancouver") + XCTAssertEqual(form.eventStartDate, "2026-09-04") + XCTAssertEqual(form.eventEndDate, "2026-09-05") + XCTAssertEqual(form.eventStartUnixSeconds, 1_788_571_800) + XCTAssertEqual(form.eventEndUnixSeconds, 1_788_575_400) + } + + func testExclusiveNextDayUsesGregorianComponentsAtLeapAndDomainBoundaries() throws { + XCTAssertEqual(try TeraCivilDate(year: 2024, month: 2, day: 28).nextDay?.canonical, "2024-02-29") + XCTAssertEqual(try TeraCivilDate(year: 2024, month: 2, day: 29).nextDay?.canonical, "2024-03-01") + XCTAssertEqual(try TeraCivilDate(year: 1900, month: 2, day: 28).nextDay?.canonical, "1900-03-01") + XCTAssertEqual(try TeraCivilDate(year: 2025, month: 12, day: 31).nextDay?.canonical, "2026-01-01") + XCTAssertNil(try TeraCivilDate(year: 9999, month: 12, day: 31).nextDay) + } + + func testHistoricalSpringGapNeverBecomesANormalizedInstant() throws { + let zone = try XCTUnwrap(TimeZone(identifier: "America/Vancouver")) + let gap = try TeraWallTime(date: TeraCivilDate(year: 2024, month: 3, day: 10), hour: 2, minute: 30) + XCTAssertEqual(gap.instants(in: zone), []) + XCTAssertEqual(TeraWallTime(date: gap.date, hour: 3, minute: 30).instants(in: zone).count, 1) + XCTAssertEqual(TeraWallTime(date: gap.date, hour: 25, minute: 0).instants(in: zone), []) + } + + func testHistoricalOverlapOffersBothExactInstantsWithoutDiscardingSeconds() throws { + let zone = try XCTUnwrap(TimeZone(identifier: "America/Vancouver")) + let wall = try TeraWallTime(date: TeraCivilDate(year: 2024, month: 11, day: 3), hour: 1, minute: 30, second: 17) + let expected: [UInt64] = [1_730_622_617, 1_730_626_217] + XCTAssertEqual(wall.instants(in: zone), expected) + for instant in expected { + XCTAssertEqual(TeraWallTime.from(instant, timeZone: zone)?.instants(in: zone), expected) + let date = try XCTUnwrap(TeraCalendarEditing.pickerInstant(instant)) + XCTAssertEqual(TeraCalendarEditing.pickerSeconds(date), instant) + } + } + + func testSkippedLocalDayRemainsAValidCivilDateAndHasNoTimedInstant() throws { + let zone = try XCTUnwrap(TimeZone(identifier: "Pacific/Apia")) + let date = try TeraCivilDate(year: 2011, month: 12, day: 30) + XCTAssertEqual(TeraCivilDateInput(raw: date.canonical).value, date) + XCTAssertEqual(TeraWallTime(date: date, hour: 12, minute: 0).instants(in: zone), []) + XCTAssertEqual(date.nextDay?.canonical, "2011-12-31") + } + + func testUnsupportedUnsignedValuesAndDatesCannotSilentlySeedOrRoundPickers() { + for value: UInt64 in [0, 253_402_300_800, (1 << 53) + 1, .max] { + XCTAssertNil(TeraCalendarEditing.pickerInstant(value)) + } + for value in [Double.nan, .infinity, -1, 253_402_300_800] { + XCTAssertNil(TeraCalendarEditing.pickerSeconds(Date(timeIntervalSince1970: value))) + } + var form = TeraAddForm.empty(.createEvent) + TeraCalendarEditing.initialize(&form, now: .max, timeZone: .gmt) + XCTAssertNil(form.eventStartDate) + XCTAssertNil(form.eventEndDate) + XCTAssertNil(form.eventStartUnixSeconds) + XCTAssertNil(form.eventEndUnixSeconds) + } + + @MainActor + func testModeAndZoneChangesPreserveEveryOtherUnsavedField() async throws { + let client = try await TeraScopeFixtures.client(TeraScopeBackend()) + let store = TeraAddStore(runtimeClient: client, initialType: .createEvent, clock: .fixed(unixSeconds: 1_788_568_200)) + store.updateForm(\.title, "Unsaved harvest") + store.updateForm(\.content, "Unsaved details") + store.updateForm(\.eventStartDate, "2026-09-") + store.updateForm(\.eventEndDate, "2026-09-07") + store.updateForm(\.eventTimezone, "America/Vancouver") + let original = store.form + var expected = original + expected.eventTiming = .allDay + store.updateForm(\.eventTiming, .allDay) + XCTAssertEqual(store.form, expected) + store.updateForm(\.eventTiming, .timed) + XCTAssertEqual(store.form, original) + expected = original + expected.eventTimezone = "Pacific/Kiritimati" + store.updateForm(\.eventTimezone, "Pacific/Kiritimati") + XCTAssertEqual(store.form, expected) + _ = try await client.stop() + } +} diff --git a/project.yml b/project.yml @@ -100,6 +100,8 @@ targets: - path: TeraTests/TeraCalendarFFICompatibilityTests.swift - path: TeraTests/TeraCalendarTimingTests.swift - path: TeraTests/TeraCalendarPresentationTests.swift + - path: TeraTests/TeraCalendarEditingTests.swift + - path: TeraTests/TeraCalendarComposerFFITests.swift - path: TeraTests/TeraRemoteQualificationTests.swift - path: TeraTests/TeraUserMessagesTests.swift settings: diff --git a/release/provenance.json b/release/provenance.json @@ -2,11 +2,11 @@ "artifacts": { "app_api_sha256": "020924097c0d7efc33128cb8fd3d3b2026d95f57c44da71880e585aff80f070b", "ffi_api_sha256": "1a5ada68d14cac611670683cb74c8779c341dabaf205445b2e608aef5a5ce12d", - "ffi_provenance_sha256": "f0dc6fe46d5dff2abd7cb2b225f3449d3e8d28a8ec8179936f3b7a18c2e6592c", + "ffi_provenance_sha256": "f2423a84ad3adf11a7e35c4ed7b7ab3f3c5951815857bd771dd55b07683b0bed", "info_plist_sha256": "15ef08b1cdd1096cfb9eeaf5be5bf8f814807a7ca9350bbbb47860fa72ec13ef", "privacy_manifest_sha256": "a331d51864743ebe4e00dd22360b4a538b6b3ac26a6b3eb54094e60a36959a12", "sbom_sha256": "bea2d48c06e5f7e84cdad085d81290712674b74664456f2030064d9cf94c4b33", - "xcode_project_sha256": "c1c42bc8deec2d46e8047d39c4278beb882410891f0d8221689dd3236c65028c" + "xcode_project_sha256": "dca902077d2a625788b396b990647432af03f6c2f64eef6d2b48cdb64b5aa8fb" }, "disposition": "unsigned", "platforms": [ @@ -22,7 +22,7 @@ "lib_revision": "13ac1f2135199702a07ba6aa14ab22101f17fcb0", "source_date_epoch": 1787871027, "swift_package_lock_sha256": "94ae067a374726cdaf6b4ca0a5e44663c57fcdc5334060c5ffef5e79cfbf04c0", - "tera_ffi_source_tree": "a304a7b5ce6f52f7f3b4a13a59baed9571879bed", + "tera_ffi_source_tree": "114cb7bc9ce1f3ac0808c7d6a6dd4887298bceec", "xcode_package_lock_sha256": "c7f41934ea25f7a287bdc4f3a6ecabbf09a3a0bdd0f5a3e58183f355ca814096" }, "version": "0.1.0-alpha" diff --git a/test-fixtures/legacy-identifiers.v1.json b/test-fixtures/legacy-identifiers.v1.json @@ -2159,7 +2159,7 @@ "category": "native_ui_contract", "occurrences": [ { - "path": "Tera/Views/TeraAddView.swift", + "path": "Tera/Views/TeraCalendarComposerFields.swift", "count": 2 }, { @@ -2173,7 +2173,7 @@ "category": "native_ui_contract", "occurrences": [ { - "path": "Tera/Views/TeraAddView.swift", + "path": "Tera/Views/TeraCalendarComposerFields.swift", "count": 2 }, { @@ -2187,7 +2187,7 @@ "category": "native_ui_contract", "occurrences": [ { - "path": "Tera/Views/TeraAddView.swift", + "path": "Tera/Views/TeraCalendarComposerFields.swift", "count": 1 }, { diff --git a/test-fixtures/swiftlint-maintainability-baseline.v1.json b/test-fixtures/swiftlint-maintainability-baseline.v1.json @@ -1 +1 @@ -[{"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 +[{"text":" init(","violation":{"reason":"Initializer body should span 50 lines or less excluding comments and whitespace: currently spans 52 lines","severity":"error","location":{"file":"Tera\/App\/TeraAppModel.swift","line":21,"character":3},"ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","ruleName":"Function Body Length"}},{"text":"actor TeraAddMediaCoordinator: TeraAddMediaHandling {","violation":{"reason":"Actor body should span 250 lines or less excluding comments and whitespace: currently spans 330 lines","severity":"error","location":{"file":"Tera\/Runtime\/TeraAddMediaCoordinator.swift","line":51,"character":1},"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length"}},{"text":"#endif","violation":{"reason":"File should contain 400 lines or less: currently contains 458","severity":"error","location":{"file":"Tera\/Runtime\/TeraAddMediaCoordinator.swift","line":458,"character":1},"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length"}},{"text":"private final class TeraGeneratedRuntimeBackend: TeraRuntimeBackend, @unchecked Sendable {","violation":{"reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 491 lines","severity":"error","location":{"file":"Tera\/Runtime\/TeraGeneratedRuntimeBackend.swift","line":111,"character":15},"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length"}},{"text":"}","violation":{"reason":"File should contain 1000 lines or less: currently contains 1531","severity":"error","location":{"file":"Tera\/Runtime\/TeraGeneratedRuntimeBackend.swift","line":1531,"character":1},"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length"}},{"text":"actor TeraRuntimeClient {","violation":{"reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 753 lines","severity":"error","location":{"file":"Tera\/Runtime\/TeraRuntimeClient.swift","line":3,"character":1},"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length"}},{"text":" func changes(bufferCapacity: Int = 16) async throws -> AsyncStream<TeraRuntimeChange> {","violation":{"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 70 lines","severity":"error","location":{"file":"Tera\/Runtime\/TeraRuntimeClient.swift","line":379,"character":3},"ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","ruleName":"Function Body Length"}},{"text":"}","violation":{"reason":"File should contain 400 lines or less: currently contains 835","severity":"error","location":{"file":"Tera\/Runtime\/TeraRuntimeClient.swift","line":835,"character":1},"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length"}},{"text":"}","violation":{"reason":"File should contain 1000 lines or less: currently contains 1154","severity":"error","location":{"file":"Tera\/Runtime\/TeraRuntimeModels.swift","line":1154,"character":1},"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length"}},{"text":"final class TeraAddStore: ObservableObject {","violation":{"reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 731 lines","severity":"error","location":{"file":"Tera\/State\/TeraAddStore.swift","line":11,"character":7},"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length"}},{"text":" func retract(_ card: TeraTodayCard) async {","violation":{"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 51 lines","severity":"error","location":{"file":"Tera\/State\/TeraAddStore.swift","line":459,"character":3},"ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","ruleName":"Function Body Length"}},{"text":"}","violation":{"reason":"File should contain 400 lines or less: currently contains 796","severity":"error","location":{"file":"Tera\/State\/TeraAddStore.swift","line":796,"character":1},"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length"}},{"text":"actor TeraConfigurationStore {","violation":{"reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 468 lines","severity":"error","location":{"file":"Tera\/State\/TeraConfigurationStore.swift","line":78,"character":1},"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length"}},{"text":" func load() throws -> TeraAppConfiguration {","violation":{"reason":"Function body should span 100 lines or less excluding comments and whitespace: currently spans 109 lines","severity":"error","location":{"file":"Tera\/State\/TeraConfigurationStore.swift","line":148,"character":5},"ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","ruleName":"Function Body Length"}},{"text":"}","violation":{"reason":"File should contain 400 lines or less: currently contains 759","severity":"error","location":{"file":"Tera\/State\/TeraConfigurationStore.swift","line":759,"character":1},"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length"}},{"text":"actor TeraSessionStore {","violation":{"reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 390 lines","severity":"error","location":{"file":"Tera\/State\/TeraSessionStore.swift","line":39,"character":1},"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length"}},{"text":" private func start(acceptingReconfiguration: Bool) async -> TeraSessionPhase {","violation":{"reason":"Function should have complexity 10 or less; currently complexity is 13","severity":"error","location":{"file":"Tera\/State\/TeraSessionStore.swift","line":125,"character":11},"ruleDescription":"Complexity of function bodies should be limited.","ruleIdentifier":"cyclomatic_complexity","ruleName":"Cyclomatic Complexity"}},{"text":"}","violation":{"reason":"File should contain 400 lines or less: currently contains 452","severity":"error","location":{"file":"Tera\/State\/TeraSessionStore.swift","line":452,"character":1},"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length"}},{"text":"struct TeraAddView: View {","violation":{"reason":"Struct body should span 350 lines or less excluding comments and whitespace: currently spans 381 lines","severity":"error","location":{"file":"Tera\/Views\/TeraAddView.swift","line":4,"character":1},"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length"}},{"text":"}","violation":{"reason":"File should contain 400 lines or less: currently contains 479","severity":"error","location":{"file":"Tera\/Views\/TeraAddView.swift","line":479,"character":1},"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length"}},{"text":"struct TeraSettingsView: View {","violation":{"reason":"Struct body should span 250 lines or less excluding comments and whitespace: currently spans 259 lines","severity":"error","location":{"file":"Tera\/Views\/TeraSupportingViews.swift","line":365,"character":1},"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length"}},{"text":"}","violation":{"reason":"File should contain 400 lines or less: currently contains 629","severity":"error","location":{"file":"Tera\/Views\/TeraSupportingViews.swift","line":629,"character":1},"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length"}},{"text":"final class TeraAddStoreTests: XCTestCase {","violation":{"reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 664 lines","severity":"error","location":{"file":"TeraTests\/TeraAddStoreTests.swift","line":6,"character":7},"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length"}},{"text":"private actor AddBackend: TeraRuntimeBackend {","violation":{"reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 492 lines","severity":"error","location":{"file":"TeraTests\/TeraAddStoreTests.swift","line":843,"character":9},"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length"}},{"text":" private func makeStatus(","violation":{"reason":"Function should have 5 parameters or less: it currently has 8","severity":"error","location":{"file":"TeraTests\/TeraAddStoreTests.swift","line":1279,"character":11},"ruleDescription":"Number of function parameters should be low.","ruleIdentifier":"function_parameter_count","ruleName":"Function Parameter Count"}},{"text":"}","violation":{"reason":"File should contain 1000 lines or less: currently contains 1430","severity":"error","location":{"file":"TeraTests\/TeraAddStoreTests.swift","line":1430,"character":1},"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length"}},{"text":"final class TeraRemoteQualificationTests: XCTestCase {","violation":{"reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 307 lines","severity":"error","location":{"file":"TeraTests\/TeraRemoteQualificationTests.swift","line":7,"character":7},"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length"}},{"text":" func testQualificationRejectsAmbiguousOrUnsafeHarnessValues() {","violation":{"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 94 lines","severity":"error","location":{"file":"TeraTests\/TeraRemoteQualificationTests.swift","line":45,"character":3},"ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","ruleName":"Function Body Length"}},{"text":" func testAuthorizationEvidenceIsRedactedEphemeralAndRelaunchCleaned() throws {","violation":{"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 53 lines","severity":"error","location":{"file":"TeraTests\/TeraRemoteQualificationTests.swift","line":212,"character":3},"ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","ruleName":"Function Body Length"}},{"text":"final class TeraRuntimeClientTests: XCTestCase {","violation":{"reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 298 lines","severity":"error","location":{"file":"TeraTests\/TeraRuntimeClientTests.swift","line":4,"character":7},"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length"}},{"text":"final class TeraStateMigrationTests: XCTestCase {","violation":{"reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 580 lines","severity":"error","location":{"file":"TeraTests\/TeraStateMigrationTests.swift","line":6,"character":7},"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length"}},{"text":" func testRelayValidationMatchesRuntimeProfiles() throws {","violation":{"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 61 lines","severity":"error","location":{"file":"TeraTests\/TeraStateMigrationTests.swift","line":7,"character":5},"ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","ruleName":"Function Body Length"}},{"text":"}","violation":{"reason":"File should contain 400 lines or less: currently contains 622","severity":"error","location":{"file":"TeraTests\/TeraStateMigrationTests.swift","line":622,"character":1},"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length"}},{"text":"private actor SupportingBackend: TeraRuntimeBackend {","violation":{"reason":"Actor body should span 250 lines or less excluding comments and whitespace: currently spans 252 lines","severity":"error","location":{"file":"TeraTests\/TeraSupportingStoreTests.swift","line":207,"character":9},"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length"}},{"text":"}","violation":{"reason":"File should contain 400 lines or less: currently contains 486","severity":"error","location":{"file":"TeraTests\/TeraSupportingStoreTests.swift","line":486,"character":1},"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length"}},{"text":"final class TeraRemoteQualificationUITests: XCTestCase {","violation":{"reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 1493 lines","severity":"error","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":4,"character":7},"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length"}},{"text":" func testLocalSocialFiveFlowScenario() throws {","violation":{"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 73 lines","severity":"error","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":62,"character":3},"ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","ruleName":"Function Body Length"}},{"text":" func testLocalSocialDeterministicPersonas() throws {","violation":{"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 66 lines","severity":"error","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":174,"character":3},"ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","ruleName":"Function Body Length"}},{"text":" func testRemoteBlossomUploadAndRecovery() throws {","violation":{"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 54 lines","severity":"error","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":247,"character":3},"ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","ruleName":"Function Body Length"}},{"text":" private func expectedAddFieldLabel(_ identifier: String, type: String) -> String {","violation":{"reason":"Function should have complexity 10 or less; currently complexity is 18","severity":"error","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":573,"character":11},"ruleDescription":"Complexity of function bodies should be limited.","ruleIdentifier":"cyclomatic_complexity","ruleName":"Cyclomatic Complexity"}},{"text":" private func preparePhotoUpdate(_ app: XCUIApplication, marker: String) -> XCUIElement? {","violation":{"reason":"Function should have complexity 10 or less; currently complexity is 11","severity":"error","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":767,"character":11},"ruleDescription":"Complexity of function bodies should be limited.","ruleIdentifier":"cyclomatic_complexity","ruleName":"Cyclomatic Complexity"}},{"text":" private func preparePhotoUpdate(_ app: XCUIApplication, marker: String) -> XCUIElement? {","violation":{"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 66 lines","severity":"error","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":767,"character":11},"ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","ruleName":"Function Body Length"}},{"text":" static func environment(","violation":{"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 67 lines","severity":"error","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":1736,"character":10},"ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","ruleName":"Function Body Length"}},{"text":"}","violation":{"reason":"File should contain 1000 lines or less: currently contains 2212","severity":"error","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":2212,"character":1},"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length"}}] +\ No newline at end of file