Scenario: naive-timestamps
Proofs: docs/scenarios/proofs/naive-timestamps/ (8 scripts, 386 checks, real node:http + TZ-fixed child processes)
No disclosure content — correctness and observability findings. Nothing here crosses a principal:
the cache key still folds the principal, so the collision below is between two calls of the same
caller. Every date coercion measured is Zod's z.coerce.date() (3.25.76), which is
new Date(input); the library contributes the outbound pipeline.
First, what works
- The library has no date code, deliberately, and it shows. On
origin/main: new Date( 0,
getTimezoneOffset 0, Intl. 0, timezone/timeZone 0. There is nothing to
misconfigure.
.report().raw is the one observer that holds the truth — the exact wire string, identical on
every host zone, where the parsed value is not.
z.string().datetime({ offset: true }) refuses every naive form in one method call, on every
host alike. It is the cheapest correct guard measured in the whole scenario.
output: z.string() already gives you the exact wire bytes, one seam earlier than
wire.response: 'text' — so the scenario's escape hatch costs less than the analogous one for
numeric precision.
1. cache.ts:45 folds the host's timezone into the key — both directions are wrong
cache.ts:45 is if (value instanceof Date) return JSON.stringify(value.toISOString());. A Date
derived from a naive wall clock therefore carries the host's zone into the key.
Measured directly (keys for { method: 'GET', url, body: { since } }):
| literal wall clock |
UTC |
America/New_York |
Asia/Tokyo |
Europe/Berlin |
2026-03-08T02:30:00 |
56ed935d… |
8c34a197… |
31408c8f… |
c9a4fe1f… |
2026-03-08T03:30:00 |
eebe22b0… |
8c34a197… (same) |
474eb546… |
56ed935d… (= UTC's 02:30) |
Two consequences, both measured end to end over a store all three processes share:
- Collision. On
America/New_York the distinct readings 02:30 and 03:30 derive the
identical key, because 02:30 does not exist and resolves to the same instant as 03:30. The
second call was answered from the first's entry — a question nobody asked, answered with
another question's body, and the store did not grow.
- Zero sharing. Three hosts asking the identical logical question produced 3 keys, 3 vendor
requests, 3 entries, 0 hits. With the same body as an uncoerced string: 1 entry, 1 request,
3 hosts.
Ask: canonicalise a Date in the cache key to something that cannot silently alias — or at
minimum note in the caching guide that a Date in the key is host-zone-dependent. The fix and the
bug differ only by whether a schema said z.coerce.date().
2. The default output reports nothing when validation changes the data
validateOutput (engine.ts:460-474) computes findings only on the drift branch:
if ((out as { __kind?: unknown }).__kind === 'drift')
return { value: validated, findings: classifyDiff(raw, validated, …) };
return { value: validated, findings: [] };
So output: Event where Event contains a z.coerce.date() returns the coerced value with 0
findings — a silent transformation of the data by the contract that was supposed to describe it.
You only hear about it if you wrote drift().
Ask: this is defensible (drift is opt-in), and it is worth a line in the validation guide:
a plain output schema may change the value, silently, and only drift() reports it.
3. drift()'s coerced detail is kinds-only, and here that is the whole information
The finding is { level: 'warn', path: 'created_at', change: 'coerced', detail: 'string -> object' }
(drift.ts:61, :67, :78-79) — byte-identical on UTC and America/New_York while the underlying
data differs by five hours, and the same grade as a "4200" → 4200 re-typing in the same run.
verdict.flag/verdict.accept cannot see it at all. Across nine schema arms × two host zones the
entire vocabulary is: string -> object, Expected date, received string, Invalid datetime,
string -> number. Zero occurrences of zone, offset, UTC, local, DST or TZ.
This is the second scenario to file the same shape (a coerced detail that cannot grade its own
coercion); it is recorded here because a date is the case where the kinds are least informative —
string -> object is true of every possible answer.
Ask: include the old and new values in a coerced finding, or their rendered forms. The
diff already has both (diff.ts:91 carries oldValue and value).
4. util.ts:549 sends a value the vendor never mentioned
out.push([key, value.toISOString()]); — always UTC, always Z. Read off the server's own request
line, the vendor that sent 2026-03-08T02:30:00 was asked back about:
| host |
request the vendor received |
| UTC |
GET /search?since=2026-03-08T02%3A30%3A00.000Z |
| America/New_York |
GET /search?since=2026-03-08T07%3A30%3A00.000Z |
| Asia/Tokyo |
GET /search?since=2026-03-07T17%3A30%3A00.000Z |
The Tokyo row is the wrong day. The JSON body path reaches the same place through
Date.prototype.toJSON.
Not a bug — toISOString() is the only correct serialisation of a Date — but the pairing of §1's
silent coercion with this line is what turns "we read a wall clock" into "we queried a different
instant", with no diagnostic anywhere between them.
5. A declared input does not stop it
Of six outbound spellings, four reached the vendor carrying a host-dependent UTC instant —
including a fully declared input: { query: z.object({ since: z.date() }) }, which validates a
Date and then hands it to util.ts:549. The only enforcement that worked was a Zod string
constraint that refuses the Date type outright (invalid query: Expected string, received date).
Ask: a note in the URL-shaping guide that a Date in query is serialised as UTC, since the
type system actively encourages putting one there.
Reproduction
npx tsx docs/scenarios/proofs/naive-timestamps/c6-cache-key.ts
Eight scripts run offline. Host timezone is varied by spawning fresh node children with TZ set
before boot — each child's zone is proven by getTimezoneOffset() at two fixed instants plus an
Intl-resolved name before any claim is scored. Every timestamp is a literal; nothing depends on
the wall clock at run time.
Source references (verified against origin/main)
cache.ts:45 — if (value instanceof Date) return JSON.stringify(value.toISOString());
util.ts:549 — out.push([key, value.toISOString()]);
engine.ts:460-474 — validateOutput; :468 if ((out as { __kind?: unknown }).__kind === 'drift')
drift.ts:61 — change: 'coerced', · :67 — coerced: 'warn', · :78-79 — return `${kindOf(d.oldValue)} -> ${kindOf(d.value)}`;
diff.ts:91 — out.push({ op: 'change', path, oldValue: before, value: after });
types.ts:1540-1549 — export interface Clock with now, sleep, setTimer, clearTimer
resilience.ts:72 — const when = Date.parse(raw); // HTTP-date (the only Date.parse in core)
- Zero-counts across
packages/core/src/*.ts: new Date( 0, getTimezoneOffset 0, Intl. 0, timezone/timeZone 0
Found by a scenario pass that researches a real-world API integration problem, captures it, and proves or refutes each claim with runnable offline code. Every source reference above was read on origin/main.
Scenario: naive-timestamps
Proofs:
docs/scenarios/proofs/naive-timestamps/(8 scripts, 386 checks, realnode:http+TZ-fixed child processes)First, what works
origin/main:new Date(0,getTimezoneOffset0,Intl.0,timezone/timeZone0. There is nothing tomisconfigure.
.report().rawis the one observer that holds the truth — the exact wire string, identical onevery host zone, where the parsed value is not.
z.string().datetime({ offset: true })refuses every naive form in one method call, on everyhost alike. It is the cheapest correct guard measured in the whole scenario.
output: z.string()already gives you the exact wire bytes, one seam earlier thanwire.response: 'text'— so the scenario's escape hatch costs less than the analogous one fornumeric precision.
1.
cache.ts:45folds the host's timezone into the key — both directions are wrongcache.ts:45isif (value instanceof Date) return JSON.stringify(value.toISOString());. ADatederived from a naive wall clock therefore carries the host's zone into the key.
Measured directly (keys for
{ method: 'GET', url, body: { since } }):2026-03-08T02:30:0056ed935d…8c34a197…31408c8f…c9a4fe1f…2026-03-08T03:30:00eebe22b0…8c34a197…(same)474eb546…56ed935d…(= UTC's 02:30)Two consequences, both measured end to end over a store all three processes share:
America/New_Yorkthe distinct readings02:30and03:30derive theidentical key, because 02:30 does not exist and resolves to the same instant as 03:30. The
second call was answered from the first's entry — a question nobody asked, answered with
another question's body, and the store did not grow.
requests, 3 entries, 0 hits. With the same body as an uncoerced string: 1 entry, 1 request,
3 hosts.
Ask: canonicalise a
Datein the cache key to something that cannot silently alias — or atminimum note in the caching guide that a
Datein the key is host-zone-dependent. The fix and thebug differ only by whether a schema said
z.coerce.date().2. The default
outputreports nothing when validation changes the datavalidateOutput(engine.ts:460-474) computes findings only on the drift branch:So
output: EventwhereEventcontains az.coerce.date()returns the coerced value with 0findings — a silent transformation of the data by the contract that was supposed to describe it.
You only hear about it if you wrote
drift().Ask: this is defensible (drift is opt-in), and it is worth a line in the validation guide:
a plain
outputschema may change the value, silently, and onlydrift()reports it.3.
drift()'scoerceddetail is kinds-only, and here that is the whole informationThe finding is
{ level: 'warn', path: 'created_at', change: 'coerced', detail: 'string -> object' }(
drift.ts:61,:67,:78-79) — byte-identical on UTC and America/New_York while the underlyingdata differs by five hours, and the same grade as a
"4200" → 4200re-typing in the same run.verdict.flag/verdict.acceptcannot see it at all. Across nine schema arms × two host zones theentire vocabulary is:
string -> object,Expected date, received string,Invalid datetime,string -> number. Zero occurrences of zone, offset, UTC, local, DST or TZ.This is the second scenario to file the same shape (a
coerceddetail that cannot grade its owncoercion); it is recorded here because a date is the case where the kinds are least informative —
string -> objectis true of every possible answer.Ask: include the old and new values in a
coercedfinding, or their rendered forms. Thediff already has both (
diff.ts:91carriesoldValueandvalue).4.
util.ts:549sends a value the vendor never mentionedout.push([key, value.toISOString()]);— always UTC, alwaysZ. Read off the server's own requestline, the vendor that sent
2026-03-08T02:30:00was asked back about:GET /search?since=2026-03-08T02%3A30%3A00.000ZGET /search?since=2026-03-08T07%3A30%3A00.000ZGET /search?since=2026-03-07T17%3A30%3A00.000ZThe Tokyo row is the wrong day. The JSON body path reaches the same place through
Date.prototype.toJSON.Not a bug —
toISOString()is the only correct serialisation of aDate— but the pairing of §1'ssilent coercion with this line is what turns "we read a wall clock" into "we queried a different
instant", with no diagnostic anywhere between them.
5. A declared
inputdoes not stop itOf six outbound spellings, four reached the vendor carrying a host-dependent UTC instant —
including a fully declared
input: { query: z.object({ since: z.date() }) }, which validates aDateand then hands it toutil.ts:549. The only enforcement that worked was a Zod stringconstraint that refuses the
Datetype outright (invalid query: Expected string, received date).Ask: a note in the URL-shaping guide that a
Dateinqueryis serialised as UTC, since thetype system actively encourages putting one there.
Reproduction
Eight scripts run offline. Host timezone is varied by spawning fresh
nodechildren withTZsetbefore boot — each child's zone is proven by
getTimezoneOffset()at two fixed instants plus anIntl-resolved name before any claim is scored. Every timestamp is a literal; nothing depends onthe wall clock at run time.
Source references (verified against
origin/main)cache.ts:45—if (value instanceof Date) return JSON.stringify(value.toISOString());util.ts:549—out.push([key, value.toISOString()]);engine.ts:460-474—validateOutput;:468if ((out as { __kind?: unknown }).__kind === 'drift')drift.ts:61—change: 'coerced',·:67—coerced: 'warn',·:78-79—return `${kindOf(d.oldValue)} -> ${kindOf(d.value)}`;diff.ts:91—out.push({ op: 'change', path, oldValue: before, value: after });types.ts:1540-1549—export interface Clockwithnow,sleep,setTimer,clearTimerresilience.ts:72—const when = Date.parse(raw); // HTTP-date(the onlyDate.parsein core)packages/core/src/*.ts:new Date(0,getTimezoneOffset0,Intl.0,timezone/timeZone0Found by a scenario pass that researches a real-world API integration problem, captures it, and proves or refutes each claim with runnable offline code. Every source reference above was read on
origin/main.