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
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.
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.
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.
When a date is displayed, three separate instruments stop us getting it wrong. When a date is decided, nothing checks anything.
| Shown | Decided | |
|---|---|---|
| Agreed standard | YesThe property's clock | NoneWhatever the server runs on |
| Blocks a bad merge | Lint rule, errorNO_UNZONED_DATE_RENDER | Nothing |
| Fails after merge | Drift testproperty-timezone.drift.test.ts | Nothing |
| Flagged in review | Reviewer lensFour graded rules | Nothing |
This is not a blank page. Parts of the answer are written down. Nothing made them the rule.
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
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
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.ts — recipient_clock_unresolved
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.
They differ mostly in how hard it becomes to write the wrong version next time.
| Option | What you'd be deciding | Cost | Where 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. |
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.
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 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.
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.