Problem
A batch of wire-level gaps where the adapter or spec quietly delivers less than the query/API surface promises. Individually small; grouped because they're all "the wire contract lies by omission" and #52's conformance fixtures are the natural enforcement point for the lot.
1. relatedTo.label is silently dropped
StackQuery supports relatedTo: { recordId, label? } (spec §Queries), but buildQueryParams serializes only the record ID:
if (f.relatedTo) p.set('relatedTo', f.relatedTo.recordId);
(packages/adapter-api/src/index.ts:157) — and the spec's GET /records param list has no label param either. A query for "records related to X as author" returns records related to X under any label. Superset result, no error, no signal. Fix: add relatedToLabel to the spec's param list and send it.
2. content filters silently dropped on non-contentFieldQuery servers
When capabilities.contentFieldQuery is false, queryRecords falls back to GET /records with buildQueryParams — which has no handling for filter.content at all. The content filter evaporates and the server returns everything else that matched.
This is the dangerous one: the caller wrote a narrower query than what ran, and gets a superset presented as the filtered result. Any app logic that trusts the filter (dedup checks, "does a record with this slug exist", anything selection-sensitive) is wrong on exactly the adapters that can't be blamed for it. Silent capability degradation on a write of trust is the same shape #50 fixed for pagination.
Fix: fail loudly, don't widen silently. If filter.content is present and the server lacks contentFieldQuery, throw (capability error naming the missing capability) rather than degrade. Client-side post-filtering is tempting but interacts badly with pagination (a page of 50 post-filtered to 3 breaks limit/cursor semantics — the #50 discussion applies); apps that want it can do it deliberately. Same audit for search when fullTextSearch is false — today it's serialized unconditionally and behavior is whatever the server does with an unsupported param.
3. DELETE /records/:id/associations takes its payload in the body
The spec itself specifies it (§Associations: "remove an association (by body)") and dissociate() implements it (packages/adapter-api/src/index.ts:359–361). DELETE request bodies have no defined semantics (RFC 9110 §9.3.5: "has no generally defined semantics; …might lead some implementations to reject the request") — proxies, caches, gateways, and some fetch stacks drop or reject them. For a protocol meant to be implemented by arbitrary servers behind arbitrary localhost/reverse-proxy setups, this is a portability landmine in the spec, not just the adapter. Fix: POST /records/:id/associations/delete (or encode kind/label/payload discriminants as query params on DELETE). Wire-shape change, so cheapest now while the ecosystem is one server.
4. Discovery omits the attachment size limit
The spec's error table defines 413: attachment upload exceeds the server's size limit — but the discovery document (§Wire protocol, /.well-known/stack) has no field exposing that limit, and AdapterCapabilities (packages/core/src/types.ts:312–316) can't carry it. Clients learn the ceiling only by uploading the whole payload and failing. Add maxAttachmentBytes (or null = unlimited) to discovery + AdapterCapabilities, so apps can pre-check and surface limits in UI before burning the upload.
5. Every create()/update() costs an extra HTTP round trip for getType
Stack.create() and Stack.update() each call adapter.getType(...) to validate content (packages/core/src/stack.ts:377, 472). Through APIAdapter that's a GET /types/:id per write, doubling write latency. Versioned type IDs are immutable once defined — the schema-drift guard direction (backlog; schemaHash exists exactly to enforce this) makes typeId → schema a cacheable fact. Fix: a type cache in Stack (or the adapter), populated on first fetch and by defineType/saveType, invalidated never (immutable) or on explicit listTypes refresh. Owner priority says writes may be slower than reads, but this is a free 2× on every write and a per-write load on the server for a value that cannot change.
Proposed fix (summary)
- Spec + adapter:
relatedToLabel param.
- Adapter: throw on
content filter (and audit search) when the capability is absent — never silently widen.
- Spec + adapter: replace DELETE-with-body for dissociation.
- Spec + core types + adapter:
maxAttachmentBytes in discovery/capabilities.
- Core: type cache keyed on versioned typeId.
Items 1–4 get #52 conformance fixtures (round-trip each query filter; dissociate through a body-stripping proxy simulation is overkill — asserting the wire shape is enough; discovery field presence). Item 5 is a core-only perf fix with a unit test that create×N hits getType once.
Non-goals
- Full capability-negotiation framework — the existing boolean capabilities are fine; this is about honoring them loudly.
- Offline queueing / request coalescing (explicitly deferred in
adapter-api's charter).
Refs #52 (conformance fixtures; endpoint-splitting touches the same adapter surface), #50 (the pagination analogue of "silently narrower/wider than asked"), #46 (native adapter will re-run the same capability matrix), future schema-drift guard (type immutability assumption behind the cache).
Problem
A batch of wire-level gaps where the adapter or spec quietly delivers less than the query/API surface promises. Individually small; grouped because they're all "the wire contract lies by omission" and #52's conformance fixtures are the natural enforcement point for the lot.
1.
relatedTo.labelis silently droppedStackQuerysupportsrelatedTo: { recordId, label? }(spec §Queries), butbuildQueryParamsserializes only the record ID:(
packages/adapter-api/src/index.ts:157) — and the spec'sGET /recordsparam list has no label param either. A query for "records related to X asauthor" returns records related to X under any label. Superset result, no error, no signal. Fix: addrelatedToLabelto the spec's param list and send it.2.
contentfilters silently dropped on non-contentFieldQueryserversWhen
capabilities.contentFieldQueryis false,queryRecordsfalls back toGET /recordswithbuildQueryParams— which has no handling forfilter.contentat all. The content filter evaporates and the server returns everything else that matched.This is the dangerous one: the caller wrote a narrower query than what ran, and gets a superset presented as the filtered result. Any app logic that trusts the filter (dedup checks, "does a record with this slug exist", anything selection-sensitive) is wrong on exactly the adapters that can't be blamed for it. Silent capability degradation on a write of trust is the same shape #50 fixed for pagination.
Fix: fail loudly, don't widen silently. If
filter.contentis present and the server lackscontentFieldQuery, throw (capability error naming the missing capability) rather than degrade. Client-side post-filtering is tempting but interacts badly with pagination (a page of 50 post-filtered to 3 breaks limit/cursor semantics — the #50 discussion applies); apps that want it can do it deliberately. Same audit forsearchwhenfullTextSearchis false — today it's serialized unconditionally and behavior is whatever the server does with an unsupported param.3.
DELETE /records/:id/associationstakes its payload in the bodyThe spec itself specifies it (§Associations: "remove an association (by body)") and
dissociate()implements it (packages/adapter-api/src/index.ts:359–361). DELETE request bodies have no defined semantics (RFC 9110 §9.3.5: "has no generally defined semantics; …might lead some implementations to reject the request") — proxies, caches, gateways, and some fetch stacks drop or reject them. For a protocol meant to be implemented by arbitrary servers behind arbitrary localhost/reverse-proxy setups, this is a portability landmine in the spec, not just the adapter. Fix:POST /records/:id/associations/delete(or encode kind/label/payload discriminants as query params on DELETE). Wire-shape change, so cheapest now while the ecosystem is one server.4. Discovery omits the attachment size limit
The spec's error table defines 413: attachment upload exceeds the server's size limit — but the discovery document (§Wire protocol,
/.well-known/stack) has no field exposing that limit, andAdapterCapabilities(packages/core/src/types.ts:312–316) can't carry it. Clients learn the ceiling only by uploading the whole payload and failing. AddmaxAttachmentBytes(ornull= unlimited) to discovery +AdapterCapabilities, so apps can pre-check and surface limits in UI before burning the upload.5. Every
create()/update()costs an extra HTTP round trip forgetTypeStack.create()andStack.update()each calladapter.getType(...)to validate content (packages/core/src/stack.ts:377, 472). ThroughAPIAdapterthat's aGET /types/:idper write, doubling write latency. Versioned type IDs are immutable once defined — the schema-drift guard direction (backlog;schemaHashexists exactly to enforce this) makestypeId → schemaa cacheable fact. Fix: a type cache inStack(or the adapter), populated on first fetch and bydefineType/saveType, invalidated never (immutable) or on explicitlistTypesrefresh. Owner priority says writes may be slower than reads, but this is a free 2× on every write and a per-write load on the server for a value that cannot change.Proposed fix (summary)
relatedToLabelparam.contentfilter (and auditsearch) when the capability is absent — never silently widen.maxAttachmentBytesin discovery/capabilities.Items 1–4 get #52 conformance fixtures (round-trip each query filter; dissociate through a body-stripping proxy simulation is overkill — asserting the wire shape is enough; discovery field presence). Item 5 is a core-only perf fix with a unit test that
create×N hitsgetTypeonce.Non-goals
adapter-api's charter).Refs #52 (conformance fixtures; endpoint-splitting touches the same adapter surface), #50 (the pagination analogue of "silently narrower/wider than asked"), #46 (native adapter will re-run the same capability matrix), future schema-drift guard (type immutability assumption behind the cache).