Skip to content

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 into
mainfrom
claude/issue-21601-git-history-bare-date-since
Oct 4, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-21601-git-history-bare-date-since

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #21601
Clause-②: no

What was wrong

scripts/pm/git-history.mjs proved a window with one reading of --since and counted it with another:

  • The coverage proof reads --since with Date.parse. ECMA-262 reads a date-only YYYY-MM-DD as that day's 00:00:00Z.
  • windowArgs() passed the same string to git verbatim. git's approxidate fills a missing time of day from the current wall clock.
  • The receipt printed since cut to its first ten characters, so it named the wide window while git counted the narrow one.

So --since=2026-09-30 answered 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 a7ab047cf6 with the tool as it stood at 1968d5e8. git's clock was injected through GIT_TEST_DATE_NOW, the clock git's approxidate reads:

invocation (old tool) git's clock answer
count --since=2026-09-30 --ref=a7ab047cf6 00:30Z 472
same 12:45Z 410, the card's first reading
same 15:17Z 393, the card's second reading
count --since=2026-09-30T00:00:00Z --ref=a7ab047cf6 any 474

Injecting 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)

  • New exported windowInstant(raw) returns the complete UTC instant that Date.parse places raw at. It returns null when Date.parse cannot place raw.
    • A bare date becomes YYYY-MM-DDT00:00:00Z.
    • A complete instant already in Z with whole seconds comes back byte-identical.
    • An offset is restated in Z, and milliseconds survive. That is the shape --days already produced.
  • resolveSince() returns that instant instead of the raw string. The proof, git and the receipt now read one value. An unplaceable --since is still refused as usage, exactly as before.
  • --until gets the same normalisation through a new resolveUntil(). A bare 2026-10-01 now means "before that day's 00:00:00Z" at every hour.
    • No coverage proof reads --until. So a spelling Date.parse cannot place, such as now, still reaches git as given, and the receipt prints it as given.
    • The ruling forbade a new refusal, and none was added.
  • The receipt and the refusal print the full normalised instant (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 through Date.parse instead. 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= raw git Date.parse places it at
2026-09-30 4 (drops 00:10Z) 2026-09-30T00:00:00Z
Sep 30 2026 4 2026-09-30T00:00:00Z
2026/09/30 4 2026-09-30T00:00:00Z
30 Sep 2026 4 2026-09-30T00:00:00Z
2026-09-30T00:00:00Z 5 2026-09-30T00:00:00Z

A 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 own parseSince() already hands git new 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 1968d5e8 with git grep -l git-history -- scripts/, plus the files the dispatch listed.

  • scripts/check-engine-split-ratio.mjs: not affected. It imports historyHorizon(), which takes sinceMs and is untouched, and runs its own git log --since with a complete toISOString() instant. --self-test and --days 90 are 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-authority is green.
  • scripts/check-step-collectors.mjs: not affected. A header comment cites the past red self-test. It reads lint.yml step commands, which this PR does not change. --self-test is green.
  • scripts/collect-release-notes.sh: not affected in any count. It calls ensure --no-fetch with complete instants (%cI of the previous ref; 2026-06-20T00:00:00Z in its self-test), and ensure counts nothing.
    • The refusal block it pastes into release notes now reads window: since with the full instant instead of ten characters. An offset %cI is restated in Z.
    • Its self-test greps WITHHELD, shallow floor: and unshallow, none of which moved. --self-test is green.
  • scripts/pm/changeset-deadline-census.mjs: not affected. It calls historyHorizon() with sinceMs taken from the card's created_at, with no CLI and no git window. --self-test is green.
  • scripts/pm/check-governed-merges.mjs: not affected. It imports historyHorizon(), and its own parseSince() already normalises the way this PR does. --self-test is green.
  • scripts/pm/check-half-states.mjs: not affected. Two comments mirror this file's unknown-option refusal wording, which is unchanged. --self-test is green.
  • scripts/pm/check-harness-current.mjs: not affected. It imports isShallow and touchIsProvable (the touch path) and reads no window. --self-test is green.
  • Also hit by the grep:
    • scripts/pm/check-widening-tells.mjs has comments citing a past ensure --days=30 reading. That was a complete instant, so it is unaffected.
    • scripts/pm/dispatch-gates.mjs is frozen and was not edited. It names this tool's self-test command, which is unchanged.
    • .github/workflows/lint.yml runs the same node scripts/pm/git-history.mjs --self-test line, 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).

$ node scripts/pm/git-history.mjs count --since=2026-09-30 --ref=a7ab047cf6
474
method: git rev-list --count --first-parent a7ab047cf6 since 2026-09-30T00:00:00Z · floor 2026-09-20 · tip 2026-10-03 · floor already predates the window (no fetch)
$ node scripts/pm/git-history.mjs count --since=2026-09-30T00:00:00Z --ref=a7ab047cf6
474
method: git rev-list --count --first-parent a7ab047cf6 since 2026-09-30T00:00:00Z · floor 2026-09-20 · tip 2026-10-03 · floor already predates the window (no fetch)

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.

$ node scripts/pm/git-history.mjs log --since=2026-08-14 --ref=3711e0b763        # shallow container, before any deepen
exit 2, stdout empty
⛔ git-history REFUSES to answer — history is truncated inside the window and '3711e0b763' names no remote to deepen from (remotes here: origin).
   ref: 3711e0b763   window: since 2026-08-14T00:00:00Z
$ node scripts/pm/git-history.mjs ensure --since=2026-08-14 --ref=origin/main     # the tool's own additive deepen
✓ history covers the window — method: git rev-list --count --first-parent origin/main since 2026-08-14T00:00:00Z · floor 2026-08-07 · tip 2026-10-04 · fetch --shallow-since=2026-08-07T00:00:00.000Z origin main
$ node scripts/pm/git-history.mjs log --since=2026-08-14 --ref=3711e0b763 > out; wc -l out
5455
method: git log --first-parent 3711e0b763 since 2026-08-14T00:00:00Z · floor 2026-08-07 · tip 2026-09-29 · floor already predates the window (no fetch)

The explicit --since=2026-08-14T00:00:00Z gives the same 5,455. The old tool, bare date, reproduces the doc's figure:

git's clock answer
00:30Z 5452
12:00Z 5412
22:10Z 5365
23:00Z 5364
23:50Z 5363

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)

leg anchor, then replacement on-disk proof self-test
since return instant; replaced by return opts.since; in resolveSince() anchor 1 to 0, blob 838505ba to f01295eb 6 FAILED
until return instant === null ? opts.until : instant; replaced by return opts.until; anchor 1 to 0, blob 838505ba to 1bc863b6 2 FAILED

The six reds of the since leg:

  1. The bare --since window gives 21 at 06:00Z and 21 at 18:00Z. Ablated, it gave 21 and 20.
  2. The receipt names since 2026-06-20T00:00:00Z.
  3. 2026/06/20 is normalised.
  4. The card's shallow shape answers 3. Ablated, it gave 2.
  5. log returns 21 lines.
  6. The refusal names the instant.

The two reds of the until leg are the bare --until count (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 empty git diff HEAD. After the legs, the tree at HEAD runs git-history --self-test: all cases passed. with 79 cases.

Self-test registration

  • New battery bare dates: the instant the proof reads is the instant git counts, declared in SELF_TEST_BATTERIES with a floor of 16.
  • The roster floor SELF_TEST_BATTERY_FLOOR moves from 4 to 5, and the selfTestReachedVerdict handshake is untouched.
  • The battery runs every pin at two clocks injected through GIT_TEST_DATE_NOW, 06:00Z and 18:00Z, with TZ=UTC, so it is red at every hour of the day.
  • Its three BASELINE cases first prove that the injected clock reaches raw git's approxidate: 21 versus 20 for the bare --since, 20 versus 21 for the bare --until, and 20 for 2026/06/20. So the pins after them cannot pass because the clock was ignored.
  • The complete-instant cases of real repos are unchanged. One case of the new battery asserts that windowInstant() hands each of their edges back byte-identical. The only edit inside real repos is one comment sentence.

Gates (at 558f43fe6, the last code commit)

Derived by node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack from the merge base, with no paths passed. The derivation gave 34 commands. Against the dispatch's list it adds pnpm check:doc-authoring and pnpm --filter @objectstack/lint run check:doc-formula-expressions, both from the doc edit.

command exit the gate's own verdict line (abridged)
node scripts/pm/git-history.mjs --self-test 0 git-history --self-test: all cases passed. (79 cases)
node scripts/check-ci-filter-parity.mjs 0 OK: all 20 build input(s) turbo.json declares outside the packages …
node scripts/check-closing-keyword-parity.mjs 0 check-closing-keyword-parity: OK (3 parsers agree on all 9 keywords …)
node scripts/check-closing-keyword-parity.mjs --self-test 0 ✓ … 40 assertions, 5 mutations of the shipped parsers each driven to red.
node scripts/check-comment-mask-corpus.mjs 0 ✓ comment-mask corpus sweep …: 8119 files, 0 disagree, 0 unparseable
node scripts/check-declaration-mirrors.mjs 0 OK: 10 hand-written declaration(s) agree with their modules …
node scripts/check-declaration-mirrors.mjs --self-test 0 All 29 self-test cases passed.
node scripts/check-engine-split-ratio.mjs --days 90 0 ratio 98.4%, history 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 answered
node scripts/check-engine-split-ratio.mjs --self-test 0 check-engine-split-ratio --self-test: all cases passed.
node scripts/check-scripts-symbol-anchors.mjs 0 ✅ check-scripts-symbol-anchors: 3757 anchors across 282 scripts resolve …
node scripts/check-scripts-symbol-anchors.mjs --self-test 0 ✅ … --self-test: every finding class provoked …
node scripts/check-self-test-wired.mjs 0 ✓ 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-test 0 check-self-test-wired --self-test: 3 live ledger row(s) verified …
node scripts/check-self-test-workflow-commands.mjs 0 ✓ … 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 0 … --self-test: both measured parse rules pinned …
node scripts/check-whole-set-label-write.mjs 0 ✓ check-whole-set-label-write: 0 violations …
node scripts/check-whole-set-label-write.mjs --self-test 0 ✓ … all cases pass (24 fixture trees + 5 refusals + 1 allowlist hatch)
node scripts/pm/bare-root-worklist.mjs --self-test 0 OK self-test: 81 live row(s) … none stale, none missing, none contradicted …
pnpm --filter @objectstack/lint run check:doc-formula-expressions 0 ✓ 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 passed
pnpm check:agent-test-spelling 0 ✓ check-agent-test-spelling: 0 violations …
pnpm check:bash32-floor 0 real tree: 32 shell file(s) … 0 finding(s)
pnpm check:cli-command-ids 0 ✓ check-cli-command-ids: 65 module(s) … examined
pnpm check:cross-package-test-inputs 0 OK: 30 package(s) read outside themselves, all declared …
pnpm check:doc-authoring 0 ✓ doc authoring guard: 407 files clean — no bare metadata literals.
pnpm check:driver-memory-census 0 check-driver-memory-census: OK …
pnpm check:entry-guard 0 ✓ check:entry-guard: 282 scripts/ file(s) — every entry guard goes through invoked-as.mjs …
pnpm check:gitlink-declared 0 check-gitlink-declared: OK …
pnpm check:nul-bytes 0 check-nul-bytes: OK (scanned 10023 text file(s) … no raw ASCII control bytes).
pnpm check:parse-guard 0 ✓ check:parse-guard: 281 scripts/ file(s) — every TypeScript parse goes through ts-parse.mjs.
pnpm check:pnpm-filter-targets 0 ✓ check:pnpm-filter-targets: 155/209 --filter occurrence(s) … resolve …
pnpm check:ratchet-remedy-authority 0 OK check-ratchet-remedy-authority: 272 scripts swept …
pnpm check:refd-timer-probe 0 OK check-refd-timer-probe: 8114 source file(s) swept …
pnpm check:watch-hint-literal 0 ✓ check-watch-hint-literal: 72 declaration(s) across 4 rostered name(s) …
pnpm check:pm-dispatch-gates (detached, waited on by pid) 0 ✓ dispatch-gates self-test: 1976 cases pass. and the battery took 1249.0s on this box

node scripts/pm/dispatch-gates.mjs --ran ran.list answered ✓ 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:

command exit verdict line
bash scripts/collect-release-notes.sh --self-test 0 collect-release-notes --self-test: all cases passed.
node scripts/pm/check-harness-current.mjs --self-test 0 check-harness-current --self-test: all 26 cases passed.
node scripts/pm/changeset-deadline-census.mjs --self-test 0 ✓ changeset-deadline-census --self-test: all cases passed across 5 batteries …
node scripts/check-step-collectors.mjs --self-test 0 ✓ 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 0 ✓ check-governed-merges --self-test: 454 assertions …
node scripts/pm/check-half-states.mjs --self-test 0 ✓ check-half-states self-test: 4912 cases pass. …

Lint is a proven narrowing, not a full run:

  • Population: ESLint.isPathIgnored('scripts/pm/git-history.mjs') is false, from the repo's own eslint.config.mjs.
  • File count: eslint --no-inline-config --format json returned 1 file with 0 errors and 0 warnings.
  • Invariance: the calculated config has no parserOptions.project or projectService, and the config's own comment states type-aware linting is never enabled. So this diff cannot move any untouched file's verdict.
  • The .md file is outside eslint's population, which has no markdown config.

The full pnpm lint is left to CI. No package is touched, so there is no package build or test step. No changeset: scripts/pm/** and docs/design/** publish nothing.

Acceptance notes

  • Reported for filing (class a): git-history.mjs log truncates its answer when stdout is a pipe.
    • Evidence: node scripts/pm/git-history.mjs log --since=2026-08-14T00:00:00Z --ref=3711e0b763 | wc -l printed 346 three times in a row with PIPESTATUS[0] 0. The same command redirected to a file gives 5,455.
    • Likely mechanism: process.stdout.write() of the whole answer followed by process.exit(). 346 lines is about one 64 KiB pipe buffer.
    • The earlier injected-clock readings that came out as 715 and 346 were this, not the window. The counts above come from a file.
    • Not fixed here: a different mechanism from this card, outside the ruling. It is listed in the os-dev report for the seat to file.
  • The no-remote refusal prints shallow floor: unknown. ensureWindowCovered()'s no-remote return carries no boundaries, although it read them. The refusal itself is correct; only the floor line is uninformative. An observation, not filed.
  • Re-taking (b) required the tool's own additive deepen of the shared clone (fetch --shallow-since=2026-08-07), and the check-engine-split-ratio --days 90 gate required a second one (--shallow-since=2026-06-29). Both went through git-history.mjs ensure, so neither could shorten history.

Generated by Claude Code

claude added 3 commits October 4, 2026 00:32
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>
@github-actions github-actions Bot added the size/m label Oct 4, 2026
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 4, 2026
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Oct 4, 2026
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 4, 2026 01:42
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 4, 2026 01:42
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 4, 2026
Merged via the queue into main with commit 55e6f14 Oct 4, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21601-git-history-bare-date-since branch October 4, 2026 02:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants