Repository navigation
fix(pm): git-history hands git the instant its coverage proof reads, so a bare date no longer shrinks the window with the hour - #21657
Merged
objectstack-fleet[bot] merged 3 commits intoOct 4, 2026
Conversation
A bare YYYY-MM-DD given to --since was passed to git verbatim, and git's approxidate fills the missing time of day from the current wall clock, while the coverage proof read the same string with Date.parse as that day's 00:00:00Z. The tool counted a narrower window than it proved, the answer shrank as the day went on, and the receipt named the wider window. windowInstant() now normalises every --since (and every placeable --until) to the complete UTC instant Date.parse puts it at; that instant is what git is handed and what the receipt and the refusal print. A complete instant comes back unchanged; an unplaceable --until still reaches git as given. Claude-Session: https://claude.ai/code/session_01CB6W87z22K2yjUCDyVrJRk Co-authored-by: Claude <noreply@anthropic.com>
…cted clocks A new battery, floored at 16 cases, runs every bare-date pin at 06:00Z and at 18:00Z through git's own GIT_TEST_DATE_NOW, behind baselines proving the injected clock reaches raw git's approxidate: the bare --since and --until windows give the explicit-instant answer at both hours, the receipt and the refusal name the instant, and the complete-instant cases above are untouched. The roster floor moves from 4 to 5 batteries. Claude-Session: https://claude.ai/code/session_01CB6W87z22K2yjUCDyVrJRk Co-authored-by: Claude <noreply@anthropic.com>
The 2026-08-14 window count went through a bare date, which git-history then passed to git verbatim and git read at the time of day of the run. Re-taken with the fixed tool the window holds 5,455 commits, not 5,364; the old tool reproduces 5,364 exactly with git's clock at 23:00Z. Both citing lines carry the note; the counts below the first stay as measured over the 5,364. Claude-Session: https://claude.ai/code/session_01CB6W87z22K2yjUCDyVrJRk Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Oct 4, 2026
objectstack-fleet
Bot
deleted the
claude/issue-21601-git-history-bare-date-since
branch
October 4, 2026 02:15
This was referenced Oct 4, 2026
This was referenced Oct 4, 2026
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 #21601
Clause-②: no
What was wrong
scripts/pm/git-history.mjsproved a window with one reading of--sinceand counted it with another:--sincewithDate.parse. ECMA-262 reads a date-onlyYYYY-MM-DDas that day's00:00:00Z.windowArgs()passed the same string to git verbatim. git's approxidate fills a missing time of day from the current wall clock.sincecut to its first ten characters, so it named the wide window while git counted the narrow one.So
--since=2026-09-30answered fewer commits the later in the day it ran, at exit 0. The receipt was identical on every run. The bare date is the spelling the usage text recommends.Reproduced on
a7ab047cf6with the tool as it stood at1968d5e8. git's clock was injected throughGIT_TEST_DATE_NOW, the clock git's approxidate reads:count --since=2026-09-30 --ref=a7ab047cf6count --since=2026-09-30T00:00:00Z --ref=a7ab047cf6Injecting the two clock times the card recorded gives its two numbers exactly. That confirms the mechanism instead of inferring it.
The fix (the triage ruling: normalise, do not refuse)
windowInstant(raw)returns the complete UTC instant thatDate.parseplacesrawat. It returns null whenDate.parsecannot placeraw.YYYY-MM-DDT00:00:00Z.Zwith whole seconds comes back byte-identical.Z, and milliseconds survive. That is the shape--daysalready produced.resolveSince()returns that instant instead of the raw string. The proof, git and the receipt now read one value. An unplaceable--sinceis still refused as usage, exactly as before.--untilgets the same normalisation through a newresolveUntil(). A bare2026-10-01now means "before that day's 00:00:00Z" at every hour.--until. So a spellingDate.parsecannot place, such asnow, still reaches git as given, and the receipt prints it as given.since 2026-09-30T00:00:00Z) instead of the first ten characters. The window a receipt names is now the window git counted. This includes--days: before, its receipt printed only the date while git counted from the time of day.One deliberate widening of the suggested route. The dispatch suggested matching only the regex
^\d{4}-\d{2}-\d{2}$. This PR normalises every placeable spelling throughDate.parseinstead. The same mechanism bites other spellings. Measured on a scratch fixture at 00:27Z, where one commit landed at 00:10Z on the day:--since=Date.parseplaces it at2026-09-30Sep 30 20262026/09/3030 Sep 20262026-09-30T00:00:00ZA regex would have fixed one row of five. Handing git the instant the proof read fixes the whole class with the same code size.
scripts/pm/check-governed-merges.mjs's ownparseSince()already hands gitnew Date(Date.parse(arg)).toISOString(), so this is the repo's existing spelling, not a new one.This rests on the bounded in-place-fix exemption: same defect class, same function, same file, the same gate family, and no other claim on the file.
Consumers, one line each
Read at
1968d5e8withgit grep -l git-history -- scripts/, plus the files the dispatch listed.scripts/check-engine-split-ratio.mjs: not affected. It importshistoryHorizon(), which takessinceMsand is untouched, and runs its owngit log --sincewith a completetoISOString()instant.--self-testand--days 90are green on this branch.scripts/check-ratchet-remedy-authority.mjs: not affected. The only reference is a comment naming "a git-history helper" in a roster note. No call.pnpm check:ratchet-remedy-authorityis green.scripts/check-step-collectors.mjs: not affected. A header comment cites the past red self-test. It readslint.ymlstep commands, which this PR does not change.--self-testis green.scripts/collect-release-notes.sh: not affected in any count. It callsensure --no-fetchwith complete instants (%cIof the previous ref;2026-06-20T00:00:00Zin its self-test), andensurecounts nothing.window: sincewith the full instant instead of ten characters. An offset%cIis restated inZ.WITHHELD,shallow floor:andunshallow, none of which moved.--self-testis green.scripts/pm/changeset-deadline-census.mjs: not affected. It callshistoryHorizon()withsinceMstaken from the card'screated_at, with no CLI and no git window.--self-testis green.scripts/pm/check-governed-merges.mjs: not affected. It importshistoryHorizon(), and its ownparseSince()already normalises the way this PR does.--self-testis green.scripts/pm/check-half-states.mjs: not affected. Two comments mirror this file's unknown-option refusal wording, which is unchanged.--self-testis green.scripts/pm/check-harness-current.mjs: not affected. It importsisShallowandtouchIsProvable(thetouchpath) and reads no window.--self-testis green.scripts/pm/check-widening-tells.mjshas comments citing a pastensure --days=30reading. That was a complete instant, so it is unaffected.scripts/pm/dispatch-gates.mjsis frozen and was not edited. It names this tool's self-test command, which is unchanged..github/workflows/lint.ymlruns the samenode scripts/pm/git-history.mjs --self-testline, which now runs 79 cases (63 before).The two past counts, re-taken with the fixed tool
(a) #5930's T2 (the os-dev report's bare-date reading, 410).
The bare form also answers 474 with git's clock injected at 12:45Z and at 15:17Z. Old 410 and 393 become new 474, which equals the explicit-instant answer the design doc already cites at section 2.3's T2 row. The note on #5930 is the seat's to write. This PR does not comment there.
(b)
docs/design/predicate-compilation-convergence.md§2.3 (log --since=2026-08-14 --ref=3711e0b763, 5,364 in the doc). Lines are counted from a file redirect, never a pipe; see Acceptance notes.The explicit
--since=2026-08-14T00:00:00Zgives the same 5,455. The old tool, bare date, reproduces the doc's figure:The doc's run read the window from late on 2026-08-14 and dropped the 91 commits that landed earlier that day. Both citing lines (:154 and :684) now carry a note with the re-taken 5,455. The §2.3 table is left as measured over the 5,364, and the note says so. ⛔ No silent correction.
Reverse verification (fix committed first; every leg through
scripts/ablation-replace.mjs)return instant;replaced byreturn opts.since;inresolveSince()838505batof01295ebreturn instant === null ? opts.until : instant;replaced byreturn opts.until;838505bato1bc863b6The six reds of the since leg:
--sincewindow gives 21 at 06:00Z and 21 at 18:00Z. Ablated, it gave 21 and 20.since 2026-06-20T00:00:00Z.2026/06/20is normalised.logreturns 21 lines.The two reds of the until leg are the bare
--untilcount (20 at both hours; ablated, 20 at 06:00Z and 21 at 18:00Z) and its receipt.Both legs restored with blob equal to HEAD (
838505ba) and an emptygit diff HEAD. After the legs, the tree at HEAD runsgit-history --self-test: all cases passed.with 79 cases.Self-test registration
bare dates: the instant the proof reads is the instant git counts, declared inSELF_TEST_BATTERIESwith a floor of 16.SELF_TEST_BATTERY_FLOORmoves from 4 to 5, and theselfTestReachedVerdicthandshake is untouched.GIT_TEST_DATE_NOW, 06:00Z and 18:00Z, withTZ=UTC, so it is red at every hour of the day.--since, 20 versus 21 for the bare--until, and 20 for2026/06/20. So the pins after them cannot pass because the clock was ignored.real reposare unchanged. One case of the new battery asserts thatwindowInstant()hands each of their edges back byte-identical. The only edit insidereal reposis one comment sentence.Gates (at
558f43fe6, the last code commit)Derived by
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackfrom the merge base, with no paths passed. The derivation gave 34 commands. Against the dispatch's list it addspnpm check:doc-authoringandpnpm --filter @objectstack/lint run check:doc-formula-expressions, both from the doc edit.node scripts/pm/git-history.mjs --self-testgit-history --self-test: all cases passed.(79 cases)node scripts/check-ci-filter-parity.mjsOK: all 20 build input(s) turbo.json declares outside the packages …node scripts/check-closing-keyword-parity.mjscheck-closing-keyword-parity: OK (3 parsers agree on all 9 keywords …)node scripts/check-closing-keyword-parity.mjs --self-test✓ … 40 assertions, 5 mutations of the shipped parsers each driven to red.node scripts/check-comment-mask-corpus.mjs✓ comment-mask corpus sweep …: 8119 files, 0 disagree, 0 unparseablenode scripts/check-declaration-mirrors.mjsOK: 10 hand-written declaration(s) agree with their modules …node scripts/check-declaration-mirrors.mjs --self-testAll 29 self-test cases passed.node scripts/check-engine-split-ratio.mjs --days 90history horizon: shallow clone, oldest visible commit 2026-06-29 (predates the window). The first run exited 2 (shallow refusal, floor 2026-08-07); after the tool's own deepen it answerednode scripts/check-engine-split-ratio.mjs --self-testcheck-engine-split-ratio --self-test: all cases passed.node scripts/check-scripts-symbol-anchors.mjs✅ check-scripts-symbol-anchors: 3757 anchors across 282 scripts resolve …node scripts/check-scripts-symbol-anchors.mjs --self-test✅ … --self-test: every finding class provoked …node scripts/check-self-test-wired.mjs✓ check-self-test-wired: every one of the 232 script(s) CI runs that ship a --self-test has that self-test run by CI …node scripts/check-self-test-wired.mjs --self-testcheck-self-test-wired --self-test: 3 live ledger row(s) verified …node scripts/check-self-test-workflow-commands.mjs✓ … no self-test CI runs prints a line the Actions runner would parse as a workflow command.node scripts/check-self-test-workflow-commands.mjs --self-test… --self-test: both measured parse rules pinned …node scripts/check-whole-set-label-write.mjs✓ check-whole-set-label-write: 0 violations …node scripts/check-whole-set-label-write.mjs --self-test✓ … all cases pass (24 fixture trees + 5 refusals + 1 allowlist hatch)node scripts/pm/bare-root-worklist.mjs --self-testOK self-test: 81 live row(s) … none stale, none missing, none contradicted …pnpm --filter @objectstack/lint run check:doc-formula-expressions✓ check:doc-formula-expressions: 22 record-scoped formula example(s) across 460 files … judged clean. The first run exited 3 (PREREQUISITE NOT MET, formula and lint not built); after the build it prescribes it passedpnpm check:agent-test-spelling✓ check-agent-test-spelling: 0 violations …pnpm check:bash32-floorreal tree: 32 shell file(s) … 0 finding(s)pnpm check:cli-command-ids✓ check-cli-command-ids: 65 module(s) … examinedpnpm check:cross-package-test-inputsOK: 30 package(s) read outside themselves, all declared …pnpm check:doc-authoring✓ doc authoring guard: 407 files clean — no bare metadata literals.pnpm check:driver-memory-censuscheck-driver-memory-census: OK …pnpm check:entry-guard✓ check:entry-guard: 282 scripts/ file(s) — every entry guard goes through invoked-as.mjs …pnpm check:gitlink-declaredcheck-gitlink-declared: OK …pnpm check:nul-bytescheck-nul-bytes: OK (scanned 10023 text file(s) … no raw ASCII control bytes).pnpm check:parse-guard✓ check:parse-guard: 281 scripts/ file(s) — every TypeScript parse goes through ts-parse.mjs.pnpm check:pnpm-filter-targets✓ check:pnpm-filter-targets: 155/209 --filter occurrence(s) … resolve …pnpm check:ratchet-remedy-authorityOK check-ratchet-remedy-authority: 272 scripts swept …pnpm check:refd-timer-probeOK check-refd-timer-probe: 8114 source file(s) swept …pnpm check:watch-hint-literal✓ check-watch-hint-literal: 72 declaration(s) across 4 rostered name(s) …pnpm check:pm-dispatch-gates(detached, waited on by pid)✓ dispatch-gates self-test: 1976 cases pass.andthe battery took 1249.0s on this boxnode scripts/pm/dispatch-gates.mjs --ran ran.listanswered✓ dispatch-gates --ran: 34 derived famil(ies) accounted for — 34 run, 0 NOT-MEASURED (a DERIVED zero …).The consumers' own self-tests were also run, because this script is their dependency:
bash scripts/collect-release-notes.sh --self-testcollect-release-notes --self-test: all cases passed.node scripts/pm/check-harness-current.mjs --self-testcheck-harness-current --self-test: all 26 cases passed.node scripts/pm/changeset-deadline-census.mjs --self-test✓ changeset-deadline-census --self-test: all cases passed across 5 batteries …node scripts/check-step-collectors.mjs --self-test✓ check-step-collectors --self-test: 191 assertions, 6 block(s) driven under a real bash -e.node scripts/pm/check-governed-merges.mjs --self-test✓ check-governed-merges --self-test: 454 assertions …node scripts/pm/check-half-states.mjs --self-test✓ check-half-states self-test: 4912 cases pass. …Lint is a proven narrowing, not a full run:
ESLint.isPathIgnored('scripts/pm/git-history.mjs')is false, from the repo's owneslint.config.mjs.eslint --no-inline-config --format jsonreturned 1 file with 0 errors and 0 warnings.parserOptions.projectorprojectService, and the config's own comment states type-aware linting is never enabled. So this diff cannot move any untouched file's verdict..mdfile is outside eslint's population, which has no markdown config.The full
pnpm lintis left to CI. No package is touched, so there is no package build or test step. No changeset:scripts/pm/**anddocs/design/**publish nothing.Acceptance notes
git-history.mjs logtruncates its answer when stdout is a pipe.node scripts/pm/git-history.mjs log --since=2026-08-14T00:00:00Z --ref=3711e0b763 | wc -lprinted 346 three times in a row withPIPESTATUS[0]0. The same command redirected to a file gives 5,455.process.stdout.write()of the whole answer followed byprocess.exit(). 346 lines is about one 64 KiB pipe buffer.shallow floor: unknown.ensureWindowCovered()'s no-remote return carries noboundaries, although it read them. The refusal itself is correct; only the floor line is uninformative. An observation, not filed.fetch --shallow-since=2026-08-07), and thecheck-engine-split-ratio --days 90gate required a second one (--shallow-since=2026-06-29). Both went throughgit-history.mjs ensure, so neither could shorten history.Generated by Claude Code