From 2026fabfa757988a3802075438a4a836014dfb26 Mon Sep 17 00:00:00 2001 From: Simon Schrottner Date: Mon, 24 Aug 2026 14:48:04 +0200 Subject: [PATCH 1/5] feat: add a schema for machine-readable provider conformance reports First step of the design discussed in #424: the report format, before any implementation emits one, so four languages do not invent four dialects. Three properties are deliberate and are the parts worth arguing about. Four outcomes, not two. not-declared and not-applicable are distinct, and collapsing them misrepresents a provider: @strict-numeric-typing is unsatisfiable in JavaScript because the language has no integer type, so reporting it as not-declared would show every JS provider as missing something no JS provider can have. Per-scenario results are required, and required to be complete. That is what makes the appendix's own rule -- a skipped scenario is never reported as passed -- checkable rather than aspirational. It is not hypothetical: godog's summary counts capability-gated skips as passes, so the Go runner satisfies the rule only in a separate log line. A runner with that flaw still produces a truthful report here. The spec revision sits inside the tck object rather than standing alone, because it is a property of the artifact that ran rather than an independent assertion, and it travels with a digest of the assets actually executed. The revision is a claim; the digest is the check that catches a locally edited vendored file. There is deliberately no aggregate score. A single percentage would reward declaring fewer capabilities, which inverts the incentive the suite exists to create. Signed-off-by: Simon Schrottner --- specification/assets/provider-tck/README.md | 1 + .../report/conformance-report.schema.json | 171 ++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 specification/assets/provider-tck/report/conformance-report.schema.json diff --git a/specification/assets/provider-tck/README.md b/specification/assets/provider-tck/README.md index 292946c8..bc776e4c 100644 --- a/specification/assets/provider-tck/README.md +++ b/specification/assets/provider-tck/README.md @@ -14,6 +14,7 @@ These validate a **provider** against a real backend. For assets that validate a | [`gherkin/lifecycle.feature`](./gherkin/lifecycle.feature) | initialisation against a healthy backend and against an unreachable one | | [`flags/canonical-flags.json`](./flags/canonical-flags.json) | the flag set every scenario assumes | | [`openapi/control-api.yaml`](./openapi/control-api.yaml) | the HTTP surface a backend under test must expose | +| [`report/conformance-report.schema.json`](./report/conformance-report.schema.json) | the shape of a machine-readable conformance report | ## These three travel together diff --git a/specification/assets/provider-tck/report/conformance-report.schema.json b/specification/assets/provider-tck/report/conformance-report.schema.json new file mode 100644 index 00000000..f5a420a6 --- /dev/null +++ b/specification/assets/provider-tck/report/conformance-report.schema.json @@ -0,0 +1,171 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://openfeature.dev/schema/provider-tck/conformance-report-1.json", + "title": "OpenFeature provider conformance report", + "description": "The machine-readable result of one run of the provider conformance suite (Appendix F) against one provider in one configuration. Emitted by a TCK implementation; consumed by anything that aggregates or displays conformance. See https://github.com/open-feature/spec/issues/424.", + "type": "object", + "additionalProperties": false, + "required": ["schemaVersion", "provider", "sdk", "tck", "capabilities", "scenarios"], + "properties": { + "schemaVersion": { + "description": "The version of this schema the report conforms to. Integer-as-string so that a consumer can reject a report it does not understand rather than guessing.", + "type": "string", + "pattern": "^[1-9][0-9]*$" + }, + + "provider": { + "type": "object", + "additionalProperties": false, + "required": ["name", "language"], + "properties": { + "name": { "type": "string", "minLength": 1 }, + "version": { "type": "string" }, + "language": { + "enum": ["go", "java", "javascript", "dotnet", "python", "php", "ruby", "kotlin", "swift", "rust", "other"] + }, + "configuration": { + "description": "Which configuration of the provider was tested, when a provider has more than one materially different mode. One provider may therefore produce several reports, and they are not interchangeable: flagd's RPC and in-process resolvers differ in whether they emit PROVIDER_STALE, so a report keyed on provider name alone would have to pick one and misrepresent the other.", + "type": "string" + }, + "repository": { "type": "string", "format": "uri" } + } + }, + + "sdk": { + "type": "object", + "additionalProperties": false, + "required": ["name", "version"], + "properties": { + "name": { "type": "string", "minLength": 1 }, + "version": { "type": "string", "minLength": 1 } + } + }, + + "tck": { + "description": "What asked the questions, and which questions. The spec revision is a property of the TCK artifact rather than an independent assertion: a given TCK build consumes exactly one revision of the assets.", + "type": "object", + "additionalProperties": false, + "required": ["implementation", "version", "specRevision"], + "properties": { + "implementation": { + "description": "Which TCK implementation produced this, e.g. 'go-sdk-contrib/tools/provider-tck'.", + "type": "string", + "minLength": 1 + }, + "version": { "type": "string", "minLength": 1 }, + "specRevision": { + "description": "The open-feature/spec commit the executed assets came from.", + "type": "string", + "minLength": 7 + }, + "specRelease": { + "description": "A human-readable spec release tag, when one applies. A commit is precise; a tag is what a person can reason about.", + "type": "string" + }, + "gherkinDigest": { + "description": "Digest over the feature files and canonical flag set actually executed, as 'sha256:'. The revision is an assertion; this is a check. A recorded revision can be wrong if a vendored file was edited locally, and this is what catches that -- which is the drift the whole suite exists to prevent.", + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$" + } + } + }, + + "backend": { + "description": "What the provider was pointed at. Omitted for a provider with no backend, which is itself informative.", + "type": "object", + "additionalProperties": false, + "properties": { + "description": { "type": "string" }, + "controlApi": { + "description": "How the backend was driven. 'http' means the normative control API; 'in-process' is the narrow allowance for providers with no backend, and a report claiming it for a provider that has one should be treated with suspicion.", + "enum": ["http", "in-process"] + } + } + }, + + "provenance": { + "description": "Where this report came from. A report is a claim, not an audit, and this is what lets a consumer decide how much to trust it.", + "type": "object", + "additionalProperties": false, + "properties": { + "runUrl": { "type": "string", "format": "uri" }, + "commit": { "type": "string" }, + "timestamp": { "type": "string", "format": "date-time" }, + "attestation": { + "description": "An optional signed attestation (e.g. sigstore bundle) covering this document.", + "type": "string" + } + } + }, + + "capabilities": { + "description": "Per-capability outcome, keyed by Gherkin tag including the leading at-sign.", + "type": "object", + "propertyNames": { "pattern": "^@[a-z0-9-]+$" }, + "additionalProperties": { "$ref": "#/$defs/capabilityResult" } + }, + + "scenarios": { + "description": "Per-scenario outcome. Required, and required to be complete: it is what makes 'a skipped scenario is never reported as passed' checkable rather than a promise. A runner whose own summary counts skips as passes still produces a truthful report here.", + "type": "array", + "items": { "$ref": "#/$defs/scenarioResult" } + } + }, + + "$defs": { + "outcome": { + "description": "Four outcomes, not two. 'not-declared' and 'not-applicable' are distinct and collapsing them misrepresents a provider: @strict-numeric-typing is unsatisfiable in JavaScript because the language has no integer type, so reporting it as 'not-declared' would show every JS provider as missing something no JS provider can have.", + "enum": ["passed", "failed", "not-declared", "not-applicable"] + }, + + "capabilityResult": { + "oneOf": [ + { "$ref": "#/$defs/outcome" }, + { + "type": "object", + "additionalProperties": false, + "required": ["state"], + "properties": { + "state": { "$ref": "#/$defs/outcome" }, + "reason": { + "description": "Why, in a form a human reading a comparison page can use. Required in practice for anything other than 'passed' -- 'this provider does not support configuration-change events' is useful information for someone choosing a provider.", + "type": "string" + }, + "knownDeviation": { + "description": "Set when the provider fails a capability the specification does not treat as optional, and the gap is tracked. Distinguishes 'chose not to implement an optional feature' from 'has a known bug', which the tag vocabulary alone cannot express.", + "type": "object", + "additionalProperties": false, + "properties": { + "issue": { "type": "string", "format": "uri" }, + "summary": { "type": "string" } + } + } + } + } + ] + }, + + "scenarioResult": { + "type": "object", + "additionalProperties": false, + "required": ["feature", "name", "outcome"], + "properties": { + "feature": { + "description": "The feature file, without extension, e.g. 'errors'.", + "type": "string" + }, + "name": { "type": "string", "minLength": 1 }, + "tags": { + "type": "array", + "items": { "type": "string", "pattern": "^@[a-z0-9-]+$" } + }, + "outcome": { "$ref": "#/$defs/outcome" }, + "reason": { + "description": "For a skip, why it was skipped. For a failure, what failed.", + "type": "string" + }, + "durationMs": { "type": "number", "minimum": 0 } + } + } + } +} From 060b7a4962e2465aa8066add430c5c3b07e2c615 Mon Sep 17 00:00:00 2001 From: Simon Schrottner Date: Mon, 24 Aug 2026 16:20:46 +0200 Subject: [PATCH 2/5] feat: identify executed assets by git tree hash rather than a content digest The schema carried a `gherkinDigest`, a SHA-256 over the feature files and the canonical flag set, on the argument that `specRevision` is an assertion and a digest is a check. The check is worth having; computing it ourselves is not. It would have made every implementation carry a normative hashing algorithm -- agreeing on file selection, path form, ordering, framing and line-ending normalisation, the last of which matters because Go embeds committed copies that a Windows checkout converts to CRLF. Four implementations of that is a lot of surface for something git already computes. `git rev-parse :specification/assets/provider-tck` is the same idea with none of the specification. It has two properties the digest was reaching for. It is verifiable: a third party can recompute it from the recorded revision, so a revision recorded wrongly does not pass unnoticed. And it is stable across unrelated spec commits, so two runs that executed identical assets report the same value even when pinned to different commits -- which the commit SHA alone gets wrong, since it changes for every unrelated edit to the specification. The drift the digest was chiefly aimed at -- a vendored copy edited locally while the recorded revision still points at the original -- is in any case already handled where it arises. Java, Python and JavaScript read the assets straight from the submodule, so no second copy exists to diverge. Go has committed copies and a CI check that regenerates them and fails on any difference. It is a build-time problem with a build-time fix, and pushing it into the report format would have every implementation re-detect what its own CI already catches. The cost is that the tree covers the asset directory's documentation too, so a README edit changes it. That errs towards reporting two comparable runs as different rather than the reverse, which is the safe direction. Signed-off-by: Simon Schrottner --- .../provider-tck/report/conformance-report.schema.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/specification/assets/provider-tck/report/conformance-report.schema.json b/specification/assets/provider-tck/report/conformance-report.schema.json index f5a420a6..ea696474 100644 --- a/specification/assets/provider-tck/report/conformance-report.schema.json +++ b/specification/assets/provider-tck/report/conformance-report.schema.json @@ -62,10 +62,10 @@ "description": "A human-readable spec release tag, when one applies. A commit is precise; a tag is what a person can reason about.", "type": "string" }, - "gherkinDigest": { - "description": "Digest over the feature files and canonical flag set actually executed, as 'sha256:'. The revision is an assertion; this is a check. A recorded revision can be wrong if a vendored file was edited locally, and this is what catches that -- which is the drift the whole suite exists to prevent.", + "assetsTree": { + "description": "The git tree object ID of specification/assets/provider-tck at that revision, as 40 lowercase hex characters. Two things make this worth carrying alongside the revision. It is stable across unrelated spec commits, so two runs that executed identical assets report the same value even when pinned to different commits -- which is precisely the comparability question a consumer is asking. And it is checkable rather than merely asserted: `git rev-parse :specification/assets/provider-tck` must reproduce it, so a revision recorded wrongly does not go unnoticed. Note that it covers the whole asset directory including its documentation, so a README edit changes it; that errs towards reporting two comparable runs as different, rather than the reverse.", "type": "string", - "pattern": "^sha256:[0-9a-f]{64}$" + "pattern": "^[0-9a-f]{40}$" } } }, From cc5cbd52377f26fefb87791621dc572f3990579f Mon Sep 17 00:00:00 2001 From: Simon Schrottner Date: Mon, 24 Aug 2026 17:26:21 +0200 Subject: [PATCH 3/5] feat: enforce reasons, and let a single scenario carry a known deviation Three changes, all of them found by implementing the format rather than by reading it. Writing four emitters against this schema was always going to be the real review, and it was. A reason is now required whenever an outcome is not "passed", at both the capability and the scenario level. The schema previously said it was "required in practice", which is another way of saying not required: nothing enforced it, and a report of bare tags with no explanation would have validated. The reason is most of the value to a person comparing providers -- "does not support configuration-change events" is information, "@configuration-change: not-declared" is a shrug. knownDeviation moves to a shared definition and becomes available on a scenario as well as a capability, because the two are different claims. A capability-level deviation says the provider does not do X. A scenario-level one pins the deviation to the single case that fails, which is what an implementation marking one expected failure has to record -- the Python suite marks exactly one, for open-feature/python-sdk#619, and until now had nowhere to put it but prose in the reason string. The capabilities object now states plainly that it is not a verdict. Scenarios carrying no capability tag are mandatory, roll up into no capability, and are therefore invisible in that summary -- so a provider can fail a mandatory scenario while every entry reads "passed". The Python implementation produced exactly that report. A consumer deciding whether a provider conforms has to read the scenarios, which is the reason those are required and required to be complete, and the schema should say so where someone will read it rather than leaving it to be discovered. All reports emitted by the Go, Python and JavaScript implementations still validate, and the two new constraints were checked to reject a report that omits a reason rather than merely being decorative. Signed-off-by: Simon Schrottner --- .../report/conformance-report.schema.json | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/specification/assets/provider-tck/report/conformance-report.schema.json b/specification/assets/provider-tck/report/conformance-report.schema.json index ea696474..9136dc89 100644 --- a/specification/assets/provider-tck/report/conformance-report.schema.json +++ b/specification/assets/provider-tck/report/conformance-report.schema.json @@ -99,7 +99,7 @@ }, "capabilities": { - "description": "Per-capability outcome, keyed by Gherkin tag including the leading at-sign.", + "description": "Per-capability outcome, keyed by Gherkin tag including the leading at-sign. This is a summary of the OPTIONAL contract only and is not a verdict: scenarios carrying no capability tag are mandatory, roll up into no capability, and are therefore invisible here. A provider can fail a mandatory scenario while every entry in this object reads 'passed', so a consumer deciding whether a provider conforms must read 'scenarios'. That is why per-scenario results are required and required to be complete.", "type": "object", "propertyNames": { "pattern": "^@[a-z0-9-]+$" }, "additionalProperties": { "$ref": "#/$defs/capabilityResult" } @@ -125,21 +125,15 @@ "type": "object", "additionalProperties": false, "required": ["state"], + "if": { "properties": { "state": { "const": "passed" } } }, + "else": { "required": ["reason"] }, "properties": { "state": { "$ref": "#/$defs/outcome" }, "reason": { - "description": "Why, in a form a human reading a comparison page can use. Required in practice for anything other than 'passed' -- 'this provider does not support configuration-change events' is useful information for someone choosing a provider.", + "description": "Why, in a form a human reading a comparison page can use. Required for anything other than 'passed', and enforced by this schema rather than left to convention -- 'this provider does not support configuration-change events' is useful information for someone choosing a provider, and a bare tag with no explanation is not.", "type": "string" }, - "knownDeviation": { - "description": "Set when the provider fails a capability the specification does not treat as optional, and the gap is tracked. Distinguishes 'chose not to implement an optional feature' from 'has a known bug', which the tag vocabulary alone cannot express.", - "type": "object", - "additionalProperties": false, - "properties": { - "issue": { "type": "string", "format": "uri" }, - "summary": { "type": "string" } - } - } + "knownDeviation": { "$ref": "#/$defs/knownDeviation" } } } ] @@ -149,6 +143,8 @@ "type": "object", "additionalProperties": false, "required": ["feature", "name", "outcome"], + "if": { "properties": { "outcome": { "const": "passed" } } }, + "else": { "required": ["reason"] }, "properties": { "feature": { "description": "The feature file, without extension, e.g. 'errors'.", @@ -161,10 +157,21 @@ }, "outcome": { "$ref": "#/$defs/outcome" }, "reason": { - "description": "For a skip, why it was skipped. For a failure, what failed.", + "description": "For a skip, why it was skipped. For a failure, what failed. Required whenever the outcome is not 'passed', and enforced rather than merely expected: 'not-declared' without a reason is the difference between a report a person can act on and a wall of tags.", "type": "string" }, - "durationMs": { "type": "number", "minimum": 0 } + "durationMs": { "type": "number", "minimum": 0 }, + "knownDeviation": { "$ref": "#/$defs/knownDeviation" } + } + }, + + "knownDeviation": { + "description": "Set when a provider fails something the specification does not treat as optional, and the gap is tracked. It distinguishes 'chose not to implement an optional feature' from 'has a known bug', which the tag vocabulary alone cannot express. It applies at both levels because the two are not the same claim: a capability-level deviation says the provider does not do X, while a scenario-level one pins the deviation to the single case that fails, which is what an implementation marking one expected failure has to record.", + "type": "object", + "additionalProperties": false, + "properties": { + "issue": { "type": "string", "format": "uri" }, + "summary": { "type": "string" } } } } From ad22c1955528b9ecf44181093b84277a7df34d5a Mon Sep 17 00:00:00 2001 From: Simon Schrottner Date: Mon, 24 Aug 2026 21:09:11 +0200 Subject: [PATCH 4/5] feat: identify a Scenario Outline row by its parameters A scenario entry was identified by feature and name, and every row of a Scenario Outline shares one name. The type-mismatch matrix is eleven rows, so a report carried eleven entries distinguished by nothing but a duration measurement. When one row fails and ten pass -- which is what the Python implementation actually produced -- the report cannot say which failed, and a consumer keying on feature and name keeps whichever row it happened to see last. An entry now carries the Examples row it came from, as the row's parameters keyed by column header, and identity is feature, name and example together. The parameters are the identity, and they come from the feature file rather than from any runner, which is why this is a field rather than a naming convention. Mandating a mangled name would have put a separator, an ordering and an escaping rule for values containing the separator into normative text that four implementations must reproduce byte-for-byte, where drift stays invisible until two reports quietly fail to line up. That was not a hypothetical risk. Before this field existed the three implementations had already diverged on exactly this point, each reasonably and none compatibly: Go emitted the bare scenario name for all eleven rows, Python appended pytest's example id, JavaScript used jest-cucumber's expanded title. The same row of the same feature file had three identities, which defeats the comparison the format exists to make possible. Values are the cell contents verbatim as strings, because Gherkin has no types. Coercing "1" to a number here would make the report disagree with the table it came from. Signed-off-by: Simon Schrottner --- .../provider-tck/report/conformance-report.schema.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/specification/assets/provider-tck/report/conformance-report.schema.json b/specification/assets/provider-tck/report/conformance-report.schema.json index 9136dc89..d081e6c0 100644 --- a/specification/assets/provider-tck/report/conformance-report.schema.json +++ b/specification/assets/provider-tck/report/conformance-report.schema.json @@ -106,7 +106,7 @@ }, "scenarios": { - "description": "Per-scenario outcome. Required, and required to be complete: it is what makes 'a skipped scenario is never reported as passed' checkable rather than a promise. A runner whose own summary counts skips as passes still produces a truthful report here.", + "description": "Per-scenario outcome. Required, and required to be complete: it is what makes 'a skipped scenario is never reported as passed' checkable rather than a promise. A runner whose own summary counts skips as passes still produces a truthful report here. An entry is identified by feature, name and example together -- not by feature and name, which are shared by every row of a Scenario Outline.", "type": "array", "items": { "$ref": "#/$defs/scenarioResult" } } @@ -151,6 +151,12 @@ "type": "string" }, "name": { "type": "string", "minLength": 1 }, + "example": { + "description": "The Examples row this entry came from, as the row's parameters keyed by column header. Required for a scenario originating from a Scenario Outline, omitted otherwise.\n\nWithout it a report is ambiguous exactly where it matters most. Every row of an outline shares one scenario name, so eleven rows of a type-mismatch matrix produce eleven entries with identical feature and name; if one fails and ten pass, the report cannot say which failed, and a consumer keying on feature and name keeps whichever row it saw last.\n\nIt is a field rather than a naming convention because the parameters are the identity, and they come from the feature file rather than from any runner. Mandating a mangled name instead would put a separator, an ordering and an escaping rule into normative text that every implementation must reproduce byte-for-byte, and drift there is invisible until two reports silently fail to line up. Implementations were observed diverging on precisely this point before the field existed: one emitted the bare scenario name for all eleven rows, another appended its runner's example id, a third its runner's expanded title.\n\nValues are the cell contents verbatim, as strings, because Gherkin has no types -- no coercion, no trimming beyond what the parser does, so the report says what the table said.", + "type": "object", + "minProperties": 1, + "additionalProperties": { "type": "string" } + }, "tags": { "type": "array", "items": { "type": "string", "pattern": "^@[a-z0-9-]+$" } From 56ed4f9c993fa4504a524de6476de3b5fe0d4e29 Mon Sep 17 00:00:00 2001 From: Simon Schrottner Date: Mon, 24 Aug 2026 21:56:41 +0200 Subject: [PATCH 5/5] docs: state that example key order is not significant Two implementations asked independently whether the four languages should agree on the key order within an example object. They currently do not: Java emits Examples column order, Go sorts alphabetically because encoding/json sorts map keys when marshalling, and neither is wrong. A JSON object is unordered, so the answer is that order carries no meaning and implementations need not agree. Saying so explicitly is worth a sentence, because the alternative is three more implementations each deciding privately and someone eventually byte-comparing two reports and concluding they disagree when they do not. If byte-level reproducibility is ever required -- an attestation over the document is the obvious case -- that is a canonicalisation problem for the whole report, not something to solve inside one field. Signed-off-by: Simon Schrottner --- .../assets/provider-tck/report/conformance-report.schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specification/assets/provider-tck/report/conformance-report.schema.json b/specification/assets/provider-tck/report/conformance-report.schema.json index d081e6c0..60c2ae7c 100644 --- a/specification/assets/provider-tck/report/conformance-report.schema.json +++ b/specification/assets/provider-tck/report/conformance-report.schema.json @@ -152,7 +152,7 @@ }, "name": { "type": "string", "minLength": 1 }, "example": { - "description": "The Examples row this entry came from, as the row's parameters keyed by column header. Required for a scenario originating from a Scenario Outline, omitted otherwise.\n\nWithout it a report is ambiguous exactly where it matters most. Every row of an outline shares one scenario name, so eleven rows of a type-mismatch matrix produce eleven entries with identical feature and name; if one fails and ten pass, the report cannot say which failed, and a consumer keying on feature and name keeps whichever row it saw last.\n\nIt is a field rather than a naming convention because the parameters are the identity, and they come from the feature file rather than from any runner. Mandating a mangled name instead would put a separator, an ordering and an escaping rule into normative text that every implementation must reproduce byte-for-byte, and drift there is invisible until two reports silently fail to line up. Implementations were observed diverging on precisely this point before the field existed: one emitted the bare scenario name for all eleven rows, another appended its runner's example id, a third its runner's expanded title.\n\nValues are the cell contents verbatim, as strings, because Gherkin has no types -- no coercion, no trimming beyond what the parser does, so the report says what the table said.", + "description": "The Examples row this entry came from, as the row's parameters keyed by column header. Required for a scenario originating from a Scenario Outline, omitted otherwise.\n\nWithout it a report is ambiguous exactly where it matters most. Every row of an outline shares one scenario name, so eleven rows of a type-mismatch matrix produce eleven entries with identical feature and name; if one fails and ten pass, the report cannot say which failed, and a consumer keying on feature and name keeps whichever row it saw last.\n\nIt is a field rather than a naming convention because the parameters are the identity, and they come from the feature file rather than from any runner. Mandating a mangled name instead would put a separator, an ordering and an escaping rule into normative text that every implementation must reproduce byte-for-byte, and drift there is invisible until two reports silently fail to line up. Implementations were observed diverging on precisely this point before the field existed: one emitted the bare scenario name for all eleven rows, another appended its runner's example id, a third its runner's expanded title.\n\nValues are the cell contents verbatim, as strings, because Gherkin has no types -- no coercion, no trimming beyond what the parser does, so the report says what the table said.\n\nKey order is not significant and implementations are not required to agree on it: a JSON object is unordered, and implementations observed so far differ (one emits Examples column order, another sorts alphabetically because its language sorts map keys when marshalling). Compare these by parsing, not by comparing bytes. If byte-level reproducibility is ever needed -- for an attestation over the document, say -- that is a canonicalisation concern for the whole report rather than something to solve in this one field.", "type": "object", "minProperties": 1, "additionalProperties": { "type": "string" }