Skip to content

Design notes: Resource Resolver in core (ADR 0004/0005) + implementation handoff - #84

Merged
xinaesthete merged 3 commits into
mainfrom
claude/codebase-architecture-refactor-36013b
Jul 14, 2026
Merged

Design notes: Resource Resolver in core (ADR 0004/0005) + implementation handoff#84
xinaesthete merged 3 commits into
mainfrom
claude/codebase-architecture-refactor-36013b

Conversation

@xinaesthete

@xinaesthete xinaesthete commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Design notes only — no source changes. Two ADRs, a CONTEXT.md update, and an implementation handoff, ready to pick up.

Why

CONTEXT.md already separates the Resource Resolver ("store-agnostic … stable loaded resources for renderers") from the Renderer Adapter ("avoid: state store"), and says a Spatial Entry is resolved by the former before the latter runs. The code drifted from that: the Resolver doesn't exist as a module, and its responsibilities are split between a 1,873-line React hook in vis and a 940-line engine in layers — both above the deck.gl seam.

The decisive evidence isn't architectural taste. tgpu-htj2k renders SpatialData imagery through three.js/WebGPU today, depends on @spatialdata/core + zarrextra, and excludes layers/vis/avivatorish by name (its ADR-0010: "No deck.gl / React enters the render path"). It reached into core for a resolution layer, found only readZarr plus element discovery, and hand-rolled the whole thingSelect, Resolve, Tileset, TileCache, loadScheduler, Nyquist LOD. Ten files, with tests.

A second Resource Resolver already exists, because the first one was locked behind deck.gl. The duplication is being paid for now, in another repo.

What's decided

ADR 0004 — Resource Resolver Owned By Core

  • The Resolver lives in core. Per-kind resolvers behind one interface, not one engine.
  • Resolution<T> and SpatialEntryError are core types. Failure is per-resource, not per-entry.
  • The deck Renderer Adapter stays in layers — identity-stable memoisation is a deck requirement.
  • RenderStack moves to core (dependency direction forces it).
  • No runtime dependency (Effect, neverthrow, TanStack) in core's interface — core is also tgpu-htj2k's dependency root.
  • Group Entry / blend compositing is out of scope. Add no framebuffer hook in anticipation.

Amends ADR 0001's package-placement claim only. Its substance stands: host overlays as descriptors, no parallel layerOrder state, MobX outside the contract.

ADR 0005 — Memory Accounting Before Management

Both ingest paths have the same two-tier shape, and neither is managed:

encoded decoded
zarr nothing exists fizarrita's ChunkCache seam — empty
parquet parquetTableBytes — unbounded, never evicted parquetTableCache — unbounded, never evicted

Parquet holds both tiers for the same file, forever. Zarr holds neither — fizarrita has no cache, only a cache-shaped hole we never fill, so every tile re-fetches and re-decodes. The only eviction machinery in the repo is PointsDataEngine.evict(), keyed on element unload.

Decided as a ladder with an explicit stop line: adopt the byteLength scalar, bound the two unbounded caches, fill the empty chunk seam — then stop. The encoded tier and the Resource Ceiling wait for measurement, following tgpu-htj2k, which built and unit-tested selectWithinBudget and then never called it.

What's sequenced

Resource Resolver — implementation handoff

Step 0 (types) and Step 1 (interface + four thin behaviour-identical adapters) are shared and land first. Step 1 is the fork point — after it, three tracks touch different files:

  • Track A — points state model. RequestSlot replaces four hand-rolled supersede implementations whose guards compare values (a memory cap, a signature string) instead of request identity. Four live races are named with their triggers; none is reachable by the existing 845-line engine spec. Carries the Effect spike, scoped to the matching scan only, with a kill criterion agreed up front.
  • Track B — shapes. The loader seam ADR 0003 already specifies, then a flat transferable batch. Needs nothing from Track A — viewport loading lives behind loadInBounds() inside deck's TileLayer, not in the resolver. Can start immediately.
  • Track C — memory. ADR 0005 rungs 1–3. Leak fixes, not architecture.

Definition of done is written as invariants that fail loudly on drift: no react in core, no 'use no memo' left in vis, the resolver exercised by a test that constructs no GL context.

Also here

  • CONTEXT.md gains Resolution, Spatial Entry Error, Entry Notice, Encoded/Decoded Tier, Resource Ceiling; Resource Resolver and Renderer Adapter sharpened.
  • docs/plans/layer-data-engine-decomposition.md is superseded — its diagnosis stands, its target home was one package too high, and its open question 2 is now answered (per-kind).
  • Four bugs found on the way in, recorded in ADR 0005. The one worth acting on soonest: fizarrita's probeDecompressedSize doesn't recognise imagecodecs_jpeg2k or HTJ2K, so for JP2K-backed images it hands the compressed byte length to inferChunkShape as the decompressed size. That's our actual imagery, and it's owed upstream.

Review notes

The interface behind ADR 0004 was designed four ways in parallel under four different constraints (minimise the interface; maximise extensibility; optimise for the caller; error-as-value spine). All four independently produced per-kind resolvers with per-resource resolutions — that convergence is the evidence for decisions 2 and 3, not my preference.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Documentation
    • Added an architecture decision record defining a framework-independent Resource Resolver and its ownership of caching, cancellation, streaming, eviction, and resource resolution.
    • Clarified the separation between resource resolution and renderer output transformation.
    • Documented resolution states, typed entry failures, notices, memory tiers, and resource limits.
    • Added an implementation handoff plan covering resolver phases, memory accounting, and renderer adapter improvements.
    • Marked the previous layer-data engine plan as superseded.

xinaesthete and others added 2 commits July 14, 2026 16:53
…ing it

CONTEXT.md already separates the Resource Resolver ("store-agnostic ... stable
loaded resources for renderers") from the Renderer Adapter ("avoid: state
store"), and says a Spatial Entry is resolved by the former before the latter
runs. The code drifted: the resolver doesn't exist as a module, and its
responsibilities are split between a 1,873-line React hook in vis and a 940-line
engine in layers — both above the deck.gl seam.

The decisive evidence is not architectural taste. tgpu-htj2k renders SpatialData
imagery through three.js/WebGPU today, depends on @spatialdata/core + zarrextra,
and excludes layers/vis/avivatorish by name. It reached into core for a
resolution layer, found only readZarr plus element discovery, and hand-rolled
Select / Resolve / Tileset / TileCache / loadScheduler / LOD — ~10 files with
tests. A second Resource Resolver already exists, because the first was locked
behind deck.gl.

ADR 0004 — Resource Resolver Owned By Core:
- the Resolver lives in core; per-kind resolvers behind one interface
- Resolution<T> and SpatialEntryError are core types; failure is per-resource
- the deck Renderer Adapter stays in layers (identity-stable memoisation is a
  deck requirement and belongs on the renderer side)
- RenderStack moves to core, forced by dependency direction
- no runtime dependency (Effect, neverthrow, TanStack) enters core's interface
- Group Entry / blend compositing explicitly out of scope; add no framebuffer
  hook in anticipation

Amends ADR 0001's package-placement claim only. Its substance stands: host
overlays as descriptors, no parallel layerOrder state, MobX outside the contract.

ADR 0005 — Memory Accounting Before Management:
Both ingest paths have the same two-tier shape and neither is managed. Parquet
holds encoded bytes AND the decoded Arrow table for the same file, unbounded,
forever. Zarr holds neither: fizarrita has no cache, only a cache-shaped hole we
never fill, so every tile re-fetches and re-decodes. The only eviction machinery
in the repo is PointsDataEngine.evict(), keyed on element unload.

Adopt in rungs: the byteLength scalar, bound the two unbounded caches, fill the
empty chunk seam. Defer the encoded tier and the Resource Ceiling until
measurement justifies them — following tgpu-htj2k, which built and tested
selectWithinBudget and then never called it.

CONTEXT.md gains: Resolution, Spatial Entry Error, Entry Notice, Encoded/Decoded
Tier, Resource Ceiling. Resource Resolver and Renderer Adapter sharpened.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ngine plan

Sequencing for ADR 0004/0005, ready to pick up.

Step 0 (types only) and Step 1 (the resolver interface + four thin
behaviour-identical adapters) are shared and land first. Step 1 is the fork
point: after it, three tracks touch different files and can run in parallel.

  Track A — points state model. RequestSlot replaces four hand-rolled
    dedup/supersede implementations whose guards compare values (a memory cap, a
    signature string) instead of request identity. Four live races are named with
    their triggers; none is reachable by the existing 845-line engine spec,
    because PointsEntry is private and nothing can hand it two deferred runs.
    Carries the Effect spike, scoped to the matching scan only, behind the slot
    interface, with a kill criterion agreed up front.

  Track B — shapes. The loader seam ADR 0003 already specifies, then a flat
    transferable batch. Needs nothing from Track A: viewport loading lives behind
    loadInBounds() inside deck's TileLayer, not in the resolver. Starts
    immediately.

  Track C — memory, ADR 0005 rungs 1-3. Leak fixes, not architecture.

Definition of done is written as invariants that fail loudly on drift: no react
in core, no 'use no memo' left in vis, the resolver exercised by a test that
constructs no GL context.

The LayerDataEngine plan is superseded. Its diagnosis stands and is still worth
reading — the god-hook, the unreachable-headless problem — but its target home
was one package too high, and its open question 2 ("one engine or per-type
sub-engines?") is now answered: per-kind.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@xinaesthete, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 36 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cd759faa-0ceb-4773-bac0-950f21be012e

📥 Commits

Reviewing files that changed from the base of the PR and between 6870a13 and b615d97.

📒 Files selected for processing (5)
  • CONTEXT.md
  • docs/adr/0001-render-stack-owned-by-layers.md
  • docs/adr/0004-resource-resolver-owned-by-core.md
  • docs/adr/0005-memory-accounting-before-management.md
  • docs/plans/resource-resolver-handoff.md
📝 Walkthrough

Walkthrough

Documentation updates establish Resource Resolver ownership in @spatialdata/core, define resolution and memory-accounting vocabulary, amend related architecture guidance, supersede an earlier decomposition plan, and add a phased implementation handoff.

Changes

Resource Resolver architecture

Layer / File(s) Summary
Core ownership and resolution contracts
CONTEXT.md, docs/adr/0001-render-stack-owned-by-layers.md, docs/adr/0004-resource-resolver-owned-by-core.md
Defines core-owned, renderer-agnostic resource resolution, resolution outcomes, failure metadata, and the corrected RenderStack location.
Architecture consequences and boundaries
docs/adr/0004-resource-resolver-owned-by-core.md
Records renderer-adapter boundaries, package impacts, out-of-scope areas, and resulting architectural consequences.
Memory accounting direction
docs/adr/0005-memory-accounting-before-management.md
Proposes byte-level accounting, bounded cache work, resolver authority, implementation constraints, and deferred resource-ceiling decisions.
Resolver implementation handoff
docs/plans/resource-resolver-handoff.md, docs/plans/layer-data-engine-decomposition.md
Sequences resolver phases and implementation tracks, defines completion criteria, records exclusions, and marks the prior decomposition plan superseded.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the PR’s main design-doc changes: Resource Resolver ownership in core, ADR 0004/0005, and the implementation handoff.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/codebase-architecture-refactor-36013b

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CONTEXT.md`:
- Around line 63-65: The Resolution contract must define the fallback when stale
is unavailable, so qualify the “never blanks” guarantee to cover only retained
stale values and specify the behavior for eviction or non-retryable failure
without stale data. Update CONTEXT.md at lines 63-65 and
docs/adr/0005-memory-accounting-before-management.md at lines 178-181 so the
documented eviction policy and Resolution semantics agree; both sites require
documentation changes only.

In `@docs/adr/0001-render-stack-owned-by-layers.md`:
- Around line 3-10: Update the retained ADR paragraph to state that the
canonical RenderStack lives in `@spatialdata/core`, matching the ADR 0004
amendment, while preserving the remaining rendering and overlay contract
statements.

In `@docs/adr/0004-resource-resolver-owned-by-core.md`:
- Around line 72-75: Clarify the ADR’s ownership boundary for viewport-driven
loading: state that the deck.gl adapter only supplies viewport state to the core
Resource Resolver, while core initiates and reconciles requests, including
supersession, cancellation, caching, and eviction. Update the corresponding
out-of-scope section as well so it does not imply that TileLayer owns request
reconciliation.
- Around line 95-100: Clarify the RenderStack export migration contract in the
ADR: explicitly state whether the existing `@spatialdata/layers` and
`@spatialdata/vis` re-exports remain as compatibility shims or are removed after
`@spatialdata/core` becomes canonical. Include the status of RenderStack and its
persistence schemas, and align the migration wording with the chosen behavior.

In `@docs/plans/resource-resolver-handoff.md`:
- Around line 14-27: Add the text language identifier to the Markdown fence
surrounding the architecture diagram, changing the opening fence to ```text
while preserving the diagram content and closing fence.
- Around line 47-64: Update Step 0 in the resource resolver handoff plan to
reflect that it contains runtime behavior as well as shared types: either rename
it to shared contracts or move the implementations of toSpatialEntryError() and
fromResult() into a later behavior step, while keeping the Resolution and error
type definitions in the initial shared step.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5667df37-0f56-48fa-b7a3-dea61ed0db03

📥 Commits

Reviewing files that changed from the base of the PR and between d8fa44b and 6870a13.

📒 Files selected for processing (6)
  • CONTEXT.md
  • docs/adr/0001-render-stack-owned-by-layers.md
  • docs/adr/0004-resource-resolver-owned-by-core.md
  • docs/adr/0005-memory-accounting-before-management.md
  • docs/plans/layer-data-engine-decomposition.md
  • docs/plans/resource-resolver-handoff.md

Comment thread CONTEXT.md Outdated
Comment thread docs/adr/0001-render-stack-owned-by-layers.md Outdated
Comment on lines +72 to +75
1. **The Resource Resolver lives in `@spatialdata/core`.** Framework-free: no
React, no deck.gl, no Viv. It owns the cache, request supersession,
cancellation, streaming partials, eviction, entry resolution
(element + transform to the active coordinate system), and world bounds.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Clarify who owns viewport-driven request reconciliation.

The decision assigns supersession, cancellation, caching, and eviction to the renderer-agnostic core resolver, but the out-of-scope section leaves viewport-driven loading inside deck's TileLayer. If that layer still initiates or owns request reconciliation, the resolver is not the shared lifecycle owner described here. State that the adapter only supplies viewport state to core, or narrow the ownership claim.

Also applies to: 154-159

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/adr/0004-resource-resolver-owned-by-core.md` around lines 72 - 75,
Clarify the ADR’s ownership boundary for viewport-driven loading: state that the
deck.gl adapter only supplies viewport state to the core Resource Resolver,
while core initiates and reconciles requests, including supersession,
cancellation, caching, and eviction. Update the corresponding out-of-scope
section as well so it does not imply that TileLayer owns request reconciliation.

Comment thread docs/adr/0004-resource-resolver-owned-by-core.md
Comment thread docs/plans/resource-resolver-handoff.md Outdated
Comment thread docs/plans/resource-resolver-handoff.md Outdated
… shims

The sharpest review comment and the sharpest human one turned out to be the same
one, from opposite directions: ADR 0004 assigned supersession, cancellation,
caching and eviction to a renderer-agnostic core resolver, then left
viewport-driven loading inside deck's TileLayer. Those contradict.

Resolved by saying out loud that tile scheduling is renderer-owned, by design:

  element-level lifecycle (preload, catalog, row codes, geometry, tooltip,
    fill colour) -> Resource Resolver, in core. Identical for every renderer;
    duplicating it is what tgpu-htj2k was forced to do.

  tile-level lifecycle (which tiles, what LOD, when to abort on pan)
    -> Renderer Adapter. deck's TileLayer and tgpu-htj2k's Select +
    loadScheduler implement different, correct policies for different viewports
    and budgets. A renderer-neutral tile scheduler would serve neither.

  the loader facet (capabilities, loadInBounds) and the byte-level cache
    -> core, shared. Both tile schedulers call them.

Adds an explicit Non-goals section, because "deck-agnostic resolver" was reading
as "deck-agnostic render path", which it is not:

- A renderer-neutral render abstraction is NOT a goal. The adapter seam exists so
  the deck adapter can lean all the way into deck-geoarrow and viv, not so we can
  reimplement them. (ADR 0003 already said this.)
- Batch representation is decided per encoding, not by policy. Delegate where an
  existing deck layer can consume the encoding; hand-roll flat typed arrays where
  it cannot — both are expected outcomes, and the strategy registry is already the
  dispatch. Wild-type shapes are WKB, not GeoArrow, so a decode is unavoidable;
  points are x/y columns, not encoded geometry, so GeoArrowScatterplotLayer buys
  nothing. The only requirement on a batch is that it be transferable across the
  worker seam and not allocate one JS object per vertex.
- This ADR does not decide viv-vs-own-renderer for images. That is a Renderer
  Adapter question. zarrextra's VivCompatiblePixelSource already serves both viv
  and tgpu-htj2k, so the shared image seam already exists below the resolver. A
  renderer-agnostic resolver buys the 3D option without spending it.

Also from review:

- Resolution.stale is a retention, not a guarantee. CONTEXT.md promised a failed
  refine "never blanks a working view" while ADR 0005 permitted dropping stale on
  eviction and non-retryable failure. Both now say the same thing: the no-blank
  behaviour holds while stale is retained; once released the resource is not
  renderable and the UI shows the error. Callers must handle the no-stale case.
- RenderStack migration contract stated: core becomes canonical; layers and vis
  retain re-exports as compatibility shims. No consumer import moves. Removing the
  shims is a separate deprecation, coordinated with MDV.
- Handoff Step 0 renamed "shared contracts" — it does carry toSpatialEntryError()
  and fromResult(), which are inseparable from the types.
- ADR 0001's body marked historical inline, so a reader who skims past the
  amendment banner is not misdirected. The body is not rewritten: an amended ADR
  is a historical record.
- Fence language on the architecture diagram.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@xinaesthete
xinaesthete merged commit 03f5858 into main Jul 14, 2026
4 checks passed
@xinaesthete
xinaesthete deleted the claude/codebase-architecture-refactor-36013b branch July 14, 2026 16:58
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.

1 participant