Skip to content

Commit eeae600

Browse files
committed
release: v2.7.229
1 parent 356b0e7 commit eeae600

File tree

6 files changed

+86
-18
lines changed

6 files changed

+86
-18
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44

55
All notable changes to this project will be documented in this file.
66

7+
## [2.7.229] — 2026-03-15
8+
9+
- changed(web/navigation): added shared `comparePaletteRoutes(...)` in `apps/web/src/components/mcp/nav-validation.ts` so quick-switch route ranking now has one reusable contract for favorite priority, recency priority, and stable title fallback ordering.
10+
- refactor(web/sidebar): `apps/web/src/components/Sidebar.tsx` now delegates palette route sorting to the shared comparator instead of keeping the ranking rules embedded inline inside the palette memo.
11+
- test(web/navigation): expanded `apps/web/src/components/mcp/nav-validation.test.ts` with explicit palette ranking coverage for favorites, recency, and title fallback ordering; focused nav suites now pass with `40` total tests.
12+
713
## [2.7.228] — 2026-03-15
814

915
- refactor(web/sidebar): added shared `safeStorageGetJson(...)` in `apps/web/src/components/Sidebar.tsx` so stored navigation preference hydration now reads and parses localStorage payloads through one helper instead of repeating inline `safeStorageGet(...)` + `JSON.parse(...)` flows.

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.7.228
1+
2.7.229

VERSION.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
# Borg Project Version: 2.7.228
1+
# Borg Project Version: 2.7.229

apps/web/src/components/Sidebar.tsx

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { SortableContext, arrayMove, rectSortingStrategy, useSortable } from "@d
1010
import { CSS } from "@dnd-kit/utilities";
1111
import { cn } from "@/lib/utils";
1212
import { SIDEBAR_SECTIONS } from "./mcp/nav-config";
13-
import { buildExportedNavPreferences, buildNavItemsByNormalizedHref, buildRecentRouteHistory, buildRecentSearchHistory, getNavDescription, hasNavValidationIssues, isNavHrefActive, matchesNavQuery, normalizeNavHref, sanitizeCollapsedSections, sanitizeFavoriteRoutes, sanitizeNavPreferences, sanitizeRecentRoutes, sanitizeRecentSearches, validateSidebarSections } from "./mcp/nav-validation";
13+
import { buildExportedNavPreferences, buildNavItemsByNormalizedHref, buildRecentRouteHistory, buildRecentSearchHistory, comparePaletteRoutes, getNavDescription, hasNavValidationIssues, isNavHrefActive, matchesNavQuery, normalizeNavHref, sanitizeCollapsedSections, sanitizeFavoriteRoutes, sanitizeNavPreferences, sanitizeRecentRoutes, sanitizeRecentSearches, validateSidebarSections } from "./mcp/nav-validation";
1414

1515
interface SidebarProps extends React.HTMLAttributes<HTMLDivElement> { }
1616

@@ -250,21 +250,7 @@ export function Sidebar({ className }: SidebarProps) {
250250
return matchesNavQuery(q, item, item.section);
251251
});
252252

253-
rows.sort((a, b) => {
254-
const aFav = favoriteSet.has(a.href) ? 1 : 0;
255-
const bFav = favoriteSet.has(b.href) ? 1 : 0;
256-
if (aFav !== bFav) {
257-
return bFav - aFav;
258-
}
259-
260-
const aRecent = recencyRank.has(a.href) ? recencyRank.get(a.href)! : Number.MAX_SAFE_INTEGER;
261-
const bRecent = recencyRank.has(b.href) ? recencyRank.get(b.href)! : Number.MAX_SAFE_INTEGER;
262-
if (aRecent !== bRecent) {
263-
return aRecent - bRecent;
264-
}
265-
266-
return a.title.localeCompare(b.title);
267-
});
253+
rows.sort((a, b) => comparePaletteRoutes(a, b, favoriteSet, recencyRank));
268254

269255
const routeItems: PaletteItem[] = rows.map((item) => ({
270256
kind: 'route',

apps/web/src/components/mcp/nav-validation.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
buildNavItemsByNormalizedHref,
99
buildRecentRouteHistory,
1010
buildRecentSearchHistory,
11+
comparePaletteRoutes,
1112
extractStringArray,
1213
filterNavHrefsByAllowedSet,
1314
getNavDescription,
@@ -255,6 +256,56 @@ describe('buildRecentSearchHistory', () => {
255256
});
256257
});
257258

259+
describe('comparePaletteRoutes', () => {
260+
const library = {
261+
title: 'Library',
262+
href: '/dashboard/library',
263+
section: 'Knowledge',
264+
};
265+
266+
const tools = {
267+
title: 'Tools',
268+
href: '/dashboard/tools',
269+
section: 'Operations',
270+
};
271+
272+
const audit = {
273+
title: 'Audit',
274+
href: '/dashboard/audit',
275+
section: 'Security',
276+
};
277+
278+
it('ranks favorited routes ahead of non-favorited routes', () => {
279+
expect(comparePaletteRoutes(
280+
library,
281+
tools,
282+
new Set(['/dashboard/library']),
283+
new Map(),
284+
)).toBeLessThan(0);
285+
});
286+
287+
it('ranks more recent routes ahead when favorite status is equal', () => {
288+
expect(comparePaletteRoutes(
289+
tools,
290+
audit,
291+
new Set(),
292+
new Map([
293+
['/dashboard/tools', 0],
294+
['/dashboard/audit', 2],
295+
]),
296+
)).toBeLessThan(0);
297+
});
298+
299+
it('falls back to title ordering when favorite and recency status are equal', () => {
300+
expect(comparePaletteRoutes(
301+
audit,
302+
library,
303+
new Set(),
304+
new Map(),
305+
)).toBeLessThan(0);
306+
});
307+
});
308+
258309
describe('getNavDescription', () => {
259310
it('returns trimmed explicit descriptions before fallback text', () => {
260311
expect(getNavDescription({

apps/web/src/components/mcp/nav-validation.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ export interface NavDescriptionCandidate {
1414
description?: string;
1515
}
1616

17+
export interface NavPaletteRouteCandidate extends NavSearchCandidate {
18+
section: string;
19+
}
20+
1721
export interface NavDuplicateIssue {
1822
href: string;
1923
sections: string[];
@@ -210,6 +214,27 @@ export function buildRecentSearchHistory(currentSearches: string[], nextQuery: s
210214
], limit);
211215
}
212216

217+
export function comparePaletteRoutes(
218+
a: NavPaletteRouteCandidate,
219+
b: NavPaletteRouteCandidate,
220+
favoriteHrefs: ReadonlySet<string>,
221+
recencyRank: ReadonlyMap<string, number>,
222+
): number {
223+
const aFav = favoriteHrefs.has(a.href) ? 1 : 0;
224+
const bFav = favoriteHrefs.has(b.href) ? 1 : 0;
225+
if (aFav !== bFav) {
226+
return bFav - aFav;
227+
}
228+
229+
const aRecent = recencyRank.get(a.href) ?? Number.MAX_SAFE_INTEGER;
230+
const bRecent = recencyRank.get(b.href) ?? Number.MAX_SAFE_INTEGER;
231+
if (aRecent !== bRecent) {
232+
return aRecent - bRecent;
233+
}
234+
235+
return a.title.localeCompare(b.title);
236+
}
237+
213238
export function buildFallbackNavDescription(title: string, section: string, href?: string): string {
214239
const normalized = title.toLowerCase();
215240

0 commit comments

Comments
 (0)