Skip to content

fix: detect edge index changes by ref object ID - #1

Open
mparrett wants to merge 1 commit into
nnunley:mainfrom
mparrett:fix/edge-index-timestamp-comparison
Open

fix: detect edge index changes by ref object ID#1
mparrett wants to merge 1 commit into
nnunley:mainfrom
mparrett:fix/edge-index-timestamp-comparison

Conversation

@mparrett

@mparrett mparrett commented Aug 4, 2026

Copy link
Copy Markdown

Why

ensure_edge_index_current refreshes the derived dependency graph at refs/notes/dep-graph before deps, ready, and topo run. It decided which issues to re-parse by comparing each note ref's commit date against a wall-clock marker stored in the index, which misses changes two ways.

The comparison was lexical between incompatible formats. %(committerdate:iso-strict) renders the offset recorded in the commit, so a ref written at 2026-08-02T22:26:21-07:00 sorts below a marker of 2026-08-03T05:26:20Z despite being a second newer.

More fundamentally, a commit date does not say when a ref changed locally. A note arriving by fetch or import keeps the date it was written with, which can predate the marker, so its dependency headers are skipped permanently. hooks/post-merge fetches issue notes on every pull, so this is the ordinary sync path rather than a corner case.

The symptom is the same either way. Dependency headers are the documented source of truth, and the graph silently disagrees with them. Anything editing a header outside dep add/dep rmgit issue update --blocks=, git issue import, or a fetched note — becomes invisible to the graph:

$ git issue dep rebuild
$ git issue update <a> --relates-to=<b>
$ git issue show <a> | grep '^relates_to'
relates_to: <b>
$ git issue deps
No dependency edges found.

This is easy to miss, because the index self-heals while it has no cursor. The guard short-circuits on an empty marker, so the first graph query after add_edge rebuilds from headers and installs one. Every query after that is affected. dep rebuild repairs the index but reinstalls the cursor, so it is a one-shot.

What

Snapshot issue ref object IDs in a refs blob beside the existing edges blob, and re-parse the refs whose ID moved. An object ID changes whenever a ref does, however it moved and whatever date it carries. Refs that have disappeared have their edges dropped.

The snapshot lives in a sibling blob rather than in the edges text, so existing readers of the index are unaffected. An index with no snapshot reads as "everything changed" and rebuilds in full, so an existing index heals on first read and there is no migration step.

Verification

Two regression tests in tests/test_deps.sh, both failing before this change and passing after:

  • an out-of-band header edit, with TZ pinned west of UTC so the note records a negative offset and the lexical comparison fails on any host
  • an edit backdated with GIT_COMMITTER_DATE, covering the fetched-note case, which fails on a date cursor in every timezone

Both were run against the parent commit under America/Los_Angeles, UTC, Europe/Berlin, and Asia/Tokyo to confirm they fail everywhere rather than only in one offset. tests/test_deps.sh goes from 55 to 59 assertions; the other suites are unchanged.

tests/test_github_integration.sh fails both before and after this change, unrelated to it.

ensure_edge_index_current decided which issues to re-parse by comparing each
note ref's commit date against a wall-clock marker. That misses changes two
ways.

The comparison was lexical between incompatible formats.
%(committerdate:iso-strict) renders the offset recorded in the commit, so
2026-08-02T22:26:21-07:00 sorts below 2026-08-03T05:26:20Z despite being a
second newer.

More fundamentally, a commit date does not say when a ref changed locally. A
note arriving by fetch or import keeps the date it was written with, which can
predate the marker, so its dependency headers are skipped permanently. The
post-merge hook fetches issue notes on every pull, so this is the ordinary sync
path rather than a corner case.

Snapshot issue ref object IDs in a sibling blob of the edge index and re-parse
the refs whose ID moved. Object IDs change whenever a ref does, however it
moved and whatever date it carries. Refs that disappeared have their edges
dropped. An index with no snapshot reads as "everything changed" and rebuilds
in full, so an older index heals on first read with no migration step.

Two regression tests cover it: an out-of-band header edit with TZ pinned west
of UTC, and an edit backdated with GIT_COMMITTER_DATE. Both fail against the
previous implementation in every timezone tested.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mparrett
mparrett force-pushed the fix/edge-index-timestamp-comparison branch from b353dba to 5e015cf Compare August 4, 2026 04:50
@mparrett

Copy link
Copy Markdown
Author

@nnunley Thanks again for sharing your project. I have a few bug fixes if you're interested. This is the first.

@nnunley

nnunley commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Awesome. Glad you're trying it out and finding gaps.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants