Skip to content

feat: flags and scenarios for numeric precision - #392

Open
aepfli wants to merge 1 commit into
mainfrom
feat/precision-flags
Open

aepfli wants to merge 1 commit into
mainfrom
feat/precision-flags

Conversation

@aepfli

@aepfli aepfli commented Sep 11, 2026

Copy link
Copy Markdown
Member

A flag value can survive a round trip through the wrong numeric type and come back subtly wrong, and nothing in this repository notices today:

  • 2147483647 (2^31 − 1) routed through a 32-bit float returns 2147483648.
  • 9007199254740991 (2^53 − 1) through anything narrower than a double is rounded.
  • A float whose value happens to be integral, 10.0, can arrive as the integer 10 and look correct until something divides by it.

This adds three flags and scenarios in both harnesses that would catch each case.

large-integer-flag 2^31 − 1, the largest 32-bit signed integer
huge-integer-flag 2^53 − 1, the largest integer a double holds exactly
integral-float-flag 10.0, a float whose value is integral
  • flags/precision-flags.json — the provider harness. Picked up by the default configuration automatically, since that configuration is every file in flags/ combined, so the launchpad needs no change.
  • gherkin/evaluation.feature — a @precision outline for the first two cases, and a separate scenario for 2^53 − 1.
  • evaluator/flags/testkit-flags.json and a new evaluator/gherkin/precision.feature — the same coverage for the evaluator, following zero-values.feature.

Related

How this was found: running the Go flagd provider against this testbed through the new conformance suite failed four scenarios with FLAG_NOT_FOUND. Three were a naming divergence, now fixed on the specification side by adopting this repository's names. The fourth, large-integer-flag, was genuinely missing — hence this PR.

A flag value can survive a round trip through the wrong numeric type and come
back subtly wrong, and nothing here noticed. 2147483647 routed through a 32-bit
float returns 2147483648. 9007199254740991 through anything narrower than a
double is rounded. A float that happens to be integral, 10.0, can arrive as the
integer 10 and look correct until something divides by it.

So three flags, and scenarios in both harnesses that would catch each case:

  large-integer-flag    2^31 - 1, the largest 32-bit signed integer
  huge-integer-flag     2^53 - 1, the largest integer a double holds exactly
  integral-float-flag   10.0, a float whose value is integral

The flags file is picked up by the default configuration automatically, since
that configuration is every file in flags/ combined.

huge-integer-flag is tagged @large-integers separately from the other two,
because a language whose integer type is 32 bits cannot ask for that value at
all -- excluding the tag is the honest answer there, rather than a failure. The
rest are tagged @precision so a provider that cannot pass them yet can exclude
them and migrate, which is what the tagging scheme in the README is for.

The variant names are max-int32, max-safe and ten because those are the names
the OpenFeature provider conformance suite asserts (open-feature/spec#423). The
same reasoning applies as for the falsy flags: this harness and that suite
describe the same backend, and two vocabularies for one flag set is how they
drift apart. A backend serving this configuration now satisfies that suite's
precision scenarios without transcribing anything.

No coercion scenarios here. Whether a provider may return a float through an
integer accessor is unsettled in the specification (open-feature/spec#430) and
is capability-gated where it is tested, so it does not belong in a harness every
SDK runs by default.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
@aepfli
aepfli requested a review from a team as a code owner September 11, 2026 12:02
@coderabbitai

coderabbitai Bot commented Sep 11, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 49dc980b-be59-4922-ab25-364d60443276

📥 Commits

Reviewing files that changed from the base of the PR and between b308c1b and 1e37d7e.

📒 Files selected for processing (4)
  • evaluator/flags/testkit-flags.json
  • evaluator/gherkin/precision.feature
  • flags/precision-flags.json
  • gherkin/evaluation.feature

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The change adds precision-focused flag fixtures and Gherkin scenarios. Tests cover 32-bit integers, safe integers up to 9007199254740991, and integral floats. Each scenario verifies the resolved value, variant, and STATIC reason.

Changes

Numeric precision validation

Layer / File(s) Summary
Precision flag fixtures
evaluator/flags/testkit-flags.json, flags/precision-flags.json
Adds enabled flags for large integers, safe integers, and integral floats with explicit default variants.
Precision evaluation scenarios
evaluator/gherkin/precision.feature, gherkin/evaluation.feature
Adds scenarios that verify exact numeric values, variants, and STATIC resolution reasons.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Other

Suggested reviewers: toddbaert

Merge Risk: ⚪ Minimal · up to 1e37d

The precision coverage additions are internally consistent and introduce no actionable merge risk.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding flags and test scenarios for numeric precision in both harnesses.
Description check ✅ Passed The description is directly related to the changeset. It explains the numeric precision risks, identifies the added flags and scenarios, and describes the affected files and test scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

@aepfli

aepfli commented Sep 11, 2026

Copy link
Copy Markdown
Member Author

Sibling: #393 adds type-mismatch scenarios to both harnesses. The two are independent — different flags, different features, no shared lines — so either can merge first. Both came out of the same exercise: running the cross-language provider conformance suite (open-feature/spec#417) against this testbed for the first time.

aepfli added a commit to open-feature/go-sdk-contrib that referenced this pull request Sep 11, 2026
…rest

@lifecycle was withheld, and that was the expensive mistake in this
adoption rather than a cautious one. The provider implements
openfeature.StateHandler, Init builds the client and blocks until the
event stream is up or the deadline expires, and Init can and does fail
-- so the lifecycle scenarios assert something real here. Withholding
the tag made Go blind to six scenarios Java was running, which is why
Java ran 36 of the 40 and Go ran 29.

Declaring it costs nothing now. Measured, per resolver:

  flagd-rpc         40 scenarios, 6 gated skips, 34 executed,
                    33 passed, 1 failed
  flagd-in-process  40 scenarios, 5 gated skips, 35 executed,
                    34 passed, 1 failed

The one failure in each is "A large integer resolves without loss of
precision", which fails with FLAG_NOT_FOUND because large-integer-flag
is absent from flagd-testbed. open-feature/flagd-testbed#392 adds it.
It says nothing about the provider and gets no deviation entry, because
the gap is in the fixture and an entry there would attribute it to the
provider.

Two capabilities are withheld without a deviation entry, and the reason
in both cases is that the specification permits the behaviour rather
than requiring it. This is the correction this commit is really about:

@reinitialization is undeclared. "A provider that was shut down can be
initialized again" fails in both resolvers -- Shutdown clears the
initialised flag, the second Init proceeds, and what it waits for never
arrives, so it returns "provider initialization deadline exceeded".
Requirement 2.5.2 says a provider SHOULD revert to its uninitialized
state and its supporting text says "some providers MAY allow
reinitialization from this state". Permitted, not required. The
scenario is gated as of spec fc99d5ac and is now reported as skipped.
An earlier draft of this file recorded it as a known deviation against
tck.Lifecycle, which would have published a defect claim against a
provider exercising a choice the specification offers it.

@Stale stays undeclared on RPC, and that too is now a design choice
rather than a defect. Requirement 5.1.1's supporting text offers both
behaviours in one breath: a provider unable to evaluate flags "can"
signal that with PROVIDER_ERROR, and one that caches rule-sets or
evaluations "can" signal PROVIDER_STALE. The RPC resolver takes the
first -- losing the stream sends of.ProviderError directly, and the SDK
then short-circuits to the code default rather than serving cached
values. Confirmed by running rather than by reading: declaring @Stale
on RPC fails with "timed out after 15s waiting for a PROVIDER_STALE
event". The in-process resolver does emit it and passes the scenario in
12.2s, so @Stale is declared there. That two resolvers of one provider
report an outage differently is worth knowing and is written down in the
file; it is not a conformance gap.

JS and Java declare @Stale for both resolvers. On this evidence that is
a vacuous declaration for RPC, which is a reason to leave it withheld
here rather than to copy them.

One capability keeps its deviation entry, and it is worth saying why it
survives the same check: @numeric-coercion's rule is not in the
specification at all (open-feature/spec#430), but flagd accepted it for
itself in an ADR and has open-feature/flagd#1996 open to implement it.
The summary now says so, so a consumer does not read the entry as a
specification violation.

Also fixed: Config.KnownDeviations was never populated. The per-suite
knownDeviations field was collected and then dropped, because the call
site passed Capabilities and not KnownDeviations -- the quietest
possible way for a report to lose the one field that tells a defect
apart from a design choice, and it read as wired because the struct
field was filled in.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
aepfli added a commit to open-feature/go-sdk-contrib that referenced this pull request Sep 11, 2026
Running this suite at spec fc99d5ac produces 12 or 13 failures, and a
different set each time, so anyone reading a result needs to know which
of them say anything about the provider. Two do not, and the rest are
not even about the provider.

Stable, and gaps in the fixture: integral-float-flag and
large-integer-flag are absent from flagd-testbed, so the scenarios that
ask for them fail with FLAG_NOT_FOUND against any provider at all.
number-zero-flag and huge-integer-flag are missing too, though those are
gated or unasked here. open-feature/flagd-testbed#392 adds them. No
deviation entry, because the gap is in the fixture and an entry there
would attribute it to the provider.

Everything else moves between runs. Two consecutive runs gave 13 then 12
failures with almost disjoint failing sets, every one FLAG_NOT_FOUND on
a flag the testbed certainly has -- boolean-flag, string-zero-flag,
object-flag. The cause is the control API rather than this file: the
per-scenario reset calls POST /start, which stops flagd, deletes the
combined flag file, regenerates it, restarts flagd and polls
:8014/readyz -- and flagd answers readyz before its file source has
loaded the flags. A provider with a lifecycle never notices, because its
own Init blocks until the stream is up or the sync completes and the
flags have arrived by then; both flagd suites are stable against the
same backend at the same revision, which is the control. A stateless
provider evaluates the instant POST /start returns and races the load.

So POST /start returning before the backend serves flags makes the reset
unusable by precisely the providers that have no way to wait for it.
Adding a sleep or a retry here would hide that from every other
language's adoption, so it is written down and left alone.

@numeric-coercion is kept, and now deliberately rather than by inertia.
The comment claiming the lossless direction had no scenario is stale --
integral-float-flag added two, so the tag is a stronger claim than when
it was first declared. One of the two cannot be answered by this
backend, but the other two scenarios pass, and withholding the tag would
also skip the lossy one. That is the scenario worth keeping: narrowing
0.5 to 0 with no error code is the failure mode flagd has and this
provider does not.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
aepfli added a commit to open-feature/java-sdk-contrib that referenced this pull request Sep 11, 2026
…s not a deviation

Three of the flags the suite's assets added are absent from flagd-testbed v3.8.0:
large-integer-flag, huge-integer-flag and integral-float-flag. Only the first is
reached by a scenario that runs here -- huge-integer-flag is asked for solely
under @large-integers, which is not applicable in Java, and integral-float-flag
solely under @numeric-coercion, which this provider withholds -- so exactly one
untagged scenario, the 32-bit precision one, fails FLAG_NOT_FOUND in both modes.
open-feature/flagd-testbed#392 is open for it; the Compose tag gets bumped when
it lands, which is why the note lives next to the tag as well as in the class.

A missing flag is a gap in the stack, not in the provider, so it is documented
rather than declared as a KnownDeviation. A deviation says the provider is wrong,
and the provider was never given the flag to get wrong.

The three falsy flags used to fail the same way and no longer do, which is worth
writing down because the failure looked identical. The testbed's zero-flags.json
already served boolean-zero-flag, integer-zero-flag and string-zero-flag with
zero/non-zero variants, while the canonical set called them false-flag,
zero-flag and empty-string-flag; the base moved the canonical names onto the
testbed's rather than the other way round, so those three scenarios now resolve
against flags that were always there.

Also says why capabilities() calls declarableExcept rather than
EnumSet.complementOf, which now matters more than it did: the complement would
claim @large-integers as well as the two reserved tags, and the suite refuses
that declaration at startup.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
aepfli added a commit to open-feature/java-sdk-contrib that referenced this pull request Sep 11, 2026
…s not a deviation

Three of the flags the suite's assets added are absent from flagd-testbed v3.8.0:
large-integer-flag, huge-integer-flag and integral-float-flag. Only the first is
reached by a scenario that runs here -- huge-integer-flag is asked for solely
under @large-integers, which is not applicable in Java, and integral-float-flag
solely under @numeric-coercion, which this provider withholds -- so exactly one
untagged scenario, the 32-bit precision one, fails FLAG_NOT_FOUND in both modes.
open-feature/flagd-testbed#392 is open for it; the Compose tag gets bumped when
it lands, which is why the note lives next to the tag as well as in the class.

A missing flag is a gap in the stack, not in the provider, so it is documented
rather than declared as a KnownDeviation. A deviation says the provider is wrong,
and the provider was never given the flag to get wrong.

The three falsy flags used to fail the same way and no longer do, which is worth
writing down because the failure looked identical. The testbed's zero-flags.json
already served boolean-zero-flag, integer-zero-flag and string-zero-flag with
zero/non-zero variants, while the canonical set called them false-flag,
zero-flag and empty-string-flag; the base moved the canonical names onto the
testbed's rather than the other way round, so those three scenarios now resolve
against flags that were always there.

Also says why capabilities() calls declarableExcept rather than
EnumSet.complementOf, which now matters more than it did: the complement would
claim @large-integers as well as the two reserved tags, and the suite refuses
that declaration at startup.

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

@lifecycle was withheld, and that was the expensive mistake in this
adoption rather than a cautious one. The provider implements
openfeature.StateHandler, Init builds the client and blocks until the
event stream is up or the deadline expires, and Init can and does fail
-- so the lifecycle scenarios assert something real here. Withholding
the tag made Go blind to six scenarios Java was running, which is why
Java ran 36 of the 40 and Go ran 29.

Declaring it costs nothing now. Measured, per resolver:

  flagd-rpc         40 scenarios, 6 gated skips, 34 executed,
                    33 passed, 1 failed
  flagd-in-process  40 scenarios, 5 gated skips, 35 executed,
                    34 passed, 1 failed

The one failure in each is "A large integer resolves without loss of
precision", which fails with FLAG_NOT_FOUND because large-integer-flag
is absent from flagd-testbed. open-feature/flagd-testbed#392 adds it.
It says nothing about the provider and gets no deviation entry, because
the gap is in the fixture and an entry there would attribute it to the
provider.

Two capabilities are withheld without a deviation entry, and the reason
in both cases is that the specification permits the behaviour rather
than requiring it. This is the correction this commit is really about:

@reinitialization is undeclared. "A provider that was shut down can be
initialized again" fails in both resolvers -- Shutdown clears the
initialised flag, the second Init proceeds, and what it waits for never
arrives, so it returns "provider initialization deadline exceeded".
Requirement 2.5.2 says a provider SHOULD revert to its uninitialized
state and its supporting text says "some providers MAY allow
reinitialization from this state". Permitted, not required. The
scenario is gated as of spec fc99d5ac and is now reported as skipped.
An earlier draft of this file recorded it as a known deviation against
tck.Lifecycle, which would have published a defect claim against a
provider exercising a choice the specification offers it.

@Stale stays undeclared on RPC, and that too is now a design choice
rather than a defect. Requirement 5.1.1's supporting text offers both
behaviours in one breath: a provider unable to evaluate flags "can"
signal that with PROVIDER_ERROR, and one that caches rule-sets or
evaluations "can" signal PROVIDER_STALE. The RPC resolver takes the
first -- losing the stream sends of.ProviderError directly, and the SDK
then short-circuits to the code default rather than serving cached
values. Confirmed by running rather than by reading: declaring @Stale
on RPC fails with "timed out after 15s waiting for a PROVIDER_STALE
event". The in-process resolver does emit it and passes the scenario in
12.2s, so @Stale is declared there. That two resolvers of one provider
report an outage differently is worth knowing and is written down in the
file; it is not a conformance gap.

JS and Java declare @Stale for both resolvers. On this evidence that is
a vacuous declaration for RPC, which is a reason to leave it withheld
here rather than to copy them.

One capability keeps its deviation entry, and it is worth saying why it
survives the same check: @numeric-coercion's rule is not in the
specification at all (open-feature/spec#430), but flagd accepted it for
itself in an ADR and has open-feature/flagd#1996 open to implement it.
The summary now says so, so a consumer does not read the entry as a
specification violation.

Also fixed: Config.KnownDeviations was never populated. The per-suite
knownDeviations field was collected and then dropped, because the call
site passed Capabilities and not KnownDeviations -- the quietest
possible way for a report to lose the one field that tells a defect
apart from a design choice, and it read as wired because the struct
field was filled in.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
aepfli added a commit to open-feature/go-sdk-contrib that referenced this pull request Sep 11, 2026
Running this suite at spec fc99d5ac produces 12 or 13 failures, and a
different set each time, so anyone reading a result needs to know which
of them say anything about the provider. Two do not, and the rest are
not even about the provider.

Stable, and gaps in the fixture: integral-float-flag and
large-integer-flag are absent from flagd-testbed, so the scenarios that
ask for them fail with FLAG_NOT_FOUND against any provider at all.
number-zero-flag and huge-integer-flag are missing too, though those are
gated or unasked here. open-feature/flagd-testbed#392 adds them. No
deviation entry, because the gap is in the fixture and an entry there
would attribute it to the provider.

Everything else moves between runs. Two consecutive runs gave 13 then 12
failures with almost disjoint failing sets, every one FLAG_NOT_FOUND on
a flag the testbed certainly has -- boolean-flag, string-zero-flag,
object-flag. The cause is the control API rather than this file: the
per-scenario reset calls POST /start, which stops flagd, deletes the
combined flag file, regenerates it, restarts flagd and polls
:8014/readyz -- and flagd answers readyz before its file source has
loaded the flags. A provider with a lifecycle never notices, because its
own Init blocks until the stream is up or the sync completes and the
flags have arrived by then; both flagd suites are stable against the
same backend at the same revision, which is the control. A stateless
provider evaluates the instant POST /start returns and races the load.

So POST /start returning before the backend serves flags makes the reset
unusable by precisely the providers that have no way to wait for it.
Adding a sleep or a retry here would hide that from every other
language's adoption, so it is written down and left alone.

@numeric-coercion is kept, and now deliberately rather than by inertia.
The comment claiming the lossless direction had no scenario is stale --
integral-float-flag added two, so the tag is a stronger claim than when
it was first declared. One of the two cannot be answered by this
backend, but the other two scenarios pass, and withholding the tag would
also skip the lossy one. That is the scenario worth keeping: narrowing
0.5 to 0 with no error code is the failure mode flagd has and this
provider does not.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
aepfli added a commit to open-feature/java-sdk-contrib that referenced this pull request Sep 11, 2026
…s not a deviation

Three of the flags the suite's assets added are absent from flagd-testbed v3.8.0:
large-integer-flag, huge-integer-flag and integral-float-flag. Only the first is
reached by a scenario that runs here -- huge-integer-flag is asked for solely
under @large-integers, which is not applicable in Java, and integral-float-flag
solely under @numeric-coercion, which this provider withholds -- so exactly one
untagged scenario, the 32-bit precision one, fails FLAG_NOT_FOUND in both modes.
open-feature/flagd-testbed#392 is open for it; the Compose tag gets bumped when
it lands, which is why the note lives next to the tag as well as in the class.

A missing flag is a gap in the stack, not in the provider, so it is documented
rather than declared as a KnownDeviation. A deviation says the provider is wrong,
and the provider was never given the flag to get wrong.

The three falsy flags used to fail the same way and no longer do, which is worth
writing down because the failure looked identical. The testbed's zero-flags.json
already served boolean-zero-flag, integer-zero-flag and string-zero-flag with
zero/non-zero variants, while the canonical set called them false-flag,
zero-flag and empty-string-flag; the base moved the canonical names onto the
testbed's rather than the other way round, so those three scenarios now resolve
against flags that were always there.

Also says why capabilities() calls declarableExcept rather than
EnumSet.complementOf, which now matters more than it did: the complement would
claim @large-integers as well as the two reserved tags, and the suite refuses
that declaration at startup.

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

@lifecycle was withheld, and that was the expensive mistake in this
adoption rather than a cautious one. The provider implements
openfeature.StateHandler, Init builds the client and blocks until the
event stream is up or the deadline expires, and Init can and does fail
-- so the lifecycle scenarios assert something real here. Withholding
the tag made Go blind to six scenarios Java was running, which is why
Java ran 36 of the 40 and Go ran 29.

Declaring it costs nothing now. Measured, per resolver:

  flagd-rpc         40 scenarios, 6 gated skips, 34 executed,
                    33 passed, 1 failed
  flagd-in-process  40 scenarios, 5 gated skips, 35 executed,
                    34 passed, 1 failed

The one failure in each is "A large integer resolves without loss of
precision", which fails with FLAG_NOT_FOUND because large-integer-flag
is absent from flagd-testbed. open-feature/flagd-testbed#392 adds it.
It says nothing about the provider and gets no deviation entry, because
the gap is in the fixture and an entry there would attribute it to the
provider.

Two capabilities are withheld without a deviation entry, and the reason
in both cases is that the specification permits the behaviour rather
than requiring it. This is the correction this commit is really about:

@reinitialization is undeclared. "A provider that was shut down can be
initialized again" fails in both resolvers -- Shutdown clears the
initialised flag, the second Init proceeds, and what it waits for never
arrives, so it returns "provider initialization deadline exceeded".
Requirement 2.5.2 says a provider SHOULD revert to its uninitialized
state and its supporting text says "some providers MAY allow
reinitialization from this state". Permitted, not required. The
scenario is gated as of spec fc99d5ac and is now reported as skipped.
An earlier draft of this file recorded it as a known deviation against
tck.Lifecycle, which would have published a defect claim against a
provider exercising a choice the specification offers it.

@Stale stays undeclared on RPC, and that too is now a design choice
rather than a defect. Requirement 5.1.1's supporting text offers both
behaviours in one breath: a provider unable to evaluate flags "can"
signal that with PROVIDER_ERROR, and one that caches rule-sets or
evaluations "can" signal PROVIDER_STALE. The RPC resolver takes the
first -- losing the stream sends of.ProviderError directly, and the SDK
then short-circuits to the code default rather than serving cached
values. Confirmed by running rather than by reading: declaring @Stale
on RPC fails with "timed out after 15s waiting for a PROVIDER_STALE
event". The in-process resolver does emit it and passes the scenario in
12.2s, so @Stale is declared there. That two resolvers of one provider
report an outage differently is worth knowing and is written down in the
file; it is not a conformance gap.

JS and Java declare @Stale for both resolvers. On this evidence that is
a vacuous declaration for RPC, which is a reason to leave it withheld
here rather than to copy them.

One capability keeps its deviation entry, and it is worth saying why it
survives the same check: @numeric-coercion's rule is not in the
specification at all (open-feature/spec#430), but flagd accepted it for
itself in an ADR and has open-feature/flagd#1996 open to implement it.
The summary now says so, so a consumer does not read the entry as a
specification violation.

Also fixed: Config.KnownDeviations was never populated. The per-suite
knownDeviations field was collected and then dropped, because the call
site passed Capabilities and not KnownDeviations -- the quietest
possible way for a report to lose the one field that tells a defect
apart from a design choice, and it read as wired because the struct
field was filled in.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
aepfli added a commit to open-feature/go-sdk-contrib that referenced this pull request Sep 11, 2026
Running this suite at spec fc99d5ac produces 12 or 13 failures, and a
different set each time, so anyone reading a result needs to know which
of them say anything about the provider. Two do not, and the rest are
not even about the provider.

Stable, and gaps in the fixture: integral-float-flag and
large-integer-flag are absent from flagd-testbed, so the scenarios that
ask for them fail with FLAG_NOT_FOUND against any provider at all.
number-zero-flag and huge-integer-flag are missing too, though those are
gated or unasked here. open-feature/flagd-testbed#392 adds them. No
deviation entry, because the gap is in the fixture and an entry there
would attribute it to the provider.

Everything else moves between runs. Two consecutive runs gave 13 then 12
failures with almost disjoint failing sets, every one FLAG_NOT_FOUND on
a flag the testbed certainly has -- boolean-flag, string-zero-flag,
object-flag. The cause is the control API rather than this file: the
per-scenario reset calls POST /start, which stops flagd, deletes the
combined flag file, regenerates it, restarts flagd and polls
:8014/readyz -- and flagd answers readyz before its file source has
loaded the flags. A provider with a lifecycle never notices, because its
own Init blocks until the stream is up or the sync completes and the
flags have arrived by then; both flagd suites are stable against the
same backend at the same revision, which is the control. A stateless
provider evaluates the instant POST /start returns and races the load.

So POST /start returning before the backend serves flags makes the reset
unusable by precisely the providers that have no way to wait for it.
Adding a sleep or a retry here would hide that from every other
language's adoption, so it is written down and left alone.

@numeric-coercion is kept, and now deliberately rather than by inertia.
The comment claiming the lossless direction had no scenario is stale --
integral-float-flag added two, so the tag is a stronger claim than when
it was first declared. One of the two cannot be answered by this
backend, but the other two scenarios pass, and withholding the tag would
also skip the lossy one. That is the scenario worth keeping: narrowing
0.5 to 0 with no error code is the failure mode flagd has and this
provider does not.

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

@lifecycle was withheld, and that was the expensive mistake in this
adoption rather than a cautious one. The provider implements
openfeature.StateHandler, Init builds the client and blocks until the
event stream is up or the deadline expires, and Init can and does fail
-- so the lifecycle scenarios assert something real here. Withholding
the tag made Go blind to six scenarios Java was running, which is why
Java ran 36 of the 40 and Go ran 29.

Declaring it costs nothing now. Measured, per resolver:

  flagd-rpc         40 scenarios, 6 gated skips, 34 executed,
                    33 passed, 1 failed
  flagd-in-process  40 scenarios, 5 gated skips, 35 executed,
                    34 passed, 1 failed

The one failure in each is "A large integer resolves without loss of
precision", which fails with FLAG_NOT_FOUND because large-integer-flag
is absent from flagd-testbed. open-feature/flagd-testbed#392 adds it.
It says nothing about the provider and gets no deviation entry, because
the gap is in the fixture and an entry there would attribute it to the
provider.

Two capabilities are withheld without a deviation entry, and the reason
in both cases is that the specification permits the behaviour rather
than requiring it. This is the correction this commit is really about:

@reinitialization is undeclared. "A provider that was shut down can be
initialized again" fails in both resolvers -- Shutdown clears the
initialised flag, the second Init proceeds, and what it waits for never
arrives, so it returns "provider initialization deadline exceeded".
Requirement 2.5.2 says a provider SHOULD revert to its uninitialized
state and its supporting text says "some providers MAY allow
reinitialization from this state". Permitted, not required. The
scenario is gated as of spec fc99d5ac and is now reported as skipped.
An earlier draft of this file recorded it as a known deviation against
tck.Lifecycle, which would have published a defect claim against a
provider exercising a choice the specification offers it.

@Stale stays undeclared on RPC, and that too is now a design choice
rather than a defect. Requirement 5.1.1's supporting text offers both
behaviours in one breath: a provider unable to evaluate flags "can"
signal that with PROVIDER_ERROR, and one that caches rule-sets or
evaluations "can" signal PROVIDER_STALE. The RPC resolver takes the
first -- losing the stream sends of.ProviderError directly, and the SDK
then short-circuits to the code default rather than serving cached
values. Confirmed by running rather than by reading: declaring @Stale
on RPC fails with "timed out after 15s waiting for a PROVIDER_STALE
event". The in-process resolver does emit it and passes the scenario in
12.2s, so @Stale is declared there. That two resolvers of one provider
report an outage differently is worth knowing and is written down in the
file; it is not a conformance gap.

JS and Java declare @Stale for both resolvers. On this evidence that is
a vacuous declaration for RPC, which is a reason to leave it withheld
here rather than to copy them.

One capability keeps its deviation entry, and it is worth saying why it
survives the same check: @numeric-coercion's rule is not in the
specification at all (open-feature/spec#430), but flagd accepted it for
itself in an ADR and has open-feature/flagd#1996 open to implement it.
The summary now says so, so a consumer does not read the entry as a
specification violation.

Also fixed: Config.KnownDeviations was never populated. The per-suite
knownDeviations field was collected and then dropped, because the call
site passed Capabilities and not KnownDeviations -- the quietest
possible way for a report to lose the one field that tells a defect
apart from a design choice, and it read as wired because the struct
field was filled in.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
aepfli added a commit to open-feature/go-sdk-contrib that referenced this pull request Sep 11, 2026
Running this suite at spec fc99d5ac produces 12 or 13 failures, and a
different set each time, so anyone reading a result needs to know which
of them say anything about the provider. Two do not, and the rest are
not even about the provider.

Stable, and gaps in the fixture: integral-float-flag and
large-integer-flag are absent from flagd-testbed, so the scenarios that
ask for them fail with FLAG_NOT_FOUND against any provider at all.
number-zero-flag and huge-integer-flag are missing too, though those are
gated or unasked here. open-feature/flagd-testbed#392 adds them. No
deviation entry, because the gap is in the fixture and an entry there
would attribute it to the provider.

Everything else moves between runs. Two consecutive runs gave 13 then 12
failures with almost disjoint failing sets, every one FLAG_NOT_FOUND on
a flag the testbed certainly has -- boolean-flag, string-zero-flag,
object-flag. The cause is the control API rather than this file: the
per-scenario reset calls POST /start, which stops flagd, deletes the
combined flag file, regenerates it, restarts flagd and polls
:8014/readyz -- and flagd answers readyz before its file source has
loaded the flags. A provider with a lifecycle never notices, because its
own Init blocks until the stream is up or the sync completes and the
flags have arrived by then; both flagd suites are stable against the
same backend at the same revision, which is the control. A stateless
provider evaluates the instant POST /start returns and races the load.

So POST /start returning before the backend serves flags makes the reset
unusable by precisely the providers that have no way to wait for it.
Adding a sleep or a retry here would hide that from every other
language's adoption, so it is written down and left alone.

@numeric-coercion is kept, and now deliberately rather than by inertia.
The comment claiming the lossless direction had no scenario is stale --
integral-float-flag added two, so the tag is a stronger claim than when
it was first declared. One of the two cannot be answered by this
backend, but the other two scenarios pass, and withholding the tag would
also skip the lossy one. That is the scenario worth keeping: narrowing
0.5 to 0 with no error code is the failure mode flagd has and this
provider does not.

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

@lifecycle was withheld, and that was the expensive mistake in this
adoption rather than a cautious one. The provider implements
openfeature.StateHandler, Init builds the client and blocks until the
event stream is up or the deadline expires, and Init can and does fail
-- so the lifecycle scenarios assert something real here. Withholding
the tag made Go blind to six scenarios Java was running, which is why
Java ran 36 of the 40 and Go ran 29.

Declaring it costs nothing now. Measured, per resolver:

  flagd-rpc         40 scenarios, 6 gated skips, 34 executed,
                    33 passed, 1 failed
  flagd-in-process  40 scenarios, 5 gated skips, 35 executed,
                    34 passed, 1 failed

The one failure in each is "A large integer resolves without loss of
precision", which fails with FLAG_NOT_FOUND because large-integer-flag
is absent from flagd-testbed. open-feature/flagd-testbed#392 adds it.
It says nothing about the provider and gets no deviation entry, because
the gap is in the fixture and an entry there would attribute it to the
provider.

Two capabilities are withheld without a deviation entry, and the reason
in both cases is that the specification permits the behaviour rather
than requiring it. This is the correction this commit is really about:

@reinitialization is undeclared. "A provider that was shut down can be
initialized again" fails in both resolvers -- Shutdown clears the
initialised flag, the second Init proceeds, and what it waits for never
arrives, so it returns "provider initialization deadline exceeded".
Requirement 2.5.2 says a provider SHOULD revert to its uninitialized
state and its supporting text says "some providers MAY allow
reinitialization from this state". Permitted, not required. The
scenario is gated as of spec fc99d5ac and is now reported as skipped.
An earlier draft of this file recorded it as a known deviation against
tck.Lifecycle, which would have published a defect claim against a
provider exercising a choice the specification offers it.

@Stale stays undeclared on RPC, and that too is now a design choice
rather than a defect. Requirement 5.1.1's supporting text offers both
behaviours in one breath: a provider unable to evaluate flags "can"
signal that with PROVIDER_ERROR, and one that caches rule-sets or
evaluations "can" signal PROVIDER_STALE. The RPC resolver takes the
first -- losing the stream sends of.ProviderError directly, and the SDK
then short-circuits to the code default rather than serving cached
values. Confirmed by running rather than by reading: declaring @Stale
on RPC fails with "timed out after 15s waiting for a PROVIDER_STALE
event". The in-process resolver does emit it and passes the scenario in
12.2s, so @Stale is declared there. That two resolvers of one provider
report an outage differently is worth knowing and is written down in the
file; it is not a conformance gap.

JS and Java declare @Stale for both resolvers. On this evidence that is
a vacuous declaration for RPC, which is a reason to leave it withheld
here rather than to copy them.

One capability keeps its deviation entry, and it is worth saying why it
survives the same check: @numeric-coercion's rule is not in the
specification at all (open-feature/spec#430), but flagd accepted it for
itself in an ADR and has open-feature/flagd#1996 open to implement it.
The summary now says so, so a consumer does not read the entry as a
specification violation.

Also fixed: Config.KnownDeviations was never populated. The per-suite
knownDeviations field was collected and then dropped, because the call
site passed Capabilities and not KnownDeviations -- the quietest
possible way for a report to lose the one field that tells a defect
apart from a design choice, and it read as wired because the struct
field was filled in.

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

Follows the TCK's move to spec 26362f85. The e2e module reaches the TCK through
a replace, and a replace does not bring the replacement's requirements with it,
so this module's own pin is what selects the Gherkin the suite actually runs --
leaving it at fc99d5ac would have run the previous revision's assets against the
new vocabulary and reported a spec revision this module does not use.

Both capabilities are declared because the scenarios passed, not because flagd
plausibly supports them. The run is the whole justification, so the numbers:
each resolver goes from 40 canonical scenarios to 52, and each reports 50 passed
and 2 failed.

@Variants passes seven of its eight rows in both resolvers. The variant name
survives the trip from the ruleset through the wire format into ResolutionDetail
for booleans, strings, integers, floats and all three falsy flags. The eighth
row asks large-integer-flag for its max-int32 variant and receives "", because
that flag is absent from flagd-testbed -- the same fixture gap that already
fails the untagged large-integer scenario, and the reason tck.LargeIntegers
stays undeclared. Withholding the tag over it would skip seven working rows to
hide one missing flag, so the comment on the capability list now says the suite
carries two fixture failures rather than one. open-feature/flagd-testbed#392
adds the flags and both go away together; neither gets a knownDeviations entry,
because the gap is in the fixture and an entry there would attribute it to the
provider.

@targeting was reserved rather than declarable until this spec revision, on the
reasoning that asserting anything about the evaluation context needed an echo
endpoint on the control API. It does not: targeting-key-flag carries one
JsonLogic rule on the targeting key, so a matching context resolves to a
different value than a non-matching one or none at all, and a provider that
dropped the context is caught by the resolved value itself. All three scenarios
pass in both resolvers, and running both was not a formality -- in-process
evaluates the rule itself while RPC has flagd evaluate it, so agreement between
them is evidence rather than duplication. The flag has been in flagd-testbed
since flagd-testbed#103, released in v0.5.1 in February 2024, so unlike
tck.LargeIntegers this needs no image bump.

The new mandatory scenario that supplies an evaluation context to an untargeted
flag passes in both resolvers too, which is worth stating because it is the half
of requirement 2.2.1 that nothing here was testing: every resolve method takes
a context, no scenario supplied one, and a provider that threw on any context or
serialised it into a malformed request would have passed the whole suite.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
aepfli added a commit to open-feature/go-sdk-contrib that referenced this pull request Sep 12, 2026
Running this suite at spec fc99d5ac produces 12 or 13 failures, and a
different set each time, so anyone reading a result needs to know which
of them say anything about the provider. Two do not, and the rest are
not even about the provider.

Stable, and gaps in the fixture: integral-float-flag and
large-integer-flag are absent from flagd-testbed, so the scenarios that
ask for them fail with FLAG_NOT_FOUND against any provider at all.
number-zero-flag and huge-integer-flag are missing too, though those are
gated or unasked here. open-feature/flagd-testbed#392 adds them. No
deviation entry, because the gap is in the fixture and an entry there
would attribute it to the provider.

Everything else moves between runs. Two consecutive runs gave 13 then 12
failures with almost disjoint failing sets, every one FLAG_NOT_FOUND on
a flag the testbed certainly has -- boolean-flag, string-zero-flag,
object-flag. The cause is the control API rather than this file: the
per-scenario reset calls POST /start, which stops flagd, deletes the
combined flag file, regenerates it, restarts flagd and polls
:8014/readyz -- and flagd answers readyz before its file source has
loaded the flags. A provider with a lifecycle never notices, because its
own Init blocks until the stream is up or the sync completes and the
flags have arrived by then; both flagd suites are stable against the
same backend at the same revision, which is the control. A stateless
provider evaluates the instant POST /start returns and races the load.

So POST /start returning before the backend serves flags makes the reset
unusable by precisely the providers that have no way to wait for it.
Adding a sleep or a retry here would hide that from every other
language's adoption, so it is written down and left alone.

@numeric-coercion is kept, and now deliberately rather than by inertia.
The comment claiming the lossless direction had no scenario is stale --
integral-float-flag added two, so the tag is a stronger claim than when
it was first declared. One of the two cannot be answered by this
backend, but the other two scenarios pass, and withholding the tag would
also skip the lossy one. That is the scenario worth keeping: narrowing
0.5 to 0 with no error code is the failure mode flagd has and this
provider does not.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
aepfli added a commit to open-feature/go-sdk-contrib that referenced this pull request Sep 12, 2026
This module reaches the TCK through a replace, and a replace does not bring the
replacement's requirements with it, so its own requirement is what selects the
Gherkin the suite runs. Left at fc99d5ac it would have run the previous
revision's assets while reporting conformance against a revision it does not
use -- the drift the flagd e2e module has the same one-line guard against.

Both capabilities are declared on the run rather than on the code. The suite is
documented above as non-deterministic, because POST /start on the launchpad
returns before flagd serves flags and a stateless provider races the load, so
one green run would not be evidence of anything. Three consecutive runs gave
the identical result: 52 scenarios, 9 skipped as undeclared, 43 run, 40 passed,
3 failed, with the same three failures every time.

@Variants passes seven of its eight rows. OFREP's evaluation response carries a
variant field and this provider passes it straight into ResolutionDetail, for
booleans, strings, integers, floats and all three falsy flags. The eighth row
asks large-integer-flag for its max-int32 variant and receives "", because that
flag is absent from flagd-testbed -- the same absence that already fails the
untagged large-integer scenario, now counted twice rather than a new defect. The
third stable failure is integral-float-flag, absent for the same reason. All
three are fixture gaps, all three go green with
open-feature/flagd-testbed#392, and none gets a knownDeviations entry, because
an entry there would attribute the fixture's gap to the provider.

@targeting was reserved rather than declarable until this spec revision. For a
JSON-over-HTTP provider it is the cheapest capability here to get right, because
the evaluation context IS the request body and there is no separate passthrough
path to get wrong. All three scenarios pass -- targeting-key-flag resolves to
"hit" for the matching targeting key and "miss" for a non-matching one or none
at all -- and so does the new untagged scenario that supplies a context to an
untargeted flag. Worth having precisely because of how this provider is built:
until this revision no scenario supplied a context at all, so one that
serialised it into a malformed body would have passed the whole suite.

The README's capability table gains rows for both, and its counts are corrected
while they are being touched: it claimed 24 of 29 scenarios and that @events
gated lifecycle.feature, when @lifecycle now does and @lifecycle and
@large-integers were missing from the table entirely. Its "Known asymmetry"
section claimed no scenario covers integer-flag requested as a float; one does
now, it is declared under @numeric-coercion, and it passes -- so the asymmetry
is deliberate rather than untested.

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

The tag was declared and two of its three scenarios failed. errors.feature says
a declarer must satisfy all three, and says why: the two lossless rows exist to
catch the shortcut of rejecting every float. This provider takes that shortcut.

json.loads yields `int` for 10 and `float` for 0.5, and the check at
ofrep/__init__.py:249-256 admits a value only on an exact isinstance against one
of them. Nothing in that path widens or narrows a number. So the lossy row
passes -- float-flag asked for as an Integer is a TYPE_MISMATCH rather than a
silent 0 -- and integer-flag asked for as a Float fails, because 10 is not an
instance of float.

That is the same architecture as the Java OFREP adoption, which withholds the
tag for the same reason. The Go adoption declares it, and the difference is the
JSON decoder rather than a decision either author made: encoding/json makes
every JSON number a float64, so integer-ness never survives the wire and
ResolveInt has to round-trip through int64 -- which yields lossless coercion,
and TYPE_MISMATCH on loss, for free. Over OFREP this capability follows the
language's JSON library.

No knownDeviation accompanies the withdrawal. A deviation records a gap in
behaviour a provider is required to have, and numeric coercion is a declared
capability rather than a requirement. The honest record is the undeclared tag
and the three skips it produces.

Measured before and after, 52 scenarios both times: 4 failed / 38 passed /
9 skipped / 1 xfailed becomes 2 failed / 37 passed / 12 skipped / 1 xfailed. The
two remaining failures are both large-integer-flag, which the pinned testbed
does not serve (open-feature/flagd-testbed#392).

The header also claimed every declared capability had been seen to pass, which
was untrue while this tag was declared, and the closing note claimed the withheld
set matched Go's as well as Java's. Both corrected.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
aepfli added a commit to open-feature/go-sdk-contrib that referenced this pull request Sep 12, 2026
Running this suite at spec fc99d5ac produces 12 or 13 failures, and a
different set each time, so anyone reading a result needs to know which
of them say anything about the provider. Two do not, and the rest are
not even about the provider.

Stable, and gaps in the fixture: integral-float-flag and
large-integer-flag are absent from flagd-testbed, so the scenarios that
ask for them fail with FLAG_NOT_FOUND against any provider at all.
number-zero-flag and huge-integer-flag are missing too, though those are
gated or unasked here. open-feature/flagd-testbed#392 adds them. No
deviation entry, because the gap is in the fixture and an entry there
would attribute it to the provider.

Everything else moves between runs. Two consecutive runs gave 13 then 12
failures with almost disjoint failing sets, every one FLAG_NOT_FOUND on
a flag the testbed certainly has -- boolean-flag, string-zero-flag,
object-flag. The cause is the control API rather than this file: the
per-scenario reset calls POST /start, which stops flagd, deletes the
combined flag file, regenerates it, restarts flagd and polls
:8014/readyz -- and flagd answers readyz before its file source has
loaded the flags. A provider with a lifecycle never notices, because its
own Init blocks until the stream is up or the sync completes and the
flags have arrived by then; both flagd suites are stable against the
same backend at the same revision, which is the control. A stateless
provider evaluates the instant POST /start returns and races the load.

So POST /start returning before the backend serves flags makes the reset
unusable by precisely the providers that have no way to wait for it.
Adding a sleep or a retry here would hide that from every other
language's adoption, so it is written down and left alone.

@numeric-coercion is kept, and now deliberately rather than by inertia.
The comment claiming the lossless direction had no scenario is stale --
integral-float-flag added two, so the tag is a stronger claim than when
it was first declared. One of the two cannot be answered by this
backend, but the other two scenarios pass, and withholding the tag would
also skip the lossy one. That is the scenario worth keeping: narrowing
0.5 to 0 with no error code is the failure mode flagd has and this
provider does not.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
aepfli added a commit to open-feature/go-sdk-contrib that referenced this pull request Sep 12, 2026
This module reaches the TCK through a replace, and a replace does not bring the
replacement's requirements with it, so its own requirement is what selects the
Gherkin the suite runs. Left at fc99d5ac it would have run the previous
revision's assets while reporting conformance against a revision it does not
use -- the drift the flagd e2e module has the same one-line guard against.

Both capabilities are declared on the run rather than on the code. The suite is
documented above as non-deterministic, because POST /start on the launchpad
returns before flagd serves flags and a stateless provider races the load, so
one green run would not be evidence of anything. Three consecutive runs gave
the identical result: 52 scenarios, 9 skipped as undeclared, 43 run, 40 passed,
3 failed, with the same three failures every time.

@Variants passes seven of its eight rows. OFREP's evaluation response carries a
variant field and this provider passes it straight into ResolutionDetail, for
booleans, strings, integers, floats and all three falsy flags. The eighth row
asks large-integer-flag for its max-int32 variant and receives "", because that
flag is absent from flagd-testbed -- the same absence that already fails the
untagged large-integer scenario, now counted twice rather than a new defect. The
third stable failure is integral-float-flag, absent for the same reason. All
three are fixture gaps, all three go green with
open-feature/flagd-testbed#392, and none gets a knownDeviations entry, because
an entry there would attribute the fixture's gap to the provider.

@targeting was reserved rather than declarable until this spec revision. For a
JSON-over-HTTP provider it is the cheapest capability here to get right, because
the evaluation context IS the request body and there is no separate passthrough
path to get wrong. All three scenarios pass -- targeting-key-flag resolves to
"hit" for the matching targeting key and "miss" for a non-matching one or none
at all -- and so does the new untagged scenario that supplies a context to an
untargeted flag. Worth having precisely because of how this provider is built:
until this revision no scenario supplied a context at all, so one that
serialised it into a malformed body would have passed the whole suite.

The README's capability table gains rows for both, and its counts are corrected
while they are being touched: it claimed 24 of 29 scenarios and that @events
gated lifecycle.feature, when @lifecycle now does and @lifecycle and
@large-integers were missing from the table entirely. Its "Known asymmetry"
section claimed no scenario covers integer-flag requested as a float; one does
now, it is declared under @numeric-coercion, and it passes -- so the asymmetry
is deliberate rather than untested.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
aepfli added a commit to open-feature/java-sdk-contrib that referenced this pull request Sep 12, 2026
…s not a deviation

Three of the flags the suite's assets added are absent from flagd-testbed v3.8.0:
large-integer-flag, huge-integer-flag and integral-float-flag. Only the first is
reached by a scenario that runs here -- huge-integer-flag is asked for solely
under @large-integers, which is not applicable in Java, and integral-float-flag
solely under @numeric-coercion, which this provider withholds -- so exactly one
untagged scenario, the 32-bit precision one, fails FLAG_NOT_FOUND in both modes.
open-feature/flagd-testbed#392 is open for it; the Compose tag gets bumped when
it lands, which is why the note lives next to the tag as well as in the class.

A missing flag is a gap in the stack, not in the provider, so it is documented
rather than declared as a KnownDeviation. A deviation says the provider is wrong,
and the provider was never given the flag to get wrong.

The three falsy flags used to fail the same way and no longer do, which is worth
writing down because the failure looked identical. The testbed's zero-flags.json
already served boolean-zero-flag, integer-zero-flag and string-zero-flag with
zero/non-zero variants, while the canonical set called them false-flag,
zero-flag and empty-string-flag; the base moved the canonical names onto the
testbed's rather than the other way round, so those three scenarios now resolve
against flags that were always there.

Also says why capabilities() calls declarableExcept rather than
EnumSet.complementOf, which now matters more than it did: the complement would
claim @large-integers as well as the two reserved tags, and the suite refuses
that declaration at startup.

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

The tag was declared and two of its three scenarios failed. errors.feature says
a declarer must satisfy all three, and says why: the two lossless rows exist to
catch the shortcut of rejecting every float. This provider takes that shortcut.

json.loads yields `int` for 10 and `float` for 0.5, and the check at
ofrep/__init__.py:249-256 admits a value only on an exact isinstance against one
of them. Nothing in that path widens or narrows a number. So the lossy row
passes -- float-flag asked for as an Integer is a TYPE_MISMATCH rather than a
silent 0 -- and integer-flag asked for as a Float fails, because 10 is not an
instance of float.

That is the same architecture as the Java OFREP adoption, which withholds the
tag for the same reason. The Go adoption declares it, and the difference is the
JSON decoder rather than a decision either author made: encoding/json makes
every JSON number a float64, so integer-ness never survives the wire and
ResolveInt has to round-trip through int64 -- which yields lossless coercion,
and TYPE_MISMATCH on loss, for free. Over OFREP this capability follows the
language's JSON library.

No knownDeviation accompanies the withdrawal. A deviation records a gap in
behaviour a provider is required to have, and numeric coercion is a declared
capability rather than a requirement. The honest record is the undeclared tag
and the three skips it produces.

Measured before and after, 52 scenarios both times: 4 failed / 38 passed /
9 skipped / 1 xfailed becomes 2 failed / 37 passed / 12 skipped / 1 xfailed. The
two remaining failures are both large-integer-flag, which the pinned testbed
does not serve (open-feature/flagd-testbed#392).

The header also claimed every declared capability had been seen to pass, which
was untrue while this tag was declared, and the closing note claimed the withheld
set matched Go's as well as Java's. Both corrected.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
aepfli added a commit to open-feature/go-sdk-contrib that referenced this pull request Sep 12, 2026
Running this suite at spec fc99d5ac produces 12 or 13 failures, and a
different set each time, so anyone reading a result needs to know which
of them say anything about the provider. Two do not, and the rest are
not even about the provider.

Stable, and gaps in the fixture: integral-float-flag and
large-integer-flag are absent from flagd-testbed, so the scenarios that
ask for them fail with FLAG_NOT_FOUND against any provider at all.
number-zero-flag and huge-integer-flag are missing too, though those are
gated or unasked here. open-feature/flagd-testbed#392 adds them. No
deviation entry, because the gap is in the fixture and an entry there
would attribute it to the provider.

Everything else moves between runs. Two consecutive runs gave 13 then 12
failures with almost disjoint failing sets, every one FLAG_NOT_FOUND on
a flag the testbed certainly has -- boolean-flag, string-zero-flag,
object-flag. The cause is the control API rather than this file: the
per-scenario reset calls POST /start, which stops flagd, deletes the
combined flag file, regenerates it, restarts flagd and polls
:8014/readyz -- and flagd answers readyz before its file source has
loaded the flags. A provider with a lifecycle never notices, because its
own Init blocks until the stream is up or the sync completes and the
flags have arrived by then; both flagd suites are stable against the
same backend at the same revision, which is the control. A stateless
provider evaluates the instant POST /start returns and races the load.

So POST /start returning before the backend serves flags makes the reset
unusable by precisely the providers that have no way to wait for it.
Adding a sleep or a retry here would hide that from every other
language's adoption, so it is written down and left alone.

@numeric-coercion is kept, and now deliberately rather than by inertia.
The comment claiming the lossless direction had no scenario is stale --
integral-float-flag added two, so the tag is a stronger claim than when
it was first declared. One of the two cannot be answered by this
backend, but the other two scenarios pass, and withholding the tag would
also skip the lossy one. That is the scenario worth keeping: narrowing
0.5 to 0 with no error code is the failure mode flagd has and this
provider does not.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
aepfli added a commit to open-feature/go-sdk-contrib that referenced this pull request Sep 12, 2026
This module reaches the TCK through a replace, and a replace does not bring the
replacement's requirements with it, so its own requirement is what selects the
Gherkin the suite runs. Left at fc99d5ac it would have run the previous
revision's assets while reporting conformance against a revision it does not
use -- the drift the flagd e2e module has the same one-line guard against.

Both capabilities are declared on the run rather than on the code. The suite is
documented above as non-deterministic, because POST /start on the launchpad
returns before flagd serves flags and a stateless provider races the load, so
one green run would not be evidence of anything. Three consecutive runs gave
the identical result: 52 scenarios, 9 skipped as undeclared, 43 run, 40 passed,
3 failed, with the same three failures every time.

@Variants passes seven of its eight rows. OFREP's evaluation response carries a
variant field and this provider passes it straight into ResolutionDetail, for
booleans, strings, integers, floats and all three falsy flags. The eighth row
asks large-integer-flag for its max-int32 variant and receives "", because that
flag is absent from flagd-testbed -- the same absence that already fails the
untagged large-integer scenario, now counted twice rather than a new defect. The
third stable failure is integral-float-flag, absent for the same reason. All
three are fixture gaps, all three go green with
open-feature/flagd-testbed#392, and none gets a knownDeviations entry, because
an entry there would attribute the fixture's gap to the provider.

@targeting was reserved rather than declarable until this spec revision. For a
JSON-over-HTTP provider it is the cheapest capability here to get right, because
the evaluation context IS the request body and there is no separate passthrough
path to get wrong. All three scenarios pass -- targeting-key-flag resolves to
"hit" for the matching targeting key and "miss" for a non-matching one or none
at all -- and so does the new untagged scenario that supplies a context to an
untargeted flag. Worth having precisely because of how this provider is built:
until this revision no scenario supplied a context at all, so one that
serialised it into a malformed body would have passed the whole suite.

The README's capability table gains rows for both, and its counts are corrected
while they are being touched: it claimed 24 of 29 scenarios and that @events
gated lifecycle.feature, when @lifecycle now does and @lifecycle and
@large-integers were missing from the table entirely. Its "Known asymmetry"
section claimed no scenario covers integer-flag requested as a float; one does
now, it is declared under @numeric-coercion, and it passes -- so the asymmetry
is deliberate rather than untested.

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

The tag was declared and two of its three scenarios failed. errors.feature says
a declarer must satisfy all three, and says why: the two lossless rows exist to
catch the shortcut of rejecting every float. This provider takes that shortcut.

json.loads yields `int` for 10 and `float` for 0.5, and the check at
ofrep/__init__.py:249-256 admits a value only on an exact isinstance against one
of them. Nothing in that path widens or narrows a number. So the lossy row
passes -- float-flag asked for as an Integer is a TYPE_MISMATCH rather than a
silent 0 -- and integer-flag asked for as a Float fails, because 10 is not an
instance of float.

That is the same architecture as the Java OFREP adoption, which withholds the
tag for the same reason. The Go adoption declares it, and the difference is the
JSON decoder rather than a decision either author made: encoding/json makes
every JSON number a float64, so integer-ness never survives the wire and
ResolveInt has to round-trip through int64 -- which yields lossless coercion,
and TYPE_MISMATCH on loss, for free. Over OFREP this capability follows the
language's JSON library.

No knownDeviation accompanies the withdrawal. A deviation records a gap in
behaviour a provider is required to have, and numeric coercion is a declared
capability rather than a requirement. The honest record is the undeclared tag
and the three skips it produces.

Measured before and after, 52 scenarios both times: 4 failed / 38 passed /
9 skipped / 1 xfailed becomes 2 failed / 37 passed / 12 skipped / 1 xfailed. The
two remaining failures are both large-integer-flag, which the pinned testbed
does not serve (open-feature/flagd-testbed#392).

The header also claimed every declared capability had been seen to pass, which
was untrue while this tag was declared, and the closing note claimed the withheld
set matched Go's as well as Java's. Both corrected.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
aepfli added a commit to open-feature/go-sdk-contrib that referenced this pull request Sep 13, 2026
This was the last of the eight adoptions across four languages still driving
containers by hand, which made it the one adoption that did not answer the
question the compose work exists to answer: whether the harness is sufficient.
It is. There is no container code in tck_test.go any more, and the module no
longer requires testcontainers-go directly -- it arrives as an indirect
dependency of tools/tck.

Deleted: startTestbed, which called compose.NewDockerCompose itself, created a
temporary flags directory and passed it in as FLAGS_DIR because the testbed
submodule's compose file bind-mounts ${FLAGS_DIR} and an unset value defaults to
the submodule's own directory, registered two t.Cleanups, declared its own wait
strategy, looked up two mapped ports by string, built the tck.HTTPControl
itself, hard-coded localhost as the host and slept two seconds for the
launchpad. What replaces it is three options -- WithComposeFile, WithBackendPorts
and WithProviderFromEndpoint -- and a Compose file of six non-comment lines.

95 lines of adopter-written code become 43 plus 6 of YAML. endpoint.Host() is
also correct where the hard-coded localhost was only usually correct: with a
remote Docker daemon, Docker Desktop on some platforms or a rootless setup the
host is not localhost.

The Compose file is this package's own rather than the testbed submodule's, for
the same reasons the flagd adoption's is: no ${FLAGS_DIR} bind mount to work
around, no envoy sidecar, and a service called "backend", which is the TCK's
default and the name both the flagd adoption here and Java's use. It publishes
8016 and 8080 only, since nothing here speaks flagd's gRPC protocols. A welcome
consequence is that this suite no longer needs the submodule checked out at all.

Nothing the harness could not express. The one thing worth reporting is what it
made unnecessary: the FLAGS_DIR plumbing and the two-second launchpad sleep both
went away rather than needing a new option, the former because a purpose-built
compose file has nothing to mount and the latter because AwaitReady is a real
readiness check against the control API.

Verified by running, not by compiling. Five runs of the harness version produced
41, 12, 11, 33 and 5 failures of 47 executed scenarios; three runs of the
hand-rolled version it replaces, on the same machine and the same image,
produced 19, 21 and 40. So the harness is no worse, and the flapping is the
backend's: the launchpad answers 404 to /reset, so every scenario is isolated
with /start, which returns before flagd's file source has loaded the
regenerated flags. Three failures are the floor and all three are the fixture
gap in open-feature/flagd-testbed#392. The README now carries those numbers
instead of a single good run's.

Also here: TCK_RUN in place of PROVIDER_TCK_RUN, the assets pin following the
base to v0.0.0-20260912211427-ccdb88790bb4, the CI reasoning handed to Appendix
F's "Running the suite in CI", and an Adopters row in the harness README so both
Go adoptions are visible as having taken the Compose path.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
aepfli added a commit to open-feature/go-sdk-contrib that referenced this pull request Sep 13, 2026
The assets pin brings reason.feature, gated as a whole on @standard-reasons --
a provider's claim that it uses the standard resolution vocabulary with the
standard meanings.

For OFREP it is the thinnest of the six capabilities this suite declares: the
provider passes the server's reason string straight into ResolutionDetail, so
what runs here verifies flagd's OFREP endpoint plus this provider's refusal to
rewrite what it sends. Worth verifying anyway -- a provider that mapped reasons
onto an enumeration and dropped the ones it did not recognise would fail exactly
here, which is python-sdk-contrib#418 in another language's OFREP provider.

Declared on evidence from three runs rather than from reading the source. Two of
them pass all six scenarios. The third failed two rows of the STATIC outline with
reason ERROR, which is the launchpad race this README already documents -- the
same FLAG_NOT_FOUND as every other flapping failure, surfacing in the reason
field instead of the value -- and not a vocabulary disagreement. The README now
says to judge the capability on consistent failures rather than on a red run
containing one.

Counts move with the pin. 56 canonical scenarios become 65 and the nine skips are
the same nine, because @standard-reasons is declared and the @targeting and
@disabled-flags capabilities its scenarios compose with are declared too, so 47
executed becomes 56. The three-failure floor is unchanged and is still the
fixture gap, open-feature/flagd-testbed#392. The failure distribution recorded in
the README gains this pass's three runs: 20, 4 and 3 of 65.

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

The tag was declared and two of its three scenarios failed. errors.feature says
a declarer must satisfy all three, and says why: the two lossless rows exist to
catch the shortcut of rejecting every float. This provider takes that shortcut.

json.loads yields `int` for 10 and `float` for 0.5, and the check at
ofrep/__init__.py:249-256 admits a value only on an exact isinstance against one
of them. Nothing in that path widens or narrows a number. So the lossy row
passes -- float-flag asked for as an Integer is a TYPE_MISMATCH rather than a
silent 0 -- and integer-flag asked for as a Float fails, because 10 is not an
instance of float.

That is the same architecture as the Java OFREP adoption, which withholds the
tag for the same reason. The Go adoption declares it, and the difference is the
JSON decoder rather than a decision either author made: encoding/json makes
every JSON number a float64, so integer-ness never survives the wire and
ResolveInt has to round-trip through int64 -- which yields lossless coercion,
and TYPE_MISMATCH on loss, for free. Over OFREP this capability follows the
language's JSON library.

No knownDeviation accompanies the withdrawal. A deviation records a gap in
behaviour a provider is required to have, and numeric coercion is a declared
capability rather than a requirement. The honest record is the undeclared tag
and the three skips it produces.

Measured before and after, 52 scenarios both times: 4 failed / 38 passed /
9 skipped / 1 xfailed becomes 2 failed / 37 passed / 12 skipped / 1 xfailed. The
two remaining failures are both large-integer-flag, which the pinned testbed
does not serve (open-feature/flagd-testbed#392).

The header also claimed every declared capability had been seen to pass, which
was untrue while this tag was declared, and the closing note claimed the withheld
set matched Go's as well as Java's. Both corrected.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
aepfli added a commit to open-feature/java-sdk-contrib that referenced this pull request Sep 13, 2026
…s not a deviation

Three of the flags the suite's assets added are absent from flagd-testbed v3.8.0:
large-integer-flag, huge-integer-flag and integral-float-flag. Only the first is
reached by a scenario that runs here -- huge-integer-flag is asked for solely
under @large-integers, which is not applicable in Java, and integral-float-flag
solely under @numeric-coercion, which this provider withholds -- so exactly one
untagged scenario, the 32-bit precision one, fails FLAG_NOT_FOUND in both modes.
open-feature/flagd-testbed#392 is open for it; the Compose tag gets bumped when
it lands, which is why the note lives next to the tag as well as in the class.

A missing flag is a gap in the stack, not in the provider, so it is documented
rather than declared as a KnownDeviation. A deviation says the provider is wrong,
and the provider was never given the flag to get wrong.

The three falsy flags used to fail the same way and no longer do, which is worth
writing down because the failure looked identical. The testbed's zero-flags.json
already served boolean-zero-flag, integer-zero-flag and string-zero-flag with
zero/non-zero variants, while the canonical set called them false-flag,
zero-flag and empty-string-flag; the base moved the canonical names onto the
testbed's rather than the other way round, so those three scenarios now resolve
against flags that were always there.

Also says why capabilities() calls declarableExcept rather than
EnumSet.complementOf, which now matters more than it did: the complement would
claim @large-integers as well as the two reserved tags, and the suite refuses
that declaration at startup.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
aepfli added a commit to open-feature/java-sdk-contrib that referenced this pull request Sep 13, 2026
…s not a deviation

Three of the flags the suite's assets added are absent from flagd-testbed v3.8.0:
large-integer-flag, huge-integer-flag and integral-float-flag. Only the first is
reached by a scenario that runs here -- huge-integer-flag is asked for solely
under @large-integers, which is not applicable in Java, and integral-float-flag
solely under @numeric-coercion, which this provider withholds -- so exactly one
untagged scenario, the 32-bit precision one, fails FLAG_NOT_FOUND in both modes.
open-feature/flagd-testbed#392 is open for it; the Compose tag gets bumped when
it lands, which is why the note lives next to the tag as well as in the class.

A missing flag is a gap in the stack, not in the provider, so it is documented
rather than declared as a KnownDeviation. A deviation says the provider is wrong,
and the provider was never given the flag to get wrong.

The three falsy flags used to fail the same way and no longer do, which is worth
writing down because the failure looked identical. The testbed's zero-flags.json
already served boolean-zero-flag, integer-zero-flag and string-zero-flag with
zero/non-zero variants, while the canonical set called them false-flag,
zero-flag and empty-string-flag; the base moved the canonical names onto the
testbed's rather than the other way round, so those three scenarios now resolve
against flags that were always there.

Also says why capabilities() calls declarableExcept rather than
EnumSet.complementOf, which now matters more than it did: the complement would
claim @large-integers as well as the two reserved tags, and the suite refuses
that declaration at startup.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
aepfli added a commit to open-feature/js-sdk-contrib that referenced this pull request Sep 13, 2026
The adoption hand-rolled its container lifecycle: `FlagdComposeContainer.build()`
at module scope, a `beforeAll` to start it, a guarded `afterAll` to stop it, a
port lookup per resolver, and an `HttpControl` built from a thunk over the
launchpad's mapped port. Every one of those is now the suite's job, so all of it
goes: `runContainerizedProviderTck` is handed a Compose file, the two ports the
two resolvers connect to, and a factory that reads the endpoint it discovers.

The Compose file is this adoption's own, at `src/e2e/tck/docker-compose.yaml`,
and it is deliberately not `test-harness/docker-compose.yaml` next door. Three
differences, each of which is the point:

  - **No `/flags` bind mount.** The launchpad writes the configuration it is
    asked for into the container's own `/flags` from the rawflags baked into the
    testbed image, so the mount buys nothing and costs isolation: the
    neighbouring e2e suites share one host directory through it, and with two
    containers up each rewrites the other's flags. That is why this branch
    previously grew a per-instance temp directory -- and why that commit is gone
    rather than kept. Removing the shared resource beats scheduling around it,
    and a conformance suite whose failures read as provider defects
    (`error-code was 'FLAG_NOT_FOUND', expected 'TYPE_MISMATCH'` on a flag that
    exists) is the worst possible thing to leave to a scheduling guarantee.
    `flagdComposeContainer.ts` is therefore untouched by this branch again: the
    pre-existing e2e suites still import it, so it cannot be deleted, but the
    conformance suites no longer go near it.
  - **No environment variables.** The image tag is pinned in the file rather
    than read from `test-harness/version.txt`. Every capability the two suites
    declare rests on a run against v3.10.1 specifically, so a bump is a
    deliberate act with a result to record, not something a dependency update
    does quietly.
  - **No host port bindings.** The suite requires dynamically mapped ports and
    discovers them after startup.

One Compose file for both suites, as in Java: 8013 is RPC evaluation and 8015
the in-process sync stream, both served by the one testbed service, so each
suite declares only the port its resolver uses.

`retryGracePeriod` stops being a per-suite knob. Both resolvers passed 30, and
they have to: the grace period is emitted from the shared provider layer rather
than from either transport, which is the same reason both declare `@stale`.

Measured rather than asserted, on v3.10.1, under node 24:

  - flagd-rpc: 49 passed, 5 skipped, 2 failed.
  - flagd-in-process: 49 passed, 5 skipped, 2 failed.

Identical, which is the finding the two suites exist to produce. Both failures
are the two already recorded against the testbed image rather than against the
provider -- the mandatory 32-bit precision scenario and the `max-int32` row of
the `@variants` outline, both wanting a `large-integer-flag` that v3.10.1 does
not serve (open-feature/flagd-testbed#392). The `@stale`, `@lifecycle` and
`@unavailable` scenarios all pass, which is what establishes that the suite's
await-ready on the control API is sufficient without the post-command settle
delay that was dropped.

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

@lifecycle was withheld, and that was the expensive mistake in this
adoption rather than a cautious one. The provider implements
openfeature.StateHandler, Init builds the client and blocks until the
event stream is up or the deadline expires, and Init can and does fail
-- so the lifecycle scenarios assert something real here. Withholding
the tag made Go blind to six scenarios Java was running, which is why
Java ran 36 of the 40 and Go ran 29.

Declaring it costs nothing now. Measured, per resolver:

  flagd-rpc         40 scenarios, 6 gated skips, 34 executed,
                    33 passed, 1 failed
  flagd-in-process  40 scenarios, 5 gated skips, 35 executed,
                    34 passed, 1 failed

The one failure in each is "A large integer resolves without loss of
precision", which fails with FLAG_NOT_FOUND because large-integer-flag
is absent from flagd-testbed. open-feature/flagd-testbed#392 adds it.
It says nothing about the provider and gets no deviation entry, because
the gap is in the fixture and an entry there would attribute it to the
provider.

Two capabilities are withheld without a deviation entry, and the reason
in both cases is that the specification permits the behaviour rather
than requiring it. This is the correction this commit is really about:

@reinitialization is undeclared. "A provider that was shut down can be
initialized again" fails in both resolvers -- Shutdown clears the
initialised flag, the second Init proceeds, and what it waits for never
arrives, so it returns "provider initialization deadline exceeded".
Requirement 2.5.2 says a provider SHOULD revert to its uninitialized
state and its supporting text says "some providers MAY allow
reinitialization from this state". Permitted, not required. The
scenario is gated as of spec fc99d5ac and is now reported as skipped.
An earlier draft of this file recorded it as a known deviation against
tck.Lifecycle, which would have published a defect claim against a
provider exercising a choice the specification offers it.

@Stale stays undeclared on RPC, and that too is now a design choice
rather than a defect. Requirement 5.1.1's supporting text offers both
behaviours in one breath: a provider unable to evaluate flags "can"
signal that with PROVIDER_ERROR, and one that caches rule-sets or
evaluations "can" signal PROVIDER_STALE. The RPC resolver takes the
first -- losing the stream sends of.ProviderError directly, and the SDK
then short-circuits to the code default rather than serving cached
values. Confirmed by running rather than by reading: declaring @Stale
on RPC fails with "timed out after 15s waiting for a PROVIDER_STALE
event". The in-process resolver does emit it and passes the scenario in
12.2s, so @Stale is declared there. That two resolvers of one provider
report an outage differently is worth knowing and is written down in the
file; it is not a conformance gap.

JS and Java declare @Stale for both resolvers. On this evidence that is
a vacuous declaration for RPC, which is a reason to leave it withheld
here rather than to copy them.

One capability keeps its deviation entry, and it is worth saying why it
survives the same check: @numeric-coercion's rule is not in the
specification at all (open-feature/spec#430), but flagd accepted it for
itself in an ADR and has open-feature/flagd#1996 open to implement it.
The summary now says so, so a consumer does not read the entry as a
specification violation.

Also fixed: Config.KnownDeviations was never populated. The per-suite
knownDeviations field was collected and then dropped, because the call
site passed Capabilities and not KnownDeviations -- the quietest
possible way for a report to lose the one field that tells a defect
apart from a design choice, and it read as wired because the struct
field was filled in.

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

Follows the TCK's move to spec 26362f85. The e2e module reaches the TCK through
a replace, and a replace does not bring the replacement's requirements with it,
so this module's own pin is what selects the Gherkin the suite actually runs --
leaving it at fc99d5ac would have run the previous revision's assets against the
new vocabulary and reported a spec revision this module does not use.

Both capabilities are declared because the scenarios passed, not because flagd
plausibly supports them. The run is the whole justification, so the numbers:
each resolver goes from 40 canonical scenarios to 52, and each reports 50 passed
and 2 failed.

@Variants passes seven of its eight rows in both resolvers. The variant name
survives the trip from the ruleset through the wire format into ResolutionDetail
for booleans, strings, integers, floats and all three falsy flags. The eighth
row asks large-integer-flag for its max-int32 variant and receives "", because
that flag is absent from flagd-testbed -- the same fixture gap that already
fails the untagged large-integer scenario, and the reason tck.LargeIntegers
stays undeclared. Withholding the tag over it would skip seven working rows to
hide one missing flag, so the comment on the capability list now says the suite
carries two fixture failures rather than one. open-feature/flagd-testbed#392
adds the flags and both go away together; neither gets a knownDeviations entry,
because the gap is in the fixture and an entry there would attribute it to the
provider.

@targeting was reserved rather than declarable until this spec revision, on the
reasoning that asserting anything about the evaluation context needed an echo
endpoint on the control API. It does not: targeting-key-flag carries one
JsonLogic rule on the targeting key, so a matching context resolves to a
different value than a non-matching one or none at all, and a provider that
dropped the context is caught by the resolved value itself. All three scenarios
pass in both resolvers, and running both was not a formality -- in-process
evaluates the rule itself while RPC has flagd evaluate it, so agreement between
them is evidence rather than duplication. The flag has been in flagd-testbed
since flagd-testbed#103, released in v0.5.1 in February 2024, so unlike
tck.LargeIntegers this needs no image bump.

The new mandatory scenario that supplies an evaluation context to an untargeted
flag passes in both resolvers too, which is worth stating because it is the half
of requirement 2.2.1 that nothing here was testing: every resolve method takes
a context, no scenario supplied one, and a provider that threw on any context or
serialised it into a malformed request would have passed the whole suite.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
aepfli added a commit to open-feature/go-sdk-contrib that referenced this pull request Sep 13, 2026
Follows the assets pin to spec 009afe06 and declares the capability it adds.
Anything that moves the TCK's pin has to move this module's go.mod too, because
the e2e module carries the assets as an indirect dependency and a suite reading
one revision of the Gherkin against another revision's flag set is the exact
drift the module arrangement exists to prevent. Bumped by hand rather than with
`go get`, which also wanted to take the go directive to 1.26.0 and flagd/core
to v0.17.0 -- both unrelated to this change.

@disabled-flags was expected to split the two resolvers and does not. The
capability is gated because what a disabled flag resolves to depends on where
the caller's default is substituted, which put the RPC resolver on the wrong
side of the line by construction: it asks flagd to resolve every flag, and the
caller's default never leaves the process. Both resolvers pass all four rows.

Running it is what showed why, and it is not the reason the expectation
assumed. flagd answers with reason DISABLED, an empty variant and a zero value,
and isDefaultOrDisabledFallback in pkg/service/rpc/service.go recognises that
pair and keeps defaultValue rather than taking the response's. The zero value
is not what carries it: only the boolean row's default (false) coincides with
its zero, and "bye" against "", 1 against 0 and 0.1 against 0.0 all fail if the
response value is taken. So the line is "does the response distinguish a
disabled flag from a resolved one" rather than "does the server see the
caller's default" -- and an OFREP response does not, which is what keeps the
tag worth having. The in-process resolver reads the state out of the ruleset it
synced and arrives at the same answer by a different route, so running both was
evidence rather than duplication.

Both suites now run 56 scenarios: 54 pass and 2 fail. The two failures are the
pre-existing pair in both resolvers and neither is about the provider --
large-integer-flag is absent from flagd-testbed, so the large-integer scenario
fails with FLAG_NOT_FOUND and the last @Variants row has no variant to name.
open-feature/flagd-testbed#392 adds the flag and both go green together.
Neither gets a knownDeviations entry, because the gap is in the fixture.

One caution is written down because it cost a wrong conclusion here: the first
verification pass failed this outline with FLAG_NOT_FOUND and failed the object
scenario with reason ERROR in the same run, and three subsequent runs of the
RPC suite and a fourth of both showed neither. That is the launchpad reset race
the OFREP suite documents -- POST /start returns before flagd's file source has
loaded the regenerated flag file -- and not a property of these scenarios. Any
single red run against this backend has to be re-run before it means anything.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
aepfli added a commit to open-feature/go-sdk-contrib that referenced this pull request Sep 13, 2026
The assets pin brings reason.feature, whose six scenarios are gated as a whole
on @standard-reasons -- a claim that the provider uses the standard resolution
vocabulary with the standard meanings, not an exemption from anything.

Declared for both resolvers, on evidence from running rather than from reading
the provider: all nine executed rows pass in both. flagd reports STATIC for a
rule-less flag, TARGETING_MATCH for a matching rule, DEFAULT for a rule that
exists and did not match, DISABLED for a disabled flag and ERROR for a failed
evaluation, which is Appendix F's mapping exactly. Both suites already declare
@targeting and @disabled-flags, so all six scenarios run here rather than three
of them skipping for a composed capability.

Withholding it would have cost nothing in coverage of MUSTs -- values, variants
and error codes are asserted elsewhere, ungated -- which is what makes declaring
it a claim worth making rather than a default worth taking.

Counts move with the pin: 56 scenarios to 65, and both resolvers still produce
the identical result, 63 passed and 2 failed. The two failures are unchanged and
are still the fixture gap, open-feature/flagd-testbed#392: large-integer-flag is
absent from flagd-testbed:v3.8.0. This was an unusually quiet run of a suite
whose launchpad race has produced 2 to 20 failures; the README's instruction to
re-run a red result before reading anything into it still stands.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
aepfli added a commit to open-feature/go-sdk-contrib that referenced this pull request Sep 13, 2026
Running this suite at spec fc99d5ac produces 12 or 13 failures, and a
different set each time, so anyone reading a result needs to know which
of them say anything about the provider. Two do not, and the rest are
not even about the provider.

Stable, and gaps in the fixture: integral-float-flag and
large-integer-flag are absent from flagd-testbed, so the scenarios that
ask for them fail with FLAG_NOT_FOUND against any provider at all.
number-zero-flag and huge-integer-flag are missing too, though those are
gated or unasked here. open-feature/flagd-testbed#392 adds them. No
deviation entry, because the gap is in the fixture and an entry there
would attribute it to the provider.

Everything else moves between runs. Two consecutive runs gave 13 then 12
failures with almost disjoint failing sets, every one FLAG_NOT_FOUND on
a flag the testbed certainly has -- boolean-flag, string-zero-flag,
object-flag. The cause is the control API rather than this file: the
per-scenario reset calls POST /start, which stops flagd, deletes the
combined flag file, regenerates it, restarts flagd and polls
:8014/readyz -- and flagd answers readyz before its file source has
loaded the flags. A provider with a lifecycle never notices, because its
own Init blocks until the stream is up or the sync completes and the
flags have arrived by then; both flagd suites are stable against the
same backend at the same revision, which is the control. A stateless
provider evaluates the instant POST /start returns and races the load.

So POST /start returning before the backend serves flags makes the reset
unusable by precisely the providers that have no way to wait for it.
Adding a sleep or a retry here would hide that from every other
language's adoption, so it is written down and left alone.

@numeric-coercion is kept, and now deliberately rather than by inertia.
The comment claiming the lossless direction had no scenario is stale --
integral-float-flag added two, so the tag is a stronger claim than when
it was first declared. One of the two cannot be answered by this
backend, but the other two scenarios pass, and withholding the tag would
also skip the lossy one. That is the scenario worth keeping: narrowing
0.5 to 0 with no error code is the failure mode flagd has and this
provider does not.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
aepfli added a commit to open-feature/go-sdk-contrib that referenced this pull request Sep 13, 2026
This module reaches the TCK through a replace, and a replace does not bring the
replacement's requirements with it, so its own requirement is what selects the
Gherkin the suite runs. Left at fc99d5ac it would have run the previous
revision's assets while reporting conformance against a revision it does not
use -- the drift the flagd e2e module has the same one-line guard against.

Both capabilities are declared on the run rather than on the code. The suite is
documented above as non-deterministic, because POST /start on the launchpad
returns before flagd serves flags and a stateless provider races the load, so
one green run would not be evidence of anything. Three consecutive runs gave
the identical result: 52 scenarios, 9 skipped as undeclared, 43 run, 40 passed,
3 failed, with the same three failures every time.

@Variants passes seven of its eight rows. OFREP's evaluation response carries a
variant field and this provider passes it straight into ResolutionDetail, for
booleans, strings, integers, floats and all three falsy flags. The eighth row
asks large-integer-flag for its max-int32 variant and receives "", because that
flag is absent from flagd-testbed -- the same absence that already fails the
untagged large-integer scenario, now counted twice rather than a new defect. The
third stable failure is integral-float-flag, absent for the same reason. All
three are fixture gaps, all three go green with
open-feature/flagd-testbed#392, and none gets a knownDeviations entry, because
an entry there would attribute the fixture's gap to the provider.

@targeting was reserved rather than declarable until this spec revision. For a
JSON-over-HTTP provider it is the cheapest capability here to get right, because
the evaluation context IS the request body and there is no separate passthrough
path to get wrong. All three scenarios pass -- targeting-key-flag resolves to
"hit" for the matching targeting key and "miss" for a non-matching one or none
at all -- and so does the new untagged scenario that supplies a context to an
untargeted flag. Worth having precisely because of how this provider is built:
until this revision no scenario supplied a context at all, so one that
serialised it into a malformed body would have passed the whole suite.

The README's capability table gains rows for both, and its counts are corrected
while they are being touched: it claimed 24 of 29 scenarios and that @events
gated lifecycle.feature, when @lifecycle now does and @lifecycle and
@large-integers were missing from the table entirely. Its "Known asymmetry"
section claimed no scenario covers integer-flag requested as a float; one does
now, it is declared under @numeric-coercion, and it passes -- so the asymmetry
is deliberate rather than untested.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
aepfli added a commit to open-feature/go-sdk-contrib that referenced this pull request Sep 13, 2026
This was the last of the eight adoptions across four languages still driving
containers by hand, which made it the one adoption that did not answer the
question the compose work exists to answer: whether the harness is sufficient.
It is. There is no container code in tck_test.go any more, and the module no
longer requires testcontainers-go directly -- it arrives as an indirect
dependency of tools/tck.

Deleted: startTestbed, which called compose.NewDockerCompose itself, created a
temporary flags directory and passed it in as FLAGS_DIR because the testbed
submodule's compose file bind-mounts ${FLAGS_DIR} and an unset value defaults to
the submodule's own directory, registered two t.Cleanups, declared its own wait
strategy, looked up two mapped ports by string, built the tck.HTTPControl
itself, hard-coded localhost as the host and slept two seconds for the
launchpad. What replaces it is three options -- WithComposeFile, WithBackendPorts
and WithProviderFromEndpoint -- and a Compose file of six non-comment lines.

95 lines of adopter-written code become 43 plus 6 of YAML. endpoint.Host() is
also correct where the hard-coded localhost was only usually correct: with a
remote Docker daemon, Docker Desktop on some platforms or a rootless setup the
host is not localhost.

The Compose file is this package's own rather than the testbed submodule's, for
the same reasons the flagd adoption's is: no ${FLAGS_DIR} bind mount to work
around, no envoy sidecar, and a service called "backend", which is the TCK's
default and the name both the flagd adoption here and Java's use. It publishes
8016 and 8080 only, since nothing here speaks flagd's gRPC protocols. A welcome
consequence is that this suite no longer needs the submodule checked out at all.

Nothing the harness could not express. The one thing worth reporting is what it
made unnecessary: the FLAGS_DIR plumbing and the two-second launchpad sleep both
went away rather than needing a new option, the former because a purpose-built
compose file has nothing to mount and the latter because AwaitReady is a real
readiness check against the control API.

Verified by running, not by compiling. Five runs of the harness version produced
41, 12, 11, 33 and 5 failures of 47 executed scenarios; three runs of the
hand-rolled version it replaces, on the same machine and the same image,
produced 19, 21 and 40. So the harness is no worse, and the flapping is the
backend's: the launchpad answers 404 to /reset, so every scenario is isolated
with /start, which returns before flagd's file source has loaded the
regenerated flags. Three failures are the floor and all three are the fixture
gap in open-feature/flagd-testbed#392. The README now carries those numbers
instead of a single good run's.

Also here: TCK_RUN in place of PROVIDER_TCK_RUN, the assets pin following the
base to v0.0.0-20260912211427-ccdb88790bb4, the CI reasoning handed to Appendix
F's "Running the suite in CI", and an Adopters row in the harness README so both
Go adoptions are visible as having taken the Compose path.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
aepfli added a commit to open-feature/go-sdk-contrib that referenced this pull request Sep 13, 2026
The assets pin brings reason.feature, gated as a whole on @standard-reasons --
a provider's claim that it uses the standard resolution vocabulary with the
standard meanings.

For OFREP it is the thinnest of the six capabilities this suite declares: the
provider passes the server's reason string straight into ResolutionDetail, so
what runs here verifies flagd's OFREP endpoint plus this provider's refusal to
rewrite what it sends. Worth verifying anyway -- a provider that mapped reasons
onto an enumeration and dropped the ones it did not recognise would fail exactly
here, which is python-sdk-contrib#418 in another language's OFREP provider.

Declared on evidence from three runs rather than from reading the source. Two of
them pass all six scenarios. The third failed two rows of the STATIC outline with
reason ERROR, which is the launchpad race this README already documents -- the
same FLAG_NOT_FOUND as every other flapping failure, surfacing in the reason
field instead of the value -- and not a vocabulary disagreement. The README now
says to judge the capability on consistent failures rather than on a red run
containing one.

Counts move with the pin. 56 canonical scenarios become 65 and the nine skips are
the same nine, because @standard-reasons is declared and the @targeting and
@disabled-flags capabilities its scenarios compose with are declared too, so 47
executed becomes 56. The three-failure floor is unchanged and is still the
fixture gap, open-feature/flagd-testbed#392. The failure distribution recorded in
the README gains this pass's three runs: 20, 4 and 3 of 65.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
aepfli added a commit to open-feature/js-sdk-contrib that referenced this pull request Sep 13, 2026
The adoption hand-rolled its container lifecycle: `FlagdComposeContainer.build()`
at module scope, a `beforeAll` to start it, a guarded `afterAll` to stop it, a
port lookup per resolver, and an `HttpControl` built from a thunk over the
launchpad's mapped port. Every one of those is now the suite's job, so all of it
goes: `runContainerizedProviderTck` is handed a Compose file, the two ports the
two resolvers connect to, and a factory that reads the endpoint it discovers.

The Compose file is this adoption's own, at `src/e2e/tck/docker-compose.yaml`,
and it is deliberately not `test-harness/docker-compose.yaml` next door. Three
differences, each of which is the point:

  - **No `/flags` bind mount.** The launchpad writes the configuration it is
    asked for into the container's own `/flags` from the rawflags baked into the
    testbed image, so the mount buys nothing and costs isolation: the
    neighbouring e2e suites share one host directory through it, and with two
    containers up each rewrites the other's flags. That is why this branch
    previously grew a per-instance temp directory -- and why that commit is gone
    rather than kept. Removing the shared resource beats scheduling around it,
    and a conformance suite whose failures read as provider defects
    (`error-code was 'FLAG_NOT_FOUND', expected 'TYPE_MISMATCH'` on a flag that
    exists) is the worst possible thing to leave to a scheduling guarantee.
    `flagdComposeContainer.ts` is therefore untouched by this branch again: the
    pre-existing e2e suites still import it, so it cannot be deleted, but the
    conformance suites no longer go near it.
  - **No environment variables.** The image tag is pinned in the file rather
    than read from `test-harness/version.txt`. Every capability the two suites
    declare rests on a run against v3.10.1 specifically, so a bump is a
    deliberate act with a result to record, not something a dependency update
    does quietly.
  - **No host port bindings.** The suite requires dynamically mapped ports and
    discovers them after startup.

One Compose file for both suites, as in Java: 8013 is RPC evaluation and 8015
the in-process sync stream, both served by the one testbed service, so each
suite declares only the port its resolver uses.

`retryGracePeriod` stops being a per-suite knob. Both resolvers passed 30, and
they have to: the grace period is emitted from the shared provider layer rather
than from either transport, which is the same reason both declare `@stale`.

Measured rather than asserted, on v3.10.1, under node 24:

  - flagd-rpc: 49 passed, 5 skipped, 2 failed.
  - flagd-in-process: 49 passed, 5 skipped, 2 failed.

Identical, which is the finding the two suites exist to produce. Both failures
are the two already recorded against the testbed image rather than against the
provider -- the mandatory 32-bit precision scenario and the `max-int32` row of
the `@variants` outline, both wanting a `large-integer-flag` that v3.10.1 does
not serve (open-feature/flagd-testbed#392). The `@stale`, `@lifecycle` and
`@unavailable` scenarios all pass, which is what establishes that the suite's
await-ready on the control API is sufficient without the post-command settle
delay that was dropped.

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

The tag was declared and two of its three scenarios failed. errors.feature says
a declarer must satisfy all three, and says why: the two lossless rows exist to
catch the shortcut of rejecting every float. This provider takes that shortcut.

json.loads yields `int` for 10 and `float` for 0.5, and the check at
ofrep/__init__.py:249-256 admits a value only on an exact isinstance against one
of them. Nothing in that path widens or narrows a number. So the lossy row
passes -- float-flag asked for as an Integer is a TYPE_MISMATCH rather than a
silent 0 -- and integer-flag asked for as a Float fails, because 10 is not an
instance of float.

That is the same architecture as the Java OFREP adoption, which withholds the
tag for the same reason. The Go adoption declares it, and the difference is the
JSON decoder rather than a decision either author made: encoding/json makes
every JSON number a float64, so integer-ness never survives the wire and
ResolveInt has to round-trip through int64 -- which yields lossless coercion,
and TYPE_MISMATCH on loss, for free. Over OFREP this capability follows the
language's JSON library.

No knownDeviation accompanies the withdrawal. A deviation records a gap in
behaviour a provider is required to have, and numeric coercion is a declared
capability rather than a requirement. The honest record is the undeclared tag
and the three skips it produces.

Measured before and after, 52 scenarios both times: 4 failed / 38 passed /
9 skipped / 1 xfailed becomes 2 failed / 37 passed / 12 skipped / 1 xfailed. The
two remaining failures are both large-integer-flag, which the pinned testbed
does not serve (open-feature/flagd-testbed#392).

The header also claimed every declared capability had been seen to pass, which
was untrue while this tag was declared, and the closing note claimed the withheld
set matched Go's as well as Java's. Both corrected.

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

The tag was declared and two of its three scenarios failed. errors.feature says
a declarer must satisfy all three, and says why: the two lossless rows exist to
catch the shortcut of rejecting every float. This provider takes that shortcut.

json.loads yields `int` for 10 and `float` for 0.5, and the check at
ofrep/__init__.py:249-256 admits a value only on an exact isinstance against one
of them. Nothing in that path widens or narrows a number. So the lossy row
passes -- float-flag asked for as an Integer is a TYPE_MISMATCH rather than a
silent 0 -- and integer-flag asked for as a Float fails, because 10 is not an
instance of float.

That is the same architecture as the Java OFREP adoption, which withholds the
tag for the same reason. The Go adoption declares it, and the difference is the
JSON decoder rather than a decision either author made: encoding/json makes
every JSON number a float64, so integer-ness never survives the wire and
ResolveInt has to round-trip through int64 -- which yields lossless coercion,
and TYPE_MISMATCH on loss, for free. Over OFREP this capability follows the
language's JSON library.

No knownDeviation accompanies the withdrawal. A deviation records a gap in
behaviour a provider is required to have, and numeric coercion is a declared
capability rather than a requirement. The honest record is the undeclared tag
and the three skips it produces.

Measured before and after, 52 scenarios both times: 4 failed / 38 passed /
9 skipped / 1 xfailed becomes 2 failed / 37 passed / 12 skipped / 1 xfailed. The
two remaining failures are both large-integer-flag, which the pinned testbed
does not serve (open-feature/flagd-testbed#392).

The header also claimed every declared capability had been seen to pass, which
was untrue while this tag was declared, and the closing note claimed the withheld
set matched Go's as well as Java's. Both corrected.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
aepfli added a commit to open-feature/java-sdk-contrib that referenced this pull request Sep 13, 2026
…s not a deviation

Three of the flags the suite's assets added are absent from flagd-testbed v3.8.0:
large-integer-flag, huge-integer-flag and integral-float-flag. Only the first is
reached by a scenario that runs here -- huge-integer-flag is asked for solely
under @large-integers, which is not applicable in Java, and integral-float-flag
solely under @numeric-coercion, which this provider withholds -- so exactly one
untagged scenario, the 32-bit precision one, fails FLAG_NOT_FOUND in both modes.
open-feature/flagd-testbed#392 is open for it; the Compose tag gets bumped when
it lands, which is why the note lives next to the tag as well as in the class.

A missing flag is a gap in the stack, not in the provider, so it is documented
rather than declared as a KnownDeviation. A deviation says the provider is wrong,
and the provider was never given the flag to get wrong.

The three falsy flags used to fail the same way and no longer do, which is worth
writing down because the failure looked identical. The testbed's zero-flags.json
already served boolean-zero-flag, integer-zero-flag and string-zero-flag with
zero/non-zero variants, while the canonical set called them false-flag,
zero-flag and empty-string-flag; the base moved the canonical names onto the
testbed's rather than the other way round, so those three scenarios now resolve
against flags that were always there.

Also says why capabilities() calls declarableExcept rather than
EnumSet.complementOf, which now matters more than it did: the complement would
claim @large-integers as well as the two reserved tags, and the suite refuses
that declaration at startup.

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

The adoption withheld @numeric-coercion and recorded a tracked deviation for
it at the same time. That is the one combination Appendix F's known-deviation
guidance singles out to avoid: a withheld capability plus a deviation asserts
that the provider is broken at something the suite never put to it, and the
three skips it produces cannot say which of "does not coerce" and "coerces,
and loses information one way round" is true.

Measured over three full runs, both resolvers, identically:

  - "An integer requested as a float is widened without loss" PASSES.
    integer-flag (10) through GetFloatDetails returns 10, reason STATIC, no
    error code. So this provider does coerce, and the second shape does not
    apply: shape 2 is legitimate only when the provider cannot attempt the
    behaviour at all.
  - "A float flag is not silently narrowed to an integer" FAILS. float-flag
    (0.5) through GetIntDetails returns 0 with no error code, not
    TYPE_MISMATCH with the code default. This is the deviation, and it stands
    unchanged -- flagd measured against its own accepted numeric-coercion ADR,
    tracked as open-feature/flagd#1996.
  - "An integral float requested as an integer is coerced without loss" FAILS
    with FLAG_NOT_FOUND, because flagd-testbed v3.8.0 does not serve
    integral-float-flag. That is the backend's gap, not flagd's.

So both suites now declare the tag, keep the deviation, and run 65 scenarios
with 61 passing and 4 failing instead of 63 and 2. One of the four is the
provider's; three are the fixture's, and open-feature/flagd-testbed#392
retires all three together.

The second failure is the price of declaring and it is paid rather than
dodged: one red scenario belonging to flagd-testbed is a smaller loss than
three skips that misdescribe the provider. It is named in the deviation
summary so a consumer reading the report does not count it against flagd.

Also corrects a claim in the comment that was stated more strongly than the
evidence supports. It said the Python flagd provider gets this right "in
either resolver". It splits: the in-process resolver refuses 0.5 correctly,
and the RPC resolver narrows it to 0 exactly as this one does.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
aepfli added a commit to open-feature/go-sdk-contrib that referenced this pull request Sep 13, 2026
Running this suite at spec fc99d5ac produces 12 or 13 failures, and a
different set each time, so anyone reading a result needs to know which
of them say anything about the provider. Two do not, and the rest are
not even about the provider.

Stable, and gaps in the fixture: integral-float-flag and
large-integer-flag are absent from flagd-testbed, so the scenarios that
ask for them fail with FLAG_NOT_FOUND against any provider at all.
number-zero-flag and huge-integer-flag are missing too, though those are
gated or unasked here. open-feature/flagd-testbed#392 adds them. No
deviation entry, because the gap is in the fixture and an entry there
would attribute it to the provider.

Everything else moves between runs. Two consecutive runs gave 13 then 12
failures with almost disjoint failing sets, every one FLAG_NOT_FOUND on
a flag the testbed certainly has -- boolean-flag, string-zero-flag,
object-flag. The cause is the control API rather than this file: the
per-scenario reset calls POST /start, which stops flagd, deletes the
combined flag file, regenerates it, restarts flagd and polls
:8014/readyz -- and flagd answers readyz before its file source has
loaded the flags. A provider with a lifecycle never notices, because its
own Init blocks until the stream is up or the sync completes and the
flags have arrived by then; both flagd suites are stable against the
same backend at the same revision, which is the control. A stateless
provider evaluates the instant POST /start returns and races the load.

So POST /start returning before the backend serves flags makes the reset
unusable by precisely the providers that have no way to wait for it.
Adding a sleep or a retry here would hide that from every other
language's adoption, so it is written down and left alone.

@numeric-coercion is kept, and now deliberately rather than by inertia.
The comment claiming the lossless direction had no scenario is stale --
integral-float-flag added two, so the tag is a stronger claim than when
it was first declared. One of the two cannot be answered by this
backend, but the other two scenarios pass, and withholding the tag would
also skip the lossy one. That is the scenario worth keeping: narrowing
0.5 to 0 with no error code is the failure mode flagd has and this
provider does not.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
aepfli added a commit to open-feature/go-sdk-contrib that referenced this pull request Sep 13, 2026
This module reaches the TCK through a replace, and a replace does not bring the
replacement's requirements with it, so its own requirement is what selects the
Gherkin the suite runs. Left at fc99d5ac it would have run the previous
revision's assets while reporting conformance against a revision it does not
use -- the drift the flagd e2e module has the same one-line guard against.

Both capabilities are declared on the run rather than on the code. The suite is
documented above as non-deterministic, because POST /start on the launchpad
returns before flagd serves flags and a stateless provider races the load, so
one green run would not be evidence of anything. Three consecutive runs gave
the identical result: 52 scenarios, 9 skipped as undeclared, 43 run, 40 passed,
3 failed, with the same three failures every time.

@Variants passes seven of its eight rows. OFREP's evaluation response carries a
variant field and this provider passes it straight into ResolutionDetail, for
booleans, strings, integers, floats and all three falsy flags. The eighth row
asks large-integer-flag for its max-int32 variant and receives "", because that
flag is absent from flagd-testbed -- the same absence that already fails the
untagged large-integer scenario, now counted twice rather than a new defect. The
third stable failure is integral-float-flag, absent for the same reason. All
three are fixture gaps, all three go green with
open-feature/flagd-testbed#392, and none gets a knownDeviations entry, because
an entry there would attribute the fixture's gap to the provider.

@targeting was reserved rather than declarable until this spec revision. For a
JSON-over-HTTP provider it is the cheapest capability here to get right, because
the evaluation context IS the request body and there is no separate passthrough
path to get wrong. All three scenarios pass -- targeting-key-flag resolves to
"hit" for the matching targeting key and "miss" for a non-matching one or none
at all -- and so does the new untagged scenario that supplies a context to an
untargeted flag. Worth having precisely because of how this provider is built:
until this revision no scenario supplied a context at all, so one that
serialised it into a malformed body would have passed the whole suite.

The README's capability table gains rows for both, and its counts are corrected
while they are being touched: it claimed 24 of 29 scenarios and that @events
gated lifecycle.feature, when @lifecycle now does and @lifecycle and
@large-integers were missing from the table entirely. Its "Known asymmetry"
section claimed no scenario covers integer-flag requested as a float; one does
now, it is declared under @numeric-coercion, and it passes -- so the asymmetry
is deliberate rather than untested.

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