Skip to content

fix(feature-model): cross-tree constraints silently pass#156

Merged
avrabe merged 2 commits intomainfrom
fix/variant-cross-tree-constraints
Apr 21, 2026
Merged

fix(feature-model): cross-tree constraints silently pass#156
avrabe merged 2 commits intomainfrom
fix/variant-cross-tree-constraints

Conversation

@avrabe
Copy link
Copy Markdown
Contributor

@avrabe avrabe commented Apr 21, 2026

HIGH severity: silent false-positive PASS

Before this PR, rivet variant check reported PASS on variants that explicitly violated cross-tree constraints like `(implies X (not Y))`. The entire constraint surface was effectively decorative for any constraint whose consequent wasn't a bare feature name — only `excludes` had a real post-propagation check.

Root cause

`Expr::Implies` was used only to forward-propagate feature selection: "if antecedent is selected and consequent is a bare feature name, select it too." When the consequent was anything more complex (`not`, `and`, `or`, link predicate, regex), `extract_feature_name` returned `None`, the propagation loop fell through, and `solve` returned `Ok(...)`. No validation ever ran.

Fix

`rivet-core/src/feature_model.rs::solve` now runs a generic `eval_constraint` pass after propagation:

  • `Implies(a,b)` = `!a || b` (proper material implication)
  • `Excludes(a,b)` = `!(a && b)` (kept its dedicated diagnostic for backwards compat)
  • `And`/`Or`/`Not`/`BoolLit` proper semantics
  • Feature-name leaf via `extract_feature_name`
  • Artifact-predicate leaves default to `true` to avoid spurious violations

Evidence

Before: failing test reproduced — `(implies feature-x (not feature-y))` with both selected got `Ok`.
After: 3 regression tests pass, all 560 rivet-core lib tests still green, clippy clean.

Test plan

  • 3 new regression tests in `feature_model::tests`
  • Full `cargo test -p rivet-core --lib` green
  • `cargo clippy --all-targets -- -D warnings` clean
  • CI green

🤖 Generated with Claude Code

…ions

Before this change, `rivet variant check` silently reported PASS on
variants that explicitly violated `(implies X (not Y))` and other
cross-tree constraints where forward propagation could not auto-select
a feature to satisfy the consequent. The solver only used `Expr::Implies`
to schedule consequent features for selection; when the consequent was
a negation, a compound, or any non-feature-name shape, no check ever
fired against the propagated selection — a false-positive PASS on a
safety-critical validation surface.

Fix: add a `eval_constraint` pass after propagation that treats every
top-level constraint as a boolean assertion over the effective feature
set, with standard propositional semantics for `and`/`or`/`not`/
`implies`/`excludes`. `excludes` keeps its dedicated diagnostic string
to preserve existing error messages; other shapes report through a new
`describe_constraint` helper. Unknown artifact-oriented predicates
(link queries, regex matches) default to true so unrelated constraint
flavours do not trigger spurious violations.

Regression tests cover the reported shape `(implies X (not Y))` with
both X and Y selected (now FAIL), the companion case with only X
selected (still PASS), and ensure forward propagation of
`(implies X Y)` still works.

Fixes: REQ-044
Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

⚠️ Performance Alert ⚠️

Possible performance regression was detected for benchmark 'Rivet Criterion Benchmarks'.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 1.20.

Benchmark suite Current: 62ccd34 Previous: aa706fc Ratio
traceability_matrix/1000 58483 ns/iter (± 635) 45738 ns/iter (± 588) 1.28
query/10000 108855 ns/iter (± 1336) 90574 ns/iter (± 529) 1.20

This comment was automatically generated by workflow using github-action-benchmark.

thin-vec 0.2.14 has a Double-Free / UAF in IntoIter::drop and
ThinVec::clear. Pulled in transitively via salsa 0.26.0. Rivet does
not directly construct or iterate thin_vec::ThinVec — the exposure is
through salsa's internal data structures.

Ignore in both cargo-deny and cargo-audit until either salsa bumps
its thin-vec dependency or thin-vec 0.2.15 lands upstream.

Trace: skip
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 21, 2026

Codecov Report

❌ Patch coverage is 82.14286% with 20 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
rivet-core/src/feature_model.rs 82.14% 20 Missing ⚠️

📢 Thoughts on this report? Let us know!

@avrabe avrabe merged commit 56087b8 into main Apr 21, 2026
18 of 23 checks passed
@avrabe avrabe deleted the fix/variant-cross-tree-constraints branch April 21, 2026 18:52
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