Skip to content

feat(evaluator): add content-addressed revisions for tasks and tasksets - #1023

Open
SandyChapman wants to merge 1 commit into
mainfrom
schapman/harbor-cli-dataset-endpoint-45cb1d
Open

feat(evaluator): add content-addressed revisions for tasks and tasksets#1023
SandyChapman wants to merge 1 commit into
mainfrom
schapman/harbor-cli-dataset-endpoint-45cb1d

Conversation

@SandyChapman

@SandyChapman SandyChapman commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

What

Stored tasks and tasksets are now versioned. Creating one publishes revision 1; replacing its content publishes the next. Earlier revisions stay readable, so an evaluation can be re-run against exactly the content it ran against before.

This is phase 0 of backing a Harbor-compatible dataset publish endpoint with NeMo entities — Harbor pins dataset members by digest, which we had no way to represent.

How it works

A revision is an immutable child entity named rev.<n>, addressed by a SHA-256 digest of its content. The head record keeps current content plus mutable pointers (latest_revision, and a tag → ordinal map). Digest lookups are a real (parent, data.content_hash) query, not a denormalized index.

References gain the platform's standard # sub-entity fragment — workspace/name#<tag-or-digest> — with an absent fragment meaning latest. This uses the same convention filesets already use for a contained file.

Taskset membership is resolved to exact digests on write, and taskset expansion at eval time loads each member's pinned revision. That second half matters: without it, membership was pinned in storage and ignored at runtime, so a suite looked reproducible and wasn't.

API

Method Path
POST /{tasks,tasksets}/{name} Create + publish revision 1 (409 if taken)
PUT /{tasks,tasksets}/{name} Replace + publish; 201 if a revision was cut, 200 if content was unchanged
GET /{tasks,tasksets}/{name}/revisions List published revisions, newest first (paginated)
GET /{tasks,tasksets}/{name}/revisions/{revision} Read content as of a digest or tag
PUT /{tasks,tasksets}/{name}/tags/{tag}?revision= Point a tag at an existing revision

Publishing is idempotent against the current revision: re-PUTting unchanged content cuts no revision but still applies tags, which is how you tag a revision after the fact. Reverting to older content does publish — the record genuinely changed, and deduping onto the old revision would leave the head serving content latest doesn't name.

Reviewing this

It's a large diff, but most of it isn't judgement calls:

Lines
openapi/openapi.yaml +587 generated by make refresh-openapi — skim
tests +2678 read selectively
docs +130 quick
source +1918 the actual review

Within the source, tasks.py/tasksets.py, the two services, and the two SDK modules are near-symmetric pairs — read one and skim its twin.

Suggested order, highest value first:

  1. entities.py — what a revision is, and which fields the digest covers (REVISION_POINTER_FIELDS / REVISION_SELF_FIELDS). Everything else depends on this projection.
  2. revisions.py — publish, resolve, tag. The concurrency lives here: ordinal allocation, the forward-only latest rule, and what happens to head content when a pointer write loses its race.
  3. api/service/task_service.py — where those semantics become observable (200 vs 201, the rollback, the no-op path).
  4. Everything else — routes, SDK, docs are mechanical once the above is settled.

Worth the most scrutiny: the interleavings in publish_revision and _point_tags. Three defects found during review were concurrency or ordering cases where a comment described the intended invariant and the code handled only the common path.

Invariants

  • Every record has at least one revision. Create rolls back if its first publish fails, so there's no draft state for consumers to handle.
  • Published taskset membership is digest-pinned, enforced on the field rather than in the publish path.
  • latest only moves forward and can't be moved by hand. Ordinal allocation is serialized by the child create and retries on contention.
  • A plain read and a #latest read always agree. The head's content is always the revision latest names — including when a publish fails midway, and when two publishers race and the loser's revision is older.
  • Revision content is verified on read. Resolving a revision re-hashes its content and compares it against the digest stored beside it, so a revision mutated in place is refused rather than silently served.

Testing

  • 673 unit tests in this plugin; 2253 across the packages this touches
  • 9 integration tests against a real platform (RUN_AGENT_EVAL_INTEGRATION=1), covering what only real persistence confirms: parent-scoped ordinals, the digest query, server-side ordering, FK cascade, cross-record isolation
  • Coverage on new modules: content_hash 100%, task_refs 100%, revisions 96%, services 94%
  • tools/lint/lint-all.sh: 13/13 pass

Notes for review

  • No back-compat shims — there are no known users of the task/taskset endpoints, confirmed before starting.
  • Deleting an individual revision is deliberately not exposed. A pinned reference would become unresolvable. Registry convention (and Harbor's own model) is yank, not delete; not built, since nothing needs it.
  • Known limitation: nothing prevents deleting a task that a taskset pins. The reference dangles and fails loudly on read. Harbor has the same hole; preventing it needs a reverse index we don't have.
  • plugins/nemo-customizer/openapi/openapi.yaml is deliberately not included. make refresh-openapi regenerates ~1100 lines there from pre-existing drift unrelated to this change; reverted to keep the diff focused. Worth a separate cleanup PR.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added immutable task and taskset revisions with content digests.
    • Added revision history, retrieval by revision or tag, and revision tagging.
    • Added idempotent replace and upsert operations.
    • Taskset memberships are pinned to exact task revisions for consistent evaluations.
    • SDKs support synchronous and asynchronous revision management.
  • Documentation

    • Expanded guidance for revisions, tags, pinned evaluations, statuses, and validation errors.
  • Bug Fixes

    • Improved validation for duplicate references, missing revisions, invalid digests, and concurrent updates.

@github-actions github-actions Bot added the feat label Jul 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor
Suite Lines Covered Line Rate Branch Rate
Unit Tests 30743/39237 78.3% 62.7%
Integration Tests 18064/37189 48.6% 21.0%

@SandyChapman
SandyChapman force-pushed the schapman/harbor-cli-dataset-endpoint-45cb1d branch from 38b2279 to 2a18647 Compare August 4, 2026 12:16
@SandyChapman
SandyChapman marked this pull request as ready for review August 4, 2026 14:39
@SandyChapman
SandyChapman requested review from a team as code owners August 4, 2026 14:39
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Changes

The evaluator now supports immutable, content-hashed task and taskset revisions. It adds revision tags, digest-pinned membership, idempotent publishing, REST APIs, SDK methods, pinned evaluation reads, and validation coverage.

Evaluator revision management

Layer / File(s) Summary
Revision contracts and publication engine
plugins/nemo-evaluator/src/nemo_evaluator/api/schemas.py, plugins/nemo-evaluator/src/nemo_evaluator/entities.py, plugins/nemo-evaluator/src/nemo_evaluator/revisions.py, plugins/nemo-evaluator/src/nemo_evaluator/content_hash.py, plugins/nemo-evaluator/tests/test_*
Adds revision DTOs, immutable entities, canonical SHA-256 hashing, revision lookup, tags, idempotent publishing, optimistic-lock handling, and validation tests.
Service publishing and pinned taskset resolution
plugins/nemo-evaluator/src/nemo_evaluator/api/service/*, plugins/nemo-evaluator/src/nemo_evaluator/task_refs.py, plugins/nemo-evaluator/tests/api/service/*, plugins/nemo-evaluator/tests/test_task_refs.py
Publishes task and taskset revisions, resolves selected revisions, pins taskset members to digests, supports historical reads, and handles rollback and concurrency.
REST revision and tagging endpoints
plugins/nemo-evaluator/src/nemo_evaluator/api/v2/*, plugins/nemo-evaluator/openapi/openapi.yaml, docs/evaluator/manage-tasks-tasksets.mdx, plugins/nemo-evaluator/tests/api/v2/*
Adds replacement, revision listing and retrieval, tagging endpoints, OpenAPI contracts, documentation, status codes, and error handling.
Synchronous and asynchronous SDK resources
plugins/nemo-evaluator/src/nemo_evaluator/sdk/*, plugins/nemo-evaluator/tests/sdk/*
Adds revision-aware replace, retrieve, list, and tag operations to task and taskset resources.
Backend and integration validation
plugins/nemo-evaluator/tests/conftest.py, plugins/nemo-evaluator/tests/integration/*, services/core/entities/tests/repository/*
Validates digest-pinned reads, deduplication, parent-scoped lookup, deletion cascades, taskset republishing, child filtering, and shared entity-store behavior.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant EvaluatorAPI
  participant TasksetService
  participant TaskService
  participant EntityStore
  Client->>EvaluatorAPI: PUT taskset with task references
  EvaluatorAPI->>TasksetService: replace_taskset
  TasksetService->>TaskService: resolve each task revision
  TaskService->>EntityStore: resolve tag or digest
  EntityStore-->>TaskService: return content digest
  TasksetService->>EntityStore: publish digest-pinned taskset revision
  EntityStore-->>EvaluatorAPI: return revision-backed taskset
  EvaluatorAPI-->>Client: return 201 or 200
Loading

Possibly related PRs

Suggested labels: docs

Suggested reviewers: ngoncharenko, arpitsardhana, maxdubrinsky

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding content-addressed revisions for evaluator tasks and tasksets.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch schapman/harbor-cli-dataset-endpoint-45cb1d

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: 8

🧹 Nitpick comments (8)
plugins/nemo-evaluator/src/nemo_evaluator/api/schemas.py (1)

484-485: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse the single digest pattern.

content_hash.DIGEST_PATTERN already defines ^[0-9a-f]{64}$. revisions._DIGEST_FRAGMENT is a third copy. Import the constant here so the digest shape has one definition.

♻️ Proposed change
-#: Shape of a content digest in a ref fragment: full-length lowercase hex, never truncated.
-_DIGEST_FRAGMENT_PATTERN = re.compile(r"^[0-9a-f]{64}$")
+#: Shape of a content digest in a ref fragment: full-length lowercase hex, never truncated.
+_DIGEST_FRAGMENT_PATTERN = re.compile(DIGEST_PATTERN)

Add the import (from nemo_evaluator.content_hash import DIGEST_PATTERN). content_hash imports nothing from schemas, so no cycle.

🤖 Prompt for 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.

In `@plugins/nemo-evaluator/src/nemo_evaluator/api/schemas.py` around lines 484 -
485, Replace the local _DIGEST_FRAGMENT_PATTERN definition in schemas.py with
the shared DIGEST_PATTERN imported from nemo_evaluator.content_hash, preserving
the existing 64-character lowercase hexadecimal validation and avoiding
duplicate definitions.
plugins/nemo-evaluator/tests/test_revisions.py (2)

103-109: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Align FakeStore.list with the protocol it stands in for.

EntityStoreProtocol.list declares sort, page, and page_size. revisions.list_revisions passes sort="-created_at" and page=page. This fake accepts neither, so any test that exercises list_revisions fails with TypeError instead of testing behavior. Accept both keywords.

♻️ Proposed change
-    async def list(self, entity_type, *, workspace, filter_operation=None, page_size=100):
+    async def list(self, entity_type, *, workspace, filter_operation=None, sort=None, page=1, page_size=100):
🤖 Prompt for 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.

In `@plugins/nemo-evaluator/tests/test_revisions.py` around lines 103 - 109,
Update the FakeStore.list method to accept the protocol’s sort and page keyword
arguments in addition to workspace, filter_operation, and page_size. Preserve
its existing filter-evaluation behavior while allowing revisions.list_revisions
to pass sort="-created_at" and page=page without raising TypeError.

204-210: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the duplicated assertion and fix the test name.

Line 209 and line 210 assert the same thing. The name says indexes_the_digest, but no assertion covers a digest index. Drop the duplicate and rename to match what the test proves.

♻️ Proposed change
-async def test_first_publish_applies_latest_and_indexes_the_digest() -> None:
+async def test_first_publish_applies_latest_and_advances_latest_revision() -> None:
     store = FakeStore()
     head = _head(store)
     revision, _ = await _publish(store, head)
     assert head.tags[LATEST_TAG] == revision.revision
     assert head.latest_revision == 1
-    assert head.latest_revision == 1
🤖 Prompt for 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.

In `@plugins/nemo-evaluator/tests/test_revisions.py` around lines 204 - 210,
Update test_first_publish_applies_latest_and_indexes_the_digest by removing the
duplicated head.latest_revision assertion and renaming the test to describe only
the latest-tag and revision behavior it verifies, since it does not assert
digest indexing.
plugins/nemo-evaluator/src/nemo_evaluator/task_refs.py (1)

81-98: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Member expansion now costs two sequential store calls per member.

Each member requires a head get plus a revision lookup. A digest fragment adds a filtered list query. The loop awaits them one member at a time, so expansion latency scales with member count times two round-trips. If tasksets can hold many members, fetch members concurrently with asyncio.gather. Note that concurrency changes which failure surfaces first, so keep the error text keyed to the failing ref.

🤖 Prompt for 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.

In `@plugins/nemo-evaluator/src/nemo_evaluator/task_refs.py` around lines 81 - 98,
The taskset member expansion loop should fetch task entities and pinned
revisions concurrently instead of awaiting each member’s head and revision calls
sequentially. Update the expansion flow around parse_subentity_ref,
entity_client.get, and get_revision to use asyncio.gather while retaining
per-member error handling and messages keyed to task_ref.root and ref.root,
including digest-fragment lookups.
plugins/nemo-evaluator/src/nemo_evaluator/api/v2/tasks.py (1)

194-218: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Both revision-listing routes skip the module's error-handling convention. Every other route in these modules wraps its service call, logs the exception, and returns a 500. These two let store failures propagate raw.

  • plugins/nemo-evaluator/src/nemo_evaluator/api/v2/tasks.py#L194-L218: wrap service.list_revisions in try/except, log with logger.exception, and raise a 500.
  • plugins/nemo-evaluator/src/nemo_evaluator/api/v2/tasksets.py#L197-L218: apply the same wrapping.
🤖 Prompt for 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.

In `@plugins/nemo-evaluator/src/nemo_evaluator/api/v2/tasks.py` around lines 194 -
218, Wrap the service call in list_task_revisions in tasks.py
(plugins/nemo-evaluator/src/nemo_evaluator/api/v2/tasks.py, lines 194-218) with
the module’s standard try/except, log failures using logger.exception, and raise
an HTTP 500 while preserving the existing not-found handling. Apply the same
wrapping to the corresponding revision-listing route in tasksets.py
(plugins/nemo-evaluator/src/nemo_evaluator/api/v2/tasksets.py, lines 197-218).
plugins/nemo-evaluator/src/nemo_evaluator/sdk/taskset_resources.py (1)

60-108: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

These four methods duplicate task_resources.py exactly.

Only the DTO type differs. Two more copies exist in the async class. A generic base parameterized on the DTO and path segment would collapse eight bodies into two. This follows the module's existing pattern, so defer it if you prefer to keep the layers independent.

🤖 Prompt for 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.

In `@plugins/nemo-evaluator/src/nemo_evaluator/sdk/taskset_resources.py` around
lines 60 - 108, The methods replace, list_revisions, tag, and retrieve in this
class duplicate identical logic from task_resources.py with only the DTO type
differing. Create a generic base class parameterized on the DTO type and
resource path segment, implement the shared logic once in the base, and have
both the sync taskset resource class and the async variant inherit from it to
eliminate duplication across all eight method bodies. Follow the existing module
pattern for generic parameterization.
plugins/nemo-evaluator/tests/api/v2/test_tasks_routes.py (1)

257-273: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse the client fixture.

The client fixture already receives the same entity_store, so this test can request both and drop the duplicated app wiring.

♻️ Proposed refactor
-def test_concurrent_replace_returns_409_not_500(entity_store) -> None:
+def test_concurrent_replace_returns_409_not_500(client: TestClient, entity_store) -> None:
     """A lost optimistic lock is a retryable client conflict. Before this was mapped it fell to the
     catch-all and surfaced as a 500, wrongly implying a server fault."""
 
     async def _stale(entity, *, original_name=None):
         raise NemoEntityConflictError("modified by another request")
 
-    app = FastAPI()
-    app.include_router(tasks_routes.router, prefix="/v2/workspaces/{workspace}")
-    service = TaskService(entity_store, _FakeMetricService())
-    app.dependency_overrides[get_task_service] = lambda: service
-    client = TestClient(app)
-
     client.post(f"{_BASE}/task-1", json=_body())
     entity_store.update = _stale
🤖 Prompt for 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.

In `@plugins/nemo-evaluator/tests/api/v2/test_tasks_routes.py` around lines 257 -
273, The test_concurrent_replace_returns_409_not_500 function manually creates a
FastAPI app, includes the router, instantiates the service, and creates a
TestClient, but a client fixture already exists that is configured with the same
entity_store. Add client as a parameter to the test function signature alongside
entity_store, then remove the manual app initialization, router inclusion,
service instantiation, and client creation code. Keep the stubbing of
entity_store.update with _stale and the POST/PUT request assertions intact.
plugins/nemo-evaluator/src/nemo_evaluator/sdk/task_resources.py (1)

56-69: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

replace hides the publish outcome.

The route distinguishes "published" (201) from "unchanged" (200), but the SDK discards the status code. A caller that wants to know whether a revision was cut must diff revision against a prior read. Consider surfacing the status, or document the workaround in the docstring.

🤖 Prompt for 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.

In `@plugins/nemo-evaluator/src/nemo_evaluator/sdk/task_resources.py` around lines
56 - 69, The replace method discards the HTTP status code distinction between a
published revision (201) and an unchanged response (200), making it impossible
for callers to know whether a new revision was cut. Either modify the replace
method to return status information along with the Task object, or update the
existing docstring to document that callers must compare the returned Task
revision against a prior read to determine if a publish occurred. Choose one
approach and implement it consistently.
🤖 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 `@docs/evaluator/manage-tasks-tasksets.mdx`:
- Around line 150-159: Update the “Read a specific revision” example using
tasks.list_revisions so it does not imply data[-1] is always revision 1: either
explicitly state that the example assumes the complete history fits on one page,
or select the intended revision by its ordinal across paginated results.
Preserve the comparison between retrieving the selected revision and the current
content.
- Around line 131-134: The revised_task variable used in the tasks.replace call
is undefined, making the code example incomplete. Define revised_task inline
before the replace call so the example is self-contained and runs as written.
Preserve the tasks.replace call and its output unchanged.

In `@plugins/nemo-evaluator/src/nemo_evaluator/api/service/task_service.py`:
- Around line 220-226: The replace operation in both task_service.py and
taskset_service.py updates and persists the head via entity_client.update()
before publishing via _publish(), creating a window where publishing failure
leaves the head with content not covered by any revision. Fix this inconsistency
in both files by either reordering to call _publish() before
entity_client.update() commits the head, or by adding compensation logic to
restore the previous head content when _publish() raises an exception. Ensure
the approach matches how create_task and create_taskset handle the same
scenario. Apply this fix at task_service.py lines 220-226 in the code around
entity_client.update(head) and _publish(stored, tags=set(task_input.tags)), and
at taskset_service.py lines 286-290 with the corresponding
entity_client.update(head) and _publish() calls.

In `@plugins/nemo-evaluator/src/nemo_evaluator/api/v2/tasks.py`:
- Around line 136-139: The responses maps in the PUT routes are incomplete. They
declare the status codes but omit the response model for 201 and do not declare
the 409 conflict status that the routes raise. At
plugins/nemo-evaluator/src/nemo_evaluator/api/v2/tasks.py lines 136-139, add the
Task model reference to the HTTP_201_CREATED entry and add a new
HTTP_409_CONFLICT entry to the responses dict. At
plugins/nemo-evaluator/src/nemo_evaluator/api/v2/tasksets.py lines 141-144, add
the Taskset model reference to the HTTP_201_CREATED entry and add a new
HTTP_409_CONFLICT entry to the responses dict. After updating both route
handlers, regenerate plugins/nemo-evaluator/openapi/openapi.yaml by running make
refresh-openapi and do not manually edit the OpenAPI file.

In `@plugins/nemo-evaluator/src/nemo_evaluator/content_hash.py`:
- Line 82: Raise the minimum Pydantic version to 2.12.0 in both pyproject.toml
dependency declarations for nemo_platform_plugin and nemo-evaluator, ensuring
unconstrained installations support EntityBase.model_dump with
exclude_computed_fields; leave the existing lockfile version unchanged.

In `@plugins/nemo-evaluator/tests/conftest.py`:
- Around line 112-130: Update the list implementation around the sort and
pagination logic: allow an empty items collection to return an empty page
without raising NotImplementedError, while still validating sortable fields when
items exist. Capture total_results before slicing, and calculate total_pages
from the full result count and page_size instead of hardcoding 1; keep
current_page_size based on the sliced page.

In `@plugins/nemo-evaluator/tests/integration/test_task_revisions.py`:
- Around line 205-207: Update the assertions in the task revision test around
created.tasks[0].root to verify that member equals the task revision’s
content_hash, rather than only checking for a "#" fragment or extracting
pinned_digest. Preserve the existing taskset write coverage while asserting the
stored member resolves to the exact content digest.

In `@plugins/nemo-evaluator/tests/test_content_hash.py`:
- Around line 88-91: Update test_mapping_insertion_order_does_not_affect_digest
to construct equivalent mapping data with different insertion orders, rather
than identical metadata lists. Use a mapping-valued field or other mapping input
supported by _task and content_hash, while preserving the assertion that both
digests are equal; do not use metadata because list order is significant.

---

Nitpick comments:
In `@plugins/nemo-evaluator/src/nemo_evaluator/api/schemas.py`:
- Around line 484-485: Replace the local _DIGEST_FRAGMENT_PATTERN definition in
schemas.py with the shared DIGEST_PATTERN imported from
nemo_evaluator.content_hash, preserving the existing 64-character lowercase
hexadecimal validation and avoiding duplicate definitions.

In `@plugins/nemo-evaluator/src/nemo_evaluator/api/v2/tasks.py`:
- Around line 194-218: Wrap the service call in list_task_revisions in tasks.py
(plugins/nemo-evaluator/src/nemo_evaluator/api/v2/tasks.py, lines 194-218) with
the module’s standard try/except, log failures using logger.exception, and raise
an HTTP 500 while preserving the existing not-found handling. Apply the same
wrapping to the corresponding revision-listing route in tasksets.py
(plugins/nemo-evaluator/src/nemo_evaluator/api/v2/tasksets.py, lines 197-218).

In `@plugins/nemo-evaluator/src/nemo_evaluator/sdk/task_resources.py`:
- Around line 56-69: The replace method discards the HTTP status code
distinction between a published revision (201) and an unchanged response (200),
making it impossible for callers to know whether a new revision was cut. Either
modify the replace method to return status information along with the Task
object, or update the existing docstring to document that callers must compare
the returned Task revision against a prior read to determine if a publish
occurred. Choose one approach and implement it consistently.

In `@plugins/nemo-evaluator/src/nemo_evaluator/sdk/taskset_resources.py`:
- Around line 60-108: The methods replace, list_revisions, tag, and retrieve in
this class duplicate identical logic from task_resources.py with only the DTO
type differing. Create a generic base class parameterized on the DTO type and
resource path segment, implement the shared logic once in the base, and have
both the sync taskset resource class and the async variant inherit from it to
eliminate duplication across all eight method bodies. Follow the existing module
pattern for generic parameterization.

In `@plugins/nemo-evaluator/src/nemo_evaluator/task_refs.py`:
- Around line 81-98: The taskset member expansion loop should fetch task
entities and pinned revisions concurrently instead of awaiting each member’s
head and revision calls sequentially. Update the expansion flow around
parse_subentity_ref, entity_client.get, and get_revision to use asyncio.gather
while retaining per-member error handling and messages keyed to task_ref.root
and ref.root, including digest-fragment lookups.

In `@plugins/nemo-evaluator/tests/api/v2/test_tasks_routes.py`:
- Around line 257-273: The test_concurrent_replace_returns_409_not_500 function
manually creates a FastAPI app, includes the router, instantiates the service,
and creates a TestClient, but a client fixture already exists that is configured
with the same entity_store. Add client as a parameter to the test function
signature alongside entity_store, then remove the manual app initialization,
router inclusion, service instantiation, and client creation code. Keep the
stubbing of entity_store.update with _stale and the POST/PUT request assertions
intact.

In `@plugins/nemo-evaluator/tests/test_revisions.py`:
- Around line 103-109: Update the FakeStore.list method to accept the protocol’s
sort and page keyword arguments in addition to workspace, filter_operation, and
page_size. Preserve its existing filter-evaluation behavior while allowing
revisions.list_revisions to pass sort="-created_at" and page=page without
raising TypeError.
- Around line 204-210: Update
test_first_publish_applies_latest_and_indexes_the_digest by removing the
duplicated head.latest_revision assertion and renaming the test to describe only
the latest-tag and revision behavior it verifies, since it does not assert
digest indexing.
🪄 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: 6867909a-c938-493f-8c21-4b855b071a6a

📥 Commits

Reviewing files that changed from the base of the PR and between 79ca283 and 2a18647.

📒 Files selected for processing (27)
  • 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/api/service/task_service.py
  • plugins/nemo-evaluator/src/nemo_evaluator/api/service/taskset_service.py
  • plugins/nemo-evaluator/src/nemo_evaluator/api/v2/tasks.py
  • plugins/nemo-evaluator/src/nemo_evaluator/api/v2/tasksets.py
  • plugins/nemo-evaluator/src/nemo_evaluator/content_hash.py
  • plugins/nemo-evaluator/src/nemo_evaluator/entities.py
  • plugins/nemo-evaluator/src/nemo_evaluator/revisions.py
  • plugins/nemo-evaluator/src/nemo_evaluator/sdk/task_resources.py
  • plugins/nemo-evaluator/src/nemo_evaluator/sdk/taskset_resources.py
  • plugins/nemo-evaluator/src/nemo_evaluator/task_refs.py
  • plugins/nemo-evaluator/tests/api/service/test_task_service.py
  • plugins/nemo-evaluator/tests/api/service/test_taskset_service.py
  • plugins/nemo-evaluator/tests/api/v2/test_tasks_routes.py
  • plugins/nemo-evaluator/tests/api/v2/test_tasksets_routes.py
  • plugins/nemo-evaluator/tests/conftest.py
  • plugins/nemo-evaluator/tests/integration/test_task_revisions.py
  • plugins/nemo-evaluator/tests/sdk/test_task_sdk_resources.py
  • plugins/nemo-evaluator/tests/sdk/test_taskset_sdk_resources.py
  • plugins/nemo-evaluator/tests/test_content_hash.py
  • plugins/nemo-evaluator/tests/test_revision_entity.py
  • plugins/nemo-evaluator/tests/test_revisions.py
  • plugins/nemo-evaluator/tests/test_subentity_refs.py
  • plugins/nemo-evaluator/tests/test_task_refs.py
  • services/core/entities/tests/repository/test_child_entity_filtering.py

Comment thread docs/evaluator/manage-tasks-tasksets.mdx
Comment thread docs/evaluator/manage-tasks-tasksets.mdx
Comment thread plugins/nemo-evaluator/src/nemo_evaluator/api/service/task_service.py Outdated
Comment thread plugins/nemo-evaluator/src/nemo_evaluator/api/v2/tasks.py
Comment thread plugins/nemo-evaluator/src/nemo_evaluator/content_hash.py
Comment thread plugins/nemo-evaluator/tests/conftest.py Outdated
Comment thread plugins/nemo-evaluator/tests/integration/test_task_revisions.py
Comment thread plugins/nemo-evaluator/tests/test_content_hash.py
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@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 `@docs/evaluator/manage-tasks-tasksets.mdx`:
- Around line 27-29: Update the revision-retention statement near the versioning
description to clarify that earlier revisions remain readable only until their
parent task or taskset is deleted. Preserve the existing behavior description
that replacing content publishes a new revision.

In `@plugins/nemo-evaluator/src/nemo_evaluator/revisions.py`:
- Around line 133-151: Update validate_tag_name to enforce the same [\w\-.]+
reference-fragment syntax accepted by TaskRef, while preserving the existing
empty and digest-shaped validations. Reject tags containing slashes or
whitespace, and add tests covering both invalid forms.
🪄 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: 519e6d18-6014-4110-ad14-264cef39bb24

📥 Commits

Reviewing files that changed from the base of the PR and between 97b4cea and 38648ad.

📒 Files selected for processing (27)
  • 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/api/service/task_service.py
  • plugins/nemo-evaluator/src/nemo_evaluator/api/service/taskset_service.py
  • plugins/nemo-evaluator/src/nemo_evaluator/api/v2/tasks.py
  • plugins/nemo-evaluator/src/nemo_evaluator/api/v2/tasksets.py
  • plugins/nemo-evaluator/src/nemo_evaluator/content_hash.py
  • plugins/nemo-evaluator/src/nemo_evaluator/entities.py
  • plugins/nemo-evaluator/src/nemo_evaluator/revisions.py
  • plugins/nemo-evaluator/src/nemo_evaluator/sdk/task_resources.py
  • plugins/nemo-evaluator/src/nemo_evaluator/sdk/taskset_resources.py
  • plugins/nemo-evaluator/src/nemo_evaluator/task_refs.py
  • plugins/nemo-evaluator/tests/api/service/test_task_service.py
  • plugins/nemo-evaluator/tests/api/service/test_taskset_service.py
  • plugins/nemo-evaluator/tests/api/v2/test_tasks_routes.py
  • plugins/nemo-evaluator/tests/api/v2/test_tasksets_routes.py
  • plugins/nemo-evaluator/tests/conftest.py
  • plugins/nemo-evaluator/tests/integration/test_task_revisions.py
  • plugins/nemo-evaluator/tests/sdk/test_task_sdk_resources.py
  • plugins/nemo-evaluator/tests/sdk/test_taskset_sdk_resources.py
  • plugins/nemo-evaluator/tests/test_content_hash.py
  • plugins/nemo-evaluator/tests/test_revision_entity.py
  • plugins/nemo-evaluator/tests/test_revisions.py
  • plugins/nemo-evaluator/tests/test_subentity_refs.py
  • plugins/nemo-evaluator/tests/test_task_refs.py
  • services/core/entities/tests/repository/test_child_entity_filtering.py
🚧 Files skipped from review as they are similar to previous changes (21)
  • services/core/entities/tests/repository/test_child_entity_filtering.py
  • plugins/nemo-evaluator/tests/test_task_refs.py
  • plugins/nemo-evaluator/tests/test_subentity_refs.py
  • plugins/nemo-evaluator/src/nemo_evaluator/task_refs.py
  • plugins/nemo-evaluator/tests/integration/test_task_revisions.py
  • plugins/nemo-evaluator/tests/test_revisions.py
  • plugins/nemo-evaluator/src/nemo_evaluator/api/service/task_service.py
  • plugins/nemo-evaluator/tests/api/v2/test_tasks_routes.py
  • plugins/nemo-evaluator/tests/sdk/test_taskset_sdk_resources.py
  • plugins/nemo-evaluator/tests/api/v2/test_tasksets_routes.py
  • plugins/nemo-evaluator/tests/api/service/test_task_service.py
  • plugins/nemo-evaluator/src/nemo_evaluator/api/v2/tasks.py
  • plugins/nemo-evaluator/tests/sdk/test_task_sdk_resources.py
  • plugins/nemo-evaluator/src/nemo_evaluator/entities.py
  • plugins/nemo-evaluator/src/nemo_evaluator/sdk/task_resources.py
  • plugins/nemo-evaluator/tests/conftest.py
  • plugins/nemo-evaluator/tests/api/service/test_taskset_service.py
  • plugins/nemo-evaluator/src/nemo_evaluator/api/v2/tasksets.py
  • plugins/nemo-evaluator/src/nemo_evaluator/api/service/taskset_service.py
  • plugins/nemo-evaluator/src/nemo_evaluator/sdk/taskset_resources.py
  • plugins/nemo-evaluator/openapi/openapi.yaml

Comment thread docs/evaluator/manage-tasks-tasksets.mdx Outdated
Comment thread plugins/nemo-evaluator/src/nemo_evaluator/revisions.py
@SandyChapman
SandyChapman force-pushed the schapman/harbor-cli-dataset-endpoint-45cb1d branch from 38648ad to 47c27f8 Compare August 5, 2026 12:10
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@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: 1

🤖 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/revisions.py`:
- Around line 337-351: Update the publish retry flow in revisions.py so a
conflict during the child create step is treated as proof that the current
ordinal is already allocated, not just a transient failure. In the logic around
_point_tags and the NemoEntityConflictError handling, advance the
allocation/latest revision state after the conflicting rev.N create before
retrying, so the next attempt moves on to N+1 instead of looping on the same
ordinal. Keep the existing refresh-on-contention behavior for head pointer
allocation, but ensure the conflicting child create path also updates the
ordinal state. Add a regression test covering an existing rev.N, a head at N-1,
and new content that verifies the publish succeeds by advancing past the
occupied ordinal.
🪄 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: 20d70923-bf03-4e3a-94d6-c06702109d23

📥 Commits

Reviewing files that changed from the base of the PR and between 97b4cea and 47c27f8.

📒 Files selected for processing (27)
  • 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/api/service/task_service.py
  • plugins/nemo-evaluator/src/nemo_evaluator/api/service/taskset_service.py
  • plugins/nemo-evaluator/src/nemo_evaluator/api/v2/tasks.py
  • plugins/nemo-evaluator/src/nemo_evaluator/api/v2/tasksets.py
  • plugins/nemo-evaluator/src/nemo_evaluator/content_hash.py
  • plugins/nemo-evaluator/src/nemo_evaluator/entities.py
  • plugins/nemo-evaluator/src/nemo_evaluator/revisions.py
  • plugins/nemo-evaluator/src/nemo_evaluator/sdk/task_resources.py
  • plugins/nemo-evaluator/src/nemo_evaluator/sdk/taskset_resources.py
  • plugins/nemo-evaluator/src/nemo_evaluator/task_refs.py
  • plugins/nemo-evaluator/tests/api/service/test_task_service.py
  • plugins/nemo-evaluator/tests/api/service/test_taskset_service.py
  • plugins/nemo-evaluator/tests/api/v2/test_tasks_routes.py
  • plugins/nemo-evaluator/tests/api/v2/test_tasksets_routes.py
  • plugins/nemo-evaluator/tests/conftest.py
  • plugins/nemo-evaluator/tests/integration/test_task_revisions.py
  • plugins/nemo-evaluator/tests/sdk/test_task_sdk_resources.py
  • plugins/nemo-evaluator/tests/sdk/test_taskset_sdk_resources.py
  • plugins/nemo-evaluator/tests/test_content_hash.py
  • plugins/nemo-evaluator/tests/test_revision_entity.py
  • plugins/nemo-evaluator/tests/test_revisions.py
  • plugins/nemo-evaluator/tests/test_subentity_refs.py
  • plugins/nemo-evaluator/tests/test_task_refs.py
  • services/core/entities/tests/repository/test_child_entity_filtering.py
🚧 Files skipped from review as they are similar to previous changes (21)
  • services/core/entities/tests/repository/test_child_entity_filtering.py
  • plugins/nemo-evaluator/tests/test_subentity_refs.py
  • plugins/nemo-evaluator/src/nemo_evaluator/task_refs.py
  • plugins/nemo-evaluator/tests/test_task_refs.py
  • plugins/nemo-evaluator/tests/api/v2/test_tasksets_routes.py
  • plugins/nemo-evaluator/tests/integration/test_task_revisions.py
  • plugins/nemo-evaluator/tests/sdk/test_taskset_sdk_resources.py
  • plugins/nemo-evaluator/tests/api/v2/test_tasks_routes.py
  • plugins/nemo-evaluator/tests/sdk/test_task_sdk_resources.py
  • plugins/nemo-evaluator/tests/test_revisions.py
  • plugins/nemo-evaluator/tests/conftest.py
  • plugins/nemo-evaluator/src/nemo_evaluator/api/service/task_service.py
  • plugins/nemo-evaluator/src/nemo_evaluator/api/service/taskset_service.py
  • plugins/nemo-evaluator/src/nemo_evaluator/sdk/taskset_resources.py
  • plugins/nemo-evaluator/tests/api/service/test_task_service.py
  • plugins/nemo-evaluator/src/nemo_evaluator/sdk/task_resources.py
  • plugins/nemo-evaluator/src/nemo_evaluator/api/v2/tasksets.py
  • plugins/nemo-evaluator/src/nemo_evaluator/entities.py
  • plugins/nemo-evaluator/src/nemo_evaluator/api/v2/tasks.py
  • plugins/nemo-evaluator/tests/api/service/test_taskset_service.py
  • plugins/nemo-evaluator/openapi/openapi.yaml

Comment thread plugins/nemo-evaluator/src/nemo_evaluator/revisions.py Outdated
@SandyChapman
SandyChapman force-pushed the schapman/harbor-cli-dataset-endpoint-45cb1d branch from 47c27f8 to df436ba Compare August 5, 2026 12:15
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@SandyChapman
SandyChapman force-pushed the schapman/harbor-cli-dataset-endpoint-45cb1d branch from df436ba to 05071c2 Compare August 5, 2026 12:18

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
plugins/nemo-evaluator/src/nemo_evaluator/api/schemas.py (1)

476-484: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Normalize member order before hashing.

This validator returns the caller order. PinnedTaskRefList therefore preserves [A, B] versus [B, A], while canonical hashing treats list order as significant. Reordering the same taskset members can publish a new revision despite set semantics.

Return refs in a stable order here. Add a regression test with reversed digest-pinned members.

Proposed fix
 def _reject_duplicate_task_refs(refs: list[TaskRef]) -> list[TaskRef]:
     ...
-    return refs
+    return sorted(refs, key=lambda ref: ref.root)
🤖 Prompt for 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.

In `@plugins/nemo-evaluator/src/nemo_evaluator/api/schemas.py` around lines 476 -
484, Update _reject_duplicate_task_refs to return the validated refs in a
deterministic order, such as sorting by each ref.root, while preserving
duplicate rejection. Add a regression test using the same digest-pinned members
in reversed order and verify both inputs produce the same canonical hash or
revision.
🧹 Nitpick comments (3)
plugins/nemo-evaluator/tests/test_revisions.py (2)

286-291: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

range(1, 50) couples the test to an unstated retry bound.

The literal 50 must exceed the retry limit in publish_revision. Import that limit and derive the range from it, so the test tracks the implementation.

🤖 Prompt for 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.

In `@plugins/nemo-evaluator/tests/test_revisions.py` around lines 286 - 291,
Update test_persistent_contention_raises_rather_than_looping to import the
retry-limit constant used by publish_revision and derive store.contend_ordinals
from that symbol, rather than hard-coding 50. Keep the test’s
persistent-conflict behavior and RevisionConflictError assertion unchanged.

153-188: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

_head and _head_named duplicate each other.

The two builders differ only in the name and intent values. Merge them into one helper with both as keyword parameters.

♻️ Proposed change
-def _head(store: FakeStore, *, intent: str = "Answer the question.") -> TaskEntity:
+def _head(store: FakeStore, *, name: str = "task-1", intent: str = "Answer the question.") -> TaskEntity:
     head = TaskEntity(
-        name="task-1",
+        name=name,
         workspace="default",
         intent=intent,
         inputs=TaskInputs(instruction="What is 2+2?"),
         metrics=[MetricRef("default/stored-metric")],
     )
-    head._id = "head-1"
+    head._id = f"head-{name}"
     head._db_version = 0
     stored = head.model_copy(deep=True)
     stored._id = head.id
     stored._db_version = 0
-    store.records[store._key(TaskEntity, "task-1", "default", None)] = stored
+    store.records[store._key(TaskEntity, name, "default", None)] = stored
     return head

Then replace _head_named(store, "task-2") with _head(store, name="task-2") at Lines 446 and 459.

🤖 Prompt for 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.

In `@plugins/nemo-evaluator/tests/test_revisions.py` around lines 153 - 188, Merge
_head_named into _head by adding keyword parameters for name and intent,
preserving the current defaults and deriving the ID and storage key from the
supplied name. Remove _head_named and update its callers to invoke _head with
name="task-2".
plugins/nemo-evaluator/tests/test_content_hash.py (1)

118-122: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Strengthen the exclude assertion.

The current assertion proves only that exclude changes the digest. It passes even if exclude corrupted the payload instead of dropping the field. Compare against an entity with no metadata to prove the field was actually removed.

♻️ Proposed change
     entity = _task()
-    assert content_hash(entity, exclude={"metadata"}) != content_hash(entity)
+    assert content_hash(entity, exclude={"metadata"}) != content_hash(entity)
+    assert content_hash(entity, exclude={"metadata"}) == content_hash(_task(metadata=[]), exclude={"metadata"})
🤖 Prompt for 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.

In `@plugins/nemo-evaluator/tests/test_content_hash.py` around lines 118 - 122,
Strengthen test_extra_exclude_is_honoured by comparing content_hash(entity,
exclude={"metadata"}) with the hash of an equivalent entity whose metadata has
been removed. Keep the existing assertion only if useful, but ensure the test
proves exclusion produces the same digest as the payload without metadata rather
than merely changing the digest.
🤖 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/tests/test_content_hash.py`:
- Around line 132-133: Correct test_absent_vs_empty_collection_differ so its
name matches the values being compared: rename it to
test_empty_and_populated_metrics_differ when retaining _task()’s populated
default, or explicitly construct an entity with metrics absent if testing
absent-versus-empty behavior. Keep the assertion aligned with the selected
scenario.

In `@plugins/nemo-evaluator/tests/test_revisions.py`:
- Around line 72-75: Update _win_race in test_revisions.py so it resolves the
head record from the winning revision’s parent id instead of hardcoding
"task-1"; use the existing _key(TaskEntity, ..., winner.workspace, None) lookup
with the parent-derived identifier from the revision object. Keep the
latest_revision and LATEST_TAG advancement logic unchanged, but ensure the
lookup works for heads created by _head_named with arbitrary names so contention
tests always advance the correct head.

---

Outside diff comments:
In `@plugins/nemo-evaluator/src/nemo_evaluator/api/schemas.py`:
- Around line 476-484: Update _reject_duplicate_task_refs to return the
validated refs in a deterministic order, such as sorting by each ref.root, while
preserving duplicate rejection. Add a regression test using the same
digest-pinned members in reversed order and verify both inputs produce the same
canonical hash or revision.

---

Nitpick comments:
In `@plugins/nemo-evaluator/tests/test_content_hash.py`:
- Around line 118-122: Strengthen test_extra_exclude_is_honoured by comparing
content_hash(entity, exclude={"metadata"}) with the hash of an equivalent entity
whose metadata has been removed. Keep the existing assertion only if useful, but
ensure the test proves exclusion produces the same digest as the payload without
metadata rather than merely changing the digest.

In `@plugins/nemo-evaluator/tests/test_revisions.py`:
- Around line 286-291: Update
test_persistent_contention_raises_rather_than_looping to import the retry-limit
constant used by publish_revision and derive store.contend_ordinals from that
symbol, rather than hard-coding 50. Keep the test’s persistent-conflict behavior
and RevisionConflictError assertion unchanged.
- Around line 153-188: Merge _head_named into _head by adding keyword parameters
for name and intent, preserving the current defaults and deriving the ID and
storage key from the supplied name. Remove _head_named and update its callers to
invoke _head with name="task-2".
🪄 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: 9de48a86-12db-4d8f-a71a-41ceb76ed5c3

📥 Commits

Reviewing files that changed from the base of the PR and between 97b4cea and df436ba.

📒 Files selected for processing (27)
  • 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/api/service/task_service.py
  • plugins/nemo-evaluator/src/nemo_evaluator/api/service/taskset_service.py
  • plugins/nemo-evaluator/src/nemo_evaluator/api/v2/tasks.py
  • plugins/nemo-evaluator/src/nemo_evaluator/api/v2/tasksets.py
  • plugins/nemo-evaluator/src/nemo_evaluator/content_hash.py
  • plugins/nemo-evaluator/src/nemo_evaluator/entities.py
  • plugins/nemo-evaluator/src/nemo_evaluator/revisions.py
  • plugins/nemo-evaluator/src/nemo_evaluator/sdk/task_resources.py
  • plugins/nemo-evaluator/src/nemo_evaluator/sdk/taskset_resources.py
  • plugins/nemo-evaluator/src/nemo_evaluator/task_refs.py
  • plugins/nemo-evaluator/tests/api/service/test_task_service.py
  • plugins/nemo-evaluator/tests/api/service/test_taskset_service.py
  • plugins/nemo-evaluator/tests/api/v2/test_tasks_routes.py
  • plugins/nemo-evaluator/tests/api/v2/test_tasksets_routes.py
  • plugins/nemo-evaluator/tests/conftest.py
  • plugins/nemo-evaluator/tests/integration/test_task_revisions.py
  • plugins/nemo-evaluator/tests/sdk/test_task_sdk_resources.py
  • plugins/nemo-evaluator/tests/sdk/test_taskset_sdk_resources.py
  • plugins/nemo-evaluator/tests/test_content_hash.py
  • plugins/nemo-evaluator/tests/test_revision_entity.py
  • plugins/nemo-evaluator/tests/test_revisions.py
  • plugins/nemo-evaluator/tests/test_subentity_refs.py
  • plugins/nemo-evaluator/tests/test_task_refs.py
  • services/core/entities/tests/repository/test_child_entity_filtering.py
🚧 Files skipped from review as they are similar to previous changes (18)
  • plugins/nemo-evaluator/tests/test_subentity_refs.py
  • plugins/nemo-evaluator/src/nemo_evaluator/api/service/task_service.py
  • plugins/nemo-evaluator/src/nemo_evaluator/sdk/task_resources.py
  • plugins/nemo-evaluator/src/nemo_evaluator/entities.py
  • plugins/nemo-evaluator/tests/integration/test_task_revisions.py
  • plugins/nemo-evaluator/tests/api/v2/test_tasks_routes.py
  • services/core/entities/tests/repository/test_child_entity_filtering.py
  • plugins/nemo-evaluator/src/nemo_evaluator/api/v2/tasks.py
  • plugins/nemo-evaluator/tests/conftest.py
  • plugins/nemo-evaluator/tests/sdk/test_task_sdk_resources.py
  • plugins/nemo-evaluator/src/nemo_evaluator/task_refs.py
  • plugins/nemo-evaluator/tests/test_task_refs.py
  • plugins/nemo-evaluator/src/nemo_evaluator/sdk/taskset_resources.py
  • plugins/nemo-evaluator/tests/api/service/test_taskset_service.py
  • plugins/nemo-evaluator/tests/api/service/test_task_service.py
  • plugins/nemo-evaluator/src/nemo_evaluator/api/v2/tasksets.py
  • plugins/nemo-evaluator/src/nemo_evaluator/api/service/taskset_service.py
  • plugins/nemo-evaluator/openapi/openapi.yaml

Comment thread plugins/nemo-evaluator/tests/test_content_hash.py Outdated
Comment thread plugins/nemo-evaluator/tests/test_revisions.py
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@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

♻️ Duplicate comments (1)
docs/evaluator/manage-tasks-tasksets.mdx (1)

163-166: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Paginate before selecting revision 1.

list_revisions() returns one page. When revision 1 is outside that page, next(...) raises StopIteration. Fetch pages until the ordinal is found, or use a known digest.

🤖 Prompt for 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.

In `@docs/evaluator/manage-tasks-tasksets.mdx` around lines 163 - 166, The
revision lookup example in the taskset docs assumes revision 1 is always present
in the first page returned by list_revisions(), but this can raise StopIteration
when that ordinal is on a later page. Update the example around page and digest
to either iterate through successive list_revisions() pages until revision == 1
is found, or switch to using a known digest so the guidance matches the
paginated API behavior.
🤖 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/revisions.py`:
- Around line 331-334: Update the deduplication branch in _apply_pointers so an
existing digest is reused only when that revision is the current latest
revision; if find_by_digest matches older history, continue through new-revision
publication instead. Add a regression test covering revision 2 reverting to
revision-1 content and verify current and `#latest` reads remain consistent.

In `@plugins/nemo-evaluator/tests/test_task_refs.py`:
- Around line 36-45: Update _store to capture each entity returned by
client.create and use that persisted result when publishing TaskEntity
revisions, rather than the original input. Ensure the returned task is also used
for head_digest and the subsequent update in
test_expansion_uses_the_pinned_revision_not_current_content, avoiding reliance
on FakeEntityStore.create mutating inputs.

---

Duplicate comments:
In `@docs/evaluator/manage-tasks-tasksets.mdx`:
- Around line 163-166: The revision lookup example in the taskset docs assumes
revision 1 is always present in the first page returned by list_revisions(), but
this can raise StopIteration when that ordinal is on a later page. Update the
example around page and digest to either iterate through successive
list_revisions() pages until revision == 1 is found, or switch to using a known
digest so the guidance matches the paginated API behavior.
🪄 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: 7c5b04c2-740c-4b31-bef4-2ab8480bf424

📥 Commits

Reviewing files that changed from the base of the PR and between 97b4cea and 05071c2.

📒 Files selected for processing (27)
  • 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/api/service/task_service.py
  • plugins/nemo-evaluator/src/nemo_evaluator/api/service/taskset_service.py
  • plugins/nemo-evaluator/src/nemo_evaluator/api/v2/tasks.py
  • plugins/nemo-evaluator/src/nemo_evaluator/api/v2/tasksets.py
  • plugins/nemo-evaluator/src/nemo_evaluator/content_hash.py
  • plugins/nemo-evaluator/src/nemo_evaluator/entities.py
  • plugins/nemo-evaluator/src/nemo_evaluator/revisions.py
  • plugins/nemo-evaluator/src/nemo_evaluator/sdk/task_resources.py
  • plugins/nemo-evaluator/src/nemo_evaluator/sdk/taskset_resources.py
  • plugins/nemo-evaluator/src/nemo_evaluator/task_refs.py
  • plugins/nemo-evaluator/tests/api/service/test_task_service.py
  • plugins/nemo-evaluator/tests/api/service/test_taskset_service.py
  • plugins/nemo-evaluator/tests/api/v2/test_tasks_routes.py
  • plugins/nemo-evaluator/tests/api/v2/test_tasksets_routes.py
  • plugins/nemo-evaluator/tests/conftest.py
  • plugins/nemo-evaluator/tests/integration/test_task_revisions.py
  • plugins/nemo-evaluator/tests/sdk/test_task_sdk_resources.py
  • plugins/nemo-evaluator/tests/sdk/test_taskset_sdk_resources.py
  • plugins/nemo-evaluator/tests/test_content_hash.py
  • plugins/nemo-evaluator/tests/test_revision_entity.py
  • plugins/nemo-evaluator/tests/test_revisions.py
  • plugins/nemo-evaluator/tests/test_subentity_refs.py
  • plugins/nemo-evaluator/tests/test_task_refs.py
  • services/core/entities/tests/repository/test_child_entity_filtering.py
🚧 Files skipped from review as they are similar to previous changes (19)
  • plugins/nemo-evaluator/tests/sdk/test_taskset_sdk_resources.py
  • plugins/nemo-evaluator/tests/test_subentity_refs.py
  • plugins/nemo-evaluator/tests/test_revisions.py
  • plugins/nemo-evaluator/src/nemo_evaluator/task_refs.py
  • services/core/entities/tests/repository/test_child_entity_filtering.py
  • plugins/nemo-evaluator/tests/sdk/test_task_sdk_resources.py
  • plugins/nemo-evaluator/tests/integration/test_task_revisions.py
  • plugins/nemo-evaluator/tests/api/v2/test_tasksets_routes.py
  • plugins/nemo-evaluator/tests/api/service/test_taskset_service.py
  • plugins/nemo-evaluator/src/nemo_evaluator/api/v2/tasks.py
  • plugins/nemo-evaluator/tests/api/v2/test_tasks_routes.py
  • plugins/nemo-evaluator/src/nemo_evaluator/api/service/task_service.py
  • plugins/nemo-evaluator/src/nemo_evaluator/entities.py
  • plugins/nemo-evaluator/src/nemo_evaluator/sdk/taskset_resources.py
  • plugins/nemo-evaluator/src/nemo_evaluator/api/service/taskset_service.py
  • plugins/nemo-evaluator/tests/conftest.py
  • plugins/nemo-evaluator/src/nemo_evaluator/api/v2/tasksets.py
  • plugins/nemo-evaluator/src/nemo_evaluator/sdk/task_resources.py
  • plugins/nemo-evaluator/openapi/openapi.yaml

Comment thread plugins/nemo-evaluator/src/nemo_evaluator/revisions.py Outdated
Comment thread plugins/nemo-evaluator/tests/test_task_refs.py
@SandyChapman
SandyChapman force-pushed the schapman/harbor-cli-dataset-endpoint-45cb1d branch 4 times, most recently from c69388a to 58fccbe Compare August 5, 2026 13:15
Stored tasks and tasksets are now versioned. Creating one publishes revision 1;
replacing its content publishes the next. Earlier revisions stay readable, so an
evaluation can be re-run against exactly the content it ran against before.

A revision is an immutable child entity named `rev.<n>`, addressed by a SHA-256
digest of its content. The head record keeps the current content plus mutable
pointers (`latest_revision` and a tag -> ordinal map); digest lookups are a real
`(parent, data.content_hash)` query rather than a denormalized index.

References gain the platform's standard `#` sub-entity fragment —
`workspace/name#<tag-or-digest>` — with an absent fragment meaning `latest`.

Taskset membership is resolved to exact digests on write, and taskset expansion
at eval time now loads each member's *pinned* revision rather than its current
content. Without that, membership was pinned in storage and ignored at runtime,
so a suite looked reproducible and was not.

API:
  POST   /{tasks,tasksets}/{name}                     create + publish rev 1
  PUT    /{tasks,tasksets}/{name}                     replace + publish (201/200)
  GET    /{tasks,tasksets}/{name}/revisions           list, newest first
  GET    /{tasks,tasksets}/{name}/revisions/{rev}     read a pinned revision
  PUT    /{tasks,tasksets}/{name}/tags/{tag}          tag an existing revision

Publishing is idempotent: identical content cuts no revision but still applies
tags. Ordinal allocation is serialized by the child create and retries on
contention; `latest` only moves forward. Creating a record rolls back if its
first publish fails, so no head ever exists without a revision.

Signed-off-by: Sandy Chapman <schapman@nvidia.com>
@SandyChapman
SandyChapman force-pushed the schapman/harbor-cli-dataset-endpoint-45cb1d branch from 58fccbe to a584f54 Compare August 5, 2026 13:24
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.

1 participant