ADR-005: One denotation function, and what DST does to it
AI SLOP an AI agent wrote this page. yuri4n, a senior engineer, gave the direction and did the review. The review is human, thus errors can stay.
Status
Accepted, 2026-08-04, decided by the project owner.
Context
Two operations answer the same question: "what does this point mean on the real timeline?"
Span.member?/2asks it for one instant. It is symbolic: no horizon, no enumeration.Span.ground/3asks it for a whole horizon. It enumerates into the linear kernel.
If the two are two implementations, their answers can drift. This was a real defect class, caught in review: "the 31st" in February. Under :clamp the point fires on Feb 28 (see ADR-003). One implementation clamped; the other did not. The same instant got two different answers from one span.
The force behind the drift is duplication. Clamping, ordinal skipping, and closings are subtle rules. Two copies of a subtle rule always separate over time.
Options
Option A: two independent implementations. Each query optimizes its own path. Caveat: this is the drift that already happened. Nothing forces the copies to agree.
Option B: define member? as a call to ground over a one-instant horizon. Correct by construction. Caveat: it turns a symbolic question into an enumeration. nth grounding widens to whole cycle instances first, so even a one-instant probe can enumerate a full month. The cost model becomes wrong for the common query.
Option C: a shared kernel. One clamp-aware denotation function holds all the rules. member?/2 and ground/3 are thin interpreters over it. The pattern is a single source of truth: two query interfaces, one shared kernel. This extends the interpreter pattern from ADR-002: the span tree is the free algebra, and both interpreters now share their semantics.
Decision
Option C is the chosen one. There is one denotation function. member?/2 and ground/3 both call it. Clamping, ordinal skipping, and closings behave identically in both.
graph LR P["Point"] -- "Span.of/1" --> S["Span"] S -- "member?/2" --> D["one denotation<br/>function"] S -- "ground/3 (tz)" --> D D -- "intervals on<br/>the timeline" --> K["Zocam.Intervals<br/>(linear kernel)"]
Figure 1 — Both queries pass through one denotation function into the linear kernel. AI generated, human reviewed.
A property test pins the law, so the sharing cannot silently break:
member?(span, t)is true exactly whentfalls inside some interval ofground(span, horizon, tz), for everytin the horizon.
The precise DST rules:
- Grounding happens only in
Span.ground/3.Pointstays timezone-free and pure. The calendar meets the real timeline in one place. - Fall-back: the wall clock repeats an hour, so the preimage of a wall window is discontiguous. One instance grounds to one or more kernel intervals. The pieces are never joined: in
America/New_Yorkon 2026-11-01, wall00:30..01:30grounds to04:30Z..05:30Zand06:00Z..06:30Z. - Spring-forward: the wall time does not exist. The instance is skipped; it grounds to nothing. Wall
02:30on 2026-03-08 yields an empty result.
The 2026 fixtures in test/zocam/span_test.exs pin these real dates. All calendar facts in them are machine-verified.
Consequences
Easy now:
- You add a new denotation feature (a policy, an ordinal rule, a closing) once. Both queries then agree by construction.
- The "31st in February" defect class needs review at one site, not two.
- The law is a cheap regression check: any probe instant can test it.
Hard now:
member?/2cannot take a shortcut that the shared function does not offer. Every optimization must keep the law.- The two queries answer at different levels:
member?/2reads the wall clock of the givenDateTime, andground/3maps wall time to the timeline. The law still holds through DST. A gap instant has noDateTimein that zone, and both fall-back instants map to wall times inside the window.
Open items: the law holds in the current test suite; a generated property test is still a good next step. stream/3 keeps using the same denotation today, because it grounds chunk by chunk with ground/3. A later change must keep that path.
ADR-004: Fortnights as every-k scopes
Accepted, 2026-08-04, decided by the project owner.
ADR-006: Zocam is a standalone, released library
Accepted, 2026-08-04, decided by the project owner ("separate it as a library"). Revised 2026-08-05: the chosen shape is now a separate repository that publishes to Hex.