Skip to content

feat(variant): fields-per-variant MVP — Artifact overlay + resolver (#255)#285

Merged
avrabe merged 1 commit into
mainfrom
feat/variant-fields-per-variant
May 16, 2026
Merged

feat(variant): fields-per-variant MVP — Artifact overlay + resolver (#255)#285
avrabe merged 1 commit into
mainfrom
feat/variant-fields-per-variant

Conversation

@avrabe
Copy link
Copy Markdown
Contributor

@avrabe avrabe commented May 16, 2026

Summary

Variant-MVP (track #1 from docs/design/variant-aware-properties.md). Single-master + per-variant overlay model for artifact fields.

  • Model: Artifact::fields_per_variant: BTreeMap<variant_name, BTreeMap<field, value>> serialized as fields-per-variant:.
  • Resolver: Artifact::fields_for_variant(Option<&str>) -> Cow<'_, BTreeMap<...>> — zero-alloc Borrowed fallback for None / unknown / empty overlay; Owned only on real variant lookups.
  • #[derive(Default)] on Artifact so test fixtures use struct-update pattern instead of carrying every new field through call sites.
  • yaml_hir parser: recognizes fields-per-variant as a typed top-level key; unpacks the nested mapping into the typed field instead of falling through to the generic fields smuggler.
  • Round-trip: GenericYamlAdapter typed pass-through.

Resolution semantics (design doc §5.2): variant keys override default keys; unmentioned default keys carry through. Unknown variants silently inherit defaults — graceful degradation when variant configs aren't loaded.

What's not in this PR (deliberate scope cut)

  • Variant config loading from artifacts/variants/*.yaml (consumed in ci(release-npm): switch to workflow_run trigger so npm publish auto-fires #261 / variant-solve work; the resolver here just takes a string).
  • Schema-level documentation of fields-per-variant in schemas/common.yaml (purely cosmetic; the parser doesn't require it and validate's unknown-field check iterates fields, not the typed sibling).
  • Validation rules that read overlays during cross-artifact check. The resolver is the building block; rules can adopt it incrementally.

Test plan

  • cargo test -p rivet-core --lib — 992 pass (5 new variant tests in model::tests + 1 in yaml_hir::tests).
  • cargo test --workspace --lib — green.
  • cargo test -p rivet-cli — green.
  • cargo clippy --workspace --all-targets — clean.
  • yaml_hir round-trip: variant overlay survives parse → resolver → typed Value::Number/Value::String per YAML 1.2 core schema.

🤖 Generated with Claude Code

MVP for the variant-aware properties track (docs/design/variant-aware-properties.md):

- `Artifact::fields_per_variant: BTreeMap<variant_name, BTreeMap<field, value>>`
  serialized as `fields-per-variant:` (kebab-case) at the artifact level.
- `Artifact::fields_for_variant(Option<&str>) -> Cow<'_, BTreeMap<...>>`
  resolves the effective fields map for a variant. Zero-alloc `Borrowed`
  fallback when no overlay applies (None, unknown variant, empty overlay).
  Allocates only when a known variant has overrides.
- `#[derive(Default)]` on `Artifact` so tests can use the struct-update
  pattern instead of carrying every new field forward at each call site.

Wired through:
- yaml_hir schema-driven parser recognizes `fields-per-variant` and unpacks
  the nested mapping into the typed field (does NOT fall through to the
  generic `fields` smuggler).
- generic-yaml adapter round-trips through a typed `fields_per_variant`
  on `GenericArtifact`.
- Resolution semantics per design doc §5.2: overlay merge — variant keys
  override default keys; default keys not mentioned in the variant carry
  through. Unknown variants silently inherit default fields (graceful
  degradation when variant configs aren't loaded).

Tests cover:
- Resolver: None / unknown / known / overlay-only-some-keys / new-keys.
- Parser: nested mapping extraction populates the typed field, not the
  generic `fields` map; both variants present after parse.

Implements: REQ-010, REQ-028, REQ-029
Refs: FEAT-001

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

📐 Rivet artifact delta

No artifact changes in this PR. Code-only changes (renderer, CLI wiring, tests) don't touch the artifact graph.

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: 703f490 Previous: 01c84a9 Ratio
traceability_matrix/1000 64088 ns/iter (± 412) 44286 ns/iter (± 617) 1.45
query/10000 119942 ns/iter (± 1116) 94732 ns/iter (± 1317) 1.27

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

@avrabe avrabe merged commit 225ec49 into main May 16, 2026
18 of 38 checks passed
@avrabe avrabe deleted the feat/variant-fields-per-variant branch May 16, 2026 08:31
@codecov
Copy link
Copy Markdown

codecov Bot commented May 16, 2026

Codecov Report

❌ Patch coverage is 97.28261% with 5 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
rivet-core/src/yaml_hir.rs 94.54% 3 Missing ⚠️
rivet-core/src/formats/aadl.rs 66.66% 1 Missing ⚠️
rivet-core/src/model.rs 98.71% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

avrabe added a commit that referenced this pull request May 16, 2026
…290)

* release(v0.10.0): variant + supplier + AI session + TCL workstream A

Workspace version bump 0.9.0 → 0.10.0. Theme: audit-grade story —
three orthogonal features that together move rivet from "trace your
project" to "describe the boundary and defend the tool's role across
it."

Highlights (full notes in CHANGELOG.md):
- Variant-aware properties — per-variant field values (#285, #255).
- Cross-org / supplier-boundary coverage MVP (#286, #253).
- AI session provenance — schema half (#289, partially #127).
- Tool-qualification workstream A — typed claim + dossier (#289).
- rivet stats --qualification + --qualification-mode flag (#289).
- TCL/TQL numbering convention fix in dogfood STPA (#289).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(release): docs-check violations on v0.10.0 release commit

Two docs-check violations on PR #290:
- VersionConsistency: vscode-rivet/package.json bumped 0.9.0 → 0.10.0
  (it has its own version field, not workspace-inherited).
- SubcommandReferences: CHANGELOG mentioned `rivet audit` which is a
  Phase 2 future subcommand. Rephrased to "audit-side enforcement
  subcommand" so the literal `rivet audit` no longer parses as a
  current-cli reference.

Local `rivet docs check` now passes (54 files, 0 violations).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
avrabe added a commit that referenced this pull request May 17, 2026
…287)

Closes #287. v0.10.0 (PR #285) shipped `Artifact::fields_per_variant`
and the `fields_for_variant(Option<&str>) -> Cow<'_, BTreeMap<...>>`
resolver as a building block but nothing consumed variant overlays
during validate. This PR wires the resolver through the validation
engine.

**rivet-core/src/schema.rs** — variant-aware helpers (additive):
- `get_field_value_for_variant(artifact, field, variant)` resolves
  through `Artifact::fields_for_variant` when `variant` is `Some(_)`,
  delegates to the existing borrowed-Cow path when `None` (zero
  allocations on the no-variant path).
- `Condition::matches_artifact_for_variant_with(...)`.
- `Requirement::check_for_variant(...)` for `RequiredFields`. Per
  design §6 Phase 2 scope ("fields only"), `RequiredLinks` stays
  variant-flat — links aren't overlayed.

**rivet-core/src/validate.rs** — additive wrappers + threading:
- New public APIs: `validate_with_variant`,
  `validate_with_externals_and_variant`,
  `validate_structural_with_variant`,
  `validate_structural_with_externals_and_variant`.
- Existing `validate*` functions thin-wrap the variant-aware version
  with `variant: None`. No breaking signature changes.
- Required-fields and allowed-values reads now go through
  `artifact.fields_for_variant(variant)` (resolved once per artifact
  as `effective_fields`).
- Conditional rules (phase 8) use
  `cond.matches_artifact_for_variant_with` +
  `rule.then.check_for_variant`.

**rivet-cli/src/main.rs cmd_validate** — threads the active variant:
- When `--variant <name>` is set, falls through to the direct path
  (salsa doesn't yet take variant as a tracked input) and calls
  `validate_with_externals_and_variant(..., active_variant)`.
- Baseline-only / no-variant / `--direct` paths preserved.

Tests added (rivet-core/src/validate.rs, mod tests):
1. `conditional_rule_respects_variant_field_overlay` — artifact has
   `fields.priority=must` and `fields-per-variant.automotive.priority=
   should`. Conditional rule fires on `priority==must`. Without
   variant: rule fires. With `Some("automotive")`: doesn't fire.
   With unknown variant: behaves like no variant (1 diag).
2. `required_field_satisfied_by_variant_overlay` — required field
   `asil` absent from `fields` but present in
   `fields-per-variant.automotive.asil=D`. Without variant: 1
   required-field error. With `Some("automotive")`: 0 errors.

NOT in this PR (deliberately):
- `rivet list --variant <name>` filtering.
- `rivet coverage --variant <name>` scoping.
- Variant-aware s-expr validation rules (phase 9 in validate.rs).
- Salsa-tracked variant input (direct-path fallback for now).
- Cross-product multi-axis variants.
- `when:` clause on external-anchor.

Implements: REQ-004, REQ-007
Refs: FEAT-001, #287

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
avrabe added a commit that referenced this pull request May 18, 2026
…287) (#298)

Closes #287. v0.10.0 (PR #285) shipped `Artifact::fields_per_variant`
and the `fields_for_variant(Option<&str>) -> Cow<'_, BTreeMap<...>>`
resolver as a building block but nothing consumed variant overlays
during validate. This PR wires the resolver through the validation
engine.

**rivet-core/src/schema.rs** — variant-aware helpers (additive):
- `get_field_value_for_variant(artifact, field, variant)` resolves
  through `Artifact::fields_for_variant` when `variant` is `Some(_)`,
  delegates to the existing borrowed-Cow path when `None` (zero
  allocations on the no-variant path).
- `Condition::matches_artifact_for_variant_with(...)`.
- `Requirement::check_for_variant(...)` for `RequiredFields`. Per
  design §6 Phase 2 scope ("fields only"), `RequiredLinks` stays
  variant-flat — links aren't overlayed.

**rivet-core/src/validate.rs** — additive wrappers + threading:
- New public APIs: `validate_with_variant`,
  `validate_with_externals_and_variant`,
  `validate_structural_with_variant`,
  `validate_structural_with_externals_and_variant`.
- Existing `validate*` functions thin-wrap the variant-aware version
  with `variant: None`. No breaking signature changes.
- Required-fields and allowed-values reads now go through
  `artifact.fields_for_variant(variant)` (resolved once per artifact
  as `effective_fields`).
- Conditional rules (phase 8) use
  `cond.matches_artifact_for_variant_with` +
  `rule.then.check_for_variant`.

**rivet-cli/src/main.rs cmd_validate** — threads the active variant:
- When `--variant <name>` is set, falls through to the direct path
  (salsa doesn't yet take variant as a tracked input) and calls
  `validate_with_externals_and_variant(..., active_variant)`.
- Baseline-only / no-variant / `--direct` paths preserved.

Tests added (rivet-core/src/validate.rs, mod tests):
1. `conditional_rule_respects_variant_field_overlay` — artifact has
   `fields.priority=must` and `fields-per-variant.automotive.priority=
   should`. Conditional rule fires on `priority==must`. Without
   variant: rule fires. With `Some("automotive")`: doesn't fire.
   With unknown variant: behaves like no variant (1 diag).
2. `required_field_satisfied_by_variant_overlay` — required field
   `asil` absent from `fields` but present in
   `fields-per-variant.automotive.asil=D`. Without variant: 1
   required-field error. With `Some("automotive")`: 0 errors.

NOT in this PR (deliberately):
- `rivet list --variant <name>` filtering.
- `rivet coverage --variant <name>` scoping.
- Variant-aware s-expr validation rules (phase 9 in validate.rs).
- Salsa-tracked variant input (direct-path fallback for now).
- Cross-product multi-axis variants.
- `when:` clause on external-anchor.

Implements: REQ-004, REQ-007
Refs: FEAT-001, #287

Co-authored-by: Claude Opus 4.7 <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.

1 participant