Problem
A batch pins the project's active schema version at approval (BatchService.approve, batch_service.py:239) and nothing ever moves it. AnnotationService validates and stamps against the pin, and the annotation page deliberately fetches the schema by the pinned version (ui-core/src/annotator/jobQueries.ts). Consequence: a label class added after approval is invisible in every existing batch — the overwhelmingly common additive change (a new class) forces the user to abandon a batch mid-work to use the label they just created.
Why the pin may move
The pin protects (1) a stable validation target mid-batch and (2) jobs partitioned against a version. It does not protect release reproducibility: ReleaseService.publish already stamps the manifest with the active version (release_service.py:160-170) while annotations carry their batch-pinned versions — the system already tolerates mixed versions. So an explicit, gated re-pin breaks no downstream contract.
Decision (founder, 2026-08-02)
Add an explicit re-pin operation — never auto-follow the active version.
BatchService.repin(batch_id, *, allow_destructive=False) (name open to bikeshedding): sets Batch.schema_version to the project's current active version.
- Gate with the existing classifier:
diff_classes(pinned_classes, active_classes). Additive diff → allowed freely. Destructive diff → refuse unless allow_destructive=true, and refuse with no override when any annotation in this batch sits under a removed/narrowed class (the batch-scoped sibling of SchemaChangeWouldOrphan).
- Legal only in states where annotation work is live or pending (
approved, in_annotation); a completed batch's pin is history.
- Existing annotations keep the version they were stamped with; new writes stamp the new pin. (Precedent: releases already mix versions.)
No migration
Batch.schema_version is an existing column; this is an update path, not a schema change.
Surface work
- REST:
POST /batches/{batch_id}/repin (or similar) with the ?allow_destructive= gate as a query parameter per docs/api.md conventions; documented(404, 409).
- MCP: evaluated per the parity rule — likely ships as a tool, since the add-label-from-annotator flow (follow-up issue) is exactly an agent-shaped sequence.
- Docs:
docs/batches.md currently states the pin "never moves" — update to "moves only through the explicit re-pin operation".
Acceptance criteria
- Additive re-pin succeeds with no flag; the batch's next schema fetch returns the new version.
- Destructive re-pin refuses with
DESTRUCTIVE_SCHEMA_CHANGE semantics; retry with the flag succeeds when no batch annotation is orphaned.
- A re-pin that would orphan an annotation in this batch refuses with no override.
- Re-pin on a
completed batch is an InvalidTransition-style refusal.
- Re-pin when pinned == active is a no-op (idempotent, no event spam).
Relation to #127
This answers the additive half of the annotation-versioning question for in-flight batches. The destructive-change policy beyond the gate above remains #127's scope.
Problem
A batch pins the project's active schema version at approval (
BatchService.approve,batch_service.py:239) and nothing ever moves it.AnnotationServicevalidates and stamps against the pin, and the annotation page deliberately fetches the schema by the pinned version (ui-core/src/annotator/jobQueries.ts). Consequence: a label class added after approval is invisible in every existing batch — the overwhelmingly common additive change (a new class) forces the user to abandon a batch mid-work to use the label they just created.Why the pin may move
The pin protects (1) a stable validation target mid-batch and (2) jobs partitioned against a version. It does not protect release reproducibility:
ReleaseService.publishalready stamps the manifest with the active version (release_service.py:160-170) while annotations carry their batch-pinned versions — the system already tolerates mixed versions. So an explicit, gated re-pin breaks no downstream contract.Decision (founder, 2026-08-02)
Add an explicit re-pin operation — never auto-follow the active version.
BatchService.repin(batch_id, *, allow_destructive=False)(name open to bikeshedding): setsBatch.schema_versionto the project's current active version.diff_classes(pinned_classes, active_classes). Additive diff → allowed freely. Destructive diff → refuse unlessallow_destructive=true, and refuse with no override when any annotation in this batch sits under a removed/narrowed class (the batch-scoped sibling ofSchemaChangeWouldOrphan).approved,in_annotation); acompletedbatch's pin is history.No migration
Batch.schema_versionis an existing column; this is an update path, not a schema change.Surface work
POST /batches/{batch_id}/repin(or similar) with the?allow_destructive=gate as a query parameter perdocs/api.mdconventions;documented(404, 409).docs/batches.mdcurrently states the pin "never moves" — update to "moves only through the explicit re-pin operation".Acceptance criteria
DESTRUCTIVE_SCHEMA_CHANGEsemantics; retry with the flag succeeds when no batch annotation is orphaned.completedbatch is anInvalidTransition-style refusal.Relation to #127
This answers the additive half of the annotation-versioning question for in-flight batches. The destructive-change policy beyond the gate above remains #127's scope.