feat(kernel): a batch can re-pin to the active schema version (#229) - #235
Merged
Conversation
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
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.
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 ontothe 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_classesjudges the pinned classes against the active ones; additive goes through with no
flag, narrowing needs
allow_destructive, and a narrowing that would orphanlabels 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_RULESentry and no MCPretry_withrow 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_STATESindomain/batch.pyis when asking is legal —approvedorin_annotation. A draft has no pin yet; a completed batch's pin is history, andrewriting it would rewrite the record rather than the rules. Written out beside
BATCH_TRANSITIONSonPROMOTABLE_PROGRESS' terms, so a new state has to beclassified 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 —
publishstamps the manifest with the active version whileannotations carry their batch-pinned ones.
No migration:
Batch.schema_versionis an existing column and this is an updatepath. No domain event, so nothing is announced for a no-op re-pin.
Surfaces:
POST /batches/{batch_id}/repin?allow_destructive=and therepin_batchMCP 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.mdis regenerated. No CLI command — theissue'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_destructiverefusal or the orphan refusal each turns a namedtest red, and widening the orphan scope from the batch to the project turns
test_a_label_in_another_batch_does_not_block_this_onered.Closes #229