Add IOA field invariants for cross-field validation#110
Merged
nerdsane merged 5 commits intonerdsane:mainfrom Apr 14, 2026
Merged
Add IOA field invariants for cross-field validation#110nerdsane merged 5 commits intonerdsane:mainfrom
nerdsane merged 5 commits intonerdsane:mainfrom
Conversation
Introduces [[field_invariant]] in IOA spec grammar to declare single-entity
predicates ("when X matches, Y must also match") that run on the OData write
path alongside relation and cross-invariant checks. Extends [[cross_invariant]]
to accept related(Target, fk).<Field> (in|not in) [...] on arbitrary field
names, not just status. Motivating use case is the upcoming Crucible reference
app rejecting POST /tdata/Environments where ConfigType == "Local" sets
cloud-only fields; see docs/adrs/0041-ioa-field-invariants.md.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
7 tasks
rita-aga
added a commit
that referenced
this pull request
Apr 12, 2026
Preserves the field invariant and related-field cross-invariant work from PR #110. Fixes: - Replace production .unwrap() calls in field_invariant.rs (table.get paths), lint.rs and parser.rs (chars.next()) with explicit match/ Option control flow to satisfy the Integrity & DST Patterns gate. - Fix rustfmt drift across all changed files. - Base branch corrected from main to staging. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
rita-aga
added a commit
that referenced
this pull request
Apr 12, 2026
* fix: make field invariant CI pass Preserves the field invariant and related-field cross-invariant work from PR #110. Fixes: - Replace production .unwrap() calls in field_invariant.rs (table.get paths), lint.rs and parser.rs (chars.next()) with explicit match/ Option control flow to satisfy the Integrity & DST Patterns gate. - Fix rustfmt drift across all changed files. - Base branch corrected from main to staging. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: split field invariant code for readability ratchet Move field invariant check into field_constraints.rs, extract tests to field_invariant_tests.rs, compact constraint constructors, and update the readability baseline. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
nerdsane
added a commit
to ArunParthiban10/temper
that referenced
this pull request
Apr 14, 2026
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Introduces
[[field_invariant]]in IOA spec grammar to declare single-entitypredicates ("when X matches, Y must also match"), and extends
[[cross_invariant]]to accept
related(Target, fk).<Field> (in|not in) [...]for arbitrary fieldnames (not just
status). Field invariants run on the OData write path via anew
pre_upsert_field_invariant_checksstep insiderun_write_prechecks,alongside the existing relation and post-write checks.
Motivating use case: the upcoming Crucible reference app needs to reject
POST /tdata/EnvironmentswhereConfigType == "Local"sets cloud-only fields.See
docs/adrs/0041-ioa-field-invariants.mdfor the decision and rollout.IOA TOML — what the new grammar looks like
Atomic leaves. Each leaf inspects exactly one field.
{ field = X, absent = true }Xis missing from the payload, or its value isnull{ field = X, equals = V }Xexists and equalsV(bool, string, number){ field = X, empty = true }Xis absent,null,"", or[](shortcut forany_of [absent, equals "", equals []])Combinators.
{ any_of = [...] },{ all_of = [...] },{ not = p }. Bothwhenandrequireaccept either a bare atomic predicate or a combinator.Example 1 — simple equality rule
On
POST/PATCH /tdata/Environments, if the post-write snapshot hasConfigType == "Local"butNetworkingType != "Unrestricted", the write isrejected with HTTP 409 and
error.details = { type: "field_invariant", invariant: "LocalNetworkingMustBeUnrestricted" }.Example 2 — combinator for "absent or false"
No dedicated
absent_or_falseleaf — composable atoms plus combinators coverevery case without accumulating compound predicates.
Example 3 — derived forms via
notExample 4 — nested
all_of/any_ofExample 5 — extended
[[cross_invariant]]grammarReads as: on any action on
EnvironmentAllowedHost, load theEnvironmentwhose id equals the child's
EnvironmentIdFK; reject unless the parent'sConfigTypeis anything other than"Local". The oldrelated(...).status in [...]form still parses unchanged —.statusandinare the backwards-compatible defaults.Changes
temper-spec):FieldInvariant/FieldPredicatetree with leaves (absent,equals,empty) and combinators (any_of,all_of,not). Customserde deserializer rejects mixed operators and
absent=false/empty=falsewith precise errors.
isolate_field_invariant_sectionsslicer — extracts only[[field_invariant]]blocks before handing totoml::from_str, so strictparsing on this section doesn't trip over unrelated quirks elsewhere in
the IOA file (e.g. duplicate keys in
[integration.config]that thehand-rolled parser tolerates).
field names, and trivially-unsatisfiable equals-on-same-field.
[[cross_invariant]]parser extended torelated(Target, fk).<Field> (in|not in) [...], carrying the field name through to the evaluator.temper-server):SpecRegistry::field_invariants_forhelper — clones the invariant listbefore releasing the lock, avoiding holds across awaits.
pre_upsert_field_invariant_checksinodata/constraints.rs, wired intorun_write_prechecksbetween relation and post-write checks.ConstraintViolationType::FieldInvariant+"field_invariant"mappingin
constraint_violation_response.record_cross_invariant_violation(..., "field_invariant").crates/temper-server/tests/field_invariants.rs— 6 end-to-end#[tokio::test]scenarios covering POST happy path, POST violation, POSTinert rule, PATCH triggers violation, PATCH satisfies rule, and feature-flag
bypass.
docs/adrs/0041-ioa-field-invariants.md.Verification
cargo test -p temper-spec— 178 passcargo test -p temper-server --test field_invariants— 6 passcargo test --workspace— green (includes evolution app regression checkthat failed before the slicer fix)
awaits under locks, no spawn/threads/RNG/fs/env/network)
test for
isolate_field_invariant_sectionsin a follow-up)Test plan
cargo test -p temper-speccargo test -p temper-servercargo test --workspace🤖 Generated with Claude Code