ADR-0129 — Collections quiet hours are measured on the RESIDENT's clock, not the property's

⚠️ SUPERSEDED 2026-08-25 — the lane is back on the PROPERTY's clock

Gera, 2026-08-25, stated three times and finally as: "there should only be one quiet hours and it's based off the property. Everything else should be updated because it's stale." His reasoning: the property's jurisdiction is the law that governs the tenancy, it is unambiguous, and an area code is a weak proxy for where somebody actually is.

This reverses his own 2026-08-19 answer (decision collections-three-phases d4: "Resident clock, resolved from the number. Matches the statute; needs an area-code resolver"), which is what this ADR implemented. Same person, six days apart, opposite answers — recorded plainly so nobody reads this document as still governing.

What survives, and what does not. The argument below from 47 CFR § 64.1200(c)(1) — that TCPA measures the called party's local time — is NOT refuted by the reversal; it is a live trade-off that the founder weighed and decided against. Read it as the case for the other side.

The refuse-don't-guess rule survives intact and is the part worth keeping: the lane still declines to send rather than fall back to DEFAULT_QUIET_HOURS_TIMEZONE. propertyClockFact (collections/approved-dun-send-ops.ts) refuses when a property has no timezone or a malformed one, precisely because isWithinQuietHours would have substituted 'America/Chicago' and texted on a guessed clock.

resolveRecipientContactWindow now has zero production callers. It and nanp-area-codes.ts are retained, unwired, against the possibility that counsel-brief Q7 is revisited.

Shipped in the PR that carries this banner.

Context

TCPA fixes the contactable window at 8am–9pm in the called party's local time (47 CFR § 64.1200(c)(1)). Not the caller's, and not the building's.

PropFlow measured it at the building. decideCollectionsDunPremise took its verdict from isWithinQuietHours(property.timezone, now), which falls back to a hardcoded zone when the field is unset:

export const DEFAULT_QUIET_HOURS_TIMEZONE = 'America/Chicago';

Property.timezone has no writer in application code — not onboarding, not an admin route, not the AppFolio sync. The only assignments in the repo are two fixture seeders under scripts/. The AppFolio adapter reads the field and warns when it is missing (pms-adapter.ts:1393), which is the shape of a value someone else is expected to have supplied.

What is NOT true, stated plainly

The obvious next sentence — "so every property falls through to Central, and a Denver resident was graded an hour wrong" — is false, and this ADR was drafted asserting it before anyone checked. A read-only scan of propflow-prod on 2026-08-19 returned 17 property rows, every one of them carrying a timezone: eleven America/Denver, two America/New_York, two America/Phoenix, one America/Los_Angeles. Camellia's is America/Denver.

So the Chicago fallback has not been firing, and no resident is known to have been texted outside their own window. That correction is recorded rather than quietly dropped, because the false version is a better-sounding argument than the true one and would have been the load-bearing sentence in this document.

What IS true, and why it is still worth changing

  1. The standard is wrong. TCPA measures the called party; a building is at best a proxy for that, and the decision below settled it against the proxy.
  2. Correctness depends on a field the product cannot maintain. Those 17 values were set out of band. Nothing in the product wrote them and nothing in the product keeps them right, so a newly onboarded property — or a row rebuilt by a future sync — silently becomes Central, and the failure is invisible: the send proceeds, on the wrong clock, and nobody learns.
  3. Nothing downstream would catch it. dispatch applies consent, ADR-0092 suppression and sender policy at the wire; it does not look at the clock. The premise check is the last rung that can stop a badly-timed send.

The defect this fixes is therefore latent rather than observed — a gate whose correctness rests on out-of-band data, sitting on a federal-rule boundary, in front of the first collections text ever sent to a real resident.

Three documents, two standards

The defect had already produced doc rot, which is how it survived review:

Document What it said
compliance/quiet-hours/constants.ts Quiet hours are the property's clock — team decision 2026-07-29, argued from number portability.
compliance/collections/assess.ts recipientLocalHour is "Current hour (0–23) in the RECIPIENT's local timezone", and the gate abstains rather than guessing.
collections/legality.ts The workflow's quiet hold is "resolveOutreachQuietHoldActivity, resident-local" and "already owns timing".

The third was false on both halves, and both errors pointed the reader away from the gap. The activity is real and collections-chase-workflow.ts:226 does await it — but (a) it is not resident-local: it reads property?.timezone and falls back to Central, and its own test is titled "the PROPERTY clock decides"; and (b) it does not own the timing that matters. Post-ADR-0125 the chase touch has no outbound path — it writes a pending review row — so that hold paces when a row appears on a QUEUE, not when a resident's phone lights up. It is the sentence that read as reassurance, so it is the one that mattered.

The hold itself is left in place by this ADR: pacing composition to daylight is harmless and mildly useful. It is simply not a TCPA gate, and the comment no longer implies it is.

And ADR-0125 §4 — an Accepted ADR — codified the property clock as the standard. The wrong answer was the written one.

The decision that reopened it

Gera, 2026-08-19, decision page collections-three-phases, question d4 ("Quiet hours — property clock or resident clock?"), verbatim:

Resident clock, resolved from the number. Matches the statute; needs an area-code resolver.

He was offered and rejected two alternatives: "property clock, but actually write the field" and "the intersection of both clocks".

This is urgent rather than tidy because collections-first-send is gated on it. The first collections text ever sent to a real resident should not go out on a gate whose correctness nobody in the product maintains.

Decision

The collections contact window is computed from the RECIPIENT's own time zone, resolved from their phone number. When the number does not establish a zone, the send is REFUSED. There is no fallback zone on this path.

1. The resolver

src/lib/domain/compliance/quiet-hours/recipient-clock.ts — pure, no I/O, no ambient clock (now is a required parameter). Given a phone number it returns either the whole computed window (IANA zone, local HH:MM, the 8–21 bounds, and the verdict) or a refusal carrying a reason code and one sentence of plain English.

Its data is nanp-area-codes.ts: a checked-in NPA → IANA table.

2. A gap REFUSES. It does not guess.

This is the load-bearing property, and it is why the union has two arms rather than a boolean plus a default:

There is no ?? 'America/Chicago', no nearest-neighbour, no majority-wins. A refusal returns no timeZone field at all, so there is nothing downstream can read as a zone even by mistake.

⚠️ The rule that makes this durable: incompleteness is safe by construction. Adding an NPA to the table is a strict improvement; omitting one costs a refusal, never a wrong send. So the table is deliberately conservative — every entry is one a reviewer can check, and anything the author could not check with confidence was left out rather than guessed in.

3. Ambiguous is an answer, not a tie to break

Roughly two dozen NPAs cover territory on both sides of a boundary: 812/930 and 574/219 in Indiana, 850/448 across the Florida panhandle, 701 (all of North Dakota), 605 (all of South Dakota), 907 (all of Alaska), 208/986 (all of Idaho), 906 in Michigan's UP, 620/785 in Kansas, 308 in Nebraska, 606 and 270/364 in Kentucky, 423/729 in Tennessee, 541/458 in Oregon, 775 in Nevada, 915/432 in west Texas, and 928 in Arizona (where the Navajo Nation observes DST and the rest of the state does not).

Each is enumerated with the boundary that straddles it, and each refuses. Picking the majority zone would be right most of the time, and the times it was wrong would be exactly the hour-off federal-rule violations this ADR exists to prevent.

They are enumerated rather than simply omitted so the refusal copy can name the real cause — "area code 812 covers more than one time zone" is a fact a PM can act on; "unknown area code" reads like a bug.

Two of those entries were wrong on the first pass and were corrected before this shipped: 423 (Tennessee) was assigned Eastern despite also serving the Central-time Cumberland Plateau counties, and 270/364 (Kentucky) were assigned Central despite also serving the Eastern-time strip around Elizabethtown and Campbellsville. Recording that is the point rather than an embarrassment: a hand-built NPA map is a thing a careful author gets wrong, and the reason getting it wrong is survivable is §2's refuse-don't-guess rule — not the author's care. The module header carries the same note so the next person to add an entry knows the boundary claim is the thing under review.

4. The refusal has its own reason code, and it is retryable

CollectionsDunRefusalReason gains recipient_clock_unresolved, distinct from quiet_hours. They are different verdicts with different remedies: "it is 23:10 for them" means approve again tomorrow; "we cannot tell what time it is for them" means fix the number on file, or have a person reach out directly. Collapsing them would tell a PM to wait for a window that will never open.

It is retryable — the row stays pending — for the same reason no_recipient is: the fix is a data fix a person can make, and burning a real reminder to a terminal state over a missing table entry would be the worse error.

5. Scope: collections only, on purpose

Renewals, prospect outreach, maintenance comms, handyman paging and the touch-budget window still resolve their window from Property.timezone, and land on the Central fallback whenever that field is unset — which is not the case for any property today, and which nothing in the product prevents tomorrow. They are not changed here. d4 was asked and answered about collections; migrating five other lanes is a different blast radius and a different decision, and bundling it would put an unreviewable diff in front of the send that is actually gated.

DEFAULT_QUIET_HOURS_TIMEZONE therefore survives, with its docblock rewritten to say what it now is: the property-clock fallback for the lanes that have not moved, explicitly not the collections standard, and never to be reintroduced to that path. The two standards coexist deliberately, and the constant says so, rather than one file quietly implying the other is wrong.

The residual is real and should be named rather than buried. It is the same LATENT exposure described in §ContextProperty.timezone has no writer in application code, so what those five lanes lack is not correct values but any guarantee that the values stay correct. A newly onboarded property lands on Central in five lanes at once, silently, and nothing surfaces it.

Size it as "a gate nobody maintains", never as "we are texting people an hour early in five more places." The second phrasing is the one that would get the follow-up prioritised, and it is false — the same false claim §Context retracts, and the one this document has to avoid restating in the act of scoping around it. If you are reading §5 to decide whether to pick the follow-up up: the reason is unowned correctness on a federal-rule boundary, not an observed mis-send.

6. What the area code does and does not prove

An NPA is where a number was issued, not where its owner stands today. A resident who moved to Denver from New York keeps a 646 number. That is a real limitation of this approach and it is documented at the resolver rather than elided.

It is also the standard the statute's own safe-harbour reasoning describes — absent better information, the area code is the caller's evidence of the called party's location — and the alternative in play was not more accurate about the person, only more convenient for us. If a resident ever tells us their actual zone, that record beats the table and the resolver should be handed it directly. No such field exists today.

Consequences

Alternatives considered

Property clock, but actually write the field. Offered, rejected. It would have fixed the Chicago fallback without fixing the standard: the statute measures the called party, and a building's zone is only a proxy for that. It also has a failure mode this design does not — an unwritten or typo'd field silently becomes a guessed zone again, which is the exact defect being removed.

The intersection of both clocks (send only when inside the window on the property's clock AND the resident's). Offered, rejected. Strictly safer on timing and strictly wrong on doctrine: it keeps a writerless field on the critical path, narrows the window for reasons the statute does not ask for, and makes "why did this refuse?" a two-clock question.

Add a phone-metadata dependency instead of a checked-in table. Checked first, per the repo rule: package.json carries no phone library at all — date-fns is installed and has no notion of area codes. A dependency was therefore a genuine option and was rejected for one reason specific to this gate: the ambiguity calls in §3 are judgement, and they have to be auditable line by line by whoever is accountable for the send. A vendored mapper that resolves a straddling NPA to a single zone makes that call silently, with no diff to review. If a dependency is adopted later, its multi-zone results must map to ambiguous, never to zones[0].

Fall back to the property clock when the number cannot be resolved. The tempting middle. Rejected: it reintroduces the guess at exactly the moment we have the least information, and it makes the failure invisible again — the send proceeds and nobody learns the resolver missed. A federal-rule boundary is the wrong place for a fallback that cannot be observed.