feat(evaluator): add content-addressed revisions for tasks and tasksets - #1023
Draft
SandyChapman wants to merge 1 commit into
Draft
feat(evaluator): add content-addressed revisions for tasks and tasksets#1023SandyChapman wants to merge 1 commit into
SandyChapman wants to merge 1 commit into
Conversation
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>
Contributor
Contributor
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 meaninglatest. 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
POST/{tasks,tasksets}/{name}PUT/{tasks,tasksets}/{name}GET/{tasks,tasksets}/{name}/revisionsGET/{tasks,tasksets}/{name}/revisions/{revision}PUT/{tasks,tasksets}/{name}/tags/{tag}?revision=Publishing is idempotent — identical content cuts no revision but still applies tags, which is how you tag a revision after the fact.
Invariants
latestonly moves forward and can't be moved by hand. Ordinal allocation is serialized by the child create and retries on contention.Testing
RUN_AGENT_EVAL_INTEGRATION=1), covering what only real persistence confirms: parent-scoped ordinals, the digest query, server-side ordering, FK cascade, cross-record isolationcontent_hash100%,task_refs100%,revisions96%, services 94%tools/lint/lint-all.sh: 13/13 passNotes for review
plugins/nemo-customizer/openapi/openapi.yamlis deliberately not included.make refresh-openapiregenerates ~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