# @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/)
> One geospatial client for GeoServices, OGC APIs, WMS/WMTS/WFS, STAC, and OData —
> with first-class TypeScript, a MapLibre runtime, and a drop-in ArcGIS migration path.
**Release status: beta.** The 20-entrypoint stable tier is frozen and guarded by an
API-surface gate; remaining pre-1.0 work is hardening, not surface change. See
[`docs/decisions/scope-split-and-1.0.md`](./docs/decisions/scope-split-and-1.0.md).
The complete inventory also contains 7 experimental subpaths and 18 deprecated
compatibility subpaths. [`config/public-surface.json`](./config/public-surface.json)
is the machine-readable source consumed by CI, API reporting, TypeDoc, and
downstream documentation projection.
`@honua/sdk-js` is the JavaScript / TypeScript client for the [Honua](https://github.com/honua-io)
geospatial platform. It speaks the open protocols your data already uses (Esri GeoServices,
OGC API Features / Tiles / Maps / Processes, STAC, WMS, WMTS, WFS 2.0, OData v4), exposes a
single protocol-neutral `Dataset` → `Source` → `Query` → `Result` contract on top of them, and
ships a MapLibre-first map runtime plus an Esri compatibility layer so existing ArcGIS apps
can migrate file-by-file.
📚 **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).
- **Protocol-neutral.** One `Source.query(...)` call works against GeoServices, OGC, WFS, OData
and friends. Capability misses throw `HonuaCapabilityNotSupportedError` instead of returning
empty results.
- **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.
- **Open runtime.** `loadMapPackage(...)` + `HonuaMapRuntime` render a Honua `MapPackage` on
MapLibre GL JS. Cesium, kepler.gl, and OGC web-map sources are first-class.
> **What this is (and is not).** `@honua/sdk-js` is a typed geospatial *service client* and
> migration toolkit — it is **not a rendering engine**. 2D rendering rides MapLibre GL JS and 3D
> rides Cesium, so 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 (last release 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 protocol clients work against **any** standards-speaking server — an existing ArcGIS
> Server/Online endpoint, any OGC API implementation, a STAC catalog. A
> [Honua Server](https://github.com/honua-io/honua-server) adds the server-authored `MapPackage`,
> realtime, and AI surfaces, but it is the upgrade path, not the entry fee. **When to use it
> standalone:** if your data already sits behind an ArcGIS Server / ArcGIS Online endpoint, an OGC
> API Features server (pygeoapi, ldproxy, GeoServer OGC API), a WFS 2.0 server, a STAC API or static
> catalog, or an OData v4 service, use it today as a typed client and `esri-leaflet` successor — no
> server needed (the OGC API Features and STAC lanes discover the raw endpoint layout from the
> landing page; WFS follows the capabilities DCP URLs; set `locator.layout` for non-facade servers —
> see the [server-optional quickstart](./docs/standalone-quickstart.md) and the
> [backend-agnostic capability matrix](./docs/standalone-capability-matrix.md)). Reach for a Honua
> Server when you need authored map packages, realtime, collaboration, MCP/AI, or the OGC API Tiles /
> Maps / Processes / Records families (still facade-bound today).
```bash
npm install @honua/sdk-js
```
### 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.3 KiB |
| `@honua/sdk-js/map` | 16.2 KiB |
| `@honua/sdk-js` (root) | 108.3 KiB |
| `{ HonuaClient }` only (tree-shake guard) | 47.2 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`.
## 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, PROTOCOL_DEFAULT_CAPABILITIES } from "@honua/sdk-js/contract";
import { HonuaClient } from "@honua/sdk-js/honua";
// 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) is the **upgrade
path**, not the entry fee. It 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.
## What you can build
The versioned [SDK sample catalog](./docs/generated/sample-catalog.md) tracks all 27 executable examples: 10 flagship, 5 recipe, 8 advanced, and 4 reference. It is the source of truth for support, fixture/live modes, provenance, validation, and the honua.io projection.
## 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/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/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
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`.
- **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).
## Stability and versioning
- 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`.
- **Experimental** (subpath-only — not re-exported from the root barrels): `/agent-tools`,
`/agent-safety`, `/geoparquet`, `/query-planner`, `/plugin`, `/deckgl`, `/offline`.
- **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), and
[`docs/migration-punch-list.md`](./docs/migration-punch-list.md).
## Contributing
This SDK ships from a single repository. The shipping package is `@honua/sdk-js`;
all subpath entrypoints in [`INSTALL.md`](./INSTALL.md) live under that name.
See [`AGENTS.md`](./AGENTS.md) for contributor instructions and the Specifica
issue format used for backlog items.
## 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.
## 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` | Default barrel — re-exports the most common stable symbols |
| `@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 |
## Experimental subpath entrypoints
Subpaths marked `@experimental` in JSDoc. Useful today; the shape may change in
any minor release prior to `1.0.0`. **The experimental subpaths are subpath-only
— they are not re-exported from `@honua/sdk-js` or `@honua/sdk-js/honua`** so a
default-barrel import never pulls them in.
| Subpath | What it gives you |
|---------|-------------------|
| `@honua/sdk-js/agent-tools` | Agent-facing JSON Schema tool definitions (MCP/OpenAI compatible). Stays in the stable package (the in-repo `@honua/mcp-server` depends on it) but its symbols remain `@experimental` — not semver-frozen — while the AI surface settles. |
| `@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. |
| `@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/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. |
## 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 running Honua Server instance (for runtime queries)
## 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/honua";
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)`);
```
`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
- **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/generated/sample-catalog.md
# SDK sample catalog
This inventory is generated from [`samples/catalog.v1.json`](../../samples/catalog.v1.json). Do not edit it by hand.
Catalog contract: `honua.sdk.sample-catalog.v1` · SDK: `@honua/sdk-js` (effective version derived from `package.json`) · 27 executable examples
| Sample | Tier | Support | Data | Disposition | Demonstration |
| --- | --- | --- | --- | --- | --- |
| [`ai-spatial-app-builder`](../../examples/ai-spatial-app-builder/README.md) | advanced | experimental | fixture | rework | Builds a reviewable spatial application plan with the SDK agent-tool surface. |
| [`app-bootstrap-basic`](../../examples/app-bootstrap-basic/README.md) | reference | deprecated | fixture | retire | Bootstraps a minimal application through the legacy app-platform compatibility surface. |
| [`arcgis-source-app`](../../examples/arcgis-source-app/README.md) | reference | internal | fixture | keep | Provides the ArcGIS JavaScript source application used by the migration end-to-end harness. |
| [`edit-workflow-demo`](../../examples/edit-workflow-demo/README.md) | flagship | supported | fixture | rework | Demonstrates optimistic edits, attachments, conflicts, and safe recovery. |
| [`geocoding-quickstart`](../../examples/geocoding-quickstart/README.md) | recipe | supported | hybrid | keep | Runs forward, reverse, and suggestion workflows with map feedback. |
| [`geoprocessing-job-runner`](../../examples/geoprocessing-job-runner/README.md) | advanced | supported | hybrid | merge | Submits, polls, cancels, and inspects asynchronous geoprocessing jobs. |
| [`imagery-cog-quickstart`](../../examples/imagery-cog-quickstart/README.md) | flagship | supported | hybrid | merge | Compares WMS imagery, COG-backed ImageServer tiles, and export previews. |
| [`kepler-analytics`](../../examples/kepler-analytics/README.md) | advanced | experimental | hybrid | rework | Replays operations data through kepler.gl with linked filters and KPI evidence. |
| [`maplibre-quickstart`](../../examples/maplibre-quickstart/README.md) | flagship | supported | hybrid | keep | Connects, discovers, explains, queries, and mounts one source with linked views and inspectable evidence. |
| [`mcp-gis-assistant`](../../examples/mcp-gis-assistant/README.md) | advanced | experimental | fixture | rework | Demonstrates assistant tool discovery and safe SDK-backed spatial operations. |
| [`migration-workbench`](../../docs/migration-honua-maplibre.md) | flagship | supported | fixture | rework | Scans and transforms ArcGIS application source with auditable compatibility results. |
| [`node-backend-quickstart`](../../examples/node-backend-quickstart/README.md) | recipe | supported | hybrid | keep | Uses the protocol-neutral client from a Node service without browser dependencies. |
| [`oauth-signin`](../../examples/oauth-signin/README.md) | recipe | supported | fixture | keep | Demonstrates browser authentication and session lifecycle without embedding credentials. |
| [`overture-geoparquet`](../../examples/overture-geoparquet/README.md) | flagship | experimental | hybrid | keep | Plans and executes bounded Overture GeoParquet queries with truthful worker, range, memory, and pruning evidence. |
| [`planning-permitting-workbench`](../../examples/planning-permitting-workbench/README.md) | flagship | supported | fixture | rework | Combines parcels, hazards, sketching, editing, and export in a task-oriented application. |
| [`pmtiles-static`](../../examples/pmtiles-static/README.md) | recipe | supported | fixture | keep | Loads a static PMTiles archive without a Honua server. |
| [`react-quickstart`](../../examples/react-quickstart/README.md) | recipe | supported | hybrid | keep | Uses the React provider, hooks, and map component over the same quickstart contract. |
| [`realtime-incident-dashboard`](../../examples/realtime-incident-dashboard/README.md) | flagship | supported | hybrid | keep | Runs live-first incident command with observable reconciliation and a guarded, resettable edit lab. |
| [`runtime-parity-showcase`](../../examples/runtime-parity-showcase/README.md) | reference | experimental | fixture | replace | Compares supported rendering paths and makes fidelity differences explicit. |
| [`service-explorer`](../../examples/service-explorer/README.md) | flagship | supported | hybrid | rework | Browses heterogeneous spatial services with capability and cache diagnostics. |
| [`spatial-analytics-workbench`](../../examples/spatial-analytics-workbench/README.md) | flagship | experimental | hybrid | rework | Explains and accepts one plan linking AOI, map, table, chart, provenance, and reusable output. |
| [`stac-imagery-browser`](../../examples/stac-imagery-browser/README.md) | advanced | supported | fixture | merge | Discovers STAC collections and previews supported imagery assets. |
| [`standalone-quickstart`](../../examples/standalone-quickstart/README.md) | flagship | supported | hybrid | merge | Connects a public Esri service directly to MapLibre without a Honua server. |
| [`storytelling-25d-map`](../../examples/storytelling-25d-map/README.md) | advanced | supported | hybrid | merge | Combines terrain, extrusion, OGC overlays, and route playback in a guided story. |
| [`terrain-rgb-elevation`](../../examples/terrain-rgb-elevation/README.md) | advanced | supported | hybrid | merge | Reads Terrain-RGB tiles for point elevation and route profiles. |
| [`unified-ops-workspace`](../../examples/unified-ops-workspace/README.md) | advanced | deprecated | fixture | retire | Composes incident command, analysis, and shared workspace state. |
| [`web-components-basic`](../../examples/web-components-basic/README.md) | reference | deprecated | fixture | retire | Demonstrates the SDK custom-element controls against a map. |
The catalog also carries fixture/live commands, endpoint configuration names, provenance, attribution, freshness, validation, and the complete 21-route honua.io migration mapping. The presentation-safe projection is [`samples/dist/honua-site-samples.v1.json`](../../samples/dist/honua-site-samples.v1.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.
## 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 support, tier, data, provenance, freshness, and degradation metadata comes from the [versioned sample catalog](../../samples/contract/v1/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: `flagship` · `supported`
- 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: `flagship` · `supported`
- 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`
- 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` (`QueryBuilder`); `@honua/sdk-js/honua` (`HonuaClient`)
- 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: `flagship` · `supported`
- 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: `flagship` · `experimental`
- 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: `flagship` · `supported`
- 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: `flagship` · `supported`
- 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: `advanced` · `supported`
- 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: `flagship` · `supported`
- 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: `advanced` · `experimental`
- 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.v1.json) by `sampleId`; catalog-owned metadata is not copied into another source file.
- Sample metadata/artifact/evidence projection is coordinated by [SDK issue #401](https://github.com/honua-io/honua-sdk-js/issues/401) and [honua-site issue #120](https://github.com/honua-io/honua-site/issues/120).
---
# 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/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:** a query DSL. `Query.where` is still a SQL-92 / CQL2
string; adapters translate to their wire format.
## 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 eighteen 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`, plus three MapLibre-native (`maplibre-vector`, `maplibre-raster`, `maplibre-geojson`). |
| `Capability` | A coarse-grained protocol capability (`query`, `queryAggregate`, `spatialAggregate`, `queryExtent`, `queryObjectIds`, `queryRelated`, `applyEdits`, `attachments`, `render`, `tiles`, `sql`, `stream`, `pbf`, `connect`, `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. |
| `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` | `{ where?, spatialFilter?, outFields?, orderBy?, pagination?, aggregation?, returnGeometry?, outSr?, signal? }`. |
| `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")!;
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.
## Remote pushdown
The first compiler targets an existing GeoServices FeatureServer query path,
and the OGC API Features compiler targets a collection `/items` request. 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.
`Query.signal` never enters the IR or fingerprint. Supply cancellation only to
`executeQueryPlan`. Source URLs in plan identity are stripped of credentials,
query strings, and fragments; pass stable authorization scope identifiers, not
tokens.
## 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` and `QUERY_PLAN_VERSION` are both `1.0` for this slice.
- 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 fingerprint and the current source context.
A changed plan, locator, version, capability set, or scope is rejected; the
executor does not re-plan.
- 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. Follow-on slices
must add typed semantic predicates and temporal windows, CQL2 JSON and richer
OGC filter negotiation, WFS FES, OData, gRPC, and DuckDB compilers, spatial aggregation, joins/composition,
columnar/worker execution, cache/freshness decisions, cost models, realtime
snapshot/delta plans, receipts, golden cross-protocol fixtures, and shared
CLI/renderer/MCP consumption. Histogram and time-series aggregation are
rejected by this compiler rather than silently ignored.
---
# 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 and an ownership
transfer primitive. 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, execute queries, or create workers.
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.
## 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`
## Deliberate remaining scope
This slice does not claim the full #394 workstream. Arrow IPC decoding,
filter/projection/reprojection/aggregation workers, multi-batch streaming,
planner integration, renderer consumption, batch cache identity, realtime
patches, CSP worker factories, 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.
This slice does **not** import plugin code, install packages, maintain a global
registry, or pass credentials to an extension. A certified manifest means only
that its declaration is well formed and compatible with the supplied host
snapshot. Behavioral fixtures, cancellation, retries, cleanup, performance,
runtime registration, and support-program review remain future certification
phases.
## 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.
## Remaining work in #392
- typed lifecycle hooks and explicit per-application registration/DI;
- behavioral conformance fixtures for semantics, cancellation, retries,
cleanup, bundle metadata, and performance;
- an independently installable runner/CLI and signed or golden reports;
- an external-style plugin proving runtime registration without core imports;
- deprecation, naming, support, security-review, and certification governance.
---
# 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 an experimental, deterministic trust boundary
between untrusted plan proposals and host-owned effect execution. 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, and signs/verifies execution evidence.
The module never invokes a model, tool, source, renderer, subscription, or job.
An accepted approval is evidence for a host executor; it is not an executor.
`@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,
verifyAgentStepAuthorization,
} 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 },
);
// Immediately before an effect, compare exact current bindings and operation
// parameters, then atomically consume the single-use approval step.
const authorization = await verifyAgentStepAuthorization(
dryRun,
policy,
approval,
hostVerifier,
{ sources: { incidents: currentIncidentSourceBinding } },
"query-incidents",
exactQueryPlanInput,
hostAtomicApprovalUseStore,
);
// Execute only authorization.operation (the frozen validated snapshot), never
// the original mutable exactQueryPlanInput object.
```
`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.
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, run tools, provide a model
adapter, manage signing keys, parse query predicates to infer field use, perform
compensating actions, implement an audit sink, or establish SDK/CLI/MCP 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/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"
interface RendererAdapter {
readonly kind: "maplibre" | "deckgl" | "cesium";
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: "maplibre" | "deckgl" | "cesium";
readonly ready: Promise;
readonly diagnostics: DiagnosticChannel;
raw(kind: "maplibre" | "deckgl" | "cesium"): T | undefined;
dispose(): Promise;
}
```
`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)
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/protocol-capability-matrix.md
# Protocol × Capability Matrix
Status: implemented in `src/contract/types.ts` (`PROTOCOL_DEFAULT_CAPABILITIES`).
Update both this document and the table in code together.
The matrix below is the **default** capability set per protocol. Callers
that need a narrower surface for a specific source (for example a Feature
Service whose metadata reports `supportsStatistics: false`) must intersect
the default set themselves and pass the result on
`SourceDescriptor.capabilities`. The gRPC default is shared by the canonical
FeatureService transport, and the GeoServices, OGC, STAC, WFS, and WMS
adapter constructors do not read service metadata today, so per-source
downgrades for those protocols stay caller-side. **OData is the
exception**: the `odataSource` adapter lazily fetches `$metadata` on the
first capability-gated method, parses `Capabilities.*` annotations
(both inline inside `` and sibling
`` blocks), and intersects
the descriptor's declared `Capabilities` set with the server's
advertised flags — see the *OData* notes below for details. Other
adapters will follow the same pattern as follow-up work.
This matrix spans the full shared capability vocabulary, not just the
protocol-neutral `Source` methods implemented in this ticket. Capabilities
without a canonical `Source` method today are negotiated for
`Source.adapter()` escape hatches and follow-on adapter tickets.
`✓` = first-party support, no client-side fallback needed.
`◐` = supported only under `degraded` capability policy (client-side fallback).
`—` = not supported.
| 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 | GeoParq |
| --- | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: |
| `query` | ✓ | ✓ | ✓ | ✓ | — | — | ✓ | — | — | ✓ | ✓ | ✓ | ✓ | — | ✓ | ✓ |
| `queryAggregate` | ✓ | ✓ | ✓ | — | — | — | ◐ | — | — | — | — | — | — | — | — | ✓ |
| `spatialAggregate` | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — |
| `queryExtent` | ✓ | ✓ | ✓ | ✓ | — | — | ◐ | — | — | — | — | ✓ | — | — | — | — |
| `queryObjectIds` | ✓ | ✓ | ✓ | ✓ | — | — | ✓ | — | — | ✓ | ✓ | ✓ | — | — | ✓ | — |
| `queryRelated` | — | ✓ | ✓ | — | — | — | — | — | — | — | — | — | — | — | — | — |
| `applyEdits` | ✓ | ✓ | — | — | — | — | ✓ | — | — | — | — | ✓ | — | — | ✓ | — |
| `attachments` | — | ✓ | — | — | — | — | — | — | — | — | — | — | — | — | — | — |
| `render` | — | — | ✓ | ✓ | — | — | — | ✓ | ✓ | — | — | — | ✓ | ✓ | — | — |
| `tiles` | — | ◐ | ✓ | ✓ | — | — | — | ✓ | — | — | — | — | ✓ | ✓ | — | — |
| `sql` | — | ✓ | ✓ | — | — | — | — | — | — | — | — | — | — | — | — | — |
| `stream` | ✓ | ✓ | ✓ | — | — | — | ✓ | — | — | ✓ | ✓ | ✓ | — | — | ✓ | ✓ |
| `pbf` | — | ✓ | — | — | — | — | — | — | — | — | — | — | — | — | — | — |
| `connect` | — | ✓ | — | ✓ | ✓ | ✓ | — | — | — | — | — | — | — | — | — | — |
| `image` | — | — | — | ✓ | — | — | — | — | — | — | — | — | — | — | — | — |
| `geometry` | — | — | — | — | ✓ | — | — | — | — | — | — | — | — | — | — | — |
| `geoprocess` | — | — | — | — | — | ✓ | — | — | — | — | — | — | — | — | — | — |
| `processes` | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — |
MapLibre-native sources (`maplibre-vector`, `maplibre-raster`,
`maplibre-geojson`) are render-only and contribute `render` and (where
applicable) `tiles`. They are excluded from the table above because they
do not flow through the `Source.query` path.
`pmtiles` is a first-party protocol (`PROTOCOL_DEFAULT_CAPABILITIES.pmtiles`
= `{ tiles }`) but, like the MapLibre-native sources, is tiles-only and does
not flow through `Source.query`, so it is documented in the *PMTiles* note
below rather than as a table column.
**GeoParquet** (`geoparquet`, column `GeoParq`) is the DuckDB-WASM-backed
`Source` in `@honua/sdk-js/geoparquet`. The same `Query` (`where` /
`spatialFilter` / `outFields` / `orderBy` / `pagination` / `aggregation`)
compiles to DuckDB SQL over `read_parquet(...)`, so `query`, `queryAggregate`
(GROUP BY), and `stream` are first-party. Envelope `spatialFilter`s push down to
`ST_Intersects` over the native geometry (or a GeoParquet 1.1 bbox covering
column); non-envelope filters reduce to their bbox and are reported via
`Result.degraded`. `queryExtent`, `queryObjectIds`, `queryRelated`,
`applyEdits`, and `attachments` are honest misses — the static-file source is
read-only and exposes no server-side ids/extent endpoints. Metadata
(`describe()`: schema, geometry column, CRS, row estimate) and a raw `sql()`
escape hatch live on `Source.protocol("geoparquet")`. Because DuckDB-WASM is a
multi-megabyte optional peer, the adapter lives behind its own entrypoint (wired
into `createDataset` via `geoparquetResolver`) and is never in the `/contract`
or `/honua` graph.
`spatialAggregate` is an indexed analytics capability rather than the
field-statistics `queryAggregate` path. This SDK slice defines the
contract in `src/contract/spatial-aggregation.ts` without assigning
first-party default support to any protocol. A source may advertise
`spatialAggregate` only after backend metadata confirms an indexed
aggregation implementation for that source; the request/response shapes
keep the index model opaque so H3, Quadbin, or provider-specific grids
can all satisfy the same app contract.
## 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` and `connect`. 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` and `connect`; 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//...`; descriptors that
advertise only `connect` (service-root metadata probe) may omit the
task name.
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 ``, ``, ``,
``, `