Repository navigation
git-history --self-test is red on main every day after 12:00 UTC — a bare --since date is approxidated to the CURRENT time of day, and it fails Lint & Repo Gates for every PR #10807
Description
scripts/pm/git-history.mjs --self-test is red on main right now, and it fails Lint & Repo Gates on every PR in the repo. It is not a code regression — the same commits that CI passed this morning fail this afternoon. The self-test passes before 12:00 UTC each day and fails after it.
Measured
| where | result |
|---|---|
origin/main f4e5d916d6, run at ~12:05Z |
git-history --self-test: 3 FAILED |
01c50322a1, run at ~12:06Z |
3 FAILED — the same commit CI reported green at 10:47:13Z today |
CI agrees, and pins the transition to the top of the hour:
| PR | Lint & Repo Gates |
completed |
|---|---|---|
| #10795 | success | 11:55:03Z |
| #10796 | success | 11:57:14Z |
| #10801 | failure | 12:02:57Z |
| #10802 | failure | 12:02:35Z |
Same code either side of ~12:00Z. Nothing merged in that window touches scripts/pm/.
Root cause: git approxidate resolves a bare date at the CURRENT time of day
The fixture builds 40 commits, one per day, each stamped 12:00:00Z:
const d = new Date(Date.parse('2026-06-01T12:00:00Z') + i * day).toISOString();and the assertion asks for an absolute-looking window:
runCliAllowFail(['count', '--since=2026-06-20', '--until=2026-07-11'], full)
t('and the answer is the real one (21 commits: the daily fixture commits i=19..39)', … === '21')That window is not absolute. git rev-list --since=2026-06-20 has no time component, and git's approxidate fills it with the current wall-clock time, not midnight. So the --since boundary sweeps across the fixture's 12:00Z commits once each day:
- before 12:00 UTC →
--since≈2026-06-20T11:5x→ c19 included → 21 ✅ - after 12:00 UTC →
--since≈2026-06-20T12:0x→ c19 excluded → 20 ❌
Reproduced standalone, outside the gate, at ~12:07Z — a fresh 40-commit fixture with the identical stamps and the identical window:
$ git rev-list --count --first-parent --since=2026-06-20 --until=2026-07-11 main
20
$ git rev-list --first-parent --since=2026-06-20 --until=2026-07-11 --format='%s %cI' main | tail -2
c21 2026-06-22T12:00:00+00:00
c20 2026-06-21T12:00:00+00:00 <- oldest in window
$ git log --format='%s %cI' main | grep '^c19 '
c19 2026-06-20T12:00:00+00:00 <- the dropped boundary commit
The window holds c20..c39. c19 is the one that falls out, and it is exactly the --since boundary.
All three failures share this shape — each is off by one in a counted window:
✗ and the answer is the real one (21 commits: the daily fixture commits i=19..39)
got "20"
✗ and the answer now MATCHES the complete clone
got "20"
✗ a still-shallow clone whose floor predates the window answers WITHOUT fetching
{"stdout":"2
", "stderr":"… since 2026-07-08 until 2026-07-11 · floor 2026-07-06 · tip 2026-07-10 …"}
Why this is worse than an ordinary flake
It does not look like one. A flake that fires at random gets re-run and forgotten; this one is deterministic within a day — red for twelve hours, green for twelve — so the first person to look after noon sees a solid, reproducible failure and goes hunting for the regression that is not there. Every PR opened this afternoon inherits it, and re-queuing cannot clear it.
It is also silent about its own cause: the assertion says "21 commits" and gets 20, which reads as an off-by-one in the tool under test rather than in the question the test asked.
Direction, not a prescription
The fixture's stamps and the window are both the test's own inputs, so this is fixable without touching the tool:
- Give the window a time component —
--since=2026-06-20T00:00:00Z. Smallest change; check whether the CLI's--sinceparsing accepts it (Date.parseat:352does, but the value is also handed to git). - Move the fixture stamps off 12:00 — e.g.
00:00:01Z— so no boundary can sweep them. Weaker: it moves the collision rather than removing it, and a stamp at midnight is one timezone away from the same bug. - Assert a window whose boundaries sit strictly between commit stamps, so no commit is ever within a day of an edge.
YYYY-MM-DD to git. The same approxidate rule applies everywhere, and this one was invisible for ~10 hours after landing.
Provenance
The self-test landed in #10506 (7f8b36086f, 2026-08-21T02:10:05Z), which wired node scripts/pm/git-history.mjs --self-test into lint.yml's Shallow-history guard self-tests step. Filed by the domain:devx PM seat; the diagnosis above is measured, the remedy is not chosen.
Generated by Claude Code
Activity
claude commentedon Aug 21, 2026
Escalation: this is now evicting PRs from the merge queue, not just reddening PR checks
Measured since filing.
Two PRs were dequeued with
CI_FAILUREbygithub-merge-queue[bot]:I pulled the queue run for #10796 (
gh-readonly-queue/main/pr-10796-f4e5d916d6…, run32479614166, job96762967184—Lint & Repo Gates) and the failing line is this defect, verbatim:So the full sequence is: a PR goes green on its own checks before 12:00Z → enters the queue → the queue rebuilds against a base that runs the same self-test after 12:00Z → fails → PR is evicted. Both of those PRs were reviewed, green, and queued before the boundary.
Two others merged just before it bit — #10787 at 12:01:58Z and #10786 at 12:02:25Z — which brackets the onset precisely and is further confirmation the code did not change.
⛔ Do not re-queue anything until this lands
Re-queuing now fails identically and burns a full rebuild for every PR behind it in the batch — the exact cost the merge-queue triage workflow warns about. #10795 and #10796 stay dequeued deliberately; I will re-arm them once this is fixed, and I have said so on both.
What this changes about the fix
Nothing about the diagnosis — the root cause and the reproduction in the body still stand unchanged. What it changes is the cost of getting it wrong: a fix that is merely green when run now would be indistinguishable from the bug's own daily green window, and shipping that would leave the queue evicting PRs again tomorrow afternoon with everyone believing it was already fixed.
That is why the dispatch requires proof on both sides of 12:00 UTC — by faking the clock, or by a structural argument that no boundary can land within a day of a fixture stamp. ⛔ A single green run at any one time of day is not evidence here, and after today it is specifically the evidence the bug itself produces.
Generated by Claude Code
claude commentedon Aug 21, 2026
{ "issue": 10807, "status": "done", "branch": "claude/issue-10807-git-history-selftest-approxidate", "pr": "https://github.com/objectstack-ai/objectstack/pull/10811", "premise_still_valid": true, "summary": "Confirmed the card's measured root cause before changing anything: with a fresh standalone fixture at 12:09:40Z, `git rev-list --count --first-parent --since=2026-06-20 --until=2026-07-11 main` answered 20, and the dropped boundary commit is c19 (2026-06-20T12:00:00+00:00) \u2014 a bare --since is an approxidate that git fills with the CURRENT time of day. Fixed by giving every window edge in the self-test a complete UTC instant (2026-06-20T00:00:00Z etc.) as named constants shared by the CLI invocations, the raw-git baseline and the historyHorizon readings; 00:00:00Z also sits 12h from the nearest stamp, half the fixture's daily cadence and the widest gap available. Added one case that recomputes both properties (complete-instant shape + edge-to-stamp distance) from the constants. No assertion loosened (counts stay exactly 21/3/5) and the tool's own --since handling is untouched \u2014 resolveSince hands the value to git verbatim and Date.parse only validates it, so a complete instant survives both hops.", "tests": "All at commit 33f00f8287. `node scripts/pm/git-history.mjs --self-test` -> EXIT=0, its own line 'git-history --self-test: all cases passed.', 37 cases passed / 0 failed (36 pre-existing + 1 new guard), run at 12:14:33Z INSIDE the previously-red window; pre-fix baseline at 12:11:33Z was EXIT=1, 'git-history --self-test: 3 FAILED.' (33 passed). BOTH-SIDES-OF-NOON PROOF, by faking the clock rather than waiting: stock git reads GIT_TEST_DATE_NOW (epoch seconds) as 'now' for approxidate resolution, and the self-test's git subprocesses inherit it. Pre-fix (origin/main file): now=00:30Z/06:00Z/11:59:59Z -> EXIT=0 all passed; now=12:00:01Z/12:30Z/18:00Z/23:59:59Z -> EXIT=1 '3 FAILED'. This branch: EXIT=0 'all cases passed' at all seven faked times. The pre-fix column flipping at exactly 12:00Z is what makes the fixed column meaningful \u2014 it proves the faked clock really reaches the git processes under test, so the all-day green is a measurement and not a no-op. ABLATION: reverted via `git checkout origin/main -- scripts/pm/git-history.mjs`, mutation confirmed ON DISK BY ANCHOR COUNT (bare \"--since=2026-06-20', '--until\" back to 4 occurrences, WINDOW_SINCE to 0) rather than by an editor's exit code; failure returned at the red time of day as tabulated; restored from the branch and proved byte-identical (blob 4520ae27c0ff49163525b0f49616e46209a39207 before and after, `git status --porcelain` empty). No build/dist step is involved \u2014 the script is pure node builtins, so no rebuild leg applies. GATES (exit codes captured with `cmd > file 2>&1; ec=$?`, never through a pipe): `node scripts/pm/dispatch-gates.mjs` with no paths -> EXIT=0, derived 1 changed path from the merge base, matched check:cross-package-test-inputs; check-entry-guard selftest EXIT=0 (47 cases) + gate EXIT=0 ('every entry guard goes through invoked-as.mjs'); check-parse-guard selftest EXIT=0 + gate EXIT=0; check-cross-package-test-inputs selftest EXIT=0 (90 cases) + gate EXIT=0; check-nul-bytes selftest EXIT=0 (75 assertions) + gate EXIT=0 ('scanned 6252 text file(s) ... no raw ASCII control bytes'). entry-guard and parse-guard were run by hand because dispatch-gates is blind to them \u2014 it prints check:parse-guard as 'unreachable by construction' since its declared population 'scripts' is refused as too generic for want of a path separator.", "open_questions": [], "out_of_scope_findings": [ "none filed \u2014 swept the repo for other bare YYYY-MM-DD dates handed to git and found no second instance, and nothing else is currently red. scripts/collect-release-notes.sh uses an IDENTICAL 40-commit 12:00Z fixture but already spells its window 2026-06-20T00:00:00Z/2026-07-11T00:00:00Z (ran it at 12:12:31Z: exit 0, all cases passed) \u2014 an in-repo precedent for the remedy chosen here. scripts/check-engine-split-ratio.mjs uses relative '<N> days ago' windows with the fixture re-dated to end today and >=1 day of slack at both edges, so no edge sits knife-edge on a stamp (ran it at 12:12:45Z: exit 0, all cases passed). check-governed-merges.mjs and check-engine-split-ratio.mjs's production paths build --since with new Date(...).toISOString(), already a complete instant.", "NOT filed, reported here as an observation for PM triage: both of those self-tests run in the SAME lint.yml step, AFTER git-history.mjs, as one `run:` block under `bash -e` \u2014 so the first failure aborts the step and neither has actually executed in CI since 12:00Z today. They are green, merely masked. Whether that step should tolerate-and-collect rather than abort is a separate call, not folded in here." ] }Generated by Claude Code
git-history --self-testis RED on main — 3 cases fail, and it is the first step ofLint & Repo Gates, so every PR reds on it #10804 as a duplicate of this issuefix(ci): collect every self-test verdict in a step, instead of maskin…
fix(ci): collect every self-test verdict in a step, instead of maskin…