Skip to content
← All work

Engineering case study · Sleep Map

A promisethe codehas to keep.

Keep the night and a real place becomes yours. Miss it and nothing is taken back. How that promise holds across 24,062 places, every time zone and thirty builds.

1,190 cells3,479 neighbour edges

Scroll

A bad night slows progress but never destroys already-claimed territory.

The one rule that outranks every feature. What follows is that sentence, enforced.

The shape of it

Everything a night needs is on the phone.

One SwiftUI app with a SwiftData store that never syncs, and a Python pipeline that builds the world it ships with. The night is judged on the device. Pictures and signed words come down; only claimed ground goes out, to friends who asked for it.

pipeline/Python, offline · Shapely · SciPycells.jsonthe graphcells_polygons.binthe shapes, packedcells_land.binwhat the map paintsregion_places.jsonthe namesstories.jsonthe bedtime prosebuilds the world the app ships withthe night is judged hereSwiftData · cloudKitDatabase: .noneCloudflare R2assets.sunlitgamestudio.comphotos · signed revisionsCloudKiteach member’s iCloudCircle onlyAMBNothing watchingno analytics SDK, no advertising SDKphotosigned revisionclaimed ground onlybundledfetched when neededpipeline/Python, offline · Shapely · SciPycells.jsonthe graphcells_polygons.binthe shapes, packedcells_land.binwhat the map paintsregion_places.jsonthe namesstories.jsonthe bedtime prosejudged herenever syncsCloudflare R2assets.sunlit…photos · signed revisionsCloudKiteach member’s iCloudCircle onlyAMBNothing watchingno analytics, no ads SDKphotosigned revisiongroundbundledfetched

A world of cells

You name cells. You don’t build them.

EurekaReddingSacramentoSan FranciscoSouth Lake TahoeSanta BarbaraPalm SpringsBakersfieldLos AngelesSan DiegoFresnoHanga RoaPoint Nemo2,700 km4.5° ≈ 500 km
  1. 01 / 05

    Population cells ship jagged.

    Raw cells came in as shards: perimeter²/4π·area ran as high as 201.

  2. 02 / 05

    Every cell takes a real town.

    Each is named for a town anchor, and the anchor becomes its seed.

  3. 03 / 05

    Voronoi, at the anchors.

    repartition.py grows each cell from its seed. Spikiness falls to about 1.4.

  4. 04 / 05

    Four files, four jobs.

    Graph, shapes, painted land, names: four layers, one key.

  5. 05 / 05

    A hometown has a limit.

    Point Nemo once resolved to Hanga Roa, 2,700 km away. The fallback now stops near 500 km.

The coast, not the world

Draw the coast. Don’t move the world.

Until 1.12 the map painted whole cells, so coasts bulged into the sea. Clipping the cells would have changed the graph, the IDs and everyone’s ground. So only the drawing changed: coast_clip.py bakes each cell’s land and shoreline into a file the map paints, and a tap still lands on the original polygon.

1.12 · land1.11 · cells

Taiwan · 22 places held, grown from Zhongzheng

  • Shoreline is subtractiona cell’s land edge minus every other cell’s land
  • One coveragesimplified as one world, so neighbours keep identical borders
  • Tap the sea, get the islandhit-testing stays on the polygon; a speck island gets a 6 pt dot
  • ~16 MB, baked offlinecells_land.bin; a cell missing from it keeps its ring

Fast enough to open

61 MB of text, on every cold launch.

The cell shapes ship as a packed binary now, built by a build phase, with a test that keeps the JSON out of the bundle. Here is the load at real speed, as measured on the Debug simulator.

cells_polygons.json61 MB of text · JSONDecoder

2,370 ms

cells_polygons.binSMPG v1 · Int32 × 1e7

1,051 msready

  1. SMPG
  2. v1
  3. count
  4. id length
  5. cell ID
  6. points
  7. Int32 × 2 per point
  8. …

Ground that can’t be lost

Every shipped store, opened again.

Claimed ground is append-only, and the store on the phone is the only place it lives. Every schema change leaves a fixture: a real store from the last build that shipped the old shape. A store that still won’t open is renamed aside, never deleted, and refilled from a shadow copy.

StoreUpgradeTests opens every shipped build’s real store under today’s schema

19 / 19 open as .opened

  1. 1.1.1b5
  2. 1.2.0b6
  3. 1.3.0b7
  4. 1.4.0b8
  5. 1.4.0b9
  6. 1.4.1b10
  7. 1.5.0b11
  8. 1.6.0b12
  9. 1.7.0b13
  10. 1.7.1b14
  11. 1.7.2b15
  12. 1.7.3b16
  13. 1.8.0b17
  14. 1.8.1b18
  15. 1.9.0b20
  16. 1.9.1b21
  17. 1.10.0b22
  18. 1.10.1b23
  19. 1.11.0b24
  20. 1.12.0V3 · today
StoreOpener.swift
let movedTo = try moveAside(configuration.url)
note("moved the failed store to \(movedTo)")
let container = try make(configuration)
if let shadow { try shadow.restore(into: container.mainContext) }
return (container, .recoveredFromFailedStore(movedTo: movedTo))

A night is absolute dates

Judge a night by the rules it started under.

At Good night, clock minutes and the tap become absolute deadlines, once. Grace is baked in, so a setting changed at 3 AM can’t move the goalposts, and the verdict is a pure function, tested as a table. Try it.

The clock you read it on

Grace when you tapped Good night

Kept. The place is claimed.Nothing about the clock you read it on changes that.

NightEvaluator.verdict · four checks, in order
if tap > schedule.bedtimeDeadline {→ .sleptLate
if wake < schedule.wakeOpen {→ .wokeEarly / .brokeMidNight
if wake > schedule.wakeClose {→ .overslept
return NightVerdict(succeeded: true, failReason: nil, actualWake: wake)

Time zones

A night belongs to where you slept it.

Each night stores the zone it began in, and every surface dates it as lived. The first night after a jump of three hours or more is judged but can never be a miss. Before 1.12, a one-hour trip west could reset a streak on a good night.

UTC−6UTC−3UTC+0UTC+3UTC+6UTC+9Los AngelesDenverNew YorkLondonTaipeiTokyo1 h apart at the same instant
Last night in
Tonight in
  1. Thu, Nov 5Calendar.current
  2. Fri, Nov 6last night, as lived ✓
  3. Sat, Nov 7

Read through Calendar.current in Los Angeles, the night lands a day early: a gap, and a good night’s streak could reset. 1.12 files it under the day you slept it.

Tonight1 h is not a tripUnder three hours, judged like any night.

What crosses the wire

Signed words in. Only ground out.

Stories, names and photos can update without App Review, as a signed revision built for one exact bundle. Nothing else can ride along. A Circle publishes claimed places only, so a missed night is simply silence.

In · signed words

revisionstories · names · photosEd25519
  1. signed: checked against the bundled public key
  2. its baseline ID is this exact bundle’s
  3. applied at the next launch

Never · whatever the signature

executable codemap topologysleep rulessaved progress

Code, map topology, sleep rules and saved progress change only with an App Store build.

Out · to a Circle

claimed places
  • sleep times
  • health data
  • missed nights
CirclePublishGuard.swift
static func mayPublish(tainted: Bool, succeeded: Bool) -> Bool {
    !tainted && succeeded
}

Tests and a headless release

Taps can’t be scripted, so nothing needs them.

Every screen opens from a debug launch argument, so tests and screenshots drive the app without a tap. A preflight audits the Release build, and one script takes it from a green tree to App Review.

iOS tests
1,007

XCTest, one simulator, never parallel

pipeline tests
908

pytest, over the code that builds the world

story voice gate
exit 0

voice_report.py, before any story ships

preflight.sh · against the Release build

  1. OTA baseline matches the bundle
  2. the globe’s texture is absent
  3. privacy manifest at the bundle root
  4. zero DEBUG symbols in the binary
  5. manifest counts for the listing
  6. the iOS suite is green
  7. a store fixture for the last shipped build
  8. every ModelConfiguration says .none
  9. the string catalog covers the build

asc.py · App Store Connect, headless

  1. status
  2. create-version
  3. what’s new × 9
  4. review-notes
  5. attach-build 30
  6. submit
  7. Waiting for review1.12.0 · build 30

One lesson the runbook keeps: cancelling a submission does not spend its build.

What I take from it

Make the promise something a test can break.

  1. Keep the truth apart from the drawing.The coastline changed what the map paints, and not one claimed place.
  2. Freeze a night when it starts.Absolute dates at the tap, so settings, updates and zones can’t reach back into it.
  3. Every shipped store is a fixture.A promise about someone’s ground holds only if today’s build opens what every earlier one wrote.

Keep the night. Keep the place.

Set tonight’s hours and see which real place is waiting next door to home.