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