Dates That Decide

We agreed a date should be shown on the building's clock, and enforced it three ways. We never decided which clock a date is judged on — so from about 6pm every evening, the code thinks it is tomorrow.

2026-09-04 · finding 4 of 8 from the 2026-09-02 "one place" sweep · open, waiting on a decision · not newly broken, no customer report

The gap

Three instruments guard how a date looks. Zero guard what it does. Display was standardised in August and fenced with a lint rule, a drift test and a reviewer lens. The half that decides — comparisons, PMS writes, branch conditions — was never given a rule at all.

Denver runs six hours behind UTC in summer, seven in winter. Every place in the code that asks "is it today yet?" answers on UTC. Two things follow, both of them landing in the part of the day when vendors finish jobs and residents pick up keys.

After ~6:00pm Mountain

A job finished this evening is recorded in AppFolio with tomorrow's date.

The completion date comes from the clock the server runs on, not the building's. A vendor who finishes at 6:30pm Tuesday is filed as having finished Wednesday.

After ~6:00pm Mountain

A resident whose lease starts tomorrow flips to "active" six hours early.

The move-in comparison asks whether the start date has arrived. It has, in UTC. So the evening before move-in day, the system starts treating them as a current resident.

DENVER — THE BUILDING'S CLOCK 12am 12pm 6pm 12am 6 HOURS WRONG UTC — THE CLOCK THE CODE ACTUALLY ASKS today tomorrow — already job filed a day late lease starts early
One Denver day. UTC rolls into tomorrow at 6pm local — 7pm once daylight saving ends — and every date decision rolls with it. Both known symptoms sit inside that window.

Why it persists

When a date is displayed, three separate instruments stop us getting it wrong. When a date is decided, nothing checks anything.

ShownDecided
Agreed standardYesThe property's clockNoneWhatever the server runs on
Blocks a bad mergeLint rule, errorNO_UNZONED_DATE_RENDERNothing
Fails after mergeDrift testproperty-timezone.drift.test.tsNothing
Flagged in reviewReviewer lensFour graded rulesNothing

Three things already in the code

This is not a blank page. Parts of the answer are written down. Nothing made them the rule.

Someone already hit this and compensated — at one site

The rent-roll comparison uses >= instead of >, with a ten-line comment explaining that the day is UTC and a property west of UTC rolls into it hours early. The boundary was moved to absorb the timezone error. It works there. Nothing tells the next comparison to lean the same way — or which way that even is.

src/lib/domain/leasing/rent-roll/entity-delta.ts:328

The right pattern exists in two places, as advice

One planner takes the day as an argument — "Injected, never read off the clock" — so it cannot disagree with its caller. The AppFolio date helpers say it outright: "Callers that need 'today' should inject a clock." Both are correct. Neither is enforced, and neither is what most call sites do.

src/lib/domain/leasing/future-arrivals/plan.ts:221 · lambda/agent-runtime/vendors/appfolio/dateHelpers.ts

You have already decided the hard part, once

Quiet hours are measured on the property's clock (Gera, 2026-08-25, superseding the resident-clock rule). And when a building has no timezone set, the send is refused rather than run against a guess. That is a complete answer to "whose clock" and to "what if we don't know" — it just never generalised past quiet hours.

src/lib/data/types.tsrecipient_clock_unresolved

Where the day gets computed

The fix is not a formula. It is moving one question — what day is it, for this building? — from every site that needs an answer to a single place that hands one down. The same move the maintenance fix made, and the same one quiet hours already made.

TODAY Every site reads the clock itself server clock server clock server clock lease goes active job completion date renewal window three answers, free to disagree — and one already compensates alone PROPOSED One clock at the edge, passed down property clock lease goes active job completion date renewal window one answer, and no site can invent its own without being noticed
Left: the shape that produced both symptoms — and that already forced one site to hand-compensate on its own. Right: the day computed once, from the building's timezone, and passed to whoever needs it.

Four ways forward

They differ mostly in how hard it becomes to write the wrong version next time.

OptionWhat you'd be decidingCostWhere it breaks
A · Property clock Recommended One way to ask what day it is for a building. Decision sites take the day as an argument. No timezone on the property → refuse, exactly as quiet hours already does. Medium-highMany call sites; needs an answer for fleet-wide jobs not scoped to one building Nothing structural. Still right at forty buildings in four states.
B · One fleet clock Declare that date decisions happen on Mountain Time, full stop. One constant, one helper, largely mechanical. LowCould ship this week The day we onboard a building outside Mountain Time — and it breaks silently, which is how this whole class of bug works.
C · Make it not compile A calendar day becomes its own type, producible only by a function that is given a timezone. A UTC-derived string stops type-checking into a decision. Highest upfrontRolls out incrementally Nothing. This is the "make the wrong version impossible to write" fix the sweep named as the durable one.
D · Patch the two Fix the AppFolio date and the early lease flip. Leave the pattern alone. Lowest It regrows. That is what the sweep was about.

A and C are the same choice, staged

C is A with the rule enforced by the compiler instead of by discipline. The realistic sequence is A first, C behind it once the primitive has a name and a few dozen call sites have proved it fits.

B is defensible if you want this closed this week

All four live buildings are in Denver, so B is correct today. I would only take it with the expiry written down in the code, because a shortcut that is correct on the day it ships is exactly the kind that reads as settled six months later.

The part that actually needs a human

1 · Which option
2 · Where the line sits

What has not been verified

The early lease flip is confirmed precisely — the comparison, the UTC day it reads, and the comment explaining the compensation are all in the file named above.

The AppFolio completion date has not been pinned to a specific line. The pattern is unquestionably present and the symptom was reported in the original sweep, but the writer may live in the browser-agent path rather than the main repository. It would be located before any fix is written, not assumed.

Both counts — 135 lines, 89 files — are measured against origin/main on 2026-09-04 and exclude test paths. Neither has been triaged into decisions versus storage; that triage is the first task after the two answers above.

Context

Finding 4 of 8 from the 2026-09-02 "one place" sweep. Six of the eight are merged: fair housing (#6810, #6817, #6819), the teammate-takeover handoff (#6870), consent totality (#6942), work-order closures (#6906), invite links (#6943), plus the guard-writing runbook (#6954) and the vendor-name trio (#7007, #7010, #7031). This finding and the dead-toggles one remain open — both on a decision rather than on hands.

PropFlow Docs