Skip to content

feat: add Appendix F, provider conformance (TCK) - #423

Draft
aepfli wants to merge 3 commits into
mainfrom
feat/provider-tck-appendix
Draft

feat: add Appendix F, provider conformance (TCK)#423
aepfli wants to merge 3 commits into
mainfrom
feat/provider-tck-appendix

Conversation

@aepfli

@aepfli aepfli commented Aug 24, 2026

Copy link
Copy Markdown
Member

Implements the spec-repo half of #417, the cross-language tracking issue for the provider conformance suite. Java implementation: java-sdk-contrib#1829.

What

Adds Appendix F: Provider Conformance (TCK) — a language-agnostic conformance suite for validating OpenFeature providers, plus the three artifacts it rests on.

Marked experimental and explicitly non-normative. This is a proof of concept and the scenario set is a representative subset, not exhaustive coverage.

Why

OpenFeature's central promise is that swapping providers does not change application behaviour, and nothing currently verifies it. Every provider tests itself differently, against its own backend, with its own harness — so "implements the provider contract" is an unverified claim, and a behavioural difference between two providers gets discovered by the application that trips over it.

Why a new appendix rather than extending Appendix B

They answer different questions:

Appendix B Appendix F
Subject under test the SDK the provider
Harness an in-memory provider as a stand-in the provider under test, against a real backend

Appendix F also needs more than Gherkin. The scenarios, the flag set they assume, and the control API that produces outages are three co-dependent artifacts — a feature file that evaluates boolean-flag is meaningless without the flag definition, and a disconnect scenario is meaningless without the endpoint that produces the disconnect. An OpenAPI document has no home under "Gherkin Suites".

Both appendices now cross-reference each other.

Contents

  • specification/appendix-f-provider-conformance.md — the appendix, written for implementers in other languages: what the artifacts are, the control-API contract and its two invariants, the capability tag vocabulary, and the six things a language implementation owns
  • specification/assets/provider-tck/gherkin/ — four feature files (evaluation, errors, events, lifecycle)
  • specification/assets/provider-tck/flags/canonical-flags.json — the flag set every scenario assumes
  • specification/assets/provider-tck/openapi/control-api.yaml — what a backend under test must expose
  • specification/assets/provider-tck/README.md — a landing page for the directory, matching the convention of assets/gherkin/README.md

Assets live under assets/provider-tck/ rather than alongside the existing SDK Gherkin, because assets/gherkin/evaluation.feature already exists and means something different.

A .gitattributes normalises these files to LF. They are consumed byte for byte by every language's TCK and copied verbatim into published build artifacts, so a Windows checkout must not produce a different packaged file than a Linux one.

Design decisions worth reviewing

Capabilities. Not every provider implements every optional part of the contract. Scenarios exercising an optional capability carry a tag; a provider declares what it supports and the rest are reported as skipped with a reason, never as passed. The rule behind it: a conformance suite that quietly goes green on scenarios it did not run is worse than no suite at all.

@strict-numeric-typing is not really optional. The spec requires TYPE_MISMATCH when the requested type cannot be satisfied, and narrowing 0.5 to 0 loses information silently. It is a capability only so a provider with this defect can adopt the suite today and see the gap reported explicitly. Not declaring it is an admission of a known bug.

No container restarts. Unavailability is simulated inside the running stack via the control API. Orchestrators assign host ports dynamically and cannot reliably preserve them across a restart, so restarting silently invalidates every provider pointed at the old port — and the failure looks like a flaky provider.

In-process control is a narrow carve-out. Providers with no backend (in-memory, environment-variable, file-based) may be driven in-process. Providers with a backend must use the control API; reaching an external backend through a side channel produces a suite that passes while proving nothing.

Open questions

  • Normative status. Nothing here is expressed as a numbered requirement. Whether the control-API contract and the capability vocabulary should become normative sections is a TSC decision, and is recorded in the appendix as an open question rather than assumed.
  • Evaluation context passthrough is not covered — it needs an echo operation on the control API (GET /last-evaluation or similar). Until then a provider that silently drops the context passes. The @targeting tag is reserved for those scenarios.
  • Finer-grained flag manipulation (set/remove an individual flag) would need new control-API endpoints.
  • Hooks, flag metadata and caching are not covered.

Reference implementation

tools/provider-tck in open-feature/java-sdk-contrib#1830, adopted by the flagd provider for both its RPC and in-process resolvers. A follow-up PR there switches that module to consume these files from this repo via submodule, and verified the artifacts round-trip byte-identical.

Which of #417's open questions this answers

Q1, directory layout — proposes specification/assets/provider-tck/{gherkin,flags,openapi}/. The subdirectory is forced rather than chosen: assets/gherkin/evaluation.feature already exists and tests an SDK, not a provider.

Q4, @strict-numeric-typing as capability vs hard requirement — keeps it a capability so providers with the defect can adopt today, but the appendix says in prose that it is not an optional feature and that not declaring it is an admission of a known bug. The "known deviations" idea from #417 is left open.

Left open, unchanged by this PR: Q2 versioning, Q3 canonical flag-set format, Q5 context passthrough, Q6 reason values, Q7 conformance claims. Q5 is also recorded in the appendix's own "Open questions" section.

What the suite has found so far

Two real defects, from the outside, which is the argument for the whole exercise:

  • flagd silently narrows a float flag to an integer — the finding already described in [Tracking] A cross-language conformance suite for OpenFeature providers #417.
  • The Java SDK's MultiProvider swallows child provider events — it extends EventProvider but never subscribes to its children, so PROVIDER_CONFIGURATION_CHANGED, PROVIDER_ERROR and PROVIDER_STALE never reach the client. Independently reproduced by running the suite against it; already tracked as open-feature/java-sdk#1882 (gap 1, High), where it had been found by hand-comparing against the js-sdk reference.

OpenFeature's central promise is that swapping providers does not change
application behaviour, and nothing verifies it. Every provider tests itself
differently, against its own backend, with its own harness, so "implements the
provider contract" is an unverified claim.

Add the language-agnostic artifacts a shared provider conformance suite needs,
plus an appendix explaining how they fit together for implementers in other
languages.

A new appendix rather than an extension of Appendix B, because the two answer
different questions. Appendix B validates an SDK using an in-memory provider as a
stand-in; this validates a provider against a real backend. Appendix F also needs
more than Gherkin: the scenarios, the flag set they assume and the control API
that produces outages are three co-dependent artifacts, and an OpenAPI document
has no home under "Gherkin Suites". Both appendices now cross-reference each
other.

Assets live under specification/assets/provider-tck/ rather than alongside the
existing SDK gherkin — those already contain an evaluation.feature that means
something different.

Marked experimental and explicitly non-normative. Nothing here is expressed as a
numbered requirement yet; whether the control API contract and the capability tag
vocabulary should become normative sections is a TSC decision, and is recorded as
an open question rather than assumed.

The first implementation is tools/provider-tck in open-feature/java-sdk-contrib,
in review alongside this.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

aepfli added a commit to open-feature/java-sdk-contrib that referenced this pull request Aug 24, 2026
…pec submodule

The Gherkin, the canonical flag set and the control API document are not Java
artifacts. They are language-agnostic definitions of the provider contract that
every language's TCK must agree on byte for byte, and they only lived in this
module because the proof of concept had to start somewhere.

They now live in open-feature/spec as Appendix F, under
specification/assets/provider-tck/, and are copied in from the `spec` git
submodule at generate-resources — the same mechanism tools/flagd-api-testkit
already uses for the flagd test harness. The copies are git-ignored and carry a
do-not-edit note; changes belong in the spec repo and arrive here by bumping the
submodule.

Consumers are unaffected: the artifacts are still packaged into the release JAR,
@SelectClasspathResource("features") still resolves, and nobody needs a submodule
of their own. Verified byte-identical after the round trip.

The in-memory CI job now checks out submodules, since without them there is no
suite to run.

DEPENDS ON open-feature/spec#423. The submodule is pinned to that PR's branch
commit rather than to a commit on the spec repo's main branch. That is reachable,
so CI can fetch it, but it must be re-pinned to main once #423 merges and before
this lands.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
aepfli added a commit to open-feature/java-sdk-contrib that referenced this pull request Aug 24, 2026
…pec submodule

The Gherkin, the canonical flag set and the control API document are not Java
artifacts. They are language-agnostic definitions of the provider contract that
every language's TCK must agree on byte for byte, and they only lived in this
module because the proof of concept had to start somewhere.

They now live in open-feature/spec as Appendix F, under
specification/assets/provider-tck/, and are copied in from the `spec` git
submodule at generate-resources — the same mechanism tools/flagd-api-testkit
already uses for the flagd test harness. The copies are git-ignored and carry a
do-not-edit note; changes belong in the spec repo and arrive here by bumping the
submodule.

Consumers are unaffected: the artifacts are still packaged into the release JAR,
@SelectClasspathResource("features") still resolves, and nobody needs a submodule
of their own. Verified byte-identical after the round trip.

The in-memory CI job now checks out submodules, since without them there is no
suite to run.

DEPENDS ON open-feature/spec#423. The submodule is pinned to that PR's branch
commit rather than to a commit on the spec repo's main branch. That is reachable,
so CI can fetch it, but it must be re-pinned to main once #423 merges and before
this lands.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
lifecycle.feature was gated by @events, which is wrong in both directions.

Too strict: a stateless HTTP provider such as OFREP emits no events of its own
and so cannot declare @events, yet the readiness scenario is not really about
events at all. Too lax: every SDK synthesises PROVIDER_READY for a provider
with no initialisation step -- the Go SDK says so in a comment, "a provider
without state handling capability can be assumed to be ready immediately" --
so a provider that declares @events passes the readiness scenario without
demonstrating anything. A no-op provider passes it identically.

The two are different claims. @events is "emits lifecycle events at all";
@lifecycle is "performs an initialisation that reaches its backend, with an
observable outcome". A provider may do either without the other.

Found by adopting the suite in the OFREP providers for Go and Java, where two
independent adoptions withheld @events for the same reason and neither could
express what they actually lacked.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
aepfli added a commit to open-feature/java-sdk-contrib that referenced this pull request Aug 24, 2026
lifecycle.feature was tagged @events, which is wrong in both directions.

Too strict: a stateless provider such as OFREP emits no events of its own
and cannot declare @events, yet it still initialises against a backend and
still owes the lifecycle contract.

Too lax: FeatureProviderStateManager emits PROVIDER_READY/PROVIDER_ERROR
around initialize for any provider, whether or not it is an EventProvider.
So a provider that does no initialisation of its own reaches READY exactly
as NoOpProvider would, and the readiness scenario passes vacuously.

Adds Capability.LIFECYCLE ("@lifecycle") -- performs an initialisation that
reaches its backend, with an observable outcome -- and re-vendors
lifecycle.feature verbatim from the spec assets, where the feature-level tag
is now @lifecycle (spec dfa16586, PR open-feature/spec#423).

flagd declares LIFECYCLE in both resolver modes: RPC does a round trip and
in-process syncs the whole ruleset during initialisation, so the scenarios
assert something real there.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
aepfli added a commit to open-feature/js-sdk-contrib that referenced this pull request Aug 24, 2026
…odule

The feature files, canonical flag set and control-API document are owned by
open-feature/spec. Vendoring them here made this repository a second place the
definition of conformance could drift, which is precisely what the suite exists
to prevent. They are now a pinned submodule at libs/shared/provider-tck/spec
and the copies are gone.

Adopters are unaffected, and that is the constraint the change had to respect:
the rollup asset globs copy the artifacts out of the submodule and into the
published package, so installing @openfeature/provider-tck from npm still needs
no submodule and no particular repository layout. resolveAssetDir therefore has
to satisfy two layouts -- the packaged copy next to the bundle, and the
submodule under the library root -- and tries both. The spec calls the feature
directory `gherkin`; the package keeps the name the API talks about.

Contributors do need the submodule: without it no feature file loads at all.
`nx test` and `nx package` depend on a pullSpec target that initialises it, and
CI already checks out with `submodules: recursive`. Prettier is pointed at the
submodule instead of the old vendored paths so it never rewrites artifacts that
are consumed byte for byte by every language's TCK.

The .gitattributes normalising those files to LF goes with them; the equivalent
lives upstream, where the files now do.

Pinned to dfa1658 (open-feature/spec#423).

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
…served

Found by adopting the suite in the Python OFREP provider, which is the first
stateless adopter.

The launchpad returns from /start as soon as flagd answers its readiness probe,
roughly 40ms before its file sources reach the flag store. An evaluation in
that window gets FLAG_NOT_FOUND for a flag the configuration plainly defines.

Every previous adopter is a provider that blocks during initialisation --
streaming, or syncing a ruleset -- which absorbs the window silently. A
stateless provider evaluates over HTTP with no initialisation at all, so it has
nothing to hide the warm-up behind: the first run failed 22 of 24 scenarios and
read as a catastrophically broken provider rather than as a racing testbed.

The contract said /start MUST reseed flag state, but never said it returns only
once that state is being served. Those should be the same instant. Leaving it
implicit means every stateless adopter reimplements the same defensive probe,
and the ones that do not get a suite that looks broken for reasons that have
nothing to do with the provider under test.

Part of #417

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant