From 0a7554edf2d72d290bce2658d3b91ce45120ad42 Mon Sep 17 00:00:00 2001 From: goetchstone Date: Tue, 28 Jul 2026 06:01:52 -0400 Subject: [PATCH 1/3] Deps: pnpm overrides for the two dependabot alerts (js-yaml, brace-expansion) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - js-yaml >=4.3.0 (high: vulnerable range >=4.0.0 <4.3.0) — only one version in the tree (4.2.0), pulled in by @eslint/eslintrc; resolves to 4.3.0, same major line. - brace-expansion >=5.0.7 (high: vulnerable range >=3.0.0 <5.0.7), scoped as "minimatch@10>brace-expansion" — the tree carries TWO incompatible major lines: 1.1.15 (bare-function export) under minimatch@3.1.5 via eslint's own config-array/eslintrc chain, and 5.0.6 (named exports) under minimatch@10.2.5 via @typescript-eslint/typescript-estree. Only the 5.x line is in the advisory's vulnerable range, so the override is qualified to that parent to avoid forcing a major bump onto minimatch@3.1.5 (which would break it the way an unscoped bump broke minimatch@3.x/ @babel/core/istanbul on a sibling repo). Resolves to 5.0.8; the 1.1.15 instance is untouched and out of the alert's range. Full gate green with the new resolutions (1,245 tests, lint, build). `pnpm audit` confirms both js-yaml and the 5.x brace-expansion finding are gone; the untouched 1.x brace-expansion instance and a pre-existing postcss advisory in packages/client are unrelated to these two alerts and were left alone. Co-Authored-By: Claude Fable 5 --- package.json | 4 +++- pnpm-lock.yaml | 20 +++++++++++--------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 4344273..0a78c38 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,9 @@ ], "overrides": { "shell-quote": ">=1.8.4", - "esbuild": ">=0.28.1" + "esbuild": ">=0.28.1", + "js-yaml": "^4.3.0", + "minimatch@10>brace-expansion": "^5.0.7" } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b6782e7..961a969 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,8 @@ settings: overrides: shell-quote: '>=1.8.4' esbuild: '>=0.28.1' + js-yaml: ^4.3.0 + minimatch@10>brace-expansion: ^5.0.7 importers: @@ -614,9 +616,9 @@ packages: brace-expansion@1.1.15: resolution: {integrity: sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==} - brace-expansion@5.0.6: - resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} - engines: {node: 18 || 20 || >=22} + brace-expansion@5.0.8: + resolution: {integrity: sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==} + engines: {node: 20 || >=22} cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} @@ -852,8 +854,8 @@ packages: js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} - js-yaml@4.2.0: - resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==} + js-yaml@4.3.0: + resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} hasBin: true json-buffer@3.0.1: @@ -1324,7 +1326,7 @@ snapshots: globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.1 - js-yaml: 4.2.0 + js-yaml: 4.3.0 minimatch: 3.1.5 strip-json-comments: 3.1.1 transitivePeerDependencies: @@ -1624,7 +1626,7 @@ snapshots: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@5.0.6: + brace-expansion@5.0.8: dependencies: balanced-match: 4.0.4 @@ -1869,7 +1871,7 @@ snapshots: js-tokens@9.0.1: {} - js-yaml@4.2.0: + js-yaml@4.3.0: dependencies: argparse: 2.0.1 @@ -1902,7 +1904,7 @@ snapshots: minimatch@10.2.5: dependencies: - brace-expansion: 5.0.6 + brace-expansion: 5.0.8 minimatch@3.1.5: dependencies: From 45de9a3a32f6a3ab27888894dab9a4992f177169 Mon Sep 17 00:00:00 2001 From: goetchstone Date: Tue, 28 Jul 2026 06:06:14 -0400 Subject: [PATCH 2/3] security(deps): also clear the 1.x brace-expansion and postcss advisories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to the js-yaml / brace-expansion@5 overrides in this branch. - minimatch@3>brace-expansion -> ^1.1.16 clears GHSA-3jxr-9vmj-r5cp. Scoped to the 1.x line so minimatch@3's bare-function usage keeps working; a blanket brace-expansion override would break it (and @babel/core and istanbul), which is exactly how the equivalent bump failed on a sibling repo. - postcss -> ^8.5.18 clears the sourceMappingURL path-traversal advisory reaching packages/client via vite. Remaining after this: GHSA-mh99-v99m-4gvg still matches brace-expansion 1.1.16 under eslint>minimatch@3. It is unfixable — the advisory covers everything <= 5.0.7 and upstream patched only the 5.x line, so no patched 1.x exists — and it is dev-only. 'pnpm audit --prod' reports no known vulnerabilities, so nothing vulnerable ships. Verified on a clean 'pnpm install --frozen-lockfile': build, lint, and the full workspace suite all pass. Co-Authored-By: Claude Opus 4.8 --- package.json | 4 +++- pnpm-lock.yaml | 26 ++++++++++++++------------ 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 0a78c38..6eb06f5 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,9 @@ "shell-quote": ">=1.8.4", "esbuild": ">=0.28.1", "js-yaml": "^4.3.0", - "minimatch@10>brace-expansion": "^5.0.7" + "minimatch@10>brace-expansion": "^5.0.7", + "minimatch@3>brace-expansion": "^1.1.16", + "postcss": "^8.5.18" } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 961a969..08d3171 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,6 +9,8 @@ overrides: esbuild: '>=0.28.1' js-yaml: ^4.3.0 minimatch@10>brace-expansion: ^5.0.7 + minimatch@3>brace-expansion: ^1.1.16 + postcss: ^8.5.18 importers: @@ -613,8 +615,8 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} - brace-expansion@1.1.15: - resolution: {integrity: sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==} + brace-expansion@1.1.16: + resolution: {integrity: sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==} brace-expansion@5.0.8: resolution: {integrity: sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==} @@ -897,8 +899,8 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - nanoid@3.3.12: - resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + nanoid@3.3.16: + resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -949,8 +951,8 @@ packages: pixi.js@8.19.0: resolution: {integrity: sha512-pq1O6emA/GFjjeF+8d3Pb5t7knD8FsnfWGqQcRjYjsqFZ7QdzG1XgjLDUu0DFJRbafjV5+g8iNLFBx0b9649lg==} - postcss@8.5.15: - resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + postcss@8.5.24: + resolution: {integrity: sha512-8RyVklq0owXUTa4xlpzu4l9AaVKIdQvAcOHZWaMh98HgySsUtxRVf/chRe3dsSLqb6i40BzGRzEUddRaI+9TSw==} engines: {node: ^10 || ^12 || >=14} prelude-ls@1.2.1: @@ -1621,7 +1623,7 @@ snapshots: balanced-match@4.0.4: {} - brace-expansion@1.1.15: + brace-expansion@1.1.16: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 @@ -1908,11 +1910,11 @@ snapshots: minimatch@3.1.5: dependencies: - brace-expansion: 1.1.15 + brace-expansion: 1.1.16 ms@2.1.3: {} - nanoid@3.3.12: {} + nanoid@3.3.16: {} natural-compare@1.4.0: {} @@ -1964,9 +1966,9 @@ snapshots: parse-svg-path: 0.2.0 tiny-lru: 11.4.7 - postcss@8.5.15: + postcss@8.5.24: dependencies: - nanoid: 3.3.12 + nanoid: 3.3.16 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -2135,7 +2137,7 @@ snapshots: esbuild: 0.28.1 fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 - postcss: 8.5.15 + postcss: 8.5.24 rollup: 4.61.1 tinyglobby: 0.2.17 optionalDependencies: From 8b8ee197c38ffb4c4dc95aeac100ade8f8847747 Mon Sep 17 00:00:00 2001 From: goetchstone Date: Fri, 31 Jul 2026 12:48:03 -0400 Subject: [PATCH 3/3] Sea: paint the ORIGINAL's own seabed, not a synthetic gradient MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Owner: "maybe you can fix the center now too" — the contested middle rendered as one flat blue field. Root cause: the sea was painted by a SYNTHETIC formula (waterDepth01: shallow near the north/south edges, deep in between, 28 latitude slabs), so every cell at the same y got the same colour. The real per-cell seabed WAS being extracted into terrain.json's `depth` (deep/shallow/pink from the map's own minimap colours) but the compiler never read it — dead data that never reached the client. The original's middle is a braid of shoals and channels; ours was a gradient. - core: WaterMask gains a packed render-only `depth` band per cell (0=land,1=deep,2=shallow,3=pink), decoded from terrain.json's existing RLE; absent/short data degrades to zeros. The SIM IGNORES it — sailability stays `cells` alone, so determinism is untouched (the serializability guard now pins it alongside `cells`). - client: the static sea layer lays the true bands over the base grade, run-length merged and clipped to the viewport, inside the existing cached layer (rebuilt only on the sea signature). New pure `seabedBandTint` + 4 unit tests. 1,249 tests green; lint clean. Co-Authored-By: Claude Fable 5 --- packages/client/src/render/world.ts | 60 +++++++++++++++++++++++++++++ packages/client/test/world.test.ts | 29 +++++++++++++- packages/core/src/sim/ruleset.ts | 21 ++++++++++ packages/core/src/sim/types.ts | 18 +++++++++ packages/core/test/ruleset.test.ts | 8 +++- 5 files changed, 134 insertions(+), 2 deletions(-) diff --git a/packages/client/src/render/world.ts b/packages/client/src/render/world.ts index 4fe1bdc..d70ebc3 100644 --- a/packages/client/src/render/world.ts +++ b/packages/client/src/render/world.ts @@ -50,6 +50,7 @@ import { HP_RED, HP_YELLOW, WATER_FOAM, + WATER_RAMP, mix, scale, waterAt, @@ -132,6 +133,27 @@ export function waterDepth01( return 0.1 + 0.52 * t; } +/** + * Seabed-band tint for the ORIGINAL's own per-cell depth (WaterMask.depth: + * 1 = deep, 2 = shallow, 3 = pink passable shallows). Returned as a colour + + * alpha to lay OVER the base sea so the real shoals, channels and bars show + * through — the map's contested middle is a braid of shallows in the original, + * not the flat blue field a pure north-south gradient paints. Band 0 (land) + * returns null: land is drawn by land.ts. Pure — unit-tested. + */ +export function seabedBandTint(band: number): { color: number; alpha: number } | null { + switch (band) { + case 1: + return { color: WATER_RAMP[2] ?? 0x16526f, alpha: 0.5 }; // deep channel + case 2: + return { color: WATER_RAMP[0] ?? 0x2c7e9e, alpha: 0.42 }; // shoal / shallow + case 3: + return { color: 0x6f7fa8, alpha: 0.34 }; // pink passable shallows + default: + return null; + } +} + /** Base water fill color at a given world y (depth-graded). */ export function waterColorAt(worldY: number, bounds: { minY: number; maxY: number }): number { return waterAt(waterDepth01(worldY, bounds)); @@ -276,6 +298,44 @@ export function createWorld(renderer?: Renderer): WorldLayer { seaStatic.rect(left, sy0, width, sy1 - sy0 + 1).fill(waterColorAt(midY, bounds)); } + // TRUE SEABED: lay the ORIGINAL's own per-cell depth bands over the base + // grade so shoals, bars and the braided channels of the contested middle + // read the way they do on the real map (owner: "fix the center" — the + // pure north-south gradient painted it as one flat blue field). Only + // cells in view are drawn, so this is bounded by the viewport, not the + // map; it lives in the CACHED static layer (rebuilt only when the sea + // signature changes). Render-only: sailability is the water mask. + const sea = getCatalog().map.waterMask; + if (sea.depth.length === sea.cells.length && sea.cells.length > 0) { + const c0 = Math.max(0, Math.floor((minX - sea.bounds.minX) / sea.cellSizeX)); + const c1 = Math.min(sea.cols - 1, Math.ceil((maxX - sea.bounds.minX) / sea.cellSizeX)); + const r0 = Math.max(0, Math.floor((sea.bounds.maxY - maxY) / sea.cellSizeY)); + const r1 = Math.min(sea.rows - 1, Math.ceil((sea.bounds.maxY - minY) / sea.cellSizeY)); + for (let r = r0; r <= r1; r++) { + const wy0 = sea.bounds.maxY - r * sea.cellSizeY; + const wy1 = wy0 - sea.cellSizeY; + const sy0 = cam.worldToScreen(0, wy0).y; + const sy1 = cam.worldToScreen(0, wy1).y; + let c = c0; + while (c <= c1) { + const band = sea.depth[r * sea.cols + c] ?? 0; + let end = c; + while (end + 1 <= c1 && (sea.depth[r * sea.cols + end + 1] ?? 0) === band) end++; + const tint = seabedBandTint(band); + if (tint !== null) { + const wx0 = sea.bounds.minX + c * sea.cellSizeX; + const wx1 = sea.bounds.minX + (end + 1) * sea.cellSizeX; + const sx0 = cam.worldToScreen(wx0, 0).x; + const sx1 = cam.worldToScreen(wx1, 0).x; + seaStatic + .rect(sx0, sy0, sx1 - sx0 + 1, sy1 - sy0 + 1) + .fill({ color: tint.color, alpha: tint.alpha }); + } + c = end + 1; + } + } + } + // Faint world-space grid. const gx0 = Math.ceil(minX / GRID_STEP) * GRID_STEP; for (let wx = gx0; wx <= maxX; wx += GRID_STEP) { diff --git a/packages/client/test/world.test.ts b/packages/client/test/world.test.ts index a3527f3..699c096 100644 --- a/packages/client/test/world.test.ts +++ b/packages/client/test/world.test.ts @@ -27,7 +27,7 @@ import { structureSilhouette, trimColor, } from '../src/render/structures.js'; -import { seaStaticSignature, waterColorAt, waterDepth01 } from '../src/render/world.js'; +import { seaStaticSignature, waterColorAt, waterDepth01, seabedBandTint } from '../src/render/world.js'; const ALL_ROLES: StructureRole[] = [ 'hq', @@ -277,3 +277,30 @@ describe('seaStaticSignature', () => { expect(a).not.toBe(seaStaticSignature(rect, 1, 1280, 720)); }); }); + +describe('seabed bands (the ORIGINAL per-cell depth the sea is painted from)', () => { + it('land (band 0) paints nothing — land.ts owns it', () => { + expect(seabedBandTint(0)).toBeNull(); + }); + + it('every water band yields a visible, partially transparent tint', () => { + for (const band of [1, 2, 3]) { + const t = seabedBandTint(band); + expect(t, `band ${band}`).not.toBeNull(); + expect(t!.alpha).toBeGreaterThan(0); + expect(t!.alpha).toBeLessThan(1); + expect(Number.isFinite(t!.color)).toBe(true); + } + }); + + it('deep reads darker than shallow (the channel must contrast the shoals)', () => { + const deep = seabedBandTint(1)!.color; + const shallow = seabedBandTint(2)!.color; + const lum = (c: number) => ((c >> 16) & 0xff) * 0.3 + ((c >> 8) & 0xff) * 0.59 + (c & 0xff) * 0.11; + expect(lum(deep)).toBeLessThan(lum(shallow)); + }); + + it('an unknown band degrades to no tint (forward-compatible)', () => { + expect(seabedBandTint(9)).toBeNull(); + }); +}); diff --git a/packages/core/src/sim/ruleset.ts b/packages/core/src/sim/ruleset.ts index 8c6d820..7639076 100644 --- a/packages/core/src/sim/ruleset.ts +++ b/packages/core/src/sim/ruleset.ts @@ -2083,6 +2083,7 @@ export function compileWaterMask(bounds: MapSpec['bounds'], terrain?: RawTerrain cellSizeX: 1, cellSizeY: 1, cells: new Uint8Array(0), + depth: new Uint8Array(0), }; } @@ -2110,6 +2111,25 @@ export function compileWaterMask(bounds: MapSpec['bounds'], terrain?: RawTerrain } if (col !== cols) fail(`terrain: row ${r} runs sum to ${col}, expected ${cols}`); } + // RENDER-ONLY seabed bands (value,run pairs; 0=land,1=deep,2=shallow,3=pink). + // Optional and never consulted by the sim: a missing/short field degrades to + // all-zero, which the client falls back to a flat sea for. + const depth = new Uint8Array(cols * rows); + if (Array.isArray(terrain.depth) && terrain.depth.length === rows) { + for (let r = 0; r < rows; r++) { + const rle = terrain.depth[r]; + if (!Array.isArray(rle)) continue; + let col = 0; + for (let k = 0; k + 1 < rle.length; k += 2) { + const value = rle[k] ?? 0; + const run = rle[k + 1] ?? 0; + if (value > 0) { + for (let c = 0; c < run && col + c < cols; c++) depth[r * cols + col + c] = value; + } + col += run; + } + } + } return { bounds: { minX: mustNum(terrain.bounds.minX, 'terrain minX'), @@ -2122,6 +2142,7 @@ export function compileWaterMask(bounds: MapSpec['bounds'], terrain?: RawTerrain cellSizeX: mustNum(terrain.cellSizeX, 'terrain cellSizeX'), cellSizeY: mustNum(terrain.cellSizeY, 'terrain cellSizeY'), cells, + depth, }; } diff --git a/packages/core/src/sim/types.ts b/packages/core/src/sim/types.ts index 3d0e060..04c773d 100644 --- a/packages/core/src/sim/types.ts +++ b/packages/core/src/sim/types.ts @@ -1640,6 +1640,18 @@ export interface WaterMask { * is on the immutable Ruleset, never in serialized state. */ cells: Uint8Array; + /** + * RENDER-ONLY seabed band per cell (0 = land, 1 = deep, 2 = shallow, + * 3 = pink passable shallows), row-major like `cells`, or an empty array on + * a stub mask. This is the ORIGINAL's own seabed painted per 64u cell — the + * client draws the sea from it so the map reads like the real one (the + * owner's "the center" complaint: the sea was painted by a synthetic + * north-south depth gradient that made the contested middle a featureless + * blue field, hiding the shoals and channels the original clearly shows). + * The SIM IGNORES this — sailability is `cells` alone — so it can never + * affect determinism. + */ + depth: Uint8Array; } /** @@ -2223,6 +2235,12 @@ export interface RawTerrainFile { yOrientation: 'top-down'; /** Per-row run-length encoding; one entry per row (length === rows). */ water: number[][]; + /** + * OPTIONAL render-only seabed bands, per-row (value, run) pairs — + * 0=land, 1=deep, 2=shallow, 3=pink. The sim ignores it; the client paints + * the sea from it. Absent on older/stub terrain files. + */ + depth?: number[][]; } export interface RawDataFiles { diff --git a/packages/core/test/ruleset.test.ts b/packages/core/test/ruleset.test.ts index dddc3c6..ee15e7e 100644 --- a/packages/core/test/ruleset.test.ts +++ b/packages/core/test/ruleset.test.ts @@ -890,13 +890,19 @@ describe('ruleset integrity and determinism', () => { // NavField). Assert serializability of everything ELSE, and pin the // typed-array representation of the mask + every nav field. expect(rs.map.waterMask.cells).toBeInstanceOf(Uint8Array); + // `depth` shares the mask's rationale exactly: a packed render-only + // seabed band per cell (0=land,1=deep,2=shallow,3=pink) on the immutable + // Ruleset, never serialized per-match nor hashed. The SIM ignores it. + expect(rs.map.waterMask.depth).toBeInstanceOf(Uint8Array); + expect(rs.map.waterMask.depth.length).toBe(rs.map.waterMask.cells.length); for (const team of ['south', 'north'] as const) { expect(rs.map.navByTeam[team].dist).toBeInstanceOf(Int32Array); expect(rs.map.navHomeByTeam[team].dist).toBeInstanceOf(Int32Array); } const { waterMask, navByTeam, navHomeByTeam, navToRegion, ...mapRest } = rs.map; - const { cells, ...maskRest } = waterMask; + const { cells, depth, ...maskRest } = waterMask; void cells; + void depth; // Strip the typed `dist` from each nav field; keep the JSON-able metadata. const stripNav = (nav: (typeof navByTeam)['south']): object => { const { dist, ...rest } = nav;