* refactor(store): unify the catalog structural-equality walks
Three near-identical structural deep-equality walks had landed independently in
the same window: areValuesEqual (#13744, repo-identity-reconcile.ts),
areCatalogEntriesEqual (#13770, repos.ts — already folded into the first on this
branch's base) and catalogValuesEqual (#13662,
worktree-catalog-reconciliation.ts). All three walk plain records and arrays and
fall back to reference equality for anything exotic.
They are not interchangeable. Two axes genuinely differ, and each caller depends
on its own side:
- Own-key set. #13744/#13770 require equal own-key counts plus hasOwnProperty,
so an absent key differs from a key present and holding `undefined`. #13662
compares the union of both sides' keys, so those are equal. The strict side is
load-bearing: the repo/project merges branch on
`'localWindowsRuntimePreference' in project` (repos-project-runtime.test.ts
"clears stale local runtime preferences"), and projects are now reconciled
with this comparator. The loose side is test-pinned by
worktree-catalog-reconciliation.test.ts "reuses rows with equivalent nested
catalog data", where a locally built row carries `optional: undefined` that
the host omits.
- Leaf comparison. #13744/#13770 use `===` (NaN never equal, 0 equals -0);
#13662 uses `Object.is` (the reverse).
So instead of picking a winner, src/shared/structural-value-equality.ts holds
one walk parameterised by those two axes and exports the two policies as
`structuralValuesEqual` and `structuralValuesEqualIgnoringUndefined`. Every
caller keeps its exact current semantics; the ~40 duplicated lines and the
silent divergence go away. src/shared/persisted-ui-equality.ts (a fourth copy
with a Set branch and no plain-object guard) is deliberately left alone: it
gates a disk write in main with no direct test coverage.
Also folded, all provably behaviour-identical:
- The `${hostId}\0${repoId}` composite key had three copies
(getRepoHostIdentityForParts, repoOwnerKey, getEntryKey) that must agree or
repos silently stop reconciling. Moved to src/shared/repo-host-identity.ts
because one of them lives in src/shared; the renderer module re-exports it.
- mergeFetchedReposForHost's hand-inlined upsert loop now calls mergeByIdentity.
mergeByIdentity additionally skips replacing a structurally equal row, which
cannot change the result here: reconcileFetchedRepos runs immediately after
over the same identities in the same order and restores exactly those rows.
- Renamed repos.ts's `catalogRowsUnchanged` to `arrayElementsUnchanged`. It is a
pure element-identity compare, two files away from
`catalogRowsEqual`, which is a full structural compare.
src/shared/structural-value-equality.test.ts pins both policies over arrays,
nested records, null-prototype records, absent-vs-undefined keys, symbol keys,
and non-plain objects (Date/Map/Set/class) falling back to reference equality.
* fix(store): keep merged sourceRepoIds order host-independent
Prefixing the cross-host remainder made a cross-host project's sourceRepoIds
order a function of the refreshing host, so the projects reconcile never reused
the row. Also pins the repo-derived host-id contribution the new per-project
slice feeds the host-id resolvers.
Co-authored-by: Orca <help@stably.ai>
* refactor(store): migrate call sites that landed after this branch
github.ts and ai-vault-session-identity.ts began using areValuesEqual on main
while this branch was stale, and repo-identity-reconcile's record reconciler
still called its own deleted walker. All three now use structuralValuesEqual;
reuseEqualCatalogRows keeps its duplicate-id cap and calls the ignoring-undefined
variant, which is the key-union semantics catalogValuesEqual had.
---------
Co-authored-by: Orca <help@stably.ai>