Skip to content

Commit ab12c52

Browse files
ChrisPelatariclaude
andcommitted
feat(69): GREEN — push parity: moves, tag diffs, assignee diffs
pushParityDiffs on the LWW local-newer branch: column moves ride POST /triage (only when the remote column is KNOWN and differs — legacy payloads without a column say nothing about placement); tags and assignees push as exact toggle diffs against this cycle's fresh remote state (tag_ids on PUT is rejected live; toggles are not idempotent). Errors collect, never throw. Rider: removes a same-type forced cast warning that rode in with #44 (StepRepository) — restores the zero-warning baseline. Closes the gap found in issue #33 (option A). Full suite green; macOS zero warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 095e212 commit ab12c52

2 files changed

Lines changed: 60 additions & 2 deletions

File tree

FenixKanban/Core/Repositories/StepRepository.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ final class StepRepository {
2727

2828
/// Returns all `CardStep` rows for `card`, sorted by `sortOrder` ascending.
2929
func fetchSteps(for card: Card) -> [CardStep] {
30-
let request = CardStep.fetchRequest() as! NSFetchRequest<CardStep>
30+
let request = CardStep.fetchRequest()
3131
request.predicate = NSPredicate(format: "card == %@", card)
3232
request.sortDescriptors = [NSSortDescriptor(keyPath: \CardStep.sortOrder, ascending: true)]
3333
return (try? context.fetch(request)) ?? []

FenixKanban/Core/Services/Fizzy/FizzySyncEngine.swift

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,9 +238,15 @@ final class FizzySyncEngine {
238238
}
239239
result.itemsUpdated += 1
240240
} else if localModified > localFizzyTimestamp {
241-
// Local edited since last sync → push.
241+
// Local edited since last sync → push. Beyond the PUT
242+
// (title/description), parity push (#69) diffs local state
243+
// against the freshly-fetched remote card: column moves ride
244+
// POST /triage; tags and assignees push as EXACT toggle
245+
// diffs (tag_ids on PUT is rejected by the live server, and
246+
// toggles are not idempotent — docs/fizzy-api-notes.md).
242247
do {
243248
let updated = try await putCard(card, number: p.fizzyNumber)
249+
await pushParityDiffs(for: card, against: remote, number: p.fizzyNumber, into: &result)
244250
recordPairing(for: card, fizzyID: fizzyID, number: p.fizzyNumber, updatedAt: updated.lastActiveAt)
245251
card.modifiedAt = updated.lastActiveAt
246252
result.itemsUpdated += 1
@@ -810,6 +816,58 @@ final class FizzySyncEngine {
810816

811817
// MARK: - Apply remote → local
812818

819+
// MARK: - Push parity diffs (#69: moves, tags, assignees)
820+
821+
/// Pushes the local-vs-remote deltas the PUT body can't carry, on the
822+
/// LWW local-newer branch only. Diffs are computed against the remote
823+
/// card from THIS cycle's pull (fresh state), because the toggle
824+
/// endpoints are not idempotent. Errors collect into the result.
825+
private func pushParityDiffs(
826+
for card: Card,
827+
against remote: FizzyCard,
828+
number: Int64,
829+
into result: inout FizzySyncResult
830+
) async {
831+
// Column move → POST /cards/:n/triage with the local column's id.
832+
// Requires a KNOWN remote column: payloads without one (legacy list
833+
// shapes) say nothing about placement, and triaging on unknown would
834+
// fabricate moves.
835+
if let localColumnID = card.column?.fizzyColumnID,
836+
let remoteColumnID = remote.column?.id,
837+
localColumnID != remoteColumnID {
838+
do {
839+
try await client.triageCard(number: Int(number), columnID: localColumnID)
840+
} catch {
841+
result.errors.append("Push move '\(card.title ?? "(untitled)")': \(error)")
842+
}
843+
}
844+
845+
// Tags → exact toggle diff by case-insensitive title.
846+
let localTags = Set(((card.labels as? Set<Label>) ?? []).compactMap { $0.name?.lowercased() })
847+
let remoteTags = Set(remote.tags.map { $0.lowercased() })
848+
for tag in localTags.subtracting(remoteTags).union(remoteTags.subtracting(localTags)).sorted() {
849+
do {
850+
try await client.toggleCardTag(number: Int(number), tagTitle: tag)
851+
} catch {
852+
result.errors.append("Push tag '\(tag)': \(error)")
853+
}
854+
}
855+
856+
// Assignees → exact toggle diff by user id. A nil remote array means
857+
// the payload didn't carry the field — skip rather than mass-toggle.
858+
if let remoteAssignees = remote.assignees {
859+
let localIDs = Set(card.assignees.map(\.id))
860+
let remoteIDs = Set(remoteAssignees.map(\.id))
861+
for id in localIDs.subtracting(remoteIDs).union(remoteIDs.subtracting(localIDs)).sorted() {
862+
do {
863+
try await client.toggleCardAssignment(number: Int(number), assigneeID: id)
864+
} catch {
865+
result.errors.append("Push assignment '\(id)': \(error)")
866+
}
867+
}
868+
}
869+
}
870+
813871
// MARK: - Wire lifecycle mapping (#13)
814872

815873
/// Writes the synced fields from a `FizzyCard` onto a local `Card`.

0 commit comments

Comments
 (0)