Flight identity
As of 2026-10-19 (vulture-ios 3176c4cc4, vulture-android 85fd9cc6).
A flight has several ids. Each one is right for one job only:
| Id | What it is | Scope | Use it for |
|---|---|---|---|
Analysis id (Flight.id) | 64-bit FNV-1a over filename, header date, first-fix time and fix count | The same on every launch and every platform, for the same parsed file | Equality and hashing of a parsed flight, the phases tab’s remembered scroll position, the header.id in the golden JSON |
| Sync id | SHA-256 of the IGC file’s original bytes, as 64 lowercase hex characters | Global: the same file is the same id on every device and platform | The Firestore document id users/{uid}/flights/{id}, the Cloud Storage blob igc/{id}.igc.gz, and the Android Room primary key |
| Filename | CoreFlight.filename | Syncs through CloudKit; not unique | Handoff between Apple devices (persistentIdentifier, VultureHandoffFlights) |
| Core Data object URI | objectID.uriRepresentation() | One device’s store only | The iOS Flight widget link, Spotlight and the App Intents FlightEntity. Never put it in a link that leaves the device |
Content that crosses devices or platforms (a link, a share, a sync record) carries the sync id, never a local object id. Deep links lists the one link that breaks this rule today: the widget’s /open/flight?id=.
Analysis id: FNV-1a
Section titled “Analysis id: FNV-1a”| iOS | vulture-ios/Packages/Flights/Sources/Flights/Models/Flight+Identity.swift (FlightIdentifier.make), tests in Packages/Flights/Tests/FlightsTests/Models/FlightIdentityTests.swift |
| Android | vulture-android/domain/flight/src/main/kotlin/be/fousa/vulture/domain/flight/model/FlightIdentifier.kt, test domain/flight/src/test/.../FlightIdentityTest.kt |
| Offset basis | 0xcbf29ce484222325 |
| Prime | 0x100000001b3 |
The bytes are hashed in this order. Each byte is XORed in and then multiplied by the prime, wrapping at 64 bits.
- The filename’s UTF-8 bytes. This includes a generated filename when the file had none.
date.timeIntervalSince1970as the bit pattern of a Double (KotlindoubleToRawLongBits), least significant byte first. The date is UTC midnight of theHFDTEdate.startRecordingTime(the first fix, in seconds since UTC midnight) as anInt64, least significant byte first.fixCountas anInt64, least significant byte first.
The result is read back as a signed 64-bit integer. It is deliberately not Swift’s Hasher, which uses a new random seed in every process. The id is persisted, so it must be bit-identical on both platforms.
This id identifies a parsed flight, not a file. Two different files with the same name, date, start time and fix count would collide. Do not use it as a storage key.
Sync id: SHA-256 of the bytes
Section titled “Sync id: SHA-256 of the bytes”The rule and the other record ids (tasks, home bases, map files, competitions) are in Sync schema §4, decision P8B-D9. In short:
- The flight id is the SHA-256 of the IGC file’s original bytes, as imported. The same file on two devices makes one document.
- Android uses it as the flight’s primary key everywhere (
vulture-android/data/db/.../entity/FlightEntity.kt,FlightRepository.kt). Re-importing the same file is silently skipped. - Risk R4: iOS keeps only
CoreFlightData.rawContent(aString). For a file with non-UTF-8 header bytes (Latin-1 pilot names), re-encoding that text gives different bytes, and so a second id. The proposed fix is in sync-schema §4, “Risk: flight ids of non-UTF-8 IGC files”.
How the CloudKit model maps to it
Section titled “How the CloudKit model maps to it”iOS syncs through NSPersistentCloudKitContainer, which has no unique constraints. A flight has no id field of its own. The model is in persistence and settings §1.3.
| iOS today | How it identifies a flight | Sync-schema equivalent |
|---|---|---|
CoreFlight + CoreFlightData.rawContent | Its content. Importing looks for an existing flight with identical rawContent. After a CloudKit import, flights with the same filename and identical rawContent are de-duplicated, and the oldest by updatedAt is kept (DataManager.processPersistentHistory()) | flights/{sha256(bytes)}: the same rule made structural, so de-duplication is no longer needed |
data (logbook) / recentData (recents) relationships | n/a | inLogbook |
Handoff FlightActivity | filename (vulture-ios/Packages/Map/Sources/Map/Handoff/FlightHandoff.swift) | Could carry the sync id once iOS has one |
Widget, Spotlight, FlightEntity | Core Data URI | Local only, no mapping |
When iOS joins Firestore, it computes the sync id from the stored text. Flights need no new Core Data attribute. Tasks and map files get a syncID attribute. See sync-schema §7, “How iOS maps onto this”, and ios-changes A4.
