Skip to content

feat(variant): Phase 2 — config loading + validate/coverage wiring (#287)#291

Open
avrabe wants to merge 3 commits into
mainfrom
feat/issue-287-variant-phase-2
Open

feat(variant): Phase 2 — config loading + validate/coverage wiring (#287)#291
avrabe wants to merge 3 commits into
mainfrom
feat/issue-287-variant-phase-2

Conversation

@avrabe
Copy link
Copy Markdown
Contributor

@avrabe avrabe commented May 16, 2026

Closes #287.

Phase 2 of the variant-aware-properties work (#287). Phase 1 (#285)
landed fields_per_variant + fields_for_variant on Artifact; nothing
in the rest of the codebase consumed it. This PR wires the rest of the
flow per docs/design/variant-aware-properties.md §5.5.

Scope (issue #287 acceptance criteria)

1. Load variant configs from artifacts/variants/*.yaml — DONE

  • New rivet_core::feature_model::load_variant_configs_from_dir(path).
  • Walks *.yaml / *.yml in the directory, sorts deterministically by
    filename, returns Vec<VariantConfig>.
  • Missing dir → Ok(vec[]) so callers can use it unconditionally.
  • Duplicate name: across files → error (auditor-friendly).
  • 4 lib tests cover all branches.

2. --variant <NAME> on validate / coverage / list / query — DONE

  • Argument resolution: try as a filesystem path first; fall back to
    <project>/artifacts/variants/<NAME>.yaml. Bad name errors with the
    list of available variants in the helpful-error pattern.
  • rivet validate --variant industrial validates the default view AND
    the overlaid merged view (design doc §5.5: "both must pass").
  • rivet list --variant industrial --format json emits each artifact's
    merged fields: map plus a top-level "variant": "...".
  • rivet query --sexpr '(= max-temp-c "100")' --variant industrial
    rewrites each artifact's fields: to the merged view before the
    s-expression evaluator runs, so a filter that requires the overlay
    value matches under --variant and not without.
  • rivet coverage --variant industrial stamps the active variant name
    in text header and JSON "variant" field.

3. Validate honours per-variant overlays — DONE

  • New rivet_core::validate::validate_variants(...). Pure post-pass on
    top of the existing default-view validator.
    • Variant-key cross-check: every key in fields_per_variant must
      appear in the project's known-variant set (configs ∪ feature names).
      Warning by default, error under new --strict-variants flag.
      Message matches the design doc literally:

      variant key 'foo' in fields-per-variant is not declared in any variant config or feature; expected one of: ...

    • Overlay type-check: every declared overlay's merged view is run
      through the same required-fields / allowed-values rules as the
      default view. Diagnostics name the variant explicitly, e.g.
      field 'max-temp-c' has value '999' (variant: consumer), allowed: ["70", "80", "100"] — matches the design doc's second error class.
    • --strict-variants is a separate flag from --strict-cited-sources
      so each CI gate can be tightened independently.
  • 7 lib tests cover unknown-key warning vs strict, allowed-values
    failure on overlay, clean pass, required-field-in-merged-view check,
    empty-overlay fast path, and feature-name-as-key acceptance.

4. Coverage scopes counts by active variant — DEFERRED

The --variant flag on coverage currently stamps the active variant in
the output (text + JSON) so coverage runs are variant-tagged for the
auditor. Actually rewriting delegated chains by the variant's effective
feature set requires touching compute_coverage and the linkgraph
walk; that's a self-contained follow-up rather than a 2-line addition,
and the brief explicitly marks items 4 and 5 as lower priority than
1-3. Tracked as a follow-up issue against this work.

5. Serve / dashboard variant selector — DEFERRED

The dashboard renderer changes (variant selector at the top, override
marker per cell, "show all variants" toggle) are non-trivial UI work
and intentionally postponed per the brief's priority guidance. The
overlay data the dashboard needs is fully available via Artifact:: fields_for_variant, so a follow-up PR can land the renderer without
needing to revisit the core.

How to read the diff

  • Commit 1 (feat(variant): load variant configs from a directory):
    the loader function + 4 unit tests. Standalone, no public-API
    changes elsewhere.
  • Commit 2 (style(variant): apply rustfmt): pure cargo fmt result
    on commit 1. Trace: skip because no source-of-truth content moved.
  • Commit 3 (feat(validate): --variant flag and per-variant overlay validation): the validator + every CLI surface change + 7 new lib
    tests + 8 new CLI integration tests in
    rivet-cli/tests/variant_phase2.rs.

Test plan

  • cargo build --workspace
  • cargo clippy --workspace --all-targets -- -D warnings (clean)
  • cargo test --workspace (passes; one pre-existing flake in
    export_html_generates_static_site was a transient disk-space
    issue in the sandbox, recovers on retry — not related to this
    change).
  • cargo fmt --all -- --check (clean).
  • Dogfood rivet validate shows identical diagnostics on main
    vs this branch when no fields_per_variant is present (the new
    validator is a pure-addition: if nothing has overlays, nothing
    is emitted).

Deferred to follow-up

  • Acceptance criterion 4 (coverage delegated-chain scoping by variant).
  • Acceptance criterion 5 (dashboard variant selector + override marker).

Both are deferred deliberately, not omitted by oversight; see the
"Scope" section above for the reasoning. Items 1-3 — the highest-
leverage subset called out in the brief — are fully complete with
tests and clean lints.

https://claude.ai/code/session_01Ms4nZDTtdfzvzTu8m3ghSj


Generated by Claude Code

claude added 3 commits May 16, 2026 16:21
Add `feature_model::load_variant_configs_from_dir` so callers can
walk `artifacts/variants/*.yaml`, deserialize each entry as a
`VariantConfig`, and feed the resulting list to validate / coverage /
list / query.

This is the loader-side primitive for issue #287. It:
- ignores non-yaml files, sorts results by file name for reproducible
  output across platforms,
- returns an empty list (not an error) for a missing directory so
  callers can use it unconditionally,
- rejects duplicate `name:` keys across files so downstream consumers
  never have to guess which variant a name refers to.

Tests cover all four behaviours (missing dir, sorted load, duplicate
rejection, parse-error surfacing).

Implements: REQ-007
Pure rustfmt result — no functional changes. Tightens spacing on
multi-arg `std::fs::write` calls and a few `assert!` macros. Was
generated by running `cargo fmt --all` after the prior commit.

Trace: skip
Implements issue #287 Phase 2 acceptance criteria 1-3:

1. Variant configs in `artifacts/variants/*.yaml` are loaded on every
   command invocation (via the loader added in the previous commit).
2. `--variant <NAME_OR_PATH>` is now accepted by `rivet validate`,
   `rivet coverage`, `rivet list`, and `rivet query`. The argument
   resolves first as a filesystem path, then as a bare name against
   `<project>/artifacts/variants/<NAME>.yaml`; a bad name errors with
   the list of available variants.
3. Per-variant field overlays are now validated.
   - `validate_variants` (new) does two passes:
     * cross-check each `fields-per-variant:` key against the
       project's known-variants set (declared configs + features) —
       warning by default, error under the new `--strict-variants`
       flag, matching the `Variant key 'foo' ...` error class spelled
       out in `docs/design/variant-aware-properties.md` §5.6.
     * type-check every variant overlay's merged view against the
       same required-field / allowed-values rules as the default
       view, emitting diagnostics like `field 'X' has value 'V'
       (variant: industrial), allowed: [...]`.
   - Per design doc §5.5 the default view is still validated by the
     existing pipeline; the overlay layer is purely additive.

CLI surface:
- `rivet validate --variant industrial` validates default + overlay.
- `rivet validate --strict-variants` promotes unknown-key warnings to
  errors (CI hygiene).
- `rivet list --variant industrial --format json` emits each
  artifact's merged `fields:` plus a top-level `"variant": "..."`.
- `rivet query --sexpr '(= max-temp-c "100")' --variant industrial`
  filters against the merged view, so an overlay value satisfies the
  filter where the default would not.
- `rivet coverage --variant industrial` stamps the active variant in
  both text and JSON output (delegated-chain scoping deferred — issue
  #287 acceptance criterion 4).

Tests cover every new behaviour:
- 4 lib tests for `load_variant_configs_from_dir` (in prior commit),
- 7 lib tests for `validate_variants` (unknown key warning, strict
  promotion, allowed-values failure on overlay, clean pass, required
  field missing in merged view, empty-overlay fast path, known set
  accepts features),
- 8 CLI integration tests for the end-to-end `--variant` flag.

Implements: REQ-004, REQ-007
Refs: FEAT-001
@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.

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.

feat(variant): Phase 2 — variant config loading + validate/coverage wiring

2 participants