Skip to content

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

Merged
JArmandoAnaya merged 1 commit into
mainfrom
feat/issue-229
Aug 2, 2026
Merged

feat(kernel): a batch can re-pin to the active schema version (#229)#235
JArmandoAnaya merged 1 commit into
mainfrom
feat/issue-229

Conversation

@JArmandoAnaya

Copy link
Copy Markdown
Contributor

A batch pins the project's active schema version at approval and nothing moved
it, so a label class added afterwards was invisible in every batch already in
flight — the common additive change forced an annotator to abandon a batch to
use the label they had just created.

BatchService.repin(batch_id, *, allow_destructive=False) moves the pin onto
the current active version. Explicit, never automatic: the pin still does not
follow the schema, because a contract that moved under work in flight is what
versioning exists to prevent.

The gate is the existing classifier rather than a new rule. diff_classes
judges the pinned classes against the active ones; additive goes through with no
flag, narrowing needs allow_destructive, and a narrowing that would orphan
labels in this batch is refused with no override. Those are
SchemaService.create_version's own two refusals, reused rather than re-spelled,
so no error class, no ERROR_RULES entry and no MCP retry_with row was added.
The orphan check is scoped to the batch: only labels judged by this pin are at
stake, so a label written in another batch does not block this one.

REPINNABLE_STATES in domain/batch.py is when asking is legal — approved or
in_annotation. A draft has no pin yet; a completed batch's pin is history, and
rewriting it would rewrite the record rather than the rules. Written out beside
BATCH_TRANSITIONS on PROMOTABLE_PROGRESS' terms, so a new state has to be
classified deliberately.

Annotations already written keep the version they were stamped with; only new
writes are judged against the new pin. That is the mixed-version posture releases
already have — publish stamps the manifest with the active version while
annotations carry their batch-pinned ones.

No migration: Batch.schema_version is an existing column and this is an update
path. No domain event, so nothing is announced for a no-op re-pin.

Surfaces: POST /batches/{batch_id}/repin?allow_destructive= and the
repin_batch MCP tool, which is the second half of the agent-shaped sequence
"create the class, then make it usable where you are working". The MCP listing
goes from 33 to 34; docs/mcp-tools.md is regenerated. No CLI command — the
issue's surface list names REST, MCP and docs, and no flow command needs it yet.

2,166 Python tests. Every new guard verified by mutation: dropping the state
check, the allow_destructive refusal or the orphan refusal each turns a named
test red, and widening the orphan scope from the batch to the project turns
test_a_label_in_another_batch_does_not_block_this_one red.

Closes #229

A batch pins the project's active schema version at approval and nothing moved
it, so a label class added afterwards was invisible in every batch already in
flight — the common additive change forced an annotator to abandon a batch to
use the label they had just created.

`BatchService.repin(batch_id, *, allow_destructive=False)` moves the pin onto
the current active version. Explicit, never automatic: the pin still does not
follow the schema, because a contract that moved under work in flight is what
versioning exists to prevent.

The gate is the existing classifier rather than a new rule. `diff_classes`
judges the pinned classes against the active ones; additive goes through with no
flag, narrowing needs `allow_destructive`, and a narrowing that would orphan
labels *in this batch* is refused with no override. Those are
`SchemaService.create_version`'s own two refusals, reused rather than re-spelled,
so no error class, no `ERROR_RULES` entry and no MCP `retry_with` row was added.
The orphan check is scoped to the batch: only labels judged by this pin are at
stake, so a label written in another batch does not block this one.

`REPINNABLE_STATES` in `domain/batch.py` is when asking is legal — `approved` or
`in_annotation`. A draft has no pin yet; a completed batch's pin is history, and
rewriting it would rewrite the record rather than the rules. Written out beside
`BATCH_TRANSITIONS` on `PROMOTABLE_PROGRESS`' terms, so a new state has to be
classified deliberately.

Annotations already written keep the version they were stamped with; only new
writes are judged against the new pin. That is the mixed-version posture releases
already have — `publish` stamps the manifest with the *active* version while
annotations carry their batch-pinned ones.

No migration: `Batch.schema_version` is an existing column and this is an update
path. No domain event, so nothing is announced for a no-op re-pin.

Surfaces: `POST /batches/{batch_id}/repin?allow_destructive=` and the
`repin_batch` MCP tool, which is the second half of the agent-shaped sequence
"create the class, then make it usable where you are working". The MCP listing
goes from 33 to 34; `docs/mcp-tools.md` is regenerated. No CLI command — the
issue's surface list names REST, MCP and docs, and no flow command needs it yet.

2,166 Python tests. Every new guard verified by mutation: dropping the state
check, the `allow_destructive` refusal or the orphan refusal each turns a named
test red, and widening the orphan scope from the batch to the project turns
`test_a_label_in_another_batch_does_not_block_this_one` red.

Closes #229
@JArmandoAnaya
JArmandoAnaya merged commit a18871c into main Aug 2, 2026
12 checks passed
@JArmandoAnaya
JArmandoAnaya deleted the feat/issue-229 branch August 2, 2026 23:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant