An additive schema version advances every open batch - #591
Merged
Conversation
JArmandoAnaya
force-pushed
the
feat/advance-pins
branch
from
August 15, 2026 12:23
c780d5d to
a7f7dfd
Compare
Publishing a version and moving a batch's pin were two operations, composed differently by two surfaces: the annotator ran save → publish → repin as a client-orchestrated chain, and the schema editor published and touched no batch at all. Same act, two behaviours, and neither belonged to the kernel. In use that produced a closed loop — a project ahead of its own open batch, and an add-class dialog whose only route out was the publish it had just refused. **The rule, and it is a construction rather than a policy.** `diff_classes` already answers *does an annotation valid under the old version stay valid under the new one?* When it answers yes, moving a pin across that version cannot invalidate anything already drawn. So an additive version now takes every batch in `REPINNABLE_STATES` with it, in the same transaction, and a narrowing one still takes none — with `allow_destructive` or without it, because that flag says *publish this*, never *and drag every open batch across it*. It is also what makes it implementable. `BatchService` imports `SchemaService`, so the reverse import would close a cycle — but on the additive path every one of `repin`'s three gates is provably vacuous, so `_advance_pins` needs only `REPINNABLE_STATES` (a domain constant) and the repository. No layering inverted, and no rule spelled twice. `repin` itself is untouched and is now the manual escape for the narrowing case, which is the only case that still needs one. **The invariant this rewrites was written down**, and its own justification is what narrowed it: `Batch.schema_version` said the pin "never follows the active version on its own — a schema that evolved mid-batch would change the rules under work in flight". That is an argument about *narrowing*, which still never follows. `test_a_later_schema_version_does_not_move_an_existing_pin` inverts into three tests naming the new boundary. `create_version` returns `SchemaPublication` — the version, and the batches that moved. A publish that silently caught two batches up is the invisible success the UI rules ban by name, and the return value is the only thing that can tell an additive version with two open batches from one with none. 28 call sites read the return; the 71 that ignore it are untouched. No migration, no port change, no event — neither `create_version` nor `repin` emits one today, and adding one is its own decision. cf. #381
…ays what it moved `runAddClass` was save → publish → repin. The kernel moves the pin now, inside the publish's own transaction, so the third call is gone — along with the `canRepin` preflight that guarded it and `useRepinBatch`, which nothing else used. Finding F23's row leaves the failure table too: *the version exists and the pin has not moved* is unrepresentable rather than handled. `canRepin` still reaches the dialog, because the sentence it drives is still true: a completed batch keeps its version, and somebody publishing from inside one should be told before they press. **The one line without which none of this is visible**: `useCreateSchemaVersion` invalidated only `["projects", id]` and never `["batches"]`, so the kernel would move the pins and no screen would notice. A publish now changes `schema_version` on resources that key does not cover. The Schema tab says what its publish did. Until now it published and touched no batch at all, which is how a project came to be two versions ahead of the batch somebody was annotating in; a screen that answered only "saved" would leave that to be discovered from a batch opened later. The cycle spec carries the end-to-end proof, and it is the only suite that can: the unit suites stub the publish, so whether the *server* moved the pin in the same transaction is a fact about `SchemaService` and this is the one run where that service is real. A correction batch approved after it now pins v2, which is that publish showing up where the walk already looks. cf. #381
… to forbid it cf. #381
…nst active Found by walking the feature in a browser, which is what the walk was for. `_advance_pins` diffed the new version against the version it replaced, and advanced every open batch on that one answer. A batch that had already declined to follow a **narrowing** is behind the active version — and a version that only widens the *active* contract can still be a narrowing of **that batch's**. The publish then dragged it across the very change it had been protected from. Reproduced against a real server: batch pinned v1 with `car`; v2 drops `car` and the batch rightly stays; v3 adds a class to v2 and is additive against active — and the batch jumped to v3, losing `car` from the contract its labels were written under. That is the state `SchemaChangeWouldOrphan` exists to prevent, reached from a direction nothing guarded. The diff is now per batch, against that batch's own pinned version — the same question `BatchService.repin` asks, and the reason it reads the pinned schema rather than the active one. For a batch already on active it is the same diff the caller ran, so the common case costs nothing; the versions are read once for the whole call through the existing `_by_version`. The claim in the previous commit that all three of `repin`'s gates are "provably vacuous" was true only for a batch already on the active version, and I generalised it. Two of them still are — an additive change orphans nothing, and this path offers no flag — but the third is a real question and is now asked. A pin naming a version that is not stored raises `WorkspaceCorrupt`, which is the answer `BatchService._pinned_schema` already gives. Versions are never deleted, so it is damage rather than a state any operation leaves behind. **None of the five mutations or four tests in the previous commit could see this**, because every fixture had its batch already on the active version, where the two diffs are the same one. The two tests added here put a batch two versions behind and a second batch beside it on active, and both go red against the old one-diff-for-all code. cf. #381
…walk for the door it left Two sentences went on describing a step that no longer exists, and one of them was read on screen during the walk before it was noticed. **The pin badge** said "Classes published since are not available on this batch — adding one from here re-pins it". Neither half survives #381: the kernel moves the pin, not the dialog, and a batch that is *behind* has now declined something rather than merely not been asked. So it says which — a version narrowed the schema past this pin — and offers no remedy, because crossing a narrowing is a decision about this batch's labels and `repin` is where that lives. **The add-a-class dialog** promised "and moves this batch onto it" unconditionally while the notice below it could simultaneously say the batch would stay. Only one of those can be true on a completed batch; the sentence is now conditional on the same `canRepin` the notice reads. **And the door itself had no browser coverage at all.** `annotate.spec.ts` only asserts the dialog is absent in read-only mode, and the demo it runs against has no project behind it — so a chain that just lost a step was tested entirely against stubs. That is the shape of gap that hid the per-batch diff defect two commits ago, so it is closed rather than argued about: the cycle now opens the dialog inside a real job, publishes a class, and asserts on the **request log** — one POST to `/schema/versions`, none to `/repin` — then that the pin followed anyway and the armed class is drawable on the frame. Verified by breaking it: dropping the `["batches"]` invalidation from `useCreateSchemaVersion` turns that assertion red, which is what makes the claim that the line is load-bearing a measurement rather than a remark. A correction batch approved afterwards now pins v3, which is the second additive publish showing up where the walk already looks. cf. #381
JArmandoAnaya
force-pushed
the
feat/advance-pins
branch
from
August 15, 2026 12:31
a7f7dfd to
be7cd96
Compare
JArmandoAnaya
added a commit
that referenced
this pull request
Aug 15, 2026
* feat(kernel): a label class accepts a set of geometries `LabelClass.geometry` becomes `geometries`, a non-empty deduplicated tuple kept in one sorted order. A class labelled as a box on some frames and as a polygon on others is one class; splitting it in two made every consumer downstream re-unify them, and COCO's own docstring already recorded the cost. The write gate in `AnnotationService._validate` becomes a membership test in that class's own set — still not the version's union, which is wider. `schema_diff` answers the module's governing question per geometry, the shape the `select` options rule already had: one added is additive, one removed is destructive. So widening a class is an ordinary save and narrowing stays behind the flag. No migration. `annotation_schema.classes` is a JSON column, so a `model_validator(mode='before')` on `LabelClass` reading the retired singular key covers stored schemas and stored release manifests alike — and MCP, which takes the domain model directly. `LabelClassBody` deliberately does not read it: a REST client sending it is better told so than silently reinterpreted. `MANIFEST_VERSION` moves to 2, since `Manifest.classes` carries these verbatim. Documents already published keep their bytes and their hashes and still load, so every existing release stays verifiable. The export report is now one row per (class, geometry). Keyed by class alone it carried one verdict for a class YOLO answers twice — writing the boxes whole and reducing the polygons — and would have misdescribed half its own output. No exporter changed: every one of them already branches per annotation. cf. #584 * feat(annotator): a tool is resolved against the class's set, not derived from it `LabelClass.geometries` mirrors the wire, and `toolFor` gains the tool the host currently holds: it keeps that tool when the class accepts it and falls to the class's first drawable geometry when it does not. **An active tool the selected class forbids is unrepresentable**, because one function decides and it never returns one. `InputHost` gains `activeTool`. Without it `activate-class` compares two class defaults rather than two resolved tools, and misses a real move: a host drawing polygons under a both-shapes class that switches to a boxes-only one does change tool, and a polygon in flight has to be cancelled. `isTaggableClass` and `drawableGeometries` stop being each other's negation — a class may accept a tag and a shape — so `classAction` sends `toggle-tag` only when the class draws nothing. Folding a drawable class into it would tag the asset where somebody pressing a class digit meant to arm it. `allowedGeometriesFor` filters instead of wrapping a scalar, which its own docstring had predicted was the only change a set would need. cf. #584 * test(annotator): the tool resolution rule a geometry set made necessary cf. #584 * test(annotator): a class that is both taggable and drawable arms rather than tags The one rule the mutation battery found nothing watching: no class could be both before #584, so `classAction`'s two tests had never been ordered against each other. `PALETTE` gains a seventh row that is a tag and a box at once. cf. #584 * feat(ui): a class's geometries are a checkbox group, and a name that exists is an offer The schema editor's single-select becomes a checkbox group under the same category headings, using the native input this form already uses for an attribute's `required` flag — no new dependency, no new primitive, and what a class accepts is readable without opening anything. The last ticked box does not come off and carries why. **A defect the new test caught, worth stating:** the first draft refused that last box with `preventDefault()` on the input's click. React synthesises a checkbox's `onChange` from the same native click, so cancelling the click does not cancel the change — the class went to an empty set while the tick stayed on screen, a control lying about what it had just done. The refusal now lives where the value is computed, which cannot come apart. **The rescue flow.** A name the published version already declares stops being a red box and becomes an offer: the alert says what the class accepts today and what publishing would add, and the primary reads `Add polygon to sign`. It carries the **existing** class's colour and attributes, so a form opened to make a new class cannot quietly wipe what the old one declared. The refusal that remains is a name typed twice in one sitting, which has nothing to offer because both entries are being written now. `composeVersion` replaces a same-named class **in place** rather than appending: two classes with one name is what `create_version` refuses outright, and appending would also renumber the digit hotkeys, which are positions in the authored order. The tool strip narrows to the held class's own geometries, and the page holds the preferred tool beside the drawing class at job scope, for the query-key reason the drawing class is already there. cf. #584 * feat(app): the demo, styleguide and browser suites follow the geometry set cf. #584 * docs: geometry sets, the rescue flow, and export unchanged cf. #584 * test(ui): the retarget guard, which no fixture with a two-shape class could see cf. #584 * docs: the last two places that called a class's geometry singular cf. #584 * feat(ui): one vocabulary for geometries, and it is not the wire's The interface was showing users database identifiers, in two vocabularies. `ToolPalette` had a private `TOOL_LABELS` saying `Box`; every other surface — class rows, the reassignment menu, the add-a-class dialog's checkboxes and prose and its primary button, the schema editor's badges, the project summary — printed the raw `GeometryType`. So one thing was `Box` on the left of the canvas and `bbox` on the right, and a tag class's row read `classification_tag`. `GEOMETRY_LABELS` lives beside `GEOMETRY_CATEGORY` in the module that already owns geometry presentation, total over the union by `satisfies` so a ninth member fails the build until somebody names it. The strip capitalises at its own control; every other caller reads the word as-is. **Lowercase, because the same word is used two ways** — as a chip in a dense row (`box · polygon`) and inside a sentence ("Publishing adds polygon to it"). Only the first letter is a sentence-position question, which the test states as *never starts with a capital* rather than *is lowercase*: `3D box` is an acronym and the stricter rule would have forced `3d box`, wrong in every position. `formatGeometries` joins with ` · ` rather than ` or `. A middot is what a set reads as at this density, and in a 248px row those four characters come out of the class name. It is also the largest width saving available in the class list — a tag class's row spent about 110px of 248 on `classification_tag` and now spends 22 on `tag`, against the 32px widening the whole panel would buy. Tests address a checkbox by `data-testid`, which keeps the wire value, so a test says *which* geometry without also asserting what it is called. cf. #584 * feat(ui): the armed class row is the shape picker Arming a class stopped answering which shape the next drag produces, and until now the only place that answer lived was the tool strip at the **far left** of the canvas while the class was chosen on the right — one decision split across the width of the picture, in a loop repeated hundreds of times a job. The armed row's geometry words become a segmented control: the active shape lit, pressing another switches the tool **without moving the class**. That retarget rule already shipped in `ToolPalette` and is tested in both directions, so the panel is a second caller of an existing rule rather than a new one. **Only the armed row, and the accessible answer and the density answer agree.** `ClassListRow` is documented as "a real `<button>` spanning the whole row", and HTML forbids interactive descendants inside a button — so a row offering a choice has to become a group with an inner name button. That is also what you want at the fifty-class ontologies principle 7 is written for: an unarmed row has no live choice, and fifty pickers for one decision is noise. Exactly one row is armed, so the extra tab stops are bounded at (shapes − 1). The row is gated on `drawableGeometries`, not `geometries`: a class may accept a tag beside a box, and a tag has no canvas gesture — offering it would be a tool the canvas cannot answer. `toolForClass` is extracted from `toolFor` and exported, because a list iterating `schema.classes` holds the class and has no document. Writing `drawable.find(…) ?? drawable[0]` in the panel instead would be two spellings of one fallback, which is how a strip and a panel come to disagree about which shape is lit — the one thing they must not do. It is also why the lit segment resolves through it rather than comparing the raw preference: the held tool may be one this class forbids. The panel takes `2xl:w-80`. The extra 32px is headroom for a class naming three shapes, not a fix, and it is withheld below 1536px on purpose — `ANNOTATOR_MIN_VIEWPORT_PX` is 768, where a collapsed rail already leaves a 384px stage, and a width chosen on a large monitor must not be charged to the smallest screen the editor opens on at all. `EditorNotice`'s clearance arithmetic is stated at 1280px and stays true. cf. #584 * fix(ui): the add-a-class dialog was undersized, and could hide its own footer `ClassFields` splits Name | Geometry on `md:`, a **viewport** breakpoint rather than a container one, so on any desktop the grid splits however narrow the box is — at the default `max-w-lg` each column was ~224px against a geometry row needing ~269px, and the checkboxes wrapped onto three lines. The box has to be wide enough for a split it cannot prevent. `2xl` is the smallest that clears it. Second defect, same string: the dialog carried no `max-h` and no scroll, and `DialogContent` is centred with `-translate-y-1/2` — so content taller than the viewport overflowed off both edges and took the footer with it, which a class with a few attributes reaches. cf. #584 * docs: dialog widths, the armed row's shape picker, and the panel's breakpoint cf. #584 * test(ui): arm a class by its name, not by the row's centre `activate()` clicked the row's centre. That worked by about fourteen pixels: once an armed row carries a shape picker, a longer class name or a third shape moves that centre onto a shape segment — and the press would switch the **tool** while `data-selected` still read true, so every assertion around it kept passing. Both markups now put a `-name` handle on the name, so choosing a class has one target whether the row is a plain button or a group. cf. #584 * test(kernel): pin the narrowing gate's over-refusal, and say so in the docs Found by checking this branch against `main` rather than by a conflict: a class that can hold two shapes makes the orphan gate coarser than the question it stands in for. `car` accepts `bbox · polygon` and the project holds one **bbox** `car`. Taking `polygon` away orphans nothing, and is refused anyway by the refusal no flag overrides. `SchemaDiff.destructive_classes` is a set of *names*, and that set is what reaches `add_schema_version_unless_annotated`, whose predicate asks whether the project holds any `car` — never whether it holds one drawn as the shape being removed. While a class held a single geometry those were the same question. Conservative in the safe direction: it refuses rather than orphaning, so nothing is lost and no invariant moves. Pinned rather than fixed because making it exact changes the port method and the guarded-insert contract #589 landed for the TOCTOU race — argued in #592, which the test names and which `docs/schemas.md` links. The test is a tripwire for that work: invert it when the gate learns about geometry, never quietly delete it. cf. #584, #592 * fix(ui): a geometry set has no order, and the draft comparison has to know Integration with what landed on main while this branch was open — three places where code that arrived after the geometry sweep still spelled a class's shape singular, and one that is a real defect rather than a rename. `SchemaEditor`'s `canonical` projection — #583's fix for a draft that read as dirty against the version it had just published — compared `declared.geometry`. That field is gone, and swapping in the array is not enough: the two sides spell one set differently. The domain sorts and dedupes, so the active version always reads canonical, while a draft's copy is whatever order the boxes were ticked in. Untick the shape a class already had and tick it back, and the editor offered to publish a version identical to the one in force. Verified by breaking it: with the sort removed, `does not call a reordered geometry set an unsaved change` fails and the other nine in that file pass. Asserted on `dirty` rather than through a save, and the reason is worth writing down — going through a save **cannot see this**. The draft is re-based onto the wire's own copy afterwards, so both sides come out canonical whatever the comparison does; the first version of this test round-tripped a save, passed, and passed just as happily with the fix removed. The other two are renames in fixtures that arrived with #590 and #591: `test_schema_refusals.py` and `test_batch_tools.py` posted the retired `geometry` key, which `LabelClassBody` refuses with `extra_forbidden` — the wire is strict on purpose while `LabelClass` still lifts the old spelling, so stored documents keep loading and a client sending it is told rather than reinterpreted. cf. #584 * test(cycle): the real-server walk posted the retired geometry key #591's own addition to the cycle, written before a class held a set. The wire refuses the singular spelling with `extra_forbidden`, so the publish that walk makes to prove a pin advances answered 422 and the whole cycle failed on it. The browser suites were again the only ones that saw it: the class list this line extends comes back off the wire already plural, so nothing in Python or in vitest reads this payload. cf. #584
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.
An additive schema version advances every open batch
Closes #381 — direction given 2026-08-15: all repinnable batches, additive only.
Publishing a version and moving a batch's pin were two operations, composed differently by two
surfaces. The annotator ran save → publish → repin as a client-orchestrated chain; the schema
editor published and touched no batch at all. Same act, two behaviours, and neither belonged to
the kernel.
In use that produced a closed loop rather than friction: a project two versions ahead of its own
open batch, the class published in between invisible there, and a pin popover naming a remedy
that only the publish the add-class dialog had just refused could perform.
The rule, and why it needs no new gate
diff_classesalready answers does an annotation valid under the old version stay valid underthe new one? When it answers yes, moving a pin across that version cannot invalidate
anything already drawn — by construction, not by policy. So an additive version takes every
batch in
REPINNABLE_STATESwith it, in the same transaction, and a narrowing one takes none:allow_destructivesays publish this, never and drag every open batch across it.It is also what makes it implementable.
BatchServiceimportsSchemaService, so the reverseimport would close a cycle — but on the additive path every one of
repin's three gates isprovably vacuous, so
_advance_pinsneeds onlyREPINNABLE_STATES(a domain constant) andthe repository. No layering inverted, no rule spelled twice.
repinis untouched: same route, same MCP tool, same capability, sameallow_destructive. It isnow the manual route for the narrowing case, judged against one batch's own labels.
The invariant this rewrites was written down
Batch.schema_versionsaid the pin "never follows the active version on its own — a schema thatevolved mid-batch would change the rules under work in flight". That is an argument about
narrowing, which still never follows; the docstring's own justification is what narrowed it.
test_a_later_schema_version_does_not_move_an_existing_pininverts into three tests naming thenew boundary, plus one pinning #583's no-op.
Shape
create_versionreturnsSchemaPublication— the version, plus the batches that moved — andPOST /schema/versionsanswers{published, advanced_batches}. The reads are unchanged:which batches once followed a version is not something a
GETknows or is asked, and apermanently-empty list there would be a field a client reads meaning into.
Measured blast radius before committing to it: 28 call sites read the return, 71 ignore it.
The 71 are untouched.
No migration, no port change, no event — neither
create_versionnorrepinemits one today,and adding one is its own decision.
Frontend
The one line without which none of this is visible:
useCreateSchemaVersioninvalidated only["projects", id]and never["batches"], so the kernel would move the pins and no screen wouldnotice.
The add-a-class chain drops its third call, and
useRepinBatchwith it — nothing else used it.Finding F23's row leaves the failure table: the version exists and the pin has not moved is
unrepresentable now rather than handled, because the two happen in one transaction.
canRepinstays, because the sentence it drives is still true: a completed batch keeps its version.
The Schema tab says what its publish did — the screen that could previously strand every open
batch in silence.
Tests
Kernel: an additive version moves
approvedandin_annotationand leavesdraftandcompleted; a narrowing one moves none with or without the flag;advanced_batchesnamesexactly what moved; #583's no-op moves nothing. Server and MCP assert the same over the wire, and
the MCP agent sequence "create the class, then re-pin" becomes "create the class" — with a
sibling proving the old two-call sequence still works, so an agent written against the previous
behaviour is not broken.
Mutation-verified: 5 cases, 5 red, each naming its intended test — narrowing advancing,
completed batches moving, nothing advancing, reporting nothing while still writing, and the no-op
catching batches up. Committed before the first mutation, reverted by the recorded diff, clean
tree asserted between cases. Two anchors initially missed because
ruff formathad reflowed theexpression; the harness refused rather than silently patching nothing, which is what that
assertion is for.
The cycle spec carries the end-to-end proof, and it is the only suite that can. The unit
suites stub the publish, so they assert what the client sent and rendered; whether the server
moved the pin in the same transaction is a fact about
SchemaService, and that is the one runwhere the service is real.
Gate
Every stage, exit code recorded, run in stages against the ~10-minute ceiling. Test directories
read from
ls tests/at run time.kernel cli mcpserver formats inference jobsarchitecture examples packaging scripts test_versioning.pyruff check·ruff format --check·mypy·lint-importscheck.sh generatedcheck.sh frontendcheck.sh browsercheck.sh docsA defect this found, and the walk is what found it
The first implementation diffed the new version against the version it replaced, then
advanced every open batch on that one answer. A batch that had already declined to follow a
narrowing is behind active — and a version that only widens the active contract can still be a
narrowing of that batch's. The publish dragged it across the very change it was protected
from.
Reproduced against a real server before fixing: batch pinned v1 with
car; v2 dropscarandthe batch rightly stays; v3 adds a class to v2, is additive against active — and the batch jumped
to v3 with
cargone from the contract its labels were written under. That is the stateSchemaChangeWouldOrphanexists to prevent, reached from a direction nothing guarded.The diff is now per batch, against that batch's own pin — the same question
repinasks, andthe reason it reads the pinned schema rather than the active one. Two of
repin's three gatesreally are vacuous on an additive change; the third is a real question and is now asked.
Nothing in the first commit could have caught it: all five mutations and all four tests had
their batch already on the active version, where the two diffs are the same one. The regression
tests put a batch two versions behind and a second beside it on active, and both go red against
the old code.
Verified end to end afterwards, on the same build: the lagging batch holds at v1 through both
publishes,
repin --allow-destructivestill catches it up by hand, and a batch on activeadvances and is named in
advanced_batches.Copy, and the door that had no browser coverage
Two sentences still described the removed third call. The pin badge's "adding one from here
re-pins it" — read on screen during the walk before it was noticed — now says what a
behindbadge actually means after #381: a version narrowed the schema past this pin. And the dialog's
unconditional "moves this batch onto it", which could contradict the notice directly below it
on a completed batch, is conditional on the same
canRepinthat notice reads.The annotator's add-a-class door had no browser coverage at all —
annotate.spec.tsonlyasserts it is absent in read-only mode — so a chain that had just lost a step was tested entirely
against stubs. Given that stubs are exactly what hid the per-batch diff defect, the cycle now
opens the dialog inside a real job and asserts on the request log: one POST to
/schema/versions, none to/repin, then the pin followed anyway and the armed class isdrawable. Verified by breaking it — dropping the
["batches"]invalidation turns that assertionred, which is what makes "this line is load-bearing" a measurement.
Found, not fixed
POST /batches/{id}/repintakes a bareboolforallow_destructive, wherecreate_schema_versionuses the annotatedDestructiveQuery— so the schema route documentsthe parameter in OpenAPI and the repin route does not. Pre-existing, unrelated, one line.
IngestCompleted's docstring still says it is emitted by nobody, which stopped being truewhen
IngestService.ingeststarted publishing it.docs/events.mdis correct; the docstringis stale.
Flags for Armando
cycle spec's assertion moved from v1 to v2 for that reason. It was always "pins the active
version", so this is the same rule meeting a project whose active version now moves more
often — worth knowing, because it is the one place the change is visible somewhere nobody was
thinking about it.
is now a no-op rather than an error. There is a test for exactly that.
published, notversion.{version: {version: 2}}reads badly, andpublishedmatches the domain field, which is itself namedpublishedbecauseschemashadows
BaseModel.schemaand pydantic warns.