Skip to content

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_WRITE is missing. This is the never-silently-degrade principle the present ADR inherits for USER_METADATA.
  • AW-002 (spec 007) — "Capability Gate": write_atomic raises CapabilityNotSupported before any I/O if the backend lacks ATOMIC_WRITE. This is the raise-before-I/O mechanism the present ADR inherits for USER_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() returns FileInfo.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:

  1. does not gate the method, which works without the kwarg;
  2. gates one optional argument, so supplying it requires the capability;
  3. is enforced once, at the Store layer (not per backend), raising CapabilityNotSupported before 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 CapabilityNotSupported when the guarded kwarg is passed.
  • The Store layer is the single enforcement point — backends do not need to validate the kwarg themselves.