# @honua/sdk-js — full documentation corpus
> One TypeScript/JavaScript geospatial client for Esri GeoServices, OGC API (Features/Tiles/Maps/Processes), STAC, WMS/WMTS, WFS 2.0, and OData v4 — a single protocol-neutral Dataset → Source → Query → Result contract, a MapLibre-first map runtime, and a drop-in ArcGIS migration codemod.
This file concatenates the documents indexed in `llms.txt` for single-fetch ingestion.
---
# File: README.md
# Honua JS SDK
[](https://scorecard.dev/viewer/?uri=github.com/honua-io/honua-sdk-js)
[](https://www.npmjs.com/package/@honua/sdk-js)
[](https://www.npmjs.com/package/@honua/sdk-js)
[](./LICENSE)
[](./package.json)
[](https://honua-io.github.io/honua-sdk-js/)
> **MapLibre gives you the map. Honua gives you everything else.**
`@honua/sdk-js` is the integration layer for the open map stack: typed clients for the
protocols your data already speaks (Esri GeoServices, OGC API Features / Tiles / Maps /
Processes, STAC, WMS, WMTS, WFS 2.0, OData v4), a one-call data→map bridge and MapLibre
runtime, provider-pluggable geocoding and routing, and a drop-in ArcGIS compatibility
layer with a codemod — everything around the renderer, so MapLibre (2D) and Cesium (3D)
can do what they do best.
**Leaving ArcGIS?** Every classic Esri widget was deprecated at ArcGIS JS SDK 5.0 and is
removed at 6.0 — planned for **Q1 2027**. If your app constructs one, that code stops
compiling and running when you take the 6.0 upgrade. Run
`npm run scan:arcgis:widgets -- ./src` for a per-file readiness report, then read the
[widget-removal survival guide](./docs/widget-survival-guide.md) — every deprecated
widget mapped to its Honua/MapLibre disposition.
## A public endpoint to a styled map
Nine application lines. No Honua server, no API key, no account — a public Esri Living
Atlas FeatureServer becomes a styled, interactive MapLibre map:
```ts doc-test=compile
import { connect } from "@honua/sdk-js";
import { mountSource } from "@honua/sdk-js/map";
import maplibregl from "maplibre-gl";
const endpoint = "https://services.arcgis.com/P3ePLMYs2RVChkJx/arcgis/rest/services/2020_Census_State_Apportionment/FeatureServer/0";
const map = new maplibregl.Map({ container: "map", style: "https://demotiles.maplibre.org/style.json", center: [-98, 39], zoom: 3 });
await map.once("load");
const data = await connect({ endpoint, protocol: "auto", authorizationScopeFingerprint: "public" });
await mountSource(map, data.source(), {
popup: { factory: () => new maplibregl.Popup(), fields: ["NAME", "Seats_2020"] },
hover: true,
fitBounds: true,
});
```
For those nine lines the bridge selected a materialization strategy from the source's
declared capabilities, installed geometry-appropriate default styling, wired click popups
and hover feature-state, fit the map to the data, and returned one owned handle
(`setFilter()` diff-updates in place; `dispose()` removes everything the bridge added).
Run it locally with `npm run demo:endpoint-to-map`
([`examples/endpoint-to-map/`](./examples/endpoint-to-map/README.md)); the full cookbook
is [`docs/data-to-map-bridge.md`](./docs/data-to-map-bridge.md).
**Release status: beta** (`0.1.0-beta.0`). The 22-entrypoint stable tier is frozen and guarded
by an API-surface gate; 11 experimental subpaths may change before 1.0, and
18 deprecated compatibility subpaths have explicit removal versions. See
[`config/support-manifest.v1.json`](./config/support-manifest.v1.json) for the versioned support truth,
[`config/public-surface.json`](./config/public-surface.json) for its generated package projection,
[`support/projections/sdk-support.v1.json`](./support/projections/sdk-support.v1.json) for the generic
site/sample consumer contract, and
[the scope decision](./docs/decisions/scope-split-and-1.0.md).
📚 **Hosted docs:** [honua-io.github.io/honua-sdk-js](https://honua-io.github.io/honua-sdk-js/) —
quickstart, the full guide corpus, the [TypeDoc API reference](https://honua-io.github.io/honua-sdk-js/api/),
and the [demo gallery](https://honua-io.github.io/honua-sdk-js/gallery.html).
## Pick your path
| | 🗺️ **Building on MapLibre** | 🚦 **Leaving ArcGIS** |
| --- | --- | --- |
| **You are…** | adding typed data access, styling, and interactions to a MapLibre (or brand-new) app | facing the classic-widget removal at ArcGIS JS 6.0 (planned Q1 2027) |
| **Start** | [Standalone quickstart](./docs/standalone-quickstart.md) — the SDK against any public endpoint, no Honua server | `npm run scan:arcgis:widgets -- ./src` — per-file 6.0 readiness report from the migration scanner |
| **Then** | [Data-to-map bridge cookbook](./docs/data-to-map-bridge.md) — `connect()` → `mountSource()` strategies, styling, filters | [Widget survival guide](./docs/widget-survival-guide.md) — all 38 deprecated widgets mapped to automated / assisted / manual dispositions |
| **Go deeper** | [MapLibre runtime](./docs/maplibre-runtime.md) · [React bindings](./docs/react.md) · [geometry ops](./docs/geometry.md) · [geocoding & routing providers](./docs/geocoding-routing-providers.md) | [`esri-compat`](./docs/migration-honua-maplibre.md) drop-ins + the `honua-migrate` codemod · [migration punch list](./docs/migration-punch-list.md) |
| **Runnable proof** | [`examples/endpoint-to-map/`](./examples/endpoint-to-map/README.md) — the headline above, live | [`migration-workbench`](./docs/migration-honua-maplibre.md) (`npm run demo:migration-workbench`) — scan → codemod → run, end to end |
## Where it fits
The rendering war is settled — MapLibre is the open 2D engine of record and Cesium owns
open 3D. What the open stack has been missing is the layer *above* the renderer: service
clients, styling, interactions, editing, geocoding, migration tooling — the glue every
team hand-rolls. That integration layer is what `@honua/sdk-js` owns:
- **Protocol-neutral data access.** One `Source.query(...)` call works against
GeoServices, OGC, WFS, OData and friends. Capability misses throw
`HonuaCapabilityNotSupportedError` instead of returning empty results.
- **Data to map in one call.** `connect()` + `mountSource()` turn a bare endpoint into a
styled, interactive MapLibre layer; `loadMapPackage(...)` + `HonuaMapRuntime` render
server-authored `MapPackage`s. Cesium, kepler.gl, and OGC web-map sources are first-class.
- **TypeScript first.** `strict` + `verbatimModuleSyntax`, exported types for every public
symbol, declaration maps, and JSDoc on the public client surface.
- **Migrate, don't rewrite.** `FeatureLayerCompat`, `MapImageLayerCompat`, `MapViewCompat`,
`SceneViewCompat`, `WebMapCompat`, and a safe codemod (`honua-migrate`) keep existing ArcGIS
code running while you cut over.
- **No provider lock-in for the extras.** Geocoding and routing are provider-pluggable
interfaces with open-source adapters, not a facade for one vendor's API
([`docs/geocoding-routing-providers.md`](./docs/geocoding-routing-providers.md)).
The honest comparisons are the service-client libraries, not the renderers:
- vs **`@esri/arcgis-rest-js`** — that's Esri's own client for Esri services only. Honua speaks
GeoServices *plus* OGC API / WFS / WMS / WMTS / STAC / OData under one typed contract, with a
capability model that throws instead of returning silently-empty results.
- vs **`esri-leaflet`** — dormant (no release since September 2025) and Leaflet-bound. Honua's
esri-compat + `honua-migrate` codemod is an actively maintained migration path that targets
MapLibre.
- vs **`openlayers` / `maplibre-gl` directly** — pick those when you need a renderer and are
happy hand-rolling service calls; pick Honua *on top of* MapLibre when you want the typed
client, the ArcGIS migration path, or the server-authored `MapPackage` runtime.
The numbers behind those claims — generated bundle sizes, a protocol-coverage matrix against
raw MapLibre / `@esri/arcgis-rest-js` / OpenLayers, and a scripted time-to-first-map benchmark
with a runnable repro (`npm run bench:ttfm`) — live in
[`docs/comparison.md`](./docs/comparison.md).
**Honua Server is optional for standards clients.** Supported GeoServices, OGC API
Features, WFS 2.0, STAC, and OData claims work against raw standards-speaking endpoints.
OGC API Tiles (`beta`), Maps (`beta`), and Records
(`beta`) also discover and use raw advertised paths. OGC API Processes
keeps two honest lanes: raw discovery is `experimental`, while typed execution
is `facade-required`.
A [Honua Server](https://github.com/honua-io/honua-server) adds server-authored
`MapPackage`s, realtime, collaboration, MCP/AI execution, compatibility metadata, and
the facade-required execution paths. See the generated
[backend-agnostic capability matrix](./docs/standalone-capability-matrix.md) for every
claim, execution mode, and evidence link.
## What Honua does not do
In the spirit of the [migration punch list](./docs/migration-punch-list.md), the
non-goals are explicit rather than implied:
- **It is not a rendering engine, on purpose.** 2D rendering rides
[MapLibre GL JS](https://maplibre.org/) and 3D rides [CesiumJS](https://cesium.com/platform/cesiumjs/);
Honua does not fork, wrap-and-hide, or compete with either. If you need renderer
features (custom shaders, globe projections, visual effects), take them from the
renderer directly — Honua stays out of the way.
- **No 3D parity with ArcGIS SceneView.** Esri's 3D moat is real and we say so. Honua's
`SceneViewCompat` covers 2D-safe behavior only, and 3D-only widgets (e.g. `Daylight`)
are honestly marked `no-equivalent` in the
[widget survival guide](./docs/widget-survival-guide.md). For SceneView-class 3D, use
[CesiumJS](https://cesium.com/platform/cesiumjs/) or stay on the
[ArcGIS Maps SDK](https://developers.arcgis.com/javascript/latest/) for that part of
your app.
- **Offline is experimental.** The `/offline` subpath is a replica-sync specification
without a production storage engine; it is excluded from the 1.0 narrative until a real
engine ships.
## Install
```bash
npm install @honua/sdk-js
```
Everything documented here ships in `@honua/sdk-js` as subpath entrypoints
(see [`INSTALL.md`](./INSTALL.md)). Focused standalone packages are also
published from this repository for consumers who only want a subset:
| Package | What it is |
|---------|------------|
| [`@honua/sdk-js`](https://www.npmjs.com/package/@honua/sdk-js) | **The canonical install** — full SDK with all subpath entrypoints + the `honua` CLI |
| [`@honua/mcp-server`](https://www.npmjs.com/package/@honua/mcp-server) | Platform-free geospatial MCP server (`honua-mcp`, `honua-mcp-proxy`) — see [`mcp/`](./mcp/README.md) |
| [`@honua/react`](https://www.npmjs.com/package/@honua/react) | React provider, hooks, and map components ([`docs/react.md`](./docs/react.md)) |
| [`@honua/geometry`](https://www.npmjs.com/package/@honua/geometry) | Curated turf/proj4 geometry ops + reprojection ([`docs/geometry.md`](./docs/geometry.md)) |
| [`@honua/sdk`](https://www.npmjs.com/package/@honua/sdk) | Core client + contract only (split build) |
| [`@honua/sdk-esri-compat`](https://www.npmjs.com/package/@honua/sdk-esri-compat) | ArcGIS JS compatibility layer (split build) |
| [`@honua/honua-migrate`](https://www.npmjs.com/package/@honua/honua-migrate) | Migration codemod + scanner (split build) |
| [`@honua/app-platform`](https://www.npmjs.com/package/@honua/app-platform) | Application-platform surfaces extracted from the SDK (own pre-1.0 cadence) |
The split builds exist for packaging workflows and subset consumers; details in
[`docs/split-packages.md`](./docs/split-packages.md).
### Build-less / CDN usage
For static sites, prototypes, or CSP-strict pages that can't run a bundler, a
prebuilt browser bundle is published under `dist/browser/`. Drop in the
minified IIFE build and use the global `window.HonuaSDK`:
```html
```
Or, for native ES module imports via an ESM CDN:
```html
```
The runtime peers (`maplibre-gl`, `cesium`, `@bufbuild/*`, `@connectrpc/*`) are
kept external — load them yourself when you need map rendering or gRPC
transport. See [`docs/browser-bundle.md`](./docs/browser-bundle.md) for details.
## Bundle size
Small and honest about size: every subpath entrypoint carries a min+gzip byte
budget that CI enforces on every PR (`npm run verify:bundle-budgets`), so drift
fails the build instead of shipping. Sizes are measured the way a consumer
builds — esbuild `--bundle --minify`, runtime peers external. A tree-shake guard
proves that importing a single symbol from the root doesn't drag the whole SDK
in.
| Entrypoint (gzip) | Size |
| --- | ---: |
| `@honua/sdk-js/geocoding` | 1.9 KiB |
| `@honua/sdk-js/expr` | 2.4 KiB |
| `@honua/sdk-js/webmap` | 5.9 KiB |
| `@honua/sdk-js/style` | 8.4 KiB |
| `@honua/sdk-js/map` | 31.2 KiB |
| `@honua/sdk-js` (root) | 94.9 KiB |
| `{ HonuaClient }` only (tree-shake guard) | 48.6 KiB |
Full per-entrypoint table (min + gzip, generated, not hand-written):
[`docs/bundle-sizes.md`](./docs/bundle-sizes.md). Refresh it with
`npm run report:bundle-sizes`. For how these sizes stack up against
`@arcgis/core` and friends, see the generated
[comparison page](./docs/comparison.md).
## 60-second quickstart
**No Honua server required.** The first block below runs against a *public* Esri
GeoServices endpoint — no API key, no account, no infrastructure. The canonical
surface is protocol-neutral: build a `Dataset` over one or more `Source`s, then
call `queryAll()` (or `query()` / `stream()`).
```ts doc-test=compile
import { createDataset, HonuaClient, PROTOCOL_DEFAULT_CAPABILITIES } from "@honua/sdk-js";
// A public Esri Living Atlas FeatureServer — nothing of Honua's is running.
const client = new HonuaClient({
baseUrl: "https://services.arcgis.com/P3ePLMYs2RVChkJx/arcgis",
});
const dataset = createDataset({
id: "states",
client,
sources: [
{
id: "apportionment",
protocol: "geoservices-feature-service",
locator: {
url: "https://services.arcgis.com/P3ePLMYs2RVChkJx/arcgis",
serviceId: "2020_Census_State_Apportionment",
layerId: 0,
},
capabilities: PROTOCOL_DEFAULT_CAPABILITIES["geoservices-feature-service"],
},
],
});
const states = dataset.source("apportionment")!;
const result = await states.queryAll({
where: "Seats_2020 > 10",
outFields: ["NAME", "Total_Pop_2020", "Seats_2020"],
returnGeometry: true,
pagination: { limit: 100 },
});
console.log(`Loaded ${result.features.length} states`);
```
The same code works against any GeoServices, OGC API Features, WFS, OData, or
STAC endpoint. Migrating from `esri-leaflet`? The raw GeoServices shape and the
`esri-compat` drop-in point at `services.arcgis.com`-style URLs unchanged:
```ts doc-test=skip reason="partial excerpt requires application host context"
const { features } = await client.queryFeatures({
serviceId: "2020_Census_State_Apportionment",
layerId: 0,
where: "1=1",
outFields: ["*"],
returnGeometry: true,
resultRecordCount: 25,
});
```
Run the complete standalone app locally — public endpoint in, MapLibre map out:
```bash
npm install
npm run demo:standalone:mock # deterministic fixture lane (what CI runs)
npm run demo:standalone # live lane against the public Esri endpoint
```
See [`docs/standalone-quickstart.md`](./docs/standalone-quickstart.md) for the
guided server-optional walkthrough,
[`docs/standalone-capability-matrix.md`](./docs/standalone-capability-matrix.md)
for the backend-agnostic vs server-enhanced breakdown, and
[`examples/standalone-quickstart/`](./examples/standalone-quickstart/README.md)
for the committed source.
### Add a Honua Server
A [Honua Server](https://github.com/honua-io/honua-server) unlocks
server-authored `MapPackage`s (`loadMapPackage()`), realtime subscriptions,
collaboration / saved maps, and the MCP + AI surfaces. Point the same code at a
local server (`docker compose up` in a honua-server checkout), and gate
production reads on the compatibility check:
```ts doc-test=skip reason="partial excerpt requires application host context"
const { supported, reasons } = await client.checkCompatibility();
if (!supported) throw new Error(`Unsupported Honua server: ${reasons.join("; ")}`);
```
The server-connected lane is the [`maplibre-quickstart`](./examples/maplibre-quickstart/README.md)
example (`npm run demo:quickstart:mock`); see
[`docs/quickstart.md`](./docs/quickstart.md) and
[`docs/quickstart-troubleshooting.md`](./docs/quickstart-troubleshooting.md).
## Command-line client (`honua`)
Installing the SDK also installs a first-class **`honua` CLI** — the same
querying and catalog browsing without writing code. It wraps the SDK (no raw
HTTP, no URL-encoding, no `f=json`), prints readable tables by default, and adds
`--json` / `--format geojson` for machine output.
```bash
npm i -g @honua/sdk-js # or: npx @honua/sdk-js honua
export HONUA_BASE_URL=https://demo.honua.io # anonymous reads on the public demo
honua services # list published services
honua layers maui-parcels # list a service's layers
honua query maui-parcels/1 --count
honua query maui-parcels/1 --where "tmk_txt LIKE '2%'" --limit 5
honua query maui-parcels/1 --bbox -156.7,20.7,-156.3,21.0 --format geojson
honua stac collections
honua geocode "1 Honolulu Pl, HI"
honua map export maui-parcels --bbox -156.7,20.7,-156.3,21.0 --size 800x600 -o maui.png
```
Authentication resolves from `--api-key`, `HONUA_API_KEY`, or a saved
`honua login`. Run `honua --help` for the full command surface. This is the
recommended replacement for `curl` in docs and demos.
For support-safe interoperability evidence, `honua doctor` emits a local,
schema-validated diagnostic bundle with explicit classification/consent,
credential and PII redaction, bounded previews, and original-byte SHA-256
metadata. `honua doctor --replay` permits only one bounded, abortable
`GET`/`HEAD` and fails closed before network access for mutations,
subscriptions, unsafe paths, credentials, malformed schemas, or hash drift. It
never uploads. See [`docs/diagnostic-bundles.md`](./docs/diagnostic-bundles.md).
## What you can build
The versioned [SDK sample catalog](./docs/generated/sample-catalog.md) tracks all 34 executable examples: 0 qualified golden samples, 15 recipes, 17 labs, and 2 fixtures. Seven journey IDs are reserved; 7 remain explicitly planned candidates. The catalog is the source of truth for track, support, lifecycle, fixture/live evidence, quality profiles, and the honua.io projection.
Linking to Honua from a plugin directory or ecosystem list? Point at the
[server-optional standalone quickstart](./docs/standalone-quickstart.md)
([hosted walkthrough](https://honua-io.github.io/honua-sdk-js/guides/standalone-quickstart.html),
[source](./examples/standalone-quickstart/README.md)) — CI keeps it green with a Playwright
browser smoke on every PR. Prepared directory entries live in
[`docs/listings/maplibre-plugin-directory.md`](./docs/listings/maplibre-plugin-directory.md).
## Mental model: `Dataset` → `Source` → `Query` → `Result`
Every Honua SDK — JavaScript, Python, .NET — speaks the same canonical
vocabulary. A `Dataset` groups one or more `Source`s. Each `Source` accepts a
protocol-neutral `Query` and returns a protocol-neutral `Result`. Operations the
canonical surface does not cover stay reachable through the typed
`source.protocol(...)` escape hatch. Method casing differs by language
(`queryAll()` / `query_all()` / `QueryAllAsync()`), the semantics do not.
Capability misses throw `HonuaCapabilityNotSupportedError` (under the default
`strict` policy) rather than silently returning empty results. See the
[60-second quickstart](#60-second-quickstart) above for the runnable shape; the
cross-language semantics, protocol/capability identifiers, language-binding
tables, and backwards-compatibility policy live in:
- [`docs/sdk-surface-alignment.md`](./docs/sdk-surface-alignment.md) — cross-language naming + semver
- [`docs/shared-client-contract.md`](./docs/shared-client-contract.md) — contract design
- [`docs/protocol-capability-matrix.md`](./docs/protocol-capability-matrix.md) — what each protocol supports
## Documentation
- [`docs/generated/learning-paths.md`](./docs/generated/learning-paths.md) — task-oriented progression backed by runnable examples and checked SDK imports
- [`docs/quickstart.md`](./docs/quickstart.md) — guided quickstart walkthrough
- [`docs/guide.md`](./docs/guide.md) — long-form reference (server compatibility, subpath
entrypoints, OGC / WFS / OData cookbooks, MapLibre runtime, migration CLI, request/auth bridge)
- [`docs/errors.md`](./docs/errors.md) — error class reference + retry policy
- [`docs/shared-client-contract.md`](./docs/shared-client-contract.md) — `Dataset` / `Source` / `Query` / `Result` design
- [`docs/protocol-capability-matrix.md`](./docs/protocol-capability-matrix.md) — what each protocol supports
- [`docs/sdk-surface-alignment.md`](./docs/sdk-surface-alignment.md) — cross-language naming & semver policy
- [`docs/maplibre-runtime.md`](./docs/maplibre-runtime.md) — `loadMapPackage()` / `HonuaMapRuntime`
- [`docs/data-to-map-bridge.md`](./docs/data-to-map-bridge.md) — `connect()` → `mountSource()` standalone bridge cookbook
- [`docs/react.md`](./docs/react.md) — React bindings (`@honua/react`): provider, hooks, and map components
- [`docs/geometry.md`](./docs/geometry.md) — `@honua/sdk-js/geometry` curated turf/proj4 ops (buffer/area/measure/simplify/reproject) + the `geometryEngine` compat shim
- [`docs/geocoding-routing-providers.md`](./docs/geocoding-routing-providers.md) — provider-pluggable geocoding & routing adapters
- [`docs/studio-package-contracts.md`](./docs/studio-package-contracts.md) — Studio package-family projections, validation envelope, capability manifest (`@honua/app-platform/studio`)
- [`docs/features/README.md`](./docs/features/README.md) — capability snapshot
- [`docs/docs-samples-ownership.md`](./docs/docs-samples-ownership.md) — SDK/site ownership boundary for versioned docs and executable samples
- [`docs/documentation-snippets.md`](./docs/documentation-snippets.md) — supported code-fence validation and explicit pseudocode directives
- [`INSTALL.md`](./INSTALL.md) — install + subpath entrypoint table
Platform-wide documentation (server concepts, deployment, Esri migration) lives
at [honua.gitbook.io/honuaio](https://honua.gitbook.io/honuaio/).
Run `npm run docs:learning:verify` from a fresh checkout to build the SDK and
validate learning-path metadata, internal links, generated Markdown, and runtime
imports. CI reuses its existing build with the internal
`npm run docs:learning:check` command, then separately compiles every selected
example through `npm run docs:learning:typecheck`.
Run `npm run docs:snippets:verify` to build the public declarations and validate
all supported JavaScript and TypeScript documentation fences.
## AI assistants
Coding agents (Claude Code, Cursor, and compatible assistants) can discover and
correctly use this SDK:
- **[`llms.txt`](./llms.txt)** — a curated [llms.txt](https://llmstxt.org/) index
of the docs, plus **[`llms-full.txt`](./llms-full.txt)** with the full corpus
concatenated for single-fetch ingestion. Both are generated from `docs/` +
`README.md` + entrypoint JSDoc by `npm run docs:llms` (freshness-checked in CI
via `npm run verify:llms`).
- **Agent skills** under [`skills/`](./skills/README.md) — `honua-sdk-quickstart`,
`honua-arcgis-migration`, and `honua-mcp-setup` load procedural instructions
into Claude Code and compatible agents. See [`skills/README.md`](./skills/README.md)
for installation.
- **MCP server** — [`@honua/mcp-server`](./mcp/README.md) is the **platform-free**
geospatial MCP server: point `honua-mcp` at **any** public ArcGIS FeatureServer
or OGC API endpoint (no Honua server required) and it exposes discovery, query,
and analysis tools to assistants over the Model Context Protocol. Tools that need
a Honua-only surface degrade gracefully with a structured "not available on this
target" result. A Honua deployment's richer `/mcp` catalog is the upgrade path
via `honua-mcp-proxy`.
- **NL map control** — [`@honua/sdk-js/nl-map-control`](./docs/nl-map-control.md)
compiles natural-language instructions into serializable, inspectable plans
(query-planner IR plus agent-tool invocations) through a **caller-provided LLM
callback** — no model-vendor SDK. Execution accepts plans only: read-only
plans may auto-execute under policy, anything mutating or viewport-changing
requires a signed agent-safety approval envelope, and every execution emits a
receipt. The same tool surface is published in MCP and OpenAI function
formats.
- **Context7** — [`context7.json`](./context7.json) registers the library so
[Context7](https://context7.com) serves current docs to coding agents; the
submission steps are in [`skills/README.md`](./skills/README.md).
- **Coding-agent evals** — a scheduled harness measures whether coding agents
can use the SDK correctly on the first try: a 16-task golden-workflow corpus
scored objectively (typecheck + runtime against deterministic fixtures +
expected-output assertions). Methodology in
[`docs/coding-agent-evals.md`](./docs/coding-agent-evals.md); latest results
in [`docs/generated/coding-agent-scorecard.md`](./docs/generated/coding-agent-scorecard.md).
## Stability and versioning
- The package root is intentionally the small, reviewed
`connect → query → explain → mount` workflow. Protocol-specific clients,
renderers, app state, styling, migration, realtime, offline, and analytics
APIs remain supported from focused subpaths; no advanced API was deleted. The
exact root inventory and every former-root replacement are published in
[`config/root-surface.json`](./config/root-surface.json) and the generated
[`root import migration table`](./docs/root-surface-migration.md).
- Hosted guides and API pages display their exact release and expose validated
current/archived navigation. See
[documentation versions and compatibility](./docs/documentation-versions.md);
the selector is generated from package, release-manifest, and changelog data.
- The SDK follows [Semantic Versioning](https://semver.org/). The public contract is the set
of symbols reachable from the documented subpath entrypoints in [`INSTALL.md`](./INSTALL.md).
- Symbols marked `@experimental` in JSDoc may change in any minor release. The full table of
stable and experimental subpaths lives in [`INSTALL.md`](./INSTALL.md). The short version:
- **Stable** (semver-protected): `@honua/sdk-js`, `@honua/sdk-js/browser`,
`@honua/sdk-js/honua`, `@honua/sdk-js/auth`, `@honua/sdk-js/contract`,
`@honua/sdk-js/esri-compat`, `@honua/sdk-js/migration`,
`@honua/sdk-js/runtime`, `@honua/sdk-js/expr`, `@honua/sdk-js/webmap`,
`@honua/sdk-js/geocoding`, `@honua/sdk-js/exploration`, `@honua/sdk-js/interactions`,
`@honua/sdk-js/filter-registry`, `@honua/sdk-js/style`, `@honua/sdk-js/map`,
`@honua/sdk-js/realtime`, `@honua/sdk-js/react`, `@honua/sdk-js/geometry`,
`@honua/sdk-js/cli`, `@honua/sdk-js/agent-tools`, `@honua/sdk-js/agent-safety`.
The agent surface's security posture is documented in the
[agent-safety threat model](./docs/agent-safety-threat-model.md).
- **Experimental subpath-only APIs** (not re-exported from the root barrels):
`/nl-map-control`, `/geoparquet`, `/source-schema`, `/source-capabilities`, `/source-capability-discovery`, `/plugin`, `/deckgl`,
`/offline`, `/diagnostics`, `/routing` — with `/query-planner` below, 11 experimental subpaths in total.
- The complete `/query-planner` subpath remains **experimental**. The stable root promotes a
reviewed query-planner subset: `explainQuery`, `executeQueryPlan`, `hashQueryPlan`, the plan
errors/version constants, and the types required to name the common explain/mount workflow.
- **Application-platform surfaces** (`/app`, `/app-controller`, `/app-workspace`,
`/scene-workspace`, `/collaboration`, `/control-plane`, `/replica-sync`, `/share`,
`/operate`, `/generated-app`, `/studio`, `/controls`, `/web-components`, `/operator`,
`/operator/*`) have **moved to the separate `@honua/app-platform` package**; the old
18 deprecated compatibility subpaths remain through `0.1.x` and are removed in
`0.2.0`. The `/console` entrypoint was removed outright (no shim).
## Support and lifecycle
We publish a lifecycle because a library you build on should tell you what it promises.
`esri-leaflet` never did, and "will this break under me?" is the question that decides adoption.
- **Today (pre-1.0, `0.x`).** The **stable tier** — the subpath entrypoints listed under
"Stable subpath entrypoints" in [`INSTALL.md`](./INSTALL.md) — is where we invest
compatibility effort, and it is guarded in CI by a public-API report
(`npm run verify:api-report`): no symbol leaves or changes shape by accident. While we are
on `0.x` a minor _may_ still change a stable symbol, but only as a reviewed, called-out
change — never silently. Symbols marked `@experimental` in JSDoc may change in any minor.
- **At 1.0.** The stable tier freezes under [Semantic Versioning](https://semver.org/):
breaking or removing a stable symbol requires a major version; minors are additive. Major
versions are coordinated across the Honua SDK family (JavaScript, Python, .NET) so one semver
line describes the contract on every platform.
- **Application-platform surfaces move separately.** App-shell, builder, and hosted-product
entrypoints are being extracted to a separate `@honua/app-platform` package that versions at
its own pre-1.0 cadence, so the client SDK can reach a frozen 1.0 without waiting on them.
Their old `@honua/sdk-js/*` subpaths remain as `@deprecated` re-export shims through
`0.1.x` and are removed in `0.2.0`. See
[`docs/decisions/scope-split-and-1.0.md`](./docs/decisions/scope-split-and-1.0.md).
- **What we don't promise.** No security-backport window or LTS branch pre-1.0; fixes land on
the current line. We will publish that policy when we cut 1.0.
## More guides
Long-form reference material now lives in [`docs/guide.md`](./docs/guide.md):
- Demo apps in depth (each example's env contract, browser hooks, run lanes)
- Server compatibility baseline + `checkCompatibility()` contract
- Subpath entrypoints and the `@experimental` tier
- Protocol cookbooks — OGC Features / Tiles / Maps / Processes / STAC, WMS / WMTS, WFS 2.0, OData v4
- Mixed Esri + OGC composition, streaming pagination, event lifecycle
- MapLibre `MapPackage` runtime + Generated App preview runtime
- Request/Auth bridge (interceptors, ArcGIS token + esri-request interop)
- `honua-migrate` CLI, admin scanner, parity matrix, sample-corpus harness
Protocol-specific deep dives also live alongside the guide: see
[`docs/wfs.md`](./docs/wfs.md), [`docs/ogc-api.md`](./docs/ogc-api.md),
[`docs/maplibre-runtime.md`](./docs/maplibre-runtime.md),
[`docs/webmap-json-compatibility.md`](./docs/webmap-json-compatibility.md),
[`docs/protocol-capability-matrix.md`](./docs/protocol-capability-matrix.md),
[`docs/migration-punch-list.md`](./docs/migration-punch-list.md), and
[`docs/widget-survival-guide.md`](./docs/widget-survival-guide.md) (every ArcGIS widget
deprecated at 5.0 mapped to its Honua/MapLibre disposition ahead of the 6.0 removal — run
`npm run scan:arcgis:widgets -- ./src` for a per-file readiness report).
## Related Honua repositories
| Repo | What it is |
|------|------------|
| [honua-server](https://github.com/honua-io/honua-server) | Flagship multi-protocol geospatial server (ELv2 open core) |
| [honua-console](https://github.com/honua-io/honua-console) | Unified web console — Studio, Catalog, Operate, Share |
| [honua-sdk-python](https://github.com/honua-io/honua-sdk-python) | Python SDK (same `Dataset`/`Source`/`Query`/`Result` contract) |
| [honua-sdk-dotnet](https://github.com/honua-io/honua-sdk-dotnet) | .NET SDKs (same contract) |
| [honua-esri-assess](https://github.com/honua-io/honua-esri-assess) | Esri footprint assessment CLI for migration discovery |
| [geospatial-mcp](https://github.com/honua-io/geospatial-mcp) | Open, vendor-neutral geospatial MCP standard |
## Contributing
This SDK ships from a single repository: the canonical package is
`@honua/sdk-js` (all subpath entrypoints in [`INSTALL.md`](./INSTALL.md) live
under that name), with the standalone packages in the table above built from
the same source tree. The MCP server lives in [`mcp/`](./mcp/README.md).
See [`AGENTS.md`](./AGENTS.md) for contributor instructions and the Specifica
issue format used for backlog items.
## Security
Report vulnerabilities to [security@honua.io](mailto:security@honua.io) — see the
[org security policy](https://github.com/honua-io/.github/blob/main/SECURITY.md).
Please do not open public issues for security reports.
## License
[Apache 2.0](./LICENSE)
---
# File: INSTALL.md
# Installing the Honua JavaScript SDK
The Honua JavaScript SDK ships as a single npm package — **`@honua/sdk-js`** — with multiple
subpath entrypoints. The core client, the Esri compatibility layer, the migration helpers,
and the protocol-neutral contract are all reachable from this one install.
## Generated support status
The versioned source of truth is [`config/support-manifest.v1.json`](./config/support-manifest.v1.json).
It projects 22 supported (documented below as stable), 11 experimental,
and 18 deprecated package entrypoints. Protocol status is independent
of package lifecycle: raw endpoint support, facade requirements, execution mode, and
evidence are listed in the generated
[backend-agnostic capability matrix](./docs/standalone-capability-matrix.md). The generic
[support projection](./support/projections/sdk-support.v1.json) carries explicit contracts
for both honua.io and the canonical `samples/catalog.v2.json` inventory.
## Stable subpath entrypoints
Subpaths covered by the SDK's semver contract. Symbols reachable from these
entrypoints are stable across minor versions.
| Subpath | What it gives you |
|---------|-------------------|
| `@honua/sdk-js` | Reviewed common `connect → query → explain → mount` surface (37 runtime / 123 declarations) |
| `@honua/sdk-js/browser` | Prebuilt browser ESM build of the default barrel (same API as `@honua/sdk-js`) |
| `@honua/sdk-js/honua` | `HonuaClient` (the raw GeoServices/OGC client) |
| `@honua/sdk-js/auth` | OAuth2/PKCE, client credentials, static providers, and credential stores |
| `@honua/sdk-js/contract` | Protocol-neutral `Dataset` / `Source` / `Query` / `Result` + `createDataset` |
| `@honua/sdk-js/esri-compat` | Esri ArcGIS JS-API compatibility layer for migration |
| `@honua/sdk-js/migration` | Programmatic migration helpers (codemod runner, scan reports) |
| `@honua/sdk-js/runtime` | MapLibre `MapPackage` runtime (`loadMapPackage`, `HonuaMapRuntime`) |
| `@honua/sdk-js/expr` | Honua expression builder |
| `@honua/sdk-js/webmap` | WebMap JSON load/save helpers |
| `@honua/sdk-js/geocoding` | Geocoding adapters |
| `@honua/sdk-js/exploration` | Linked-view exploration state + presets |
| `@honua/sdk-js/interactions` | Hit-test + pointer normalization + chart/map bindings |
| `@honua/sdk-js/filter-registry` | Shared filter clause registry + projections |
| `@honua/sdk-js/style` | Honua style spec + source parsers/validators |
| `@honua/sdk-js/map` | `HonuaMap` programmatic map container |
| `@honua/sdk-js/realtime` | Realtime transport adapters (SSE) — a client-transport primitive |
| `@honua/sdk-js/react` | React provider, hooks, and map components (optional `react` / `react-dom` peers; published standalone as `@honua/react`) |
| `@honua/sdk-js/geometry` | Curated turf/proj4 client-side geometry ops (buffer/area/simplify/reproject) |
| `@honua/sdk-js/cli` | Programmatic `run()` entrypoint for the `honua` command-line client |
| `@honua/sdk-js/agent-tools` | Agent-facing JSON Schema tool definitions and the bounded tool executor (MCP/OpenAI compatible); the in-repo `@honua/mcp-server` consumes it. See the [agent-safety threat model](./docs/agent-safety-threat-model.md). |
| `@honua/sdk-js/agent-safety` | Bounded runtime validation, effect budgets, signed approval envelopes, authenticated single-use consumption, context revalidation, and deterministic execution-receipt verification for JSON-compatible plans ([threat model](./docs/agent-safety-threat-model.md)). |
## Experimental subpath entrypoints
Subpaths marked `@experimental` in JSDoc. Useful today; the shape may change in
any minor release prior to `1.0.0`. The stable root promotes a reviewed
query-planner subset: `explainQuery`, `executeQueryPlan`, `hashQueryPlan`, the
plan errors/version constants, and the types required to name the common
explain/mount workflow. The complete `@honua/sdk-js/query-planner` subpath
remains experimental. Every other experimental subpath is subpath-only and is
not re-exported from `@honua/sdk-js` or `@honua/sdk-js/honua`.
| Subpath | What it gives you |
|---------|-------------------|
| `@honua/sdk-js/geoparquet` | GeoParquet / DuckDB-WASM–backed protocol-neutral `Source`; the optional DuckDB peer loads lazily. |
| `@honua/sdk-js/query-planner` | Deterministic query IR, side-effect-free explain plans, GeoServices compilation, and explicitly bounded local execution. |
| `@honua/sdk-js/source-schema` | Focused experimental SourceSchemaV2 validation, GeoServices/OData/GeoParquet normalization, and one-pass opt-in `connectWithSourceSchemaV2()` discovery. |
| `@honua/sdk-js/source-capabilities` | SourceSchemaV2-bound static evidence/CRS validation plus lightweight claimed/observed/effective evaluation, current-source cache checks, bounded strict transport, and dynamic policy/peer/authorization gates; serialized caller data remains potentially sensitive ([guide](./docs/source-capabilities.md)). |
| `@honua/sdk-js/source-capability-discovery` | Focused GeoServices/OData `connectWithSourceCapabilities()` integration with canonical descriptor replay binding and fresh policy/environment/peer/authorization evaluation. |
| `@honua/sdk-js/plugin` | Versioned, data-only plugin manifests plus deterministic compatibility and authority-boundary certification reports. |
| `@honua/sdk-js/deckgl` | Bounded, zero-copy typed-array projection into an optional deck.gl peer, with stable picking identity and deterministic disposal. |
| `@honua/sdk-js/offline` | [Versioned downloadable-region manifests](./docs/offline-regions.md) plus storage-neutral quota, integrity, cancellation, and atomic commit contracts. |
| `@honua/sdk-js/diagnostics` | Dependency-free diagnostic-bundle validation, sanitization, integrity pinning, and bounded read-only replay used by `honua doctor`. |
| `@honua/sdk-js/nl-map-control` | Natural-language map control ([safety model + walkthrough](./docs/nl-map-control.md)): compiles NL instructions into serializable, inspectable plans (query-planner IR + agent-tool invocations) via a caller-provided LLM callback; execution accepts plans only, gates mutations behind agent-safety envelopes, and emits receipts. |
| `@honua/sdk-js/routing` | Provider-pluggable routing: typed `RoutingProvider` contract, OSRM and Valhalla adapters, the Honua facade bridge, and per-provider attribution/usage-policy metadata ([provider cookbook](./docs/geocoding-routing-providers.md)). |
## Deprecated compatibility entrypoints
These temporary `@honua/sdk-js` shims were introduced in `0.1.0-beta.0` when
the application platform moved. They remain available throughout `0.1.x` and
are removed in `0.2.0`; new code must import the replacement directly.
| Deprecated subpath | Replacement | Remove in |
|--------------------|-------------|-----------|
| `@honua/sdk-js/app` | `@honua/app-platform/app` | `0.2.0` |
| `@honua/sdk-js/app-controller` | `@honua/app-platform/app-controller` | `0.2.0` |
| `@honua/sdk-js/app-workspace` | `@honua/app-platform/app-workspace` | `0.2.0` |
| `@honua/sdk-js/scene-workspace` | `@honua/app-platform/scene-workspace` | `0.2.0` |
| `@honua/sdk-js/collaboration` | `@honua/app-platform/collaboration` | `0.2.0` |
| `@honua/sdk-js/control-plane` | `@honua/app-platform/control-plane` | `0.2.0` |
| `@honua/sdk-js/replica-sync` | `@honua/app-platform/replica-sync` | `0.2.0` |
| `@honua/sdk-js/share` | `@honua/app-platform/share` | `0.2.0` |
| `@honua/sdk-js/operate` | `@honua/app-platform/operate` | `0.2.0` |
| `@honua/sdk-js/generated-app` | `@honua/app-platform/generated-app` | `0.2.0` |
| `@honua/sdk-js/studio` | `@honua/app-platform/studio` | `0.2.0` |
| `@honua/sdk-js/operator` | `@honua/app-platform/operator` | `0.2.0` |
| `@honua/sdk-js/operator/controllers` | `@honua/app-platform/operator/controllers` | `0.2.0` |
| `@honua/sdk-js/operator/workspace` | `@honua/app-platform/operator/workspace` | `0.2.0` |
| `@honua/sdk-js/operator/theming` | `@honua/app-platform/operator/theming` | `0.2.0` |
| `@honua/sdk-js/operator/i18n` | `@honua/app-platform/operator/i18n` | `0.2.0` |
| `@honua/sdk-js/controls` | `@honua/app-platform/controls` | `0.2.0` |
| `@honua/sdk-js/web-components` | `@honua/app-platform/web-components` | `0.2.0` |
## Application-platform entrypoints (`@honua/app-platform`)
App-shell, app-builder, and hosted-product surfaces have moved out of the client
SDK into the separate **`@honua/app-platform`** package, which versions at its
own pre-1.0 cadence so `@honua/sdk-js` can reach a frozen 1.0 without waiting on
them (see [`docs/decisions/scope-split-and-1.0.md`](./docs/decisions/scope-split-and-1.0.md)).
| `@honua/app-platform` subpath | What it gives you |
|-------------------------------|-------------------|
| `@honua/app-platform/app` | App bootstrap helper for browser shells |
| `@honua/app-platform/app-controller` | `HonuaController` — renderer-neutral app controller |
| `@honua/app-platform/app-workspace` | Framework-neutral workspace state orchestration |
| `@honua/app-platform/scene-workspace` | 3D scene workspace + MapLibre/Cesium adapters (optional `cesium` peer) |
| `@honua/app-platform/collaboration` | Saved-map collaboration client |
| `@honua/app-platform/control-plane` | Hosted-product / admin client |
| `@honua/app-platform/replica-sync` | Offline-replica sync client |
| `@honua/app-platform/share` | Embed-token + DCAT sharing helpers |
| `@honua/app-platform/operate` | Operations/observability client |
| `@honua/app-platform/generated-app` | Manifest projection + preview runtime for generated apps |
| `@honua/app-platform/studio` | Studio package-family projections, validation/preview envelopes, capability manifest, publish/share/embed contracts (MCP/QGIS-safe) |
| `@honua/app-platform/controls` | Native UI control kit (``) for MapLibre maps |
| `@honua/app-platform/web-components` | Framework-neutral custom elements |
| `@honua/app-platform/operator` | Operator-native chat/plan-review/approval controllers |
| `@honua/app-platform/operator/controllers` | Framework-neutral controllers behind `/operator` |
| `@honua/app-platform/operator/workspace` | Operator workspace state container |
| `@honua/app-platform/operator/theming` | Operator design-system theme provider + tokens |
| `@honua/app-platform/operator/i18n` | Operator message catalog + resolution |
During the transition, the deprecated imports listed above keep working through
`0.1.x`; they are removed in `0.2.0`. Update imports to
`@honua/app-platform/`. The `@honua/sdk-js/console`
entrypoint was removed outright (its projection helpers are owned by the
`@honua/console` application) and has no shim.
## Prerequisites
- Node.js 20 or later
- A Honua Server instance only for capabilities marked `facade-required`; standalone protocol clients do not require one
## Install
```bash
npm install @honua/sdk-js
```
### Optional peer dependencies
A few integration paths are gated behind **optional peer dependencies** so a
Node-only or REST-only consumer never pays the install cost:
| Integration | Peer to install |
|-------------|-----------------|
| MapLibre `MapPackage` runtime (`@honua/sdk-js/runtime`) | `npm install maplibre-gl` |
| deck.gl binary projection (`@honua/sdk-js/deckgl`) | `npm install @deck.gl/layers` |
| Cesium 3D adapters (`@honua/app-platform/scene-workspace`) | `npm install cesium` |
| gRPC-Web transport (`new HonuaClient({ transport: "grpc-web" })`) | `npm install @connectrpc/connect @connectrpc/connect-web @bufbuild/protobuf` |
| Geometry ops (`@honua/sdk-js/geometry`) | `npm install proj4 @turf/buffer @turf/area …` (only the ops you import) — or use the `@honua/geometry` split package |
| Migration API (`@honua/sdk-js/migration`) | `npm install typescript` |
If you stay on the default REST transport with no MapLibre/Cesium scene work,
no extra installs are required.
## Quick Start
```typescript doc-test=compile
import { HonuaClient } from "@honua/sdk-js";
const client = new HonuaClient({
baseUrl: "https://your-honua-server.com",
});
const compatibility = await client.checkCompatibility();
if (!compatibility.supported) {
throw new Error(
`Unsupported Honua server. Minimum supported version: ${HonuaClient.minimumSupportedServerVersion}. ` +
`Reasons: ${compatibility.reasons.join("; ")}`,
);
}
const result = await client.queryFeatures({
serviceId: "natural-earth",
layerId: 0,
where: "1=1",
returnGeometry: true,
outFields: ["*"],
outSr: 4326,
resultRecordCount: 25,
});
const featureCount = result.features?.length ?? 0;
console.log(`Found ${featureCount} feature(s)`);
```
Use focused stable subpaths for advanced APIs. The generated
[`root import migration table`](./docs/root-surface-migration.md) maps every
symbol removed from the transition-era root to its supported replacement; the
machine-readable source is [`config/root-surface.json`](./config/root-surface.json).
`checkCompatibility()` reads the parsed `data.compatibility` contract from
`GET /api/v1/admin/capabilities`. For a runnable browser example from this repo,
including the renderable-geometry checks used by the committed MapLibre quickstart,
see [`examples/maplibre-quickstart/README.md`](./examples/maplibre-quickstart/README.md).
## Canonical Contract And Exploration
The SDK exposes a protocol-neutral client contract and exploration state module that
wrap the existing `HonuaFeatureLayer` / `HonuaMapService` / `HonuaOgcFeatureCollection`
classes. These are reachable via the `@honua/sdk-js/contract` and
`@honua/sdk-js/exploration` subpaths:
- [`docs/shared-client-contract.md`](./docs/shared-client-contract.md) — `Dataset`, `Source`, `Capabilities`,
`Query`, `Result`, `MapBinding`, and `createDataset(...)`.
- [`docs/exploration-context.md`](./docs/exploration-context.md) — `createExplorationContext(...)` with
linked-view presets (`globalLinked`, `mapDriven`, `gridDriven`, `chartDriven`, `decoupled`).
- [`docs/protocol-capability-matrix.md`](./docs/protocol-capability-matrix.md) — capability coverage per
protocol (`geoservices-feature-service`, `geoservices-map-service`, `geoservices-image-service`,
`geoservices-geometry-service`, `geoservices-gp-service`, `ogc-features`, `ogc-tiles`, `ogc-maps`,
`stac`, `wfs`, `wms`, `odata`).
- [`docs/wfs.md`](./docs/wfs.md) — first-party WFS 2.0 adapter, FES filter translation,
content-type negotiation, transactions, stored queries, and the `Source.protocol("wfs")`
escape hatch.
- [`docs/source-binding-alignment.md`](./docs/source-binding-alignment.md) — round-trip mapping between
`SourceDescriptor` and the server `SourceBinding` document.
- [`docs/maplibre-runtime.md`](./docs/maplibre-runtime.md) — `loadMapPackage(...)` and `HonuaMapRuntime`
for the MapLibre GL JS-first `MapPackage` runtime.
## Esri Migration
The migration helpers live behind the `@honua/sdk-js/migration` subpath. They
power the same codemod that the standalone CLI runs:
```typescript doc-test=compile
import { runEsriCompatCodemod, scanArcGisUsage } from "@honua/sdk-js/migration";
const report = scanArcGisUsage("./src");
const migration = runEsriCompatCodemod({ rootDir: "./src", write: true });
```
## Version Policy
- **Documentation version:** Hosted guides and API entrypoints identify their
exact SDK release. Use the generated
[documentation version selector](./docs/documentation-versions.md) for tagged
archived guides, compatibility ranges, and migration links.
- **Pre-release** (`-alpha.*`, `-beta.*`): Published to npm with `@alpha` / `@beta` dist-tags.
- **Stable** (`1.0.0+`): Published to npm as `@latest`.
- **Semver:** All releases follow [Semantic Versioning](https://semver.org/). Public symbols
reachable from the documented subpaths above are covered by the contract; symbols marked
`@experimental` in JSDoc may change in any minor release.
- **Cross-language alignment:** Major versions are coordinated across the Honua SDK family
(JavaScript, Python, .NET) so a single semver line tells you what the contract is on every
platform.
> **Advanced packaging.** Downstream packagers can also produce a three-package
> split (`@honua/sdk` / `@honua/sdk-esri-compat` / `@honua/honua-migrate`) via
> `npm run build:split-packages`. This is an opt-in build target, not the default
> consumer install. See [`docs/split-packages.md`](./docs/split-packages.md) if you
> are integrating with a downstream registry that needs the smaller surfaces.
Maintainers can verify the artifact consumers actually install with
`npm run verify:packed-sdk` after the library and browser builds. The gate packs
the root package, installs it offline into an isolated ESM project, imports every
stable and experimental subpath, typechecks every shipped declaration target,
and runs the packaged `honua --help` command. Temporary package and consumer
directories are removed after both successful and failed runs.
---
# File: docs/documentation-versions.md
# Documentation versions and compatibility
You are reading the documentation for `@honua/sdk-js` **trunk development (package baseline 0.1.0-beta.0)**.
Hosted pages are development documentation built from `trunk`, not immutable
documentation for the package baseline. Every built page and the deployed
`versions.json` identify the exact 40-character source commit.
The version navigation in every hosted guide is generated from
[`docs/versions.json`](https://honua-io.github.io/honua-sdk-js/versions.json), which is derived from `package.json`,
`.release-please-manifest.json`, and released `CHANGELOG.md` entries. A version
cannot appear in the selector unless those authoritative sources agree and the
release tag exists in the authoritative GitHub repository.
## Release documentation
| Version | Status | Channel | Documentation |
| --- | --- | --- | --- |
| `0.1.0-beta.0` | latest-prerelease | beta | [Tagged README fallback](https://github.com/honua-io/honua-sdk-js/blob/js-sdk-v0.1.0-beta.0/README.md) · [release notes](https://github.com/honua-io/honua-sdk-js/compare/js-sdk-v0.0.20-alpha.0...js-sdk-v0.1.0-beta.0) |
| `0.0.20-alpha.0` | archived | alpha | [Tagged README fallback](https://github.com/honua-io/honua-sdk-js/blob/js-sdk-v0.0.20-alpha.0/README.md) · [release notes](https://github.com/honua-io/honua-sdk-js/compare/js-sdk-v0.0.19-alpha.0...js-sdk-v0.0.20-alpha.0) |
| `0.0.19-alpha.0` | archived | alpha | [Tagged README fallback](https://github.com/honua-io/honua-sdk-js/blob/js-sdk-v0.0.19-alpha.0/README.md) · [release notes](https://github.com/honua-io/honua-sdk-js/compare/js-sdk-v0.0.18-alpha.0...js-sdk-v0.0.19-alpha.0) |
| `0.0.18-alpha.0` | archived | alpha | [Tagged README fallback](https://github.com/honua-io/honua-sdk-js/blob/js-sdk-v0.0.18-alpha.0/README.md) · [release notes](https://github.com/honua-io/honua-sdk-js/compare/js-sdk-v0.0.17-alpha.0...js-sdk-v0.0.18-alpha.0) |
| `0.0.17-alpha.0` | archived | alpha | [Tagged README fallback](https://github.com/honua-io/honua-sdk-js/blob/js-sdk-v0.0.17-alpha.0/README.md) · [release notes](https://github.com/honua-io/honua-sdk-js/compare/js-sdk-v0.0.16-alpha.0...js-sdk-v0.0.17-alpha.0) |
| `0.0.16-alpha.0` | archived | alpha | [Tagged README fallback](https://github.com/honua-io/honua-sdk-js/blob/js-sdk-v0.0.16-alpha.0/README.md) · [release notes](https://github.com/honua-io/honua-sdk-js/compare/js-sdk-v0.0.15-alpha.0...js-sdk-v0.0.16-alpha.0) |
| `0.0.15-alpha.0` | archived | alpha | [Tagged README fallback](https://github.com/honua-io/honua-sdk-js/blob/js-sdk-v0.0.15-alpha.0/README.md) · [release notes](https://github.com/honua-io/honua-sdk-js/compare/js-sdk-v0.0.14-alpha.0...js-sdk-v0.0.15-alpha.0) |
| `0.0.14-alpha.0` | archived | alpha | [Tagged README fallback](https://github.com/honua-io/honua-sdk-js/blob/js-sdk-v0.0.14-alpha.0/README.md) · [release notes](https://github.com/honua-io/honua-sdk-js/compare/js-sdk-v0.0.13-alpha.0...js-sdk-v0.0.14-alpha.0) |
| `0.0.13-alpha.0` | archived | alpha | [Tagged README fallback](https://github.com/honua-io/honua-sdk-js/blob/js-sdk-v0.0.13-alpha.0/README.md) · [release notes](https://github.com/honua-io/honua-sdk-js/compare/js-sdk-v0.0.12-alpha.0...js-sdk-v0.0.13-alpha.0) |
| `0.0.12-alpha.0` | archived | alpha | [Tagged README fallback](https://github.com/honua-io/honua-sdk-js/blob/js-sdk-v0.0.12-alpha.0/README.md) · [release notes](https://github.com/honua-io/honua-sdk-js/compare/js-sdk-v0.0.11-alpha.0...js-sdk-v0.0.12-alpha.0) |
| `0.0.11-alpha.0` | archived | alpha | [Tagged README fallback](https://github.com/honua-io/honua-sdk-js/blob/js-sdk-v0.0.11-alpha.0/README.md) · [release notes](https://github.com/honua-io/honua-sdk-js/compare/js-sdk-v0.0.10-alpha.0...js-sdk-v0.0.11-alpha.0) |
| `0.0.10-alpha.0` | archived | alpha | [Tagged README fallback](https://github.com/honua-io/honua-sdk-js/blob/js-sdk-v0.0.10-alpha.0/README.md) · [release notes](https://github.com/honua-io/honua-sdk-js/compare/js-sdk-v0.0.9-alpha.0...js-sdk-v0.0.10-alpha.0) |
| `0.0.9-alpha.0` | archived | alpha | [Tagged README fallback](https://github.com/honua-io/honua-sdk-js/blob/js-sdk-v0.0.9-alpha.0/README.md) · [release notes](https://github.com/honua-io/honua-sdk-js/compare/js-sdk-v0.0.8-alpha.0...js-sdk-v0.0.9-alpha.0) |
| `0.0.8-alpha.0` | archived | alpha | [Tagged README fallback](https://github.com/honua-io/honua-sdk-js/blob/js-sdk-v0.0.8-alpha.0/README.md) · [release notes](https://github.com/honua-io/honua-sdk-js/compare/js-sdk-v0.0.7-alpha.0...js-sdk-v0.0.8-alpha.0) |
| `0.0.7-alpha.0` | archived | alpha | [Tagged README fallback](https://github.com/honua-io/honua-sdk-js/blob/js-sdk-v0.0.7-alpha.0/README.md) · [release notes](https://github.com/honua-io/honua-sdk-js/compare/js-sdk-v0.0.6-alpha.0...js-sdk-v0.0.7-alpha.0) |
| `0.0.6-alpha.0` | archived | alpha | [Tagged README fallback](https://github.com/honua-io/honua-sdk-js/blob/js-sdk-v0.0.6-alpha.0/README.md) · [release notes](https://github.com/honua-io/honua-sdk-js/compare/js-sdk-v0.0.5-alpha.0...js-sdk-v0.0.6-alpha.0) |
| `0.0.5-alpha.0` | archived | alpha | [Tagged README fallback](https://github.com/honua-io/honua-sdk-js/blob/js-sdk-vv0.0.5-alpha.0/README.md) · [release notes](https://github.com/honua-io/honua-sdk-js/compare/js-sdk-vv0.0.4-alpha.0...js-sdk-vv0.0.5-alpha.0) |
| `0.0.4-alpha.0` | archived | alpha | [Tagged README fallback](https://github.com/honua-io/honua-sdk-js/blob/js-sdk-vv0.0.4-alpha.0/README.md) · [release notes](https://github.com/honua-io/honua-sdk-js/compare/js-sdk-vv0.0.3-alpha.0...js-sdk-vv0.0.4-alpha.0) |
| `0.0.3-alpha.0` | archived | alpha | [Tagged README fallback](https://github.com/honua-io/honua-sdk-js/blob/js-sdk-vv0.0.3-alpha.0/README.md) · [release notes](https://github.com/honua-io/honua-sdk-js/compare/js-sdk-vv0.0.2-alpha.0...js-sdk-vv0.0.3-alpha.0) |
| `0.0.2-alpha.0` | archived | alpha | [Tagged README fallback](https://github.com/honua-io/honua-sdk-js/blob/js-sdk-vv0.0.2-alpha.0/README.md) · [release notes](https://github.com/honua-io/honua-sdk-js/compare/js-sdk-vv0.0.1-alpha.0...js-sdk-vv0.0.2-alpha.0) |
The hosted guides and generated TypeDoc API are the development channel. The
released prereleases did not publish immutable TypeDoc sites, so every release
destination is explicitly labelled a source fallback: it opens the README at
the exact release tag and links to canonical release evidence. Released code is
never silently redirected to today's development API.
## Compatibility and migration
- The exact Node and optional-peer ranges are published in
[`versions.json`](https://honua-io.github.io/honua-sdk-js/versions.json) from the current package metadata.
- There is no supported-prior line while the SDK has no GA stable release. The
machine manifest records that state as `not-applicable`; the first stable
line will establish a supported-prior designation policy.
- Stable and experimental entrypoint policy is defined in
[Install and choose an entrypoint](../INSTALL.md) and checked against the
package exports.
- Breaking ArcGIS-to-Honua changes and replacement imports are documented in
[Migrate ArcGIS applications to Honua + MapLibre](./migration-honua-maplibre.md).
- Release-level changes are recorded in the tagged
[changelog](../CHANGELOG.md).
When an archived guide lacks an equivalent page, use its tagged repository
source and release notes. Do not combine code from an archived guide with the
current API reference without following the migration notes.
---
# File: docs/generated/sample-catalog.md
# SDK sample catalog
This inventory is generated from [`samples/catalog.v2.json`](../../samples/catalog.v2.json). Do not edit it by hand.
Catalog contract: `honua.sdk.sample-catalog.v2` · SDK: `@honua/sdk-js` (effective version derived from `package.json`) · 34 executable examples
## Golden journey readiness
Journey IDs are stable roadmap slots. `planned` candidates remain recipes or labs until their golden quality profile and evidence are satisfied; only `qualified` candidates use the golden track.
| Journey | Status | Candidate sample |
| --- | --- | --- |
| `first-map` | planned | [`maplibre-quickstart`](#maplibre-quickstart) |
| `service-explorer` | planned | [`service-explorer`](#service-explorer) |
| `planning-permitting` | planned | [`planning-permitting-workbench`](#planning-permitting-workbench) |
| `incident-operations` | planned | [`realtime-incident-dashboard`](#realtime-incident-dashboard) |
| `imagery-terrain` | planned | [`imagery-cog-quickstart`](#imagery-cog-quickstart) |
| `cloud-native-analysis` | planned | [`spatial-analytics-workbench`](#spatial-analytics-workbench) |
| `arcgis-migration` | planned | [`migration-workbench`](#migration-workbench) |
## Executable samples
| Sample | Track | Journey candidate | Support | Lifecycle | Quality profile | Data | Configuration | Demonstration |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| [`ai-spatial-app-builder`](../../examples/ai-spatial-app-builder/README.md) | lab | - | experimental | active | browser-lab | hybrid | approved | Proves typed proposals, shared policy validation, signed single-use approval, bounded execution, refusal, and verified receipt states. |
| [`app-bootstrap-basic`](../../examples/app-bootstrap-basic/README.md) | lab | - | deprecated | retire | browser-lab | fixture | not-required | Bootstraps a minimal application through the legacy app-platform compatibility surface. |
| [`arcgis-source-app`](../../examples/arcgis-source-app/README.md) | fixture | - | internal | active | internal-fixture | fixture | not-required | Provides the ArcGIS JavaScript source application used by the migration end-to-end harness. |
| [`automatic-source-workflow`](../../docs/examples/automatic-source-workflow/README.md) | fixture | - | internal | active | browser-fixture | fixture | not-required | Exercises automatic MapLibre strategy selection, interaction state, refresh, and disposal against deterministic browser fixtures. |
| [`cesium-route-playback`](../../docs/examples/cesium-route-playback/README.md) | lab | - | experimental | rework | browser-lab | hybrid | legacy-unsafe | Explores bounded route playback and elevation context through an optional Cesium consumer integration. |
| [`edit-workflow-demo`](../../examples/edit-workflow-demo/README.md) | recipe | - | supported | rework | browser-recipe | fixture | not-required | Demonstrates optimistic edits, attachments, conflicts, and safe recovery. |
| [`endpoint-to-map`](../../examples/endpoint-to-map/README.md) | recipe | - | experimental | active | browser-recipe | hybrid | approved | mountSource() turns a public FeatureServer into a styled, interactive MapLibre map in four statements. |
| [`geocoding-quickstart`](../../examples/geocoding-quickstart/README.md) | recipe | - | supported | rework | browser-recipe | hybrid | legacy-unsafe | Runs forward, reverse, and suggestion workflows with map feedback. |
| [`geoprocessing-job-runner`](../../examples/geoprocessing-job-runner/README.md) | recipe | - | supported | merge | browser-recipe | hybrid | legacy-unsafe | Submits, polls, cancels, and inspects asynchronous geoprocessing jobs. |
| [`imagery-cog-quickstart`](../../examples/imagery-cog-quickstart/README.md) | recipe | imagery-terrain | supported | rework | browser-recipe | hybrid | legacy-unsafe | Compares WMS imagery, COG-backed ImageServer tiles, and export previews. |
| [`kepler-analytics`](../../examples/kepler-analytics/README.md) | lab | - | experimental | rework | browser-lab | hybrid | legacy-unsafe | Replays operations data through kepler.gl with linked filters and KPI evidence. |
| [`maplibre-quickstart`](../../examples/maplibre-quickstart/README.md) | recipe | first-map | supported | rework | browser-recipe | hybrid | legacy-unsafe | Connects, discovers, explains, queries, and mounts one source with linked views and inspectable evidence. |
| [`mcp-gis-assistant`](../../examples/mcp-gis-assistant/README.md) | lab | - | experimental | rework | browser-lab | fixture | not-required | Demonstrates assistant tool discovery and safe SDK-backed spatial operations. |
| [`migration-workbench`](../../docs/migration-honua-maplibre.md) | lab | arcgis-migration | supported | rework | browser-lab | fixture | legacy-unsafe | Scans and transforms ArcGIS application source with auditable compatibility results. |
| [`nl-map-control`](../../examples/nl-map-control/README.md) | lab | - | experimental | active | browser-lab | fixture | not-required | A recorded fixture LLM compiles a canned instruction into an inspectable plan; read-only plans auto-execute, mutating plans require a signed agent-safety approval, and every execution emits a receipt beside the live map effects. |
| [`node-backend-quickstart`](../../examples/node-backend-quickstart/README.md) | recipe | - | supported | active | headless-recipe | hybrid | approved | Uses the protocol-neutral client from a Node service without browser dependencies. |
| [`oauth-signin`](../../examples/oauth-signin/README.md) | recipe | - | supported | active | browser-recipe | fixture | not-required | Demonstrates browser authentication and session lifecycle without embedding credentials. |
| [`overture-geoparquet`](../../examples/overture-geoparquet/README.md) | lab | - | experimental | active | browser-lab | hybrid | not-required | Plans and executes bounded Overture GeoParquet queries with truthful worker, range, memory, and pruning evidence. |
| [`planning-permitting-workbench`](../../examples/planning-permitting-workbench/README.md) | lab | planning-permitting | supported | rework | browser-lab | fixture | not-required | Combines parcels, hazards, sketching, editing, and export in a task-oriented application. |
| [`pmtiles-static`](../../examples/pmtiles-static/README.md) | recipe | - | supported | active | browser-recipe | fixture | not-required | Loads a static PMTiles archive without a Honua server. |
| [`react-quickstart`](../../examples/react-quickstart/README.md) | recipe | - | supported | active | browser-recipe | hybrid | approved | Uses the React provider, hooks, and map component over the same quickstart contract. |
| [`realtime-incident-dashboard`](../../examples/realtime-incident-dashboard/README.md) | lab | incident-operations | supported | active | browser-lab | hybrid | approved | Runs live-first incident command with observable reconciliation and a guarded, resettable edit lab. |
| [`runtime-parity-showcase`](../../examples/runtime-parity-showcase/README.md) | lab | - | experimental | replace | browser-lab | fixture | not-required | Compares supported rendering paths and makes fidelity differences explicit. |
| [`service-explorer`](../../examples/service-explorer/README.md) | lab | service-explorer | supported | rework | browser-lab | hybrid | legacy-unsafe | Browses heterogeneous spatial services with capability and cache diagnostics. |
| [`shared-renderer-state`](../../docs/examples/shared-renderer-state/README.md) | lab | - | experimental | active | browser-lab | fixture | not-required | Exercises loop-safe camera, selection, filter, and time synchronization across MapLibre and Cesium. |
| [`sketch-editing`](../../examples/sketch-editing/README.md) | recipe | - | experimental | active | browser-recipe | fixture | not-required | terra-draw draw modes drive the edit-sketch workflow: undo/redo, snapping, and applyEdits submission. |
| [`spatial-analytics-workbench`](../../examples/spatial-analytics-workbench/README.md) | lab | cloud-native-analysis | experimental | rework | browser-lab | hybrid | approved | Explains and accepts one plan linking AOI, map, table, chart, provenance, and reusable output. |
| [`stac-imagery-browser`](../../examples/stac-imagery-browser/README.md) | recipe | - | supported | merge | browser-recipe | fixture | not-required | Discovers STAC collections and previews supported imagery assets. |
| [`standalone-quickstart`](../../examples/standalone-quickstart/README.md) | recipe | - | supported | merge | browser-recipe | hybrid | approved | Connects a public Esri service directly to MapLibre without a Honua server. |
| [`storytelling-25d-map`](../../examples/storytelling-25d-map/README.md) | lab | - | supported | merge | browser-lab | hybrid | legacy-unsafe | Combines terrain, extrusion, OGC overlays, and route playback in a guided story. |
| [`temporal-playback`](../../examples/temporal-playback/README.md) | recipe | - | experimental | active | browser-recipe | fixture | not-required | Animates a month of seeded synthetic seismic events with createTemporalPlayback, styled by a first-class classBreaksRenderer whose legend derives from renderer.legendItems(). |
| [`terrain-rgb-elevation`](../../examples/terrain-rgb-elevation/README.md) | recipe | - | supported | merge | browser-recipe | hybrid | legacy-unsafe | Reads Terrain-RGB tiles for point elevation and route profiles. |
| [`unified-ops-workspace`](../../examples/unified-ops-workspace/README.md) | lab | - | deprecated | retire | browser-lab | fixture | not-required | Composes incident command, analysis, and shared workspace state. |
| [`web-components-basic`](../../examples/web-components-basic/README.md) | lab | - | deprecated | retire | browser-lab | fixture | not-required | Demonstrates the SDK custom-element controls against a map. |
The catalog also carries fixture/live evidence, evidence expiry, endpoint configuration names, provenance, attribution, freshness, lifecycle targets, validation profiles, and the complete 21-route honua.io migration mapping. The presentation-safe projection is [`samples/dist/honua-site-samples.v2.json`](../../samples/dist/honua-site-samples.v2.json).
---
# File: docs/quickstart.md
# Five-minute quickstart: endpoint to linked MapLibre map
The canonical server-connected browser workflow is the tested app in
[`examples/maplibre-quickstart`](../examples/maplibre-quickstart/README.md). It makes the SDK's five-stage journey
visible instead of hiding network and fallback decisions:
```text
connect → discover → explain → query → mount
```
If you do not need a Honua server, start with the
[`standalone quickstart`](./standalone-quickstart.md). It connects directly to a public GeoServices endpoint. This page
covers the protocol-neutral Honua lane with compatibility, discovery, planning, evidence, and linked views.
## Run the deterministic lane
```bash
npm ci
npm run demo:quickstart:mock
```
Open the printed `quickstartMockUrl`. No account, credential, or network-hosted basemap is required. The app:
1. checks SDK/server compatibility;
2. discovers layer metadata and constructs the protocol capability contract;
3. explains a deterministic query plan before fetching rows;
4. executes that accepted plan through `Dataset → Source → Query → Result`;
5. mounts the result in MapLibre and links map, table, filter, detail, and popup state.
The page also exposes provenance, capture/observation time, auth mode, SDK/server/data versions, metadata cache state,
plan fingerprint, pushdown, fidelity, and degradation. Fixture replay is labeled explicitly and never presented as live
data.
### What the five-minute claim measures
Required CI sets up the pinned Node runtime, then starts a monotonic clock before `npm ci`, provisions Chromium, builds
this fixture app, and stops only after all five stages complete with renderable features and a mounted MapLibre canvas.
The 300-second ceiling is enforced by:
```bash
npm run docs:quickstart:time-to-map
```
CI uploads `quickstart-time-to-map.json` on success or failure. It records the actual elapsed duration, fixture mode,
completed stages, renderable feature count, package version, and revision; it never substitutes a configured or
estimated duration. A local invocation measures script-to-map because dependencies and the browser are already
installed. Only CI evidence with `cleanInstallIncluded: true` covers runtime setup and a clean install.
This automated gate proves the documented path is reproducible within the budget on a fresh runner. It is not evidence
of a first-time human usability study; that separate observation remains required before claiming the broader learning
architecture acceptance criterion is complete.
## Use an anonymous live endpoint
Copy [`.env.example`](../examples/maplibre-quickstart/.env.example), point it at a public CORS-enabled Honua layer, then
run the same app:
```bash
cp examples/maplibre-quickstart/.env.example examples/maplibre-quickstart/.env
npm run demo:quickstart
```
The required live values are:
- `VITE_HONUA_QUICKSTART_BASE_URL`
- `VITE_HONUA_QUICKSTART_SERVICE_ID`
- `VITE_HONUA_QUICKSTART_LAYER_ID`
- `VITE_HONUA_QUICKSTART_DATA_VERSION`
The filter, bounded record count, basemap style, and optional snapshot timestamp are documented in the
[sample README](../examples/maplibre-quickstart/README.md#secret-free-live-run).
The browser quickstart rejects API keys and bearer tokens because Vite embeds environment values in public JavaScript.
Use an anonymous endpoint or a server-side proxy/session. Protected server-only staging validation remains separate.
## The SDK shape
The sample uses stable subpath imports and the explicitly experimental planner:
```ts doc-test=compile
import { PROTOCOL_DEFAULT_CAPABILITIES, createDataset } from "@honua/sdk-js/contract";
import { HonuaClient } from "@honua/sdk-js/honua";
import { executeQueryPlan, explainQuery } from "@honua/sdk-js/query-planner";
const client = new HonuaClient({ baseUrl: "https://your-public-honua.example" });
const metadata = await client.getLayerMetadata("public-service", 0);
const descriptor = {
id: "public-features",
protocol: "geoservices-feature-service" as const,
locator: { url: "https://your-public-honua.example", serviceId: "public-service", layerId: 0 },
capabilities: PROTOCOL_DEFAULT_CAPABILITIES["geoservices-feature-service"],
schema: { fields: metadata.fields },
};
const dataset = createDataset({ id: "public-map", client, sources: [descriptor] });
const source = dataset.source("public-features");
if (!source) throw new Error("Source resolution failed");
const query = { where: "1=1", outFields: ["*"], returnGeometry: true, pagination: { limit: 25 } };
const plan = explainQuery({ descriptor, query, sourceVersion: "public-service-2026-07" });
const execution = await executeQueryPlan(plan, source, { sourceVersion: "public-service-2026-07" });
console.log(execution.result.features);
```
Planning is synchronous and side-effect free. Execution validates plan integrity and source context before invoking the
accepted step. Capability gaps and unsafe fallback bounds throw structured errors; they do not become silent empty maps.
## Requests and validation
A healthy startup performs three Honua requests before the basemap's own assets:
1. `GET /api/v1/admin/capabilities`
2. `GET /rest/services/{serviceId}/FeatureServer/{layerId}?f=json`
3. `GET /rest/services/{serviceId}/FeatureServer/{layerId}/query?...`
The required CI lane is fixture-only:
```bash
npm run demo:quickstart:typecheck
npx vitest run test/quickstart-config.test.ts test/quickstart-data.test.ts test/quickstart-linked-exploration.test.ts
npm run demo:quickstart:build
npm run test:playwright:quickstart
```
See [`quickstart-troubleshooting.md`](./quickstart-troubleshooting.md) for compatibility, discovery, configuration,
geometry, plan, CORS, and staging diagnostics.
---
# File: docs/generated/learning-paths.md
# Learn the Honua SDK by task
Choose the outcome you need, then follow the linked guide and runnable implementation. The examples are the canonical executable source; this guide deliberately contains no copied implementation snippets.
API reference is SDK-owned at [https://honua-io.github.io/honua-sdk-js/api/](https://honua-io.github.io/honua-sdk-js/api/); the task narrative and deployed sample catalog are site-owned at [https://honua.io/samples](https://honua.io/samples).
These paths describe the SDK version in [`package.json`](../../package.json). Sample track, support, lifecycle, data, provenance, freshness, and degradation metadata comes from the [versioned sample catalog](../../samples/contract/v2/README.md); use the [installation and compatibility guide](../../INSTALL.md) when reading docs for another release.
## Execution labels
- **Fixture** (`fixture`): Deterministic committed data; no live-service claim.
- **Public live** (`public-live`): Reads a public standards endpoint without a Honua account.
- **Demo live** (`demo-live`): Can read the deployed demo.honua.io service when configured and reports availability and freshness.
- **Authenticated** (`authenticated`): Requires caller-supplied credentials; documentation never embeds a secret.
- **Degraded** (`degraded`): A reduced capability remains visible with a structured reason and recovery path.
- **Experimental** (`experimental`): Uses a pre-1.0 surface that may change in a minor release.
## Learning paths
### 1. Start with a public map
Open a public GeoServices endpoint and render useful data without an account.
Labels: `fixture` · `public-live`
- Guide: [docs/standalone-quickstart.md](../standalone-quickstart.md)
- Runnable example: [standalone-quickstart](../../examples/standalone-quickstart)
- Executable entry: [examples/standalone-quickstart/src/main.ts](../../examples/standalone-quickstart/src/main.ts)
- Example notes: [examples/standalone-quickstart/README.md](../../examples/standalone-quickstart/README.md)
- Compile check: `npm run demo:standalone:typecheck`
- Sample contract: `recipe` · `supported` · `merge`
- Data and auth: `hybrid` · `anonymous`
- Provenance: Committed public-response fixture or public Esri endpoint.
- Freshness: Fixture retrieval metadata or live response time.
- Catalog degradation: Protocol capability gaps are surfaced without requiring a Honua facade.
- Live sample: [sample-expr-builder.html](https://honua.io/sample-expr-builder.html)
- Supported API imports: `@honua/sdk-js/esri-compat` (`FeatureLayerCompat`); `@honua/sdk-js/honua` (`HonuaClient`); `@honua/sdk-js/map` (`loadHonuaFeatureServiceGeoJson`)
- honua.io journey: `connect-existing-gis`
### 2. Connect and inspect sources
Discover services, layers, schemas, and capability gaps before choosing a workflow.
Labels: `fixture` · `demo-live` · `authenticated` · `degraded`
- Guide: [docs/shared-client-contract.md](../shared-client-contract.md)
- Runnable example: [service-explorer](../../examples/service-explorer)
- Executable entry: [examples/service-explorer/src/data.ts](../../examples/service-explorer/src/data.ts)
- Example notes: [examples/service-explorer/README.md](../../examples/service-explorer/README.md)
- Compile check: `npm run demo:service-explorer:typecheck`
- Sample contract: `lab` · `supported` · `rework`
- Data and auth: `hybrid` · `api-key`
- Provenance: Committed multi-protocol catalog or configured Honua catalog.
- Freshness: Metadata cache and live observation timestamps.
- Catalog degradation: Unavailable admin metadata falls back to service discovery with a visible diagnostic.
- Live sample: [sample-service-explorer.html](https://honua.io/sample-service-explorer.html)
- Supported API imports: `@honua/sdk-js/contract` (`createDataset`); `@honua/sdk-js/honua` (`HonuaClient`)
- honua.io journey: `connect-existing-gis`
- Degradation: The current shell still uses the 0.1.x app-workspace compatibility shim; #399 owns its supported-import migration.
### 3. Query from Node or browser code
Issue bounded queries, inspect typed results, and keep capability failures explicit.
Labels: `fixture` · `demo-live` · `authenticated` · `degraded`
- Guide: [docs/composition.md](../composition.md)
- Runnable example: [node-backend-quickstart](../../examples/node-backend-quickstart)
- Executable entry: [examples/node-backend-quickstart/src/server.ts](../../examples/node-backend-quickstart/src/server.ts)
- Example notes: [examples/node-backend-quickstart/README.md](../../examples/node-backend-quickstart/README.md)
- Compile check: `npm run demo:node-backend:typecheck`
- Sample contract: `recipe` · `supported` · `active`
- Data and auth: `hybrid` · `api-key`
- Provenance: Committed mock responses or configured Honua endpoint.
- Freshness: Fixture replay or live query observation time.
- Catalog degradation: The recipe fails explicitly when required server capabilities are absent.
- Supported API imports: `@honua/sdk-js/honua` (`HonuaClient`, `QueryBuilder`)
- honua.io journey: `query-map-style`
- Degradation: The live endpoint may omit requested query capabilities; the sample keeps the typed capability error visible.
### 4. Connect, explain, and map query results
Connect, discover, explain, query, and mount one source with linked MapLibre views and visible runtime evidence.
Labels: `fixture` · `demo-live` · `experimental`
- Guide: [docs/quickstart.md](../quickstart.md)
- Runnable example: [maplibre-quickstart](../../examples/maplibre-quickstart)
- Executable entry: [examples/maplibre-quickstart/src/main.ts](../../examples/maplibre-quickstart/src/main.ts)
- Example notes: [examples/maplibre-quickstart/README.md](../../examples/maplibre-quickstart/README.md)
- Compile check: `npm run demo:quickstart:typecheck`
- Sample contract: `recipe` · `supported` · `rework`
- Data and auth: `hybrid` · `anonymous`
- Provenance: Versioned Honolulu fixture replay or an anonymous configured Honua source, identified in runtime evidence.
- Freshness: Fixture capture time and data version, or live response observation time.
- Catalog degradation: Capability misses, plan warnings, and bounded fallback are shown rather than silently returning an empty map.
- Live sample: [demo.html](https://honua.io/demo.html)
- Supported API imports: `@honua/sdk-js/contract` (`createDataset`); `@honua/sdk-js/exploration` (`createExplorationContext`); `@honua/sdk-js/honua` (`HonuaClient`); `@honua/sdk-js/query-planner` (`executeQueryPlan`, `explainQuery`)
- honua.io journey: `query-map-style`
### 5. Analyze linked spatial views
Keep map, table, chart, and spatial aggregation state aligned with visible fallback evidence.
Labels: `fixture` · `experimental` · `degraded`
- Guide: [docs/warehouse-analytics-sources.md](../warehouse-analytics-sources.md)
- Runnable example: [spatial-analytics-workbench](../../examples/spatial-analytics-workbench)
- Executable entry: [examples/spatial-analytics-workbench/src/main.ts](../../examples/spatial-analytics-workbench/src/main.ts)
- Example notes: [examples/spatial-analytics-workbench/README.md](../../examples/spatial-analytics-workbench/README.md)
- Compile check: `npm run demo:spatial-analytics:typecheck`
- Sample contract: `lab` · `experimental` · `rework`
- Data and auth: `hybrid` · `anonymous`
- Provenance: Committed analysis fixtures or a configured public GeoServices source.
- Freshness: Fixture replay uses a fixed observation; configured live execution records its observation time.
- Catalog degradation: Remote fixtures are labeled replayed, bounded-local execution is capped, unsafe materialization is rejected, and OGC/DuckDB planner execution is a structured #389 follow-on.
- Live sample: [demo-analyst-workbench.html](https://honua.io/demo-analyst-workbench.html) · [sample-spatial-analytics.html](https://honua.io/sample-spatial-analytics.html)
- Supported API imports: `@honua/sdk-js/contract` (`resolveSpatialAggregationWidgetSummary`); `@honua/sdk-js/exploration` (`createExplorationContext`)
- honua.io journey: `linked-large-data-analysis`
- Degradation: The current shell still uses the 0.1.x app-workspace compatibility shim; #399 owns its supported-import migration.
### 6. Edit with recovery and capability checks
Apply optimistic edits, attachments, and conflict recovery without hiding unsupported mutations.
Labels: `fixture` · `degraded`
- Guide: [docs/shared-client-contract.md](../shared-client-contract.md)
- Runnable example: [edit-workflow-demo](../../examples/edit-workflow-demo)
- Executable entry: [examples/edit-workflow-demo/src/main.ts](../../examples/edit-workflow-demo/src/main.ts)
- Example notes: [examples/edit-workflow-demo/README.md](../../examples/edit-workflow-demo/README.md)
- Compile check: `npm run demo:edit-workflow:typecheck`
- Sample contract: `recipe` · `supported` · `rework`
- Data and auth: `fixture` · `none`
- Provenance: Committed deterministic edit fixture.
- Freshness: Deterministic fixture replay time.
- Catalog degradation: Editing is read-only when the service does not advertise mutation capability.
- Live sample: [demo-editing.html](https://honua.io/demo-editing.html)
- Supported API imports: `@honua/sdk-js/contract` (`createEditSession`); `@honua/sdk-js/honua` (`HonuaCapabilityNotSupportedError`)
- honua.io journey: `realtime-operations`
- Degradation: The current shell still uses the 0.1.x app-workspace compatibility shim, and mutations become read-only when capability or auth is absent.
### 7. Operate through realtime and offline transitions
Reconcile snapshots and deltas while showing reconnect, staleness, and deterministic fixture fallback.
Labels: `fixture` · `demo-live` · `degraded`
- Guide: [docs/realtime-subscriptions.md](../realtime-subscriptions.md)
- Runnable example: [realtime-incident-dashboard](../../examples/realtime-incident-dashboard)
- Executable entry: [examples/realtime-incident-dashboard/src/realtime-transport.ts](../../examples/realtime-incident-dashboard/src/realtime-transport.ts)
- Example notes: [examples/realtime-incident-dashboard/README.md](../../examples/realtime-incident-dashboard/README.md)
- Compile check: `npm run demo:incident:typecheck`
- Sample contract: `lab` · `supported` · `active`
- Data and auth: `hybrid` · `anonymous`
- Provenance: Deployed demo stream when advertised; otherwise visibly labeled deterministic replay. Edits are limited to an isolated resettable fixture profile.
- Freshness: Snapshot, observation, and event times plus cursor, sequence, lag, reconnect, deduplication, and reconciliation outcomes.
- Catalog degradation: Unavailable live capability becomes a visibly labeled read-only replay; stale/offline state disables mutation, and live writes fail closed without an isolated resettable profile.
- Live sample: [demo-public-safety.html](https://honua.io/demo-public-safety.html)
- Supported API imports: `@honua/sdk-js/realtime` (`createRealtimeServerSentEventsTransport`)
- honua.io journey: `realtime-operations`
- Degradation: The production offline persistence path is not complete; reconnect and stale-state behavior remains explicit while fixture replay stays deterministic.
### 8. Add terrain and 3D context
Progress from stable 2D map primitives to an explicitly experimental 2.5D/3D experience.
Labels: `fixture` · `authenticated` · `experimental` · `degraded`
- Guide: [docs/scene-workspace.md](../scene-workspace.md)
- Runnable example: [storytelling-25d-map](../../examples/storytelling-25d-map)
- Executable entry: [examples/storytelling-25d-map/src/map.ts](../../examples/storytelling-25d-map/src/map.ts)
- Example notes: [examples/storytelling-25d-map/README.md](../../examples/storytelling-25d-map/README.md)
- Compile check: `npm run demo:25d:typecheck`
- Sample contract: `lab` · `supported` · `merge`
- Data and auth: `hybrid` · `api-key`
- Provenance: Committed Oahu story fixtures or configured Honua services.
- Freshness: Fixture capture or live layer observation time.
- Catalog degradation: Terrain and live overlays fail independently with structured status.
- Live sample: [demo-maui-3d.html](https://honua.io/demo-maui-3d.html)
- Supported API imports: `@honua/sdk-js/map` (`HonuaMap`); `@honua/sdk-js/runtime` (`HonuaMapRuntime`)
- honua.io journey: `imagery-terrain-3d`
- Degradation: The runnable 2.5D shell still reaches the scene-workspace compatibility surface; stable map/runtime imports are the taught foundation until #399 migrates it.
### 9. Migrate an ArcGIS application
Scan and transform one file at a time with explicit compatibility evidence and manual gaps.
Labels: `fixture` · `public-live` · `degraded`
- Guide: [docs/migration-honua-maplibre.md](../migration-honua-maplibre.md)
- Runnable example: [migration-workbench](../../examples/migration-workbench)
- Executable entry: [examples/migration-workbench/src/main.ts](../../examples/migration-workbench/src/main.ts)
- Example notes: [docs/migration-honua-maplibre.md](../migration-honua-maplibre.md)
- Compile check: `npm run demo:migration-workbench:typecheck`
- Sample contract: `lab` · `supported` · `rework`
- Data and auth: `fixture` · `none`
- Provenance: Committed ArcGIS source fixtures and migration reports.
- Freshness: Versioned with the migration corpus.
- Catalog degradation: Unsupported ArcGIS modules are reported as manual work, never silently removed.
- Live sample: [sample-codemod.html](https://honua.io/sample-codemod.html)
- Supported API imports: `@honua/sdk-js/migration` (`runEsriCompatCodemod`, `scanArcGisUsage`)
- honua.io journey: `migrate-arcgis`
- Degradation: Unsupported ArcGIS modules remain manual migration work and are reported rather than removed silently.
### 10. Automate safely with agent tools
Expose bounded map actions with capability explanations while keeping writes behind host approval.
Labels: `fixture` · `experimental` · `degraded`
- Guide: [docs/ai-map-kit.md](../ai-map-kit.md)
- Runnable example: [mcp-gis-assistant](../../examples/mcp-gis-assistant)
- Executable entry: [examples/mcp-gis-assistant/src/assistant.ts](../../examples/mcp-gis-assistant/src/assistant.ts)
- Example notes: [examples/mcp-gis-assistant/README.md](../../examples/mcp-gis-assistant/README.md)
- Compile check: `npm run demo:mcp-gis-assistant:typecheck`
- Sample contract: `lab` · `experimental` · `rework`
- Data and auth: `fixture` · `none`
- Provenance: Committed MCP response fixture.
- Freshness: Deterministic fixture replay time.
- Catalog degradation: Write tools are unavailable without host policy and explicit approval.
- Supported API imports: `@honua/sdk-js/agent-tools` (`HONUA_AGENT_TOOL_DEFINITIONS`, `createHonuaAgentToolExecutor`)
- honua.io journey: `safe-agent-automation`
- Degradation: The current assistant shell still uses the app-workspace compatibility surface, and write execution stays disabled without explicit host approval.
## Publication boundary
- Guides link to runnable example source and never maintain a copied implementation snippet.
- Repository documentation uses relative links; canonical API and site narrative links use the declared owners.
- Site consumers join the [learning navigation manifest](../learning-paths.v1.json) to the [static sample projection](../../samples/dist/honua-site-samples.v2.json) by `sampleId`; catalog-owned metadata is not copied into another source file.
- Sample metadata and evidence are owned by [SDK issue #540](https://github.com/honua-io/honua-sdk-js/issues/540); the generated consumer projection is coordinated by [SDK issue #550](https://github.com/honua-io/honua-sdk-js/issues/550).
---
# File: docs/standalone-quickstart.md
# Standalone Quickstart: The SDK Against Any Public Endpoint
`@honua/sdk-js` is a typed geospatial service client first and a Honua client
second. Its protocol clients speak raw **Esri GeoServices**, so they work against
*any* ArcGIS Server / ArcGIS Online endpoint with **no Honua server, no API key,
and no account**. This is the maintained, MapLibre-targeting successor to
`esri-leaflet`.
Start here. A [Honua Server](https://github.com/honua-io/honua-server) is the
*upgrade path* (authored MapPackages, realtime, collaboration, MCP) — not the
entry fee. The [capability matrix](./standalone-capability-matrix.md) draws the
honest line between what is backend-agnostic and what needs a Honua Server.
## The first code block runs with zero Honua infrastructure
Point the SDK at a public FeatureServer and get typed query results:
```ts doc-test=compile
import { HonuaClient } from "@honua/sdk-js/honua";
// A public Esri Living Atlas FeatureServer — no key, no account, no Honua server.
const url =
"https://services.arcgis.com/P3ePLMYs2RVChkJx/arcgis/rest/services/2020_Census_State_Apportionment/FeatureServer/0";
const client = new HonuaClient({ baseUrl: "https://services.arcgis.com/P3ePLMYs2RVChkJx/arcgis" });
const { features } = await client.queryFeatures({
serviceId: "2020_Census_State_Apportionment",
layerId: 0,
where: "1=1",
outFields: ["NAME", "Total_Pop_2020", "Seats_2020"],
returnGeometry: false,
resultRecordCount: 5,
});
console.log(`Loaded ${features?.length ?? 0} states`);
```
`HonuaClient` builds the standard GeoServices request path
(`/rest/services/{serviceId}/FeatureServer/{layerId}/query`) and joins it to
whatever origin you pass as `baseUrl`, so `services.arcgis.com`,
`sampleserver6.arcgisonline.com`, or your own ArcGIS Server all work unchanged.
### Render it on MapLibre in one call
`@honua/sdk-js/map` turns a public FeatureServer URL straight into a MapLibre
`geojson` source:
```ts doc-test=skip reason="partial excerpt requires application host context"
import { HonuaClient } from "@honua/sdk-js/honua";
import { loadHonuaFeatureServiceGeoJson } from "@honua/sdk-js/map";
import maplibregl from "maplibre-gl";
const client = new HonuaClient({ baseUrl: "https://services.arcgis.com/P3ePLMYs2RVChkJx/arcgis" });
const source = await loadHonuaFeatureServiceGeoJson(client, url, { outFields: ["NAME"] });
const map = new maplibregl.Map({ container: "map", style: "https://demotiles.maplibre.org/style.json" });
map.on("load", () => {
map.addSource("states", source);
map.addLayer({ id: "states-fill", source: "states", type: "fill", paint: { "fill-color": "#0f766e" } });
});
```
### The esri-leaflet migration path works standalone too
Pointing `FeatureLayerCompat` (and the rest of `@honua/sdk-js/esri-compat`) at a
`services.arcgis.com`-style URL Just Works — it parses the URL, derives the
origin, and constructs its own client. Existing ArcGIS-shaped code migrates
file-by-file without standing up any Honua infrastructure:
```ts doc-test=skip reason="partial excerpt requires application host context"
import { FeatureLayerCompat } from "@honua/sdk-js/esri-compat";
// Same familiar shape as esri-leaflet / arcgis-rest — no server required.
const layer = new FeatureLayerCompat({ url });
const { features } = await layer.queryFeatures({ where: "Seats_2020 > 10", outFields: ["NAME"] });
```
## Run the committed example
The [`examples/standalone-quickstart/`](../examples/standalone-quickstart/README.md)
app does exactly the above — public GeoServices query → MapLibre render → the
`FeatureLayerCompat` drop-in proof — and ships two lanes:
```bash
npm install
# Live lane: hits the pinned public Esri endpoint.
npm run demo:standalone
# Deterministic lane: replays recorded fixtures on same-origin paths (what CI runs).
npm run demo:standalone:mock # prints standaloneMockUrl=http://127.0.0.1:PORT
```
CI only ever runs the fixture lane, so it never depends on a third party. Refresh
the recordings from the live endpoints on demand with
`npm run demo:standalone:refresh-fixtures`. A scheduled, non-blocking workflow
(`.github/workflows/standalone-live-smoke.yml`) probes the live endpoints weekly
and never gates a PR.
## Documented public endpoints
These are the pinned, stable public services the docs and example use. All are
anonymous, read-only, and permitted for demonstration use.
| Endpoint | Protocol | Provider / license note |
| --- | --- | --- |
| `https://services.arcgis.com/P3ePLMYs2RVChkJx/arcgis/rest/services/2020_Census_State_Apportionment/FeatureServer/0` | GeoServices FeatureServer | Esri Living Atlas; US Census apportionment (public-domain data), publicly shared by Esri. |
| `https://sampleserver6.arcgisonline.com/arcgis/rest/services/SampleWorldCities/MapServer/0` | GeoServices MapServer | Esri's public sample server, hosted expressly for samples/demos. |
| `https://demo.pygeoapi.io/master/collections` | OGC API Features | pygeoapi's official public demo (MIT project); Natural Earth (public-domain) data. |
> **OGC endpoints and the typed surface.** The GeoServices client is the fully
> backend-agnostic lane today: it targets raw ArcGIS request paths. The SDK's
> *typed* OGC API / WFS / STAC surfaces currently address the Honua server's OGC
> facade paths — see the [capability matrix](./standalone-capability-matrix.md)
> for the honest, per-capability breakdown and roadmap.
## Add a Honua Server (the upgrade path)
When you want more than reads against open services, a
[Honua Server](https://github.com/honua-io/honua-server) unlocks:
- **Authored `MapPackage`s** — `loadMapPackage()` + `HonuaMapRuntime` render a
server-authored map with styles, layer order, and metadata baked in.
- **Realtime** — subscription-backed feature updates (`@honua/sdk-js/realtime`).
- **Collaboration** — shared/saved maps and multi-user sessions.
- **MCP + AI** — the `@honua/mcp-server` and agent tools surface.
Spin one up locally and point the same code at it:
```bash
# From a honua-server checkout:
docker compose up
# then set baseUrl to your local server, e.g. http://127.0.0.1:8080
```
See the [maplibre-quickstart](../examples/maplibre-quickstart/README.md) for the
server-connected lane, [`docs/maplibre-runtime.md`](./maplibre-runtime.md) for
the `MapPackage` runtime, and [`docs/realtime-subscriptions.md`](./realtime-subscriptions.md)
for realtime.
---
# File: docs/quickstart-troubleshooting.md
# Quickstart Troubleshooting
Use this guide when the committed quickstart app at
[`examples/maplibre-quickstart/`](../examples/maplibre-quickstart/README.md) does not load as expected.
## Unsupported Compatibility Baseline
Symptoms:
- the app stops before querying data
- the overlay shows an unsupported compatibility message
- `window.__HONUA_QUICKSTART_RUNTIME__.serverVersion` or `releaseChannel` is missing or below the SDK baseline
- the app reports `Server does not expose GET /api/v1/admin/capabilities.`
Checks:
- confirm `GET /api/v1/admin/capabilities` is reachable from the browser
- confirm the endpoint responds with a JSON object containing `success` and `data.compatibility`
- confirm the server reports version `>= 1.0.0`
- confirm `data.compatibility.controlPlaneApi.major` is integer `1` with base path `/api/v1/admin`
- confirm `data.compatibility.controlPlaneApi.deprecated` is `false`
- confirm `data.compatibility.metadataSchemas[]` entries include `version` and `deprecated`
- confirm `data.compatibility.features` includes the expected boolean flags
- confirm the release channel is `preview` or newer
The quickstart app intentionally fails before the feature query when this contract is not satisfied.
## Protected Endpoint Or Invalid Auth
Symptoms:
- `401` or `403` responses on compatibility or feature-query requests
- browser console shows a request error before the map becomes ready
Checks:
- use a CORS-enabled endpoint that permits anonymous reads for the browser flagship
- do not add browser API keys or bearer tokens; the app rejects them because Vite would publish their values
- put protected browser traffic behind an application backend or session flow
- for server-only staging CI, set `HONUA_STAGING_API_KEY` or `HONUA_STAGING_BEARER_TOKEN` when required
The page always reports its auth mode. It never renders credential values.
## CORS Or Browser Network Failures
Symptoms:
- the browser reports `TypeError: Failed to fetch`
- requests work from server-side tools but fail from the browser app
Checks:
- confirm the Honua server allows the browser origin used by Vite or preview
- confirm preflight responses allow the headers your auth mode sends
- use the same-origin fixture lane first:
```bash
npm run demo:quickstart:mock
```
If the fixture lane works but the live lane fails, the problem is likely environment or browser policy rather than the
example code.
## Invalid Quickstart Config
Symptoms:
- the app fails before the compatibility check runs
- the overlay or inline status reports `A quickstart layer id must be an integer.`
- the overlay or inline status reports `A quickstart result record count must be greater than zero.`
- the overlay reports that the browser quickstart is intentionally secret-free
- local staging runs fail with `HONUA_STAGING_BASE_URL is required.` or the matching `HONUA_STAGING_*` required-variable message
- the staging workflow fails during its validation step with `Missing HONUA_STAGING_BASE_URL`, `Missing HONUA_STAGING_SERVICE_ID`, or `Missing HONUA_STAGING_LAYER_ID`
- `npm run test:quickstart:staging` fails before issuing any network requests when staging env values are malformed
Checks:
- set `VITE_HONUA_QUICKSTART_LAYER_ID` to an integer
- set `VITE_HONUA_QUICKSTART_RESULT_RECORD_COUNT` to a positive integer
- remove `VITE_HONUA_QUICKSTART_API_KEY` and `VITE_HONUA_QUICKSTART_BEARER_TOKEN`; use an anonymous endpoint or proxy
- set `HONUA_STAGING_BASE_URL`, `HONUA_STAGING_SERVICE_ID`, and `HONUA_STAGING_LAYER_ID` for local or CI staging runs
- for staging CI, keep `HONUA_STAGING_LAYER_ID` integer-valued and `HONUA_STAGING_RESULT_RECORD_COUNT` positive when overridden
These validation failures happen before the app or staging integration issues `GET /api/v1/admin/capabilities`.
## Empty Feature Queries
Symptoms:
- the app reports `The feature query returned no features...`
- staging integration fails with `featureCount` equal to `0`
Checks:
- confirm `VITE_HONUA_QUICKSTART_SERVICE_ID` and `VITE_HONUA_QUICKSTART_LAYER_ID`
- confirm the configured `where` clause matches seeded data
- reduce filter complexity and retry with `1=1`
- keep `resultRecordCount` bounded but non-zero
The quickstart app expects at least one feature because it fits the map and drives an inspection popup from the query
result.
## Unsupported Or Missing Geometry
Symptoms:
- the app reports `none included renderable geometry`
- the feature query succeeds but the map never renders data
Checks:
- confirm the layer returns geometry-bearing features
- confirm the quickstart query keeps `returnGeometry: true`
- confirm the requested layer returns geometry that converts into the app's point, line, or polygon render buckets
- keep `outSr: 4326` so MapLibre receives browser-safe coordinates
The app filters out non-renderable records and only creates render layers for returned points, lines, or polygons.
That means `featureCount` can be greater than `renderableFeatureCount` when the server returns mixed-quality data.
## Basemap Style Load Failures
Symptoms:
- the overlay reports `Failed to load the basemap style "..."`
- Honua requests succeed but the map never initializes
Checks:
- confirm `VITE_HONUA_QUICKSTART_BASEMAP_STYLE` is reachable from the browser
- confirm the style JSON and its dependent assets are public or otherwise accessible
- use the fixture lane to remove basemap-network variability from the initial debug loop
The fixture lane serves `/__honua-quickstart__/basemap-style.json` locally to keep smoke coverage deterministic.
## Staging CI Config Drift
The live staging suite is triggered by `npm run test:quickstart:staging` and `.github/workflows/quickstart-staging.yml`.
The deterministic browser smoke lane runs from `.github/workflows/ci.yml` on `trunk` plus `release/**` pushes and pull
requests.
Required environment variables:
- `HONUA_STAGING_BASE_URL`
- `HONUA_STAGING_SERVICE_ID`
- `HONUA_STAGING_LAYER_ID`
Optional environment variables:
- `HONUA_STAGING_WHERE`
- `HONUA_STAGING_RESULT_RECORD_COUNT`
- `HONUA_STAGING_API_KEY`
- `HONUA_STAGING_BEARER_TOKEN`
- `HONUA_QUICKSTART_STAGING_SUMMARY_FILE`
If staging CI starts failing after an environment change:
- confirm the configured service and layer still exist
- confirm the environment still exposes non-empty geometry-bearing data
- confirm the auth policy still matches the configured secret or public access path
- remember the staging suite reuses the shared quickstart data loader only; it does not open the browser app or fetch the basemap style
- review the workflow step summary for `serverVersion`, `releaseChannel`, `featureCount`,
`renderableFeatureCount`, `geometryTypes`, and `queryDurationMs`
## Inspecting Runtime State
For browser debugging and Playwright smoke assertions, inspect:
- `window.__HONUA_QUICKSTART_EVENTS__`
- `window.__HONUA_QUICKSTART_RUNTIME__`
- `window.__HONUA_QUICKSTART_DISPOSE__()`
Useful fields:
- `baseUrl`, `serviceId`, `layerId`
- `serverVersion`, `releaseChannel`
- `sdkVersion`, `dataVersion`, `planId`, `planFingerprint`, `planPushdown`
- `featureCount`, `renderableFeatureCount`, `geometryTypes`
- `queryDurationMs`
- `layerIds`, `mapReady`, `selectedFeatureId`, `popupOpen`, `lastError`
- `journeyComplete`, `disposed`
Every telemetry event is also dispatched as `CustomEvent("honua:quickstart")`.
## External Follow-on
This repo still depends on one bounded external child ticket for long-term staging stability:
- `honua-server`: expose and document a stable staging quickstart dataset for JS SDK CI, including the canonical
service, layer, auth policy, and non-empty geometry contract.
---
# File: docs/comparison.md
# How Honua compares
**MapLibre gives you the map. Honua gives you everything else.** `@honua/sdk-js` is a typed,
protocol-neutral geospatial *service client* and migration toolkit that rides open renderers —
it is **not** a rendering engine, so this page does not compare rendering. It compares the
things Honua actually claims: the bytes you ship, the protocols you get a real client for,
and how fast a new project reaches a working map.
Three ground rules keep this page honest:
1. **Every Honua number is generated, never hand-edited.** This whole file is produced by
`npm run docs:comparison` from committed, regenerable inputs; CI fails when it drifts
(`npm run docs:comparison:check`). See [Methodology](#methodology-and-freshness).
2. **Every external number carries a source URL and an as-of date.**
3. **Non-goals are stated.** Esri's 3D/SceneView stack and MapLibre's rendering quality are
not competitions we enter; see the
[three-lanes strategy](./decisions/market-strategy-2026-three-lanes.md).
## Bundle size
Honua per-entrypoint sizes below are projected from the generated
[`docs/bundle-sizes.md`](./bundle-sizes.md) (measured 2026-07-16 at commit `10fe4df`;
esbuild `--bundle --minify`, target `es2020`, runtime peers external — the way a real consumer
builds). CI enforces a byte budget on every entrypoint (`npm run verify:bundle-budgets`).
| What you import | Minified | Gzip |
| --- | ---: | ---: |
| Full root entrypoint: connect → query → explain → mount workflow | 422.9 KiB | 110.6 KiB |
| Importing only `HonuaClient` (tree-shake guard) | 210.5 KiB | 53.2 KiB |
| Data→map bridge only: `mountSourceToMapLibre` from `/map` | 33.3 KiB | 10.8 KiB |
| Protocol-neutral contract (`Dataset`/`Source`/`Query`/`Result`) | 258.8 KiB | 68.3 KiB |
| ArcGIS compatibility layer (drop-in migration surface) | 978.1 KiB | 243.2 KiB |
| Geocoding client | 25.1 KiB | 7.3 KiB |
| Routing client | 18.7 KiB | 6.0 KiB |
For context, the rendering engine itself — `maplibre-gl` 5.21.1, measured locally from
this repo's pinned dependency (`dist/maplibre-gl.js`) — is 1024.9 KiB minified / **267.9 KiB gzip**.
A complete Honua + MapLibre app therefore ships roughly the engine plus whichever Honua
entrypoints it imports.
### Against the alternatives
**`@arcgis/core` 4.30 (ArcGIS Maps SDK for JavaScript).** Esri's own automated build metrics for its minimal `@arcgis/core` map samples (esbuild, Angular, React, Vue, Rollup, Webpack lanes). As of 2024-06-27 (4.30, the last core-sample metrics Esri published in jsapi-resources):
the main bundle alone is 1.31–1.49 MB minified (0.36–0.42 MB gzip), a
simple map view loads **3.5–4.1 MB of JavaScript** at startup, and the
on-disk build output is 8.3–10 MB across ~300–740 files. Source (pinned, retrieved 2026-07-13):
.
To be fair in both directions: `@arcgis/core` bundles its own renderer, so the honest
apples-to-apples is *Honua + MapLibre* against `@arcgis/core`. Compared conservatively —
our **uncompressed minified** bytes (engine 1024.9 KiB + Honua root 422.9 KiB ≈
1.41 MB) against Esri's reported startup JavaScript total
(3.5–4.1 MB) — the open stack ships roughly a third of the JavaScript, and
0.37 MB over the wire with gzip. Esri's totals also grow with widgets;
these figures are its *minimal* samples.
**`@esri/arcgis-rest-js`** (, retrieved 2026-07-13).
Esri's lightweight REST client family. Small per-package footprint; speaks ArcGIS services only, no map runtime. If all you need is small requests against ArcGIS-only services, it is a
fine, lighter choice — the comparison that matters is protocol coverage (below), not bytes.
**OpenLayers (`ol`)** (, retrieved 2026-07-13). A full renderer + formats library, not a typed multi-protocol service client; size depends heavily on tree-shaken imports.
## Protocol coverage
What you get a first-party, *typed* client for — versus what you hand-roll. Honua's column is
derived from the maintained
[protocol × capability matrix](./protocol-capability-matrix.md) (per-operation detail lives
there; capability misses throw `HonuaCapabilityNotSupportedError` instead of returning empty
results). Competitor columns are deliberately coarse: ✓ first-party support, ◐ partial or
manual assembly, — not provided.
| Protocol lane | Honua SDK | raw `maplibre-gl` | `@esri/arcgis-rest-js` | OpenLayers |
| --- | --- | --- | --- | --- |
| Esri GeoServices (FeatureServer query/edit) | ✓ typed client | — | ✓ | ◐ (a) |
| Esri GeoServices (MapServer / ImageServer render) | ✓ typed client | ◐ (b) | — | ✓ |
| OGC API Features (query/edit) | ✓ typed client | ◐ (c) | — | ◐ (d) |
| OGC API Tiles / Maps | ✓ typed client | ◐ (b) | — | ✓ |
| OGC API Records (catalog search) | ✓ typed client | — | — | — |
| STAC (cross-collection search) | ✓ typed client | — | — | ◐ (e) |
| WFS 2.0 (typed filters, transactions) | ✓ typed client | — | — | ◐ (f) |
| WMS (GetMap + typed GetFeatureInfo) | ✓ typed client | ◐ (b) | — | ✓ |
| WMTS (capabilities-driven tiles) | ✓ typed client | ◐ (b) | — | ✓ |
| OData v4 (tabular + spatial query, edits) | ✓ typed client | — | — | — |
| GeoParquet (client-side SQL via DuckDB-WASM) | ✓ typed client (lazy peer) | — | — | — |
| PMTiles archives | ✓ auto-registered protocol | ◐ (g) | — | ◐ (h) |
| Geocoding (provider-pluggable) | ✓ Nominatim / Photon / Pelias / Honua | ◐ (i) | ✓ (j) | — |
| Routing (provider-pluggable) | ✓ OSRM / Valhalla / Honua | — | ✓ (j) | — |
| ArcGIS migration codemod | ✓ `honua-migrate` + esri-compat | — | — | — |
Notes:
- (a) OpenLayers ships an `EsriJSON` format; service discovery, paging, auth, and edits are hand-rolled requests.
- (b) Raw MapLibre renders XYZ/raster endpoints you template by hand; there is no capabilities negotiation, feature query, or typed error surface.
- (c) Point a MapLibre GeoJSON source at an `items` URL; paging, filters, CRS negotiation, and edits are yours to build.
- (d) OpenLayers parses GeoJSON and has OGC API tile sources, but has no OGC API Features items client (paging, filters, transactions).
- (e) Via the third-party `ol-stac` package, not OpenLayers itself.
- (f) OpenLayers ships `ol/format/WFS` (GML parsing, filter builders); request orchestration, paging, and transaction bookkeeping are manual.
- (g) Via the official `pmtiles` JS package: install it and call `maplibregl.addProtocol` yourself. Honua auto-registers the protocol on map attach.
- (h) Via the third-party `ol-pmtiles` package.
- (i) Via the `maplibre-gl-geocoder` control plugin, which needs a geocoding API adapter you supply.
- (j) `@esri/arcgis-rest-geocoding` / `-routing` speak Esri's ArcGIS location services only (token/credit metering applies); Honua's providers are pluggable across open services.
## Time to first map
One scripted, reproducible measurement — from *nothing installed* to a working map against
the deterministic fixture lane (mock GeoServices server; **no live endpoints**):
| Phase | What is measured | Time |
| --- | --- | ---: |
| Cold install | `npm install @honua/sdk-js` (v0.1.0-beta.0) into a fresh temp project with an empty npm cache | 14.0 s |
| First map | rendered-map-ready in headless Chromium (all five quickstart journey stages complete and a MapLibre canvas mounted) | 7.5 s |
| **Total** | | **21.4 s** |
Reference run: 2026-07-13, Node v24.7.0, win32/x64,
22 logical CPUs, lane `browser-first-map`. Exact definition of the measured signal:
> Cold `npm install @honua/sdk-js` (empty npm cache) in a fresh temp project, plus: build the deterministic maplibre-quickstart fixture-lane example, serve it with the mock GeoServices server, and wait in headless Chromium for the rendered-map-ready signal (all five journey stages complete and a MapLibre canvas mounted). No live endpoints.
Reproduce on your machine from a clean checkout:
```bash
npm ci
npm run bench:ttfm # prints install / first-map / total; evidence JSON in test-results/
```
**Caveats, stated plainly.** The figure is machine-, network-, and registry-dependent — treat
it as a reference point and rerun it locally, not as a guarantee. The mock lane deliberately
excludes live-service latency (that is the point: it measures the SDK + toolchain path, not
someone's server). When Chromium is unavailable the script falls back to the
`node-first-query` lane and the evidence says exactly that — install + fixture-server ready +
first successful query, with no browser rendering claimed.
## Methodology and freshness
Every number on this page is regenerable from a clean checkout with one command:
| Figure | Regenerate with |
| --- | --- |
| Honua per-entrypoint sizes | `npm run report:bundle-sizes` (budgets enforced in CI by `npm run verify:bundle-budgets`) |
| `maplibre-gl` engine size | measured from the pinned `node_modules/maplibre-gl` during `npm run docs:comparison` |
| Protocol lanes (Honua column) | derived from [`docs/protocol-capability-matrix.md`](./protocol-capability-matrix.md) during `npm run docs:comparison` |
| Time to first map | `npm run bench:ttfm -- --write-reference` |
| This page | `npm run docs:comparison` |
This file is **generated** (`scripts/generate-comparison-page.mjs`) and CI fails when it is
hand-edited or stale (`npm run docs:comparison:check`), mirroring the README's
generated-bundle-table discipline. External figures are cited claims with pinned source URLs
and retrieval dates in the generator source — update them there, with a new date, or not at
all.
## Try it
- [60-second quickstart](../README.md#60-second-quickstart) — public endpoint in, typed features out.
- [`standalone-quickstart`](./standalone-quickstart.md) — the CI-kept-green, server-optional example: any public GeoServices endpoint → styled MapLibre map.
- [Demo gallery](https://honua-io.github.io/honua-sdk-js/gallery.html) — all runnable examples.
---
# File: docs/shared-client-contract.md
# Shared Client Contract
Status: implemented in `src/contract/` (ticket `honua-sdk-js-23`).
The shared contract is the protocol-neutral vocabulary every Honua data
adapter speaks. It exists so cross-protocol code — exploration views,
visual builders, and the server `SourceBinding`/`MapPackage` exporters
— can be written once against `Dataset` / `Source` / `Query` / `Result`
/ `MapBinding` rather than re-litigating the surface in each ticket.
## Goals (and non-goals)
- **Goal:** one canonical name for "the dataset", "the source", "the
capability", "the query", "the result", "the map binding", and "the
exploration state" across `HonuaFeatureLayer`, `HonuaMapService`,
`HonuaOgcFeatures`, first-party OGC render/search adapters,
first-party WMS / WMTS adapters, the first-party WFS 2.0 adapter,
and the first-party OData adapter.
- **Goal:** wrap (do not replace) the existing runtime classes in
`src/core/surfaces.ts`. Existing callers continue to work; adapter
tickets opt in to the canonical surface.
- **Goal:** stable serialization shape that survives a round-trip with
the server `SourceBinding` / `MapPackage` documents (see
[`source-binding-alignment.md`](./source-binding-alignment.md)).
- **Non-goal:** a runtime rewrite. The contract is a typed surface plus
thin adapter functions — one per built-in protocol
(`geoServicesFeatureSource`, `geoServicesMapServiceSource`,
`geoServicesImageSource`, `geoServicesGeometryServiceSource`,
`geoServicesGPServiceSource`, `ogcFeaturesSource`, `ogcTilesSource`,
`ogcMapsSource`, `stacSearchSource`, `wmsSource`, `wmtsSource`,
`wfsSource`, `odataSource`).
- **Non-goal:** replacing the v1 `Query` envelope while protocol compilers
migrate. `Query.where` is a deprecated, source-native compatibility string;
new filters use the typed semantic AST from `@honua/sdk-js/query-planner`.
## Module layout
```
src/contract/
├── index.ts # barrel — re-exports types and source factories
├── spatial-aggregation.ts # indexed aggregation request/response metadata
├── tiles.ts # dynamic query tile descriptors, cache keys, identity
├── types.ts # protocol, capability, source, dataset, query, result
└── source.ts # createDataset + built-in adapters
```
Public entrypoint: `@honua/sdk-js/contract` (also re-exported from the
top-level `@honua/sdk-js` and `@honua/sdk-js/honua` barrels).
## Cross-SDK binding policy
This JS contract is also the draft semantic anchor for the Python and .NET SDKs.
The SDKs should align behavior and vocabulary while keeping language-native
names and casing. For example, the same operation may surface as `queryAll()`
in TypeScript, `query_all()` in Python, and `QueryAllAsync()` in .NET.
The binding policy and cross-SDK fixture pack are documented in
[`sdk-surface-alignment.md`](./sdk-surface-alignment.md). The JSON fixture pack
lives under `test/fixtures/sdk-contract/`; it is intentionally language-neutral
so downstream SDKs can consume the same protocol, capability, result,
unsupported-capability, and degraded-result scenarios.
## Canonical nouns
| Type | What it is |
| --- | --- |
| `Protocol` | One of twenty identifiers — shared canonical gRPC FeatureService transport (`grpc`), five GeoServices service types (`geoservices-feature-service`, `geoservices-map-service`, `geoservices-image-service`, `geoservices-geometry-service`, `geoservices-gp-service`), OGC API + STAC adapters (`ogc-features`, `ogc-tiles`, `ogc-maps`, `ogc-records`, `stac`), `wfs`, `wms`, `wmts`, `odata`, static-data adapters (`pmtiles`, `geoparquet`), plus three MapLibre-native sources (`maplibre-vector`, `maplibre-raster`, `maplibre-geojson`). |
| `Capability` | A coarse-grained per-source operation capability (`query`, `queryAggregate`, `spatialAggregate`, `queryExtent`, `queryObjectIds`, `queryRelated`, `applyEdits`, `attachments`, `render`, `tiles`, `sql`, `stream`, `pbf`, `image`, `geometry`, `geoprocess`, `processes`). The canonical `Source` surface standardizes the query / edit / related / attachment / object-id subset today; `spatialAggregate`, `image` / `geometry` / `geoprocess` / `processes` are negotiated for indexed analytics, `Source.protocol()` escape hatches, and for the `IJobRun`-based OGC API Processes runner because their request shapes are too protocol-specific to belong on the unified query envelope. Top-level `connect()` is product discovery, not an operation on one `Source`, and is tracked as a `discovery` support claim instead. |
| `Capabilities` | `ReadonlySet`. Set membership = first-party protocol support, whether the caller consumes it through a canonical `Source` method or the typed protocol escape hatch. Under `strict` (default) a missing capability throws `HonuaCapabilityNotSupportedError`. Under `degraded` only call sites with a defined fallback proceed (today: OGC `queryAggregate` and `queryExtent`); every other missing capability still throws. |
| `SourceLocator` | Protocol-specific endpoint info (`url`, `serviceId`, `layerId`, `collectionId`, `tileMatrixSetId`, `styleId`, `typeName`, `entitySet`, `taskName`). Field-compatible with the server `SourceBinding.locator`; `tileMatrixSetId` / `styleId` carry OGC API Tiles / Maps route hints for downstream `SourceBinding` work tracked in [`source-binding-alignment.md`](./source-binding-alignment.md). |
| `SourceDescriptor` | `{ id, protocol, locator, capabilities, schema?, attribution? }`. The serializable identity of one source. |
| `Source` | Runtime handle. Methods: `query`, `queryAll`, `queryAggregate`, `queryExtent`, `stream`, `queryObjectIds`, `applyEdits`, `queryRelated`, `attachments` (namespace), `protocol` (typed escape hatch; `adapter` is the legacy alias). |
| `Dataset` | Logical grouping of sources sharing identity. Methods: `source(id)`, `sourceIds()`, `isCompatible()`, `supportsFeature()`. |
| `Query` | The v1 compatibility envelope `{ where?, spatialFilter?, outFields?, orderBy?, pagination?, aggregation?, returnGeometry?, outSr?, signal? }`; `where` is deprecated and source-native. New filters use the typed query-planner AST. |
| `Result` | `{ features, exceededTransferLimit, totalCount?, aggregateRows?, extent?, fields?, degraded? }`. |
| `SpatialAggregationRequest` / `SpatialAggregationResult` | Indexed spatial aggregation contract for large result sets. Requests carry `where`, `spatialFilter`, `viewport`, zoom/index-resolution hints, opaque index selection, summary specs (`category`, `histogram`, `range`, `count`, `sum`, `avg`, `min`, `max`), and optional `groupBy`. Results carry opaque indexed cells, grouped/totals summaries, backend index metadata, widget metadata, and progressive loading state. |
| `EditEnvelope` | `{ adds?, updates?, deletes?, rollbackOnFailure?, signal? }`. Each add / update is a `CanonicalFeature` (attributes + optional geometry + optional id). |
| `EditResult` | `{ added, updated, deleted, degraded? }` — one `EditOutcome` per requested operation. |
| `EditWorkflowSession` | Form/edit session over one `Source`: projects field metadata and domains, exposes relationship / attachment capability states, stages feature and attachment edits, runs optimistic hooks, and returns a normalized `EditWorkflowSubmitResult`. |
| `RelatedQuery` / `RelatedResult` | Canonical related-records request and response. Adapters that lack relationships (OGC, OData, ImageServer) throw rather than return empty groups. |
| `AttachmentApi` | Namespace returned by `Source.attachments`. Methods: `query`, `list`, `add`, `update`, `delete`. Adapters that do not advertise `attachments` throw `HonuaCapabilityNotSupportedError` from each method so the namespace property is always present and capability negotiation stays uniform. |
| `MapBinding` | `{ sourceId, layerIds, style?, minzoom?, maxzoom? }`. Maps onto `MapPackage.sourceBindings` + `MapPackage.mapSpec` server-side. The `@honua/sdk-js/runtime` module consumes a full `MapPackage` on the client — see [`maplibre-runtime.md`](./maplibre-runtime.md). |
| `QueryTileSourceDescriptor` | Typed dynamic vector/query tile descriptor. It binds a canonical `Source` or source id to tile endpoint metadata, query/projection cache identity, fallback policy, and feature identity hooks. Runtime MapLibre helpers and the canonical `/query-tiles` server contract live in [`dynamic-query-tiles.md`](./dynamic-query-tiles.md). |
## Dynamic query tile server contract
Dynamic query tiles have a server-side contract in addition to the client
descriptor. The canonical route prefix is `/query-tiles`, with TileJSON,
vector tile, and feature-detail routes under
`/query-tiles/sources/{sourceId}`. The contract defines request parameters for
filters, projection fields, output spatial reference, tile matrix set, extent,
simplification tolerance, max features, cache partitioning, and cache busting.
`@honua/sdk-js/contract` exports route builders, parser helpers, response
types, and the `QUERY_TILE_SERVER_CONTRACT_VERSION` constant. The reusable
fixture lives at `test/fixtures/sdk-contract/query-tile-server.v1.json` and is
intended for Honua server implementation repos to consume in their own
conformance tests.
## Capability negotiation
Two policies, declared at `createDataset({ capabilityPolicy })`:
- `strict` (default): `Source` operations whose required capability is
missing throw `HonuaCapabilityNotSupportedError`. Callers can branch on
`error.capability` and `error.protocol` to swap protocols, fall back, or
surface the limitation to the user.
- `degraded`: `Source` operations attempt a client-side fallback when the
server cannot serve the capability natively. The result envelope carries
`degraded: DegradedReason[]` documenting what was approximated and why.
The capability matrix lives in
[`protocol-capability-matrix.md`](./protocol-capability-matrix.md). Callers
must pass the capability set they want enforced via
`SourceDescriptor.capabilities` — per-source overrides (e.g. downgrading
`queryAggregate` on a Feature Service whose metadata reports
`supportsStatistics: false`) are the caller's responsibility for the
GeoServices, OGC, STAC, WFS, and WMS adapters today. **OData is the
first adapter to implement automatic metadata-driven downgrades**: the
entity-set adapter lazily fetches `$metadata` on the first
capability-gated method, parses `Capabilities.*` annotations, and
intersects the declared capability set with what the server advertises.
See [`protocol-capability-matrix.md`](./protocol-capability-matrix.md)
under *OData* for the implementation details. Other adapters (GeoServices
`supportsStatistics`, OGC `conformsTo`) follow the same pattern as
follow-up work.
The registry is intentionally broader than the current `Source` method list so
downstream adapter tickets can negotiate `render` / `tiles` / `sql` /
`queryObjectIds` / `spatialAggregate` / etc. without inventing a second
capability vocabulary. `spatialAggregate` has no default protocol support
today; sources should advertise it only when source-specific metadata confirms
an indexed aggregation backend. Apps must treat returned cell ids and
`index.model.id` as opaque, so H3, Quadbin, or a provider-specific grid can
drive the same widgets.
For multi-source compositions, use `intersectCapabilities` from
`@honua/sdk-js/contract` to compute the **weakest** capability set
across the participating sources before fanning a call out — the rule
plus the partition-then-intersect pattern for per-operation reasoning
lives in [`composition.md`](./composition.md). Adapters that emit
`Result.degraded[]` populate `DegradedReason.sourceId` from
`descriptor.id` so a fan-out can attribute each degradation back to
the exact source that triggered it.
## Source factory
```ts doc-test=skip reason="partial excerpt requires application host context"
import { createDataset, type SourceDescriptor } from "@honua/sdk-js/contract";
const dataset = createDataset({
id: "parcels",
client,
capabilityPolicy: "strict",
sources: [
{
id: "parcels-fs",
protocol: "geoservices-feature-service",
locator: { url: "...", serviceId: "Parcels", layerId: 0 },
capabilities: PROTOCOL_DEFAULT_CAPABILITIES["geoservices-feature-service"],
},
] satisfies SourceDescriptor[],
});
const parcels = dataset.source("parcels-fs")!;
// Deprecated source-native compatibility text during semantic compiler adoption.
const result = await parcels.query({ where: "STATE = 'CA'", pagination: { limit: 100 } });
```
The built-in resolver handles `geoservices-feature-service`,
`geoservices-map-service`, `geoservices-image-service`,
`geoservices-geometry-service`, `geoservices-gp-service`, `ogc-features`,
`ogc-tiles`, `ogc-maps`, `stac`, `wfs`, `wms`, `wmts`, and `odata`.
MapLibre-native sources register through
`CreateDatasetOptions.resolveSource`. OGC API
Processes is a job runner rather than a queryable source — reach it
through `HonuaClient.ogcProcesses().execute(...)` (returns the canonical
`IJobRun`) instead of `Dataset.source()`.
The five GeoServices factories cover the surface published in
`honua-server/docs/gis/geoservices-rest-parity.md`:
- `geoServicesFeatureSource` — FeatureServer (query, edits, related,
attachments, object ids, replica/calculate/validateSQL/append/bins/estimate
via `protocol()`).
- `geoServicesMapServiceSource` — MapServer (read-only query family,
related records; export/identify/find/legend/tile via `protocol()`).
- `geoServicesImageSource` — ImageServer (raster catalog query,
exportImage / identify / tile / legend via `protocol()`).
- `geoServicesGeometryServiceSource` — Geometry Service (utility-only;
query family throws, operations live behind `protocol()`).
- `geoServicesGPServiceSource` — GP Service (utility-only; submitJob /
jobStatus / cancelJob / jobResult via `protocol()`).
The OGC API and STAC factories cover `docs/ogc-api.md`:
- `ogcFeaturesSource` — OGC API Features (query, edits, object ids,
stream; `queryAggregate` / `queryExtent` degrade client-side).
- `ogcTilesSource` — OGC API Tiles (render-only; query family throws,
`HonuaOgcTileset` / `HonuaOgcTiles` reachable via `protocol()`).
- `ogcMapsSource` — OGC API Maps (render-only; same shape as Tiles).
- `ogcRecordsSource` — OGC API Records metadata catalog search
(`/collections/{catalogId}/items`, queryObjectIds, stream; Records-specific
`q` / `type` / `externalIds` and raw response access via `protocol()`).
- `stacSearchSource` — STAC API search (`/search` query, queryObjectIds,
stream; cross-collection scope via `locator.collectionId`).
The WMS / WMTS factories cover the OGC web-map services per
`docs/protocol-capability-matrix.md`:
- `wmsSource` — WMS 1.3.0 (render + tiles via `GetMap`; `query` via
point-only `GetFeatureInfo`; raw multi-pixel `featureInfo()` and the
per-layer service handles reachable via
`Source.protocol("wms" | "wms-layer")`).
- `wmtsSource` — WMTS 1.0.0 (render + tiles via RESTful tiles; query
family throws; service / layer / tileset handles reachable via
`Source.protocol("wmts" | "wmts-layer" | "wmts-tileset")`).
`docs/wfs.md` documents the WFS 2.0 factory in the same shape:
- `wfsSource` — WFS 2.0 (query, queryAll, queryExtent, queryObjectIds,
applyEdits, stream; FES 2.0 emission for `Query.where` /
`Query.spatialFilter`; raw GML / `` payloads via
`protocol("wfs")`).
The OData factory wraps an OData v4 entity set behind the canonical
surface:
- `odataSource` — query / queryObjectIds / stream / applyEdits
first-party (POST/PATCH/DELETE; PUT is unsupported per the parity
matrix and addressed by `PATCH` with the full canonical body).
Dialect-specific `$batch` / `$apply` / `$search` / `$deltatoken`
reach `HonuaOdataEntitySet` through `Source.protocol("odata")`.
OData is the **first adapter** to lazily fetch service metadata
(`$metadata`) and intersect the declared `Capabilities` set with
what the server advertises through `Capabilities.*` annotations —
see [`protocol-capability-matrix.md`](./protocol-capability-matrix.md)
for the precedent and [`decisions/odata-library-selection.md`](./decisions/odata-library-selection.md)
for the runtime-library posture.
## Edit Workflow Sessions
`createEditSession({ source, kind, feature, metadata, optimistic })`
is the SDK-level workflow contract for form and editor surfaces. It is
intentionally layered over `Source.applyEdits`, `Source.queryRelated`,
and `Source.attachments` rather than replacing the protocol adapters.
The session resolves fields from `source.descriptor.schema.fields` plus
caller-supplied domains, validates required / read-only / length /
coded-value / range constraints before submit, and exposes
`capabilities()` so unsupported `applyEdits`, `attachments`, and
`queryRelated` states are explicit and testable. Relationship reads use
`session.queryRelated(...)`; attachment reads and staged add / update /
delete mutations use the same source attachment namespace.
`submit()` sends the feature edit envelope first, then applies staged
attachment mutations against the committed feature id when the backend
returns one. Optimistic hooks run in this order: `optimistic.apply`
before the remote edit, `optimistic.commit` on full success, and
`optimistic.rollback` for failed or partial feature / attachment
results. `EditWorkflowSubmitResult.failures` normalizes per-row errors
from GeoServices, OGC Features, WFS, and OData into `validation`,
`conflict`, `capability`, `transport`, `server`, or `partial-failure`.
When a version / ETag / updated-at field is present (or supplied in
`metadata.conflict`), conflict failures carry that information so hosts
can present reload / merge / overwrite workflows without parsing
protocol-specific error text.
`createEditSketchWorkflow(...)` wraps the same session contract with
UI-independent sketch state. It exposes explicit support for point,
line, polygon, rectangle, circle, and buffer tools; dirty state;
undo / redo / discard; attachment staging; validation; submit; and an
optional annotation persistence hook that runs only after a successful
edit commit. Unsupported sketch, attachment, relationship, conflict, or
annotation persistence capabilities remain visible in the returned
snapshot instead of being hidden by component state.
`Source.queryAll()` and `Source.stream()` drain every page the server
returns — the built-in adapters override the core helpers' 100-page
default so a large `queryAll()` is not silently truncated. Callers who
want a hard cap should paginate with `Query.pagination` (`offset` skips
ahead; `limit` clips, and its meaning depends on the method):
- `query()` — `limit` is the single-page record count.
- `queryAll()` — `limit` is the total-row cap on the materialized result.
The adapter sizes `pageSize` and `maxPages` from `limit` so the paging
loop fetches at most `limit + 1` rows; the extra row lets the result
stamp `exceededTransferLimit: true` when more records exist.
- `stream()` — `limit` is the per-batch page size (not a global cap).
Each yielded `Result` carries up to `limit` features; callers that
want a global cap must stop iterating explicitly.
## Compatibility gating
`Dataset.isCompatible()` calls `HonuaClient.checkCompatibility()` once and
caches the result. `Dataset.supportsFeature(feature)` proxies to
`HonuaClient.supportsFeature` for fine-grained checks. Both reuse the
existing compatibility-gate workflow — no new wire calls were introduced.
## Protocol escape hatch
`Source.protocol("geoservices-feature-service")` returns the underlying
`HonuaFeatureLayer` instance (or `undefined` for the wrong kind). The
accessor name is `protocol` because it surfaces protocol-specific
operations — raw `where`, raw `outFields`, GeoServices `calculate` /
`validateSQL` / replica / `queryBins` / `getEstimates` — that the
canonical `Source` surface intentionally does not expose. The
`adapter()` method is preserved as a legacy alias for callers written
against the original ticket-23 surface; it returns the same instance.
The `AdapterTypeMap` interface uses TypeScript declaration merging so
adapter tickets can plug in their own kind → instance type mapping
without touching this file.
`grpc` is a canonical protocol id for the shared FeatureService transport
and fixture pack; it is consumed through the client gRPC-Web transport
rather than a `Source.protocol("grpc")` adapter handle in this package.
```ts doc-test=skip reason="partial excerpt requires application host context"
declare module "@honua/sdk-js/contract" {
interface AdapterTypeMap {
"my-protocol": MyProtocolLayer;
}
}
```
The shipped map covers `geoservices-feature-service` →
`HonuaFeatureLayer`, `geoservices-map-service` → `HonuaMapService`,
`geoservices-map-layer` → `HonuaMapLayer`, `geoservices-image-service`
→ `HonuaImageService`, `geoservices-geometry-service` →
`HonuaGeometryService`, `geoservices-gp-service` →
`HonuaGeoprocessingService`, `ogc-features` →
`HonuaOgcFeatureCollection`, `ogc-tiles` → `HonuaOgcTileset |
HonuaOgcTiles`, `ogc-maps` → `HonuaOgcMaps |
HonuaOgcCollectionMap`, `ogc-processes` → `HonuaOgcProcesses`,
`stac` → `HonuaStacSearch`, `wms` → `HonuaWms`, `wms-layer` →
`HonuaWmsLayer`, `wmts` → `HonuaWmts`, `wmts-layer` →
`HonuaWmtsLayer`, `wmts-tileset` → `HonuaWmtsTileset`,
`wfs` → `HonuaWfsFeatureType`, and `odata` → `HonuaOdataEntitySet`.
The WFS root handle (capabilities cache, stored-query discovery) is
reachable through `Source.protocol("wfs").root`.
## What downstream tickets must consume
1. New protocol adapters must implement `Source` and register either
as a built-in `case` in `buildBuiltInSource` (the precedent followed
by `wmsSource` / `wmtsSource` / `wfsSource` / `odataSource`) or
through `CreateDatasetOptions.resolveSource`. They must declare
their default capability set in `PROTOCOL_DEFAULT_CAPABILITIES`
(this file owns that table — adapter PRs extend it).
2. Visual builder, exploration, and server-export tickets must consume
`Dataset` / `Source` / `Query` / `Result` / `MapBinding` rather than
the per-class request shapes (`QueryFeaturesRequest`, etc.). Per-class
shapes are still available via `Source.adapter()` for legacy paths.
3. New error types must flow through `HonuaError` and `isHonuaError`.
This ticket added `HonuaCapabilityNotSupportedError` and
`HonuaExplorationContextError`. The first-party WMS / WMTS adapter
ticket extended the union with `HonuaWmsCapabilitiesParseError` and
`HonuaWmtsCapabilitiesParseError` so callers can classify XML parser
failures through the same guard.
## Async operations: `IJobRun`
Long-running server-side operations surface through the canonical
`IJobRun` interface in `@honua/sdk-js/contract`. OGC API Processes,
GeoServices REST GPServer, and the open `honua-io/geospatial-grpc`
`ProcessService` all normalize onto the same lifecycle vocabulary:
```ts doc-test=skip reason="partial excerpt requires application host context"
import type { IJobRun } from "@honua/sdk-js/contract";
const job: IJobRun = await client.ogcProcesses().execute({
processId: "buffer",
inputs: { feature: someGeoJson },
mode: "async",
});
const unwatch = job.watch((snap) => {
console.log(snap.status, snap.progress);
});
const { outputs } = await job.results();
unwatch();
```
For app code that should not know which protocol is behind a workflow,
use `HonuaProcessRunner`:
```ts doc-test=skip reason="partial excerpt requires application host context"
const ogc = client.ogcProcessRunner();
const gp = client.geoprocessingRunner("Analysis", "OverlayFacilities");
const grpc = client.geospatialGrpcProcessRunner(processServiceClient);
await ogc.execute({ processId: "buffer", inputs });
await gp.execute({ inputs: gpParameters, resultNames: ["outputLayer"] });
await grpc.execute({ plan: executionPlan, context: executionContext });
```
`createOgcProcessesAdapter`, `createGeoServicesGpAdapter`, and
`createGeospatialGrpcProcessAdapter` expose the same adapter contract for
callers that construct protocol handles outside `HonuaClient`. The
geospatial-grpc adapter is structural: it expects a generated
`ProcessService` client with `validatePlan`, `dryRunPlan`, `submitJob`,
`getJob`, `getJobResult`, and `cancelJob`, but this package does not
vendor the `honua-io/geospatial-grpc` generated process proto directly.
`IJobRun` exposes `id`, `type`, `status`, `progress`, `poll()`,
`watch()`, `results()`, and `cancel()`. The OGC API Processes 1.0
status vocabulary (`accepted`, `running`, `successful`, `failed`,
`dismissed`) is canonical. GeoServices `esriJobSubmitted` /
`esriJobExecuting` / `esriJobSucceeded` / `esriJobFailed` /
`esriJobCancelled` and geospatial-grpc `JobState` values
(`JOB_STATE_RUNNING`, `JOB_STATE_COMPLETED`, `JOB_STATE_FAILED`,
`JOB_STATE_CANCELLED`, etc.) translate onto it. Failed OGC runs reject
`results()` with `HonuaJobFailedError`, whose `message` is populated
from the server's `statusInfo.exception.message` when present and falls
back to `statusInfo.message` otherwise (to match honua-server's
`StatusInfo` DTO, which exposes only `message`).
`cancel()` is idempotent against the two documented benign paths:
"job gone" (404) returns the cached status, and the terminal race
(409 "Cannot dismiss completed job" from honua-server) triggers a
follow-up GET and returns the authoritative terminal status — but
only if the poll confirms a terminal state. honua-server also emits
409 for "Dismiss could not be confirmed" (backend dismissal unconfirmed)
and "Cancellation not supported" (backend lacks cancel support); both
rethrow as `HonuaHttpError` so callers can branch or retry instead of
seeing a fabricated success. Submitted processes are typed as
`IJobRun`; `HonuaOgcProcessJobRun` is the implementation behind that
interface and should not be the caller-facing contract.
## OGC API Tiles / Maps / Records / Processes / STAC
The first-party OGC adapters live alongside `HonuaOgcFeatures`:
| Conformance area | Entry point | Source protocol | Contract capabilities |
| --- | --- | --- | --- |
| OGC API Features | `client.ogcFeatures()` / `HonuaOgcFeatures` | `ogc-features` | `query`, `queryObjectIds`, `applyEdits`, `stream` |
| OGC API Tiles | `client.ogcTiles()` / `HonuaOgcTiles`, `HonuaOgcTileset` | `ogc-tiles` | `render`, `tiles` (tileset-bound when locator includes `tileMatrixSetId`; root discovery handle otherwise) |
| OGC API Maps | `client.ogcMaps()` / `HonuaOgcMaps`, `HonuaOgcCollectionMap` | `ogc-maps` | `render` |
| OGC API Records | `client.ogcRecords()` / `HonuaOgcRecords`, `HonuaOgcRecordCollection` | `ogc-records` | `query`, `queryObjectIds`, `stream` |
| OGC API Processes | `client.ogcProcesses()` / `HonuaOgcProcesses` | (no source — job runner) | `processes` from conformance negotiation, not `PROTOCOL_DEFAULT_CAPABILITIES` |
| STAC API | `client.stac()` / `HonuaStacSearch` | `stac` | `query`, `queryObjectIds`, `stream` |
OGC API Tiles and OGC API Maps are render-only — their `Source.query*`
methods throw, and renderers reach the underlying class through
`Source.adapter("ogc-tiles")` / `Source.adapter("ogc-maps")`. OGC API
Records and STAC both flow through the canonical `Source.query()` path,
but Records is metadata catalog search and STAC is asset/item search. OGC
API Processes does not register as a `Source` because its inputs are not queryable; it produces `IJobRun` from
`execute(...)`.
## WMS / WMTS web-map services
The first-party OGC web-map adapters share the contract surface:
| Service | Entry point | Source protocol | Contract capabilities |
| --- | --- | --- | --- |
| WMS 1.3.0 | `client.wms(serviceId)` / `HonuaWms`, `HonuaWmsLayer` | `wms` | `render`, `tiles`, `query` (point-only `GetFeatureInfo`) |
| WMTS 1.0.0 | `client.wmts(serviceId)` / `HonuaWmts`, `HonuaWmtsLayer`, `HonuaWmtsTileset` | `wmts` | `render`, `tiles` |
`Source.protocol("wms")` returns the service handle and
`Source.protocol("wms-layer")` a layer-bound handle (when
`locator.typeName` is set). WMTS exposes three handles —
`Source.protocol("wmts")` (service), `"wmts-layer"` (layer-bound), and
`"wmts-tileset"` (layer × style × tile-matrix-set bound). MapLibre
integration ships through the runtime helpers
`buildWmsRasterSourceSpec(descriptor)` /
`buildWmtsRasterSourceSpec(descriptor)` from `@honua/sdk-js/runtime` —
they emit a `raster` source spec without forcing callers to hand-assemble
a `GetMap` URL or RESTful tile template. The style-spec resolver
`createSources(client, style)` (from `@honua/sdk-js`) and
`HonuaMap.getSource(name)` both produce the same `HonuaWms` /
`HonuaWmsLayer` / `HonuaWmts` / `HonuaWmtsTileset` handles when a
style declares a `honua-wms` / `honua-wmts` source type — the shared
`src/style/wms-wmts-resolvers.ts` module owns the URL parsing and
layer / tileset selection rules so the two surfaces never diverge.
The WMS `LAYERS=` / `locator.typeName` / `spec.layers` value is
parsed through the canonical `parseWmsLayerNames` helper (in
`src/core/wms.ts`); the layer-bound `HonuaWmsLayer` handle is only
returned for a single non-empty token, while multi-layer composites
(`"a,b"`) stay on the service-level `HonuaWms` handle.
See [`docs/protocol-capability-matrix.md`](./protocol-capability-matrix.md)
for axis-order, dimension, legend, and TileMatrixSet notes.
OGC conformance class identifiers are intentionally kept *internal*.
`negotiateOgcCapabilities(protocol, conformsTo)` from
`@honua/sdk-js/honua` translates a server-advertised `conformsTo[]`
list into a canonical `Capabilities` set; downstream callers that want
to gate on a specific extension (CQL2, transactions, etc.) use
`hasOgcConformanceClass(...)` with a substring match. No OGC
conformance class name appears as a primary SDK type, per the ticket
constraint.
## Test coverage
Conformance fixtures under `test/contract/` exercise the canonical
surface against mock adapters for each protocol. Adding a new protocol
adapter means adding a fixture there; the parametrized scenarios run
unchanged.
- `test/contract/conformance.test.ts` — cross-protocol parametric
scenarios. Each new adapter registers a harness; the suite runs the
same query / queryExtent / queryAggregate / stream cases against
every harness.
- `test/contract/odata-conformance.test.ts` — adapter-specific
translation rules and escape-hatch surface (`metadata`, `batch`,
`apply`, `search`, `delta`, `raw`).
- `test/contract/ogc-conformance.test.ts` — the conformance-class
→ capability negotiation translation table.
---
# File: docs/query-planner.md
# Deterministic query planner
`@honua/sdk-js/query-planner` is the first production slice of the execution
planner described by the [north-star application-kernel
decision](./decisions/north-star-sdk-application-kernel.md). It turns the
current protocol-neutral `Query` plus an already-discovered `SourceDescriptor`
into a versioned, serializable IR and an immutable explain plan. Explaining is
synchronous and side-effect free: it does not fetch metadata or rows, mutate a
renderer, or execute the query.
The subpath is experimental while the remaining compiler and columnar slices
land. It is intentionally not exported from the root barrel.
## Typed semantic query AST
The experimental planner subpath also exposes the first typed semantic-query
surface. It is additive: existing v1 plans and protocol compilers continue to
consume the compatibility `Query.where` path until the compiler migration
lands. New code can build immutable property/literal, comparison, boolean,
null, list, range, pattern, spatial, and temporal nodes without choosing a
wire dialect:
```ts doc-test=compile
import { createSemanticQueryBuilder, defineSemanticQuery } from "@honua/sdk-js/query-planner";
interface Incident {
id: number;
status: string;
score: number;
}
const q = createSemanticQueryBuilder();
const semanticQuery = defineSemanticQuery(
q.features({
select: ["id", "status"] as const,
filter: q.and(
q.comparison("eq", q.property("status"), q.literal("active")),
q.between(q.property("score"), 50, 100),
),
sort: [{ field: "score", direction: "desc" }],
page: { kind: "first", limit: 100 },
}),
);
console.log(semanticQuery.filter);
```
`parseSemanticQuery(untrusted, { schema, protocol })` is the JavaScript and JSON
boundary. It reparses the supplied `SourceSchemaV2`, validates field existence,
operator/type compatibility, closed domains, declared ranges, geometry and
temporal eligibility, safe length and `multiple-of` constraints,
projection/sort/group/metric fields, native payload form, and native dialect
compatibility. Literal type admission reuses the canonical `SourceSchemaV2`
value semantics, including binary encodings, numeric precision, and temporal
precision. Source-provided ECMA-262 field patterns remain metadata and are not
executed at this untrusted boundary. Parsed queries are deeply frozen, with
equivalent omitted defaults (case-sensitive patterns and native null ordering)
normalized before hashing or interchange. Parsing is bounded by byte, node,
depth, collection, and text limits; cyclic values, accessors, non-JSON
prototypes, unknown members, blank identifiers or native text/XML payloads,
invalid protocol options, and non-finite numbers fail closed.
Protocol-native filters remain explicit and dialect tagged. For example, an
OGC source may carry `cql2-json` or `cql2-text`; it cannot carry
`geoservices-sql92`. A native expression is an escape hatch, not a claim that
the expression is protocol neutral.
This semantic surface does not change compilation or execution. Protocol
compiler adoption remains in #527-#529.
### Canonical bytes and query identity
`canonicalSemanticQueryBytes()` and `hashSemanticQuery()` first run the same
bounded runtime/schema validation, then encode a versioned envelope with sorted
object keys. Array order remains significant. The identity envelope always
contains schema fingerprint, protocol, CRS-registry version, and policy version
slots; unavailable values are explicit `null`, not omitted. Cancellation,
realtime cursors, observation timestamps, and other volatile execution state
are not members of the semantic AST.
```ts doc-test=compile
import {
canonicalSemanticQueryBytes,
createSemanticQueryBuilder,
hashSemanticQuery,
} from "@honua/sdk-js/query-planner";
interface Parcel {
id: number;
status: string;
}
const query = createSemanticQueryBuilder();
const request = query.features({
select: ["id"] as const,
filter: query.comparison("eq", query.property("status"), "active"),
});
const identity = {
protocol: "ogc-features" as const,
crsVersion: "epsg-db:2026.1",
policyVersion: "query-policy:7",
};
console.log(canonicalSemanticQueryBytes(request, identity).byteLength);
console.log(hashSemanticQuery(request, identity));
```
Equivalent validated inputs produce identical UTF-8 bytes and a
domain-separated SHA-256 hash. Changing schema, CRS, policy, protocol, field
order, sort precedence, or another semantic member changes identity.
### CQL2 JSON interchange
`semanticFilterToCql2Json()` and `semanticFilterFromCql2Json()` implement a
strict, lossless supported subset of the
[OGC CQL2 JSON encoding](https://docs.ogc.org/is/21-065r2/21-065r2.html).
Import uses the same byte/node/depth/collection bounds and duplicate-name
rejection as `parseSemanticQuery`; imported filters are runtime validated and
deeply frozen.
```ts doc-test=compile
import {
createSemanticQueryBuilder,
semanticFilterFromCql2Json,
semanticFilterToCql2Json,
} from "@honua/sdk-js/query-planner";
interface Road {
roadClass: string;
}
const query = createSemanticQueryBuilder();
const filter = query.like(query.property("roadClass"), "primary%", {
caseSensitive: false,
});
const cql2 = semanticFilterToCql2Json(filter, { protocol: "ogc-features" });
const restored = semanticFilterFromCql2Json(cql2, { protocol: "ogc-features" });
console.log(cql2, restored);
```
The supported subset includes property/literal comparisons, `and`/`or`/`not`,
null tests, lists, numeric ranges, case-sensitive and `casei` patterns,
standard topological/non-wrapping-bbox predicates, and the four semantic
temporal predicates. CQL2 carries spatial CRS outside its JSON expression, so
spatial import/export requires an explicit executable `filterCrs` binding and
verifies every operand against it. JSON-number-encoded decimal fields preserve
their supported CQL2 scalar representation; string-encoded high-precision
numbers remain unsupported. Both import and export enforce the normative CQL2
JSON schema's two-member minimum for `GeometryCollection`. Distance extensions,
native expressions, property-property comparisons, arithmetic/custom
functions, measured geometry layouts, and wrapping bounding boxes fail closed
rather than being weakened.
### Deprecated raw `where` compatibility
The v1 `Query.where` member remains operational but is deprecated and explicitly
source-native. `legacyWhereToNativeFilter()` is the migration bridge for text
whose dialect is losslessly known: GeoServices SQL-92, CQL2 text, OData 4.0, or
DuckDB SQL. WFS's parsed legacy grammar and Honua gRPC's JSON dialect are not
mislabelled as raw text.
```ts doc-test=compile
import { legacyWhereToNativeFilter } from "@honua/sdk-js/query-planner";
const compatibilityFilter = legacyWhereToNativeFilter(
"geoservices-feature-service",
"STATUS = 'OPEN'",
);
console.log(compatibilityFilter.dialect); // geoservices-sql92
```
Existing v1 planning still serializes `where` as `{ kind: "source-native",
expression }`, preserving compatibility until #527-#529 adopt semantic nodes.
New code should use typed builders instead of the bridge.
## Remote pushdown
The remote compilers target existing GeoServices FeatureServer, OGC API
Features `/items`, WFS 2.0 GetFeature, OData v4 entity-set query, DuckDB SQL over
GeoParquet, and Honua gRPC `FeatureService/QueryFeatures` paths. The compiled
request is included in the plan so diagnostics, CLIs, agents, and renderers can
inspect the same decision before execution.
```ts doc-test=skip reason="partial excerpt requires application host context"
import { PROTOCOL_DEFAULT_CAPABILITIES } from "@honua/sdk-js/contract";
import { executeQueryPlan, explainQuery } from "@honua/sdk-js/query-planner";
const descriptor = {
id: "incidents",
protocol: "geoservices-feature-service",
locator: { url: "https://demo.honua.io", serviceId: "incidents", layerId: 0 },
capabilities: PROTOCOL_DEFAULT_CAPABILITIES["geoservices-feature-service"],
} as const;
const plan = explainQuery({
descriptor,
sourceVersion: "2026-07-10",
authorizationScope: ["data:read"],
query: {
where: "status = 'open'",
aggregation: {
groupBy: ["severity"],
metrics: [{ fn: "count", field: "OBJECTID", alias: "incidents" }],
},
},
});
console.log(plan.fingerprint, plan.steps[0]?.compiled);
// `source` is the matching Source from Dataset.source(...). Version and scope
// are repeated so execution can reject a stale or differently-authorized plan.
const execution = await executeQueryPlan(plan, source, {
sourceVersion: "2026-07-10",
authorizationScope: ["data:read"],
});
console.log(execution.result.aggregateRows);
```
For an OGC API Features source, the same `explainQuery()` call selects
`ogc-api-features-query-v1` from the descriptor protocol. Source-native
`Query.where` is identified as CQL2 text; projection, sorting, pagination,
CRS, and envelope-intersects filters compile to `properties`, `sortby`,
`limit`/`offset`, `crs`, and `bbox` respectively:
```ts doc-test=compile
import { PROTOCOL_DEFAULT_CAPABILITIES } from "@honua/sdk-js/contract";
import { explainQuery } from "@honua/sdk-js/query-planner";
const ogcPlan = explainQuery({
descriptor: {
id: "parcels",
protocol: "ogc-features",
locator: { url: "https://example.test/ogc", collectionId: "parcels" },
capabilities: PROTOCOL_DEFAULT_CAPABILITIES["ogc-features"],
},
query: {
where: "status = 'active'",
outFields: ["parcel_id", "owner"],
orderBy: [{ field: "updated_at", direction: "desc" }],
spatialFilter: {
geometryType: "esriGeometryEnvelope",
geometry: { xmin: -158.4, ymin: 20.5, xmax: -157.6, ymax: 21.8 },
},
pagination: { limit: 100 },
},
});
const firstStep = ogcPlan.steps[0];
if (!firstStep || firstStep.engine !== "remote") throw new Error("Expected a remote query step");
console.log(firstStep.compiled);
// { compiler: "ogc-api-features-query-v1", collectionId: "parcels",
// filter: "status = 'active'", filterLang: "cql2-text", ... }
```
The OGC compiler rejects non-envelope spatial filters, relationships other
than envelope-intersects/intersects, non-EPSG:4326 or malformed bounds,
portable geometry-suppression claims, and claimed remote aggregation. It never
weakens those requests to a broader bbox query.
Aggregation remains available through the explicit bounded-local policy below:
the CQL2 filter and required-field projection stay remote while the exact
aggregate runs only after the materialization ceilings pass.
WFS plans compile the supported SQL-92 subset and spatial predicates to FES
2.0 XML, and expose `propertyName`, `sortBy`, `startIndex`, `count`, and
`srsName`. XML literals and identifiers go through the same escaping compiler
used by the WFS adapter; the portable compiler deliberately retains that
adapter's reviewed `the_geom` default. OData plans translate the canonical predicate to
`$filter`, including supported `geo.intersects` geometry, and expose
`$select`/`$expand`, `$orderby`, `$skip`, and `$top`. Both compilers fail
closed when exact translation is impossible. In particular, metadata-free
planning requires explicit `outFields` before it can prove geometry
suppression, and a descriptor geometry field before it can compile an OData
spatial predicate. OData output-CRS requests, untranslatable SQL predicates,
unterminated literals, contradictory WFS geometry projections, and WFS spatial
filters that would misuse response `outSr` as an input-geometry label also fail
closed. The planner never claims exact execution based on guessed or silently
ignored protocol behavior. A descriptor WFS `srsName` remains transaction
metadata and is not invented as a GetFeature response CRS.
`Query.signal` never enters the IR or fingerprint. Supply cancellation only to
`executeQueryPlan`. Network-protocol source URLs in plan identity are stripped
of credentials, query strings, and fragments. GeoParquet v1 planning rejects a
credential-bearing locator entirely; use the opaque v2 path below. Pass stable
authorization scope identifiers, not tokens.
## Opaque GeoParquet resource identity
`GeoParquetResourceHandleV1` is a versioned JSON value containing only a
resolver namespace, logical resource id, stable non-secret authorization
partition, and optional data revision. `explainQuery()` emits a `2.0` IR/plan
and `duckdb-sql-v2` template when `geoparquetResource` is present. Raw paths,
globs, signed URLs, and expiry remain private to a lifecycle-scoped resolver
registry and are injected only while the accepted plan executes:
```ts doc-test=skip reason="partial excerpt requires application private locator and execution host"
import {
createGeoParquetResourceRegistry,
executeQueryPlan,
explainQuery,
queryPlanCacheKey,
serializeQueryPlan,
} from "@honua/sdk-js/query-planner";
const resources = createGeoParquetResourceRegistry({ resolver: "io.honua.app-assets" });
const handle = resources.register({
id: "parcels:current",
authorizationContextId: "tenant:alpha/role:analyst",
resourceVersion: "snapshot:42",
sources: [privateSignedGeoParquetUrl],
expiresAt: privateSignedGeoParquetExpiryMs,
});
const plan = explainQuery({
descriptor: source.descriptor,
geoparquetResource: handle,
authorizationScope: ["data:read"],
sourceVersion: "source:9",
query: { where: "population > 10", pagination: { limit: 100 } },
});
// Both projections contain only the stable handle and placeholder SQL.
const persistedPlan = serializeQueryPlan(plan);
const cacheKey = queryPlanCacheKey(plan);
// The resolver's private source crosses directly into the GeoParquet adapter.
// Repeat the planning context so execution can reject drift.
const execution = await executeQueryPlan(plan, source, {
authorizationContextId: "tenant:alpha/role:analyst",
geoParquetResourceResolver: resources.resolver,
authorizationScope: ["data:read"],
sourceVersion: "source:9",
signal,
});
// Clears all private locator material when the owning client/session ends.
resources.dispose();
```
Re-registering the same resolver/id/authorization-context/resource-version
atomically rotates private credentials without changing the handle or its
fingerprint. Resolution compares authorization context before invoking the
resolver, enforces exact expiry at `now >= expiresAt`, supports pre-flight and
in-flight cancellation, and copies resolver output through fixed source-count
and UTF-8 size ceilings. Unknown, revoked, closed, or cross-context resources
fail closed. Foreign resolver failures are rebuilt as fixed-message SDK errors;
their messages, causes, contexts, and raw locators are never retained.
GeoParquet adapter failures are likewise rebuilt as
`query.execution.resource-execution-failed`. Cancellation remains an
`HonuaAbortError` before, during, and after resolution.
`resource.id`, `resourceVersion`, and `authorizationContextId` are
identity-bearing. Derive them only from stable, non-secret facts such as dataset
revision, tenant, and role ids—never from a bearer token, API key, signed query
string, or a hash of credential material. Their validators enforce bounded
syntax, not arbitrary secret detection, so callers remain responsible for this
boundary. The registry is ephemeral in-memory isolation, not encrypted storage.
Resolved `sources` cross the redaction boundary and must never be logged,
persisted, fingerprinted, placed in telemetry, or sent over a network API.
`serializeQueryPlan()`, `parseQueryPlan()`, `hashQueryPlan()`, and
`queryPlanCacheKey()` validate the persistence boundary before returning. A v2
cache identity binds the resolver/id, authorization partition, data revision,
scope, schema/source versions, query, and planner policy. Credential rotation
does not change it, and no secret value is hashed. Serialization is synchronous
and performs no resolver, filesystem, network, or DuckDB I/O. Validation does
not treat the public SHA-256 fingerprint as authentication: it reconstructs the
complete canonical v2 plan through the pure planner and compares every field,
including the derived id, IR, steps, compiled template, warnings, and exact
nested keys. A re-signed non-canonical projection is rejected with the same
fixed redacted `invalid-plan` error.
Existing `1.0` GeoParquet IR and `duckdb-sql-v1` artifacts remain supported only
when every locator is credential-free. Query strings, fragments, user-info, and
credential-bearing legacy addresses are rejected before planning, hashing,
parsing, or migration. Upgrade an accepted v1 plan explicitly with
`migrateGeoParquetQueryPlanV1(legacyPlan, handle)`; migration reconstructs a v2
plan and does not copy the old locator. There is no implicit v1 rewrite.
## DuckDB SQL and gRPC compilers
Opaque `geoparquet` sources compile through `compileDuckDbQueryV2` to a
deterministic `duckdb-sql-v2` template over the fixed
`honua-resource://resolve-at-execution` placeholder. The trusted GeoParquet
adapter recompiles the same canonical query against the ephemeral resolved
sources and consumes them without placing them in its metadata/profile cache.
Credential-free legacy sources continue to use `compileDuckDbQuery` and
`duckdb-sql-v1`. Both compilers reuse the same dependency-free, injection-safe
SQL builder. Envelope spatial filters push down as
`ST_Intersects(..., ST_MakeEnvelope(...))` (or a GeoParquet 1.1 `bbox`-covering
column when declared); non-envelope geometries are reduced to their bounding box
and reported with `bboxApproximated: true`. Geometry is projected as GeoJSON via
`ST_AsGeoJSON`. Output-CRS requests fail closed (no portable DuckDB reprojection),
and a spatial filter without a resolvable geometry column is rejected. The
geometry column and encoding come from `locator.geoparquet.geometryColumn` /
`geometryEncoding` (default `wkb`) or a descriptor geometry field, so planning
never needs a profiling round-trip.
`grpc` sources compile to `honua-grpc-query-features-v1`, a faithful,
protobuf-free description of the `honua.v1.FeatureService/QueryFeatures` unary
request. Field names and proto enum *value names* (`SPATIAL_RELATIONSHIP_*`,
`STATISTIC_TYPE_*`) mirror the generated message, so the plan is a hashable
pre-image of the wire request without importing the protobuf runtime.
## Spatial aggregation
Spatial aggregation — grouped statistics constrained by a spatial predicate —
has both a server-pushdown path and a bounded local/columnar path. GeoServices
(`outStatistics` + `groupByFieldsForStatistics` + geometry) and gRPC
(`outStatistics` + `groupBy` + `spatialFilter`) push the whole aggregation to the
server (`pushdown: "full"`); DuckDB pushes it to the columnar engine as a
`GROUP BY` with the spatial predicate in the `WHERE` clause. When a source cannot
push aggregation down at all, the bounded degraded path (below) computes it
locally after enforcing a row/byte ceiling. Histogram and time-series
aggregation remain rejected rather than silently ignored.
## Plan consumers
The plan is consumed, not re-derived. `honua explain [` (CLI) builds a
`SourceDescriptor` from flags and calls `explainQuery`, printing the stages,
pushdown, per-protocol compiled request, fidelity, warnings, and fingerprint —
with no server call, since planning is side-effect free. `executeQueryPlan`
consumes the accepted plan for execution after verifying its fingerprint and
source context.
For GeoParquet, the positional CLI value is an opaque resource id, never a
locator:
```sh
honua explain parcels:current --protocol geoparquet \
--resolver io.honua.app-assets \
--authorization-context tenant:alpha/role:analyst \
--resource-version snapshot:42 --json
```
## Bounded degraded execution
Fallback is disabled by default. When a source can query features but cannot
push down aggregation, local execution requires both `capabilityPolicy:
"degraded"` and an explicit `bounded-local` budget:
```ts doc-test=skip reason="partial excerpt requires application host context"
const plan = explainQuery({
descriptor,
capabilityPolicy: "degraded",
fallback: { mode: "bounded-local", maxRows: 5_000, maxBytes: 8_000_000 },
estimates: { rows: 3_200, bytes: 4_100_000 },
query: {
where: "status = 'open'",
aggregation: { metrics: [{ fn: "count", field: "OBJECTID" }] },
},
});
```
The plan pushes filters and required-field projection to the server. Its
logical input limit is `maxRows`; the compiled wire request exposes the
adapter-owned `maxRows + 1` overflow sentinel. Aggregation runs locally only
after the row and byte ceilings pass. Planning rejects a known over-budget
estimate.
Execution rejects an overflow sentinel or transfer-limit response; it never
silently reports a partial aggregate. `maxRows` is also capped by the SDK at
`MAX_LOCAL_MATERIALIZATION_ROWS`.
## Determinism and plan validity
- `QUERY_IR_VERSION` / `QUERY_PLAN_VERSION` remain `1.0` for compatible plans;
`QUERY_IR_V2_VERSION` / `QUERY_PLAN_V2_VERSION` are `2.0` for opaque
GeoParquet resources.
- Objects serialize with sorted keys; array order remains semantically
significant. SHA-256 fingerprints are identical in browsers, workers, and
Node for the same descriptor, query, policy, versions, scope, and estimates.
- Capabilities, authorization scopes, source/schema versions, CRS/query
fields, and fallback budgets participate in the fingerprint.
- `executeQueryPlan` verifies the complete canonical v2 projection, its
fingerprint, and the current source context. A changed plan, stable source
identity, version, capability set, or scope is rejected; the executor does
not re-plan. Opaque GeoParquet credential rotation is intentionally outside
that identity.
- Feature/query/result caching remains bypassed. Opt-in materialization is a
separate workflow.
## Deliberate first-slice boundaries
This foundation does not close the full planner workstream. The typed semantic
AST, temporal values, canonical identity, and CQL2 JSON interchange are now
available, while the existing v1 compilers remain on their compatibility path.
Follow-on slices must adopt the AST in those compilers, negotiate richer OGC
filter support, and add spatial-binning aggregation (grid/hex),
joins/composition, cache/freshness decisions, cost models, realtime
snapshot/delta plans, receipts, and richer renderer/MCP consumption. Histogram
and time-series aggregation are rejected by the current compiler rather than
silently ignored. The GeoServices, OGC API Features, WFS, OData, credential-free
and opaque DuckDB SQL, gRPC, bounded columnar/worker paths,
spatial-aggregation pushdown, and CLI plan consumer are now delivered.
---
# File: docs/columnar-data-plane.md
# Columnar batch transfer contract
`@honua/sdk-js/query-planner` includes the first bounded data-plane slice for large query
results. It defines a dependency-free Honua batch envelope, an ownership
transfer primitive, and a lazy bounded worker-session protocol. Arrow/GeoArrow adapters may populate its buffers and
metadata, but the envelope does not define a standalone Arrow layout and is not
independently interoperable without the originating adapter's layout contract.
It does not decode Arrow or ship built-in filter/reprojection/aggregation
operators. Applications register those operations in their worker module.
The entrypoint is experimental while the broader planner, streaming, renderer,
and realtime work in issue #394 is completed.
## Create a batch without copying payload bytes
```ts doc-test=skip reason="partial excerpt requires application host context"
import { createColumnarBatch, leaseColumnarBatch } from "@honua/sdk-js/query-planner";
const coordinates = new Float64Array([21.31, -157.86, 21.44, -157.77]);
const batch = createColumnarBatch({
id: "places:0",
sequence: 0,
rowCount: 2,
schema: {
id: "places-schema-v1",
fields: [
{
name: "geometry",
type: { name: "geoarrow.point", parameters: { dimensions: 2 } },
nullable: false,
metadata: { "ARROW:extension:name": "geoarrow.point" },
},
],
metadata: { crs: "EPSG:4326" },
},
buffers: [
{
id: "geometry.values",
field: "geometry",
role: "geometry",
data: coordinates.buffer,
byteOffset: coordinates.byteOffset,
byteLength: coordinates.byteLength,
},
],
});
const lease = leaseColumnarBatch(batch);
const receipt = await lease.transfer((message, transfer) => {
worker.postMessage(message, { transfer: [...transfer] });
});
console.log(receipt.metrics);
// { rows: 2, logicalBytes: 32, backingBytes: 32,
// transferBytes: 32, copiedBytes: 0, ... }
```
Payload bytes are never copied. Schema and metadata descriptors are normalized
and frozen, while batch creation retains each caller-provided `ArrayBuffer`.
Multiple views over one buffer produce one transfer-list entry. Zero-byte
backing buffers are valid; detached buffers are rejected using an attachment
check that distinguishes the two cases.
## Memory ceilings
Creation and transfer default to at most 1,000,000 rows and 64 MiB of unique
backing allocations per batch. Descriptor normalization also defaults to at
most 4,096 total schema fields, 8,192 metadata/type-parameter entries, 16,384
buffer views, and 1 MiB of UTF-8 descriptor identifiers, keys, and string
values. The corresponding `maxRows`, `maxBackingBytes`, `maxSchemaNodes`,
`maxMetadataEntries`, `maxBufferViews`, and `maxStringBytes` limits may be
lowered or explicitly raised; there is no unbounded mode.
Array widths are checked from one captured length before element access, and
metadata keys are accumulated only to the configured bound. Normalization
therefore fails before copying an oversized schema or descriptor list. Empty
views and many views sharing one small backing allocation still count against
`maxBufferViews`; they cannot bypass the CPU/heap ceiling by keeping
`backingBytes` low.
Limits supplied when a lease is created remain its transfer defaults, so a
deliberately raised ceiling is not accidentally replaced by the global default.
A transfer may tighten either ceiling; a pre-handoff limit failure leaves the
lease owned and invokes no target.
`backingBytes` sums `ArrayBuffer.byteLength` for every unique backing allocation.
It does not claim operating-system resident or physical memory usage.
This intentionally rejects a tiny view backed by an unexpectedly large buffer.
`logicalBytes` is the sum of described view lengths and can differ when views
overlap or share memory. `copiedBytes` is always zero for this API.
## Ownership, cancellation, and acknowledgement
A `ColumnarBatchLease` starts in `owned` and can be transferred once. Live
leases reserve their unique backing buffers, so the same batch—or another batch
sharing one buffer—cannot be leased concurrently. Disposal releases the
reservation.
The SDK checks an `AbortSignal`, then performs a structured-clone ownership
transfer itself before invoking the consumer. The original buffers are detached,
the lease becomes `transferred`, and the consumer receives the SDK-owned clone
plus its exact transfer list for an optional subsequent worker/port handoff. The
optional promise returned by the consumer is an acknowledgement and
backpressure boundary.
Cancellation only applies before ownership handoff. If the consumer throws or
acknowledgement fails, the error is `transport-failed`, but the lease stays
`transferred`: the original buffers are already detached and retrying them would
be unsafe. A limit or structured-clone failure before handoff leaves the lease
owned.
`dispose()` is idempotent for an owned or transferred lease and releases the
lease's references. It cannot revoke other references held by the caller.
## Lazy worker execution
`createColumnarWorkerSession()` supplies the lifecycle missing from a raw
`postMessage` call: lazy worker creation, a bounded serial queue, exact request
correlation, monotonic progress, cross-thread cancellation, returned-batch
validation, typed failures, and deterministic teardown. The SDK does not import
or construct a browser or Node worker. The application injects a small
`ColumnarWorkerTransport`, so its worker URL, CSP policy, credentials, module
type, and bundler remain explicit.
```ts doc-test=skip reason="browser Worker URL and worker module are application-owned"
import { createColumnarWorkerSession } from "@honua/sdk-js/query-planner";
const session = createColumnarWorkerSession({
maxPendingRequests: 8,
createWorker: () => {
const worker = new Worker(new URL("./columnar.worker.js", import.meta.url), {
type: "module",
});
return {
postMessage: (message, transfer) => worker.postMessage(message, [...transfer]),
addEventListener: worker.addEventListener.bind(worker),
removeEventListener: worker.removeEventListener.bind(worker),
dispose: () => worker.terminate(),
};
},
});
const result = await session.execute("filter-active", batch, {
signal: abortController.signal,
onProgress: ({ fraction, stage }) => updateProgress(fraction, stage),
});
// result.batch now owns the buffers returned by the worker.
session.dispose();
```
The worker module registers application-owned operations against its transport:
```ts doc-test=skip reason="worker global transport wrapper is application-owned"
import { startColumnarWorkerHost } from "@honua/sdk-js/query-planner";
startColumnarWorkerHost({
transport: wrapDedicatedWorkerGlobal(self),
operations: {
async "filter-active"(input, { signal, reportProgress }) {
signal.throwIfAborted();
reportProgress(0.25, "filter");
const output = await filterActiveRows(input, { signal });
reportProgress(1, "complete");
return output;
},
},
});
```
Only one request is transferred to a session worker at a time. Queued batches
remain owned by the caller until dispatch, and `maxPendingRequests` (16 by
default) includes the active request. There is no unbounded mode. An
acknowledged result is validated against the same batch ceilings before the
next request starts.
Cancellation before dispatch removes the request without transferring its
buffers. Cancellation after dispatch sends the versioned cancel message and
retires the worker transport; queued work resumes on a newly created worker.
This makes cancellation deterministic even when an application operator fails
to observe its `AbortSignal`. Worker operators should still poll the signal so
worker-local resources are released promptly. Late or duplicate messages from
a retired worker cannot settle another request.
The main session and worker host both fail closed on protocol-version drift,
unknown operations, batch/metric disagreement, invalid or decreasing progress,
transport faults, and malformed results. Progress callbacks are observational:
an exception thrown by a callback cannot corrupt ownership or settlement.
Worker factories and hosts snapshot transport methods and batch ceilings before
the first asynchronous boundary; later mutation of the caller-owned options
object cannot redirect transferred buffers. Abort signals are accessed through
a failure-contained listener/read seam, so a throwing foreign signal settles
the request instead of losing it. A closed host transport can prevent a response
from being delivered, but that delivery failure is contained and the host still
releases its active-request slot without an unhandled rejection.
## Typed errors
`HonuaColumnarTransferError.code` is one of:
- `invalid-batch`
- `row-limit-exceeded`
- `memory-limit-exceeded`
- `schema-limit-exceeded`
- `metadata-limit-exceeded`
- `buffer-view-limit-exceeded`
- `string-limit-exceeded`
- `already-leased`
- `aborted`
- `already-transferred`
- `disposed`
- `transport-failed`
`HonuaColumnarWorkerError.code` is one of:
- `invalid-request`
- `invalid-response`
- `unknown-operation`
- `queue-full`
- `aborted`
- `operation-failed`
- `worker-failed`
- `disposed`
## Deliberate remaining scope
This slice does not claim the full #394 workstream. Arrow IPC decoding, built-in
filter/projection/reprojection/aggregation operators, multi-batch streaming
across more than one in-flight worker, planner integration, renderer
consumption, batch cache identity, realtime patches, application-specific CSP
worker URL policy, and bounded conversion back to feature objects remain
separate work.
---
# File: docs/realtime-resume.md
# Resumable realtime delivery
The `@honua/sdk-js/realtime` subpath includes an opt-in, transport-neutral
delivery gate for snapshot-plus-delta streams. It sits between a transport and
the existing realtime reducer/store. The gate does not open or reconnect a
network transport; it decides whether an event is safe to apply and whether a
durable cursor can resume the exact accepted subscription.
```ts doc-test=skip reason="partial excerpt requires application host context"
import {
createRealtimeFeatureStore,
createResumableRealtimeSubscription,
} from "@honua/sdk-js/realtime";
const featureStore = createRealtimeFeatureStore();
const delivery = await createResumableRealtimeSubscription({
context: {
kind: "honua.realtime-resume-context",
version: 1,
sourceId: "incidents",
queryFingerprint: acceptedPlan.fingerprint,
sourceVersion: "incident-snapshot-v7",
schemaVersion: "incident-schema-v3",
authorizationScopeFingerprint: aclFingerprint,
},
checkpointStore: durableCheckpointStore,
apply: (event, signal) => {
if (signal.aborted) return;
featureStore.apply(event);
},
});
transportObserver.next = (event) => {
if (event.type === "snapshot" || event.type === "delta" || event.type === "upsert" || event.type === "delete") {
void delivery.enqueue(event);
}
};
```
## Safety model
A `honua.realtime-checkpoint@1` binds all resume positions to:
- source identity and source version;
- accepted query/plan fingerprint;
- schema version;
- an opaque authorization-scope fingerprint;
- the last contiguous sequence plus available cursor, watermark, timestamp,
and delta-token positions;
- a bounded recent event-id window.
Changing any bound identity produces `resnapshot-required`; the SDK never
silently reuses the cursor. A new subscription without a compatible checkpoint
also requires a replacement snapshot before deltas can apply.
Adapters project a server-expired cursor, an unsupported resume mode, or a
transport-detected gap through `delivery.requireResnapshot(...)`. That method
invalidates queued work and accepts only a replacement snapshot next; it does
not silently restart from the newest delta.
After a baseline exists, only the next contiguous safe-integer sequence can
advance it. Older sequences are reported as duplicates. Missing sequences,
conflicting top-level/nested checkpoint fields, or reuse of a recent event id
at a new sequence stop delivery and require a replacement snapshot. A
replacement snapshot received during ordinary live delivery must advance the
existing sequence; a stale or equal snapshot cannot regress the baseline. A
replacement snapshot may establish a lower sequence only after an explicit
`requireResnapshot(...)` transition, which marks a deliberate new recovery
epoch. Accepted replacement snapshots reset the bounded event-id window.
`enqueue` treats transport input as untrusted at runtime. Only snapshot,
upsert, delete, and delta discriminators reach the consumer. The SDK captures
event identity and resume metadata synchronously, projects only cursor,
watermark, timestamp, sequence, and delta-token fields into the versioned
checkpoint envelope, and drops unrelated fields before application or
persistence. Caller mutation after enqueue therefore cannot change durable
deduplication identity, and credentials or adapter metadata cannot hitchhike
inside a saved checkpoint.
The persisted recent-event-id history defaults to 256 entries and has an
absolute 4,096-entry safety ceiling. Oversized loaded histories are rejected
before their elements are scanned; accepted histories are copied directly from
their configured bounded tail.
This first gate requires a trustworthy monotonic sequence on every snapshot or
delta. Cursor-only and delta-token-only protocols are not silently assigned a
client sequence: their future adapters must obtain an ordering guarantee from
the server or report resume as unsupported and resnapshot.
## Backpressure and cancellation
`maxPendingEvents` bounds the applying event plus queued data events (default
64). Overflow aborts the active delivery, resolves queued work as
`resnapshot-required`, and refuses more deltas. One replacement snapshot may
wait behind an abort-ignoring consumer because it is the only recovery path;
ordinary data remains bounded. Consumers should honor the supplied
`AbortSignal` and make application idempotent, because JavaScript cannot undo a
side effect already performed by a callback that ignores cancellation.
Closing or aborting the gate prevents any later result from advancing its
checkpoint. Consumer errors leave the prior checkpoint unchanged. Checkpoint
save errors are explicit: the in-memory accepted position remains visible with
`checkpointPersisted: false`, phase becomes `error`, and no further events are
accepted by that gate.
Without a `checkpointStore`, accepted checkpoints remain available in memory
but `checkpointPersisted` stays false. Callers may persist them as part of their
own atomic application transaction; the SDK does not claim durability it did
not observe.
Checkpoint persistence occurs after successful consumer application. This is
an at-least-once boundary, not a transaction spanning an arbitrary application
store and checkpoint database. Applications that require atomic exactly-once
effects must persist their materialized state and checkpoint transactionally,
or use event ids/versions to make replay idempotent.
## Scope and remaining work
This is the first production slice of issue
[#393](https://github.com/honua-io/honua-sdk-js/issues/393), not completion of
the full workstream. It does not claim:
- automatic SSE, WebSocket, OData-delta, or protocol-feed reconnection;
- cursor-only protocol adaptation where no trustworthy ordering sequence is
available;
- server support for cursor retention or expiry negotiation;
- renderer, cache, columnar-batch, or offline-store patch integration;
- lag/retry transport telemetry or a shared transport conformance suite.
Adapters should declare unsupported resume behavior before subscription when
the protocol can determine it. Expired server cursors must be projected as an
explicit replacement-snapshot transition rather than fixture fallback or an
unverified continuation.
---
# File: docs/plugin-manifest-certification.md
# Plugin manifest and certification contract
The experimental `@honua/sdk-js/plugin` entrypoint is the first bounded slice
of the plugin SDK tracked by [issue #392](https://github.com/honua-io/honua-sdk-js/issues/392).
It lets a third-party package describe its compatibility and authority boundary
as inert JSON, then produces a deterministic report for a specific host.
The SDK does **not** import plugin code, install packages, or maintain a global
registry. A certified manifest means only that its declaration is well formed
and compatible with the supplied host snapshot. Applications explicitly import
factories and may run them through the application-local lifecycle described
below. A separate, deterministic behavioral-conformance harness then proves a
certified plugin's runtime behavior (retries, performance bounds, and bundle
metadata) against committed golden reports, and a machine-readable
support-status program records each plugin's lifecycle stage.
## Authoring a manifest
```ts doc-test=compile
import {
HONUA_PLUGIN_API_VERSION,
HONUA_PLUGIN_MANIFEST_VERSION,
certifyHonuaPluginManifest,
type HonuaPluginManifest,
} from "@honua/sdk-js/plugin";
const manifest = {
manifestVersion: HONUA_PLUGIN_MANIFEST_VERSION,
id: "com.example.cloud-tiles",
version: "1.0.0",
kind: "protocol",
package: { name: "@example/honua-cloud-tiles", entrypoint: "./plugin.js" },
compatibility: {
pluginApi: HONUA_PLUGIN_API_VERSION,
minimumSdk: "0.1.0-beta.0",
maximumSdkExclusive: "0.2.0",
environments: ["browser", "worker"],
},
capabilities: ["tiles"],
requestedGrants: { networkOrigins: ["https://tiles.example.com"] },
data: {
cache: "memory",
freshness: "ttl",
authentication: "none",
provenance: "preserved",
mutation: "none",
realtime: "none",
},
lifecycle: { initialization: "explicit", disposal: "required" },
support: "community",
} as const satisfies HonuaPluginManifest<"protocol">;
const host = {
pluginApi: HONUA_PLUGIN_API_VERSION,
sdkVersion: "0.1.0-beta.0",
environment: "browser",
grants: { networkOrigins: ["https://tiles.example.com"] },
};
// The trust boundary accepts JSON text, never executable object values.
const report = certifyHonuaPluginManifest(JSON.stringify(manifest), JSON.stringify(host));
```
`JSON.stringify` is appropriate for trusted application-owned literals like
the example above. Do not import an untrusted plugin module and stringify its
exports: serialization itself can run getters or Proxy traps before the SDK is
called. Read third-party manifest bytes as text (for example, from the package
file or an HTTP response) and pass that text directly to the validator.
The report contains no time, machine path, or random identifier, so the same
manifest and host snapshot serialize identically. Its `manifest` and `host`
blocks contain the complete canonical snapshots plus SHA-256 fingerprints;
changing an entrypoint, capability, data semantic, requested authority, peer,
grant, environment, API version, or SDK version changes the corresponding
fingerprint. CI can reject when `status` is `"rejected"` and archive the deeply
frozen JSON as certification evidence without permitting manifest/host swaps.
The top-level `sha256` covers every other report field, including both complete
snapshots, their hashes, status, checks, and diagnostics. It is an integrity
receipt for externally archived evidence, not a signature or proof of issuer.
## Compatibility policy
- `manifestVersion` and `pluginApi` are exact versions. Unknown versions fail
closed; consumers must not guess how to reinterpret them.
- SDK and peer bounds use exact SemVer values rather than arbitrary range
expressions. `minimumSdk` is inclusive and `maximumSdkExclusive` is optional.
- Prerelease ordering follows SemVer, so `0.1.0-beta.2` satisfies a
`0.1.0-beta.0` minimum but does not satisfy `0.1.0`.
- SemVer parsing is linear-time over ASCII input and comparison uses SemVer's
deterministic ASCII identifier order rather than the process locale.
- Environment and peer checks use only the explicit host snapshot. A missing
required peer rejects certification; a missing optional peer is a warning.
- This experimental API is not yet the GA compatibility promise requested by
#392. Promotion requires the remaining runtime and independent-kit phases.
## Security boundary
- Network grants are exact HTTPS origins. Wildcards, paths, embedded
credentials, and non-TLS origins are rejected.
- Credential grants contain scope identifiers only—never tokens, passwords,
headers, environment-variable names, or credential values.
- Persistent data requires scoped storage. Declared mutation requires an
explicit mutation grant. Authenticated access requires credential scopes.
- Capability semantics also fail closed. `protocol:edit` and
`source-format:write` require both `data.mutation: "explicit"` and a mutation
grant; `cache:write` and `cache:invalidate` require persistent-cache semantics
and scoped storage. The full machine-readable mapping is exported as
`HONUA_PLUGIN_CAPABILITY_REQUIRED_GRANTS`.
- Certification fails if the application grant set is weaker than the plugin
request. A report is not itself an enforcement mechanism; the future host
runtime must inject only the certified grants and must never expose ambient
credentials or mutation authority.
- The package entrypoint is repeatedly percent-decoded to a stable form before
validation, normalized in the certified snapshot, and rejected on encoded or
literal traversal, absolute escape, backslashes, query/fragment suffixes, or
malformed/excessive encoding. This API never resolves or executes it.
- Manifest and host inputs must be JSON text. Raw objects—including accessors,
proxies, custom prototypes, symbols, functions, and other executable or
noncloneable values—are rejected from their primitive `typeof` result before
any reflection or user code can run. A side-effect-free lexical pass enforces
text, depth, and node-count bounds before `JSON.parse` materializes values.
Validation and fingerprinting then use only the parser-created, detached,
deeply frozen snapshot; caller objects are never reflected on or returned.
## Extension inventory
The closed `HONUA_PLUGIN_KINDS` and `HONUA_PLUGIN_CAPABILITIES` registries cover
protocol, source/format, renderer, auth, geocoder/routing, analysis, style,
cache, and realtime declarations. Capability names are kind-specific so a
manifest cannot inflate its advertised surface with unknown strings. The
manifest also records support status and cache, freshness, auth, provenance,
mutation, realtime, peer, environment, and disposal semantics for inventory
tools.
## Application-local registry and lifecycle
`HonuaPluginRegistry` turns certified declarations into explicit application
instances without adding plugins to root SDK workflows:
```ts doc-test=compile
import {
HonuaPluginRegistry,
type HonuaPluginFactory,
type HonuaPluginManifest,
} from "@honua/sdk-js/plugin";
declare const manifest: HonuaPluginManifest<"style">;
declare const host: unknown;
const stylePlugin: HonuaPluginFactory<"style"> = {
manifest: JSON.stringify(manifest),
initialize(context) {
return {
extension: { id: context.manifest.id, kind: "style" },
start() {},
stop() {},
dispose() {},
};
},
};
const registry = new HonuaPluginRegistry({
host: JSON.stringify(host),
services: {},
});
await registry.register([stylePlugin]);
const extension = registry.get("style", manifest.id);
await registry.dispose();
```
- A factory carries inert manifest JSON plus an optional exact dependency list.
The registry certifies every declaration, builds a stable id-sorted
topological order, and certifies again immediately before `initialize`.
`context.resolve()` exposes only dependencies named by that factory; it cannot
discover another plugin merely because both share an application registry.
- `initialize`, `start`, `stop`, and `dispose` are typed by all nine extension
kinds. Only `initialize` is universally required; `dispose` is also required
when the certified manifest declares required disposal, while inert plugins
that declare no disposal pay for no empty hooks.
- Factories, callbacks, dependencies, host services, and registration options
are captured before an asynchronous boundary. Registration operations on one
registry are serialized; separate registries have no shared state and may run
concurrently in browsers, workers, Node, or SSR.
- Context exposes only services supplied to that registry and allowed by the
certified declaration. Network calls are origin-restricted, credential
lookups are scope-restricted, and mutation/storage/realtime services appear
only when their matching grants and data semantics permit them. No ambient
credential, environment variable, fetch override, or global cache is used.
- Batch registration is transactional. Cancellation or a hook failure stops
and disposes initialized plugins in reverse order. Cleanup continues after a
cleanup failure; `HonuaPluginRegistryError.cleanupErrors` preserves those
failures separately, so the primary `cause` is not hidden. Cleanup receives a
fresh non-aborted signal even when the registration signal caused rollback.
- `HonuaPluginRegistryError` participates in the common tagged SDK error
envelope while preserving its existing `PLUGIN_*` `.code`, message,
`instanceof`, primary `cause`, and frozen cleanup aggregate. Its grouped
`.sdkCode` is always non-retryable. Serialization reports only a fixed known
reason code plus safe cause classification; manifests, configuration/plugin
identifiers, cause payloads, and cleanup failures stay local. Unknown runtime
codes fail closed as `plugin.internal` / `PLUGIN_UNKNOWN` without coercion.
- `dispose()` is idempotent, returns the same promise to concurrent callers, and
performs reverse stop/dispose exactly once. Duplicate ids prevent implicit
upgrades or state migration; because manifest v1 has no state-migration
declaration, replacement is refused rather than guessed.
- `diagnostics` returns immutable, sequence-stable machine events with codes,
phases, statuses, and certified identities. Thrown error messages are not
copied into diagnostics, preventing accidental credential/PII persistence.
Plugin modules remain external to SDK core. Merely importing the root SDK or an
unrelated subpath does not import a plugin factory, initialize a registry, or
pull mapping/database peers into the bundle.
## Reference plugins for every kind
Every declared kind ships a minimal, external-style reference implementation so
authors can copy a working shape rather than a bare interface. Each is a small
factory carrying inert manifest JSON, a typed extension, and lifecycle hooks;
none of them are imported by SDK core. They live beside the tests at
`test/fixtures/plugins/` (`external-style.ts` plus `reference/`) and are
exercised end to end — certification, registration, extension call, and
disposal — in `test/plugin-reference-samples.test.ts`.
| Kind | Reference plugin | Capabilities | What it demonstrates |
| --- | --- | --- | --- |
| `protocol` | `referenceProtocolPlugin` | `query` | Bounding-box feature count over the origin-restricted network service |
| `source-format` | `referenceSourceFormatPlugin` | `read` | Read-only `lng,lat` text parsing with no requested authority |
| `renderer` | `referenceRendererPlugin` | `2d` | Pure point-to-draw-command translation |
| `auth` | `referenceAuthPlugin` | `authorize` | Header resolution from a scope-restricted credential service |
| `geocoder-routing` | `referenceGeocoderPlugin` | `geocode` | Offline gazetteer lookup |
| `analysis` | `referenceAnalysisPlugin` | `execute`, `cancel` | Cancellable reduction honouring an `AbortSignal` |
| `cache` | `referenceCachePlugin` | `read`, `write`, `invalidate` | Persistent cache over granted scoped storage |
| `realtime` | `referenceRealtimePlugin` | `subscribe` | Push subscription through the realtime service |
| `style` | `externalStylePlugin` | `validate` | The original external-style sample (issue #424/#466) |
Each manifest certifies against one shared host that grants exactly the
authorities the samples request, so the security boundary stays honest: the
`auth` sample only ever receives the `reference.read` scope identifier, the
`cache` sample only writes through scoped storage, and the `protocol` sample's
network calls are restricted to its declared origin.
## Running the certification kit independently
The certification logic is also exposed as a runnable kit so a third party can
validate their own plugin outside this repository. Installing the SDK provides
the `honua-plugin-certify` bin, which reads a manifest and a host snapshot as
inert JSON text, certifies one against the other, prints the deterministic
report to stdout (or `--out`), and resolves an exit code:
```sh
# 0 = certified, 1 = rejected, 2 = usage/input error
npx honua-plugin-certify --manifest ./manifest.json --host ./host.json --pretty
```
The bin never resolves or executes the plugin entrypoint; it only reads the two
JSON documents. The same logic is available programmatically through the
`certifyHonuaPluginManifest` public API shown above, and the CLI core
(`runPluginCertificationCli`) takes an injected I/O boundary so it can run in
any host, not just Node. This makes the harness suitable for a plugin author's
own CI: fail the build when `status` is `"rejected"`, and archive the frozen
report — bound to both canonical snapshots by SHA-256 — as certification
evidence.
## Signed reports and tamper-evident verification
Every certification report carries a top-level `sha256` receipt over all its
other fields (including both canonical snapshots and their fingerprints). The
kit re-checks that receipt so an archived report is verifiably tamper-evident:
```sh
# 0 = verified intact, 1 = tampered, 2 = usage/input error
npx honua-plugin-certify --verify ./report.json
```
The same check is available programmatically as
`verifyHonuaPluginCertificationReport(reportText)`. It reads the report as inert
JSON text and recomputes every stored digest — the top-level receipt plus the
`manifest.sha256` and `host.sha256` snapshot fingerprints — from the report's own
content. Any altered field, swapped snapshot, or edited diagnostic changes a
recomputed digest and yields a structured `REPORT_SIGNATURE_MISMATCH` or
`REPORT_FINGERPRINT_MISMATCH` diagnostic. This is a deterministic in-repo
integrity proof, not a public-key signature: it proves a report is internally
self-consistent and unmodified since issue, not which authority issued it. A
trusted-issuer signing authority (which key, which registry) is a product
decision and is deliberately out of scope here.
## Support-status program
`support` records who backs a plugin (`community`, `partner`, or `honua`). The
optional, machine-readable `supportStatus` attestation records the plugin's
lifecycle stage so applications and agents can act on it without loading code:
- `state` is `supported`, `experimental`, or `deprecated`.
- `since` and `removedIn` are exact SemVer values; `replacement` is a plugin id.
- Certification fails closed when `state` is `deprecated` but the manifest names
neither a `removedIn` version nor a `replacement` id, so a deprecation always
carries an actionable migration path. Support findings are reported under a
dedicated `support` certification check.
The state vocabulary is exported as `HONUA_PLUGIN_SUPPORT_STATES`. The broader
governance program (who may set `partner`/`honua` tiers, security-review sign-off,
naming policy) is a process decision layered on top of this schema.
## Behavioral conformance and golden reports
`runHonuaPluginConformance(spec, host)` runs a certified plugin through a
deterministic behavioral suite and returns a frozen, digest-sealed
`HonuaPluginConformanceReport`. The harness registers the plugin in an
application-local registry with instrumented host services and counts integer
observations only — no wall-clock time, randomness, or host path — so the report
serializes identically on every run and is committed as a golden report
(`test/fixtures/plugins/golden/conformance-report.json`) and asserted
byte-for-byte in `test/plugin-conformance.test.ts`.
Three scenarios are covered, each an observation compared against a declared
bound:
| Scenario | What it proves |
| --- | --- |
| `retries` | The plugin recovers from injected transient host failures within its declared attempt budget (`hostAttempts <= maxAttempts`, `recovered`). |
| `performance` | One operation's host-interaction cost stays within bound and is identical across runs (`serviceCalls <= maxServiceCalls`, `deterministic`). |
| `bundle-metadata` | The declared bundle footprint stays within budget with complete inventory metadata (`minifiedBytes`, `gzipBytes`, `metadataComplete`). |
The report binds the certification digest it was run against, so a conformance
result cannot be replayed against a different manifest or host. The reference
retry-capable plugin exercised end to end lives at
`test/fixtures/plugins/reference/conformance.ts`.
## Remaining governance decisions in #392
The code-buildable core of every named criterion is delivered above. What
remains is process, not code: a trusted third-party signing authority and key
distribution for reports, and the certification governance policy (who grants
`partner`/`honua` support tiers, security-review sign-off, and naming rules).
---
# File: docs/deckgl-adapter.md
# deck.gl binary adapter (experimental)
`@honua/sdk-js/deckgl` is a renderer-neutral boundary between Honua plan/source
identity and deck.gl binary layer data. It is an experimental first slice of
[#388](https://github.com/honua-io/honua-sdk-js/issues/388), not the complete
GPU analytics workstream.
The adapter currently projects scatterplot data from caller-owned typed arrays.
It does not convert to GeoJSON or create one object per feature. Array views are
forwarded to deck.gl unchanged and `projection.metrics.copiedBytes` is always
zero. Default ceilings reject more than 1,000,000 rows, 32 attributes, 64
forwarded properties, or 256 MiB of unique backing allocations before a deck.gl
layer is constructed.
The adapter reads foreign request, data, identity, attribute, and property
descriptors once into a bounded frozen snapshot. Typed-array byte length,
offset, component width, and backing allocation metrics come from JavaScript
intrinsics rather than overridable getters. Attribute offsets and strides must
be component-aligned, and `normalized`, when present, must be boolean.
## Optional peer
deck.gl is not part of the root SDK dependency graph:
```sh
npm install @deck.gl/layers
```
Load the peer lazily or inject the constructor from an existing deck.gl install:
```ts doc-test=compile
import { createDeckGlAdapter, loadDeckGlPeers } from "@honua/sdk-js/deckgl";
const adapter = createDeckGlAdapter({ peers: await loadDeckGlPeers() });
```
Injecting peers is useful for import maps, custom builds, and tests. The lazy
loader never chooses a CDN and reports `HonuaDeckGlAdapterError` with code
`missing-peer` when the package or required export is unavailable.
## Binary projection and picking identity
```ts doc-test=skip reason="partial excerpt requires application host context"
const positions = new Float32Array([157.85, 21.3, 157.86, 21.31]);
const featureIds = new Uint32Array([301, 302]);
const projection = adapter.project({
layer: "scatterplot",
layerId: "incidents",
data: {
length: 2,
attributes: {
getPosition: { value: positions, size: 2 },
},
},
identity: {
sourceId: "incidents-live",
planId: "plan:sha256:…",
sourceVersion: "42",
featureIds,
},
props: { radiusUnits: "meters" },
});
projection.selectionForPick(1);
// { sourceId, planId, sourceVersion, featureId: 302, rowIndex: 1 }
```
`featureIds` is copied once into a private bounded scalar array at projection
construction. Picks never reread caller-owned identity or row-count objects, so
later caller mutation cannot change selection identity. This identity copy is
separate from the binary payload; geometry and attribute buffers remain
zero-copy. A caller can map the pick result into exploration/selection state
without relying on unstable deck.gl object identity.
Mounting uses a small host contract so standalone Deck instances and MapLibre
overlay owners can retain control of their own layer collections:
```ts doc-test=skip reason="partial excerpt requires application host context"
const mounted = projection.mount(host); // host implements addLayer/removeLayer
mounted.dispose();
adapter.dispose(); // also disposes every still-owned mount
```
Successful removal is idempotent. If a host removal throws, the handle stays
owned and reports `dispose-failed`; calling `mounted.dispose()` or
`adapter.dispose()` again retries it. Mount registration happens before the
foreign `addLayer` callback, so synchronous adapter disposal rolls the newly
added layer back before `mount()` returns.
## Diagnostics and boundaries
`DECK_GL_CAPABILITIES` is the capability truth for this contract version.
Scatterplot is `gpu-binary`; feature path/polygon, vector tile, H3, Quadbin,
heatmap, cluster, contour, and trips are explicitly `not-implemented`. The
projection diagnostic reports the chosen strategy, input-array precision,
fidelity, and absence of an implicit fallback. Unsupported or malformed paths
throw a typed error rather than materializing feature objects or silently
downgrading.
Remaining #388 work includes normative Arrow/GeoArrow mappings after the
columnar data-plane contract lands, indexed and aggregate layer families,
realtime buffer patch/rebuild rules, direct MapLibre overlay and standalone Deck
hosts, WebGPU boundaries, and the million-feature browser benchmark against
[#387](https://github.com/honua-io/honua-sdk-js/issues/387).
---
# File: docs/cesium-entity-adapter.md
# Experimental Cesium entity adapter
`@honua/sdk-js/scene-workspace` exposes an experimental accepted-plan workflow
for projecting a canonical `Source` query into a Cesium `EntityCollection`:
```ts doc-test=skip reason="partial excerpt requires application host context"
import { explainQuery } from "@honua/sdk-js/query-planner";
import { mountSourceToCesium } from "@honua/sdk-js/scene-workspace";
const plan = explainQuery({
descriptor: source.descriptor,
query: {
pagination: { limit: 5_000 },
returnGeometry: true,
outSr: 4326,
},
sourceVersion,
schemaVersion,
authorizationScope,
});
const mounted = await mountSourceToCesium(viewer.entities, source, plan, {
sourceVersion,
schemaVersion,
authorizationScope,
time: { startField: "observed_at", endField: "expires_at" },
verticalDatum: "ellipsoidal-wgs84",
});
await mounted.refresh();
mounted.dispose();
```
The core projection (`projectSourceToCesium`) does not import Cesium. Mounting
accepts either a minimal injected Cesium module or async loader; if neither is
provided, the optional `cesium` peer is imported lazily. Importing the
entrypoint in Node/SSR therefore does not initialize a browser or WebGL runtime.
## Supported slice
- One accepted, remote `query` step whose executable query exactly matches the
canonical IR, with explicit geometry and a positive row limit no greater than
`maxEntities` (10,000 by default).
- Explicit WGS84 output (`outSr: 4326`). Coordinates are never silently
reinterpreted or reprojected.
- Point, single-part line, and single-ring polygon geometries in GeoJSON or
common Esri shapes. Finite Z values require an explicit
`verticalDatum: "ellipsoidal-wgs84"`; otherwise the feature is omitted with
a stable fidelity diagnostic.
- Stable entity identity from `featureIdField` or the source primary key.
- Attributes are copied into deeply frozen JSON-like scalar, dense-array, and
plain-object snapshots. Dates, class instances, sparse arrays, accessors, and
other non-JSON values omit the feature with an unsupported-fidelity
diagnostic rather than retaining mutable caller-owned objects.
- Optional offset-bearing ISO instants with at most millisecond precision, or
integer Unix epoch-millisecond start/end attributes, mapped to Cesium
availability intervals. Ambiguous local-time strings and precision-losing
timestamps are omitted with a fidelity diagnostic.
- Serialized snapshot refresh, cancellation checks before renderer mutation,
reentrant-disposal guards, rollback attempts, deterministic cleanup, and
retryable failed disposal.
- Stable diagnostics for transfer-limited results, source degradation, missing
identity, unsupported geometry, invalid time intervals, and snapshot rebuilds.
Unsupported features are omitted with `fidelity: "unsupported"`; they are not
rendered as plausible substitutes. Invalid plans, CRS, limits, and adapter
options fail before mounting.
## Deliberate non-goals for this slice
This is not completion of issue #395 and is not yet a beta Cesium adapter.
Terrain and imagery providers, glTF/models, point clouds and 3D Tiles continue
through the existing scene primitive adapter. Multi-part geometry and polygon
holes, vertical datum transforms, styling, clustering, streaming/tiled
execution, live deltas, camera/selection/filter synchronization, attribution
UI, asset authorization/caching, browser examples, and WebGL leak/performance
certification remain future work.
Refresh currently declares an `entity-snapshot` rebuild boundary. Stable IDs
are preserved across rebuilds, but this first slice does not claim fine-grained
property or sampled-position delta application.
---
# File: docs/offline-regions.md
# Downloadable offline regions (experimental)
`@honua/sdk-js/offline` is the first bounded slice of issue
[#396](https://github.com/honua-io/honua-sdk-js/issues/396). It defines a
versioned manifest and a storage-neutral download coordinator. It does not make
the broader local-first feature complete.
```ts doc-test=skip reason="partial excerpt requires application host context"
import {
createOfflineRegionManifest,
downloadOfflineRegion,
} from "@honua/sdk-js/offline";
const manifest = await createOfflineRegionManifest({
name: "Field area",
sourceId: "incidents",
endpoint: "https://example.test/FeatureServer/0",
authorizationScopeFingerprint: currentAclFingerprint,
bounds: { minX: -158.3, minY: 21.4, maxX: -157.6, maxY: 21.8, crs: "EPSG:4326" },
minZoom: 8,
maxZoom: 14,
sourceVersion: "source-v3",
schemaVersion: "schema-v7",
planVersion: "plan-v2",
observation: { state: "live", observedAt: "2026-07-10T10:00:00Z" },
resources: plannedResources,
});
const receipt = await downloadOfflineRegion(manifest, {
store: applicationStore,
load: applicationResourceLoader,
logicalQuotaBytes: 512 * 1024 * 1024,
signal: abortController.signal,
onProgress: renderProgress,
});
```
## Contract guarantees
- Manifest identity is deterministic over normalized content. Resource ids,
attribution ids, and object keys use locale-independent code-unit ordering.
- URL credentials and recognized signed/auth query parameters are removed.
Only a domain-separated SHA-256 digest of the caller's authorization scope
fingerprint is persisted.
- Every resource has an exact logical byte length and required SHA-256 digest.
Loader output is copied into coordinator-owned memory before hashing, progress,
or writing, so later loader mutation cannot alter committed bytes. Each resource also carries
source, schema, plan, and attribution identities inherited from the manifest
unless the planner supplies more specific versions. Snapshot observation,
validity, expiration, and HTTP validators remain explicit instead of making
cached bytes appear live.
- Untrusted manifests are synchronously normalized into an owned snapshot before
any hash or adapter await. Plain shapes, dense arrays, resource/attribution/
metadata counts, logical bytes, and incremental UTF-8 string bytes are bounded
before copying, sorting, or canonical JSON construction.
- Quota is explicitly **logical payload-byte quota**: it sums declared resource
payload lengths and does not claim physical disk occupancy, unique backing
bytes, compression, deduplication, or store overhead. Admission is deterministic:
expired regions, then least-recently-used regions, then code-unit id order.
Pinned regions are never automatically evicted.
- The injected transaction stages evictions and writes, then publishes them
atomically with the manifest and receipt. Commit compares the inventory
revision used for planning and independently enforces logical quota in the
same atomic mutation; a concurrent winner makes the loser return typed
`inventory-changed` and roll back. Storage implementations must copy bytes
before `write()` resolves and satisfy this CAS/atomicity contract.
- Progress is explicit across planning, download, write, commit, and completion.
A successful receipt reports `integrity: "verified"` and
`quotaAccounting: "logical-payload-bytes"`.
The manifest contains logical resource ids, not request URLs. The injected
loader may resolve short-lived signed URLs or authorization at download time;
those values never cross the persistent-store boundary.
## Non-goals and remaining work
This slice intentionally provides no IndexedDB or service-worker adapter, no
network reachability policy, no resumable partial transaction, no encryption
policy, no storage schema migration, no cached query/read integration, and no
local edit queue or replica conflict replay. Those remain required before issue
#396 can satisfy its GA acceptance criteria. This entrypoint is `@experimental`
and subpath-only so the root and browser bundles do not absorb it.
---
# File: docs/agent-safety.md
# Safe agent plan boundary
`@honua/sdk-js/agent-safety` is a deterministic trust boundary
between untrusted plan proposals and host-owned effect execution. It is part of
the SDK's stable tier (semver-protected; see
[`docs/decisions/agent-surface-stabilization.md`](./decisions/agent-surface-stabilization.md)),
and its security posture — envelope forgery, replay, effect-budget bypass,
receipt tampering, plan-fingerprint mismatch — is documented with per-threat
conformance tests in the
[agent-safety threat model](./agent-safety-threat-model.md). It validates
JSON-compatible structured data, produces an immutable dry run and effect
budget, binds a reviewer signature to the exact plan and policy, revalidates
current source context, executes an exact approved operation through a
host-supplied adapter, and signs/verifies execution evidence.
Plan normalization, dry run, approval, and verification never invoke a model,
tool, source, renderer, subscription, or job. `executeAgentPlanStep` is the
explicit effect boundary; it invokes only a matching host executor after
authorization, single-use consumption, and a durable start audit.
`@honua/sdk-js/agent-tools` remains the runtime action adapter, and
`@honua/sdk-js/query-planner` remains the source of query-plan fingerprints.
## Plan and policy
Every step declares its tool, effect, fields, row and byte ceilings, exact query
plan fingerprint, canonical parameters digest, canonical operation-input digest,
and source binding. The operation digest is recomputed from the visible tool,
effect, source ID, query-plan identity, fields, and parameters digest, so those
claims cannot disagree. The source binding includes schema/source
versions, stable authorization-scope identifiers, data mode, observation time,
attribution, and credential-free citations. The plan also identifies its actor
and can identify the proposing provider/model. Do not put prompts, credentials,
tokens, or tool arguments in this envelope.
```ts doc-test=skip reason="partial excerpt requires application host context"
import {
dryRunAgentPlan,
issueAgentApproval,
executeAgentPlanStep,
} from "@honua/sdk-js/agent-safety";
const policy = {
allowedTools: ["query"],
// Omission is deliberately read-only. Other effects require explicit opt-in.
allowedEffects: ["read"],
sources: {
incidents: {
fields: ["OBJECTID", "status"],
authorizationScope: ["incidents:read"],
schemaVersions: ["schema-7"],
sourceVersions: ["snapshot-9"],
dataModes: ["live"],
maxProvenanceAgeMs: 60_000,
citationOrigins: ["https://data.example.test"],
citationResourcePrefixes: ["/incidents"],
},
},
maxSteps: 1,
maxRows: 500,
maxBytes: 2_000_000,
maxFieldsPerStep: 16,
maxAuthorizationScopesPerSource: 8,
maxCitationsPerSource: 4,
maxOperationParameterBytes: 16_384,
maxOperationParameterNodes: 256,
maxOperationParameterDepth: 8,
};
const dryRun = dryRunAgentPlan(untrustedPlan, policy, {
now: "2026-07-10T20:00:00.000Z",
});
// The signer is supplied by a trusted host. Keep keys out of browser bundles.
const approval = await issueAgentApproval(
dryRun,
policy,
{
id: "approval-42",
approver: "reviewer@example.test",
issuedAt: "2026-07-10T20:00:00.000Z",
expiresAt: "2026-07-10T20:05:00.000Z",
maxRows: 100, // single-step shorthand; may narrow, never widen
},
hostSigner,
{ now: "2026-07-10T20:00:00.000Z", maxClockSkewMs: 5_000 },
);
const execution = await executeAgentPlanStep({
dryRun,
policy,
approval,
approvalVerifier: hostVerifier,
context: { sources: { incidents: currentIncidentSourceBinding } },
stepId: "query-incidents",
operation: exactQueryPlanInput,
useConsumer: hostAtomicApprovalUseStore,
executor: hostQueryExecutor,
auditSink: hostDurableAuditSink,
receiptSigner: hostReceiptSigner,
executionId: "execution-42",
});
// execution.value is an owned frozen JSON snapshot; execution.receipt binds it.
```
`dryRunAgentPlan` rejects unknown properties, accessors, non-plain objects,
invalid or duplicate values, non-canonical timestamps, credentials in citation
URLs, conflicting bindings, unknown tools/sources, operation-input drift,
disallowed effects or fields, scope/version/data-mode drift, stale provenance,
and row/byte overflow.
Its default effect allowlist is only `read`.
Foreign policy data is descriptor-snapshotted and frozen exactly once per
public operation. That same snapshot supplies the signed policy digest,
dry-run revalidation, freshness check, and parameter limits. Reflection errors
are reported as `HonuaAgentSafetyError`; a raw Proxy trap error is not exposed.
Policy ceilings are themselves capped by `AGENT_SAFETY_HARD_LIMITS`. Collection
lengths are rejected before indexed descriptors are read, and operation JSON is
bounded by node count, depth, object/array count, and cumulative UTF-8 bytes.
The same cumulative pre-signature budget applies to dry runs, approval requests,
approval step arrays, current contexts/source maps, execution evidence,
consumption records, and receipts—including both property-key and value bytes.
## Signatures, cancellation, and receipts
Signer/verifier interfaces carry an algorithm and key ID and receive canonical
JSON. Hosts can back them with WebCrypto, KMS, HSM, or a remote same-origin
signing service. Approval and receipt verifiers require the configured
algorithm/key ID to match the envelope. Signatures cover the canonical payload;
SHA-256 envelope digests provide deterministic identity and diagnostics.
All operations accept `AbortSignal`. Cancellation is checked before validation,
across awaited signer/verifier boundaries, and before a successful value is
returned. Aborted work cannot return a usable approval or receipt.
Approval envelopes are explicitly single-use per step. The required
`AgentApprovalUseConsumer` must atomically consume the approval-digest/step key
and return an unforgeable host record containing a nonce, consumption time,
opaque authentication token, and exact approval/step/input binding. The SDK
immediately asks the same store to verify that record. This host-owned replay
store is the concurrency boundary that prevents duplicate mutation, publish,
share, subscription, and job effects.
Signed approvals contain a row/byte allocation for every step. Multi-step
approvals must use `stepLimits` when narrowing; an ambiguous aggregate-only
narrowing is rejected. Step authorization returns a copy with those narrowed
limits, not the wider proposed limits.
After a separately authorized host executor finishes, it can call
`issueAgentExecutionReceipt` with bounded outcome evidence. The helper first
re-verifies approval and current context, refuses evidence beyond approved
row/byte ceilings, and signs the plan, policy, source bindings, approval,
step ID, operation-input digest, atomic-use digest, outcome, result digest, and
completion time. `verifyAgentExecutionReceipt`
repeats those checks deterministically and rejects any modified field. Receipt
issuance and verification require a host consumption verifier; public digests
alone cannot create a receipt. The authenticated consumption record and token
are included under the receipt signature. Receipt
verification evaluates the historical approval at the signed completion time,
so an authentic receipt remains verifiable after approval expiry. Supply the
bound historical source context, not newly discovered current metadata.
## Approved execution and audit
`executeAgentPlanStep` composes the authorization and receipt primitives. It
requires an executor with one exact `tool` and `effect`, rejects mismatches
before consuming the approval, and passes only the frozen operation and narrowed
approval limits. All host data descriptors and bound callbacks are captured once
before the first await; later caller mutation cannot replace the executor,
approval store, verifier, signer, audit sink, clock, or operation. Executor
output must be a bounded plain JSON value. Arrays use one captured length and
objects stop discovery at the width ceiling before reading excess values. The
SDK snapshots data descriptors without invoking accessors, derives the canonical
UTF-8 byte count and SHA-256 digest, and refuses row or byte overflow.
The audit sink must durably append a `started` event before execution and a
`completed` event after every observed success, failure, or cancellation. Audit
events carry pseudonymous digests for plan/actor/provider/model/step/tool/source/
schema/source-version identity, the finite effect and data mode, observation
time, and plan/policy/binding/approval/input/use/result/receipt digests. They
deliberately omit raw free-text identity, parameters, results, citations,
authorization scopes, signatures, consumption nonce/token, and thrown error
messages. A failed start audit prevents execution but deliberately leaves the
already-consumed approval unusable; retry requires a new approval so a host
cannot mistake an uncertain persistence boundary for an unused grant. A failed
terminal audit is a typed `HonuaAgentExecutionError` carrying the signed receipt
so a host can reconcile it without pretending persistence succeeded.
`@honua/mcp-server/agent-execution` provides
`createReadOnlyMcpAgentExecutor`. It binds one named standalone MCP read tool to
this same SDK execution contract; it does not create a parallel approval model
or enable mutation tools. Names must be bounded exact identifiers—wildcards and
patterns are rejected—and every adapter must provide a deterministic
`countRows(result)` callback. An uncounted result cannot silently produce a
zero-row receipt.
The API precondition is JSON-compatible structured data produced by JSON parsing
or an equivalent structured clone. Indexed and object accessors are rejected
without invocation. JavaScript `Proxy` traps are executable by language-level
reflection itself and cannot be made side-effect free by a portable library;
do not pass Proxies across this boundary. When reflection nevertheless fails,
the failure is typed and no partially normalized authority escapes.
Citation URLs must be HTTPS and contain no user info, query, or fragment. Paths
are repeatedly percent-decoded within a fixed bound, Unicode-normalized,
checked for traversal, and canonicalized before identity hashing. Every citation
must match the source policy's exact origin and decoded resource-prefix
allowlist. The SDK does not claim to recognize secrets embedded in arbitrary
opaque path text; hosts prevent those paths by choosing narrow resource IDs or
prefixes.
## Deliberate boundaries
This is one production vertical slice of #397, not completion of that XL
workstream. It does not translate natural language, provide a model adapter,
manage signing keys or audit persistence, parse query predicates to infer field
use, perform compensating actions, or establish CLI/renderer/mutation execution
parity. Hosts must declare every field a step may read or write; query compiler
integration can automate that declaration in a later slice.
---
# File: docs/agent-safety-threat-model.md
# Agent-safety threat model
`@honua/sdk-js/agent-safety` is the deterministic trust boundary between
untrusted agent plan proposals and host-owned effect execution, and
`@honua/sdk-js/agent-tools` is the bounded runtime-action surface those plans
ultimately drive. Both entrypoints are in the SDK's **stable tier** (see
[`docs/decisions/agent-surface-stabilization.md`](./decisions/agent-surface-stabilization.md)),
so the contracts described here are semver-protected: security reviews may cite
this document knowing a breaking change to any envelope or verification step
requires a major version.
This document enumerates the threats the surface is designed to stop, the
mechanism that stops each one, and the conformance test that exercises it.
Companion reference: [`docs/agent-safety.md`](./agent-safety.md) (full API
walkthrough) and [`docs/nl-map-control.md`](./nl-map-control.md) (the NL layer
that consumes this boundary end to end).
## Trust boundaries and assumptions
- **The plan proposer is untrusted.** Plans, operations, approvals, contexts,
evidence, and receipts all enter as `unknown` and are descriptor-snapshotted,
bounded (`AGENT_SAFETY_HARD_LIMITS`), and frozen before any digest or
signature check. Accessors are rejected without invocation; `Proxy` inputs
are out of contract.
- **Key custody and persistence are host-owned.** `AgentEnvelopeSigner` /
`AgentEnvelopeVerifier` and the atomic `AgentApprovalUseConsumer` replay
store are host callbacks. The SDK guarantees *what* is signed and verified,
not *where* keys live.
- **The SDK never invokes an effect on its own.** Dry runs are side-effect
free; `executeAgentPlanStep` is the single explicit effect boundary and only
runs a host executor after authorization, single-use consumption, and a
durable start audit.
## Threats and mitigations
Every threat below is exercised by a committed conformance test; the tests run
in the default `npm test` suite.
### 1. Envelope forgery
**Threat.** An attacker fabricates or alters an approval envelope (or the dry
run it binds to) to authorize a plan no reviewer signed — forging digests,
swapping the approver, or presenting an approval for a different plan/policy.
**Mitigation.** An approval binds `planDigest`, `policyDigest`,
`bindingsDigest`, and `evaluatedAt` to one exact dry run, which is itself
recomputed (`revalidateDryRun`) from the supplied plan and policy — a forged
dry run cannot digest-match. The envelope digest is recomputed over the
canonical unsigned payload, the signer identity (algorithm + key ID) must match
the configured verifier, and the host `verifier.verify` must return exactly
`true`. Any mutated field fails `integrity-failed` or `signature-invalid`
before an effect is possible.
**Tests.**
- `test/agent-safety.test.ts` — "rejects forged dry runs, signature tampering,
expiry, and context drift"
- `test/agent-safety.test.ts` — "binds the exact plan/policy/context and
permits only budget narrowing"
- `test/agent-safety.test.ts` — "cannot issue a receipt from public digests
without host-authenticated consumption evidence"
### 2. Replay (single-use consumption)
**Threat.** A valid, signed approval is presented twice — concurrently or
sequentially — to run the same approved step more than once (duplicate
mutation, publish, job, …).
**Mitigation.** Approvals are `use: "single"` per step. Authorization requires
the host `AgentApprovalUseConsumer` to *atomically* consume the
(approval digest, step ID, input digest) key and return an authenticated
consumption record (nonce, consumption time, opaque token), which the SDK
immediately re-verifies with the same store. A second consumption attempt
returns nothing and fails `policy-denied`; the consumption record is bound into
the receipt so post-hoc verification also proves single use. Expiry
(`expiresAt`, bounded `maxClockSkewMs`) limits the replay window of any stolen
envelope.
**Tests.**
- `test/agent-safety.test.ts` — "binds exact operation input and atomically
consumes each approved step once"
- `test/agent-execution.test.ts` — "does not invoke an effect after
start-audit failure, executor mismatch, or replay"
- `test/agent-safety.test.ts` — "rejects forged dry runs, signature tampering,
expiry, and context drift" (expiry arm)
### 3. Effect-budget bypass
**Threat.** A plan, approval, or execution result exceeds the reviewed
row/byte/step/effect budget — a widened approval, an aggregate that hides a
per-step overrun, an oversized operation payload, or an executor returning more
data than approved.
**Mitigation.** `dryRunAgentPlan` computes a frozen `AgentEffectBudgetV1` and
rejects plans over policy ceilings (which are themselves capped by
`AGENT_SAFETY_HARD_LIMITS`). Approvals may only *narrow* step limits — widening
fails `policy-denied`, multi-step narrowing requires explicit `stepLimits`, and
aggregate `approvedRows`/`approvedBytes` must equal the per-step sums. Operation
parameters are bounded by policy byte/node/depth budgets before the replay
store is touched. Execution evidence and receipts exceeding the approved
per-step rows/bytes are refused before signing and on verification.
**Tests.**
- `test/agent-safety.test.ts` — "binds the exact plan/policy/context and
permits only budget narrowing"
- `test/agent-safety.test.ts` — "requires explicit per-step allocation when
narrowing a multi-step approval"
- `test/agent-safety.test.ts` — "enforces the policy operation-parameter budget
before replay-store consumption"
- `test/agent-safety.test.ts` — "rejects over-budget evidence before receipt
signing and detects receipt tampering" (over-budget arm)
- `test/agent-execution.test.ts` — "bounds wide results before reading excess
values and uses one captured array length"
### 4. Receipt tampering
**Threat.** An execution receipt is altered after signing — rows, result
digest, approval binding, step ID, or use digest — to misrepresent what was
executed or how much data was returned; or a receipt is minted from publicly
visible digests without a real authenticated consumption.
**Mitigation.** `issueAgentExecutionReceipt` signs the canonical union of
evidence and the plan/policy/bindings/approval digests; the receipt digest is
recomputed on verification and the receipt signer identity must match the
verifier. `verifyAgentExecutionReceipt` deterministically repeats every
issuance check (budget, binding, consumption authentication via the host
`verify` callback, approval signature, digest equality) and rejects any
modified field. Receipts are append-only evidence: nothing in the SDK mutates
or re-issues one.
**Tests.**
- `test/agent-safety.test.ts` — "rejects over-budget evidence before receipt
signing and detects receipt tampering"
- `test/agent-safety.test.ts` — "cannot issue a receipt from public digests
without host-authenticated consumption evidence"
- `test/agent-safety-evidence.test.ts` — "fails closed on plan, discovery,
capability, and receipt substitution"
### 5. Plan-fingerprint mismatch
**Threat.** The operation a host is about to execute quietly differs from what
the reviewer approved: same step ID and tool but a substituted query-plan body,
different fields, a different source, or drifted parameters — including a
"fingerprint-consistent" forgery where the attacker recomputes content hashes
over altered content.
**Mitigation.** Every plan step carries `queryPlan.fingerprint` (from
`@honua/sdk-js/query-planner`), a canonical `parametersDigest`, and an
`inputDigest` recomputed from the visible tool/effect/source/query-plan/fields
identity — the claims cannot disagree at parse time. At authorization,
`digestAgentOperationInput` recomputes the digest from the *proposed* operation
and requires exact equality with the approved step's digest before the replay
store is consumed; any divergence (id, fingerprint, fields, parameters) fails
`integrity-failed`. The same binding is revalidated when receipts are issued
and verified. In the NL layer, plan execution additionally re-derives effects
and tool identity from plan content, so recomputed-fingerprint forgeries are
rejected there too.
**Tests.**
- `test/agent-safety.test.ts` — "rejects an operation whose query-plan
fingerprint differs from the approved step"
- `test/agent-safety.test.ts` — "binds exact operation input and atomically
consumes each approved step once" (query-plan id divergence arm)
- `test/nl-map-control.test.ts` — "rejects a fingerprint-consistent plan whose
executed call differs from its declared tool" / "…that launders action
effects as read-only" / "…whose step names an unknown tool"
## Adjacent hardening (context, not primary threats)
- **Context drift** — approvals are re-verified against the *current* source
bindings (schema/source versions, data mode, provenance freshness); drift
fails `context-mismatch` ("rejects forged dry runs, signature tampering,
expiry, and context drift", "rechecks provenance freshness at the execution
clock").
- **Secret exfiltration through tool results** — the `/agent-tools` executor
deeply redacts credential-bearing metadata before results reach a model
(`test/agent-tools.test.ts`), and audit events carry pseudonymous digests
only (`test/agent-execution.test.ts` — "persists only digests for every
free-text plan and source identity").
- **Hostile host-callback shapes** — accessors on executors, stores, and
callbacks are rejected without invocation (`test/agent-execution.test.ts`).
## What this model does not cover
The SDK does not manage signing keys, persist the replay store or audit log,
authenticate approvers, or constrain what a *host-authorized* executor does
with data it legitimately received. Prompt-injection resistance of any LLM
driving the surface is out of scope here; the mitigation posture is that a
compromised model can only propose plans, and every effect still requires a
signed, budgeted, single-use, context-checked approval.
---
# File: docs/nl-map-control.md
# Natural-language map control (`@honua/sdk-js/nl-map-control`)
> **Experimental.** Symbols on this subpath are `@experimental` and may change
> in any minor release before `1.0.0`.
`@honua/sdk-js/nl-map-control` turns a natural-language instruction into a
typed, serializable, inspectable plan over the existing
[`agent-tools`](../src/agent-tools/index.ts) surface, and executes only
reviewed plans. It composes three things the SDK already ships:
- **agent-tools** — the ten bounded JSON-Schema map/runtime tools
(`inspectMap`, `listSources`, `setViewport`, `addLayer`, `setFilter`,
`selectFeature`, `runWidgetQuery`, `explainCapabilityGap`, …),
- **query-planner** — the canonical query IR that data operations are
expressed in, and
- **agent-safety** — signed approval envelopes, effect budgets, and receipts.
There is intentionally **no opaque NL execution path**: per the
[north-star ADR](./decisions/north-star-sdk-application-kernel.md), an agent
prompt must compile to the same typed, inspectable plan that human-authored
code uses.
## Safety model
1. **Plan-first.** `propose(instruction)` returns a serializable
`NlMapPlan` — ordered agent-tool invocations for map operations, with
query-planner IR attached to data operations — and never executes
anything. The plan is content-addressed: `fingerprint` is the SHA-256 of
its canonical JSON, and `execute()` recomputes it, so a plan edited after
review is rejected (`plan-invalid`).
2. **Plans only.** `execute()` is the single execution path and accepts plans
only. Passing raw natural language (or anything that is not a
`honua.nl-map-plan`) throws a typed `plan-required` error.
3. **Policy-gated read auto-execution.** A plan whose every step is `read`
may auto-execute when `policy.autoExecuteReadOnly` allows it (the
default). Set it to `false` to require approval for everything.
4. **Envelopes for effects.** Any plan containing a `viewport` or `mutation`
step requires a signed agent-safety approval envelope. The envelope is a
real `agent-safety` approval: the plan is deterministically projected into
an `AgentPlanV1`, dry-run against a policy, signed by a host-owned signer,
and verified (signature, expiry, policy, bindings, and per-step parameter
digests) before anything runs. An approval issued for one plan does not
verify against another.
5. **Receipts.** Every execution — auto or approved — emits an
`NlMapPlanReceipt` recording the plan identity, mode, per-step tool
status, and outcome. Receipts are content-addressed (`receiptDigest`) and
optionally signed with a host-provided `receiptSigner`.
6. **BYO LLM.** The model transport is a caller-provided callback
`(request: NlCompletionRequest) => Promise`. The SDK
depends on no model-vendor SDK, sends the model only the bounded tool
schemas plus the semantic map context, and persists no NL or model output.
## API walkthrough
```ts doc-test=skip reason="requires a live map host and an LLM callback"
import {
approveNlMapPlan,
createNlMapControl,
nlMapRuntimeBinding,
} from "@honua/sdk-js/nl-map-control";
const nl = createNlMapControl({
// Any HonuaAgentRuntime adapter: a HonuaController, a MapPackage runtime,
// or your own object over a MapLibre map.
tools: { runtime },
// Bring your own model. Any provider, any transport.
llm: async (request) => callMyModel(request),
policy: { autoExecuteReadOnly: true, maxSelfCorrections: 2, actor: "app@example" },
approvalVerifier, // host-owned envelope verifier (agent-safety)
});
// 1. Compile NL into an inspectable plan. Nothing executes here.
const plan = await nl.propose("zoom to downtown and only show open incidents");
console.log(JSON.stringify(plan, null, 2)); // render for human review
// 2. Read-only plans can run directly; effectful plans need an envelope.
if (plan.readOnly) {
const { receipt } = await nl.execute(plan);
} else {
const approval = await approveNlMapPlan({
plan,
actor: "app@example",
approver: "operator@example",
signer, // host-owned envelope signer
bindings: {
map: nlMapRuntimeBinding({ observedAt: new Date().toISOString() }),
incidents: incidentsSourceBinding, // agent-safety source binding
},
issuedAt: new Date().toISOString(),
expiresAt: new Date(Date.now() + 300_000).toISOString(),
});
const { receipt } = await nl.execute(plan, { approval });
console.log(receipt.outcome, receipt.approvalDigest);
}
```
### The completion contract
`NlCompletionRequest` carries everything a provider adapter needs: the
`system` prompt (semantic map context from agent-tools), the user
`messages`, and the tool schemas in provider-neutral MCP-compatible shape
(`{ name, description, inputSchema }`). Adapters map those onto their
provider's native function-calling API and return
`{ toolCalls: [{ name, arguments }] }` — `arguments` may be a parsed object
or a raw JSON string. A provider refusal is returned as `{ refusal }` and
surfaces as a typed `refusal` error.
### Self-correction
When a completion fails validation — an unknown tool, arguments that fail
the JSON schema, an invalid canonical query, or a **capability miss** — the
layer sends a structured retry (`purpose: "self-correct"`) containing the
previous tool calls and typed `issues`. Capability misses embed the full
`explainCapabilityGap` output (supported flag, declared capabilities, and a
suggested action) so the model can re-plan against a source that actually
supports the operation. Retries are bounded (`maxSelfCorrections`, default
2); exhaustion throws `retries-exhausted` carrying the final issues.
### Deterministic replay
`createRecordedNlLlm(exchanges)` replays committed request/response
fixtures in order and fails loudly on drift. The SDK's own test lane
(`test/nl-map-control.test.ts` + `test/fixtures/nl-map-control/`) replays
recorded completions to byte-identical plans, effects, and receipts against
a mock runtime host; the gallery demo (`npm run demo:nl-map-control`) uses
the same fixtures so it is deterministic with no API key.
## The MCP path
The NL layer's tool surface is published through the same agent-tools
exporters used by `@honua/mcp-server`, so the capability works in-app and
over the Model Context Protocol:
```ts doc-test=skip reason="illustrative tool publication snippet"
import {
toNlMapControlMcpToolDefinitions,
toNlMapControlOpenAiToolDefinitions,
} from "@honua/sdk-js/nl-map-control";
// proposeMapPlan + executeMapPlan + the ten agent tools
const mcpTools = toNlMapControlMcpToolDefinitions();
const openAiTools = toNlMapControlOpenAiToolDefinitions();
```
`proposeMapPlan` is a read tool (planning only); `executeMapPlan` is an
action tool that requires opt-in and accepts a serialized plan plus an
optional approval bundle — the same plan-only, envelope-gated contract as
the in-app API. MCP servers hosting these tools keep the identical safety
semantics because they delegate to `createNlMapControl`.
## Errors
All failures are typed `HonuaNlMapControlError`s with a `code`:
| Code | Meaning |
|------|---------|
| `refusal` | The model declined the instruction. Not retried. |
| `retries-exhausted` | No valid plan within the self-correction budget; `issues` carries the typed reasons (including `explainCapabilityGap` output). |
| `plan-required` | `execute()` received something other than a proposed plan (for example raw NL). |
| `plan-invalid` | Plan content does not match its fingerprint, or is structurally invalid. |
| `approval-required` | The plan has `viewport`/`mutation` effects and no envelope was supplied (or read auto-execution is disabled). |
| `approval-invalid` | Envelope verification failed, or the envelope does not bind to this plan. |
| `fixture-mismatch` | A recorded-replay LLM saw a request that drifted from the recording. |
| `invalid-options` | Missing runtime, LLM callback, or source binding. |
Approval issuance itself uses `agent-safety` and throws
`HonuaAgentSafetyError` (for example `policy-denied` when the policy forbids
the plan's effects).
## See also
- [`docs/agent-safety.md`](./agent-safety.md) — the envelope, budget, audit,
and receipt contracts this layer builds on.
- [`docs/agent-safety-threat-model.md`](./agent-safety-threat-model.md) — the
threat model for the (stable) `agent-tools`/`agent-safety` surface underneath
this layer: envelope forgery, replay, effect-budget bypass, receipt
tampering, and plan-fingerprint mismatch, each with its conformance test.
- [`examples/nl-map-control/`](../examples/nl-map-control/) — deterministic
MapLibre demo with a fixture LLM, plan/receipt JSON panes, and an approve
button.
- [North-star ADR](./decisions/north-star-sdk-application-kernel.md) — why NL
must compile to the same plan human code uses.
---
# File: docs/decisions/north-star-sdk-application-kernel.md
# North-star SDK application-kernel contract
Status: **accepted for review** on 2026-07-10 for
[`honua-sdk-js#386`](https://github.com/honua-io/honua-sdk-js/issues/386).
Merging this decision accepts the product boundary and TypeScript direction;
it does not claim that the proposed facade is implemented.
## Decision summary
Honua will be a **universal geospatial application kernel**, not a proprietary
renderer and not an application shell:
> Connect to a spatial source, understand it automatically, explain and execute
> the best available plan, render it through an interchangeable engine, and
> automate the same plan under explicit policy.
The small front-door vocabulary is:
1. `createHonua()` creates one resource owner and policy boundary.
2. `honua.connect(locator)` discovers an endpoint or static asset and returns a
connection.
3. `connection.inspect()` returns schema, effective capabilities, diagnostics,
freshness, and provenance.
4. `connection.explain(query)` returns a serializable execution plan without
reading result data.
5. `connection.query(queryOrPlan)` executes either a query or a previously
inspected plan.
6. `connection.mount(target, options)` projects the same source/plan and linked
state through an optional renderer adapter.
7. Every owned handle has idempotent `dispose()` and `Symbol.asyncDispose`.
`Dataset -> Source -> Query -> Result` remains the protocol-neutral semantic
contract. The kernel is an orchestration facade over it, not a replacement.
Focused subpaths remain the advanced API and escape hatch.
## Product boundary
### The kernel owns
- URL and static-asset classification, endpoint-layout discovery, metadata
caching, and effective capability negotiation.
- Resource ownership, cancellation, diagnostics, provenance, and operation IDs.
- A protocol-neutral query intermediate representation, plan selection, and
execution receipts.
- Renderer adapter lifecycle and renderer-fidelity reporting.
- Policy hooks used by people, applications, and agents to execute the same
plans.
- Stable extension seams for protocol, loader, renderer, auth, cache, realtime,
and analysis plugins.
### The kernel does not own
- Raster/vector/3D rendering engines. MapLibre, deck.gl, and Cesium remain
optional peers maintained by their respective projects.
- Application shells, dashboards, widget libraries, Studio, Operator, or
hosted-product administration. Those remain in `@honua/app-platform` or
product repositories, consistent with the accepted
[1.0 scope split](./scope-split-and-1.0.md).
- An opaque AI execution path. Agent prompts must compile to the same typed,
inspectable plan used by human-authored code.
- Silent protocol emulation. Fallback and fidelity loss are plan steps and
diagnostics; unsupported operations remain typed failures.
- Ownership of sample-gallery prose or a second copy of example code in the
website repository.
## Public TypeScript shape
This section fixes names, ownership, and behavior. Exact helper overloads can
be refined in implementation issues only when they preserve these invariants.
The compile-only contract is committed under
[`test/design/north-star-api/`](../../test/design/north-star-api/).
### Kernel and connection
```ts doc-test=skip reason="partial excerpt requires application host context"
interface HonuaKernel extends AsyncDisposable {
readonly diagnostics: DiagnosticChannel;
connect>(
locator: string | URL | ConnectLocator,
options?: ConnectOptions,
): Promise>;
dispose(): Promise;
}
interface HonuaConnection extends AsyncDisposable {
readonly id: string;
readonly dataset: Dataset;
readonly sourceDescriptors: readonly SourceDescriptor[];
readonly diagnostics: DiagnosticChannel;
inspect(options?: { refresh?: boolean; signal?: AbortSignal }): Promise;
source(id?: SourceId): Source;
explain(
query: Readonly>,
options?: ExplainOptions,
): Promise>;
query(
queryOrPlan: Readonly> | ExecutionPlan,
options?: QueryOptions,
): Promise>;
mount(
target: string | Element | object,
options: MountOptions,
): Promise>;
dispose(): Promise;
}
```
`createHonua({ plugins })` accepts versioned lightweight plugin descriptors.
Static/analytics engines such as GeoParquet/DuckDB are activated by importing a
focused plugin factory and injecting its peer module; `connect()` never reaches
out to a CDN or loads a heavy engine by surprise. The complete plugin lifecycle
and certification metadata are owned by #392.
`connect()` completes baseline discovery before resolving. A layer/collection
URL has one default source. A service/catalog root may discover many; calling
`source()`, `query()`, `explain()`, or `mount()` without a source then throws a
typed ambiguity error listing valid source IDs and recovery guidance. It never
chooses the first collection silently.
`connection.source(id)` returns the existing `Source`. Advanced callers keep
`Source.protocol(kind)` for typed raw protocol access. The legacy
`Source.adapter()` alias is not promoted into the facade.
### Inspection
```ts doc-test=skip reason="partial excerpt requires application host context"
interface ConnectionInspection {
readonly id: string;
readonly endpoint: string;
readonly defaultSourceId?: SourceId;
readonly sources: readonly SourceInspection[];
readonly observation: Observation;
readonly diagnostics: readonly KernelDiagnostic[];
}
interface SourceInspection extends SourceDescriptor {
readonly discovery: "metadata" | "declared" | "inferred" | "unavailable";
readonly title?: string;
readonly geometryType?: string;
readonly crs?: string;
readonly observation: Observation;
}
```
The snapshot is immutable. `inspect()` uses the metadata snapshot established
by `connect()`; `{ refresh: true }` conditionally revalidates it. Discovery
failure is explicit (`discovery: "unavailable"` plus a diagnostic), not
fabricated protocol defaults presented as observed server truth.
### Explain and query
An execution plan includes:
- a stable ID and deterministic fingerprint;
- connection/source identity and the canonical query;
- ordered remote, worker, renderer, or client steps;
- pushdown degree and reason for each step;
- expected requests, rows, and bytes where estimable;
- cache decision and freshness policy;
- required authorization scopes;
- exact/equivalent/approximate/unsupported fidelity;
- provenance, expiry, and structured diagnostics.
`explain()` may refresh metadata under the caller's freshness policy but must
not fetch result rows or mutate application/renderer state. `query(plan)`
executes the reviewed plan only when its fingerprint, source version, policy,
and validity window still match. Otherwise it throws `plan-stale` with a
replacement plan; it does not silently re-plan.
Feature output is additive to today's result contract:
```ts doc-test=skip reason="partial excerpt requires application host context"
type FeatureQueryResult = Result & {
readonly format: "features";
readonly execution: ExecutionReceipt;
};
```
Columnar output is a distinct result so a million-row path does not materialize
JavaScript feature objects:
```ts doc-test=skip reason="partial excerpt requires application host context"
interface ColumnarQueryResult {
readonly format: "columnar";
readonly schema: readonly { name: string; type: string }[];
readonly batches: AsyncIterable>;
readonly execution: ExecutionReceipt;
}
```
Cancellation is end-to-end. Aborting a query stops page fetches, worker work,
and renderer ingestion. A partial stream carries a terminal diagnostic and is
never represented as a complete `Result`.
### Mount and renderer escape hatches
```ts doc-test=skip reason="partial excerpt requires application host context"
type BuiltInRendererKind = "maplibre" | "deckgl" | "cesium";
type RendererKind = BuiltInRendererKind | (string & {});
interface RendererAdapter {
readonly kind: TKind;
readonly environments: readonly ("browser" | "node" | "worker")[];
readonly peer: unknown; // caller-injected module; never a core import
}
interface MountedMap
extends AsyncDisposable {
readonly id: string;
readonly renderer: TKind;
readonly ready: Promise;
readonly diagnostics: DiagnosticChannel;
raw(kind: TKind): TRaw | undefined;
dispose(): Promise;
}
```
The three built-in identifiers are convenience literals, not a closed renderer
vocabulary. A third-party renderer declares a stable namespaced identifier in
its plugin manifest, registers it explicitly, and receives the same typed raw
handle through the generic adapter. Registration rejects unknown or
API-incompatible adapters with structured diagnostics; core never infers a
renderer merely from an arbitrary string. Certification is independent evidence,
not a hard-coded runtime allowlist. The kit in #392 must prove this seam with an
external-style renderer that is not imported by SDK core.
`mount()` resolves after the adapter accepts the source/layer plan. `ready`
resolves after the first usable frame and rejects with render diagnostics.
Automatic styles are deterministic schema/geometry projections and include a
fidelity report; `"auto"` is never a remote generative call.
When passed a selector or element, the SDK owns the renderer it creates. When
passed an existing renderer host, it borrows the host by default and removes
only Honua-owned sources, layers, listeners, workers, and requests. `raw(kind)`
is the renderer-specific escape hatch; renderer methods do not leak into the
kernel contract.
MapLibre adapter construction lives under `@honua/sdk-js/runtime`; deck.gl and
Cesium adapters may use focused subpaths decided by their implementation
issues. Importing `@honua/sdk-js` must not load any renderer, DuckDB, Arrow,
Cesium, or model-provider code.
### Diagnostics, freshness, and provenance
Diagnostics use one channel at kernel, connection, execution, and mounted-map
scope. `snapshot()` supports post-failure inspection; `subscribe()` supports
live developer tooling. Every diagnostic has an ID, stage, severity, operation
ID, optional source ID, remediation, and preserved cause.
```ts doc-test=skip reason="partial excerpt requires application host context"
interface Observation {
readonly state: "live" | "cached" | "replayed" | "pending-local";
readonly observedAt: string;
readonly validAt?: string;
readonly expiresAt?: string;
readonly cursor?: string;
readonly provenance: readonly ProvenanceRecord[];
}
```
- `live`: read from the addressed service during this operation.
- `cached`: served from a cache and accompanied by observation/expiry time.
- `replayed`: deterministic fixture, offline log, or resumed cursor replay.
- `pending-local`: an optimistic local edit not yet accepted upstream.
Signed URLs and credentials never appear in provenance or diagnostics. The
canonical source URL drops query credentials and records a stable asset ID or
origin/path. Receipts distinguish the metadata observation from the result-data
observation so fresh metadata cannot make replayed rows appear live.
### Agent proposal contract
Agent planning stays in `@honua/sdk-js/agent-tools` and requires a caller-owned
model/provider. No provider SDK enters the root bundle.
```ts doc-test=skip reason="partial excerpt requires application host context"
const proposal = await agent.propose(instruction, {
connections: [incidents],
policy: { allow: ["data:read", "map:write"], deny: ["data:write", "publish"] },
});
const preview = await proposal.dryRun();
const grant = preview.allowed ? await host.requestApproval(proposal.approval) : undefined;
if (grant) await proposal.execute({ approval: grant });
```
`dryRun()` performs validation and estimates but no data or renderer mutation.
Every agent-generated execution requires a host-issued, opaque approval grant
bound to the proposal ID and plan fingerprint. A changed plan invalidates the
grant. Read-only is the default policy; data edits and publish are denied unless
independently granted. Every attempt, denial, execution, and compensating action
emits an audit diagnostic and execution receipt.
## Lifecycle and ownership
```mermaid
stateDiagram-v2
[*] --> Kernel: createHonua
Kernel --> Discovering: connect(locator)
Discovering --> Connected: metadata + capability snapshot
Discovering --> Kernel: typed connect failure
Connected --> Connected: inspect / explain
Connected --> Executing: query(query or reviewed plan)
Executing --> Connected: result + receipt
Connected --> Mounted: mount(adapter)
Mounted --> Connected: mountedMap.dispose()
Connected --> Disposed: connection.dispose()
Kernel --> Disposed: kernel.dispose() cascades
Disposed --> [*]
```
Disposal rules:
- Every `dispose()` is idempotent and awaits in-flight cancellation/worker
shutdown.
- `MountedMap.dispose()` disposes its owned renderer session, not the
connection or borrowed host.
- `HonuaConnection.dispose()` aborts its executions and disposes mounted maps,
metadata watches, loader workers, and source-local caches it owns.
- `HonuaKernel.dispose()` disposes all connections and kernel-owned auth/cache
resources. Caller-supplied providers and renderer hosts remain caller-owned.
- Any operation after disposal throws the existing typed `disposed` pattern.
## Dependency direction
```mermaid
flowchart TD
Apps[Applications / @honua/react / app-platform] --> Kernel[Small kernel facade]
Agents[@honua/sdk-js/agent-tools] --> Planner[Query IR + execution planner]
Kernel --> Planner
Kernel --> Contract[Dataset / Source / Query / Result]
Planner --> Contract
Contract --> Protocols[First-party protocol adapters]
Kernel --> RenderContract[Renderer adapter contract]
MapLibre[MapLibre adapter] --> RenderContract
Deck[deck.gl adapter] --> RenderContract
Cesium[Cesium adapter] --> RenderContract
MapLibre -. optional peer .-> MLPeer[maplibre-gl]
Deck -. optional peer .-> DeckPeer[deck.gl / Arrow]
Cesium -. optional peer .-> CesiumPeer[cesium]
Plugins[Certified third-party plugins] --> Contract
Plugins --> RenderContract
```
The arrows are dependencies. Core never points to React, app-platform,
renderer peers, DuckDB, or model providers. A renderer adapter may depend on
the kernel contract; the kernel refers only to the adapter interface.
## Golden workflows
The authoritative compile fixtures are:
1. [`public-url-to-map.ts`](../../test/design/north-star-api/public-url-to-map.ts)
2. [`large-data-linked-analysis.ts`](../../test/design/north-star-api/large-data-linked-analysis.ts)
3. [`safe-agent-proposal.ts`](../../test/design/north-star-api/safe-agent-proposal.ts)
4. [`third-party-renderer.ts`](../../test/design/north-star-api/third-party-renderer.ts)
They are checked by `npm run verify:north-star-api`. They import a design-only
contract under `test/design`; no production API was added for this ADR.
### 1. Public URL to useful map
This is the normative basic workflow: nine application lines including imports,
inspection, first-frame readiness, and cleanup; no key or Honua account.
```ts doc-test=skip reason="normative future API; compile fixture lives under test/design"
import maplibregl from "maplibre-gl";
import { createHonua } from "@honua/sdk-js";
import { maplibreRenderer } from "@honua/sdk-js/runtime";
const honua = createHonua();
const data = await honua.connect(PUBLIC_FEATURE_LAYER_URL);
const info = await data.inspect();
const map = await data.mount("#map", { renderer: maplibreRenderer(maplibregl), style: "auto" });
await map.ready;
await honua.dispose();
```
### 2. Large-data linked analysis
The workflow connects an AWS-hosted GeoParquet/PMTiles asset, requests columnar
or tiled display execution, reuses the stable `ExplorationContext` for map,
table, chart, filter, and selection state, and pushes an aggregate summary to
DuckDB or the remote service. The plan must prove that the display path does
not materialize unbounded feature objects. `deck.gl` and DuckDB are injected
optional peers. A viewport/filter change creates a new fingerprinted plan; it
does not mutate the reviewed plan in place.
### 3. Safe agent-proposed plan
The workflow connects the deployed `demo.honua.io` feature surface, compiles a
prompt into the same execution-plan type, dry-runs it, requests approval, and
executes only with a plan-bound grant. The fixture proves at compile time that
`proposal.execute()` cannot be called without an `ApprovalGrant`.
### DX review
| Workflow | Application lines / introduced concepts | Required configuration | Cleanup contract |
| --- | --- | --- | --- |
| Public URL -> map | 9 lines including imports; kernel, connection, renderer adapter, mounted handle | Public URL only; no account/key | One awaited `honua.dispose()` cascades connection and owned map |
| Large-data linked analysis | 1 connection, 2 reviewed plans, 1 existing exploration context, 2 optional peer factories | AWS asset locator supplied by public config or runner; never an embedded signed URL | Kernel disposes worker/connection/map; caller disposes its exploration context |
| Safe agent proposal | 1 connection, proposal, dry run, approval grant, receipt | Caller-owned model provider and host approval callback | Kernel disposal plus provider lifecycle retained by caller |
Only the first workflow is constrained to ten application lines. Advanced
workflows deliberately expose plans, peers, and ownership rather than hiding
cost or authority behind defaults.
## Deterministic and live validation lanes
Each golden workflow has two evidence lanes; neither substitutes for the other.
| Lane | Trigger | Inputs | Required evidence |
| --- | --- | --- | --- |
| Deterministic | Every pull request, offline | Committed metadata/result/columnar fixtures and a deterministic agent provider | Typecheck, semantic assertions, recorded provenance sidecar, replayed freshness state, disposal/leak assertions |
| Public live | Scheduled and manual | Public AWS assets, public endpoints, and deployed `https://demo.honua.io` | Discovery/query/render or headless-plan success, live provenance/freshness, latency/bytes, explicit endpoint/version report |
| Authenticated live | Scheduled/manual when configured | Private AWS objects or protected demo capabilities | GitHub OIDC or repository secrets supplied only to the runner; explicit executed/credential-unavailable status; no silent green skip |
Credential rules:
- Example source, HTML, browser bundles, fixture metadata, logs, diagnostics,
snapshots, and PR artifacts contain no secret, bearer token, access key,
signed-URL query, or credential-bearing source URL.
- Prefer public read-only AWS sample assets. Authenticated AWS CI uses short-lived
OIDC credentials and resolves private locators inside the runner.
- Browser examples never receive CI secrets through `VITE_*` variables.
- Fixture refresh is a reviewed script that strips credentials, records source
identity/version/retrieval time/license, and produces a deterministic digest.
- A live-lane result reports `executed`, `failed`, or
`credential-unavailable`; missing credentials do not masquerade as execution.
The SDK repository owns executable samples and tests. Each flagship example
will expose a small, versioned catalog manifest containing slug, title,
capabilities, protocols, renderer, fixture command, live command, screenshot,
and provenance metadata. The `honua-site` samples directory/gallery consumes a
generated projection of that manifest and links to or embeds the deployed SDK
artifact. It must not fork `src/`, mock servers, fixtures, or workflow logic.
Changes flow SDK example -> validated catalog artifact -> site projection. The
site may own narrative/marketing copy and ordering, but not a second executable
implementation.
The current sample inventory is evidence, not a preservation constraint. The
program optimizes for compelling task journeys, time to first success, and
clear proof of differentiated capability. During curation, each current sample
receives an explicit **keep, rework, merge, replace, or retire** disposition;
legacy URLs get redirects where appropriate, but weak examples are not retained
solely to avoid change. SDK-side execution is tracked by #398 (modernization
epic), #399 (flagship migrations), #400 (generated learning/docs), and #401
(versioned manifest/artifact/evidence contract). The site-side catalog
projection is [honua-site#120](https://github.com/honua-io/honua-site/issues/120)
and the task-journey experience redesign is
[honua-site#121](https://github.com/honua-io/honua-site/issues/121). This
decision PR intentionally does not edit `honua-site`.
## Environment and distribution compatibility
| Environment | Supported kernel operations | Constraints |
| --- | --- | --- |
| Node 20+ | `connect`, `inspect`, `explain`, `query`, streaming, agent planning | No DOM `mount`; a headless adapter must explicitly advertise Node support. Browser auth stores are unavailable. |
| Browser ESM/bundler | All operations | Renderer/analytics peers are explicit imports; workers obey CSP and are disposable. |
| Web Worker | `connect`, `inspect`, `explain`, `query`, columnar processing | No element/selector mount. `OffscreenCanvas` requires an adapter that advertises worker support. Auth/cache implementations are injected. |
| React 18/19 | Same kernel through `HonuaProvider` and hooks | StrictMode cannot duplicate connections/subscriptions; component unmount disposes only resources it owns. |
| Build-less ESM | Root/browser kernel plus URL-imported focused adapters | Import maps resolve optional peers; no implicit bare import or dynamic CDN dependency. |
The root stays ESM-only, NodeNext-compatible, strict, and side-effect free.
Renderer and analytics adapters declare peers and carry their own bundle
budgets. `createHonua()` and built-in lightweight discovery must preserve the
one-lightweight-runtime-dependency posture established by #357.
## Mapping to the current SDK
| North-star concept | Existing symbol(s) | Disposition |
| --- | --- | --- |
| `createHonua()` | `new HonuaClient()`, `createDataset()` | Add a thin owner/facade after discovery/planner contracts land; retain both low-level APIs. |
| `connect()` | `createDataset`, `SourceLocator.layout`, URL parsers, OGC/STAC landing-page discovery, GeoParquet/PMTiles resolvers | Compose and generalize. Do not add another protocol-specific constructor. Tracked by #391. |
| `HonuaConnection` | `Dataset` plus owned metadata/worker/runtime handles | New orchestration handle; its `dataset` exposes the existing contract. |
| `inspect()` | `SourceDescriptor`, `SourceSchema`, `Capabilities`, `Source.protocol(...).describe()`, metadata cache docs | Normalize into immutable observed metadata; distinguish discovered vs declared defaults. |
| `query()` | `Source.query`, `queryAll`, `stream`, `queryAggregate` | Reuse adapter execution. The facade adds plan/receipt/format; direct `Source` calls remain valid. |
| `explain()` / `ExecutionPlan` | Per-adapter compilers, `DegradedReason`, query-tile diagnostics | New shared planner contract, not a wrapper around log strings. Tracked by #389. |
| Feature results | `Result` | Preserve fields; add mandatory execution receipt only on the facade result. |
| Columnar results | GeoParquet/DuckDB runtime and row mapping | Replace row-object default for large paths with batches; tracked by #394. |
| `mount()` | `loadMapPackage`, `HonuaMapRuntime`, `HonuaMap`, source bridge | Adapt behind renderer contract; do not add renderer methods to `Dataset`/`Source`. |
| MapLibre adapter | `HonuaMapRuntime`, `MaplibreMap`, `loadHonuaFeatureServiceGeoJson` | Productionize automatic source/style projection under #390. |
| deck.gl adapter | No stable production adapter | New optional adapter under #388. |
| Cesium adapter | App-platform scene adapters and compatibility examples | New stable renderer adapter without moving scene-workspace back into core; #395. |
| Linked state | `ExplorationContext`, view controllers, interaction bindings | Reuse unchanged; `mount`/planner accept snapshots and bindings. |
| Diagnostics | typed error hierarchy, `Result.degraded`, runtime diagnostics/events, request/runtime telemetry | Unify through a scoped channel while preserving typed errors. |
| Freshness/provenance | `SourceFreshnessContract`, cache diagnostics, realtime cursors | Keep declared freshness and add observed state/receipts; #393 and #396 implement storage/stream semantics. |
| Agent proposal | `createHonuaAiMapKit`, tool executor, audit events, MCP evals | Reuse tools/audit; add provider-neutral proposal -> plan -> grant flow under #397. |
| Plugin | `resolveSource`, declaration-merging adapter map, auth/GeoParquet/PMTiles injection seams | Converge on one versioned plugin lifecycle and certification contract under #392. |
| Disposal | `HonuaMapRuntime.dispose`, `ExplorationContext.dispose`, `GeoparquetRuntime.dispose`, React cleanup | Compose under kernel ownership; retain leaf disposal methods. |
| React | `HonuaProvider`, `useDataset`, `useQuery`, `HonuaMap` | Evolve bindings to accept kernel/connection; no second cache or planner. |
| Raw escape hatch | `Source.protocol()`, runtime `.map`, protocol clients | Keep `Source.protocol`; replace direct runtime leakage in the facade with typed `MountedMap.raw(kind)`. |
## Name collisions and migration rules
- `HonuaClient` remains the raw transport/service client. It is not renamed to
`HonuaKernel`; `createHonua()` owns one or more clients as discovery requires.
- `HonuaMap` remains a programmatic source/layer model. The mounted resource is
named `MountedMap`, avoiding a third `HonuaMap` class.
- `loadMapPackage()` remains the explicit saved-package path. `mount()` may
compile a transient package internally but must not hide hosted-package fetch
or validation semantics.
- `inspect()` is normalized kernel metadata. Protocol-specific `describe()`,
`getCapabilities()`, and layer-metadata calls remain raw escape hatches.
- `ExecutionPlan` is distinct from app-platform Operator/Studio plans. The
latter may reference an execution-plan fingerprint but do not define SDK
execution semantics.
- React's `` is a component, while `MountedMap` is its owned/borrowed
imperative handle. The component delegates to `mount()`.
- No existing stable symbol is deprecated by this ADR. Low-level contracts are
essential advanced APIs. Deprecation may begin only after the facade has
equivalent conformance and migration documentation.
- Expired app-platform shims remain governed by #385; this design does not use
them or move app-shell concepts back into the stable package.
## Follow-on contract
The implementation issues must reference and preserve this decision:
- #391: `connect`, discovery, inspection, and effective capabilities.
- #389: query IR, plan fingerprint, explain semantics, and receipts.
- #390, #388, #395: MapLibre, deck.gl, and Cesium adapters.
- #394: columnar result and worker semantics.
- #393 and #396: observed freshness, cursors, persistent caches, and pending
local edits.
- #397: agent proposal, policy, approval grant, audit, and provenance.
- #392: plugin lifecycle and certification.
- #385 and #387: stable surface/bundle reconciliation and measurable DX gates.
- #398, #399, #400, and #401: compelling flagship journeys, generated learning
material, and the SDK-owned sample artifact/evidence projection contract.
- [honua-site#120](https://github.com/honua-io/honua-site/issues/120) and
[honua-site#121](https://github.com/honua-io/honua-site/issues/121): consume
the SDK projection and redesign the samples/docs experience without copying
executable implementations.
An implementation may extend a focused option type, but changing ownership,
silent-fallback rules, plan review semantics, observed-state vocabulary, or
dependency direction requires a superseding ADR.
## Consequences
Positive:
- The common path is materially smaller without hiding the existing powerful
contract.
- Protocols, renderers, human-authored queries, and agent actions converge on
one explainable execution model.
- Optional peers and app-platform separation remain enforceable.
- Live AWS/demo evidence and deterministic CI prove different failure classes
while sharing provenance semantics.
Costs:
- `connect()` cannot ship honestly until metadata discovery is consistent.
- Plans and receipts become versioned public contracts requiring conformance
fixtures across every adapter.
- Renderer adapters must expose fidelity differences instead of promising
impossible parity.
- Resource ownership and live-lane evidence add work to every new plugin.
## Acceptance evidence for #386
- Product boundary and API/lifecycle/dependency decisions: this ADR.
- Three compile-tested workflows: `test/design/north-star-api/*.ts`.
- Current-symbol collision and migration inventory: mapping sections above.
- Node/browser/worker/React/build-less and optional-peer rules: compatibility
sections above.
- No production API: the proposed declarations live only under `test/design`.
- Follow-on traceability: issue list above; each implementation ticket receives
a link to this accepted-for-review contract.
---
# File: docs/geocoding-routing-providers.md
# Provider-pluggable geocoding and routing
> Status: **experimental** (`@experimental` JSDoc). Shapes may change in any
> minor release prior to `1.0.0`.
`@honua/sdk-js/geocoding` and `@honua/sdk-js/routing` expose provider-neutral
contracts — `GeocodingProvider` (`geocode` / `reverse` / `suggest`) and
`RoutingProvider` (`route`) — with first-party adapters for open-source
services. No Honua server is required: the Honua facade is just one provider
among several, and the same typed calls work against every adapter.
Three rules apply to every adapter:
1. **Explicit endpoints only.** Every third-party adapter requires a
`baseUrl`. The SDK never bakes in a default third-party endpoint, so you
cannot accidentally send production traffic to a community service whose
usage policy you have not read.
2. **Declared capabilities.** Capability differences are declared on the
provider and enforced by the SDK's existing capability policy: an
unsupported operation throws `HonuaCapabilityNotSupportedError` under the
default `"strict"` policy (or resolves empty under `"degraded"`).
3. **Attribution travels with results.** Every provider exposes
`attribution` / `usagePolicyUrl`, and every result carries a `provenance`
object identifying the provider and repeating those obligations, so hosts
can render attribution wherever results surface.
## Capability matrix
| Provider | geocode | reverse | suggest | route | Notes |
|----------|---------|---------|---------|-------|-------|
| `honua` (facade) | ✓ | ✓ | ✓ | ✓ | Wraps `HonuaGeocodingClient` / the esri-compat route solver |
| `nominatim` | ✓ | ✓ | — (throws typed error) | | No typeahead endpoint; the OSMF policy forbids autocomplete traffic |
| `photon` | ✓ | ✓ | ✓ | | Typeahead-first engine |
| `pelias` | ✓ | ✓ | ✓ (`/v1/autocomplete`) | | Hosted instances usually need an `apiKey` |
| `osrm` | | | | ✓ | OSRM HTTP API v1, GeoJSON geometry |
| `valhalla` | | | | ✓ | 1e-6 encoded polylines, decoded for you |
## Geocoding
### Nominatim (geocode + reverse; no suggest)
```ts doc-test=compile
import { nominatimGeocodingProvider } from "@honua/sdk-js/geocoding";
const geocoder = nominatimGeocodingProvider({
baseUrl: "https://nominatim.example.org", // your instance
userAgent: "my-app/1.0 (ops@example.org)", // REQUIRED by the OSMF policy on the public instance
email: "ops@example.org", // public-instance etiquette
});
const [best] = await geocoder.geocode("530 South King Street, Honolulu", { limit: 1 });
console.log(best.latitude, best.longitude, best.provenance.attribution);
const here = await geocoder.reverse(21.3059, -157.858); // latitude first
```
Capability miss, by declaration rather than by surprise:
```ts doc-test=skip reason="partial excerpt requires application host context"
import { HonuaCapabilityNotSupportedError } from "@honua/sdk-js";
try {
await geocoder.suggest("honol");
} catch (error) {
if (error instanceof HonuaCapabilityNotSupportedError) {
// error.capability === "suggest", error.protocol === "nominatim"
}
}
// Or opt into degraded mode: suggest() resolves to [] instead of throwing.
nominatimGeocodingProvider({ baseUrl, capabilityPolicy: "degraded" });
```
**Usage policy** (public `nominatim.openstreetmap.org`): absolute maximum of
1 request/second, a real `User-Agent` identifying your application, no
autocomplete traffic, and results must credit OpenStreetMap. Read
before pointing at
the public instance; self-host for anything heavier.
**Attribution**: `Data © OpenStreetMap contributors, ODbL 1.0`.
### Photon (geocode + reverse + suggest)
```ts doc-test=compile
import { photonGeocodingProvider } from "@honua/sdk-js/geocoding";
const photon = photonGeocodingProvider({ baseUrl: "https://photon.example.org" });
const hints = await photon.suggest("honol", { limit: 5 }); // typeahead-first engine
```
**Usage policy**: the komoot-operated `photon.komoot.io` is fair-use for
moderate volumes (see ); self-host for production.
**Attribution**: OpenStreetMap (ODbL), as above.
### Pelias (geocode + reverse + suggest via autocomplete)
```ts doc-test=compile
import { peliasGeocodingProvider } from "@honua/sdk-js/geocoding";
const pelias = peliasGeocodingProvider({
baseUrl: "https://pelias.example.org",
apiKey: process.env.PELIAS_API_KEY, // hosted instances (e.g. geocode.earth)
attribution: "© OpenStreetMap contributors; © OpenAddresses; Who's On First",
});
const hints = await pelias.suggest("honol"); // dedicated /v1/autocomplete endpoint
```
**Usage policy / attribution**: depends on the datasets your instance is
built from (OSM, OpenAddresses, Who's On First, geonames, …) and on your
hosting provider's terms. Override `attribution` / `usagePolicyUrl` to match;
see .
### Honua facade
```ts doc-test=compile
import { HonuaGeocodingClient, honuaGeocodingProvider } from "@honua/sdk-js/geocoding";
const client = new HonuaGeocodingClient({ baseUrl: "https://your-honua-server.example" });
const provider = honuaGeocodingProvider(client); // geocode + reverse + suggest
```
Existing `HonuaGeocodingClient` code keeps working unchanged; the provider
wrapper exists so provider-neutral hosts (search UIs, the CLI, agents) can
treat Honua like any other provider.
## Routing
### OSRM
```ts doc-test=compile
import { osrmRoutingProvider } from "@honua/sdk-js/routing";
const router = osrmRoutingProvider({
baseUrl: "https://osrm.example.org",
profile: "driving", // or "foot", "bicycle", … as exposed by your instance
});
const route = await router.route([
{ longitude: -157.858, latitude: 21.306 },
{ longitude: -157.802, latitude: 21.262 },
]);
// route.geometry: [lon, lat][] route.distanceMeters route.durationSeconds route.legs
```
**Usage policy**: the public demo (`router.project-osrm.org`) and the FOSSGIS
instances (`routing.openstreetmap.de`) are for light, non-commercial use — see
. Self-host for production.
**Attribution**: `Route data © OpenStreetMap contributors, ODbL 1.0`.
### Valhalla
```ts doc-test=compile
import { valhallaRoutingProvider } from "@honua/sdk-js/routing";
const router = valhallaRoutingProvider({
baseUrl: "https://valhalla.example.org",
costing: "auto", // or "bicycle", "pedestrian", …
});
```
Leg shapes (Valhalla's 1e-6 encoded polylines) are decoded into plain
`[longitude, latitude]` geometry for you; `decodePolyline` is exported if you
need it directly.
**Usage policy**: the FOSSGIS community instance
(`valhalla1.openstreetmap.de`) expects fair use; self-host for production.
**Attribution**: OpenStreetMap (ODbL), as above.
### Honua facade + esri-compat bridge
```ts doc-test=skip reason="partial excerpt requires application host context"
import {
honuaRoutingProvider,
osrmRoutingProvider,
routingProviderToCompatRouteProvider,
} from "@honua/sdk-js/routing";
import { RouteTaskCompat } from "@honua/sdk-js/esri-compat";
// Wrap the existing Honua facade solver (the esri-compat routeProvider shape)
// in the provider contract:
const honua = honuaRoutingProvider(myExistingRouteSolver);
// Or drive esri-compat widgets (RouteTaskCompat / RouteLayerCompat /
// DirectionsCompat) with any RoutingProvider:
const task = new RouteTaskCompat({
routeProvider: routingProviderToCompatRouteProvider(
osrmRoutingProvider({ baseUrl: "https://osrm.example.org" }),
),
});
```
## CLI
```bash
# Default behavior (Honua locator) is unchanged:
honua geocode "530 South King Street" --limit 3
# Third-party provider — endpoint is always explicit:
honua geocode --provider nominatim --base-url https://nominatim.example.org "530 South King Street"
honua geocode --provider photon --base-url https://photon.example.org "Honolulu" --json
honua geocode --provider pelias --base-url https://pelias.example.org --api-key $PELIAS_KEY "Honolulu"
# Nominatim public-instance policy compliance:
honua geocode --provider nominatim --base-url https://nominatim.openstreetmap.org \
--user-agent "my-app/1.0 (ops@example.org)" "Honolulu Hale"
```
Table output prints the provider's attribution and usage-policy URL after the
results; `--json` emits the normalized results with `provenance` stamped on
each row.
## Caching and rate limits
Suggest paths should debounce in the UI (see the typeahead example in the
`/geocoding` package docs). The SDK deliberately does **not** persist provider
results: most community usage policies (Nominatim in particular) prohibit
systematic caching/bulk harvesting, and provenance is recorded per result so
downstream stores can honor per-provider terms.
## Testing
Unit tests run against recorded fixtures (`test/fixtures/providers/`) and
never touch the network. The live smoke lane
(`test/provider-live-smoke.test.ts`) is opt-in via
`HONUA_PROVIDER_LIVE_SMOKE=1`, sends exactly one request per test, and sets a
real `User-Agent` for Nominatim.
---
# File: docs/protocol-capability-matrix.md
# Protocol × Capability Matrix
Status: generated from [`config/support-manifest.v1.json`](../config/support-manifest.v1.json); do not edit this section by hand.
Native (`✓`) claims mirror the default capability set per protocol; per-source
metadata may narrow them at runtime. Client-fallback (`◐`) claims are explicit
opt-in paths and are not protocol defaults. An absent operation is explicitly
`unsupported`; capability misses throw `HonuaCapabilityNotSupportedError`
rather than returning empty data.
- `✓` supported with native execution
- `◐` supported through an explicit client fallback
- `β` beta
- `◇` experimental
- `†` deprecated
- `F` facade-required
- `—` unsupported
| Capability | gRPC | GS Feature | GS Map | GS Image | GS Geometry | GS GP | OGC Features | OGC Tiles | OGC Maps | OGC Records | STAC | WFS | WMS | WMTS | OData | PMTiles | GeoParquet | MapLibre vector | MapLibre raster | MapLibre GeoJSON |
| --- | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: |
| `query` | ✓ | ✓ | ✓ | ✓ | — | — | ✓ | — | — | ✓ | ✓ | ✓ | ✓ | — | ✓ | — | ◇ | — | — | — |
| `queryAggregate` | ✓ | ✓ | ✓ | — | — | — | ◐ | — | — | — | — | — | — | — | — | — | ◇ | — | — | — |
| `spatialAggregate` | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — |
| `queryExtent` | ✓ | ✓ | ✓ | ✓ | — | — | ◐ | — | — | — | — | ✓ | — | — | — | — | — | — | — | — |
| `queryObjectIds` | ✓ | ✓ | ✓ | ✓ | — | — | ✓ | — | — | ✓ | ✓ | ✓ | — | — | ✓ | — | — | — | — | — |
| `queryRelated` | — | ✓ | ✓ | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — |
| `applyEdits` | ✓ | ✓ | — | — | — | — | ✓ | — | — | — | — | ✓ | — | — | ✓ | — | — | — | — | — |
| `attachments` | — | ✓ | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — |
| `render` | — | — | ✓ | ✓ | — | — | — | ✓ | ✓ | — | — | — | ✓ | ✓ | — | — | — | ✓ | ✓ | — |
| `tiles` | — | ◐ | ✓ | ✓ | — | — | — | ✓ | — | — | — | — | ✓ | ✓ | — | ✓ | — | ✓ | ✓ | — |
| `sql` | — | ✓ | ✓ | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — |
| `stream` | ✓ | ✓ | ✓ | — | — | — | ✓ | — | — | ✓ | ✓ | ✓ | — | — | ✓ | — | ◇ | — | — | — |
| `pbf` | — | ✓ | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — |
| `image` | — | — | — | ✓ | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — |
| `geometry` | — | — | — | — | ✓ | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — |
| `geoprocess` | — | — | — | — | — | ✓ | — | — | — | — | — | — | — | — | — | — | — | — | — | — |
| `processes` | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — |
### Generated claim evidence
Every non-unsupported operation claim maps to executable evidence and a freshness
policy in the manifest.
| Protocol | Operations | Status | Environment | Execution mode | Evidence |
| --- | --- | --- | --- | --- | --- |
| `grpc` | `query`, `queryAggregate`, `queryExtent`, `queryObjectIds`, `applyEdits`, `stream` | `supported` | `honua-facade` | `native` | [fixture: contract-conformance](../test/contract/conformance.test.ts)]
[conformance: grpc-conformance](../test/conformance/feature-service.conformance.ts) |
| `geoservices-feature-service` | `query`, `queryAggregate`, `queryExtent` | `supported` | `protocol-adapter` | `native` | [fixture: contract-conformance](../test/contract/conformance.test.ts) |
| `geoservices-feature-service` | `queryObjectIds`, `queryRelated`, `applyEdits`, `attachments` | `supported` | `protocol-adapter` | `native` | [fixture: geoservices-conformance](../test/contract/geoservices-conformance.test.ts) |
| `geoservices-feature-service` | `sql` | `supported` | `protocol-adapter` | `native` | [fixture: core-client-fixtures](../test/core-client.test.ts) |
| `geoservices-feature-service` | `stream` | `supported` | `protocol-adapter` | `native` | [fixture: contract-conformance](../test/contract/conformance.test.ts)
[fixture: honua-surface-fixtures](../test/honua-surface.test.ts) |
| `geoservices-feature-service` | `pbf` | `supported` | `protocol-adapter` | `native` | [fixture: core-client-fixtures](../test/core-client.test.ts) |
| `geoservices-feature-service` | `tiles` | `supported` | `protocol-adapter` | `client-fallback` | [fixture: query-tiles-fixtures](../test/contract/query-tiles.test.ts) |
| `geoservices-map-service` | `query`, `queryAggregate`, `queryExtent` | `supported` | `protocol-adapter` | `native` | [fixture: contract-conformance](../test/contract/conformance.test.ts)
[fixture: honua-surface-fixtures](../test/honua-surface.test.ts) |
| `geoservices-map-service` | `queryObjectIds`, `queryRelated` | `supported` | `protocol-adapter` | `native` | [fixture: geoservices-conformance](../test/contract/geoservices-conformance.test.ts)
[fixture: honua-surface-fixtures](../test/honua-surface.test.ts) |
| `geoservices-map-service` | `render` | `supported` | `protocol-adapter` | `native` | [fixture: honua-surface-fixtures](../test/honua-surface.test.ts) |
| `geoservices-map-service` | `tiles` | `supported` | `protocol-adapter` | `native` | [fixture: connect-fixtures](../test/connect.test.ts)
[fixture: tile-layer-fixtures](../test/tile-layer-compat.test.ts) |
| `geoservices-map-service` | `sql` | `supported` | `protocol-adapter` | `native` | [fixture: honua-surface-fixtures](../test/honua-surface.test.ts) |
| `geoservices-map-service` | `stream` | `supported` | `protocol-adapter` | `native` | [fixture: contract-conformance](../test/contract/conformance.test.ts)
[fixture: honua-surface-fixtures](../test/honua-surface.test.ts) |
| `geoservices-image-service` | `query`, `queryExtent`, `queryObjectIds` | `supported` | `protocol-adapter` | `native` | [fixture: geoservices-conformance](../test/contract/geoservices-conformance.test.ts) |
| `geoservices-image-service` | `image`, `render` | `supported` | `protocol-adapter` | `native` | [fixture: geoservices-conformance](../test/contract/geoservices-conformance.test.ts) |
| `geoservices-image-service` | `tiles` | `supported` | `protocol-adapter` | `native` | [fixture: geoservices-conformance](../test/contract/geoservices-conformance.test.ts) |
| `geoservices-geometry-service` | `geometry` | `supported` | `protocol-adapter` | `native` | [fixture: geoservices-conformance](../test/contract/geoservices-conformance.test.ts) |
| `geoservices-gp-service` | `geoprocess` | `supported` | `protocol-adapter` | `native` | [fixture: geoservices-conformance](../test/contract/geoservices-conformance.test.ts)
[fixture: geoprocessing-job-run-fixtures](../test/contract/geoprocessing-job-run.test.ts)
[fixture: process-runner-fixtures](../test/process-runner.test.ts) |
| `ogc-features` | `query`, `queryObjectIds`, `applyEdits`, `stream` | `supported` | `protocol-adapter` | `native` | [fixture: ogc-features-fixtures](../test/contract/ogc-features-backend-agnostic.test.ts) |
| `ogc-features` | `queryAggregate`, `queryExtent` | `supported` | `protocol-adapter` | `client-fallback` | [fixture: contract-conformance](../test/contract/conformance.test.ts) |
| `ogc-tiles` | `render`, `tiles` | `supported` | `protocol-adapter` | `native` | [fixture: ogc-tiles-fixtures](../test/connect-ogc-tiles.test.ts) |
| `ogc-maps` | `render` | `supported` | `protocol-adapter` | `native` | [fixture: ogc-maps-fixtures](../test/connect-ogc-maps.test.ts) |
| `ogc-records` | `query`, `queryObjectIds`, `stream` | `supported` | `protocol-adapter` | `native` | [fixture: ogc-records-fixtures](../test/connect-ogc.test.ts) |
| `stac` | `query`, `queryObjectIds`, `stream` | `supported` | `protocol-adapter` | `native` | [fixture: stac-fixtures](../test/contract/stac-backend-agnostic.test.ts) |
| `wfs` | `query`, `queryExtent`, `queryObjectIds`, `applyEdits`, `stream` | `supported` | `protocol-adapter` | `native` | [fixture: wfs-fixtures](../test/contract/wfs-backend-agnostic.test.ts) |
| `wms` | `render`, `tiles`, `query` | `supported` | `protocol-adapter` | `native` | [fixture: wms-fixtures](../test/contract/wms.test.ts) |
| `wmts` | `render`, `tiles` | `supported` | `protocol-adapter` | `native` | [fixture: wmts-fixtures](../test/contract/wmts.test.ts) |
| `odata` | `query`, `queryObjectIds`, `stream`, `applyEdits` | `supported` | `protocol-adapter` | `native` | [fixture: odata-fixtures](../test/contract/odata-backend-agnostic.test.ts) |
| `pmtiles` | `tiles` | `supported` | `client-only` | `native` | [fixture: pmtiles-fixtures](../test/pmtiles-contract.test.ts) |
| `geoparquet` | `query`, `queryAggregate`, `stream` | `experimental` | `client-only` | `native` | [fixture: geoparquet-fixtures](../test/geoparquet-source.test.ts) |
| `maplibre-vector` | `render` | `supported` | `client-only` | `native` | [fixture: maplibre-automatic-source-fixtures](../test/automatic-source-strategy.test.ts)
[integration: maplibre-automatic-browser](../test/playwright/automatic-source-workflow.spec.mjs) |
| `maplibre-vector` | `tiles` | `supported` | `client-only` | `native` | [fixture: maplibre-automatic-source-fixtures](../test/automatic-source-strategy.test.ts) |
| `maplibre-raster` | `render` | `supported` | `client-only` | `native` | [fixture: maplibre-raster-source-fixtures](../test/raster-source-strategy.test.ts)
[integration: maplibre-automatic-browser](../test/playwright/automatic-source-workflow.spec.mjs) |
| `maplibre-raster` | `tiles` | `supported` | `client-only` | `native` | [fixture: maplibre-raster-source-fixtures](../test/raster-source-strategy.test.ts) |
MapLibre-native and PMTiles sources are included in the generated table even
though they do not flow through the `Source.query` path. GeoParquet is the
experimental DuckDB-WASM-backed `Source` from `@honua/sdk-js/geoparquet`;
envelope filters push down to `ST_Intersects`, while non-envelope filters reduce
to their bounding box and are reported through `Result.degraded`.
`maplibre-vector` and `maplibre-raster` are executable descriptor strategies
with release-gated renderer coverage. `maplibre-geojson` remains a reserved
protocol identifier with no positive default capability: the runtime can load
an inline MapLibre `type: "geojson"` source, but the automatic/app descriptor
path does not implement that protocol today.
`spatialAggregate` is an indexed analytics capability rather than the
field-statistics `queryAggregate` path. No protocol receives it by default. A
source may advertise it only after backend metadata confirms an indexed
aggregation implementation for that source.
## Notes by protocol
### gRPC FeatureService
Canonical transport shared across the SDKs and generated from the
`geospatial-grpc` FeatureService definitions. The default capability set is
`query`, `queryAggregate`, `queryExtent`, `queryObjectIds`, `applyEdits`, and
`stream`. In JS, gRPC-Web is selected on `HonuaClient` with
`transport: "grpc-web"`; it is not exposed as a `Source.protocol("grpc")`
adapter handle.
### GeoServices Feature Service
First-class. Aggregations set `outStatistics`, `groupByFieldsForStatistics`,
and `returnGeometry=false` as top-level fields on the translated
`QueryFeaturesRequest` so both the REST serializer and the gRPC-Web
adapter pick them up (they are not stashed in `extraParams`, which the
gRPC path would silently drop). Pagination uses `resultOffset` /
`resultRecordCount`. Streaming wraps
`HonuaFeatureLayer.queryFeaturesStream`; the adapter derives `pageSize`
from `Query.pagination.limit` so `source.stream({ pagination: { limit } })`
yields pages of at most `limit` rows instead of the core helper's
default 2000. `pbf` is supported when the server returns `f=pbf`; the
contract accepts both encodings transparently.
### GeoServices Map Service / Map Layer
Same query semantics as Feature Service for the layers it exposes
(including the root-level aggregation encoding and `pagination.limit`
→ `pageSize` bridge for `stream()`). `render` and `tiles` come from
the service-level export endpoints. `applyEdits` and `attachments` are
not supported because the Map Service endpoint is read-only —
`Source.applyEdits()` and `Source.attachments.*` throw
`HonuaCapabilityNotSupportedError` so a mixed-source app does not
silently drop the edits. `Source.queryObjectIds()` and
`Source.queryRelated()` round-trip through the same canonical envelopes
as the FeatureServer adapter.
### GeoServices Image Service
The Image Service adapter wraps Honua Server's ImageServer endpoints
(see `feature-server-matrix.md`'s sibling
`image-server-matrix.md`). `Source.query()` returns the raster catalog
as canonical features (one row per raster, footprint geometry on each).
`Source.queryAll()` drains pages from the catalog endpoint internally
(`resultOffset` / `resultRecordCount`) and uses a `limit + 1` lookahead
row to stamp `exceededTransferLimit: true` when the cap is hit, mirroring
the FeatureServer / OGC `queryAll` semantics. `Source.queryExtent()` and
`Source.queryObjectIds()` reuse the same catalog endpoint with the
standard GeoServices shaping flags (`returnExtentOnly`, `returnIdsOnly`).
Tile URLs come from
`Source.protocol("geoservices-image-service").tileUrl(level, row, col)`.
`exportImage`, `identify`, `legend` live on the same
`HonuaImageService` typed escape hatch — these protocol-specific
operations are not on the canonical `Source` because their request
shapes (mosaic rule, rendering rule, pixel size, raster function chains)
are ImageServer-specific. The wrapper accepts both `GET` (params on the
query string) and `POST` (form-encoded body) per request; `POST` is the
correct mode when payload size or proxy URL limits would truncate a
`GET` URL. The catalog endpoint does not honor `Query.spatialFilter`,
`Query.orderBy`, or `Query.outFields`, so the adapter rejects those
fields explicitly rather than silently widening the result; use
`Query.where` to constrain the catalog or move to a FeatureServer
source for richer query semantics. `applyEdits`, `attachments`,
`queryRelated`, `queryAggregate`, and `stream` are intentionally absent
from the default capability set; the canonical methods throw
`HonuaCapabilityNotSupportedError` rather than silently no-op.
### GeoServices Geometry Service
Geometry Service is a stateless utility — it does not host features —
so the canonical query family throws on every method. The default
capabilities advertise only `geometry`. Operations
(`buffer`, `simplify`, `project`, `intersect`, `union`, `clip`,
`difference`) live behind
`Source.protocol("geoservices-geometry-service")` on a
`HonuaGeometryService` instance whose request shapes match the routes
in `honua-server/docs/gis/geometry-service-matrix.md`. The wrapper
targets the `EndpointRegistry` prefix
`/rest/services/Utilities/Geometry/GeometryServer/` (the canonical
Esri Utilities path); `POST` requests submit form-encoded bodies (the
default), `GET` keeps params in the query string. Operations the server
does not implement (`autoComplete`, `convexHull`, `cut`, `densify`,
etc.) intentionally have no wrapper.
### GeoServices GP Service
GP Services run async tasks rather than hosting features. The default
capabilities advertise only `geoprocess`; the canonical
query family throws. Task lifecycle — `submitJob`, `jobStatus`,
`cancelJob`, `jobResult` — lives behind
`Source.protocol("geoservices-gp-service")` on a
`HonuaGeoprocessingService` instance. The service id and task name come
from `SourceLocator.serviceId` / `SourceLocator.taskName` so a single
descriptor uniquely identifies a task without leaking task parameters
into the canonical descriptor shape. `createDataset` rejects descriptors
that advertise `geoprocess` without a `locator.taskName` because Honua
Server publishes the lifecycle routes only under
`/rest/services//GPServer//...`.
GeoServices GPServer also participates in the unified process runner:
`client.geoprocessingRunner(serviceId, taskName)` returns a
`HonuaProcessRunner` that submits GP parameters and exposes the same
`IJobRun` lifecycle as OGC API Processes and geospatial-grpc
ProcessService clients.
### Geospatial gRPC Process Service
The open `honua-io/geospatial-grpc` protocol defines
`geospatial.v1.ProcessService` with `ValidatePlan`, `DryRunPlan`,
`ExecutePlan`, `ExecutePlanStream`, `SubmitJob`, `GetJob`,
`GetJobResult`, and `CancelJob`. The JS SDK keeps the adapter structural
because generated process proto lives outside this package today:
`client.geospatialGrpcProcessRunner(processServiceClient)` accepts a
generated Connect client and normalizes `JobState` values onto
`IJobRun`. `JOB_STATE_COMPLETED` maps to `successful`,
`JOB_STATE_FAILED` to `failed`, `JOB_STATE_CANCELLED` to `dismissed`,
and draft/clarification/validated/approval states map to `accepted`.
### OGC API Tiles
Render-only adapter. `tiles` and `render` are the first-party capabilities; the
canonical `Source.query*` family throws `HonuaCapabilityNotSupportedError`
because the conformance class is tile-fetch, not feature-query. The
canonical tile path is `/collections/{id}/tiles/{tms}/{z}/{y}/{x}`;
tile-matrix-set discovery uses `/tileMatrixSets` and `/tileMatrixSets/{id}`.
Styled-tile access (OGC `/styles/{styleId}/tiles/...`) is part of the
standard but is not exposed by honua-server today, so the SDK does not
synthesize that route.
`Source.adapter("ogc-tiles")` returns a `HonuaOgcTileset` bound to
`(collectionId, tileMatrixSetId)` when both are set in the locator; when
only `collectionId` is set, the adapter falls back to the root
`HonuaOgcTiles` handle so callers can discover which tile-matrix-sets
the server advertises before rebinding.
### OGC API Maps
Render-only adapter. `render` is the first-party capability; same
escape-hatch model as Tiles. `Source.adapter("ogc-maps")` returns either
the dataset-level `HonuaOgcMaps` (when `locator.collectionId` is unset)
or a `HonuaOgcCollectionMap` bound to the descriptor's collection +
optional style. The wire path is
`/maps[/collections/{id}][/styles/{styleId}]/map`; bbox / crs / format
flow through query parameters. The `format` field is normalized to the
server's short-name token (`png`, `jpeg`, `jpg`, `tiff`, `tif`) before
it is written to `f=`; media-type aliases (`image/png`, etc.) are
accepted for ergonomics and translated. The public request envelope has
no `filter` field because honua-server's Maps request model has none.
### OGC API Processes
No `Source` adapter — Processes is a job runner, not a queryable source.
`HonuaClient.ogcProcesses().execute(...)` returns the canonical
`IJobRun` (the same interface every other long-running operation in
the SDK speaks). The implementation polls `/jobs/{jobId}` until
terminal, fetches `/jobs/{jobId}/results` on `successful`, and maps
failure terminals onto `JobSnapshot.error`: it prefers
`statusInfo.exception` (OGC Processes Part 1 vocabulary) and falls back
to `statusInfo.message` so honua-server's single-`message` failure text
still surfaces through `HonuaJobFailedError.message`. `cancel()` issues
`DELETE /jobs/{jobId}` and is idempotent only on the documented benign
paths: 404 (job gone) returns the cached status; 409 with problem-details
title `"Cannot dismiss completed job"` triggers a follow-up GET and
returns the authoritative terminal status — but only if the poll confirms
a terminal state, otherwise the original 409 is rethrown. The
non-benign 409 titles `"Dismiss could not be confirmed"` (backend
dismissal unconfirmed) and `"Cancellation not supported"` (backend
lacks dismissal capability) are rethrown verbatim. The canonical
`processes` capability is part of `CAPABILITIES` and is returned by
`negotiateOgcCapabilities("ogc-processes", conformance)`; it is
intentionally absent from `PROTOCOL_DEFAULT_CAPABILITIES` because there
is no `ogc-processes` `Source` protocol.
### OGC API Records
Metadata-catalog adapter. `query`, `queryObjectIds`, and `stream` are
first-party capabilities over `/ogc/records/collections/{catalogId}/items`.
The direct `client.ogcRecords()` surface exposes Records-specific query
parameters (`q`, `type`, `externalIds`, `datetime`, `bbox`, `ids`,
`profile`) and raw response access for HTML/JSON/profile negotiation.
The canonical `Source.query` path maps `Query.where` to CQL2 `filter`
and `spatialFilter` envelopes to Records `bbox`; aggregation, edits,
attachments, related records, and extent-only queries are not advertised.
Records describes metadata about resources and remains separate from STAC
asset search and Honua admin/control-plane metadata APIs.
### STAC API
STAC piggy-backs on OGC API Features for items but adds a
cross-collection `/search` endpoint. The canonical `Source.query` uses
`/search` (GET by default; opt into POST with `usePost: true`). Both
the GET and POST paths serialize `intersects` (as JSON on GET, raw on
POST) and `fields` (as a CSV with `-` prefixes on GET, structured on
POST) so caller-supplied geometry constraints and selections are not
silently dropped. `spatialFilter` translates to STAC `bbox` only —
`intersects` geometry support requires CQL2 and is left to a downstream
extension. Paging follows the server's `rel=next` link: honua-server
emits `?offset=N` on the href and the adapter parses that numeric
offset; non-Honua STAC servers that emit an opaque `?next=…` token
remain supported as a fallback. `Query.pagination.offset` propagates
through to the STAC `offset` parameter on the initial request.
`queryAggregate` and `queryExtent` are not advertised. STAC's
collection-scoping is handled via `locator.collectionId`; the adapter
forwards it as the `collections=[id]` parameter on the wire.
### OGC API Features
`query`, `queryObjectIds`, `applyEdits`, `stream` are first-party.
OGC has no batch edit endpoint, so `applyEdits` fans out to per-item
`createItem` / `replaceItem` / `deleteItem` calls and forwards
`EditEnvelope.signal` to every request — aborting the signal cancels
every operation that has not yet been issued, while operations already
in flight resolve into per-item failures on the returned `EditResult`.
`queryAll()` requests `limit + 1` rows from `itemsAll()` when the caller
caps the result with `Query.pagination.limit` so the adapter can stamp
`exceededTransferLimit: true` when more records exist (mirroring the
GeoServices lookahead-row pattern). `queryAggregate` is degraded —
`Source.query({ aggregation })` aggregates client-side over the returned
page, while `Source.queryAggregate()` drains every page first and then
aggregates. Both stamp a `queryAggregate` `DegradedReason` on the
`Result` so downstream views can flag the number as non-authoritative.
`queryExtent` is also degraded: an unfiltered `queryExtent()` with no
`outSr` returns the collection metadata's `extent.spatial.bbox[0]`
shortcut, while a filtered request (`where` or `spatialFilter`) — or
any request that sets `outSr` — drains `itemsAll()` and computes the
bbox client-side over matching features. The `outSr` carve-out exists
because the metadata bbox is frozen in the collection's native CRS
(typically CRS84) and the OGC `/collections/{id}` endpoint does not
accept a target CRS, so reusing the shortcut when the caller asked for
a different CRS would silently return the wrong coordinates.
`queryExtent` returns `{ extent, count? }` and does not carry a
`degraded` array. Only `spatialFilter.geometryType =
"esriGeometryEnvelope"` is translated (to the OGC `bbox` query param);
other geometry types would require CQL2, which the adapter does not
yet emit, so they throw rather than silently drop the constraint.
Likewise, only `spatialRel` values of `esriSpatialRelIntersects` or
`esriSpatialRelEnvelopeIntersects` are accepted — the OGC `bbox`
parameter is defined as an envelope-intersects predicate (OGC
17-069r4 §7.15.3), so `contains`/`within`/`crosses`/etc. throw rather
than silently widen to bbox semantics. `queryRelated`, `attachments`,
and `pbf` are out-of-scope for the OGC standard.
### WFS
First-party WFS 2.0 adapter (`wfsSource`); see [`wfs.md`](./wfs.md) for
the full reference. `query` / `queryAll` / `stream`
all route through `GetFeature` after a one-time `GetCapabilities`
negotiation; `Query.where` compiles to FES 2.0 (comparison, `IN`,
`BETWEEN`, `LIKE`, `IS NULL`, boolean combinators, parenthesization), and
`Query.spatialFilter` becomes either a KVP `bbox=` for envelope-only
requests or a `` for everything else (envelope, point,
polygon, polyline). Filters longer than ~7 KB switch to POST GetFeature
with the `` body. Anything richer than the supported subset
(subqueries, function calls, vendor extensions, curves / surfaces)
throws `HonuaCapabilityNotSupportedError("query")` rather than ship a
silent partial filter — callers reach the wire through
`Source.protocol("wfs")`.
WFS `propertyName=` drops every property the caller does not list,
including the geometry column, so `Query.outFields` and
`Query.returnGeometry` are resolved together: an `outFields` list with
`returnGeometry !== false` appends the geometry property
(`the_geom` by default) before the projection lands on the wire so
geometry survives; `returnGeometry === false` paired with an
`outFields` list emits exactly the requested fields (no geometry); a
`returnGeometry === false` request without an `outFields` list throws
`HonuaCapabilityNotSupportedError("query")` because WFS cannot
suppress geometry without enumerating non-geometry properties.
`queryExtent` prefers the per-feature-type `WGS84BoundingBox` from
`GetCapabilities` for unfiltered requests so no extra HTTP traffic is
issued; filtered or `outSr`-bearing requests drain every matching
page (2000 features per page) and compute the bbox client-side,
ignoring caller pagination, `Query.outFields`, and
`Query.returnGeometry` so geometry is preserved on every drained
page and the returned extent covers the full matching set.
`queryObjectIds` has no interoperable server-side ids-only mode, so the
adapter drains the matching set in 2000-feature pages and projects each
GeoJSON `id`. The drain strips `Query.outFields` and
`Query.returnGeometry` (the GeoJSON `id` is read from each feature's
top-level field, so neither knob affects the result) so the request
cannot push the geometry property onto the wire and a caller-supplied
`returnGeometry: false` cannot trip the field-projection guard.
`Query.pagination.limit` caps the global id count (callers can stop
the drain without learning the server's page size) and
`Query.pagination.offset` chooses where the drain starts.
`pagination.limit === 0` is treated as an explicit zero cap across
`query`, `stream`, and `queryObjectIds` (each short-circuits before
the wire call); `queryAll` still issues a single 1-row lookahead so
`exceededTransferLimit` can flip when more records exist — matching
the `withPagingBounds` / `applyQueryAllLimit` semantics shared with
GeoServices and OGC Features.
Content negotiation prefers `application/geo+json` /
`application/json` when the server's `OperationsMetadata`
advertises it; if only GML is offered the canonical `query()` throws
and callers reach the GML payload through `Source.protocol("wfs")`. GML
decoding is intentionally out of scope. `applyEdits` builds a single
`` POST body (`` / `` /
``) and surfaces the per-handle `InsertResults` IDs onto
`EditOutcome.id`. Each `` is stamped with a stable
`handle="add-N"` (1-based, matching `envelope.adds` order) and the
returned `` buckets are indexed by that
handle, so reordered or omitted (`releaseAction="SOME"` partial
failure) buckets never misassign IDs to the wrong `envelope.adds[i]`;
inserts whose handle is missing from the response surface as
`{ success: false }`. The handle attribute is informational in WFS
2.0, so when no `` carries one the adapter falls back
to the legacy positional pairing rather than dropping every id.
`rollbackOnFailure` drives the transaction `releaseAction` (`ALL` vs
`SOME`). Updates whose `id` is `undefined` / `null` are filtered out
before the transaction body is built and surface as per-item failures
(`{ success: false, error: { code: 400, description: "update.id is
required" } }`) so an unaddressed `` can never reach the
server; if every operation in the envelope is absent or malformed the
wire round-trip is skipped.
Stored-query discovery (`ListStoredQueries`) and execution
(`GetFeature?storedquery_id=...`) are reachable through
`Source.protocol("wfs")!.root.storedQuery(id).execute({ parameters })`.
Stored queries that advertise only GML (e.g. Honua Server's
`urn:ogc:def:query:OGC-WFS::GetFeatureById`) cannot be projected onto
the canonical `Source.query()` envelope; the canonical surface throws
`HonuaCapabilityNotSupportedError("query")` and points the caller at
the protocol escape hatch.
Locking (`LockFeature` / `GetFeatureWithLock`) is not exposed in the
canonical surface; callers that need it reach the wire through
`Source.protocol("wfs")`.
The capabilities XML walker refuses any document declaring
`` or `` to defend against XXE-class attacks.
WFS `Result.totalCount` populates from the `numberMatched` GeoJSON
field; `exceededTransferLimit` is set when `numberMatched >
features.length`.
### WMS
First-party WMS 1.3.0 adapter. `render` and `tiles` come from `GetMap`;
`query` is supported through `GetFeatureInfo` with a point spatial
filter (`Query.spatialFilter.geometryType === "esriGeometryPoint"`). The
adapter constructs a 1×1 render envelope around the requested point,
asks for `INFO_FORMAT=application/json`, and decodes the JSON response
into the canonical `Result` envelope. The wire CRS is derived from
the spatial filter geometry's `spatialReference` (`latestWkid` first,
then `wkid`, then `wkt`) and falls back to `CRS:84` (the WMS 1.3.0
longitude/latitude code that preserves the canonical `(x, y)` axis
order). `Query.outSr` is intentionally not consulted on this path
because it is the **output** spatial reference, not the input CRS for
GetFeatureInfo. Non-point queries throw
`HonuaCapabilityNotSupportedError("query", "wms", id)` because WMS has
no spatial-rel semantics for envelopes / polygons; raw multi-pixel
GetFeatureInfo lives behind `Source.protocol("wms").featureInfo()`.
Other canonical `Query` fields that GetFeatureInfo cannot honor are
rejected up front rather than silently dropped: `query({ aggregation })`
throws `HonuaCapabilityNotSupportedError("queryAggregate", ...)`, and
`Query.where` / `Query.outFields` / `Query.orderBy` /
`Query.pagination.offset` / `Query.returnGeometry === false` /
`Query.outSr` throw typed `Error` messages so a mixed-source caller
cannot get an unfiltered, reprojected, or differently-shaped result.
`Query.outSr` fails fast because honua-server's WMS GetFeatureInfo
projects the response in the request CRS itself and exposes no
separate output-SR knob — callers that need a specific projection
must stamp the spatial filter geometry's `spatialReference` with the
desired CRS (the wire CRS is derived from there) or reproject the
result client-side. `Query.pagination.limit` is honored — it maps to
`FEATURE_COUNT` on the wire.
`Source.protocol("wms-layer")` is registered only when
`locator.typeName` parses to a single non-empty layer token because
`HonuaWmsLayer` is a single-layer handle (its `describe()` resolves
exactly one `` from the parsed Capabilities). Multi-layer
composites (`typeName: "a,b"`) keep `Source.protocol("wms-layer")`
unset and route through the service-level `Source.protocol("wms")`
handle, which can target the composite verbatim via
`featureInfo()` / `map()`.
Styled-map selection enumerates per-layer styles from
`HonuaWms.capabilities()` and is bound on the layer handle (`layer.map`,
`layer.featureInfo`) via the `style` parameter or descriptor
`locator.styleId`. Dimension handling (`TIME` / `ELEVATION`) flows
through the typed `WmsMapRequest` envelope; defaults come from
`Capabilities`, request overrides go on the wire. honua-server does not
implement `GetLegendGraphic` today, so the adapter raises
`HonuaCapabilityNotSupportedError` from `legend()` when the parsed
Capabilities advertise no `` request element. The
gating always runs: when the caller does not pre-supply
`options.capabilities`, the handle lazy-loads them once via
`getWmsCapabilities` and caches the in-flight promise on the instance
so repeat `legend()` calls reuse the same fetch (transient failures
clear the cache so the next call retries). The `HonuaWms` parser
extracts each ``'s own ``, ``, ``,
``, `