Skip to content

fix(scanner): Sort importers so identical scans give identical answers - #178

Merged
JordanCoin merged 6 commits into
mainfrom
claude/codemap-deterministic-edges
Sep 4, 2026
Merged

fix(scanner): Sort importers so identical scans give identical answers#178
JordanCoin merged 6 commits into
mainfrom
claude/codemap-deterministic-edges

Conversation

@JordanCoin

@JordanCoin JordanCoin commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Prerequisite for #172's "exact importer list" exit criterion, and for the golden test being written elsewhere. Relates to #153.

What was wrong

Importers are appended while iterating analyses, and nothing fixes that order. Twelve consecutive runs of the same binary over the same unmodified fixture, on main:

app/sidebar.tsx,app/header.tsx,app/layout.tsx,app/footer.tsx,app/page.tsx
app/page.tsx,app/header.tsx,app/sidebar.tsx,app/footer.tsx,app/layout.tsx
app/page.tsx,app/header.tsx,app/layout.tsx,app/footer.tsx,app/sidebar.tsx
… twelve runs, twelve orderings

So --importers output shifted between identical runs, a diff of codemap output showed changes that weren't changes, and no caller could assert an exact list. After: 12/12 identical.

Scope: importers only, deliberately

The first version of this sorted Imports too, and that broke TestCueImportFiltersPackageSelector — which turned out to be encoding a real contract, not an incidental order. The CUE resolver returns a selected package before the package it falls back to, and that sequence is meaningful.

So I measured which half was actually unstable, rather than sorting both and adjusting the test to match:

Imports   of app/user.ts, 10 runs   ->  10/10 identical
Importers of lib/a.ts,    10 runs   ->  6 distinct orderings

Imports is appended per file in resolution order, which is already stable, and DepsProject sorts its own copy for JSON output regardless — so --deps --json is unchanged by this PR. Only the reverse map needed fixing, and only it is touched.

Tests

testdata/deterministic-edges/ — four files importing one shared module, named so that alphabetical order differs from every plausible discovery order. TestFileGraphEdgeOrderIsDeterministic builds the graph eight times and requires identical output, then requires the result be sorted rather than merely stable, since a caller asserting an exact list needs to know which order it gets.

Against unsorted code it fails with:

run 1 importers = map[lib/shared.ts:[app/charlie.ts app/bravo.ts app/alpha.ts app/delta.ts]],
want the same order as run 0 map[lib/shared.ts:[app/charlie.ts app/alpha.ts app/delta.ts app/bravo.ts]]

TestSortEdgesHandlesNilGraph covers the nil receiver.

Also on this branch

  • Cache invalidation. Sorting the builder does not reach a repo whose watch daemon already wrote a state.json: ValidateCachedGraph accepts any cache whose builder revision matches, and the revision had not changed. graphBuilderRevision is bumped to filegraph-v2 so pre-sort state files fail provenance and are rebuilt.
  • HubFiles() ordering. It ranged over the Importers map and returned the slice unsorted; cmd/hooks.go and watch/publication.go pass it through and the hook renderer truncates at maxHubs, so which hubs a hook printed still varied. Now ordered by non-test importer count descending, then path.
  • Ported test for 09d3f4c. That commit ported @reneleonhardt's cargo-metadata deadline fix from perf: Bound repository analysis hot paths #171 without its test; TestCargoMetadataDeadlinePreservesFallbackTopology is now ported from perf: Bound repository analysis hot paths #171 as well and no-ops once that PR merges.

Release note

--importers and the file graph now return importers in sorted order. Identical scans of an unchanged repository produce identical output; previously the order varied between runs. Import lists and --deps --json are unaffected.

Verification

go vet ./... clean, gofmt clean, go test ./... at the main baseline (only the three known root-environment permission failures). Determinism confirmed end-to-end with a built binary at 12/12 identical on the fixture that previously gave twelve orderings.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PEUvjGsJemDFSV8nbxvxBo


Generated by Claude Code

Importers are appended while iterating analyses, whose order the scanner
does not fix, so scanning the same repository twice produced the same
importers in a different sequence. Twelve consecutive runs of one binary
over one unmodified fixture produced twelve different orderings.

That made --importers output shift between identical runs, made diffs of
codemap output show changes that were not changes, and left no caller able
to assert an exact importer list.

Imports are deliberately not sorted. They are appended per file in
resolution order, which is already stable across runs (verified: 10/10
identical) and which callers rely on — the CUE resolver returns a selected
package before the package it falls back to, and DepsProject sorts its own
copy for JSON output regardless.

Relates to #153, #172

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PEUvjGsJemDFSV8nbxvxBo
Copilot AI lite review requested due to automatic review settings September 4, 2026 14:20

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Ported from @reneleonhardt's open PR #171 so this PR can go green rather
than waiting on that one to merge; it no-ops once main carries it.

buildRustWorkspaceIndex shadowed its caller's ctx with the cargo-metadata
deadline, so an expired deadline failed the whole graph build instead of
falling back to the manually derived Rust workspace.

Relates to #153, #172

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PEUvjGsJemDFSV8nbxvxBo
JordanCoin and others added 4 commits September 4, 2026 10:43
The sort landed in the graph builder, but a state.json written before it
still validates: ValidateCachedGraph only checks the builder revision, and
that revision did not change. A repo that had a watch daemon running keeps
serving map-ordered edge lists from cache until something else invalidates
it, so the fix does not reach existing checkouts.

Bump graphBuilderRevision to filegraph-v2 so any state file carrying
filegraph-v1 fails provenance and is rebuilt.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TcyheQmM3HCvxF5wRL3s5t
HubFiles ranged over the Importers map and returned the result unsorted.
cmd/hooks.go and watch/publication.go pass that slice straight through, and
the hook renderer truncates it at maxHubs, so which hubs a hook printed
varied run to run over an unchanged graph.

Order by non-test importer count descending, then by path, so the truncated
head is the most-imported files rather than whichever ones the map yielded
first.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TcyheQmM3HCvxF5wRL3s5t
09d3f4c ported reneleonhardt's fix for buildRustWorkspaceIndex shadowing
its caller's ctx with the cargo-metadata deadline, but not the test that
proves it. Port TestCargoMetadataDeadlinePreservesFallbackTopology from
PR #171 so the fallback topology stays covered on this branch too; it
no-ops once #171 merges.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TcyheQmM3HCvxF5wRL3s5t
@JordanCoin
JordanCoin merged commit 3d9675a into main Sep 4, 2026
12 checks passed
@JordanCoin
JordanCoin deleted the claude/codemap-deterministic-edges branch September 4, 2026 14:55
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.

3 participants