Skip to content

chore(lint): sync the contract schema from the backend - #16

Merged
mivds merged 4 commits into
mainfrom
chore/sync-contract-schema-from-backend
Sep 10, 2026
Merged

mivds merged 4 commits into
mainfrom
chore/sync-contract-schema-from-backend

Conversation

@mivds

@mivds mivds commented Sep 9, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

go/internal/lint/schema/soda_data_contract_json_schema_1_0_0.json was branched from soda-core's copy in May and has drifted from the schema the backend validates every contract against on ingestion. sodacli contract lint therefore passed contracts that the backend then rejected — the opposite of what a linter is for.

Change

The embedded schema is now the backend's canonical contract schema, byte-identical to it apart from one docs-link fix (below). What that adds over the old copy:

  • four dataset-level keys — check_attributes, diagnostics, failed_rows, reconciliation
  • seven check types — aggregate_diff, duplicate_diff, freshness_diff, row_count_diff, group_by, hierarchy, reconciliation

No check type is lost. The 167 schema paths that disappear are all $defs reorganization (generic_check_properties, format, numeric_range, and the threshold if/then conditionals), which the backend expresses differently. soda_runner and soda_agent are both in the upstream schema now, so the backwards-compatible alias from PLATL-650 survives the swap.

Byte-identical is the point: the next sync is cp with nothing to remember. An earlier revision of this PR kept additionalProperties: false at the root, reasoning that a linter should catch typo'd keys; that deviation is gone. The backend sets it to true so a contract written for a newer server does not fail against an older one, and the lint schema should not be stricter than the thing it predicts.

Unknown keys deeper in the document are still rejected — the schema carries additionalProperties: false in 67 places, covering check bodies, thresholds and reconciliation blocks. Columns are permissive, because their definition is an allOf and a false inside one branch would reject keys defined by sibling branches.

Worth knowing what that leaves uncovered: soda-core's parser reads only the root keys it knows and silently ignores the rest (no warning at any log level — the sole exception is an unknown threshold key, which warns), and the backend accepts them by design. So a typo'd root key is reported by nothing. That is the backend's deliberate forward-compatibility trade, and this PR follows it rather than diverging from it.

The lint tests no longer assert what the schema means

This repo holds a copy of the backend's schema; what that schema means is the backend's business. Re-deriving it here bought nothing and made every sync a test-editing exercise — a real cost, since a legitimate upstream change fails CI here until someone comes and loosens the assertions. That is part of why the copy sat four months stale.

Seven tests are removed on those grounds. Three were added earlier in this PR (AcceptsDatasetLevelContractOptions, AllowsUnknownRootKeysLikeTheBackend, RejectsUnknownKeyInsideACheck); four predate it (MissingColumnName, AcceptsSodaRunner, AcceptsSodaAgentLegacyAlias, RejectsBothSodaRunnerAndSodaAgent, from 92ed81e and 284d7f3). The soda_runner pair shows the problem plainly: PLATL-650 added the key to this copy and a test for it, the backend has since added the key upstream, so those tests now pass for a reason unrelated to why they were written and would break if the backend restructured that oneOf.

Six tests remain, all covering this repo's code rather than the schema's content:

test what it covers
ValidContract the embedded schema compiles and a valid contract passes
YAMLParseError malformed YAML is reported at the document root
EmptyFile an empty file is invalid
LintFiles_Mixed one result per input, in input order
ReportsErrorPathsForAnInvalidContract the jsonschema-to-LintError translation: every error carries a message and a $-rooted path
SegmentsToPath JSON path formatting

ReportsErrorPathsForAnInvalidContract replaces what the removed invalid-contract cases incidentally covered. It asserts path and message shape, not which key is wrong, and leans only on required: ["dataset", "columns"] — the one thing identical across every copy and schema version compared here.

One content fix on top of the sync

The upstream schema's check-attributes description pointed at https://beta.docs.soda.io/…, a host that does not resolve at all. It now points at the published page, https://docs.soda.io/soda-v4/manage-issues/check-and-dataset-attributes#attributes-in-checks (verified 200, and the anchor matches the ## Attributes in checks heading), which is the form every other link in the schema uses.

Fixed upstream too — sodadata/soda#13496 — so the next sync keeps the correction instead of reverting it. Until that merges, this file differs from master's copy by exactly that one line.

Testing

go build ./..., go vet ./... and go test ./... clean; the six remaining lint tests pass.

Validated on a 15-contract corpus — the backend's own contract-parser fixtures plus every complete contract example in the docs — by building the binary before and after and diffing the verdicts: identical, re-confirmed after both the root-permissiveness change and the test removal. The two contracts that fail are incomplete documentation snippets missing the required columns, which the old schema rejected too.

The embedded lint schema was branched from soda-core's copy in May and
has drifted from the schema the backend validates contracts against on
ingestion, so `contract lint` passed files the backend then rejected.

It is now the backend's canonical schema, which adds four dataset-level
keys (check_attributes, diagnostics, failed_rows, reconciliation) and
seven check types (aggregate_diff, duplicate_diff, freshness_diff,
row_count_diff, group_by, hierarchy, reconciliation). No check type is
lost; soda_runner and soda_agent are both upstream now, so the
backwards-compatible alias from PLATL-650 survives the swap.

One deliberate deviation: the root keeps additionalProperties: false
where the backend has true. A linter exists to catch typo'd keys, and
TestLintFile_InvalidProperty pins it — that test fails against a schema
with the backend's permissive root.

Verified on 15 real contracts (the backend's parser fixtures plus every
complete contract example in the docs): verdicts are identical before
and after, and the two that fail are incomplete snippets missing the
required `columns`, which the old schema rejected too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mivds
mivds force-pushed the chore/sync-contract-schema-from-backend branch from ae3ffa2 to 1c08cb9 Compare September 9, 2026 23:04
The upstream schema carries a beta.docs.soda.io URL for check
attributes; that host does not resolve. The published page is
docs.soda.io/soda-v4/manage-issues/check-and-dataset-attributes, which
matches how every other link in this schema is formed.

Also fixed upstream, so the next sync keeps it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@m1n0 m1n0 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked the swap: the embedded file is byte-equal to origin/master's soda-server/src/main/resources/contract/schema.json plus your one root edit, so the provenance claim holds. Built the binary and ran the contract from SCS-1441, where a customer put attributes on a metric check. Valid now, 6 errors before.

Two things before this lands.

The root additionalProperties: false deviation reintroduces the bug this PR fixes. A contract with a top-level kind: gets rejected by lint but is accepted by both Cloud and the engine:

kind: contract
dataset: ds/db/schema/t
columns:
  - name: id

lint says $: additional properties 'kind' not allowed, soda contract test says valid, and the canonical schema accepts it. kind is the collection dispatch key in soda-core at check_collections/base.py:237 and session.py:163, with values contract, data-standard, metric-monitoring. I'd keep the deviation, I agree a linter should catch typo'd keys, but add kind to the properties list so it's a two-line delta instead of one.

Second, nothing runs go test in CI. .github/workflows/ only has release.yml, tag-triggered, goreleaser. So TestLintFile_AcceptsDatasetLevelContractOptions never runs on a PR and the drift this fixes can come back without anyone noticing. A small test workflow here or straight after would close it.

Two smaller ones. The body says seven new check types, it's eleven. reconciliation is a top-level key rather than a check type, and fuzzy_duplicate, llm, metric_diff, reference_diff, rows_diff are missing from the list. And if the 15-contract corpus gives identical verdicts before and after, none of those fixtures exercised a newly-allowed key, so it's good regression evidence but doesn't show the fix works. The SCS-1441 contract would make a better fixture.

Nice catch on the dead ~/.soda/cache path, that had me looking too.

@mivds mivds self-assigned this Sep 10, 2026
mivds and others added 2 commits September 10, 2026 08:57
The sync kept additionalProperties: false at the root, on the reasoning
that a linter should catch typo'd keys. The backend sets it to true so a
contract written for a newer server does not fail against an older one,
and the lint schema should not be stricter than the thing it predicts.

The embedded schema is now byte-identical to the backend's, so the next
sync is a plain copy with no edit to remember.

TestLintFile_InvalidProperty asserted the opposite; it now covers a
place the schema is still strict (an unknown key inside a check body),
and TestLintFile_AllowsUnknownRootKeysLikeTheBackend documents the root
behaviour. TestLintFiles_Mixed's invalid file relied on a missing
required 'columns' rather than the unknown key it also carried, so it
now says that plainly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This repo holds a copy of the backend's contract schema; what that
schema means is the backend's business, and re-deriving it here buys
nothing while making every sync a test-editing exercise. That cost was
real: a legitimate upstream change failed CI here until someone came and
loosened the assertions, which is part of why the copy sat four months
stale.

Removed, all of them assertions about schema content rather than about
this repo's code:

- AcceptsDatasetLevelContractOptions, AllowsUnknownRootKeysLikeTheBackend
  and RejectsUnknownKeyInsideACheck, added earlier in this PR
- MissingColumnName, AcceptsSodaRunner, AcceptsSodaAgentLegacyAlias and
  RejectsBothSodaRunnerAndSodaAgent, which predate it (92ed81e, 284d7f3)

What stays tests the lint command itself: the embedded schema compiles
and a valid contract passes, malformed YAML and an empty file are
reported at the document root, LintFiles returns one result per input in
order, and segmentsToPath formats JSON paths. In place of the removed
invalid-contract cases, ReportsErrorPathsForAnInvalidContract covers the
jsonschema-to-LintError translation — that every error carries a message
and a $-rooted path — without asserting which key is wrong.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@m1n0 m1n0 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-checked at bceaaba. kind is fixed, the schema is byte-identical to origin/master apart from the docs link, and my corpus is green: the SCS-1441 contract, attributes on metric and failed_rows at both levels, column_expression, remediation, group_by, check_attributes, the DET-107 additional: block. filter on a schema check is still correctly rejected. Good call fixing the link upstream in sodadata/soda#13496 too, that's the right order, though it's still open so a sync before it merges would revert your line.

The test deletions in bceaaba go too far.

I copied the old pre-sync schema over the new one and ran the current suite against it. It passes, all 6 tests. So nothing left in the suite can tell whether this PR's schema change happened at all. Revert the sync and the tests stay green.

TestLintFile_AcceptsSodaRunner, TestLintFile_AcceptsSodaAgentLegacyAlias and TestLintFile_RejectsBothSodaRunnerAndSodaAgent aren't assertions about the backend's schema. They're the soda_runner/soda_agent alias promise from PLATL-650, which is soda-cli's own commitment, and those tests are what stops a later sync quietly dropping it. I'd put all three back.

TestLintFile_AcceptsDatasetLevelContractOptions was the one test proving this PR does something. The SCS-1441 contract would be a good second fixture, it's four lines and it's the case that generated the ticket.

I verified all five deleted behaviours still hold at head, so this is lost coverage rather than a break. TestLintFile_ReportsErrorPathsForAnInvalidContract doesn't substitute: asserting that paths start with $ and messages are non-empty would pass even if the schema rejected every contract on earth.

Body's stale in the Change section. It still says the one deviation is the root keeping additionalProperties: false and cites TestLintFile_InvalidProperty as pinning it. b77622e undid both.

And still nothing runs go test in CI, which matters more now the suite is 6 tests instead of 11.

@mivds
mivds marked this pull request as ready for review September 10, 2026 07:23
@mivds

mivds commented Sep 10, 2026 •

Copy link
Copy Markdown
Collaborator Author

Thanks for the review @m1n0. I'll enable CI testing in a separate PR. Apart from that: I dropped contract validation tests since this is a copy of the schema from BE. Validation should live in one central place, not here.

@mivds
mivds requested a review from m1n0 September 10, 2026 08:26
@mivds
mivds merged commit 14dda71 into main Sep 10, 2026
1 check passed
mivds added a commit that referenced this pull request Sep 11, 2026
The offline CLI suite fails on main: TestContractLint/invalid_contract
expects `bogus_field` at the contract root to exit 2, but lint accepts it
and exits 0.

Two PRs merged a minute apart caused this. #16 synced the contract JSON
schema from the backend, which flips the root from
"additionalProperties": false to true — unknown top-level keys are now
allowed by design — and dropped its own TestLintFile_InvalidProperty
accordingly. #17 branched before that landed and added a CLI test
asserting the old behavior. Both were green against their own base; the
merge of the two is not.

The schema is generated backend-side, so pinning the root closed again
would be reverted by the next sync and would reject contracts the
backend accepts. Point the subtest at a check object instead: those are
still closed, so it keeps CLI-level coverage of the "additional
properties not allowed" path rather than deleting it.

Refs: #16, #17

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.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.

2 participants