ADR-0026: Strict-Gate Pattern for Optional Capability Kwargs¶
Status¶
| Field | Value |
|---|---|
| Status | Accepted |
| Supersedes | — |
| Superseded by | — |
| Amends | — |
Context¶
USER_METADATA (RFC-0011 / ID-146) adds an optional metadata= kwarg to
Store.write*(). The backend either supports user metadata natively
(Capability.USER_METADATA) or it does not. When it does not, the question
is: raise or silently drop?
The same question was settled for atomic writes. Two atomic-write invariants together establish the precedent:
- AW-007 (spec 007) — "Atomicity is Never Assumed": the core never
silently falls back to non-atomic writes when
ATOMIC_WRITEis missing. This is the never-silently-degrade principle the present ADR inherits forUSER_METADATA. - AW-002 (spec 007) — "Capability Gate":
write_atomicraisesCapabilityNotSupportedbefore any I/O if the backend lacksATOMIC_WRITE. This is the raise-before-I/O mechanism the present ADR inherits forUSER_METADATA.
Together, these two invariants name a pattern: every optional behaviour that the caller explicitly requests — rather than a capability that merely upgrades a default path — raises before I/O if the backend cannot honour it, and never silently drops.
USER_METADATA is a second instance of this pattern. The decision deserves
its own ADR to name the pattern so future contributors can follow it
deliberately rather than rediscovering it case by case.
The case against silent drop¶
Saga consumers treat "write returned" as "metadata is durable." A silent drop means:
- The write succeeds.
- The caller believes metadata was stored.
- A downstream
get_file_info()returnsFileInfo.metadata == None. - Data integrity invariants break silently, without a traceable error.
Silent degradation is worse than a loud failure: the failure is deferred, possibly to a different service or audit step, and by then the context that would explain it is gone.
The case against silent drop even for idempotent metadata¶
One might argue that metadata is "advisory" and a drop is tolerable. This argument fails for the target consumer: saga orchestrators use metadata to carry correlation IDs and idempotency tokens. A drop is not a degraded experience; it is a correctness failure.
Why not a capability guard on the method?¶
WRITE already gates write(). Adding a second gate (USER_METADATA) to
gate the entire method would prevent callers from writing on non-declaring
backends when metadata= is absent — which is wrong. The capability gates
only the use of the kwarg, not the method.
This is a new pattern: a capability that gates a specific kwarg on an existing method rather than the method as a whole.
Decision¶
An optional kwarg that requests a specific backend capability MUST raise
CapabilityNotSupported before any I/O when the backend does not declare
that capability. Never silently drop the kwarg: a silent drop turns a caller's
durability assumption (correlation IDs and idempotency tokens carried in
metadata=) into an untraceable correctness failure discovered later, in a
different service, with the explaining context already gone. Reverse if a
future consumer class treats such a kwarg as purely advisory with no downstream
correctness dependence; then silent degradation, not a raise, becomes the
defensible default.
Strict gate on kwarg (the pattern). A capability is a strict gate on kwarg when it:
- does not gate the method, which works without the kwarg;
- gates one optional argument, so supplying it requires the capability;
- is enforced once, at the Store layer (not per backend), raising
CapabilityNotSupportedbefore any I/O when the capability is absent and the argument is supplied.
USER_METADATA gating metadata= on write*() is the first instance. The
live registry of strict-gate capabilities is CAP-007 (spec 003); each
instance's per-backend contract lives in its feature spec (e.g. WR-010). A new
instance is added by declaring a capability, gating its kwarg at the Store
layer, and registering it in CAP-007, with no new enforcement site. Reverse if a
gated argument becomes universally supported across backends; then the gate for
that capability is removed rather than relocated.
Consequences¶
- Callers get a clear, early error rather than a silent correctness failure.
- The pattern is named and documented; future contributors have a precedent.
- Test coverage requirement: every non-declaring backend must have a negative
test asserting
CapabilityNotSupportedwhen the guarded kwarg is passed. - The Store layer is the single enforcement point — backends do not need to validate the kwarg themselves.