Skip to content

feat(evaluator): let evaluations pin a taskset revision - #1129

Merged
SandyChapman merged 3 commits into
mainfrom
taskset-ref-revision-pinning/schapman
Aug 7, 2026
Merged

feat(evaluator): let evaluations pin a taskset revision#1129
SandyChapman merged 3 commits into
mainfrom
taskset-ref-revision-pinning/schapman

Conversation

@SandyChapman

@SandyChapman SandyChapman commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

What

A TasksetRef could only ever name a taskset's head, so replacing the taskset changed what a re-submitted spec evaluated. Member content was already digest-pinned within a revision, which made an evaluation look reproducible while its membership could still move underneath it.

This lets a reference pin the taskset revision too: default/geography-suite#<tag-or-digest>.

Fast-follow to #1023, raised in review there (thread on api/schemas.py). Stacked on #1023 — please merge that first; this targets its branch.

The two pins compose

They cover different things:

Reference Member content Which members
TasksetRef("suite") pinned current revision
TasksetRef("suite#<digest>") pinned pinned

A bare ref already grades identical task content across re-runs. Pinning the taskset additionally holds membership still across a replace that adds or drops a task — what you want when a benchmark number has to stay comparable.

How

TasksetRef moves onto _SUBENTITY_REF_PATTERN, so it carries #<tag-or-digest> exactly as TaskRef does, with an absent fragment meaning latest. MetricRef stays on the narrower pattern until metrics gain revisions — the comment above that constant already anticipated this split and is updated.

resolve_taskset_ref resolves the fragment and expands the members of the named revision. Both the pinned and the bare paths go through get_revision rather than reading the head's own tasks: a head and its latest revision are guaranteed to agree, and resolving one way for pinned refs and another for bare ones would let the two drift apart on the next bug. It also buys content verification for the bare case.

A fragment that no longer resolves fails the evaluation rather than falling back to the current revision.

Reviewing this

Small: 6 files, +249/−51, and 160 of the added lines are tests.

  1. task_refs.py — the resolution change; everything else follows from it.
  2. api/schemas.py — the pattern move and why MetricRef stays behind.
  3. Tests and docs are mechanical once the above is settled.

The test fixtures needed fixing, and that is worth a look. They were building tasksets the services cannot produce: inserted with no published revision, and with bare (unpinned) member refs. Routing resolution through get_revision surfaced that immediately, and TasksetRevisionEntity then rejected the unpinned members outright. _create_published / _pin_members now publish and digest-pin the way _resolved_content does, so every fixture describes a state reachable through the real API.

test_sibling_ref_types_still_reject_fragments was parameterized over MetricRef and TasksetRef; it is split, since half of its premise no longer holds.

Testing

  • 693 evaluator unit tests pass (685 before).
  • The three pinning tests were verified to fail against the old head-reading behavior and pass with the fix, rather than assumed to be meaningful.
  • Covered: bare ref follows the tip; digest-pinned ignores members added later; tag-pinned resolves through the tag; a pinned ref survives a replace with identical expansion; unresolvable digest and unresolvable tag both raise; field-level accept/reject of fragment shapes.
  • tools/lint/lint-all.sh: 12/13. lint-web-sdk fails locally only — this box has no web/node_modules and runs Node 22.19 against a >=22.23.2 engine pin. Its output is gitignored and no web code is touched here, so CI should pass it; I could not verify that locally.
  • openapi.yaml regenerated with make refresh-openapi (+11/−10: the TasksetRef pattern and description only).

Notes

  • No back-compat concern: widening a validation pattern only admits input that was previously rejected. Every existing bare ref keeps its current meaning.
  • The docs Note added in feat(evaluator): add content-addressed revisions for tasks and tasksets #1023 recording this as a known limitation is replaced by a "Pin the taskset itself" section, and the edge-case table gains the unresolvable-fragment row.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Taskset references can be pinned to a specific revision, tag, or content digest.
    • Supports tracking the latest taskset membership or freezing membership to a selected revision.
    • Pinned taskset members remain stable when tasksets are replaced.
    • Taskset reference validation now supports valid revision fragments.
  • Bug Fixes

    • Evaluation now fails with a clear error when a referenced taskset revision cannot be resolved.
  • Documentation

    • Added guidance on taskset revision pinning, membership behavior, and related API edge cases.

@github-actions github-actions Bot added the feat label Aug 6, 2026
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Base automatically changed from schapman/harbor-cli-dataset-endpoint-45cb1d to main August 6, 2026 14:52
@SandyChapman
SandyChapman force-pushed the taskset-ref-revision-pinning/schapman branch from e3c0575 to 32fc4f7 Compare August 6, 2026 15:57
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor
Suite Lines Covered Line Rate Branch Rate
Unit Tests 31438/40061 78.5% 63.0%
Integration Tests 18305/38013 48.1% 20.8%

A `TasksetRef` could only ever name a taskset's head, so replacing the
taskset changed what a re-submitted spec evaluated. Member content was
already digest-pinned within a revision, which made an evaluation look
reproducible while its membership could still move underneath it.

`TasksetRef` moves onto `_SUBENTITY_REF_PATTERN`, so a reference may
carry `#<tag-or-digest>` exactly as a `TaskRef` does, with an absent
fragment meaning `latest`. `MetricRef` stays on the narrower pattern
until metrics gain revisions.

`resolve_taskset_ref` resolves that fragment and expands the members of
the named taskset revision. Both the pinned and the bare paths go through
`get_revision` rather than reading the head's own `tasks`: a head and its
`latest` revision are guaranteed to agree, and resolving one way for
pinned refs and another for bare ones would let the two drift apart. It
also buys content verification for the bare case.

The two pins cover different things and compose — a bare ref grades
identical task content across re-runs, and a pinned ref additionally
holds membership still across a `replace` that adds or drops a task.

Signed-off-by: Sandy Chapman <schapman@nvidia.com>
@SandyChapman
SandyChapman force-pushed the taskset-ref-revision-pinning/schapman branch from 32fc4f7 to 7ef23d6 Compare August 7, 2026 11:22
@SandyChapman
SandyChapman marked this pull request as ready for review August 7, 2026 12:13
@SandyChapman
SandyChapman requested review from a team as code owners August 7, 2026 12:13
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: f19760f3-7469-464e-a9c8-452f37e490b6

📥 Commits

Reviewing files that changed from the base of the PR and between 7af227f and 2a0ba1a.

📒 Files selected for processing (2)
  • plugins/nemo-evaluator/tests/api/service/test_task_service.py
  • plugins/nemo-evaluator/tests/api/service/test_taskset_service.py

📝 Walkthrough

Walkthrough

Taskset references now support optional tag or digest revisions. Evaluation resolves membership from the selected taskset revision and loads each member at its pinned revision. Schemas, OpenAPI definitions, tests, and documentation cover tracking, pinning, validation, and unresolved revisions.

Changes

Taskset revision resolution

Layer / File(s) Summary
Reference contracts
plugins/nemo-evaluator/src/nemo_evaluator/api/schemas.py, plugins/nemo-evaluator/openapi/openapi.yaml, plugins/nemo-evaluator/tests/test_subentity_refs.py
TasksetRef accepts optional tag or digest fragments. MetricRef remains fragment-free.
Revision-backed expansion
plugins/nemo-evaluator/src/nemo_evaluator/task_refs.py
Taskset references resolve through get_revision. Expansion reads the selected TasksetRevisionEntity and loads each member at its pinned revision. Unresolved revisions raise descriptive errors.
Validation and behavior coverage
plugins/nemo-evaluator/tests/test_task_refs.py, plugins/nemo-evaluator/tests/api/service/test_task_service.py, plugins/nemo-evaluator/tests/api/service/test_taskset_service.py, docs/evaluator/manage-tasks-tasksets.mdx
Tests cover bare, digest-pinned, and tag-pinned references, stable membership, malformed fragments, and unresolved revisions. Documentation describes the same behavior and REST API failure case.

Sequence Diagram(s)

sequenceDiagram
  participant Evaluator
  participant task_refs
  participant EntityClient
  Evaluator->>task_refs: resolve TasksetRef
  task_refs->>EntityClient: get_revision taskset
  EntityClient-->>task_refs: TasksetRevisionEntity
  task_refs->>EntityClient: load pinned task revisions
  EntityClient-->>task_refs: resolved task members
  task_refs-->>Evaluator: expanded taskset
Loading

Possibly related PRs

Suggested labels: docs

Suggested reviewers: arpitsardhana, ngoncharenko

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: allowing evaluations to pin a taskset revision.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch taskset-ref-revision-pinning/schapman

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@plugins/nemo-evaluator/src/nemo_evaluator/api/schemas.py`:
- Around line 237-240: Qualify the TasksetRef reproducibility documentation:
bare refs resolve latest and keep task content fixed only while that selected
taskset revision remains current; recommend digest-pinned taskset refs for
reproducibility across replace operations. Apply this correction in
plugins/nemo-evaluator/src/nemo_evaluator/api/schemas.py:237-240, regenerate the
matching description in plugins/nemo-evaluator/openapi/openapi.yaml:5167-5172,
update the resolver comment in
plugins/nemo-evaluator/src/nemo_evaluator/task_refs.py:96-98, and correct the
table explanation and recommendation in
docs/evaluator/manage-tasks-tasksets.mdx:352-358.

In `@plugins/nemo-evaluator/tests/test_task_refs.py`:
- Around line 63-71: Update the non-digest branch in the task-reference
resolution flow to use the client’s get_revision method for resolving the
fragment (such as “blessed”), rather than head_digest(task). Store the digest
returned by that resolved revision, while preserving the existing
NemoEntityNotFoundError fallback and pinned TaskRef construction.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 7bca7d3a-d40e-4351-8f62-8589506d0cfd

📥 Commits

Reviewing files that changed from the base of the PR and between 20e39e0 and 7ef23d6.

📒 Files selected for processing (6)
  • docs/evaluator/manage-tasks-tasksets.mdx
  • plugins/nemo-evaluator/openapi/openapi.yaml
  • plugins/nemo-evaluator/src/nemo_evaluator/api/schemas.py
  • plugins/nemo-evaluator/src/nemo_evaluator/task_refs.py
  • plugins/nemo-evaluator/tests/test_subentity_refs.py
  • plugins/nemo-evaluator/tests/test_task_refs.py

Comment thread plugins/nemo-evaluator/src/nemo_evaluator/api/schemas.py Outdated
Comment thread plugins/nemo-evaluator/tests/test_task_refs.py
Address CodeRabbit review on #1129.

The docstring, resolver comment, and docs table all claimed a bare ref
"grades identical task content across re-runs". That overstates it.
Members are re-resolved on every write, so a `replace` submitting the
same member names can still pin newer task digests — changing the
content a bare ref expands to, not just the membership.

What actually holds is narrower and worth stating exactly, since the
whole feature is about reproducibility: a taskset revision pins its
members by digest, so no ref is disturbed when a member republishes on
its own. The forms differ only on `replace`, where a bare ref follows the
new revision and a pinned ref does not.

The test helper's member pinning also diverged from the write path: a
member named `task#blessed` was pinned to the head digest rather than to
the revision the tag names. It now resolves non-digest fragments through
`get_revision`, matching `resolve_revision`. The two agree for a bare
ref, whose fragment is `latest`, so no existing fixture changes — and a
new test covers the case that was silently wrong.

Signed-off-by: Sandy Chapman <schapman@nvidia.com>
The previous commit fixed a test helper that pinned tag-qualified members
to the head digest. The same hole existed in the production path's
coverage, which is the one that matters.

`_FakeTaskService.resolve_revision` derived its digest from
`(workspace, name)` and ignored `fragment` entirely, so every taskset
service assertion about digest-pinned membership would pass unchanged if
`_pin_member` dropped the fragment. The fake now folds the fragment into
the digest, and a new test asserts a `#blessed` member is stored as the
tagged revision's digest and not the current one.

`test_resolve_revision_returns_the_digest_for_a_tag` never used a tag: it
called `resolve_revision` with no fragment against a task with a single
revision, where every fragment resolves alike. Split into three that
mean what they say — default-to-latest, a tag left behind on an older
revision, and a digest round-trip — each against a task with two
revisions so the fragment can be observed.

Verified by mutation: dropping the fragment in `_pin_member` now fails
one test, and ignoring it in `TaskService.resolve_revision` fails two.
Neither was caught before.

Signed-off-by: Sandy Chapman <schapman@nvidia.com>
Comment thread plugins/nemo-evaluator/src/nemo_evaluator/task_refs.py
@SandyChapman
SandyChapman added this pull request to the merge queue Aug 7, 2026
Merged via the queue into main with commit 8944625 Aug 7, 2026
56 checks passed
@SandyChapman
SandyChapman deleted the taskset-ref-revision-pinning/schapman branch August 7, 2026 17:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants