Cross-SDK Surface Alignment
Status: pending review for https://github.com/honua-io/honua-sdk-js/issues/44.
Once the PR merges this becomes the canonical reference for downstream Python
and .NET SDK alignment work; flip the status line to reviewed at that point.
The Honua SDKs should share one mental model even when each language keeps its
own naming conventions. JavaScript may use queryAll(), Python may use
query_all(), and .NET may use QueryAllAsync(). Those names are allowed to
differ. The semantics behind them must not.
Canonical Semantics
Use the @honua/sdk-js/contract vocabulary as the current source of truth:
| Concept | Meaning |
|---|---|
Dataset |
Logical grouping of one or more sources plus compatibility checks. |
SourceDescriptor |
Serializable source identity: protocol, locator, capabilities, schema, and attribution. |
Source |
Runtime handle for the common query, edit, related-record, attachment, and escape-hatch surface. |
Protocol |
Stable source protocol identifier. |
Capability |
Stable operation capability identifier. |
Query |
Protocol-neutral request intent. |
Result |
Protocol-neutral result envelope. |
EditEnvelope / EditResult |
Protocol-neutral write envelope and result. |
MapBinding |
Source → renderer-layer binding (used by MapPackage round-trips). |
protocol(...) |
Explicit native protocol escape hatch. |
These are the canonical nouns pinned in
test/fixtures/sdk-contract/semantic-contract.v1.json
and validated by the drift gate
(test/contract/sdk-contract-fixtures.test.ts).
Renaming a noun without updating both sites trips CI.
Language Bindings
Language bindings must be idiomatic. The binding table below is guidance for public API names, not a byte-for-byte requirement:
| Concept | TypeScript | Python | .NET |
|---|---|---|---|
| query all records | queryAll() |
query_all() |
QueryAllAsync() |
| stream pages | stream() |
stream() / iter_*() |
StreamAsync() / QueryPagesAsync() |
| query object ids | queryObjectIds() |
query_object_ids() |
QueryObjectIdsAsync() |
| apply edits | applyEdits() |
apply_edits() |
ApplyEditsAsync() |
| return geometry | returnGeometry |
return_geometry |
ReturnGeometry |
| output fields | outFields |
out_fields |
OutFields |
| pagination | pagination |
pagination / limit / offset |
Pagination / Limit / Offset |
| native protocol escape hatch | source.protocol(...) |
source.protocol(...) |
source.Protocol(...) |
Existing SDK names can remain as aliases or facades. For example, Python's
FeatureQuery and .NET's FeatureQueryRequest can continue to exist while
new source-oriented facades converge on the same behavior.
Stable Protocol IDs
The canonical protocol identifiers are the values exported from
PROTOCOLS in src/contract/types.ts. Serialized descriptors and docs should
prefer the canonical identifiers; SDKs may accept short or legacy aliases for
already-shipped public names by normalizing on read through the
protocolAliases map in the fixture pack:
grpcgeoservices-feature-servicegeoservices-map-servicegeoservices-image-servicegeoservices-geometry-servicegeoservices-gp-serviceogc-featuresogc-tilesogc-mapsstacwfswmswmtsodatamaplibre-vectormaplibre-rastermaplibre-geojson
Stable Capability IDs
The canonical capability identifiers are the values exported from
CAPABILITIES in src/contract/types.ts. SDKs should gate behavior on these
semantic identifiers rather than inventing per-repo names for the same
operation. Default capability sets per protocol live in
PROTOCOL_DEFAULT_CAPABILITIES and are mirrored in
docs/protocol-capability-matrix.md.
Every identifier must also have exactly one reviewed entry in the support
manifest's operationSurfaces registry. That registry binds the operation to
either a compile-checked canonical Source member or a named typed adapter
member, plus executable evidence; CI fails if vocabulary and surfaces drift.
Top-level connect() discovery is tracked separately as a product support
claim because it is not an operation on a Source.
Capability misses must be explicit. The required behavior is a typed SDK error
that names the missing capability and protocol (HonuaCapabilityNotSupportedError
in JS; equivalent typed errors in Python and .NET). Returning an empty result
for an unsupported operation is not acceptable because it is indistinguishable
from a valid query with no matches.
Query Semantics
All SDKs should preserve these query semantics even when the local request type uses language-specific names:
where: logical attribute filter. Adapters translate it to SQL-92, CQL2, OData$filter, FES, or provider-native syntax as appropriate.spatialFilter/bbox: spatial constraint. SDKs may expose a compact bbox helper, but the behavior must be documented.outFields: fields or properties to return.orderBy: provider-supported sort order.pagination.offset: records to skip before returning data.pagination.limit: record cap.query(),queryAll(), andstream()must document whether the limit is per-page or total.returnGeometry: whether returned features include geometry.outSr: requested output spatial reference when the protocol supports it.signal/ cancellation token: caller-driven cancellation.
Result Semantics
All SDKs should expose the same result facts:
- features are an array, empty when no records matched.
- each feature has an identifier when available, attributes/properties, and optional geometry.
- result envelopes preserve paging state (
exceededTransferLimit,hasMoreResults, or an idiomatic alias). - total count is present when the protocol reports it.
- field schema is present when the protocol reports it.
- extent is present for extent-only or extent-enriched queries.
- degraded results carry structured reasons with capability, protocol, and optional source id.
- raw/provider-native payload access remains available for advanced workflows.
Backwards Compatibility And Deprecation Policy
The RFC aligns semantics — it does not delete shipped APIs.
- Existing public names are preserved as aliases or facades over the
canonical surface. JS keeps
Source.adapter()as a documented alias ofSource.protocol(); Python keepsFeatureQueryand friends; .NET keepsFeatureQueryRequestand friends. Each binding may add the canonical name alongside the legacy name without renaming the legacy name. - Deprecation requires a documented removal timeline. A binding that introduces a canonical alternative may mark the legacy name deprecated and remove it at the next major version, never sooner. The deprecation note must point at the canonical replacement and (where applicable) at this RFC.
- Protocol aliases normalize on read. Short and legacy protocol names that
already shipped in any SDK appear in the
protocolAliasesmap ofsemantic-contract.v1.json. SDKs accept those aliases as input, but every SDK-side serialized descriptor —SourceDescriptorJSON, fixture pack, telemetry, log line — emits the canonical kebab-case id. The serverSourceBindingwire format is independent: it stays on its documented snake_case enum (geoservices_feature_service,ogc_features, …) persource-binding-alignment.md, and the runtime normalizes server wire values to canonical SDK ids insidesrc/runtime/source-bridge.tsbefore they reachcreateDataset. A server-side wire-format rename would be a coordinated cross-repo contract change, not a downstream SDK edit. Adding a new alias is backwards- compatible; renaming a canonical id is not. - Capability behavior may not silently change. Adding a capability to a
protocol is backwards-compatible. Removing one (or downgrading from
first-party to client-side fallback) is a breaking change and must ship in a
major version with a
degraded[]migration path documented in the per-protocol notes ofdocs/protocol-capability-matrix.md. canonicalNounsis part of the contract. RenamingDataset,SourceDescriptor,Source,Protocol,Capability,Query,Result,EditEnvelope,EditResult, orMapBindingis a breaking change to every downstream SDK; the drift gate enforces this.
Decisions Locked By This RFC
These are the alignment points the RFC nails down. Any future change to one of these requires a follow-up RFC plus coordinated multi-SDK updates.
- Canonical protocol identifiers and the canonical → alias normalization direction.
- Canonical capability identifiers and per-protocol default capability sets.
- Query field names and semantics for
where, spatial filter / bbox,outFields,orderBy,pagination,returnGeometry,outSr, and cancellation. - Result envelope shape: features array (never undefined), pagination state,
totalCount, field schema, extent, degraded reasons, and raw payload access. - Unsupported-capability behavior: typed error, never a silent no-op or empty result.
- Native protocol escape hatch: every protocol-specific operation that does
not belong on the shared surface lives behind the binding-idiomatic
protocol(...)accessor (Source.protocol(...)in JS / Python,Source.Protocol(...)in .NET). The shared surface stays free of protocol-typed structures.
Fixture Pack
The cross-SDK fixture pack lives under
test/fixtures/sdk-contract/semantic-contract.v1.json.
It is intentionally JSON-only so Python and .NET can consume the same payloads
without depending on the JS test helpers.
Coverage today:
protocols,capabilities, anddefaultCapabilitiesare pinned against the live JS exports — drift trips CI.protocolAliasesrecords the short / legacy names already shipped (or about to ship) in Python and .NET so each SDK normalizes on read.canonicalNounsenumerates the ten contract nouns the drift gate enforces.languageBindingsdocuments idiomatic per-language method and field names.resultScenarioscover FeatureServer, OGC API Features, STAC, OData, and WFS; included variants exercise nominal queries, empty pages, exceeded transfer limits, and thequeryAggregateclient-side degraded fallback for OGC API Features.unsupportedCapabilityScenarioscover render-only protocols (WMTS, OGC API Tiles, OGC API Maps), utility-only GeoServices protocols (Geometry Service, GP Service), and protocol-specific capability gaps (OGC API FeaturesqueryRelated).
The JS drift gate in test/contract/sdk-contract-fixtures.test.ts validates
that the fixture pack stays aligned with the exported JS contract registries
and with the documented result/error/degraded semantics. Downstream SDK work
should consume this fixture pack directly or mirror it with a clear
checksum/update process.
Downstream Implementation Work
This RFC is JS-side and docs-only. The Python and .NET SDK alignment lands as separate downstream tickets:
- Python:
honua-sdk-pythonwill pin againstsemantic-contract.v1.json, expose the canonical surface asSource.query_all/Source.protocol(...), and keepFeatureQueryplus the existing free-function helpers as backwards-compatible facades. Track viahonua-io/honua-sdk-pythonissue (TODO: add link once issue is filed). - .NET:
honua-sdk-dotnetwill mirror the same fixture pack, exposeSource.QueryAllAsync/Source.Protocol(...)/Source.QueryObjectIdsAsync, and keepFeatureQueryRequestas a facade. Track viahonua-io/honua-sdk-dotnetissue (TODO: add link once issue is filed).
This ticket explicitly does not edit either repository. Filing the downstream
issues is part of merging this RFC; updating the placeholders above with the
real issue links is the only follow-up edit allowed inside honua-sdk-js
once the downstream tickets are open.
References
- Canonical type definitions:
src/contract/types.ts - Cross-protocol matrix:
docs/protocol-capability-matrix.md - Shared client contract design:
docs/shared-client-contract.md - Source-binding round-trip:
docs/source-binding-alignment.md - Drift gate test:
test/contract/sdk-contract-fixtures.test.ts - Tracking issue: https://github.com/honua-io/honua-sdk-js/issues/44
- Cross-repo coordination:
honua-io/honua-server#813