test(driver-sql): derive §3's span ceiling from the conclusion it guards, not from a superseded one - #13959
Merged
zhuangjianguo merged 1 commit intoAug 31, 2026
Conversation
…rds (#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
Contributor
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
zhuangjianguo
marked this pull request as ready for review
August 31, 2026 19:46
zhuangjianguo
enabled auto-merge
August 31, 2026 19:46
zhuangjianguo
deleted the
claude/issue-13691-stamp-precision-span-budget
branch
August 31, 2026 20:08
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #13691
§3's
spanbound is a validity precondition, not a claim about the product. It establishes that second-precision truncation could not have produced the distinctness §3 then concludes from — so its correct value is whatever that conclusion needs. #11572 changed the conclusion and did not move it.The finding that decided the route
Traced by
git log -Lon the assertion itself, not inferred:64505a5122(#11224, PR #11320)new Set(stamps).size > 1— two distinct stampsspan < 1000d2cacbcf51(#11572, PR #12112).toBe(ROUNDS)— six distinct stampsTwo truncated values need one whole second, so
1_000was the tight bound for the original conclusion.ROUNDStruncated stamps areROUNDSmultiples of a second and cannot span less thanROUNDS - 1of them, so the bound for the current conclusion is5_000.⭐
d2cacbcf51wrote that arithmetic into the file in the same commit —MIN_GAP_MS's note has read "against the 5 whole SECONDS second-precision stamps would need to yieldROUNDSdistinct values" since 2026-08-25 — while leaving the executable bound on the superseded number. The file's prose and its code have disagreed by a factor ofROUNDS - 1for six days.So this is not "raise the ceiling for headroom". The ceiling was stale, over-strict by exactly
ROUNDS - 1against the assertion it guards, and over-strictness in a validity precondition is not conservatism: such an assertion cannot fail in a way that says anything about the product, so every millisecond of excess is only ever a false red on somebody else's PR. That is how it surfaced, atspan = 1006— six milliseconds over — with monotonicity, the real invariant on the line above, holding.Zone-2 verdicts
main?ROUNDS = 6,MIN_GAP_MS = 5, walltoBeLessThan(1_000). Line numbers had moved; the card's:141/:159no longer point at them.A2.2 — the decomposition
30 trials per leg of §3's exact loop, sqlite cell, this container (shared box — treat absolutes as ratios):
(min / p50 / max.) On the sqlite cell the deliberate spacing is 90–98 % of the whole span and the round-trips are noise.
OS_TEST_POSTGRES_URL/OS_TEST_MYSQL_URL, no listener on 3306/5432, no docker daemon; both cells reported as named SKIPs. The failure being fixed was on live MySQL, so component (b), live round-trip latency, is NOT MEASURED and I am not claiming a number for it. What can be said without it: the deliberate term measures 25–86 ms across every condition I could produce, so for it to account for a 1006 ms span each of the five gaps would have had to overshoot its 5 ms request by roughly 195 ms.A2.3 — measured, and it does not go the way the card assumed
The helper does not spin on the clock:
It waits with
setTimeout, so its cost is bounded by event-loop delivery, not by clock granularity.Date.now()granularity on this host is 1 ms (200 tight reads returned one value), so the granularity story does not apply here at all.The conclusion the card drew from that hypothesis nevertheless holds, by the other mechanism: the spacing is bounded below by
ROUNDS - 1gaps and not bounded above by anything in the code. 300 trials of the five gaps §3 takes:ROUNDS * MIN_GAP_MSis 30. Under a modest synthetic load the real spacing is already 2× that, with no ceiling in the code. So a budget sized onROUNDS * MIN_GAP_MSwould be a budget sized on a number the machine is free to exceed — recorded inMIN_GAP_MS's note so the next author does not re-derive it.⭐ But the STOP condition attached to A2.3 does not fire, because the answer does not narrow the routes: the gaps are individually measured and retained in
gaps[], so the deliberate total is knowable exactly at runtime whatever it costs. Unboundedness would only have killed a route that had to assume the spacing. None of the three does.A2.4 — one grain fits the matrix; the multiplier did not
The guarded defect class is second truncation on every cell that can have it: MySQL's unqualified
CURRENT_TIMESTAMP(#11224 itself), and, for the SQLite cell, the truncation this file's own head note prescribes as its ablation (new Date().toISOString()forced to.000Z). Postgres stamps at microsecond precision and has nothing to truncate, so the bound is vacuous there rather than wrong. One wall does fit all three — the error was never the grain, it was multiplying it by 1 instead of byROUNDS - 1.Route chosen, and why the advisory route was overruled
Chosen: derive the ceiling from the conclusion it guards.
MAX_SPAN_MS = (ROUNDS - 1) * TRUNCATED_STAMP_GRAIN_MS.Both guards are preserved, and one of them is preserved by construction rather than by care:
sql-driver-11224-update-stamp-precision§3 is a probabilistic guard that reds with CORRECT code on a fast runner — and its failure is byte-identical to the defect it guards against #11572's guarantee is untouched.awaitClockAdvancestill drives the clock, both gap assertions (count, then floor) still run, and the conclusion is stillROUNDSdistinct stamps. Not one line of [finding]sql-driver-11224-update-stamp-precision§3 is a probabilistic guard that reds with CORRECT code on a fast runner — and its failure is byte-identical to the defect it guards against #11572's mechanism is in this diff.updated_atstamp truncates to whole seconds against aDATETIME(3)column — a row updated in its first second readsupdated_atEARLIER thancreated_at#11224's guarantee is untouched. The ceiling is now the exact point at which truncation could reachROUNDSdistinct values. Below it, truncation is ruled out — which is the entire service the precondition performs. Nothing that a truncated stamp could do now passes.ROUNDS, so the coupling that broke cannot break the same way twice, and the pin below reds if it is decoupled again.⛔ Overruling the card's ⭐ route (subtract the deliberate spacing from the span), on two grounds.
awaitClockAdvanceor into round-trips. Subtracting the deliberate half makes the precondition measure "was this test slow for reasons other than its own spacing", which is not what makes the following assertion readable — so it would weaken the guard in precisely the case where the guard matters.span = 1006into 920–981 against an unchanged 1000 ms wall: a 19–80 ms margin on a budget whose dominant term is load-driven and unbounded. It buys one build.The other two named moves are also declined: raising the ceiling to a chosen number would leave the same disconnect in place one constant further out, and retrying the section would add nondeterminism to a cell whose whole point since #11572 is that it is deterministic.
⭐ The decomposition in A2.2 is reported because it was asked for and because it is now in the failure message, but note that it did not decide the route — the route follows from the arithmetic relating the ceiling to the conclusion, which is true whichever term dominates the budget. That is the property that makes it a fix rather than a headroom adjustment.
What else the diff does
maxDistinctUnderTruncation()models the defect class, and two assertions hold the ceiling to being both sound (one ms under it, truncation cannot reachROUNDSdistinct) and tight (at it, truncation can). This is arithmetic, not a measurement of the machine, so it runs on every runner including ones with no live dialects — which matters, because nothing that executes on a fast runner discriminated the six-day drift, as the ablation below shows.Ablation
Implementation committed first; both legs mutate the committed constant, prove the mutation on disk before reading anything, and restore under
trap … EXIT INT TERMwith absolute paths.perl -0pipattern put\*inside a\Q…\Eblock, where the backslash is itself literal, so nothing matched. The on-disk proof caught it —injected=0, original anchor still present, blob unchanged from HEAD — and the leg was discarded rather than reported. Re-run with an explicit mutator that refuses on an anchor count other than 1.1_00012515b0028…vs HEAD97d7c78d29…expected 2 to be greater than or equal to 6. §1–§6 all stay green.ROUNDS * grainb69e3e0e9b…vs HEADexpected 6 to be less than 6.Restore proven after each leg and at exit:
git hash-objectback to97d7c78d29f38f87032a6f689c31f7ca83b19d2f(equal to the HEAD blob) andgit diff HEADempty.⭐ Read the TIGHT leg's second half. With the ceiling wrong by a factor of five, every live section stays green on this runner. That is exactly why the drift survived six days and surfaced only as somebody else's queue eviction — and it is the argument for the pin being arithmetic rather than another timed assertion.
Clause ② — self-declared from the actual diff
packages/spec/src/**packages/drivers/driver-sql/src/sql-driver-11224-update-stamp-precision.test.ts.git diff --statagainst the merge base names no second path.*.test.ts, not part of any package entry point;check:published-filesgreen. Nothing in the diff touchesupdatedAtStamp(),stampUpsertUpdatedAt(), emitted SQL, or any schema — §4 and §5, which pin the emitted expression per dialect, are unmodified. The new symbols are file-localconsts and one file-local function; no export was added.check:api-surfacehas no path population here and the diff adds no.d.tsinput.Both limbs read NO, so per the repo's 不预挂 rule (maintainer, 2026-08-28)
needs:contract-reviewis not attached — an open carrier must always mean genuinely pending review.Verification
Run on the final head
a58d60af.Green — measured:
pnpm --filter @objectstack/driver-sql exec vitest run src/…—Tests 9 passed | 2 skipped (11). §1–§6 green on the sqlite cell; both new pin assertions green.pnpm --filter @objectstack/driver-sql exec tsc --noEmit— exit 0. Confirmed with--listFilesthat the edited file is in the tsc program (this package excludes no tests), so the green covers the edit rather than talking past it.check:cross-package-test-inputs,check:engine-double-contract,check:where-matcher,check:query-options-erasure,check:type-check-coverage,check:driver-conformance,check:test-source-alias. Derived withnode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(no hand-built path list), harvested with--commands, each exit code captured before any pipe.pnpm lint— the full repo-wideeslint . --no-inline-config, exit 0 in 59 s. Not narrowed; no narrowing declaration needed.pnpm check:nul-bytes— OK over 7652 files, plus a targeted control-byte scan of the edited file.node scripts/check-test-completeness.mjs— exit 3, its own text: "Nothing was measured: this gate exited before parsing a single summary line."pnpm check:dual-build-cjs-loads— exit 3,PREREQUISITE NOT MET, 78 packages withoutdist/.pnpm check:type-check-debt— exit 1, but this is a throw, not a finding: "--re-measure cannot run: 50 workspace dependencies of the ledgered packages have no built type entry point on disk … measuring now would not fail, it would silently measure a DIFFERENT WORLD." Its--self-test(47 + 65 + 43 + 28 + 19 cases) and the structural halfcheck:type-check-coverageboth passed.driver-sqlcarries no DEBT or TEST_DEBT ledger entry and its owntypecheckis green above, so this ratchet has nothing this diff can move — but that reasoning is not a measurement and it is not recorded as one.All three need a built full-workspace closure; each is a repo-wide sweep CI performs on every PR regardless. Declared rather than run, per the local-verification scope.
Temporal Conformance (live PG + MySQL)is the job that actually exercises §3 against a server.Not run, deliberately: the repo configures no Prettier (no config file, no
prettierkey, no workflow step). Invoking it picks up upstream defaults that disagree with the repo's eslint-enforced style and rewrites the whole file; that was tried, caught by inspection, and reverted byte-identically before commit.Out of scope
it()timeout — a different mechanism, missing budgets rather than a stale validity precondition). This file'sit()blocks are still untimed and that stays driver-sql: 37 more live-DDL it() blocks across 9 files inherit vitest's 5000ms default (same shape as #13688) #13902's to decide; adding a budget here would pre-empt it.sql-driver-11176-bulk-and-merge-updated-at.test.ts:321carries a superficially similartoBeLessThan(1_000), but it is a different shape: a created-to-updated delta on a single row with no forced spacing, so the stale-multiplier defect does not apply to it. Inspected, not changed, nothing filed.Landing
Draft, per dispatch. ⛔ Not flipped ready, no auto-merge armed — enqueue on this repo is irreversible and landing is the PM's.
Session:
https://claude.ai/code/session_01F3jdziLbAPGeceVNmSox5LGenerated by Claude Code
Generated by Claude Code