Skip to content

TDD-driven fixes from comprehensive review: 14 bugs, 2 optimizations - #8

Merged
TheGreatAlgo merged 43 commits into
mainfrom
review/tdd-fixes
Jul 21, 2026
Merged

TDD-driven fixes from comprehensive review: 14 bugs, 2 optimizations#8
TheGreatAlgo merged 43 commits into
mainfrom
review/tdd-fixes

Conversation

@Faolain

@Faolain Faolain commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR lands the full output of a comprehensive multi-agent review of dclimate-client-js: 14 proven bug fixes and 2 performance optimizations, every one implemented test-first. Three of the bugs disabled the library's main spatial-selection APIs outright; four more started as "worth a human look" suspicions and were proven red before fixing.

Each item went through a four-role TDD pipeline:

  1. Red — Codex: a failing test asserting the correct behavior, verified to fail inside src/ at the claimed defect (not from test scaffolding), committed before any fix.
  2. Green — Codex: a tightly-scoped fix; full vitest suite + eslint + both tsconfig typechecks run to log files under .tdd-logs/<slug>/; committed only after independent re-verification.
  3. Grok review: an unprimed second-model review of each item's diff.
  4. Claude Fable adversarial review: attempts to refute each change with probe tests executed against the worktree (differential tests vs the pre-fix code, 500-case fuzzing, live checks against production endpoints), adjudicating Grok's findings; must-fix issues triggered a review: fixup round with regression tests.

Gate: 169/169 tests green (28 files), eslint clean, tsc clean for both node and browser configs. Baseline before this PR: 112 tests. The 57 new tests are red-phase reproductions and review-driven regressions under tests/review-fixes/.

The review pipeline earned its keep — several items had their first fix rejected by review and repaired in a fixup round, including one where the fix passed its red test but was proven broken against the production STAC server's actual pagination protocol.

Dependency note: @dclimate/jaxray is pinned to the in-progress dClimate/jaxray#7 branch (review/tdd-followups, 0.6.11); several fixes rely on its exports (cfTimeToDate, lazy-block machinery). Re-pin to the released version once that PR merges.

Bug fixes

Critical — the spatial-selection APIs were broken

Fix What was wrong
points-vectorized points() passed DataArray objects as sel values expecting xarray-style vectorized selection, which jaxray silently ignores → the full grid came back with integer indices in place of lat/lon coords, and the snapToGrid branches were swapped (true did exact match, false did nearest). Now performs real per-point nearest-neighbor selection along a point dimension; tolerance misses raise NoDataFoundError.
circle-real-datasets Three independent failures: with a time dim the validity scan isNaN()'d whole arrays and returned an empty dataset; on lazy zarr-backed data (the client's actual load path) where() threw; and the trim was dead code (where fills null, isNaN(null) === false) so even eager 2D calls returned the untrimmed globe. Rewritten: bbox pre-filter → isel → haversine mask over bbox cells only → mask-derived trim → where last (also the review's measured 594x perf win — the old code haversine'd 1.04M cells for a 77-cell circle).
rectangle-cartesian rectangle() built a cartesian product of coords (M·N entries per axis) instead of unique values → a 2×2 selection came back 4×4, a 153-cell bbox as 23,409 cells, O((M·N)²) overall.

High — client / auto-concatenate

Fix What was wrong
autoconcat-variant-crash loadDataset({autoConcatenate: true}) threw "Multiple variants available" for the exact case the feature exists for: after finding concatenable variants it re-resolved with the always-undefined request.variant. Variants like part1+part2 with valid dclimate:concatPriority crashed.
concat-items-resolved-id getConcatenableItemsFromStac matched item ids against the raw collection argument, not the resolved id — unprefixed "era5" (→ ecmwf_era5) returned [], silently degrading autoConcatenate to a single variant.
timerange-endpoint-clamping timeRange() passed user endpoints straight to jaxray's exact-match slice → "Jan 1–Jan 5" against data starting Jan 2 threw InvalidSelectionError instead of returning the overlap (the xarray semantics this API mirrors). Now clamps to the axis; empty intersections raise NoDataFoundError. Existing tests missed it because their fake sel was more forgiving than real jaxray.

Medium — caching and plumbing

Fix What was wrong
client-cache-gateway The client-level catalog cache ignored gatewayUrl — a catalog cached from gateway A was served for calls targeting gateway B. Review follow-up: the client-level cache was then deleted entirely (it duplicated the module cache and let catalogs live ~2x the TTL via re-stamping).
stac-cache-rootcid loadStacCatalog's cache ignored an explicit rootCid — pinning a version returned whatever was cached, and a pinned load poisoned later latest-version calls for the TTL. Cache key is now stac:<gateway>:<rootCid|latest>.
concat-shard-read-mode A requested shardReadMode: "full" silently became "sparse" for every variant on the concatenate path.
isempty-empty-dataset isEmpty() only checked zero-size dims and missed Dataset({}) → empty bounds selections resolved successfully instead of raising NoDataFoundError like the point path. Now delegates to the existing isDatasetEmpty.

Proven suspicions (flagged as unproven in the review; all four proved red)

Fix What was wrong
root-cid-endpoint Root-CID discovery hardcoded https://ipfs-gateway.dclimate.net/stac with no way for a client to avoid it — getRootCatalogCid now takes an endpoint and ClientOptions.rootCid pins the catalog version (and disables the latest-only STAC server path, which silently bypassed the pin).
stac-server-pagination resolveCidFromStacServer sent limit: 100 and never read links — items past the first page were invisible, silently falling back to the slow IPFS walk. Now follows STAC next links, including the production server's POST+token protocol (live-verified against api.stac.dclimate.net).
haversine-length-mismatch Mismatched array lengths modulo-cycled instead of erroring — haversine([5,6],[7,8,99],[0],[0]) fabricated a phantom (5, 99) coordinate pair. Now RangeError unless lengths are 1 or the common max.
classify-retrieval-error undici's TypeError: fetch failed matched no classifier substring → essentially every Node connection failure was recorded as generic "error", hollowing out the connection_error metric. Now unwraps error.cause (depth-capped, throw-safe) and matches the exact undici/browser messages.

Performance (measured; methodology in commit bodies and the review)

Optimization Before → After
catalog-walk-parallel — the IPFS catalog walk fetched ~139 documents one-at-a-time in three nested await loops; now fans out per level with Promise.all review prototype: 3603 ms → 104 ms (35x) at 25 ms RTT; regression test pins concurrency ≥ 4 and ≤ the new 12-fetch cap
concat-variants-perfconcatenateVariants did a per-step linear scan with repeated date parsing per variant two 30k-step variants: ~550 ms → well under the 250 ms pin (review measured 710 ms → 1.2 ms); binary-search split over once-parsed timestamps

(The circle() globe-mask elimination — 229.9 ms → 0.39 ms in the review — ships as part of the circle-real-datasets bug fix above.)

What the adversarial gauntlet caught (fixup rounds)

  • circle(): six confirmed defects in the first green — 0–360/dateline longitude grids lost the wrap side, over-the-pole circles dropped far-side cells, the flat cos scaling under-covered high latitudes, a 111.32-vs-111.195 km/° mismatch dropped boundary cells, a coord-rounding mismatch crashed on float32-decoded grids, and where stripped variable attrs. All fixed with a haversine-derived geometry (asin(sin(r/R)/cos φ), modular longitude membership, shared Earth radius + epsilon).
  • timeRange(): the first green compared on host-local Date.parse while zarr time coords are timezone-less UTC — selection shifted by the machine's UTC offset (red tests were invisible to this because bare dates parse as UTC); and numeric CF axes were compared against epoch-milliseconds, silently returning the entire dataset for out-of-range requests. Reworked on UTC/CF-aware timelines via jaxray's cfTimeToDate; regressions run green under TZ=America/New_York.
  • stac-server-pagination: passed its red test but the red test modeled a GET-href server; the production server paginates with POST+token next links, so the follow degenerated into an unfiltered default-limit crawl. Reworked to honor method/body/merge and tolerate null link entries.
  • root-cid-endpoint: the rootCid pin was silently bypassed unless the caller also passed stacServerUrl: null; pinning now disables the server fast path.
  • points(): NaN defeated snapToGrid: false via jaxray's NaN comparisons, undefined elements resurrected the silent-full-grid symptom, and a coordinate-key typo returned the entire grid — all now validated up front.
  • concatenateVariants: the binary-search split silently dropped non-ascending variants where the old code appended them; now throws with a clear message (verified parity on ascending axes via 500-case differential fuzz).
  • auto-concat metadata: concatenatedVariants/cid followed catalog discovery order rather than concatPriority order, and variants that never opted in (no concatPriority) were silently merged — both fixed per the documented DatasetVariantConfig contract.
  • catalog walk: uncapped fan-out fired ~200 simultaneous gateway requests (throttled responses would be warn-skipped into a silently partial catalog cached for the TTL) — now capped at 12; an org whose collection listing is malformed stays in catalog.organizations with zero collections, matching the serial walk.

Deliberate test corrections

Two legacy tests encoded the buggy behavior and were corrected rather than preserved: the "proper circle" test used a 10 km radius that could never span 0.1° (~11.12 km) latitude rows — it only passed because the trim was dead code (now 12 km); and a far-future timeRange expected InvalidSelectionError where "no data in range" is NoDataFoundError.

Known API inconsistency (left for a team decision)

GeoTemporalDataset.rectangle()/circle() are documented raw-Dataset passthroughs and still return (possibly empty) datasets, while select({bounds}) and points() throw NoDataFoundError on empty results. Changing the documented contract was out of scope here.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added an option to pin catalog resolution to a specific root CID.
    • Improved STAC server search to follow pagination, and sped up STAC catalog loading with concurrent traversal.
    • Enhanced time-range selection for numeric time axes, CF-time (including non-Gregorian calendars), and string/date inputs.
    • Improved geographic selectors (points, circle, rectangle) with stricter validation and more accurate trimming.
    • Strengthened auto-concatenation ordering and overlap handling.
  • Bug Fixes

    • More robust connection-error detection across nested fetch failures.
    • Fixed catalog caching isolation across gateways and pinned roots.
    • Corrected selection behavior and errors for empty results and point tolerance cases.

Faolain and others added 30 commits July 17, 2026 21:23
Pulls in dClimate/jaxray#review/tdd-followups (typed-array flat read
path, CF calendar decoding, zone-less UTC time parsing, where(NaN)
semantics) ahead of publishing, so the upcoming bugfix pipelines land
against current jaxray behavior. Baseline verified: build clean, lint
clean, 111/111 tests passing.
…on tests

Follow-ups from grok review and fable adversarial review of ebf6562/2216c9c:
- Replace O(n²) Array.includes dedupe with Set (measured ~230ms on a
  43200-point axis with the old approach)
- Pin descending-grid-order preservation and one-axis-empty behavior

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pins vectorized per-point selection along a 'point' dim, nearest-grid
snapping when snapToGrid=true, and NoDataFoundError for off-grid points
when snapToGrid=false — all currently broken because points() passes
DataArray selection values jaxray ignores.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the ignored DataArray-valued sel with per-point scalar sel and
assembles results along a 'point' dimension with point-aligned lat/lon
coords. snapToGrid=true snaps without a distance cap; snapToGrid=false
enforces the tolerance and maps misses to NoDataFoundError.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pins: spatial trim to the circle's bounding rows/cols (dead today since
where() fills null and isNaN(null) is false), time-dim preservation
(validity scan currently isNaN()'s arrays and returns empty), and the
lazy zarr path (where() on lazy operands currently throws).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… trim

Bbox pre-filter via isel before any masking (no more whole-globe
haversine), trim derived from the mask itself instead of the dead
isNaN(null) scan, and where() applied last on the small materialized
region. Variables are transposed back after jaxray's condition-first
where broadcasting so dim order (e.g. time/lat/lon) is preserved.

Also corrects the legacy proper-circle test: its 10 km radius could
never span 0.1-degree (~11.12 km) latitude rows — it only passed
because the old trim was dead code returning the untrimmed grid.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Follow-ups from grok review and fable adversarial review of 630b285
(both reproduced): NaN silently defeated snapToGrid=false via jaxray's
findNearestIndex NaN comparisons, undefined/null elements were skipped
by sel and resurrected the silent-full-grid symptom, and a
latitudeKey/longitudeKey typo returned the entire grid untouched.
Validates element finiteness and key existence up front, mirroring
circle(). The custom-key test now uses a dataset that actually has
both custom dims and asserts the selected value.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pins that loadDataset({autoConcatenate: true}) concatenates part1/part2
variants carrying dclimate:concatPriority instead of throwing
'Multiple variants available' from the variant-less
resolveDatasetFromStac call in the concatenable branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
resolveDatasetFromStac was called with the always-undefined
request.variant on the concatenable branch, throwing 'Multiple variants
available' for variant sets outside the no-variant fallback list.
Resolving with the first discovered concatenable variant yields the
same collection/organization ids without the crash.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pins that an unprefixed collection name plus organization (era5 +
ecmwf, resolving to ecmwf_era5) returns the concatenable items instead
of [], which today silently disables autoConcatenate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Six confirmed defects fixed: over-the-pole circles now widen to the
full longitude range (and clamp latitude to the pole), longitude bbox
membership uses modular angular difference so 0-360 and dateline grids
keep the wrap side, longitude extent uses asin(sin(r/R)/cos(lat))
instead of the flat cos scaling that under-covers high latitudes, the
bbox shares haversine's 6371 km Earth radius plus an epsilon so it is
a strict superset of the disk, per-variable mask coords avoid jaxray's
Dataset coord-rounding mismatch crash on float32 grids, and
where({keepAttrs: true}) preserves variable attrs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
selectCollectionFromCatalog already resolves unprefixed collection
names (era5 + ecmwf -> ecmwf_era5); the item loop now compares item-id
prefixes against that resolved id instead of the raw argument, so
autoConcatenate no longer silently degrades to a single variant (or
crashes) for unprefixed requests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Follow-ups from grok and fable adversarial reviews of 3258b60/149500c:
variants without an explicit dclimate:concatPriority are excluded from
auto-concatenation (the documented DatasetVariantConfig contract —
previously any multi-variant dataset merged silently with priority 0),
and loadAndConcatenateVariants sorts by concatPriority so
metadata.concatenatedVariants/cid reflect concatenation order rather
than catalog discovery order; concatDimension is now populated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pins xarray-style slice clamping: endpoints outside the axis clamp to
the overlapping data instead of throwing InvalidSelectionError, and a
range with no overlap raises NoDataFoundError.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Endpoints are compared on a parsed timeline and the selection is made
by index, so ranges overlapping the axis return the in-range data
(xarray label-slice semantics) instead of throwing on exact-match
misses; an empty intersection now raises NoDataFoundError. The legacy
integration test expecting InvalidSelectionError for a far-future
range encoded the old exact-match behavior and now pins
NoDataFoundError.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Follow-up from grok and fable adversarial reviews: item ids are parsed
by stripping the resolved collection id as a prefix (mirroring
resolveDatasetFromStac) instead of split('-'), so collection and
dataset names containing hyphens no longer silently disable
auto-concatenation. Pins the link-level concatPriority fallback and
the single-opted-in-variant case while here.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pins that a per-call options.gatewayUrl gets its own catalog rather
than whatever the client cached from a previous gateway.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A catalog cached from one gateway is no longer served for calls
targeting a different options.gatewayUrl; a gateway mismatch is a
cache miss.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pins both directions: a rootCid-pinned load must not be served the
cached latest catalog, and a pinned load must not poison later
latest-version calls for the TTL.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Follow-ups from grok and fable adversarial reviews of abb3ee6 (both
confirmed with repros): timezone-less datetime coords (jaxray's zarr
formatDate output) are now parsed as UTC instead of host-local
Date.parse semantics, and numeric time axes convert through the
coordinate's CF units via jaxray's cfTimeToDate instead of comparing
CF offsets against epoch-milliseconds (which silently returned the
whole dataset for out-of-range requests). Numeric axes without units
accept only numeric endpoints. Regressions run green under
TZ=America/New_York.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pinned rootCid loads no longer receive the cached latest catalog, and
a pinned load no longer poisons later latest-version lookups for the
TTL — the cache key is now stac:<gateway>:<rootCid|latest>.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Follow-up from the fable adversarial review of c00849d: the client
slot duplicated loadStacCatalog's module cache (same TTL, same gateway
keying) and re-stamping a module-cache hit let catalogs live up to ~2x
the TTL. getStacCatalog now delegates caching entirely to the module
cache.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pins that a requested shardReadMode reaches openDatasetFromCid for
every concatenated variant instead of silently reverting to sparse.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Each concatenated variant's openDatasetFromCid call now receives
options.shardReadMode instead of silently reverting to sparse.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…otCid

Follow-up from grok and fable adversarial reviews of 56c2388: the
stored entry.rootCid was write-only, and an empty-string rootCid now
keys the same slot as latest, matching loadStacCatalog's
rootCid || getRootCatalogCid() resolution.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pins that a variable-less dataset is empty, ensureHasData throws
NoDataFoundError for it, and an out-of-range bounds selection rejects
like the point path does.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A Dataset with no data variables now counts as empty, so empty
spatial selections raise NoDataFoundError via ensureHasData like the
point path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Faolain and others added 9 commits July 17, 2026 23:38
Pins concurrent fetching of independent catalog branches (currently
maxConcurrency is exactly 1 across ~30 fetches) while also pinning
traversal completeness and per-node error tolerance, which the
parallelization must preserve.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Organizations, collections, and items fan out with Promise.all instead
of three nested one-at-a-time awaits (~139 serial fetches on the real
catalog; a review prototype measured 3603ms -> 104ms at 25ms RTT).
Ordering follows link order via mapped results and per-node error
tolerance (warn + skip) is preserved, both pinned by the red test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Follow-ups from grok and fable adversarial reviews of a1a81d5: gateway
fetches are limited to 12 in flight (uncapped fan-out fired ~200
simultaneous requests; throttled responses would be warn-skipped into
a silently partial catalog cached for the TTL — the limiter guards
only the fetch so nested levels cannot deadlock), an org whose
collection listing is malformed stays in catalog.organizations with
zero collections as the serial walk did, and the regression tests now
pin exact link order, thrown (not just non-ok) failures at every
level, and the concurrency cap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pins overlap-splitting correctness (priority variant wins the overlap,
exact boundaries) and a <250ms budget for two 30,000-step variants —
currently ~550ms via full index-array isel plus per-step linear date
parsing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The overlap split point is found by binary search over once-parsed
timestamps instead of a per-step linear scan (O(n^2) across variants);
two 30,000-step variants now concatenate well under the 250ms pin
(previously ~550ms here, 710ms in the review's measurement).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pins desired behavior for: ClientOptions.rootCid avoiding the
hardcoded public discovery endpoint, resolveCidFromStacServer
following STAC next links past the first page, haversine rejecting
mismatched multi-element array lengths instead of modulo-cycling
phantom pairs, and classifyRetrievalError mapping undici
'fetch failed' / browser 'Failed to fetch' to connection_error.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- getRootCatalogCid takes an optional discovery endpoint and
  ClientOptions.rootCid pins the catalog version so private-gateway
  clients never contact the public discovery endpoint
- resolveCidFromStacServer follows STAC rel:next links (GET href,
  10-page cap) instead of silently missing items past the first page
- haversine throws RangeError on mismatched multi-element array
  lengths instead of modulo-cycling phantom coordinate pairs
- classifyRetrievalError unwraps error.cause (depth-capped) and maps
  undici 'fetch failed' / browser 'Failed to fetch' to
  connection_error

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Follow-up from grok and fable adversarial reviews of 4cf1a04: the
binary-search split assumes ascending coordinates; descending or
unsorted axes now throw (cheap first/last probes — full validation
would reintroduce the per-step parse cost) instead of silently
dropping the variant, and the perf regression asserts output shape
and boundary values so a fast-but-wrong split cannot pass on speed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… disables the server path

Follow-ups from grok and fable adversarial reviews of 6957bc9 (the
serious ones live-verified against api.stac.dclimate.net): the real
server paginates with POST next links carrying a token body, so next
links are now re-POSTed (honoring method/body/merge) with hrefs
resolved against the server URL and null link entries tolerated; a
ClientOptions.rootCid pin now disables the STAC server fast path,
which only serves the latest catalog and silently bypassed the pin in
default config; classifyRetrievalError's fetch-failure match is
anchored to undici/browser exact messages and survives throwing cause
getters.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change revises STAC catalog loading, root-CID handling, dataset time selection, variant concatenation, spatial shape selection, retrieval error classification, and empty-dataset behavior. It adds regression and performance tests covering these updated flows.

Changes

Core data flows

Layer / File(s) Summary
Temporal selection and variant concatenation
src/geotemporal-dataset.ts, src/actions/concatenate-variants.ts, src/types.ts, src/utils.ts, tests/review-fixes/*timerange*, tests/review-fixes/concat-variants-*
Time ranges support numeric, date, timezone-normalized, and CF-calendar values. Variant splitting uses cached comparable coordinates, monotonicity checks, and optimized searches.
STAC resolution and auto-concatenation
src/stac/*, src/client.ts, src/types.ts, tests/review-fixes/*catalog*, tests/review-fixes/*stac*, tests/review-fixes/*concat*, .gitignore
Catalog traversal is root-CID-aware, concurrent, deduplicated, and pagination-capable; auto-concatenation resolves variants, priorities, dimensions, and shard read modes in sorted order.
Spatial distance and shape selection
src/math/haversine.ts, src/shapes/*, tests/review-fixes/*circle*, tests/review-fixes/*points*, tests/review-fixes/*rectangle*, tests/shapes.test.ts
Haversine input lengths are validated, circle filtering uses bounding-box preselection, points are selected per coordinate pair, and rectangle coordinates are deduplicated.
Error classification and empty datasets
src/instrumentation.ts, src/geotemporal-dataset.ts, tests/review-fixes/classify-retrieval-error.test.ts, tests/review-fixes/isempty-empty-dataset.test.ts
Nested retrieval causes are classified as connection errors, and empty datasets use shared emptiness detection with NoDataFoundError behavior.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant CatalogLoader
  participant Gateway
  participant DatasetOpener
  Client->>CatalogLoader: request catalog with optional rootCid
  CatalogLoader->>Gateway: fetch root, organizations, collections, and items
  CatalogLoader-->>Client: return resolved catalog
  Client->>DatasetOpener: open ordered variant CIDs
  DatasetOpener-->>Client: return datasets for concatenation
Loading

Suggested reviewers: thegreatalgo

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the PR’s main theme: bug fixes and performance optimizations driven by TDD.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch review/tdd-fixes

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 594382e86b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +153 to +156
while (start < end) {
const midpoint = start + Math.floor((end - start) / 2);
if (
getComparableCoord(coords, midpoint, comparableCoords) > afterComparable

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Validate coordinate order before binary search

When a lower-priority variant has a non-monotonic but not descending axis, this binary search can return a later split point and silently drop valid coordinates instead of hitting the unsorted-axis guard; for example with previous coverage ending at 4 and next coordinates [1, 5, 3, 6], the search returns index 3, so the value at 5 is omitted. Since the new path relies on sorted input, it should either fully validate ascending order or fall back to a linear scan when unsorted data is possible.

Useful? React with 👍 / 👎.

Comment thread package.json Outdated
"homepage": "https://github.com/dClimate/dclimate-client-js#readme",
"dependencies": {
"@dclimate/jaxray": "^0.6.11",
"@dclimate/jaxray": "github:dClimate/jaxray#review/tdd-followups",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Pin jaxray to a released version

Because package.json is what npm consumers resolve and the lockfile is not shipped with the package, this mutable review branch will be resolved at install time; if the branch moves, is deleted, or GitHub is unavailable, installs of @dclimate/dclimate-client-js can change or fail. Please depend on an immutable published version or pinned commit before publishing this client.

Useful? React with 👍 / 👎.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR delivers a large set of test-driven correctness fixes and performance improvements across the client’s spatial selection utilities, STAC catalog/server resolution, variant concatenation, and time-range selection logic.

Changes:

  • Fixes and expands spatial selection behavior (points, circle, rectangle) and adds extensive regression coverage under tests/review-fixes/.
  • Improves STAC behavior (root CID pinning, cache keying, pagination support, and faster catalog walking with bounded concurrency).
  • Enhances time-range selection semantics (endpoint clamping, UTC handling, numeric/CF time support) and improves error classification/validation.

Reviewed changes

Copilot reviewed 31 out of 33 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
yarn.lock Switches @dclimate/jaxray resolution to a GitHub tarball for the review branch.
package.json Pins @dclimate/jaxray to a GitHub branch ref instead of a registry semver.
.gitignore Ignores .tdd-logs/ artifacts.
tests/shapes.test.ts Updates/extends shape tests (custom coord keys, non-finite inputs, circle radius correction).
tests/geotemporal-dataset.test.ts Updates expectation to NoDataFoundError for out-of-range timeRange.
tests/review-fixes/timerange-endpoint-clamping.test.ts Adds endpoint clamping + UTC/CF/numeric time axis regression coverage.
tests/review-fixes/stac-server-pagination.test.ts Adds pagination coverage including POST+token next links and null link entries.
tests/review-fixes/stac-cache-rootcid.test.ts Adds cache isolation tests for pinned vs latest root CID.
tests/review-fixes/root-cid-endpoint.test.ts Adds tests for custom discovery endpoint and client rootCid behavior.
tests/review-fixes/rectangle-cartesian.test.ts Adds regression tests for avoiding cartesian duplicates + descending grids.
tests/review-fixes/points-vectorized.test.ts Adds regression tests for vectorized point selection semantics.
tests/review-fixes/isempty-empty-dataset.test.ts Adds regression tests for empty Dataset({}) handling.
tests/review-fixes/haversine-length-mismatch.test.ts Adds regression tests for array-length validation in haversine.
tests/review-fixes/concat-variants-perf.test.ts Adds overlap-splitting correctness + performance pin coverage.
tests/review-fixes/concat-shard-read-mode.test.ts Adds regression ensuring shardReadMode is propagated through concat loads.
tests/review-fixes/concat-items-resolved-id.test.ts Adds regression tests for concat item parsing (resolved id, hyphens, opt-in priority).
tests/review-fixes/client-cache-gateway.test.ts Adds regression test for gateway-isolated catalog behavior.
tests/review-fixes/classify-retrieval-error.test.ts Adds coverage for undici/browser fetch-failure classification.
tests/review-fixes/circle-real-datasets.test.ts Adds real-dataset fixtures for circle trimming + lazy support.
tests/review-fixes/circle-geometry.test.ts Adds geometry regression coverage (poles, 0–360 wrap, float32 coords, attrs).
tests/review-fixes/catalog-walk-parallel.test.ts Adds bounded-concurrency + traversal correctness tests for catalog walk.
tests/review-fixes/autoconcat-variant-crash.test.ts Adds regression for auto-concat variant selection/metadata ordering.
src/types.ts Adds ClientOptions.rootCid documentation.
src/client.ts Wires rootCid to catalog load; fixes auto-concat resolution and shardReadMode propagation; orders concat variants by priority.
src/geotemporal-dataset.ts Reworks timeRange selection (clamping, UTC parsing, CF/numeric support) and empty-dataset detection.
src/shapes/points.ts Replaces non-vectorized selection with per-point selection + coordinate validation.
src/shapes/circle.ts Rewrites circle selection to bbox-prefilter + trimmed haversine mask with lazy-safe compute ordering.
src/shapes/rectangle.ts Fixes rectangle selection to avoid cartesian-product duplication and handle empty-axis results.
src/stac/stac-server.ts Adds pagination support via STAC next links (GET and POST+body).
src/stac/stac-catalog.ts Adds rootCid-aware cache keying; parallel catalog walk with bounded concurrency; fixes concatenable item parsing.
src/math/haversine.ts Exports EARTH_RADIUS_KM and adds strict array-length validation.
src/instrumentation.ts Improves connection-error classification by unwrapping cause and anchoring message matches.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread package.json Outdated
"homepage": "https://github.com/dClimate/dclimate-client-js#readme",
"dependencies": {
"@dclimate/jaxray": "^0.6.11",
"@dclimate/jaxray": "github:dClimate/jaxray#review/tdd-followups",
Comment thread src/stac/stac-server.ts
Comment on lines +23 to +29
links?: Array<{
rel: string;
href: string;
method?: string;
body?: Record<string, unknown>;
merge?: boolean;
}>;
Comment thread src/shapes/points.ts
Comment on lines +25 to +27
if (coordinates.length === 0) {
throw new Error("Cannot select from an empty coordinate array");
}
Comment thread src/shapes/points.ts
Comment on lines +29 to +31
if (!coordinates.every((coordinate) => typeof coordinate === "number")) {
throw new Error("Point selection requires numeric spatial coordinates");
}
Comment on lines 228 to 231
async timeRange(
range: TimeRange,
range: TimeRangeInput,
dimension = "time",
): Promise<GeoTemporalDataset> {
Comment on lines +190 to 193
const comparable = toComparable(coords[index]);
cachedCoords[index] = comparable;
return comparable;
}

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (4)
tests/review-fixes/classify-retrieval-error.test.ts (1)

5-19: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test name implies cause unwrapping but match happens at depth 0.

The top-level message "fetch failed" matches the anchored regex on the first iteration, so the ECONNREFUSED cause is never actually inspected. The test validates the anchored regex match but not the nested-cause traversal. Consider adding a separate test where the top-level message is non-matching and only a deeper cause contains a connection-error pattern, to exercise the depth > 0 path.

🧪 Suggested additional test for nested cause traversal
+  it("classifies an error whose nested cause contains ECONNREFUSED", () => {
+    const root = new TypeError("some unrelated wrapper error");
+    const cause = Object.assign(
+      new Error("connect ECONNREFUSED 127.0.0.1:1"),
+      { code: "ECONNREFUSED" },
+    );
+    (root as Error & { cause?: unknown }).cause = cause;
+    expect(classifyRetrievalError(root)).toBe("connection_error");
+  });
+
+  it("returns 'error' when a throwing cause getter prevents deeper inspection", () => {
+    const root = new TypeError("some unrelated wrapper error");
+    Object.defineProperty(root, "cause", {
+      get() { throw new Error("getter exploded"); },
+    });
+    expect(classifyRetrievalError(root)).toBe("error");
+  });
🤖 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 `@tests/review-fixes/classify-retrieval-error.test.ts` around lines 5 - 19, Add
a separate test near the existing classifyRetrievalError case where the
top-level error message does not match connection-error patterns and only a
nested cause contains one. Construct the cause chain to require traversal beyond
depth 0, then assert classifyRetrievalError returns "connection_error"; keep the
existing undici fetch-failure test unchanged.
tests/review-fixes/isempty-empty-dataset.test.ts (1)

1-5: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Inconsistent import extension convention across test files.

This file imports source modules with .ts extensions (L3-5), while tests/review-fixes/classify-retrieval-error.test.ts uses .js extensions for the same kind of import. Pick one convention consistently — typically .js for ESM/NodeNext resolution — to avoid confusion and potential resolution issues across build targets.

🤖 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 `@tests/review-fixes/isempty-empty-dataset.test.ts` around lines 1 - 5,
Standardize the source-module imports in this test to use the project’s `.js`
ESM/NodeNext extension convention, updating the imports for NoDataFoundError,
GeoTemporalDataset, and DatasetMetadata while leaving the package imports
unchanged.
.gitignore (1)

5-5: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Ignore common environment-file variants.

.env.local, .env.production, and similar files remain eligible for commit.

Proposed fix
 .env
+.env.*
+!.env.example
+!.env.*.example
🤖 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 @.gitignore at line 5, Update the .env ignore rule in .gitignore to cover
common environment-file variants, including .env.local, .env.production, and
similar suffixed files, while retaining the existing exclusion of .env.
tests/review-fixes/root-cid-endpoint.test.ts (1)

55-58: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the casts so these tests enforce the new public API types.

The unknown casts allow the tests to compile even if the endpoint parameter or ClientOptions.rootCid disappears later. Call and annotate these APIs directly.

Proposed fix
-    const getRootCid = getRootCatalogCid as unknown as (
-      endpoint?: string,
-    ) => Promise<string>;
-    const cid = await getRootCid(discoveryEndpoint);
+    const cid = await getRootCatalogCid(discoveryEndpoint);

-    const clientOptions = {
+    const clientOptions: ClientOptions = {
       gatewayUrl,
       stacServerUrl: null,
       rootCid: pinnedRootCid,
       ipfsElements: {} as IpfsElements,
-    } as unknown as ClientOptions;
+    };

     const pinnedDefaultServerClient = new DClimateClient({
       gatewayUrl,
       rootCid: pinnedRootCid,
       ipfsElements: {} as IpfsElements,
-    } as unknown as ClientOptions);
+    });

Also applies to: 173-178, 203-207

🤖 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 `@tests/review-fixes/root-cid-endpoint.test.ts` around lines 55 - 58, Remove
the unknown/function-type cast around getRootCatalogCid and call it directly
with discoveryEndpoint, allowing TypeScript to validate the public endpoint
parameter. Apply the same change to the additional locations and remove any
equivalent casts around ClientOptions.rootCid, using direct API calls and
annotations so the tests enforce the public types.
🤖 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 `@package.json`:
- Line 54: Update the `@dclimate/jaxray` dependency declaration in package.json to
reference an immutable commit SHA or release tag instead of the mutable
review/tdd-followups branch, and synchronize the corresponding yarn.lock entry.

In `@src/actions/concatenate-variants.ts`:
- Around line 78-109: Replace the first/last-coordinate probes in the
concatenation flow with a complete validation that nextCoords is monotonically
ascending before calling findSplitIndex. Reject any out-of-order adjacent
coordinates with the existing descending/unsorted coordinate error, then
preserve the binary-search split behavior only for validated axes.

In `@src/shapes/circle.ts`:
- Around line 101-103: Update the radius validation in the circle selection
logic to reject all non-finite values, including NaN and Infinity, while
continuing to reject zero and negative radii. Use the existing
InvalidSelectionError path and preserve the current behavior for finite positive
radii.

In `@src/stac/stac-catalog.ts`:
- Around line 417-475: Track traversal failures across the item, collection, and
organization loading branches in the catalog-loading flow, including the catch
handlers around item requests, collection processing, and organization
processing. Before calling setCachedCatalog, only cache the catalog when no
branch failed; otherwise return the assembled result without caching, or
propagate the load failure. Preserve the existing filtering and returned partial
data behavior if choosing to skip the cache.
- Around line 354-415: Update the limitedFetch usage in the organization,
collection, and item-loading paths so each limiter task includes both fetch()
and response.json() before releasing its slot. Preserve existing non-OK handling
and error behavior, and add a test using delayed response.json() that verifies
concurrent response-body reads never exceed MAX_CONCURRENT_CATALOG_FETCHES.

In `@src/stac/stac-server.ts`:
- Around line 116-125: Update the pagination loop in the STAC search flow around
nextLink so that finding a next link on MAX_STAC_SEARCH_PAGES - 1 throws a
pagination-limit error instead of breaking and returning incomplete results.
Preserve the existing break behavior when no next link exists, and continue
normally for earlier pages.

---

Nitpick comments:
In @.gitignore:
- Line 5: Update the .env ignore rule in .gitignore to cover common
environment-file variants, including .env.local, .env.production, and similar
suffixed files, while retaining the existing exclusion of .env.

In `@tests/review-fixes/classify-retrieval-error.test.ts`:
- Around line 5-19: Add a separate test near the existing classifyRetrievalError
case where the top-level error message does not match connection-error patterns
and only a nested cause contains one. Construct the cause chain to require
traversal beyond depth 0, then assert classifyRetrievalError returns
"connection_error"; keep the existing undici fetch-failure test unchanged.

In `@tests/review-fixes/isempty-empty-dataset.test.ts`:
- Around line 1-5: Standardize the source-module imports in this test to use the
project’s `.js` ESM/NodeNext extension convention, updating the imports for
NoDataFoundError, GeoTemporalDataset, and DatasetMetadata while leaving the
package imports unchanged.

In `@tests/review-fixes/root-cid-endpoint.test.ts`:
- Around line 55-58: Remove the unknown/function-type cast around
getRootCatalogCid and call it directly with discoveryEndpoint, allowing
TypeScript to validate the public endpoint parameter. Apply the same change to
the additional locations and remove any equivalent casts around
ClientOptions.rootCid, using direct API calls and annotations so the tests
enforce the public types.
🪄 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: 4bca3990-7487-42e6-bc1c-5b9f11d0a7df

📥 Commits

Reviewing files that changed from the base of the PR and between 54d65a1 and 594382e.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (32)
  • .gitignore
  • package.json
  • src/actions/concatenate-variants.ts
  • src/client.ts
  • src/geotemporal-dataset.ts
  • src/instrumentation.ts
  • src/math/haversine.ts
  • src/shapes/circle.ts
  • src/shapes/points.ts
  • src/shapes/rectangle.ts
  • src/stac/stac-catalog.ts
  • src/stac/stac-server.ts
  • src/types.ts
  • tests/geotemporal-dataset.test.ts
  • tests/review-fixes/autoconcat-variant-crash.test.ts
  • tests/review-fixes/catalog-walk-parallel.test.ts
  • tests/review-fixes/circle-geometry.test.ts
  • tests/review-fixes/circle-real-datasets.test.ts
  • tests/review-fixes/classify-retrieval-error.test.ts
  • tests/review-fixes/client-cache-gateway.test.ts
  • tests/review-fixes/concat-items-resolved-id.test.ts
  • tests/review-fixes/concat-shard-read-mode.test.ts
  • tests/review-fixes/concat-variants-perf.test.ts
  • tests/review-fixes/haversine-length-mismatch.test.ts
  • tests/review-fixes/isempty-empty-dataset.test.ts
  • tests/review-fixes/points-vectorized.test.ts
  • tests/review-fixes/rectangle-cartesian.test.ts
  • tests/review-fixes/root-cid-endpoint.test.ts
  • tests/review-fixes/stac-cache-rootcid.test.ts
  • tests/review-fixes/stac-server-pagination.test.ts
  • tests/review-fixes/timerange-endpoint-clamping.test.ts
  • tests/shapes.test.ts

Comment thread package.json Outdated
Comment thread src/actions/concatenate-variants.ts Outdated
Comment thread src/shapes/circle.ts
Comment on lines 101 to 103
if (radiusKm <= 0) {
throw new InvalidSelectionError("Radius must be a positive number");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject non-finite radii.

NaN bypasses this check and returns an empty dataset, while Infinity can select the entire grid.

Proposed fix
-  if (radiusKm <= 0) {
+  if (!Number.isFinite(radiusKm) || radiusKm <= 0) {
     throw new InvalidSelectionError("Radius must be a positive number");
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (radiusKm <= 0) {
throw new InvalidSelectionError("Radius must be a positive number");
}
if (!Number.isFinite(radiusKm) || radiusKm <= 0) {
throw new InvalidSelectionError("Radius must be a positive number");
}
🤖 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 `@src/shapes/circle.ts` around lines 101 - 103, Update the radius validation in
the circle selection logic to reject all non-finite values, including NaN and
Infinity, while continuing to reject zero and negative radii. Use the existing
InvalidSelectionError path and preserve the current behavior for finite positive
radii.

Comment thread src/stac/stac-catalog.ts
Comment thread src/stac/stac-catalog.ts
Comment on lines +417 to +475
if (!itemResponse.ok) {
console.warn(`Failed to load item from ${itemLink.href}: ${itemResponse.status}`);
return undefined;
}

return await itemResponse.json() as StacItem;
} catch (itemError) {
console.warn(`Error loading item ${itemLink.href}:`, itemError);
return undefined;
}
})
);

collection.items = itemResults.filter((item): item is StacItem => item !== undefined);
collection.organizationId = orgId;
collection.organizationTitle = link.title;
const category = collectionCategories.get(collection.id);
if (category) {
collection.category = category;
}

if (!itemResponse.ok) {
console.warn(`Failed to load item from ${itemLink.href}: ${itemResponse.status}`);
continue;
const datasetNames = datasetSlugs
.filter((slug) => slug.startsWith(`${collection.id}/`))
.map((slug) => slug.split("/")[1])
.filter(Boolean);
if (datasetNames.length) {
collection.datasetNames = datasetNames;
}

const item: StacItem = await itemResponse.json();
items.push(item);
} catch (itemError) {
console.warn(`Error loading item ${itemLink.href}:`, itemError);
return collection;
} catch (collectionError) {
console.warn(`Error loading collection ${collectionLink.href}:`, collectionError);
return undefined;
}
}

collection.items = items;
collection.organizationId = orgId;
collection.organizationTitle = link.title;
const category = collectionCategories.get(collection.id);
if (category) {
collection.category = category;
}

const datasetNames = datasetSlugs
.filter((slug) => slug.startsWith(`${collection.id}/`))
.map((slug) => slug.split("/")[1])
.filter(Boolean);
if (datasetNames.length) {
collection.datasetNames = datasetNames;
}

collections.push(collection);
} catch (collectionError) {
console.warn(`Error loading collection ${collectionLink.href}:`, collectionError);
}
})
);

return {
organization,
collections: collectionResults.filter(
(collection): collection is StacCollection => collection !== undefined
),
};
} catch (orgError) {
console.warn(`Error loading organization ${link.href}:`, orgError);
return { organization, collections: [] };
}
} catch (orgError) {
console.warn(`Error loading organization ${link.href}:`, orgError);
}
}
})
);
const loadedOrganizations = organizationResults.filter(
(result): result is NonNullable<typeof result> => result !== undefined
);
const organizations = loadedOrganizations.map(({ organization }) => organization);
const collections = loadedOrganizations.flatMap((result) => result.collections);

catalog.collections = collections;
catalog.organizations = organizations;

setCachedCatalog(gatewayUrl, catalog, cid);
setCachedCatalog(gatewayUrl, rootCid, catalog);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Do not cache a catalog assembled after traversal failures.

Item, collection, and organization failures are swallowed as undefined, but the resulting partial catalog is still cached for one hour. A transient gateway error can therefore make valid datasets appear absent until TTL expiry.

Track whether any branch failed and skip setCachedCatalog for incomplete walks, or fail the load instead.

🤖 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 `@src/stac/stac-catalog.ts` around lines 417 - 475, Track traversal failures
across the item, collection, and organization loading branches in the
catalog-loading flow, including the catch handlers around item requests,
collection processing, and organization processing. Before calling
setCachedCatalog, only cache the catalog when no branch failed; otherwise return
the assembled result without caching, or propagate the load failure. Preserve
the existing filtering and returned partial data behavior if choosing to skip
the cache.

Comment thread src/stac/stac-server.ts

@da-code-reviewer da-code-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Codex Automated Review

Found 5 actionable correctness issues.
Posted 5 inline comment(s).


// Find the index in nextDataset where coords start AFTER lastCombinedCoord
const splitIndex = findSplitIndex(nextCoords, lastCombinedCoord);
const splitIndex = findSplitIndex(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

HIGH
The endpoint probes do not prove that nextCoords is sorted, so this binary search can silently drop valid data. For example, with coverage ending at 5 and coordinates [2, 100, 3, 4, 10], the checks pass but the split is 4, discarding 100. Validate full monotonicity or fall back to the linear scan for non-monotonic axes.

Comment thread src/geotemporal-dataset.ts Outdated
"Numeric time axis contains a non-numeric value",
);
}
return cfTimeToTimelineValue(coordinate, units, calendar);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

HIGH
cfTimeToDate returns null for valid non-Gregorian dates that cannot be represented by Date, such as February 30 in a 360_day calendar. Because every coordinate is converted here, one such value makes all timeRange() calls fail, even for ranges that do not include it. Compare CF coordinates on a calendar-native ordinal timeline instead.

};
} else {
if (
typeof range.start !== "number" ||

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MEDIUM
GeoSelectionOptions.timeRange still exposes only Date | string endpoints, so every type-correct select() call on an unannotated numeric axis reaches this error; the required numeric endpoints are rejected by TypeScript. With a simultaneous point selection, the resulting InvalidSelectionError is swallowed and the time filter is silently skipped. Widen the exported TimeRange type or retain endpoint coercion.

Comment thread src/client.ts
resolvedCollection || request.collection || "",
request.dataset,
request.variant,
concatenableItems[0].variant,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MEDIUM
Auto-concatenation still fails for dataset names containing hyphens. getConcatenableItemsFromStac now recognizes IDs such as collection-wind-speed-part1, but this call immediately passes them to resolveDatasetFromStac, which parses the dataset as only wind via split("-")[0]. Use the same prefix-based parsing in the resolver or avoid reparsing the item ID here.

Comment thread src/stac/stac-server.ts Outdated
variant: string;
}

const MAX_STAC_SEARCH_PAGES = 10;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MEDIUM
Pagination is silently truncated after ten pages even when the tenth response contains a valid next link. A target on any later page is therefore reported as missing. Follow links until exhaustion with cycle detection, or explicitly surface a pagination-limit error so callers can distinguish truncation from absence.

@da-code-reviewer da-code-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Codex Automated Review

Found 3 actionable correctness and reliability issues.
Posted 3 inline comment(s).


// Find the index in nextDataset where coords start AFTER lastCombinedCoord
const splitIndex = findSplitIndex(nextCoords, lastCombinedCoord);
const splitIndex = findSplitIndex(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MEDIUM
The binary search assumes the entire axis is ascending, but only the first and last values are checked. For nextCoords = [4, 1, 5] and a combined endpoint of 3, this returns index 2 and silently drops the valid coordinate 4; the later guard does not fire. Validate adjacent coordinates or fall back to a safe scan for non-monotonic axes.

Comment thread src/stac/stac-server.ts Outdated
// STAC API pagination: next links may be plain GET hrefs, or POST links
// carrying a token body (the dClimate server's shape) that must be
// re-POSTed — optionally merged with the original search body.
const nextUrl = new URL(nextLink.href, serverUrl).toString();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MEDIUM
Relative next links are resolved against serverUrl instead of the current /search request URL. For example, ?token=x with https://host/stac resolves to /stac?token=x, not /stac/search?token=x, so valid STAC pagination can fetch the wrong endpoint. Track the current request URL and resolve each link against it.

Comment thread src/stac/stac-catalog.ts
const datasetSlugs = extractDatasetSlugsFromOrgLink(link);
// The limiter guards only the fetch itself (not whole mapper bodies), so
// a parent level never holds a slot while awaiting its children.
const limitedFetch = createFetchLimiter(MAX_CONCURRENT_CATALOG_FETCHES);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MEDIUM
This limiter is created per catalog load, so it does not enforce the advertised 12-request cap across concurrent cold-cache calls. Because the cache stores no in-flight promise, N simultaneous loads can run 12 × N gateway fetches, triggering throttling and potentially caching partial catalogs. Deduplicate loads by cache key or share the limiter.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@src/geotemporal-dataset.ts`:
- Around line 258-261: Update endpointToOrdinal to validate numeric
CoordinateValue inputs with a finite-number check before returning them. Reject
NaN and infinities through the existing invalid-selection error path rather than
allowing invalid bounds to reach the query and become NoDataFoundError; preserve
conversion for valid numeric and non-numeric values.
🪄 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: fca157d1-3195-4fc7-9987-cf3f1711122c

📥 Commits

Reviewing files that changed from the base of the PR and between 594382e and 6a3625a.

📒 Files selected for processing (10)
  • src/actions/concatenate-variants.ts
  • src/geotemporal-dataset.ts
  • src/stac/stac-catalog.ts
  • src/stac/stac-server.ts
  • src/types.ts
  • src/utils.ts
  • tests/review-fixes/catalog-walk-parallel.test.ts
  • tests/review-fixes/concat-variants-monotonicity.test.ts
  • tests/review-fixes/stac-server-pagination.test.ts
  • tests/review-fixes/timerange-cf-calendar.test.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/actions/concatenate-variants.ts
  • tests/review-fixes/catalog-walk-parallel.test.ts
  • src/stac/stac-catalog.ts
  • src/stac/stac-server.ts

Comment on lines +258 to +261
const endpointToOrdinal = (value: CoordinateValue): number => {
if (typeof value === "number") {
// Already a raw CF coordinate value on the axis's timeline.
return value;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject non-finite CF ordinal endpoints.

NaN or infinity bypasses toTimelineValue, produces invalid bounds, and is misreported as NoDataFoundError instead of an invalid selection.

Proposed fix
 if (typeof value === "number") {
+  if (!Number.isFinite(value)) {
+    throw new TypeError("CF time range endpoints must be finite numbers");
+  }
   // Already a raw CF coordinate value on the axis's timeline.
   return value;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const endpointToOrdinal = (value: CoordinateValue): number => {
if (typeof value === "number") {
// Already a raw CF coordinate value on the axis's timeline.
return value;
const endpointToOrdinal = (value: CoordinateValue): number => {
if (typeof value === "number") {
if (!Number.isFinite(value)) {
throw new TypeError("CF time range endpoints must be finite numbers");
}
// Already a raw CF coordinate value on the axis's timeline.
return value;
🤖 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 `@src/geotemporal-dataset.ts` around lines 258 - 261, Update endpointToOrdinal
to validate numeric CoordinateValue inputs with a finite-number check before
returning them. Reject NaN and infinities through the existing invalid-selection
error path rather than allowing invalid bounds to reach the query and become
NoDataFoundError; preserve conversion for valid numeric and non-numeric values.

@da-code-reviewer da-code-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Codex Automated Review

Found two actionable issues: silent timestamp loss and incomplete STAC pagination support.
Posted 2 inline comment(s).

>,
variantName: string,
concatDim: string
): number {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

HIGH
String coordinates are sent to binary search without validating interior ordering. With a previous endpoint of Jan 3 and next coordinates [Jan 4, Jan 1, Jan 5], this returns index 2 and silently drops Jan 4; the endpoint check cannot detect it. Validate monotonicity or use the safe scan before slicing.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

this is super edge case and honestly not worth it. i doubt we would ever have this.

Comment thread src/stac/stac-server.ts Outdated
: nextLink.body ?? body;
response = await fetch(nextUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MEDIUM
Pagination ignores nextLink.headers and hard-codes only Content-Type. Headers are part of the STAC pagination link contract, so header-based cursors can repeat page one or fail, eventually causing an unnecessary IPFS fallback. Model and forward/merge the link headers for both POST and GET next requests.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
tests/review-fixes/stac-server-pagination.test.ts (1)

136-181: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test title overstates coverage — only GET cursor forwarding is exercised.

The test name says "on both GET and POST," but the mocked next link always uses method: "GET" (Line 161), so the POST branch of cursor-header handling in resolveCidFromStacServer (which merges a Content-Type floor differently than the GET branch) is never exercised here.

♻️ Suggested fix: rename or add a POST variant
-  it("forwards header-based cursors from the next link on both GET and POST", async () => {
+  it("forwards header-based cursors from the next link on GET", async () => {

Consider adding a companion case with method: "POST" in the next link to actually cover the POST cursor-header path.

🤖 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 `@tests/review-fixes/stac-server-pagination.test.ts` around lines 136 - 181,
Update the pagination test around “forwards header-based cursors from the next
link” so it no longer claims POST coverage while only using a GET next link, or
add a companion case whose next link uses method "POST" and verifies the cursor
plus POST-specific Content-Type handling in resolveCidFromStacServer.
🤖 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.

Nitpick comments:
In `@tests/review-fixes/stac-server-pagination.test.ts`:
- Around line 136-181: Update the pagination test around “forwards header-based
cursors from the next link” so it no longer claims POST coverage while only
using a GET next link, or add a companion case whose next link uses method
"POST" and verifies the cursor plus POST-specific Content-Type handling in
resolveCidFromStacServer.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 68277df5-424c-474b-b126-36157bcaa4e3

📥 Commits

Reviewing files that changed from the base of the PR and between 6a3625a and d048041.

📒 Files selected for processing (2)
  • src/stac/stac-server.ts
  • tests/review-fixes/stac-server-pagination.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/stac/stac-server.ts

@TheGreatAlgo
TheGreatAlgo merged commit 5a8a7b3 into main Jul 21, 2026
2 checks passed

@da-code-reviewer da-code-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Codex Automated Review

Found 6 actionable correctness and reliability issues.
Posted 6 inline comment(s).

concatDim: string
): number {
return typeof coords[0] === "string"
? findSplitIndexBinary(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

HIGH
String axes use binary search without validating interior ordering. For a previous endpoint of Jan 3 and [Jan 4, Jan 1, Jan 5], the split is index 2 and Jan 4 is silently dropped. Validate monotonicity or use the safe scan.

Comment thread src/client.ts
resolvedCollection || request.collection || "",
request.dataset,
request.variant,
concatenableItems[0].variant,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MEDIUM
Auto-concatenation still fails for hyphenated dataset names. The updated item matcher accepts collection-wind-speed-part1, but resolveDatasetFromStac() parses the dataset as wind using split("-")[0] and throws here. Use the same prefix-based parsing in the resolver.

? normalizedValue
: normalizedValue instanceof Date
? normalizedValue.getTime()
: Date.parse(normalizedValue);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MEDIUM
Date.parse() collapses sub-millisecond ISO timestamps to millisecond precision. Coordinates such as .000001Z and .000002Z compare equal, so a range targeting one silently returns both. Preserve fractional-second precision when building the timeline.

Comment thread src/stac/stac-catalog.ts

let organization: StacOrganization;
try {
const orgResponse = await limitedFetch(() => fetch(orgUrl));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MEDIUM
The limiter wraps only fetch(), which resolves after headers; response.json() runs after the slot is released. Consequently, arbitrarily many response bodies can still download and parse concurrently, defeating the 12-request cap. Keep fetch, status handling, and body parsing inside the limited task.

Comment thread src/stac/stac-catalog.ts
catalog.organizations = organizations;

setCachedCatalog(gatewayUrl, catalog, cid);
setCachedCatalog(gatewayUrl, rootCid, catalog);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MEDIUM
Organization, collection, and item failures are swallowed as undefined, but the resulting partial catalog is cached for an hour here. A transient gateway failure therefore makes valid datasets appear absent until expiry. Track traversal failures and skip caching incomplete results, or fail the load.

const endpointToOrdinal = (value: CoordinateValue): number => {
if (typeof value === "number") {
// Already a raw CF coordinate value on the axis's timeline.
return value;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LOW
Raw CF ordinal endpoints are returned without checking finiteness. NaN becomes a misleading NoDataFoundError, while infinite bounds can select unintended data. Reject non-finite numbers through the existing invalid-selection path.

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.

3 participants