feat(provider-tck): emit a conformance report, with results in Cucumber Messages by aepfli · Pull Request #1610 · open-feature/js-sdk-contrib · GitHub
Skip to content

feat(provider-tck): emit a conformance report, with results in Cucumber Messages - #1610

Draft
aepfli wants to merge 1 commit into
feat/provider-tckfrom
feat/provider-tck-report
Draft

aepfli wants to merge 1 commit into
feat/provider-tckfrom
feat/provider-tck-report

Conversation

@aepfli

@aepfli aepfli commented Aug 24, 2026 •

Copy link
Copy Markdown
Member

Stacked on #1606 — base is feat/provider-tck, so review only the commits above it. Part of
open-feature/spec#424; the schema the envelope
validates against is open-feature/spec#425, and the
appendix defining the capability vocabulary is
open-feature/spec#423. Tracking issue for the suite
itself is open-feature/spec#417.

The last two commits are a capability rename the specification made
(dc4d7ae8) and a defect in what this
library declares; both are described below.

Results are carried in Cucumber Messages, not in a format
we maintain.
The first version of this PR defined one: a scenarios[] list, a four-value outcome
enum, an example field for Scenario Outline row identity, and a git tree hash for the executed
assets. That was the wrong call, and the last commit undoes it. Everything in that list is specified
by Cucumber Messages already, and a second definition is a second thing to version and two places
for the same fact to disagree.

Set TCK_REPORT_DIR and each suite writes two files:

$ TCK_REPORT_DIR=./reports npx jest --config libs/shared/tck/jest.config.ts
$ ls reports/
in-memory.json  in-memory.ndjson
$ jq -r 'select(.testStepFinished).testStepFinished.testStepResult.status' reports/in-memory.ndjson \
    | sort | uniq -c
     24 PASSED
      5 SKIPPED

in-memory.ndjson is a Cucumber Messages stream: Meta, then per feature a Source, a
GherkinDocument and a Pickle per scenario, then a TestCase per pickle, TestRunStarted, a
TestCaseStarted/TestStepFinished/TestCaseFinished triple per scenario, and TestRunFinished.
in-memory.json is the envelope from the reshaped schema: what was tested, what the provider
declares, and where the results are with a sha256 digest over them. Twenty-nine scenarios,
twenty-nine pickles, twenty-nine test cases, twenty-nine outcomes.

Building the stream

jest-cucumber has no output layer at all — no reporter, no JSON, nothing; it is a binding from
Gherkin to Jest and that is all. The stream is therefore constructed by the harness, and two things
it already did make that cheap rather than painful.

It plans every scenario before the run — file order, then outline examples, which is the order
autoBindSteps defines them in — and asserts each definition against the plan as it arrives. That
plan is exactly where Pickle and TestCase messages come from. And it owns the test and
test.skip calls
, because jest-cucumber accepts the describe/test pair it makes them through,
so an outcome is recorded at the point the decision is made rather than scraped back out of a
reporter afterwards: a skip where the capability gate chooses it, a pass or failure where the test
body settles.

@cucumber/gherkin compiles the Source, GherkinDocument and Pickle messages straight from the
feature files, so no message here is hand-rolled and no id is invented. @cucumber/messages
joins it as a peer dependency and supplies the generated types; jest-cucumber already depends on
both transitively, so no adopter gains an install.

Each planned scenario is bound to one compiled pickle positionally, and the binding is checked
rather than assumed: jest-cucumber substitutes an outline row into the scenario title and so does
the pickle compiler, so the two strings must agree or the suite fails instead of reporting an
outcome against a scenario that did not run.

One TestStep per test case, not one per Gherkin step. jest-cucumber runs a whole scenario as a
single Jest test and reports one outcome for it; it never says which step failed. A step per Gherkin
step would mean inventing per-step results — marking them all failed over-claims, marking one of
them failed picks a step at random — so the stream carries the granularity the runner actually has.
The steps are in the stream on the Pickle, with the outline row already substituted into them, and
jest-cucumber's failure message happens to name the step it was on:

{"status":"FAILED","duration":{"seconds":0,"nanos":8000000},
 "message":"Failing step: \"the error-code should be \"FLAG_NOT_FOUND\"\"\n\nStep arguments: [\"FLAG_NOT_FOUND\"]\n\nError: error-code was 'GENERAL', expected 'FLAG_NOT_FOUND'"}

What was deleted, and what replaced it

scenarios[] and the four-value outcome enum. A scenario is a Pickle with a TestCase, and
its outcome is TestStepFinished.testStepResult.status. A capability-gated scenario is SKIPPED
with the reason on the result — whichever kind of skip it was.

example. Pickle.astNodeIds identifies an outline row exactly: the second id is the
TableRow in the GherkinDocument, and the document carries its cells. Four implementations had
each invented an answer to this and they had diverged — a bare scenario name, a runner's example id,
jest-cucumber's expanded title, and a line number matched back against a re-parse of the Examples
tables. errors.feature's eleven type-mismatch rows share a name and share their expanded title,
because that outline's title carries no placeholders, so the AST node is the only thing that tells
them apart:

$ jq -r 'select(.pickle) | select(.pickle.name | test("wrong type")) | .pickle.astNodeIds | @tsv' \
    reports/in-memory.ndjson
25      9
25      10
25      11
...

25 is the Scenario Outline; the second id is the row. No separator, ordering or escaping rule
has to be agreed between four languages for that to resolve, which is what a name-mangling
convention would have required. src/lib/examples.ts, which re-parsed each feature file to recover
the rows, is deleted with the field — the rows now come from the same parse that produced the
pickles, and are kept only so a harness diagnostic can name a row the way the feature file writes
it.

tck.assetsTree. The stream carries every executed feature file verbatim as a Source, which
identifies the assets by content, covers only what actually ran, and sits under the report's digest.
That is strictly better than a tree hash of the whole asset directory, so write-revision.js stops
computing one. tck.specRevision stays: it is what identifies the two artifacts the stream does not
carry, canonical-flags.json and control-api.yaml.

not-applicable as a per-scenario outcome. It moves into declaration.notApplicable, which is
where the reshaped schema puts it and where it belongs. @numeric-coercion is unsatisfiable in
JavaScript because the language has no integer type — a fact about the language, not about a run —
so stating it once with a reason is better than repeating it on every scenario carrying the tag:

"declaration": {
  "declared": ["@configuration-change", "@events", "@object"],
  "notApplicable": {
    "@numeric-coercion": "JavaScript has no integer type: typeof 10 and typeof 0.5 are both \"number\" and the Evaluation API exposes only getNumberDetails, so requesting a flag as an Integer is indistinguishable from requesting it as a Float. Neither half of the coercion contract can be put to a provider in this language: there is no lossless coercion to permit, because 10.0 and 10 are the same value and no narrowing happens, and no lossy one to reject with TYPE_MISMATCH, because 0.5 asked for as an Integer is indistinguishable from a perfectly valid Float request"
  }
}

The reason is now required, because a reader has no other way to tell an impossibility from an
excuse. TckOptions.notApplicable becomes a map from capability to reason, and
NO_INTEGER_TYPE_IN_JAVASCRIPT is exported so every JavaScript report words this one the same way.
The gate is unchanged: those scenarios are skipped with the reason in the test name exactly as
before, and the scenario is SKIPPED in the stream with the impossibility spelled out on the
result. What changed is what is declared, not what runs.

The per-capability roll-up, which the reshaped schema does not have. It is derivable from the
declaration and the stream, and the vacuous passes it used to guard against are avoided by counting
test cases that ran rather than tags present: every scenario carrying a capability can be skipped
for a different one — both scenarios in
events.feature carry @events as well as @stale or @configuration-change. The README says so
for a consumer computing one, and also says what a roll-up is not: scenarios carrying no capability
tag are mandatory and roll up into nothing, so a provider can fail a mandatory scenario with every
capability intact.

What stays OpenFeature's, and why

A Messages stream cannot say what it was a test of. No standard results format identifies the
subject under test, so the envelope carries the four things that are genuinely ours:

  • provider — what the provider calls itself through its own metadata, not the suite name. The
    suite name is chosen to read well in a failure message (flagd-rpc), which makes it the
    configuration, and it is reported as such. One provider with two materially different modes
    produces two reports that are not interchangeable.
  • sdk — read at run time from the installed @openfeature/server-sdk rather than declared. A
    declared version is a second place to be wrong: the report would keep claiming 1.17.0 after a
    dependency bump moved the code underneath it.
  • tck — which implementation asked the questions, and which revision of the artifacts.
  • declaration — the capability set the provider claims. This is an input to reading the
    results, not a summary of them, which is why it cannot be derived from the stream and has to be
    stated. The stream says a scenario was skipped; only the declaration says whether that is because
    the provider declines the capability it needed, or because the capability cannot hold for it at
    all. Given the declaration and a scenario's tags — both present — the reason for every skip
    follows without being transported per scenario.

results.digest covers the .ndjson, so a consumer that fetched the two separately can tell that
what it has is what the envelope describes. The stream is written first and the envelope second, so
an envelope never names results that are not there.

Every scenario appears exactly once

That is still the load-bearing property, and the reason the report exists. Appendix F requires that
a scenario skipped for an undeclared capability is reported as skipped and never as passed, and
the report is what makes the rule checkable by a consumer instead of dependent on each runner's
summary being trustworthy — godog counts capability skips in its passed tally, which is what
motivated the format.

Two checks, both on every run rather than only in a test:

  • the harness refuses outright to run a scenario whose capabilities are unmet. A gated scenario
    cannot reach the branch that records a pass, structurally.
  • the accounting is verified at the end, report or no report: the harness fails the suite if a
    planned scenario is missing from the stream or recorded twice. It is now keyed on the pickle,
    which is unique per outline row — keyed on the name, dropping one of eleven rows and recording
    another twice tallied as eleven expected and eleven recorded, and the check that exists to catch a
    dropped scenario would have passed.

A scenario is registered when it is defined, not when it runs, so one Jest never finished still
appears — as a FAILED whose message says it never reported an outcome. A run filtered with -t is
therefore partial by construction and should not be published; that is documented.

The plan is positional, not keyed on the scenario name. Gherkin permits tags on an individual
Examples block, so two rows of one outline can differ in whether the capability gate stops them; a
name-keyed plan would gate all of them together and the rows that ran would vanish. No canonical
feature does that today, so it is latent rather than live, but it is the same defect that was found
and fixed in the Go implementation.

@strict-numeric-typing is now @numeric-coercion, and the rule it states is different

The specification renamed the tag and corrected the rule
(dc4d7ae8), so the submodule moves to that
commit and Capability.StrictNumericTyping becomes Capability.NumericCoercion.

The old name asserted the stricter rule — never coerce between integer and float — and that forbids
a case the specification wants to work. flagd is implementing an accepted
numeric coercion ADR
(open-feature/flagd#1996) whose rule is
lossless coercion is permitted; lossy coercion must fail with TYPE_MISMATCH: 10.0 requested
as an integer succeeds, 0.5 does not. The existing scenario survives the difference only by luck,
because it asks about 0.5. flagd's own testbed is gaining @numeric-coercion scenarios too, so
keeping a second name for one property would have left the reference implementation and the
specification disagreeing about what a rule is called.

No scenario was added here, and two gaps were recorded by the appendix rather than closed. One has
since closed:

  • The lossless half has no scenario, in any language. Closed. The canonical flag set
    gained integral-float-flag, and errors.feature now carries three @numeric-coercion
    scenarios — the lossy one, 10.0 requested as an integer, and 10 requested as a float — all
    three of which a declarer must satisfy. That is what stops the shortcut of rejecting every float
    and calling it strictness, and two of the four OFREP adoptions were taking it. It changes nothing
    for JavaScript, where the capability is refused outright for the reason above.
  • Accessor width is modelled, and what remains open is narrower than it looks. The ADR
    distinguishes a 64-bit integer accessor from a 32-bit one, and flagd's testbed tags the 32-bit-only
    scenarios @int32-bounded. Appendix F draws the same boundary from the other side: the 2^31 − 1
    precision scenario is untagged and mandatory because every language's accessor can ask for it,
    and the 2^53 − 1 one carries @large-integers because only some can. Mandatory-versus-gated
    instead of two tags. Nothing above 2^53 − 1 is asked for at all, and JavaScript is the reason — a
    double cannot represent it. What is genuinely open is the negative half: nothing asserts what a
    provider owes a value that does not fit the requested accessor, which is
    spec#430 and unsettled.

JavaScript is the language where this capability is notApplicable rather than merely undeclared, so
NO_INTEGER_TYPE_IN_JAVASCRIPT is the sentence a report carries — and it now has to be true of
both halves, which is a slightly different claim than before. It is: with one number type and
only getNumberDetails, there is no lossless coercion to permit, because 10.0 and 10 are the
same value and nothing is narrowed, and no lossy one to reject, because 0.5 asked for as an Integer
is indistinguishable from a perfectly valid Float request. What fails here is the premise both halves
rest on — that an integer request is distinguishable from a float one — not either half.

A reserved capability must not be declarable

@caching is in the vocabulary but no scenario carries it. @targeting was in the same position when this was written and stopped being so at spec 26362f85, once scenarios started carrying it — which is the shape a reserved tag is meant to have: a placeholder that either grows scenarios or is removed, never a claim a report can carry indefinitely. The mechanism below is unchanged and still has one member to enforce; the examples show both. The report
schema now states that such a tag must not be declared and must not appear in
declaration.declared: a capability no executed scenario carries cannot produce a skip, so it plays
no part in interpreting the results, and listing it invites a reader to believe it was verified when
nothing examined it.

This was a live defect, not a tidy-up. A real Java conformance report asserts both tags as
declared — not by anyone's decision, but because that adoption declares "every capability except X"
and picks up every reserved tag in the vocabulary on the way past. The default here had the same
shape (ALL_CAPABILITIES minus what cannot apply), so any JS suite that omitted capabilities
would have published the same claim.

  • RESERVED_CAPABILITIES is the one list, and DECLARABLE_CAPABILITIES — the vocabulary without it
    — is what capabilities defaults to, so "declare everything" cannot mean "declare things nothing
    tested".
  • Which capabilities are reserved is decided upstream, so the harness checks the list against the
    features it actually ran
    and fails if a reservation has expired. An out-of-date list would make
    a newly testable capability permanently unclaimable, which is the same mistake pointing the other
    way.
  • An adoption naming a reserved capability in capabilities or notApplicable is refused, not
    silently filtered
    . The schema permits a warning or a failed run; this is a suite whose whole
    point is that unverified claims are loud, and a console line competes with Jest's own output, is
    invisible in the log of a green CI build, and would leave the adopter believing a claim was made
    that the report then dropped. The fix is a one-line edit, so refusing costs nothing.
    notApplicable is refused for the same reason: with no scenario to be inapplicable to, the
    statement is about nothing.
  • The emitter also filters reserved tags out of declaration.declared. The options check is where
    an adopter hears about it; that filter is the document being unable to carry the claim by any
    other route into a declared set, and it is what report.spec.ts asserts.

Capability resolution moves out of runProviderTck into resolveCapabilities, so the three rules
that exist to stop a wrong report can be tested without Jest — options.spec.ts is new.

Three schema changes since this was opened

All three are in spec#425 and this branch emits
against them.

  • backend.controlApi is required. A provider with no backend still had its flag state
    manipulated somehow, and which of the two ways that was is the single most important thing a reader
    needs in order to know what the results are worth. The old shape made the whole backend block
    omissible, which made the one value most worth knowing — in-process, the case the enum exists for
    — the one that could never appear. The control states it; nothing infers it.
  • A known deviation's summary is required, and its issue link is not. A deviation with no prose
    is a line of tags that tells a reader nothing; a deviation with no filed issue is an honest state to
    be in, and requiring one encourages filing a placeholder.
  • The declaration no longer claims to explain every skip, and this is the one JavaScript
    provoked. It explains the skips that come from an undeclared capability. It does not explain a skip
    the implementation refuses — @numeric-coercion here, because typeof 10 and typeof 0.5 are
    both 'number' and no JS provider can be asked for an integer as distinct from a float — which is a
    property of the language rather than of the provider. A reader inferring "the provider declined"
    from that absence is wrong in the direction that matters. Appendix F records which capabilities
    this applies to per language, and the results payload carries the distinction per skip.

Verification

Run against Node 18.19.1.

  • npx jest --config libs/shared/tck/jest.config.ts — 6 suites, 85 passed, 5 skipped. The
    five skips are the capability-gated scenarios, as before.
  • npx nx lint tck — clean, including @nx/dependency-checks for the new peer dependency.
    npx tsc -p libs/shared/tck/tsconfig.spec.json --noEmit — clean.
  • The executed feature files carry @numeric-coercion and no @strict-numeric-typing, and
    src/lib/revision.ts reports 8374621763f04a3d0785a911f12035db4794179e, which is the submodule's
    HEAD and what the envelope's tck.specRevision says.
  • declaration.declared is ["@configuration-change", "@events", "@object"], and carries no
    reserved tag. declaration.notApplicable is gone — the schema's declaration object is
    additionalProperties: false over declared alone, and @numeric-coercion is now refused by the
    implementation rather than declared inapplicable by each adopter, so there is nothing left for the
    field to carry. report.spec.ts asserts the emitted declaration has no such property.
  • The envelope validates against the reshaped schema from feat: add a schema for machine-readable provider conformance reports spec#425 with ajv
    (ajv/dist/2020), a Draft 2020-12 validator, and results.digest matches a sha256 of the
    .ndjson beside it.
  • The stream validates against the Cucumber Messages JSON schema, not merely against this
    library's idea of it. @cucumber/messages ships schema/*.json; all 23 files are loaded into
    ajv and every one of the 156 envelopes is checked against Envelope.json, which is
    additionalProperties: false throughout. messages.spec.ts does this on every run, so a
    malformed message is a test failure rather than something a consumer discovers.
run outcomes total
in-memory {"PASSED": 24, "SKIPPED": 5} 29
multi-provider {"PASSED": 8, "FAILED": 16, "SKIPPED": 5} 29

Both add up to 29, the scenario count of the canonical features (17 in errors counting outline
examples, 7 in evaluation, 2 in events, 3 in lifecycle), and the multi-provider numbers match
Jest's own tally exactly.

The gated skips are truthful. Resolving pickle → test case → attempt → step result the way a
consumer has to, all five gated scenarios in the in-memory run are SKIPPED, each with the
capabilities that gated it named on the result, and none is PASSED. The @numeric-coercion one
reads requires @numeric-coercion, which cannot hold for this provider: JavaScript has no integer type…, and the other four read …which this provider does not declare. The same holds in the
multi-provider run, where 16 scenarios failed around them.

Row identity resolves end to end. The eleven rows of the type-mismatch matrix carry eleven distinct
astNodeIds[1], each resolving in the GherkinDocument to its own TableRow — lines 28–31, 35–37
and 41–44 of errors.feature, matching the file's three Examples blocks in order:

pickle 62  astNodeIds ["25","9"]   -> row line 28  {"key":"string-flag","requested":"Boolean","default":"false"}
pickle 70  astNodeIds ["25","10"]  -> row line 29  {"key":"string-flag","requested":"Integer","default":"1"}
pickle 78  astNodeIds ["25","11"]  -> row line 30  {"key":"string-flag","requested":"Float","default":"0.1"}
…
pickle 142 astNodeIds ["25","23"]  -> row line 44  {"key":"float-flag","requested":"String","default":"fallback"}

Every pickle's tags match the tags the harness gated on, feature-level tags and Examples-block
tags included, which is what lets a consumer recover the capability behind a skip from the stream
and the declaration alone.

Honest caveats:

  • The multi-provider numbers were not re-measured after the last two commits. Neither commit
    changes a scenario count or an outcome, so they should still hold, but they are from an earlier
    run. They are also from a one-off run with testPathIgnorePatterns overridden on the command
    line. jest.config.ts still excludes that suite and this PR does not change that; it
    is the only thing that exercises the failure path end to end, and it reproduces the known SDK bug
    (open-feature/js-sdk#1452) exactly.
  • Schema validation did not check format keywords — ajv-formats is not installed in this
    workspace. Neither document emits a field with a format constraint (no provenance, no
    provider.repository), so nothing was skipped in practice. Anything adding those fields later
    should re-check with formats on.
  • nx test provider-tck still cannot run in my environment: its pullSpec step shells out to
    git submodule update --init, and this checkout is a git worktree whose .git file holds a
    Windows path the WSL git cannot follow. Jest was run directly. node scripts/write-revision.js
    was run and reproduces the committed revision.ts byte for byte.
  • The .ndjson is 84 KB for 29 scenarios, most of it the four feature sources. That is the cost of
    the stream being self-contained, and it is why the envelope references the results rather than
    inlining them.

Findings

  • jest-cucumber has no output layer whatsoever. Not a lossy one — none. There is no reporter, no
    JSON writer and no message emitter in its dist/, so any language binding of Cucumber that wants
    to report results has to build them itself. Of the four TCK implementations this was the only one
    where the results format had to be constructed from scratch rather than configured.
  • jest-cucumber never applies scenarioNameTemplate to Scenario Outline examples. It uses the
    expanded example title instead, so the reason a skipped example was skipped could not be shown.
    Worked around by composing the name in the harness; that fix lives in feat(provider-tck): add the JavaScript conformance suite for OpenFeature providers #1606. Worth reporting
    upstream.
  • jest-cucumber discards the Examples table. It substitutes each row into the outline's steps
    and title and keeps only the result, so the row a scenario came from cannot be recovered from what
    it hands back. Compiling the feature with @cucumber/gherkin gives it back properly, as pickle
    AST node ids, which is what the report now uses.
  • loadFeatures(glob) returns features in the glob's order with no file identity. In practice
    the order was neither alphabetical nor file order, which made the suite's own output ordering
    arbitrary between features. Enumerating the directory fixes both.
  • The SDK's MultiProvider reports its metadata name as _MultiProvider. Cosmetic, but it is
    what appears in a conformance report, and the leading underscore looks like it leaked from an
    internal name rather than being chosen.

@coderabbitai

coderabbitai Bot commented Aug 24, 2026 •

Copy link
Copy Markdown

@aepfli
aepfli force-pushed the feat/provider-tck-report branch from d3214d5 to 7021126 Compare August 24, 2026 20:01
@aepfli aepfli changed the title feat(provider-tck): emit a machine-readable conformance report feat(provider-tck): emit a conformance report, with results in Cucumber Messages Sep 10, 2026
@aepfli
aepfli force-pushed the feat/provider-tck-report branch 2 times, most recently from 8e88c9b to 14191cb Compare September 11, 2026 11:17
@aepfli
aepfli force-pushed the feat/provider-tck-report branch from 14191cb to 4752d27 Compare September 11, 2026 14:23
@aepfli
aepfli force-pushed the feat/provider-tck-report branch 16 times, most recently from c889079 to d2f8e3a Compare September 16, 2026 12:08
@aepfli
aepfli force-pushed the feat/provider-tck-report branch from d2f8e3a to 89c9131 Compare September 16, 2026 19:45
@aepfli
aepfli force-pushed the feat/provider-tck-report branch 2 times, most recently from 84f703d to 42a59f3 Compare October 2, 2026 08:57
@aepfli
aepfli force-pushed the feat/provider-tck-report branch from 42a59f3 to 4ff7b45 Compare October 8, 2026 08:20
@aepfli
aepfli force-pushed the feat/provider-tck branch from 8572e28 to 891a5a9 Compare October 9, 2026 07:38
@aepfli
aepfli force-pushed the feat/provider-tck-report branch from 4ff7b45 to 6cc113a Compare October 9, 2026 07:38
@aepfli
aepfli force-pushed the feat/provider-tck branch from 891a5a9 to e76d937 Compare October 14, 2026 13:21
@aepfli
aepfli force-pushed the feat/provider-tck-report branch 2 times, most recently from 56f9810 to 5c560e0 Compare October 14, 2026 14:31
@aepfli
aepfli force-pushed the feat/provider-tck branch from 4c47a42 to 907e4a3 Compare October 16, 2026 15:40
@aepfli
aepfli force-pushed the feat/provider-tck-report branch 2 times, most recently from cfecea2 to 630ec2b Compare October 16, 2026 15:58
Collapsed from 29 commits when this branch was replayed onto the comment cull.
The cull rewrote prose across the same files these commits touch, so a
commit-by-commit rebase produced a conflict in most of them, several carrying
real code rather than wording -- the kind of sequence where a wrong resolution
is easy and silent. One squashed replay gave one conflict pass instead, and this
repository squash-merges, so the granularity was never going to survive anyway.

The original subjects, oldest first:

  - feat(tck): emit a machine-readable conformance report
  - feat(tck): identify an outline scenario by its Examples row
  - fix(tck): give a failed capability a reason, and stop claiming untested ones
  - refactor(tck): carry results in Cucumber Messages
  - feat(tck): record which Cucumber Messages release produced the stream
  - refactor(tck): rename @strict-numeric-typing to @numeric-coercion
  - fix(tck): stop a reserved capability reaching a report's declaration
  - feat(tck): fail a run that did not execute the canonical set
  - fix(tck): record the spec revision the assets came from
  - fix(tck): read the spec revision from the submodule, not the environment
  - fix(tck): name a canonical feature relative to the asset directory
  - fix(tck): emit knownDeviations, which the report discarded
  - fix(tck): stop emitting declaration.notApplicable
  - fix(tck): follow the pin into the recorded spec revision
  - fix(tck): record the pin @disabled-flags arrived on
  - style(tck): format the example-table reader
  - fix(tck): record the revision the pin moved to
  - fix(tck): always emit backend and controlApi, which the schema requires
  - refactor(tck)!: drop the PROVIDER_ prefix from the report-directory variable
  - fix(tck): record the revision the pin moved to
  - docs(tck): stop saying CI sets the report directory, because it does not
  - fix(tck): record the revision the pin moved to
  - fix(tck): record the revision the pin moved to
  - fix(tck): say in the report which kind of skip a skip was
  - docs(tck): record the pin, and stop illustrating the discouraged shape
  - docs(tck): document reports in the rewritten README
  - docs(tck): link the report's rules to Appendix F rather than restating them
  - fix(tck): follow the narrowed mismatch matrix into the report's tallies
  - fix(tck): refresh the recorded spec revision and record the revision-check gaps

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
@aepfli
aepfli force-pushed the feat/provider-tck-report branch from 630ec2b to e13f111 Compare October 16, 2026 16:06

This branch has not been deployed

No deployments
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