Skip to content

Commit a58d60a

Browse files
committed
test(driver-sql): derive §3's span ceiling from the conclusion it guards (#13691)
§3's `span` bound is a validity precondition, not a claim about the product: it establishes that second-precision truncation could not have produced the distinctness §3 concludes from. Its correct value is whatever that CONCLUSION needs — and #11572 changed the conclusion without moving it. #11224's §3 asked for two distinct stamps, and two truncated values need one whole second, so `span < 1000` was the tight bound for it. #11572 (d2cacbc) strengthened the conclusion to `ROUNDS` distinct stamps — which cannot span less than `ROUNDS - 1` whole seconds — and wrote that arithmetic into MIN_GAP_MS's own note in the same commit, while leaving the executable bound on the superseded number. The precondition has been over-strict by a factor of `ROUNDS - 1` ever since, and #11572's fix spends real wall-clock inside that same budget by design. MAX_SPAN_MS is now derived from ROUNDS and the truncation grain, the failure message reports how much of the span was deliberate spacing and how much was everything else, and the derivation is pinned by server-free arithmetic so the ceiling and the conclusion cannot drift apart in silence again. Nothing #11572 established is traded away: awaitClockAdvance still drives the clock, both gap assertions still run, and the conclusion is still `ROUNDS` distinct stamps. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3jdziLbAPGeceVNmSox5L
1 parent 9c4c431 commit a58d60a

1 file changed

Lines changed: 155 additions & 5 deletions

File tree

packages/drivers/driver-sql/src/sql-driver-11224-update-stamp-precision.test.ts

Lines changed: 155 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,41 @@
104104
* the measured span and the arithmetic that rules a fast runner out, so the red
105105
* discriminates itself instead of requiring a reader to check §1 and §2 first.
106106
*
107+
* ## [#13691] §3's validity ceiling had to move with §3's conclusion
108+
*
109+
* The `span` bound sitting above §3's distinctness assertion is a VALIDITY
110+
* PRECONDITION, not a claim about the product: it establishes that truncation
111+
* could not have produced the distinctness §3 then concludes from. Its correct
112+
* value is therefore whatever the CONCLUSION needs — and #11572 changed the
113+
* conclusion.
114+
*
115+
* #11224's §3 asked for two distinct stamps out of {@link ROUNDS}, and two
116+
* truncated values need one whole second, so `span < 1000` was the TIGHT bound
117+
* for it. #11572 strengthened the conclusion to `ROUNDS` distinct stamps —
118+
* `ROUNDS` multiples of a second cannot span less than `ROUNDS - 1` of them —
119+
* and wrote exactly that arithmetic into {@link MIN_GAP_MS}'s note in the same
120+
* commit ("the 5 whole SECONDS second-precision stamps would need to yield
121+
* `ROUNDS` distinct values"), while leaving the executable bound on the
122+
* superseded number. From then on this file's prose and its code disagreed by
123+
* a factor of `ROUNDS - 1`.
124+
*
125+
* The cost of that is one-sided, which is what makes it worth a section rather
126+
* than a comment. An over-strict validity precondition cannot fail in a way
127+
* that says anything about the product; it can only red on time this test did
128+
* not have, on somebody else's PR. It did: a merge-queue build on a diff
129+
* touching no file under `packages/drivers/` reported `span = 1006` — six
130+
* milliseconds over — while the real invariant (monotonicity, asserted on the
131+
* line above) HELD, and the same commit passed on a re-queue with nothing
132+
* changed. Meanwhile #11572's fix spends real wall-clock time inside that same
133+
* budget by design, so the two guards had come to bound §3 from both sides.
134+
*
135+
* {@link MAX_SPAN_MS} is now DERIVED from `ROUNDS` and the truncation grain
136+
* instead of written as a literal, and the derivation is pinned by arithmetic
137+
* at the foot of this file, so the two halves cannot drift apart in silence
138+
* again. ⛔ Nothing #11572 established was traded for it: the clock is still
139+
* DRIVEN, both gap assertions still run, and the conclusion is still `ROUNDS`
140+
* distinct stamps.
141+
*
107142
* ## Reverse verification (direction predicted before running)
108143
*
109144
* Restoring `main`'s `updatedAtStamp()` body turns §1, §2 and §3 red on the
@@ -155,9 +190,52 @@ const ROUNDS = 6;
155190
* SECONDS second-precision stamps would need to yield `ROUNDS` distinct
156191
* values — which is what makes §3's distinctness unreachable by truncation
157192
* instead of merely unlikely under it.
193+
*
194+
* ⚠️ [#13691] `ROUNDS - 1` gaps of this size is the FLOOR of what the spacing
195+
* costs, never an estimate of it. {@link awaitClockAdvance} waits with
196+
* `setTimeout`, whose delivery is bounded by the event loop rather than by the
197+
* clock, so the spacing is `>= 25 ms` and grows with load with no upper bound
198+
* in the code. Measured on the container #13691 was fixed on, over 300 trials
199+
* of the five gaps §3 takes: 26 ms median (64 ms max) on an idle loop, and
200+
* 60 ms median (86 ms max) with the loop held by a re-scheduling 12 ms hog.
201+
* That is why {@link MAX_SPAN_MS} is derived from the truncation arithmetic and
202+
* not from this floor — a budget sized on `ROUNDS * MIN_GAP_MS` would be a
203+
* budget sized on a number the machine is free to exceed.
158204
*/
159205
const MIN_GAP_MS = 5;
160206

207+
/**
208+
* [#13691] The grain a SECOND-precision stamp truncates to — the defect class
209+
* §3 exists to catch, on every cell it runs on.
210+
*
211+
* One grain covers the whole matrix because the defect has one shape wherever
212+
* it can occur: on MySQL an unqualified `CURRENT_TIMESTAMP` truncates to whole
213+
* seconds (#11224 itself), and the SQLite ablation this file's head note
214+
* prescribes is that same truncation written into the branch that cell
215+
* executes (`new Date().toISOString()` forced to `.000Z`). Postgres stamps at
216+
* microsecond precision and has nothing to truncate, so the bound is vacuous
217+
* there rather than wrong.
218+
*/
219+
const TRUNCATED_STAMP_GRAIN_MS = 1_000;
220+
221+
/**
222+
* [#13691] §3's validity ceiling: the span at or above which second-precision
223+
* truncation could ITSELF have produced the distinctness §3 concludes from.
224+
*
225+
* Derived, not chosen. {@link ROUNDS} truncated stamps are `ROUNDS` multiples
226+
* of {@link TRUNCATED_STAMP_GRAIN_MS}, so they cannot span less than
227+
* `ROUNDS - 1` whole grains; below that, truncation cannot reach `ROUNDS`
228+
* distinct values at all, which is precisely what makes §3's distinctness
229+
* assertion readable as a statement about resolution.
230+
*
231+
* ⛔ This is not a raised ceiling. The literal it replaces was the TIGHT bound
232+
* for the assertion §3 used to make — see the `[#13691]` section of the head
233+
* note for the drift, and `maxDistinctUnderTruncation` at the foot of this file
234+
* for the pin that keeps this constant and §3's conclusion from parting company
235+
* again.
236+
*/
237+
const MAX_SPAN_MS = (ROUNDS - 1) * TRUNCATED_STAMP_GRAIN_MS;
238+
161239
/**
162240
* [#11572] Block until the process clock has ADVANCED by at least `ms`, and
163241
* return the advance actually observed.
@@ -382,13 +460,27 @@ function measure(cell: DialectCell): void {
382460
).toBeGreaterThanOrEqual(MIN_GAP_MS);
383461

384462
// Monotone regardless (the invariant), and — the point — the run spans
385-
// less than the full second a truncated stamp would need to distinguish
386-
// any two of these at all.
463+
// less than the `ROUNDS - 1` whole grains a truncated stamp would need to
464+
// reach the `ROUNDS` distinct values concluded below. See
465+
// {@link MAX_SPAN_MS} for why that is `ROUNDS - 1` grains and not one.
387466
for (let i = 1; i < stamps.length; i++) expect(stamps[i]).toBeGreaterThanOrEqual(stamps[i - 1]);
388467
const span = stamps[stamps.length - 1] - stamps[0];
389-
expect(span, 'this run took over a second, so second-precision stamps could have differed too').toBeLessThan(
390-
1_000,
391-
);
468+
// [#13691] Reported in the failure, because the first question a reader
469+
// has is which half of the budget went where — and neither half is a
470+
// product signal.
471+
const deliberate = gaps.reduce((total, gap) => total + gap, 0);
472+
expect(
473+
span,
474+
`[#13691] §3's VALIDITY PRECONDITION failed, which says nothing about the product: the ` +
475+
`run spanned ${span} ms, at or above the ${MAX_SPAN_MS} ms (${ROUNDS} - 1 grains of ` +
476+
`${TRUNCATED_STAMP_GRAIN_MS} ms) a second-precision stamp would need to reach the ` +
477+
`${ROUNDS} distinct values asserted below — so truncation is no longer ruled out and ` +
478+
`the distinctness below is unreadable, whichever way it lands. Of that span, ` +
479+
`${deliberate} ms was clock-advance spacing this cell spent ON PURPOSE ` +
480+
`(${gaps.join('/')} ms) and ${span - deliberate} ms was everything else (round-trips ` +
481+
`and scheduling). The ordering invariant asserted on the line above is unaffected ` +
482+
`either way — read this as a slow runner, not as a regression.`,
483+
).toBeLessThan(MAX_SPAN_MS);
392484

393485
// The property, now entailed rather than raced: with every update spaced
394486
// by a measured gap five times the declared resolution, a stamp that kept
@@ -479,3 +571,61 @@ function measure(cell: DialectCell): void {
479571
for (const cell of DIALECT_CELLS) {
480572
declareDialectCell(cell, 'update stamp precision (#11224)', measure);
481573
}
574+
575+
/**
576+
* [#13691] The most distinct values SECOND-precision truncation can show across
577+
* a span of `ms` — the model {@link MAX_SPAN_MS} is derived from.
578+
*
579+
* Truncated stamps are multiples of {@link TRUNCATED_STAMP_GRAIN_MS}, so a span
580+
* of `ms` covers at most `floor(ms / grain) + 1` of them.
581+
*/
582+
function maxDistinctUnderTruncation(ms: number): number {
583+
return Math.floor(ms / TRUNCATED_STAMP_GRAIN_MS) + 1;
584+
}
585+
586+
// ── The coupling #11572 broke, asserted instead of commented ─────────────────
587+
//
588+
// §3's ceiling and §3's conclusion are two halves of one argument, and they
589+
// drifted apart in silence: #11572 strengthened the conclusion from "at least
590+
// two distinct" to "`ROUNDS` distinct" and left the ceiling on the number that
591+
// had been tight for the OLD conclusion. Nothing failed at the time — the
592+
// precondition merely became `ROUNDS - 1` times stricter than its own argument
593+
// needed, and stayed that way until a slow runner paid for it on an unrelated
594+
// PR.
595+
//
596+
// So the relationship is pinned. It runs on every runner with no server and no
597+
// clock: this is arithmetic about the defect class, not a measurement of the
598+
// machine, and it is the assertion that reds if the ceiling is ever put back on
599+
// a literal belonging to a superseded conclusion.
600+
describe("#13691 — §3's span ceiling is the tight bound for §3's conclusion", () => {
601+
it('rules truncation out below the ceiling, and is not stricter than that above it', () => {
602+
// SOUND — one millisecond under the ceiling, truncation cannot reach the
603+
// `ROUNDS` distinct values §3 concludes from. This is the whole reason the
604+
// precondition exists, and it is the leg a LOOSENED ceiling reds.
605+
expect(
606+
maxDistinctUnderTruncation(MAX_SPAN_MS - 1),
607+
`a span of ${MAX_SPAN_MS - 1} ms must leave second-precision truncation unable to reach ` +
608+
`${ROUNDS} distinct stamps, or §3's distinctness assertion concludes nothing`,
609+
).toBeLessThan(ROUNDS);
610+
611+
// TIGHT — at the ceiling truncation CAN reach them, so the ceiling is not
612+
// stricter than the argument requires. This is the leg a ceiling restored
613+
// to `1_000` reds, and over-strictness here is not conservatism: a validity
614+
// precondition cannot fail informatively, so every millisecond of slack it
615+
// demands is a false red carrying no product signal.
616+
expect(
617+
maxDistinctUnderTruncation(MAX_SPAN_MS),
618+
`the ceiling is ${MAX_SPAN_MS} ms, but second-precision truncation still cannot reach ` +
619+
`${ROUNDS} distinct stamps at that span — so the precondition is stricter than §3's own ` +
620+
`argument needs, and the excess can only ever red on time the run did not have`,
621+
).toBeGreaterThanOrEqual(ROUNDS);
622+
});
623+
624+
it("is the number #11224's superseded conclusion needed, one grain for two stamps", () => {
625+
// Why `1_000` was right until #11572 and wrong after it, kept executable so
626+
// the head note's history is checkable rather than remembered: the original
627+
// §3 asked for TWO distinct stamps, and two need exactly one grain.
628+
expect(maxDistinctUnderTruncation(TRUNCATED_STAMP_GRAIN_MS - 1)).toBe(1);
629+
expect(maxDistinctUnderTruncation(TRUNCATED_STAMP_GRAIN_MS)).toBe(2);
630+
});
631+
});

0 commit comments

Comments
 (0)