Skip to content

fix(cli): write mops.lock with sorted keys - #741

Merged
Kamirus merged 1 commit into
v3from
kamil-claude/v3-sorted-lockfile
Aug 13, 2026
Merged

fix(cli): write mops.lock with sorted keys#741
Kamirus merged 1 commit into
v3from
kamil-claude/v3-sorted-lockfile

Conversation

@Kamirus

@Kamirus Kamirus commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

mops.lock was serialized with a plain JSON.stringify, so deps, hashes, graph and github came out in insertion/traversal order. This repo's own lockfile is ~69 KB with 621 per-file hash entries, so an install unrelated to a given package could still reorder its block — diff churn and merge conflicts for no reason.

Every key is now sorted: the sections, the package keys inside hashes, the per-file hash keys inside each package, and graph's inner per-version edge maps.

Two details that decide whether this actually works

Sorting happens at the single write site, not where records are computed. computeLockFile carries an existing package's hash record over verbatim when its version has not changed — that is a deliberate optimisation, since published versions are immutable. A sort applied at computation time would therefore preserve whatever order a carried-over record already had, and the file would never converge. There is a test for exactly this: it scrambles an existing lock's file-key order, makes the lock legitimately stale so it gets rewritten, and asserts the carried-over record comes out sorted.

Plain code-unit comparison, never localeCompare. A locale-sensitive sort orders differently depending on the machine's locale, which would make the lockfile less reproducible than before.

Not a format change

CURRENT_LOCK_VERSION stays 3. Validation never inspects key position — hasValidShape, inspectLockFile, checkLockedDeps, checkLockedGithubDeps and checkLockConsistency all index by name or compare key sets — so key order is invisible to every reader.

An existing unsorted lockfile therefore stays valid, still passes mops install --locked, and is reordered only the next time something legitimately updates it. It is deliberately not treated as stale: a "rewrite because unsorted" trigger would churn every user's lockfile on their next install for no correctness reason, and would fail --locked on a lock that is otherwise perfectly good. A test pins both halves — an unsorted lock passes --locked and is byte-identical afterwards, and a plain mops install also leaves it byte-identical. That second assertion is the regression guard against someone adding such a trigger later.

Verification

locked, cache-resilience, local-dep-manifest-lock and the new lockfile-ordering suites: 40/40. The new tests were confirmed to bite by reverting the call site — the two ordering tests fail without it, while the two --locked guarantees correctly pass either way, since they assert behaviour that must hold before and after.

github-dep-lock.test.ts fails locally, and it is not from this change: with the call site reverted the same suite fails a superset of those tests, and the count varies run to run. It downloads real archives from GitHub and that cache has been churned repeatedly today. Worth a look if CI disagrees.

Item 8 in #723, where the triage records that this needs no version bump.

🤖 Generated with Claude Code

deps, hashes, graph and github came out in insertion/traversal order, so
unrelated installs reordered a 69 KB file with 621 per-file hash entries and
produced diff churn and merge conflicts for no reason.

Sorting happens at the single write site rather than where the records are
computed, which is what covers the carry-over path: computeLockFile copies an
existing package's hash record verbatim when the version is unchanged, so a sort
applied at computation time would preserve whatever order that record already
had.

Plain code-unit comparison, never localeCompare — a locale-sensitive sort orders
differently between machines, which is the opposite of the goal.

Not a format change. Validation never inspects key position: hasValidShape,
inspectLockFile, checkLockedDeps, checkLockedGithubDeps and checkLockConsistency
all index by name or compare key sets. CURRENT_LOCK_VERSION stays 3, an existing
unsorted lockfile stays valid and still passes --locked, and it is deliberately
not treated as stale — a "rewrite because unsorted" trigger would churn every
user's lockfile on their next install for no correctness reason.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Kamirus Kamirus added the cli label Aug 12, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Cursor AI review

👍 APPROVE — looks safe to merge

Category Assessment Details
Summary Sorts mops.lock keys at the sole write site (serializeLockFileupdateLockFile) so committed lockfiles stop churning from traversal order, without bumping the lock format or treating unsorted locks as stale.
Code Quality Sort applied only in serializeLockFile (not in computeLockFile), matching the carry-over path; plain code-unit compare mirrors hashGithubDir; no speculative helpers beyond nested/top-level sort.
Consistency ## Next changelog, docs/docs/10-mops.lock.md “Key ordering”, and targeted (non-snapshot) tests align with AGENTS.md; CURRENT_LOCK_VERSION stays 3.
Security Traced hasValidShape, inspectLockFile, checkLockedDeps, checkLockedGithubDeps, checkLockConsistency — all key-lookup / set compares; hash values and verification paths untouched; only write site is updateLockFilewriteLockFileAtomic.
Tests lockfile-ordering.test.ts covers sorted write (fixture deps reverse-alpha), carry-over scrambled file keys, byte-identical regen, and unsorted lock unchanged under --locked and plain install.
Maintainability Comments document why write-time sort (vs compute-time) and why unsorted locks are not staleness triggers; future --locked regressions are pinned by the byte-identical assertions.

Verdict

Decision: APPROVE
Risk: Low
Reason: High-risk path (cli/integrity.ts) but the diff is serialization-only: readers ignore key order, lock version is unchanged, and tests pin both sorted writes and non-rewrite of unsorted locks under --locked.


Generated for commit b944175

@automation-sa-sre automation-sa-sre 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.

Automated approval: the AI review verdict for b944175 is APPROVE. See the "Cursor AI review" comment for details.

@Kamirus
Kamirus merged commit 889b994 into v3 Aug 13, 2026
32 of 39 checks passed
@Kamirus
Kamirus deleted the kamil-claude/v3-sorted-lockfile branch August 13, 2026 11:12
Kamirus added a commit that referenced this pull request Aug 13, 2026
Establishes the manual tracking convention for the install benchmark
from [#744](#744): the most
recent meaningful run lives in `perf/install-bench/RESULTS.md`, replaced
wholesale on each re-run, with a one-line history of prior headline
numbers. Deliberately not a CI gate — runs are only comparable within
one host and network, so a committed run is a point of reference.

## The run itself

v2.22.0 vs v3 at f8c619c — the first measurement with the whole
performance stack landed (#741 sorted lock, #742 update alignment, #743
resolve memo, #745 parallel installs, #746 in-flight re-check). Medians
of 3 against the live registry:

| scenario | v2 | v3 | v3 vs v2 |
| --- | --- | --- | --- |
| install-cold-nolock | 139.6s | 107.4s | 0.77x |
| install-cold-validlock | 106.6s | 61.1s | 0.57x |
| install-cold-stalelock | 128.0s | 95.7s | 0.75x |
| install-warm-nolock | 3.20s | 5.62s | 1.76x † |
| install-warm-validlock | 6.20s | 2.99s | 0.48x † |
| install-warm-stalelock | 3.31s | 2.45s | 0.74x |
| add-two | 4.00s | 3.84s | 0.96x |
| update-few | 8.02s | 5.42s | 0.68x |
| update-all | 14.9s | 2.87s | 0.19x |

† transient registry-latency window, not code: v2's own warm numbers
tripled against every prior run of the same released binary. A warm-only
re-check half an hour later returned to line — v3 warm-nolock 2.33s
(0.75x), warm-validlock 1.08s (0.42x) — and is recorded in RESULTS.md as
the representative warm numbers, with the main table kept unedited.

**v3 now beats released v2 in every scenario.** The cold no-lock install
— v3's one regression before #745 (1.23–1.25x in both pre-parallel
baselines) — is 0.77x, and CI-shaped installs (cold + committed lock)
are 43% faster.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants