Skip to content

feat(kernel): a batch can re-pin to the active schema version #229

Description

@JArmandoAnaya

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

  1. Additive re-pin succeeds with no flag; the batch's next schema fetch returns the new version.
  2. Destructive re-pin refuses with DESTRUCTIVE_SCHEMA_CHANGE semantics; retry with the flag succeeds when no batch annotation is orphaned.
  3. A re-pin that would orphan an annotation in this batch refuses with no override.
  4. Re-pin on a completed batch is an InvalidTransition-style refusal.
  5. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    apivisionset.server — REST API and OpenAPI contractenhancementNew feature or requestkernelvisionset.kernel — domain, ports, adapters, servicesmcpvisionset.mcp — MCP server and tools

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions