Skip to content

fix(maps): honour the legacy dark tile URL, document raw-URL limits (#390) - #399

Merged
dborup merged 1 commit into
masterfrom
codex/issue-390-tile-legacy-followups
Oct 13, 2026
Merged

dborup merged 1 commit into
masterfrom
codex/issue-390-tile-legacy-followups

Conversation

@adminopenclaw8-sketch

Copy link
Copy Markdown
Collaborator

Relates to #390.

Follow-ups to #366 (#364, merged): the dark legacy tile override, attribution for legacy raw URLs, and the zoom cap.

Plan

  1. Recheck master (ce29f5dc), then mirror the fix(maps): preserve legacy light tile URL overrides (#364) #366 precedence for dark in the shared resolver.
  2. Write the unit cases and the real-browser cases first (red), then the code.
  3. Document the raw-URL limits that stay by design, pinning the documented numbers to the ones the resolver really returns so they cannot drift.

1 — Dark legacy override is no longer masked (fixed)

map-tile-providers.js gets getSelectedDarkId(), the mirror of getSelectedLightId(), so an implicit fallback stays distinguishable from a choice the user or operator actually made. getActiveId() is now getSelectedDarkId() || DEFAULT_ID — a pure refactor of the same resolution chain, so MC_getDarkTileProvider() keeps returning a registry id and the layer control is unaffected.

getTileSpec() resolves the legacy branch per theme. Precedence, for both themes:

  1. A valid explicit browser provider choice, including an explicit choice of the built-in id.
  2. A valid configured/programmatic provider default (darkDefault / lightDefault, MC_setServerDefault*).
  3. The legacy raw URL.
  4. The built-in fallback.

roles.js records MC_DARK_TILE_URL_OVERRIDE from tiles.dark / map.tiles.darkUrl, mirroring the light override. Like light, a raw URL resolves with id: null, so it borrows no vendor credit, no reference overlay and no invert filter.

One extra fix the dark branch forces: MC_applyTileFilter() resolved the filter straight out of REGISTRY[getActiveId()], which would have inverted the operator's own dark tileset on top of it already being dark — a light basemap inside a dark page. It now resolves through getTileSpec() like every other reader. Light-mode and no-legacy behaviour is identical (the fallback chain is the same object).

2 — Attribution for legacy raw URLs (documented)

Taken as the issue's "or document it" branch. An attribution override would be new config, so it is left for the owner (below). config.example.json now carries map.tiles._comment_legacy, documenting all four legacy keys, the precedence above, the generic © OpenStreetMap contributors credit a raw URL gets, that it is never CSS-inverted, and that darkDefault / lightDefault with a registered style id is the way to get a correct vendor credit and licence link.

A test asserts the documented credit is the string the resolver really returns, for both themes.

3 — Zoom cap (documented; configurable cap left for the owner)

Documented in the same note, and a test pins the documented ceiling to MC_getTileSpec(theme).maxZoom for a legacy URL in both themes, so the doc cannot drift from the code (the same guard #332 M9 uses for the default provider's zoom).

Left for the owner

Both are "consider" points that would add new config, so they are proposals only — nothing in this PR implements them.

  • Attribution override alongside a legacy URL. Shape: map.tiles.darkAttribution / lightAttribution (plus top-level tiles.darkAttribution / lightAttribution for symmetry), used verbatim as the legacy spec's attribution when present, falling back to the generic credit. Small in code, but it is a new operator-facing setting and the value is rendered into the Leaflet attribution control, so it needs an escaping decision (the registry's own attributions contain real <a> markup) before it is safe to add.
  • Configurable max zoom for a legacy URL. Shape: map.tiles.darkMaxZoom / lightMaxZoom, replacing the inherited osm-* ceiling of 18 in the legacy spec. Raising the cap unconditionally is not safe: a provider that stops at 18 would then be asked for z19 and render blank tiles with no on-screen explanation, so the ceiling has to come from the operator rather than be guessed.

Verification

Requirement Test Mutant shown red
Dark legacy URL overrides the implicit fallback (both config shapes) test-issue-390-legacy-tile-followups.js P1 (21 cases) legacy dark branch removed → 11 red; roles.js stops reading map.tiles.darkUrl → 6 red
Dark precedence: explicit choice and configured default beat the legacy URL P1 browser-choice / darkDefault / server-default cases !selected guard dropped → 6 red; getSelectedDarkId() returns the fallback id → 11 red
A legacy dark tileset is never inverted P1 "leaves the tile pane filter cleared" MC_applyTileFilter() back to the REGISTRY lookup → 2 red
Documented credit matches the resolver P2 generic credit dropped from the note → 1 red
Documented zoom ceiling matches the resolver P3 note changed to "zoom 19" → 1 red
Real browser, both themes, both config shapes test-issue-332-no-carto-tiles-e2e.js #390 steps (10) legacy dark branch removed, server restarted → 6 red

Suites run locally: test-issue-390-legacy-tile-followups.js 24/24; test-issue-332-tile-provider-fallback.js 71/71; test-issue-1420-tile-providers.js 39/39; test-frontend-helpers.js 712/712; sh test-all.sh 236/237 files; cmd/server go test ./... ok 40.8s; test-issue-332-no-carto-tiles-e2e.js 31/31 against a local Go server with e2e-fixture.db prepared as in CI.

The one test-all.sh failure is test-backup-sqlite-safety.js (9 cases, all "missing tool setsid") — a macOS-only environment gap on this machine, unrelated to this change and present on plain master; the file is untouched here and the cases pass in CI.

Both themes were also checked visually in Chromium against the local server with a legacy dark URL configured: dark + legacy renders the operator's tiles unfiltered with the generic credit, dark + default keeps the osm-dark invert filter, and light is unchanged either way.

Scope and hygiene

cmd/server untouched (no Go files in the diff, so no new map[string]interface{} outside tests). No hardcoded colours — the one new literal is a URL-free attribution string hoisted into a shared constant. scripts/check-xss-sinks.sh --diff origin/master exits 0. npx eslint public/*.js 0 errors (89 pre-existing warnings). Fork guards unchanged: 9 in deploy.yml, 1 in release-fast-path.yml. No new config keys, dependencies or workflow changes. The default provider for existing installs is unchanged, and a config that selects providers by id never enters the legacy branch.

…390)

Follow-ups to #366. #366 restored the legacy LIGHT tile-URL override in
front of the registry's implicit fallback; dark kept being masked by the
registry default exactly the way light was.

- map-tile-providers.js: getSelectedDarkId() mirrors getSelectedLightId()
  so an implicit fallback stays distinguishable from an explicit choice,
  and getTileSpec() resolves the legacy branch per theme. Precedence for
  both themes is now browser choice > configured provider default >
  legacy URL > built-in fallback. A raw URL keeps id:null, so it borrows
  no vendor credit, reference overlay or invert filter.
- MC_applyTileFilter() resolves through getTileSpec() too: an operator's
  own dark tileset must not be CSS-inverted on top of already being dark.
- roles.js: record MC_DARK_TILE_URL_OVERRIDE from tiles.dark /
  map.tiles.darkUrl, mirroring the light override.
- config.example.json: document the four legacy raw-URL keys, their
  precedence, the generic credit they get, and their zoom ceiling.

Configuring a provider by id (darkDefault/lightDefault) is unchanged and
still takes priority, so installs that configure providers explicitly
resolve through the registry exactly as before.

Relates to #390.
@adminopenclaw8-sketch

Copy link
Copy Markdown
Collaborator Author

Rapport — CS-Macmini PR#399 #390 — head eb44231

Status: All three issue points addressed — point 1 fixed in code, points 2 and 3 documented with the configurable variants proposed under "Left for the owner"; every point has a test red before and green after, each with at least one mutant shown red; all CI jobs green on the first run.

Evidence tags: [T] test/tool run, [A] architecture/code reasoning, [K] known/accepted.

Rechecked master first: ce29f5dc. The branch is one regular commit on top of it, author and committer dborup <kontakt@meshview.dk>. [T]

Per requirement

# Requirement (from the issue) Resolution Test Mutant shown red
1 Dark legacy override is still masked — mirror the #366 precedence for tiles.dark / map.tiles.darkUrl Fixed. getSelectedDarkId() added as the mirror of getSelectedLightId(); getTileSpec() resolves the legacy branch per theme; roles.js records MC_DARK_TILE_URL_OVERRIDE test-issue-390-legacy-tile-followups.js P1, 21 cases, both config shapes [T] legacy dark branch removed from getTileSpec() → 11 red; roles.js stops reading map.tiles.darkUrl → 6 red [T]
1a Precedence: explicit choice > configured default > legacy URL > built-in fallback Fixed, per theme. An explicit choice of the built-in id itself still beats the legacy URL P1 browser-choice (carto-dark, osm-dark), darkDefault, MC_setServerDefaultTileProvider and invalid-choice cases [T] !selected guard dropped → 6 red; getSelectedDarkId() returns the fallback id instead of null → 11 red [T]
1b A legacy dark tileset must not be CSS-inverted on top of being dark Fixed. MC_applyTileFilter() resolves through getTileSpec() instead of REGISTRY[getActiveId()] P1 "leaves the tile pane filter cleared" (both shapes) [T] MC_applyTileFilter() reverted to the REGISTRY lookup → 2 red [T]
1c A raw URL borrows no vendor metadata (credit, reference overlay, invert filter) Fixed. Legacy spec keeps id: null, so getActiveTileProvider() returns null for dark too P1 inset case: runs the real nodes.js _applyTilesToNodeMap with synthetic unused metadata on osm-dark [T] covered by the branch-removal mutant above (the inset cases are among the 11) [T]
2 Attribution for legacy raw URLs — "allow an override, or document it" Documented (the issue's second branch). config.example.json map.tiles._comment_legacy states the generic credit, the precedence and that a provider id is the way to get a correct vendor credit. The override itself would be new config → proposed, not implemented P2, 2 cases: the note names all four legacy keys, and the documented credit equals what the resolver returns in both themes [T] generic credit removed from the note → 1 red [T]
3 Zoom cap — "consider a configurable max zoom" Documented in the same note; the configurable cap would be new config → proposed, not implemented P3: the documented ceiling is pinned to MC_getTileSpec(theme).maxZoom for a legacy URL, both themes [T] note changed to "capped at zoom 19" → 1 red [T]
— Real browser, both themes, both config shapes 10 new steps added to the existing tile E2E test-issue-332-no-carto-tiles-e2e.js #390 steps, run against a local Go server with e2e-fixture.db prepared as in CI [T] legacy dark branch removed, source rebuilt and server restarted → 6 red (the dark steps; the light steps stayed green, so the E2E is not passing vacuously) [T]

Why points 2 and 3 were not implemented as config

Both are phrased as "consider" and both would add new operator-facing settings, so per the brief they are written up under "Left for the owner" in the PR body rather than built. [A]

  • An attribution override is rendered into the Leaflet attribution control, and the registry's own attribution strings contain real <a> markup, so the value needs an escaping decision before it can be accepted from config. Not small, not obvious.
  • Raising the legacy zoom ceiling unconditionally is not safe either: a provider that stops at z18 would then be asked for z19 and render blank tiles with no on-screen explanation. The ceiling has to come from the operator rather than be guessed, which is again new config.

Both proposals name a concrete config shape in the PR body so the owner can decide.

Tests

Suite Result
node test-issue-390-legacy-tile-followups.js (new) 24 passed / 0 [T]
node test-issue-332-tile-provider-fallback.js 71 passed / 0 [T]
node test-issue-1420-tile-providers.js 39 passed / 0 [T]
node test-frontend-helpers.js 712 passed / 0 [T]
sh test-all.sh 236 of 237 files passed [T]
cmd/server go test ./... ok 40.8s [T]
test-issue-332-no-carto-tiles-e2e.js (local Go server on a free high port, fixture freshened → migrated → seeded 2073/199/245 as in CI) 31 passed / 0, including the 10 new #390 steps [T]

The single test-all.sh failure is test-backup-sqlite-safety.js: 9 cases, all reporting "missing tool setsid". That is a macOS-only gap on this machine, not a regression — the file is untouched by this branch and the same 9 cases fail on plain master here. They pass in CI, where setsid exists. [T][K]

Visual check, both themes

Chromium against the local server, /#/map, four states, reading the computed tile-pane filter and the on-map credit: [T]

State Resolved id Tile-pane filter Credit
dark + legacy dark URL null none — operator tiles rendered as supplied generic OSM credit
dark + no legacy config osm-dark the osm-dark invert filter generic OSM credit
light + legacy dark URL light provider unchanged none unchanged
light + no legacy config light provider unchanged none unchanged

Screenshots confirmed the dark page renders the operator's own tiles un-inverted, and that the default dark map still gets its invert filter.

CI per job (run on head eb44231a)

Job Result
Go Build & Test success [T]
Playwright E2E Tests success [T]
Build & Publish Docker Image success [T]
Release Artifacts skipped (expected, non-release PR)
Deploy Staging skipped (expected)
Publish Badges & Summary skipped (expected)

Green on the first run — no rerun needed, and the known-unstable test from #271 was not hit. [T]

Hygiene

  • cmd/server untouched: no Go file is in the diff at all, so no new map[string]interface{} outside tests. [T]
  • No hardcoded colours. The only new literal in the resolver is an attribution string, hoisted into a shared LEGACY_ATTRIBUTION constant so the code and the documentation cannot drift apart. [A]
  • sh scripts/check-xss-sinks.sh --diff origin/master → exit 0. [T]
  • npx eslint public/*.js → 0 errors (89 pre-existing warnings, none in the touched files' new lines). [T]
  • Fork guards unchanged: 9 in deploy.yml, 1 in release-fast-path.yml; no workflow file is in the diff. [T]
  • No new config keys, no new dependencies. Title carries no closing keyword; the body opens with "Relates to Follow-ups to #366: dark legacy tile override, legacy attribution, zoom cap #390". PR is a draft. [T]

Compatibility note

The default provider for existing installs is unchanged. An install that selects its providers by id (darkDefault / lightDefault, which is how our deployments are configured) makes getSelectedDarkId() / getSelectedLightId() non-null and therefore never enters the legacy branch — the registry resolves exactly as before, with the proper vendor credit. [A] The behaviour change is confined to installs that set a raw URL and no provider id, which is precisely the case the issue reports as broken. MC_getDarkTileProvider() still returns a registry id in every case, so the layer control and the customizer picker are unaffected — pinned by its own test case. [T]

Residuals

  • The legacy branch still prefers cfg.tiles over cfg.map.tiles when both config shapes are set at once, for dark as for light. Unchanged from the TILE_DARK / TILE_LIGHT logic directly above it, so not a regression; not separately tested. [K][A]
  • map.js and live.js attach their tile layer with a hard-coded maxZoom: 19 and ignore spec.maxZoom entirely, so the legacy zoom ceiling only reaches the customizer preview maps in practice. Pre-existing and outside this issue; worth a separate look if the owner takes up the configurable-cap proposal. [A]
  • The layer-control picker lists registry providers only, so a legacy URL is reachable through "Auto" but is not itself a selectable entry. Pre-existing and matches the light behaviour shipped in fix(maps): preserve legacy light tile URL overrides (#364) #366. [K][A]

@dborup

dborup commented Oct 13, 2026

Copy link
Copy Markdown
Owner

Review — CS-Minimax PR#399 — head eb44231

Dom: APPROVE with nits

Independent read-only review against origin/master ce29f5dc. The branch is one commit on top of it, author and committer dborup <kontakt@meshview.dk>; git merge-tree --write-tree produces a tree byte-identical to head, so "merged tree" and "head" are the same thing here. git ls-remote returned eb44231a before and after the review. [T]

Evidence tags: [T] test/tool run, [A] code reasoning, [K] known/accepted.

Findings

# Severity Where Finding
1 nit config.example.json map.tiles._comment_legacy The note tells operators a legacy raw URL "is capped at zoom 18". Measured against a local server, /#/map requests the legacy host at z19. The documented cap is not the effective cap on the primary map surfaces.
2 nit public/map-tile-providers.js getSelectedDarkId() The REGISTRY[stored].type === 'dark' guard became load-bearing for a new decision (whether the legacy URL applies), but nothing covers it — my mutant survives all 237 test files.
3 observation config.example.json _comment_legacy (707 chars) is forwarded verbatim to every browser via /api/config/client. Precedent exists (_comment_defaults, 410 chars), so this is consistent, not new.
4 observation test-issue-390-legacy-tile-followups.js P2 The "documented credit equals the resolver's credit" assertion is near-tautological; the real coverage comes from a different case. Adequate as shipped.

Neither nit blocks: point 1 is a one-clause wording fix, point 2 is pre-existing logic that was relocated verbatim. No behaviour regresses.

Finding 1 — detail

map.js:408 and live.js:1603 attach their base tile layer with a hard-coded maxZoom: 19 and never read spec.maxZoom; the only consumer of the resolved ceiling is customize-v2.js:1786 (the customizer preview). [A] Measured on a local server with tiles.dark set to a raw URL, dark theme, zooming /#/map to its deepest level: [T]

MC_getTileSpec("dark").maxZoom (what the note documents): 18
legacy-host zoom levels actually requested on /#/map:     [10,...,17,18,19]
deepest z requested from the legacy URL:                  19

The P3 test pins the note against MC_getTileSpec(theme).maxZoom, so it can only ever confirm the note matches the spec — it cannot catch the note disagreeing with what the maps do. Two consequences worth the owner's attention:

  • The shipped note is the artefact that resolves issue point 3, and it currently overstates the restriction.
  • Issue point 3's own premise ("a custom provider that supports z19 is clipped one level") is therefore only true for the customizer preview. On the main maps a z19-capable custom provider is already asked for z19, which weakens the case for the proposed configurable cap.

Suggested fix: add a clause to the note (e.g. the resolved spec declares a ceiling of 18, while /#/map and /#/live currently attach tiles at zoom 19 regardless), or widen the P3 assertion to the ceiling the maps actually use. The author did disclose this under "Residuals" in the PR report — the gap is that the disclosure did not reach the operator-facing note. [K]

The hard-coded 19 itself is pre-existing and outside this issue: neither map.js nor live.js is in the diff.

Answers to the issue points

Point 1 — dark legacy override is still masked. Fixed, and the fix reaches the real maps: both map.js:_resolveTileUrl and live.js:_liveResolveTile prefer MC_getTileSpec, so URL and attribution come from the new per-theme branch. [A] getActiveId() becoming getSelectedDarkId() || DEFAULT_ID is a faithful refactor — getSelectedDarkId() only returns _hasId()-verified ids and MC_initTileRegistry guarantees DEFAULT_ID is in the registry, so the BASE_STYLES fallback inside getTileSpec is unreachable on this path and MC_applyTileFilter's switch to getTileSpec changes nothing for the non-legacy case. [A]

  • 1a precedence (explicit choice > configured default > legacy URL > built-in fallback): held per theme, including an explicit choice of the built-in id itself. [T]
  • 1b not CSS-inverted: held. [T]
  • 1c borrows no vendor metadata: held — id: null, generic credit, no reference overlay. [T]

Point 2 — attribution for legacy raw URLs. Honestly taken as the issue's own "or document it" branch, with the override written up under "Left for the owner". The stated reason is real: the registry's own attribution strings contain live <a> markup, so accepting a value from config needs an escaping decision first. I confirmed the proposal is genuinely unimplemented — darkAttribution / lightAttribution / darkMaxZoom / lightMaxZoom appear nowhere in the tree. [T] Documented instead, with a test red before. Accepted.

Point 3 — zoom cap. Likewise moved to "Left for the owner" with a sound reason (raising the ceiling unconditionally would ask a z18-only provider for z19 and render blank tiles). Documented, test red before. Accepted, subject to finding 1 above.

No scope creep: the diff adds no config key that any code reads (_comment_legacy is inert), no new dependency, no workflow change. The only new literal is an attribution string hoisted into a shared constant so the code and the note cannot drift.

Compatibility — the default provider is unchanged

Browser check against a local Go server, /#/map, reading the resolved spec, the tile hosts actually requested, the computed tile-pane filter and the on-map credit: [T]

State spec.id Tile host requested Pane filter MC_getDarkTileProvider()
dark + no legacy config osm-dark tile.openstreetmap.org invert applied osm-dark
light + no legacy config osm-standard tile.openstreetmap.org none osm-dark
dark + tiles.dark null legacy host none osm-dark
light + tiles.dark only osm-standard tile.openstreetmap.org none osm-dark
dark + map.tiles.darkUrl null legacy host none osm-dark
light + map.tiles.lightUrl null legacy host none osm-dark

Both config shapes behave identically, the legacy dark URL never leaks into light, the light legacy path shipped in #366 still works, and MC_getDarkTileProvider() keeps returning a resolvable registry id in every state — so the layer control and the customizer picker are unaffected. An install that selects providers by id never reaches the legacy branch at all. [T][A]

Tests

Everything below run by me on the merged tree.

Suite Result
node test-issue-390-legacy-tile-followups.js (new) 24 passed / 0 [T]
node test-frontend-helpers.js 712 passed / 0 [T]
sh test-all.sh 236 of 237 files [T]
cmd/server go test ./... ok 48.0s [T]
cmd/ingestor go test ./... ok 110.1s [T]
test-issue-332-no-carto-tiles-e2e.js vs local server 31 passed / 0, incl. the 10 new #390 steps [T]
test-issue-332-tile-provider-fallback.js / -1420- / -1614- 71 / 39 / 3, all 0 failed [T]
scripts/check-xss-sinks.sh --diff origin/master exit 0, having really scanned both changed public/ files [T]

The one test-all.sh failure is test-backup-sqlite-safety.js, 9 cases, every one reporting missing tool setsid. Independently corroborated: the file is not in the diff, and my own attempt to start a background server on this machine failed with command not found: setsid. Environmental on macOS, not a regression. [T][K]

E2E prepared as in CI: freshen, the Kpa-clawbot#1486/Kpa-clawbot#1791 seed SQL, corescope-migrate, then seeds 2073, 199 and 245, served by a locally built corescope-server on port 13701, stopped by port lookup. Known-flaky #256 and #267 were not hit.

Red before / green after. Taking the new tests onto master's source: test-issue-390-legacy-tile-followups.js goes 14 failed / 10 passed, green on head. Every issue point is red before — P1 "overrides the implicit dark fallback", P1 "leaves the tile pane filter cleared" and P1 "inset without borrowing metadata" for both config shapes, plus both P2 cases and P3. The 10 that pass on master are the precedence and no-regression guards, which is correct. The E2E goes 6 failed / 25 passed on master's source with the server restarted, and the light steps stay green — so the new steps are neither vacuous nor over-broad. [T]

My own mutants

Five, applied to head and reverted from a scratch snapshot each time (never via git checkout --).

Mutant Result
M1 — legacy spec stops inheriting the fallback's ceiling (maxZoom: 19 literal) 1 red — P3. The doc↔code pin is real. [T]
M2 — getSelectedDarkId() drops the dark-type guard on the stored choice survives — 0 red in the #390, #332, Kpa-clawbot#1420 and Kpa-clawbot#1614 suites, and 0 red across all 237 test-all.sh files. Finding 2. [T]
M3 — roles.js ignores the top-level tiles.dark shape 5 red — both config shapes are genuinely distinguished, not just aliased. [T]
M4 — the !selected guard dropped, so the legacy URL outranks an explicit choice 6 red — the precedence chain is pinned in both directions. [T]
M5 — legacy spec borrows the fallback provider's vendor credit 1 red — only the case with a configured OSM vendor separates the two strings, since _getOsmAttribution() otherwise returns the identical generic credit. Finding 4; coverage is adequate because that case exists. [T]

Hygiene

  • cmd/server read-only — no Go file in the diff at all, so no new map[string]interface{} outside tests. [T]
  • No hardcoded colours; the diff's #364 / #390 tokens are issue references. [T]
  • scripts/check-xss-sinks.sh --diff origin/master clean, verified git-aware so the result is not a vacuous "nothing to scan". [T]
  • Fork guards: 9 in deploy.yml, 1 in release-fast-path.yml; no workflow file in the diff. [T]
  • No closing keyword in title or body; the body opens "Relates to Follow-ups to #366: dark legacy tile override, legacy attribution, zoom cap #390". [T]
  • CI on eb44231a: Go Build & Test, Playwright E2E and Docker Image all success; Release Artifacts, Deploy Staging and Publish Badges skipped as expected for a non-release PR. Green with no rerun. [T]

Not verified

  • Staging and production: out of scope and not accessed.
  • Real third-party tile rendering — tile images are stubbed with a 1x1 PNG by design, so this review checks which URL is requested, not what a vendor returns.
  • Pixel-level visual comparison: I read the computed tile-pane filter, the resolved spec, the requested hosts and the credit text rather than diffing screenshots.
  • The remaining internal/* Go modules and the cmd/migrate / cmd/decrypt suites — the diff contains no Go, so I ran cmd/server and cmd/ingestor only.
  • Accessibility / axe passes, and the Docker image beyond CI's own job result.
  • The residual the author flagged where cfg.tiles outranks cfg.map.tiles when both shapes are set at once: unchanged from the light-side logic, so I confirmed it is not a regression but did not test it. [K]

@dborup
dborup marked this pull request as ready for review October 13, 2026 07:30
@dborup
dborup merged commit 1aaa41c into master Oct 13, 2026
6 checks passed
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.

2 participants